09 | 2017/10 |  11

  1. 無料サーバー

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

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


スポンサーサイト

上記の広告は1ヶ月以上更新のないブログに表示されています。
新しい記事を書く事で広告が消せます。

WIndows7 インストール直後メモ(1) スリープからの自動復帰を止めさせた

1/13 追記:イベントビューアーからも原因が特定出来た

以下に述べるように、手作業的試行錯誤の結果スリープ状態からの自動・強制復帰問題は解決したが、或るサイト(こちら:Windows7の勝手にスリープから復帰・起動する問題その3(解決糸口の探り方編))にイベントビューアーで原因を特定する方法が述べられていたので、それを参考に早速ビューアーでチェックしてみた。

その結果、見事に( って当たり前だが(苦笑) )以下で特定した問題が原因であることが再確認された。

そもそも、最初からこの方法で原因を特定すれば苦労はなかった。職場のPCでスリープに入れない問題が発生しているので、早速イベントビューアで原因を特定してみるつもりだ。

ハイブリッドスリープは結構な機能だが...

Vista で2年余り使い続けた PC の OS を、この正月休みに 7 に新規アップグレードインストールしたことは前のエントリイで触れた。

このエントリイでは、各種アプリのインストールが約7割方終わり、愈々各種アプリケーションを使い始めたその直後から、困り果てていた或る問題が解決したので、その顛末を綴っておきたい。

問題とは、スリープに入れないことだ。今回それが、「一応、さしあたり」解決したのである。

ハイブリッドスリープは 7 で初めて導入された機能だ。それはスリープと休止状態を兼ねた、それ故にハイブリッドなスリープ、またはハイブリッドな休止状態である。しかし、これに対応していないハードが結構存在していて、それが混乱の元となっているのだ。

【例1】
例えば、職場のカードリーダー/ライター(日立製)の場合(これは個人認証に使っているため、業務上使用しないわけにはいかない代物だ)、スリープには入れるものの、眠りから覚ますことが出来ない。PC を覚醒させるには、当該カード読取/書込装置を USB ポートから物理的に取り外してから覚醒操作をするか、数秒電源ボタンを押し続けてから強制シャットダウンして、通常の電源オン操作を行って再起動するしかない。

【例2】
自宅の PC の場合、結論を言えばネットワークアダプタの電源遠隔操作機能を停止させないと、スリープに一旦入るものの、10 秒ほどでスリープから自動的・強制的に復帰してしまうことを止めることが出来なかった。

なお、遠隔操作を停止した操作説明図は以下の通り

deviceManager deviceManager

2 番目の図の「このデバイスで、コンピューターのスタンバイ状態を解除出来るようにする」をオフにしたのだ。その後ハイブリッドスリープに入らせてみたところ、これまでのように強制的・自動的に目覚めることはなく、所定通り、キーボード操作によって PC をハイブリッドな眠りから目覚めさせることに成功したのだ。(後述するように、マウス操作によってはスリープから復帰出来ないように、デバイスドライバ設定を変更した。)

▲ToTop

原因究明に"あくせく"

ネット検索を掛けた結果、usb ハードディスクとの相性問題や、電源タップ上での電源コードの接続問題(連動機能のあるタップかどうか)など、物理的な問題が原因と思われたので、タップを取り替えたり、差し込む位置を変えたり、usb ハードディスクを全て切り離してみたり、色々と試してみた。しかし、それらの全てにおいて、スリープ状態からの 10 秒程度での自動的・強制的な復帰を止めることが出来なかった。

昨晩と今晩、合計 2 時間以上要して試行錯誤したが、結局解決策を見いだすことが出来ないまま、時が流れたのだ。

そこで、どうにも分からないので更に検索を掛けたところ、デバイスドライバを弄って、各デバイスからの電源管理機能を変化させることによって、スリープ問題から脱出出来た、との情報を入手出来た。それがこれだ。→ MacBook (Pro)でWindows Vistaのスリープ時のトラブルを回避するには - パソコンよろずQ&A

このサイトの中程に、「スリープに移行できない・勝手に復帰してしまう問題を解消するには」なる項目があり、そこでデバイスマネージャ上で、デバイスの電源管理機能を変更して解決した例が解説されていた。

即座に「これだ! 」と感づき、怪しいデバイスを全てチェックし、1つずつ影響を調べてみることにした。

▲ToTop

電源管理を行えるデバイスはかなり多く存在しているが...

実施に影響を与えていると思われるものは殆どなく、デバイスドライバ一覧のチェック開始から数分後に辿り着いた犯人とおぼしきもの、それがネットワークアダプターだった。

それにも電源管理タブがあり遠隔操作が可能となっていた。直感的に、これが原因かもしれない、2年半前に購入した PC なのでハード的に、あるいは BIOS が、ハイブリッドなスリープには対応していないのかもしれない。遠隔操作( Wake On 操作)は全くしないのだから、デバイスのこの機能を止めても何の支障もない。ならば、とりあえず、さしあたり遠隔で可能となる Wake On 機能を止めてみよう!───と判断したのだ。

そして実際、ネットワークアダプタの Wake On 機能を停止したところ、全く問題なく眠りには入れたし、自動的/強制的に目覚めてしまうこともなくなったのだ。

以上、試行錯誤の結果ハイブリッドスリープのエラーから脱出出来た、簡単なメモである。

付記:マウスからは復帰出来ないようにした

マウス操作によってスリープ状態から復帰出来てしまうと困ることがある。

物が一寸マウスに触れただけで、あるいはマウスを一寸動かしただけで、PCの眠りを覚ましてしまうことがママあるからである。起動するつもりはなかったのにマウスが動いたためPCが起動してしまった、と言う事例には事欠かないのではないかと思う。

だからデバイスドライバのチェック序でに、マウスによる電源管理機能をオフにしたのである。その結果電源ボタン以外はキーボードからのみ復帰が行えるように改善されたのだ。

信じ難い IE の Javascript インタープリタの挙動

animatedPopup も makeTableContents も IE では動かない。

その他各種の jQuery plugin を作ってきたが、それらの多くが IE で動かない原因をずっと解明できないまま、時は空しく流れ去るばかりだった。IE 用にはお粗末なデバッガーしかないので、動かない原因がなかなか特定出来ないでいたのだ。

しかし、未だに相変わらずトップシェアを占めている IE で、目次作成プラグインなど全てのエントリイにおいて活用させたいものが動かないのは、ブログ製作のあり方として耐え難い。そこで、Firebug Lite や IE 8 の開発者ツール、あるいは DebugBar を駆使して、前から見当を付いていた " 怪しげな箇所 " を部分に細分化し、時には一行ずつ、場合によっては一文ずつ、一式ずつテストしてみた。

そしてやっとのことで、余りに<お粗末な> IE 固有のエラー発生原因の いくつかが究明できた。以下そのお粗末さを明らかにしておきたい。

なお、今回特定した問題はいちがいにバグとは言い切れないかもしれない。

余りにお粗末なインタープリタ

それは「 論理積演算子 」&& に関わる。この演算子のオペランドは左右とも「式」が許されているのに、IE では左側のオペランドが特定の或る式の場合には、「 式なのにエンドマークの;がないからエラー! 」 と判断するのである!

例えばこうだ。

 1: $("body").mousemove(function(e){
 2:   if (e.pageX-$(window).scrollLeft()<10 && $contents.is(":hidden")){
 3:     direction = false;
 4:     displayJumpList(e.pageY);
 5:   }
 6: });
 7: $(window).scroll(function(){
 8:   if (!$contents.is(":hidden") && Math.abs(initScrollTop-$(window).scrollTop())>20){
 9:     slideFuncOut.call($contents);
10:     initScrollTop = $(window).scrollTop();
11:   }
12: });

上のコードの 2 行目と 8 行目を比較すると、前者では&&演算子の左オペランドは比較式で、右オペランドは jQuery インスタンスメソッド、つまり関数呼び出し式である。他方、後者ではそれが逆となっており、左側に jQuery インスタンスメソッドがあり、右側には比較式がある。

さて、IE で、これらの 2 つの if 文のどちらが正常に働き、どちらがエラーとして認識されてしまうか、お分かりになるだろうか??

結論を言えば、Firefox、chrome、safari (いずれも windows 版)ではいずれの行も問題なく作動するし、IE に最も近い Opera でさえ問題はない。しかし、独り IE だけが 8 行目の && 演算子の左側オペランドにセミコロンがないからエラーとして評価してしまうのだ。

『Javascript 第 5 版』によれば「 関数呼び出しは厳密には式であるが、Web ブラウザを制御する副作用があるので文の仲間に入る 」。

つまり、IE のインタープリタは、「 関数呼び出しは厳密には式であるが、副作用を伴う文の仲間に入るからセミコロンが必要だ 」と判断するわけだ。

一方他のブラウザのインタープリタは、関数呼び出しは副作用を伴うが厳密には式なので、セミコロンは求めない、ということになる。

お粗末なインタープリタに対する 1 つのささやかな対策

2 行目は問題なく作動することが確認されている。しかし 8 行目はエラー。───ではオペランドの順番を変えてみたらどうか?───こう思い付くまでにさほどの時間は掛らなかった。

2 行目に準じて 8 行目の && 演算子のオペランドの左右を次のように入れ替えるのである。

8: if (Math.abs(initScrollTop-$(window).scrollTop())>20 && !$contents.is(":hidden")){

つまり、左オペランドには比較式を置き、jQuery インスタンスメソッド呼び出し式は右オペランドに移すのだ。このように入れ替えてから、開発者ツール等で試したところ、エラーはあっさり消え去り、問題は解決してしまったのだ。

構文ミスでも、記述ミスでもないこのような記述を、敢えて「 文だからエラー 」として認識してしまうとは、余りにお粗末ではないか。

因みに、次のように括弧で括って明確に式であることを明示してみたところ、これもエラーにはならなかった。しかしこの方法は可読性が低下するので決して好ましい方法ではないだろう。&& 演算子が登場する度に、関数呼び出しオペランド部分を一々括弧で括るなんて、余りに手間が掛りすぎる!

8: if ( (!$contents.is(":hidden")) && Math.abs(initScrollTop-$(window).scrollTop())>20 ){

2 つ目は mousemove メソッドの呼び出し元に関する問題である

最終的には 2 行目の mousemove メソッド呼び出し元は、body 要素としたが、コード作成時から最終改訂の直前までは、window としていた。そして $("window").mousemove(function(e){・・・}) で IE 以外のブラウザは意図を解釈してくれた。しかし IE だけはうんとも寸とも言わないのだ。

確かに、マウスは body 要素の上、あるいは中で動くのだから、mousemove イベントの呼び出し元は body であるべきだろう。それが論理的である。しかし、マウスは window の上あるいは中で動いていることもまた真なのだから、window が起動元であっても mousemove メソッドは作用すべきだと思われる。

独自仕様を乱発してきた IE はその裏で余りに厳格な解釈を行っている

上の 2 つの事例から言えることは、そういうことである。余りに厳格な解釈によりエラーを乱発するのだ。「 仕様だから仕方がない 」という立場もあるかもしれないが、インタープリタは運用上のスマートさも兼ね備えるべきだ。

次に想起されることはこうだ─── || 演算子は IE で正常に働くのだろうか?

こちらは<予想を裏切り>、他のブラウザと同様に正常に機能した。

8: if (!$contents.is(":hidden") || Math.abs(initScrollTop-$(window).scrollTop())>20){

上の 1 行では&&演算子の左側オペランドは関数呼び出し文であるが、実行してもエラーははき出されないのだ。このことから言えることは、「 IE のインタープリタには一貫性がない 」ということだ。

余りに厳格な解釈を行うと思えば、論理的一貫性を欠く。───それが IE の Javascript インタープリタなのだ!

【 結論 】&& 演算子における IE 対策

  1. && 演算子の左側オペランドに関数呼び出し文を置かないようにする。
  2. どうしても左側に関数呼び出しを置かざるを得ない場合には、&&演算子の左側オペランドを一括りの ( ) で括って、IE が文としての厳格な解釈を適用しないようにするのが得策だ。

ブログ頁内ジャンプ移動に際して、ジャンプ先からジャンプ元に直ぐに戻れるようにする 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})});
});
 

任意の数の clone 要素を、任意の位置に配置する jQuery プラグイン

ID 名または class 名を持つ要素の clone 要素の活用

Web ページ内に存在する要素には一般に ID 属性や class 属性を有する要素が沢山存在し、それぞれに固有の ID 値が与えられ、あるいは複数の種類の class 名が、複数の要素に対して与えられています。

さて、多くの需要があるとは思えませんが、ID や class 名を有する要素を複写して、あれこれの位置に複数表示させたい場合が、時々発生します。例えば、popup 要素を複数同時に表示させたい場合等です。

あるいはページスクロールが発生した際に、或る位置に或る要素を常時表示させたい場合に、当該要素は元のままにして、clone 要素を作って常時表示させることにも活用出来ます。このように頁内での「影武者活用」は他にもあり得ると思われます。

今回作成した jQuery プラグインは ID 属性または class 属性を有する要素の存在を前提として、それらの clone 要素を任意の数だけ作成し、それをまとめて任意の要素の前後、内部あるいは外部に配置するものです。

要素を頁内に挿入するための各種 jQuery インスタンスメソッド

HTML 要素を Javascript で操作することは DOM の操作として大変興味深いものですが、jquery が登場するまでは、スクリプトを使って要素の配置換えや挿入を行うには、大変な労力を費やす必要がありました。

しかし、jquery が登場したことにより、要素の配置換えや挿入は極めて容易な行為に変化しました。因みに要素を操作する各種 jQuery インスタンスメソッドこそ、その容易さ故に jquery の威力を知らしめる格好のツールと言えるかもしれません。

さて、頁内の任意の位置に挿入配置するか、あるいは既に頁内に存在する要素を、任意の要素の前/後/内/外に配置換えするための jQuery インスタンスメソッドは数多く存在しています。よく知られていて利用されるメソッド群だと思いますが、列挙すれば以下の通りです。

凡例:A は操作対象とする要素で S、P、B は A 要素の配置対象とする要素

  1. S.after(A) or A.insertAfter(S) : A 要素を兄弟要素 S の直後に配置
  2. S.before(A) or A.insertBefore(S) : A 要素を兄弟要素 S の直前に配置
  3. P.append(A) or A.appendTo(P) : A 要素を親要素 P の最後の子要素として配置
  4. P.prepend(A) or A.prependTo(P) : A 要素を親要素 P の最初の子要素として配置
  5. P.html(A) : A 要素を親要素 P の子要素として配置
  6. P.wrapInner(A) : P 要素に子要素 A を挿入して、それ迄存在した全ての子要素を A の子要素( = P の孫要素)に変更する
  7. A.wrap(P) : A 要素(複数ある場合にはその各々)を P 要素で包含する
  8. A.wrapAll(P) : A 要素の全てをまとめて P 要素で包含する
  9. B.replaceWith(A) or A.replaceAll(B) : B 要素を A 要素で置換する

▲ToTop

プラグインの動作結果を実例で示します

何はともあれ、ぐだぐだ説明する前にどんなプラグインが完成したのか示すべきでしょう。

行われる一連の作業の概要は次の通りです。

  1. 以下のボタンをクリックすると、
  2. 直ぐに clone 要素の配置方法を選択する select ボックスが表示されます
  3. ボックス内のメソッドリストから配置メソッドを選択すると、
  4. この頁内にあって display:'none' で隠蔽されている ID名 'popup' 要素を元とする clone が作成され、
  5. 選択された配置メソッドによってボタン内に書かれた要素を配置基準として clone 要素が配置されます。

それぞれのボタンをクリックすると、要素配置用メソッドの一覧が選択枝として表示されるので、適当なメソッドをクリックしてください。すると指定した要素の前後、内部または外部に、popup 要素の clone が配置されます。

頁内に存在する ID 名「popup」要素の clone を 5 つ作成し、entry_body 部を配置対象として配置する。
頁内に存在する ID 名「popup」要素の clone を 3 つ作成し、この下のボタンを配置対象として配置する

実例2

今度は頁内に表示されている要素の clone を作成・配置します。ボタンをクリックして表示されるメソッドリストから、適当なメソッドを選択してクリックすれば、clone 要素が各々のボタンの近傍の指定した位置に配置されます。

まずページトップにある時計から。なお、元の時計は絶対配置されていてドラグ可能ですが、clone 時計は相対配置しますのでドラグは出来ません。

次はナビゲーションバーの clone を作って配置します。時計の場合もそうでしたが、makeNputClone 起動に際して第三引数の「 content 」を指定しなければ、第一引数である clone 元要素の clone がコンテンツとしてそのまま配置されます。

プラグイン設計上考慮したこと(1):数ある要素配置メソッドを選択的に適用させるために

プラグイン作成に際してまず検討したことは、上記のように沢山ある要素配置メソッドを、プラグイン使用時にどのように指定させるか、と言う点でした。

まさか、一々引数にメソッドを書き込んで指定する方法では、とても供用出来ません。実際、直前のエントリイで述べた方法は、まさに引数でメソッドを指定する類のものでした。

これではいけない、汎用性が全くないと判断し、その改善を思い立った訳ですが、select ボックスの活用を結論づけるまでにさほど時間は掛りませんでした。

しかし、select ボックスを表示させる方法やその配置方法などを考え始めた時、それをこのプラグイン内でゼロからコーディングすることは、いかにも稚拙であると思えました。

そこで閃きました。汎用性を持たせるために時間を掛けて改訂した animatedPopup プラグインメソッドを利用すればよい!───

このプラグインを使って、select ボックスをポップアップさせれば、clone 要素を配置した直後に、その位置までページスクロールさせるコーディングを行った時に、animatedPopup メソッドで表示させたポップアップ要素は、スクロールイベントに対応しているので、select ボックスも追随してスクロールされ、window 内の同じ位置に表示され続ける!

こうして、clone を作成・配置するプラグインは、その中から別のプラグイン animatedPopup を呼び出すものとなりました。

▲ToTop

プラグイン設計上考慮したこと(2):配置メソッドの選択が連続的に行えるように

このプラグインは clone 要素の作成それ自体よりもその頁内配置に興味を持っています。ですから。あれこれの配置を容易に試みることが出来なければ意味がありません。

そこで、連続して異なるメソッドを選択出来、かつ作成した clone 要素はその都度削除され、新たに作成された clone 要素を異なるメソッドで配置することにしました。

こうして各種配置メソッドを連続的に試すことを可能としたため、結果として各種配置メソッドの効果と差異を、一目瞭然に閲覧することが可能となりました。

2 つのプラグインから使用できる変数について

今回のプラグインで最も苦労したのは、makeNputClone メソッド内から animatedPopup メソッドを起動するにあたり、前者と後者で共通して利用する変数をどう定義するか、ということでした。

前者から一旦後者が呼び出されてしまった後でば、一般に前者内で定義した変数は後者では使用できませんし、前者から後者の変数も利用できません。引数を使って渡す方法もありますが、既に後者の引数定義は確定しており、makeNputClone メソッドからの呼び出しのために後者の引数を追加するのは邪道です。またそれぞれの関数のプロパティに変数を登録する選択肢もありますが、これは変数の記述文字数が多くなるので、コード作成上面倒です。

そこでグローバル変数である $ 変数のプロパティ利用を考えざるを得ませんでした。この方法は 「 余り美しくない 」 のかもしれませんが、他の方法を思いつかず、やむを得ないと判断しました。$.o プロパティを作り、そこに両方のメソッドで共通して使用する変数を登録することにしました。

▲ToTop

makeNputClone プラグインコード

詳細な解説は面倒なので、大きなブロック単位で簡単に説明します。

1 ~ 23 行【エラー処理ブロック】

引数をチェックしてエラーを表示するためのコーディングです。引数なしでプラグインを起動した場合には、簡易 HELP 機能が表示され、引数の指定方法を説明する alert が表示されるようにしました。

25 ~ 27 行【初期値整理ブロック】

初期値は例によって、$.extend クラスメソッドを活用して、既定値と引数値を併合します。プラグインメソッドの呼び出し元となる jQuery インスタンスを $.o プロパティに $target 名で登録します。

29 ~ 71 行 【 clone 作成・配置メソッド定義ブロック 】
fireMethod 関数は animate メソッドによるアニメーションが終わった時点で呼び出される関数として定義しました。その名の通り、配置「メソッド」を起動する機能を持たせました。

プラグインメソッドの起動元は、clone 要素の配置基準となる要素です。一旦プラグインを起動すれば select ボックス内からどのような配置方法を選択した場合でも、配置される clone はずっと同じものです。ですから配置の都度作成する必要はありません。

また、別の要素から当該プラグインを起動した後に、再び以前起動元とした要素から起動した場合でも、clone の作成元として指定した要素が替わらない限り、clone を作り直す必要はありません。

