PhotoSwipeとInfinite Scrollを共存させる
LightBox系のプラグインとしては、非常に多機能でインタラクションも秀逸な「PhotoSwipe」。
PhotoSwipe http://photoswipe.com/
非jQueryという点もポイントになりますが、 反面、ちょっとピーキーだったり、導入がちょっと面倒だったり、jQueryしか使ったことが無い方には少し敷居がある感じです。(※jQuery版もあるようですが)
今回、そんなPhotoSwipeでギャラリーページをつくる際のちょっとした勘所として『PhotoSwipeとInfinite Scrollを共存させる』を書かせていただきます。
以前、クライアントワークでギャラリーページの作成があり、画像拡大を「PhotoSwipe」で行うことにしました。しかし、ギャラリーというと…
・今後どんどん画像は増えていくことでしょう。
・1ページ内にあまりに画像が大量にあったり、ページをめくっていくのもこのご時世、面倒です。
じゃあ、やっぱりInfinite Scrollで追加読み込みしていくパターンでしょう!
と、作成しました。その時のお話です。
「とりたてて書くことか?」と思われる方もいらっしゃるところかもしれませんが、実はこの二つの組み合わせにはちょっと難点があり、 Stack Overflowなどでも頭を抱えている方が多かったりします。
最初に、PhotoSwipeの特徴とInfinite Scrollについて
PhotoSwipeの使い方はDocumentationのGetting Startedにあるので割愛しますが、動作するために結構長いコードが必要になります。ここがほとんどのjQueryプラグインとの違いであり、敷居を感じさせるところではないかなと思います。
(※具体的には上記リンク先の最後、”How to build an array of slides from a list of links“の項目にあるコード)
PhotoSwipeのポイント
・wrapperに指定した要素以下に、決まったDOMしか受け付けない
PhotoSwipeでは(上記リンクのコード最後に記述されている)initPhotoSwipeFromDOM('selector')
に指定した要素をwrapperとし、
その子要素に対して、PhotoSwipeのイベントを定義しています。
その為、initPhotoSwipeFromDOM('selector')
以下に、「決まったDOMしか受け付けません」。
具体的には、figure>a>img
のDOM構造を持った子要素です。
<figure> <a href="large_img.jpg" data-size="..x..."> <img src="img.jpg" alt=""> </a> <!-- ここはあっても無くても問題なし <figcaption> description... </figcaption> --> </figure>
この構造以外のDOMが入るとTypeErrorを返して動きません。気難しい子です…
その為、Masonryなどのグリッドレイアウトをサポートするプラグインの一部と非常に相性が悪く、 使い所が少し限られている感もあります。
・リロード的なメソッドが用意されていない
その為、動的に要素を読み込んだ際などに、「後から読み込んだ要素に対してのみ」再度イベント付与する方法がありません。
つまり、Infinite ScrollやAjaxとも相性が悪い、ということになります。Stack Overflowを見ても、ここに頭を抱えていらっしゃる方が多くいらっしゃるようです。
しかし、実はとっても簡単な方法でこの二つを共存させることができます。
Infinite Scrollのポイント
今更説明の必要も無いほど有名なプラグインで、ページ送りを行わず、「スクロールに合わせてどんどんコンテンツを読み込んでいく」プラグインですね。
具体的には
$(function(){ $(selector).infinitescrol({ navSelector: '.next', nextSelector: '.next a', itemSelector: '.item' }); });
のように指定した
nextSelectorに指定したリンク先(にある’selector’)から、’selector’に記述した要素内へ
itemSelectorに指定した要素を読み込んで追加します。
(※navSelectorへ指定した要素の位置がInfinite Scroll発火位置)
またInfinite Scrollは追加読み込みをする際、infinite Scrollを付与した要素に対してdiv#infscr-loading
というDOM要素をappendします。
つまり、この仕様がPhotoSwipeの『wrapperに指定した要素以下に、決まったDOMしか受け付けない』という仕様とバッティングします。
もちろん、ここまで書いているからには回避策があります。
また、このコンテンツの追加読み込みは都度Ajaxで読み込んでくる為、jQueryベースでやっていると多くの場合、追加したコンテンツにイベントを再付与する必要があったりします。
しかし、そもそも非同期で読み込んで来たコンテンツに対して、必ずしも都度イベントを付与しなくてもよいのです。(そういう方法があります)
解答編
1.PhotoSwipeFromDOMの直下に、決まったDOM以外を入れなければいい
それができないから苦労している!なんて声も返ってきそうですが 心配ご無用。
前述の通り、PhotoSwipeは
figure>a>img
のDOM構造を持った子要素以外がselector内に入るとTypeErrorを返す。- Infinite Scrollではコンテンツ読み込み時に
div#infscr-loading
が追加されてしまい、figure>a>img
以外のDOM構造の子要素が生まれてしまう=追加読み込みをするとエラーで動作を停止してしまう
しかし、Infinite Scrollのオプションに、このdiv#infscr-loading
の追加先を指定することができるオプションが存在します。
$(selector).infinitescroll({ loading: { selector: '#pager' }, // 省略 });
このloadingオプションのselectorを指定すると、その指定した要素へdiv#infscr-loading
以下のDOMを追加するようになります。
ここで、PhotoSwipeFromDOM
に指定した要素以外へappendすることができるようになるということです。
例として、このような感じのhtmlで
<sectioin id="gallery" class="photo-gallery"> <figure id="photo1" class="photo"> <a href="large_img.jpg" data-size="..x..."> <img src="img.jpg" alt=""> </a> </figure> <figure id="photo2" class="photo"> <a href="large_img.jpg" data-size="..x..."> <img src="img.jpg" alt=""> </a> </figure> <figure id="photo3" class="photo"> <a href="large_img.jpg" data-size="..x..."> <img src="img.jpg" alt=""> </a> </figure> <figure id="photo4" class="photo"> <a href="large_img.jpg" data-size="..x..."> <img src="img.jpg" alt=""> </a> </figure> </section> <nav id="pager"> <ul> <li class="prev"> <a href=""></a> </li> <li class="next"> <a href=""></a> </li> </ul> </nav>
var initPhotoSwipeFromDOM = function(gallerySelector) { // 省略 }; initPhotoSwipeFromDOM('.photo-gallery') $('#garelly').infinitescroll({ loading: { selector: '#pager' }, navSelector: '#pager', nextSelector: '#pager a:first', itemSelector : '.photo' });
としてやると、div#infscr-loading
は#garellyの外にある#pager内へappendされることになり、#garellyにはPhotoSwipeに対応したDOM以外入り込みません。
これで何度追加読み込みが発生しても、PhotoSwipeのエラーで動作が止まることはありません。
2.そもそもリロードメソッドもイベント再付与も必要無い
「え?」と思われる方もいらっしゃるかもしれません。
Ajaxで読み込んできたコンテンツには、必ず都度イベントを付与しなければならないと考えている方も多いでしょう。また、例えば今回のPhotoSwipeのようなライトボックス系プラグインで、リロードメソッドが無い場合、普通にcallすると元々あったコンテンツにイベントが重複してバグが発生するか、エラーで動作を止めてしまうのが関の山。しかし、何らかのかたちでイベントを付与しなければAjaxで読みこんてきたコンテンツをクリックしようと何も起こらない…
確かに、事実そういったケースに向き合わなければならなくなることがほとんどでしょう。
しかし、イベントの定義方法をちょっと変えてやることで解消できます。
JavaScriptには「イベントバブリング(/キャプチャリング)」というものがあります。
簡単に言うと、イベントは伝播するということです。
よく、親要素と子要素それぞれにクリックイベントを付与した際などに両方のイベントが同時発生したりするアレです。
イベントバブリング/キャプチャリングについてはどちらかと言えばネガティブな情報が多かったりしますが、この性質を理解していると Ajaxとの組み合わせ時のスマートなイベントハンドリングやイベントリスナの節約(=メモリの節約)にもなります。
親要素にイベント定義(例えばクリックイベント)してあると、その要素内で(子要素をクリックなどの)イベントが発生すると、それを監視することができます。
この時、そのイベントはevent.targetとして取得できるため、targetが「どの要素か?」などで条件分岐させることも可能であり、細かなハンドリングも可能です。
また、『event.targetがfooというクラスを持っていたら』といった条件にすれば、親要素以下の子要素がスクリプトロード以後に増えようが、子要素として追加された時点でイベントの対象となっている為、問題になりません。
Event.target
https://developer.mozilla.org/ja/docs/Web/API/Event/target
つまり、
上位ノードでイベントを定義しておけば
いちいち一つ一つの要素に対して直接イベントを定義する必要がありません。
PhotoSwipeはPhotoSwipeFromDOM
の引数に渡した要素でイベントを定義します。
つまり、引数に渡した要素に子要素が後からいくら増えても、問題なく動作します。
このイベント付与方法は、ページのリフレッシュを行わないpjaxなどの技術を使った時に、安定性や動作速度など大きな差を生みます。
まとめ
- Infinite ScrollのloadingオプションにPhotoSwipeのwrapper以外の要素を定義
- PhotoSwipeはwrapper要素でイベント定義がされている為、リロードやイベントの再付与は不要
Comments