December 15, 2007

Dragオブジェクトがscrollなdivを乗り越える方法1

今回は、技術的なネタを書いてみようと思います。二回に分けて書こうと思います。今日はですます調な気分です。

ネタは、3ヶ月程前にはまった内容にしてみようと思います。前々から忘れてしまう前にポストしないとなぁと思いながら、先延ばしにしていました。問題としては、以下のように当時は書いておりますね。

draggableな要素がoverflowのhidden, scroll, auto(たぶん)のときには、そのoverflowを指定してある要素の外では見えなくなっちゃう。

また、この問題はscript.aculo.usの開発者も知っているのだけど、なかなか対応が面倒なもののようです。こんな感じに言っていますから。

I discussed this with Thomas some time ago, and the problem is that the element we move (be it in ghosting mode or not) usually must remain at the same DOM level because otherwise, styles might get lost. I'm working on a solution right now, too, it will involve a new option you can use to assign a class name to the element while it's being dragged. If this class name is supplied, the element will be attached to the body. Let's see if this works, I'll provide the patch as soon as it is ready

一応、closedとなっており、 Effects Treasure Chestにその解決方法が上がっているのですが、以下の質問があるように、私もこのソースでは、実際に動いているところを見たことがありません。動作は見ていないのですが、ソースを軽くみたら、がんばったら動きそうな感じがしますけどね。そして、この方法は、実際のライブラリには組み込まれていません。

Could you show an example of how to use this effect?

Edit 01/08/2007 by Greg Hinch – added a line to turn this.dragging on and off when calling initDrag() and finishDrag(). This was to fix an issue where if a draggable was clicked but not dragged the duplicate element would not be removed from the page.

さて、この問題は、scrollなdivの要素の下にDragオブジェクトがあったら、そのdivを乗り越えることができないというものです。これはライブラリの問題ではなく、ブラウザの実装の話です。そういう仕様なのですね。まぁ、IE6では大丈夫だけど、IE7、Mozillaではダメというのがなんともやるせないですが。

実際どういう状態になるか、簡単にスクリプトを書いてみました。
下のページを見てください。

Drag object over scroll div(Not Working!)

右ペインにあるtakoとかikaといったdiv要素はドラッグが可能です。しかし、右ペインから左ペインにドラッグしようとすると見えなくなってしまいます。この状態をなんとかしたいのです。さらに不思議なことに、見えなくなっても、実はドロップすることはできます。再び書きますが、おそらくIE6ではちゃんと左ペインにDragオブジェクト持っていってもちゃんと見えると思います。確認はしていませんが、3ヶ月前に調査したときは、IE6では想定した動作をしていました。

JavaScriptの方では、動かないバージョンでは以下のようになっています。

  1. Event.observe(window, 'load', function() {
  2.   $A($('scroll-b').getElementsByClassName('drag-obj')).each(function(obj) {
  3.     new Draggable(obj, { revert: true, scroll: 'scroll-b' });
  4.   });
  5.   Droppables.add('drop-container', {
  6.     hoverclass: 'dragdrop-in',
  7.     onDrop: function(obj, target) { target.innerHTML += obj.innerHTML+"<br />"; }
  8.   });
  9. });

大したことはしていません。class属性の値にdrag-objを持つ要素をドラッグ可能にしています。また、id属性の値にdrop-containerを持つ要素をドロップ可能にしており、ドロップした際に、その要素のinnerHTMLにドラッグ要素のテキストを書き足します。

そして、CSSの方で重要な場所が以下のようになっています。

  1. #scroll-a {
  2.   float: left;
  3.   width: 70%;
  4.   height: 100%;
  5.   overflow-y: scroll;
  6. }
  7.  
  8. #scroll-b {
  9.   height: 100%;
  10.   overflow-y-: scroll;
  11. }

別にscroll-aはoverflowがscrollでなくてもいいのですが。。。あと、overflow-yって、IEだけだと思っていましたが、手元のfirefoxでは動いたので、とりあえずこれでいきます。

今回はここまでです。動く方のソースは実はまだ頭の中で、ソースには書き出してはいません。どうやるか、ということですが、Dragオブジェクトをscroll-bの下ではなく、body直下に置く、もしくは、scroll指定していない場所に置くという方法を採用する予定です。

来週中に書くことを目指します。

そういえば、2ヶ月振りにscript.aculo.usを使いました。いつの間にか1.8.0が出ていましたね。prototype.jsのバージョン1.6ともcompatibleでうれしい限りです。

November 16, 2007

for文書きたくない。

タイトルにYUIとか書くと全く関係ないところが、キーワードを拾って、リンクを張ってきやがる。日本人にとって、YUIという言葉は、普通Yahoo! User Interfaceなんかではなく、人の名前になる。しかも、女性の可能性が高いので、リファラーがYUIだらけに。

というわけで、YUIを最近使っているわけだが、イテレートするのにfor文を書くのが嫌なので、eachとmapだけでも、prototype.jsから移植してみた。勝手に、YAHOO.util.Collectionと名前を付けてみたりw。改め、YAHOO.utilx.Collectionにした。また、使いそうなコレクションがあったら追加してみる予定。別に、anyやらallやら全部prototype.jsから持ってきてもいいのだけど、面倒なので必要があれば実装するし、使わなければ実装しない。
というわけで、以下がソース

  1. YAHOO.namespace('utilx');
  2. YAHOO.utilx.Collection = function() {
  3.   var _each = function(data, iterator) {
  4.     if (YAHOO.lang.isArray(data)) {
  5.       for (var i = 0, l = data.length; i <l; i++) {
  6.         iterator(data[i]);
  7.       }
  8.     } else if (data && typeof data === 'object') {
  9.       for (var property in data) {
  10.         iterator({key: property, value:data[property]});
  11.       }
  12.     }
  13.   };
  14.   return {
  15.     $break: {},
  16.     $continue: new Error('"throw $continue" is deprecated, use "return" instead'),
  17.     each: function(data, iterator) {
  18.       var index = 0;
  19.       try {
  20.         _each(data, function(value) {
  21.           iterator(value, index++);
  22.         });
  23.       } catch (e) {
  24.         if (e != this.$break) throw e;
  25.       }
  26.       return data;
  27.     },
  28.     map: function(data, iterator) {
  29.       var results;
  30.       this.each(data, function(value, index) {
  31.         var result = (iterator || function(v) { return v;})(value, index);
  32.         if (YAHOO.lang.isArray(data)) {
  33.           results = results || [];
  34.           results.push(result);
  35.         } else {
  36.           results = results || {};
  37.           if (result && result['key'] && result['value']) {
  38.             results[result['key']] = result['value'];
  39.           }
  40.         }
  41.       });
  42.       return results;
  43.     }
  44.   }
  45. }();

