jQuery.viewportまたは画面上のアイテムの検索方法



すべての女の子が「小さな黒いドレス」を持っているのと同じように、すべてのフロントエンド開発者は「小さな黒いプラグイン」を持っている必要があります...どういうわけかそれはあまりよく聞こえません、「小さな機能的なプラグイン」があるので、私は何を言っているのか、つまり私はそのようなものを共有したいこと。



提示されたプラグインを使用すると、表示領域に対する要素/要素セットの位置を決定できます。 機能的には、疑似セレクターのセットを拡張し、要素トラッカーも追加します。



また、カットの下で、プラグインを作成するプロセス、どのような困難に遭遇したかなどについて話します。あなたに興味があれば、カットの下で歓迎されるようお願いします。



私の直前に問題が発生し、スコープ内にエレメントが出現した瞬間にエレメントをキャッチして処理しました。さらに、スコープは常に画面全体ではなく、 overflow: auto;



あるブロックである場合がありますoverflow: auto;



、画面全体に表示される場合にのみ要素を処理する必要があり、さらにすべての方向(垂直または/および水平)に要素をスクロールする必要があります。

準備ができたものを探してグーグルに行きましたが、驚いたことに、私のニーズを完全に満たすものが見つかりませんでした、またはここでのようにタスクが部分的に解決されました (正直に言って、擬似セレクタを拡張するというアイデアを盗みました、しかし、 この写真は嘘ではありませんか?)、またはプラグインはそれについてまったくありませんでした。 だから私は自分で書く必要があるという事実に直面したので、このビジネスをgithubで共有することにしました。そして実際に、この記事を書くことにしました。実際に、 有名人になるという私の計画の最初の2つのポイントを完了しました。



開発プロセスに興味がない場合は、 こちら >>を突いてください-そしてすぐに、彼らがそれを配布している場所について説明します。



プロローグ


jQuery用のプラグインの作成について知らないが、実際にこれを行う方法を学びたい場合は、まずこの記事を読むことを強くお勧めします。すべてがわかりやすく、理解できるものです(少なくともJSとJQの基本的な知識があると仮定します)。






更新1(2014年10月13日)


-プラグインの一部が書き直されました。githubの変更を参照してください。

-解決策がまだ見つからない問題が発見されました。

親に制限要因がない場合(パディング、ボーダー、オーバーフロー!=可視)、マージンは内側の要素から外側の要素に移動し、親要素のoffsetHeightは子孫のマージンを考慮せずに計算されますが、scrollHeightは高さを正しく決定します子要素のマージンを考慮します。 その結果、そのような親要素はスクロールを持つものとして定義されます。 コンテンツの高さ<要素自体の高さ。





更新2(2014年10月16日)


-上記の問題の解決策として、ビューポートセレクターを追跡するように設定する機能(githubの詳細)が追加されました。

-プラグインの速度が大幅に向上しました。








始めましょう



そのため、タスク:スコープに対する要素の位置を何らかの方法で決定する必要があります。コンテキストによっては、スコープはブラウザウィンドウだけでなく、スクロールする小さな要素にもなります。



スコープとは何ですか?


このコンテキストのスコープを定義することから始めましょう。

私のタスクでは、まずプラグインを書いたときに、ニーズを満たすために、スコープはスクロールする最も近い親です。



残念ながら、スクロールバーがあるかどうかを判断するための保証されたクロスブラウザの方法はありません(少なくともそれについては知りません)。さらに、カスタムスクロールバーを使用し、適切にスタイル設定できますが、コンテナにoverflow: hidden;



が適用さoverflow: hidden;



その結果、ストックスクロールバーが非表示になります。

しかし、方法はありますが、コンテナの高さ( containerElem.offsetHeight



)とそのコンテンツの高さ( containerElem.offsetHeight



)を比較することができ、コンテンツの高さがコンテナの高さを超える場合、ほとんどの場合、しかし、私のプロジェクトには常にスクロールがあります。

