overflow: auto;
, and sometimes it is necessary to process the elements only when they appear on the screen as a whole and moreover, scrolling there in all directions (vertically or / and horizontally).If the parent element has no limiting factors (padding, border, overflow! = Visible), then the margin moves from the inner element to the outer one, and the offsetHeight of the parent element will be calculated without taking into account the margin of its descendants, while the scrollHeight correctly determines the height from considering the margin of the child elements. As a result, such a parent element is defined as having a scrolling, since content height <height of the element itself.
overflow: hidden;
applies to the container overflow: hidden;
and, as a result, the stock scrollbar is hidden.containerElem.offsetHeight
) and the height of its contents ( containerElem.scrollHeight
) and if the height of the content exceeds the height of the container, then most likely, and for my projects - always, such a container has scrolling. (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 );
top = $( element ).offset().top; left = $( element ).offset().left;
But no, .offset()
positions any element relative to the upper left corner of the browser window, and the scope, as mentioned, is not always the browser window - does not fit, we dismiss. top = $( element ).position().top; left = $( element ).position().left;
Also not, .position()
positions the element only relative to the upper left corner of its nearest parent, it would seem that here, but consider the structure: <div id="viewport"> <div class="element"> <span></span> </div> </div>
And the task is to track the span
with respect to #viewport
, in this case, .position()
will position the span
with respect to the .element
that does not suit us, we drove on. 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 ); }
Why is $( this ).get( 0 ).offsetTop
, not $( this ).position().top
? - some will ask..css('margin-top')
and then another .css('margin-bottom')
.css('margin-top')
and .css('margin-bottom')
return a value of 13px
, that is, you also need to do parseInt (str, 10) to perform basic mathematical operations, the option of subtracting heights taking into account different indents (.innerHeight (), .outerHeight, .outerHeight (true)) I do not even consider, because they can be set asymmetrically, but this is important to us.$( this ).position().top
works one and a half to two times slower than the version with this.offsetTop
on my overclocked i7, and the user can sit on some hemp, and scary to imagine what it all pours out..scrollTop()
and .scrollLeft()
, able to get the value of vertical and horizontal scrolling, respectively. 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
, what next to do the boom? Fan, expand the pseudo-selector literal: "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 ); } } );
threshold
input parameter? And remember the standard parametric pseudo-selector :not(selector)
? $( element ).is( ":in-viewport(10)" );
In this case, treshold will expand the scope by 10 px.jQuery.event.special
we are in extremely bad relationships, and .trigger()
is, in my opinion, a so- .trigger()
idea, not for this case - for sure. Therefore, we will have the most brutal function that calls the callBack function in a no less brutal way. $.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 ] ) ] ); } ); } } ); };
.bind( "scroll.viewport")
and .bind( "scroll")
on the same element, and then .unbind( ".viewport")
on the same element, then it is untied there will be only a scroll.viewport
event scroll.viewport
but not just a scroll
. generateEUID: function() { var result = ""; for( var i = 0; i < 32; i++ ) { result += Math.floor( Math.random() * 16 ).toString( 16 ); } return result; }
Further, when initializing for each element, we push this .generated euid (element's unique id) into .data (), and when we hang up the scroll handlers, we create the namespace .viewport + EUID
. And of course, the destructor, who iterates over the EUID of the set and removes unnecessary handlers, without affecting those that we still need. In the final version, we obtain: $.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 ] ) ] ); } ); } } ); };
var ret = { "inside": false, "posY": '', "posX": '' };
If inside
returned with a value of true
, then posY
and posX
remain empty, since tracked item fully fit in viewport. 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; }
Source: https://habr.com/ru/post/240083/
All Articles