次のように使う。配列のとき

  1. var hogeArray = ["foo", "bar", "baz"];
  2. YAHOO.utilx.Collection.each(hogeArray, function(data) {
  3.     console.log(data + "hogehoge")
  4. });
  5. hogeArray = YAHOO.utilx.Collection.map(hogeArray, function(data) {
  6.   if (data == 'baz') {
  7.     throw YAHOO.utilx.Collection.$break;
  8.   }
  9.   data = data + "hogehoge";
  10.   return data;
  11. });
  12. console.log(hogeArray);

オブジェクトのとき

  1. var hogeObj = {"foo": "This is Foo", "bar": "This is Bar", "baz": "This is Baz"};
  2. YAHOO.utilx.Collection.each(hogeObj, function(data) {
  3.     console.log(data.value + "hogehoge")
  4. });
  5. hogeObj = YAHOO.utilx.Collection.map(hogeObj, function(data) {
  6.   if (data.key == 'baz') {
  7.     throw YAHOO.utilx.Collection.$break;
  8.   }
  9.   data.value = data.value + "hogehoge";
  10.   return data;
  11. });
  12. console.log(hogeObj);

とりあえず、for文を書きたくないので、これで少しすっきりした。

(more...)

November 14, 2007

JavaScript Module Patternいいね。半年遅れだけど。。。

Module Pattern祭があったのは、今年の6月だったみたいですね。まだ時代の流れに遅れていますが、少しずつ追いかけていこうと思います。

現在は、jQueryでもprototype.jsでもなく、YUIを調査しています。まだまだ頭がprototype.js脳なので、多少苦しんでいますが、だいぶ掴めてきました。そして、YUIでも直接呼び出せばいいような小さなプログラムは書けるようになってきました。

ところで、prototype.jsでは、Class.createされるとinitializeが呼ばれ、ほげほげするように書きますよね。

  1. var Hoge = Class.create();
  2. Hoge.prototype = {
  3.   // public vars
  4.   varA: "hogehoge",
  5.   varB: "foobar",
  6.  
  7.   initialize: function() {
  8.     console.log("initialized");
  9.   },
  10.  
  11.   alertA: function() {
  12.     alert(this.varA);
  13.   }
  14. }
  15. var hoge = new Hoge();
  16. hoge.alertA();

とか。そして、それなりに大きいプログラムでもクラスごと(こんなことを言ったら怒られる?)に分けて、整理して構成することができますね。

どうやったら上のように、そしてYUIっぽく書けるかな、と昨日の晩から調査をしていました。そして、ようやくその手がかりとなるポストを見つけることができました。(別にYUI以外でもこの書き方していいけど。。。)
A JavaScript Module Patternです。名前はこのポストの著者のEricさんが勝手に付けたっぽいのですが、いいです。これ。日本でも半年前に話題になっていたみたいですね。時代遅れですいません。

クロージャを使うことによって、privateメンバっぽく書けるのもうれしいですね。上の例では、結局varAとvarBはpublicメンバとして評価されてしまうので、適当なコーディング規約がないとグループで開発したときに呼び出してしまいそうです。

そして、そのポストで紹介されていたような書き方は以下の通り。

  1. var Hoge = function() {
  2.   console.log("initialized");
  3.   var varA = "hogehoge";
  4.   var varB = "foobar";
  5.  
  6.   return {
  7.     alertA: function() {
  8.       alert(varA);
  9.     }
  10.   }
  11. }();
  12. var hoge = Hoge;
  13. hoge.alertA();

これだとvarAとvarBがHogeの中からしか参照できません。そして、returnでalertAという関数オブジェクト(メソッドのように使う)を持ったオブジェクトを返してくれます。

これからはこれで行こうと思います。

また、JavaScriptでは、無駄にnewをしない方がいいようです。YUIの調査をしていたら私のヒーローのDouglasさんがそうおっしゃっています。
JavaScript, We Hardly new Ya
なんでもprototypeオブジェクトを無駄に持ってしまうことが問題であるとおっしゃっているのです。つまり、メモリの無駄遣いは止めましょうとのことです。そのため、newしない方がいいそうです。というか、newすると時代遅れだそうです。

てっきり、prototypeオブジェクトにいろいろ書いておくと、newされたオブジェクトが使いまわしてくれるので、積極的にprototypeオブジェクトを使用して、newでオブジェクト生成し、それを使っていくのが通だと思っていたのですが、そうでもないのですね。

ただDouglasさんは、newを使用するときもなくはない、と言っています。私のpoorな頭ではどんなときにnewを使用した方がよいのか、ということがわかりませんでした。何度もそのオブジェクトを作成する場合と理解したらいいのでしょうか?

どちらにせよ、Douglasさんを信じて、あまりnewをしないように、prototypeオブジェクトは使用しないように今回は作っていこうと思います。

しかし、まだまだprototype.js脳から離れられていないので、Array.eachやらArray.mapやらのイテレーション系の関数がないのは苦しいです。自作しようかしらん。

次は、CustomEventが使えるようになりたいです。この辺がまだ理解できていませんので、読んでいきます。きっとここら辺が理解できたら、YUIでそれなりのプログラムが書けそうです。
Event-Driven Web Application Design
The Bubbling Technique & Custom Event, YUI’s Secret Weapon by Caridy Patiño Mayea

October 19, 2007

removeChildをするときには、その前にEventを全部取ること!

removeChildのみならずreplaceChildのときも必要!さらに、innerHTMLで内でEventを指定していた要素にも必要!

前からIE6にはメモリリークがあるってことは知っていた。そして、要素同士が双方にポインタを持つこと?で、そのメモリリークが起こると知ってはいた。そして、それを避けるべきだということも言葉では理解していた。でも、実際にどうすればいいの?ってことがわからなかったのだ!そして、つい先ほど、その問題解決がわかった!!!超感動!

