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

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

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

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

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

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

Пролог

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

UPDATE 1 ( 13.10.2014 ) UPDATE 2 ( 16.10.2014 )

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

Приступим-с

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

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

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

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

С этого момента мы можем использовать .is( ":have-scroll" ) для определения имеет ли элемент прокрутку (или предпосылки к ее наличию) или нет.

Позиционирование элемента

Следующий этап — определение местоположения интересующего нас блока относительно области видимости. Первое что приходит на ум: Но нет, .offset() позиционирует любой элемент относительно левого верхнего угла окна браузера, а область видимости, как говорилось, не всегда окно браузера — не подходит, отметаем.

Второе что приходит на ум: Тоже нет, .position() позиционирует элемент только относительно левого верхнего угла своего ближайшего родителя, казалось бы вот оно, но рассмотрим структуру:А задача — отслеживать именно span относительно #viewport , в таком случае, .position() будет позиционировать span относительно .element что нам не подходит, погнали дальше.

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

Итак, теперь мы знаем где, относительно области видимости, расположен отслеживаемый объект, но полученные данные, при скроллинге меняться не будут, тут нам помогут .scrollTop() и .scrollLeft() , умеющие получать значение вертикального и горизонтального скроллинга соответственно. Более того, нам необходимо знать положение всех сторон отслеживаемого блока и размеры области видимости. Оформляем в очередной метод:

Возвращает данная функция хэш-таблицу, с ней просто удобнее работать впоследствии. Все, с относительным позиционированием блока разобрались.

И все-таки, сверху или снизу

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

А строгое меньшинство указано по причине того, что если границы совпадают, то элемент еще не пересек границу и пока вписывается в зону видимости, а значит находится внутри. Так же, для частичного нахождения в области видимости, тут уже чуть сложнее, но по прежнему просто. просто на этот раз уже проверяем что соответствующая границы вышла за пределы области видимости а противоположная еще находится в ее пределах.

Нет смысла описывать проверку остальных границ, там все аналогично, можно разве что привести код метода проверяющего нахождение элемента внутри области видимости:

А как же селекторы?

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

Трекинг

Такс, псевдо-селекторы расширили, надо бы теперь все это дело как-то удобным образом отслеживать что ли. В идеале, конечно надо бы создать свое событие, но так уж исторически сложилось, что с jQuery.event.special мы в крайне плохих отношениях, а .trigger() — на мой взгляд так себе затея, не для данного случая — точно. Поэтому, у нас будет самая что ни на есть брутальная функция, которая не менее брутальным образом вызывает callBack функцию.

NAILED IT! На самом деле — нет, надо бы научить нашего брутала «откручиваться»… к черту эти выдумывания слов, короче прекращать отслеживание того или иного элемента, с этим как-раз связан тот момент, что для отслеживания каждого отдельного элемента создается свой обработчик события scroll. В случае если все callback функции будут вызываться из одного обработчика события scroll, у нас не будет возможности влиять на набор отслеживаемых элементов, не переустанавливая обработчик заново. Тут нам помогут пространства имен событий, если произвести .bind( "scroll.viewport") и .bind( "scroll") на один и тот же элемент, а затем .unbind( ".viewport") на тот же элемент, то отвязан будет только обработчик события scroll.viewport но, не просто scroll . И как же это поможет в текущей задаче? — спросите Вы, отвечаю, придется конечно подзасрать пространство пространства имен(вот такая тавтология), но цель будет достигнута, итак, добавляем метод генерирующий случайный id. тут все просто даже комментировать не буду: далее, при инициализации для каждого элемента, пушим в .data() этот самый, сгенерированный euid (element's unique id), а когда навешиваем обработчики скролла, то создаем пространство имен .viewport + EUID . Ну и конечно же деструктор, который перебирает EUID набора и удаляет ненужные обработчики, не задевая те которые нам еще понадобятся. В конечном варианте получаем:

  • Логика, по которой определялось относительное положение элемента, дублирована в данном методе для того, чтобы не делать лишних вычислений и выборок, а лишь один раз получить все нужные данные и уже с ними работать, кода больше но зато и выборок на 8 меньше.
  • Состояние возвращается в виде объекта с тремя параметрами Если inside возвращается со значением true , то posY и posX остаются пустыми, т.к. отслеживаемый элемент полностью поместился в область просмотра. В противном случае указывается состояние элемента относительно каждой оси, подробнее смотрите на гитхабе.

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

Ссылки

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

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

📎📎📎📎📎📎📎📎📎📎