JavaScript > script.aculo.usのSortable.createにおいてonUpdateが実行されない原因

2006年03月26日

script.aculo.usのSortable.createにおいてonUpdateが実行されなかったのでメモ。

説明がかなり長くなるので結論から言う。 Sortable.CreateでonUpdateを実行するには、ドラッグ&ドロップを行う要素の id属性に'_'(アンダーバー)を含める必要があり、かつ'_'以降の文字列が要素ごとにユニークになるようにする必要がある。

例えば、

<div id="col">
    <div id="drag_1"></div>
    <div id="drag_2"></div>
</div>

みたいにする必要がある。 これを、

<div id="col">
    <div id="drag1"></div>
    <div id="drag2"></div>
</div>

のようにしてしまうと、 ドラッグ&ドロップで要素の順番の変更が確定しても、 onUpdateは実行されない。

optionでformatを指定すれば'_'を含めなくても回避することが出来る。 formatにはついては後半で説明する。

これに気づかずに、しばらく悩んだのでscript.aculo.usのソースを引用しながら詳しく説明してみる。 かなり長くなるのでお手洗いを済ませた上で、飲み物を用意した方がいい。

まずはサンプルのソースから

<div id="col">
   <div id="drag_1">id="drag_1"</div>
   <div id="drag_2">id="dtag_2"</div>
</div>
<script language="javascript">
    Sortable.create("col",
      {
        dropOnEmpty:true,
        tag:'div',
        handle:'handle',
        constraint:false,
        onUpdate:function(el)
        {
            alert('順番の変更確定')
        }
      }
    );
</script>

Sortable.createの第一引数には内部の要素をドラッグ可能にするための列のid属性の値を指定。 第二引数にはオプション値を指定する。
オプション値の中にonUpdateとして実行する処理を関数オブジェクトで書く。

このonUpdateは何をするのかと言うと、ドラッグで要素を移動し、ドロップした時に列内の要素の 順番が変更した時に呼ばれるイベントハンドラ。

次は問題になるSortable.serialize()

/**
*引数で渡されたElementオブジェクトのid値から、子要素のid値を順番通りに取得してから文字列にして返す
*/
serialize: function(element) {
  
    element = $(element);
    var sortableOptions = this.options(element);
    var options = Object.extend({
      tag:  sortableOptions.tag,
      only: sortableOptions.only,
      name: element.id,
      format: sortableOptions.format || /^[^_]*_(.*)$/
    }, arguments[1] || {});
    return $(this.findElements(element, options) || []).map( function(item) {
      return (encodeURIComponent(options.name) + "[]=" + 
              encodeURIComponent(item.id.match(options.format) ? item.id.match(options.format)[1] : ''));
    }).join("&");
  }

Sortable.createは引数を渡して実行した時に、Sortable.serialize()を使って列内の要素順を 文字列にしてスクリプト内部に保存している。

上記のサンプルを実行した時にスクリプト内部で保存している文字列。
col[]=1&col[]=2

colはサンプルで示した列の<div>要素のid値、1と2はそれぞれ、 colの子要素の<div>要素のid値id=<drag_1>'_'で区切った時の後ろの値。

で、ドラッグした要素をドロップした時に、もう一度Sortable.serialize()を実行して、 現在の要素順を取得する。
ドロップした時の順番と最初に保存した文字列が違ってれば、列内の順番変わったとみなし onUpdateが実行される。

例
//初期位置
//この状態ではcol[]=1&col[]=2を初期値として内部に保存してる。
<div id="col">
   <div id="drag_1">id="drag_1"</div>
   <div id="drag_2">id="dtag_2"</div>
</div>

//ドロップで順番が変更した時
//Sortable.serialize()はcol[]=2&col[]=1を返すので、初期位置の文字列と比較する。
<div id="col">
   <div id="drag_2">id="drag_2"</div>
   <div id="drag_1">id="dtag_1"</div>
</div>

/*
*[初期値と変更時の値を比較]
*col[]=1&col[]=2(初期値)
*col[]=2&col[]=1(ドロップ時)
*違ってるいのでonUpdateを実行
*/

onUpdateが呼ばれる仕組みが分かった所で、Sortable.serialize()のソースを見てみる。

    //引数で渡されたelementオブジェクトのid値からオブジェクトを取得
    element = $(element);
    
    //id値からSortable.createを実行した時のオプションを取得
    var sortableOptions = this.options(element);
    
    //取得したオプション値をオブジェクトにする。
    var options = Object.extend({
      tag:  sortableOptions.tag,
      only: sortableOptions.only,
      name: element.id,
      format: sortableOptions.format || /^[^_]*_(.*)$/
    }, arguments[1] || {});