確かに最近開発したWebアプリでは、再読み込みをする度に重くなっていき、絶対メモリリークしているなってことに気づいていた。でも、だからってどうしたらいいかはわからなかった。って、それはそれで問題なのだけど、こうやってわかった以上は早速月曜日に修正するぜ!

で、どうやって気づいたかというと、YUI TheaterのDouglas Crockford氏のThe Theory of the DOMからだった。いいよ!いいよ!Qualityも良かったけど、本当に勉強になるっす。一応3つも映像があって、全部で一時間半弱なんだけど、見てよかった!ここには、そのメモリリークについて話があったPart3を貼り付けておく。メモリリークの話だけなら、Part3の最初の6分くらいなので、それだけチェックしてもいいかも。

で、講義の中で上げられていた削除する前にイベントを削除する方法としては、上の映像では次の関数を使用していた。実際に動かしていないから間違っているかもしれんけど、だいたいこんな感じ。

  1. function purgeEventHandlers(node) {
  2.   walkTheDOM(node, function(e) {
  3.     for (var n in e) {
  4.       if (typeof e[n] === 'function') {
  5.         e[n] = null;
  6.       }
  7.     }
  8.   });
  9. }
  10. function walkTheDOM(node, func) {
  11.   func(node);
  12.   node = node.firstChild;
  13.   while (node) {
  14.     walkTheDOM(node, func);
  15.     node = node.nextSibling;
  16.   }
  17. }

もしくはYUIのpurgeElementで指定した要素以下の要素のイベントを一掃してくれるとのことだ。
まぁ、方法がわかれば簡単に作れるさ。prototype.jsなどでElement.remove()とかしているところもremoveする前にイベントを一掃しないとメモリリークしてしまうので、気をつけないといけない。私の問題はまさにこれだったと思う。というわけで、Douglasさんのファンになりましたので、当分YUIを調べることになりそうす。
うーむ。やってみたけど、メモリが開放されない。。。なんでだろ。dragdrop.jsのdraggableをたくさん作って、その上のDIVコンテナのinnerHTMLを書き換えているので、おそらくdraggableな要素が無くなってもメモリは持っているかのようだ。。。ちょっと調査せなかんな。

つーか、Wordpressのバージョンを上げたらwysiwygエディタを消すオプションが無くなってた。。。orz
ソースコードのスペースを勝手に削りやがって!!!!空行も削りやがる。許せん!くそー。腹が立ってきた。
って、少し調べたら私の他にも腹が立っている人はいるみたい。
Idea: Option to turn off WYSIWYG editor, sitewide!
解決方法がtinymceのディレクトリを削除しろだって?やってらんねー。
(more...)

October 11, 2007

つーか、全く速くならんかった。

昨日からはてブで賑わっている「一行でIEのJavaScriptを高速化する方法」を試したのだけど、私の環境では全く速くならんかった。記事を読んだときは、私も「これはすごい」と思って、今朝早速、最近ずっと開発していた場所に組み込んでベンチを取ってみたのだけど、全く変わらないという結果が出た。

まず、 IE では document にそのままアクセスすると window オブジェクトの内部メソッドが実行されてしまいます。これが非常に重いのです。

まぁ、確かに理解はできるし、軽くなりそうな気がするけども、実際はdocumentを呼ぶ機会は少ない(DOMで検索した要素もキャッシュ化している)ので、高速化されなかったってことかな。きっと、

この方法は document と書かれた部分を 5 倍以上(ループのコストを引くとたぶん 10 倍以上?)速くすることができるのですが、その数倍というのは「プロパティアクセス」と「関数呼び出し」の差です(関数呼び出しを減らしていると考えてください)。

ですので、ほとんどのウェブサイトでは効果はあまり感じられないかもしれません。

とのことなので、私のスクリプトが重いのはDOM検索なので、変わらなかったんだろうな。

つーか、変数の初期化に関しては、勉強になったよ。特にJavaScript高速化 - まさにっき(使えないプログラマーの記録)の「とおりすがり」氏、説明わかりやすい!というわけで、私もメモとして書かしてもらおうっと。

とおりすがり 『> これもまた、 JavaScript では変数はスコープの先頭で生成されるため、 document は空の変数となり undefined になってしまいます。

例えば var foo = 1, bar = 2; var foobar = foo + bar; なんていう宣言があった場合、
var foo = bar = foobar = undefined; //宣言された変数はスコープの最初に全部まとめて生成される。
foo = 1; bar = 2; foobar = foo + bar;
みたいに解釈されます。なので
var doc = document; var document = doc; は
var doc = document = undefined; doc = document; document = doc;
になって undefined になるんですよ。

> 2行目の時点での doc が undefined になっているのか?
var document; がある時点で、そのスコープに入った瞬間に document が undefined になるわけです。
と、全然関係ない通りすがりでした。』 (2007/10/11 08:05)

あと、amachangさんの言っているevalのタイミングについても勉強になった。

eval で var 宣言することでスコープ途中から変数を生成することができるのです。

しかし、

  1. /*@cc_on _d=document;eval('var document=_d')@*/

といった、いかにも黒魔術的な書き方はハックって感じがするね。

September 30, 2007

JavaScriptで重い処理をするとアニメーションGIFが止まる件について

当分の間JavaScriptオンリーです。今日はですます調です。

Ajaxのローディングとかでクルクル回るアニメーションGIFてありますよね?あのローディングのアニメーションGIFをJavaScriptで重い処理をするときに使おうと思っていてその画像を探していました。そして、先日調べていたら、こんなサイトを発見しました。激しくいいですね。
Ajaxload - Ajax loading gif generator
実は、結構有名なサイトなんですね。知らなかったです。でも、先日知って、早速作ってみました。
ローディング画像

そして、早速使おうと思ったのですが、重い処理をしている間にアニメーションGIFを表示させていてもアニメーションがされないのです。。。つまり、アニメーションではない状態のGIF画像が表示されるだけなのです。その原因はJavaScriptが処理されている間だからです。また、重いJavaScriptが処理中の際にはその間はブラウザが固まります。

