jQuery.viewport или как я искал элементы на экране


    Равно как у каждой девушки должно быть «маленькое черное платьице», у каждого front-end разработчика должен быть «маленький черный плагинчик»… как-то не очень звучит, пусть будет «маленький функциональный плагинчик», так о чем это я, я это о том, что хочу одним таким поделиться.

    Представленный плагин позволяет определять положение какого-либо элемента/набора элементов, относительно области просмотра. Функционально, он расширяет набор псевдо-селекторов, а так же добавляет трекер элементов.

    Так же, под катом, я расскажу о процессе написания плагина, с какими трудностями столкнулся и т.д., если я Вас заинтересовал — милости прошу под кат.

    Встала давеча передо мной задача, ловить и обрабатывать элементы в момент их появления в области видимости, причем, область видимости — не всегда весь экран, иногда это блок с overflow: auto;, а иногда надо обрабатывать элементы только когда они появятся на экране целиком и более того прокрутка там во все стороны( вертикально или/и горизонтально ).
    Пошел я значится в гугол на поиски чего-либо готового и к моему удивлению, ничего, что полностью удовлетворяло бы моим запросам, я не нашел, либо задача решалась частично, как например тут (признаться честно, идею с расширением псевдо-селекторов стырил оттуда, но ведь эта картинка не врет, правда?), либо вовсе плагин был не про то. Вот я и встал перед фактом что надо писать свое, потом решил поделиться этим делом на гитхабе, а еще позже решил написать эту статью, чем собственно, успешно выполнив первые два пункта моего плана по становлению знаменитостью, и занимаюсь.

    Если Вам не интересен процесс разработки тыкаете ->>вот сюда<<- и попадаете сразу к месту о том, где раздают.

    Пролог

    Если Вы не знаете про написание плагинов для jQuery, но очень хотите этому научиться — крайне советую, для начала, прочитать эту статью, все доходчиво и понятно (предполагает наличие хотя бы базовых знаний JS и JQ).



    UPDATE 1 ( 13.10.2014 )

    — Переписана часть плагина, изменения смотрите на гитхабе.
    — Обнаружена проблема, решения которой я пока не могу найти:
    Если у родительского эл-та нет ограничивающих факторов ( padding, border, overflow != visible ), то margin переходит от внутреннего элемента к внешнему, а offsetHeight родительского элемента будет рассчитан без учета margin своих потомков, в то время как scrollHeight исправно определяет высоту с учетом margin дочерних элементов. В результате, такой родительский элемент определяется как имеющий скроллинг, т.к. высота содержимого < высоты самого элемента.



    UPDATE 2 ( 16.10.2014 )

    — В качестве решения вышеописанной проблемы была добавлена возможность задать селектор вьюпорта, который необходимо отслеживать, подробности на гитхабе.
    — Скорость работы плагина заметно увеличена.




    Приступим-с


    Итак, задача: надо как-то определять положение элемента относительно области видимости, причем в зависимости от контекста, областью видимости может быть не только окно браузера, но и более мелкие элементы имеющие прокрутку.

    Что есть область видимости?

    Начнем, пожалуй, с определения того, что же для данного контекста является областью видимости.
    Для моей задачи, а писал я плагин, в первую очередь, для удовлетворения своих нужд, областью видимости является ближайший родитель, имеющий прокрутку.

    К сожалению, нет гарантированного и кросс-браузерного способа определить наличие полосы прокрутки (по крайней мере я о таком не знаю), да и к тому же я использую кастомный скроллбар, его можно адекватно оформлять, но, к контейнеру применяется overflow: hidden; и, как следствие, стоковый скроллбар скрывается.
    Но выход есть, можно сравнивать высоту контейнера( containerElem.offsetHeight ) и высоту его содержимого( containerElem.scrollHeight ) и в случае, если высота содержимого превышает высоту контейнера, то, скорее всего, а для моих проектов — всегда, такой контейнер имеет прокрутку.
    Оформляем это дело в код:
    (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() позиционирует любой элемент относительно левого верхнего угла окна браузера, а область видимости, как говорилось, не всегда окно браузера — не подходит, отметаем.

    Второе что приходит на ум:
    top = $( element ).position().top;
    left = $( element ).position().left;
    
    Тоже нет, .position() позиционирует элемент только относительно левого верхнего угла своего ближайшего родителя, казалось бы вот оно, но рассмотрим структуру:
    <div id="viewport">
    	<div class="element">
    		<span></span>
    	</div>
    </div>
    
    А задача — отслеживать именно span относительно #viewport, в таком случае, .position() будет позиционировать span относительно .element что нам не подходит, погнали дальше.

    Решением будет свой метод, который будет обходить всех родителей вверх по дереву 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? — спросят некоторые.
    Причин на то две:
    • .position() учитывает смещение (top, bottom, left, right), не учитывает margin-ы, а поэтому придется использовать .css('margin-top') и потом еще .css('margin-bottom')
    • эти самые .css('margin-top') и .css('margin-bottom') возвращают значение в виде 13px, то есть надо еще и parseInt( str, 10) делать, чтобы производить базовые математические операции, вариант с вычитанием высот с учетом разных отступов( .innerHeight(), .outerHeight, .outerHeight(true) ) я даже не рассматриваю, ибо они могут быть заданы несимметрично, а нам это важно.
      В конечном итоге, с учетом всех этих излишних операций, вариант с использованием $( this ).position().top работает в полтора-два раза медленнее нежели вариант с this.offsetTop на моем разогнанном i7, а юзер может сидеть на каком-нибудь пеньке, и страшно представить во что это вообще выльется.

    Дописываем аналогичный метод для определения положения от левого края. Хорошая мысля приходит опосля, их же можно в один метод объединить, так получится в два раза меньшее количество обходов 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;
    }
    

    Тут, я думаю, все ясно, единственное уточню по поводу threshold и строгого меньшинства.
    Threshold — параметр задающий отступ от края области видимости, для некоторых задач может быть необходимой обработка немного раньше, чем объект войдет в область видимости или немного позже.

    А строгое меньшинство указано по причине того, что если границы совпадают, то элемент еще не пересек границу и пока вписывается в зону видимости, а значит находится внутри.
    Так же, для частичного нахождения в области видимости, тут уже чуть сложнее, но по прежнему просто. просто на этот раз уже проверяем что соответствующая границы вышла за пределы области видимости а противоположная еще находится в ее пределах.
    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 );
    	}
    } );
    

    Стоит отметить одну фишку, помните я говорил про входной параметр threshold? А помните стандартный параметрический псевдо-селектор :not(selector)?
    Так вот, мы тоже можем использовать такую конструкцию, для указания трешолда прямо в псевдо-селекторе:
    $( element ).is( ":in-viewport(10)" );
    
    В данном случае трешолд будет расширять область видимости на 10 px.

    Трекинг


    Такс, псевдо-селекторы расширили, надо бы теперь все это дело как-то удобным образом отслеживать что ли.
    В идеале, конечно надо бы создать свое событие, но так уж исторически сложилось, что с 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 ] ) ] );
    			} );
    		}
    	} );
    };
    

    NAILED IT!
    На самом деле — нет, надо бы научить нашего брутала «откручиваться»… к черту эти выдумывания слов, короче прекращать отслеживание того или иного элемента, с этим как-раз связан тот момент, что для отслеживания каждого отдельного элемента создается свой обработчик события scroll. В случае если все callback функции будут вызываться из одного обработчика события scroll, у нас не будет возможности влиять на набор отслеживаемых элементов, не переустанавливая обработчик заново.
    Тут нам помогут пространства имен событий, если произвести .bind( "scroll.viewport") и .bind( "scroll") на один и тот же элемент, а затем .unbind( ".viewport") на тот же элемент, то отвязан будет только обработчик события scroll.viewport но, не просто scroll.
    И как же это поможет в текущей задаче? — спросите Вы, отвечаю, придется конечно подзасрать пространство пространства имен(вот такая тавтология), но цель будет достигнута, итак, добавляем метод генерирующий случайный id. тут все просто даже комментировать не буду:
    generateEUID: function() {
    	var result = "";
    	for( var i = 0; i < 32; i++ ) {
    		result += Math.floor( Math.random() * 16 ).toString( 16 );
    	}
    
    	return result;
    }
    
    далее, при инициализации для каждого элемента, пушим в .data() этот самый, сгенерированный euid (element's unique id), а когда навешиваем обработчики скролла, то создаем пространство имен .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 ] ) ] );
    			} );
    		}
    	} );
    };
    


    Один метод я пропустил, оставив на самый конец, в силу того что это тупо триггер с тучей ветвлений, зависящих от настроек которые были переданы при инициализации. Правда есть пара вещей, которые стоит отметить:
    • Логика, по которой определялось относительное положение элемента, дублирована в данном методе для того, чтобы не делать лишних вычислений и выборок, а лишь один раз получить все нужные данные и уже с ними работать, кода больше но зато и выборок на 8 меньше.
    • Состояние возвращается в виде объекта с тремя параметрами
      var ret = { "inside": false, "posY": '', "posX": '' };
      
      Если inside возвращается со значением true, то posY и posX остаются пустыми, т.к. отслеживаемый элемент полностью поместился в область просмотра.
      В противном случае указывается состояние элемента относительно каждой оси, подробнее смотрите на гитхабе.


    Код метода methods['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;
    }
    



    Фьюффф, вот вроде и все. Вот такой вот плагин, на 301 строку у меня получился.

    Ссылки


    Забрать плагин можно с моего гитхаба: https://github.com/xobotyi/jquery.viewport
    Как пользоваться подробнейшим образом описано в readme.

    Искренне надеюсь, что данная статья кому-то принесет пользу и расскажет что-нибудь новое.
    За сим откланяюсь, всем кода, сна и отсутствия желания писать статью в 4 ночи.

    P.s. стоит ли ставить галку «обучающий материал»?

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 60

      0
      Недавно искал подобное, спасибо.
        +1
        Буду благодарен за багрепорты на гитхабе.
        0
        Вы забыли учесть случай когда элемент по высоте/ширине больше viewport
        Ну или outside это не удачное название для такой ситуации
          0
          Интересно, в случае если часть эл-та над областью скроллинга и часть под действительно будет возвращаться outside.

          Может возвращать что-то из серии «exceeds-vertically»? получится если включены allowMixedStates будет возвращаться exceeds-vertically exceeds-horizontally если блок во все стороны выходит за пределы области видимости
          –1
          Чем эта штука лучше по сравнению с https://github.com/thesmart/jquery-scrollspy/?

          Чем эта штука хуже по сравнению с https://github.com/thesmart/jquery-scrollspy/?
            +1
            Быстренько пролистал исходник предложенного Вами плагина.

            jQuery.viewport лучше:
            • Возможность отслеживать относительно местоположение элемента за пределами области видимости
            • Областью видимости может быть не только окно браузера
            • Более точный и кроссбразуерный вариант функции позиционирующей элемент относительно области вдимости

            jQuery.viewport хуже:
            • В случае с scrollspy создается свое событите, с котором можно работать более широко и гибко нежели с моим вариантом трекера, взять тот же one( event, fn )
            • Более высокая производительность, в силу того, что у scrollspy много меньше расчетов по отслеживанию расчетов позиции элемента

            В остальном, если брать мой плагин с настройками по-умолчанию, они вроде равны (при первом приближении к коду)
              0
              Так же можно добавить к плюсам jQuery.viewport:
              — Более точное определение момента вхождения элемента в область видимости, по причине наличия у scrollspy троттлинга.
              — Возможность работы в случае использования кастомного скроллбара.
              –1
              Хмм, куча имён псевдоселекторов, да ещё и не иерархических.
                0
                Кстати об иерархичности селекторов, планирую доработать этот момент, вспомнил про иерархичность уже после написания статьи, когда спать ложился, пасибо что напомнил^_^
                  +1
                  Так вот, перепроверил, в текущем виде иерархичность селекторов поддерживается более чем полностью, у меня закономерный вопрос: что вы понимаете под иерархичностью?
                    0
                    Под иерархическим я понимаю имя, которое составлятся из категорий от более общих, к более частным.
                    Возьмём для примера два имени: right-of-viewport, partly-above-the-viewport. Самая общая категория здесь это viewport, соответственно, имя должно начинаться с него. Т.е, если переделать, то это будет viewport-right, viewport-above-partly, или что-то в таком роде (с артиклями или без, по вкусу). При этом все имена заключены в «пространство имён» viewport и далее в субпространства, относящиеся к более мелким категориям (субкатегориям). Такое имя проще генерить, проще получить список всех имён, проще запомнить.

                    Его сложней читать, это обратная сторона. Если взглянуть на псевдоселекторы CSS, то многие из них написаны в «литературном» порядке, но т.к. их длина меньше, это не так критично. У вас же селекторов много, они однотипные, imo, тут уже лучше иерархический подход.
                      0
                      Я подошел со точки зрения CSS, т.е. читаемости. суть то нейминга селекторов как раз в хорошей читаемости.
                        +1
                        В данном случае их крайне много. И иерархическое имя позволяет избавиться от всех отношений типа of, above-the и тд.
                  +1
                  Следующий этап — определение местоположения интересующего нас блока относительно области видимости.


                  Решением будет свой метод, который будет обходить всех родителей вверх по дереву DOM, вплоть до области видимости данного контекста.


                  Можно использовать голый javascript, чтобы получить координаты относительно области видимости элемента element:

                  top = element.getBoundingClientRect().top;
                  bottom = element.getBoundingClientRect().bottom;
                  left = element.getBoundingClientRect().left;
                  right = element.getBoundingClientRect().right;
                  

                  и нет нужды в обходе цепочки родительских объектов.
                    +1
                    Вообще в JS практически всегда когда решение включает перебор элементов для получения скалярных данных, вы делаете что-то не так. Браузер уже все посчитал за вас, просто не поленитесь найти где он это показывает.
                      +1
                      Стесняюсь спросить, но все же, ткните меня пожалуйста носом в функцию которая будет выдавать мне позицию элемента относительно ближайшего элемента имеющего скроллинг, с учетом того что это не обязательно окно браузера.
                          0
                          Статью то читал? :-) Оно, само по себе, не решает задачу.
                            +1
                            да нет, как раз offsetParent помог мне избавиться от parseInt( val, 10 )
                              0
                              Я к тому, что ты это и так использовал уже в своей статье.
                            +1
                            Категорически не понимаю чем вас не устраивает:
                            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 );
                            }
                            
                            который написан на чистом javascript, и использует приведенный Вами .offsetParent
                              0
                              Перепутал, думал перебор для оффсета относительно родителя а не предка.
                        +1
                        Во первых: у меня и так голый JS используется в том месте
                        Во вторых:
                        Начнем, пожалуй, с определения того, что же для данного контекста является областью видимости.
                        Для моей задачи, а писал я плагин, в первую очередь, для удовлетворения своих нужд, областью видимости является ближайший родитель, имеющий прокрутку.
                        .getBoundingClientRect() получает позицию элемента относительно окна браузера пользователя, что уже не подходит в случае если если область видимости у нас — не окно, а какой-либо другой блок ниже по дереву DOM.
                        Во всяком случае сейчас сравнил цифры возвращаемые моим методом и .getBoundingClientRect(), последний возвращает левак какой-то, и я не горю желанием выяснять почему, ибо есть уже работающий вариант функции.
                          0
                          Э, нет, не то написал.
                            0
                            Быстро время редактирования истекает…

                            А почему нельзя взять bounding rect'ы для viewport и для element и проверить их на наличие коллизий? Координаты там абсолютные, какая разница, от какой они точки.
                          0
                          Выглядит интересно.
                          Как оно по скорости? Насколько я помню, события на скроле дёргаются довольно часто, что приводит к беспрерывному выполнению привязаного кода.
                          Сколько элементов на странице можем так отслеживать, до момента начала тормозов?
                            +1
                            Полноценные нагрузочные пока не проводил, но да, быстродействие на большом кол-ве отслеживаемых элементов — пока узкое место плагина.

                            Конкретнее, на 170-200 отслеживаемых эл-тах появляются просадки.

                            Буду работать в этом направлении, решение, скорее всего, заключается в минимизации количества хэндлеров события scroll
                              0
                              Я когда-то решал схожую задачу. Могу сказать несколько моментов, которые использовал.

                              1) Проверять в хэндлере сдвиг, хотя бы на 10 пикселей, в ином случае ничего не делать (или делать по отпусканию скрола). Уже уменьшит количество вызовов раз в 10 :-)
                              Точнее количество вызовов будет тоже, но часть быстро отсеется.

                              2) Запоминать элементы: нет смысла каждый раз оборачивать $( element ) их, когда можно 1 раз обернуть и пользоваться.
                                +1
                                Вот прям щас сделал пункт 2 и делаю пункт 1 :D

                                + повысил до 250 элементов, объединив getFromTop и getFromLeft в один метод, сократив кол-во обходов DOM в два раза, странно что я сразу до этого не допер, а только в момент написания статьи=)
                                  +1
                                  Всегда пожалуйста :-)
                                  +1
                                  как вариант — сделать еще троттлинг, даже если сделать 100мс очень сильно сократится кол-во расчетов и обхода дерева элементов в случае скроллинга перетаскиванием ползунка скроллбара.
                                    0
                                    Ага. не забудь еще бинды сделать на pgup, pgdown, home, end
                                      +1
                                      они ж сами по себе вызывают onScroll, не?
                                      во всяком случае, я когда проверял — все работало, и не помню чтобы onScroll не вызывался этими кнопками.
                                        0
                                        Вызывают, но может быть ситуация, когда 100мс не прошло, а человек нажал end — ивент с предложеным подходом пропустится и будет странное поведение.
                                        Суть в том, что подобные сдвиги надо внести в исключения, если хочешь ограничивать по времени.
                                        Для начала я бы остановился на сдвигах в пикселях, а не на времени, т.к. там много хитрых ситуаций, которые не сразу отлавливаются.
                                          +1
                                          Подумал ты про бинд этих кнопок в принципе, ели в контексте троттлинга — спасибо учту.

                                          Но пока — я, пожалуй, буду копать в сторону оптимизации работы с набором отслеживаемых эл-тов, это на текущий момент очевидное узкое место плагина.
                                            +1
                                            «250 отслеживаемых элементов хватит всем».
                                            На самом деле представить не могу зачем так много. У меня даже в сложных интерфейсах было, ну максимум, 3 подобных проверки.
                                            Для одинаковых элементов они группируются, так что проблем нет.
                                            Можно пример использования, пожалуйста?
                                              +1
                                              На гитхабе (линк в конце поста) лежит пример, правда, пока, тот который на 170 эл-тах тупить начинает.
                                                +1
                                                Нет, я имел ввиду живой пример. Абстрактно то всё понятно.
                                                Где может понадобится отслеживать отображение такого количества элементов?
                                                  +1
                                                  А, по количеству отслеживаемых… да честно говоря — фиг знает, но хочется иметь большой задел.
                                                  У меня в одном проекте, например, есть лента уведомлений, уведомление считается прочитанным если если оно попало в область видимости, за пределами области видимости может быть до 100 элементов, но как только элемент попадает в область видимости, производится нужная кухня и трэкер отвязывается, так что это не совсем тот пример.

                                                  Ссылку на проект дать не могу, да и честно говоря не хочу, в силу того что я пока не очень доволен своим детищем.
                                                    +1
                                                    уведомление считается прочитанным если если оно попало в область видимости, за пределами области видимости может быть до 100 элементов

                                                    Я для подобных уведомлений использовал прочтение по ховеру.

                                                    Вообще имеет место следующий подход: во время бинндинга добавляем элемент в массив вида
                                                    arr = [
                                                        {
                                                            x: {
                                                                left: int, // левая граница
                                                                right: int // правая граница
                                                            },
                                                            y: {
                                                                top: int,   // верхняя граница
                                                                bottom: int // нижняя граница
                                                            },
                                                            ev: event // событие, когда на экране
                                                        },
                                                        ...
                                                    ]
                                                    

                                                    Массив отсортирован, в зависимости от текущей задачи, например по y.top
                                                    Во время скрола мы проходим только по части массива (где x;y попадает на экран), проверяя нужно ли вызывать ивент.
                                                    Небольшим увеличением потребляемой памяти получим возможность отлова событийбесконечного количества элементов, т.к. ресурсов затрачиваться больше не будет (за исключением оперативной памяти).
                                                      0
                                                      Сейчас там +- схожий код.

                                                      Плагин написал потому что хотел написать что-то более универсальное, что можно приложит вообще к чему угодно.
                                                        0
                                                        Просмотрел весь код — не похоже там на +-, скорее похоже на постоянные вычисления всего и вся заного.
                                                          +1
                                                          «Там» это в моем проекте=)
                                                          здесь же ситуевина немного другая, не позволяющая использовать такой подход, ибо он не подходит для отслеживания положения вне области видимости.
                                                            0
                                                            Подходит. Просто надо грамотно обходить массив. В любом случае вычислений меньше будет.
                                                              0
                                                              В конечном итоге, самым затратным по времени выполнения оказался метод определения позиции элемента.
                                                              Увеличил скорость работы плагина просто введя некий аналог кеширования значений на 1 секунду:
                                                              getRelativePosition: function( forceViewport ) {
                                                              	var fromTop = 0;
                                                              	var fromLeft = 0;
                                                              	var $obj = null;
                                                              
                                                              	for( var obj = $( this ).get( 0 ); obj && !$( obj ).is( forceViewport ? forceViewport : ':have-scroll' ); obj = $( obj ).parent().get(0) ) {
                                                              		$obj = $( obj );
                                                              		if( typeof $obj.data( 'pos' ) == 'undefined' || new Date().getTime() - $obj.data( 'pos' )[1] > 1000 ){
                                                              			/*
                                                              			* Making some kind of a cache system, it takes a bit of memory but helps us veeery much, reducing calculation
                                                              			* */
                                                              			fromTop += obj.offsetTop;
                                                              			fromLeft += obj.offsetLeft;
                                                              			$obj.data( 'pos', [ [ obj.offsetTop, obj.offsetLeft ], new Date().getTime() ] );
                                                              		} else{
                                                              			fromTop += $obj.data( 'pos' )[0][0];
                                                              			fromLeft += $obj.data( 'pos' )[0][1];
                                                              		}
                                                              	}
                                                              
                                                              	return { "top": Math.round( fromTop ), "left": Math.round( fromLeft ) };
                                                              }
                                                              

                                                              1 — секунда это не так уж и мало чтобы в значительной степени снизить кол-во вычислений, в то же время это и не так много чтобы прозевать вероятные изменения в DOM.
                                                              По идее надо бы вообще вынести этот лимит в конфиг, т.к. по идее программист то знает когда и как часто меняется DOM
                              +1
                              Все уже есть :)

                              github.com/protonet/jquery.inview
                                +1
                                Опять же, не будет работать в случае если viewport != window
                                0
                                Если кому интересно, есть стандартный способ детекции элемента во вьюпорте, реализованный в т. ч. Polymer: это lifecycle события enteredViewport и leftViewport.
                                Они полноценно реализованы в polymer, или полифиле lifecycle-events.

                                Lifecycle-events также генерит события attached, detached, сигнализирующие о том, был элемент вставлен или изъят из DOM.
                                  +1
                                  Ну, это полимер, я, например, его не использую
                                    0
                                    lifecycle-events это 2.1kb плагин, он не относится к полимеру.
                                      +1
                                      сам плагин — да, ток вот ему до кучи еще 5 других плагинов надо:
                                      var MO = require('mutation-observer');
                                      var evt = require('emmy');
                                      var matches = require('matches-selector');
                                      var getElements = require('tiny-element');
                                      var intersects = require('intersects');
                                      
                                        0
                                          0
                                          иии… он содержит всё те же зависимости.
                                          Ах, похоже там все зависимости прямо в плагине и есть. Даже не в сжатом.
                                          Странное использование require — я подумал что оно по другому работает — тащит внешние зависимости.
                                            0
                                            Не особо листая вниз кода, я подумал как zav, что он тащит внешние зависимости ибо раньше такого использования require я не встречал.

                                            З.ы. И все равно, даже это плагин работатет с viewport=window
                                            З.ы.ы. и нечего тут минусами кидаться, кармы итак нету Т_Т
                                              +1
                                              Извиняюсь, поспешил :) Привык работать в концепции browserify с модульными зависимостями как в node, забыл, что не все в курсе могут быть.
                                              Да, lifecycle-events не такой гибкий, конечно. Но для простой задачи забить гвоздь это отличный молоток :)
                                    0
                                    1
                                      0
                                      1
                                        0
                                        Извиняюсь, но что это? У меня звоночек отметил, что на старинной статье в избранном кто-то фигней страдает, а это еще и автор оказывается )
                                          +1
                                          Подглядывал как у хабра работают древовидные комменты, я и забыл что тут их нельзя удалять :D
                                            +1
                                            Ну, главное чтобы для дела )

                                    Only users with full accounts can post comments. Log in, please.