jquery.scrollless: обновление

в 13:12, , рубрики: jquery, jquery plugin, scrolling, навигация, скроллинг

Привет!
Предыдущий мой пост был встречен мягко говоря прохладно и, должен признать, для этого имелось достаточно оснований. Во-первых, это конечно отсутствие кода и каких-либо объяснений, из-за чего пост был воспринят как рекламный. Кроме того, в комментариях прозвучало такое мнение, что мгновенная смена кадров (блоков) выглядит убого по сравнению с нативной прокруткой. И с этим я не могу не согласиться. В самом деле, необходима какая-то анимация при переходе от одной позиции к другой. Плюс к тому, не мешало бы вернуть ставший уже привычным вертикальный скроллбар, вернее, предоставить его полноценную замену. Эти недостатки были устранены в новой версии плагина (во всяком случае я на это надеюсь). Теперь в нем есть поддержка анимации (эффектов перехода), а также кастомный скроллбар в виде расширения. А что касается первого недостатка, то его я постараюсь восполнить в этой статье.

Итак, плагин jquery.scrollless заменяет нативный скроллинг такой штукой, которую я назвал «блочным» рендерингом (еще вариант — «дискретный» рендеринг). Суть его в том, что основное содержимое документа (контейнера) разбивается на «блоки», каждый из которых либо отображается в окне полностью, либо не показывается совсем. При этом позиция в документе (объем просмотренного содержимого) теперь определяется не величиной вертикальной прокрутки (window.scrollY), а номером «блока», первого (сверху) среди видимых. Т.е. для того, чтобы определить позицию внутри документа (контейнера) достаточно указать номер первого видимого «блока». Номер последнего видимого блока при этом определяется самим плагином в зависимости от текущих размеров окна. Множество видимых в текущий момент «блоков» образует «окно просмотра» (viewport, или просто view).

Детали реализации

Реализуется блочный рендеринг тривиальным способом — путем переключения CSS свойства "display" у «блоков» (в новой версии есть поддержка анимации, но она используется только как промежуточный эффект, после чего все равно применяется переключение свойства "display").
В слегка упрощенном виде это выглядит примерно так:

Фрагмент кода

/**
 * @var aoItems - коллекция jquery-объектов, соответствующих «блокам»
 */
var aoItems;

/**
 * @var nItems - кол-во «блоков»
 */
var nItems;

/**
 * @var aSize - значения текущих высот «блоков».
 * обновляются при каждом изменении размеров окна
 */
var aSize;

/**
 * @var hWnd - текущая высота окна браузера
 */
var hWnd;

/**
 * @var hCntrXtra - суммарная высота всех элементов вне контейнера
 */
var hCntrXtra;

/**
 * @var iViewStart - начало/позиция «окна просмотра»
 * (номер первого видимого блока)
 */
var iViewStart;

/**
 * @var iViewEnd - конец «окна просмотра»
 * (номер последнего видимого блока + 1)
 */
var iViewEnd;

/**
 * Установка/изменение позиции «окна просмотра»
 * @param iPos - позиция окна просмотра (номер 1-го видимого блока)
 * @param iLeft - левая (визуально - верхняя) граница «окна просмотра»
 * @param iRight - правая (визуально - нижняя) граница «окна просмотра»
 */
function setPos(iPos, iLeft, iRight) {
    iPos = typeof iPos == 'number'? iPos : 0;
    if (!(iPos >= 0 && iPos < nItems) || iViewEnd >= nItems && iPos > iViewStart)
        return;
    /* доступная (макс-ная) высота для «окна просмотра» */
    var h = hWnd - hCntrXtra;
    var bL = typeof iLeft == 'number' && iLeft >= 0 && iLeft <= iPos,
        bR = typeof iRight == 'number' && iRight > 0 && iRight < nItems;
    iLeft = bL? iLeft : 0;
    iRight = bR? iRight : nItems;
    for (var s = 0, i = iPos; i < iRight && s + aSize[i] < h; i++)
        s += aSize[i];
    var iSt = iPos, iEnd = i;
    if (bR || iEnd == nItems) {
        for (i = iSt - 1; i >= iLeft && s + aSize[i] < h; i--)
            s += aSize[i];
        if (i + 1 < iSt && i + 1 != iViewStart) iSt = i + 1;
    }
    if (bR && iSt == iLeft) {
        for (i = iEnd; i < nItems && s + aSize[i] < h; i++)
            s += aSize[i];
        if (i > iEnd) iEnd = i;
    }
    if (iSt == iViewStart && iEnd == iViewEnd) return;
    iViewStart = iSt; iViewEnd = iEnd;
    aoItems.hide().slice(iViewStart, iViewStart).show();
}

Параметр iRight (правая граница) может использоваться для перемещения на один «экран» назад. Например так:

setPos(iViewStart, null, iViewStart);

где iViewStart — текущее начало «окна просмотра»

Параметр iLeft используется в более сложных ситуациях. Пример такого использования можно посмотреть в расширении «pagenav».

