Написание плагина для jQuery — задача не сложная. Однако, большинство плагинов под jQuery обладают рядом недостатков. Как правило, функционал плагина полностью изолирован внутри одной функции, что не позволяет расширять его функционал не изменяя код плагина. В начале своего пути каждый плагин решает достаточно специфическую задачу, обрастая функционалом по мере развития. Это приводит к появлению гигантских плагинов с сотнями опций и нездоровым размером файла.

Но авторы Twitter Bootstrap нашли хорошее решение этой проблемы. Все плагины этого фреймворка обладают минимально необходимым функционалом, но позволяют расширять его благодаря особому паттерну, заложеному в основу каждого плагина. Этот паттерн можно условно разделить на три составляющие: класс с логикой, функция-плагин и обработчики событий.

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

Класс с логикой

Начнем с написания класса с логикой нашего плагина. Создадим конструктор класса и определим его свойство prototype. Весь код плагина будет размещен внутри самовызывающейся анонимной функции:

(function ($) {
    "use strict"; // включает строгий режим работы javascript

    var PsFloatScroll = function (element, options) {
        this.$element = $(element);
        this.options = $.extend({}, $.fn.psFloatScroll.defaults, options);
    };

    PsFloatScroll.prototype = {
        constructor: PsFloatScroll
    };

    // Тут будет код функции-плагина

    // Тут будет код обработчиков событий
})(window.jQuery);

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

Пока что наш класс еще ничего не умеет. Мой плагин будет регулярно пересчитывать позицию элемента, значит нужно добавить метод с этим функционалом. Кроме того, нужно определять целевой элемент. Я буду задавать его спомощью атрибута data-floatscroll-target:

var PsFloatScroll = function (element, options) {
    this.$element = $(element);
    this.options  = $.extend({}, $.fn.psFloatScroll.defaults, options);
    this.$target  = $(this.$element.data('floatscroll-target'));

    this.update();
};

PsFloatScroll.prototype = {
    constructor: PsFloatScroll,

    update: function () {
        if (this.$target.length === 0) return;
        var scrollTop    = $(document).scrollTop(),
            offsetY      = this.options.offsetY,
            targetY      = this.$target.position().top,
            targetHeight = this.$target.outerHeight(),
            height       = this.$element.outerHeight();
        if (scrollTop < targetY - offsetY) {
            // Верхняя граница целевого элемента не достигла верха экрана
            this.$element.css({
                'position': 'absolute',
                'top': targetY + 'px'
            });
        } else if (scrollTop > targetY - offsetY + targetHeight - height) {
            // Нижняя граница элемента достигла нижней границы целевого элемента
            this.$element.css({
                'position': 'absolute',
                'top': (targetY + targetHeight - height) + 'px'
            });
        } else {
            // Элемент находится в пределах целевого элемента
            this.$element.css({
                'position': 'fixed',
                'top': offsetY + 'px'
            });
        }
    }
};

Целевой элемент определяется во время инициализации экземпляра класса. В конце инициализации запускается метод update(), определяющий позицию целевого элемента и устанавливающий состояние прикрепляемого элемента. При определении позиции целевого элемента учитывается опция offsetY — вертикальное смещение границы экрана. Эта опция может быть полезна, например, если у меня к верхней границе экрана прикреплена панель навигации.

Класс с логикой готов.

Функция-плагин

Функция-плагин будет вызываться на jQuery-коллекции элементов. Такая коллекция может содержать сколько угодно элементов, экземпляр класса с логикой нужно создать для каждого из них. Функция должна давать возможность вызывать произвольный метод класса с логикой. Результатом выполнения функции должна быть jQuery-коллекция элементов, чтобы не прерывать цепочку вызовов. Имеем следующий код:

$.fn.psFloatScroll = function (option) {
    return this.each(function () {
        var $this   = $(this),
            data    = $this.data('psFloatScroll'),
            options = typeof option == 'object' && option;
        if (!data) $this.data('psFloatScroll', (data = new PsFloatScroll(this, options)));
        if (typeof option == 'string') data[option]();
    });
};

Экземпляр класса с логикой для каждого элемента хранится с использованием индивидуального хранилища данных элемента (jQuery метод data()). Если экзепляр еще не создан, он создается и сохраняется автоматически. Если параметр option — хэш, он передается в конструктор класса с логикой. Если же параметр option — строка, будет вызван соответствующий метод экземпляра класса с логикой.

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

$.fn.psFloatScroll.defaults ={
    offsetY: 0
};

$.fn.psFloatScroll.Constructor = PsFloatScroll;

Вот как использовать код нашего класса с логикой в другом плагине:

var MyPlugin = function (element, options) {
    // Код конструктора
};

// Расширяем прототип класса новым прототипом:
MyPlugin.prototype = $.extend({}, $.fn.psFloatScroll.Constructor.prototype, {
    constructor: MyPlugin
    // ...
});

// Расширяем опции по умолчанию новыми:
$.fn.myPlugin.defaults = $.extend({} , $.fn.psFloatScroll.defaults, {
    // ...
});

Обработчики событий

В моем плагине обработчик события всего один: событие scroll на всем HTML документе:

$(document).on('scroll.floatscroll.data-api', function () {
    $('[data-floatscroll-target]').each(function () {
        var $this  = $(this),
            option = $this.data('psFloatScroll') ? 'update' : $this.data();
        $this.psFloatScroll(option);
    });
});

При срабатывании события scroll на документе для каждого элемента с атрибутом data-floatscroll-target будет вызван плагин. Если экземпляр плагина еще не был создан, то в качестве параметра в функцию-плагин будут переданы опции (значения всех data- атрибутов), иначе будет вызван метод update() (передана строка 'update').

Чтобы отключить автоматическое срабатывание плагина, достаточно воспользоваться jQuery методом off():

$(document).off('.floatscroll.data-api'); // Отключить плагин FloatScroll

После отключения при прокрутке документа метод update() автоматически запускаться не будет.