その解決方法を探していたのですが、その際にようやく私もsetTimeoutの使いかたがわかりました。そして、ページ描画のタイミングを制御できるようになりました。そうです。ポイントはsetTimeoutです。重い処理を一つの関数に入れちゃダメなんですね。つまり、これはダメなのです。

  1. Event.observe(window, 'load', function() {
  2.         var loadingImage = Builder.node('img', {src: 'loading.gif'});
  3.         var sync = $('sync');
  4.         var async = $('async');
  5.         var loading = $('loading');
  6.         var working = $('working');
  7.         var loop = $('loop');
  8.         Event.observe(sync, 'click', function() {
  9.           var breakNumber = 500;
  10.           var callback = function() {
  11.             // ここが重いということにする。例えば、以下はまぁまぁ重い。
  12.             while (breakNumber--) {
  13.               working.appendChild(Builder.node('div', {className: 'test'}));
  14.               document.getElementsByClassName('test');
  15.             }
  16.             loading.removeChild(loading.firstChild);
  17.             loop.innerHTML = 501 - breakNumber;
  18.             sync.disabled = false;
  19.           }
  20.           loading.appendChild(Builder.node('div', [loadingImage, "Loading..."]));
  21.           sync.disabled = true;
  22.           setTimeout(callback, 0);
  23.         });
  24.      });

あ。ちなみにprototype.jsとscript.aculo.usがあることを前提に書いているので、その辺は適当にほげほげしてください。まぁ、ここではsetTimeoutを使用して、その中で重い処理を実行していますね。処理を始める前にアニメーション画像を出して、処理が終わる際にローディングの画像を消す処理をしています。そして、while文の中を勝手に重い処理としてここでは使用しています。しかし、このwhile文が流れている間、いや正確にはcallback関数が実行されている間はアニメーションGIFが止まります。

そこで、アニメーションGIFを止めないような方法を考えました。その根本にある考えは。。。重い処理は一つの関数に入れないということです。つまり、一つ関数の中にループで重い処理を実行させるのではなく、再帰を使用して何個も関数を呼び出す必要があります。もちろんそれが再帰処理ができないような重い処理であればこの方法は無理ですが、往々にして重い処理というのはDOMを使ってイテレーションを使用するところになる可能性が高いと思いますので、それを再帰処理に置き換えましょう。そして、修正したコードは以下の通りです。

  1. Event.observe(window, 'load', function() {
  2.         var loadingImage = Builder.node('img', {src: 'loading.gif'});
  3.         var sync = $('sync');
  4.         var async = $('async');
  5.         var loading = $('loading');
  6.         var working = $('working');
  7.         var loop = $('loop');
  8.         Event.observe(async, 'click', function() {
  9.           var breakNumber = 500;
  10.           var callback = function() {
  11.             if (breakNumber--) {
  12.               working.appendChild(Builder.node('div', {className: 'test'}));
  13.               document.getElementsByClassName('test');
  14.               setTimeout(callback, 0);
  15.             } else {
  16.               loading.removeChild(loading.firstChild);
  17.               async.disabled = false;
  18.             }
  19.             loop.innerHTML = 501 - breakNumber;
  20.           };
  21.           loading.appendChild(Builder.node('div', [loadingImage,"Loading..." ]));
  22.           async.disabled = true;
  23.           setTimeout(callback,0);
  24.        });
  25.       });

これで、重いループををしていても、だいたいの場合はアニメーションGIFが止まらずに済みます。「だいたいの場合」と書いたのは理由があります。実は、私の実環境では、もっと重い処理をしなければいけない用件がありましたので、修正版でもアニメーションGIFが動きませんでした。正確にはfirefoxでは大丈夫でした(たぶんそれほど重くなかったのが原因かもしれません。)が、IE6ではアニメーションGIFが止まりました。原因は、再帰の中で呼び出しているsetTimeoutのmillisecondの値でした。私はどうせ次の処理を実行してもらうのだから0でいいのではないか、と考えていたのですが、0だとすぐ関数をエンキューするのはいいのですが、現在走っている処理が終わると、すぐキューにたまった関数をデキューして実行するので、JavaScriptの処理が常に動いていることになったようです。あくまで想像ですが。なので、ここにラグを与える必要があったのです。そこで、setTimeoutに渡すmillisecondの値を適当な数値に変更すると、カクカクするけど、アニメーションGIFが動いてくれました。
動くサンプルは以下に置いておきます。ちなみにこのサンプルはカクカクしません。
Animation GIF and JavaScript setTimeout

404 Blog Not Found:javascript - ページはいつ再描画されるかが激しく参考になりました。

先日、jQuery開発者向けメモを見て、一通り試してみましたがなかなか直感的に書けそうなライブラリなので今後使用することを検討してみます。今、開発の勢いが一番あるライブラリなので追いかけたりするのが楽しそうですね。prototype.jsとscript.aculo.usは、なかなか新たなリリースが無く、ちょっとさみしいですが、これはこれでライブラリとして成熟段階に入ったといことでアリなのでしょうね。

さて、本日はTOEICを受けてきました。過去に2回ほど受けたのですが、相変わらず時間が足らなかったです。問題が中途半端に全問解けそうなので、ついついちゃんと読んでやってしまうのが原因です。そのため最後に15問ほど残ってしまいます。スピード勝負はつらいですね。でもまぁ、おそらくそれなりのスコアは出たでしょう。試験地の愛知大学が思ったよりもとてもキレイでした。

先日激しくヘコむことがありましたが、ようやくすっきりしました。今日から強く生きていこうと思います。

September 24, 2007

DOM検索効率化のメソッド群を作ってみた。

というわけで、もういっちょ。考えていたDOM検索効率を考えたメソッド群を作ってみた。