そのため、data クラスメソッドを活用して効率的なインタープリトが可能となるようにしました。( #52~54 )

73 ~ 85 行 【 DOM Ready・selectリスト表示アニメーションブロック 】

最初の 2 行は animate メソッドの complete 引数に代入する関数です。第二引数で makeNputClone メソッドの全ての引数を fireMethod 関数に渡しています。引数を一括指定出来る function.apply メソッドの大きな利点を活用しました。

DOM Ready 関数の最初で改めて第一引数 selector のチェックを行っています。これはエラー処理ブロックでは引数をその文字列で評価しましたが、ここでは指定した ID 名や class 名の要素の存在をチェックしています。

指定した名称が、jquery が解釈する文字列としては正しくても、肝心の該当要素が存在しなければ意味がないからです。

82 ~ 83 行で animatedPopup プラグインを使って、select ボックスを表示するアニメーションを引き起こします。

▲ToTop

88 ~ 164 行は既定値オブジェクトです。3 つのプロパティ別に説明します。

88 ~ 101 行 【 select ボックスリスト作成用 】

select ボックスを作成するための HTML 文字列です。

select リストの文字がデフォルトでは小さすぎるので、fontサイズを指定しました。

適用する配置メソッドの作用が分かりやすいよう、リスト文字列では配置先、メソッド、配置される要素の区別を表示しました。

102 ~ 162 行 【 配置メソッドオブジェクト 】

適用する配置メソッドをこのオブジェクトのプロパティ名とプロパティ値として格納しています。select ボックスでメソッドが選択されると、change イベントが発生し、対応するイベントハンドラー内で、このオブジェクトの関数が呼び出されて clone 要素が所定の位置に配置されます。

この部分で最も苦労したのは html メソッドです。作った clone 要素を敢えて html メソッドの引数として挿入するわけですから、HTML 文字列に変換しなければなりません。当然 id 名や class 名も保持させなければなりません。またスタイル属性値も最小限のものは適用させるようにしないと、clone らしくありません。

これらのハードルはそれぞれにかなり高く、文字列化とスタイル属性値の複写コードには相当苦労しました。

163 行 【 第一引数 selector の格納庫 】
同一要素からの二度目以降 のmakeNputClone メソッド呼び出しの際に、第一引数 selector の値を従前値のそれと比べるために使用します。52 行で行う比較が終わる迄は、このプロパティには、以前に起動された時の selector 名(但し、一度目の起動の際には null )が記憶されます。比較後には今回の selector 値がこのプロパティに保存されます(55 行)。
(function($){
$.fn.makeNputClone = function(selector,n,content,cloneCSS){
/* selector で抽出される最初の要素の clone を n 個作り、それを select
 * ボックス内から選択した jQuery メソッドによって、起動元インスタンス
 * の内外または前後に配置するメソッド。当該プラグインから animatedPopup
 * メソッドを起動し、select ボックスの表示に利用する。
 * selectBox から順次異なる配置位置を選択し、clone の配置換えが可能。
 * 一旦 clone を作成後に n を増やしたい場合にも、重複作成はしないよう
 * コーディングしてあるので、単純に n を指定すればよい。
 * 引数なしで起動すると引数の指定方法を alert 表示するようした。
 * release:2010/11/21
 */
  1:  var errFunc=function(al){al!==undefined && alert(al);return},p;
  2:  if (!arguments.length){
  3:    var text ="■引数の指定方法■\n"
  4:    text+="jQuery(elem).makeNputClone(selector,n,content,cloneCSS)\n\n";
  5:    text+="selector : string of original ID name\n";
  6:    text+="n : number of clones\n";
  7:    text+="content : HTML strings of clone's inner content\n";
  8:    text+="cloneCSS : CSS Object of clone";
  9:    return errFunc(text);
 10:  }
 11:  if (typeof selector !== "string" || selector.indexOf("#")<0 && selector.indexOf(".")<0){
 12:    return errFunc("指定した id 名や class 名の要素は存在しません。\n
          存在する id 名や class 名を指定してください。");
 13:  }
 14:  if (n===undefined || n===null || typeof n!=="number")
 15:    return errFunc("第二引数が不適切です。\n\n第二引数には、作成する
        clone の数を正の整数で指定してください。\n0 を指定すると作成済み
        の clone を削除します。");
 16:  if (content && typeof content !=="string"){
 17:    if (typeof content ==="object" && cloneCSS===undefined){
 18:      cloneCSS = content;
 19:    } else return errFunc("第三引数が適切ではありません。\n\n
 20:      第三引数は clone に挿入する HTML テキスト文としてください。\n
          content を指定しなければ、元要素のコンテンツが clone 要素の
          コンテンツになります。");
 21:  }
 22:  if (cloneCSS && typeof cloneCSS !=="object")
 23:    return errFunc("第四引数が適切ではありません。\n\n
        第四引数は clone に適用する CSS オブジェクトとしてください。\n
        cloneCSS を指定しなければ、元要素のスタイル設定が clone 要素に
        そのまま複写されます。");
 24:
 25:  $.o = $.data(this[0],"opts") || $.data(this[0],"opts",
 26:    $.extend({}, $.fn.makeNputClone.opts, {content:content || null,
          cloneCSS:cloneCSS || null, pluginFunc:arguments.callee,
          pluginArgs:arguments}));
 27:  $.o.$target = this;
 28:
 29:  var fireMethod = function(selector,n,content,cloneCSS){
 30:    var createClone = function(){
 31:      var clonesArray=[], $tmp=$(selector+i), temp, i=1, k=1;
 32:      for (;$tmp.length;i++) $tmp.remove();
 33:      for (; k< n+1; k++){
 34:        var temp = $($(selector)[0]).clone(true)
                .addClass(selector.slice(1) +"clone"+" draggable")
 35:        if (selector.indexOf("#")===0)
 36:          temp.attr('id',selector.slice(1)+k);
 37:        if ($.o.content)
 38:          temp.html('This is No.'+ k +' clone. '+$.o.content);
 39:        if ($.o.cloneCSS)
 40:          temp.css($.o.cloneCSS);
 41:        var $c = temp.children(":[id]");
 42:        if ($c.size()){
 43:          $c.each(function(){
 44:            $(this).attr("id", $(this).attr(selector.slice(1)+k));
 45:          });
 46:        }
 47:        clonesArray[k-1]=temp[0];
 48:      }
 49:      return $(clonesArray);
 50:    }
 51:    
 52:    $.o.$clone = $.o.beforeSelector === selector &&
 53:      $.data($.o.$target[0],"createClone") || 
 54:      $.data($.o.$target[0],"createClone",createClone());
 55:    $.o.beforeSelector = selector;
 56:    $("#thissel").change(function(){
 57:      var index = this.selectedIndex,  
 58:        resMethodName = this.options[index].value;
 59:      for (p in $.o.methodList){
 60:        if (p !== resMethodName) continue;
 61:        else {
 62:          $.o.methodList[p](); // cloneを頁に挿入する
 63:          var $its1=$("[class*='clone']:first");
 64:          $its1.length && $(window).scrollTop($its1.offset().top);
 65:          $its1.length && $its1.offset().left > $.F.getWinSize &&
                $(window).scrollLeft($its1.offset().left);
 66:          break;
 67:        }
 68:      }
 69:      // return $.o.$clone;
 70:    });
 71:  }
 72: 
 73:  var completeFunc = function(){
 74:    return fireMethod.apply($.o.$target, $.o.pluginArgs);
 75:  }
 76:  $(function(){
 77:    if (selector.indexOf("#")===0 && !$(selector).length){
 78:      return errFunc("指定した ID 名の要素は存在しません。\n
          存在する ID 名を指定してください。\nなお、冒頭に#を付ける
          ことを忘れないでください。");
 79:    } else if (selector.indexOf(".")===0 && !$(selector).length){
 80:      return errFunc("指定した class 名の要素は存在しません。\n
          存在する class 名を指定してください。\nなお、冒頭に '.' を
          付けることを忘れないでください。");
 81:    }
 82:    $.o.$target.animatedPopup($.o.selBoxHTML,{
 83:      popupCSS:{left:"center",top:"center"},duration:400,
            easing:"easeOutBack",complete:completeFunc
 84:    });
 85:  });
 86:};
 87: 
 88:$.fn.makeNputClone.opts = {
 89:  selBoxHTML :'<form><select id="thissel" name="thissel" size=12 style="font-weight:bold;font-size:16px">'
 90:    + '<option value="" disabled="desabled" style="text-align:center">メソッドを選択</option>'
 91:    + '<option value="append" selected="selected">target.append(clone)</option>'
 92:    + '<option value="prepend">target.prepend(clone)</option>'
 93:    + '<option value="after">target.after(clone)</option>'
 94:    + '<option value="before">target.before(clone)</option>'
 95:    + '<option value="wrap">target.wrap(clone)</option>'
 96:    + '<option value="wrapAll">target.wrapAll(clone)</option>'
 97:    + '<option value="wrapInner">target.wrapInner(clone)</option>'
 98:    + '<option value="replaceWith">target.replaceWith(clone)</option>'
 99:    + '<option value="replaceAll">target.replaceAll(clone)</option>'
100:    + '<option value="html">target.html(clone)</option>'
101:    + '<option value="remove">clone.remove</option></select></form>',
102:  methodList : {
103:    append: function(){
104:      $.o.methodList.remove();
105:      $.o.$target.append($.o.$clone);
106:    },
107:    prepend: function(){
108:      $.o.methodList.remove();
109:      $.o.$target.prepend($.o.$clone);
110:    },
111:    after: function(){
112:      $.o.methodList.remove();
113:      $.o.$target.after($.o.$clone);
114:    },
115:    before: function(){
116:      $.o.methodList.remove();
117:      $.o.$target.before($.o.$clone);
118:    },
119:    wrap: function(){
120:      $.o.methodList.remove();
121:      $.o.$target.wrap($.o.$clone);
122:    },
123:    wrapAll: function(){
124:      $.o.methodList.remove();
125:      $.o.$target.wrapAll($.o.$clone);
126:    },
127:    wrapInner: function(){
128:      $.o.methodList.remove();
129:      $.o.$target.wrapInner($.o.$clone);
130:    },
131:    replaceWith: function(){
132:      $.o.methodList.remove();
133:      $.o.$target.replaceWith($.o.$clone);
134:    },
135:    replaceAll: function(){
136:      $.o.methodList.remove();
137:      $.o.$clone.replaceAll($.o.$target);
138:    },
139:    html: function(){
140:      $.o.methodList.remove();
141:      var text="";
142:      $.o.$clone.each(function(){
143:        text +="<"+this.nodeName+" id='"+ +$(this).attr("id")+
            "' class='"+$(this).attr("class")+"'>"+$(this).text()+
            "</"+this.nodeName+">";
144:      });
145:      $.o.$target.html(text);
146:      var clone = $("[class*=clone]");
147:      for (var i=0; i<clone.length; i++){
148:        $.each(["width","height","margin","border","padding",
              "background"],  function(){
149:          clone[i].style[this] = $.o.$clone[i].style[this];
150:        })
151:      }
152:    },
153:    remove: function(){
154:      var clone = $("[class*=clone]");
155:      if (clone.length) {
156:        if (clone.has($.o.$target).length){
157:          $(clone[0]).before($(clone[0]).children());
158:        }
159:        clone.remove();
160:      }
161:    }
162:  },
163:  beforeSelector:null
164:}
165:})(jQuery);

▲ToTop

コード解説を行うためにコード全文から抜粋を行うコード

上記の説明では副題をクリックすると該当部分のコードが抜粋表示されます。その表示にも animatedPopup メソッドを活用したのは余興ですが、初めて採用したこの方法では、一箇所に説明用コードを全文書き込むだけで、後は抜粋作業を Javascript を使って行わせれば、容易に抜粋が可能となることを実証したつもりです。

長いエントリイの最後に、コード全文からその一部を抜粋するためのコーディングについて触れておきます。

ネームエンティティ問題

最初は、ブラウザに表示されたエントリイ内のコード全文を検索対象として、抜粋を行おうとしました。ところがこの方法には大きな落とし穴が待っていました。ネームエントリイ問題です。

コード全文掲載に当たっては、HTML 文上意味のある記号(全角で表記すると、"<"、">"、"&" 等)をネームエンティティ変換しててから、エントリイを投稿しています。それによってブログが描画される際に、これらの特殊記号は HTML 文上意味のないものとして扱われ、正しく表記されることになります。こうして、例えば投稿時には &lt; と半角で書いていた記号は、< になって表記されることになります。

さて、この正しく表記されたエントリイ上の文字列から、Javascript を使って抜粋対象を抽出すると、その抽出部分に < が含まれていた場合に、そのまま抽出結果をブログに貼り付けると問題が発生します。単なる記号としての < ではなく、タグ開始文字として HTML 文上意味のある記号となってしまうのです。

この結果、ブログエントリイから部分抽出した結果をそのまま投稿すると、表示が崩れてしまうのです。

再びネームエンティティ変換して原稿とすればよい、と考え試みてみましたが成功しませんでした。投稿した時点のネームエンティティ変換と全く同一にはならないのです。空白文字(改行マークや半角スペースなど)で区切られていない ; がネームエンティティ表記の;と誤解されるらしく、正しく復号することが出来ないのです。(ネームエンティティ変換実施(A)→ブラウザで表記(B)→再度のネームエンティティ変換(C)と辿った時に、A と C は同一にはならないのです)

解明しませんでしたが、他にも正しい「復号」を妨げる要因が複数存在しているようでした。

そこで AJax 通信を使用した

試行錯誤の結果、表記されたブラウザ内の全文コードから Javascript を使って部分抜粋を行い、抜粋部分を再び投稿する方法は、適切ではないと判断するに至りました。

そして替わりに思いついたのが、Ajax 通信を活用することでした。これによりブログエントリイ全文をテキスト形式で取得し、その中から必要部分を抜粋すればよい。そうすればネームエンティティ変換した文字は原文のまま、変換されたままで取得できます。つまり、そこから抜粋した文字列もネームエンティティ変換されたままの状態なので、意図したとおりの結果に辿り着ける───こう考えたのです。

そしてそれは見事に成功しました。

そのコーディングは以下の通りです。

  1:  var pageBody;
  2:  $.ajax({
  3:    url: このサイトのurl,
  4:    dataType:"text",
  5:    success: function(data){
  6:      pageBody = data;
  7:    }
  8:  });
・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・
  9:  var selectText = function(start,end){ 
 10:    return this.slice(this.indexOf(start),this.indexOf(end)-1);
 11:  };

利用は 1 行目から 8 行目がブログ・エントリイ・オープン時に実行させることを前提にしています。開いた暫く後になってからしか、コードの部分閲覧は発生しないので、その頃には十分 Ajax 通信は complete している訳で、pageBody 変数に通信結果が代入されているはずです。

つまり、Ajax 通信の成功有無はチェックする必要がないと判断しました。

利用は、selectText.call(pageBody,"抽出開始文字","次の抽出開始文字") で行います。敢えて、pageBody から関数を call 起動し、呼び出された関数内で this が pageBody を参照するようにしました。

「開始文字」から「次の開始文字」の 1 文字前迄の文字列群を、Ajax 通信で取得したブログページ全文文字列情報 pageBody から sliceする訳です。これによりコード全文から、必要部分を抜粋することが出来ます。

具体的に抽出例に触れてみます。

下のコードはコード全文から、「 1:」で始まる部分から、「 24:」で始まる部分の 1 文字前迄を抽出し、それを pre タグで囲んで animatedPopup メソッドの第一引数とし、その他当該メソッドに必要な引数を指定して、animatedPopup メソッドを起動するコーディングです。

ID 名 「dt792-1」要素のクリックイベントハンドラーとして、animatedPopup 関数を登録しています。

padding 指定を行っていますが、ブログテンプレートの指定が優先されてしまうようで、引数で指定した padding 通りには animatedPopup しません。これは animatedPopup において popup アップするコンテンツは div 要素で囲まれているのですが、この div 要素にテンプレート固有のスタイル設定が施されているためであると、推測できます。

$("#dt792-1").click(function(){
  $(this).animatedPopup("<pre id='code792-1' class='point ml_1 lh_11 ov_a'>"
    + selectText.call(pageBody,"  1:"," 24:")+ "</pre>",
    {popupCSS:{top:"10px",right:"10px",width:600,origin:2,textAlign:"left",
    paddingLeft:"3px !important",paddingTop:"3px !important",paddingBottom:"3px !important",paddingLeft:"3px !important"}});
}); 

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

恐縮ですが 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 プラグインを管理するために必要となる共通関数

マウスカーソル位置、要素位置、要素サイズ、window サイズ、スクロール値などの値は、jQuery プラグインを作成する上で、あれこれの複数のプラグインに共通して必要となる基本的な情報であり、どのプラグインからも使用できるように定義することが求められる。

こうして複数のプラグインから共通して利用できるオブジェクトを作成した。

概要は以下の通り。

  1. 各プラグインから共通して使用する jQuery クラスオブジェクト F を、任意の 1 以上のプラグインコードが記されたファイル内の無名関数内に配置する。
  2. プラグインファイルが 2 つ以上に分割されていても利用できるようにする。
  3. F のプロパティには HTML 要素や winodw の位置、大きさ、スクロール値を算出する関数を登録する。
  4. また、window resize、window scroll 及び mousemove イベントの各ハンドラーも F のプロパティに登録する。
  5. イベントハンドラーを除く上記の諸関数による取得値を、各プラグインコード内の変数や、HTML 要素を参照するオブジェクト( jQuery インスタンスオブジェクトを含む )に登録できるように、諸関数の引数を工夫する。
  6. F オブジェクトには getPos、getSize、getScroll、eF、winSize、winscrollPos、mousePos の 8 つのプロパティを設け、各プロパティ値を全て関数か、関数の実行返値とする。
  7. getPos、getSize、getScroll の 3 つの関数は、eF 関数オブジェクトから、あるいは各プラグインコード内に設けられた DOM Reaady 関数内から直接び出され、これらに返値を返す。
  8. 複数のプラグインから利用することを想定しているイベントハンドラーは、試行錯誤の結果、各プラグインコード内の変数や、HTML 要素を参照するオブジェクトに登録するのではなく、直接 $.F オブジェクトのプロパティ値として登録することにした。
  9. 言わずもがなのことであるが、mousemove イベントは DOM が Ready された状態でマウスカーソルが移動することによって発生する。そのため例えば DOM が Ready される前に、マウスカーソルを当該ブラウザとは別のアプリケーションに移動した場合には $.F.mousePos オブジェクトが誕生する前に、それを利用するコードが進行する場合もある。このような場合に対処するために、$.F.mousePos = {X:"0px",Y:"0px"};の一行を挿入した。

その jQuery クラスオブジェクトについて

オブジェクト名は単純明快に F とし、次のように定義した。因みに jquery.js による jQuery クラスオブジェクトには F プロパティは存在しない。

  $.F ={
    // 要素の位置計測
    getPos : function(elem){
      if (!$(elem)||!$(elem).offset()) return false;
      var off = $(elem).offset(),
      	  pos = $(elem).position();
      return {
        left : off.left, // 絶対座標横方向値
        top : off.top, // 絶対座標縦方向値
        posLeft : pos.left, // offsetParent座標横方向値
        posTop : pos.top // offsetParent座標横方向値
      }
    },
    // 要素の大きさ計測
    getSize : function(elem){
      if (!$(elem)||!$(elem).width()) return false;
      if (elem===window){
        return { //内容辺サイズ、border 辺サイズ
          cW:$(elem).width(), cH:$(elem).height(),
          oW:$(elem).outerWidth(), oH:$(elem).outerHeight()
        }
      } else {
        return { //内容辺サイズ、padding 辺サイズ、border 辺サイズ及び margin 返サイズ
          cW:$(elem).width(), cH:$(elem).height(),
          iW:$(elem).innerWidth(), iH:$(elem).innerHeight(),
          oW:$(elem).outerWidth(), oH:$(elem).outerHeight(),
          oMW:$(elem).outerWidth(true), oMH:$(elem).outerHeight(true)
        }
      }
    },
    // スクロール値取得
    getScroll : function(elem){
      if (!$(elem)) return false;
      return {L:$(elem).scrollLeft(), T:$(elem).scrollTop()}
    },
    // ノードに位置、サイズ及びスクロールの各プロパティを設定する。
    eF : function(elem){
      if (!$(elem)) return false;
      elem.pos = $.data(elem,"pos") || $.data(elem,"pos",$.F.getPos(elem));
      elem.size = $.data(elem,"size") || $.data(elem,"size",$.F.getSize(elem));
      elem.scroll =$.data(elem,"scroll") || $.data(elem,"scroll",$.F.getScroll(elem));
    }
  }
  // $.F オブジェクトにwindowサイズとスクロール値及び
  // resize event ハンドラー, scroll event ハンドラーを登録する。
  $(function(){
      $.F.winSize = $.F.getSize(window); // 現在値の取得
      $.F.winScrollPos = $.F.getScroll(window); // 現在値の取得
      $.F.mousePos = {X:"0px",Y:"0px"}; // 初期値設定
      //resize,scroll イベントハンドラー
      $(window).resize(function(){$.F.winSize = $.F.getSize(window)})
        .scroll(function(){$.F.scrollPos = $.F.getScroll(window)});
      // body 上での mousemove イベントハンドラー。マウス位置取得
      $(document.body).mousemove(function(e){
        $.F.mousePos={X:e.pageX, Y:e.pageY};
      });
    }
  })

▲ToTop

各プラグインから共用する F オブジェクトの説明

$.F.eF メソッドにより任意の HTML 要素の位置、大きさおよびスクロール値を取得する。また、$.F オブジェクトの winSize 及び winScrollPos プロパティに、頁オープン時の window の大きさとスクロール値を取得し、かつ、これらの値は window resize 及び window scroll イベントに対応させた。更に $.F.mousePos プロパティに、mousemove イベントによるマウスカーソル位置を取得した。

なお、$.F オブジェクト定義時(つまり頁を開いた時)にマウスカーソルが当該 window 内にない場合もあるので、mousePos プロパティは初期値として {top:0,left:0} を設定した。そうしないとマウスカーソルが window 外にある場合には、$.F オブジェクト定義時に mousePos プロパティが未定義になってしまうからである。

eF メソッドにより取得された値は、対象となった HTML 要素オブジェクトの pos、size 及び scroll プロパティ値に格納される。このことを jQuery インスタンスで表現すれば、jQuery における対象要素表現 strings を expr として、jQuery(expr)[0].pos、jQuery(expr)[0].size、jQuery(expr)[0].scroll である。

DOM Ready 関数内で取得された window サイズ、スクロール値あるいはマウスカーソル絶対座標値は、$.F オブジェクトの winSize、winScrollPos あるいは mousePos プロパティに格納される。

F オブジェクトや複数のプラグインを内包する無名関数の構成

■ 1 以上のプラグインコードを含む 1 つの任意のプラグインファイル内の冒頭で、
  $.F オブジェクトを登録する。
(function($){
  // 複数のプラグインから利用する F オブジェクトを定義する
  $.F={……}; 
  $(function(){
    $.F.winSize = $.F.getSize(window);
    ・・・
    ・・・
    $(document.body).mousemove(function(e){
      $.F.mousePos={X:e.pageX, Y:e.pageY};
    });
  });
  $.fn.pluginA = function(){ // プラグイン A
    // eF 関数を jQuery インスタンスの i 番目の要素を引数にして起動。これにより、
    // 当該要素の絶対位置とオブセットペアレント内の位置が当該要素の pos プロパティに、
    // 当該要素の大きさが当該要素の size プロパティに、
    // 当該要素のスクロール値が当該要素の scroll プロパティに、それぞれ設定される。
    $.F.eF(this[i]);
    ・・・・・・・
  };
  $.fn.pluginB = function(){ // プラグイン B
    $.F.eF(this[i]);
	・・・・・・・
  };
  ・・・・・・・
})(jQuery)

■ 別のプラグインファイル内からの $.F オブジェクトの利用
(function($){
  $.fn.pluginC = function(){ // プラグイン C
    // eF 関数を jQuery インスタンスの i 番目の要素を引数にして起動。これにより、
    // 当該要素の絶対位置とオブセットペアレント内の位置が当該要素の pos プロパティに、
    // 当該要素の大きさが当該要素の size プロパティに、
    // 当該要素のスクロール値が当該要素の scroll プロパティに、それぞれ設定される。
    $.F.eF(this[i]);
    ・・・・・・・
  };
  $.fn.pluginD = function(){ // プラグイン D
    $.F.eF(this[i]);
	・・・・・・・
  };
  ・・・・・・・
})(jQuery)

jQuery を使って画像も動画も Popup する

popup メソッドは2年半前に一度作成していた

2007年12月。PopupTips と題して、jquery.js を最小限度使用してポップアップを作りました。お陰様で 6 件の拍手を戴きましたが、パソコンの高速化がますます定着し、加速されている今、jquery.js をフル活用したコードでも、描画に大きな支障は出ない程度にまでパソコン性能が向上しました。
だから、というわけでもありませんが、今回改めて jquery.js をフル活用して popup メソッドを作り直しました。

それは上の「関連エントリイ」に掲載した jQuery()活用(2) Popup Tips を自作する──jQuery解読(22) に詳述したので、関心のある方はそちらをご覧戴ければ幸いです。

jquery.js をフル活用して作成した popup メソッドの概要

ここでは自作の setPopup プラグインメソッドの概要を紹介させて戴きます。なお、以下のアンダーライン部に popup を仕込みましたので、どんな具合に作動するのか、このエントリイで確認して戴くことが出来ます。

  1. 改めて作成した popup は それを引き起こすタグ要素の title 属性値をそのコンテンツとするように設計しました。つまり、何らかの class 指定を行わなくても、jQuery('*[title]').setPopup(args) で popup 励起要素が指定出来るようにしました。
  2. 勿論、特定の要素に class 指定を施して、それらの要素に対してだけ popup を表示させることも可能です。つまり、popup 励起要素はいかようにも自由に指定できるようにしました。
  3. popup 励起要素内で mouseenter/mouseleave イベントが発生すると、popup 要素が表示/隠蔽されることは勿論ですが、更に mousemove イベントが起きると、popup 要素はマウスカーソルに追随して移動するように設計しました。なお、その際には popup 要素が画面からはみ出さないようにもしました。
  4. popup する内容はテキスト文字列だけではなく、任意の HTML タグ要素を指定出来るようにしました。つまり、title 属性値に記述した HTML 文が HTML 文として解釈され、popup 要素内に描画されるようにしました。例えば、番号付き箇条書きや画像を popup させることも可能です。(ここではパノラマ写真を popup します)
  5. 次に動画を popup させてみます。なお動画表示の場合には HTML 文に若干の工夫が必要です。popup 要素内にはマウスカーソルを移動出来ないので、自動再生を指定しておく必要がある、ということです。
    なお、youtube 動画の自動再生指定方法は、 youtubeの動画を自動再生する方法 に大変分かりやすく記されていました。

▲ToTop

どのエントリイでも、どのブログでも使えるように plug-in 化

作成した Javascript コードの全容は こちら に掲載しておきましたが、このコードをプラグイン化したことを強調しておこうと思います。

プラグイン化により、このブログのどのエントリイでも、否、このブログだけではなく、また Fc2 ブログでなくても、任意のブログで使える仕様になっているはずです。

jquery.js をフル活用しつつ、汎用的に利用できる───今後作成するjquery.js を利用したコードは、可能な限りこのような仕様にするつもりです。それこそが jquery.js を真の意味で活用することになるはずですし、そうなればこその「やり甲斐」でもあります。

jQueryを使ってブログエントリイの目次を自動作成する。

この上の目次は以下に述べるスクリプトにより自動作成されたものである。

このエントリイの後に、別のもっとクールな目次自動作成プラグインを作った。このため、このエントリでのみ 2 通りの目次が作成され、表示されるようにした。この他のエントリイでは新しい目次作成プラグインを使用するようにしたため、このエントリイでのみ旧バージョンとなってしまった目次作成プラグインを表示している。

なお、新しい目次自動作成プラグインについては、こちらで 触れている。

エントリイ目次の作成意義

私のブログエントリイは、各エントリイの文章が長くなる傾向にある。

そして長いエントリイの場合には、全体を概観する必要が生じるので目次があった方が良い。

しかも、その目次は或るルールに基づいて作成することにすれば、エントリイ毎に作らなくてもスクリプトで自動作成させることが可能だ。

そこで、jQuery を活用する簡単な例として作ってみた。

エントリイ目次のスクリプトによる作成方法

目次項目はヘッダー要素のコンテンツをピックアップすれば良く、それに頁内リンクを貼った上で箇条書き整形してから、エントリイ内に挿入すればよい。

なお、頁内リンクを貼るということは、リンク先となるヘッダー要素のコンテンツを a タグで包含しなければならず、その行為もスクリプトで自動的に行うべきだろう。

目次自動作成:makeContents メソッドコード

以下に makeContents コードとその各行説明を掲載したが、コードの要点を列挙しておきたい。

  1. makeContents メソッドは汎用性が高いプラグイン形式とした。
  2. このメソッドに 2 つの引数を用意した。これは、初期状態で目次を表示するか否かを指定し、また目次の背景色を自在に変更可能にするためだ。なお、2 つの引数の記述順は指定せず、いずれが先でもインタープリタが「正しく理解」できるよう工夫した。
  3. 初期状態で目次が表示されているか否かに応じて、目次表示/隠蔽ボタンの機能は変化するが、それを 1 つの toggle イベントハンドラーで扱うにあたり、ボタン背景色と slideDown/Up を交互に入れ替えるために配列の reverse メソッドを活用した。
  4. blur メソッドを使ってボタン要素にフォーカスが残らないようにした。
  5. 自作コードで初めて wrapInner メソッドを利用した。これは或るタグ要素内のコンテンツを、新たな子要素で囲む場合に重宝するメソッドだ。或る要素Aの子要素Bを新たな子要素Cを挿入してCの子要素(つまりAの孫要素)にする便利なメソッドである。
  6. $btn オブジェクトに見られるように jQuery インスタンスメソッドチェーンを多用した。メソッドチェーンは余りの便利さに快感すら覚える。これもまた jquery.js の醍醐味と言えよう。
    因みにここでチェーンしたメソッドは、順に .css、.prependTo、.toggle だ。しかもjQuery インスタンスメソッドの返値が jQuery インスタンス自身となることを利用して、チェーンメソッドが適用されている jQuery インスタンスを右辺に配置して、これを左辺の変数に代入することまで一つの式で実現した。
  7. 上で述べた jQuery インスタンスメソッドの返値を利用した式は、他の箇所でも多用している。変数 $contents や $h4 への代入式もまた、 jQuery インスタンスメソッド返値が jQuery インスタンスであることを利用している。
// makeContents メソッドをプラグインとして登録する。
(function ($){$.fn.makeContents = function(display,color){
  // 個別エントリイ表示モード以外の場合何もしない。
  if (!/.+blog-entry.+html$/.test(location.href)) (function(){return;})();
  else { // 個別エントリイ表示モードならば実行する
    $(function(){ // DOM ready イベントに登録する。
    // もし #contents 要素が存在すればそれを削除する。
    if ($("#contents").size()) $("#contents").remove();
    // ローカル変数定義
    var $entry, $contents, $h4, $btn, o = [], bgColor=
      // 目次ブロックの背景色を設定する。
      (typeof display==="boolean" && typeof color==="string" && color) 
      || (typeof color==="boolean" && typeof display==="string" && display)
      || "darkslategray";
    // エントリイ本体部分へのショートカット作成
    $entry = $("div.entry_body");
    // 目次を表示する ol 要素をスタイルシート付きでエントリイ
    // 最上部に挿入する。但し、コード進行が終了間際迄は非表示としておく。
    $contents = $("<ol id='contents' />").css({
      margin:"1em 2em",padding:"0.5em 2em",border:"1px dotted white",
      lineHeight:"1.1em",background:bgColor
    }).prependTo($entry).hide();
    // H4 ヘッダーコンテンツに id を付け、H4 ヘッダーコンテンツを
    // 頁内リンク付きの LI 要素として目次の OL 要素内に挿入する
    $h4 = $("h4",$entry).each(function(i){
      $(this).wrapInner("<a id='contents"+i +"'></a>");
      $contents.append(
        "<li><a href='#contents"+i+"'>"+$(this).text() +"</a></li>"
      );
    });
    // ボタン背景色と slide メソッドを配列に登録
    o[0]={btncolor:"pink",fn:function(){$contents.slideDown()}};
    o[1]={btncolor:"palegreen",fn:function(){$contents.slideUp()}};
    // 目次を最初に表示しない指定の場合、
    // 色と slide メソッドのセットを入れ替える。
    if (!display) o.reverse();

    // 目次の表示/隠蔽を操作するボタンをエントリイ本体上部に配置し、
    $btn = $("<button>目次の表示/隠蔽</button>").css({
      display:"block",background:o[0].btncolor,
      fontWeight:"bold",width:"150px",margin:"0 auto"
    }).prependTo($entry).toggle(function(){ // toggle イベントを登録する。
      // slide メソッドの実行。
      o[1].fn();
      // ボタンの背景色を変更する。
      $(this).css({background:o[1].btncolor}).blur();
    },function(){
      o[0].fn();
      $(this).css({background:o[0].btncolor}).blur();
    });
    // 目次表示モードが指定されている場合には、slideDown メソッドを実行する。
    if (display===true || color===true) $contents.slideDown();
  });}
}})(jQuery);
// makeContents メソッドを目次表示モード( true 指定 )で起動する。
$().makeContents(true);
//$().makeContents(); //エントリイ表示時に目次を非表示にする場合の指定
//$().makeContents(true, "navy"); //エントリイ表示時に目次を表示しその背景色をnavyにする場合の指定

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 を利用した Ajax 通信の一例(改訂版コードについて)

コード改訂に至る経緯

約 2 年程前に上記関連リストに掲載したエントリイを投稿しました。

そこではjquery.js の Ajax コードを活用する一例として、今閲覧しているエントリイの直前 10 エントリイ、直後 10 エントリイ及び最新の 10 エントリイの、それぞれのタイトルを Ajax 通信で取得してみました。

その後ずっとそれを使い続けてきたのですが、あることからコードを見直す必要に迫られ、今回改めてコードを全面的に見直し、要所をいくつか改訂しました。

改訂を思い立ったのは、直前/最新/直後エントリイタイトルの表示位置が、ブラウザによっては、どうも思ったように描画されていないことに最近気がついたからです。(セットしてから 2 年も経っているのに今更かい?!(^^;; )

IE と Firefox では意図したとおりになっていたのですが、safari や chrome ではずれていたのです。

そしてその原因は、自作コードにおける offsetParent に対する曖昧な対処方法であろう、と推測できました。

しかし、これだけのことでは 2 年前のコードを改訂する動機として弱すぎました。2 年も経つと自作コードであっても可読性はかなり低下するので、思い出す時間と、当時の到達点まで改めて登る努力が必要となります。

重い腰を上げたのは、先月に解読した jQuery(~).offset メソッドや jQuery(~).position メソッドを、今後様々に活用したいと思ったからです。このブログ内で、これらのメソッドや Animation に関するメソッドを活用して、様々な実験を行いたいと思っているのですが、そのためには HTML 要素の位置が正確に把握できなければなりませんし、CSS の決まり事をしっかり押さえなければなりません。

こうして 「offsetParent の扱いを曖昧にしたままでは適切な実験が行えない」 と判断し、曖昧なコードを改善しなければならなくなったのです。

▲ToTop

今回の作業(コード改訂作業を含む)の苦労談(戒めのためのメモ)

1. offsetParent 問題

今年 1 月迄はこのブログの container 要素 には position 指定を行ってきませんでしたが、今回の一連の改訂作業を通じて container 要素には position:relative を指定しました。(※ このブログの container要素は body 要素直下にあり、ブログタイトルや時計、カレンダーそしてエントリイ等を包含する要素 )

その container の中に Javascript コードによって、10項目ずつの以前 / 以後 / 最新エントリイタイトルを表示する 3 つの要素( 以後「popup要素」と呼ぶ )を絶対配置するのですが、container の親要素及び祖先要素に position 指定をしていない状態では、W3C 勧告に従えば、popup 要素の offsetParent は body となります。

しかし、IE( ver 8 の標準モード以外の IE )では、祖先要素に明示的に width や height 値が指定されている要素があると、それが offsetParent となってしまいます。そしてこれまで position 指定をしていなかった container には width が指定されています。この結果 IE 7 以前で閲覧すると、popup 要素が container 内に意図したとおりに表示されていたのです。

他方、safari や chrome では、popup 要素の表示位置が意図した位置から「ずれて」いました。W3C 勧告に従っているこれらのブラウザでは、container 要素内に置かれた popup 要素の offsetParent が body となっていたために、思惑と異なってずれていたのです。(但し、同じように W3C に従っているはずの Firefox では、IE 同様に container 内に収まっていました。その理由を未解明です。)

さて、このようなブラウザによる「ぶれ」を放置したままでは、要素を自在に配置することは出来ません。当然画面内で要素を任意の位置に移動させるアニメーションも思うように操れません。

しかし、offsetParent や offsetTop、offsetLeftプロパティは、ブラウザによって解釈が異なる場合があるだけではなく、そもそも W3C 標準ですらありませんHTML要素の位置取得 - Rails で行こう!)。そのためそのまま使うだけでは、クロスブラウザ対応にはなりません。そこで利用したいと考えているメソッドが、jquery.js の offset と position インスタンスメソッドです。

ところで、W3C 勧告には至っていませんが、offsetParent などについては 「 CSSOM View Module 」という草案が存在しています。そして、jquery.js はそれに準拠するよう配慮されています。

そこで、Ajax コードを改定する前に、まず container 要素に position:relative を指定し、当該要素内に配置される要素の offsetParent が container 要素になるようにしました。これにより CSSOM View Module( W3C Working Draft 04 August 2009) に従って、要素を配置する前提条件が整うことになります。

こうしておいて、jquery.js の offset と position インスタンスメソッドを活用して、 CSSOM View Module に従って、どのブラウザでも同じ位置に要素を配置することを目指します。但し、この問題はエントリイを改めて述べます。

▲ToTop

2. Ajax 通信が終わったことを Javascript インタープリタに知らせる方法

案の定、Ajax 通信コードの改訂作業には時間が掛りました。

久しく Ajax コードを弄っていなかったため、コード内での Ajax 通信独特の「時間差」への配慮も、最初は全く忘れていました。また jqquery.js における Ajax 通信処理の理解が半可通であることも思い知らされました。そこで、自戒を込めて再学習した jquery.js の Ajax 通信コードの要点を整理しておくこととします。

2 年前に完成を見て、つい数日前まで使ってきた従前バージョンの get3modeEntryTitles.js では、setInterval タイマー関数と jQuery.active プロパティ( このプロパティは Ajax 通信の終了を記録する flag の役割を果たしている )を組み合わせて、Ajax 通信の終了を Javascript インタープリタに認識させていました。

しかし、今回改訂した最新の get3modeEntryTitles.js では、ajaxStop イベントハンドラーを使って同じ処理をさせることにしました。この方がコードがすっきりするし、そもそのそのような用途として開発された Ajax イベントハンドラー関数を利用しない手はないからです。

3. 先行する Ajax 通信による取得値を利用する後発 Ajax 通信の起動タイミングの設定

後発 Ajax 通信が先発 Ajax 通信結果を利用する場合、後発 Ajax 通信の開始タイミングがコードの正否を分けます。

勿論、単に後発通信コードを先発通信コードの後に記述するだけでは、何の解決にもなりません。先発通信結果を利用するのですから、それが終わらない限り後発通信を開始してはいけないのに、単にコード記述上前後の順番通りに書くだけでは後発通信は成功しません。

コード進行速度は Ajax 通信速度を遙かに上回るので、先行通信が終わらないうちに後発通信が開始されてしまい、後発通信は必要な情報を先発通信から取得できないからです。

そこで改訂版では、setInterval 関数を起動して、後発通信で使用する先発通信結果の変数が存在するまでループさせ、その存在を確認できてから後発通信を開始するようにしました。

改訂前は jQuery.active 値がゼロになるまでタイマーを繰り返し起動していたのですが、必要な変数の存在確認の方が直接的・具体的であり、可読性も高まります。

▲ToTop

改訂した get3modeEntryTitles.js コード

HTML 文や CSS 文は、jQuery Ajax を活用したエントリイタイトルの各種取得法──集大成! に掲載したものを大部分踏襲しました。但し、CSS 文において visibility 属性の使用はやめ、display属性で統一することにしました。今回 faseIn/Out メソッドによって popup 要素の表示/隠蔽を行うように変えたのですが、これは jquery.js の 各種 animation メソッドは、表示/隠蔽の切り替えに display 属性を利用しているためです。

■改訂した get3modeEntryTitles.js コード
/*
 * fileName : get3modeEntryTitles.js
 * released : 2008/03/01
 * verup : 2008/07/04, 2008/08/08, 2008/08/17, 2010/02/14, 2010/05/15
 */
  1:// 個別エントリイ表示モードでない場合には、何もしないでコード進行を終える。
  2:if (location.href.indexOf("entry")==-1) (function (){return;})();
  3:else{ //個別エントリイ表示の時
  4:(function($){
  5:  var now=function(){return +new Date;};  // 現在時刻取得
  6:  $.tr={ //所要時間計測・待機起動回数記録用 jQuery 拡張オブジェクト
  7:    start:now(),
  8:    registerEvent:"",
  9:    ajax:{ recent:{},before:{},after:{} },
 10:    end:"",
 11:    setEndingCnt:0,waitRecentAjaxCnt:0
 12:  };
 13:  var loading="<div id='loading' class='ta_c'><img src='http://blog-imgs-31.fc2.com/h/k/o/hkom/loading_16.gif' alt='' border='0' width='16' height='16' /> Now Loading...</div>";
 14:  $(function(){
 15:    $("#container").append(
 16:      "<div id='popup_before' class='popup'>" +loading+"</div>",
 17:      "<div id='popup_recent' class='popup'>"  +loading+"</div>",
 18:      "<div id='popup_after' class='popup'>"  +loading+"</div>"
 19:    );
 20:
 21:    // イベント発生要素に沿って popup 要素を配置する(表示へ別メソッドで行う))
 22:    var $popup = $(".popup"),$navi=$(".navi_container"),left,top;
 23:    // 各 popup の縦位置(ナビゲブロックの高さだけ下に)指定
 24:    top = $navi.position().top + (parseInt($navi.css("marginTop")) || 0)+ $navi.outerHeight();
 25:    $popup.width(600);
 26:    var space = $("#container").innerWidth()-$popup.outerWidth();
 27:    // 各 popup の横位置指定
 28:    $.each( ["before","recent","after"] ,function(i,item){
 29:        left= !i ? 0 : i===1 ? space/2: space;
 30:        $('#popup_'+item).css({left:left+"px", top:top+"px"});
 31:    });
 32:
 33:    // 前・最新・後リストの表示/隠蔽イベントハンドラー
 34:    $.each( ["before","recent","after"] ,function(i,item){
 35:      $("#"+ item + "EntryTitle" ).hover(
 36:        function(){  $('#popup_'+item).fadeIn(); },
 37:        function(e){
 38:          var itsIdName = e ? e.relatedTarget.id : window.event.toElement.id;
 39:          if (itsIdName.indexOf("popup") < 0)
 40:            $popup.fadeOut();
 41:        }
 42:      );
 43:    });
 44:
 45:    // 表示された popup の hover イベントハンドラー
 46:    $popup.hover(
 47:      function(e){if (!$(this).is(":hidden")) return;},
 48:      function(e){ $(this).fadeOut(); }
 49:    );
 50:  });  //End of $(function(){})
 51:
 52:  // variable
 53:  var html = {recent:[],before:[],after:[]}, getStr={recent:0,before:0,after:0},
 54:    lastNo, regExpr,thisEntryNo, complement,itval,border={before:0, after:0},realElm = {before:0,after:0},flagStopHandler = false;
 55:
 56:  // 最近のタイトルを取得する関数を定義
 57:  var makeRecentEntryList = function (limit){
 58:    var No, subject, date, iter=0, ret=[], $chd;
 59:    var target ={    //数字は xml ファイル内で登場する順番
 60:          link:[],    //0
 61:          title:[],    //1
 62:      //  description:[],  //2
 63:        //  content:[],    //3
 64:          subject:[],    //4
 65:          date:[]      //5
 66:      };
 67:    $.ajax({
 68:      url: /http:.+fc2\.com\//.exec(location.href)[0] + "?xml" || null,
 69:      type: "GET",
 70:      dataType: "xml",
 71:      global:false,
 72:      success: function(xml){
 73:        var tmpStr = '<div>'+decodeURI(encodeURI("最新のエントリイ情報がありません。"))+'</div>';
 74:        if (xml==undefined || xml==null) {getStr.recent = tmpStr; return;}
 75:        $.tr.ajax.recent["start"]=now();
 76:        $.each(target,function(key){
 77:          $.each($("item",xml), function(i,n){
 78:            $chd = $(n).children();
 79:            ret[i]= [$chd.eq(0), $chd.eq(1), $chd.eq(4),$chd.eq(5)];
 80:            target[key].push( ret[i][iter].text() );
 81:          });
 82:          iter++;
 83:        });
 84:        for (var i=0; i < limit; i++) {
 85:          No = /entry-([0-9]+)/.exec(target.link[i])[1];
 86:          i==0 && (lastNo = Number(No));
 87:          subject =" , " +target.subject[i];
 88:          date =" , " +target.date[i].substring(0,10);
 89:          html.recent.push( "<li><a href='" + target.link[i] + "' target='_blank'>" +  target.title[i] + "</a> (No." + No + subject + date + ")</li>");
 90:        }
 91:        $.tr.ajax.recent["end"]=now();
 92:        getStr.recent =  "<div class='ta_c'><em>Recent " + limit + " Entries</em></div><ul class='ml_1_5'>" + html.recent.join('') + "</ul>";
 93:      }  //End of success()
 94:    });  //End of ajax()
 95:  };  //End of makeRecentEntryTitle func
 96:
 97:  // 前後のタイトルを取得するための準備を行う
 98:  var makeEntryList = function(beforeafter,limit){
 99:    var thisHTTP, getEntryNos=[], thisURL=[];
100:    regExpr = /(http:.+entry-)([0-9]+)/;
101:    thisEntryNo = Number(regExpr.exec(location.href)[2]);
102:    thisHTTP = regExpr.exec(location.href)[1];
103:    border[beforeafter] = Math.min(limit+1, beforeafter == "before" ? thisEntryNo :(Number(lastNo)-thisEntryNo+1) );
104:    if (border[beforeafter]==1) thisURL.length=0;
105:    else {
106:      for (var i=1; i < border[beforeafter]; i++)
107:        thisURL.push(thisHTTP + (thisEntryNo - (beforeafter == "before" ? i : -i)) +".html" );
108:    }
109:    // 準備完了! Ajax 通信開始
110:    getTitlesByAjax.call(this, beforeafter,thisURL);
111:  };
112:
113:  // Ajax 通信により前後のエントリイタイトル等を取得する関数の定義
114:  function getTitlesByAjax(tense,thisURL){
115:    if ( thisURL.length != 0 ) {
116:    $.each(thisURL,function(i,aryitem){
117:      $.tr.ajax[tense][i]={};
118:      $.tr.ajax[tense][i]["start"]=now();
119:      $.get(aryitem,function(data){  //data は thisURL[i] の html テキスト文
120:        regExpr = /<title>(.*)<\/title>/;
121:        var titleStr = regExpr.exec(data)[1].substring(19);
122:        if ( /\S+/.test(titleStr)){  //空白だけのタイトル名は補足しない。
123:          ++realElm[tense];
124:          html[tense][i] = "<li><a href='" + aryitem + "' target='_blank'>" + titleStr +" (EntryNo." + /entry-([0-9]+)/.exec(aryitem)[1] + ")</a></li>";
125:        }
126:        $.tr.ajax[tense][i]["end"]=now();
127:      },"text");
128:    });
129:    }
130:  };
131:
132:  function ajaxStopHandler(){
133:    flagStopHandler = true;
134:    $.each(["before","after"],function(i,tense){
135:      if (realElm[tense]==0)
136:        getStr[tense]="<div class='ta_c'>"+ (tense=='before' ? "Before " : "After ") + "Entry "+ decodeURI(encodeURI('はありません。')) + "</div>";
137:      else {
138:        complement = (border[tense]-1 -realElm[tense]!==0) ? 
139:          " ( " +decodeURI(encodeURI('欠番があります。')) +" )" : "";
140:        getStr[tense] = "<div class='ta_c'><strong>"+ (tense==='before' ? "Before " : "After ") + realElm[tense] + " Entries" + complement +"</strong></div><ul style='margin-left:1.5em;list-style-type:disc'>" + html[tense].join('') + "</ul>";
141:      }
142:    });
143:    var beforeTime= realElm.before!==0 ? "<li>以前タイトル取得 Ajax 通信所要時間: "+ ($.tr.ajax.before[realElm.before-1].end-$.tr.ajax.before[0].start)/1000 +" 秒</li>" : "";
144:    var afterTime= realElm.after!==0 ? "<li>以後タイトル取得 Ajax 通信所要時間: "+ ($.tr.ajax.after[realElm.after-1].end-$.tr.ajax.after[0].start)/1000 +" 秒</li>" : "";
145:    var AjaxLog ="<ul style='margin:0.5em;border-top:white dotted 1px;padding:0.5em 0.5em 0 1em;list-style-type:circle'>"+
146:      "<li>クリック後 Ajax 通信開始迄の所要時間: "+($.tr.ajax.recent.start-$.tr.start)/1000 +" 秒</li>"+
147:      "<li>最新タイトル取得 Ajax 通信所要時間: "+ ($.tr.ajax.recent.end-$.tr.ajax.recent.start)/1000 +" 秒</li>"+ beforeTime + afterTime +
148:      "<li>このプロジェクト全体の所要時間: "+ (($.tr.end=now())-$.tr.start)/1000 +" 秒</li></ul>"+
149:      "<div style='text-align:right;margin:-2em 1em 0.5em 0'><button style='display:block;margin:-1em 0 0 auto;width:16em' title='このボタンをクリックするとここで行った Ajax 通信に関するこのブログのエントリイが開きます。' onclick='this.blur();window.open(\"http://hkom.blog1.fc2.com/blog-entry-626.html\",target=\"_blank\")'>この Ajax 通信や所要時間について</button></div>";
150:    $(function(){
151:      $("#popup_before").html(getStr.before + AjaxLog);
152:      $("#popup_after").html(getStr.after + AjaxLog);
153:      $("#popup_recent").html(getStr.recent + AjaxLog);
154:    });
155:  }
156:
157:  $(document).ajaxStop(function(){
158:    ajaxStopHandler();
159:  });
160:  // 前後及び最新のタイトルリスト作成
161:  makeRecentEntryList(10);  //最近リスト取得実行
162:  makeEntryList("before",10);  // 以前エントリイ
163:  function nextAjaxTimer(){
164:    $.tr.waitRecentAjaxCnt++;
165:    // makeRecentEntryList により lastNo 値を取得するまでは起動しない。
166:    if (!!lastNo) {
167:      if (ival) {clearInterval(ival);ival=null;}
168:      makeEntryList("after",10); // 以後エントリイ
169:    }
170:  };
171:  var ival = setInterval(nextAjaxTimer,100);
172:})(jQuery);
173:}

jquery.js のアニメーションコードの活用 ( 4 ) slideToggleEx プラグインの簡便な活用のために(完全版)

slideToggleEx プラグインは不完全だった

エントリイNo.731 において、slideToggleEx 自作プラグインメソッドについて詳述しました。

ところが、そこで紹介したプラグインメソッドコードは不完全でした。隠蔽状態から起動するアニメーション対応を全く考慮していなかったので、その状態で起動すると思わぬ動きをしてしまうのです。つまり、実は半分しか完成していないのでした。(^^;; ヒヤアセ

そのことに気がついたのは 9 月中旬。その時点から、改めてコード全体を見直さざるを得なくなり、改めて animate メソッドの再学習を迫られたのです。

その後一ヶ月余り。四苦八苦、七転び八起きを繰り返した末に、昨晩やっと隠蔽状態からの起動にも対応した slideToggleEx 版が完成したので、ここに投稿することにします。

用意するもの・ことは以下の 5 つです。(この部分は No.731 の再掲)
  1. include 用 slideToggleEx プラグインファイルと include タグ要素
  2. slideToggleEx が適用されるタグ要素
  3. 起終点を右側にする場合には、対象タグ要素に width スタイル属性を設定します。また起終点を下にする場合には height スタイル属性を設定します。そうしないと animate メソッドの中で適正な幅や高さが計測されない場合があるためです。
  4. slideToggleEx を呼び出し、適用させるタグ要素(一般的には button タグでしょう)
  5. その button タグのクラス属性に fireSlideToggleEx-n を追加します。( n は 0 から 8 の整数)

n は起終点を意味し次の通りの意味を持ちます。

0:ボックスの中心、1:左上端、2:右上端、3:右下端、4:左下端、
5:上辺、6:右辺、7:下辺、8:左辺

これだけ設定すれば、トリガーである button をクリックすると、n で指定した位置を起終点として、トリガーボタンの直後に配置された要素が slideToggleEx 適用対象要素となり、隠蔽されたり表示されるようなコード( エントリイ末尾に掲載 )を書きました。

「 これだけ 」と言っても結構たくさんあるため、最初は面倒に感じますが、直ぐに慣れますし、実際に使ってみると決して大変な手間ではありません。

必要な HTML 構造(この部分も No.731 の再掲)

上の 5 つだけで、slideToggleEx プラグインが簡単に使えるようにするために、もう1つ該当部分の HTML 文を特別な構造にする必要があります。これが 6 つ目にして最後の要件となります。その要件とは起動ボタンの直ぐ後に slideToggleEx 対象要素があるということです。

そうでない場合には 個々に slideToggleEx メソッドを呼び出すコードを書くことになります。

該当部分の HTML を以下のいずれかにする必要があります。(いずれのケースもボックスの左上を起終点とする場合で例示します。)

■ケース 1
<div><button class="fireSlideToggleEx-1">起動ボタン</button></div>
<div>・・・slideToggleEx対象要素・・・</div>

■ケース 2
<button class="fireSlideToggleEx-1" style="display:block;width:120px;">起動ボタン</button>
<div>・・・slideToggleEx対象要素・・・</div>

※ 2 行目の div 要素は別に div でなくてもブロック要素ならば何でも構いません。

ケース 1 は一般的な button タグの使い方の場合です。つまり button タグを inline 要素として配置する場合の HTML 文です。この場合には button タグの親要素の next sibling ブロック要素が slideToggleEx メソッドの適用対象となるようにコードを作りました。

一方ケース 2 は button タグをブロック要素として配置する場合です。この場合には当該のボタンタグの next sibling ブロック要素が slideToggleEx メソッドの適用対象となるようにコードを作りました。

このような当然と思われるHTML文構成にしておけば、運用に当たってどのようにしたかを思い出すのも容易ですし、仮に構造を忘れても直感的に思い出すことが可能となります。

▲ToTop

以上の前提で作った slideToggleEx プラグイン活用コード

アニメ継続時間と easing 関数は、後述する 「 click イベントを登録し slideToggleEx メソッド適用対象をピックアップするコード 」 において、1 秒に設定し、easing は無指定としています。このコードを弄れば自在に継続時間と easing 関数は変更できますが、使う際にその都度 duration と easing 関数を変更する必要はないと考え、(1) トリガーとなるボタンタグと (2) slideToggleEx 対象のブロックタグ、及び (3) slideToggleEx-n クラス名を指定するだけで使えればよいと判断しました。

以下に掲載したコードリストの表示と隠蔽には、当然 slideToggleEx を適用しています。直下のボタンをクリックすると、 slideToggleEx メソッドが起動し、アニメ対象要素であるコードリストが当該要素の右上から展開し、再びボタンをクリックすると右上へ隠蔽されます。( 起終点= 2 )

コードには各行に詳細な説明を記述したので、何を行っているのか分かると思います。

■プラグイン slideToggleEx() メソッドコード(完全版)
  0:// jQuery plugin : slideToggleEx() Release at 2009.8.21, ver 1.1:9.9, 1.3TheLastVer:10.9 01:00am
  1:(function($){
  // アニメーションコードの性格を簡単に記述しておく
  2:var ver="relative animation under position relative";
  // プラグイン指定
  3:$.fn.slideToggleEx = function( type, duration, easing, fn ){
  4: if (type > 8) // エラー対処
  5:  (function(){
  6:   alert("第 1 引数は 0~8 だけが指定できます。\nやり直してください。
      \n"+"slideToggleEx( 0~8, 継続時間, easing関数, アニメ後実行関数 )"); return;
  7:  })();
   // 変数設定、初期値複写
  8: var $target = this, o = $.extend({},$.fn.slideToggleEx.opts);
   // 9 ~ 23 行は、o オブジェクトにtargetとプロパティがない場合、
    * またはターゲットが異なった場合、つまり初めてそのインスタンスから呼ばれたか、
    * インスタンスが変わった場合に起動される。
    * このブロックはアニメ対象に対して様々な初期化を行う。
    */
  9: if(!o.target || o.target[0]!==$target[0]) {
 10:  $.extend(o,{
 11:   orig : o, // 当面使わないが念のために o をバックアップ
 12:   target : $target,
 13:   el : $target[0], // アニメ対象ノードへの参照を取得
 14:   type : type,
 15:   initstate : !$target.is(":hidden"), // アニメ前の状態が隠蔽か表示か
 16:   cnt : 1 // インスタンス毎の起動回数カウンター
 17:  });
    // アニメ対象が切り替わり、再びあるインスタンスに slideToggleEx メソッドが
    // 適用された時には、最初に呼び出された時の設定を再利用できるように data
    // メソッドを活用してコード進行の効率化を図る。
    // また o オブジェクトにアニメ対象要素等のサイズに係るプロパティを追加する。
 18:  $.extend(o, $.data(o.el,"initdone") || getSize());
    // アニメ対象ノードの固有 style 値を取得する。
 19:  for (var p in o.cssdefault.H) o.cssOrig.H[p] = $.css(o.el,p,true);
 20:  for (var p in o.cssdefault.W) o.cssOrig.W[p] = $.css(o.el,p,true);
    // 上のメソッドでは単位が付かないので付与する。
 21:  o.cssOrig.H.height +="px"; o.cssOrig.W.width +="px";
      // o オブジェクトを $.fn.slideToggleEx.opts オブジェクトに併合する。
 22:  $.extend($.fn.slideToggleEx.opts,o);
 23: }
   /* 24 ~ 36 行迄のブロックは、slideToggleEx が呼び出される度に
    * 作用する。27 行で type 値をチェックするのは、同一要素における異なる type 値
    * によるslideToggleEx メソッドの連続起動の場合に対処するためである。
    * slideToggleEx メソッドの連続起動の場合には、呼び出される度に o.duration、
    * o.easing、o.complete が変化する可能性があるので、これらも呼び出し毎に
    * 定義する必要がある。
    */
   // window サイズが変更された時には親要素サイズを計測し直す。
 24: window.onresize = getParentWH;
 25: o.cnt = $.fn.slideToggleEx.opts.cnt++; // カウンター値を 1 増する。
 26: o.odd = o.cnt % 2; // o.cnt が奇数か偶数かを判定する
 27: o.type = o.type!==type ? type : o.type;
   // duration などの初期値は $.fn.slideToggleEx.opts に用意しておく。
 28: o.duration = duration || o.duration;
 29: o.easing = typeof duration !=="object" ? (easing || o.easing) : "";
 30: o.complete = typeof duration !=="object" ? ($.isFunction(fn) && fn || o.complete) : "";
 31:
   /* アニメ対象ノードに AdjustN( N は type 値 )名で
    * 関連づけられたオブジェクトがなければ、adjustcssAnim 関数を呼び出し、
    * その返値を o.cssAdjust.show 及び .hide プロパティに代入する。
    * 関連づけられたオブジェクトがあればそのまま利用する。
    * ここに o.cssAdjust は animate メソッドで利用するアニメ用 CSS プロパティの内、
    * 起終点位置に応じて変化する top 及び lef 値だけを保持するオブジェクトである。
    * show 及び hide プロパティには "+=" などの relative アニメーション値が設定される。
    */
 32: o.cssAdjust = $.data(o.el,"Adjust"+o.type) ||
 33:  $.data(o.el,"Adjust" + o.type, {show:adjustcssAnim(true), hide:adjustcssAnim(false)});
   /* アニメ対象ノードに AnimN( N は type 値 )名で関連づけられた
    * オブジェクトがなければ、setAnimCSS 関数を呼び出してその返値を o.cssAnim
    * オブジェクトに代入する。関連づけられたオブジェクトがあればそのまま利用する。
    * ここに cssAnim は animate メソッドで利用するアニメ対象プロパティの全てを
    * 保持するオブジェクトであり、29 行で取得した top値と left値も併合する。
    */
 34: o.cssAnim = $.data(o.el,"Anim" + o.type) || $.data(o.el,"Anim" + o.type, setcssAnim());
   /* アニメ対象ノードに BeforeN( N は type 値 )名で関連づけられた
    * オブジェクトがなければ、makecssBefore 関数を呼び出してその返値を o.cssBefore
    * オブジェクトに代入する。関連づけられたオブジェクトがあればそのまま利用する。
    * ここに cssBefore は animate メソッド起動直前のアニメ対象要素のスタイル属性値を
    * 取得し、あるいは設定するためのオブジェクトである。
    */
 35: o.cssBefore = $.data(o.el,"Before" + o.type) || $.data(o.el,"Before" + o.type, makecssBefore());
   // 当該のアニメ対象要素に初めて、あるいは別のアニメ対象操作後に再び
   // slideToggleEx メソッドを適用した時に、最初に起動された場合にのみ適用するコード。
   // 対象要素の表示状態に応じて、animate メソッド適用前の CSS 値を設定する。
 36: if (o.cnt==1) $target.css( o.initstate ? o.cssBefore.show : o.cssBefore.hide );
 37:
   /* アニメーション起動
    * 第 2 引数がオブジェクトではない時
    * 要素の animate メソッド適用前の表示状態と slideToggleEx メソッドの起動
    * 回数に応じて、当該メソッド第一引数である CSS オブジェクトを選択的に適用する。
    */
 38: if (typeof o.duration !=="object")
 39:  return this.animate( (o.initstate && !o.odd || !o.initstate && o.odd)
 40:   ? o.cssAnim.show : o.cssAnim.hide, o.duration, o.easing, o.complete );
   // 第 2 引数がオブジェクトの時
   // 要素の animate メソッド適用前の表示状態と slideToggleEx メソッドの起動
   // 回数に応じて、当該メソッド第一引数である CSS オブジェクトを選択的に適用する。
 41: else
 42:  return this.animate( (o.initstate && !o.odd || !o.initstate && o.odd)
 43:   ? o.cssAnim.show : o.cssAnim.hide, o.duration ); // アニメ起動
 44:// 以下は以上迄のコードから呼び出される関数群である。
   /* getSize はアニメ対象ノードの position 指定を行い、サイズを測る関数。
    * この関数は或る要素に対して slideToggleEx メソッドを適用した初回だけにたった1 回
    * だけ起動される。 54 行と 35 行の data メソッドによって data 関連づけを行わっている
    * ので、同じ要素に対する 2 度目以降の呼び出し時には、たとえ非連続的に呼び出された
    * としても、この関数は呼び出されない。
    */
 45: function getSize(){
    // position 指定を relative に
 46:  if ($target.css("position") ==="static") $target.css({position:"relative"});
 47:  var obj={}, parent={};
 48:  return obj= {
       // 親要素の幅と高さを取得
 49:   parent : {W:getParentWH().W, H:getParentWH().H},
       // アニメ対象要素のマージン辺までの高さ(算出スタイル値)取得
 50:   oH : parseInt($.css(o.el,"height",true,"margin")),
       // アニメ対象要素のマージン辺までの幅(算出スタイル値)取得
 51:   oW : parseInt($.css(o.el,"width",true,"margin")),
       // アニメ対象要素のボーダー辺までの高さ(算出スタイル値)取得
 52:   oHb : parseInt($.css(o.el,"height",true,"border")),
       // アニメ対象要素のボーダー辺までの幅(算出スタイル値)取得
 53:   oWb : parseInt($.css(o.el,"width",true,"border"))
 54:  };
 55: }
 56: function getParentWH(){ // アニメ対象の親要素のサイズを計測取得する関数
 57:  var parent = $target.parent();
 58:  return o.parent={W:parent().width(),H:parent().height()};
 59: }
   // アニメーション起終点に応じたアニメ用 top/left 値設定関数
 60: function adjustcssAnim(showhide){
 61:  var plmn = showhide ? "-=" : "+=";
 62:  return o.type==0 ? {top:plmn + o.oH/2 +"px", left:plmn + o.oW/2 +"px"} : // center
 63:   o.type==1 ? {} : // 左上端。top も left もアニメ対象プロパティとしない。
 64:   o.type==2 ? {left:plmn + o.oW +"px"} : // 右上端
 65:   o.type==3 ? {top:plmn + o.oH +"px", left:plmn + o.oW +"px"} : // 右下端
 66:   o.type==4 ? {top:plmn + o.oH +"px"} : 3aspan class="aquamarine"
 
 67:   o.type==5 ? {} : // 上辺。top も left もアニメ対象プロパティとしない。
 68:   o.type==6 ? {left:plmn + o.oW +"px"} : // 右辺
 69:   o.type==7 ? {top:plmn + o.oH +"px"} : // 下辺
 70:   {} ; // 左辺。top も left もアニメ対象プロパティとしない。
 71: }
   /* アニメ用プロパティを作成する関数
    * o.cssdefault を基にして横方向/縦方向のプロパティを作成し、またその有無を
    * type 値毎に変化させる。
    * 例えば、type 値が 5 の場合、横方向のプロパティはアニメ対象とはしない。
    * また、margin は auto が指定されている場合にはアニメ用プロパティとせず、
    * その他の場合には他のプロパティと同様に"toggle"とする。
    * 79 行で横/縦方向のプロパティを合体させ、不透明度もアニメ用プロパティと
    * して追加する。
    * 最後に 80~82 行において、それまでに作ったアニメ用 CSS オブジェクトに
    * アニメーション起終点に応じて変化する top と left プロパティを追加する。
    */
 72: function setcssAnim(){
 73:  var obj = {};
 74:  obj.W = (o.type!=5 && o.type!=7) ? $.extend({}, o.cssdefault.W,
 75:    (o.oW!==o.oWb) ? {marginLeft:"toggle",marginRight:"toggle"} : {}) : {};
 76:  obj.H = (o.type!=6 && o.type!=8) ? $.extend({}, o.cssdefault.H,
 77:    (o.oH!==o.oHb) ? {marginTop:"toggle",marginBottom:"toggle"} : {}) : {};
 78:  obj = $.extend({}, obj.W, obj.H, {opacity :"toggle"});
 79:  return o.cssAnim = {
 80:   show:$.extend({},obj,o.cssAdjust.show),hide:$.extend({},obj,o.cssAdjust.hide)
 81:  };
 82: }
   // animate メソッド適用前の適用対象要素の CSS 値を作成する関数
 83: function makecssBefore(){
 84:  var obj = {}, ret = {};
    /* margin をアニメ対象にした場合において、margin 値が "auto" の時には、
     * jquery.js の各種メソッドでサイズを測定しても margin 値が捕捉できない。
     * その結果アニメーションは予期せぬ動きをしてしまうため、まず、margin 値を
     * 強制的にゼロにし、替わりに auto 指定によって確保された margin 値に相当する
     * left 値と top 値に置き換える。これにより予期したとおりのアニメーションを
     * 引き起こさせる。
     */
 85:  // if marginLeft/Top == "auto", fix marginLeft/Top.
 86:  obj.W = (o.el.style && $.attr(o.el.style,"margin-left")==="auto") ?
 87:   {left : (o.parent.W - o.oWb)/2 +"px", marginLeft : "0px", marginRight : "0px"} :
     /* この場合も、margin 値は $.css メソッドを使って取得しないと駄目。
      * 例えば margin 指定が簡略的に margin: 10px のように行われている場合には、
      * jquery.js は margin-left、margin-right などの個々の margin 値を取得しない。
      */
 88:   {left :"0px",
 89:    marginLeft:$.css(o.el,"margin-left",true), marginRight:$.css(o.el,"margin-right",true)};
 90:  obj.H = (o.el.style && $.attr(o.el.style,"margin-top")==="auto") ?
 91:   {top : (o.parent.H - o.oHb)/2 +"px",marginTop : "0px", marginBottom : "0px"}:
 92:   {top : "0px",
 93:    marginTop:$.css(o.el,"margin-top",true), marginBottom:$.css(o.el,"margin-bottom",true)};
    // ここ迄の処理結果を show プロパティに格納する。
    // このプロパティがアニメ対象が表示されている時のアニメ前 CSS 値となる。
 94:  ret.show = $.extend({}, o.cssOrig.H, o.cssOrig.W, obj.W, obj.H);
    /* 以下はアニメ対象が隠蔽されている場合の処理
     * アニメ用CSSオブジェクトの hide オブジェクトに top プロパティが存在している
     * 場合にのみ適用する。
     * top プロパティが存在していない場合には、top プロパティはアニメ対象としない。
     */
 95:  if (o.cssAdjust.hide.top) // アニメ用 CSS オブジェクト作成関数を利用して
     // アニメ前 CSS オブジェクトを作成する。
 96:   obj.top = new Number(parseInt(obj.H.top))
 97:    + new Number(parseInt(o.cssAdjust.hide.top.slice(2)))+"px";
    // アニメ用CSSオブジェクトの hide オブジェクトに left プロパティ
    // が存在している場合にのみ適用する。
    // left プロパティが存在していない場合には、left プロパティはアニメ対象としない。
 98:  if (o.cssAdjust.hide.left) // アニメ用 CSS オブジェクト作成関数を利用
     // してアニメ前 CSS オブジェクトを作成する。
 99:   obj.left = new Number(parseInt(obj.W.left))
100:    + new Number(parseInt(o.cssAdjust.hide.left.slice(2)))+"px";
    // ここ迄の処理結果を hide プロパティに格納する。
    // このプロパティがアニメ対象が隠蔽されている時のアニメ前 CSS オブジェクトとなる。
101:  ret.hide = $.extend({}, ret.show, obj);
    // data メソッドによる関連づけを行ってから ret オブジェクトを返す。
102:  return ret;
103: }
104:} // End of $.fn.slideToggleEx
105:
106:// default 値設定と記憶用プロパティとしての機能のために
107:$.fn.slideToggleEx.opts = {
   // 既定値としての引数設定
108: duration : 1000, easing : null, complete : function(){},
   /* auto 指定への対応が必要な margin、及び type 値に応じて変化させる
    * 必要のある top と left ───これらを除くアニメ対象プロパティリスト。
    * 羅列されているプロパティは、最小値の 0 又は 1 (width と height のみ)と
    * 要素固有のプロパティ値との間でアニメーションさせることになる。
    * 高さ方向と横方向とを区別するのは、type 値に応じてアニメ対象プロ
    * パティの要/不要が変化するためである。
    */
109: cssdefault : {
110:  H : {
111:   height:"toggle",paddingTop:"toggle",paddingBottom:"toggle",
112:   borderTopWidth:"toggle",borderBottomWidth:"toggle"
113:  },
114:  W : {
115:   width:"toggle", paddingLeft:"toggle",paddingRight:"toggle",
116:   borderLeftWidth:"toggle",borderRightWidth:"toggle"
117:  }
118: },
119: cssAdjust : {},
120: cssOrig : {H:{},W:{}},
121: cssBefore : {}
122:};
123:})(jQuery);

▲ToTop

slideToggleEx Javascript コード解説

直上に掲載したコードに至るまで、実は多くのバージョンを作成し、失敗しては作り直す「らせん的」過程を何度も繰り返しました。しかし、その苦労話の前に、まずコード作成の基本的方針を述べておくべきでしょう。

slideToggleEx が想定しているシーン
  1. slideToggleEx アニメ対象ノードは表示されていても隠蔽されていても、いずれの状態でも良い。
  2. 1 つの homepage や blog エントリイ内に、複数の slideToggleEx アニメ対象ノードが存在し、それぞれのアニメ起終点は、各々同一でも良いし、異なっても良い。
  3. 1 つのアニメ対象ノードに対して連続して slideToggleEx メソッドを起動することもあり、その場合にはアニメ起終点がその都度変わることもあり得る。
  4. 独自仕様の多い IE においても起動出来るアニメーションとする。
slideToggleEx コードの効率化
  1. slideToggleEx メソッドを同一ノードに適用する場合には、コード進行過程を出来る限り繰り返さないようにする。そのために jQuery.data クラスメソッドを活用してアニメ対象ノードに情報を関連づける。
  2. 一度計算した値や使用した関数は、結果を出来る限り再利用する。そのために、jQery プロトタイプオブジェクトのプロパティを活用する。

▲ToTop

コード作成上の苦労話・強調したい点

1. メソッド終了後にも情報を保持させる措置

まず苦労したのは、slideToggleEx プラグインの再起動の際に、従前値を保持させる措置でした。

無名関数のトップレベルで、slideToggleEx メソッド外に変数を用意すれば解決することなのですが、是が非でもプラグインのメソッド外に変数を置かないでまとめたかったので、拘ってコードを作りました。その結果、メソッド外となる点では変わらないのですが、『Javascript 第 5 版』p.144 や cycle プラグインを参考にして slideToggleEx メソッドのプロパティに値を保持させることにしました。

メソッド実行が終わるとその中で定義された変数は消失してしまうので、当該メソッドの起動回数を記憶させる変数は当該メソッド外になければなりません。しかし外に変数を置くのは美しくありません。

そこでメソッドのプロパティを記憶装置にすることにしました。コードの 107 行から 122 行がその記憶装置です。

2. アニメーション開始前の対象ノードの CSS 値の処理

要素が隠蔽されていても表示されていても、要素固有の margin、border、padding、width、height 値などは保持されています。隠蔽されていてもそのスタイル諸値がゼロになるわけではありません。そのことをまず把握し理解する必要があります。( 実は私はこのことへの理解が不十分でした。 )

次に、slideToggleEx メソッドの適用対象が複数存在しているエントリイにおいて、アニメ起動が異なる要素に対してアト・ランダムに行われる場合、それぞれの要素に対して適切なアニメを起こさせることがかなり難しいことでした。

以前、slideToggleEx メソッド対象となった要素は偶々隠蔽されているかもしれないし、表示されているかもしれません。今 slideToggleEx アニメを引き起こした要素も表示されているかもしれないし、隠れているかもしれません。このようなアト・ランダムな状態においても、それぞれの要素毎に適切な slideToggleEx アニメーションが引き起こされるようにしなければなりません。

以上を踏まえた処理として cssBefore オブジェクトを用意しました(32 ~ 33 行)。このオブジェクトによりアニメーション起動前の要素のプロパティを事前に設定するわけです。これにより隠蔽状態から表示へとアニメートする要素が、適切な位置で表示されるようになりました。

3. margin 値の扱い

margin 値は一般に 「 margin:1em auto 」 などのように簡略的に指定されている場合が多いでしょう。

ところがこのような指定の場合には、jquery.js の css メソッド等を活用しても margin-left 等の個々の margin 値を取得することが出来ません。特に auto 指定がされている場合、margin をアニメプロパティに指定すると全く予想外のアニメーションが引き起こされてしまいます。jquery.js は auto 指定された margin 値を数値的に捉えることが出来ないので、margin-left、margin-right 等のプロパティをゼロと判読してしまうためです。

こうして、margin 値が様々な様式で指定されている場合でも、それを適切な数値として把握する必要が生じます。

そのための処理は makecssBefore 関数で行わせました。

▲ToTop

4. アニメ開始前における対象要素の CSS 値設定

animate メソッドは、アニメ開始前の要素の様々な属性値(スタイル属性値を含む。width、margin etc.)をアニメ開始値として採用します。(もし或るプロパティの属性値がない場合には開始値を 0 とします。)このため表示されている要素にアニメーションを適用する場合には、一般的にはアニメ前 CSS 値を設定する必要はありません。

例えばアニメ対象要素に width="380px" が設定されていれば、ブラウザがその値を解釈して、380px 幅の要素を表示しているわけです。この場合 width プロパティのアニメ開始値が何も指定されていなければ、 animate メソッドは要素固有値である 380px を width プロパティのアニメ開始値とします。

ところが隠蔽されている要素にアニメーションを適用する場合には様相が異なってきます。

隠蔽されている要素を表示するアニメーションの場合でも、その隠蔽が一般的にそうであるように display = "none" 指定によって行われている場合には、対象要素の固有の属性値は存在し続けます。要素が単に非表示になっているだけで、display = "none" 以外の属性値は表示されていた時の値が保持され続けます。

この状態に対して隠蔽要素を表示させる animate メソッドを適用すると、animate メソッドの仕様上、要素固有値がアニメ開始値になるのですから、アニメプロパティが要素固有値となってしまいます。この結果、0 からスタートさせたかったアニメーションは引き起こされません。

従って、隠蔽要素を表示させるアニメーションの場合、アニメ開始値を animate メソッド起動前に強制的に指定する必要があります。こうして強制的に CSS 値を指定するための cssBefore オブジェクトが必要・不可欠となります。

さて、その cssBefore はアニメーションの初期値を定めるわけですから、アニメーションの態様に応じて( つまり起終点位置に応じて )変化します。ボックス要素の左上、上辺、左辺などを起終点とするアニメーションの場合には問題になりませんが、例えば、アニメが要素の中心位置から始まる場合には、top と left の開始値をボックス内容辺、ボーダー辺あるいは margin 辺までの、幅や高さの半分の値に設定する必要があります。また、開始点がボックスの右上の場合には、left の開始値をボックス内容辺、ボーダー辺あるいは margin 辺までの幅に設定する必要があります。

以上に述べた隠蔽状態における cssBefore オブジェクトの作成を makecssBefore 関数の後半で行っています。

なお、アニメプロパティ値を toggle や show に指定すると、animate メソッドは要素の固有属性値をアニメの終了値として利用します。この利便性故に、アニメプロパティ値に toggle を多用することになります。

▲ToTop

5. $.data メソッドの活用

slideToggleEx プラグインでは以下の 4 箇所で $.data メソッドを利用しました。

  1. 18 行 …… $.extend(o, $.data(o.el,"initdone") || getSize());
  2. 32~33 行 ……o.cssAdjust = $.data(o.el,"Adjust"+o.type) ||    $.data(o.el,"Adjust" + o.type, $.data(o.el,"Adjust" + o.type, adjustcssAnim(false));
  3. 34 行 …… o.cssAnim = $.data(o.el,"Anim" + o.type, $.data(o.el,"Anim" + o.type, setcssAnim());
  4. 35 行 …… o.cssBefore = $.data(o.el,"Before" + o.type) || $.data(o.el,"Before" + o.type, makecssBefore());

このメソッド利用した目的は、ずばりコード進行の効率化/高速化です。このメソッドを使うことにより、不必要に同じ計算を繰り返させたくなかったのです。

上の 4 例は皆同様な形式を取っているので、どれか 1 つ分かれば他は推して知るべしです。なので、ここでは一番最後の 35 行で使った $.data メソッドについて触れておきます。

$.data(o.el,"Before" + o.type) は、アニメ対象要素 o.el に "BeforeN" ( N は type 値 )と言う名称で関連づけられたオブジェクトがある場合には、そのオブジェクト( cssBefore オブジェクト)を返し、それが左辺の o.cssBefore に代入されます。一方、関連づけられた情報がなければ false を返すので、次にある || 演算子が働きます。

|| 演算子が作用すると、オペランドである makecssBefore 関数が実行されます。

この関数では最後の行に $.data メソッドがあるので、関数によって算出したオブジェクトを、このメソッドによりアニメ対象要素 o.el に "BeforeN" 名で関連づけます。関連づけられたオブジェクトは return により左辺の o.cssBefore オブジェクトに返されます。

こうして、同一要素に対する同一 type の slideToggleEx メソッド起動時には───実際、表示/隠蔽はセットで履行されるはずだから、多くの場合 slideToggleEx メソッドは、同一要素に対して同一 type 値で繰り返し実行されます。───最初の呼び出し時にだけ makecssBefore 関数が実行され、二度目以降は固有の名称で要素に関連づけられているオブジェクト値が利用されて、 makecssBefore 関数は実行されません。これがコード進行の「最初の」「効率化」であり「高速化」です。

$.data メソッドを利用したことによる効率化は、これだけではありません。或る要素Aが slideToggleEx によって表示/隠蔽され、次に別の要素Bがやはり slideToggleEx で表示/隠蔽されたと仮定します。この後に再び要素Aに slideToggleEx メソッドを適用した場合に、更なる効率化・高速化が発揮されます。

既に $.data メソッドによるA、B各々の要素へのオブジェクトの関連づけは済んでいますから、要素Bに続けてAに slideToggleEx アニメを適用した時には、既にAに関連づけられているオブジェクトが利用され、makecssBefore 関数は起動されません。こうして無駄なコード進行が排除されます。

以上のことが 4 つの関数に対して行われるので、現在操作対象となっている頁がリロードされない限り、slideToggleEx アニメーションが一度適用された要素においては、 slideToggleEx 内にある様々な関数を殆ど実行する必要がなくなります。こうしてコード進行が大いに高速化されます。高速化は言い換えれば省エネでもあるので、流行の言葉を使えば「エコ」なコード進行が行われると言っても良いかもしれません。

▲ToTop

クリックイベント登録と slideToggleEx メソッド適用対象の簡便な登録コード

さて、slideToggleEx を使うシーンは、ボタンをクリックして或るコンテンツを表示したり隠蔽する場合が多いと思われます。そうだとすれば、その一連の操作( クリックイベントの登録・或るコンテンツの指定・slideToggleEx メソッドの要素への適用 )をコード化して半自動化してしまえば、大変楽になります。

次に述べるコードは、この一連の操作、つまりボタンへのクリックイベント登録と、クリック時の slideToggleEx メソッド適用対象の特定とそのメソッドの起動をまとめて行うものです。

具体的には 「 或るボタンがクリックされたら、slideToggleEx アニメーションが適用される或る要素が特定され、slideToggleEx メソッドが起動する 」 そのような Javascript コードです。

そのコードでは、同一ページ内に存在する、複数の同一又は異なる type の slideToggleEx アニメーションの、トリガーボタンとアニメ対象を一括して登録します。slideToggleEx の引数であるアニメ継続時間、easing 関数及びアニメ終了後の実行関数は、コードの冒頭で登録し、これを slideToggleEx メソッドに渡すようにしています。

そもそもコード作成当初では、slideToggleEx メソッドの引数を何とか HTML 文内で行いたいと考えました。クラス指定によって、アニメの起終点 ( type ) を指定するだけではなくその他の引数もまとめて指定したいと思いました。

しかし、クラス名文字列の中に、duration、easing などの文字をその都度組み込むと、冗長なクラス名指定となってしまい、いかにも面倒ですし、値だけを羅列するようにしても煩雑で順番を含めてわかりにくくなります。それにボタンクリックによる要素の表示/隠蔽操作において、その都度 duration や easing の値を変化させる必要性が高いとは思えません。

以上から、クラス名の中に slideToggleEx メソッドの引数を盛り込むことはやめました。

なお、ここにおける duration 等の指定は、ここで対象としている「 ボタンクリックによって起動される slideToggleEx 起動方法 」 において、slideToggleEx メソッドの引数を指定するものです。他方、slideToggleEx プラグインコードにおける $.fn.slideToggleEx.opts オブジェクト内でも duration 等を指定していますが、これは slideToggleEx メソッドの一般的な既定値を設定するものです。

下のボタンをクリックすると隠蔽状態にあるそのコードが、右下から展開され、再びボタンをクリックすると右下に隠れます。(type 3)

▼このコードは、メソッドプラグインではありませんが、プラグインとセットで include
 ファイルに登録しておくと重宝します。これがないと fireSlideToggleEx-n のクラス指定を
 しても next sibling 要素などを slideToggleEx の適用対象とすることは出来ません。

// button 要素へのクリックイベント登録及び slideToggleEx メソッド適用対象のピックアップ
// クリックイベント起動ボタンと slideToggleEx 適用対象を登録する。
124:// regist click Event to fire slideToggleEx() for nextBlock
125:$(function(){ // 当然 DOM が Ready されてから起動する必要があります。
   // 初期値設定
126: var duration = 1000, easing = null, complete = function(){}, clk = {};
127: function func(j){
128:  $.each(clk[j],function(i){
     // target オブジェクトは slideToggleEx 対象の個々のノード 1 つだけを格納する。
129:   var $target={};
     // $target オブジェクトの j プロパティに slideToggleEx 対象ノードを格納する。
     // fireSlideToggleEx-x クラス指定されたボタン要素がブロック要素の場合には
     // next siblingを block 以外の inline 要素の場合には親要素の
     // next sibling 要素を slideToggleEx 対象とする。
130:   $target[i] = $(this).css("display")=="block" ? $(this).next() : $(this).parent().next();
131:   if ($target[i].length) {
132:    $(this).click(function(){
       // slideToggleEx を起動する。
133:     $target[i].slideToggleEx(j, duration, easing, fn);
134:     $(this).blur(); // ボタン要素のフォーカスを外す
135:    });
136:   }
137:  });
138: }
139: for (var j=0; j<9; j++){ // 0 から 8 迄の巡回処理
    // クリックイベントを登録するオブジェクト clk の i プロパティを作成し、
    // それに "fireSlideToggleEx-i"( i は 0~8 ) class 名のノードをもつ
     // jQuery インスタンスを登録する。これにより、ページ内にある
     // fireSlideToggleEx-n クラス名を持つノードを捉える。
140:  clk[j] = $(".fireSlideToggleEx-" + j);
141:    if (!clk[j].length) continue;
    // i 番名のノードがなければ loop の先頭行に戻って i++ する。
142:  func(j);
143:  clk[j].hover( // マウスオーバー/アウト時のボタン背景色の変更
144:   function(){$(this).css({backgroundColor:"pink"})},
145:   function(){$(this).css({backgroundColor:""})}
146:  )
    // マウスが押し下げられた場合の色替え
147:  .mousedown(function(){$(this).css('background-color','palegreen')})
148: }
149:});
150:});

▲ToTop

jquery.js のアニメーションコードの活用 ( 3 ) slideToggleEx プラグインの簡便な活用のために(不完全版)

このエントリイで言及し掲載した slideToggleEx プラグインは不完全でした。完全版はエントリイ No. 733 をご覧ください。

slideToggleEx プラグインの簡便な活用方法が必要だ

jquery.js の自作プラグイン slideToggleEx を実際に各エントリイ内で随時利用するためには、簡便な利用方法を作っておく必要があります。こうした自らの需要に基づいて以下の方法をまとめました。

用意するもの・ことは以下の 5 つです。
  1. include 用 slideToggleEx プラグインファイルと include タグ要素
  2. slideToggleEx が適用されるタグ要素
  3. 起終点を右側にする場合には、対象タグ要素に width スタイル属性を設定します。また起終点を下にする場合には height スタイル属性を設定します。そうしないと animate メソッドの中で適正な幅や高さが計測されない場合があるためです。
  4. slideToggleEx を呼び出し、適用させるタグ要素(一般的には button タグでしょう)
  5. その button タグのクラス属性に fireSlideToggleEx-n を追加します。( n は 0 から 8 の整数)

n は起終点を意味し次の通りの意味を持ちます。

0:ボックスの中心、1:左上端、2:右上端、3:右下端、4:左下端、
5:上辺、6:右辺、7:下辺、8:左辺

これだけ設定すれば、トリガーである button をクリックすると、n で指定した位置を起終点として、トリガーボタンの直後に配置された要素が slideToggleEx 適用対象要素となり、隠蔽されたり表示されるようなコード( エントリイ末尾に掲載 )を書きました。

「 これだけ 」と言っても結構たくさんあるため、最初は面倒に感じますが、直ぐに慣れますし、実際に使ってみると決して大変な手間ではありません。

必要な HTML 構造

上の 5 つだけで、slideToggleEx プラグインが簡単に使えるようにするために、もう1つ該当部分の HTML 文を特別な構造にする必要があります。これが 6 つ目にして最後の要件となります。その要件とは起動ボタンの直ぐ後に slideToggleEx 対象要素があるということです。

そうでない場合には 個々に slideToggleEx メソッドを呼び出すコードを書くことになります。

該当部分の HTML を以下のいずれかにする必要があります。(いずれのケースもボックスの左上を起終点とする場合で例示します。)

■ケース 1
<div><button class="fireSlideToggleEx-1">起動ボタン</button></div>
<div>・・・slideToggleEx対象要素・・・</div>

■ケース 2
<button class="fireSlideToggleEx-1" style="display:block;width:120px;">起動ボタン</button>
<div>・・・slideToggleEx対象要素・・・</div>

※ 2 行目の div 要素は別に div でなくてもブロック要素ならば何でも構いません。

ケース 1 は一般的な button タグの使い方の場合です。つまり button タグを inline 要素として配置する場合の HTML 文です。この場合には button タグの親要素の next sibling ブロック要素が slideToggleEx メソッドの適用対象となるようにコードを作りました。

一方ケース 2 は button タグをブロック要素として配置する場合です。この場合には当該のボタンタグの next sibling ブロック要素が slideToggleEx メソッドの適用対象となるようにコードを作りました。

このような当然と思われるHTML文構成にしておけば、運用に当たってどのようにしたかを思い出すのも容易ですし、仮に構造を忘れても直感的に思い出すことが可能となります。

▲ToTop

以上の前提で作った slideToggleEx プラグイン活用コード

プラグインは position 無指定版と指定版の 2 種類を作りました。どちらも全く同様の動きをしますが、後者の方がコード全体のバランスがよいかもしれません。

当初 position 指定版を作成し、アニメ適用対象に position:relative をその都度指定していました。top と left をアニメ対象とするので、アニメ対象要素には static 以外の position 指定が為されていなければならないためです。しかし、slideToggleEx プラグインの運用を重ねるに連れ、その都度指定することが非常に面倒であることに気がつきました。

そこで改めて position 無指定版を作成したのですが、それが完成してから、ふと position 指定をコードで行えばよいことに気がつきました。そして、仮にアニメ対象に position 指定がされていない場合でも、相対指定となるよう、Javascript コードによって強制的に指定することにしました。

結果として、回り道をしてしまったのですが、このようなケアレスミスを犯すこと自体が、まだまだコード開発に不慣れである証明かもしれません。

こうして、今実際に使っている slideToggleEx プラグインメソッドは最初に作った版を改訂した position 指定版となりました。

なお、アニメ継続時間と easing 関数は、初期値を設定する slideToggleEx.opts オブジェクトにおいて、1 秒に設定し、easing は無指定としています。opts オブジェクトを変えれば、自在に継続時間と easing 関数を変更できますが、使う際にその都度 duration と easing 関数を変更する必要はないと考え、(1) 指定するボタンタグと (2) 指定されるブロックタグ及び (3) slideToggleEx-n クラス名を指定するだけで使えればよいと判断しました。

以下のコードリストの表示/隠蔽には当然ながら、slideToggleEx を適用しています。

▲ToTop

▼ver not use "position:relative/absolute" 
/* jQuery plugin : slideToggleEx() Release at 2009.8.21, ver2 at 2009.9.8*/
(function($){
var ver="absolute animation, non position assignment";
$.fn.slideToggleEx = function( type, duration, easing, fn ){
 $target=this;
 var o = $.extend({},$.fn.slideToggleEx.opts);
 // 初めてそのインスタンスから呼ばれたか、インスタンスが変わった場合のみ
 if(!o.target || o.target[0]!==$target[0]) {
  $.extend(o,{
   orig : o, // 当面使わないが念のために $.fn.slideToggleEx.opts をバックアップ
   target : $target, // jQuery インスタンスへの参照を登録する。
   // ボックスを show する時に初期のマージン値を使用するので記録しておく。
   initMTL:{marginTop:$target.css("margin-top"),marginLeft:$target.css("margin-left")},
   /* ボックスのマージン辺までの外枠サイズをゲットする。
    * 但し、jquery.js の 圧縮ファイルでは outerWidth(true) のように引数を指定しても
    * margin 辺までの外寸を取得しない。そこでやむなく引数を付けずに border 辺までの値を
    * 取得してから、それに margin 値を加算して margin 辺までの外寸を取得する。*/
   oH : $target.outerHeight() + parseInt($target.css("margin-top"))
    + parseInt($target.css("margin-bottom")),
   oW : $target.outerWidth() + parseInt($target.css("margin-left"))
    + parseInt($target.css("margin-right")), 
   // プラグイン起動回数が奇数か偶数かを記録するプロパティ
   odd : false
  });
  
  /* メモリ上の opts オブジェクトにこの上で作成したばかりの o オブジェクトを統合
   * して、初期値として保存する。同じインスタンスに対する 2 度目以降のプラグイン
   * 起動時には o オブジェクトに opts オブジェクトが転写され、かつこの if 条件は
   * falseとなるので if ブロックは履行されない。こうして効率的なコード進行となる。
   * また、2 度目以降のプラグイン起動が異なるインスタンスに対して行われた場合には
   * 2 番目の if 条件 o.target[0]!==$target[0] が true となるので、if ブロックが履行される。
   * こうして o オブジェクトも $.fn.slideToggleEx.opts オブジェクトも新しい
   * インスタンスの諸属性値を格納する。*/
  $.fn.slideToggleEx.opts = $.extend($.fn.slideToggleEx.opts,o);
 }
 
 /* 奇数回目か偶数回目か、起動回数を記録する。ここでは $.fn.slideToggleEx.opts
  * オブジェクトを利用して slideToggleEx メソッドの実行が終わった後も
  * odd 値を残しておくことがポイントである。*/
 o.odd = $.fn.slideToggleEx.opts.odd = !$.fn.slideToggleEx.opts.odd;
 o.hidden = $target.is(":hidden"); // アニメ対象が隠れているかどうか
 o.duration = duration || o.duration; // 第 2 引数か opts プロパティの利用
 // 第 3 引数か opts プロパティの利用。duration がオブジェクトの時には空。
 o.easing = typeof duration !=="object" ? (easing || o.easing) : "";
 // 第 4 引数か opts プロパティの利用。duration がオブジェクトの時には空。
 o.complete = typeof duration !=="object" ? ($.isFunction(fn) && fn || o.complete) : "";

 // アニメ用の CSS オブジェクトを作成する関数
 function makeAnimCSS(type,showhide){
  type = new Number(type);
  if (showhide === false) { // 隠蔽されている時のアニメ用 CSS 作成
   return type==0 ? {marginTop:o.oH/2+"px", marginLeft:o.oW/2+"px"}:// center
   type==1 ? {marginTop:0,marginLeft:0}: // 左上端
   type==2 ? {marginTop:0,marginLeft:o.oW + "px"}: // 右上端
   type==3 ? {marginTop:o.oH+"px", marginLeft:o.oW+"px"}:// 右下端
   type==4 ? {marginTop:o.oH+"px", marginLeft:0}: // 左下端
   type==5 ? {marginTop:0}: // 上辺
   type==6 ? {marginLeft:o.oW+"px"}:// 右辺
   type==7 ? {marginTop: o.oH+"px"}:// 下辺
   {marginLeft:0}; // 左辺
  // 表示されている時のアニメ用 CSS 作成
  // type 値が 5 と 7 の時にはトップ値のみ、6 と 8 の時にはレフト値のみ
  // 0 ~ 4 の時にはトップ値とレフト値を使用する。
  } else return (type==5 || type==7) ? {marginTop:o.initMTL.marginTop}
   : (type==6 || type==8) ? {marginLeft:o.initMTL.marginLeft} : o.initMTL;
 }

 if (type > 8) // エラー処理
  (function(){
   alert("第 1 引数は 0~8 だけが指定できます。\nやり直してください。\n"+
   "slideToggleEx( 0~8, 継続時間, easing関数, アニメ後実行関数)"); return;
  })();
 else { // type 値が 0 ~ 8 の時
  // アニメ用 CSS の最終作成( margin 以外のプロパティ値からなるオブジェクトに、
  // makeAnimCSS 関数で作ったオブジェクトを併合する)
  var obj =$.extend( {}, // margin 以外のプロパティ群
    ( type==5 || type==7 ) ? {} : o.animWidth,
    ( type==6 || type==8 ) ? {} : o.animHeight,
    {opacity:"toggle"} );
  /* 偶数回目の起動か、アニメ対象が隠蔽時の起動の時には、makeAnimCSS の
   * 第 2 引数を true に、奇数回目の起動か、アニメ対象が表示されている時には、
   * makeAnimCSS の第 2 引数を false にする。
   * これによりアニメ用 CSS 値を二通りの場合に対応させて作成する。*/
  $.extend(obj,(!o.odd || o.hidden) 
   ? makeAnimCSS(type,true) : makeAnimCSS(type,false));
  return this.animate(obj, o.duration, o.easing,o.complete); // アニメーション起動
 }
} // End of $.fn.slideToggleEx function

/* デフォルト値を slideToggleEx メソッドの opts プロパティに設定しておく。
 * このオブジェクトは slideToggleEx メソッドの実行が終わった後の情報記録装置
 * としても機能する。*/
$.fn.slideToggleEx.opts = {
 duration:1000, easing:null, complete:function(){},
 animHeight : { // 高さに係るアニメ CSS の既定値
  height:"toggle",marginTop:"toggle",marginBottom:"toggle",paddigTop:"toggle",
  paddingBottom:"toggle",borderTopWidth:"toggle",borderBottomWidth:"toggle"
 }, animWidth : { // 幅に係るアニメ CSS の既定値
  width:"toggle",marginLeft:"toggle",marginRight:"toggle",paddigLeft:"toggle",
  paddingRight:"toggle",borderLeftWidth:"toggle",borderRightWidth:"toggle"
 }
};
})(jQuery);

▲ToTop

▼ver use "position:relative"
// jQuery plugin : slideToggleEx() Release at 2009.8.21, verup 2009.9.9
(function($){
var ver="not absolute but relative animation on position relative";
// top と left プロパティによって起終点を操る position relative バージョン
$.fn.slideToggleEx = function( type, duration, easing, fn ){
 var $target = this;
 var o = $.extend({},$.fn.slideToggleEx.opts);
 // 初めてそのインスタンスから呼ばれたか、インスタンスが変わった場合のみ
 if(!o.target || o.target[0]!==$target[0]) {
  $.extend(o,{
   orig : o,
   target : $target,
   oH : $target.outerHeight() + parseInt($target.css("margin-top"))
    + parseInt($target.css("margin-bottom")),
   oW : $target.outerWidth() + parseInt($target.css("margin-left"))
    + parseInt($target.css("margin-right")),
   odd : false
  });
  $.extend($.fn.slideToggleEx.opts,o);
  if ($target.css("position") ==="static")
   $target.css({position:"relative",top:"0",left:"0"});
 }
 o.odd = $.fn.slideToggleEx.opts.odd = !$.fn.slideToggleEx.opts.odd;
 o.hidden = $target.is(":hidden");
 o.duration = duration || o.duration;
 o.easing = typeof duration !=="object" ? (easing || o.easing) : "";
 o.complete = typeof duration !=="object" ? ($.isFunction(fn) && fn || o.complete) : "";

 function makeAnimCSS(type,showhide){ // アニメCSSプロパティを作成する関数
  var type = new Number(type);
  var plmn = showhide ? "-=" : "+="; // showhide == true → "-="
  return type==0 ? {top: plmn + o.oH/2 +"px", left: plmn + o.oW/2 +"px"} : // center
  type==1 ? {top: plmn + 0, left: plmn + 0} : // 左上端
  type==2 ? {top: plmn + 0, left: plmn + o.oW +"px"} : // 右上端
  type==3 ? {top: plmn + o.oH +"px", left: plmn + o.oW +"px"} : // 右下端
  type==4 ? {top: plmn + o.oH +"px", left: plmn + 0} : // 左下端
  type==5 ? {top: plmn + 0, left: plmn + 0} : // 上辺
  type==6 ? {top: plmn + 0, left: plmn + o.oW +"px"} :// 右辺
  type==7 ? {top: plmn + o.oH +"px", left: plmn + 0} :// 下辺
  type==8 ? {top: plmn + 0, left:plmn + 0} : {top:plmn + 0, left:plmn + 0} ;// 左辺
 }

 if (new Number(type) > 8) // エラー対処
  (function(){
   alert("第 1 引数は 0~8 だけが指定できます。\nやり直してください。\n"+
   "slideToggleEx( 0~8, 継続時間, easing関数 )"); return;
  })();
 else {
  var obj = (!o.odd || o.hidden) 
   ? makeAnimCSS(type,true)  // 偶数回目の起動か、または要素が隠蔽されている時の CSS 作成
   : makeAnimCSS(type,false); // 奇数回目の起動か、要素が表示されている時の CSS 作成
  $.extend( obj, // top と left だけのCSSオブジェクトに幅と高さ関係値も追加
    ( type==5 || type==7 ) ? {} : o.animWidth, // 上辺か下辺の時には幅方向CSSは対象外
    ( type==6 || type==8 ) ? {} : o.animHeight, // 右辺か左辺の時には高さ方向CSSは対象外
    {opacity:"toggle"} ); // 透明度もアニメ対象プロパティとする
  return this.animate(obj, o.duration, o.easing, o.complete); // アニメ起動
 }
}; // End of $.fn.slideToggleEx

// 初期値設定とプロパティ記憶装置としての機能のために
// ※ これは position 無指定版と指定版に共通する同一のものです。
$.fn.slideToggleEx.opts = {
 duration:1000, easing:null, complete:function(){},
 animHeight : {
  height:"toggle",marginTop:"toggle",marginBottom:"toggle",paddigTop:"toggle",
  paddingBottom:"toggle",borderTopWidth:"toggle",borderBottomWidth:"toggle"
 }, animWidth : {
  width:"toggle",marginLeft:"toggle",marginRight:"toggle",paddigLeft:"toggle",
  paddingRight:"toggle",borderLeftWidth:"toggle",borderRightWidth:"toggle"
 }
};
})(jQuery);

▲ToTop

▼このコードは、メソッドプラグインではありませんが、プラグインとセットで include
 ファイルに登録しておくと重宝します。これがないと fireSlideToggleEx-n のクラス指定を
 しても next sibling 要素などを slideToggleEx の適用対象とすることは出来ません。
/* regist click Event to fire slideToggleEx() for nextBlock
 * クリックイベント起動ボタンと slideToggleEx 適用対象を登録する。
 * これも position 無指定版、指定版に共通するコードです。*/
 1:$(function(){ // 当然 DOM が Ready されてから起動する必要があります。
 2: var duration = 1000, easing = null,fn = function(){}, clk = {};
 3: function func(i){
 4:  $.each(clk[i],function(){
    // target オブジェクトは slideToggleEx 対象の個々のノード 1 つだけを格納する。
 5;   var $target={};
     // $target オブジェクトの j プロパティに slideToggleEx 対象ノードを格納する。
     // fireSlideToggleEx-x クラス指定されたボタン要素がブロック要素の場合には
     // next siblingを block 以外の inline 要素の場合には親要素の
     // next sibling 要素を slideToggleEx 対象とする。
 6:   $target[i] = $(this).css("display")=="block" ? $(this).next() : $(this).parent().next();
 7:   if ($target[i].length) {
 8:    $(this).click(function(){
       // IE においては右側をアニメ起終点にすると固まる事が多いので、
       // 強制的に同種の左側起終点に指定を替える。
 9:     if (jQuery.browser.msie)
10:      var k = i==2 && 1 || i==3 && 4 || i==6 && 8 || i;
11:     else k=i;
       // slideToggleEx を起動する。
12:     $target[i].slideToggleEx(k, duration, easing, fn);
13:     $(this).blur(); // ボタン要素のフォーカスを外す
14:    });
15:   }
16:  });
17: }
18: for (var i=0; i<9; i++){ // 0 から 8 迄の巡回処理
    // クリックイベントを登録するオブジェクト clk の i プロパティを作成し、
    // それに "fireSlideToggleEx-i"( i は 0~8 ) class 名のノードをもつ
    // jQuery インスタンスを登録する。これにより、ページ内にある
    // fireSlideToggleEx-n クラス名を持つノードを捉える。
19:  clk[i] = $(".fireSlideToggleEx-" + i);
    // i 番名のノードがなければ loop の先頭行に戻って i++ する。
20:   if (!clk[i].length) continue;
    // i 番名のノードが存在した場合には、当該インスタンスの
    // 各ノード毎に巡回処理を行うために func 関数を呼び出す。
21:  func(i);
22:  clk[i].hover( // マウスオーバー/アウト時のボタン背景色の変更
23:   function(){$(this).css({backgroundColor:"pink"})},
24:   function(){$(this).css({backgroundColor:""})}
25:  )
26:  // マウスが押し下げられた場合の色替え
27:  .mousedown(function(){$(this).css('background-color','palegreen')})
28: }
29:});

▲ToTop

コード作成上の苦労話

非常に苦労したのは、slideToggleEx プラグインの再起動の際に、従前値を保持させる措置でした。

無名関数のトップレベルで、slideToggleEx メソッド外に変数を用意すれば解決することなのですが、是が非でもプラグインのメソッド外に変数を置かないでまとめたかったので、拘ってコードを作りました。

その結果、メソッド外となる点では変わらないのですが、『Javascript 第 5 版』p.144 や cycle プラグインを参考にして slideToggleEx メソッドのプロパティに値を保持させることにしました。

この点は極めて重要なポイントなので、じっくりと記述することにします。

メソッド実行が終わるとその中で定義された変数は消失してしまうので、当該メソッドの起動回数を記憶させる変数は当該メソッド外になければなりません。しかし外に変数を置くのは美しくありません。

そこでメソッドのプロパティを記憶装置にすることにしました。

そのことについて具体的にコードに触れながら、以下で説明したいと思います。

▲ToTop

メソッドのプロパティに情報を記録させる方法
 1: var $target=this;
 2: var o = $.extend({},$.fn.slideToggleEx.opts);
 3: // 初めてそのインスタンスから呼ばれたか、インスタンスが変わった場合のみ
 4: if(!o.target || o.target[0]!==$target[0]) {
 5:  $.extend(o,{
 6:   orig : o,
 7:   target : $target,
 8:   oH : $target.outerHeight() + parseInt($target.css("margin-top"))
 9:    + parseInt($target.css("margin-bottom")),
10:   oW : $target.outerWidth() + parseInt($target.css("margin-left"))
11:    + parseInt($target.css("margin-right")),
12:   odd : false
13:  });
14:  $.fn.slideToggleEx.opts = $.extend($.fn.slideToggleEx.opts,o);
15:  if ($target.css("position") ==="static")
16:   $target.css({position:"relative",top:"0",left:"0"});
17: }
18: o.odd = $.fn.slideToggleEx.opts.odd = !$.fn.slideToggleEx.opts.odd;

上の 18 行のコードは slideToggleEx プラグイン( position 指定版 )の最初の部分から抽出したものです。

  1. まずインスタンスへの参照を $target 変数に登録します(#1)。

    これはインスタンスへの参照が必要な場合にその都度 this を使うと、シーンによっては参照先が変わってしまうことがあるので、何らかの変数が必要だからです。別に self でも良いのですが、cycle プラグインに倣って jQuery インスタンスへの参照であることが視覚的にわかるように $ を使い、また target とすることによりアニメ対象であることを意味的にも表示するようにしました。

  2. 次にメソッドの opts プロパティを o オブジェクトに転写します(#2)。

    この行は極めて重要な役割を果たします。これで o オブジェクトに初期値が設定されると共に、同じインスタンスからの 2 度目以降の slideToggleEx の呼び出しの際には、初回呼び出し時に設定された諸値が、初回呼び出し時の 14 行の実行によって、opts プロパティに登録されているので、情報満載のその opts が o に複写されます。

    この結果、4 行目の if 文において、o.target プロパティは存在するし、o.target[0] === $target[0] となるので、4 ~ 17 行がパスされます。こうして同じインスタンスに対して、諸値設定を繰り返させない効率化を実現しました。

  3. 他方、slideToggleEx の最初の呼び出しの際、またはインスタンスが変わった時の呼び出し時には、o.target プロパティが存在しないか、o.target[0] と $.target[0] は異なるため、4 行目の if 文が成立し、5 ~16 行が実行されます。

    まず、opts オブジェクトの初期値をバックアップするために、それが転写された o オブジェクトを orig プロパティに保存します(#6)。このバックアップは当面使いませんが、何らかの必要が後で生じるかもしれないとの老婆心です。

    次に、target プロパティに $target への参照を登録します(#7)。これにより上で述べたような 二度目以降の呼び出し時の if 文不成立を成り立たせます。

  4. o.oH と o.oW には インスタンスに登録されている最初の要素の外寸を登録します。これも 1 回だけ測ればよいので if 文の中に入れました(#8 ~ 11)。
  5. o オブジェクトの最後のプロパティは起動回数を記録するためのプロパティです。これは 18 行との関係で false とする必要があります(#12)。
  6. 以上により初回呼び出し時にだけ、インスタンスに係る諸情報を設定・獲得し、その値を 2 行目で得た o オブジェクトに上書きし(#5)、その直後にその拡張された o オブジェクトを opts オブジェクトに併合します(#18)。こうすることにより、o オブジェクトが持つ情報が opts オブジェクトに複写・保存され、slideToggleEx が実行を終えても、当該インスタンスに係る情報を slideToggleEx.opts プロパティが保持し続けます。
  7. 15 ~ 16 行では 当該インスタンスに登録された要素の position 指定をチェックし、relative になっていなければそのように設定します。

    こうすることにより slideToggleEx 対象要素を作成する時には一々 position 指定をしなくても良いようにしました。なおこの処置も当然 1 回だけ行えばよいので、if 文の中で行います。

  8. 最後の 18 行は、起動回数を記録するためのコードです。

    12 行で false とされた o.odd 値は 14 行によって opts.odd = false と複写されます。そして 18 行でこれを否定することにより opt.odd は trueとなり、これが o.odd にも代入されます。

    こうして初回起動時の o.odd 値は true となり奇数回目であることが登録されます。二度目の呼び出し時には 2 行からいきなり 18 行に飛ぶので 18行の o.odd は false、つまり偶数回目となります。

    ここにおいて、常に opts.odd に偶数/奇数回数を示す値が保持され続け、そこから o.odd 値が複写され、呼び出しの都度変更されることがポイントです。

  9. 最後に、slideToggleEx を呼び出すインスタンスが変わった場合のコード進行を説明します。

    この時には if 文の 2 番目の条件が効果を発揮します。起動元インスタンスが変われば、2 行目によってゲットされる o.target はそれ以前の別のインスタンスへの参照を保持しているため、必ず o.target[0] は $.target[0] と異なります。こうして if 文が成立し、改めて起動元インスタンスの要素サイズ( o.oH と o.oW )が計測されます。

    そして、その結果は 14 行で opts オブジェクトに複写され、当該インスタンスからの二度目以降の呼び出し時には改めて o オブジェクトに複写されます。

    こうして以前のインスタンスに登録されていた要素のサイズ計測結果は、上書きされて消失し、新しい現在の呼び出し元インスタンスに登録されている要素のサイズが opts オブジェクトに保持され、o オブジェクトに登録されます。

▲ToTop

クリックイベント登録と slideToggleEx 対象要素を特定するためのコードについて

このコードはプラグインメソッドではありませんが、容易に slideToggleEx プラグインを利用するために不可欠なものです。そして作成にかなり苦労を重ねた難産コードでした。そこで一節を設けて詳細に説明しておきたいと思います。

■ クリックイベントと slideToggleEx 適用対象を登録するためのコード ( 再掲です )
/* regist click Event to fire slideToggleEx() for nextBlock */
 1:$(function(){ // 当然 DOM が Ready されてから起動する必要があります。
 2: var duration = 1000, easing = null,fn = function(){}, clk = {};
 3: function func(i){
 4:  $.each(clk[i],function(){
     // target オブジェクトは slideToggleEx 対象の個々のノード 1 つだけを格納する。
 5;   var $target={};
     // $target オブジェクトの j プロパティに slideToggleEx 対象ノードを格納する。
     // fireSlideToggleEx-x クラス指定されたボタン要素がブロック要素の場合には
     // next siblingを block 以外の inline 要素の場合には親要素の
     // next sibling 要素を slideToggleEx 対象とする。
 6:   $target[i] = $(this).css("display")=="block" ? $(this).next() : $(this).parent().next();
 7:   if ($target[i].length) {
 8:    $(this).click(function(){
       // IE においては右側をアニメ起終点にすると固まる事が多いので、
       // 強制的に同種の左側起終点に指定を替える。
 9:     if (jQuery.browser.msie)
10:      var k = i==2 && 1 || i==3 && 4 || i==6 && 8 || i;
11:     else k=i;
       // slideToggleEx を起動する。
12:     $target[i].slideToggleEx(k, duration, easing, fn);
13:     $(this).blur(); // ボタン要素のフォーカスを外す
14:    });
15:   }
16:  });
17: }
18: for (var i=0; i<9; i++){ // 0 から 8 迄の巡回処理
    // クリックイベントを登録するオブジェクト clk の i プロパティを作成し、
    // それに "fireSlideToggleEx-i"( i は 0~8 ) class 名のノードをもつ
    // jQuery インスタンスを登録する。これにより、ページ内にある
    // fireSlideToggleEx-n クラス名を持つノードを捉える。
19:  clk[i] = $(".fireSlideToggleEx-" + i);
    // i 番名のノードがなければ loop の先頭行に戻って i++ する。
20:   if (!clk[i].length) continue;
    // i 番名のノードが存在した場合には、当該インスタンスの
    // 各ノード毎に巡回処理を行うために func 関数を呼び出す。
    // ここに func 関数の第 1 引数 clk[i] が外側の関数内にある変数 clk への
    // 参照を保持しているので、func 関数はクロージャーとなっている。しかし、
    // グローバル環境から func 関数への参照はないので、クロージャーを通じた
    // ローカル変数の活用は行っていない。
21:  func(i);
22:  clk[i].hover( // マウスオーバー/アウト時のボタン背景色の変更
23:   function(){$(this).css({backgroundColor:"pink"})},
24:   function(){$(this).css({backgroundColor:""})}
25:  )
26:  // マウスが押し下げられた場合の色替え
27:  .mousedown(function(){$(this).css('background-color','palegreen')})
28: }
29:});

要点は以下の通りです。

  1. 一般的に同一ページ内で複数の slideToggleEx 対象があるのは当たり前であり、更に同一の起終点から起動させる slideToggleEx 対象が複数ある場合も決して少なくないこと。
  2. 従って class 名を頼りに click イベントを登録するものの( 使う度に決めなければならない id を頼りにすることは出来ません。)、同一 class 名の要素がそれぞれ個別に識別できなければなりません。さもないと、1 つのボタンをクリックした際に、同一クラス名を有する slideToggleEx 対象ノードの全てが一斉にアニメートしてしまいます。

以上の要点を踏まえたコードは結構複雑な構成になりました。

  1. (#2) イベント登録オブジェクト clk を用意します。
  2. (#3 ~ 17) 個々のノードに対してクリックイベントを登録する関数を定義します。この関数を設けたことには大きな意味があります。関数化しないで 21 行の位置に each メソッドを置くと、for loop 内にあるために、どんな場合でも slideToggleEx の第 1 引数が 9 となってしまうからです。
  3. (#18) 9 回の巡回処理により、クラス名を頼りに、個々のノードに対してクリックイベントを登録します。
  4. (#19)まず、クリックイベントを登録するオブジェクト clk の i プロパティを作成し、それに "fireSlideToggleEx-i"( i は 0~8 ) class のノードをもつ jQuery インスタンスを登録します。このインスタンスは複数のノードを含んでいる場合もあり得ます。
  5. (#20) ノードが 1 つも含まれなければ、i が 1 増されてループの最初から処理が再開されます。
  6. (#21) func(i)を呼び出します。この関数が個々のノードに対してクリックイベントを登録します。
  7. (#4) clk[i] インスタンスに登録されたノードの数だけ each 巡回処理を繰り返します。
  8. (#5) $target オブジェクトはその i プロパティに slideToggleEx 対象の個々のノードを 1 つずつを格納するオブジェクトです。
  9. (#6) this は clk[i] に登録されている 1 つのノード を意味します。その jQuery インスタンス $(this) の display style 値を調べて、next sibling 要素への参照を $target[i] プロパティに登録します。
  10. (#7) 返されたインスタンスに slideToggleEx 対象となるノードが 1 つ以上あれば 8 行目以降が実行されます。
  11. (#8) 個々の要素 ( $(this)、つまり clk[i] に登録されている各ノード) に click イベントを登録します。こうして個々の要素毎に、個々の slideToggleEx 対象が特定されてクリックイベントが登録されます。
  12. (#9 ~ 10) IE の場合ブロックの右側を起終点とするアニメーションは固まる場合が多々あります。

    そこで IE に限って右側の起終点を利用せずに、同等の左側起終点となるように変換します。

  13. (#11) IE 以外のブラウザの場合の処理です。
  14. (#12) $target[i] から slideToggleEx メソッドを起動します。
  15. (#22 ~ 27) これらの行は一般的なコードです。

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 におけるアニメーションコードの解読 ( 3 )

このエントリイでは、jquery アニメーションで活用される基本的な次の 5 つの関数やメソッドを解読します。

genFx、show、hide、toggle、fadeTo

genFx( type, num )

引数:第 1 引数の type には show 、hide 又は toggle を代入し、第 2 引数 num には 1~3 を代入して利用します。

返値:オブジェクト

機能:配列 fxAttrs からプロパティ名を CSS style 属性名、プロパティ値を type とするオブジェクトを生成します。

この関数は、要素を 「非表示 → 表示へ」、「表示 → 非表示へ」、「表示/非表示の循環」とアニメートさせるために、animate メソッドの第 1 引数である prop オブジェクトを作成するものです。CSS スタイル値を toggle、show、hide とするので最初は違和感があります。しかし、アニメーションの全容を理解すれば、この関数の必要性や重要性が理解できます。

■genFx( type, num ) 関数コード解読
3765:var elemdisplay = {}, // トップレベルで 3 つの変数を定義する。
3766: timerId,
3767: fxAttrs = [
3768:  // height animations
3769:  [ "height", "marginTop", "marginBottom", "paddingTop", "paddingBottom" ],
3770:  // width animations
3771:  [ "width", "marginLeft", "marginRight", "paddingLeft", "paddingRight" ],
3772:  // opacity animations
3773:  [ "opacity" ]
3774: ];
3775:
3776:function genFx( type, num ){
3777: var obj = {};
    // each 適用対象は num==1 ならば ["height",・・・,"paddingBottom"]、
    // num==3 ならば ["height",・・・,"paddingBottom","width",・・・,"opacity"] となる。
3778: jQuery.each( fxAttrs.concat.apply([], fxAttrs.slice(0,num)), function(){
3779:  obj[ this ] = type; // each 適用対象ごとに type 値を代入し、
3780: });
3781: return obj; // obj={ height:type, marginTop:type, ・・・} となる。
    // つまり、返値は CSS style 属性名をプロパティ名とし、
    // type をプロパティ値とする複数のプロパティからなるオブジェクトとなる。
3782:}

▲ToTop

jQuery().show(speed,callback)

引数:アニメの継続時間、アニメ終了後に起動させるcallback 関数

返値:jQuery インスタンス

機能:言うまでもなく jQuery インスタンスに登録されている要素を表示させるメソッドです。

要素の display プロパティ値は、block だけではなく inline やその他の指定もあります( inline-block、table、inline-table 等々 )。ですから、「このメソッドは引数を与えなければ、単に display="block" とするだけの単純な関数だろう」との推測は、見事に裏切られてしまいます。

コードは2つの部分から成り、引数がある場合には animate メソッドが起動されますが、その解説は animate メソッドの項に譲り、ここでは引数がない場合の show メソッドの進行を跡付けます。

show メソッドの要点

第一は、hide メソッドも同様ですが、当該メソッドが同一インスタンスに対して複数回呼び出された場合の対応についてです。2 度目以降の呼び出しに対して省力化の仕組みが組み込まれており、そのことやその意義を理解する必要があります。

第二は、display プロパティについて、その算出( IE の場合には「カレント」と呼ぶことが多い)スタイル値と style オブジェクトのプロパティ値の区別を理解しなければならないということです。明示的なスタイル設定と暗黙裏に設定されるスタイル値は別物であることを踏まえなければなりません。

▲ToTop

以上のことを踏まえてコード進行を見てみます。

或る jQuery インスタンスに初めて show メソッドを適用する場合
  • あるインスタンスに対して初めて show メソッドを適用すると、jQuery.data メソッドの定義から、この時点では olddisplay 名で関連づけられた情報は存在しないので、style.display に空文字が代入されます(#3790-3792)。つまり this.style.display プロパティ値を初期化します。
  • 次に、jQuery.css メソッドにより当該要素の算出(カレント)display スタイル値を検証し、それが none だったら(#3794)トップレベル変数である elemdisplay オブジェクトの、当該要素のタグネーム名のプロパティ値の有無を調べます。この時、同一名称のタグに対して過去に show メソッドが呼び出されていなければ、このプロパティは存在していませんから、3800 行以下が履行されます。
    • 3800 行から3808 行では、show メソッドが対象としているタグ要素と同じ名称のタグを当該サイト内の body 部に追加し、そのカレントスタイル値を取得し、その値が none 以外ならばそのプロパティ値を、他方 none ならば "block" を、elemdisplay[ tagName ] に代入します。(#3808)
    • こうして elemdisplay[ tagName ]プロパティ値を利用するか、あるいはそれに none 以外のプロパティ値を代入した後に、インスタンスの各要素に olddisplay 名で display 値を関連づけます(#3811)。
    • ここに変数 display 値は、elemdisplay 値が存在した場合、すなわち同じタグ名称の他の要素に対して show メソッドが適用済みの場合(当該インスタンスタンス内の他の同一名称のタグに適用済みの場合を含む。対象としたインスタンス内のタグ名称が全て同じ場合には、2巡目以降の場合がこれに該当する)には、elemdisplay 値になります。
    • 他方、elemdisplay が存在しなかった場合には、当該サイト内において今対象としているタグ要素に暗黙裏に設定されている display スタイル値が、none ならば block に、none 以外であれば当該値( block、inline等々 )となります。
  • こうして当該要素にそれに相応しい display 値が与えられ、ブラウザがそれにより当該要素を描画し、ディスプレイ画面に表示させます。

▲ToTop

同じ要素に対して二度以上の show メソッドを適用した場合

次に、同じ要素に対して二度以上の show メソッドを適用した場合を跡付けます。

この場合には、既に一度目の show メソッド適用によって、変数 elemdisplay オブジェクトの当該要素と同一のタグネームプロパティ値が none 以外となっており、かつ当該要素には olddisplay 名で none 以外の display 属性値がテキスト文字列で関連づけられています。

この結果、3792 行により当該要素の style オブジェクトの display 値が none 以外の値に設定され、更に constant reflow を避けるために設けられた 3817 行により、再度 style.display 値が none 以外の block 等となります。

なお、constant reflow を直訳すれば「定期的な再流」ですが、これでは何のことか皆目分かりません。ネット検索してみましたが未解明です。

show インスタンスメソッドが複雑なコードになっている理由

ところで、当初は show メソッドは単純に if (this[i].style.display=="none") this[i].style.display="block" とするだけで用が足りると思っていましたから、どうしてこのように複雑なコードになっているのか、その理由をここで考えてみたいと思います。

まず、elemdisplay オブジェクトの存在意義です。どうしてトップレベルの変数に、対象としている要素と同一のタグネームプロパティを設け、その値を none 以外とするのでしょうか。あたかも同一タグ名称の要素の display プロパティ値を、1 つに統一する必要があるかのようです。

例えば、ある p タグの n 番目の要素( p(n)と名付ける )を show メソッドで表示させたとします。すると elemdisplay.p = "block" となり、p タグの n 番目以外の要素( p(m)と呼ぶ )に show メソッドを適用する場合には、#3799~3809 がスルーされ、#3811 で olddisplay 名で文字列 block が p(m) に関連づけられ、最後に #3818 によって p(m) の display 値が block となり、ブラウザ上に p(m) 要素が表示されます。

append 行為を同一名称の要素に対して 2 度以上引き起こさせない効率化が目的なのかもしれません。但し、それ以外の目的があるかどうか、また、あるとすれば何かについては解明できていません。

次に、同一タグ名の要素を わざわざ append するのは何故でしょうか。

それは次のような理由によると考えられます。

body に対象要素と同一タグ名称の要素を追加し、その算出(カレント) display 属性を調べれば、そのサイトにおいて、当該タグがどのような display 値をとっているのか把握することが出来ます。この方法によって、当該タグがサイト内で有する算出(カレント)display 値を取得することにしたのでしょう。

これに対して、olddisplay 名による要素への none 以外の文字列の関連づけは、分かりやすいものです。jQuery.data メソッドを利用するのは、一度当該要素の display 属性値を none 以外に登録した要素に対して、二度目以降はこの登録済み文字列を利用して容易に display 値を同じ値に設定するためです。show メソッドにおいても hide メソッドにおいても、jQuery.data メソッドがしっかり活かされています。

■$().show メソッドコード解読
3785: show: function(speed,callback){
3786:  if ( speed ) { // 引数 speed が定義されていれば
      // 「genFx("show",3 )を CSS スタイル値、speed をアニメ所要時間とし、
      // callback 関数をアニメ終了後に起動する animateメソッドを、
      // jQuery インスタンスから呼び出して実行し、その返値を返す。
3787:   return this.animate( genFx("show", 3), speed, callback);
3788:  } else { // 引数 speed が未定義か null 値の時、インスタンスの個々の要素に対して
3789:   for ( var i = 0, l = this.length; i < l; i++ ){
       // olddisplay なる名称で各要素ノードに関連付けられている値を取り出す。
       // 関連づけられた値がない場合には old = false となる。
       // 当該要素ノードに対する初めての show() 適用時には old は false と
       // なるが、二度目以降の呼び出し時には 3811 行で関連づけが定義済みなので
       // old = "block" となる。
3790:    var old = jQuery.data(this[i], "olddisplay");
3791:    // old があれば各要素ノードの display スタイル値に old を、
       // old が false ならば null を代入する。
3792:    this[i].style.display = old || "";
3793:    // 各要素ノードの display スタイル値が "none" ならば
3794:    if ( jQuery.css(this[i], "display") === "none" ) {
3795:     var tagName = this[i].tagName, display; // ローカル変数定義
3796:     // 当該要素ノードに対する初めての show() 適用時には
        // elemdisplay[ tagName ] プロパティは存在していないが、二度目以降の
        // 呼び出し時には 3808 行で定義済みなので存在していることになる。
        // elemdisplay オブジェクトに tagName プロパティがあれば、
3797:     if ( elemdisplay[ tagName ] ) {
3798:      display = elemdisplay[ tagName ]; // display 値をその値とする。
3799:     } else { // elemdisplay オブジェクトに tagName プロパティがなければ
         // tagName と同じ名称の要素ノードを作成し、body 追加後に参照を取得する。
3800:      var elem = jQuery("<" + tagName + " />").appendTo("body");
3801:      // 追加したノードの display 属性を取得する。
3802:      display = elem.css("display");
3803:      if ( display === "none" ) // もしもその値が "none" ならば
3804:       display = "block"; // "block" に変更する。
3805:      // body に追加したノードを削除し、
3806:      elem.remove();
3807:      // elemdisplay の tagName プロパティ値を display つまり "block" とする。
3808:      elemdisplay[ tagName ] = display;
3809:     }
3810:     // 各要素ノードに olddisplay 名の display 値 つまり "block" を関連づける。
3811:     jQuery.data(this[i], "olddisplay", display);
3812:    }
3813:   }
3814:
3815:   // Set the display of the elements in a second loop
3816:   // to avoid the constant reflow
3817:   for ( var i = 0, l = this.length; i < l; i++ ){
       // 各要素ノードの display スタイル属性を olddisplay 名で関連づけら
       // れている値("block")、又は null に設定する。
3818:    this[i].style.display = jQuery.data(this[i], "olddisplay") || "";
3819:   }
3820:
3821:   return this; // インスタンスを返す。
3822:  }
3823: },

▲ToTop

jQuery().hide(speed,callback)メソッドの解読

引数:継続時間、callback関数

返値:jQueryインスタンス

機能:言うまでもなく jQuery インスタンスに属する要素を隠蔽させるメソッドです。

このメソッドにおいても引数が与えられた場合には、animate メソッドが利用されますがここでは触れません。引数がない場合のみ解読します。

ある要素に初めて hide メソッドを適用した場合

インスタンスの個々の要素に対して、最初に hide メソッドが適用された場合において、既に当該要素に show メソッドが適用されていれば、当該要素に olddisplay 名で関連づけられた情報は block や inline となっており、一度も show メソッドが適用されていなければ、当該要素に olddisplay 名で関連づけられた情報は存在しません。

つまり、#3830 の変数 old には、block、inline などのスタイル値か、又は undefined が代入されます。そして old が undefined 値の場合、!old==true となり、勿論 old!=="none" が成立します。

こうして事前に show メソッドが適用されていない要素に対して、初めて hide メソッドを適用した時には、#3832 が実行され、data メソッドと css メソッドにより、当該要素の style.display 値、または算出(カレント)display 値が、olddisplay 名で当該要素に関連づけられます。
(関連づけられる値は block、inlineblock、inline 等となりますが、if 条件から none だけはあり得ません。olddisplay 値は block や inline などの表示用の値となり、none となることは決してありません。)

show メソッドの後に hide メソッドを適用した場合、あるいは hide メソッドを二度以上適用した場合

他方、show メソッドが適用された後に hide メソッドを適用した場合や、同一要素に対して二度目以降の hide メソッドを適用した場合には、old は未定義ではなくなっていますから !old=false となり、#3831~3832 は実行されないまま、#3837~3838 が実行され当該要素は非表示、あるいは非表示のママとなります。

上の行為の後に再び show メソッドを適用した場合

olddisplay 値は block や inline 等となっているので、3792 行により display 値はそのままとなります。よって #3794-3812 は通過し、3818 行が実行されて当該要素が表示されることになります。

■$().hide メソッドコード解読
3825: hide: function(speed,callback){
3826:  if ( speed ) { // speed があれば animate メソッドを実行する。
3827:   return this.animate( genFx("hide", 3), speed, callback);
3828:  } else { // speed がないか null の時
3829:   for ( var i = 0, l = this.length; i < l; i++ ){
       // 各要素に olddisplay 名で関連づけられているデータを変数 old に代入する
3830:    var old = jQuery.data(this[i], "olddisplay");
3831:    if ( !old && old !== "none" ) // old が false か "none"でない時には
        // 各要素ノードの display スタイル値を
        // olddisplay 名で各要素ノードに関連づける。
3832:     jQuery.data(this[i], "olddisplay", jQuery.css(this[i], "display"));
3833:   }
3834:
3835:   // Set the display of the elements in a second loop
3836:   // to avoid the constant reflow
3837:   for ( var i = 0, l = this.length; i < l; i++ ){
3838:    this[i].style.display = "none"; // 非表示を設定する。
3839:   }
3840:
3841:   return this; // インスタンスを返す。
3842:  }
3843: },
3844:

▲ToTop

jQuery().toggle(fn,fn2)

引数:fn,fn2

返値:jQueryインスタンス

機能:jQuery インスタンスに属する要素を表示/隠蔽させる循環メソッドです。

toggle メソッドはイベント処理にも登場します。それが最初の分岐(#3851)の2つの引数が共に関数の場合です。この場合には 3852 行が適用されてイベントの toggle 処理が行われます。

fn、fn2 が共に存在しない場合、あるいはいずれか一方以上が関数でない場合が、アニメ-ション処理で利用する toggle メソッドです。

アニメで利用する toggle メソッドは 2 つのケースに分かれます。

第 1 のケースは、fn、fn2が共に存在しない場合、あるいは第1引数が真偽値の場合です。このときにはインスタンスに登録されている各要素毎に次のことを行います。

  1. 最初の引数が真偽値で true ならばその値を state に代入します。
  2. bool がない場合(つまり引数が全くない場合)や、最初の引数が false の場合には、対象要素のhidden 属性の有無を調べ、あれば true、なければ false を state に代入します。つまり引数を全く指定しない場合や、fn == false と指定した場合には、当該要素の hidden 属性の有無によって、state に代入される値が決まります。hidden 属性があれば true、なければ false となります。(#3855)
  3. 次に、state が true ならば、jQuery(this).show() メソッドを実行し、false ならば、jQuery(this).hide() メソッドを実行します。(#3856)

第 2 のケースは fn、fn2 のいずれか一方以上が関数でない場合で、かつ最初の引数が真偽値でない場合です。このときには、"toggle" をアニメ用オブジェクトのプロパティ値とし、fn と fn2 を順に speed、easing として、インスタンスから animate メソッドを起動します。(#3858)この animate メソッドにおいて第 1 引数であるオブジェクトのプロパティ値を toggle とする場合の挙動は別途詳述します。

■ toggle インスタンスメソッド
3845: // Save the old toggle function
3846: _toggle: jQuery.fn.toggle,
3847:
3848: toggle: function( fn, fn2 ){
3849:  var bool = typeof fn === "boolean"; // fn が真偽値かどうかを bool に代入
3850:  // (1)fn も fn2 も関数ならば_toggle メソッドを実行する。
     //(このケースはイベント処理時に登場する)
3851:  return jQuery.isFunction(fn) && jQuery.isFunction(fn2) ?
3852:   this._toggle.apply( this, arguments ) :
     // (2)引数がない場合、あるいはfn か fn2 のいずれか1つ以上が関数でない場合には、
      // (2-1)引数がないか、または fn が bool(真偽値)ならば
3853:   fn == null || bool ?
3854:    this.each(function(){ //各インスタンス毎に
        // bool が true ならば fn を、bool がない場合や false ならば当該要素の
        // hidden 属性有無(true or false)を変数 state に代入する。
3855:     var state = bool ? fn : jQuery(this).is(":hidden");
        // state が true ならば show メソッドを、
        // state が false ならば hide メソッドをそれぞれ起動する。
3856:     jQuery(this)[ state ? "show" : "hide" ]();
3857:    }) :
      // (2-2)fn が null でもなく真偽値でもない場合animateメソッドを実行する。
3858:    this.animate(genFx("toggle", 3), fn, fn2);
3859: },

▲ToTop

jQuery().fadeTo()

引数:speed、to、callback

返値:jQueryインスタンス

機能:不透明度を to、speed を duration、callback をアニメ終了後に実行される関数とする、animate メソッドを起動します。

■ fadeTo メソッド
3861: fadeTo: function(speed,to,callback){
3862:  return this.animate({opacity: to}, speed, callback);
3863: },

▲ToTop

自作 jQuery プラグイン :animatedPopup 後に別のアニメーションを連続起動する

ここのアニメーションは IE では動きません。

jquery.js を使用した Animation 連鎖にチャレンジ

animatedPopup 起動後に、続けて別の Animation を連続起動してみる。

jQuery では連続アニメーションを容易に実行させることが出来る。例えば animatedPopup による表示を終えた後に、その要素を例えば画面内で移動するとか、次第に不透明化しながら消滅させるとか。表示領域を縮小/拡大するとか、様々なアニメーションを連続起動させることが可能だ。

あるいはまた複数のアニメーションを同時に履行させることも出来る。更にアニメーション終了後に callback 関数を起動させて何らかの処理をさせることも可能だ。自作プラグイン animatedPopup の連続起動も行えるだろう。

このエントリイではそんな複数アニメーションの連続履行を試みる。

連続アニメ 1. animatedPopup 後の要素を画面内で移動する。

animatedPopup 関数起動が終了すると、ユーザーが隠蔽操作を行わない限り、その要素は画面内に表示され続ける。これを活かして当該要素を自在に画面内に移動させてみる。

表示するコンテンツは先のエントリイ同様に他サイトの写真素材を活用し、画面内を次のように移動させる。まず画面左上に animatedPopup 表示する。その後、slideUp、slideDown で遊んでから、画面内を右上 → 右下 → 左下と時計回りに連続して移動させ、最後に最初の位置に戻してから fadeOut させてみる。

移動速度は 0.5、1、1.5、2 秒と次第に遅くし、それぞれの動きを加減速する easing も微妙に変化させ、消滅には 2.5 秒を掛けてみた。

この一連の動きを実行するには、下のボタンをクリックするだけでよい。この一連の自動実行アニメーションを楽しんで(?)戴きたい。

(出典は pixta

▲ToTop

連続アニメ 2. animatedPopup 後複数のアニメーションを同時起動する。

今度は animatedPopup によるアイテム表示後に、それを画面内で横移動させながら、フェードアウトさせ、直ぐにフェードインさせ、その後上へ移動させてから消滅させる。

(出典は pixta

連続アニメ 3. 2つの animatedPopup とその後のアニメーションを活かしつつ、相互に連続起動する。

下のボタンをクリックすると上で定義した 2 つのアニメーションを連続実行する。もう一度同じボタンを押した場合には、連続実行の順番を変えて実行するようにした。

▲ToTop

ここで実現した連続アニメーションのコード解説

ここで行った連続アニメーション作成の過程において、animatedPopup プラグインは更に進化を遂げざるを得なかった。元々連続アニメーションを想定していない仕様だったからだ。

その拡張に関する詳細は別項にて述べるとして、ここではこのエントリイで実現した連続アニメーションのコードについてメモとして解説しておきたい。

連続アニメーションのコード作成過程において、jquery.js のアニメーションコードをますます深く学習せざるを得なかったし、作成途上でスクリプトに工夫を加えた箇所も多数に上ったからである。

■このエントリで実現した連続アニメーションのコード
  // animatedPopup プラグインのインクルード
 1:(function(){var s=document.createElement('script');s.src='http://blog-imgs-31.fc2.com/h/k/o/hkom/my_Utilize_jQuery2.js';s.type='text/javascript';document.getElementsByTagName('head')[0].appendChild(s);})();
   // ボタンの色づけ
 2: $(".btn711").css("backgroundColor","burlywood")
 3:  .mousedown(function(){$(this).css("backgroundColor","plum")})
 4:  .mouseup(function(){$(this).css("backgroundColor","burlywood").blur()});
   // 表示するコンテンツの作成
 5: var contents0="次のアニメーションを起動します。";
 6: var contents1='<img src="http://image.pixta.jp/image/thumb/51/14d687c77d91104811600b9d9916948a.jpg" width=450 height=301 />';
 7: var contents2='<img src="http://image.pixta.jp/image/thumb/41/bf5a19a4278538425a476ed1e113f0a9.jpg" width=450 height=316 />';
 8: // 画像先読み
 9: $("<div></div>").css("display","none").prependTo(document.body).html(contents1+contents2);
10:
11:$(function(){
   // animatedPopup プラグイン終了後に履行する関数定義その1
12: startAfterAnima = function(){
13:  var ani1=$(window).width()-$(this).outerWidth()+"px";
14:  var ani2=$(window).height()-$(this).outerHeight()+'px';
15:  $("#dispElem").slideUp("slow").slideDown()
16:  .animate({"left":'+='+ani1},500,"linear")
17:  .animate({"top":"+="+ani2},{duration:1000,easing:"swing"})
18:  .animate({"left":"-="+ani1},{duration:1500,easing:"easeInOutExpo"})
19:  .animate({"top":"-="+ani2},{queue:true,duration:2000,easing:"easeInOutSine"})
20:  .animate({"opacity":0},2500,"swing",function(){$(this).empty().hide().css({"opacity":1})});
21: }
   // animatedPopup プラグイン終了後に履行する関数定義その2
22: var startAfterAnima2 = function(){
23:  var ani=$(window).scrollTop();
24:  $("#dispElem").slideUp("slow").slideDown("slow")
25:  .animate({"left":"0px"},{queue:false,duration:2000,easing:"swing"})
26:  .fadeOut("slow").fadeIn("slow").animate({"top":ani},{queue:true,duration:2000,easing:"easeInOutSine"})
27:  .animate({"opacity":0},2500,"swing",function(){$(this).empty().hide().css({"opacity":1})});
28: }
29: // 各ボタンクリック時の animatedPopup 起動
30: $("#btn711-0").animatedPopup(contents0,["c","c"],1500,null,null,true);
31: $("#btn711-1").animatedPopup(contents1,["l","t"],2000,"easeOutElastic",null,null,true,startAfterAnima);
32: $("#btn711-2").animatedPopup(contents2,["c","c"],2000,"easeInOutExpo",null,null,true,startAfterAnima2);
 // 連続してanimatedPopup メソッドを起動する処理
33: // 3 つのボタンの jQuery インスタンスを配列に登録
34: var ary = [$("#btn711-0"),$("#btn711-1"),$("#btn711-2")];
   // 次のアニメーションを起動させる関数
35: var nextFunc =function(jQelem){
36:  if (!$("#dispElem").queue("fx").length){
37:   jQelem===ary[0] ? clearInterval(ival) : clearInterval(ival2);
38:   jQelem.trigger("click");
39:  }
40: }
41: var ival,ival2;
   // 3 番目のボタンに対する toggle イベント定義
42: $("#btn711-3").toggle(function(){
43:  $(this).children(0).text("リゾートプール+清水寺");
44:  ary[1].trigger("click");
    // 2つの待機関数を定義
45:  ival = setInterval(function(){nextFunc(ary[0]);},1000);
46:  ival2 = setInterval(function(){nextFunc(ary[2]);},1500);
47: },function(){
48:  $(this).children(0).text("清水寺+リゾートプール");
49:  ary[2].trigger("click");
50:  ival = setInterval(function(){nextFunc(ary[0]);},1000);
51:  ival2 = setInterval(function(){nextFunc(ary[1])},1500);
52: });
53:});

▲ToTop

コード内に記述したコメントでかなり説明になっていると思うが、苦労した点を中心にメモしておきたい。

アニメーション同時進行

幾つかの箇所でアニメーションの同時起動を試みた。queue:false に設定している箇所が同時進行のためのコードで、具体的には移動しながら slideUp/Down を行わせた。

アニメーション終了後の別アニメーションの起動

jQuery の animate インスタンスメソッドの complete 属性を活用して、animatedPopup メソッドが終了した後に別のアニメーションを起動させた。#124-140 で定義した関数が complete プロパティ値となる関数である。

2 つの animatedPopup 連続起動に最も苦労した

animatedPopup プラグインは jQuery インスタンスとして定義した。そしてここで扱った 2 つの写真表示アニメーションは、それぞれの呼び出し元が別々のボタンであり、animatedPopup には巧みに返値を設定することが出来ていない。つまりメソッドチェーンが使えない。

そこで jquery.js の イベント処理メソッドとして定義されている trigger メソッドを使って、異なる呼び出し元から呼び出される animatedPopup メソッドを連続起動させるようにした。

さて要点はタイマー設定だ。単純に 2 つの animatedPopup を並べるだけでは最後のそれしか作動しないのだ。コード進行はアニメーション実行とは無関係に次行へ次行へと進んでしまうから、前のアニメーションを待つことなく次のアニメーションが起動されてしまい、結局最後のアニメーションだけが表示されるのだ。

このため或るアニメーションが動作している間は、次のアニメーションが起動されないようにしなければならない。

そこで、タイマーを使って次のアニメーション起動を抑制することにした。

そこで問題となるのは、アニメーションが進行中であることを Javascript インタープリタに知らせる方法だ。jQuery インスタンス毎に、メソッド queue("fx") に animation が登録されることを利用する方法が或る書籍に書いてあったのでそれを使ってみた。

2 つ以上の animatedPopup の連続起動を別の方法で行ってみる

もっと巧い方法はないのだろうか、と思いたち本家サイトを改めて眺めてみた。すると、極めて簡単な方法が実は存在していることを思い知らされた。それは jQuery インスタンスの add メソッドを利用するものだ。こんな風にaddメソッドが使えたのか!───と感心してしまった。

しかし、この方法は 1 つの絶対配置要素を使ってアニメーションを実現している animatedPopup では巧く採用することが出来ないでいる。

jQuery プラグイン としてアニメーションポップアップ : animatedPopup を自作した。

このエントリイの改訂履歴など
  • 初稿:2009/04/19
  • 完結:2009/04/23
  • 第 1 回改訂:2009/04/25 right、bottom 指定をやめ全て left 又は top に統一した。
  • 第 2 回改訂:2009/04/26 画像も表示できるように引数処理を修正した。またポップアップ表示ボックスの色味を自在に操れるように引数を1つ追加した。これらによりこのプラグインの使いやすさが向上した。
  • 第 3 回改訂:背景やボーダーの色が思うように変化しないバグを修正
  • 第 4 回改訂:全面的改訂を断行。その内容は別のエントリイ → 自作プラグイン-アニメーションポップアップを全面改訂 にまとめました。これに伴いここで説明するプラグイン名を animatedPopupOld に変更しました。

残念ながら IE ではここで説明するプラグインは動きません。

その理由は IE8 のエラーメッセージに拠れば「 jquery.js のアニメーション関連コードに問題有り 」と表示されます。他方、Firefox、Opera 及び Windows 版 safari では問題なくこのページのアニメーションが表示されます。IE 8 で動かない理由は解明できていません。IE でも動くように改訂したいのですが、原因が解明できないため対応策が見つかりません。

ここで言う Animated Popup とは

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

Windows Vista でウィンドウ開閉時に行われるような、あのようなズームイン/アウトをポップアップ表示/隠蔽にも取り入れてみたいと思い立ち 4月11日頃に作成を開始しました。

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

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

 クリックした位置の傍にポップアップ表示します。

クリックすると画面の中央にポップアップ表示します。

クリックすると画面の右下にポップアップ表示します。

クリックすると指定してある位置にポップアップ表示します。

クリックすると画面の中央に或るサイトの或る写真をポップアップ表示します。写真出典元は フォトライブラリ です。

▲ToTop

Animation と Easing

Easing は Animation 動作速度を変化させる「加速度メソッド」のことで、元々 Flash ツールである Acrion Script の Tween クラスの加速度メソッドとして誕生したようです。(半可通の知識しか持ち合わせていないので「ようです」としておきます。)

この Easing 関数が jQuery プラグインとして提供されているので、早速それを導入し、このページでは敢えて動きの激しい easeOutElastic を使用しました。本来、文字を表示するには変化の激しい elastic は相応しくないでしょうが、Easing を初めて使用した記念として「激しい」ものを選択してみたのです。

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

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

そこで、このデモサイトをまねて、このページ上で実現しているアニメーションポップアップに対して 32 種類(jquery.js にビルトインされている linear と swing の 2 種類を追加したため 32)の Easing 効果を適用するリストボックスを設置してみました。easing_demo に比べて文字表示の場合には各 easing の差異がわかりにくいのですが、まあそれはご愛敬ということで...。

下のリストボックスの説明
  • アニーメーションポップアップの表示位置は 「 画面中央 」 としました。
  • Easing に要する時間はそれぞれの関数の差異が分かり易いように長めに設定しました(2秒)。
  • jquery.js に組み込まれている linear と swing の 2 つの Easing 関数もリストアップしたので都合 32 種類の easing 関数が登録されています。
  • 別の easing 関数を選択する前に表示されているポップアップを隠蔽する必要はありません。自動的に前の表示を消して、新たな表示を行います。
補足:上の リストボックスに係るスクリプトの説明

このページのソースコードを表示すれば分かることですが、jquery.js を使って初めて form を扱ったので、記録を残すために敢えてここに記します。

■アイテムが選択された際の処理を行うスクリプトコード
$("#sel707").change(function(){
 var index = this.selectedIndex,
   selEasing = this.options[index].value,
   txt="これは easing 関数: "+selEasing+" を使って<br />表示/隠蔽するアニメーションポップアップです。";
 $(this).animatedPopupOld(txt,["c","c"],2000,selEasing,{width:"400px"});
});
html説明
selectタグに id="sel707" と size=10を、最初の option タグに非選択属性 disabled="disabled" を、二番目の option タグに初期値 selected="selected" を設定しました。
Javascript 説明
  • 要点は selectタグの jQuery インスタンスに change イベントを登録することです。
  • まずリストボックスから選択されたアイテムのインデックス番号を変数に代入し、その値を使って options 配列から選択されたアイテムの value 値を取りだし、最後にポップアップ文を構成します。
  • 以上の僅か 3 つの変数を定義するだけで準備は完了。後は適切な引数付きで animatedPopup メソッドを登録するだけです。

以上の簡単なコードによって、リストボックス内の項目が選択されたその瞬間に発生する change イベントにより、イベントハンドラーが起動されて必要な処理を行います。

▲ToTop

作成した Animaeted Popup の仕様

  • 作成した Animaeted Popup は jquery.js のプラグインとして組み込める形式としました。
  • Animaeted Popup の呼び出しは次の書式で行います。jQuery(expr) はjQueryインスタンスならば何でも構いません。
    jQuery(expr).animatedPopupOld(contents, layout, duration, easing, options, options2);
  • <引数の説明>
     * contents: Strings in Animated Popup
     * layout: displaying position of Animated Popup
     *         text,ex. ["c","t"] (It means :left="center",top="top")
     *         or object,ex.{left:"100px",top:"200px"}
     *         or {left:"center",top:"100px"}
     * duration: durating time of animation(msec), ex.500 or "slow" etc.
     * easing: easing name (text), ex."easeInOutQuad","easeOutBounce",..32通りの指定が可能
     * 但し 32 種類を利用するには Easing Plugin をインクルードする必要があります。
     * このサイトで利用している jquery.js には Easing Plugin を組み込み済みです。
     * options: Add CSS Object to Popup DIV, ex.{width:"400px",・・・}
     * options2: Add CSS Object to Popup DIV's CLOSE Bar, ex.{background:"midnightblue",・・・}
     * 全ての引数にはデフォルト値が設置済みなので、複数の任意の引数が省略され
     * ても誤動作しません。
     * 何も引数がない場合、その旨を表示する animatedPopupOld を画面中央に表示します。

▲ToTop

作成した Animaeted Popup の Javascript コード

コードは長めになりました。説明を含めて 190 行あります。

ここでその全てを掲載しブロックごとに解説を加えることとします。

animatedPopupOld プラグインの全コード
  1:(function($){
  2:$.fn.extend({
  3:animatedPopupOld: function(contents,layout,duration,easing,options,options2){
  4:/*
  5: * <Example of Implementation>
  6: * jQuery("p").animatedPopupOld("And I love you so. Yesterday. Yellow Submarine.",
  7: *   ["c","b"],800,"swing",{width:"300px"})
  8: *
  9: * <arguments explanation >
 10: * contents: Strings in Animated Popup
 11: * layout: displaying position of Animated Popup
 12: *      text,ex. ["c","t"] (It means :left="center",top="top")
 13: *      or object,ex.{left:"100px",top:"200px"}
 14: *      or {"left":"center","top":"100px"}
 15: * duration: durating time of animation(msec), ex.500 or "slow" etc.
 16: * easing: easing name (text), ex."easeInOutQuad","easeOutBounce",...
 17: * options: Add CSS Object to Popup DIV, ex.{"width":"400px",・・・}
 18: * options2: Add CSS Object to Popup DIV's Title DIV, ex.{background:"midnightblue",・・・}
 19: *
 20: * <history>
 21: * released 2009/4/22 ver0.1
 22: * update 2009/4/25 ver0.2, 2009/4/26 ver0.3
 23: */
 24: var jQInst=$(this), winWH = {width:$(window).width(),height:$(window).height()},
 25:  errFlag,lcr,tcb,xName,yName,m=[],addValue={x:0,y:0},isImg,
 26:  mousePos={left:0,top:0},xCenter=$(window).width()/2,yCenter=$(window).height()/2;
 27:  // CSS 既定値は width 値が contents により変わるので、関数化しインスタンス毎に設定する。
 28: var defaultCSS =function(){
 29:  isImg = contents.match(/.*((<img.+src.+)|(<object.+)|(<embed.+)).*/i);
 30:  return {
 31:   "color":"white","font-weight":"bold","margin":0, "padding":"19px 5px 5px 5px",
 32:   /* 画像の場合ここで width を決めては駄目*/
 33:   "width": (isImg && isImg[1]) ? null : "300px",
 34:   "background-color":"royalblue", "border":"5px plum ridge", "text-align":"center",
 35:   "display":"block","visibility":"visible"
 36:  }
 37: }
 38: /* ポップアップに使用するCSS値を設定する*/
 39: var popupCSS = $.extend(true,defaultCSS(),options || {});
 40: /* closeBar CSS値も容易に変更できるように設定する */
 41: var closeBarCSS = function(){return $.extend(true,{
 42:  "position":"absolute","zIndex":"1001","text-align":"center",
 43:  "opacity":0.75,"top":0,"left":0,"width":"100%","cursor":"pointer",
 44:  "font-size":"small","lineHeight":"1.2em","background-color":"midnightblue"
 45: },options2 || {})};
 46:
 47: // bind onmousemove event on document
 48: mousePos.oX = 4; mousePos.oY = 16; //マウスカーソルからの離隔距離
 49: $(window).mousemove(function(e){
 50:  mousePos.left = (jQuery.browser.msie ? window.event.clientX - document.body.clientLeft : e.clientX) + mousePos.oX;
 51:  mousePos.top = (jQuery.browser.msie ? window.event.clientY - document.body.clientTop : e.clientY) + mousePos.oY;
 52: });
 53:
 54: // error 処理関数(後に拡張できるように関数にしておく)
 55: var errFunc = function(){ errFlag=true; return function(){return}}
 56:
 57: // animatedPopupOld の配置位置算出関数
 58: var getPos = function(layout){
 59:  if (layout && layout.constructor!= Object && layout.constructor!= Array ){
 60:   alert("配置は2つのテキスト:例 \"Center\",\"Top\" 、または\nオブジェクト形式:例 {left:\"10px\",top:\"100px\"} で指定してください。");
 61:   errFunc()();
 62:  }
 63:  var setPos = layout;
 64:  if (setPos.constructor==Array) {
 65:   var chk1 = /^((l.*)|(c.*)|(r.*))/.exec(setPos[0].toLowerCase());
 66:   var chk2 = /^((t.*)|(c.*)|(b.*))/.exec(setPos[1].toLowerCase());
 67:   if (chk1 && chk2){
 68:    lcr = chk1[2] && "left" || chk1[3] && "center" || chk1[4] && "right";
 69:    tcb = chk2[2] && "top" || chk2[3] && "center" || chk2[4] && "bottom";
 70:    xName = (lcr!=="right") ? "left" : "right";
 71:    yName = (tcb!=="bottom") ? "top" : "bottom";
 72:   } else {
 73:    if (!chk1 && !chk2) {
 74:     alert("左右上下 \""+ setPos[0] +" , "+ setPos[1] +"\" 共に指定が間違っています。\nやり直してください。");
 75:    } else if (!chk1 && chk2){
 76:     alert("左右の指定 \""+ setPos[0] +"\" が間違っています。\nやり直してください。");
 77:    } else if (chk1 && !chk2 ){
 78:     alert("上下の指定 \""+ setPos[1] +"\" が間違っています。\nやり直してください。");
 79:    }
 80:     errFunc()();
 81:   }
 82:  } else if (typeof setPos!=="string" && setPos.constructor==Object) {
 83:   for (name in setPos){
 84:    name=name.toLowerCase();
 85:    if (name=="left" || name=="right") {xName = name;lcr=name}
 86:    if (name=="top" || name=="bottom") {yName = name;tcb=name}
 87:   }
 88:   if (xName==null || yName==null){
 89:    alert("left、right、top、bottom 以外の\n指定は無効です。やり直してください。");
 90:    errFunc()();
 91:   }
 92:   // 位置指定値が "left" のように文字で為されたときの対応
 93:   if (!setPos[xName].match(/\d+/))
 94:    m[0] = setPos[xName].toLowerCase().match(/^(left|center|right)$/);
 95:   if (!setPos[yName].match(/\d+/))
 96:    m[1] = setPos[yName].toLowerCase().match(/^(top|center|bottom)$/);
 97:   if (!setPos[xName].match(/\d+/) && m[0]===null || !setPos[yName].match(/\d+/) && m[1]==null){
 98:    alert("配置指定値が間違っています。\nやり直してください。");
 99:    errFunc()();
100:   }
101:   addValue={
102:    x :( m[0] ? ((m[0]==="center") ? xCenter :0) : parseInt(setPos[xName])),
103:    y :( m[1] ? ((m[1]==="center") ? yCenter :0) : parseInt(setPos[yName]))
104:   };
105:  } else if(layout){
106:   alert("配置指定が無効です。やり直してください。"); errFunc()();
107:  }
108:  var obj={};
109:  obj[xName]= ((lcr==="center") ? xCenter : 0) + addValue.x;
110:  obj[yName]= ((tcb==="center") ? yCenter : 0) + addValue.y;
111:  return obj;
112: };
113:
114:$(function(){
115: // popup 表示用の div 要素タグの作成
116: if (!$("#dispElem").size()) {
117:  $("<div id='dispElem'></div>").css({
118:   position:"absolute",display:"none",zIndex:"1000"
119:  }).appendTo(document.body);
120: }
121: var disp=$("#dispElem");
122:
123: // Popup 隠蔽用×タグの作成
124: if (!$("#xMark").size()){
125:  $("<div id='xMark'>CLOSE</div>").append("<div style='width:13px;float:right;margin-top:-1em;'>×</div>").appendTo(disp);
126: }
127: var xMark = $("#xMark").css(closeBarCSS());
128:
129: // STEP1:*************** 表示前に popup エレメントの高さを測定する
130: var getElemWH= function(){ // 表示 popup サイズ算定
131:  // popup 表示前に横幅と文字数に応じた高さを計測する(非表示描画で測定)
132:  contents = contents || "ポップアップする内容が指定されていません。<br />やり直してください。";
133:  disp.html(contents).css($.extend(true,popupCSS,{"visibility":"hidden","height":null}));
134:  if(isImg && isImg[1]) disp.css("width",null);
135:  return {
136:   iW: popupCSS.width=(options && parseInt(options.width) || defaultCSS().width && parseInt(defaultCSS().width) || disp.width()),
137:   iH: popupCSS.height=disp.height(),
138:   oW: disp.outerWidth(),oH: disp.outerHeight()
139:  }
140: }
141:
142:  // 幅/高さが極小の要素 css 値を設定する。これにより拡張/縮小を演出する。
143:  // またここで初めてスクロールされていた場合の変動値を配置px値に追加する。
144: var shrinkCSS=function(){
145:  var scrLeft=$(window).scrollLeft(),scrTop=$(window).scrollTop(),
146:   obj={"width":"1px", "height":"1px", "border":"0px", "padding":"0px","margin":"0px"};
147:  if (!layout){
148:   obj.left=mousePos.left+scrLeft+"px";
149:   obj.top=mousePos.top+scrTop+"px";
150:  } else {
151:   var pos = getPos(layout);
152:   if (errFlag) return;
153:   obj.left=((xName=="left")-(xName=="right"))*pos[xName]+
154:    (xName=="right")*winWH.width + scrLeft+"px";
155:   obj.top=((yName=="top")-(yName=="bottom"))*pos[yName]+
156:    (yName=="bottom")*winWH.height + scrTop+"px";
157:  }
158:  return obj;
159: }
160:
161: var hideElem = function(e){ // popup 要素をアニメーション隠蔽する
162:  disp.empty().animate(shrinkCSS(),{queue:false,duration:duration,easing:easing});
163: }
164: var animaElem = function(e){
165:  $(":animated").queue('fx',[]).stop(); // 登録済みのアニメを全て削除停止する
166:  var doneShrink = shrinkCSS(); // この関数内で1回だけ起動する
167:  if (errFlag) return;
168:  // STEP2:***************
169:  var elemWH = getElemWH(); //エレメントサイズ計測実行
170:  if (!layout){ // 画面からはみ出さないように CSS 値を調整
171:   if (winWH.width < mousePos.left+elemWH.oW)
172:    popupCSS.left = winWH.width + $(window).scrollLeft() - elemWH.oW +"px";
173:   if (winWH.height < mousePos.top+elemWH.oH)
174:    popupCSS.top =  winWH.height + $(window).scrollTop() - elemWH.oH +"px";
175:  } else { // popup をセンター配置する場合の CSS 設定
176:   popupCSS.left = parseInt(doneShrink.left) -
177:    ((lcr==="center" || m[0] && m[0]==="center") ? Math.round(elemWH.oW/2) : (xName==="right") ? Math.round(elemWH.oW) :0) +"px";
178:   popupCSS.top = parseInt(doneShrink.top) -
179:    ((tcb==="center" || m[1] && m[1]==="center") ? Math.round(elemWH.oH/2) : (tcb==="bottom") ? Math.round(elemWH.oH) :0) +"px";
180:  }
181:
182:  // STEP3:*************** 極小要素を指定されたアニメーション起動位置に配置
183:  // dispElem の幅と高さを1pxにして所定位置に配置する(但し非表示描画)
184:  disp.empty().css(doneShrink);
185:  // STEP4:*************** 表示アニメーション
186:  disp.html(contents).append(xMark.css(closeBarCSS()))
187:   .css({visibility: "visible",display:"block"})
188:   .animate(popupCSS,{queue:false,duration:duration||"slow",easing:easing||"swing"});
189:  // STEP5:*************** × クリック時に隠蔽アニメーションを起動する。
190:  // ここで隠蔽関数を起動するようにしておかないと、複数回ボタンがクリックされた
191:  // 場合に隠蔽操作ができなくなる。理由は未解明。
192:  xMark.click(hideElem);
193: }
194: jQInst.click( errFlag ? function(){errFlag=false; $(this).unbind("click");} : animaElem );
195:}); // End of "DOMReady function"
196:} // End of "animatedAlert function"
197:}); // End of "Extend function"
198:})(jQuery);

▲ToTop

animatedPopupOld プラグインの解説

その意味や役割から幾つかのブロックに分けて解説します。

第Ⅰブロック(#1-22)

まず最初のブロックは、このメソッドの引数を定義しそれらを解説している部分です。

無名関数による起動と DOMReady 関数の利用(#1、#100)

このプラグインでは、一般的方法に倣って全体を無名関数で括り、即実行するようにしました。またポップアップ表示やポップアップ消去のための絶対配置 div 要素を追加し、かつそれらに様々なメソッドを適用する必要があることから、必要最小限の範囲を DOMReady function で括りました。

$.fn.extend クラスメソッドの利用(#2)

これにより、jQuery インスタンスのメソッドとして animatedPopupOld 関数を登録します。

animatedPopupOld 関数の引数(#3-18)

引数は6つあります。第一引数はポップアップボックス内に表示する文字列で、第2引数 layout は様々な方法でポップアップ位置を指定できるよう工夫し、第2引数を指定しない場合には、起動元 jQuery インスタンスが指し示す要素の近傍にポップアップするように設計しました。

第5及び第6引数も工夫しました。extend メソッドの2つめの使い方、つまり「 オブジェクト拡張 」を利用して、popup 要素のデフォルトCSS を設定しておくと共に、options で自在にそれを変更できるようにしました。幅や色、ボーダーの色と幅等々気分自由に変更することが出来るようにすることが目的であり、それを達成しました。

なお、この方法は jquery.js の Ajax ブロックで採用されていますのでそれを参考に考案しました。

▲ToTop

第Ⅱブロック(#24-45)

このブロックは、ローカル変数を定義している部分です。

■変数定義部分(再掲)
 24: var jQInst=$(this), winWH = {width:$(window).width(),height:$(window).height()},
 25:  errFlag,lcr,tcb,xName,yName,m=[],addValue={x:0,y:0},isImg,
 26:  mousePos={left:0,top:0},xCenter=$(window).width()/2,yCenter=$(window).height()/2;
 27:  // CSS 既定値は width 値が contents により変わるので、関数化した。
 28: var defaultCSS =function(){
 29:  isImg = contents.match(/.*((<img.+src.+)|(<object.+)|(<embed.+)|(<iframe.+)).*/i);
 30:  return {
 31:   "color":"white","font-weight":"bold","margin":0, "padding":"19px 5px 5px 5px",
 32:   /* 画像の場合ここで width を決めては駄目*/
 33:   "width": (isImg && isImg[1]) ? null : "300px",
 34:   "background-color":"royalblue", "border":"5px plum ridge", "text-align":"center",
 35:   "display":"block","visibility":"visible"
 36:  }
 37: }
 38: /* ポップアップに使用するCSS値を設定する*/
 39: var popupCSS = $.extend(true,defaultCSS(),options || {});
 40: /* closeBar CSS値も容易に変更できるように設定する */
 41: var closeBarCSS = function(){return $.extend(true,{
 42:  "position":"absolute","zIndex":"1001","text-align":"center",
 43:  "opacity":0.75,"top":0,"left":0,"width":"100%","cursor":"pointer",
 44:  "font-size":"small","lineHeight":"1.2em","background-color":"midnightblue"
 45: },options2 || {})};
jQuery インスタンスの取得(#24)

まず最初にこのプラグインの呼び出し元となる jQuery インスタンスを取得します。

DOMReady 関数内で jQuery インスタンスを呼び出そうとしたのですが、関数内では this は window オブジェクトを参照してしまうため、前もってここで取得しておくことにしました。

表示中の window の幅と高さの取得(#24)

これは jquery.js で定義されているクロスブラウザな便利なメソッドをそのまま活用しました。

これらの値を取得する意味は、画面の中央や左や右にピタリと寄せて配置する場合に利用するためです。

ポップアップ窓のデフォルト CSS 設定(#28-37)

ボーダーやパディング、そして色と背景色──これらの初期値を定めておかないとその都度思案し確定し指定しなければなりません。これはいかにも面倒なので既定値を設定しました。options で特に指定しなければ既定値が適用された popup 窓が表示されるわけです。

実は画像や動画にも対応させるために、最終段階でこのブロックを大きく改変しました。

#29 では正規表現を利用して img タグ、object タグ、embed タグ、あるいは iframe タグ文字列が第一引数 contents に含まれるかどうかをチェックします。そしてそれらのいずれかが含まれる場合には、デフォルト値としての画像サイズを設定しないこととしました。(#33)

これは引用先のCSS設定を反映する場合があったので、敢えて width 値を定めずにおいて、引用先サイトの padding 設定値なども反映した outerWidth 値を取得するためです。

実際に表示するポップアップ窓の CSS 値の指定(#38-39)

これには extend メソッドの2つめの機能である"ボブジェクト拡張"を利用しました。

optionsで任意の CSS 値を与えれば、デフォルトCSSを上書きしてユーザーの要望通りの表現を実現することが出来ます。また ||{} により options が指定されなかった場合にデフォルト値を利用するように工夫しました。

ポップアップ窓上辺の CLOSE バー CSS 値の指定(#40-45)

これも extend メソッドの"ボブジェクト拡張"を利用しました。

options2で任意の CSS 値を与えれば、デフォルト値を上書きしてユーザーの要望通りの表現を実現することが出来ます。また ||{} により options2 が指定されなかった場合にデフォルト値を利用するように工夫しました。

その他の変数(#25-27)

重要な役割を果たすローカル変数をここで定義しました。

addValue は配置位置がピクセル指定された場合のそのピクセル値を格納します。

mousePos はイベント発生要素の近傍にポップアップを配置する場合に必要となるマウスカーソルの現在値を所得するための変数です。

xCenter、yCenter は画面中央に配置するために必要な値で、それぞれ横方向の画面中央位置、縦方向の画面中央位置を取得します。

▲ToTop

第Ⅲブロック:マウス move イベントの登録とエラー対応(#4752)

第2引数 layout を指定しない場合に、マウスカーソル近傍にポップアップさせるためには、マウスの動きを常に Javascriptが 「監視」 しその位置を取得していなければなりません。そのためのイベント登録を行う部分です。これにより document の任意の箇所においてマウスカーソルの動きを常駐監視し、その位置を取得することになります。

 47: // bind onmousemove event on document
 48: mousePos.oX = 4; mousePos.oY = 16; //マウスカーソルからの離隔距離
 49: $(window).mousemove(function(e){
 50:  mousePos.left = (jQuery.browser.msie ? window.event.clientX - document.body.clientLeft : e.clientX) + mousePos.oX;
 51:  mousePos.top = (jQuery.browser.msie ? window.event.clientY - document.body.clientTop : e.clientY) + mousePos.oY;
 52: });
 53:
 54: // error 処理関数
 55: var errFunc = function(){ errFlag=true; return function(){return}}

ここでも IE が、IE だけが特殊な方法を採用しており、そのために面妖な設定をしなければならないのはユーザーにとって不幸なことです。早く IE のユーザー比率が低下することを願ってやみません。IE8 の登場によりその願いはまた叶うことなく先送りされてしまいましたが、中長期的には IE 固有の仕様は消え去る運命にあることは間違いないでしょう。

実際、後のエントリイで触れましたが、IE8 では少なくともスタイル設定に関しては Web 標準準拠に切り替わりました。これは 1 社だけで頑迷に固執し続けてきた奢りが、10年来のブラウザ戦争の中でやっと崩れ去ったことを意味しており、記念すべき歴史的事件と言って差し支えないでしょう。

#54 のエラー対応は「関数を返値とする」特殊な関数を利用します。これにより返値は return 値を有する関数となるので、errFunc()() のようにして、返値である関数を呼び出し先で実行することによりトップレベルにおいて return 値を返すことが可能となります。

ここに最初の括弧は errFunc 関数を実行するため、2つめの括弧は errFunc 関数の返値としての関数を実行するためです。#61 など随所で活用しています。

▲ToTop

第Ⅳブロック:ポップアップ配置位置の設定メソッド(#57-112)

このブロックはたった1つのメソッド登録ですが長大になりました。しかし、様々な指定方法に対応するために必要不可欠な部分であり、長くなってしまったのはエラー処理をふんだんに盛り込んだためでもあります。

#59-62 は最初のエラー処理です。

不適切な layout 指定があった場合の警告表示とコード進行停止を指定しています。

#64-82 は layout が配列だった場合の処理です。

#65-72 で、left、le あるいは l だけでも受容するように指定文字を簡略化できるように工夫し、所定の文字が与えられた場合には、それらを left、center、right、top、bottom の文字列に変換しています。

lcr には left、center、または right の文字が入力され、tcb には top、center、または bottom が代入されます。

また、xName には left か right が、yName には top か bottom が代入されます。

#73-81 は配列指定が適切ではなかった場合のエラー処理です。

#82-107 は layout がオブジェクト指定された場合の処理です。

#83-87では layout オブジェクトを走査して、配置指定に係る定義値を取得し、変数に代入します。捜査の結果必要な文字がなければ #88-91 でエラー処理します。

#92-100 は 配置指定が px 値ではなく "center" などの文字列で行われた場合の処理です。

#93-97 で配列 m に横方向と縦方向の配置指定文字列を代入し、layout が適切な指定でなければ、#97-100 でエラー処理します。

#101-107 では縦横方向の配置指定値を取得します。

二項演算子を活用して、センター配置か否か、及び px 指定値がある場合の2つのケースから値を取得します。

エラー発生時には空オブジェクトを返すようにしました。

#108-110 ではここ迄の処理で確定した配置用変数を活用して CSS 値を設定します。

センター配置の場合のみ画面中央位置を取得し、その他は指定されていればピクセル値を代入します。

▲ToTop

第Ⅴブロック:ポップアップ用 div 要素の作成(#111-134)

ブラウザにとってポップアップ用要素が用意できなければ何も始められないため、これ以降は DOMReady 関数で括ります。

popup 表示用の div 要素タグの作成(#115-120)

2回目以降の animatedPopupOld 起動時に dispElem が重複設置されないように 116 行でこのノードの存在確認を行います。もし存在すれば、改めて変数 disp に当該ノードを参照する jQuery インスタンスを登録します。

存在しない場合には、タグ要素を CSS 設定値を含めて作成し、その後に jQuery インスタンスを作成し変数に代入します。

この CSS 設定では絶対配置、非表示、レイヤー順を指定し、ポップアップを自在に配置できるように、また body 部に追加した時点ではそれが表示されないよう準備します。

popup 隠蔽用×印タグの作成(#123-127)

ポップアップ用 div 要素の右上にこのポップアップを隠蔽するためのバツ印を用意しました。ここでも二重登録しないようにこの要素の存在確認を行います。

更に×印だけではマウスカーソルをその位置にフィットする手間が面倒なので、隠蔽クリックを受け入れる箇所を点から線に拡張して、タイトルバー形式にしました。

▲ToTop

第Ⅵブロック:アニメーション用の諸関数定義(#129-193)

ここ迄でアニメーションポップアップのための準備が終わりました。愈々、動きのあるポップアップ表示や隠蔽操作のための関数を定義します。

ポップアップする要素の高さのサイズを測定する getElemWH 関数

まず第一引数 contents が未指定の場合の対応を 132 行で行いました。未指定の場合には所定の文章( || の右側の文章 )を用意しておき、これを animetedPopup 関数を使ってポップアップします。

文字列をポップアップする場合特に、その要素サイズを「表示前に」如何にブラウザに知らせるかが課題となります。文字列の文字数が固定されていても、文字の横幅は一定ではないので内容によってはサイズは変わりますし、一般にポップアップ表示を行う文字列は長さは不定・可変です。そのためブラウザは、幅が指定されていなければ表示領域一杯の幅が指定されたものと見なし、或いは要素の幅が指定されている場合にはその幅で要素を表示しますが、ブラウザはこれらのいずれの場合においても、要素表示前にはその要素の高さを認識しません。

別のエントリイで不定型ボックスサイズを表示する前にブラウザに認識させる方法について詳細に触れましたが、このプラグインで採用した方法は、自前で考案した透明化してサイズを測定する方法ではなく、jquery.js で利用されている visibility 属性を利用する方法を採用しました。

jquery.js で採用している表示前サイズ計測方法は、1. position : absolute、2. display : block、3. visibility : hidden とすることにより、「絶対配置のブロック表示状態にしてそれを表示させない」状態を作るものです。(jquery.js: css 関数内の #773 で呼び出される #735-748 の swap 関数)

なお、このプラグインで実際に必要となるのは outerHeight と innerHeight なのですが、この際無意味ですが4つのサイズを測定しておくようにしました(苦笑)。

またinnerHeight については、代入式を iH のプロパティ値とすることにより、同時に 2 つのプロパティに値を取得させました。

ポップアップボックスの拡張/縮小を演出する shrinkCSS 関数

アニメーションを演出するために幅と高さが極小のスタイル値を設定します。これによりアニメーションの始点と終点の位置と要素の表示状態を取得します。

またアニメーションの始点/終点を取得するためには、縦横のスクロール状態を Javascript インタープリタに認識させなければなりませんから、この関数内でスクロール値を取得します(#145)。

後述するように、クリック時のイベントハンドラー内から、shrinkCSS 関数を呼び出して、そのときのスクロール値を取得します。

次にこの関数内から #151 で getPos 関数を呼び出していますが、これにより画面内の左右上下中心のどの位置に配置するか、あるいはマウスカーソルの近傍に配置するかを確定します。

エラーフラグが立っている場合にはコード進行を止めます。(#147)

#153-156で right や bottom 指定された場合に left や top に変換しています。

right / bottom 指定された値をleft / top に変換する計算式は、最後の最後まで苦労を重ねました。getPos 関数からの戻り値は、right / bottom 指定のママですが、#153-156 においてこれらを left / top に変換します。この結果 shrinkCSS 関数からの戻り値は left / top のみとなります。

popup 要素をアニメーション隠蔽する hideElem 関数

これはいわば逆アニメーションで、animaElem 関数で表示したポップアップを縮小しながら隠蔽します。最初にコンテンツを削除してから shrinkCSS 関数をよびだし、かつこのアニメーションを待ち行列に登録しないよう queue 値を false にします。

なお、shrinkCSS 関数でエラーが発生する可能性がありますが、hideElem 関数は、必ずanimaElem 関数が走行した後にしか起動されず、animaElem 関数内において shrinkCSS 関数にエラーが発生した場合の処理は記述されています。エラー発生時には hideElem 関数呼び出しまで到達しませんので、当該関数ではエラー処理を必要としません。

アニメーションポップアップ表示を行う animaElem 関数

ここでは次のような様々な処理を行っています。1.登録アニメーションの削除と停止(#165)、2. この関数内で一回だけ shrinkCSS 関数を呼び出し、結果を変数に記録(#166)、この返値が空の場合の処理(#167)3.画面からポップアップをはみ出させない処理(#170-174)、4.画面センターに配置する場合、あるいは right / bottom が指定された場合の、ポップアップサイズと表示位置の調整(#176-180)、4.アニメーションの始点設定(#184)、5.アニメーション表示(#186-188)(ここでも隠蔽処理同様に待ち行列には登録しません。)、そして 6.ポップアップ窓の隠蔽ハンドラー呼び出しです(#190-192)。

ここでの要点は以下の点です。

  • ポップアップ要素を画面外にはみ出させない処理のために必要となる要因は、画面サイズ、スクロール値及びポップアップ要素のサイズです。
  • アニメーション待ち行列の扱いは、全てのアニメーションを非登録としました。当然ですが登録してしまうと、二度目以降の animatedPopupOld() 起動時において、最初以降から直前までの以前に利用したアニメーションが順次起動してしまうためです。
  • 179行で、duration や easing が指定されなかった場合のデフォルト値を設定しました。
  • ポップアップの隠蔽はクリックイベントを登録して行いますが、表示関数の中から行うようにしました。連続してクリックしてポップアップさせた場合に、クリックしてもそれを消せない場合が発生したためです(原因は不明)。

▲ToTop

第Ⅶブロック:クリックイベントの登録(#190)

最後の処理です。要素タグへのクリックイベント登録では、エラー発生時に登録済みクリックイベントを削除するようにしました。ここで最初にして最後ですが errflag 値を利用します。

イベントハンドラー内で使用した $(this) は click が jQInst のメソッドですから、jQInst を参照します。

jQuery の挙動を解読する(35):Traversing メソッドについて(1) ──jQuery解読(52)

何回かに分けて Traversing に分類されているインスタンスメソッドの解読を行います。

全体の項目とこのエントリイの項目
  • eq()、slice()、is()、filter()、closest()、not()、add()
  • children()、contents()、find()、next()、nextAll()
  • offsetParent()、parent()、parents()、prev()、prevAll()、siblings()、andSelf()、end()

このエントリイでは、本家サイトにおいて Traversing に分類されているインスタンスメソッドの内、次のメソッドを解読対象とします。

eq()、slice()、is()、filter()、closest()、not()、add()

eq()、slice() メソッド解読

eq() メソッドはインスタンスに登録されている要素ノードから、i+1 番目の 1 つのノードを取り出します。

そのために使用される slice() メソッドはインスタンス内に登録されている要素ノードから、連続した一部を取り出すためのメソッドです。このメソッドを適用すると、元のインスタンスの一部のプロパティを保持する新たなインスタンスが得られます。このとき、pushStack() メソッドにより、新たなインスタンスの selector プロパティには抽出経過を記録する文字列が代入されます。

■ eq() メソッド……インスタンスの配列 like なプロパティから i 番目の要素を取り出す
495: eq: function( i ) {
496:  return this.slice( i, +i + 1 );
497: }, 

■ slice() メソッド …… インスタンスの配列 like なプロパティからその連続した部分を取り出す
499: slice: function() {
500:  return this.pushStack( Array.prototype.slice.apply( this, arguments ),
501:   "slice", Array.prototype.slice.call(arguments).join(",") );
502: },
このエントリイ上で eq()、slice() を試してみる

左のボタンをクリックすると、最初の pre タグの背景色が teal になり、もう一度このボタンをクリックすると元に戻ります。

左のボタンをクリックすると 6 番目から 9 番目迄の 4 つの p タグ背景色が teal に変わり、もう一度クリックすると元に戻ります。

尚、ボタンをクリックした際には、mousedown 時にそのボタンの背景色が変わり、mouseup すると元に戻るように設定しました。

is()、filter() メソッド解読

is() メソッドは或るノードがインスタンス内に登録されているかどうかを調べます。filter() メソッドはその名の通りインスタンス内に登録されているノードを対象として、一定の条件でフィルタリングを行います。

■ jQuery(a).is(s) …… a にマッチする要素ノード内に s にマッチする要素があるかどうか調査する。
401: is: function( selector ) {
    // 引数 selector が存在し、かつ jQuery インスタンスの中に selector に
    // マッチするノードが 1 つ以上あれば true を、なければ false を返す。
402:  return !!selector && jQuery.multiFilter( selector, this ).length > 0;
403: },

■ jQuery(s,c).filter(selector) メソッド
349: filter: function( selector ) {
    // selector が関数の場合とそうでない場合とに分けて pushStack() メソッ
    // ドの結果を受け取る。
350:  return this.pushStack(
351:   jQuery.isFunction( selector ) && // selector が関数ならば
     // インスタンスの各プロパティに登録されている要素ノードに対して
352:   jQuery.grep(this, function(elem, i){
353:    return selector.call( elem, i ); // 順番に関数を適用し結果を返す。
354:   }) || // あるいは
355:   // 関数でない場合には、multiFilter メソッドを使用して、インスタンス
     // に登録されている要素ノードから selector にマッチする要素を取り出す。
356:   jQuery.multiFilter( selector, jQuery.grep(this, function(elem){
357:    return elem.nodeType === 1;
358:   }) ), "filter", selector );
359: },
このエントリイ上で is()、filter() を試してみる

左のボタンをクリックすると、この p タグの contents に「左のボタン」という文字列があるかどうか調べ、結果を animate 表示 します。当然それは存在しているのでtrueであることを示す文字列がアニメーションポップアップで表示されます。

尚、このボタンは単純なクリックボタンで toggle にはなっていません。表示された popup を消去するにはその popup 内にある×をクリックします。またボタンが複数回クリックされた場合には、popup を即ぐに消し去ってから新たなポップアニメーションが起動します。

また、このボタンだけは IE8 では作動しません。おそらくは IE8 の Animation に関するバグのためだと思われますが、正確な原因は不明です。

左のボタンをクリックすると「 左のボタン 」という文字列を含む全ての p タグ背景色が teal に変わり、もう一度クリックすると元に戻ります。

尚、ボタンをクリックした際には、mousedown 時にそのボタンの背景色が変わり、mouseup すると元に戻るように設定しました。

closest() メソッド解読

これは 1.3.x から追加された新しいメソッドです。jQuery(s,c)にマッチする要素( A )を対象として、closest(selector) を適用すると、A 及びその親ノードの中から、selector フィルタにマッチするノードをを取得します。

要点はインスタンスが参照しているノードからだけではなく、その親ノードからも条件に合致するノードを探す点にあります。イベントオブジェクトがバブリングして親要素にも達するように、このメソッドは親要素をも対象としている点が注目されます。「 closest(近親者)」と名付けられた所以です。

■ jQuery(s,c).closest(selector) メソッド
361: closest: function( selector ) {
    // jQuery.expr.match.POS は /:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^-]|$)/
    // である( #1663 から )。そしてこの正規表現は次のような文字列となる。
    //「 コロンの後に nth から odd までのいずれかの文字列があり、その後に(数字)が 
    // 1 回以下続き、その後に - ではない 1 文字があるか行末まで何もない 」
    // ── selector がこのような文字列の場合には、 jQuery(selector) の
    // 結果を変数 pos に代入し、そうでない場合には null 値を代入する。
362:  var pos = jQuery.expr.match.POS.test( selector ) ? jQuery(selector) : null,
363:   closer = 0;
364:
365:  return this.map(function(){ // インスタンスに map メソッド処理を行って返す。
     // この結果、インスタンスの各要素及びその親ノードの中から、selector 条件に
     // 合致するノードが抽出され、それらをプロパティとする jQuery インスタンス
     // が返される。
366:   var cur = this; // この this は map メソッド内にあるので、
             // インスタンスに登録されている個々の要素ノードを指す。
367:   while ( cur && cur.ownerDocument ) { // cur 及びその document ノードが
      // ある限り、変数 pos があれば cur が pos の何番目かにあるかどうか
      // チェックし、pos がなければ cur にマッチする要素の中に selector
      //  にマッチするものがあるかどうかをチェックし、いずれかが true であれば、
368:    if ( pos ? pos.index(cur) > -1 : jQuery(cur).is(selector) ) {
       // cur 要素に closest と言う名前と closer 値を関連づける。
369:     jQuery.data(cur, "closest", closer);
370:     return cur; // curを返す。
371:    }
372:    cur = cur.parentNode; // cur をその親ノードとし、closer 値を増加する。
373:    closer++;
374:   }
375:  });
376: },

左のボタンをクリックすると「 メソッド 」という文字列を含む全ての p タグ背景色が teal に変わり、もう一度クリックすると元に戻ります。

尚、ボタンをクリックした際には、mousedown 時にそのボタンの背景色が変わり、mouseup すると元に戻るように設定しました。

not()、add() メソッド解読

not メソッドは名前からその機能が推測しにくいメソッドです。is() メソッドは「あるかないか」の真偽値を返しますが、notメソッドの返値は jQuery オブジェクトです。

このメソッドは jQuery インスタンスから、selector で指定された条件を満たさないノードを抽出します。

他方、add() メソッドはその名の通り、jQuery インスタンスに selector 条件に合致するノードを「追加」します。

■ jQuery(s,c).not(selector) メソッド
378: not: function( selector ) {
379:  if ( typeof selector === "string" ) // selector が文字列の時
380:   // test special case where just one selector is passed in
     // isSimple は #33 から /^.[^:#\[\.,]*$/。つまり最初に何か 1 文字があり、
     // 次に : でも、# でも、 [ でも、.でも、 ,でもない文字がゼロ個以上続く
     // 文字列を意味する。つまり、何らかのフィルターでもなく、クラス名でもなく、
     // id でもなく、複数のセレクタでもない単純な文字列を意味する。
381:   if ( isSimple.test( selector ) ) // selector が上のような条件の文字列ならば
      // インスタンスから selector 条件を満足するノードを削除し、その結果を
      // jQuery インスタンスを返す。
382:    return this.pushStack( jQuery.multiFilter( selector, this, true ), "not", selector );
383:   else // 単純ではない文字列だったら、インスタンスから selector 条件に
      // 合致するノードを抽出して selector に代入する。
384:    selector = jQuery.multiFilter( selector, this );
385:  // selector に length 値があって、最後の selector 値が定義されていて、
    // selector がノードではなければ true を返す。
386:  var isArrayLike = selector.length && selector[selector.length - 1] !== undefined && !selector.nodeType;
387:  return this.filter(function() { // インスタンスに登録されている各ノード
     // に対してfilter メソッドにより引数の関数を適用する。filter メソッド
     // 内では grep メソッドが条件に合致するノードだけを抽出する。
     // これにより各ノードが selector に合致しない場合には true が返され、
     // 合致した場合には false が返される。この結果 selctor 条件に合致しない
     // ノードだけが残され、これをプロパティとする jQuery インスタンスが返される。
388:   return isArrayLike ? jQuery.inArray( this, selector ) < 0 : this != selector;
389:  });
390: },

■ jQuery(s,c).add(selector) メソッド
392: add: function( selector ) {
393:  return this.pushStack( jQuery.unique( jQuery.merge(
394:   this.get(), // merge メソッドの第 1 引数は配列でなければならない。
395:   typeof selector === "string" ?
      // merge メソッドは第 2 引数が配列 like なプロパティ値を持つオブ
      // ジェクトであっても、配列そのものでも同様に扱える。
396:    jQuery( selector ) :
397:    jQuery.makeArray( selector )
398:  )));
399: },

jQuery の挙動を解読する(33):DOM Manipulate メソッド解読──jQuery解読(50)

Contents
  • domManip() メソッド解読
  • 各種 DOM Manipulate メソッドの性質と分類
  • 各種 DOM Manipulate メソッドの例を試す
  • 各種 DOM Manipulate メソッドを解読する───親ノード追加系
  • 各種 DOM Manipulate メソッドを解読する───子ノード追加系
  • 各種 DOM Manipulate メソッドを解読する───兄弟ノード追加系
  • 各種 DOM Manipulate メソッドを解読する───置換系

このエントリイでは DOM 要素操作 に係るインスタンスメソッドに焦点を当て、それらのメソッドを解読します。

対象とするメソッドは、domManip()、wrapAll()、wrapInner()、wrap()、append()、prepend()、before()、after() 、appendTo()、prependTo()、insertBefore()、insertAfter()、replaceWith()、replaceAll()、text()、html() です。

domManip()

まず、DOM 走査の基本的な役割を担う domManip() メソッドから解読します。

このメソッドは args 内の html 文字列によってインスタンスにノードを追加する際に、(1) clean メソッドを適用して html 文字列の記述ミスを正し、(2) またブラウザバグを補正すると共に、(3) DocumentFragment を使用することにより script タグをインスタンスに追加する、以上のことを行います。

■ domManip() インスタンスメソッド解読
  // 引数は順に、1:走査対象となるノードを要素とする配列、2:table 有無の指定、
  // 及び 3:適用するメソッドで、返値は jQuery インスタンスである。
514:domManip: function( args, table, callback ) {
515: if ( this[0] ) { // インスタンスに最初の要素があれば
    // 3 つの変数を定義する。まず文書断片( DocumentFragment )を作る。ここに、
    // this[0] が document ノードの時には owwnerDocument 値が null となる
    // ため < || this[0] > が必要となる。いずれにしても document ノードに
    // 文書断片を作成し、それへの参照を fragment 変数に代入する。
516:  var fragment = (this[0].ownerDocument || this[0]).createDocumentFragment(),
     // 引数 args を clean メソッドでチェックし return 値を変数 scripts に代入
     // する。ここに clean メソッド #961 から返される値は配列であり、その要素は
     // args 内に存在していた script タグである。
517:   scripts = jQuery.clean( args, (this[0].ownerDocument || this[0]), fragment ),
     // 上の clean メソッドにより args 配列要素の script タグ以外のタグ内にあった
     // script タグが削除された後に fragmentノードの chileNodes となっている。
     // ( #957 )。その中の第一子ノードを 変数 first に参照させる。
518:   first = fragment.firstChild;
519:  // このブロックは args 内に記述されていた script タグを、インスタンスの各要素
    // ノードから呼び出される実行される callback 関数の引数にするためのもの。
520:  if ( first ) // 第一子ノードが取得できれば
     // インスタンスに格納されている要素ノードの数だけ巡回処理する。
521:   for ( var i = 0, l = this.length; i < l; i++ )
      // root 関数で table ノードの場合の処理を行い、call の第 1 引数とし、
      // this に格納されている要素ノード数が 2 以上か イテレート変数が
      // 1 以上ならば fragment ノードの cloneNode をイベント付きで作成して
522:    callback.call( root(this[i], first), this.length > 1 || i > 0 ?
        // 第 2 引数とし、要素ノード数が 1 以下か i = 0 の時には
        // fragment ノード自体を第 2 引数とする。
523:      fragment.cloneNode(true) : fragment );
524: 
525:  if ( scripts ) // scripts が取得できたらば
     // その 1 つ 1 つを evalScript メソッドで評価する。
526:   jQuery.each( scripts, evalScript );
527: }
528: // 以上の処理を終えた this インスタンスを呼び出し元に返す。
529: return this; // また this[0] がない場合には this をそのまま返す。
530: 
531: function root( elem, cur ) {
    // 引数 table が true で、要素ノード=this[i] が table ノードで
    // cur が tr ノードならば
532:  return table && jQuery.nodeName(elem, "table") && jQuery.nodeName(cur, "tr") ?
     // 取得できた最初の tbody ノードを返すか、取得できなければ tbody 要素ノードを
     // elem ノードに追加して return する。
533:   (elem.getElementsByTagName("tbody")[0] ||
534:   elem.appendChild(elem.ownerDocument.createElement("tbody"))) :
     // その他の場合には elem 自身=this[0] を return する。
535:   elem;
536: }
537:}

▲ToTop

各種 DOM Manipulate メソッドの性質と分類

DOM 操作を行う jQuery インスタンスメソッドは次のように沢山あります。─── $(s,c).wrapAll(html)、$(s,c).wrapInner(html)、$(s,c).wrap(html)、$(s,c).append(elem)、$(s,c).prepend(elem)、$(s,c).before(elem)、$(s,c).after(elem)、$(s,c).appendTo(elem)、$(s,c).prependTo(elem)、$(s,c).insertBefore(elem)、$(s,c).insertAfter(elem)、$(s,c).replaceWith(elem)、$(s,c).replaceAll(elem)、$(s,c).text(str)、$(s,c).html(html) 等々。

これらの 15 個のメソッドの共通点は、jQuery インスタンス内に参照が取得されている要素ノードと、各のメソッドの引数である html や elem 等によって定まるノードとの、二者の組み合わせ方を定めることです。これらのインスタンスメソッドは、2 つの(あるいは 2 つの群の) DOM 要素間の前後関係(つまり兄弟関係)、包含/被包含関係(親子関係)、あるいは置換/被置換関係を定義づけます。

まず、組み合わせの対象となる 2 つ( 2 群 )の位置関係を整理しておくべきでしょう。分かりやすく表示するために、インスタンス内で参照付けられているノード群をA、引数によって参照付けられるノードをBとします。

親ノード追加系
  • $(s,c).wrapAll(html)……A[0]~A[last] までの全体を B で包含する。A[i] = B.childNodes[i]
  • $(s,c).wrapInner(html)……A の子ノード C を B で囲む。B.childNodes =A[i].childNodesとなる。
  • $(s,c).wrap(html)……B を個々の A の親ノードとする。A[i].parentNode = B
子ノード追加系
  • $(s,c).append(elem)……B を個々の Aの最後の子ノードとする。A[i].lastChild = B
  • $(s,c).prepend(elem)……B を個々の A の最初の子ノードとする。A[i].firstChild = B
  • $(s,c).appendTo(elem)……A を B の最後の子ノードとする。B.lastChild = A
  • $(s,c).prependTo(elem)……A を B の最初の子ノードとする。B.firstChild = A
兄弟ノード追加系
  • $(s,c).before(elem)……B を個々の A の直前の兄弟ノードとする。A[i].previousSibling = B
  • $(s,c).after(elem)……B を個々の A の直後の兄弟ノードとする。A[i].nextSibling = B
  • $(s,c).insertBefore(elem)……A を B の直前の兄弟ノードとする。A = B.previousSibling
  • $(s,c).insertAfter(elem)……A を B の直後の兄弟ノードとする。A = B.lastSibling
置換系
  • $(s,c).replaceWith(elem)……A を B で置換する。A[i] → B
  • $(s,c).replaceAll(elem)……B を A で置換する。A[i] ← B
  • $(s,c).text(str)……A の 子要素を B で置換する。A[i].childNodes = B
  • $(s,c).html(html)……A の 子要素を B で置換する。A[i].childNodes = B

▲ToTop

各種 DOM Manipulate メソッドの例を試す

解読の前に各種 DOM Manipulate メソッドの適用例をこのページ上で見てみます。それぞれのメソッドの働き方や差異を見るにはそれが手っ取り早いからです。

適用例の説明
  • 下のボタンは各種 DOM Manipulate メソッドを実行するためのもの。15 のいずれかをクリックすると、ボタンの下にある薄緑色のボックス内で各種メソッドの効果を表示します。
  • メソッドの適用効果を消去するにはもう一度同じボタンをクリックします。
  • メソッドの適用対象は、薄緑色のボックス内に配置した 3 つの p タグです。これをボタン上では $(this) と表示しました。3 つの pタグはピンクの枠で示していますが、それらを対象として各種 DOM Manipulate メソッドを適用します。
  • 他方、働き方を逆転させるメソッドの場合には、メソッド起動要素がサイト内に存在していなければ成りませんので、そのための要素として DIV タグ( class="targetDiv" )を用意しました。

メソッドの引数として次の html 文字列を準備しました。

  • "<div style='background-color:maroon;border:4px dotted gray;margin:2px;padding:2px;'></div>"
  • "<p style='background-color:maroon;border:4px dotted gray;margin:2px;padding:2px;'>これはボタンをクリックしたことにより追加されたノードです。</p>"
  
   
   
   

(第1 pタグ)これは各種 DOM Manipulate メソッドの働き方を視覚的に見やすくしたサンプルです。

(第2 pタグ)このブロックを包含する div タグがあり、その中に複数の 3 つの p タグと 2 つの div タグを配置し、3 つめのパラグラフには画像を配置しました。

(第1 divタグ)3 つの p タグを操作対象として各種のメソッドを適用します。

(第3 pタグ) これは画像を含むパラグラフです。

(第2 divタグ)
逆転メソッドで使用する div タグ

各種 DOM Manipulate メソッドを解読する───親ノード追加系

$(s,c).wrapAll(html) メソッド解読

このメソッドは、jQueryインスタンスが参照するノード全体を、 html 文字列によってサイトに追加されるノードの子ノードにします。

■ $(s,c).wrapAll(html) メソッド
219: wrapAll: function( html ) {
220:  if ( this[0] ) {
221:   // The elements to wrap the target around
     // html 文字列で指定されたノード群を document 内で検索し、マッチ
     // したノードの clone ノードを作り、変数 wrap に代入する。
222:   var wrap = jQuery( html, this[0].ownerDocument ).clone();
223:   // インスタンスの最初のノードに親ノードがあれば、
224:   if ( this[0].parentNode )
      // clone ノード ( wrap ) をインスタンスの、最初の配列もどきプロ
      // パティが参照するノード ( this[0] ) の前に配置する。
225:    wrap.insertBefore( this[0] );
226:
227:   wrap.map(function(){
      // この this は mapメソッド内にあり、map メソッドが wrap から
228:    var elem = this; // 呼び出されているので wrap を参照する。
229:
230:    while ( elem.firstChild ) // 最も深い階層にある最初の
231:     elem = elem.firstChild; // 子ノードを elem に代入し、
232:
233:    return elem; // その最後の子ノードを返す。
     // 返された最深層の最初の子ノードの中に、this を最後の子ノードとして追加する。
     // なおここの this はインスタンスから呼び出される wrapAll メソッド内にある
234:   }).append(this); // ので、インスタンスを参照する。
235:  }
236:
237:  return this; // インスタンスを返す。
238: },
$(s,c).wrapInner(html) 解読

このメソッドはインスタンスの各々の子ノードを html 文字列による追加ノードで包含します。

■ $(s,c).wrapInner(html) メソッド
240: wrapInner: function( html ) {
241:  return this.each(function(){ // インスタンスの各々のノードに対して
     // 当該ノードの子ノード jQuery( this ).contents() を html により
     // 挿入されるノードで包含する。なお、contents() は #1186 で定義
     // されているインスタンスメソッドである。
242:   jQuery( this ).contents().wrapAll( html );
243:  });
244: },
$(s,c).wrap(html) 解読

このメソッドはインスタンスが参照する各々のノードを html 文字列による追加ノードで包含します。

■ $(s,c).wrap(html) メソッド
246: wrap: function( html ) {
    // インスタンスの各々のノードを巡回処理し結果を返す。
247:  return this.each(function(){
     // 各々のノードを html 文字列による追加ノードの子ノードとする。
248:   jQuery( this ).wrapAll( html );
249:  });
250: },

▲ToTop

各種の DOM Manipulate メソッドを解読する───子ノード追加系

子ノードを追加する 最初の 2 つのメソッドでは共通して domManip メソッドが使われています。また、appendTo メソッドと prependTo メソッドはそれぞれ append と prepend メソッドを利用して対象を逆転させているだけですから、結局 4 つのメソッドで domManip メソッドが使われていることになります。

これは、誤記や table に係る IE のバグ対策を施さないと追加により、インスタンスが指し示すノードが文法的におかしくなってしまうことがあるためです。

■ 子ノード追加系 メソッド
▼ append() メソッド
252: append: function() {
253:  return this.domManip(arguments, true, function(elem){
254:   if (this.nodeType == 1)
      // この this は domManip メソッドの定義から、インスタンス内で指し
      // 示されている個々のノードを指す。( #522 )その個々のノードの
      // 最後の子ノードとして elem により定義されるノードを挿入する。
255:    this.appendChild( elem );
256:  });
257: },
258:

▼ prepend() メソッド
259: prepend: function() {
260:  return this.domManip(arguments, true, function(elem){
261:   if (this.nodeType == 1)
      // この this は domManip メソッドの定義から、インスタンス内で指し
      // 示されている個々のノードを指す。( #522 )その個々のノードの
      // 最初の子ノードの前に、elem により定義されるノードを挿入する。
262:    this.insertBefore( elem, this.firstChild );
263:  });
264: },

▼insertBefore() 及び insertAfter() メソッド
// 以下のブロックの解読は別途こちらのエントリイ ( jQuery の挙動を解読する(31):
jQuery.each({ nameN:fnN(){}},function(){}) によるメソッド登録──jQuery解読(48) )
で行いました。
1198:jQuery.each({
1199: appendTo: "append",
1200: prependTo: "prepend",
1201: insertBefore: "before",
1202: insertAfter: "after",
1203: replaceAll: "replaceWith"
1204:}, function(name, original){
1205: jQuery.fn[ name ] = function( selector ) {
1206:  var ret = [], insert = jQuery( selector );
1207:
1208:  for ( var i = 0, l = insert.length; i < l; i++ ) {
1209:   var elems = (i > 0 ? this.clone(true) : this).get();
1210:   jQuery.fn[ original ].apply( jQuery(insert[i]), elems );
1211:   ret = ret.concat( elems );
1212:  }
1213:
1214:  return this.pushStack( ret, name, selector );
1215: };
1216:});

▲ToTop

各種の DOM Manipulate メソッドを解読する───兄弟ノード追加系

兄弟ノードを追加する最初の 2 つのメソッドでも共通して domManip メソッドが使われ、その中では clean メソッドが呼び出されます。また、insertBefore メソッドと insertAfter メソッドはそれぞれ before と after メソッドを利用して対象を逆転させているだけですから、結局 4 つのメソッドで domManip メソッドが使われていることになります。

このような回り道をするのは、clean メソッドを使って誤記や table に係る IE のバグ対策を施さないと、ノード追加によって、インスタンスが指し示すノードが文法的におかしくなってしまうことがあるためです。

■ 兄弟ノード追加系 メソッド
▼ before() メソッド
266: before: function() {
267:  return this.domManip(arguments, false, function(elem){
     // この this は domManip メソッドの定義から、インスタンス内で指し
     // 示されている個々のノードを指す。( #522 )その個々のノードの
     // 前に、elem により定義されるノードを兄弟要素として挿入する。
268:   this.parentNode.insertBefore( elem, this );
269:  });
270: },
271:

▼ after() メソッド
272: after: function() {
273:  return this.domManip(arguments, false, function(elem){
     // この this は domManip メソッドの定義から、インスタンス内で指し
     // 示されている個々のノードを指す。( #522 )その個々のノードの
     // 次の兄弟ノードの前に、elem により定義されるノードを挿入する。
     // つまり個々のノードの後ろに elem を兄弟ノードとして挿入する。
274:   this.parentNode.insertBefore( elem, this.nextSibling );
275:  });
276: },
// insertBefore と insertAfter の 2 つのメソッドに関しては、直上の
// 「子ノード追加系」でリストアップした #1198-1216 を参照のこと。

▲ToTop

各種の DOM Manipulate メソッドを解読する───置換系

replaceWith(elem) メソッドは極めて簡単なコードで定義されています。インスタンスが指し示す個々の要素ノードの直後に兄弟要素 elem を追加してから、当該要素ノードをを削除します。

■ 置換系 メソッド
▼ replaceWith() メソッド
491: replaceWith: function( value ) {
492:  return this.after( value ).remove();
493: },

▼ replaceAll() メソッド
// replaceAll メソッドに関しては、こちらを参照のこと。

▼ text() メソッド
201: text: function( text ) {
    // 引数がオブジェクトでもなく空でもなければ
202:  if ( typeof text !== "object" && text != null )
     // インスタンスが指し示す個々の要素の子ノードを削除してから、
     // 0 プロパティが存在したら document ノードにテキストノードを作り、
     // それを個々の要素の子ノードとして追加する。
203:   return this.empty().append( (this[0] && this[0].ownerDocument || document).createTextNode( text ) );
204:  // 引数がオブジェクトか、空文字のとき
205:  var ret = ""; // 空文字を変数 ret に代入
206:
207:  jQuery.each( text || this, function(){ // 
208:   jQuery.each( this.childNodes, function(){ // 子ノードを巡回処理
209:    if ( this.nodeType != 8 ) // 子ノードがコメントノードでない場合
210:     ret += this.nodeType != 1 ? // 子ノードが要素ノードでなければ
211:      this.nodeValue : // その値、つまりテキスト文字列を ret に代入
        // 子ノードが要素ノードの場合 this を配列の要素に入れて
212:      jQuery.fn.text( [ this ] ); //text メソッドを再帰呼び出しする。
213:   });
214:  });
215:
216:  return ret; // 文字列 ret を返す。
217: },

▼ html() メソッド
483: html: function( value ) {
484:  return value === undefined ? // value が未定義ならば
485:   (this[0] ? // 最初の配列もどきプロパティ 0 が存在すれば
      // プロパティ 0 の innerHTML 文字列内に変数 expando による
      // 「半角スペース + "jQuery" + 数字 = 数字」文字列があれば
      // それを削除してから、innerHTML 文字列を返値とする。
      // プロパティ 0 が存在しなければ null を返す。
486:    this[0].innerHTML.replace(/ jQuery\d+="(?:\d+|null)"/g, "") :
487:    null) :
     // value が存在すれば個々の要素の子ノードを空にしてから、
     // value を子ノードとして追加する。
488:   this.empty().append( value );
489: },



・・・・・

jQuery の挙動を解読する(31):jQuery.each({ nameN:fnN(){}},function(){}) によるメソッド登録──jQuery解読(48)

$.each( { nameN,funcN }, fn ) 形式によるメソッドの一括登録

jquery.js #1177-1263 では $.each クラスメソッドを利用して、 fn の内容別に 3 つに分けて各種メソッドの一括登録が行われています。

この方法によるメソッド登録は他にも、2 箇所 ( #2888-2892、 #3954-3964 )ありますが、今回対象とする 3 ブロックのメソッド一括登録は、jQuery prototype オブジェクト、すなわちインスタンスメソッドの登録である点において、他の 2 箇所での使用方法と一線を画しています。

さて、#1177-1263 では obj オブジェクトプロパティとして、複数のメソッド名とメソッドを定義し、それらを巡回処理により順に jQuery インスタンスメソッドとして登録しています。まず #1177-1196 では トラバース系の DOM メソッドを、続く #1198-1216 では 挿入置換系の DOM メソッドを、#1218-1263 では属性処理 DOM メソッドを、それぞれ定義しています。

ここに、これらは共通して $.each( obj,fn ) メソッド実行コードなので、jquery.js 読み込み時に実行され、こうして obj オブジェクト内にリストアップされている各種メソッドは、プロトタイプオブジェクトに登録され jQuery インスタンスメソッドとなります。

以下に、これらの 3 つの $.each({nameN,funcN},fn) 形式によるインスタンスメソッド一括登録コードを解読します。

DOM トラバース系のインスタンスメソッド登録

次のコードは、親子、兄弟姉妹、前後などの要素ノードを扱うインスタンスメソッドを一括してプロトタイプオブジェクトに登録するものです。

   // #1177-1187 は「登録するメソッドリスト」をプロパティとするオブジェクト
1177:jQuery.each({ // $.dir、.nth、.sibling は Sizzle 内で定義されている。
1178: parent: function(elem){return elem.parentNode;},
1179: parents: function(elem){return jQuery.dir(elem,"parentNode");},
1180: next: function(elem){return jQuery.nth(elem,2,"nextSibling");},
1181: prev: function(elem){return jQuery.nth(elem,2,"previousSibling");},
1182: nextAll: function(elem){return jQuery.dir(elem,"nextSibling");},
1183: prevAll: function(elem){return jQuery.dir(elem,"previousSibling");},
1184: siblings: function(elem){return jQuery.sibling(elem.parentNode.firstChild,elem);},
1185: children: function(elem){return jQuery.sibling(elem.firstChild);},
1186: contents: function(elem){return jQuery.nodeName(elem,"iframe")?elem.contentDocument||elem.contentWindow.document:jQuery.makeArray(elem.childNodes);}
1187:}, function(name, fn){// name はリスト内のメソッド名称、fn はリスト内の関数を指す
    // プロトタイプオブジェクトに function name(selector){・・・ } を登録する。 
1188: jQuery.fn[ name ] = function( selector ) { // selector は name メソッドの引数
     // インスタンス this に fn メソッドを map する。
     // 例えばイテレート処理の最初には、parent プロパティが対象となるが、
     // map メソッドの定義から ret は this.length 個の this[i].parentNode 
     // を要素とする配列となる。
1189:  var ret = jQuery.map( this, fn );
1190:  // もし selector が文字型の場合
1191:  if ( selector && typeof selector == "string" )
      // ノードを要素とする配列 ret から selector 条件に
      // 合致するノードを抽出する。
1192:   ret = jQuery.multiFilter( selector, ret );
1193:  // 重複チェックを掛けて jQuery オブジェクトとして return
     // する。このとき this インスタンスは return オブジェクトの
     // prevObject プロパティに保持する。
1194:  return this.pushStack( jQuery.unique( ret ), name, selector );
1195: };
1196:});

以下には、上の each メソッド第 1 引数オブジェクトのプロパティ内で、繰り返し登場する Sizzle ユーティリティ内にある jQuery クラスメソッドを簡単に解読しました。

なお、これらの 3 つのコードは全て jQuery クラスメソッドの登録ですが、興味深いことに extend メソッドを使わずメソッドを直接 jQuery オブジェクトに登録する「 素朴な 」方法を採用しています。

■ jQuery.multiFilter
2383: jQuery.multiFilter = function( expr, elems, not ) {
2384:  if ( not ) {
2385:   expr = ":not(" + expr + ")"; // "not(expr)"文字列を生成して代入
2386:  }
2387:  // 要素ノード elems から expr に合致する要素を抽出し return 値とする。
2388:  return Sizzle.matches(expr, elems);
2389: };
■ jQuery.dir
2391: jQuery.dir = function( elem, dir ){
2392:  var matched = [], cur = elem[dir];
2393:  while ( cur && cur != document ) { // elem の dir プロパティを
2394:   if ( cur.nodeType == 1 ) // 走査し、それが要素ノードであれば
2395:    matched.push( cur );  // matched 配列に格納する。
     // cur の dir プロパティを cur に代入してloopを繰り返す。
     // これにより cur ノードの子ノードがあれば、それが走査対象となる
2396:   cur = cur[dir];
2397:  }
2398:  return matched; // 該当要素ノードを格納した配列を返す。
2399: };
■ jQuery.nth
2401: jQuery.nth = function(cur, result, dir, elem){
2402:  result = result || 1;
2403:  var num = 0;
2404:  // cur がある限りその dir プロパティを走査する
2405:  for ( ; cur; cur = cur[dir] ) // cur が要素ノードで num 値が
2406:   if ( cur.nodeType == 1 && ++num == result ) // result に等しくなるまで
2407:    break; // loopする。
2408: 
2409:  return cur;  // 該当した要素ノードを返す。
2410:
 };

▲ToTop

挿入置換系の DOM インスタンスメソッド

2 つめは、登録済みの jQuery インスタンスメソッドを使って、その機能を逆転させるためのメソッド群を一括登録するコードです。

1198: jQuery.each({
1199:  appendTo: "append",
1200:  prependTo: "prepend",
1201:  insertBefore: "before",
1202:  insertAfter: "after",
1203:  replaceAll: "replaceWith"
    // name はプロパティ名、original はプロパティ値
1204: }, function(name, original){
1205:  jQuery.fn[ name ] = function( selector ) { // selector は name メソッドの引数
      // selector に合致するノードを insert jQuery オブジェクトに代入
1206:   var ret = [], insert = jQuery( selector );
1207:    // 配列のようなプロパティを走査
1208:   for ( var i = 0, l = insert.length; i < l; i++ ) {
       // 要素があれば clone をイベント付きで作ってその、なければ this の
       // 要素を配列として取り出す。
1209:    var elems = (i > 0 ? this.clone(true) : this).get();
       // jQuery(insert[i]).original( elems ) メソッドを起動。
       // これにより insert[i] ノードに elems が追加される。
1210:    jQuery.fn[ original ].apply( jQuery(insert[i]), elems );
1211:    ret = ret.concat( elems );  // elems を ret 配列に線形化して結合する。
1212:   }
1213:    // ret jQuery オブジェクトを返し、その prevObject プロパティに this を保持する。
1214:   return this.pushStack( ret, name, selector );
1215:  };
1216: });

▲ToTop

属性処理系の DOM インスタンスメソッド

最後に、タグ属性を操作するメソッド群の登録です。

1218:jQuery.each({
1219: removeAttr: function( name ) {
1220:  jQuery.attr( this, name, "" );
1221:  if (this.nodeType == 1)
1222:   this.removeAttribute( name );
1223: },
1224:
1225: addClass: function( classNames ) {
1226:  jQuery.className.add( this, classNames );
1227: },
1228:
1229: removeClass: function( classNames ) {
1230:  jQuery.className.remove( this, classNames );
1231: },
1232:
1233: toggleClass: function( classNames, state ) {
1234:  if( typeof state !== "boolean" )
1235:   state = !jQuery.className.has( this, classNames );
1236:  jQuery.className[ state ? "add" : "remove" ]( this, classNames );
1237: },
1238:
1239: remove: function( selector ) {
1240:  if ( !selector || jQuery.filter( selector, [ this ] ).length ) {
1241:   // Prevent memory leaks
1242:   jQuery( "*", this ).add([this]).each(function(){
1243:    jQuery.event.remove(this);
1244:    jQuery.removeData(this);
1245:   });
1246:   if (this.parentNode)
1247:    this.parentNode.removeChild( this );
1248:  }
1249: },
1250:
1251: empty: function() {
1252:  // Remove element nodes and prevent memory leaks
1253:  jQuery(this).children().remove();
1254:
1255:  // Remove any remaining nodes
1256:  while ( this.firstChild )
1257:   this.removeChild( this.firstChild );
1258: }
1259:}, function(name, fn){
1260: jQuery.fn[ name ] = function(){ // prototype オブジェクトに登録する。
     // each メソッドの定義から次のように読み解けます。
     // jQuery インスタンス(this) の各プロパティに fn(arguments) メソッド
     // を登録し、処理後の this インスタンスを返値とする。例えば、
     // name が remove の時には、remove.apply(this,arguments) となる。
1261:  return this.each( fn, arguments );
1262: };
1263:});
1264:

jQuery が 1.3.1 に、そして FireFox は 3.0.6 へ

jQuery が 1.3.1 へバージョンアップ

2008/5/24 に 1.2.6 が公開されてから 8 ヶ月を経て、恒例の誕生月におけるメジャーバージョンアップ版 1.3 が 2009/1/14 に公開され、その 1 週間後の 1/21 にバグフィックスを施した 1.3.1 が公開された。

2006/8/26 に Ver 1.0、2007/1/14 に Ver 1.1、2008/1/15 に Ver 1.2.2、そして2009/1/14 に Ver 1.3───ほぼ 1 年毎に jQuery は 0.1 ずつバージョン番号を上げている。

今年もそうなるだろうと思っていたら案の定、誕生日の 1/14 に 1.3 がいわば強引にリリースされ、追いかけるようにバグフィックス版である 1.3.1 が公開されたのである。

さて、最も必要な情報は変更点である。既に多くのサイトで報じられているが、私にとって気になった変更点を列挙すると以下の通りだ。

  • CSS セレクタ Sizzle の搭載……より高速なCSSセレクタだそうだ。
  • Live イベント……類似要素へのイベント登録を代表する1つの要素にバインドするだけで済ますことが出来るようだ。
  • offset処理の高速化……これはページの表示やコンテンツ移動の高速化に大きく寄与すると思う。
  • HTMLへの要素追加の高速化

Firefox が 3.0.6 へアップ

3.0 はこれで 6 回目のマイナーバージョンアップだ。ベータ2 迄到達している 3.1 版の登場も間近だと思われるが、未対応アドオンがたくさんあると思われるので、当面は 3.0 版を使い続けることになる。

3.1 では Javascript が更に高速になり、あの chrome を越えるそうなので、早く登場して欲しいと切望している中での、暫定的対応と言えよう。

▲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が開きます。

----------
201101130146
201012082037
201011250125
201011220020
201011120654
201008080834
201007272344
201007150038
201007090028
201002150132
200910082246
200909070032
200908161701
200907201110
200905010758
200904060045
200904050150
200903300116
200903192026
200902090114
FC2 Management
上記広告は1ヶ月以上更新のないブログに表示されています。新しい記事を書くことで広告を消せます。