FC2ブログ

10 | 2019/11 |  12

  1. 無料サーバー

User forum-FC2BLOG-Info-Edit Template-Post-Edit-Upload-LogOut

CSSやJavascript自習の苦闘史を綴っていきたい。恐縮ですがJavascriptを有効にしてご覧ください。
2005年12月から社会問題も掲載!


ブログ頁内ジャンプ移動に際して、ジャンプ先からジャンプ元に直ぐに戻れるようにする jQuery プラグイン

このエントリイの後に、別のもっとクールな 目次自動作成プラグイン を作りました。

このエントリでは 2 通りの目次が作成され、表示されるようにしましたが、それはここで述べているプラグインが、旧バージョンの目次作成プラグインを前提に作成されているからです。

なお、新しい目次自動作成プラグインについては、こちらで 述べています。

ここで作成した backNforth プラグインは animatedPopup プラグインの使用を前提にしていますが、その animatedPopup プラグインは恐縮ですが IE では動きません。原因は jquery.js ver 1.4.2 の CSS スタイルに係る部分に、IE に対してバグとなってしまう箇所があるようです。この問題は ver 1.4.4 でも解決していないようです。

長文 Web サイト閲覧で困ること

エントリイ情報が多い場合スクロールも大変であるとともに、目次があってそこからそれぞれのアイテム箇所にジャンプできるとしても、ジャンプ後に再び目次に戻りたい場合があります。

その場合には「目次に戻る」ジャンプ機能を搭載すればよいわけですが、他にも直前にジャンプした箇所に戻りたいこともあります。

つまり、あちこちの箇所に容易に行ったり来たりすることが出来れば、閲覧に際して目的の箇所に簡単に辿り着けることになります。

そこでそのようなことを可能とする jQuery プラグインを作ってみました。

当面の運用に際して、次の5つのジャンプ先がリスト表示されて、目的の箇所名をクリックすれば、直ぐにその箇所が表示されるようにしました。

その 5 つのジャンプ先とは、ページトップ、ページエンド、直前の箇所、その前の箇所、最初の箇所です。

▲ToTop

頁内移動を容易にするために

上記 5 つのジャンプ先に簡単に移動するためには、それらがダイアログ内にリスト表示され、目的箇所名をクリックすれば直ぐにそこにジャンプするようにすればよいと考え、そのリストダイアログをポップアップ表示することにしました。

ポップアップは、目次などジャンプする為に使用する箇所に起動イベントを仕込み、ジャンプした後にその画面内のトップ位置にリストダイアログが表示されるようにしました。

ジャンプ先ダイアログをアニメーション表示/隠蔽する

ダイアログは animatedPopup プラグインを使用して出現を演出しました。

しかも、そのポップアップは二段階にして、最初は余り露骨ではなく控えめに表示し、ジャンプダイアログを使いたくなければ直ぐに閉じられるようにしました。

控えめなポップアップからリストダイアログ表示への移行を容易にするために、単純にcloseBar 以外の箇所にマウスオーバーすれば良いようにしました。

何はさておき実例を

  1. このページの上の方にスクロールして、目次を表示してください。
  2. その中の適当な行をクリックすると頁内の該当箇所にジャンプします。
  3. すると移動直後に画面上部中央に小さなポップアップが表示されます。
  4. その closeBar 以外の箇所にマウスオーバーすると、ジャンプダイアログがアニメートポップアップされますので、その 5 つのリスト内から適当な箇所をクリックすれば、目的の箇所に移動します。
  5. ジャンプダイアログにはスクロールイベントハンドラーを組み込み、移動後も常にページ上部中央部に表示され続けるようにしたので、次のジャンプも容易に行えます。

スクリプトコード

  1:(function($){
  2:$.fn.backNforth = function(opts){
    /* 頁内リンク移動を行うポップアップを表示し、容易にジャンプ元に戻ったり
     * 頁トップ、頁ボトムなどへの移動をポップアップ内から指定出来るメソッドで、
     * 全面的に animatedPopup プラグインメソッドを利用している。
     * 当該メソッドの起動元インスタンスは、任意のジャンプ元要素(複数可) とする。
     * 特徴的なことは、href 属性に基づくリンク先移動のためのクリックイベントと
     * 当該プラグインメソッドを起動するイベントが、同じクリックイベントであること。
    * 勿論、他のイベントでも当該メソッドを起動出来るがクリックが一般的だろう。
     * 引数はオブジェクト形式で指定する。ポップアップ内に表示するジャンプ先情報と、
     * ポップアップの現れ方 (0~8)を指定する。
     * 但しジャンプ先情報はスクリプト本体を書き換える必要があるので、一般には指定する必要がなく、
     * 既定値と異なるジャンプ先を表示したい場合には、コードの書き換えが必要となる。
     * release:2010/11/12
     */
  3:  var jQ=this; if (jQ.size()<1) (function(){return false})();
  4:  var jumpList=[], o, $animPopup,$destination,guidePos;
  5:  if (opts==="?" || opts==="?"){
  6:    var mes = "■backNforth プラグインの引数指定方法■\n";
  7:    mes +="{ guidePos : num }\nnum は頁内移動 guide の表示位置を意味し、\n0~8 の数値にする。\n";
  8:    mes +="0:center of window,1:leftTop,2:rightTop,3:rightBottom\n";
  9:    mes +="4:leftBottom,5:topEdge,6:rightEdge,7:bottomEdge,8:leftEdge";
 10:    alert(mes);return;
 11:  }
 12:  o = $.extend({}, $.fn.backNforth.opts, opts && typeof opts==="object" && opts.constructor === Object && opts || {});
 13:  var errFunc = function(){
 14:    if (o.guidePos <1 || o.guidePos >8){
 15:      alert("guidePos の値が不正です。\nguidePosは 0~8 の数値にしてください。"); return;
 16:    }
 17:  }
 18:  var setGuidePos = function(num){
 19:    var obj = num === 0 ? {left:"center",top:"center"} :
 20:      num === 1 ? {left:"0px",top:"0px"} :
 21:      num === 2 ? {right:"0px",top:"0px"} :
 22:      num === 3 ? {right:"0px",bottom:"0px"} :
 23:      num === 4 ? {left:"0px",bottom:"0px"} :
 24:      num === 5 ? {left:"center",top:"0px"} :
 25:      num === 6 ? {right:"0px",top:"center"} :
 26:      num === 7 ? {left:"center",bottom:"0px"} : {left:"0px",top:"center"};
 27:    return $.extend({},obj,{opacity:0.8,origin:num}); //若干透明にする
 28:  }
 29:  var displayJumplist = function(){
 30:    $(this).animatedPopup(o.moveStr,{
 31:      popupCSS:$.extend({},this.guidePos,{textAlign:"left",cursor:"pointer",opacity:1}),duration:200,easing:"swing"}
 32:    );
 33:    $animPopup.children().eq(0).children().each(function(i){
 34:      var movePos = i===0 ? 0 :
 35:        i===1 ? jumpList[0] && parseInt(jumpList[0].top) || null:
 36:        i===2 ? jumpList[1] && parseInt(jumpList[1].top) || null:
 37:        i===3 ? jumpList[2] && parseInt(jumpList[2].top) || null:
 38:        i===4 ? $(document).height() : null;
 39:      $(this).click(function(e){
 40:        jumpList[2] = jumpList[3];
 41:        $("html,body").animate(
              {scrollTop:movePos,scrollLeft:0},500,"easeInSine",
              function(){!$("#popup").size() && $("#popup").bind("hover");});
 42:        jumpList[3] = {top:movePos+"px",left:0};
 43:        $(this).blur();
 44:        return false;
 45:      });
 46:    });
 47:  }
 48:
 49:  $(function(){
 50:    var txt = "この頁内で移動する";
 51:    jQ.each(function(){
 52:      var that = this;
 53:      that.pos = $(that).offset();
 54:      $destination = $($(that).attr("href"));
 55:      that.destinationPos = $destination.offset();
 56:      jumpList = [that.pos, that.destinationPos, that.pos];
 57:      that.guidePos = setGuidePos(o.guidePos);
          // ダミーアニメ(サイズ計測が適切に行われないため、一度起動する。
          // 但し何も表示させないよう透明化する)
 58:      $("body").animatedPopup(" ",{popupCSS:{opacity:0},queue:false,duration:10});
          // guide窓を若干透明にして表示する。
 59:      $("body").animatedPopup(txt,{popupCSS:that.guidePos,duration:200,easing:"easeInOutQuint"});
 60:      $(":animated").queue('fx',[]); // 登録済みのアニメを削除する
 61:      $animPopup = $animPopup===undefined ? $("#animPopup") : $animPopup;
 62:      $animPopup.children().eq(0).mouseover(function(){
 63:        if ($(this).text()===txt){
              // guide の替わりにジャンプ先リストを表示する。
 64:          displayJumplist.call(that);
 65:        } else return;
 66:      });
 67:    });
 68:  });
 69:  return jQ;
 70:};
 71:
 72:$.fn.backNforth.opts = {
 73:  moveStr : "<div accesskey='T'>(T) 頁最上部へ移動</div><div accesskey='1'
        >(1) 最初の位置へ移動</div><div accesskey='2'>(2) 次の位置へ移動
        </div><div accesskey='3'>(3) 直前の位置へ移動</div><div accesskey='B'>(B) 頁最下部へ移動</div>",
      // 0:center of window,1:leftTop,2:rightTop,3:rightBottom
      // 4:leftBottom,5:topEdge,6:rightEdge,7:bottomEdge,8:leftEdge
 74:  guidePos : 5
 75:};
 76:})(jQuery);

// backNforth プラグイン起動用スクリプト
$(function(){
	$("a[href*='contents'],[id*='contents']").addClass("backNforth");
	$(".backNforth").click(function(){$(this).backNforth({guidePos:5})});
});
 

自作プラグイン-アニメーションポップアップを全面改訂

恐縮ですが animatedPopup プラグインは IE では動きません。原因は jquery.js ver 1.4.2 の CSS スタイルに係る部分に、IE に対してバグとなってしまう箇所があるようです。この問題は ver 1.4.4 でも解決していないようです。

animatedPopup プラグインを全面的に改訂した

1 年半ほど前に作成した animatedPopup プラグインは、いわば「勢い」で作ったものでプラグインとしての基本的作法は全く考慮せず、我流の作品に止まっていました。

しかし、真にプラグインとして汎用性を持たせるには、スクリプトコードの論理性・明快性・可読性等を考慮しなければならないと考えるに至り、この度数ヶ月を掛けて全面改訂を行いました。

なお、今回の改訂は各種プラグインから利用出来る、完成済みの jQueryクラスレベルの関数オブジェクト jQuery.F を前提に開発しています。その $.F については、こちら( 複数の jQuery プラグインで共有するオブジェクトを無名関数内に定義する )で詳細に説明してあります。

▲ToTop

animated Popup メソッドの実例

ポップアップ表示をアニメーションメソッドを使って行うことを意味しています。例えば極小の点から一定の幅と高さを有する表示ボックスをズームポップアップし、それを消すときにはズームアウトする、そんなポップアップです。

何はさておき、その実例を示すことが先決でしょう。

下のボタンをクリックするとあるいはその近傍に、あるいは画面中央に、あるいは画面右下に、あるいは指定してある位置 {left:100px,top:200px} に、ポップアップがズームイン/アウトされます。表示する際のポップアップサイズは順に300、350、400、500px に、またアニメーションに要する時間はそれぞれ 1200、600、1600、2000、2500 ミリ秒に設定してあります。

 クリックした位置の傍に、左上から現れ、左上に消えるポップアップを表示します。

クリックすると画面の中央に、中心から現れ中心に消えるポップアップを表示します。

クリックすると画面の右下に、右下から現れ右下に消えるポップアップを表示します。

クリックすると指定してある位置に、左側から現れ左側に消えるポップアップを表示します。

クリックすると画面の中央に、上部からぶら下がり上部に巻き上がるポップアップ内に或る写真を表示します。写真出典元は フォトライブラリ です。

▲ToTop

animatedPopup に様々な Easing を適用する

George Smith 氏により提供されている jQuery プラグイン「Easing Plugin」は 10 種類あり、各々に 3 つの効果(easeIn、easeOut、easeInOut)を持たせて Easing 関数が定義されているため、都合 30 種類の easing 関数があります。→ Plugins | jQuery Plugins

その Easing の各々の動きの違いを示したサイトとして興味深いのは、flash ムービーですが easing_demo が実に分かりやすく有益です。これによりそれぞれの Easing がどんな挙動をするのか一目瞭然に理解できます。easing 関数内の代数式がグラフ化されていることも非常に有益です。

そこで、このデモサイトにあやかって、jquery.js 内蔵の linear と swing イージングを加えた 32 種類の Easing 効果を、animatedPopup メソッドに適用するリストボックスを設置してみました。easing_demo に比べて文字表示の場合には各 easing の差異がわかりにくいのですが、まあそれはご愛敬ということで...。

下のリストボックスの説明
  • アニーメーションポップアップの表示位置は 「 画面中央 」 としました。
  • Easing に要する時間はそれぞれの関数の差異が分かり易いように長めに設定しました(2秒)。
  • 別の easing 関数を選択する前に表示されているポップアップを隠蔽する必要はありません。自動的に前の表示を消して、新たな表示を行います。

▲ToTop

全面改訂に当たって心掛けたこと

アンダーライン箇所には、マウスオーバーポップアップによる補足説明が設定されています。

  1. プラグイン内で作成する関数名を、その役割が一目で分かるようなものとしました。命名は基本的に「動詞+関数の機能や内容をを表す名詞」としました。
  2. 作成した関数は、それぞれが果たす役割が明確になるよう基本的に単機能としました。こうしておくとメンテナンスがしやすいからです。
  3. 各種初期値用のオブジェクトを $.fn.animatedPopup.opts プロパティに用意しました。この方法は初期値の明確化、可読性向上、メンテナンスの容易さ等のメリットがあります。
  4. 初期値に対して、可能な限りアニメーションポップアップ要素のCSS値(色、サイズ、位置等々)を自在に変更できるようにコードを工夫しました。
  5. 引数をどのように設定したのか、時間が経つほどにとかく忘れてしまうものです。そこで 特別な文字「?」(半角全角は問わない)を引数にすれば、window.alert 関数で引数の指定方法を表示するようにしました。これで引数の指定内容がいつでも分かるようになりました(いわば簡易 HELP機能です)。
  6. コード内での関数呼び出しには function.call メソッドを多用しました。呼び出された関数の実行時に、キーワード this が当該関数の呼び出し元オブジェクトを参照することを利用して、特定のオブジェクトを当該関数内で参照しやすくするためです。
  7. 当然のことですが、メソッドチェーンを可能とするために animatedPopup 関数の返値は jQuery インスタンスとしました。
  8. 初めて行ったことですが、プラグイン関数のプロパティにプラグイン関数の内部関数を登録し、プラグイン関数が終了した後でもその内部関数を呼び出せるようにしました。

▲ToTop

プラグインコード解説

コードは 200 行強の比較的短いものです。ポップアップ要素のスタイル設定もスタイルシートは使わずスクリプトで行いました。

以下コード全体をいくつかのブロックに分けて解説します。なお、末尾にはコード全体を掲載してあります。

変数設定

トップレベルに定義した変数は animatedPopup 関数全体で使用するものに限定しました。$ で始まる変数は jQuery インスタンスへの参照のためであることを示しています。

errFunc は文字通りエラー発生時に使用する関数です。errFlag 変数に true を代入することにより、animatedPopup 関数の動作を停止します。

  2:$.fn.animatedPopup = function(content,opts){
  3:  var jQ = this,errFlag = false,wait,ival,$animPopup,$xMark,
  4:      errFunc = function(){errFlag=true; return false;};
エラー対応および HELP コンテンツ定義及び各種の値の設定 makeCSS

makeCSS 関数は、$.fn.animatedPopup.opt で定めた初期値と引数 (content,opts) から、プラグイン起動元である jQuery インスタンスに対して適用する様々な値を作成するものです。作成値は CSS 値だけではありませんが、CSS 値作成部分が大半なので代表させて makeCSS と命名しました。

  5:  var makeCSS = function(){
  6:    this.startScrollPos = $.F.getScroll(window);
  7:    this.startWinSize = $.F.getSize(window);
  8:    if (opts && opts.constructor!== Object ){
  9:      alert("配置はオブジェクト形式で指定してください。\n
            例1 popupCSS:{left:\"10px\",top:\"100px\"}\n
            例2 popupCSS:{right:\"20px\",bottom:\"100px\"}");
 10:      errFunc();
 11:    };
 12:    if (!content || content==="") {
 13:      alert("ポップアップする内容が指定されていません。");
 14:      errFunc();
 15:    }
 16:    if (content==="?" || content==="?"){
 17:      var text ="jQuery(anyElement).animatedPopup(contents,opts);\n";
 18:      text +="contents: HTML strings\n";
 19:      text +="opts: Object \n";
 20:      text +="{ popupCSS:{CSS styles set of animatedPopup Element},\n";
 21:      text +="  closeBarCSS:{CSS styles set of closeBar Element},\n";
 22:      text +="  duration:time, easing:name, queue:true or false,complete:function\n";
 23:      text +="}";
 24:      alert(text);
 25:      errFunc();
 26:    }
        // popup 用の各種 CSS オブジェクトを作る。
 27:    var ret= $.extend({}, $.fn.animatedPopup.opts, {content:content}, opts &&  opts.constructor === Object && opts || {});
       // ret のサブオブジェクトが引数 opts により上書きされて
        // しまっている場合があるので、改めて拡張する
 28:    ret.popupCSS = $.extend({}, $.fn.animatedPopup.opts.popupCSS, ret.popupCSS);
 29:    ret.popupOffset = $.extend({}, $.fn.animatedPopup.opts.popupOffset, ret.popupOffset);
 30:    ret.closeBarCSS = $.extend({}, $.fn.animatedPopup.opts.closeBarCSS, ret.closeBarCSS);
 31:    ret.initPopupCSS = $.extend({},ret.popupCSS);
 32:    return ret;
 33:  }
  1. 6~7 行の this.start で始まる 2 つの変数は、ポップアップ要素表示後に window をスクロールしたり、window のサイズを変更した時に、それらのイベントに対応してポップアップ要素の位置を変化させるために使用するもので、animatedPopup 関数起動時の window スクロール値とサイズを取得します(詳細は後述)。
  2. 8~26 行は引数エラーチェックと HELP コンテンツの作成です。引数チェックは、当然 jQuery インスタンス毎に行うことになりますが、インスタンスが複数の要素からなる場合には引数が要素毎に異なる場合もありえます。そこで makeCSS 関数内にエラーチェックコードを配置し、インスタンスの要素毎にチェックするようにしました。
  3. エラーチェック後に、初期値オブジェクトに引数を合成したオブジェクトを作ります(27 行)。このとき、extend クラスメソッドが極めて効果的に働くことは言うまでもありません。このメソッドは、先置した引数オブジェクトのプロパティが後置したオブジェクトにも存在していれば、後置オブジェクトのプロパティ値で上書きし、後置オブジェクトのプロパティが前置オブジェクトに存在しなければ前置オブジェクトに当該プロパティを追加し、複数オブジュエクトを合体させます。この結果、前置オブジェクトのプロパティ値が後置オブジェクトの同一名プロパティ値で置き換えられ、前置オブジェクトになかった後置オブジェクトのプロパティは前置オブジェクトにそのまま追加されます。
  4. こうして初期値を引数値で上書きしますが、初期値のプロパティ値がオブジェクトの場合には、当該オブジェクトが opts オブジェクトの同一プロパティ名のオブジェクトで丸ごと上書きされるため、補正が必要となります。その補正を 28~30 行で行います。
  5. 補正もまた extend メソッドを使います。値がオブジェクトである初期値プロパティに、27 行で作成された同一名のプロパティ値を併合させることにより、27 行目で一部消失してしまった初期値オブジェクトの全てのプロパティを復活させ、これに、27 行目で作成した同一プロパティ名の値を上書きします。こうして全ての初期プロパティを保持したままで、その値を引数値で上書きしたオブジェクト ret が完成します。
  6. 31 行の initPopupCSS プロパティは popupCSS プロパティの初期値を保持するためのもので、window リサイズイベントハンドラー関数内で使用します。

▲ToTop

ポップアップ要素のサイズ計測関数 getSize

ポップアップされるコンテンツ毎にポップアップ要素の幅も高さも変化します。そしてその都度変化するポップアップ要素のサイズに応じて、ポップアップ要素の位置を適切に算出しなければなりません。

しかし、要素サイズは一般にそれが描画されなければ確定できません。文字列の場合特にそうです。最も簡単な解決方法は、全てのコンテンツに対して同一の幅とし、高さは自動的にブラウザに決めさせれば良いのですが、これでは余りに芸がありません。出来る限りコンテンツに相応しい幅と高さにしたいものです。

従って要素を適切にポップアップするには、描画する前にどのようにしてその幅と高さをブラウザに認識させるのか───これがポイントとなります。

 35:  var getCloseBarHeight = function(){
 36:    $animPopup.css({display:"block",visibility:"hidden"});
 37:    var temp = $xMark.outerHeight(true);
 38:    $animPopup.css({display:"none",visibility:"visible"});
 39:    return temp;
 40:  }
 41:  var getSize = function(){
 42:    var ret = this.o, pCSS = $.extend({},ret.popupCSS);
        // popup 要素にコンテンツを挿入し、popup 要素の width と height を計測
        // 1.right や bottom が指定された場合高さを正確に測れなくなるので、
        // これらの属性を削除する。
 43:    if (pCSS.right !== undefined) delete pCSS.right;
 44:    if (pCSS.bottom !== undefined) delete pCSS.bottom;
        // 2.既定の幅と高さをキャンセル後に $animPopup's width を計測し、
        // コンテンツを挿入する。
 45:    $animPopup.css({width:null,height:null,top:0,left:0})
 46:      .children().eq(0).css({width:null,height:null}).html(ret.content);
 47:    var cW = $animPopup.width();
        // 3.幅値を最適化する
 48:    cW = ret.content.match(/\ssrc/) ? cW : Math.min(cW,parseInt(pCSS.width));
        // 4.padding 値を調整する
 49:    pCSS.paddingTop = getCloseBarHeight() + parseInt(pCSS.paddingTop) + "px";
        // 5.得られた幅値を適用して隠蔽状態のまま
        // $animPopup.css メソッドを実行し、各種サイズを計測・取得する
 50:    ret.popupSize = $.F.getSize($animPopup.css($.extend(pCSS,{width:cW+"px"}))[0]);
        // 6.コンテンツが画像などではなく、かつ、幅か高さが 600 px 以上
        // の場合にはスクロールバーを配置する
 51:    if (!ret.content.match(/\ssrc/) && (ret.popupSize.cW >=600 || ret.popupSize.cH >=600)) ret.popupCSS.overflow="auto";
        // 7.取得した padding 値をポップアップ要素表示 CSS に設定する
 52:    ret.popupCSS.paddingTop = pCSS.paddingTop;
        // 8.取得したコンテンツサイズをポップアップ要素表示 CSS に設定する
 53:    ret.popupCSS.width = $animPopup.children().eq(0)[0].style.width = ret.popupSize.cW +"px";
 54:    ret.popupCSS.height = $animPopup.children().eq(0)[0].style.height = ret.popupSize.cH +"px";
 55:  }
サイズ計測のための付随関数 getCloseBarHeight(35~40 行)
  1. この関数はポップアップされた要素を閉じるための部品である closeBar の高さを計測するもので、計測結果を使ってポップアップ要素の paddingTop 値を補正します。わざわざこのようにしたのは、closeBar の CSS 値も引数で変更出来るため、padding 値を自在に設定出来るようにしておかないと使い勝手が悪くなるからです。
  2. この関数の肝は、display:none 状態の要素内にある、子要素のサイズ計測をどのように行うか、にあります。親要素の display 属性が none である限り、子要素に対して width メソッドを適用してもサイズは測れません。ここでは jQuery.swap クラスメソッドで使用している方法を利用しました。
  3. CSS 値を display:block、visibility:hidden に変更してからサイズを計測します。これにより描画させつつ表示させない状態を作って計測可能とします。計測後に display:none、visibility:visible に戻す必要があることは言うまでもありません。
サイズ計測のための前処理(42~44 行)
  1. HTML 要素の描画前にその大きさを知る為に必要となるのは、ポップアップすべきコンテンツそのものと、ポップアップ要素の CSS 値です。(なお、CSS 値は必要不可欠ではありません。また、画像や動画の場合には、要素の属性値に幅などが記載されている場合が多いため事情が異なりますが、そのことはさしあたり無視します。)
  2. しかし、いきなり CSS 値をポップアップ要素に適用すると誤った計算をしてしまう場合があります。何故ならば、CSS値には right や bottom プロパティが存在する場合があり、これらのプロパティがあるとポップアップ要素サイズは、最大で top:0、left:0、right:0、bottom:0 で囲まれた範囲、すなわちブラウザの window サイズになってしまうからです。
  3. そこで一工夫が必要となります。どんな場合であっても right 及び bottom プロパティを強制的に削除した作業用 CSS オブジェクトを用意すれば良いと考えました。その作業用 CSS オブジェクトとコンテンツから、ポップアップ要素サイズを計測すれば、当該要素ボックスの右下隅が window 右下隅になってしまう事態を避けることが出来ます。
  4. 42 行で宣言した変数 pCSS は、この作業用 CSS オブジェクトを参照させるものです。
  5. これに先立って、変数 ret に this.o を代入していますが、この this は jQuery インスタンスに登録されている要素を参照しています。後述するように jQuery インスタンスに登録されている要素を起動元として、getSize 関数に対して call メソッドを適用するからです。
ポップアップ要素の横幅計測
  1. ブラウザに対象要素を表示させることなく当該要素の幅や高さを知るには、jquery.js の inner メソッドである swap クラスメソッドを使う方法があります。しかしここでは、swap メソッドは使用せず、それが使用しているロジックを踏襲してサイズ計測を行いました。
  2. jquery.js は、隠蔽した要素に width メソッドを適用すると、算出スタイル値を取得するよう設計されているので、これを利用すれば良いわけです。但し計測対象の親要素が隠蔽状態にないことが前提となります。(別のエントリイでその具体的な利用方法を記述しました。)
  3. このコードでは 45~47行において、ポップアップ要素へのコンテンツの挿入と横幅計測を行っています。

▲ToTop

ポップアップ要素の横幅設定
  1. ポップアップする要素の大きさは、一般的に何通りかの設定スタイルがあります。ポップアップされる要素が何であれ、全て同一の幅に設定することもあれば、何通りかの既定値を定めておいて、コンテンツに応じて選択的に既定値を選択する方法もあるでしょう。あるいはコンテンツの種類(画像か文字列化など)に応じて何通りかの幅を設定する選択肢もあるでしょう。
  2. このコードでは 48 行でコンテンツ幅の設定を行いますが、画像の場合にはそのコンテンツが持つ固有の幅をポップアップ要素のコンテンツ幅とし、それ以外の場合には既定のコンテンツ幅と取得したコンテンツ幅とを比べて小さい方を採用することとしました。
  3. 言い換えれば、文字列の場合のコンテンツ幅は、固有幅が既定値に満たない場合にはその固有幅に、固有幅が既定値を超える場合には既定値幅に設定します。なお、既定値は初期値として定めた値を、引数で変更することが出来るようにしました。つまり、固有値、初期値及び引数で定めた値の 3 つから選択的に幅が決定されます。(なお、動画表示も十分可能ですが、さしあたり今後の課題とします。)
ポップアップ要素の paddingTop 値計測

getCloseBarHeight 関数を使って closeBar の外寸高さを取得し、これに既定の paddingTop 値を加算して、新たな paddingTop 値を取得します。後は取得した値と既定値を組み合わせて新しい padding 値を作るだけです。

ポップアップ要素の各種サイズ計測
  1. 横幅の計測と設定を終えれば、後はその他のサイズ(コンテンツ高さ、マージン辺幅など)を取得するだけです。その計測は 50 行で行っていますが、ここでは自作のプラグイン共用関数を使用しました。$.F.getSize(elem) 関数を使えば、elem 要素のコンテンツ、パディング辺間、ボーダー辺間及びマージン辺間の、各々の幅(cW、pW、bW、mW)と高さ(cH、pH、bH、mW)を取得することが出来ます。
  2. ここに、幅と高さをコンテンツだけでなく各種辺間距離も取得するのは、window からのポップアップ要素のはみ出し防止のために、要素マージン辺間の幅と高さを利用するためです。確かに、パディング辺間距離とボーダー辺間距離はこのコード内では利用しませんが、自作のサイズ計測関数があるので序でに取得したまでです。
  3. 51 行ではポップアップ要素サイズが大きな場合に必要となるスクロールバーの配置を行います。幅又は高さが 600 px を越えた場合には要素にスクロールバーを配置するよう、overflow 属性を "auto" に指定します。
  4. 以上による各取得値を、52~54 行で当該要素の各種プロパティに代入しアニメーションに備えます。

▲ToTop

ポップアップ要素の配置位置決定 setPos

以上でポップアップ要素の大きさが確定したので、次に行うべきことは引数などに応じて当該要素の配置位置を確定することです。つまり、top 値と left 値を与条件から算出し確定することです。そのための関数が 56~77 行の setPos 関数です。

 56:  var setPos = function(){
 57:    var pCSS = this.o.popupCSS, offCSS = this.o.popupOffset, pSize = this.o.popupSize;
        // right や bottom 指定がされた場合
 58:    if (pCSS.right !== undefined) {
 59:      pCSS.left = this.startWinSize.cW - pSize.mW - parseInt(pCSS.right) +"px";
 60:      delete pCSS.right;
 61:    }
 62:    if (pCSS.bottom !== undefined){
 63:      pCSS.top = this.startWinSize.cH - pSize.mH - parseInt(pCSS.bottom) +"px";
 64:      delete pCSS.bottom;
 65:    }
        // left 値や top 値が "center" で指定された場合
 66:    if (pCSS.left ==="center") pCSS.left = this.startWinSize.cW/2 - pSize.mW/2+"px";
 67:    if (pCSS.top ==="center") pCSS.top = this.startWinSize.cH/2 - pSize.mH/2+"px";
        // mouse cursor 近傍に popup する場合の位置指定
        // left 値か top 値に false を指定することによりその旨を指定する
 68:    if (pCSS.left === false || pCSS.top === false) {
 69:      pCSS.left = parseInt($.F.mousePos.X) + parseInt(offCSS.left) + "px";
 70:      pCSS.top = parseInt($.F.mousePos.Y) + parseInt(offCSS.top) +"px";
 71:    } else {
        // mouse cursor 近傍以外の場所に表示する場合には scroll 値を加算
 72:      pCSS.left=parseInt(pCSS.left) + this.startScrollPos.L+"px";
 73:      pCSS.top=parseInt(pCSS.top) + this.startScrollPos.T+"px";
 74:    }
        // 画面外への飛び出し防止補正
 75:    pCSS.left = Math.max(0, Math.min(this.startWinSize.cW + this.startScrollPos.L - pSize.mW, parseInt(pCSS.left)))+"px";
 76:    pCSS.top = Math.max(0, Math.min(this.startWinSize.cH + this.startScrollPos.T - pSize.mH, parseInt(pCSS.top)))+"px";
 77:  }

位置決めは引数で与えられた情報によりケース分けして行いますが、全てのケースにおいて、自作のプラグイン共用関数をフル活用しています。

  1. 引数で位置指定を行わない場合には、top も left も初期値を "center" と定めたので、ポップアップ要素は window 中心に表示されることになります。
  2. 引数で right や bottom 属性が指定された場合には、問題を単純化するためにも left やtop に置換し、right や bottom 属性を削除します。(58~65行)
  3. 引数で top や left 値に center が指定された場合の対応を 66、67行で行っています。
  4. 引数で top 又は left 値に false を指定すると、それはマウス位置の近傍にポップアップ要素を表示することを意味するように定めました。false が指定された場合の対応は、68~71 行で行い、top も left も false 値が指定されていない場合には、top 値 と left 値に window のスクロール値を加算します。ここでも自作のプラグイン共用関数をフル活用しています。(72、73行)
  5. 最後に、画面外へのポップアップ要素の飛び出しを防止する措置を 75、76 行で講じています。
ポップアップ要素の展開/縮小を演出する setShrinkOuterCSS

アニメーションポップアップの要点は当該要素の「登場・退場」の仕方にあります。

アニメーションとは言い換えれば、要素の CSS 現在値が CSS 到達値に変化する過程が動的に表示されることに他なりません。従って、隠蔽状態から表示状態へ、そしてその逆への 2 つの過程を演出するには、隠蔽状態の CSS 値を作成しなければなりません。その隠蔽状態 CSS 値を setShrinkOuterCSS 関数で作成します。

なお、OuterCSS と命名したのはポップアップ要素のマージン辺内を対象としてシュリンクさせるからです。

 79:  var setShrinkOuterCSS = function(){
 80:    var pCSS = this.o.popupCSS,
 81:        pSize = this.o.popupSize,
 82:        Xdir = pCSS.origin ===5 || pCSS.origin ===7,
 83:        Ydir = pCSS.origin ===6 || pCSS.origin ===8;
 84:    return {opacity:0,
 85:      borderLeftWidth: Xdir ? pCSS.borderLeftWidth : 0,
 86:      borderRightWidth: Xdir ? pCSS.borderRightWidth : 0,
 87:      borderTopWidth: Ydir ? pCSS.borderTopWidth : 0,
 88:      borderBottomWidth:  Ydir ? pCSS.borderBottomWidth : 0,
 89:      paddingLeft: Xdir ? pCSS.paddingLeft : 0,
 90:      paddingRight: Xdir ? pCSS.paddingRight : 0,
 91:      paddingTop: Ydir ? pCSS.paddingLeft : 0,
 92:      paddingBottom: Ydir ? pCSS.paddingBottom : 0,
 93:      marginLeft: Xdir ? pCSS.marginLeft : 0,
 94:      marginRight: Xdir ? pCSS.marginRight : 0,
 95:      marginTop: Ydir ? pCSS.marginTop : 0,
 96:      marginBottom: Ydir ? pCSS.marginBottom : 0,
 97:      width: (Xdir ? pSize.cW : 0) +"px",
 98:      height: (Ydir ? pSize.cH : 0) +"px",
 99:      width: ((pCSS.origin ===5 || pCSS.origin ===7) ? pSize.cW : 0) +"px",
100:      height: ((pCSS.origin ===6 || pCSS.origin ===8) ? pSize.cH : 0) +"px",
101:      left:parseInt(pCSS.left) + 
102:        (pCSS.origin ===0 ? pSize.mW/2 :
103:        pCSS.origin ===2 ? pSize.mW :
104:        pCSS.origin ===3 ? pSize.mW :
105:        pCSS.origin ===6 ? pSize.mW : 0) +"px",
106:      top:parseInt(pCSS.top) +
107:        (pCSS.origin ===0 ? pSize.mH/2 :
108:        pCSS.origin ===3 ? pSize.mH :
109:        pCSS.origin ===4 ? pSize.mH :
110:        pCSS.origin ===7 ? pSize.mH : 0)+"px"
111:    }
112:  }
  1. この関数の肝は origin プロパティによる CSS 属性値の編集設定です。
  2. origin プロパティは、初期値 0 を $.fn.animatedPopup.opts.popupCSS.origin に定めておき、makeCSS 関数により要素の o.popupCSS.origin に複写させます。
  3. origin 値は 0:center of element,1:leftTop,2:rightTop,3:rightBottom,
    4:leftBottom,5:topEdge,6:rightEdge,7:bottomEdge,8:leftEdge の 9 つの値を取ることが出来るよう設計しました。(これらの値以外が与えられるとエラー関数処理を行いコード進行を停止します。)
  4. 0~8 の値は順に、表示状態の対象要素の中心、左上隅、右上隅、右下隅、左下隅、上辺、右辺、下辺、左辺の位置を意味します。origin 値の初期値は 0 、つまり要素の中心から登場し、中心に消える設定にしてありますが、引数で任意に指定することにより、9 つの起終位置を指定することが出来ます。
  5. setShrinkOuterCSS 関数において、アニメがある一点から始まり、その点に収斂する場合には(これを便宜的に「点モードアニメ」と呼びます)、隠蔽状態の各種 CSS 値をゼロとしていること、これに対して線モードアニメの場合には、アニメ終了線が要素の上下の時には幅値を固定し、アニメ終了線が要素の左右の時には高さ値を固定していることに注目してください。

▲ToTop

popup 要素タグの作成

以上でポップアップ要素のサイズと位置を決める関数が終わりました。続いてポップアップ要素タグそのものを Web 頁に挿入するコードです。

113:  var makePopupElem = function(){
114:    if (!$("#animPopup").size()) {
115:      $("<div id='animPopup'></div>").css({position:"absolute",display:"none",zIndex:"1000"})
            .append("<div></div>")
            .appendTo(document.body);
116:    }
117:    $animPopup=$("#animPopup");
        // Popup 隠蔽用×タグの作成
118:    if (!$("#xMark").size()){
119:      $("<div id='xMark'>CLOSE</div>").css($.fn.animatedPopup.opts.closeBarCSS)
            .append("<div style='position:absolute;z-index:1003;top:0;right:2px;width:13px'>×</div>")
            .appendTo($animPopup);
120:    }
121:    $xMark = $("#xMark");
122:  };
  1. この部分は他言は要しないと思いますので、最小限の説明に留めます。
  2. ポップアップ要素は絶対配置、隠蔽指定を行い、コンテンツ用の div 要素を内包させます。
  3. ポップアップ要素の中にこれを閉じるための closeBar を配置しますが、これも絶対配置します。そのバーの右隅に×印があった方がそれらしくなるので、これも絶対配置で配置します。
  4. size インスタンスメソッドを使って、ポップアップ要素等が重複登録されないようにしています。また $ で始まるショートカット変数を指定して要素へのアクセスを簡便化しています。
  5. 引数で closeBar の CSS 値を適切に指定すれば、cloceBar の高さ、色等を変更出来るように、また closeBar 自体を不要とすることも出来るようにしました。但し、その場合には開いたポップアップ要素を閉じるボタン等を作るか、animPopup 要素自身をクリックした時に、hideAnim 関数を起動するように別途設定する必要があります。そのイベントハンドラーは、極めて簡単な例としては、次のようになるでしょう。但し、このコードの場合には収斂位置は指定出来ませんので、animatedPopup を使用した場合に比べると消え方はシンプルになります。
    $("#animPopup").click(function(){$(this).fadeOut()})

▲ToTop

隠蔽アニメーション関数 hideAnim

既に作成した outerShrinkCSS オブジェクトと setShrinkInnerCSS 関数呼び出し結果を第一引数として、2 つの同期的に作動する隠蔽アニメーション関数を定義します。2 つのアニメーションは同時に引き起こされること、待ち行列 fx の登録済みアニメーションを削除してから隠蔽アニメを行う必要があることに留意してください。

ここに setShrinkInnerCSS 関数はコンテンツ要素の CSS 値をシュリンクさせるものです。

また、ポップアップ要素だけでなく、コンテンツ要素にも隠蔽アニメーションを適用していることに注目してください。そうすることにより、ポップアップ要素を隠蔽する際に、そのコンテンツも同じ終点または終線に向かって収斂するようになります。

      // make CSS Object for hide animation
124:  var setShrinkInnerCSS = function(){
125:    var pCSS = this.o.popupCSS,
126:        Xdir = pCSS.origin ===5 || pCSS.origin ===7,
127:        Ydir = pCSS.origin ===6 || pCSS.origin ===8;
128:    return {
129:      width: (Xdir ? this.o.popupSize.cW : 0) +"px",
130:      height: (Ydir ? this.o.popupSize.cH : 0) +"px",
131:      opacity:0
132:    };
133:  }
134:  var hideAnim = function(){ // popup 要素の隠蔽アニメーション
135:    this.o.hidden=true;
136:    $(":animated").queue('fx',[]).stop(); >// 登録済みのアニメを全て停止する
137:    $animPopup.animate(this.o.shrinkOuterCSS,
          {queue:false,duration:this.o.duration,easing:this.o.easing})
          .children().eq(0).animate(innerShrinkCSS.call(this),
          {duration:this.o.duration,easing:this.o.easing});
138:  }
ポップアップ表示アニメーション関数 showAnim

この関数でもポップアップ要素とコンテンツ要素の 2 つを対象にして、同期アニメーションを実行します。関数の起動元は animatedPopup プラグインを起動した jQuery インスタンスです。

139:  var showAnim = function(){ // popup 要素の表示アニメーション
140:    if (errFlag) {
141:      errFlag = false; //$(this).unbind("click");
142:      return errFunc();
143:    }
144:    $(":animated").queue('fx',[]).stop(); // 登録済みのアニメを全て停止する
        // 表示する前に、animPopup の幅と高さをゼロにして所定位置に
        // 非表示描画で配置する。
145:    if (this.o.hidden===undefined)
146:      $animPopup.css(this.o.shrinkOuterCSS).children().eq(0).css(innerShrinkCSS);
        // 表示アニメーション
147:    this.o.popupCSS.display="block";
148:    var fn = function(){$.fn.animatedPopup.running=false;} // 初期化
149:    $animPopup.animate(this.o.popupCSS,{
150:      queue:this.o.queue, duration:this.o.duration,
151:      easing:this.o.easing, complete:this.o.complete}
152:    ).children().eq(0).animate(
153:      {width:this.o.popupCSS.width,height:this.o.popupCSS.height,opacity:this.o.popupCSS.opacity},
154:      {queue:false,duration:this.o.duration,easing:this.o.easing}
155:    );
156:  }
  1. 実行に先立ち、もしエラーがあれば、errFunc 関数を起動して return 値を return して、コード進行から抜け出します。二度の return が必要なのは showAnim メソッドがトップレベルではなく、doAnim メソッド内から呼び出されるためです。なお、jQuery インスタンスから click バインドを外すことも検討しましたが、一度きりのエラーの alert 表示だけでは不十分と考え、アンバインドは無効としました。(140~143 行)
  2. 次に、待ち行列に登録されている全てのアニメーションの登録を解除します。そうしないと アニメ起動時に、待ち行列に登録されている他の同期アニメーションも起動してしまうからです。(144 行)
  3. 既に隠蔽アニメーションが起動済みの時には、隠蔽 CSS 設定を行う必要がないため、o.hidden プロパティ値を検査します。未だ一度も隠蔽アニメーションが起動していなければ、つまり初めての表示アニメーションである場合には、ポップアップ要素に隠蔽 CSS 値を設定して、表示アニメ起動前のポップアップ要素の状態を作ります。(145~147 行)
  4. 148 行で display 値を block にすることで、表示アニメーションによってポップアップ要素が実際に表示されるようにします。
  5. 後はポップアップ要素とコンテンツに対する 2 つの同期アニメーションを起動するだけです。なお、ここに 2 つの要素をアニメ対象としたのは、コンテンツの器であるポップアップ要素だけでなく、そのコンテンツにもアニメを適用することにより、アニメーションの視覚効果をより一層高めるためです。(149~155 行)

▲ToTop

表示アニメーションの起動 doAnim

初期値は false である running プロパティ値を true にして、アニメーションが始まることを記録します。タイマー変数を解除してから、表示アニメーション関数 showAnim を起動します。この関数も呼び出し元は jQuery インスタンスです。

157:  var doAnim = function(){
158:    $.fn.animatedPopup.running=true;
159:    if (wait) {clearInterval(wait);wait=null};
160:    showAnim.call(this);
161:  }
DOM Ready 関数

これまでの長い過程を経てやっと animatedPopup プラグインの最後の関数に辿り着きました。最後の関数である DOM Ready 関数の中で、これまで説明してきた全ての関数の起動が行われる、と言っても過言ではありません。

163:  $(function(){
164:    makePopupElem();
165:    return jQ.each(function(){
166:      var that=this, // this=jQ[i]
          // animPopup を目的の位置に配置するCSSオブジェクトを作る。
167:      if ($xMark.css("display")!=="none")
168:        $xMark.click(function(){hideAnim.call(that)});
169:      else $animPopup.click(function(){hideAnim.call(that)});
170:      that.o = makeCSS.call(that);
171:      getSize.call(that);
172:      setPos.call(that);
173:      that.o.shrinkOuterCSS = setShrinkOuterCSS.call(that);
174:      if ($.fn.animatedPopup.running){
175:        wait = setInterval(function(){doAnim.call(that)},1000);
176:      } else doAnim.call(that);
177:      $(window).scroll(function(){
178:        this.endScrollPos = $.F.getScroll(window);
179:        that.o.popupCSS.left = parseInt(that.o.popupCSS.left)
180:          + this.endScrollPos.L - this.startScrollPos.L + "px";
181:        that.o.popupCSS.top = parseInt(that.o.popupCSS.top)
182:          + this.endScrollPos.T - this.startScrollPos.T + "px";
183:        that.o.shrinkOuterCSS = setShrinkOuterCSS.call(that);
184:        $animPopup.css({left:that.o.popupCSS.left,top:that.o.popupCSS.top});
185:        this.startScrollPos = this.endScrollPos;
186:      });
187:      $(window).resize(function(){
188:        this.endWinSize = $.F.getSize(window),
189:          x = that.o.initPopupCSS.left==="center" && 2 ||
                  that.o.initPopupCSS.right!==undefined && 1 || false,
190:          y = that.o.initPopupCSS.top==="center" && 2 ||
                  that.o.initPopupCSS.bottom!==undefined && 1 || false;
191:        if (x){
192:          that.o.popupCSS.left = parseInt(that.o.popupCSS.left)
193:            + this.endWinSize.cW/x - this.startWinSize.cW/x + "px";
194:        }
195:        if (y){
196:          that.o.popupCSS.top = parseInt(that.o.popupCSS.top)
197:            + this.endWinSize.cH/y - this.startWinSize.cH/y + "px";
198:        }
199:        that.o.shrinkOuterCSS = setShrinkOuterCSS.call(that);
200:        $animPopup.css({left:that.o.popupCSS.left,top:that.o.popupCSS.top});
201:        this.startWinSize = this.endWinSize;
202:      });
203:    });
204:  }); // End of "DOMReady function"
205:  arguments.callee.hideAnim = function(){hideAnim.call(jQ[0]);};
206;  return jQ;
207:}; // End of "animatedPopup function"
アニメーション起動前の各種準備とアニメ起動
  1. 何はさておき、animatedPopup プラグイン起動直後に、ポップアップ要素や closeBar 要素を頁内に設置します。(163 行)
  2. 165 行以下は jQuery インスタンスが複数のタグ要素を含んでいることを前提にしています。また、jQuery インスタンスに登録されている要素を指し示すために this を多用しますが、参照先が随時変動する this の悪影響を受けぬよう、変数 that に this を代入して利用します。こうしておけば関数内で this がグローバルオブジェクト(つまり window オブジェクト)を参照してしまっても、that によって意図したとおり確実に jQuery インスタンスに登録されている要素を指し示すことが出来ます。(166 行)
  3. 諸関数の起動前に closeBar に対する click イベントを登録します。(163行)
  4. その後順次、makeCSS、getSize、setPos、setShrinkOuterCSS を起動し、ポップアップ要素のアニメーションに必要となる表示状態と隠蔽状態の各 CSS 値設定を行います。(170~173 行)なお、全ての関数が call メソッドを適用して呼び出されていることに注目してください。また、呼び出された関数内では this キーワードが起動元を参照することを積極的に活用していることにも注目してください。
  5. 全ての設定を終えた後に、174~176 行でアニメーション起動を行います。

▲ToTop

アニメーション起動後のイベント登録

ポップアップ要素のアニメーションが終わってから、スクロールすることもあれば、window サイズを変更することもあり得ます。これらに対応出来るように、window オブジェクトに 2 つのイベントを登録しました。

まずスクロールイベントから。

このスクロールイベントはポップアップ後に左右上下のスクロールが発生した場合、表示済みのポップアップ要素を window 内での「初期表示位置」に保つことを目的とします。初期表示位置とは、引数 opts で指定した位置( 指定しない場合には、初期値で指定してある window 座標の top:"center",left:"center" )です。

このイベントハンドラーにより、例えば、画面右下に配置指定を行って animatedPopup プラグインを起動した場合、スクロールイベントが発生しても、当該のポップアップ要素はずっと画面右下に配置され続けるようにします。

  1. 最初にスクロールイベント発生後の頁座標上の top 値、left 値を this.endScrollPos オブジェクトのプロパティに取得します。(178 行)
  2. 次に、このオブジェクトのプロパティ値と、スクロールイベント発生前に作成済みの startScrollPos オブジェクトのプロパティ値の差分値を算出し、top 値と left 値を補正します(179~182 行)。この差分値がスクロール量になります。
  3. 補正した top 値、left 値をポップアップ要素の隠蔽用 CSS セットと表示用 CSS セットに上書きし、それにより表示済みポップアップ要素の位置を変化させ、隠蔽アニメーションの収斂点や収斂線を移動します。(183~184 行)
  4. 最後にスクロールイベント発生後の top 値、left 値をスクロールイベント発生前の値に代入し、更なるスクロールイベントの発生に備えます。

次はwindow リサイズイベントです。

このリサイズイベントはポップアップ後に window サイズが変更された場合、表示済みのポップアップ要素を window 内での「初期表示位置」に保つことを目的とします。初期表示位置とは、引数 opts で指定した位置( 指定しない場合には、初期値で指定済みの top:"center",left:"center" )です。

スクロールイベントの場合には全ての配置形式において、配置位置を変更させるようにコーディングしましたが、window リサイズイベント発生時に要素配置を変更させる意味があるのは、センター配置と right プロパティや bottom プロパティが指定された場合のみと考えました。top 値、left 値が値で指定された場合には、リサイズしても位置を変更させる必要はありません。

このイベントハンドラーにより、例えば、画面横及び縦方向の中心位置にポップアップ要素を配置するように指定して animatedPopup プラグインを起動した場合、window サイズを変更しても当該のポップアップ要素は引き続き画面横及び縦方向の中心位置に配置され続けます。

  1. 最初に、リサイズイベント発生直後の window サイズを取得し this.endWinSize オブジェクトのプロパティに記録します。
  2. また配置指定が center で行われた場合には x や y に 2 を代入し、right や bottom 指定が行われた場合には x や y に 1 を代入します。x や y はリサイズ時に必要な補正を行うための変数です。
  3. その補正を 191~198 行で行います。
  4. その後、表示及び隠蔽アニメーション用 CSS セットに補正値を代入し、配置位置を決めます。(199~200 行)
  5. リサイズ直後の配置位置オブジェクトを更なるwindowリサイズに備えて、this.startWinSize オブジェクトに代入します。(201 行)

▲ToTop

プラグイン関数に、よく使うその内部関数を登録する

これはふと思いついたにしては、大変効果が大きく素晴らしいと思っているコードです。

プラグイン関数の中で定義した、プラグイン内部だけで使用する関数を、当該プラグインが起動し終えた後でも、つまりコード進行が終わった後でも使用したい場合にはどうすればよいだろうか?───それが事の発端です。

特にポップアップ要素の場合、随時任意に閉じられることが重要で、jQuery インスタンスメソッドである animatedPopup 関数が起動を終えてから、隠蔽関数 hideAnim を起動させる方法はないものか...と考えた結果、ふと思いついたものです。

205 行の arguments.callee.hideAnim = function(){hideAnim.call(jQ[0])}; がそれです。animatedPopup 関数内部におかれた arguments.callee は、animatedPopup 関数それ自体を指し示しますから、そのプロパティに hideAnim を登録し、hideAnim が起動するコードと同様のコードを function 内に記述したのです。

こうすることにより、animatedPopup 関数が終了した後でも、$("#animPopup").animatedPopup.hideAnim() を起動すれば、収斂点や収斂線の指定はそのままに、設計通りの隠蔽アニメーションが起動出来るのです。

これを使用すれば、closeBarCSS を表示しない引数指定を行った場合に、次のように、#animPopup 要素にクリックイベントを登録することによって、隠蔽アニメーションを意図したとおりに起動させることが出来ます。

$("#animPopup").click(function(){$(this).animatedPopup.hideAnim()});

引数の初期値オブジェクト $.fn.animatedPopup.opts

初期値をこの形式で設定する方法は、cycle プラグインに拠りました。非常に重宝するので必ず使用している程使いやすいものです。プラグイン関数の opts プロパティにオブジェクトを代入して設定します。

    // 引数の初期値を設定
208:$.fn.animatedPopup.opts = {
209:  content:"",
210:  popupOffset:{left:"16px",top:"16px"},
211:  popupCSS:{
212:    position:"absolute",zIndex:1000,left:"center",top:"center",
213:    color:"white",fontWeight:"bold",
214:    width:"400px",backgroundColor:"royalblue", margin:0,
215:    paddingTop:"5px",paddingBottom:"5px",paddingLeft:"5px",paddingRight:"5px",
216:    borderWidth:"5px", borderColor: "plum", borderStyle:"ridge",
217:    textAlign:"center", display:"none",
218:    opacity:1, overflow:"visible",
        // 0:center of element,1:leftTop,2:rightTop,3:rightBottom
        // 4:leftBottom,5:topEdge,6:rightEdge,7:bottomEdge,8:leftEdge
219:    origin:0
220:  },
221:  closeBarCSS:{
222:    position:"absolute",zIndex:"1002",
223:    textAlign:"center",
224:    opacity:0.75,top:0,left:0,cursor:"pointer",
225:    fontSize:"small",lineHeight:"1.2em",width:"100%",
226:    backgroundColor:"midnightblue",display:"block"
227:  },
228:  queue:true, duration:800, easing:"swing", complete:function(){}
229:};
230:})(jQuery);
  1. content は何も指定せず、引数でこれを指定しない場合にはエラー関数が起動するようにしました。
  2. popupOffset プロパティはマウスカーソル位置をクリックした位置から、どの程度離すかを定めたものです。初期値は右及び下に 16px 離す設定です。
  3. popupCSS プロパティは animatedPopup によって起動するポップアップ DIV 要素の CSS 値を定めるものです。
  4. closeBarCSS はポップアップ DIV 要素の上部に表示するボックス隠蔽機能を持った要素のCSSを設定するものです。
  5. 228 行は animate メソッドの第 2 引数オブジェクトのプロパティとなる各種初期値です。

animatedPopup 全コード

  1:(function($){
  2:$.fn.animatedPopup = function(content,opts){
  3:  var jQ=this,errFlag=false,wait,ival,$animPopup,$xMark,
  4:      errFunc = function(){errFlag=true; return false;};
  5:  var makeCSS = function(){
  6:    this.startScrollPos = $.F.getScroll(window);
  7:    this.startWinSize = $.F.getSize(window);
        // 入力エラー及び help 対応
  8:    if (opts && opts.constructor!== Object ){
  9:      alert("配置はオブジェクト形式で指定してください。\n
            例1 popupCSS:{left:\"10px\",top:\"100px\"}\n
            例2 popupCSS:{right:\"20px\",bottom:\"100px\"}");
 10:      errFunc();
 11:    };
 12:    if (!content || content==="") {
 13:      alert("ポップアップする内容が指定されていません。");
 14:      errFunc();
 15:    }
 16:    if (content==="?" || content==="?"){
 17:      var text ="jQuery(anyElement).animatedPopup(contents,opts);\n";
 18:      text +="contents: HTML strings\n";
 19:      text +="opts: Object \n";
 20:      text +="{ popupCSS:{CSS styles set of animatedPopup Element},\n";
 21:      text +="  closeBarCSS:{CSS styles set of closeBar Element},\n";
 22:      text +="  duration:time, easing:name, queue:true or false,complete:function\n";
 23:      text +="}";
 24:      alert(text);
 25:      errFunc();
 26:    }
        // popup 用の各種 CSS オブジェクトを作る。
 27:    var ret= $.extend({}, $.fn.animatedPopup.opts, {content:content}, opts && opts.constructor !== Object && {} || opts);
        // ret のサブオブジェクトが引数 opts により上書きされて
        // しまっている場合があるので、改めて拡張する
 28:    ret.popupCSS = $.extend({}, $.fn.animatedPopup.opts.popupCSS, ret.popupCSS);
 29:    ret.popupOffset = $.extend({}, $.fn.animatedPopup.opts.popupOffset, ret.popupOffset);
 30:    ret.closeBarCSS = $.extend({}, $.fn.animatedPopup.opts.closeBarCSS, ret.closeBarCSS);
 31:    ret.initPopupCSS = $.extend({},ret.popupCSS);
 32:    return ret;
 33:  }
 34:
 35:  var getCloseBarHeight = function(){
 36:    $animPopup.css({display:"block",visibility:"hidden"});
 37:    var temp = $xMark.outerHeight(true);
 38:    $animPopup.css({display:"none",visibility:"visible"});
 39:    return temp;
 40:  }
 41:  var getSize = function(){
 42:    var ret = this.o, pCSS = $.extend({},ret.popupCSS);
        // popup 要素にコンテンツを挿入し、popup 要素の width と height を計測
        // 1.right や bottom が指定された場合高さを正確に測れなくなるので、
        // これらの属性を削除する。
 43:    if (pCSS.right !== undefined) delete pCSS.right;
 44:    if (pCSS.bottom !== undefined) delete pCSS.bottom;
        // 2.既定の幅と高さをキャンセル後に
        // $animPopup's width を計測し、コンテンツを挿入する。
 45:    $animPopup.css({width:null,height:null,top:0,left:0})
 46:      .children().eq(0).css({width:null,height:null}).html(ret.content);
 47:    var cW = $animPopup.width();
        // 3.幅値を最適化する
 48:    cW = ret.content.match(/\ssrc/) ? cW : Math.min(cW,parseInt(pCSS.width));
        // 4.padding 値を調整する
 49:    pCSS.paddingTop = getCloseBarHeight() + parseInt(pCSS.paddingTop) + "px";
        // 5.得られた幅値を適用して隠蔽状態のまま
        // $animPopup.css メソッドを実行し、各種サイズを計測・取得する
 50:    ret.popupSize = $.F.getSize($animPopup.css($.extend(pCSS,{width:cW+"px"}))[0]);
        // 6.コンテンツが画像などではなく、かつ、幅か高さが 600 px 以上
        // の場合にはスクロールバーを配置する
 51:    if (!ret.content.match(/\ssrc/) && (ret.popupSize.cW >600 || ret.popupSize.cH >600)) ret.popupCSS.overflow="auto";
        // 7.取得した padding 値をポップアップ要素表示 CSS に設定する
 52:    ret.popupCSS.paddingTop = pCSS.paddingTop;
        // 8.取得したコンテンツサイズをポップアップ要素表示 CSS に設定する
 53:    ret.popupCSS.width = $animPopup.children().eq(0)[0].style.width = ret.popupSize.cW +"px";
 54:    ret.popupCSS.height = $animPopup.children().eq(0)[0].style.height = ret.popupSize.cH +"px";
 55:  }
 56:  var setPos = function(){
 57:    var pCSS = this.o.popupCSS, offCSS = this.o.popupOffset, pSize = this.o.popupSize;
        // right や bottom 指定がされた場合
 58:    if (pCSS.right !== undefined) {
 59:      pCSS.left = this.startWinSize.cW - pSize.mW - parseInt(pCSS.right) +"px";
 60:      delete pCSS.right;
 61:    }
 62:    if (pCSS.bottom !== undefined){
 63:      pCSS.top = this.startWinSize.cH - pSize.mH - parseInt(pCSS.bottom) +"px";
 64:      delete pCSS.bottom;
 65:    }
        // left 値や top 値が "center" で指定された場合
 66:    if (pCSS.left ==="center") pCSS.left = this.startWinSize.cW/2 - pSize.mW/2+"px";
 67:    if (pCSS.top ==="center") pCSS.top = this.startWinSize.cH/2 - pSize.mH/2+"px";
        // mouse cursor 近傍にpopupする場合の位置指定
        // left 値か top 値に false を指定することによりその旨を指定する
 68:    if (pCSS.left === false || pCSS.top === false) {
 69:      pCSS.left = parseInt($.F.mousePos.X) + parseInt(offCSS.left) + "px";
 70:      pCSS.top = parseInt($.F.mousePos.Y) + parseInt(offCSS.top) +"px";
 71:    } else {
        // mouse cursor 近傍以外の場所に表示する場合には scroll 値を加算
 72:      pCSS.left=parseInt(pCSS.left) + this.startScrollPos.L+"px";
 73:      pCSS.top=parseInt(pCSS.top) + this.startScrollPos.T+"px";
 74:    }
        // 画面外への飛び出し防止補正
 75:    pCSS.left = Math.max(0, Math.min(this.startWinSize.cW + this.startScrollPos.L - pSize.mW, parseInt(pCSS.left)))+"px";
 76:    pCSS.top = Math.max(0, Math.min(this.startWinSize.cH + this.startScrollPos.T - pSize.mH, parseInt(pCSS.top)))+"px";
 77:  }
      // 幅/高さが極小の要素 css 値を設定する。これにより展開/縮小を演出する。
 78:  var setShrinkOuterCSS = function(){
 79:    var pCSS = this.o.popupCSS,
 80:        pSize = this.o.popupSize,
 81:        Xdir = pCSS.origin ===5 || pCSS.origin ===7,
 82:        Ydir = pCSS.origin ===6 || pCSS.origin ===8;
 83:    return {opacity:0,
 84:      borderLeftWidth: Xdir ? pCSS.borderLeftWidth : 0,
 85:      borderRightWidth: Xdir ? pCSS.borderRightWidth : 0,
 86:      borderTopWidth: Ydir ? pCSS.borderTopWidth : 0,
 87:      borderBottomWidth:  Ydir ? pCSS.borderBottomWidth : 0,
 88:      paddingLeft: Xdir ? pCSS.paddingLeft : 0,
 89:      paddingRight: Xdir ? pCSS.paddingRight : 0,
 90:      paddingTop: Ydir ? pCSS.paddingLeft : 0,
 91:      paddingBottom: Ydir ? pCSS.paddingBottom : 0,
 92:      marginLeft: Xdir ? pCSS.marginLeft : 0,
 93:      marginRight: Xdir ? pCSS.marginRight : 0,
 94:      marginTop: Ydir ? pCSS.marginTop : 0,
 95:      marginBottom: Ydir ? pCSS.marginBottom : 0,
 96:      width: (Xdir ? pSize.cW : 0) +"px",
 97:      height: (Ydir ? pSize.cH : 0) +"px",
 98:      width: ((pCSS.origin ===5 || pCSS.origin ===7) ? pSize.cW : 0) +"px",
 99:      height: ((pCSS.origin ===6 || pCSS.origin ===8) ? pSize.cH : 0) +"px",
100:      left:parseInt(pCSS.left) + 
101:        (pCSS.origin ===0 ? pSize.mW/2 :
102:        pCSS.origin ===2 ? pSize.mW :
103:        pCSS.origin ===3 ? pSize.mW :
104:        pCSS.origin ===6 ? pSize.mW : 0) +"px",
105:      top:parseInt(pCSS.top) +
106:        (pCSS.origin ===0 ? pSize.mH/2 :
107:        pCSS.origin ===3 ? pSize.mH :
108:        pCSS.origin ===4 ? pSize.mH :
109:        pCSS.origin ===7 ? pSize.mH : 0)+"px"
110:    }
111:  }
112:
      // popup 要素及び幅事前計測用の div 要素タグの作成
113:  var makePopupElem = function(){
114:    if (!$("#animPopup").size()) {
115:      $("<div id='animPopup'></div>")
            .css({position:"absolute",display:"none",zIndex:"1000"})
            .append("<div></div>").appendTo(document.body);
116:    }
117:    $animPopup=$("#animPopup");
        <span class="aquamarine">// Popup 隠蔽用×タグの作成</span>
118:    if (!$("#xMark").size()){
119:      $("<div id='xMark'>CLOSE</div>")
            .css($.fn.animatedPopup.opts.closeBarCSS)
            .append("<div style='position:absolute;z-index:1003;top:0;right:2px;width:13px'>×</div>")
            .appendTo($animPopup);
120:    }
121:    $xMark = $("#xMark");
122:  };
      // for hide animation
123:  var setShrinkInnerCSS = function(){
124:    var pCSS = this.o.popupCSS,
125:        Xdir = pCSS.origin ===5 || pCSS.origin ===7,
126:        Ydir = pCSS.origin ===6 || pCSS.origin ===8;
127:    return {
128:      width: (Xdir ? this.o.popupSize.cW : 0) +"px",
129:      height: (Ydir ? this.o.popupSize.cH : 0) +"px",
130:      opacity:0
131:    };
132:  }
133:  var hideAnim = function(){ // popup 要素の隠蔽アニメーション
134:    this.o.hidden=true;
135:    $(":animated").queue('fx',[]).stop(); >// 登録済みのアニメを全て停止する
136:    $animPopup.animate(this.o.shrinkOuterCSS,
          {queue:false,duration:this.o.duration,easing:this.o.easing})
          .children().eq(0).animate(setShrinkInnerCSS.call(this),
          {duration:this.o.duration,easing:this.o.easing});
137:  }
138:
139:  var showAnim = function(){ // popup 要素の表示アニメーション
140:    if (errFlag) {
141:      errFlag = false; //$(this).unbind("click");
142:      return errFunc();
143:    }
144:    $(":animated").queue('fx',[]).stop(); // 登録済みのアニメを全て停止する
        // 表示する前に、animPopup の幅と高さをゼロにして
        // 所定位置に非表示描画で配置する。
145:    if(this.o.hidden===undefined)
146:      $animPopup.css(this.o.shrinkOuterCSS).children().eq(0).css(setShrinkInnerCSS.call(this));
        // 表示アニメーション
147:    this.o.popupCSS.display="block";
148:    var fn = function(){$.fn.animatedPopup.running=false;} // 初期化
149:    $animPopup.animate(this.o.popupCSS,{
150:      queue:this.o.queue, duration:this.o.duration,
151:      easing:this.o.easing, complete:this.o.complete}
152:    ).children().eq(0).animate(
153:      {width:this.o.popupCSS.width,height:this.o.popupCSS.height,opacity:this.o.popupCSS.opacity},
154:      {queue:false,duration:this.o.duration,easing:this.o.easing}
155:    );
156:  }
157:  var doAnim = function(){
158:    $.fn.animatedPopup.running=true;
159:    if (wait) {clearInterval(wait);wait=null};
160:    showAnim.call(this);
161:  }
162:
163:  $(function(){
164:    makePopupElem();
165:    jQ.each(function(){
166:      var that=this, // this=jQ[i]
          // animPopup を目的の位置に配置するCSSオブジェクトを作る。
167:      if ($xMark.css("display")!=="none")
168:        $xMark.click(function(){hideAnim.call(that)});
169:      else $animPopup.click(function(){hideAnim.call(that)});
170:      that.o = makeCSS.call(that);
171:      getSize.call(that);
172:      setPos.call(that);
173:      that.o.shrinkOuterCSS = setShrinkOuterCSS.call(that);
174:      if ($.fn.animatedPopup.running){
175:        wait = setInterval(function(){doAnim.call(that)},1000);
176:      } else doAnim.call(that);
177:      $(window).scroll(function(){
178:        this.endScrollPos = $.F.getScroll(window);
179:        that.o.popupCSS.left = parseInt(that.o.popupCSS.left)
180:          + this.endScrollPos.L - this.startScrollPos.L + "px";
181:        that.o.popupCSS.top = parseInt(that.o.popupCSS.top)
182:          + this.endScrollPos.T - this.startScrollPos.T + "px";
183:        that.o.shrinkOuterCSS = setShrinkOuterCSS.call(that);
184:        $animPopup.css({left:that.o.popupCSS.left,top:that.o.popupCSS.top});
185:        this.startScrollPos = this.endScrollPos;
186:      });
187:      $(window).resize(function(){
188:        this.endWinSize = $.F.getSize(window),
189:          x = that.o.initPopupCSS.left==="center" && 2 ||
                  that.o.initPopupCSS.right!==undefined && 1 || false,
190:          y = that.o.initPopupCSS.top==="center" && 2 ||
                  that.o.initPopupCSS.bottom!==undefined && 1 || false;
191:        if (x){
192:          that.o.popupCSS.left = parseInt(that.o.popupCSS.left)
193:            + this.endWinSize.cW/x - this.startWinSize.cW/x + "px";
194:        }
195:        if (y){
196:          that.o.popupCSS.top = parseInt(that.o.popupCSS.top)
197:            + this.endWinSize.cH/y - this.startWinSize.cH/y + "px";
198:        }
199:        that.o.shrinkOuterCSS = setShrinkOuterCSS.call(that);
200:        $animPopup.css({left:that.o.popupCSS.left,top:that.o.popupCSS.top});
201:        this.startWinSize = this.endWinSize;
202:      });
203:    });
204:  }); // End of "DOMReady function"
205:  arguments.callee.hideAnim = function(){hideAnim.call(jQ[0]);};
206;  return jQ;
207:}; // End of "animatedPopup function"
    // 引数の初期値を設定
208:$.fn.animatedPopup.opts = {
209:  content:"",
210:  popupOffset:{left:"16px",top:"16px"},
211:  popupCSS:{
212:    position:"absolute",zIndex:1000,left:"center",top:"center",
213:    color:"white",fontWeight:"bold",
214:    width:"400px",backgroundColor:"royalblue", margin:0,
215:    paddingTop:"5px",paddingBottom:"5px",paddingLeft:"5px",paddingRight:"5px",
216:    borderWidth:"5px", borderColor: "plum", borderStyle:"ridge",
217:    textAlign:"center", display:"none",
218:    opacity:1, overflow:"visible",
        // 0:center of element,1:leftTop,2:rightTop,3:rightBottom
        // 4:leftBottom,5:topEdge,6:rightEdge,7:bottomEdge,8:leftEdge
219:    origin:0
220:  },
221:  closeBarCSS:{
222:    position:"absolute",zIndex:"1002",
223:    textAlign:"center",
224:    opacity:0.75,top:0,left:0,cursor:"pointer",
225:    fontSize:"small",lineHeight:"1.2em",width:"100%",
226:    backgroundColor:"midnightblue",display:"block"
227:  },
228:  queue:true, duration:800, easing:"swing", complete:function(){}
229:};
230:})(jQuery);

jQuery の各種メソッドを活用して、マウスカーソルの現在位置や要素の位置とサイズなどを取得する

はじめに───ここで行ったこと

このエントリイでは、jQuery の各種メソッドを活用して、まず、マウスカーソルの現在位置の頁座標を取得/表示し、また要素の頁座標と offsetParent 座標値並びに大きさを取得/表示させます。

次に、要素をアニメーションさせた直後に、当該要素の絶対/相対座標値や要素の各サイズ値を取得/表示させます。その際には、値が変わった箇所を一目で分かるように工夫しました。

このアニメーションは要素が往復運動するもので、1の往復運動毎にその動く方向・距離・内容幅・内容高さを乱数を発生させて変動させています。何度か繰り返しアニメーションさせてみると乱数の効果を良く確認出来るでしょう。box2 と box3 に別々の乱数を割り振ったので、「往路」の方向も移動距離も幅と高さの変化率もお互いに異なりますが、復路は必ず最初の位置とサイズに戻るようにプログラミングしました。

また、往路復路の別を分かりやすくするために、往路が終わると要素が半透明になるようにしました。この状態は復路が終わると当初の不透明に戻ります。

testArea...pos:rel
tester1
pos:stat
box1
pos:rel
tester2
pos:rel
box2
pos:abs
tester3
pos:rel
box3
pos:abs
tester4
pos:abs
tester's position
itemAreatester1tester2tester3tester4
Left
Top
posLeft
posTop
width
innerW
outerW
marginW
height
innerH
outerH
marginH
box's position
box1box2box3

※ ピンクの背景色セルは、その値が直前値から変化したことを示す。

▲ToTop

1. マウスカーソルの現在座標値をリアルタイムで取得し表示する

マウスカーソルの現在座標値は、jQuery のイベントに係るメソッドを利用すれば、極めて簡単に取得できます。ブラウザ毎の計測方法の差異は jquery.js が処理してくれるので、後述するように、座標値取得のための javascript コードは極めて簡潔になります。

次に、取得したマウスカーソル座標値の表示については、上の背景色ロイヤルブルーのボックス内にマウスカーソルが入った時にのみ、マウスカーソルの右下にマウスカーソルの頁座標値を表示するようにしました。更に、当該ボックス内でマウスカーソルが移動した場合には、マウスカーソルに追随して座標表示ボックスを移動させ、かつ、刻々と変化する座標値を瞬時に表示するようにしました。

つまり、マウスカーソルの移動に合わせてリアルタイムでその座標値を取得し、座標値の表示場所はマウスカーソルに追随して移動するようにしました。

2. 要素の頁座標値とoffsetParentからの座標値、並びに大きさを取得し表示する

要素の位置と大きさを取得するために利用する jQuery メソッドは次の 10 個です。

offset、position、width、height、
innerWidth/Height、outerWidth/Height、outerWidth(true)/Height(true)

これらの jQuery インスタンスメソッドを使用すれば、極めて容易に要素の位置とサイズが取得出来ます。

jQuery(要素).offset インスタンスメソッドは、対象要素が含まれる表示領域の左辺または上辺から、対象要素のボーダー辺までの横又は縦方向の距離(頁内絶対座標とでも呼ぶべきか?)を計測し、jQuery(要素).position は、対象要素の offsetParent 要素のパディング辺からの、対象要素のマージン辺の横又は縦方向の距離を計測するメソッドです。このことの詳細は、「 jquery.js (1.4) による要素位置の測定と適正な配置 (2) コード解読(1) 」 に詳述しました。

また、width/height、innerWidth/Height、outerWidth/Height及びouterWidth(true)/Height(true)は、順に対峙する内容辺間距離、パッディング辺間距離、ボーダー辺間距離、マージン辺間距離を計測するメソッドです。詳細は拙エントリイ:「 jquery.js (1.4) による要素位置の測定と適正な配置 (5) コード解読 (4) 」を参照してください。画像付きで説明しています。

取得した値は、このエントリイ上部に配置した tester's position 表と box's position 表に表示させました。ここに、Left と Top は頁座標値で offset メソッドを使って、また posLeft と posTop はoffsetParent からの座標値で、position メソッドを使って、それぞれ取得しています。

また、offsettParent の定義から、各 tester ボックスの offsettParent は testAreaとなり、各 box の offsetParent は各 tester ボックスとなります。( 因みに testArea の offsetParent は div#container であり、更に div#container の offsetParent は body となります。body 以外の offsetParent とする要素は、全て CSS スタイル設定で position:relative を指定し、明示的にoffsetParent となるようにしました。)

▲ToTop

3. 要素を移動させるアニメーションと移動後の座標値の取得/表示について

ここで作成したアニメーションは 2 つの div 要素を、ほぼ同時に上 or 下かつ左 or 右に移動させながら、同時に幅と高さを変化させるものです。目的はアニメーションそのものよりも、移動前後の座標値の取得/表示を主眼としましたが、アニメーションそのものにも、少しは興味を引くであろう様々な工夫を凝らしました。(アニメーションの起動はエントリイ上部に配置した animate ボタンをクリックして行います。)

工夫は、単純な移動やサイズ変更の繰り返しでは詰まらないので、乱数 Math.random() メソッドを使って、移動の度に方向・距離・内容サイズが変わるようにしました。easing 関数も乱数によって 11 種類からその都度任意に選択されるようにセットしたので、クリックする度に異なる方向、異なる距離、,異なるサイズ、異なる easing を使ってアニメが展開されます。

また、ここで作成した移動と大きさ変更アニメーションは、往復運動で 1 サイクルになるようにしたので、往路の移動とサイズ変更が終わると要素が半透明になるようにして、往路であることが分かるようにしました。当然ですが復路が終わると位置と大きさは元に戻り、不透明度も 100 %に戻るようにしました。

4. このエントリイのためのスタイルシート

■スタイルシート
#testArea {
  position:relative;background:royalblue;width:520px;padding:1em;/*height:360px;*/
}
.tester {margin:0.5em;width:100px;height:150px;}
#tester1 {border:solid white 2px;} /*position:static*/
#tester2 {position:relative;background:darkred;border:solid yellow 2px;}
#tester3 {position:relative;background:teal;border:solid white 2px;}
#tester4 {position:absolute;top:10px;left:370px;border:solid lime 2px;}
.box {
  position:absolute;top:50px;left:20px;background:indigo;border:solid lime 2px;
  width:66px;height:80px;
}
#box1 {position:relative;}
#result {margin:1em 0;border:1px lightgray dotted;padding:2px;background:dimgray;}
table#table1 input,table#table2 input {width:65px;text-align:right;background:white}
table#table1 caption,table#table2 caption {color:lime}
table#table1 th,table#table2 th {line-height:0.5em;text-align:center}
table#table1 tr,table#table2 tr {line-height:1em;}
button#animBtn {display:block;position:absolute;top:180px;left:400px;z-index:10;}
div#testArea div#log {position:relative;z-index:9;padding-top:4em;line-height:1.1em;}

▲ToTop

5. jQuery の各種メソッドを活用したこのエントリイのための Javascript コードの解説

■javascript code
  // マウスカーソルの座標値を表示するためのdiv要素を作成する。
  $("<div id='testBalloon' />").css({
      position:"absolute",padding:"0.5em",border:"1px solid green",
      display:"none",background:"navy",zIndex:11
  }).appendTo("body");
  var o ={ // 各種 jQuery インスタンスへのショートカットなどを登録するオブジェクト
    $cont: $("#container"),
    $tArea: $("#testArea"),
    $t1: $("#tester1"),  $t2: $("#tester2"),
    $t3: $("#tester3"),  $t4: $("#tester4"),
    $tbl1: $("#table1"), $tbl2: $("#table2"),
    $input: $("input","#result"),  $log: $("#log"),
    $tBalloon: $("#testBalloon"), rnd:{}, // 乱数を格納するオブジェクト
    easing:["swing","easeInOutQuad","easeInOutCubic","easeInOutQuart","easeInOutQuint",
    "easeInOutSine","easeInOutExpo","easeInOutCirc","easeInOutElastic","easeInOutBack",
    "easeInOutBounce"], //easing 関数を登録
    cnt: 0, //カウンター
    flag: true //奇数偶数区別用
  };
  // 各 box へのショートカットを登録する
  o.$b1= $(o.$t1).children(":eq(1)");
  o.$b2= $(o.$t2).children(":eq(1)").css("zIndex","2");
  o.$b3= $(o.$t3).children(":eq(1)").css("zIndex","3");
  // document 内でマウスカーソルが動いた時に、その座標値を o オブジェクトのプロパティに登録する。
  // たったこれだけのコードで目的を達成することが出来る。
  $(document).bind('mousemove',function(e){ 
    o.x = e.pageX;
    o.y = e.pageY;
  });
  // エリア内にマウスカーソルがある時と外れた時の、
  // マウスカーソル座標値を表示するボックスの表示/非表示を制御する。
  o.$tArea.hover(
    function(){o.$tBalloon.fadeIn()},
    function(){o.$tBalloon.fadeOut()}
  ).mousemove(function(){
    // エリア内でマウスカーソルが動いた時に座標値を表示する。
    // 座標値の表示ボックスは、マウスカーソルの右下に 16 pxずれた箇所に表示する。
    o.$tBalloon.html("<div>top: "+ o.y+ "px<br />left: "+o.x+"px</div>")
      .css({top:parseInt(o.y)+16+"px",left:parseInt(o.x)+16+"px"})
  });

  // 要素の位置と大きさを計測する関数を再呼び出し出来るように定義する。
  var doCalc = function(){
    $.each([o.$cont,o.$tArea,o.$t1,o.$t2,o.$t3,o.$t4,o.$b1,o.$b2,o.$b3],function(){
      this.oSet = $(this).offset(); // jQuery.offset メソッド
      this.pos = $(this).position(); // jQuery.position メソッド
      this.left = this.oSet.left; // jQuery.offset().left 値取得
      this.top = this.oSet.top; // jQuery.offset().top 値取得
      this.posLeft = this.pos.left; // jQuery.position().left 値取得
      this.posTop = this.pos.top; // jQuery.position().top 値取得
      this.width = $(this).width(); // 内容辺間の幅取得
      this.innerW = $(this).innerWidth(); // padding 辺間の幅取得
      this.outerW = $(this).outerWidth(); // border 辺間の幅取得
      this.marginW = $(this).outerWidth(true); // margin 辺間の幅取得
      this.height = $(this).height(); // 内容辺間の高さ取得
      this.innerH = $(this).innerHeight(); // padding 辺間の高さ取得
      this.outerH = $(this).outerHeight(); // border 辺間の高さ取得
      this.marginH = $(this).outerHeight(true); // margin 辺間の高さ取得
    });
  }
  doCalc(); //関数実行
  // アニメーションで使用するために、4 つのプロパティの初期値を
  // アニメ対象オブジェクトの2 つのプロパティの初期値を org プロパティに記憶させる
  $.each([o.$b2,o.$b3],function(i,n){
    n.org={};
    $.each(["posLeft","posTop","width","height"],function(){
      n.org[this] = n[this];
    });
  });

  // 位置やサイズ値をエントリイ内の表内に挿入する関数
  var insertValue = function(ary,obj){ // ary は計測対象要素、obj は挿入対象 table。
    $.each(ary,function(i,n){
      $.each(["left","top","posLeft","posTop","width","innerW","outerW","marginW","height","innerH","outerH","marginH"],
        function(j){
          o.tmp=$(obj).find("input:eq("+(i+j*ary.length)+")");
          o.tmp.before=o.tmp.attr("value") || 0; // 直前の値
          // 各要素の各属性値を取得する。
          o.tmp.attr("value",parseInt(n[this],10)); // n は各要素、this は属性
          o.tmp.after=o.tmp.attr("value");
          // 前後の属性値が異なれば背景色を変化させる。
          o.tmp.before !== o.tmp.after ? o.tmp.css("background","pink")
          : o.tmp.css("background","white");
      });
    });
  };
  insertValue([o.$tArea,o.$t1,o.$t2,o.$t3,o.$t4],o.$tbl1);
  insertValue([o.$b1,o.$b2,o.$b3],o.$tbl2);
  o.$input.css("background","white"); // (初期値として)背景色をホワイトに設定する。
  // 移動アニメーション関数の定義
  var trans = function(){
    o.$input.css("background","white"); // 背景色の初期化
    // box2 の移動アニメーション。trans 関数は call メソッドを使ってo.rndオブジェクト
    // から呼び出すため、this は o.rnd オブジェクトを参照することになる。
    // this の各プロパティは $("#animBtn").click メソッドを参照のこと。
    o.$b2.animate({
      // アニメ終了値は全て相対移動量で設定する
      left: this[1]+"="+this[5][0]+"px",
      top: this[2]+"="+parseInt(this[5][0]/2)+"px",
      width: this[1]+"="+ Math.round(o.$b2.org.width*this[6][0])+"px",
      height: this[2]+"="+ Math.round(o.$b2.org.height*this[6][1])+"px",
      opacity: o.flag ? 0.5 : 1
      // 奇数回目には半透明に、偶数回目には不透明に。
      opacity: o.flag ? 0.5 : 1
    },1000,o.easing[parseInt(o.rnd[0][0]*11)]);
    o.$b3.animate({
      left: this[3]+"="+ this[5][1] +"px",
      top: this[4]+"="+ parseInt(this[5][1]/2) +"px",
      width: this[3]+"="+ Math.round(o.$b3.org.width*this[6][1]) +"px",
      height: this[4]+"="+ Math.round(o.$b3.org.height*this[6][0]) +"px",
      opacity: o.flag ? 0.5 : 1
    },1000,o.easing[parseInt(o.rnd[0][1]*11)],function(){ //アニメーションが終わってから、移動後の座標値を挿入する。
      doCalc(); insertValue([o.$b1,o.$b2,o.$b3],o.$tbl2);
    });
  }
  // アニメの動作ログを作る。
  var insertLog = function(){
    var ary = []; // 偶数回目の移動方向を反転表示とするため
    $.each([this[1],this[2],this[3],this[4]],function(i,n){
      ary[i] = o.flag ? n : (n==="+" ? "-" : "+");
    });
    o.$log.html(
      "<div>box2 の移動方向と距離のための乱数:"+ this[0][0] + "</div>"+
      "<div>box3 の移動方向と距離のための乱数:"+ this[0][1] + "</div>"+
      "<div>box2 の横移動距離:"+ary[0] +"=" + this[5][0]+"px, "+
      "box2 の縦移動距離:"+ary[1] +"=" + parseInt(this[5][0]/2)+"px</div>"+
      "<div>box3 の横移動距離:"+ary[2] +"=" + this[5][1]+"px, "+
      "box3 の縦移動距離:"+ary[3] +"=" + parseInt(this[5][1]/2)+"px</div>"+
      "<div>サイズ増減のための 2 つの乱数:<br />  "+this[6][0]+","+this[6][1]+"</div>"+
      "<div>box2 の横方向サイズ増減倍率、又は box3 の縦方向サイズ増減倍率:<br />  "+this[6][0] +"</div>"+
      "<div>box3 の横方向サイズ増減倍率、又は box2 の方向サイズ増減倍率:<br />  "+this[6][1] +"</div>"+
      "<div>box2 の移動に適用されたeasing:"+o.easing[parseInt(o.rnd[0][0]*11)]+"</div>"+
      "<div>box3 の移動に適用されたeasing:"+o.easing[parseInt(o.rnd[0][1]*11)] +"</div>"
    );
  };
  // animate ボタンがクリックされたら実行する。
  $("#animBtn").click(function(){
    var i=1;
    o.flag = ++o.cnt % 2; // 奇数回目(true) か、偶数回目(false )か?
    if (o.flag){ // 奇数回目ならば
      // 移動方向、移動距離、サイズ変更等に使用する乱数を発生させる。
      // o.rnd は乱数を使った各種プロパティを格納するオブジェクトである。
      o.rnd[0]=[Math.random(),Math.random()];
      // 乱数値の小数点第 1 ~ 4 位迄の各桁の数値が偶数ならば+を、
      // 奇数ならば-を o.rnd[i] に順に代入する。これにより 2 つのボックス
      // それぞれの横及び縦方向の移動方向をランダムに設定する。
      for (;i<5;i++)
          o.rnd[i] = parseInt(o.rnd[0][0] * Math.pow(10,i)) % 2 === 0 ? "+" : "-";
      // box2 と box3 の移動距離を設定 (0 ~ 199)
      o.rnd[5] =[parseInt(o.rnd[0][0]*200),parseInt(o.rnd[0][1]*200)];
      // box2 と box3 のサイズ変動倍率を設定 (-0.5 ~ +0.49999)
      o.rnd[6] =[o.rnd[0][0]-0.5,o.rnd[0][1]-0.5];
    } else {
        // 偶数回目では正負記号を反転させる
        for (; i<5; i++) o.rnd[i] = o.rnd[i]==="+" ? "-" : "+";
    }
    trans.call(o.rnd); // アニメーション開始
    insertLog.call(o.rnd); // アニメーションに係るログを表示する。
    $(this).blur(); // ボタンからフォーカスを外す。
  });

▲ToTop

6. javascript コード作成上留意したこと

HTML、CSS、Javascriptの役割分担

一部に style 属性を設けましたが、出来るだけ文書構造(HTML)、表示(CSS)及び動作(Javascript)は区別しました。

出来るだけ変数の数を減らしました

無名関数で括る必要はなかったのでトップレベルの変数はグローバルになります。そこで可読性をたかめるためにも、トップレベルの変数は 1 つにまとめました。具体的には基礎的な変数を var o オブジェクトプロパティにまとめて、関数以外のトップレベルの変数は var o だけとしました。

jQuery インスタンスの表記上の扱い

jQuery インスタンスを複数回使用する場合には、そのショートカットを作成し、かつ jQuery インスタンスと一目で分かるような独自の名称にしました。具体的には、変数名の 1 文字目に $ 記号を付けました。

ランダムなアニメーションとするために、乱数をさまざまに加工しました

移動する方向、移動距離、サイズの変動率、アニメに適用する easing 関数の選択などを、ランダムに設定しましたが、その各々に乱数を発生させるのではなく、発生させた 1 つの乱数を加工して使い回しました。

アニメ終了後に実行させる行為の扱い

jQuery(要素).animate メソッドでは当該アニメーション終了後に行わせる行為は、所定の方法で記述しなければなりません。或る行為を単にこのメソッド以降の行に既述すると、アニメーション実行中もコード進行は進みますから、アニメーション終了を待たずに当該行為が実行されてしまうからです。

久々に animate メソッドを使用したため、そのことに気がつくのに時間を要してしまい、アニメーション終了後の処置を相応しいタイミングでブラウザに反映させることがなかなか出来ず、徒労を重ねてしまいました。

ボタンクリックによる往復アニメの工夫

アニメーションを起動するボタンは簡潔に 1 つだけとしましたが、ここで作ったアニメーションには往路と復路があり、2 回のアニメーションが 1 セットになっています。そこで最初の段階では、click メソッドではなく toggle メソッドで往復アニメーションを実現させていました。

そして移動方向/距離もサイズ変更も、before と afterで、符号を変えて往復アニメを実現していました。

しかし、効率的なコード記述にすべきだと考え、click 数に応じた flag を用意して click メソッドを採用することにしました。

また、アニメの終点値には、"+=" 等の相対変動値を採用しましたが、これには大きな意味があります。絶対変動値指定を採用すると、コードは長大になるばかりで美しくないのです。相対変動値の採用は往復アニメーションにおける重要なポイントでしょう。

▲ToTop

jquery.js のアニメーションコードの活用 ( 2 ) アニメを使ってグラフを動的に描く

アニメーションを使ってグラフを描く

アニメーション活用エントリイの 2 つ目は、アニメーションを活用してグラフを書くコンテンツです。

そもそも、easing 関数のグラフ化を考えていたのですが、エクセルなどで作って貼り付けるのは余りにも芸がないと思われ、何とか Web サイト上で動的にグラフが描けないだろうか、と前から思っていました。

そして jquery.js のアニメーション解読を進めていく内に、easing 関数それ自体に興味を持ったことと併せて、アニメーションの options.step メソッドを使えば、アニメ途中の情報を取得できるのでグラフが書ける───入浴中にふと(苦笑)、そのことに気付きました。

構想ニ 1 日間、コーディングに約 2 日を要して( と言っても勤めながらなので、夜と朝だけのマイタイムですが )、何とか具体的な成果に漕ぎ着けました。

以下がその結果ですが、何はともあれ、アニメ継続時間を 2 秒か 4 秒程度にし、適当な easing 関数を選択してから、グラフ作成ボタンをクリックしてみてください。

僅か 3 クリックで、今選択した継続時間を要して青いボックスが左下に閉じられ、その後また同じ継続時間を使って右上方向に開かれます。これで「 グラフの座標 」が完成します。

そしてボックスが開き終わると、数10ミリ~数100ミリ秒後にそのボックス内に、今適用された easing 関数のグラフが描かれます。このときボックス下辺はグラフ横軸となって継続時間の推移割合を示し、、ボックス左辺は縦軸となり easing 関数値を表しています。

なお、グラフの大きさは相対的な問題に過ぎません、以下の例示の場合には、青いボックスの大きさ(つまりグラフ座標)を、レイアウト上の必要性から偶々 350 px 角としました。その結果X軸、Y軸両方向とも、時間推移割合と関数値を 350 倍してグラフを作成しています。

【注意】
IE では関数値が 0 未満または 1 を越える easing 関数の場合、固まってしまうようです。ですから Back、Elastic などは IE ではグラフ描画は出来ませんが、もし固まった場合には、この頁をリロードすれば、Javascript の 「 凝固状態 」 を解除出来ます。

  1. まず選択した easing 関数の効果を、ボックス隠蔽 / 表示の 2 つのアニメーションを通じて確認してください。
  2. ボックスが開き終わると直ぐに、今選択し、ボックスの開閉に利用した easing 関数のグラフが、開き終わったボックス内に描かれます。
アニメ継続時間
適用 easing

▲ToTop

グラフ描画の仕組み

jquery.js における easing 関数の扱い
easing 関数とは

「 easing 」 は速度を増減するときに使うもの、とされています。( イージング (Easing) とは | FLASH関連用語集 | ミツエーリンクス )。つまり easing 関数は動きを加速し、あるいは減速する加速度の役割をもっています。

そして、横軸に時間を、縦軸に変動値を取って「 動き 」をグラフ化した場合において、それが直線であれば等速で動いたことを表し、直線の角度は速度の高低を意味します。そして直線でない曲線部分は、加速度が働いた部分であり、上に凸ならば減速、下に凸ならば加速となります。

さて、easing 関数は Tween クラスの場合も jquery.js の場合も、基本的に次の 4 つの変数により変化する二次元関数です。初期値(b)、変動幅(c)、アニメの継続時間(d)、そして現在までの経過時間(t)です。つまり、中学校時代に倣った馴染みの関数式 y = f (x) になぞらえれば、easing 関数の基本形式は次のようになります。

変動後の値 = f ( 初期値, 変動幅, 経過時間, 継続時間 )

ここにおいて時間と共に変化する変数はただ 1 つ 経過時間であり、後は全て定数値となります。勿論、変動幅を時間の関数にすれば、それも変化することになりますが、その場合であっても変動幅は経過時間の関数であり、結局経過時間が唯一の変数となります。

jquery.js の easing 関数

jquery.js では変動幅( c : change で表される場合が多い )を正規化しています。つまり 0 から始まり 1 で終わるように引数が設定されています(jquery.js ver1.32 #4139)。これにより関数はいわば純化され、事の本質が分かりやすくなっていますし、そのことが easing 関数への理解を深める一助ともなっていると思います。

jquery.js における easing 関数の詳細(plugin 30種類を含む)

jquery.js に含まれている easing 関数は linear と swing の 2 類だけですが、それに30種類を追加するプラグイン ( George Smith 氏による 30 種類の easing 関数 ) があります。

エントリイを改めて、これらの 32 種類の easing 関数の全貌を、昔習った数学の関数式を思い出しながら解明しようと思っています。

▲ToTop

どのように関数値を取得するか

options.step メソッドを使います。

私が今自宅で使用しているパソコンでは、 Firefox でこの頁を開いて計測してみたところ、jquery.js の内部メソッドである e.step メソッドは、1 つのアニメ対象要素の 1 つのアニメ対象プロパティ当たり、1 秒間に約 80 回実行されます。(IE の場合、IE 8 であっても回数はもっと少なくなります。Javascript 実行速度が遅いためです。)

このそれぞれの瞬間毎に、 animate メソッドの引数として指定する options.step メソッドを利用して、( jquery.js の内部で作成される ) e インスタンスに保持されているアニメ諸情報を取得します。

easing 関数値は e.pos プロパティに保持されていますので、これを取りだして配列に格納し、その後その配列の各要素を使って、e.step が間歇的に取得した情報から、取得しなかった中間値を補間する計算を行わせて、空隙部を埋めます。

こうして横軸の全てのピクセル数 350 個に対応する関数値を取得又は推定し、グラフ化します。

グラフは 1 × 1 dot の点を 順に青いボックス内に append することにより描きます。

詳細はコード解説部分で触れます。

options.step メソッドで取得できない座標値の補間方法

「 無視できるほど小さい部分では曲線は直線と見なすことが出来る 」という微分の考え方に拠りました。決して複雑ではなく、つまりは単なる直線補間です(苦笑)。

当然アニメ継続時間を長くすればするほど、stepメソッドの呼び出し回数が増えるので、補間値が減り、step メソッドによる算出値が増え、グラフは正確さを増します。

このことは継続時間を長短変えてグラフを描かせてみると良く分かります。継続時間が短いとグラフは直線部分が増え、長くすると直線が減っていきます。これは計算により算出した補間値の数が減るためです。

この件に関する詳細もコード解説部分で触れます。

コーディングの苦労話

ここで行ったことは、ボックスアニメーションを引き起こしつつ、そのボックス内にそのアニメに使用した easing 関数のグラフを書かせるという複合的なアニメーションです。

それ故に、コーディングではグラフを描かせるタイミングについて、幾つかテストしながら調整しました。辿り着いたのは、options.step メソッドの最中でもなく、完全にアニメが終わってからでもなく、step メソッドによる情報取得が終わった段階で直ぐに補間値算出を始めさせ( この段階ではアニメーション動作は進行中です )、かつその補間値計算が終わったら直ぐにグラフ描画を始めさせる、という併行的なコード進行でした。

補間値計算やグラフのプロット作業がアニメーションの進行に干渉せず、かつアニメ完了後出来るだけ短時間でグラフ描画を行わせる最適な方法を探った結果こうなりました。

次に、2 つのバージョンを作った経緯を書いておきたいと思います。

最初に作成したのは以下に掲げた Ver 1 です。このコードは良く見渡すと、drawGraph メソッドが呼ばれる度毎に、アニメ対象の高さ・幅を計算し、隠蔽/表示用アニメ CSS を作り、duration・easing・complete を算出し、step メソッドを登録しています。これはいかにも無駄です。

そこで二度目以降の drawGraph メソッド呼び出し時には、初回呼び出し時に算出した各値を可能な限り再利用するようなコード進行に変更すべきだと思い、Ver 2 を作りました。

▲ToTop

プラグイン drawGraph() メソッドコード
■プラグイン drawGraph() メソッドコード
// ▼ Ver 2 : コード進行の効率化を考慮した改訂版
 1:(function($){
 2:$.fn.drawGraph = function(d,e,fn){
 3: var $box=this, o={}, cnt=0, allsteps=0,layout=[], dotAry=[], arrgAry=[];
   // メソッド実行後も o オブジェクトのプロパティ値を保持させるために、
   // drawGraph メソッドの opts プロパティを作る。
 4: if (!arguments.callee.opts) arguments.callee.opts={};
   // drawGraph メソッドの opts プロパティがあれば、o オブジェクトに併合する。
   // これにより 2 度目以降の drawGraph 呼び出し時に o オブジェクトに opts が複写される。
   // その opts は、直前の drawGraph メソッド実行時に 6 ~ 19 行で作成された o オブジェクトが
   // 20 行において複写されたものなので、2 度目以降の drawGraph 呼び出し時には、
   // 直前実行時に作成したプロパティが o オブジェクトに格納される。
 5: else $.extend(o, arguments.callee.opts);
   // box プロパティがないか(初回呼び出し時)、登録されているノードが異なる場合
   //(今回の使い方では呼び出し元は常に同一なので、ノードが異なることはないが
   // 一般化するために敢えて後者の条件を書いておく。)
 6: if (!o.box || o.box[0] !== $box[0] ) {
 7:  o.box = $box; // this への参照を登録する
 8:  o.log = $box.next(); // log 表示用ノードへの参照を獲得する。
 9:  o.h = $box.height(); // アニメ対象ノードの高さを測定する。
10:  o.w = $box.width(); // アニメ対象ノードの幅を測定する。
11:  o.hideCSS = {height:"toggle",width:"toggle",top:"+="+o.h+"px"}; // 隠蔽アニメ用CSS
12:  o.showCSS = {height:"toggle",width:"toggle",top:"-="+o.h+"px"}; // 表示アニメ用CSS
13: }
   // duration プロパティがないか、あっても引数と異なる場合にのみ 引数 d を o.duration に登録する。
14: if (!o.duration || o.duration !== d) o.duration = d;
   // easing プロパティがないか、あっても引数と異なる場合にのみ 引数 e を o.easing に登録する。
15: if (!o.easing || o.easing !== e) typeof o.duration ==="object" ? "" : o.easing = e;
   // fn が存在して関数で complete プロパティがないか、complete が fnと異なる場合
   // fn を o.complete に代入し、その他の場合には何もしない関数を代入する。
16: o.complete = typeof o.duration ==="object" ? "" :
17:  fn && $.isFunction(fn) && (!o.complete || o.complete!==fn) ? fn : function(){};
   // stepObj は毎回作成しないと doStep が初期化されない。
18: o.stepObj = typeof o.duration ==="object" ? $.extend({},o.duration,{step:doStep}) :
19:  {duration:o.duration, easing:o.easing, complete:o.complete, step:doStep};
   // 以上迄で作成された o オブジェクトを opts に複写し、2 度目以降の drawGraph
   // メソッド起動時に o オブジェクトの複写元とする。
20: $.extend(arguments.callee.opts, o);
21:
22: function doStep(){ // step メソッドを使ってグラフの横軸値と縦軸値を取得する
23:  var e = arguments[1]; ++allsteps; // e.options.step の第 2 引数である e オブジェクトを代入
24:  if (e.prop!=="height") return; // height プロパティ以外の時には何もしない。
25:  ++cnt ;
26:  layout[cnt]={};
27:  layout[cnt]["top"] = ( 1-e.pos ) * o.h; // グラフの縦軸値( easing 関数値 )
28:  layout[cnt]["left"] = e.state * o.w; // グラフの横軸値( 時間推移率 )
29:  dotAry.push(layout[cnt]); // layout 配列の要素を dptAry 配列に格納する。
    // 時間推移率が 100 %以上になったら直線補間関数を呼び出す。
30:  if (e.state >= 1) arrangeGraph();
31: }
32: function arrangeGraph(){ // 直線補間関数
33:  var len = dotAry.length;
34:  while (len){
     // dotAry の先頭要素を削除して arrgAry 配列に格納
35:   arrgAry.push(dotAry.shift());len--;
     // dotary 配列の次の要素のleft値と、格納されたばかりの dotstack 配列要素の left 値の差をとり
36:   if (!len) break;
37:   var diffLeft = Math.round(dotAry[0].left) - Math.round(arrgAry[arrgAry.length-1].left);
     // 差がなければ同一値と見なして dotary 配列の先頭要素を削除
38:   if (diffLeft == 0) {dotAry.shift();len--;}
     // 差が 1 ならば次の値なので dotAry 配列の先頭要素を削除し、それを arrgAry 配列に追加する
39:   else if (diffLeft == 1) {arrgAry.push(dotAry.shift());len--;}
40:   else {// 差が 2 以上あれば、補完値算出用に、その差で top 値を割って平均値を出す
41:    var avr = (dotAry[0].top - arrgAry[arrgAry.length-1].top) / diffLeft;
42:    for (var j=1, obj={}; j<diffLeft ;j++){
43:     // 差の数-1 回だけ補完値を算出し、arrgAry 配列に追加する。
44:     obj ={
45:      top: arrgAry[arrgAry.length-1].top + avr, 
46:      left: ++arrgAry[arrgAry.length-1].left
47:     };
48:     arrgAry.push (obj);
49:    }
50:   }
51:  }
52:  plotGraph();
53: }
54: function plotGraph(){ // ボックス内にグラフを描く
55:  $box.append("<div id='selectedEasingName' style='position:absolute;top:5px;left:15px;'>"+ o.easing +"</div>");
56:  o.log.html("height prop の step 起動回数 :"+ cnt +"<br />全ての step 起動回数:" + allsteps);
57:  for (var i=0, len = arrgAry.length; i<len; i++){
58:   $("<div class='dot hack' />").css({top:arrgAry[i].top+"px",left:arrgAry[i].left+"px"}).appendTo($box);
59:  }
60: }
61:
62: $box.empty(); o.log.empty(); // 表示されている easing 名と log を消す。
   // 隠蔽及び表示アニメを引き起こす。表示アニメの時にグラフも描く。
63: return $box.animate(o.hideCSS,o.duration,o.easing ).animate(o.showCSS, o.stepObj);
64:};
65:})(jQuery);
// ▲ Ver 2 終わり

// ▼ Ver 1 :サイズ測定と o オブジェクト作成を毎回行っている非効率版
(function($){
$.fn.drawGraph=function(duration,easing,fn){
 var cnt=0, allsteps=0, $box=this, layout=[], dotAry=[], arrgAry=[],
   o = $.extend({},{duration:duration, easing:easing});
 $box.h=$box.height(); $box.w=$box.width(); $log=$box.parent().next();
 var hideCSS =  {width:"toggle",height:"toggle",top:"+="+$box.h+"px"},
   showCSS =  {width:"toggle",height:"toggle",top:"-="+$box.h+"px"};
 o.step = doStep;
 o.complete = function(){$.isFunction(fn) ? fn() :function(){} };

 function doStep(){
  var e = arguments[1]; ++allsteps;
  if (e.prop!=="height") return;
  ++cnt ;
  layout[cnt]={};
  layout[cnt]["top"] = ( 1-e.pos ) * $box.h;
  layout[cnt]["left"] = e.state * $box.w;
  dotAry.push(layout[cnt]);
  if (e.state >= 1) arrangeGraph();
 }

 function arrangeGraph(){
  var len = dotAry.length;
  while (len){
   // dotAryの先頭要素を削除して arrgAry 配列に格納
   arrgAry.push(dotAry.shift());len--;
   // dotary 配列の次の要素の left 値と、格納されたばかりの dotstack 配列要素の left 値の差をとり
   if (!len) break;
   var diffLeft = Math.round(dotAry[0].left) - Math.round(arrgAry[arrgAry.length-1].left);
   // 差がなければ同一値と見なして dotary 配列の先頭要素を削除
   if (diffLeft == 0) {dotAry.shift();len--;}
   // 差が 1 ならば次の値なので dotAry 配列の先頭要素を削除し、それを arrgAry 配列に追加する
   else if (diffLeft == 1) {arrgAry.push(dotAry.shift());len--;}
   else {// 差が 2 以上あれば、補完値算出用に、その差で top 値を割って平均値を出す
    var avr = (dotAry[0].top - arrgAry[arrgAry.length-1].top) / diffLeft;
    for (var j=1, obj={}; j<diffLeft ;j++){
     // 差の数 - 1 回だけ補完値を算出し、arrgAry 配列に追加する。
     obj ={
      top: arrgAry[arrgAry.length-1].top + avr, 
      left: ++arrgAry[arrgAry.length-1].left
     };
     arrgAry.push (obj);
    }
   }
  }
  plotGraph();
 }

 function plotGraph(){
  $box.append("<div id='selectedEasingName' style='position:absolute;top:5px;left:15px;'>"+ o.easing +"</div>");
  $log.html("height prop の step 起動回数 :"+cnt+"<br />全ての step 起動回数:" + allsteps);
  for (var i=0, len = arrgAry.length; i<len; i++){
   $("<div class='dot' />").css({top:arrgAry[i].top+"px",left:arrgAry[i].left+"px"}).appendTo($box);
  }
 }

 $box.empty();$log.empty();
 return $box.animate(hideCSS,duration,easing ).animate(showCSS, o);
};})(jQuery);
// ▲ Ver 1 終わり

// drawGraph プラグインを使って実際にグラフを書かせるためのコード
(function($){
 var selDuration=2000,selEasing="swing";
 // アニメ継続時間コンボから 選択された duration を取得
 $("#sel1_728").change(function(){selDuration = +$(this).val();});
 // easing リストから 選択された easing 関数を取得
 $("#sel2_728").change(function(){selEasing = $(this).val();});
 $("#btn1_728").click(function(){$("#box1_728").drawGraph(selDuration,selEasing);});

 $("input").mouseup(function(){$(this).css({backgroundColor:""}).blur();})
  .hover(function(){$(this).css({backgroundColor:"pink"});}
  ,function(){
    $(this).css({backgroundColor:""});
  })
  .mousedown(function(){$(this).css({backgroundColor:"lightgreen"})});
})(jQuery);

▲ToTop

drawGraph() メソッド Ver 2 に関する補足説明

直前起動時に作成したアニメ対象に関する諸情報を出来る限り 2 度目以降に活用する───これが Ver 2 作成の動機であり目的です。『 Javascript 第 5 版 』p.144 を参考にして、メソッドのプロパティを活用する方法を作ったまでは良かったのですが、なかなか順調に動きませんでした。18 行目の stepObj も他のプロパティと同様に初回作成時の値を使うコードを書いたのですが、これが躓きの原因でした。

14~17 の 4 行では、o.duration、o.easing、o.complete の各プロパティの存在を確認し、それがあれば何もしないようなコードを書きました。こうしてコード進行の効率化を図ったのでした。

そしてそれ自体は問題なく動いたので、安心して 18 行も if (o.stepObj) 文を書いてしまったのです。

ところが、こうすると 2 度目以降においては o.stepObj は再定義されないので、初回時の値をそのまま保持し続けてしまいます。その結果 3 行目の cnt = 0 が 2 度目以降に読み込まれ初期化されても、その後の過程で opts プロパティに o オブジェクトを複写・待避させているため、o.stepObj 内の cnt は初期化されず累積されてしまうのです。

そのことが分かるまでに数時間を要したのですが、大きな落とし穴に落ち込んでしまった結果、貴重な経験を重ねることが出来ました。

▲ToTop

jquery.js におけるアニメーションコードの解読 ( 8 )

アニメーションの進行途中の情報を見るサンプル

このエントリイでは前エントリイに続いてサンプルを掲載します。今回のサンプルでは Web サイトでは余り見かけない「 アニメ-ションが進行しているその途中の情報 」を取り上げます。

これまでの 「 jquery.js におけるアニメーションコードの解読 (1) ~ (6) 」 からも分かるとおり、アニメーションが実現されるコードの進行過程は大変複雑で、様々なプロパティとメソッドが駆使されて 「 動き 」 が演出されています。

そしてアニメを扱ったサイトの多くは、アニメそのもの多様性・独自性・ユニークさを競い合っていても、アニメ-ション進行過程における情報を可視化したものは余り見受けません。

そこで、実際にアニメーションを起こしながら、その進行過程における諸情報を取りだすサンプルを作ってみました。

アニメ途中の諸情報とは何か

それはずばり jQuery.fx コンストラクタの e インスタンスが保持している情報です。

e インスタンスは既に見たように模式図的に示せば以下のような盛りだくさんの情報を内包しています。

■ jQuery.fx コンストラクタの e インスタンス
 e = {
 elem:elem,// 第 1 引数
 prop:propName, // 第 3 引数
 options: {
  queue:false || true, step:function(){・・・}, // これらはユーザーが指定する。
  old,complete,easing,duration,display,overflow,// animateメソッドから引き継がれる。
  curAnim, //animateメソッドの第一引数である prop オブジェクトが格納される。
  orig:{prop},
  show:true || false || undefined, hide:true || false || undefined
 },
 startTime, start, end, unit, now, pos, state,
 // メソッド
 cur(), custuom(), step(), update(), show(), hide()
}

■ jQuery.fx コンストラクタのクラスプロパティ
 jQuery.fx.speeds = {"slow":600, "fast":200, _default:400}
 jQuery.fx.step = {opacity, _default}

▲ToTop

「 アニメ進行過程 」における当該アニメの諸情報は、e.step メソッドがアニメ対象要素( 以下 A )のアニメ対象プロパティ( 以下 props )毎に、ミリ秒単位で静止画を繰り返し描画する過程において、刻々と変化します。

諸情報は e インスタンスオブジェクトのプロパティ【 startTime, start, end, unit, now, pos, state 】に、何百回となく上書きが繰り返されます。

このエントリイではこれらの値を取りだしてみます。

もちろんその全てを取りだしていたら数千,数万のデータとなるため、ここではアニメ継続時間 ( duration )の 15%、30%、50%、75%及び 100%の時間が経過した 5 つの時点に絞って、それぞれの経過時点におけるプロパティ値を取り出すこととします。

なお、折角閲覧して戴くので、閲覧者が操作にある程度関われるよう配慮しました。

第 1 に props(アニメ対象プロパティ)を選択できるように、第 2 に、アニメで使用する easing 関数を多数用意し、これも閲覧者が選択できるようにしました。

アニメ途中経過情報を表示するサンプル

以下がサンプルです。ボックスが 6 つ。左から順にアニメ素材としたボックス、アニメ開始後の時間が、設定済みの継続時間の 15 %進行した段階で止まるボックス、以下 30%、50%、75%でそれぞれ停止するボックス、最後が通常通りアニメーションされるボックスです。なお、アニメ継続時間は、進行過程をゆっくり見せるために一律に 4 秒としました。

4 つの進行時間が異なるアニメの諸情報 ( 各 A の props。具体的には開始時刻, 開始値, 終了値, 経過割合, 値の変化度合, 停止値, 経過時間 ) は下の方に配置した 4 つの表内に、アニメが停止したその瞬間に描画されます。

アニメ
素材
15 %
30 %
50 %
75 %
100%

適用 easing 選択

アニメ継続時間の選択 

アニメ対象プロパティの選択

  
  

▲ToTop

途中停止アニメサンプルの使い方
  1. まず、easing 関数、アニメ継続時間(所要時間と言っても良い)およびアニメーションの対象プロパティを選択します。
  2. 「 Do 隠蔽アニメ 」 ボタンをクリックします。

    これで隠蔽アニメーションが始まり、各々のボックスアニメが指定した途中の段階で停止します。同時にその瞬間に、停止時のアニメに係る諸情報が下の表に出力されます。

    この情報によって jquery.js が処理しているアニメ情報の具体的な内容を知ることが出来ます。

  3. 「 最後まで動かす 」 ボタンをクリックします。これで途中で停止していたアニメが再開されて最後まで進行します。最後まで進行するとアニメ素材以外のボックスは全て隠蔽された状態になります。

    なお、アニメ再開時の諸情報は表には出力されません。

  4. 「 Do 表示アニメ 」 ボタンをクリックします。これによって今度は表示アニメーションが始まります。ここでも途中で停止し、その時の情報が表に上書き出力されます。
  5. 最後に再びアニメを再開させます。「 最後まで動かす 」 ボタンをクリックすれば、表示アニメーションが最後まで進行して、一連の 「 停止─再開 」サンプルアニメーションが終了します。 この段階で初期の 6 つのボックス表示状態に戻ります。

言うまでもなく、easing 関数を変え、継続時間を変え、あるいはアニメ対象プロパティを変えて、何回でも途中停止アニメーションサンプルを起動させることが出来ます。

なお、ボタン、セレクトボックス及びチェックボックスは、アニメーション進行中や途中停止状態の時には使えないように disabled 属性を調整すると共に、カバーボックスをアニメートさせ、使用可能/不能が視覚的にも一目瞭然となるように工夫しました。

所定の継続時間(4000ミリ秒に設定)の 15 % 経過した時点の諸情報

sT:開始時刻, sV:開始値, eV:終了値, s:経過割合,pos:変化度合,
nV:停止値, eT:経過時間, nV = sV + ( eV -sV ) * pos

propertysTsVeV sposnVeT
marginTop
marginRight
marginBottom
marginLeft
borderTopWidth
borderRightWidth
borderBottomWidth
borderLeftWidth
paddingTop
paddingRight
paddingBottom
paddingLeft
width
height
opacity
fontSize
所定の継続時間(4000ミリ秒に設定)の 30 % 経過した時点の諸情報

sT:開始時刻, sV:開始値, eV:終了値, s:経過割合,pos:変化度合,
nV:停止値, eT:経過時間, nV = sV + ( eV -sV ) * pos

propertysTsVeV sposnVeT
marginTop
marginRight
marginBottom
marginLeft
borderTopWidth
borderRightWidth
borderBottomWidth
borderLeftWidth
paddingTop
paddingRight
paddingBottom
paddingLeft
width
height
opacity
fontSize
所定の継続時間(4000ミリ秒に設定)の 50 % 経過した時点の諸情報

sT:開始時刻, sV:開始値, eV:終了値, s:経過割合,pos:変化度合,
nV:停止値, eT:経過時間, nV = sV + ( eV -sV ) * pos

propertysTsVeV sposnVeT
marginTop
marginRight
marginBottom
marginLeft
borderTopWidth
borderRightWidth
borderBottomWidth
borderLeftWidth
paddingTop
paddingRight
paddingBottom
paddingLeft
width
height
opacity
fontSize
所定の継続時間(4000ミリ秒に設定)の 75 % 経過した時点の諸情報

sT:開始時刻, sV:開始値, eV:終了値, s:経過割合,pos:変化度合,
nV:停止値, eT:経過時間, nV = sV + ( eV -sV ) * pos

propertysTsVeV sposnVeT
marginTop
marginRight
marginBottom
marginLeft
borderTopWidth
borderRightWidth
borderBottomWidth
borderLeftWidth
paddingTop
paddingRight
paddingBottom
paddingLeft
width
height
opacity
fontSize

▲ToTop

アニメ途中経過情報を表示するサンプルの要点

アニメ-ション進行過程のあらまし(再掲)

jquery.js の animate メソッドはアニメーション動作開始前に e インスタンスを作成し、そのプロパティ上にアニメーションに係る多数の情報を保持し続け、上書きを繰り返してアニメを実現しています。そのことはアニメーションに関するシリーズエントリイの最初( jquery.js におけるアニメーションコードの解読 ( 1 ) )でも触れました。

その e インスタンスには、数種類のメソッド ( cur, custuom, step, update, show, hide ) も形成され、これらのメソッドが有機的に連携して 「 動き 」 を演出します。animate メソッド内でのこれらのメソッド連携を改めて跡付けてみると次のようになっています。

  1. 最初に animate メソッドの第 1 引数内で指定されたアニメーション終了値を処理します。

    その指定値が show、hide または toggle の時には e.show 又は e.hide メソッドが起動されて、その中で必要な処理を行ってから、e.custum メソッドが呼び出されます。

    指定された終了値がそれらの文字列ではない場合には、その値をanimateメソッド内で評価処理してから( この時に現在値を計算するメソッドが e.cur です )、やはり、e.custum メソッドが呼び出されます。

    こうして animate メソッド内の e インスタンスサブルーチンメソッド連鎖は必ず e.custom メソッドから始まります。

  2. e.custum メソッド内ではアニメ開始時刻を記録し、アニメーションの開始値を取得した後に、e.step メソッドを呼び出します。
  3. e.step は呼び出された時刻を記録し、その時点におけるアニメプロパティの現在値を算出します。この時に現在値を計算するメソッドが e.cur です。
  4. その時点の現在値が決まれば後はブラウザ上に描くだけです。それを e.update メソッドが行います。
  5. この過程が何百回、何千回となく繰り返されて 「 動き 」 が描かれます。
アニメ途中で何かを実行させるメソッド: e.options.step

さて、アニメーション実行中にユーザーが出来ることは、ブラウザを停止する、パソコンの電源を切る等の外的作用の他には、(1) アニメが終わるのを待つか、(2) e.options.step メソッドをあらかじめ指定しておき、アニメーション進行途中でそれを実行させるか、この 2 択 しかありません。アニメを途中で停止させる指示を与える場合も、e.options.step メソッド内に $().stop メソッドをあらかじめ仕込んでおくことになります。

ここでは (2) の途中で何かをさせるメソッド e.options.step の指定方法について言及します。

e.options.step の指定について(jquery.js #4018 参照)

このメソッドは e.update メソッドの冒頭で起動されます。アニメ対象プロパティ毎に、数ミリ秒毎に繰り返し実行される e インスタンスメソッドチェーン内において、何百回、何千回と描画が繰り返されますが、その描画直前毎に、e.options.step メソッドが実行されます。

この e.options.step の引数は、アニメプロパティ現在値と e インスタンスの 2 つで、起動元の this.elem と併せて jquery.js #4018 で無条件に指定されます。起動元はアニメーション対象要素タグに指定されますから、e.options.step メソッド内において this キーワードを使った場合、その参照先は対象要素タグとなります。

また、 引数に e インスタンスがあると言うことはアニメに係る全ての情報に、 e.options.step メソッドからアクセスできることを意味しています。この点は極めて重要なポイントです。

以上の実行タイミングと引数を踏まえた約 200 行のコードによって、このエントリイのサンプルを実現しました。具体的な方法は、後に掲載したコードの通りですが、要点を記しておきます。

サンプルコードの要点

ユーザーフレンドリイに!

これまでは簡単に言えば「 面倒なので 」(苦笑)、form 要素の使用を出来るだけ避けてきました。せいぜいボタンを単独で多用してきた程度で、リストボックス、コンボボックス、チェックボックスなどのブラウザ閲覧者が操作し、その結果を反映させられるツールは殆ど使ってきませんでした。

その意味では今回のチャレンジは、step メソッドの使い勝手を試してみる以外に、form 要素を駆使するコーディングに真正面から取り組んだ点が大きな特徴であり、またそれがハードルともなりました。

ここで配置した form 要素の使いやすさについては閲覧者の批判を甘受するしかありませんが、アニメ中に form 要素を変更されなくするための、思いつきで作った二重の処理( disabled 属性の処理と隠蔽カバーをアニメで被せたこと)には、それなりに満足しています。

なお、このカバーアニメーションにおいてもユーザー指定の easing 関数を使うように設計してあります。

途中停止のための動作ステータスキャッチ

options.step メソッドにおけるアニメの途中停止は、動作ステータス ( e.state ) の操作により行いました。

プロパティ値の変動割合で行うことも可能ですが、この値は easing 関数によって増減する動的なものなので、時間経過によって途中の段階を捉える方が確実だと考えました。

e.state は 或る e.step メソッドの 「 呼び出し時迄の経過時間÷アニメ継続時間 」で定義されている、「 経過時間割合 」とでもよぶべきものですが、この値は決して数直線のような連続的な数値にはなりません。

当該要素の当該プロパティ毎に、繰り返し呼び出される e.step の呼び出し時迄の経過時間は必ず一定間隔が空くからです。ちょうど 15 %過ぎた、そのタイミングの時に、偶々 e.step が呼び出されているという保証は全くありません。

このため 15 %前後に一定の幅を持たせた 「 許容域 」を設定しないと、アニメ停止のタイミングが捉えられません。このため、if ( e.state > (( stopPoint - permitter )/100 ) && e.state < (( stopPoint + permitter )/100) ) と言う式によって、一定の許容域に収まるタイミングを捉えて、その瞬間にアニメを停止させるようにしました。stopPoint は 15 %等の値、permitter は許容幅です。

諸情報の表における s 列がその許容域内で補足されたことを示す経過時間割合です。

しかも、許容幅はアニメ継続時間との関係で変動させる必要がありました。余り短時間の継続時間だと e.step の起動回数が少なくなるので、許容幅を広げないと「 その瞬間 」を捉えられないのです。こうして継続時間が長ければ許容幅は小さく、継続時間が短い場合には長くなるよう、動的に変動するようにしました。

4 つの機能を持たせたアニメ起動ボタン

出来るだけ簡単な操作になるよう、隠蔽アニメ起動、隠蔽アニメ停止後の再開、表示アニメ起動、表示アニメ停止後の再開───という一連の操作を全て 1 つのボタンだけで行うようにしました。

その結果コードは複雑にならざるを得ませんでした。4 回のそれぞれの操作の意味が全て異なるため、それぞれ毎に異なるコーディング他必要となるためです。これを click イベントの Toggle メソッド利用と、その中の 2 つの関数の起動回数(奇数回か偶数回か)によって 4 つに分けるようにしました。

IE 対策

今回もまた IE 対策を施さねばなりませんでした。W 3 C 準拠になったはずの IE 8 においてもまだ、準拠が不十分な点が残っているのでしょう。

具体的にはアニメーション対象プロパティとしてフォントサイズを指定した場合の対応です。Firefox の場合には全く問題がなかったのですが、フォントサイズをアニメ対象とした場合において、IE においては初期値のフォントサイズを指定しないと、アニメ前のフォントサイズを勝手に最大サイズ( おそらく96 pt )と解釈してしまうのです。

仕方なく、アニメ領域だけフォントサイズを指定せざるを得ませんでした。

Web サイト作成上、フォントサイズを固定的に指定することは推奨されていません。ユーザーがブラウザの拡大/縮小機能を使った場合に、フォントサイズが固定されているとフォントだけ拡大/縮小されないからです。ですから私の場合フォントサイズ指定は全く行ってこなかったのですが、今回の場合やむを得ず指定せざるを得ませんでした。

アニメ停止後再開時点における、開始値と終了値の操作に最も苦労しました

隠蔽アニメーションは開始時点のプロパティ値( 開始値 )を e.hide メソッド内で取得し、表示アニメは終了時点のプロパティ値( 終了値 )を e.show メソッド内で取得します。そして e.hide メソッドはアニメ対象要素の属性値や style 値を一切変化させません。だからこそ、e.show メソッドの終了値は、その直前に行われた e.hide メソッドの開始値に等しくなります。

ここに、開始値も終了値も共に、e.cur メソッドによって、アニメ対象タグの (1) 属性値や (2) style オブジェクト値、あるいは (3) 算出スタイル値から算出されます。

さて、停止後に再開されるアニメの初期値は、停止時点のプロパティ値となります。そして、停止時のプロパティ値は、既に動いた後なのですから、最初にアニメを起動した時のプロパティ値とは決して同一ではありません。

従って、アニメ対象プロパティを何も変更せず、単純に停止していたアニメを再開させると、再開後の最終値は当初の設定値と異なってしまいます。

例えばここのサンプルで言えば、15%で停止させたアニメを再開して隠蔽させ、その後最後まで表示させると、その表示サイズは 15% で停止した時のサイズであって、決して当初の大きさまでは戻りません。

この問題をどうやって解決するか───この点こそ、今回のコーディングで最も頭を悩ませた箇所でした。

一旦 animate メソッドが起動してしまうと、その途中でアニメ対象プロパティをユーザーが直接変更することは、options.step メソッドを使ってアニメを進行途中で止めたとしても困難です。

何故ならば、アニメ対象プロパティは、既に animate メソッドによって作成済みの e インスタンスオブジェクト内の各プロパティに値が格納されてしまっていて、コードによって e インスタンスを操作することは出来ないからです。

出来ることは、e インスタンスのメソッドが作動する際に、style オブジェクト、要素属性又は算出スタイルから、各プロパティ値を算出することに着目して、要素の style オブジェクトの各プロパティ値や、定義できる属性であれば要素属性値を、次の animate メソッドが始まる前に、強制的に書き換えてしまい、それによって次の animate メソッドの開始値や最終値を前もって変更してしまうことだけです。

このことに気がついた結果、途中停止後に再開したアニメーションも、最初のアニメーションの開始値と終了値を参照するように強制的なコーディングを施して、やっとこのサンプルが完成したのでした。

▲ToTop

 #animArea_725 {
  position: relative; width:620px; margin:1em auto; height:140px;
  background-color:lavender;border:0;font-size:16px;
 }
 #box0_725, #box15_725, #box30_725, #box50_725, #box75_725, #box100_725 {
  position:absolute; top:10px; width:64px; height:84px; padding:3px;margin:5px;
  background-color:darkgreen; border:8px ridge bisque;
 }
 #box0_725 {
  left:10px;
 }
 #box15_725 {
  left:110px;
 }
 #box30_725 {
  left:210px;
 }
 #box50_725 {
  left:310px;
 }
 #box75_725 {
  left:410px;
 }
 #box100_725 {
  left:510px;
 }
 #formBlock {
  position:relative; width:600px;height:220px;margin:1em auto; border:1px dotted aquamarine;
  line-height:1.1em; clear:both;
 }
 #fm_725 {
  position:absolute; z-index:2; width:170px; margin:1em;
 }
 #fm_725 p{
  color:black;background:ivory;text-indent:-0.5em;text-align:center;line-height:1.2em;
  width:170px;
 }
 #fm2_725 {
  position:absolute;left:190px;margin:1em;
  width:345px; padding:10px;color:black;background:ivory;
  height:175px;
 }
 #submit_725 {margin:0.5em 1em 1em; padding:5px;border:2px solid royalblue;text-align:center;}
 #fm1shutter_725 {
  position:relative; z-index:2; top:-176px;
  width:170px; height:178px; background:teal; display:none;
 }
 #fm2shutter1_725 {
  position:absolute; z-index:2; left:6px;top:8px;
  width:350px; height:130px; background:teal; display:none;
 }
 #fm2shutter2_725 {
  position:absolute; z-index:2; left:6px;top:142px;
  width:350px; height:50px; background:teal; display:none;
 }
 input,button {overflow:visible; padding:2px;}
■ Javascript codes
(function(){
 var $fm2 = $("#fm2_725"), $chk = $(".chkbox"), $done = $("#go"),$initAnim = $("#initAnim_725"),
  $box0=$("#box0_725"), $box1=$("#box15_725"), $box2=$("#box30_725"),
  $box3=$("#box50_725"), $box4=$("#box75_725"), $box5=$("#box100_725"),
  $t1=$("#table1_725 tr"), $t2=$("#table2_725 tr"),
  $t3=$("#table3_725 tr"), $t4=$("#table4_725 tr"),
  $shutter1=$("#fm1shutter_725"), $shutter2=$("#fm2shutter1_725"), $shutter3=$("#fm2shutter2_725"),
  selEasing="swing", // 選択された easing 関数を入れる変数。既定値はswingとしておく
  chkList=[], // 選択されたチェックボックスリスト
  animProp=[], animaCSS, boxCSS={},initCSS=[], dur=4000, //既定値4秒;
  permitter=0.9, // パーセント許容幅(既定値0.9)
  odd = [false,false] , // 第 1 関数及び第 2 関数の起動回数が奇数かどうかを odd 配列に記録
  ival, obj={},
  fxAttrs = [
   ["marginTop","marginRight","marginBottom","marginLeft"],
   ["borderTopWidth","borderRightWidth","borderBottomWidth","borderLeftWidth"],["paddingTop","paddingRight","paddingBottom","paddingLeft"]
  ], 
  attrs=[];

 // 全プロパティ名の単純なリスト配列作成
 for (var i=0; i<fxAttrs.length; i++)
  for (var j=0; j<fxAttrs[i].length; j++)
   attrs.push(fxAttrs[i][j]);
 attrs = attrs.concat("width","height","opacity","fontSize");

 // easing リストから 選択された easing 関数を知る
 $("#sel_725").change(function(){
  selEasing = $(this).val();
 });

 // アニメ継続時間コンボボックスから 選択された duration 値を dur 変数に代入する
 $("#fm2_725 select").change(function(){
  dur = +$(this).val();
  permitter = (dur >2000) ? 0.9 : (dur > 800) ? 3 : 4;// 許容値を継続時間毎に変化させる
 });

 // チェックボックスの既定値選択設定
 $("#defaultSelect_725").click(function(){
  $("#fm2_725 input:checkbox").val(
   ["margin","padding","width","height","opacity"]
  );
 });
 // チェックボックス全選択設定
 $("#allSelect_725").click(function(){
  $("#fm2_725 input:checkbox").val(
   ["margin","border","padding","width","height","opacity","fontSize"]
  );
 });

 // アニメ開始前の全てのCSSプロパティ値を initCSS 配列に記録する
 $.each([$box1,$box2,$box3,$box4],function(){
  for (var j=0; j<attrs.length; j++)
   boxCSS[ attrs[j] ] = this.css( attrs[j] );
  initCSS.push(boxCSS);
 });

 // アニメ用のCSSオブジェクト( margin、borderWidth、padding )を作成するmeta関数
 var makeCSS =function(i,type){
  $.each( fxAttrs[i],function(){obj[this]=type;} ); return obj;
 }

 // チェックボックス情報を踏まえたアニメ用の CSS オブジェクト作成
 var make_animObj = function(type){
  var retObj={}, mStr = chkList.join();
  if (mStr.match("margin")) retObj = $.extend(retObj,makeCSS(0,type));
  if (mStr.match("border")) retObj = $.extend(retObj,makeCSS(1,type));
  if (mStr.match("paddin")) retObj = $.extend(retObj,makeCSS(2,type));
  if (mStr.match("width")) retObj = $.extend(retObj,{width:type});
  if (mStr.match("height")) retObj = $.extend(retObj,{height:type});
  if (mStr.match("opacity")) retObj = $.extend(retObj,{opacity:type});
  if (mStr.match("fontSize")) retObj = $.extend(retObj,{fontSize:type});
  for (var p in retObj) animProp.push(p); // アニメ対象プロパティリストを animProp 配列に格納
  return retObj;
 }

 // 一時停止時の情報を table に書き込む
 function printOut(o,box){
  var startTime =o.startTime.toString();
  var output=[
   startTime.substring(startTime.length-5,startTime.length), o.start, 
   Math.round(o.end*100)/100,
   Math.round(o.state*10000)/100+"%", Math.round(o.pos*10000)/100+"%",
   Math.round(o.now*10000)/10000, parseInt(dur * o.state)
  ];
  for (var i=0;i<16;i++){
   if (attrs[i]==o.prop ){
    for (var j=0; j<7; j++)
     box===$box1 ? $t1.eq(i+1).children().eq(j+1).text(output[j]) :
     box===$box2 ? $t2.eq(i+1).children().eq(j+1).text(output[j]) :
     box===$box3 ? $t3.eq(i+1).children().eq(j+1).text(output[j]) :
     box===$box4 ? $t4.eq(i+1).children().eq(j+1).text(output[j]) : null;
   }
  }
 }
 // goAnim メソッドから呼び出されて一時停止時の情報を取得する
 function doStep(box,obj,stopPoint){
  box.animate(obj,{
   step:function(){
    if (stopPoint==100) return;
    var e = arguments[1];
    if (e.state > ((stopPoint-permitter)/100) && e.state < ((stopPoint+permitter)/100)){
     var o = $.extend({},e);
     if (o.prop === animProp[animProp.length-1]) {
      box.stop();
     }
     printOut.call(null,o,box);
    }
   },
   duration:dur, easing:selEasing
  });
 }
 // type="toggle" で show/hide を循環させる
 function goAnima(type){
  var stopTime =[];
  stopTime =[15,30,50,75,100];
  animaCSS = make_animObj(type);
  $.each([$box1,$box2,$box3,$box4,$box5],function(i){
   doStep(this,animaCSS,stopTime[i]);
  });
 }

 // アニメと対象画像の初期化
 $initAnim.click(function(){
  $.each([$box1,$box2,$box3,$box4],function(i){
   this.stop(true,true);
   for (var j=0; j<attrs.length; j++){
    $.attr(this.get(0).style, attrs[j] ,initCSS[i][attrs[j]]);
   }
  });
  $(this).blur();
 });

  // select オプション・チェックボックス・コンボボックスの使用不能/可能を切り替える
 function elementsDisabled(type){
  $("#fm_725 option").attr("disabled",type).get(0).disabled="disabled"; // easingリストボックス
  $("#fm2_725 option").attr("disabled",type); // duration コンボボックス
  $("#fm2_725 input:checkbox").attr("disabled",type); // プロプ選択ボックス
  $("#defaultSelect_725").attr("disabled",type); // 既定選択ボタン
  $("#allSelect_725").attr("disabled",type); // 全選択ボタン
 }

  // チェックボックスのオンオフ状態をチェックし、
  // チェックボックス・コンボボックス・セレクトリストを使用不能とする
  function checkChkbox(){
  chkList=[]; // 必ず初期化する必要がある
  $("#fm2_725 input:checkbox:checked").map(function(){
   chkList.push( this.value );
  }); // オンされたチェックボックスリストを配列 chkList に格納
  if (!chkList.length) { // No check 時には警告を発し、アニメを起動させない。
   alert("アニメ対象プロパティを何か 1 つ以上指定してください");
   return;
  }
  elementsDisabled("disabled"); //全て使用不可とする
 }

 // 囲み内ボタンの使用可確認とフォーム要素のカバー不透明度設定
 $done.attr("disabled","");
 $initAnim.attr("disabled","");
 elementsDisabled(""); // 起動時には form elements を全て使用可能とする
 $.each([$shutter1,$shutter2,$shutter3],function(){
  if (this.css("opacity")==1) this.css("opacity","0.5");
 });
 // 2 つの form 内の、2 つのボタンの使用可/不可状態の設定とシャッターアニメの起動設定
 function setForm(type,duration,easing){
  $done.attr("disabled",type);
  $initAnim.attr("disabled",type);
  var disp = (type==="disabled") ? "show" : "hide";
  if ( disp=="show" && odd[0] !== odd[1] || disp=="hide" && !odd[0] === !odd[1] ){
    $shutter1.animate({width:disp,height:disp},duration,easing);
    $shutter2.animate({width:disp,height:disp},duration,easing);
  }
  $shutter3.animate({width:disp,height:disp},duration,easing);
 }

 // アニメが動作中か否かを確認し、状態に応じて form 要素の使用不可/可を切り替える
 function isbusy(){
  ival =setInterval(function(){
   if ( $box1.is(":animated") || $box2.is(":animated") || $box3.is(":animated") || $box4.is(":animated") ) {
    setForm("disabled","slow",selEasing);
   } else {
    setForm("","slow",selEasing);
    clearInterval(ival);
    ival=undefined;
   }
  },20);
 }

 $done.toggle(function(){ // click toggle
  odd[0]=!odd[0]; // 奇数か偶数かを反転させる
  isbusy();
  checkChkbox();
  goAnima("toggle");
  $done.attr("value","最後まで動かす");
  $(this).blur();
 },function(){ // 奇数回目はe.hide 、偶数回目はe.showメソッドとなる
  if (isbusy()) return;
  odd[1] = !odd[1]; // 奇数か偶数かを反転させる
  for (var p in animaCSS) animaCSS[p] = odd[1] ? "hide" : null;
  $.each([$box1,$box2,$box3,$box4],function(i,n){
   if (!odd[1]) { // 偶数回目つまり e.show メソッドの2度目の起動前に
    for (var p in animaCSS) // 終了値を強制的にアニメ開始値とする
     animaCSS[p] = initCSS[i][p];
   }
   this.animate(animaCSS,dur,selEasing,function(){ // complete funcを指定
    for (var j=0; j<attrs.length; j++) // CSS style オブジェクトの各値を初期値に戻す。
     $.attr(n.get(0).style, attrs[j] ,initCSS[i][attrs[j]]);
    if (i==3) { // 75%ボックスが最後まで表示されたら
     $done.attr("value",odd[1] ? "Do 表示アニメ" : "Do 隠蔽アニメ");
     elementsDisabled(""); //全て使用可とする
    }
   });
  });
  $(this).blur();
 });
})();

jquery.js におけるアニメーションコードの解読 ( 7 )

ボックスアニメーションのサンプル

このエントリイでは jquery.js におけるアニメーションコードのこれまでの解読を踏まえて、一寸一息いれることとします。アニメサンプルを多数用意して、その解説を試みます。

ここでいう、ボックスアニメーションとは私が勝手に名付けた名称ですが、配置指定された div 要素のアニメーションを意味しています。

個々のボックスのアニメーションを 12 種類、その他これらのボックスを利用したアニメーションを 6 種類、計 18 種類のアニメを作成し、その挙動に関する解説を行い、末尾には作成したスクリプトを付けました。

18 種類のアニメーションサンプル

下のボタンをクリックすると隠蔽/表示アニメーションが起動します。 各々のボタンには、隠蔽と表示の 2 機能を持たせました。

ぼっくす 1
toggle()
ぼっくす 2
toggle(2000)
ぼっくす 3
起終点:中心
ぼっくす 4
起終点:右上
ぼっくす 5
起終点:右下
ぼっくす 6
起終点:左下
ぼっくす 7
起終線:上
(文字も変化)
ぼっくす 8
起終線:左
(文字無変化)
ぼっくす 9
起終点:左上(文字も変化)
ぼっくす 10 width だけ toggle
ぼっくす 11 borderWidth だけ toggle
ぼっくす 12 borderWidthをAnim 

▲ToTop

18 のサンプルアニメーションの解説

1 : $().toggle() の挙動

jQuery インスタンスの toggle メソッドの例です。引数なしでこのメソッドを起動すると、$(htis).is(":hidden") メソッドによって当該要素の隠蔽/表示状態をチェックします。

その結果に応じて、もし true ならば $(this).hide() が、あるいは false ならば $(this).show() がそれぞれ起動され、それぞれのメソッドによって、this.style.display.none と this.style.display.block の切り替えが行われ、隠蔽と表示がアニメートされます。「時間」に係るコードは全く含まれないので、瞬時に行われる単純なアニメとなります。

2 : $().toggle(2000) の挙動

1 と同じ jQuery インスタンスの toggle メソッドの例ですが、動作時間を指定した引数がある点が異なります。

この引数があると toggle メソッドの定義から animate(genFx("toggle",3),"slow") メソッドが起動され、この genFx("toggle",3) 関数実行によって margin、padding 及び content サイズ( height と width )並びに opacity の 4 つの( 正確には margin と padding にはそれぞれ左右上下があるので、2×4+3 で 11種類の )プロパティがアニメーション対象となります。

ここで興味深いことは border がアニメ対象になっていないということです。当然のことですが、color プロパティと違って、border は jquery.js のアニメに馴染まない訳ではありません。実際 borderWidth を問題なくアニメ対象プロパティとして扱えることは、上のサンプル 11&12 で示した通りです。

※ 実は color をアニメ対象とする jquery 拡張版 ui があります。effects.core.js の中に含まれています。

そもそも、border には幅の他に、値が数値ではない色とスタイルの属性があります。そのため border そのものはアニメ対象プロパティから除外したのかもしれません。borderWidth だけでもアニメ対象プロパティにしてもおかしくないはずなのですが、fxAttrs の対象にしなかったのはどうしてでしょうか。理由は判然としません。

さて、上の 11 のアニメ対象プロパティを対象として、animate メソッドの沢山のコードが進行します。animate メソッドの第 1 引数に genFx("toggle",3) が指定されたことにより、jQuery.fx のインスタンス e から、e.hide() や e.show() メソッドが起動され、そこから e.step() メソッドが起動されます。

こうして、11 のプロパティの値が、当初の表示状態値と 1 ( e.show() メソッドにおいて width と height プロパティを扱う場合 ) 又は 0 ( その他のプロパティの場合 )の間を漸減/漸増させられて、アニメーションが引き起こされます。

最後に、$().toggle(2000) メソッドが、アニメ対象要素の左上に収斂し、その左上から展開されることに留意する必要があります。何故ならば、e.hide() や e.show() メソッドは、margin、padding、width、height を 0 に( e.show() メソッドの場合 width と height は 1 に )遷移させるのですから、左上に収斂しそこから展開することになるのです。

更に、これらのメソッドは不透明度も 0 と 1 の間を漸減/漸増させるため、不透明と透明の間を行き来することになります。

ここで触れたアニメの終点/起点の位置指定こそ、animate メソッドを扱う上での最も重要なポイントです。

▲ToTop

3 : ボックス中心を起終点とするアニメの挙動

ボックス 2 では左上がアニメの起終点でした。当然その起終点位置を変えてみたくなります。

「 $().toggle メソッドを使う前提で、どうすれば起終点位置を変えられるか? 」───かなり悩みました。toggle メソッドは e.show() や e.hide() メソッドを呼び出しますが、その中でプロパティ値は 0 又は 1 に変更されます。つまり起終点は左上です。そして toggle メソッドの定義から、何らかの引数により位置指定を変えることは不可能です。結局、位置指定は toggle メソッドでは行えません。このメソッドでは必ず左上に収斂し、そこから展開するアニメしか演出できません。

そしてさんざん考え抜いた結果ついに...。
「 位置指定を別のメソッドで行い、かつそれを toggle メソッドと並行起動すればよい」───このことに気がつきました。

起終点位置だけを指定するアニメーションメソッドは、基本的に top、left の初期値と終了値を適切に設定することになります。(アニメ対象要素は一般に position 指定を「絶対」か「相対」に指定しますので top 及び left が指定できます。)

具体的な対応方法はこのエントリイ最下部に掲載したコードを見て戴くとして、苦労した点を記録しておきます。

ボタンクリックでアニメを励起させていますが、そのボタンはボックスの表示アニメにも、あるいは隠蔽アニメにも、両刀使いするように設計してあります。

そして、初期状態がボックスの表示となっているのですから、toggle メソッドの別の使い方( click の度に 2 つの関数を交互に起動する toggle メソッド )を利用する方法もあります。しかしここでは、もっと一般的な方法を探りました。

ここで提示したアニメサンプルの場合、収斂/展開するアニメーションの起終点(線)を同一にしてあります。それが自然だからですが、同一の点や線から始まり/終わるアニメの場合、その初期値と終了値は正負の符号が異なる同一の絶対値となります。そこで、ボタンのクリック回数が奇数回か偶数回かによって、外サイズ取得値の正負を反転させて、起終点( あるいは起終線 )位置とするようコーディングしました。

▲ToTop

4 : ボックス右上を起終点とするアニメの挙動

今度はボックスの右上を起終点とするアニメです。style.top ±= 0 及び style.left ±= box 外幅から、起終点値を出しました。

隠蔽/表示のトグル起動は 3 と同様の奇数回/偶数回による方法で対処しました。以下 5、6 共同様。

5 : ボックス右下を起終点とするアニメの挙動

ボックスの右下を起終点とするアニメです。style.top ±= box 外高さ及び style.left ±= box 外幅から、起終点値を出しました。

6 : ボックス左下を起終点とするアニメの挙動

ボックスの左下を起終点とするアニメです。style.top ±= ボックス外高さ 及び style.left ±= 0 により起終点値を出しました。

7 : ボックス上辺を起終線とするアニメの挙動

ここからアニメの趣が変わります。これまでは全て $().toggle メソッドを利用してきました。起終点位置を ( 0, 0 ) 以外とするために animate メソッドを併行起動させた場合も、全て toggle メソッドとの併行起動でした。

ところが 7、8、10 ~ 12 番目のサンプルは toggle メソッドを使っていません。但し、12 番目のサンプル以外はアニメ用 CSS オブジェクトのプロパティ値を "toggle" としていますので、プロパティ値を "toggle" とした場合の animate メソッドのコーディングによって、不透明度はアニメ対象になりませんが、縮小するアニメの場合には必ず最後に消滅し、展開する場合には最初に表示されます。

さて、7 番目のサンプルでは、縦方向のサイズに係るプロパティだけ( height、marginTop、marginBottom、paddingTop 及び paddingBottom )をアニメ対象とした上で、更に文字サイズもアニメ対象に追加してみました。

起こされるアニメは slideUp メソッドに酷似していますが、slideUp メソッドは使用せず、fxAttrs 配列を拡張した exAttrs と、genFx と全く同一な Fx メソッドを使用してアニメートさせました。
( ここに独自に定義した配列とメソッドは、共に無名関数内のローカル変数としました。)

▲ToTop

8 : ボックス左辺を起終線とするアニメの挙動

7 の変形でボックス左辺に畳み込み、そこから拡幅するメソッドです。文字サイズはアニメ対象としていません。アニメ用プロパティ値に "toggle" を指定しているので、縮小後に隠蔽され( display.none )、展開直前に表示が再開( display.block )されます。

9 : ボックス左上を起終点とし文字サイズも変えるアニメの挙動

このアニメも exAttrs と Fn を活用して極めて短いコーディングで作りました。ボックス 2 の挙動に文字の変化を加えたものです。

10 : width だけをアニメ対象プロパティとした挙動

アニメプロパティとして width だけを対象と、animate メソッドの第 1 引数であるアニメ用 CSS プロパティ値を "toggle" としました。従って、不透明度はアニメ対象とはなりませんが、縮小後に隠蔽され、展開直前に表示が再開されます。

11 : borderWidth をアニメ対象とした挙動

jquery.js では borderWidth を標準的なアニメ操作対象としていないので、敢えてそれを対象としたアニメを作ってみました。

ここでも、animate メソッドの第 1 引数であるアニメ用 CSS プロパティ値を "toggle" としたので、不透明度はアニメ対象とはなりませんが、外枠線が細くなっていって最終的にボックスは hide され、他方、枠線が太くなる前にボックスが show されます。

12 : borderWidth だけを animate させ、かつ step メソッドを利用した挙動

最後のサンプルはボーダーサイズだけを animete メソッドの対象とし、かつ、toggle 値を使っていないので、border 幅がゼロに縮小されても決してボックスは隠蔽されません。

また、animate メソッドの第 2 引数 speed をオブジェクトとして指定し、このサンプルだけ唯一 e.options.step メソッドを利用して、アニメーションの起動回数をカウントさせました。こうして 12 のサンプルの中で最もカスタマイズ度の高いものになりました。

アニメーションを起動すると、起動回数がボックス内文字列の最後に追加され、ストップウォッチのようにカウンタ数が変化します。

この animate メソッドではアニメ対象プロパティを 4 つ指定していますので、各プロパティ毎の起動回数は、カウンタ値を 4 で割れば算出できます。

なお、アニメーションの起動回数は、duration 指定時間、CPU の性能、メモリの空き具合、ブラウザの Javascript 解析/実行性能等に左右されますから、同じ duration 指定であってもブラウザとパソコンの性能毎に異なることになります。

▲ToTop

13 : 全てのアニメを一斉に起動する挙動

12 番目までのサンプルを一斉にアニメーションさせたいと思い、それを実行しました。

スクリプトは、ボタン click メソッドの引数において、各アニメ起動関数を対象として each メソッドで巡回処理しているだけです。

14 : 全てのアニメを順次起動する挙動

12 番目までのサンプルを一斉にアニメーションさせた次には、一斉ではなく順次起動させることもおもしろい、と思い立ちそれを実行しました。

スクリプトでは、ボタン click メソッドの引数における 12 番目までのアニメ起動関数を、setTimeout 関数を使って一定間隔で遅延させながら巡回処理しました。

15: ランダムに 2 つのボックスを選択させて入れ替える挙動

一斉及び順次起動するアニメーションの次には、乱数関数を使って任意に 2 つのボックスを選択させ、それらの位置を互いに入れ替えるアニメーションを考えました。2 つのボックスの top 値と left 値を相互に入れ替えるだけなので決して複雑なコーディングは要りません。

なお、ここでは 弾性的動きを引き起こす easing 関数を採用してみました。この結果「イヤイヤしながら仕方なく移動する」ような動きとなりました(爆爆WWW)

16: 6 つのボックスを同時に入れ替える挙動

2 つのボックス入れ替えをやってみたら、もっと多くのボックスを同時に入れ替えてみたいと思い、それを実現しました。

注意した点は、必ず 異なる 6 つのボックスを選択させなければならない、ということです。1 ~ 12 迄の乱数を乱数関数で発生させ、そのうちの 6 個を使うわけですから、関数からの返値が同じ値になる可能性は非常に高いわけです。

大変冗長なコードになりましたが、返値について 1 つずつ同じかどうかチェックし、同じ場合には乱数発生をやり直して、必ず 6 つの値が異なるようにしました。

なお、折角の機会なので easing 関数は全て異なるものを使ってみました。

17: ボックス位置交換をタイムラグを持たせて行う挙動

更に、ボックス入れ替えを同時にではなく、タイムラグを持たせて実行してみたい、と思いそれを実現しました。

タイムラグは setTimeout 関数で animate メソッドの起動を遅延させて実現しました。

18: ボックスの並び順を整列する挙動

12 個全てのボックスを瞬時に番号順に整列します。ボックスの番号を頼りに top 値と left 値を算出する代数式を考え、コーディングして全ボックスを瞬時に整列するようにしました。入れ替わった箇所だけを元に戻すのは大変なので、簡便で安易な方法を採用しました(^^;。

上の 18 アニメサンプルに関するスタイルシートとスクリプト

■ スタイル設定
 #animArea {
  width:485px; height:440px;margin-top:1em;
  position:relative; background:lightcyan;
  padding:10px;
  text-align:center;
  line-height:1.2em;
 }
 button.btn {margin:5px; overflow:visible; padding:2px;}
 #box1_724, #box2_724, #box3_724, #box4_724, #box5_724, #box6_724,
 #box7_724, #box8_724, #box9_724, #box10_724, #box11_724, #box12_724 {
  position:absolute;left:10px;
  margin:10px 10px 10px 10px;
  border:palegoldenrod 10px ridge;
  padding:5px;
  width:110px; height:60px;
  background:royalblue;
 }
 #box4_724, #box5_724, #box6_724 {
  top:120px;
 }
 #box7_724, #box8_724, #box9_724 {
  top:230px;
 }
 #box10_724, #box11_724, #box12_724 {
  top:340px;
 }
 #box2_724, #box5_724, #box8_724, #box11_724 {
  left:170px;
  border:lightblue 10px outset;
 }
 #box3_724, #box6_724, #box9_724, #box12_724 {
  left:330px;
  border:plum 10px inset;
 }
■Javascript
(function(){
 var exAttrs = [
  // height animations
  [ "height", "marginTop", "marginBottom", "paddingTop", 
   "paddingBottom", "borderTopWidth","borderBottomWidth" ],
  // width animations
  [ "width", "marginLeft", "marginRight", "paddingLeft",
   "paddingRight", "borderLeftWidth","borderRightWidth" ],
  // opacity animations
  [ "opacity" ],
  // fontsize animations
  [ "fontSize" ]
 ];
 function Fn( type, num ){
  var obj = {};
  jQuery.each( exAttrs.concat.apply([], exAttrs.slice(0,num)), function(){
   obj[ this ] = type;
  });
  return obj;
 }
 var i=1,j=13,$btn=[],$box=[],hidden=[],even=[],w=[],h=[],flag=[],duration=2000;
 for (; i<j; i++) {
  $btn[i] = $("#btn" + i +"_724");
  $box[i] = $("#box" + i + "_724");
  hidden[i] = $box[i].is(":hidden");
  w[i] = !hidden[i] && $box[i].outerWidth() || 0;
  h[i] = !hidden[i] && $box[i].outerHeight() || 0;
  flag[i]=0;
 }
 for (var i=13; i<19; i++) {
  $btn[i] = $("#btn" + i +"_724");
 }
 $("button.btn").hover(
 	function(){$(this).css("background-color","paleturquoise")},
 	function(){$(this).css("background-color","")}
 ).toggle(
  function(){$(this).css("background-color","thistle")},
  function(){$(this).css("background-color","palegreen")}
 );

 if(!$box[12].is(":hidden")) {
  var bordLeft =  $box[12].css("border-left-width"),
  bordRight = $box[12].css("border-right-width"),
  bordTop = $box[12].css("border-top-width"),
  bordBottom = $box[12].css("border-bottom-width");
 }
 $box[12].append("<span/>");

 function anim1(){$box[1].toggle();}
 function anim2(){$box[2].toggle(duration);}
 function anim3(){
  var even = ++flag[2] % 2 === 0,
   leftAnim=( ( hidden[3] + even ) ? "-=": "+=" ) + w[3]/2 +"px",
   topAnim=( ( hidden[3] + even ) ? "-=" : "+=" ) + h[3]/2 +"px";
  $box[3].toggle({queue:false,duration:duration})
   .animate( {left:leftAnim, top:topAnim},duration );
 }
 function anim4(){
  var even = ++flag[3] % 2 === 0,
   leftAnim=( ( hidden[4] + even ) ? "-=": "+=" ) + w[4] +"px";
  $box[4]
   .animate( {left:leftAnim},{queue:false,duration:duration} )
   .toggle(duration);
 }
 function anim5(){
  var even = ++flag[5] % 2 === 0,
   leftAnim=( ( hidden[5] + even ) ? "-=": "+=" ) + w[5] +"px",
   topAnim=( ( hidden[5] + even ) ? "-=" : "+=" ) + h[5] +"px";
  $box[5]
   .animate( {left:leftAnim, top:topAnim},{queue:false,duration:duration} )
   .toggle(duration);
 }
 function anim6(){
  var even = ++flag[6] % 2 === 0,
   topAnim=( ( hidden[6] + even ) ? "-=" : "+=" ) + h[6] +"px";
  $box[6]
   .animate( {top:topAnim},{queue:false,duration:duration} )
   .toggle(duration);
 }
 function anim7(){$box[7].animate(
  $.extend(Fn("toggle",1),{fontSize:"toggle"}),duration
 )}
 function anim8(){$box[8].animate({
  width:"toggle",marginLeft:"toggle",marginRight:"toggle",
  paddigLeft:"toggle",paddingRight:"toggle",borderLeftWidth:"toggle",
  borderRightWidth:"toggle"},duration
 )}
 function anim9(){$box[9].animate(Fn("toggle",4),duration);}
 function anim10(){$box[10].animate({width:"toggle"},duration)}
 function anim11(){$box[11].animate({
  borderLeftWidth:"toggle", borderRightWidth:"toggle",
  borderTopWidth:"toggle", borderBottomWidth:"toggle"
 },duration)}
 function anim12(){
  var k=0, even = ++flag[12] % 2 === 0,
  leftAnim = (( hidden[12] + even ) ? "+=": "-=") + bordLeft;
  rightAnim = (( hidden[12] + even ) ? "+=": "-=") + bordRight;
  topAnim = (( hidden[12] + even ) ? "+=": "-=") + bordTop;
  bottomAnim = (( hidden[12] + even ) ? "+=": "-=") + bordBottom ;
  $box[12].animate({
    borderLeftWidth :leftAnim, borderTopWidth:topAnim, 
    borderRightWidth:rightAnim, borderBottomWidth:bottomAnim
   },{
	  duration:duration, step : function(){k++; $(this).children().text(k);}
   }
  );
 }

 var animList =[anim1,anim2,anim3,anim4,anim5,anim6,anim7,anim8,anim9,anim10,anim11,anim12];
 $.each(animList,function(i,n){ $btn[i+1].click(n) });
 $btn[13].click(function(){$.each(animList,function(i,n){n()})});
 $btn[14].click(function(){
  $.each(animList,function(i,n){setTimeout(n,400*i)})
 });
 // 乱数を使って 2 つのボックスの位置を交換する
 $btn[15].click(function(){
  var A = parseInt(Math.random()*12)+1,
    B = parseInt(Math.random()*12)+1,posA={},posB={};
  while ( A===B ){
    B = parseInt(Math.random()*12)+1;
    if (A!==B) break;
  }
  var $boxA=$("#box"+A+"_724"),$boxB=$("#box"+B+"_724");
  $boxA.animate({top:$boxB.css("top"),left:$boxB.css("left")},duration,"easeInOutBounce");
  $boxB.animate({top:$boxA.css("top"),left:$boxA.css("left")},duration,"easeInOutElastic");
 });
 // 乱数を使って 6 つのボックス位置を一瞬で入れ替える
 $btn[16].click(function(){
  var A = parseInt(Math.random()*12)+1,
   B = parseInt(Math.random()*12)+1,
   C = parseInt(Math.random()*12)+1,
   D = parseInt(Math.random()*12)+1,
   E = parseInt(Math.random()*12)+1,
   F = parseInt(Math.random()*12)+1,
   posA={},posB={},posC={},posD={},posE={},posF={};
  while ( A===B ){
   B = parseInt(Math.random()*12)+1;
   if (A!==B) break;
  }
  while ( C===A || C===B){
   C = parseInt(Math.random()*12)+1;
   if (C!==A &&C!==B) break;
  }
  while ( D===A || D==B || D===C){
   D = parseInt(Math.random()*12)+1;
   if (D!==A && D!==B && D!==C) break;
  }
  while ( E===A || E==B || E===C || E===D){
   E = parseInt(Math.random()*12)+1;
   if (E!==A && E!==B && E!==C && E!==D) break;
  }
  while ( F===A || F==B || F===C || F===D || F===E){
   F = parseInt(Math.random()*12)+1;
   if (F!==A && F!==B && F!==C && F!==D && F!==E) break;
  }
  var $boxA=$("#box"+A+"_724"), $boxB=$("#box"+B+"_724"),
   $boxC=$("#box"+C+"_724"), $boxD=$("#box"+D+"_724"),
   $boxE=$("#box"+E+"_724"), $boxF=$("#box"+F+"_724");
  $boxA.animate({top:$boxB.css("top"),left:$boxB.css("left")},duration,"easeInOutBounce");
  $boxB.animate({top:$boxA.css("top"),left:$boxA.css("left")},duration,"easeInOutElastic");
  $boxC.animate({top:$boxD.css("top"),left:$boxD.css("left")},duration,"easeInOutExpo");
  $boxD.animate({top:$boxC.css("top"),left:$boxC.css("left")},duration,"easeInOutQuart");
  $boxE.animate({top:$boxF.css("top"),left:$boxF.css("left")},duration,"easeInOutCirc");
  $boxF.animate({top:$boxE.css("top"),left:$boxE.css("left")},duration,"easeInOutBack");
 });

 // ボックス位置交換をタイムラグを持たせて行う
 $btn[17].click(function(){
  var A = parseInt(Math.random()*12)+1,
    B = parseInt(Math.random()*12)+1,
    C = parseInt(Math.random()*12)+1,
    D = parseInt(Math.random()*12)+1,
    E = parseInt(Math.random()*12)+1,
    F = parseInt(Math.random()*12)+1,
    posA={},posB={},posC={},posD={},posE={},posF={},delay=800;
  while ( A===B ){
   B = parseInt(Math.random()*12)+1;
   if (A!==B) break;
  }
  while ( C===A || C===B){
   C = parseInt(Math.random()*12)+1;
   if (C!==A &&C!==B) break;
  }
  while ( D===A || D==B || D===C){
   D = parseInt(Math.random()*12)+1;
   if (D!==A && D!==B && D!==C) break;
  }
  while ( E===A || E==B || E===C || E===D){
   E = parseInt(Math.random()*12)+1;
   if (E!==A && E!==B && E!==C && E!==D) break;
  }
  while ( F===A || F==B || F===C || F===D || F===E){
   F = parseInt(Math.random()*12)+1;
   if (F!==A && F!==B && F!==C && F!==D && F!==E) break;
  }
  var $boxA=$("#box"+A+"_724"), $boxB=$("#box"+B+"_724"),
    $boxC=$("#box"+C+"_724"), $boxD=$("#box"+D+"_724"),
    $boxE=$("#box"+E+"_724"), $boxF=$("#box"+F+"_724");
  $boxA.animate({top:$boxB.css("top"),left:$boxB.css("left")},duration,"easeInOutBounce");
  $boxB.animate({top:$boxA.css("top"),left:$boxA.css("left")},duration,"easeInOutElastic");
  setTimeout(function(){
   $boxC.animate({top:$boxD.css("top"),left:$boxD.css("left")},duration,"easeInOutExpo");
   $boxD.animate({top:$boxC.css("top"),left:$boxC.css("left")},duration,"easeInOutQuart");
  },delay);
  setTimeout(function(){
   $boxE.animate({top:$boxF.css("top"),left:$boxF.css("left")},duration,"easeInOutCirc");
   $boxF.animate({top:$boxE.css("top"),left:$boxE.css("left")},duration,"easeInOutBack");
  },delay*2);
 });

 // ボックスを整列する
 $btn[18].click(function(){
  for (var i=1; i<13; i++){
   $box[i].css({top:110*parseInt((i-1)/3)+10 +"px",left:(i-1)%3*160+10 +"px"});
  }
 });
})();

▲ToTop

jQuery() の挙動を解読する(29) jQuery で DOM エレメントの CSS 値を取得する upon ver1.3.2──jQuery解読(43)

jquery.js は jQuery.css() や jQuery.curCSS() クラスメソッドで、スタイル宣言されていない CSS スタイル値や未描画要素の CSS スタイル値を取得する

CSS 値はそれが明示的に style 宣言されている場合には elem.style.styleName で容易に所得出来ます。しかし明示的に宣言されていない限り、要素が隠蔽( display : none )されていたり、ブラウザによる描画前の時点では、当該要素の幅や高さは取得できません。

さて、jQuery サンプルで多用される 1 つのメソッドに jQuery(x).css(y) インスタンスメソッドがあります。これは jQuery(x) によって Web サイト内から抽出した DOM Nodes を対象として、y で指定された様々な CSS スタイルを設定したり、宣言されているスタイル値を取得する汎用的なメソッドです。

これに対して jQuery.css( elem, name, ・・・) は elem DOM Node の name スタイル属性値を取得するためのクラスメソッドで、このメソッドの前後に定義されている swap() メソッド(#734-#748)や curCSS() メソッド(#781-#845)と連携して、明示的にスタイルが宣言されていない場合や要素が隠蔽されている場合も含めて、スタイル値を取得します。

このエントリイでは jquery.js がどのように CSS 値の取得を行うのかを解読しようと思います。

なお、以下に登場するボックスモデルに係る名称は下図に拠ります。

ボックスモデルの寸法説明図

呼称は全て日本語化し、マージン辺、ボーダー辺、パディング辺、内容辺とします。

jQuery.css() クラスメソッド解読

■ jQuery.css() クラスメソッド ■
750: css: function( elem, name, force, extra ) {
    // 引数は順に、タグ要素、スタイル名称。force は算出スタイル値を求めか否か、
    // extra は内容辺、ボーダー辺またはマージン辺のどこまでのスタイル値を求めるか
751:  if ( name == "width" || name == "height" ) {
752:   var val, props = { position: "absolute", visibility: "hidden", display:"block" },
       which = name == "width" ? [ "Left", "Right" ] : [ "Top", "Bottom" ];
753:
754:   function getWH() {
755:    val = name == "width" ? elem.offsetWidth : elem.offsetHeight;
756:
757:    if ( extra === "border" )
758:     return;
759:
760:    jQuery.each( which, function() {
761:     if ( !extra )
762:      val -= parseFloat(jQuery.curCSS( elem, "padding" + this, true)) || 0;
763:     if ( extra === "margin" )
764:      val += parseFloat(jQuery.curCSS( elem, "margin" + this, true)) || 0;
765:     else
766:      val -= parseFloat(jQuery.curCSS( elem, "border" + this + "Width", true)) || 0;
767:    });
768:   }
769:
770:   if ( elem.offsetWidth !== 0 )
771:    getWH();
772:   else
773:    jQuery.swap( elem, props, getWH );
774:
775:   return Math.max(0, Math.round(val));
776:  }
777:
778:  return jQuery.curCSS( elem, name, force );
779: },

jQuery.css() メソッドは次のようにして CSS 値を取得します。

  1. ユーザーが値を取得したいスタイル( name で指定)が幅か高さである場合(#751)

    この場合には Microsoft 社が IE において定義し、他のブラウザも追随した便利なメソッド offsetWidth あるいは offsetHeight を使って幅や高さを求めます。

    1. elem.offsetWidth 値があれば(#770)、つまり当該要素がブラウザで描画されていれば、getWH() を起動し、要素の幅又は高さを計測します。勿論、幅 or 高さのいずれを取得するかはユーザーが name で指定します。
    2. elem.offsetWidth 値が取得できない場合、そうなるのは描画されていないからですから jQuery.swap() を起動して、ブラウザ表示に変化が出ないように、要素の CSS スタイル設定を、「絶対配置・非表示かつブロック表示」に一時的に変更してから、getWH() メソッドを呼び出して当該要素のスタイル値を取得します。
    3. getWH() 関数内の処理……ボーダー辺までの幅か高さを求める場合を除き jQuery.curCSS() メソッドを呼び出して計測します。
      1. ボーダー辺までの幅か高さを求める場合には、offset 値はボーダー辺までの値ですから、offset 値をそのまま返します。(#757-758)
      2. マージン辺までの幅か高さを求める場合(#763-764)margin 値を offset 値に加算します。
      3. 内容辺までの幅か高さを求める場合(#761-762及び#765-766)パディング値とボーダー値を offset 値から差し引きます。
      4. パディング辺までの幅か高さを求めることは意味がないと考えたのでしょう、算出しません。
  2. ユーザーが取得したいスタイル名が幅でも高さでもない場合(#772)

    処理を jQuery.curCSS( elem, name, force ); に委ねます。(#778)

▲ToTop

jQuery.css() の肝は中で使用されている this にあり

さて、このメソッドは決して難解な箇所はありません。たった 1 つのことを除いては!

中で 3 箇所使用されている this が一寸見には何を指しているのか、大変わかりにくいのです。

前後の文脈から何を指すべきかは推測できますが、「どうしてこの this がそれを指すのか?」───それが暫く分かりませんでした。

解明の糸口は推測した内容と値から、逆に色々考えていて突如見えてきました。

Javascript の基本中の基本ですが、メソッド内で使用される this は、当該メソッドを呼び出したオブジェクトを参照します。

これを改めて踏まえて、css() クラスメソッド内で使用されている this について解明してみます。

上のコードで注目に値する箇所は getWH() です。その中では each() クラスメソッドが起動され、更にその中で curCSS() クラスメソッドが呼び出されていますが、3 カ所の this は全て getWH() 内の each() 内の curCSS() の引数内にあります。

要点は 3 つの this が全て each() クラスメソッド内にあって、jQuery.curCSS() クラスメソッドの引数となっていることです。

each メソッドは第 1 引数の配列要素毎にそれを呼び出し元にして、第 2 引数の関数を実行します。callback.call( arguments[0][i], arguments[1] ) です。

上のコードにおいて which は [ "Left", "Right" ] か又は [ "Top", "Bottom" ]です(#752)。これを第 1 引数として、第 2 引数である 760 ~ 766 行の関数が call されます。ですから呼び出された関数内で this はまず which を参照します。

しかし、呼び出された関数内部で更に別の関数 jQuery.curCSS() メソッドが起動されますから、この関数が起動した後は this は jQuery 関数オブジェクトを参照します。

しかし、this は jQuery.curCSS() メソッドの引数として登場しています。ここが肝心なところです。

もし this が jQuery.curCSS() メソッドの内部で使用されれば、それは this の仕様から jQueryを参照します。しかし、引数である this は無名関数内から jQuery.curCSS() メソッドに外挿され、投入されます。つまり、所与の値として付与されるのですから、このメソッドはそれを受け取って起動するわけで、受け取る時点では this は jQuery ではなく 巡回処理対象 which が参照するいずれかの要素を参照しているのです。

こうして this は "Left"、 "Right"、"Top"、あるいは "Bottom" に置き換えられることになります。

▲ToTop

jQuery.swap() クラスメソッド解読

このクラスメソッドは elem に対して、options による新たなスタイルを一時的に設定し、elem に設定されている CSS スタイルではなく、optionsによるその新たな CSS スタイルに callback( 実際には getWH ) 関数を適用します。

swap() メソッドは、jquery.js の中では css() メソッドからたった1度呼び出されるだけですが、隠蔽されているために、あるいは描画されていないために CSS スタイル値を取得できない要素のスタイル値を、ブラウザ表示を一切変えることなく取得するためのものです。つまり、要素の幅や高さを描画前に知るために使われます。

display:none が指定されている要素は、ブラウザから見れば存在しないことと同値です。また、ボックス内容領域幅よりも長いテキストノードは、暗黙的か明示的かに関わらず CSS スタイルが設定されていない限り、描画されなければその表示幅や高さは決まりません。

しかし、ブラウザ描画前に要素の高さや幅を知りたいケースは決して少なくありません。例えば mouseover 時のポップアップ表示はその好例です。popup は mouseover した要素の直近位置で、ブラウザ表示領域からはみ出さぬような位置に、適切な大きさの領域を表示させ、その中に解説文字などを示す場合に使用します。そのときには幅を指定し高さを文字数に応じて可変となるようにしますが、その大前提として事前に高さを知る必要があります。そうしないとポップ領域がウィンドウの上又は下にはみ出してしまうからです。

以上のようなケースに対処するための関数が、swap() メソッドです。

■ jQuery.swap() クラスメソッド ■
734: // A method for quickly swapping in/out CSS properties to get correct calculations
735: swap: function( elem, options, callback ) {
    // 引数は順にタグ要素、css() メソッド内で定義した要素の css style オブジェクト
    // callback は swapメソッド呼び出し時の引数定義により getWH() メソッドとなっている。
736:  var old = {}; // elem の本来のスタイルプロパティを記憶させるオブジェクト
737:  // Remember the old values, and insert the new ones
738:  for ( var name in options ) { // elem に設定されている各種スタイル値を
739:   old[ name ] = elem.style[ name ]; // old オブジェクトに複写する。
     // css() メソッド内で次の props が設定済みとなっている。
     // props = { position: "absolute", visibility: "hidden", display:"block" }
     // 第2引数 options に登録されているこの CSS 値を elem のスタイルオブジェクトに複写する。
740:   elem.style[ name ] = options[ name ];
741:  }
742:  // CSS 値計測用プロパティが追加された elem から callback 関数 getWH を呼び出す。
    // この処理の中で必要な CSS 値を取得する。
743:  callback.call( elem ); // css() から呼び出されたときには callback = getWH となっている。
744:
745:  // Revert the old values
746:  for ( var name in options ) //処理が終わったら記憶させてあるプロパティを
747:   elem.style[ name ] = old[ name ]; // elem に戻す。
748: },

▲ToTop

jQuery.curCSS() クラスメソッド解読

これはその名の通り Current CSS 値(算出スタイル値とも呼ぶ)を取得するメソッドで、jquery.js 内では css() メソッドなど 10 箇所から呼び出されています。

IE 以外の W3C 規格に準拠するブラウザの場合と、準拠を頑なに拒絶する IE の場合とで、全く異なる方法で CSS 現在値を取得しなければならないため、つまりクロスブラウザ対応を図る必要があるためコードが長くなっています。

■ jQuery.curCSS() メソッド ■
781: curCSS: function( elem, name, force ) { // force については後述する797行の解説を参照
782:  var ret, style = elem.style;
783:
784:  // We need to handle opacity special in IE
785:  if ( name == "opacity" && !jQuery.support.opacity ) { // IE の場合
786:   ret = jQuery.attr( style, " opacity" ); // 不透明度値を ret に取得する。
787:
788:   return ret == "" ? // 返値を決める。
789:    "1" :
790:    ret;
791:  }
792:
793:  // Make sure we're using the right name for getting the float value
794:  if ( name.match( /float/i ) ) // 正しく float セレクタ名があれば
     // jquery.js では W3C 準拠表現の cssFloat も IE 固有の styleFloat も共に
     // styleFloat で統一して扱っています。( #3210 )
795:   name = styleFloat;
796:
    // force == false で style と style.name があれば style.name 値をそのまま使用する
    // つまりブラウザによる算出スタイル値を使わずに、スタイル属性の設定値を利用する。
    // 裏返せば force=="true" ならば、style 属性の有無に拘わらず、カレントスタイル値を算出する。
797:  if ( !force && style && style[ name ] )
798:   ret = style[ name ];
    // 以下はブラウザによる描画値を取得するためのコード
    // force を指定することによりブラウザの算出描画値を取得する。
799:  // getComputedStyle 関数があれば( つまり IE 以外 )
800:  else if ( defaultView.getComputedStyle ) {
801:
802:   // Only "float" is needed here  // float 文字列が name に含まれる場合、
803:   if ( name.match( /float/i ) )
804:    name = "float"; // 使用する呼称は「 float 」だけでよい。
805:   // 所謂駱駝文字列内の大文字を小文字化してハイフンを前に付ける。
     // Javascript スタイル属性表現を CSS スタイル表現に変える。
806:   name = name.replace( /([A-Z])/g, "-$1" ).toLowerCase();
807:   // elem 要素の描画値を取得する準備。
808:   var computedStyle = defaultView.getComputedStyle( elem, null );
809:
810:   if ( computedStyle ) // computedStyle があれば値を取得する。
      // その値をretに代入する。これによりブラウザによる描画値が ret に代入される。
811:    ret = computedStyle.getPropertyValue( name );
812:
813:   // We should always get a number back from opacity
814:   if ( name == "opacity" && ret == "" )
815:    ret = "1";
816:   // 以下は #842 迄 IE の場合
817:  } else if ( elem.currentStyle ) {
818:   var camelCase = name.replace(/\-(\w)/g, function(all, letter){
819:    return letter.toUpperCase(); // 駱駝文字列への変換を行う。
820:   });
821:   // 値を取得する。
822:   ret = elem.currentStyle[ name ] || elem.currentStyle[ camelCase ];
823:
824:   // From the awesome hack by Dean Edwards
825:   // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291
826:
827:   // If we're not dealing with a regular pixel number
828:   // but a number that has a weird ending, we need to convert it to pixels
     // #822 の取得値 ret が数字始まりで "px" が途中にあるか、ない場合には、
829:   if ( !/^\d+(px)?$/i.test( ret ) && /^\d/.test( ret ) ) {
830:    // Remember the original values
831:    var left = style.left, rsLeft = elem.runtimeStyle.left;
832:
833:    // Put in the new values to get a computed value out
      // left 値を取得してランタイムスタイルの left 値に代入し、
834:    elem.runtimeStyle.left = elem.currentStyle.left;
835:    style.left = ret || 0; // style.left 値には #822 の値を入れておき、
836:    ret = style.pixelLeft + "px"; // ret 値は正しい値を取得して代入する。
837:
838:    // Revert the changed values // 元の値を戻す。
839:    style.left = left;
840:    elem.runtimeStyle.left = rsLeft;
841:   }
842:  }
843:
844:  return ret; // ret 値を返す。
845: },
846:

秀逸な秀丸マクロ 2つ

行番号を振ってくれる秀逸マクロ

スクリプトコードに関する記述を行う場合、どうしてもコードそのものを引用表示する必要に迫られ、その際には行番号を振って説明に寄与する必要がある。

しかし、まさか一行ずつ番号を手打ちするなど、折角コンピュータを使っているのに余りに愚かしい。当然自動的に番号を振ってくれるソフトがあるはずだ。

ということで秀丸マクロの中から適当なものを探してみた。

いくつもそれらしいものは存在しており、それぞれ試してみたものの、なかなか使い勝手の良いものは見つからなかった。

試行錯誤すること半日、やっと見つけた秀逸なマクロは、「 総合連番マクロ NumberingX.mac Ver.3.00 」( 秀まるおのホームページ(サイトー企画)-総合連番マクロ Ver.3.00 )である。

その素晴らしさは何と言っても設定を GUI で行えることにあるが、既に振ってある番号を変更する機能を持っていることも、他にはない素晴らしさだ。

例えば、シリーズでチャレンジしている jQuery 解読では、jquery.js コードの部分的引用が頻出するが、jquery.js のバージョンアップに伴って当然コードの行数も変化する。このため引用しているコードの行番号をまとめて変更したい需要が頻繁に生ずるのである。

この総合連番マクロは既に何度も使用しているが、群を抜いて使いやすい。

正規表現による複数行置換を行える Quarta

秀丸の正規表現による検索/置換機能は素晴らしいものだが、論理行の 1 行しか置換対象にできない恨みがある。以前からこの「 複数行一括置換」需要があったが、つい最近それを可能とするマクロの存在を知った。

それは「 Quarta 」といい、窓の杜( 窓の杜 - 【REVIEW】「秀丸エディタ」で複数行の検索・置換を可能にするマクロ「Quarta」 )でも紹介されている。

本家サイトはこちら( Wisteria - 秀丸マクロ - Quarta )だが、使ってみて非常に軽快に使えるので大変満足している。

jQuery が Ver1.2.6 へUP

1.2.3 から一気に 1.2.6 へ

1.2.6 のリリースは 5月 24 日、本家サイトに拠れば「Releases 1.2.4 and 1.2.5 were skipped 」。1.2.4 と 1.2.5 はそれぞれ問題があったためだそうです(1.2.4 was built incorrectly, rendering it effectively identical to 1.2.3, and 1.2.5 was missing a patch.)

1.2.3 からの改良は高速化と新機能にあり、以下のように紹介されています。

  • Performance Improvements(速度改善)
    • Event Handling is 103% Faster
    • CSS Selectors are 13% faster
    • .offset() is 21% faster
    • .css() is 25% faster
    • .css() is 25% faster
  • New Features and Major Changes(新機能と大きな変更)
    • Dimensions Plugin is Now Part of Core
    • .attr() overhaul
    • .toggle() can now accept more functions
    • .index() supports jQuery collections
    • beforeSend can cancel Ajax calls
    • Exposed Speeds

それなりに魅力的なマイナーバージョンアップだ

高速化は歓迎すべき改良点であり、言わずもがなの前進でしょう。

それぞれの高速化がどの様なコードの変更によってもたらされたのか、その解明に興味をそそられます。

また、Dimensions Plugin の扱いやtoggleメソッドの機能拡張も興味深い点です。ユーザーフレンドリーなコード作成に活用出来るのではないか、と期待させられます。

▲ToTop

いずれにしても・・・jQuery の解読を先に進めたい

1.2.2における解読が暫く中断してますが、更なる発展を遂げつつあるjQueryの解読を今回のバージョンアップを契機として、更に先に進めたいと改めて感じ入った次第です。

イベントコードの解読を終えたのは既に約半年前になってしまいましたが、次は jQuery.js のAjax 関連コードの解読に踏み込みたいと考えている今日この頃です。

ファイル置き場と Ajax 通信を活用して、任意のFc2ブログから情報を得る

  • 2008/3/12 : 初稿
  • 2008/7/9 : コードの抜本改訂とファイル置き場利用版up
  • 2008/7/12 : Ajax 通信関連部分を更に抜本改正
  • 2008/7/20 : Ajax 通信関連部分で確実性を更に向上(ここで Ajax 関連コードは完成し、これ以降はあれこれのユーザビリティの向上対策を施した。)
  • 2008/7/21 : id 及び class 名称を他の Fc2 ブログで使われていないと推定される固有のものに変更
  • 2008/8/12 : 各 Ajax 通信所要時間を記録し閲覧できるように変更。このプロジェクトの削除(関連cssファイルやjsファイルの登録削除)と隠蔽(単にpopup小窓を隠すだけ)を分け、各ボタンを配置。
  • 2008/8/14 : responseText の 文字化け対策として、或る <meta/> タグを追加するように変更

他ブログ上での Ajax 通信をついに実現( 2008/7/9 記 )

上の関連エントリイでつらつらと述べてきた課題──すなわち他の任意のFc2ブログサイト上において、ブックマークレットによって必要なスクリプトをインクルードし、当該スクリプトによる Ajax 通信によって当該ブログの情報を取得し、それを当該ブログ上に表示する──が、やっと解決しました。( 8/12 完結 )

上記 「 Related Entries in this Blog 」 の No1エントリイで計画したことが、No.2 エントリイで述べた、過去/未来/最新エントリイタイトル取得/表示を行う Ajax 通信の成功と、No.3 とNo.4 エントリイで述べたファイル置き場問題の解決によって実現したのです。

何はさておき、結果を画像で紹介することが最も分かりやすいでしょう。( 2008/8/14 追記 )

サンプル 1【 Fc2総合インフォメーションブログ 】

下の画像は、Fc2総合インフォメーションブログ上でこのプロジェクトを実行した際の表示結果抜粋です。

Fc2 総合インフォメーションブログ上で Ajax 通信によって最新/以前/以後エントリイのタイトル情報などを取得した結果を示すサンプル画像 1
サンプル 2【 関西ZIGZAG 】

下の画像は、人気ブログ「 関西 ZIGAZAG 」ブログ上でこのプロジェクトを実行した際の表示結果の抜粋です。

関西 ZIGAZAG ブログ上で Ajax 通信によって最新/以前/以後エントリイのタイトル情報などを取得した結果を示すサンプル画像 2
サンプル 3【 FCafe 】

下の画像は、自ブログ開設時からお世話になってきたサイト上でこのプロジェクトを実行した際の表示結果の抜粋を示すものです。

FCafe ブログ上で Ajax 通信によって最新/以前/以後エントリイのタイトル情報などを取得した結果を示すサンプル画像 3

今回のプロジェクト「Fc2ブログ エントリイ情報取得」の概要

今回作成したコード群は、任意の Fc2 ブログ上で jQuery を活用してエントリイタイトル等を取得するツールです。また併せて jQuery.js と ( Firefox 以外の browser の場合には )firebugLite もインクルードするようにしたので、IE や Opera などにおいても jQuery と firebugLite を使用し、スクリプトテストや DOM 操作を行うことも可能としました。

つまり、或る Fc2 ブログサイトの、或る個別エントリイ頁を開いた状態でブックマークを起動すれば、以前、最新及び以後の各 10 個のエントリイに係る諸情報(具体的にはタイトル、Entry番号、投稿日など)が閲覧できると共に、そのページ上で jQuery.js と firebugLite.js を使って Javascript コードの実行が出来るわけです。

プロジェクトのファイル構成( 2008/7/9 記 )

コード群はブックマークレットを含めて以下の6つから構成されます。

  1. ブックマークレット……次の setjQnFbug_getFc2EnTts.js を登録するため
  2. スクリプト登録用JSファイル( setjQnFbug_getFc2EnTts.js )……今回のプロジェクト実行に必要な以下の 4 つの全てのファイルを、今閲覧中の任意の Fc2 サイトのHTML文のヘッダー部に登録するための、短い Javascript コードファイル
  3. 上の 2. のファイルで登録するファイルは以下の 4 つです。
    1. jQuery.js、
    2. firebug.js、
    3. getFc2EntryTitles-gp.js( Ajax 通信用オリジナルJSファイル)
    4. getFc2EntryTitles-gp.css(通信結果表示用オリジナルスタイルシート……Ajax通信結果を固有に用意した絶対配置要素内で表示させるために、各ブログに依存しない固有のスタイルシートを用意)

ここに、このプロジェクトでは Ajax 通信処理をはじめとして、随所に jQuery.js を多用したコードを記述し実行しているため、jQuery.js が組み込まれていないブログの場合も想定し(大多数は組み込まれていないと思われる)、なければそれをインクルードするようにしました。

また firebugLite も組み込むようにしたので、IE や Opera 上でも Javascript のテストが容易に出来るようにしました。

以上により、Fc2のブログであれば何であれ、個別エントリイ表示モードの場合において、jQuery を活用して Ajax 通信を行い、エントリイタイトル情報を取得すると共に、firebugLite を使った Javascript の走行テストが出来るようにしました。

▲ToTop

ファイル置き場は Google Page を活用( 2008/7/9 記 )

ファイル置き場に出来るかも知れない、と考えた Just Sysytem 社のインターネットディスクは、同社への問い合わせの結果、使えないことが判明しました。また SONY の WebPocket にも問い合わせましたが、こちらも駄目でした。

有料なのに、それでもなお、ファイル置き場を認めない理由は全く理解できません。

確かに「 アクセスが集中する、ウィルスファイルを置かれる可能性も否定できないから踏み台とされる、つまりサービスの利用よりも悪用に手を貸すことになる 」等の否定的な側面が懸念されます。そしてこの懸念から解放されるためには、ファイル置き場を認めないことが安易な方法なのでしょう。

───と、いくら批判し、嘆いていても仕方ないので、調べた中で、無料で容易にファイル置き場として利用できる Google Page ウェブサイトを活用することにしました。Google page 上にマイページを立ち上げ、そこに上記 6 つのファイルの内ブックマークレットを除く 5 つのファイルを GooglePage サイトに upload し、自作ブックマークレットを作動させてその稼働を半年以上に渡って確認しています。

※ この少し下に成功したブックマークレットを置きました。

このプロジェクトの利用方法( 2008/3/12 記 )

ブックマークレット

以下のブックマークレットを(ブックマーク又はお気に入りに登録し)クリックすれば、関連する JS ファイルや CSS ファイルが head タグ内に追加され、固有の絶対配置要素が画面上部に表示されます。FireFox 、 Opera 及び IE7 で動作確認済みです。

後は Before・・・、Recent・・・、After・・・ などのボタンをクリックすれば、所定の情報が絶対配置要素内に表示されます。

Ajax 通信をやり直すボタン( ReLoad Data ボタン )や、Ajax 通信に要した時間を知ることが出来るような Log 閲覧ボタンも設置しました。また、結果を表示する小窓が邪魔になったら、隠蔽することも出来るし、ブックマークレットによってインクルードした関連スクリプト・cssファイルを削除するボタンも設置しました(但し jQuery.js と firebug.js は残すようにしました)。

こうして、Ajax通信結果を示す小窓が邪魔になったら隠蔽し、ブックマークレットを再起動すれば、隠蔽直前の状態で小窓が復帰するようにすると共に(Ajax 通信結果は閲覧中の頁がリロードされない限りメモリ上に残る仕様としたのです)、インクルードファイルが邪魔になったら簡単に剥離・削除出来る仕様にしました。

次のブックマークレットをお気に入りに登録すれば、今回のプロジェクトを任意の Fc2 ブログサイトで共通して使うことが出来ます。また、単に以下のブックマークレットをクリックするだけでも、今ご覧戴いているこのエントリイを対象としてブックマークレットを利用できます。

このプロジェクトの起点となるブックマークレット

※ 2008/8/11迄上記ブックマークレットのコードに一部ミスがあり機能しませんでした。利用しようとしてくださった方々には大変ご迷惑をお掛けいたしました。

Ajax 通信結果の表示

ブックマークレットをクリックすると、次のような小窓がブラウザの上部に表示されます。

Ajax 通信結果を表示する絶対配置要素の図

上の図は既に Ajax 通信が終わった状態の画像ですが、7つのボタンは左から順に「(1)今見ているエントリイの前の10個のエントリイタイトル表示、(2)最新の10エントリイ表示、(3)今見ているエントリイの後に投稿された10エントリイのタイトル表示、(4)Ajax通信再実施、(5)所要時間表示閲覧、(6)小窓の隠蔽、(7)関連スクリプトやcssファイルの削除」を行うためのものです。

例えば 「 Recent Entries 」 ボタンをクリックすると下図のようにタイトル名とエントリイナンバーが表示されます。Ajax 通信をやり直すことができるように設けた 「 Relaod Data 」 ボタンは、何らかの事情で取得結果がおかしかった場合などに Ajax 通信をやり直すことが出来るように配置したものです。このボタンをクリックすれば Ajax 通信に要した時間を比較して知ることも出来ます。

Ajax 通信結果である過去エントリイ10タイトルを見ている図
表示エントリイ上で jQuery と Firebug Liteを活用!

単に、エントリイタイトルを閲覧するだけでなく、このプロジェクトでは jQuery.js と FirebugLite もインクルードしますので、IE や Opera において、ちょっとした Javascript 利用が(デバッグ環境とまではいかないが、テストや学習に活用出来る)、エントリイ頁上でリアルタイムに行えるようになります。F12 キー又は CTRL+Shift+Lキーを押せば firebug Lite を起動することが出来ます。

▲ToTop

Javascript コードおよびCSSファイル説明( 2008/3/12 初稿:その後漸次改訂。確定は2008/8/14 )

1. スクリプトをインクルードするためのブックマークレット

ブックマークレットは各種のスクリプトや CSS ファイルをインクルードするための、いわば初期化スクリプトです。たった1つのスクリプトの存在をチェックし、存在していなければインクルードします。

そもそも全てのコードをブックマークレットに記述できればそれが最も合理的なのですが、ブックマークレットにはブラウザ毎に文字数制限があり、ここで行ったことはとてもその制限値内には収まらないので、複雑な手続きを経なければなりませんでした。

<a href="javascript:
(function(){
  // 文字数を減らすために変数を纏めて指定
  var%20x=0,i=0,
  h=document.getElementsByTagName('head')[0],
  t=document.getElementsByTagName('script'),
  s=document.createElement('script');
  // headタグ内を検索して
  if(t) for(;i<t.length;i++) 
    // setjQnFbug 文字の有無を走査し、有れば x に 1 を代入
    if(/setjQnFbug/.test(t[i].src)) x+=1;
    // setjQnFbug 文字が見つからなければ所定のjsファイルをインクルードする
    if(x==0){
      s.src='http://hkom007.googlepages.com/setjQnFbug_getFc2EnTts.js';
      s.type='text/javascript';
      // headタグ内にスクリプトタグを追加
      h.appendChild(s);
    // 既に所定のjsファイルがインクルード済みならば所定の関数(chkScript)を起動する。
    // この関数は3つのjsファイルと1つのCSSファイルのインクルードを行うためのもの
    }else%20chkScript();
})()">set jQuery & FirebugLite & getFc2EntryTitles.js</a>

▲ToTop

2. Javascript 及び css ファイルの一括インクルードを行う

setjQnFbug_getFc2EnTts.js は 4 つのファイルを一括してインクルードするコードだけを記述した 小さな JS ファイルです。といってもブックマークレットにするには文字数が多すぎてIEで読み込めないため、ブックマークレットとは別に設置した訳です。このファイル作成の当初段階では、インクルードするだけではなく、その後の処理も1つのファイルに纏めようとしましたが、それは無理でした。

何故ならば、インクルードのためのコードに続けて、インクルードされたファイルを使用して様々な処理を行うコードを書いて実行すると、インクルードが中断されてしまうからです。

ブラウザがファイルをインクルードする処理を行っている間に、JS インタープリタが次の処理を行おうとすると、インクルードそのものが中断されてしまうのです。

そのためインクルード専用のファイルと、インクルードされたファイルを利用して諸処理を実行するコードを記述したファイルとを別々に分けました。

// このスクリプトがインクルード済みの場合に関数が起動できるよう
// 名前付き関数とし、最後の行でこの関数を起動する。
var chkScript = function(){
  var v=0,w=0,x=0,y=0,z=0,i=0,
    h=document.getElementsByTagName('head')[0],
  m=document.getElementsByTagName('meta'),
    t=document.getElementsByTagName('script'),
    l=document.getElementsByTagName('link'),
    s=document.createElement('script');
  // 文字化け対策 
  // http-equiv="content-type" かつ content="application\/x-javascript charset=euc-jp"であるmetameguがあるかどうか走査
  // 存在すれば v 値を加算する。 
 if(m) for(;i<m.length; i++){
  if(/content-type/.test(m[i].getAttribute("http-equiv"))
   && /application\/x-javascript charset=euc-jp/.test(m[i].content)) v+=1;
 }
  // v 値がゼロならば meta タグを追加する。 
 if (!v){ 
  var k=document.createElement('meta');
  k.setAttribute("http-equiv","content-type");
  k.content = "application/x-javascript charset=euc-jp";
  h.appendChild(k);
 }
  // 所定のスタイルシートがインクルード済みかどうか link タグ走査し、あれば記録。
 if(l) for(i=0 ;i<l.length; i++) {if(/getFc2EntryTitles/.test(l[i].href)) w+=1}
  // CSSファイルのインクルード
 if(!w) {
  var k = document.createElement('link');
  k.rel = "stylesheet";
  k.type = "text/css";
  k.media = "screen, print";
  k.href='http://hkom007.googlepages.com/getFc2EntryTitles-gp.css';
  h.appendChild(k);
 }
  // jquery、firebug 及び getFc2EntryTitles の文字を含むスクリプトタグを走査し、
  // 存在していればそのことを記録する。
 if(t) for(i=0;i<t.length;i++){
  if(/jquery[^u][^i]+/.test(t[i].src)) x+=1;
  if(/firebug/.test(t[i].src)) y+=1;
  if(/getFc2EntryTitles/.test(t[i].src)) {
      // ブックマークレットが複数回起動された場合の処理
      // インクルード済みで結果表示小窓が非表示ならば表示する
      var tmp = document.getElementById("ajaxPopup");
      if (tmp && tmp.style.display == "none") tmp.style.display="block";
    } // 結果表示小窓が表示済みならば、何もしないでif文を終える。
 }
  // 以降で反復利用する文字列を定義
 s.type='text/javascript';
  // jQuery.js のインクルード
  // jQuery.js は単に firebug Lite から利用するだけではなく、Ajax通信や
  // その取得結果を表示するコードで多用します。
  // なお、min 版を利用することにしました。
 if (!x){
  s.src='http://hkom007.googlepages.com/jquery126min-gp.js';
  h.appendChild(s);
 }
  // firebugLite のインクルード
 if(!/firefox/.test(navigator.userAgent.toLowerCase()) && y==0){
  var c=document.createElement('script');
  c.src='http://hkom007.googlepages.com/firebug-gp.js';
  c.type=s.type;
  h.appendChild(c)
 }
  // Ajax 通信を行う本体コードの getFc2EntryTitles.js のインクルード
 if (!z){
  var r=document.createElement('script');
  r.type=s.type;
  r.src='http://hkom007.googlepages.com/getFc2EntryTitles-gp.js';
  h.appendChild(r);
 }
  // 無名関数としなかったのは再呼び出しがあり得るからです。
};chkScript(); //関数実行

▲ToTop

3. 結果を表示する小窓用 CSS ファイル

取得結果を絶対配置要素内に表示させるために、CSS ファイルを作成しました。これによりどんな Fc2 ブログであっても Ajax 通信結果を同様に表示するようにしました。

なお、任意のFc2ブログで活用できるようにするには、id や class の名称に工夫が必要であることを、いくつかのサイトで試してみて納得しました。このプロジェクトで利用する名称が、閲覧しているブログサイト内で利用されてる名称と重複してしまってはいけない、ということです。

このことは振り返れば余りに当たり前のことですが、実行して初めて分かった次第です(^_^;)。

経緯はこうでした。( 2008/7/21 追記 )

FC2総合インフォメーション 【ブログ】画像ファイル挿入の仕様変更のお知らせ でこのプロジェクトを試してみたのですが、その際に当該サイトで使用されている id 名称( #menuBlock )が、このプロジェクトで利用していた id 名称と重複していたため、当該サイトでは予想外の挙動が起きてしまいました。

そこで初めて、任意の Fc2 サイトと id や class 名称が干渉しないよう、このプロジェクトでは他には絶対に存在しないであろう、固有の名称を使用しなければならない、と気がついた訳です。

こうして id 及び class 名称は以下にあるように長たらしいものとなりました。全ての id 及び class 名称に、「 getEntryTitles_ 」なる接頭語を付けたのです。これで「おそらく」世界で唯一の名称になったのではないか、と一人合点しています。

更に CSS ファイルでは重要なことがあります。( 2008/8/17 追記 )

それは自分のブログで様々な CSS コードを書いている限り全く問題とはならなかったことであり、他のサイト上で自分が書いた CSS ファイルを適用する際には、必ず注意しなければならない問題です。

その問題とは CSS ファイルの優先順位です。

ブラウザにはそれぞれ固有のスタイル値があり、また当然のことですがそれぞれのブログには、作者固有の、またはテンプレート固有のスタイル値があります。そして私が考え、CSSファイルで表現したスタイル値があります。これらの優先順位についてこれまで全く考慮する必要がなかったのですが、他の方のブログ上で MyCSS ファイルを適用することになった時点で、即座にこの CSS ファイルの優先順位が切実な課題となってきたのです。

そこで数年ぶりに改めて CSS の学習を思い起こし、また一部再学習して !important 属性を使うことに到達しました。

@charset "euc-jp";
	#getEntryTitles_pasteData ul { /* important によって任意の Fc2ブログにおいて同一の */
		margin:0 0 0.5em 1.5em !important; /* 結果を得るようにした。*/
		padding:0 !important; /* ここに到達するまでに結構悩んでしまった。*/
		clear:both !important;
	}
	#getEntryTitles_pasteData ul li{
		margin:0 !important;
		padding:0 !important;
	}
	#getEntryTitles_ajaxPopup{
		position:absolute; z-index:100000; top:150px;left:50%;
		width:0; height:0; margin:0px;
		font-size:small;
		text-align:left;
		color:black;
		display:none;
	}
	#getEntryTitles_menuBlock {
		padding:10px;
		width:0px; height:0px;	/*not auto*/
		background-color:#def;
		border:2px #777 solid;
		display:none;
	}
	#getEntryTitles_loading {
		clear:both;
		padding:5px 10px;
	}
	#getEntryTitles_pasteData {
		clear:both;
		margin-top:-28px;
		display:none;
	}
	#getEntryTitles_pasteData a:link{color:blue; text-decoration: none;}
	#getEntryTitles_pasteData a:visited { color: purple; text-decoration: none;}
	#getEntryTitles_pasteData a:hover {
		color:darkgreen;
		background:#dd0; 
		text-decoration: none; 
	}
	#getEntryTitles_pasteData a:active { color: lightblue; text-decoration: none;}

	#getEntryTitles_finish, #getEntryTitles_resultbefore, #getEntryTitles_resultrecent,#getEntryTitles_resultafter {
		padding:0 10px 10px 10px;
		background:#def;
		border-left:2px #777 solid;
		border-right:2px #777 solid;
		border-bottom:2px #777 solid;
		display:none;
	}
	.getEntryTitles_btn {
		width:120px;
		float:left;
		margin:0 2px;
		padding:2px;
		border:1px black solid;
		font-weight:bold;
		color:white;
		background-color:#89A;
		text-align:center;
	}
	#getEntryTitles_viewLog {
		float:left;
		width:50px;
		margin:0 2px;
		padding:2px;
		border:1px black solid;
		font-weight:bold;
		color:white;
		background-color:#89A;
		text-align:center;
	}
	#getEntryTitles_hideThis, #getEntryTitles_removeThis {
		float:right;
		width:20px;
		margin:0px 2px;
		padding:2px;
		border:1px black solid;
		font-weight:bold;
		color:white;
		background-color:#89A;
		text-align:center;
	}
	#cmtbtn1{
		text-align:center;
		margin-top:-2em;
		display:block; width:16em; float:right;
	}
	#cmtbtn2{
		text-align:center;
		border:1px solid darkgray;
		padding:2px;
		margin-top:-2em;
		width:12em; float:right;
	}

▲ToTop

4. メインの JS コード( Ajax通信実施、結果表示等全ての処理を実行する )

Ajax 通信処理、取得結果表示処理などを行うメインコードを書いたファイルです。

必要なファイル全てのインクルードを行ってから、getFc2EntryTItles-gp.js によって Ajax 通信処理を行い、その取得結果を表示中のブログ内に表示します。

結果を表示する小窓にはスクロールイベントを登録し、スクロール時にも常に画面上部に表示され続けるようにしました。

  1 :/* getFc2EntryTItles-gp.js
  2 : * 2008/7/21 Release
  3 : * 2008/8/12, 2008/8/17 update
  4 : */
  5 :// fc2ブログでかつ個別 Entry 表示モードの場合にのみコードを進行する。
  6 :if (location.href.indexOf("fc2")!=-1 && location.href.indexOf("blog-entry-")!=-1){
  7 :(function($){
  8 : var now=function(){return +new Date;}; // 時刻取得関数
  9 : $.extend({ // 時刻・timer起動回数記録用 jQuery 拡張オブジェクト
 10 :  tr:{
 11 :   start:now(),
 12 :   registerEvent:"",
 13 :   ajax:{ recent:[],before:[],after:[]},
 14 :   end:"",
 15 :   waitRecentAjaxCnt:0,setEndingCnt:0
 16 :  }
 17 : });
 18 : // ローカル変数定義
 19 : var aP,mB,ld,pD,fn,lg,r_c,r_b,r_a, constStr="getEntryTitles_";
 20 : var html = { before:[], after:[], recent:[] },getStr ={ before:"", after:"", recent:"" },
 21 :  blogTitle =null, regExpr = "",border={ before:0, after:0},realElm={before:0,after:0},thisEntryNo, lastNo, itval,complement,cmt=0;
 22 :
 23 : // 取得結果表示用のタグを作り表示する。
 24 : if ( $("#"+constStr+"ajaxPopup").size()==0) {
 25 :  $(document.body).append(
 26 :   '<div id="getEntryTitles_ajaxPopup">'+
 27 :    '<div id="getEntryTitles_menuBlock">'+
 28 :     '<button id="getEntryTitles_before" class="getEntryTitles_btn" title="このエントリイより前のエントリイのタイトル情報を見る">Before Entries</button>'+
 29 :     '<button id="getEntryTitles_recent" class="getEntryTitles_btn" title="最新エントリイのタイトル情報を見る">Recent Entries</button>'+
 30 :     '<button id="getEntryTitles_after" class="getEntryTitles_btn" title="このエントリイより後のエントリイのタイトル情報を見る">After Entries</button>'+
 31 :     '<button id="getEntryTitles_reload" class="getEntryTitles_btn" title="Ajax通信をやり直す">ReLoad Data</button>'+
 32 :     '<button id="getEntryTitles_viewLog" title="Ajax 通信の所要時間を見る">Log</button>'+
 33 :     '<button id="getEntryTitles_removeThis" title="このプロジェクトの関連スクリプトやcssを一気に削除する。">'+decodeURI(encodeURI("×"))+'</button>'+
 34 :     '<button id="getEntryTitles_hideThis" title="この小窓を隠蔽する。再表示はブックマークレットを再度クリックすれば良い。">-</button>'+
        // 通信中であることを表示するタグ
 35 :     '<div id="getEntryTitles_loading"><img src="http://hkom007.googlepages.com/loading_16.gif" width="16" height="16" border="0" alt="" /> Now Loading...</div>'+
 36 :    '</div>'+
 37 :    '<div id="getEntryTitles_pasteData">'+
        // 連続する Ajax 通信の全てが終わったことを表示するタグ
 38 :     '<div id="getEntryTitles_finish"><div><strong>Finish Ajax Communication !</strong></div><div id="getEntryTitles_log"></div></div>'+
        // 以前エントリイタイトル名等を表示するタグ
 39 :     '<div id="getEntryTitles_resultbefore"></div>'+
        // 最新エントリイタイトル名等を表示するタグ
 40 :     '<div id="getEntryTitles_resultrecent"></div>'+
        // 以後エントリイタイトル名等を表示するタグ
 41 :     '<div id="getEntryTitles_resultafter"></div>'+
 42 :    '</div>'+
 43 :   '</div>'
 44 :  );
 45 : } else {
 46 :  $("#"+constStr+"ajaxPopup").show();
 47 :  return;
 48 : };
 49 :
 50 : // 表示/非表示を操作するために必要な要素をショートカット変数に代入する。
 51 : aP=$("#"+constStr+"ajaxPopup");
 52 :  mB=$("#"+constStr+"menuBlock");
 53 :    ld = $("#"+constStr+"loading");
 54 :  pD=$("#"+constStr+"pasteData");
 55 :    fn = $("#"+constStr+"finish");
 56 :       lg =$("#"+constStr+"log");
 57 :    r_c = $("#"+constStr+"resultrecent");
 58 :    r_b = $("#"+constStr+"resultbefore"); r_a = $("#"+constStr+"resultafter");
 59 :
 60 : // 結果表示ポップアップに対するアニメーション関数。真ん中から吹き出すような効果を狙った。
 61 : var doExplodeShrink = function(elem,w,h,padbdr,left,state){
 62 :  elem.show().animate({
 63 :   width: w=="auto" ? "auto" : w+"px", height:h=="auto"?  "auto" : h+"px",
 64 : //  display:state, // IEでは機能しないためやむなく削除
 65 :   marginLeft:left ? (-parseInt(w/2)-padbdr+"px") : 0
 66 :  },400,"swing");
 67 : };
 68 :
 69 : // イベントハンドラー登録
 70 : $.tr.registerEvent=now();
    // 固定配置的に配置するためのスクロールイベントハンドラー
 71 : $(window).scroll(function(){
 72 :  $("#"+constStr+"ajaxPopup").css({
 73 :   top:10 +( window.pageYOffset || Math.max(document.body.scrollTop, document.documentElement.scrollTop) )+"px",
 74 :   marginLeft:-320+ ( window.pageXOffset || Math.max(document.body.scrollLeft, document.documentElement.scrollLeft) )+"px"
 75 :  });
 76 : });
 77 :
    // 最新/過去/未来エントリ情報を表示させるクリックイベントハンドラー
 78 : $.each([$("#"+constStr+"recent"),$("#"+constStr+"before"),$("#"+constStr+"after")],function(j){
 79 :  $(this).click(function(){
 80 :   $([fn,r_c,r_b,r_a]).each(function(i){
 81 :    i==j+1 ? $(this).show() : $(this).hide();
 82 :   });
 83 :   this.blur();
 84 :  });
 85 : });
 86 :
    // Ajax通信をやり直すボタン
 87 : $("#"+constStr+"reload").click(function(){
 88 :  $.tr.start=now();
 89 :  this.blur();
 90 :  pD.hide().children().hide();
 91 :  mB.hide();
 92 :  doExplodeShrink(aP,0,0,0,true,"none");
 93 :  ld.show();
 94 :  makeLists(10);
 95 : });
 96 :
    // Ajax通信に要した時間を見る為のボタン
 97 : $("#"+constStr+"viewLog").click(function(){
 98 :  $([r_c,r_b,r_a]).each(function(){$(this).hide();});
 99 :  fn.show();
100 :  this.blur();
101 : });
102 :
    // 小窓を一時的に隠蔽するボタン
103 : $("#"+constStr+"hideThis").click(function(){
104 :  this.blur();
105 :  aP.hide();
106 : });
107 :
    // プロジェクトに係るjsファイルやcssファイルを削除するボタン
108 : $("#"+constStr+"removeThis").click(function(){
109 :  this.blur();
110 :  aP.remove();
111 :  $("script[src*='getFc2EntryTitles']").remove();
112 :  $("script[src*='setjQnFbug']").remove();
113 :  $("link[src*='getFc2EntryTitles']").remove();
114 : });
115 :
116 : mB.children().hover( // ボタンにマウスオーバー/アウトした際のイベントハンドラー
117 :  function(){
118 :   $(this).css({color:"black",backgroundColor:"#dd0"});
119 :  },
120 :  function(){
121 :   $(this).css({color:"white",backgroundColor:"#89A"});
122 :  }
123 : );
124 :
125 :// 最近のタイトルを取得する関数を定義
   // limit 個数のエントリイタイトル情報を Ajax 通信によって取得する。
126 :var makeRecentEntryList = function (limit){
127 : var No, subject, date, iter=0, ret=[],
128 :  target ={  //xml ファイル内での順番
129 :     link:[],     //0
130 :   title:[],     //1
131 : // description:[],  //2 これは利用しない
132 : // content:[],    //3 同上
133 :    subject:[],    //4
134 :   date:[]      //5
135 :  };
136 :
137 : // エントリイタイトルを取得する
138 : $.ajax({
     // 閲覧中のFc2ブログの xml ファイルのアドレスを設定する。
139 :  url: /(http:.+fc2\.com\/.*)blog-/.exec(location.href)[1] + "?xml" || null,
140 :  type: "GET",
141 :  dataType: "xml",
     // xmlファイルが成功裏にダウンロード出来た場合の処理関数
142 :  success: function(xml){
143 :   var tmpStr = '<div>'+decodeURI(encodeURI("最新のエントリイ情報がありません。"))+'</div>';
144 :   if (xml==null) {getStr.recent = tmpStr; return;}
145 :   // Blogタイトルを取得する
146 :   blogTitle = $(xml).find("title").eq(0).text();
147 :   // 最新エントリイ情報を巡回取得
148 :   $.tr.ajax.recent["start"]=now();
149 :   $.each(target,function(key){
150 :    $.each($(xml).find("item"), function(i,n){
        // 取得結果を ret 配列の i 番目に代入
151 :     ret[i]= [$(n).children().eq(0), $(n).children().eq(1), $(n).children().eq(4),$(n).children().eq(5)];
        // ret配列内のテキスト文字列を抽出して target 配列に代入する。
152 :     target[key].push( ret[i][iter].text() );
153 :    });
       // 次のエントリイ情報を取得するために(46行で初期値ゼロを定義済み)
154 :    iter++;
155 :   });
156 :   try {
       // エントリイ番号、エントリイタイトル、投稿年月日を取得する。
157 :    for (var i=0 ; (i < limit) && target.link[i] ; i++) {
158 :     No = /entry-([0-9]+)/.exec(target.link[i])[1];
159 :     i==0 && (lastNo = Number(No)); // 最新エントリイ番号を取得
160 :     subject =" , " +target.subject[i];
161 :     date =" , " +target.date[i].substring(0,10); // 年月日のみを抽出
        // limit 個の取得結果を表示用に整序して配列 html.recent に代入する。
162 :     html.recent.push( "<li><a href='" + target.link[i] + "' target='_blank'>" + target.title[i] + "</a> (No." + No + subject + date + ")</li>" );
163 :    }
164 :    $.tr.ajax.recent["end"]=now(); // 時刻記録
       // 格納済み配列を HTML 文字列に併合して最終表示用に整序する。
165 :    getStr.recent = "<div><strong>Recent " + Math.min(limit,i) + " Entries</strong></div><ul style='margin-left:1.5em;list-style-type:disc'>" + html.recent.join('') + "</ul>";
       // 最終表示用に整序した HTML 文字列を表示用タグに挿入する。
166 :    r_c.html(getStr.recent);
167 :   } catch (err) { // 何らかのエラーが発生した場合にはその旨を表示する。
168 :    r_c.html(tmpStr);
169 :   }
170 :  } // End of success()メソッ
171 : }); // End of ajax()メソッド
172 :}; //End of makeRecentEntryTitle func
173 :
174 :// 前後のタイトルを取得するための準備を行う
175 :var makeEntryList = function(b_a,limit){
176 : var thisHTTP, getEntryNos=[], thisURL=[]; // ローカル変数定義
    // エントリイアドレス文字列を分解するための正規表現文字列定義
177 : regExpr = /(http:.+entry-)([0-9]+)/;
    // 今開いているエントリイのエントリイ番号を取得する。
178 : thisEntryNo = Number(regExpr.exec(location.href)[2]);
    // 今開いているエントリイアドレスのエントリイ番号前までの文字列を取得する。
179 : thisHTTP = regExpr.exec(location.href)[1];
    // 以前エントリイについてアドレス名にエラーが出ないように
    // この段階では最新エントリイタイトル名取得関数内で設定した。
    // 最新エントリイ番号値は上で取得済みなので、これを活用して
    // 以後番号も存在しない番号を取得しないようにする。
180 : border[b_a] = Math.min(limit+1, b_a == "before" ? thisEntryNo : Number(lastNo)-thisEntryNo+1);
181 : if (border[b_a]==1) thisURL.length=0; // 求める以前エントリイがない場合の処理
182 : else {
     // 求める以前または以後のエントリイが存在すれば
183 :  for (var i=1; i < border[b_a]; i++)
      // そのアドレス文字列を作成し配列に格納する。
184 :   thisURL.push(thisHTTP + (thisEntryNo - (b_a == "before" ? i : -i)) +".html" );
185 : }
186 : realElm[b_a] = 0; // 初期化
187 : // 準備完了! Ajax 通信開始
188 : getTitlesByAjax.call(this, b_a,thisURL);
189 :};
190 :
191 :// Ajax 通信によりエントリイタイトル等を取得する関数の定義
   // b_a は以前のエントリイか、以後のエントリイかを示す文字列
192 :var getTitlesByAjax = function (b_a,thisURL){
193 : if ( thisURL.length != 0 ) {
     // エントリイアドレス毎に巡回処理を行う。
194 :  $.each(thisURL,function(i,aryitem){
195 :   $.tr.ajax[b_a][i]=[];
196 :   $.tr.ajax[b_a][i]["start"]=now();
197 :   $.get(aryitem,function(data){ //data は thisURL[i] の html テキスト文
       // エントリイタイトル部分を抽出するための正規表現文字列を定義する。
198 :    regExpr = /<title>(.*)<\/title>/;
       // ブログタイトル文字列があればそれをエントリイタイトル文字列から削除する。
199 :    var titleStr = blogTitle && regExpr.exec(data)[1].replace(blogTitle,"") || "" ;
200 :    if ( /\S+/.test(titleStr)){ //空白だけのタイトル名は補足しない。
201 :     ++realElm[b_a]; // 取得したタイトル数をカウントする。
        // 1つのエントリイのタイトル名、エントリイ番号を取得

202 :     html[b_a][i]="<li><a href='" + aryitem + "' target='_blank'>" + decodeURI(encodeURI(titleStr)) +" (Entry No." + /entry-([0-9]+)/.exec(aryitem)[1] + ")</a></li>";
203 :    }
204 :    $.tr.ajax[b_a][i]["end"]=now(); // 時間記録
       // 最後の通信処理が終わったらsetEnding()関数をタイマー起動する。
205 :    if (b_a == "after" && aryitem == thisURL[thisURL.length-1]) 
206 :     itval=setInterval(setEnding,20);
207 :   });
208 :  });
209 : } else {if (b_a == "after") itval=setInterval(setEnding,20)}
210 :};
211 :
   // 終了処理関数定義
212 :var setEnding = function (){
213 : $.tr.setEndingCnt++; // 回数記録
214 : if (jQuery.active==0){ // Ajax通信結果が取得出来たならば
215 :   if (itval) {clearInterval(itval);itval=null;} //タイマー変数停止無効化
     // 以前・以後別に処理
216 :   $.each(["before","after"],function(i,b_a){
      // 取得結果がない場合
217 :  if (realElm[b_a]==0)
218 :   getStr[b_a]="<div>"+ (b_a=='before' ? 'Before ' : 'After ') + "Entry "+ decodeURI(encodeURI('はありません。')) + "</div>";
219 :  else {
      // 欠番があった場合の説明文を complement 変数に代入
220 :   complement = (border[b_a]-1 -realElm[b_a]!=0) ? 
221 :    " ( " +decodeURI(encodeURI("欠番があります。")) +" )" : "";
      // 取得結果を HTML 文字列にして変数に代入
222 :   getStr[b_a] = "<div><strong>"+ (b_a=='before' ? 'Before ' : 'After ') + realElm[b_a] + " Entries" + complement +"</strong></div><ul>" + html[b_a].join('') + "</ul>";
223 :  }
     // HTML 文字列化された取得結果文字列を所定のタグに挿入する。
224 :  $("#"+constStr+"result"+b_a).html(getStr[b_a]);
225 :  if (b_a=="after") {
      // Now loading...文字を隠蔽
226 :   ld.hide();
      // 結果表示ボタンの無効化と半透明化を解除
227 :   $(".getEntryTitles_btn").attr("disabled","").animate({opacity:1.0});
      // 以前タイトルの所要時間を設定(ゼロの時にも対応)
228 :   var beforeTime=$.tr.ajax.before.length ? "<li>以前タイトル取得 Ajax 通信所要時間: "+ ($.tr.ajax.before[realElm.before-1].end-$.tr.ajax.before[0].start)/1000 +" 秒</li>" : "";
      // 以後タイトルの所要時間を設定(ゼロの時にも対応)
229 :   var afterTime=$.tr.ajax.after.length ? "<li>以後タイトル取得 Ajax 通信所要時間: "+ ($.tr.ajax.after[realElm.after-1].end-$.tr.ajax.after[0].start)/1000 +" 秒</li>" : "";
      // 所要時間を表示するためのHTML文の作成
230 :   var AjaxLog ="<ul style='margin-bottom:5px'>"+
231 :    "<li>クリック後 Ajax 通信開始迄の所要時間: "+($.tr.ajax.recent.start-$.tr.start)/1000 +" 秒</li>"+
232 :    "<li>最新タイトル取得 Ajax 通信所要時間: "+ ($.tr.ajax.recent.end-$.tr.ajax.recent.start)/1000 +" 秒</li>"+ beforeTime + afterTime +
233 :    "<li>このプロジェクト全体の所要時間: "+ (($.tr.end=now())-$.tr.start)/1000 +" 秒</li></ul>";
      // ログ閲覧回数の記録
234 :   cmt++;
      // 一度目の所要時間表示文字列
235 :   var cmtbtn1="<button id='cmtbtn1' onclick='this.blur();window.open(\"http://hkom.blog1.fc2.com/blog-entry-631.html\",target=\"_blank\")'>この Ajax 通信や所要時間について</button>";
      // 二度目以降の所要時間表示文字列
236 :   var cmtbtn2="<div id='cmtbtn2'>" +(cmt-1)+" 回目の Reload 結果</div>";
237 :   lg.append(AjaxLog+(cmt==1 ? cmtbtn1:cmtbtn2)); // 経過時間ログの挿入
      // 結果表示エレメントの表示
238 :   pD.css({width:"640px",height:"auto"}).show();
      // 通信終了を知らせる文字列の表示(アニメーション)
239 :   fn.show().children().eq(0).fadeIn("slow",function(){$(this).css({color:"darkgreen",background:"pink"})}).fadeOut("slow").fadeIn("slow",function(){lg.show()});
240 :  }
241 :   });
242 : }
243 :};
244 :
   // 一連のAjax通信関数を起動する関数
245 :var makeLists = function(num){
    // popup小窓のアニメーション表示のため
246 : doExplodeShrink(aP,640,600,0,true,"block");// ここでサイズ指定。最外側の div 要素を表示
247 : doExplodeShrink(mB,616,50,12,false,"block"); //OK42=Firefox.Opera,but50=forIE,
    // 結果表示用のボタンの無効化と半透明化並びにその他のボタンの有効化等
248 : $(".getEntryTitles_btn").each(function(i){
249 :  i<3 ? $(this).attr("disabled","disabled").animate({opacity:0.4})
250 :   : $(this).attr("disabled","").animate({opacity:1.0});
251 : });
252 : $.each(["recent","before","after"],function(i,n){ // 結果を格納する変数の初期化
253 :  getStr[n]=""; html[n].length=0;
254 : });
255 : makeRecentEntryList(num); // 最新エントリイ情報取得開始
    // Ajax通信開始待機関数の定義
256 : var nextAjaxTimer = function(){
257 :  $.tr.waitRecentAjaxCnt++; // 回数を記録
258 :  if (jQuery.active==0) { // Recent Entry Titles 情報の取得が終わったら
259 :   if (timer) {clearInterval(timer);timer=null;}
      // 「以前」情報取得のためのAjax通信関数起動
260 :   makeEntryList("before",num); // ここは連続して履行しても
      // 「以後」情報取得のためのAjax通信関数起動
261 :   makeEntryList("after",num);  // 問題なく取得結果を html 化できる。
262 :  }
263 : };
    // タイマー起動
264 : var timer = setInterval(nextAjaxTimer,100);
265 :};
266 : makeLists(10); // 一連の関数を起動
267 :})(jQuery); // 引数 jQuery で無名関数を起動
268 :} else alert("閲覧中のサイトは FC2 ブログではないか、Fc2 ブログであっても個別エントリイ表示モードではありません。\nこのプロジェクトは、Fc2ブログの個別エントリイ表示モードの場合のみ使用できます。");
269 :

jQuery()の挙動を解読する(16) Event 関連の各種クラスメソッド──jQuery解読(27)

jQuery Event 解読 contents
全てのエントリイが ver1.2.2 対応に改訂済みです。
  1. Eventオブジェクトの抽象性と特定化・個別化の必要性
  2. Event API の概要と $(args).eventtype() メソッド解読
  3. $(args).bind()、$(args).one()、$(args).toggle()、$(args).hover() 解読
  4. $.event クラスメソッドの概要と $.event.add()、$.data() 解読
  5. $.event.handle()、$.event.fix() 解読
  6. $(args).trigger()、$(args).triggerHandler()、$.event.trigger() 解読
  7. $(args).unbind()、$.event.remove()、$.removeData() 解読
  8. $(document).ready(f)、bindReady()、$.ready() 解読
  9. $.event.special[t].setup()、$.event.special[t].mouseenter()、$.event.special[t].mouseleave() 解読 (t="type")──これらのメソッドは ver1.2.2 で追加された
  10. 終わりに

$.event クラスメソッド 及び イベント処理に係る jQuery クラスメソッド等について

jQuery.js の Event 処理を理解するためには、関連するインスタンスメソッドから呼び出される各種のクラスメソッドや、その中から呼び出される関数を理解しなければなりません。というよりも、Event 関連の全てのインスタンスメソッドが直接・間接的にクラスメソッドを呼び出して定義されているので、jQuery.event クラスメソッドを理解しなければ jQuery.js のイベント処理は全く理解出来ないでしょう。

併せて、jQuery.event クラスメソッドから呼び出される jQuery クラスメソッドも理解しなければなりません。

そこで、ここでいうクラスメソッド等が何を指しているのか、まず明らかにする必要があります。

なお、「イベントハンドラー」という言葉は、イベントオブジェクトを操作するための3つのオブジェクトのセット──イベントタイプ、dataオブジェクト及びイベントハンドラー関数──を指しています。

Event 処理に不可欠な クラスメソッド等一覧
  1. $.event.add()…… eventtype()、bind()、one() 及び bindReady() の各インスタンスメソッドから直接・間接的に呼び出される。イベントハンドラーを bind (登録)するメソッドであり、jQuery.jsにおいてイベントを処理するための根幹を為すメソッド。
    なお、このメソッドは他に、clone() インスタンスメソッドからも呼び出される。
  2. $.event.remove()……unbind() メソッドから呼び出される。add()より bind されたイベントハンドラーを削除するメソッド。
  3. $.event.trigger()…… trigger() 及び triggerHandler() から呼び出されるメソッド。特定した要素ノード(特定されていない場合には全ての要素ノード)において、既に登録されているイベントハンドラーを指定して起動させる。
    なお、このメソッドは AJax 用の2つのクラスメソッド──ajax() 及び handleError()── からも呼び出される。
  4. $.event.handle()……add()メソッド及びその中の addEventListner や attachEvent() メソッドによって、DOM読み込み完了時には各種のイベントハンドラーは bind 済みとなる。 handle()メソッドは この bind 済みのイベントが発生した場合に起動されるメソッドであり、data()により定義され、管理される jQuery cache オブジェクト内から、イベント発生毎に対応するハンドラー関数を選択して、実行させるメソッドである。
    ※ 非常に分かりにくい説明であるが、簡潔に言うとこうなってしまう。
  5. $.event.fix()…… handle() クラスメソッドから呼び出される。イベントオブジェクト及びその各種プロパティをクロスブラウザ対応に正規化する。
  6. $.event.special["type"].setup()…… ver 1.2.2 で新設され、type には ready、mouseenter 及び mouseleave の3つのイベントタイプ名が入る。
    setup() はこれらのイベントを登録するメソッドである。
    例えば、$(document).bind("ready",fn) でも $(document).ready(fn) メソッドと同様の効果を得られるよう ver1.2.2 で追加されたが、この場合にこの setup() メソッドが呼び出される。
    また、mouseenter/leave イベントは、setup()メソッドによりIE 以外のブラウザの場合には mouseover/outに置換される。
  7. $.event.special["type"].teardown()…… ver 1.2.2 で新設され、 bind() で登録された ready、mouseenter/leave イベント及び mouseover/out イベントを unbind するためのメソッド。
  8. $.event.special["type"].handler()…… mouseenter/leaveイベントはIE 以外のブラウザの場合には mouseover/out イベントに置換されてバインドされるが、この時のイベントハンドラー関数で、 event オブジェクトの type プロパティ名置換(mouseover → mouseenter、mouseout → mouseleave)や、mouseenter/leaveイベントハンドラー関数の起動を行う関数。
  9. $.data()…… add()、remove() 及び handle() メソッドから呼び出される、イベント処理に欠かせないクラスメソッド。イベントハンドラーの選択・特定化の為に利用する。
  10. $.removeData()…… remove() メソッドから呼び出されるイベント処理に欠かせないクラスメソッド。$.data()と対を為すメソッドで選択・特定化されたイベントハンドラーの削除の為に利用される。
  11. $.ready()…… jQuery.js 内の数少ない名前付き関数である bindReady() 関数から呼び出される。DOM 読み込みが確かに完了したかどうか確認し、jQuery.readyListプロパティに未実行の登録関数が存在していれば、それを実行する。また memory leak を避けるための処理を行う。

以後のエントリイにおいて、以上のメソッドを全て解読する予定ですが、まずこのエントリイでは add() メソッドと、それを理解するために不可欠な $.data() クラスメソッドを解読します。

▲ToTop

$.event.add() メソッド 【ver1.2.2 で若干改訂された】 解読

タイトルで言うところの「 ver 1.2.2 における若干の改訂 」とは、 $.event.special プロパティの新設(mouseenter/leave 等への対応など)とセットになったイベントバインドに係る部分の変更を指しています。

以下で言う「中域」とは独自に作った用語で、所謂 global でもなく、かといって地域変数でもない名前空間です。それは jQuery.js コード全体を包含する無名関数内における最上位の名前空間を意味しています。他に適当な言葉が見つからないため「中域」としました。

中域変数は jQuery .js 内においてどこからでも利用できる変数であり、同時に jQuery.js 内のメソッドや関数を利用し終えた後にはメモリから消え去り、利用することが出来ない変数です。

jQuery.js 内ではこの空間を Namespaced Spaceと呼んでいます。

中域などと造語せずに、「jQuery名前空間」と呼ぶのが最適かも知れません。

さて、このメソッドは jQuery.js のイベント処理のスタートを切り、かつ根幹を為すメソッドです。このメソッドがイベントハンドラーを目的とする要素に bind(登録)するからです。

1. IEバグ対策など
1804: add: function(elem, types, handler, data) {
    //elem がテキストノードとコメントノードだった場合には何もしない。
1805:  if ( elem.nodeType == 3 || elem.nodeType == 8 )
1806:   return;
1807:
    //IE バグ対策として引数 elem を window とする。
    //なお、window だけではなく各要素の setInterval メソッドが利用出来
    //ない IE とは、どのバージョンなのかは調べ切れていない。

1808:  // For whatever reason, IE has trouble passing the window object 1809:  // around, causing it to be cloned in the process 1810:  if ( jQuery.browser.msie && elem.setInterval != undefined ) 1811:   elem = window; 1812:
2. guid 番号の設定

地域変数 handler(つまりイベントハンドラー関数)に、ユニークな番号(guid:1 から始まる整数値)を振る。この結果 guid の最終値は、当該頁においてユーザーが登録したイベントハンドラー関数の数マイナス1を表すことになる。(guid には post-increment演算子が利用されているので、値は式適用後には加算されるため)

1813: // Make sure that the function being executed has a unique ID
1814: if ( !handler.guid )
1815: handler.guid = this.guid++; // =jQuery.event.guid
1816:
3. data 引数が存在する場合の或る措置
 /* data が与えられている時には function(){fn.apply(this, arguments);} を
  * handler に代入し、handler の data プロパティに data を、guid プロパティ
  * に guid を、各々代入する。
  * ここで行っていることは、かなり回りくどい方法を取っているが、要は
  * 引数 data オブジェクトの引数 handler 関数オブジェクトへの関連づけである。
  * また handler は後述するように、指定した DOM ノードに type イベントと
  * セットになって bind 済みのイベントハンドラー関数であり、イベントハンド
  * ラーリストに登録された関数となる。
1817: // if data is passed, bind to handler
1818: if( data != undefined ) {
1819: // Create temporary function pointer to original handler
1820: var fn = handler;
1821:
1822: // Create unique handler function, wrapped around original handler
1823: handler = function() {
1824:  // Pass arguments and context to original handler
1825:  return fn.apply(this, arguments);
1826: };
1827:
1828: // Store data in unique handler
1829: handler.data = data;
1830:
1831: // Set the guid of unique handler to the same of original handler, so it can be removed
1832: handler.guid = fn.guid;
1833: }
4. 固有の要素イベント構造の設定
/* 第一引数として add() の elem を、第二引数として文字列 "events"を与えて
 * $.data() メソッドを実行する。しかし、この結果は undefined となり、論理演算
 * 子 || の働きで、続けて jQuery.data(element, "events", {}) が実行される。
 * こうして jQuery.cache[ id ][ "events" ] に {} が代入され、この空オブジェク
 * トが地域変数 events にも代入される。
 * ここに、id とは $.data() によって定義される uuid 番号であり、イベントが
 * 登録された各々の element 要素に対応する固有の整数値となる。
 * また jQuery.cache[ id ][ "events" ] は、これ以降のコードによってイベント
 * 名やイベントハンドラー関数をそのプロパティとして保有することになるオブ
 * ジェクトである。
 * いずれにせよ、この部分は $.data() クラスメソッドが分からないと全く理解出来
 * ない。そこでエントリイ後半で $.data() クラスメソッド について解読する。
 */
1835: // Init the element's event structure
1836: var events = jQuery.data(elem, "events") || jQuery.data(elem, "events", {}),

/* 次も$.data()メソッドを利用しているので、ここでは結論だけを記す。
 * jQuery.data(element, "handle", function(){・・・} により、地域変数 handle 
 * には jQuery.cache[ id ][ "handle" ] が代入される。ここの id 番号は 上の
 * events に代入された jQuery.cache[ id ][ "events" ] の id と同じ値である。
 * また、jQuery.cache[ id ][ "handle" ] には 1837-1849行までの無名関数が
 * 登録される。
 */
1837:  handle = jQuery.data(elem, "handle") || jQuery.data(elem, "handle", function(){
1838:   // returned undefined or false
1839:   var val; // 地域変数 val の初期値を undefined 又は false とする。
1840:
     // jQuery が未定義か、triggered が true ならば undefined か false
     // を返す。こうして、これらのケースの場合に新規のイベントの bind
     // (登録)が行わなわれないようにしている。
1841:   // Handle the second event of a trigger and when
1842:   // an event is called after a page has unloaded
1843:   if ( typeof jQuery == "undefined" || jQuery.event.triggered )
1844:    return val;
1845:

/* element 要素上で、jQuery.event.handle() メソッドを実行する関数を 地域変数
 * valに代入する。ここでは apply メソッドが実行される訳ではなく、定義され代
 * 入されるだけである。ここに、上の if が成立しなければ、つまり
 * typeof jQuery != "undefined" && !jQuery.event.triggered ならば
 * 1846 行で val 値に代入された関数が、1848 行により地域変数 handle に
 * return される。この val に代入された関数は、イベント発生時に、そのハンド
 * ラー関数を選択・確定し、実行するメソッド jQuery.event.handle() メソッド
 * を呼び出す働きをする。
 */
1846:   val = jQuery.event.handle.apply(arguments.callee.elem, arguments);
1847:
1848:   return val;
1849:  });

/* 次のブロックは ver 1.2.1 ではなかった。
 * elem をハンドラー関数のプロパティに代入している。
 * メモリーリークを避けるため、と記されているが、この一行が必要となる具体的
 * な現象や理由は不明にして分からない。
 */ 
1850: // Add elem as a property of the handle function
1851: // This is to prevent a memory leak with non-native
1852: // event in IE.
1853: handle.elem = elem;
1854:
5. イベントハンドラーの管理
/* ver1.2.2 から一度に複数のイベントタイプを登録出来るようになった。
 * 1856 行はその例示である。
 * これにより、同一ハンドラー関数を同一要素の複数イベントタイプに登録したい
 * 場合に、少し簡潔なコード記述が出来るようになった。
 */
1855: // Handle multiple events seperated by a space
1856: // jQuery(...).bind("mouseover mouseout", fn);

/*「Namespaced event handlers」という言葉が登場するが、これは jQuery名前空間
 * 内のイベントハンドラーとでも訳すのが最適だろう。
 * ver1.2 以降の jQuery.js においては、グローバル変数である jQuery 内に、
 * イベントを制御するために、data() メソッドを駆使して jQuery 固有の機構を設け
 * た。それは jQuery.cache オブジェクト内の様々なプロパティを通じてイベント
 * ハンドラーを管理するものである。
 * ここでは、その1つの準備としてカスタムイベントを制御するためのプロパティ
 * として handler 関数に type プロパティを定義し、その値にカスタム type 
 * 文字列を代入している。
 */
1857: jQuery.each(types.split(/\s+/), function(index, type) {
1858:  // Namespaced event handlers
    // type 文字列から" . "を分割文字として配列を作る。
1859:  var parts = type.split(".");
1860:  type = parts[0]; //地域変数 type に type 文字列を代入する
    //カスタム type 文字列を、handler 関数の type プロパティに代入する。
1861:  handler.type = parts[1];
1862:

/* 当該イベントに既に登録されたイベントハンドラー関数のリストを地域変数
 * handlers に代入する。
 * ここでも $.data() を理解しないと events[type] の具体的な意味は分からない。
 * events[type] は 1836 行の data() メソッドによって生成されるからである。
 * そして、1875-1878行が実行されない限り、(つまりイベントの登録が終わらな
 * い限り)events[type] は undefined となり、その時には空オブジェクトを代入
 * する。これにより当該要素ノードへの type 名別にイベントハンドラー関数を管
 * 理する。
 */
1863:  // Get the current list of functions bound to this event
1864:  var handlers = events[type];
1865:
1866:  // Init the event handler queue
1867:  if (!handlers) {
1868:   handlers = events[type] = {};
1869:
6. イベントハンドラーの bind (登録)

ver1.2.2 における Event 関連コードの大きな改訂の1つはこの部分である。

jQuery.event.special プロパティを新設し、中でブラウザ別に mouseover/out と mouseenter/leave を使い分けているのだが、1873 行はそのことと一体となっている。

この部分では jQuery.event.special に type プロパティが存在しないか、または elem.jQuery.event.special[type].setup() メソッドの返値が false の場合にだけ Listener()などを作動させるようにしている。

まず、special オブジェクト内に存在する type 名は ready と mouseenter/leave だけ(2126-2179)なので、その他の type 名のイベントの時には、1875 行以下が実行される。またブラウザが IE の時には setup()メソッドから false が返されて attachEvent() メソッドが実行される。

他方、IE 以外のブラウザの場合には、複雑な過程を辿る。 setup() メソッド内で、 mouseenter は mouseover に、mouseleave は mouseout に各々イベントタイプ名を変更し、かつ $.event.special.handler()メソッドをハンドラー関数として設定してから、bind() メソッドが再帰呼び出しされる。当然 bind()メソッドからは add() メソッドが再帰呼び出しされるので、二重の入れ子構造で処理を行うことになる。

この入れ子構造における処理過程は、別途 special() メソッド解読で詳細に述べることとする。

1870:   // Check for a special event handler
1871:   // Only use addEventListener/attachEvent if the special
1872:   // events handler returns false
1873:   if ( !jQuery.event.special[type] || jQuery.event.special[type].setup.call(elem) === false ) {
1874:    // Bind the global event handler to the element
1875:    if (elem.addEventListener)
1876:     elem.addEventListener(type, handle, false);
1877:    else if (elem.attachEvent)
1878:     elem.attachEvent("on" + type, handle);
1879:   }
1880:  }
1881:

/* 登録済みの handler 関数を固有番号に登録する。
 * 1864 行により handlers = events[type] だから、1883 行は
 * handlers = events[type][handler.guid] = handler ; となる。
 * 1888 行では type 名のイベントが登録されたことを global プロパティに記録
 * させている。
 */
1882:  // Add the function to the element's handler list
1883:  handlers[handler.guid] = handler;
1884:
1885:  // Keep track of which events have been used, for global triggering
1886:  jQuery.event.global[type] = true;
1887: });
1888:
/* 最後にまたしても I E対策である。
 * メモリリークを避けるために elem を空ににしている。
 */
1889: // Nullify elem to prevent memory leaks in IE
1890: elem = null;
1891: },

▲ToTop

$.data(elem,name,data) クラスメソッド

$.data(elem,name,data) の目的あるいは存在理由

jQuery.js は jQuery(args) によって、DOMエレメント集合を要素とする配列を取得します。例えば var tmp = jQuery("p") とすれば、変数 tmp には当該頁の全てのPエレメントノードをその要素とする配列が代入されます。

jQuery.js は、この取得配列から個別のエレメントノードを特定し操作するために、jQuery("p").index(num) や jQuery("p").get(num) などのインスタンスメソッドを用意しています。

また、Pエレメントノードに class 属性が設定すれば jQuery("p.className").get(num) 等により絞り込むことも出来ます。

しかし、これらの既定のメソッドでは、異なる名称のエレメントノードの、(単なる属性ではなく)特定の性質を有する要素集合内の、各要素を特定するには大変面倒な指定が必要となります。そこで考案されたメソッドが $.data() であり、$.remodeData() なのだと、推量します。

このメソッドは find() メソッド内や filter() メソッド内で、第一引数のみを取る data(elment) 形式で利用され id 番号の取得に利用されていますが、これら以外の箇所では全て event を処理するために、2番目以降の引数を与えて使用されています。ですから、このメソッドは event 処理のために導入されたと言っても過言ではないでしょう。

さて、data() メソッドは大変抽象的な内容です。そこで、具体的な内容を知るには event 処理コードにおいてそれが実際に使用されたケースや、その結果としての DOM ツリーを調べることが早道なのではないか、と考えました。実際そうしてみてやっとこのメソッドが理解できたのです。なお、data() メソッドと対を為している removeData() メソッドは 別途取り上げる予定なので、以下では data() メソッドのみを取り上げます。

ある例で event に係る data() メソッドの結果を見てみる

或る Web 頁において、複数のイベントハンドラーを jQuery.js を使って登録し、その直後の DOM ツリーを見てみました。以下の DOM ツリーは jQuery.cache オブジェクトの load イベントと mousemove イベントに係る部分を抜き出したものです。

さて cache オブジェクト内の様々なプロパティは data() メソッドにより作成されたものです。uuid 番号はイベントが登録されたエレメントノードに対応し、それぞれの uuid オブジェクトが必ず2つ──events 及び handle──のプロパティを有しています。そして events オブジェクトは必ず1つ以上のイベントタイプ名のプロパティを持ち、その中に既に bind 済みのイベントハンドラー関数が guid 番号を振られて登録されています。

つまり jQuery.data() メソッドによって作成される jQuery.cache オブジェクトは、必ず
  jQuery.cache[uuid]["events"][event.type][guid] = event handler function
のような構造となります。

$   ↓uuid   ↓guid
└ cache       object
  ├ 1       object  //$.data(elm,name) により振られた $(elm) に対応する uuid 番号1のオブジェクト
  | ├ events   object  //$.data(elm,"events") によって設定されたオブジェクト
  | | └ load  object  //イベントタイプ名のオブジェクト
  | |   ├ 1  function //引数 handler で与えられ、guid番号 1 を割り振られた bind 済みの 関数
  | |   |├ guid 1……1560 行で付与された番号
  | |   |├ data undefined……引数 data が付与された場合のdata
  | |   |└ type undefined……eventtype.custum と指定された場合の custum値
  | |   └ 7  function //引数 handler で与えられ、guid番号 7 を割り振られた bind 済みの 関数
  | |    ├ guid 7
  | |    ├ data undefined
  | |    └ type undefined
  | └ handle   function //$.data(elm,"handle") によって設定された関数オブジェクト
  ├ 2       object  //特定の Elementノードに対応する data() により振られた uuid 番号2のオブジェクト
  | ├ events   object  //data() において name値=eventsで設定されたオブジェクト
  | | └ mousemove  object  //イベントタイプ
  | |   └ 2  function //引数 handler で与えられ、guid番号 2 を割り振られた bind 済みの 関数
  | |    ├ guid 2
  | |    ├ data undefined
  | |    └ type undefined
  | └ handle   function //$.data(elm,"handle") によって設定された関数オブジェクト
  ├ 3 ・・(以下略)

▲ToTop

上のDOMツリーが作成される過程の説明
  1. ユーザーが定義した event 処理関数( jQuery(function(){})を含む )があれば、当該頁が開かれる時に jQuery.js の data()メソッドにより イベントがバインドされた各々の Elementノードに対応して uuid 番号が振られ、jQuery.cache オブジェクト内に uuid 名のオブジェクトが生成される。
  2. その uuid 番号名のオブジェクトの events プロパティに、イベントタイプ( 上の場合には load と mousemmove )プロパティが設定される。
  3. そのイベントタイププロパティに、イベントハンドラー関数が guid 番号名のプロパティで登録される。
  4. 1つの Element に複数のイベントタイプが登録されると、異なる guid 番号のイベントハンドラーが、イベントタイプのプロパティとして登録される。(上の場合には 1 と 7 )
  5. 一方、 uuid 番号が振られたオブジェクトには events プロパティの他に、handle プロパティが設定され、その値として jQuery.js で定義された関数が設定される。この handle プロパティの値である関数こそ、addEventListner()等のメソッドによってイベントハンドラーとして登録され、イベント発生時に駆動される関数となる。

▲ToTop

$.data() コードを分析する【ver1.2.2で変更なし】
   /* 中域変数 expando に当該 jQuery.js ファイルが読み込まれた時の時刻を
    * 記録し、中域変数 uuid(universal unique ID)と 同 win を初期化する。
    * これらの変数は jQuery.js 内の各種メソッドから随時呼び出されて利用さ
   * れる。このため uuid は決して重複せず、++uuidにより呼び出される度に
   * 1ずつ加算される、異なる整数値を提供する。
   */
588: var expando = "jQuery" + (new Date()).getTime(), uuid = 0, win = {};

   /* jQuery.cache を空オブジェクトとして定義する。このオブジェクトの中に
    * 各種の情報を登録していき、それによるイベントハンドラーの管理を可能
    * としている。
    */
640: cache: {},

 /* data()メソッド開始
  * elem が window の場合にはこれを変数 windowData に置き換える。
  * 次に elem の expando プロパティを 地域変数 id に代入する。
  * しかし、当該 elem に最初に data() メソッドを適用した場合には
  * このプロパティは存在しないから id 値は null 値となる。
  * 一方 elem[ expando ] が存在している場合にはその値がそのまま
  * id 値となる。
  */
642: data: function( elem, name, data ) {
643:  elem = elem == window ?
644      windowData :
645      elem;
646:
647:  var id = elem[ expando ];
648:

 /* 或る element に最初に data メソッドを適用した場合には id は null 
  * 値なので、地域変数 id と elem オブジェクトの expando プロパティに
  * uuid 番号を代入する。
  * id が存在すれば( 同じ element に対して二回以上 data メソッドを
  * 適用した場合等)何もしない。
  */
516:  // Compute a unique ID for the element
517:  if ( !id )
518:   id = elem[ expando ] = ++uuid;
519:

 // name 引数が存在し、かつ jQuery.cache オブジェクトの id 属性が存在
 // しなければ jQuery.cache オブジェクトの id 属性に空オブジェクトを登録する。
653:  // Only generate the data cache if we're
654:  // trying to access or manipulate it
655:  if ( name && !jQuery.cache[ id ] )
656:   jQuery.cache[ id ] = {};
657:

    // dataが定義されていれば
    // jQuery.cache[ id ][ name ]プロパティに data を代入する。
    // data が未定義の場合 jQuery.cache[ id ][ name ] は未定義となる。
658:  // Prevent overriding the named cache with undefined values
659:  if ( data != undefined )
660:   jQuery.cache[ id ][ name ] = data;
661:

 // name引数があれば jQuery.cache[ id ][ name ] を、なければ id 値を返す。
 // この結果 name があって data がない場合には未定義値 undefined が返される。
662:  // Return the named cache data, or the ID for the element
663:  return name ?
664:   jQuery.cache[ id ][ name ] :
665:   id;
666: },

▲ToTop

data() メソッドの要点
  1. まず、647行の id = elem[ expando ] = ++uuid; により id 値と elem[ expando ] 値に同一の ++uuid 値が代入されます。かつ、こうして一度 uuid が設定された要素に対しては、650行により異なる uuid は付与されません。
  2. 次に、656 行の jQuery.cache[ id ] = {}; により空オブジェクトである cache のid 属性値として空オブジェクトが設定されます。ここでも一旦 jQuery.cache[ id ] = {} が設定されれば、655行により、同じ name 値に対して二度と履行されません。
  3. そして引数に data が存在すれば、660 行によってそれが jQuery.cache[ id ][ name ] に代入されます。
    これにより固有の id 値と 任意の name 値によって、或る data を特定できる情報が cache オブジェクトに代入されることになります。
  4. 最後に、name があれば data が、name 値がなければ id 番号が return されます。
    これにより任意の element に対して固有の id 値を振ったり、あるいは固有の id 値と任意の name 値に対して data を設定することが出来る訳です。

jQuery()の挙動を解読する(14) Event(2) Event API の解読──jQuery解読(25)

何はともあれ、まず jQuery.js がイベントを処理する Event API のコードを順に解読してみようと思います。

そのためにまず以下に、event 関連コードの全容を自分なりに整理して外観します。

Event API の概要

Event API として次の9つが jQuery 本家サイトで説明/例示されています(順番は一部変更しました)。

言うまでもなくこれらは全てjQueryインスタンスメソッドであり、 $(args) にマッチした DOM エレメントに対して各メソッドがイベントを様々に処理します。

凡例 : args は引数、fn は関数、fn の後の数字は複数のfn を区別するための便宜上のサフィックスです。

なお、インスタンスメソッド内ではいくつかの $.event クラスメソッドが呼び出されます。そしてこれらのクラスメソッドこそ jQuery.js のイベント処理の根幹を為すものです。これらのクラスメソッドについては別のエントリイで解読することとします。

jQuery.js Event API コードの概要

   API  ─  コードの概要(利用される Event 関連メソッド)
   ※ API の概略機能

概要を示すためなので説明に記述した Javascript コードは敢えて文法に従っていません。

  1. $(args).type(fn)─ fn ? this.bind(type,fn) : this.trigger(type)

    ※ type 名のイベントとイベントハンドラー関数 fn を $(args) に登録(bind)し、$(args)で指定した要素ノードで type イベントが発生した場合に fn を起動する。
    もし fn がなければ、既に登録済みのイベント「type」を起動する。

  2. $(args).bind(type,data,fn)─ this.one(type,fn) 又は $.event.add(this, type ,fn , data)

    ※ type 名のイベント、data、fnハンドラー関数を $(args) に登録し、かつ $(args) でtypeイベントが発生した場合に、fn 関数を起動する。ここに data は event オブジェクトの data プロパティに渡される optional オブジェクトで fn を通じて付加的情報をイベントに追加できる。

  3. $(args).one(type,data,fn)─ $.event.add(this, type, function(e){this.unbind(); this.fn}, data)

    ※ bind 同様に登録し、しかし1回だけの実行でそのイベントハンドラーを unbind する。

  4. $(args).unbind(type,fn)─ $.event.remove(this.type,fn)

    ※ type 名のイベントとハンドラー関数の登録を $(args) から解除する

  5. $(args).trigger(type,data,fn)─ $.event.trigger(type, data, this, true, fn)

    ※ $(args) に登録済みの type名 のイベントを optional data (配列)付きで 励起させる。またその際に fn 関数が記されていればこれも同時に起動させる。

  6. $(args).triggerHandler(type,data,fn)─ $.event.trigger(type, data, this[0], false, fn)

    ※ 1つの要素ノードに登録されたイベントハンドラー関数をブラウザの既定の動作を行わせずに起動させる。この際、 fn 関数が記されていればこれも同時に起動させる。

  7. $(args).toggle(fn1,fn2)─ return (fn1 || fn2).apply(this.[e])

    ※ 対象ノード $(args) に click イベントと2つのイベントハンドラー関数( fn1,fn2 )を登録し、対象ノードが click された時に、関数 fn1 と fn2 を交互に起動させる。

  8. $(args).hover(fn1,fn2)
      ─ return this.bind('mouseenter ,fn1).bind('mouseleave',fn2)

    ※ マウス Enter / Leave 時のハンドラー関数 fn1、fn2 を $(args) に登録し、
    併せてイベント発生時にそれらの関数を実行する。

  9. $(args).ready(fn1)─ bindReady();
      document.fn1(jQuery) ||
       $.readyList[].push(func(){return fn1.apply(this, [jQuery])});

    ※ DOMツリー読み込み完了時に fn1 関数を実行する。完了していなければ readrList 配列に fn1 関数を登録する。

ここに、これらのインスタンスメソッドは、おしなべてjQuery.fn.extend()メソッドで登録されてますので、全て jQuery.prototype プロパティに登録されます。その結果 jQuery.js の読み込みが完了した時点で、これらのメソッドの全ては「使用可能」となっています。

▲ToTop

$(args).eventtype() メソッド解読

このメソッドが jQuery.js のどこにあるのか、最初は戸惑いました。 jQuery.js のEvent 関連メソッドは add() から始まりますが、「捜索」してもなかなか「らしい」メソッドが登場しないのです。

そしてやっと見つかったと思っても、そのコードは大変複雑「怪奇」、難解です。何回(苦笑)眺めたか知れません。その何回も眺めた難解なコードは以下の通りです。

2344:jQuery.each( ("blur,focus,load,resize,scroll,unload,click,dblclick," +
2345: "mousedown,mouseup,mousemove,mouseover,mouseout,change,select," +
2346: "submit,keydown,keypress,keyup,error").split(","), function(i,name){
2347:
2348: // Handle event binding
2349: jQuery.fn[name] = function(f){
2350: return f ? this.bind(name, f) : this.trigger(name);
2351: };
2352:});

上の「個々のイベントタイプ名のメソッドを定義する」コードは次のような仕組みになっています。

  • 2344行の ("blur から 1982 行の split(",") 迄は、イベントタイプ文字列を配列に納めるコードです。
  • each() メソッドにより、この配列の個々の要素 name に対して function(i, name) が適用されます。(2348-2350行)
  • その関数内では、第一に jQuery.fn つまり jQuery.prototype プロパティに name 、つまり個々のイベントタイプ名がプロパティとして登録されます。例えば blur イベントタイプの場合には、jQuery.prototype.blur となります。(2349行)。
  • そして各イベントタイプ名のプロパティには、f をたった1つの引数とし、 f ? this.bind(name, f) : this.trigger(name) を返値とする無名関数がその値として付与されます。(2350行)
  • この無銘関数は引数 f(ユーザーによる関数)があれば、this つまり所与のインスタンスのメソッドとして bind (name,f)を 実行してその結果を返し、他方、f がなければ this のメソッドとして trigger(name) を実行してその結果を返します。
  • なお、eventtype() メソッドを prototype オブジェクトに登録することは重要な意味を持っています。jQuery.js の読み込み完了時点でそのメソッドが登録されるからです。

以上により、jQuery.js の読み込みが完了した時点で、jQuery(args).eventtype (fn) メソッドが定義されユーザーが自在に利用するだけの環境が用意されます。

イベント発生時の挙動を決めるコードは・・・

jQuery(args).eventtype (fn) メソッドは、目的の要素オブジェクトに目的のイベントタイプとそのイベントハンドラー関数を bind (登録) するだけではなく、登録された複数のイベントハンドラーを、それぞれのイベントの発生時に呼び出す機能を兼ねています。しかしそのコードはどこにあるのか、ということが次の課題となります。

この件は以後のエントリイで具体的に解明しますが、ここで簡単に結論を書いておきます。

  1. jQuery(args).eventtype (fn) メソッド実行により、jQuery(args).bind()メソッドが呼び出され、そこから更に jQuery.event.add() メソッドが呼び出されます。
  2. この add() メソッド内でお馴染みの addEventListner や attachEvent メソッドが実行され、それらのメソッドの引数として 地域変数 handle が渡されます。
  3. 既に 地域変数 handle には add()メソッド内で jQuery.event.handle.apply(arguments.callee.elem, arguments) 又は false が代入されているので、イベント発生時にはこれが呼び出されます。(1936-1848行)
  4. 呼び出されたこのメソッド内で fix() メソッドが呼び出されてクロスブラウザ対応が施され、かつ、既に登録済みでuuid番号と guid 番号によって特定可能なイベントハンドラー関数が呼び出され、実行されることにより目的が果たされます。

jQuery()の挙動を解読する(13) Eventオブジェクトの抽象性と特定化・個別化の必要性──jQuery解読(24)

jQuery.js のイベント処理

はじめに

jQuery.js のイベント処理コードは Javascript の素人である私には難しいものに思われます。特に jQuery.js Ver1.2 以降で追加されたユーザーによる独自固有名のイベントの登録と操作は、これまでの様々な書籍やWebサイトによる Javascript イベントの学習の次元を超えたものです。

それでも年末から年始に掛けて、561行に亘る jQuery.js のイベント処理コードに食らいつき、それを穴が空くほど見つめ続け、考え抜きました。

まずこうして得られた要点は、

  1. event オブジェクトの抽象性を再認識させられた
  2. それ故のイベントオブジェクトの特定化あるいは個別化措置の必要性について、納得した
  3. Private name for the event handlers については、未だに良く理解し得ない

    jQuery.js Ver1.2 の Release Note には次のように書かれています。
    You can now provide a private name for the event handlers that you bind,・・・.

等です。

event オブジェクトの抽象性

event はタイプ、バインド対象及びハンドラー関数の3つの要素から構成さますが、event オブジェクトは固有名を持たずたった一種類しかありません。そしてそれは HTML 内や Javascript コード内で、直接個別具体的に明示的に見ることは出来ません。event や e あるいは window.event などの抽象的表現でしか現れてきません。

それ故にイベントを理解しにくいのですし、ちょっと囓っただけでは理解出来ない、難解な対象として敬遠されてしまうのではないでしょうか?

jQuery はこの抽象性を処理するために、後述する「 特定化と個別化 」のための方法を採用したのだと思います。

event に係る要素を個別化する必要性

この event オブジェクトの抽象性故に、それぞれのイベントやバインド要素及びハンドラー関数を個別に取得し操作するための、何らかの特定化あるいは個別化が必要です。イベントのタイプ、バインド要素、ハンドラー関数のそれぞれを個別に特定し、相互の関係を有機的に掌握できなければ、当然それらを操作することが出来ないからです。

改めて整理すれば「個別化」が必要な対象は次の3つになります。

  1. event バインド対象要素の個別化
  2. event イベントタイプの個別化
  3. event ハンドラー関数の個別化

jQuery.js ではこれらの各々の個別化のために、Ver 1.2 以降、 jQuery.data() メソッドと jQuery.removeData() メソッドを用意し、かつ固有の ID を2つ用意しました。uuid と guid です。各々 universal unique ID 、global unique ID の略でしょう。(この2つのメソッドと2つの ID は、全て ver1.1.4 迄は存在しませんでした。)

▲ToTop

jQuery()の挙動を解読する。(12) pushStack()解読 upon ver1.3.2──jQuery解読(21)

このエントリイの改訂履歴
  • 初稿:2007/12/04
  • 改訂:2009/3/8…… ver1.3.2 対応とするために全面改訂しました。

pushStack()再考

以前こちら( 簡単なインスタンスメソッドいくつか──jQuery解読(19) )で pushStack() について次のように触れました。

この pushStack() は大変興味深いメソッドです。新しいインスタンスオブジェクトの その名も prevObject プロパティに、直前の this が指し示すオブジェクトが格納されるようにコーディングされています。このメソッドは jquery.js で7箇所利用されています。

このことの意味をもう少し掘り下げてみたいと思います。この pushStack() メソッドはどの様な場合に利用するのか、と言う点を明らかにしたいのです。そうしないとこのメソッドを本当に分かったことにはならないと思うからです。

jQuery(obj) 再解析 upon ver 1.3.2

ところで、1.3.x でこのメソッドの機能が拡張されました。従前のこのメソッドは、新しい jQuery インスタンス( newIns とする)を作成する際に、既に在る別の jQuery インスタンス( これを仮に oldIns とする)を、newIns のプロパティに格納するだけの単純な機能しかありませんでした。

ver1.3.x では pushStack() メソッドの引数を 2 つ増やして 3 つとし、find() メソッドやその他のインスタンスメソッドをそのメソッドの引き数付きで指定出来るようになりました。

この結果、単に newIns のプロパティに oldIns を格納するだけでなく、oldIns に対して様々な jQuery インスタンスメソッドを実行する際に、当該の oldIns とメソッド情報をセットにして newIns のプロパティに格納出来るようになりました。

これにより、end() メソッドで遡れる「過去」が拡張されたはずですが、その件はまだ未確認です。

以下に、こうした機能拡張を踏まえて、改めて pushStack() メソッドのコードを解読してみます。以下の色づけしたコメントがコード進行過程を解析したつもりの解説です。

119: // Take an array of elements and push it onto the stack
120: // (returning the new matched element set)
121: pushStack: function( elems, name, selector ) {
122:  // Build a new jQuery matched element set
    // elems をそのプロパティ値に保持する新しい jQuery
    // インスタンスを作成し、それを ret に代入する。
123:  var ret = jQuery( elems );
124:
125:  // Add the old object onto the stack (as a reference)
    // pushStackの起動元オブジェクトthisを ret オブジェクトの prevObject
    // プロパティに代入する。
126:  ret.prevObject = this;
127:
    // this の contextプロパティ値を ret の context プロパティに代入する。
128:  ret.context = this.context;
129:
130:  if ( name === "find" ) // 第 2 引数が文字列 find の時には
     // 起動元に selector プロパティがあればその値の後に半角スペース
     // を付け、更に 第 3 引数の selector を付けて ret の selector プロ
     // パティ値とする。 例:this.selector = "document", selector = "p"
     // の時には、→ ret.selector = "document p" となる。
     // 尚 this.selector プロパティがなければ、右辺は selector だけとなる。
     // 従って引数 selector がなければ ret.selector = "" となる。
131:   ret.selector = this.selector + (this.selector ? " " : "") + selector;
    // find ではない第 2 引数 name が存在している場合には
132:  else if ( name )
     // 例:ret.selector = "thisSelctor.method(arg)" つまり、thisSelector
     // を起動元とする引数付き method を selector プロパティ値とする。
133:   ret.selector = this.selector + "." + name + "(" + selector + ")";
134:
135:  // Return the newly-formed element set
136:  return ret; // ret オブジェクトを返す。
137: },

▲ToTop

pushStack メソッドによるインスタンスオブジェクトの変化

以上のコメントは機械的な説明に過ぎない感があるので、もう少し具体的・視覚的に分かりやすい表現はないものかと思案して、次のようにプロパティの変化を表示してみました。起動元インスタンスを oldIns、 pushStack メソッドによる elems をプロパティとして持つ新しいインスタンスを newIns とすると、pushStack() メソッドによって、newIns オブジェクトのプロパティは以下のようになります。

newIns = jQuery(sel1.cont1).pushStack(elems,name,selector) によるインスタンスの変化

ここに定義から oldIns = jQuery(sel1.cont1) です。

■ name="", selector="" の場合 newIns は次のようになります。
newIns = {
  prevObject : jQuery(sel1,cont1),
  context : cont1,
  selector : sel1,
  length : N,
  0 : elem1, // elem1 は elemsにマッチする最初のノード
  1 : elem2, // 以下ノードが順に並ぶ。
  ・・・,
  N-1 : elemN,
  jquery : "1.3.2",
  ・・・
}

■ name="find", selector="newsel" の場合 newIns は次のようになります。
newIns = {
  prevObject : jQuery(sel1,cont1),
  context : cont1,
  selector : "sel1 newsel", // findメソッドで使用する検索条件が保持される
  // 以下略
}
■ name!="find", selector="newsel" の場合 newIns は次のようになります。
newIns = {
  prevObject : jQuery(sel1,cont1),
  context : cont1,
  selector : sel1.name(newsel), // sel1の name メソッドが newsel を引数として保持される
  // 以下略
}

▲ToTop

jquery.js における pushStack メソッドの実例を見る

jquery.js における実際の pushStack() メソッドの使われ方をフォローして、学習を深めようと思います。まず、find() インスタンスメソッドを見てみます。このメソッドは呼び出し元オブジェクトの中から、selector 条件にマッチするノードをヒットするためのもので、jQuery オブジェクトのプロパティに、検索結果をセットするために、pushStack() メソッドが使用されています。

なお、 find() メソッドによる検索対象が 1 つの場合と、複数ある場合とに分けてコードが綴られています。

jQuery().find() インスタンスメソッド
288: find: function( selector ) {
    // ▼ this (= jQuery インスタンス) の length プロパティが 1 の場合
289:  if ( this.length === 1 ) {
     // 空配列に対応する jQuery インスタンスオブジェクト( A )を作り、その
     // プロパティに、起動元の jQuery() インスタンスや、find の検索条件
     // である selector の情報を保持する。そして当該 A オブジェクトを
     // 変数 ret に代入する。
290:   var ret = this.pushStack( [], "find", selector );
291:   ret.length = 0; //配列のようなプロパティの length 値を 0 とする。
     // これにより次行で求めるインスタンスの配列のようなプロパティ名は 0 
     // から始まることになる。
     // this[0]、つまり jQuery インスタンスの 0 プロパティ値を対象として、
     // jQuery.find() クラスメソッドを実行し、結果を ret オブジェクトに代入する。
292:   jQuery.find( selector, this[0], ret );
293:   return ret; // 代入された ret を this に返す。
    // ▼ this (= jQuery インスタンス) の length プロパティが 1 でない場合
294:  } else {
295:   return this.pushStack( jQuery.unique(jQuery.map(this, function(elem){
	    // this のプロパティの 1 つ 1 つを対象として走査し、その中
	    // から selector 条件に合致する要素を抽出して map する。つまり、
	    // elem を検索結果で上書きする。重複がないかどうかチェックしてから
296:    return jQuery.find( selector, elem );
     // pushStack メソッドにより生成され、find 結果である elem が登録されている
     // jQuery インスタンスの selector プロパティ値を " elem selector"とする。
297:   })), "find", selector );
298:  }
299: },
slice() インスタンスメソッド

次は、pushStack() メソッドの第 2 引数が find ではない場合です。not() メソッドにもそのようなコードがありますが、より簡単な slice() インスタンスメソッドを見てみます。

■ jQuery().slice() インスタンスメソッド(on jquery.js ver1.3.2)
499:slice: function() {
500: return this.pushStack( Array.prototype.slice.apply( this, arguments ),
501:  "slice", Array.prototype.slice.call(arguments).join(",") );
502:},

上のコードによって返値の jQuery インスタンスオブジェクトはどうなるのか考えてみます。なお、arguments は Javascript の仕様から、配列のようなオブジェクト Arguments を参照するプロパティです。そして slice メソッドの引数となるのですから、例えば [2,5] のような位置を示す数値でなければこの式には意味がありません。

以上を踏まえて slice() メソッドの返値 Insobj は次のようなプロパティを持つことになります。なお、言うまでもなく this は slice メソッドの起動元となった jQuery インスタンスオブジェクトです。

  InsObj:{
    prevObject : this, // 模式的に this と書いたが起動元 jQuery インスタンスのこと
    context : this.context, 
    selector : this.selector.slice( m, n ),
    length : n - m + 1, // augrument=[m,n] (m < n)
    m : this[m],
    m+1 : this[m+1],
    ・・・,
    n : this[n],
    jquery : "1.3.2",
    // 以下略
  },

jQuery()の挙動を解読する。(11) 簡単なインスタンスメソッドいくつか──jQuery解読(19)

このエントリイの改訂履歴
  • 初稿:2007/12/3
  • 改訂:2009/2/28……get()メソッド解読箇所をjquery Ver1.3.2対応に

this の何たるかが一応分かったことを踏まえて・・・

直前のエントリイでthisを分析しました。これを踏まえて早速jQuery()インスタンスメソッド内で、縦横無尽に活用されている this に注目して、挙動解読を行いたいと思います。

ここでは get()、pushStack()、 setArray()、index() を順に取り上げてみようと思います。とりあえず内容が簡単ですから(苦笑)。

なお、以下において共通して InsObj とはインスタンスオブジェクトの意味です。

また、以下のコードリストでは jQuery UI の resizable を利用していますので、サイズは縦横自由に変更できます。W3C の仕様を守らない IE 以外では、このブログに設定されているコンテナ幅を飛び出すリサイズは出来ないので、pre タグの overflow スタイルを auto に設定し、W3C仕様の場合でも全文が見えるようにしました。

get()……目的の DOM Elements を格納した jQuery インスタンスオブジェクトから、目的のノードを要素とする配列を取得する、あるいは num 番目のノードを抽出する。

get() は2つの役割を担っています。まず、jQuery(selector,context) で取得したノードオブジェクトを、配列として取り出す為に機能します。引数を与えずに get() メソッドを実行すればノードを要素とする配列が取得できます。但し、返される配列の要素はノードですから、そのまま alert しても [Object HTMLPagraphElement] のように表示されるだけです。nodeName などの DOM メソッドで展開しないと意味のある結果は表示されません。

例えば次のボタンをクリックすると、このサイト上で alert($("p").get();) が実行されます。このページ上にある p タグの一覧を取得する訳ですが、どの様な結果が表示されるのか試すことが出来ます。

他方、整数値を引数としてget(num)を実行すれば、目的のノードを取得した jQuery インスタンスオブジェクトから num プロパティに登録されているノードが返されます。

<jquery.js ver 1.3.2>
109: // Get the Nth element in the matched element set OR
110: // Get the whole matched element set as a clean array
111: get: function( num ) {
112:  return num === undefined ?
113:
114:   // Return a 'clean' array
115:   Array.prototype.slice.call( this ) :
116:
117:   // Return just the object
118:   this[ num ];
119: },

上で色づけした行が下の Ver 1.2.1 と異なっています。

ここに this は、0 から始まる連番をプロパティ名とし、jQuery("selector") で指定した条件を満足する DOM Element をプロパティ値とする array like なプロパティ群と、jQuery.prototype プロパティ群とで構成されるオブジェクトです。Array.prototype.slice.call( this ) メソッドはこのオブジェクトから array like なプロパティ値だけを 「 配列として 」 抽出します。( → jQueryに学ぶ Javascript の基礎(6) func.apply(obj , array) upon Ver1.3.2──jQuery解読(39) 参照 )

またこの変化した行の意味は以下の引用が簡潔明瞭に説明してくれます。

Array.prototype.slice.call(arguments) というのは Arguments オブジェクトを配列に変換するための決まり文句のようなものです。Array オブジェクトの slice メソッドは、配列の一部を抜き出し新たな配列として返しますが、引数を省略すると 0 番目から length - 1 番目までの要素を抜き出したもの、すなわち元の配列全体のコピーを返します。これを Arguments オブジェクトに適用することで、Arguments オブジェクト全体をコピーした配列が返ってくるというわけです。

( 出典: JavaScript でカリー化、再び: Days on the Moon

<jquery.js ver 1.2.1>
95: get: function( num ) { // 目的とする num 番目の n を引数とし
93:  return num == undefined ? //numが未定義ならば
94:
95:   // Return a 'clean' array
96:   jQuery.makeArray( this ) : //InsObj の中の対象要素ノードの全体を配列
97:                 //に変換して返値とし
98:   // Return just the object
99:   this[num]; //num が与えられていれば InsObj 配列から num 番目を
          //抽出して返値とする。
100: },

pushStack()……対象オブジェクトの変更とバックアップ

102: pushStack: function(a) { //所与の第一引数を受け取り、
103:  var ret = jQuery(a); //それを第一引数とするjQuery(a)を起動して
          //別のInsObjを作成して、それを ret 変数に代入する。
104:  ret.prevObject = this; //その時の this を
        //retのプロパティに代入し、this をプロパティとして
105:  return ret; //持つ新規 InsObj を呼出し元に返す。
106: },

この pushStack() は大変興味深いメソッドです。新しいインスタンスオブジェクトの その名も prevObject プロパティに、直前のthisオブジェクトが格納されるようにコーディングされています。このメソッドは jQuery.js で7箇所利用されています。なお、 pushStack()解読──jQuery解読(21) にて pushStack() の必要性、役割について考えてみました。よろしければご覧ください。

setArray() ……インスタンスデータの初期化と更新

108: setArray: function( a ) {
109:  this.length = 0; //InsObjに代入されている値を全て削除して空にする。
110:  Array.prototype.push.apply( this, a ); //空のInsObjに a 配列の要素を挿入
111:  return this; // a の各要素が代入された InsObj 配列を呼出し元にreturnする。
112: },

index()……取得したノード内でobjが何番目にあるか?

118: index: function( obj ) { //目的のオブジェクトを引数とする
119:  var pos = -1; //pos変数を初期化する。
120:  this.each(function(i){ //InsObj からeach()メソッドを起動
121:   if ( this == obj ) pos = i; //定義からthis=InsObjであり、
122:  });  //その各要素をobjと比較して、objと一致すればその位置を
       //示す i を pos 変数に代入する。
123:  return pos; //合致した位置が代入された pos の値を呼出し元に return する。
124: },

jQueryに学ぶJavascriptの基礎(3) 正規表現──jQuery解読(18)

jQuery解読作業を進めるにつれ、Javascriptの基礎が如何に分かっていないか、ほぼ毎日のように痛感させられます。その意味ではjQuery解読は無謀なチャレンジであった訳ですが、それでも誤謬を犯すマイナスを埋め合わせて遙かに余りあるプラスがあることも毎日自覚されるので、恥を忍んで引き続きjQuery解読を進めるつもりでいます。

しかし、基礎の基礎が余りに分かってない自分に嫌気が指してきてしまい、匙を投げ出すような醜態は演じたくありません。そこで、自戒を込めて敢えて誤解していたこと、理解していなかったこと、不十分な理解に留まっていたことなどについて、つらつらと記述していきたい、と思います。

jQuery.js は正規表現の勉強にも最適である

正規表現文字列の奇々怪々、何がなにやらさっぱり訳が分からない記号の羅列を見れば、this の難解さなど赤子の手をひねるに等しい水準だと言えるでしょう(苦笑)。その奇々怪々な文字列が、jQuery では、様々なシーンで活用されています。

jQuery.js に登場する20箇所以上の正規表現について、ここに論じても余り意味がないでしょうが、普段余り見かけない利用方法について学習を深めることは意義深いと思います。

話題にするのは、3つ。参照対象とする部分文字列と非参照対象の部分文字列、そして非貪欲な繰り返し、です。

部分文字列

それは () で括られた一塊の文字列のことですが、塊として位置づける意味は、後でその文字列を参照したい場合、及び参照はしないが一塊の文字列として認識させたい場合とに分かれるようです。

これらの参照/非参照の部分文字列を指定しているケースについて

その端的な例は次の2例です。(左の数字はjQuery.jsにおける行数です)

 31: var quickExpr = /^[^<]*(<(.|\s)+>)[^>]*$|^#(\w+)$/;
1202: /^(\[) *@?([\w-]+) *([!*$^~=]*) *('?"?)(.*?)\4 *\]/,

色別で部分文字列の区別を分かりやすく表示しましたが、jQuery.js ではこれらの部分指定を活用してその部分を随意に取りだして利用しているのです。

31行の場合の部分文字列の活用内容は既に言及しましたので( インスタンスの初期化:init()──jQuery解読(5)、ここでは1202行について解読しようと思います。

1202行は jQuery.parse プロパティを定義する箇所の一部です。コード本体の説明にあるように、 Match: [@value='test'], [@foo] を探すための正規表現文字列です。なおこのparseプロパティが使用されている箇所はjQuery.filer() メソッドの 1433行1箇所だけです。

さてこの正規表現文字列は、連想配列のキー文字として @value='test' や @foo の形式の文字列を探すために一般化されて定義されたものです。

  1. ^(\[)……先頭文字が [ の意。これは部分文字列1に指定されている。
  2. @?([\w-]+) ……@があるかなくて、その後に単語文字かマイナスが1以上。この1以上の単語文字が部分文字列2に指定されている。
  3. ([!*$^~=]*)……部分文字列3。!、*、$、^、~、又は = がゼロ個以上
  4. ('?"?)……部分文字列4。' がゼロ個以上で、 " がゼロ個以上続く文字列
  5. (.*?)……非参照部分文字列。何らかの文字がゼロ個以上。なおこの ? は後述する「非貪欲な繰り返し」指定のため。
  6. \4……部分文字列4を参照する。

▲ToTop

非貪欲な繰り返しについて

これはむしろ吝嗇な繰り返しとか、貧弱な繰り返しと言った方が分かりやすいのではないかと思いますが、『Javascript 第5版』に倣って「非貪欲な」繰り返しと表現しておきます。

この用法もjQuery.jsで初めて遭遇したもので、Javascript1.5以降に実装されたそうです。繰り返し文字の最後に ? を付けると非貪欲な繰り返しになるのだそうです。例:??、+?、*? 等々。

これが登場するのは上に見た 1202 行 や 778 行で、後者は clean() インスタンスメソッドの中です。

 778:  arg = arg.replace(/(<(\w+)[^>]*?)\/>/g, function(m, all, tag){

行の中程にある [^>]*? です。これは > でない文字のゼロ個以上の非貪欲な繰り返し 、ということになります。つまり、 > ではない文字(列)を見つけた場合の最初の文字だけを意味することになります。

閑話休題───jQuery関連サイトの動向

jQuery解読の合間に・・・・

jQuery 1.2.1 版及び UI の登場してから約2ヶ月が経過しました。そして今、jQueryの評判はますます高まっているのではないか、と勝手に推測しています。

そんな訳で改めてjQuery関連サイトをサーフィンしてみました。確かに一部のサイトでは動きがあるようで最新版を対象として記述を更新したサイトも見つかりました。

有名どころ、と思ったjQuery関連サイト

決して推奨順に並べた訳ではありません。これまでお世話になった順でもあり、またより総合的なサイトを優先して並べたと言うことも可能です。

特筆すべきは古籏一浩氏がずっとVer1.0.3版をベースにしたリファランスを掲載していましたが(1.)、新たに Ver1.2.1 や UI を取り上げた Web 頁を作成したことでしょう。(2.)

なお、私はリファランスには興味がなく、個人的なお勧めは、1.及び2.よりも IBM のWebサイト(5.)です。また3.の開発者向けメモは日本語化の努力成果として大いに歓迎すべきでしょう。

  1. jQuery リファレンス
  2. jQuery入門 (ver 1.2.1)(Last update:2007/10/4 AM 1:00)
  3. jQuery 開発者向けメモ
  4. jQueryの基礎知識 - Emotional Web
  5. jQuery を使って Ajax 開発を単純化する
  6. ウノウラボ Unoh Labs: JavaScriptライブラリといえば jQuery(入門編)

▲ToTop

jQuery() の挙動を解読する(8) jQuery.find() クラスメソッド解読──jQuery解読(12)

  • 初稿:2007/11/14
  • 改訂:2007/11/21……jQuery.js の挙動を FureBug により追跡し、表にまとめた。
  • 改訂:2008/02/03……抜本改訂(上の表は削除した)

jQuery.find() クラスメソッドは何をするのか?

直前のエントリイで jQuery( args ).find( selector, context ) について概観しました。$().find() メソッドは僅か5行しか記述がありませんが、その僅かの記述の中に、 $.map()、$.find()、$().pushStack()、$.unique() 及び $.data()メソッドと、4つの jQuery クラスメソッドと1つのインスタンスメソッドを呼び出しています。ここで最重要なメソッドは jQuery.find( selector,elem ) クラスメソッドで、find の具体的な検索処理はこのクラスメソッドが全部請け負います。

そこでこのエントリイでは、この長大な jQuery.find() クラスメソッドの解読を行います。

jQuery.find() の構造と各種サブルーチンメソッドの役割

jQuery.find() クラスメソッドは、1445-1635 行までの約 200 行もある長大なコードです。そこでまずその構造と各種サブルーチンの役割を概観しておきます。

下の表は$().find()から呼び出される各種のサブルーチンメソッドを登場順に一覧したもので、……より右の文章はそれぞれのサブルーチンメソッドの役割を簡潔に記したものです。

├init()
│ |
│ ├$(context).find(selector)
│ │ ├$.map(this,fn(elem){})……this 配列(=$(context)のインスタンス)の
│ │ │           各要素 elem を fn 関数で指定した内容に変換する
│ │ │└$.find(selector,elem)……elem の中から selector にマッチする要素を抜き出す
│ │ │  ├$.trim(selector)……selector の最初と最後にある空白を削除
│ │ │  ├$.data(elem)……elem に 固有 id 番号(uuid) を振る
│ │ │  ├$.trim()
│ │ │  ├$.merge(a,b)……a, b2つの配列を合体する
│ │ │  ├$.isXMLDoc(elem)……elem が XML要素かどうかを判定する
│ │ │  ├$.merge()
│ │ │  ├$.classFilter(r,m)……r 配列の中から m というclass名のelemを抽出/除外する
│ │ │  ├$.filter(t,r)……r 配列の各要素に t 文字列によるフィルタを適用する
│ │ │  │ ├$.parse……各種のフィルタを検索するための正規表現文字列の配列
│ │ │  │ ├$.filter()……再帰呼び出し
│ │ │  │ ├$(elem).not(sel)……sel に該当する要素を elem 配列から除外する
│ │ │  │ ├$.classFilter()
│ │ │  │ ├$.props……各種属性値の名称統一化のためのオブジェクト
│ │ │  │ ├$.attr……各種属性値を処理する
│ │ │  │ ├$.data
│ │ │  │ ├$.expr……":"に続く文字列などによるフィルタリングを行うためのオブジェクト
│ │ │  │ └$.grep(arr,fn(){})……関数で指定した条件に合う要素をarr配列から抽出する
│ │ │  ├$.trim()
│ │ │  └$.merge()
│ │ └$(args).pushStack(elem)……新たにjQuery(elem)のインスタンスを作り、そのプロ
│ │   │パティ値に今のインスタンスを格納してから新インスタンスを$(args)関数に返す
│ │   └$.unique(arr)……arr配列の要素で重複があれば重複をなくしその結果を受け取る。
│ │     └$.data()

上に見られるように、$.find() の中では、沢山の jQuery クラスメソッド等がサブルーチンとして処理を請け負い、役割分担して、context から selector に該当するエレメント等を検索することになります。

jQuery.find() メソッド全行解読

長大なコードなので内容に応じて適宜分節しながら解読を進めます。

1. 初期処理
1445: find: function( t, context ) {
1446: // Quickly handle non-string expressions
1447: if ( typeof t != "string" ) // tが文字列でない場合には
1448:  return [ t ];       // 配列[t]をリターンして処理を終える。
1449:
1450: // check to make sure context is a DOM element or a document
   // contextが要素ノードでもdocumentでもない場合には空配列を返して処理を終える。
1451: if ( context && context.nodeType != 1 && context.nodeType != 9)
1452:  return [ ];
1453:
1454: // Set the correct context (if none is provided)
1455: context = context || document; //context がなければ document を代入する
1456:
1457: // Initialize the search 検索処理用変数定義
1458: var ret = [context], done = [], last, nodeName;
1459:
2. 検索開始・前処理:(1460-1469)
1460: // Continue while a selector expression exists, and while
1461: // we're no longer looping upon ourselves
 /* 1462 行の while は 1620 行まで 159 行も続く長いフレーズである。
  * この中で文字列 t を様々に分解しながら切り取り、様々なフィルターに掛けて
  * それにヒットするかどうかを検証し、対象を抽出する。
  * そのための初期処理が 1463-1468 行である。
  */
1462:  while ( t && last != t ) {
1463:   var r = []; // 検索結果の要素ノードを格納する配列を準備
 // while 終了準備処置 これ以降の行で t の値が変更されれば last != t となるから 
 // while 処理が継続され、つまり以下の検索が反復される。
1464:   last = t;
1465:   // tの先頭と末尾にある空白文字を削除
1466:   t = jQuery.trim(t);
1467:   // 検索結果有無を示すフラグを「ナシ」とする
1468:   var foundToken = false;
1469:
3-1. 検索ケースA: 子要素 "> chars" 検索(1470-1487)
 /* "> 任意の文字列"形式による検索を行う箇所である。
  * つまり子要素指定が有る場合の抽出作業である。
  * 但し、検索対象文字列 t の先頭文字が > の場合を探しているので、selector
  * 文字列において、既に別の検索が処理された後の二度目以降の検索処理を行う箇所となる。
  */
1470:   // An attempt at speeding up child selectors that
1471:   // point to a specific element tag
     // 先頭文字が > でこれに何らかの文字列が続く場合の正規表現文字列
     //(1356行で定義済み)を re に代入
1472:   var re = quickChild;
1473:   var m = re.exec(t); // 検索対象 t に re が含まれるかチェック
1474:
1475:   if ( m ) { // 検索対象 t に re が含まれれば...
      //m[1] は quickChild の定義から chars=何らかの文字列 を指す
1476:    nodeName = m[1].toUpperCase();
1477:
 /* 検索対象 ret 変数の各対象毎に巡回し、
  * 更に ret[i]の第一子要素を初期値とし、その兄弟要素を巡回する。
  * ノードタイプが要素エレメントで、それが何らかの要素名か、あるいは
  * quickChild 検索で抽出済みのノードネームに等しければ
  * その子要素を r 配列に格納する。
  */
1478:    // Perform our own iteration and filter
1479:    for ( var i = 0; ret[i]; i++ )
1480:     for ( var c = ret[i].firstChild; c; c = c.nextSibling )
1481:      if ( c.nodeType == 1 && (nodeName == "*" || c.nodeName.toUpperCase() == nodeName) )
1482:       r.push( c );
1483:
 /* 取得した検索結果配列を ret に代入する。これにより検索対象配列 ret が
  * 検索結果配列に置換される。
  * 一方、検索文字列 t から 今検索した文字列=「>何らかの文字列」を削除する。
  * 直前検索文字列削除後の t に空白が含まれれば検索を続ける。
  */
1484:    ret = r;
1485:    t = t.replace( re, "" );
1486:    if ( t.indexOf(" ") == 0 ) continue;
1487:    foundToken = true; // 検索結果有無フラグをtrue、つまり検索結果ありとする。
3-2. 検索ケースB: 子要素 "> [0-9A-z_]" 検索(1488-1521)

ケースBでは、(1)先頭に>があり、(2)その後に空白文字が 0 個以上あって、かつそれに(3)数字かアルファベットかアンダースコアが続くような検索文字列であるかどうかをチェックする。

ケースAでは、数字/アルファベット/アンダースコアではない文字、例えば:、,、#、$などが含まれていても構わなかったが、ケースBではこれらの文字は対象とされない。

 /* 正規表現文字列 /^([>+~])\s*(\w*)/iは次のことを意味する。
  * 先頭文字が>、+ あるいは ~ のいずれか(第一部分文字列)であって
  * その後に空白文字が 0 個以上あり、
  * 数字/アルファベット/アンダースコアが0個以上続く文字列を
  * 大文字小文字の区別なく探す
  */
1488:   } else {
1489:    re = /^([>+~])\s*(\w*)/i;
1490:    //t に対する re 正規表現文字列の検索結果を m に代入し、それが空でなければ
1491:    if ( (m = re.exec(t)) != null ) {
1492:     r = []; //空配列 r を準備(検索結果受け取り用)
1493:
1494:     var merge = {}; //空オブジェクト merge を準備(id照合用)
1495:     nodeName = m[2].toUpperCase(); // \w* に該当する文字列を大文字にして
1496:     m = m[1]; // m に第一部分文字列(>、+ あるいは ~ のいずれか)を代入
1497:
       // 検索対象ノードを巡回する。
1498:     for ( var j = 0, rl = ret.length; j < rl; j++ ) {
 /* 第一部分文字が ~ か + ならば(つまり兄弟要素を探す場合には)
  * ret[j].nextSiblingを、また第一部分文字列が > の場合には
  * ret[j].firstChild(第一子要素)を、それぞれ n に代入する。
  */
1499:      var n = m == "~" || m == "+" ? ret[j].nextSibling : ret[j].firstChild;
         // その n 要素の兄弟要素を順に巡回し
          //そのノードタイプが要素ノードならば、それに uuid 番号を振って id に代入。
1500:       for ( ; n; n = n.nextSibling )
1501:        if ( n.nodeType == 1 ) {
1502:         var id = jQuery.data(n);
1503:
 /* 第一部分文字が ~ で(つまり兄弟要素を探していて)既に id 番号が振られた
  * 要素がある場合(正確に言えば既に 1507行で merge[id}==true が与えられて
  * いる場合)には、兄弟要素を検索するループから抜け出す。
  */
1504:         if ( m == "~" && merge[id] ) break;
1505:
 /* nodeName がないか、n の nodeName が先に求めた nodeName と等しい時に
  * 第一部分文字が ~ ならば、merge[id] に true を代入してから
  * 当該の n 要素を r 配列に格納する。
  * またもし 第一部分文字が + だった場合には(直近の兄弟要素を見つけるのだから)
  * 1つ見つかればもう探す必要はないので、兄弟要素を探すループから抜け出す。
  */
1506:         if (!nodeName || n.nodeName.toUpperCase() == nodeName ) {
1507:          if ( m == "~" ) merge[id] = true;
1508:          r.push( n );
1509:         }
1510:
1511:         if ( m == "+" ) break;
1512:        }
1513:     }
1514:
 /* 見つかった要素が入った配列 r を、検索対象要素が代入されている配列 ret に代入
  * し、検索文字列のtから re に代入された部分検索用文字列部分を消去し、かつその
  * 後の t 文字列の先頭と末尾にある空白文字を削除してから、t を更新する。
  * この節の最後に、検索対象有無を示すフラグを true とする。
  */
1515:     ret = r;
1516:
1517:     // And remove the token
1518:     t = jQuery.trim( t.replace( re, "" ) );
1519:     foundToken = true;
1520:    }
1521:   } //END OF 1488 else
1522:
4. 検索C:子要素検索ではない検索

以上の検索に引っかからない場合(つまり子要素を探すのではない場合)、次の検索処理が 1525 行から 1611 行までで行われる。この節での検索は selectors による複数条件指定、.className 指定、#idName 指定の場合が処理される。

1523:   // See if there's still an expression, and that we haven't already
1524:   // matched a token
1525:   if ( t && !foundToken ) {
 :     ・・・・・
1611:   }
4-1. 先頭文字がカンマである場合の検索前処理(1527-1539)

まず t が存在していて、検索結果有無フラグが false の時、1526-1539行でカンマを t から削除する処理が行われる。なおここでは検索そのものは行わない。

1526:    // Handle multiple expressions
1527:    if ( !t.indexOf(",") ) { // t の先頭文字が "," の場合
 /* そもそも、検索文字列にカンマが入るのは 
  * jQuery(context).find( selector1,selector2,・・・・,selectorN ) が履行されている時
  * である。
  * さて、ret 配列には 対象要素エレメントの集合である context が代入されている。
  * もし、ret 配列(それは [context] に等しい)の最初の要素が context そのものに
  * 等しいならば、元々 context に1つの要素しかなかったことになる。
  * つまり1つの要素ノードを対象として複数のパターンのセレクターを指示していること
  * になる。この時にはdoneに2つの空要素を代入することによって、jQuery.find() から
  * の返値を「なし」としている。
  */
1528:     // Clean the result set
       // context が1つの要素しかない場合、retを空白配列とする
1529:     if ( context == ret[0] ) ret.shift();
1530:
1531:     // Merge the result sets
       // 空配列を第一要素とし、それにret(これも空配列)が続く配列を done に代入する。
       // つまり done == [ [], [] ] とする。
1532:     done = jQuery.merge( done, ret );
1533:
1534:     // Reset the context
       // r と ret に [context]配列を代入
1535:     r = ret = [context];
1536:
1537:     // Touch up the selector string
       // 検索文字列 t の先頭文字(これはカンマである)を削除し、替わりに
       // 半角スペースに置き換える。これにより別の節での検索対象としている。
1538:     t = " " + t.substr(1,t.length);
1539:

次の検索処理は、いくつかの前処理が行われてから行われる。

4-2-1. "nodeName#idName" 検索の場合の検索前処理
1540:    } else {
1541:     // Optimize for the case nodeName#idName
       // 文字通り「ノードネーム#idネーム」の文字列を探す
1542:     var re2 = quickID; // 1357行で定義されている正規表現文字列
1543:     var m = re2.exec(t); //検索結果を m に代入
1544:
1545:     // Re-organize the results, so that they're consistent
       // m があればその要素を次のように並び替える。
       // [ 0, "#", idName, nodeName ]
1546:     if ( m ) {
1547:      m = [ 0, m[2], m[3], m[1] ];
1548:
4-2-2. 先頭文字が "." か "#" の場合の検索前処理
1549:     } else {
1550:      // Otherwise, do a traditional filter check for
1551:      // ID, class, and element selectors
1552:      re2 = quickClass; // 先頭文字に "." または "#"が1個以下ある場合の正規表現文字列
1553:      m = re2.exec(t); // t を re2 正規表現文字列でチェックしその結果を m に代入
1554:     }
1555:
4-2-3. 4-2-1及び4-2-2の場合の更なる検索前処理
1556:     m[2] = m[2].replace(/\\/g, ""); //chars文字列内から \ を削除
1557:
1558:     var elem = ret[ret.length-1]; //elem に検索対象要素配列の最後の要素を代入
1559:
4-3. やっと検索開始
 /* 検索文字配列 m の2番目の値が "#" の場合の検索を行う。
  * 変数 oid にgetElementByIdメソッドの結果を代入する。
  */
1560:     // Try to do a global search by ID, where we can
1561:     if ( m[1] == "#" && elem && elem.getElementById && !jQuery.isXMLDoc(elem) ) {
1562:      // Optimization for HTML document case
1563:      var oid = elem.getElementById(m[2]);
1564:
 /* IE と Opera において発生するバグ( form要素内のname属性の値をid値と誤って
  * 認識するバグ)に対する対策を施す。
  * XPathにおいて「@id は id という名前のアトリビュートを選択する」ことを指すから
  * 1569 行によって、elem 内から id 属性値として m[2] を持つ要素が正しく選別される。
  */ 
1565:      // Do a quick check for the existence of the actual ID attribute
1566:      // to avoid selecting by the name attribute in IE
1567:      // also check to insure id is a string to avoid selecting an element with the name of 'id' inside a form
1568:      if ( (jQuery.browser.msie||jQuery.browser.opera) && oid && typeof oid.id == "string" && oid.id != m[2] )
1569:       oid = jQuery('[@id="'+m[2]+'"]', elem)[0];
1570:
 /* oid が見つかった場合に、m[3]がない、つまりセレクタ文字列が nodeName#idName 
  * 型ではなかった時か、あるいは nodeName#idName 型の時には oid のノードネームが
  * m[3] つまり nodeName に一致すれば [oid]を、そうでなければ空配列を、
  * それぞれ ret と r に返す。ここに空配列とはこの検索ルーチンでは目的の要素が
  * 見つからなかったことを意味する。
  */
1571:      // Do a quick check for node name (where applicable) so
1572:      // that div#foo searches will be really fast
1573:      ret = r = oid && (!m[3] || jQuery.nodeName(oid, m[3])) ? [oid] : [];
4-4. 全ての子孫要素を対象とした検索
 // 全ての子孫要素 (all descendant elements) を対象とした巡回検索を行う。
1574:     } else {
1575:      // We need to find all descendant elements
1576:      for ( var i = 0; ret[i]; i++ ) {
 /* 最初の文字が "#" で nodeName が有れば、その nodeName 名を tag に代入し、
  * そうでない場合、"." があるか m[0] が空白ならば(つまり quickID にも quickClass
  * にもヒットしなかった場合)"*"を tag に代入し、以上のいずれでもない場合には chars
  * 文字列を tag に代入する。
  */
1577:       // Grab the tag name being searched for
1578:       var tag = m[1] == "#" && m[3] ? m[3] : m[1] != "" || m[0] == "" ? "*" : m[2];
1579:
 // tag が "*" で、ノードネームが object だったら tag に "param" を代入する。
1580:       // Handle IE7 being really dumb about <object>s
1581:       if ( tag == "*" && ret[i].nodeName.toLowerCase() == "object" )
1582:        tag = "param";
1583:
 // これまでの r 配列に対して、検索対象内のタグ名が「tag」である要素を追加する。
1584:       r = jQuery.merge( r, ret[i].getElementsByTagName( tag ));
1585:      }
1586:
4-5. class 名による検索
1587:      // It's faster to filter by class and be done with it
1588:      if ( m[1] == "." )
 // classFilterメソッドを利用して r 要素内から m[2] の条件に該当する要素を抽出する
1589:       r = jQuery.classFilter( r, m[2] );
1590:
4-6. ID名による検索
1591:       // Same with ID filtering
1592:       if ( m[1] == "#" ) {
1593:        var tmp = [];
1594:
1595:        // Try to find the element with the ID
1596:        for ( var i = 0; r[i]; i++ )
           // id 属性値が m[2] と等しければ
1597:         if ( r[i].getAttribute("id") == m[2] ) {
1598:          tmp = [ r[i] ]; //tmpにその要素ノードを要素とする配列を代入
1599:          break; //1596行の for ループを中断
1600:         }
1601:
1602:        r = tmp; // r にヒットした要素ノードが入った配列を代入
1603:       }
1604:
4-7. この分節での検索終了措置
1605:       ret = r; //検索対象ノード配列に検索結果を上書き代入
1606:     } //END OF 1574行のelse
1607:     // re2(quickID 又は quickClass正規表現文字列)に該当する文字列を
       // 検索文字列 t から削除する。
1608:     t = t.replace( re2, "" );
1609:    } //END OF 1540行の else
1610:
1611:   } //END OF 1526行の if
1612:
5. 最後の検索処理

子要素でもid名でもclass名でもない検索の場合、filter クラスメソッドを活用して検索を行う。

この filter クラスメソッドもまた長いコードなので、これについては別のエントリイで詳述する。

 // これまでの検索を終えてもなお、t に文字列が残っている場合
1613:   // If a selector string still exists
1614:   if ( t ) {
1615:    // Attempt to filter it
      // 検索対象要素 r に t 文字列によるフィルタを適用し結果を val 配列に代入。
1616:    var val = jQuery.filter(t,r);
1617:    ret = r = val.r; // 配列 valから要素ノードを抽出して ret と r に代入
1618:    t = jQuery.trim(val.t); // 配列 val から検索文字列を取りだして t に代入
1619:   }
1620:  }
1621:
6. find() クラスメソッドの終了処理
 // ここまで来ても t が残っていたらエラーが起こったはずなので、空配列を ret に代入
1622:  // An error occurred with the selector;
1623:  // just return an empty set instead
1624:  if ( t )
1625:   ret = [];
1626:
 // ret がありかつ context が ret の第一項値に等しければ、retを空配列にする
1627:  // Remove the root context
1628:  if ( ret && context == ret[0] )
1629:   ret.shift();
1630:
 // done に ret を併合し、その結果を return する。
1631:  // And combine the results
1632:  done = jQuery.merge( done, ret );
1633:
1634:  return done;
1635: },
return 連鎖がもたらす階層性

上述の第二のケースの場合、65行により新たなjQuery(context)インスタンスが生成され、上述の一連の流れを経てinit()メソッドが終了します。この流れではjQuery()インスタンスが二度作成されるため複雑な動きをします。

そこでインタープリターがどの様に各種メソッドを呼び出しているのかを Firebug を使って追跡・解読してみました。解読結果を以下に図示します。

なお、ここで行った追跡は、次のようなセレクタとメソッドを指定して、ローカルパソコン上のHTMLファイル上で行いました。追跡に使ったセレクタとメソッドは jQuery("p > a").size()です。

  1. jQuery.js がインクルードされるとグローバル変数 jQuery 及び $ 並びにこれら の prototype オブジェクトが生成され、定義されている各種 prototype プロパティが設定される。こうしてinit()メソッドの返値が配列であることも設定される。
  2. ユーザーが jQuery("p > a") の実行をインタープリタに指示する。ここに第一引数の expr は「<・・・>付きの html タグ名でもなく、# 付きの id 名称だけでもない文字列」となる。
  3. 指示されたインタープリタは jQuery()関数の定義に従い、(1)new 演算子を起動して空オブジェクトを生成し、それへの参照をキーワード this に与え、jQuery.prototype.init("p > a").prototypeを生成するする。
  4. ここに 既に 526 行(jQuery.prototype.init.prototype = jQuery.prototype;)により init.prptotype は jQuery.prototype に等しくなっているから、jQuery.prototype.init("p > a").prototype は jQuery.prototype.init("p > a") の実行となる。
  5. init("p > a")は jQuery("p > a") は init("p > a") の返値を受け取り、かつ 1. により init() の返値は配列とされているから jQuery("p > a") のインスタンス(=this)の型も配列となる。
  6. 指示を受けたインタープリタは、 jQuery("p > a") メソッドを実行し、jQuery.js に記述されているコードに従って、return new jQuery( context ).find("p > a"); を実行しようとする。
    しかし、context は与えられていないから、新たに init() コンストラクタが呼び出され、新たなインスタンスの生成が始まる。この時 this の参照先はそれまでの参照先を捨て init( context )コンストラクタによるインスタンスとなる。
  7. init()コンストラクタの引数は39行から documentとなり、42-45行により init()には[document]が返される。
  8. this.setArray(context) が実行されると、jQuery(context) インスタンスは context を唯一の要素とする配列 [context] を生成し、return this.setArray(context) によって当該配列が init(expr,context) メソッドの返値となる。
  9. ところで、この init(expr,context) は return this.init(expr,context) 行より呼び出されたのであるから、当該init()メソッドの返値は、jQuery( context ).find( expr ) コンストラクタに返される。
  10. かくして、やっと .find(expr) メソッドが開始される。

jQuery() の挙動を解読する(2) インスタンスプロパティ・メソッドとクラスプロパティ・メソッド──jQuery解読(6)

  • 初稿:2007/11/03
  • 改訂:2007/11/21……用語を正確に書き換えた
  • 再改訂:2007/11/27……ケアレスミスを修正した。
  • 再改訂:2009/3/20 jquery.js ver1.3.2 対応に

jQuery() と jQuery のそれぞれのプロパティとメソッド

init() 内では、随所に jQuery(s,c).prop や jQuery(s,c).method() と jQuery.prop や jQuery.methop() が多用されています。また init() だけではなく jQuery 全体でこのような表記が沢山出てきます。ここではこれらの差異に着目し、これらが互いに全く別のものであることを確認し、またそれぞれの役割を解明したいと思います.

まず jQuery.prop や jQuery(s,c).method() は jQuery(s,c) インスタンスのプロパティ又はメソッドです。これらは new 演算子とinit() コンストラクタ関数によって生成されるインスタンスオブジェクトのプロパティ又はメソッドであり、このインスタンスは this キーワードで参照出来ます。そしてそれは決して関数オブジェクトである jQuery 自体のプロパティやメソッドではありません

一方、jQuery.prop や jQuery.method() は関数オブジェクト jQuery のメソッドやプロパティであって、決して、new 演算子とコンストラクタ関数によって生成されるインスタンスオブジェクトのそれらではありません。

端的に言えば、前者はインスタンスオブジェクトのメソッドやプロパティであり、後者は jQuery クラスのプロパティでありメソッドです。

ここに、jQuery のプロパティやメソッドは jQuery.extend() メソッドの実行(jquery.js ver1.3.2 の 612 行以降など)により定義されますますが、その extend() メソッドは 562~610 行で定義されています。そして実に巧みだと思うのは、その extend() メソッドは、同時にインスタンスオブジェクトのメソッドとして、定義されていることです( 562 行)。こうして extend() メソッドは2つの機能を担っている訳で、jQuery クラスのメソッド及び jQuery() インスタンスオブジェクトのメソッドを拡張する手段として機能するようになっています。

そして、この拡張方法こそがプラグインを自在に作成することを容易に可能としていることも強調すべき jQuery の利点であり、同時にこの複層的構造が jQuery.js を難解にしている一因ともなっているのではないでしょうか?。

▲ToTop

jQuery() とjQuery.extend()による拡張箇所リスト(該当行数リスト)

extend() メソッドの実行によって jQuery クラスオブジェクトと jQuery()インスタンスオブジェクトの、それぞれのプロパティとメソッドが様々に拡張される訳ですが、それらは次のように多用されています。( 以下は jQuery.js ver1.2.1 非圧縮版 における行数)

▼ jQuery インスタンスオブジェクトの拡張
  • 1855-1941(Event 関係)
  • 2040-2126(Ajax 関係)
  • 2507-2665(Animation 関係)
  • 2904-2991(Offset 関係)
▼ jQuery クラスオブジェクトの拡張
  • 469-1008(基本)
  • 1023-1044(BoxModelチェック)
  • 1139-1565(DOM 関係)
  • 1943-1976(DOM Ready 関係)
  • 2137-2506(Ajax 関係)
  • 2693-2738(Animation 関係)

インスタンスオブジェクトの拡張に 241 行が、そして jQuery クラスの拡張に 1438 行も充てられています。合計すると 1679 行となり、これらに extend() の定義の為の行数 39 行(404-442行)を加えた1718行は、全行数 2965 行の約 58 %を占めることになります。extend 関数はjquery.js のまさにキーとなる関数と言えます。

▲ToTop

jQuery.jsインクルード時に定義されるグローバル変数 jQuery のプロパティとメソッド

インクルード時に定義されるjQueryオブジェクトのプロパティとメソッド図。クリックすると拡大図を表示します。

以上を踏まえて、jQuery.js インクルード時に定義されるオブジェクトやそのプロパティを一覧してみます。例えば firebug の DOM インスペクターでそれを確認することが出来ます。(左図参照)

左図はグローバルオブジェクトとして $ と jQuery が定義されていること、そして jQuery クラスの 64 項目のプロパティ及びメソッドがあることを示すものです。なお、uiプロパティは jqueryui.js をインクルードしているために発生しているもので、jQuery.js だけインクルードした場合には生じません。

▲ToTop

jQuery() の挙動を解読する(1) インスタンスの初期化:init() upon Ver1.3.2──jQuery解読(5)

  • 初稿:2007/10/31
  • 改訂:2007/11/19……thisに関する記述追加、用語の統一化
  • 再改訂:2007/11/23……何カ所かの基礎的なミスを修正
  • 再改訂:2007/12/3……表現等を抜本的に見直した
  • 再改訂:2009/2/22……Ver1.3.2 対応に改訂

init()メソッドの概要解読

このエントリイでは、init コンストラクタによるインスタンスの初期化過程について概要解読を行います。

jquery.js のインクルード完了時には、直ぐにコード全体を包含する無名関数が起動され、インスタンスオブジェクトが作成され、その初期化を int() メソッドが担います。

まず該当箇所のコードを抜粋すると以下の通りです。 ( 行数はVer1.3.2による )

24: jQuery = window.jQuery = window.$ = function( selector, context ) {
25:  // The jQuery object is actually just the init constructor 'enhanced'
26:  return new jQuery.fn.init( selector, context );
27: },
28:
29: // A simple way to check for HTML strings or ID strings
30: // (both of which we optimize for)
31: quickExpr = /^[^<]*(<(.|\s)+>)[^>]*$|^#([\w-]+)$/,
32: // Is it a simple selector
33: isSimple = /^.[^:#\[\.,]*$/;
34:
35: jQuery.fn = jQuery.prototype = {
36: init: function( selector, context ) {
37: // Make sure that a selection was provided$
38:  selector = selector || document;
39: 
40:  // Handle $(DOMElement)
41:  if ( selector.nodeType ) {
42:   this[0] = selector;
43:   this.length = 1;
44:   this.context = selector;
45:   return this;
46:  }
47:  // Handle HTML strings
48:  if ( typeof selector === "string" ) {
49:   // Are we dealing with HTML string or an ID?
50:   var match = quickExpr.exec( selector );
51: 
52:   // Verify a match, and that no context was specified for #id
53:   if ( match && (match[1] || !context) ) {
54: 
55:    // HANDLE: $(html) -> $(array)
56:    if ( match[1] )
57:     selector = jQuery.clean( [ match[1] ], context );
58: 
59:    // HANDLE: $("#id")
60:    else {
61:     var elem = document.getElementById( match[3] );
62: 
63:     // Handle the case where IE and Opera return items
64:     // by name instead of ID
65:     if ( elem && elem.id != match[3] )
66:      return jQuery().find( selector );
67: 
68:     // Otherwise, we inject the element directly into the jQuery object
69:     var ret = jQuery( elem || [] );
70:     ret.context = document;
71:     ret.selector = selector;
72:     return ret;
73:    }
74: 
75:   // HANDLE: $(expr, [context])
76:   // (which is just equivalent to: $(content).find(expr)
77:   } else
78:    return jQuery( context ).find( selector );
79: 
80:  // HANDLE: $(function)
81:  // Shortcut for document ready
82:  } else if ( jQuery.isFunction( selector ) )
83:   return jQuery( document ).ready( selector );
84: 
85:  // Make sure that old selector state is passed along
86:  if ( selector.selector && selector.context ) {
87:   this.selector = selector.selector;
88:   this.context = selector.context;
89:  }
90: 
91:  return this.setArray(jQuery.isArray( selector ) ?
92:   selector :
93:   jQuery.makeArray(selector));
94: },
95: 
96: // Start with an empty selector
97: selector: "",

▲ToTop

RegExp.exec()メソッドのための正規表現文字列用意

31行で定義されている変数 quickExpr は引数 selector に対する正規表現検索のための文字列で、その意味は次のようになります。

「先頭が"<"ではないゼロ個以上の文字で始まり、続いて部分一致文字列その1 <の後に部分一致文字列その2 何らかの文字か空白文字 が1文字以上あり、>で終わる文字列 があり、最後に>ではない文字が末尾までゼロ個以上ある文字列

あるいは

行頭が#で始まり、部分一致文字列その3 何らかの単語が1個以上 が行末まである文字列」

のいずれか。

ここに「単語」とは「文字、数字あるいはアンダースコアのいずれかで構成されている文字列」です。なお、部分一致文字列2(何らかの文字か空白文字が1つ以上ある文字列)が以後のコードで呼び出される箇所はまだ「発見」していません。

RegExp.exec() メソッドの解読

重要な点なので敢えてここに記します。

RegExp.exec()メソッドは、正規表現による「汎用的で最も強力な」検索メソッド ( 『Javascriptクイックリファランス Javascript1.5対応』pp.112~113参照 )とされていますが、そのように言わしめる理由は部分一致文字列を取得できるからである、と確信します。

jQueryにおける利用法を基にそれを紐解けば、次のようになります。

quickExpr.exec(selector) の結果はローカル変数 match に格納され、この match は exec() メソッドの仕様上配列となります。そして、当該配列には順に次の情報が格納されます。これらの正規表現を駆使した検索によって、"<・・・>" や "#idName"、".className" が抽出されます。

ローカル変数 match 配列に格納される情報
 match[0]──/^[^<]*(<(.|\s)+>)[^>]*$|^#(\w+)$/ なる検索に一致した文字列
 match[1]──/<(.|\s)+>/ なる検索に一致した文字列
 match[2]──/.|\s/ なる検索に一致した文字列
 match[3]──/\w+/ なる検索に一致した文字列

▲ToTop

init( selector,context ) の構成とインクルード時の挙動

さて、35 行以降 530 行まで、延々と jQuery.prototype プロパティの設定が行われますが、jquery.js の全体を包含する無名関数が実行されると、init() メソッドが 26行 から呼び出されます。

呼び出された init() メソッドが最初に行うことは第一引数が与えられているかどうかのチェックです。第一引数があればそれを selector に代入し、なければ document プロパティを selector に代入します(38行)。

ここに引数 selector は jquery.js が読み込まれた時点で 95 行により空文字となっていますので、|| 演算子によって右項が選ばれて document が左辺の selector に代入されます。

続く長いコードは if キーワードによって幾つかの階層に分岐されますが、その構造を視覚化すれば次のようになります。

init()
├(1) selector が DOM Element の場合…………40~46行
│  ex."document","document.myForm.elements","document.anchors[2]"
├(2) selector が文字列型の場合…………47~78行
│ ├(2-1)HTML 形式の場合 ex."<div><p>Hello</p></div>"
│ ├(2-2)"#string"の場合 ex."#example1"
│ │   ├─(2-2-1)IEかOperaにおいてID名の代わりにnameが返された場合
│ │   └─(2-2-2)その他の一般的な場合
│ └(2-3)正規表現検索文字列 quickExpr に合致しない場合
│     ex."div > p"、"*"、"div, span, p.className"、"#idName > *"、
│       "#idName ~ div"、"label + input"、"tr:first"、":header"、
│       "div:has(p)"、"selector1, selector2, ・・・,selectorN"、
│       "tr:even "、"td:gt(index) "、"div:visible "、・・・・
├(3) selectorが関数型の場合……………………………………………80~83行
└(4) 最後の処理……………………………………………………………91~93行

それぞれの場合の return 値は以下のようになります。
 (1-1)  this(インスタンスオブジェクト)
 (2-1)  指定したHTML要素の配列:selector=jQuery.clean(match[1],context)…44行
 (2-2-1) (for IE,Opera Bug):指定した idName 値により検索された要素の配列
      jQuery().find(selector)………………………………………………………53行
 (2-2-2) document.getElementById( match[3] ) による取得値に
      context:document と selector:selector プロパティを付加したオブジェクト
 (2-3)  context と selector を入れ替えて jQuery( context ).find( selector )
      を実行した結果の配列
 (3)   selectorが関数オブジェクトの場合:関数実行待機状態にする。
 (4)   2-1の場合及び 1~3 の 2-1 以外のいずれにも該当しない場合:
      配列 [selector]を返す。
jQuery(selector,context)の様々な具体的コード

それには本家サイトの API Reference が手っ取り早いでしょう。こちら(API/1.2/Core - jQuery JavaScript Library)に簡単な例示が掲載されています。

▲ToTop

init()メソッド起動まで及びinit()メソッド自体の挙動を追跡する

init()メソッドの挙動は結構複雑です。次々と jQuery 固有のメソッドが連鎖的に呼び出されて目的が遂行されます。そこで jQuery.js のインクルード時と全体を包含する無名関数の実行により、init()メソッドが最初に起動される迄の過程を追跡し、特に this がどの様に変化するか、返値は何か、に着目してその挙動をまとめてみました。

まず、jQuery.jsがインクルードされ、その後ユーザー指示による jQuery(a,c) 実行によって、インスタンスが生成される迄の動きです。

タイミング 前this 履行行為 後this 返値
include時 window 変数設定、prototypeプロパティ設定 window なし
jQuery(s,c)実行 window new演算子によるインスタンス生成とjQuery(s,c)コンストラクタ呼び出し new jQuery(s,c)によるインスタンス
(1)new jQuery(c).find(s)の結果値
(2)new jQuery(document).ready(s)の結果値
(3)[s]

上表よりも更に深化させた分析も行ってみました。よろしければ、こちらの Javascript基礎の基礎(1) return 値は「何に」返されるのか?──jQuery解読(16)をご覧ください。 こちら は、表では静的で理解しにくい部分があるため、動きを文章化し、return先迄含めてまとめたものです。(2007/11/27記)

コンストラクタによる初期化=init()メソッドの実行過程

更にインスタンス生成後に行われる初期化過程を追跡してみると、次のようになるはずです。

前this 履行行為 後this 返値
new jQuery(s,c)によるインスタンス jQuery().find(s)実行 jQuery()によるインスタンス jQuery().find(s)の結果値
new jQuery(s,c)によるインスタンス document.getElementById(・・)の実行 インスタンスに左記結果が代入され それが返値となる
new jQuery(s,c)によるインスタンス new jQuery(c).find(s)による新規インスタンスの生成 new jQuery(c)による新規インスタンス jQuery(c).find(s)の結果値
new jQuery(s,c) によるインスタンス new jQuery(document).ready(s)による新規インスタンスの生成 new jQuery(document) によるインスタンス jQuery(document).ready(s)の実行結果値
new jQuery(s,c)によるインスタンス 配列[s]の生成 インスタンスへの配列の代入 その配列

変数 jQuery と this の参照先の変化

一般にthisは、new演算子とコンストラクタから生成されたインスタンスオブジェクトを参照するか、関数を起動したオブジェクトを参照します。ですから、new 演算子によって新たなインスタンスが定義されるその都度 this の参照先は変化します。上表のとおり、thisの参照先はインスタンス作成の度に変化します。またそのインスタンスの挙動の中で起動されたメソッドによって、様々に変化します。

一方、変数 jQuery の参照先は常にコンストラクタ関数オブジェクトのままで、それは固有のクラスプロパティとクラスメソッドを保持し続けます。

ところで、このようなコンストラクタ、インスタンス及び this の振る舞いは、jQuery.js の冒頭部及び jQuery.prototype オブジェクト定義の直後に配置されている、合計6行の次のコードによってもたらされています。

24: jQuery = window.jQuery = window.$ = function( selector, context ) {
25:  // The jQuery object is actually just the init constructor 'enhanced'
26:  return new jQuery.fn.init( selector, context );
27: },
  --------------------------------------------------------
540: // Give the init function the jQuery prototype for later instantiation
541: jQuery.fn.init.prototype = jQuery.fn;

無名関数にコンストラクタの機能を持たせると同時に、そのコンストラクタから生成するインスタンスの初期化機能をも内包させる───こうしたコンストラクタ関数は一般に目にします。しかし、インスタンスの初期化結果をリターンさせると共に、自ら一部ののプロパティの prototype オブジェクトに、自信のプロトタイプオブジェクトを継承させる点、この2点がこのコードの大きな特徴なのではないでしょうか。───僅か6行でこれだけのことを成し遂げているのですから、驚いてしまいます。

▲ToTop

init()メソッド詳説

まず第一に、第一引数 selector が DOM エレメントであれば、コンストラクタにより生成されたインスタンスオブジェクト( 以下 insObj と表記 )のプロパティにそれを格納させて返値としています。(// Handle $(DOMElement) 40~45行 )

第二に selector が文字列型であれば(上の構成図の(1):コードの48~78行)、それを更に分岐するために上述の正規表現検索 quickExpr.exec(selector) が登場し、正規表現検索文字列 quickExpr の exec() メソッドが文字列 selector に適用されます。(50行)

以下 selector が文字列型だった場合の更なる分岐は次項にまとめます。

他方、selectorの型が文字列でない場合には、関数型であるかどうかチェックされ(上の構成図の(2):コードの80~83行)、関数型であれば有名なコード jQuery(document).ready(function(){ /* Your code here*/ }); が呼び出されます。(83行)

最後に文字列型の一部のケースや以上の条件分岐に掛からない場合においては、 selector を値とする配列が返されます。(91-93行)

selector が文字列型の場合の詳細

さて、selector が文字列型の場合の中身を見てみると次のように分岐されます。

上記 RegExp.exec() メソッドの結果が存在し、かつそれが "<tagName>" という文字列があるか、または"#idName"文字列がある場合

タグ名にヒットした場合には、jQuery.clean (<tagName>,context) によってエラーチェックしてから配列を求め(56~57行)、ID名称にヒットした場合には、jQuery (idName) から該当要素を抽出しています。なお IE 及び Opera のバグ対策が 63~66 行で講じられています。

ここに、HTMLタグ名から配列を求めるコードで使用される jQuery.clean クラスメソッドは、誤表記をチェックして valid な文字列にしています。その名の通り文字列の「清掃」を行うわけです。

RegExp.exec() メソッドによる検索結果が存在しない場合
──Sizzle関数オブジェクトの活用

この場合には、selector と contextを入れ替えて、jQuery( context ).find( selector ) を実行させています。(75~78行)この処理は、前半の jQuery( context ) によってcontext に該当する注目対象を抽出し、この関数処理が終わった時点で、当初の第一引数であるselectorを引数とする find ( selector ) メソッドが起動し、注目対象から selector 条件に該当する対象を絞り込んでいます。

ここにおいて、1.3.xバージョンから登場した Sizzle が発動されます。2364 行の jQuery.find = Sizzle; により、jQuery クラスメソッド find を起動すると Sizzle 関数オブジェクトが呼び出される仕様になっているからです。

以上見てきたとおり、jQuery は目的とする要素を抽出するために、そのインスタンスメソッドとクラスメソッドを縦横無尽に活用します。そのためコードは難解であり、複雑怪奇です。しかし、その複雑さが解読の興味深さであるとも言えます。

なお、ここで呼び出されている jQuery の各種インスタンスメソッド・クラスメソッド(cleanメソッド、eachメソッド及びfindメソッド)の解読は別の機会に行うことにします。

▲ToTop

 90%近いシェアを握っているインターネットエクスプローラの描画エンジンを利用したタブbrowser。沢山のタブbrowserがあるが、多機能、カスタマイズフリー、スクリプト利用等で一日の長がある。Gekkoエンジンへの対応も行われ、IEからの自立独立の方向に向かっている。2005年7月にはIE7が登場する見通しの中で、今後の発展が望まれる。

 多様なCSS作成支援機能を備えた、タグ入力式 HTML&CSS作成支援エディタ。スキンデザインもすっきりしている。テキストエディター上で作成するよりも確実で安全にタグ打ちが出来る。
文字コードを選べないのが欠点。

 StyleNote同様のタグ入力式 HTML&CSS 作成支援エディタ。長年使用してきたが現在StyleNoteに乗り換えつつある。

 クリップボード履歴情報を活用する為のソフト。画像まで履歴を取ってくれるのが嬉しい。このソフトを使わない日は絶対ない程に重宝し、愛用している。

 起動中のウィンドウの「コピーできない」説明文などの文字列を取得し、コピー可能な文字データにするツール。何かと便利。

 ストリーミングデータを保存することが出来るソフト。動画利用には不可欠なソフトだ。

 無料ながらレイヤー機能を有し、スクリプトによる拡張も可能な、sleipnir作者が提供している優れもの画像編集ソフト。

 画面キャプチャソフトと言えばこれに勝るものなし、ではないだろうか? 様々な取得方法を有しており、ブログ作成にもHomepage作成に不可欠だ。Jtrimと並んでWoodyBellsの作品。

 複数ファイルの同時編集は出来ないが、透過pngも作れる画像編集ソフト。
(以下当該サイトから抜粋)初心者にも簡単に操作が出来るフォトレタッチソフトです。多くの加工機能で画像に様々な効果を与えることができます。非常に軽快に動作するため、ストレスなく操作できます。

 Animation Gifファイルを作れる無料ソフト。

 キャプチャソフト。画面内にサイト全体が表示しきれない場合でも、これを使えば全体をキャプチャすることが出来る。

 画像処理。画像のフォーマット変換のみならず、色数やサイズ、圧縮率の変更まで一括処理できてしまう『BatchGOO!』は、大量の画像をまとめて処理したいときに大変便利なソフト。BMP, TIFF, JPEG, PCX, PNG の相互変換をはじめ、色数・サイズ・解像度の統一、JPEG圧縮率の調節など、ホームページ用の画像や携帯電話用の壁紙を揃えるのに抜群の相性を見せる。(Vectorの当該ソフト紹介頁より抜粋引用)

 名前から直ぐに想像が付くように画像のサイズを測るためのソフトだ。Homepage作成には欠かせない。2カラム、3カラムのレイアウトを行う場合に大変重宝する。

 ランチャーソフトは沢山あるが、中でもこれが一押しだ。2年以上使ってきたがその操作性には毎日満足している。これを使い始めてからデスクトップには一切のアイコンを表示することをやめてしまった。

 AdobeReader7によって、起動時間が長すぎるという長年のユーザーの不満はある程度解消した。そのためこの高速化ソフトは存在価値が低下してしまったかもしれない。AdobeReader6迄はこのソフトによる起動高速化で恩恵を受けてきた。

 IE専用が難点だが、様々なサイト内でIDやパスワードを入力するのに重宝するソフト。コンテキストメニューから簡単に起動できるのがGood! sleipnir等のIEの描画エンジンを利用しているブラウザでも使える。

 利用しているパソコンの諸元値を取得するには、このソフトがベストだ。インストール済みソフトの一覧が取得できるのも嬉しい。

 WMPは機能が豊富なだけ重い。RealPlayerも同様だ。そこでMedia Player Classicを使いたい。動作が軽快なだけではなく、対応しているファイル形式もすこぶる多く、これひとつで、wmvもrmも表示できてしまうのだから凄い! 数多あるMedia Playerの王様と言えるだろう。

 自宅でPCを起動しているときには必ず起動しているメディアプレーヤー。何かと過剰なWinampよりも、起動も速くスキンはシンプルだ。

 DivX, Xvid, Mov, Vob, Mpeg, Mpeg4, avi, wmv, dv, などの動画をDVD-Video形式に変換できるフリーソフト。クリックするとDVD関連ソフト紹介サイト=「DVDなToolたち」なるHomepageが開きます。

201011250125
201011120654
201007090028
200908242332
200908161701
200908091346
200903120123
200902220243
200806110106
200803120931
200801010008
200801010000
200801010000
200712041609
200712030032
200712021940
200711160132
200711141601
200711031900
200710311816
FC2 Management