ええと、prototype.jsのElementオブジェクトにaddMethodsしていることからもわかるようにprototype.js必須っす。ええと、私の使っているprototype.jsは、1.5.1ね。script.aculo.us(v1.7.1_beta3)と一緒に使っているので。

  1. document.getElementsByClassNameAndTagName = function(className, parentElement, tagName) {
  2.   if (Prototype.BrowserFeatures.XPath) {
  3.      return $(parentElement).getElementsByClassName(className);
  4.   } else {
  5.     var children = $(parentElement).getElementsByTagName(tagName || '*');
  6.     var elements = [], child;
  7.     for (var i = 0, length = children.length; i <length; i++) {
  8.       child = children[i];
  9.       if (Element.hasClassName(child, className))
  10.         elements.push(Element.extend(child));
  11.     }
  12.     return elements;
  13.   }
  14. }
  15.  
  16. Element.addMethods({
  17.   getElementsByClassNameAndTagName: function(element, className, tagName) {
  18.     return document.getElementsByClassNameAndTagName(className, element, tagName);
  19.   },
  20.  
  21.   nextElement: function(element) {
  22.     do {
  23.       element = element.nextSibling;
  24.     } while (element && element.nodeType != 1);
  25.     return $(element);
  26.   },
  27.  
  28.   previousElement: function(element) {
  29.     do {
  30.       element = element.previousSibling;
  31.     } while (element && element.nodeType != 1);
  32.     return $(element);
  33.   },
  34.  
  35.   firstChildElement: function(element) {
  36.     var child = element.firstChild;
  37.     while (child && child.nodeType != 1) {
  38.       child = child.nextSibling;
  39.     }
  40.     return $(child);
  41.   },
  42.  
  43.   lastChildElement: function(element) {
  44.     var child = element.lastChild;
  45.     while (child && child.nodeType != 1) {
  46.       child = child.previousSibling;
  47.     }
  48.     return $(child);
  49.   },
  50.  
  51.   childElements: function(element) {
  52.     var children = [];
  53.     var child = element.firstChild;
  54.     while (child) {
  55.       if (child.nodeType == 1) {
  56.         children.push($(child));
  57.       }
  58.       child = child.nextSibling;
  59.     }
  60.     return children;
  61.   },
  62.  
  63.   childElement: function(element, index) {
  64.     var nodeIndex = 0;
  65.     var child = element.firstChild;
  66.     while (child) {
  67.       if (child.nodeType == 1 && index == nodeIndex++) {
  68.           return $(child);
  69.       }
  70.       child = child.nextSibling;
  71.     }
  72.     return null;
  73.   },
  74.  
  75.   cleanWhitespaceRecursive: function(element) {
  76.     var f = function(element) {
  77.       var child = $(element).cleanWhitespace().firstChild;
  78.       while (child) {
  79.         if (child.nodeType == 1) {
  80.           f(child);
  81.         }
  82.         child = child.nextSibling;
  83.       }
  84.     };
  85.     f(element);
  86.     return element;
  87.   }
  88. });

nextElement, previousElementはそのまんま。textNodeはすっ飛ばして、elementNodeだけを見ている。firstChildElementとlastChildElementはfirstElementとlastElementって命名しようかと思ったけど、childだということを意識したかったのでちょいと冗長だけど、これで堪忍してや。で、意味もそのまんま。textNodeをすっ飛ばして最初や最後のelementNodeを返す。

childElementsは、前回のポストの結果を考慮してfirstChildとnextSiblingを採用。elementNodeの配列を返す。childElementは、index指定でelementNodeを返す。本当は、こんな感じでchildElementsメソッドの返す配列のindexを返す方がすっきりしていていいんだけどなぁ。

  1. childElement: function(element, index) {
  2.      return $(element).childElements()[index];
  3.   },

しかし、それだとnextSiblingを全て見てまわってしまうので、遅くなりそうなので、不採用。

cleanWhitespaceRecursiveはついでの産物。今回作成したものとは関係がないのだけども、一応。HTMLコーダにじかにHTMLを書かれるとwhitespaceが入ってしまい、困るので再帰的に消してみることにした。JavaScriptで再帰をするときってやっぱり、ローカル変数に関数をぶちこんで、それを何度も呼び出す方がいいのかな、と思ったので、自分自身を何度も呼び出すような方は不採用。
自分自身を呼び出すのは、こんな感じか。

  1. cleanWhitespaceRecursive: function(element) {
  2.     element = $(element);
  3.     var child = element.cleanWhitespace().fistChild;
  4.     while (child) {
  5.       if (child.nodeType == 1) {
  6.         child.cleanWhitespaceRecursive();
  7.       }
  8.       child = child.nextSibling;
  9.     }
  10.     return element;
  11.   }

どっちがいいんだろうなぁ。。。

で、本当は、オプションで、$指定で返すか、そのままのelementを返すかを指定できるようにするかで迷ったのだけど、prototype.jsと一緒に使うことを前提としているので、$で返すようにした。これもパフォーマンスに関係してくるんだけど、まぁ、その辺は考慮中。