Алгоритм разбиения содержимого контейнера на блоки тоже довольно прост, если не считать применение рекурсии. В простейшем случае «блоками» будут являться все непосредственные «дети» контейнера. Это в том случае, если высота каждого «блока» не превышает заданной величины, которую будем называть «квотой блока». Для простоты будем считать, что квота блока это высота окна браузера. Если высота «блока» меньше высоты окна, то он, очевидно, сможет полностью поместиться в окне. В противном случае, если блок не помещается в окне, его необходимо разбить на подблоки, которые должны занять его место в наборе блоков. Если какой-то из подблоков тоже не помещается в окне, то его тоже следует разбить на подблоки, и так далее, до тех пор, пока каждый элемент из набора «блоков» не сможет полностью поместиться в окне.
На практике этот алгоритм дополняется такими деталями, как фильтрация «блоков» по типу элемента. Так, например, нужно исключать inline-элементы, а также табличные элементы tbody, thead, tfoot.
В результате реализация разбиения на блоки выглядит примерно так:

Фрагмент кода
/**
 * @var nMaxDepth - максимальный уровень вложенности
 */
var nMaxDepth;

/**
 * @var hMaxItem - квота блока
 */
var hMaxItem;

/**
 * @param oElem - jquery-объект, соответствующий элементу,
 *  претендующего на роль «блока»
 * @param nLevel - текущий уровень вложенности
 */
function buildItems(oElem, nLevel) {
    if (nLevel > nMaxDepth) {
        throw 1; return;
    }
    var aElems = oElem.children(), nElems = aElems.length, i0 = 0;
    if (!nElems) {
        throw 2; return;
    }
    var aTags = ['tbody', 'thead', 'tfoot'];
    for (var i = 0; i < nElems; i++) {
        var oEl = aElems.eq(i);
        var oStyle = oEl.get(0).currentStyle || window.getComputedStyle(oEl.get(0), null);
        if (oStyle.display == 'inline' || oStyle.display == 'table-cell') break;
        var b = $.inArray(oEl.get(0).tagName.toLowerCase(), aTags) >= 0;
        if (!b && oEl.height() < hMaxItem) continue;
        aoItems = aoItems.add(aElems.slice(i0, i));
        i0 = i+1;
        var n = aoItems.length;
        buildItems(oEl, nLevel+1);
    }
    if (i0 < nElems) aoItems = aoItems.add(i0? aElems.slice(i0, nElems) : aElems);
}
...
try {
    buildItems(oCntr, 0);
}
catch (e) {
    /* обработка исключения */ 
}

API и события

Сам по себе плагин предоставляет только интерфейс для управления положением «окна просмотра» контейнера, а также систему кастомных событий (callback'ов) для обратной связи между плагином и его расширениями, а также основным веб-приложением. Все остальное (в том числе, пользовательский интерфейс) должно реализовываться через расширения и/или основное веб-приложение.

Интерфейс плагина — это статический объект, определенный в объекте jquery:

Интерфейс

$.scrollless = {
    on: function(sEvent, fnCallback) {
        if (sEvent in aFnQueues) addCallback(aFnQueues[sEvent], fnCallback);
        return this;
    },
    setPos: function(v) {
        if (typeof v == 'object')
            setPos(v.pos, v.left, v.right);
        else if (typeof v == 'number')
            setPos(v);
    },
    setPosComplete: function() {
        setPosComplete();
    },
    disable: function() {
        disable(false);
    }
};

Описание методов интерфейса (API)

jquery.scrollless.on — служит для добавления обработчика (коллбека) для кастомного события.

Список кастомных событий плагина:

  • preInit — запускается перед инициализацией плагина. Предназначено для тех расширений, которые изменяют структуру DOM (добавляют/изменяют/удаляют элементы). В обработчик в качестве this передается jquery-объект, соответствующий контейнеру.
  • postInit — запускается после инициализации плагина. Предназначено для всех расширений. Позволяет узнать набор «блоков», используемых плагином — через первый аргумент обработчика.
  • disable — запускается во время отключения плагина. Предназначено для всех расширений. Каждое из них должно в ответ на это событие удалять либо скрывать все следы своей работы, связанные с DOM (изменение структуры, а также обработка событий DOM).
  • changePos — запускается после каждого изменения позиции «окна просмотра». В обработчик передается один аргумент — объект, содержащий информацию о текущей позиции.
  • changeSize — запускается после каждого изменения размеров окна браузера. В обработчик передается один аргумент — объект, содержащий информацию о текущей высоте окна, а также о типе события.

jquery.scrollless.setPos — установка позиции «окна просмотра». Обертка для внутренней функции setPos(), описанной выше.

jquery.scrollless.setPosComplete — завершает установку позиции «окна просмотра». Предназначен для использования в расширениях, реализующих кастомную анимацию.

jquery.scrollless.disable — принудительное отключение плагина и всех его подключенных расширений.

Демо плагина
Репозиторий на GitHub

Спасибо за внимание.

Автор: xmeoff

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js