ここまでは大丈夫。問題は次である。

    return $(this.findElements(element, options) || []).map( function(item) {
      return (encodeURIComponent(options.name) + "[]=" + 
              encodeURIComponent(item.id.match(options.format) ? item.id.match(options.format)[1] : ''));
    }).join("&");
    
    /**
    *【this.findElements(element, options)】
    *optoinsの条件に従いelementの子要素のid値を配列にして返す。
    *elementにはElementオブジェクトのid値、
    *optionsには先に定義したoptionオブジェクト。
    *[optoins.tag]取得したい子要素のタグ名
    *[options.only]class=""で指定されるクラス値
    *[options.name]列要素のid値、サンプルの場合'col'
    *[options.formaat]idを区切る為の正規表現
    */

this.findElements(element, options)でelementの子要素のid値を取得して、 それをまとめて$()にいれてElementオブジェクトの配列を取得。
map()を使ってElementオブジェクトの配列すべてに対して関数を実行。 実行後の配列をjoin()で繋げて返すという処理を行っている。

map()で各Elementオブジェクトに対して実行される関数が問題なのである。

    function(item) {
      
      //options.nameは列要素のid値、サンプル例ならば'col'が入る。
      return (encodeURIComponent(options.name) + "[]=" + 
              encodeURIComponent(item.id.match(options.format) ? item.id.match(options.format)[1] : ''));
    }

item.id.match(options.format)は子要素のid値が正規表現に一致するか調べている。options.formatは/^[^_]*_(.*)$/となっており、 これに一致すれば'/^[^_]*_(.*)$/'の( )で指定された.*にあたる文字が返される。

この正規表現'/^[^_]*_(.*)$/'が_で区切ってるので、 ドラッグで移動させる要素のid属性は'_'で区切る必要がある訳である。

ここで正規表現に一致しないid、サンプルの例でid="drag1"が指定されてるとnullが返ってくるので、 Sortable.serialize()は最終的に'col[]=col=[]'を返す。

列要素の子要素に誤ったidを指定している限り正規表現に一致しないので、Sortable.serialize()は常に'col[]=col=[]'を返すので 初期値とドロップ時を比較しても同じになりonUpdateは実行されなくなる。

これを回避するには仕様通りid値を'_'で区切ってやるか、Sortable.create実行時のoptionでfomatを指定する必要がある。

formatを指定する時は以下のようにする。

<div id="col">
   <div id="drag_1">id="drag1"</div>
   <div id="drag_2">id="drag2"</div>
</div>
<script language="javascript">
    Sortable.create("col",
      {
        dropOnEmpty:true,
        tag:'div',
        handle:'handle',
        constraint:false,
        format: /^[a-z]+([0-9]+)$/,
        onUpdate:function(el)
        {
            alert('順番の変更確定')
        }
      }
    );
</script>

これで、id値を'_'で区切らなくても、onUpdateが実行される。
さらに言うとHTMLの構造上id属性はHTML内に唯一である必要があるので、 HTMLを正しくコーティングしていれば
format : /((.+))/
これで良い。

唯一の存在であるidなのに何故わざわざ'_'で区切ってユニークな値を取ろうとしてるのかは不明だが、 HTMLの構造が正しくなくても動くよう配慮した結果かも知れないので、そっとしておく。

長々と説明してきたが、script.aculo.usのwikiのFAQにはonUpdateが動かない時はid属性がどうたらこうたら書いてあり、 Sortable.serialize()の説明を見てくれとなってる。
Sortable.serialize()の説明にformatの事が触れられていた事を知ったのは、この記事を書き終えた後である。

  • プログラマーたるもの、英語のドキュメントに恐れてはならない。
  • プログラマーであろうが、ソースを解読する前に、英文を解読する必要がある。
  • プログラマーだからといって、ソースから解読しだすのは駄目だ。

いろんな意味で勉強になりました。

posted by 37to at : 07:51 | コメント (0) | トラックバック (0)

コメント

この記事に対するコメントはまだありません。


投稿する

投稿者情報を保存しますか?


トラックバック

トラックバックURI


一覧

この記事に対するトラックバックはまだありません。