つーか、また後で追記したり、ソース修正するかもしれん。名前はなんでもよかったのだけど、domx.jsとしよう。

  1. <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
  2. <html lang="ja">
  3.     <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  4.     <meta http-equiv="Content-Style-Type" content="text/css" />
  5.     <meta http-equiv="Content-Script-Type" content="text/javascript" />
  6.     <script language="JavaScript" type="text/javascript" src="js/prototype.js"></script>
  7.     <script language="JavaScript" type="text/javascript" src="js/scriptaculous.js"></script>
  8.     <script language="JavaScript" type="text/javascript" src="domx.js"></script>
  9.     <script language="JavaScript" type="text/javascript">
  10.     Event.observe(window, 'load', function() {
  11.       var mainContainer = $('main');
  12.       var fc = mainContainer.firstChildElement();
  13.       fc.style.backgroundColor = '#eee';
  14.  
  15.       var lc = mainContainer.lastChildElement();
  16.       lc.style.backgroundColor = '#aaa';
  17.  
  18.       var ce = mainContainer.childElement(1);
  19.       ce.style.backgroundColor = '#ddd';
  20.  
  21.       var ne = ce.nextElement();
  22.       ne.style.backgroundColor = '#ccc';
  23.  
  24.       var pe = lc.previousElement();
  25.       pe.style.backgroundColor = '#bbb';
  26.  
  27.       $A(mainContainer.childElements()).each(function(e) {
  28.         e.style.margin = '0.5em';
  29.       });
  30.  
  31.       var sushinoneta = mainContainer.getElementsByClassNameAndTagName('sushinoneta', 'input');
  32.       $A(sushinoneta).each(function(neta) {
  33.           neta.style.marginLeft = '20px';
  34.           switch (neta.name) {
  35.           case 'tako':
  36.             neta.value = '380';
  37.             break;
  38.           case 'ikura':
  39.             neta.value = '420';
  40.             break;
  41.           case 'uni':
  42.             neta.value = '600';
  43.             break;
  44.           default:
  45.             neta.value = '360';
  46.             break;
  47.           }
  48.       });
  49.       var before = $('before');
  50.       before.appendChild(Builder.node('h1', 'Before cleanWhitespaceRecursive'));
  51.       before.appendChild(Builder.node('textarea', {cols: '100', rows: '5'}, mainContainer.innerHTML));
  52.       mainContainer.cleanWhitespaceRecursive();
  53.       var after = $('after');
  54.       after.appendChild(Builder.node('h1', 'After cleanWhitespaceRecursive'));
  55.       after.appendChild(Builder.node('textarea', {cols: '100', rows: '5'}, mainContainer.innerHTML));
  56.     });
  57.     </script>
  58.     <title>DOMX Demo</title>
  59.   </head>
  60.     <div id="main">
  61.       <div>
  62.         <label>
  63.           <span>tako:</span>
  64.           <input type="text" name="tako" class="sushinoneta" />
  65.         </label>
  66.       </div>
  67.       <div>
  68.         <label>
  69.           <span>ika:</span>
  70.  
  71.           <input type="text" name="ika" class="sushinoneta" />
  72.         </label>
  73.       </div>
  74.       <div>
  75.         <label>
  76.           <span>uni:</span><input type="text" name="uni" class="sushinoneta" />
  77.         </label>
  78.       </div>
  79.  
  80.       <div>
  81.         <label>
  82.           <span>ikura:</span><input type="text" name="ikura" class="sushinoneta" />
  83.         </label>
  84.       </div>
  85.       <div>
  86.         <label>
  87.           <span>maguro:</span>
  88.  
  89.           <input type="text" name="maguro" class="sushinoneta" />
  90.         </label>
  91.       </div>
  92.     </div>
  93.     <div id="before"></div>
  94.     <div id="after"></div>
  95.   </body>
  96. </html>

ソースはここ。

domx.js
ライセンスは適当っす。自己責任で使ってちょ。

で、上のソースを動かす形にしたデモもここに置いておく。domx demo

うーん。そろそろjQuery使ってみようかなぁ。。。onReadyとか便利そうだし。。。インデントがタブなのが非常に嫌なので、敬遠しているのだけど。。。

全然関係ないが、ここ二日ほど愛知県図書館で開発をしている。なかなか良いね。ホットスポットもあるみたいだけど、契約をしていないのでインターネットにつなげない。でも、そのおかげで効率がいいよん。インターネットがあるとダラダラしちゃって、ダミだ。

DOM検索効率化をもう少し追ってみる。

やったよ。やった。ようやくキーボードが届いたよ。ここ3ヶ月、私のキーボードはC-xと]}のキーが壊れていたのだけど、ようやく快適に打てるようになったよ。私の状態を見兼ねて買ってくれた学生時代の恩師の稲葉先生どうもありがとうございます。つーか、そのくらい自分で買えばいいのだけど、なんか買う気がなかなか起こらなくて、ダラダラしてました。。。

で、さっそく簡単なプログラムを書きはじめている。えと、実はまだDOM検索周りを調べているのだけど、一つ疑問があがってきた。それは以下の通り。

childNodesでの検索とfirstChildとnextSiblingの検索について。つまり、どちらでも子ノードを取得することができるのだけど、実際どっちを使ったらいいの?

ググってみるとパフォーマンスを見ている人がいたのだが、childNodesよりも、fistChildを取ってnextSiblingで検索する方が速いということらしい。特に、IE6が良くないとのこと。ブラウザの実装によるのね。。。
new Blah().list(); node.childNodes[] performance
うーむ。てっきりnextSiblingって、いかにもリンクリストな感じでその要素の数を線形探索しないといけないから、O(log(n))で、childNodesでindex指定で取得したら、O(1)で速いんだろうなぁ、なんて思っていたのだけど、どうやらそうでないみたい。。。と鵜呑みにするのもどうか、と思うのだが、まぁ、いいや。

ええと、何でこんなことを調べているかと言うと、私はchildNodesからelementNodeだけの配列を取得するようなメソッドが欲しかったので、作ろうと思っていたのだ。そして、その際に、firstChildからnextSiblingでグルングルン回すか、childNodes分こっちもグルングルン回するかどっちにしようかな、と思っていたのだ。そこで自分でベンチマークを取ってみたよ。まぁ、結局、全てを線形探索するのだから、どっちでもいいような気がするけども。。。で、ベンチマークツールはamachang氏のbenchmark.jsを使用した。