このケースをコードにします:

 (function( $ ) { //  ,       var methods = { //            haveScroll: function() { return this.scrollHeight > this.offsetHeight || this.scrollWidth > this.offsetWidth; } }; $.extend( $.expr[':'], { //    ':'  ,     ":have-scroll" "have-scroll": function( obj ) { return methods['haveScroll'].call( obj ); //  .call()      } } ); })( jQuery );
      
      





これ以降、.is( ":have-scroll")を使用して、要素にスクロール(またはその存在の前提)があるかどうかを判断できます。



アイテムの配置


次のステップは、スコープに関連する関心のあるブロックの位置です。

頭に浮かぶ最初のもの:

 top = $( element ).offset().top; left = $( element ).offset().left;
      
      



ただし、 .offset()



はブラウザウィンドウの左上隅に関連して要素を配置します。また、 .offset()



述べたように、スコープは常にブラウザウィンドウではないため、適合しません。チェックします。



頭に浮かぶ2番目のこと:

 top = $( element ).position().top; left = $( element ).position().left;
      
      



いいえ、 .position()



は、最も近い親の左上隅に対してのみ要素を配置します。そうであるように見えますが、構造を考慮してください。
 <div id="viewport"> <div class="element"> <span></span> </div> </div>
      
      



そして、タスクは#viewport



に関連するspan



正確に追跡することです。その場合、 .position()



は、私たちに合わない.element



関連してspan



を配置しspan







解決策は独自のメソッドになり、すべての親をこのコンテキストの範囲までDOMツリーまでバイパスします。
 getFromTop: function() { var fromTop = 0; for( var obj = $( this ).get( 0 ); obj && !$( obj ).is( ':have-scroll' ); obj = obj.offsetParent ) { fromTop += obj.offsetTop; } return Math.round( fromTop ); }
      
      



なぜ$( this ).get( 0 ).offsetTop



あり、 $( this ).position().top



はないのですか? -一部は尋ねます。

これには2つの理由があります。

同様の方法を追加して、左端から位置を決定します。 その後、良い考えがあり、それらを1つの方法に結合することもできます。これは、DOMツリーウォークの半分の数になることがわかったので、それを終了します。



そのため、追跡されたオブジェクトがスコープに対してどこにあるかがわかりましたが、受信したデータはスクロールしても変更されません.scrollTop()



および.scrollLeft()



が役立ちます。それぞれ、垂直および水平スクロールの値を取得できます。

さらに、追跡対象ブロックのすべての辺の位置と可視領域のサイズを知る必要があります。

次の方法で発行します:

 getElementPosition: function() { var _scrollableParent = $( this ).parents( ':have-scroll' ); //     , .parents() ,    .closest(), span, ,       . if( !_scrollableParent.length ) { // ,     . return false; } var _topBorder = methods['getFromTop'].call( this ) - _scrollableParent.scrollTop(); //             var _leftBorder = methods['getFromLeft'].call( this ) - _scrollableParent.scrollLeft(); //      return { "elemTopBorder": _topBorder, "elemBottomBorder": _topBorder + $( this ).height(), //   ,   =  +   "elemLeftBorder": _leftBorder, "elemRightBorder": _leftBorder + $( this ).width(), "viewport": _scrollableParent, "viewportHeight": _scrollableParent.height(), //     "viewportWidth": _scrollableParent.width() //     }; }
      
      





この関数はハッシュテーブルを返します。後で使用する方が便利です。 すべてのブロックの相対的な位置を把握しました。



それでも、上または下


コードに移動します。
 aboveTheViewport: function( threshold ) { var _threshold = typeof threshold == 'string' ? parseInt( threshold, 10 ) : 0; var pos = methods['getElementPosition'].call( this ); return pos ? pos.elemTopBorder - _threshold < 0 : false; }
      
      





ここでは、すべてが明確で、しきい値と厳密な少数派について明確にする唯一のことだと思います。

しきい値-スコープの端からインデントを設定するパラメーター。一部のタスクでは、オブジェクトがスコープに入る少し前または少し後で処理する必要がある場合があります。



境界が一致する場合、要素はまだ境界を越えておらず、これまでのところ可視領域に収まっている、つまり内部にあるという理由で、厳密な少数派が示されます。

また、スコープ内に部分的にとどまるためには、もう少し複雑ですが、まだ単純です。 ちょうど今回は、対応する境界線がスコープを超えており、反対の境界線がまだスコープ内にあることを既に確認しています。
 partlyAboveTheViewport: function( threshold ) { var _threshold = typeof threshold == 'string' ? parseInt( threshold, 10 ) : 0; var pos = methods['getElementPosition'].call( this ); return pos ? pos.elemTopBorder - _threshold < 0 && pos.elemBottomBorder - _threshold >= 0 : false; }
      
      







要素がスコープ内にあるかどうかをチェックするメソッドのコードを除き、残りの境界のチェックを記述することは意味がありません。すべて同じです。
 inViewport: function( threshold ) { var _threshold = typeof threshold == 'string' ? parseInt( threshold, 10 ) : 0; var pos = methods['getElementPosition'].call( this ); return pos ? !( pos.elemTopBorder - _threshold < 0 ) && !( pos.viewportHeight < pos.elemBottomBorder + _threshold ) && !( pos.elemLeftBorder - _threshold < 0 ) && !( pos.viewportWidth < pos.elemRightBorder + _threshold ) : true; }
      
      







しかし、セレクターはどうですか?


それらはすべて問題ありません。誰もそれらを忘れていません。

それでは、オブジェクトリテラルmethods



ですべてのメソッドを規定しました。ブームを作る次のステップは何ですか? ファンアウトして、擬似セレクターリテラルを展開します。
  "in-viewport": function( obj, index, meta ) { return methods['inViewport'].call( obj, meta[3] ); }, "above-the-viewport": function( obj, index, meta ) { return methods['aboveTheViewport'].call( obj, meta[3] ); }, "below-the-viewport": function( obj, index, meta ) { return methods['belowTheViewport'].call( obj, meta[3] ); }, "left-of-viewport": function( obj, index, meta ) { return methods['leftOfViewport'].call( obj, meta[3] ); }, "right-of-viewport": function( obj, index, meta ) { return methods['rightOfViewport'].call( obj, meta[3] ); }, "partly-above-the-viewport": function( obj, index, meta ) { return methods['partlyAboveTheViewport'].call( obj, meta[3] ); }, "partly-below-the-viewport": function( obj, index, meta ) { return methods['partlyBelowTheViewport'].call( obj, meta[3] ); }, "partly-left-of-viewport": function( obj, index, meta ) { return methods['partlyLeftOfViewport'].call( obj, meta[3] ); }, "partly-right-of-viewport": function( obj, index, meta ) { return methods['partlyRightOfViewport'].call( obj, meta[3] ); }, "have-scroll": function( obj ) { return methods['haveScroll'].call( obj ); } } );
      
      





1つのチップに注目する価値があります。入力パラメータのthreshold



について話したことを思い出してください。 標準のパラメトリック擬似セレクターで:not(selector)



を覚えていますか?

そのため、このような構成を使用して、擬似セレクターでスラスラドを直接指定することもできます。
 $( element ).is( ":in-viewport(10)" );
      
      



この場合、trasholdはスコープを10ピクセル拡大します。



追跡



疑似セレクターであるダックスフントが拡張されました。今では、何らかの方法で全体を便利な方法で監視する必要があります。

理想的には、もちろん、独自のイベントを作成する必要がありますが、歴史的にはjQuery.event.special



と非常に悪い関係にjQuery.event.special



.trigger()



は、このケースではそうではないという考えです-確かに。 したがって、callBack関数をそれほど残忍な方法で呼び出す最も残忍な関数があります。



トラッカーコード
 $.fn.viewportTrack = function( callBack, options ) { var settings = $.extend( { "threshold": 0, "allowPartly": false, "allowMixedStates": false }, options ); //  - if( typeof callBack != 'function' ) { //         -     $.error( 'Callback function not defined' ); return this; } return this.each( function() { //      var $this = this; callBack.apply( $this, [ methods['getState'].apply( $this, [ settings ] ) ] ); //      var _scrollable = $( $this ).parents( ':have-scroll' ); if( !_scrollable.length ) { callBack.apply( $this, 'inside' ); return true; } if( _scrollable.get( 0 ).tagName == "BODY" ) { //  ,    body,  scroll   window,    body,       $( window ).bind( "scroll.viewport", function() { callBack.apply( $this, [ methods['getState'].apply( $this, [ settings ] ) ] ); } ); } else { _scrollable.bind( "scroll.viewport", function() { //    ,  scroll    callBack.apply( $this, [ methods['getState'].apply( $this, [ settings ] ) ] ); } ); } } ); };
      
      





釘付け!

実際、いや、私たちの残忍な「ねじを緩める」ことを教える必要があります...これらの架空の言葉で地獄に、要するに、1つまたは別の要素の追跡を停止し、瞬間がこれに関連しているので、個々の要素を追跡するために独自のスクロールイベントハンドラを作成します。 すべてのコールバック関数が1つのスクロールイベントハンドラーから呼び出された場合、ハンドラーを再度再インストールしない限り、監視対象要素のセットに影響を与えることはできません。

ここでは、イベントの名前空間が役立ちます。同じ要素で.bind( "scroll.viewport")



.bind( "scroll")



し、同じ要素で.unbind( ".viewport")



を作成すると、それは.unbind( ".viewport")



れますscroll.viewport



イベントscroll.viewport



のみがあり、 scroll.viewport



はありません。

そして、これは現在のタスクでどのように役立ちますか? -あなたは私に答えます、あなたは確かに名前空間を舐める必要があります(これはそのようなトートロジーです)が、目標は達成されるので、ランダムIDを生成するメソッドを追加します。 ここではコメントしません。
 generateEUID: function() { var result = ""; for( var i = 0; i < 32; i++ ) { result += Math.floor( Math.random() * 16 ).toString( 16 ); } return result; }
      
      



さらに、各要素の初期化時に、euid(要素の一意のID)によって生成されたこの.data()を.data()にプッシュし、スクロールハンドラーをアタッチすると、名前空間.viewport + EUID



を作成します。 そしてもちろん、セットのEUIDを反復処理し、まだ必要なハンドラーにヒットすることなく、不要なハンドラーを削除するデストラクター。 最終バージョンでは次のようになります。



トラッカーコードファイナル
 $.fn.viewportTrack = function( callBack, options ) { var settings = $.extend( { "threshold": 0, "allowPartly": false, "allowMixedStates": false }, options ); if( typeof callBack == 'string' && callBack == 'destroy' ) { //  return this.each( function() { var $this = this; var _scrollable = $( $this ).parent( ':have-scroll' ); if( !_scrollable.length || typeof $( this ).data( 'euid' ) == 'undefined' ) { return true; //    ,       } //   euid ,     ,      if( _scrollable.get( 0 ).tagName == "BODY" ) { $( window ).unbind( ".viewport" + $( this ).data( 'euid' ) ); $( this ).removeData( 'euid' ); } else { _scrollable.unbind( ".viewport" + $( this ).data( 'euid' ) ); $( this ).removeData( 'euid' ); } } ); } else if( typeof callBack != 'function' ) { $.error( 'Callback function not defined' ); return this; } return this.each( function() { var $this = this; if( typeof $( this ).data( 'euid' ) == 'undefined' ) $( this ).data( 'euid', methods['generateEUID'].call() );// EUID      callBack.apply( $this, [ methods['getState'].apply( $this, [ settings ] ) ] ); var _scrollable = $( $this ).parents( ':have-scroll' ); if( !_scrollable.length ) { callBack.apply( $this, 'inside' ); return true; } if( _scrollable.get( 0 ).tagName == "BODY" ) { $( window ).bind( "scroll.viewport" + $( this ).data( 'euid' ), function() { //  ,    EUID callBack.apply( $this, [ methods['getState'].apply( $this, [ settings ] ) ] ); } ); } else { _scrollable.bind( "scroll.viewport" + $( this ).data( 'euid' ), function() { callBack.apply( $this, [ methods['getState'].apply( $this, [ settings ] ) ] ); } ); } } ); };
      
      







これは、初期化中に渡された設定に応じてブランチのクラウドを使用する愚かなトリガーであるという事実のために、1つのメソッドをスキップして最後まで残しました。 確かに、注目に値するものがいくつかあります。



メソッドコードメソッド['getState']
 getState: function( options ) { var settings = $.extend( { "threshold": 0, "allowPartly": false }, options ); var ret = { "inside": false, "posY": '', "posX": '' }; var pos = methods['getElementPosition'].call( this ); if( !pos ) { ret.inside = true; return ret; } var _above = pos.elemTopBorder - settings.threshold < 0; var _below = pos.viewportHeight < pos.elemBottomBorder + settings.threshold; var _left = pos.elemLeftBorder - settings.threshold < 0; var _right = pos.viewportWidth < pos.elemRightBorder + settings.threshold; if( settings.allowPartly ) { var _partlyAbove = pos.elemTopBorder - settings.threshold < 0 && pos.elemBottomBorder - settings.threshold >= 0; var _partlyBelow = pos.viewportHeight < pos.elemBottomBorder + settings.threshold && pos.viewportHeight > pos.elemTopBorder + settings.threshold; var _partlyLeft = pos.elemLeftBorder - settings.threshold < 0 && pos.elemRightBorder - settings.threshold >= 0; var _partlyRight = pos.viewportWidth < pos.elemRightBorder + settings.threshold && pos.viewportWidth > pos.elemLeftBorder + settings.threshold; } if( !_above && !_below && !_left && !_right ) { ret.inside = true; return ret; } if( settings.allowPartly ) { if( _partlyAbove && _partlyBelow ) { ret.posY = 'exceeds'; } else if( ( _partlyAbove && !_partlyBelow ) || ( _partlyBelow && !_partlyAbove ) ) { ret.posY = _partlyAbove ? 'partly-above' : 'partly-below'; } else if( !_above && !_below ) { ret.posY = 'inside'; } else { ret.posY = _above ? 'above' : 'below'; } if( _partlyLeft && _partlyRight ) { ret.posX = 'exceeds'; } else if( ( _partlyLeft && !_partlyRight ) || ( _partlyLeft && !_partlyRight ) ) { ret.posX = _partlyLeft ? 'partly-above' : 'partly-below'; } else if( !_left && !_right ) { ret.posX = 'inside'; } else { ret.posX = _left ? 'left' : 'right'; } } else { if( _above && _below ) { ret.posY = 'exceeds'; } else if( !_above && !_below ) { ret.posY = 'inside'; } else { ret.posY = _above ? 'above' : 'below'; } if( _left && _right ) { ret.posX = 'exceeds'; } else if( !_left && !_right ) { ret.posX = 'inside'; } else { ret.posX = _left ? 'left' : 'right'; } } return ret; }
      
      









Fuffff、それだけです。 ここにそのようなプラグインがあります、私は301行でそれを得ました。



参照資料



プラグインはgithubから取得できますhttps : //github.com/xobotyi/jquery.viewport

使用方法については、readmeで詳しく説明しています。



この記事が誰かに利益をもたらし、何か新しいことを伝えることを心から願っています。

シムについては、すべてのコード、睡眠、4泊で記事を書きたいという欲求の欠如についてお辞儀をします。



Ps「トレーニング資料」をチェックする価値はありますか?



All Articles