ソースはこんな感じ。ここにサンプルも置いておくよ。childNodes && firstChild + nextSibling Benchmark

  1. <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
  2. <html lang="ja">
  3.     <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  4.     </meta><meta http-equiv="Content-Style-Type" content="text/css">
  5.     </meta><meta http-equiv="Content-Script-Type" content="text/javascript">
  6.     <script language="JavaScript" type="text/javascript" src="js/prototype.js"></script>
  7.     <script language="JavaScript" type="text/javascript" src="js/scriptaculous.js"></script>
  8.     <script language="JavaScript" type="text/javascript" src="benchmark.js"></script>
  9.     <script language="JavaScript" type="text/javascript">
  10.       Element.addMethods({
  11.         _childElementsByFirstChildNextSibling: function(element) {
  12.           var children = [];
  13.           var child = element.firstChild;
  14.           while (child) {
  15.             if (child.nodeType == 1) {
  16.               children.push(child);
  17.             }
  18.             var child = child.nextSibling;
  19.           }
  20.           return children;
  21.         },
  22.         _childElementsByChildNodes: function(element) {
  23.           var children = [];
  24.           var nodes = element.childNodes;
  25.           var child = null;
  26.           for (var i = 0, l = nodes.length; i <l; i++) {
  27.             child = nodes.item(i);
  28.             if (child.nodeType == 1) {
  29.               children.push(child);
  30.             }
  31.           }
  32.           return children;
  33.         },
  34.         _childElementByChildNodes: function(element, index) {
  35.           var nodeIndex = 0;
  36.           var nodes = element.childNodes;
  37.           var child = null;
  38.           for (var i = 0, j = 0, l = nodes.length; i <l; i++) {
  39.             child = nodes.item(i);
  40.             if (child.nodeType == 1 && index == j++) {
  41.                return child;
  42.             }
  43.           }
  44.           return null;
  45.         },
  46.         _childElementByFirstChildNextSibling: function(element, index) {
  47.           var nodeIndex = 0;
  48.           var child = element.firstChild;
  49.           while (child) {
  50.             if (child.nodeType == 1 && index == nodeIndex++) {
  51.               return child;
  52.             }
  53.             child = child.nextSibling;
  54.           }
  55.           return null;
  56.         }
  57.       });
  58.     benchmark({
  59.         'initialize': function() {
  60.           for (i = 0; i <5000; i++) {
  61.             $('main').appendChild(Builder.node('div', 'item:' + i));
  62.           }
  63.         },
  64.         'firstChild + nextSibling childElements': function() {
  65.           $A($('main')._childElementsByFirstChildNextSibling()).each(function(e) {
  66.             e.style.margin = '0.5em';
  67.           });
  68.         },
  69.         'childNodes childElements': function() {
  70.           $A($('main')._childElementsByChildNodes()).each(function(e) {
  71.             e.style.margin = '0.5em';
  72.           });
  73.         },
  74.         'firstChild + nextSibling childElement': function() {
  75.            var ce = $('main')._childElementByFirstChildNextSibling(50);
  76.            ce.style.backgroundColor = '#ddd';
  77.         },
  78.         'childNodes childElement': function() {
  79.            var ce = $('main')._childElementByChildNodes(50);
  80.            ce.style.backgroundColor = '#ddd';
  81.         },
  82.         'prototype.js down + next': function() {
  83.           var ce = $($('main').down()).next(49);
  84.            ce.style.backgroundColor = '#ddd';
  85.         }
  86.    });
  87.     </script>
  88.     <title>childNodes && firstChild + nextSibling Benchmark + next</title>
  89.   </script></meta></head>
  90.   <body>
  91.     <h1 style="text-align:center;">childNodes && firstChild + nextSibling Benchmark</h1>
  92.     <div id="main"></div>
  93.   </body>
  94. </html>

firefox2.0.0.7の私の環境での結果は。。。

preparing ...
let's go!
.
*** initialize ***
result : 2763.966739[ms]
.
*** firstChild + nextSibling childElements ***
result : 680.966739[ms]
.
*** childNodes childElements ***
result : 400.966739[ms]
.
*** firstChild + nextSibling childElement ***
result : 48.991075[ms]
.
*** childNodes childElement ***
result : 51.991075[ms]
.
*** prototype.js down + next ***
result : 440.966739[ms]
.
finish!

childNodesの方が速い。まぁ、あんまり変わらないけど、予想と違うぞ?つーか、downとnextの組み合わせ遅すぎ。
ついでにIE6でも見てみる。

preparing ...
let's go!
.
*** initialize ***
result : 67827.967579[ms]
.
*** firstChild + nextSibling childElements ***
result : 1151.967579[ms]
.
*** childNodes childElements ***
result : 14900.967579[ms]
.
*** firstChild + nextSibling childElement ***
result : 59.089179[ms]
.
*** childNodes childElement ***
result : 3354.967579[ms]
.
*** prototype.js down + next ***
result : 22592.967579[ms]
.
finish!

うーん。洒落になってねぇ。ブラウザが死んだかと思たよ。。。childNodes重いっす。。。まぁ、線形探索っぽくchildNodesのitemを見て回っているので、遅くなることはしょうがないのだけど、なんとかならんかな。。。textNodeをすっ飛ばさなくてもいいのだったら、childNodesでindex指定で一発で取れて速そうだけど、今回の目的のelementNodeだけが欲しいならダメだね。こりゃ。

というわけで、firefoxでは、少しだけchildNodesの方が速かったのだけど、IE6のためにfirstChildとnextSiblingを採用するということになりました。。。つーか、downとnextはさらに使いものにならん。。。

prototype.jsのcleanWhitespaceなんかを見てみると、firstChildからnextSiblingを取っているのは理由があったのね。

September 14, 2007

DOM操作は自分で書いた方が良さそう。

未だ、prototype.js。しかし、DOM周りはやっぱり重くて使い物にならん。。。nextとかあかんね。引数無しで使っても重すぎ。というわけで、自作でnextElementとかのメソッドを追加するのが良さそうだ。まぁ、とりあえず調べてみた。そしたら、はてなの匿名ダイアリーに結構コッテリしたソースが載っているじゃない?つーか、このレベルのJavaScriptが書けたら、いいよなー。

というわけで、一応リンク。つーか、読みがいもあるし、いいかもしんね。つーか、Tenって何?
http://anond.hatelabo.jp/20070719173038

えーと、特に使えそうだと思ったのは、Ten.DOM周り。このままじゃ使えんけど、まさにやりたかったことが書いてある。

  1. Ten.DOM = new Ten.Class({
  2.     getElementsByTagAndClassName: function(tagName, className, parent) {
  3.         if (typeof(parent) == 'undefined') {
  4.             parent = document;
  5.         }
  6.         var children = parent.getElementsByTagName(tagName);
  7.         if (className) {
  8.             var elements = [];
  9.             for (var i = 0; i <children.length; i++) {
  10.                 var child = children[i];
  11.                 var cls = child.className;
  12.                 if (!cls) {
  13.                     continue;
  14.                 }
  15.                 var classNames = cls.split(' ');
  16.                 for (var j = 0; j <classNames.length; j++) {
  17.                     if (classNames[j] == className) {
  18.                         elements.push(child);
  19.                         break;
  20.                     }
  21.                 }
  22.             }
  23.             return elements;
  24.         } else {
  25.             return children;
  26.         }
  27.     },
  28.     removeEmptyTextNodes: function(element) {
  29.         var nodes = element.childNodes;
  30.         for (var i = 0; i <nodes.length; i++) {
  31.             var node = nodes[i];
  32.             if (node.nodeType == 3 && !/\S/.test(node.nodeValue)) {
  33.                 node.parentNode.removeChild(node);
  34.             }
  35.         }
  36.     },
  37.     nextElement: function(elem) {
  38.         do {
  39.             elem = elem.nextSibling;
  40.         } while (elem && elem.nodeType != 1);
  41.         return elem;
  42.     },
  43.     prevElement: function(elem) {
  44.         do {
  45.             elem = elem.previousSibling;
  46.         } while (elem && elem.nodeType != 1);
  47.         return elem;
  48.     },
  49.     scrapeText: function(node) {
  50.         var rval = [];
  51.         (function (node) {
  52.             var cn = node.childNodes;
  53.             if (cn) {
  54.                 for (var i = 0; i <cn.length; i++) {
  55.                     arguments.callee.call(this, cn[i]);
  56.                 }
  57.             }
  58.             var nodeValue = node.nodeValue;
  59.             if (typeof(nodeValue) == 'string') {
  60.                 rval.push(nodeValue);
  61.             }
  62.         })(node);
  63.         return rval.join('');
  64.     },
  65.     onLoadFunctions: [],
  66.     loaded: false,
  67.     timer: null,
  68.     addEventListener: function(event,func) {
  69.         if (event != 'load') return;
  70.         Ten.DOM.onLoadFunctions.push(func);
  71.         Ten.DOM.checkLoaded();
  72.     },
  73.     checkLoaded: function() {
  74.         var c = Ten.DOM;
  75.         if (c.loaded) return true;
  76.         if (document && document.getElementsByTagName &&
  77.             document.getElementById && document.body) {
  78.             if (c.timer) {
  79.                 clearInterval(c.timer);
  80.                 c.timer = null;
  81.             }
  82.             for (var i = 0; i <c.onLoadFunctions.length; i++) {
  83.                     c.onLoadFunctions[i]();
  84.             }
  85.             c.onLoadFunctions = [];
  86.             c.loaded = true;
  87.         } else {
  88.             c.timer = setInterval(c.checkLoaded, 13);
  89.         }
  90.     }
  91. });

でも、私はElementとか思いっきり拡張しちゃうタイプなので、nextElementやpreviousElementは、addMethodしちゃう。ついでに、childElementsなども作っちゃおうかなー。そして、引数に数字を当てて、とっちゃうような感じで作ろうかしらん。他にも、prototype.jsのcleanWhitespace()辺りをrecursiveにする奴とか。

しかし、今は、うちのキーボードが壊れて、「カッコ閉じる」が入らないので、新しいキーボードが来てからにしよう。

September 7, 2007

まだまだバグが多いのね。

最近は、ずっとJavaScriptを書いていて、世間ではjQueryが注目されている中、私は未だprototype.jsとscript.aculo.usを使っている。今の仕事では結構、面倒くさいこといろいろやっているんだけど、prototype.jsやscript.aculo.usってまだまだバグが多いのね。というか、script.aculo.usの中に入っているprototype.jsが古いっていうのもあるんだろうけど、悩ましいなぁ。ここではまると結局、中のソースを見たりしないといけないので、時間がかかってしょうがない。

で、はまったついでにメモとして書いておく。

  1. Insertion.Afterでtableのtrを複数入れようとしたときに順番が逆になる。
  2. えと、今作っているところの一つはtd要素にrowspanとか使っているので、複数のtr要素を一つの行として扱わないといけないところがあった。
    そこで、rowspan分挿入しようとしたのだが、順番が逆になって入ってしまう。。。前回は、結構バグに悩んだけど、今回からはもう既存のバグをまず探す。そして、解決されていればいつ対応されたのかバージョンを探る。で、今回見つけたのは、これ。

    Insertion.After inserts table rows the wrong way round in IE

    Prototype.jsでは、1.5.2でFixedらしい。ということは、script.aculo.usと同時に使えないね。アプリ側でPrototype.Versionを見て、1.5.2より下の場合のみ拡張するコードを書いたよ。

  3. draggableな要素がoverflowのhidden, scroll, auto(たぶん)のときには、そのoverflowを指定してある要素の外では見えなくなっちゃう。
  4. えと、これはかなりはまった。。。。orz....divでoverflow:scrollな要素が二つあって、そこの間をドラッグドロップをしようとしていたのだけど、移動先のdivに入るとドラッグレイヤーの下に入ったかのように見えて、消えちゃうの。で、いろいろ探した結果、結構ホットなバグみたい。IE6ではちゃんと動くけど、mozillaとIE7ではダメというちょっと不思議なバグ。つーか、IE6の動作がそもそもいかんのかな。。。
    draggables are stuck in a div with overflow:scroll. A way to move them to upmost dom level needed

    Dragging an element outside a overflow:auto container

    Drag draggables outside an overflow:auto div to an external droppable container

    で、解決方法は結構大変。えと、問題なのは、
    draggables are stuck in a div with overflow:scroll. A way to move them to upmost dom level needed

    I discussed this with Thomas some time ago, and the problem is that the element we move (be it in ghosting mode or not) usually must remain at the same DOM level because otherwise, styles might get lost. I'm working on a solution right now, too, it will involve a new option you can use to assign a class name to the element while it's being dragged. If this class name is supplied, the element will be attached to the body. Let's see if this works, I'll provide the patch as soon as it is ready

    ということなのだ。つまり、z-indexを付けようがどうだろうが、ダメなものはダメなのだ。解決方法としては、body要素の下にdraggableな要素をappendChildしてやればいいのだが、それをするとstyleが無くなる可能性ががあるというわけ。もちろん位置も合わせないといけないしね。つまり、ドラッグする要素のstyleがそれに付けられているclassやidなどで完結していればいいのだが、そうでないときは、いきなりbody要素の下に付けてもさぁ、困ったな、といったことが起きるのだ。li要素とか。。。で、未だ正しい解決方法は出てきていない。まぁ、こういうところがハックのしがいがあるんだよね。

    今回の私が使用したかったところは、div要素で完結していたので、width指定とかもしておけば、body要素の下に置いても大丈夫だった。後は、Positionの位置を合わせるだけ。
    Draggables.addObserverのonStartとonEndを使用して、要素をbody直下に変えたり、戻したりしたらいけた。位置がずれるところはちょっと泥臭いことしたけど、もう少しキレイに書けたらここに載せるかもしれん。

しかし、
Super.mario.bros
辺とか見ると自分はまだまだっす。。。先は長いなー。

Bloglines feedburner