/nanoblocks

Primary LanguageJavaScript

nanoblocks

nanoblocks -- библиотека блоков, реализующая портальный стиль Яндекса.

Блоки

Блок состоит из двух частей: визуальное представление (html/css) и поведение (js). Вообще говоря, эти части независимы друг от друга. Одно и тоже поведение можно навешивать на визуально разные блоки.

Внешний вид блоков задается html-разметкой и набором css-классов. Поведение задается через специальные data-атрибуты:

<div class="popup" data-nb="popup">
    ...
</div>

При этом название блока в css не обязано совпадать с названием блока в js:

<!-- Внешний вид другой, а поведение такое же. -->
<div class="dialog" data-nb="popup">
    ...
</div>

Блоки определяются примерно так:

nb.define('popup', {

    // События, на которые подписан блок.
    'events': {
        'click .close': 'onclick',
        ...
    },

    // Методы блока (включая и обработчики событий).
    'onclick': function(e, node) {
        ...
        this.close();
    },

    'close': function() {
        $(this.node).hide();
        return false;
    }

    ...

});

Первым параметром в nb.define передается имя блока, вторым объект, описывающий методы и свойства блока. По сути это прототип. Свойство events имеет особое значение, оно описывает то, на какие события реагирует блок.

События

Блок может реагировать на два типа событий:

  • DOM-события (click, dblclick, keypress, ...). Этот тип событий может иметь уточняющий селектор, например, click .foo.

  • Кастомные события.

events: {

    'click': function(e, node) {
        ...
    },

    'click .foo': 'onClickFoo',

    'open': function(e, params) {
        ...
    }

}

В качестве обработчика события можно указать либо функцию, либо название метода блока.

В обработчик передаются два параметра:

  • Для DOM-событий первый параметр -- это jQuery.Event, а второй -- html-нода, на которой случилось событие. В случае, когда задан селектор, это будет нода, соответствующая селектору. Если селектора нет, то это будет нода всего блока.

  • Для кастомных событий первый параметр -- это название события, второй -- дополнительный параметр, который можно передать в метод trigger().

Внутри обработчика this указывает на текущий блок.

Экземпляры блоков

Функция nb.define определяет конструкторы соответствующих классов блоков. Экземпляры же создаются по мере необходимости и кэшируются.

В общем случае, схема такая:

  • При инициализации библиотеки на документ вешаются обработчики для всех DOM-событий (click, ...).

  • Когда пользователь кликает (например) куда-нибудь, этот обработчик пытается найти ближайший блок, внутри которого произошел клик. Для этого он проходит по всем нодам от ноды, на которой произошло событие, до самого верха (document). Для каждой ноды он смотрит, есть ли у нее атрибут data-nb.

  • Если это блок (есть атрибут data-nb), либо из кэша достается раннее созданный блок, либо же создается экземпляр блока с классом, указанным в data-nb, в конструктор передается та самая нода.

  • Сразу после создания блока, на нем генерится событие init.

  • Ключом для кэширования блоков служит атрибут id. Если такого атрибута на ноде блока нет, генерится уникальный id, ноде выставляется соответствующий атрибут.

  • После чего проверяется, подписан ли блок на DOM-событие click, если да, вызывается этот обработчик. Если нет или же обработчик вернул не false, то берется родительская нода и процесс продолжается.

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

Авто-инициализация

В случае, когда блок нужно создать сразу же после загрузки страницы, ему нужно задать специальный класс _init:

<div class="popup _init" data-nb="popup">
    ...

В момент инициализации библиотеки находятся все блоки на странице с классом _init и для всех них сразу создаются экземпляры блоков. Если блок в events указал событие init, то он сможет сразу же выполнить какое-то действие:

nb.define('popup', {
    events: {
        'init': function() {
            // do something
        },
        ...
    },
    ...
}

Расширение функциональности блоков

Есть несколько вариантов, как из существующих блоков сделать какой-то другой:

  • Миксины.
  • Расширение.
  • Замена.

Миксины

На одной html-ноде можно задать несколько js-блоков:

nb.define('foo', {
    events: {
        click: function() {
            console.log('click foo');
            return false;
        }
    }
});

nb.define('bar', {
    events: {
        click: function() {
            console.log('click bar');
            return false;
        }
    }
});
<div data-nb="foo bar">foobar</div>

В этом примере, при клике в этот div будут срабатывать оба обработчика в том порядке, в котором они заданы в атрибуте data-nb.

Расширение

Это вариант применим тогда, когда нужно слегка подкорректировать поведение блока. Или же добавить новый функционал.

nb.define('foo', {
    events: {
        click: function() {
            console.log('click foo');
            return false;
        }
    }
});

nb.define('bar', {

    events: {
        //  Если у ноды есть класс _disabled, то ничего не делаем.
        //  Иначе вызывает родительский обработчик.
        'click': function(e, node) {
            if ( $(node).hasClass('_disabled') ) {
                console.log('disabled!');
                return false;
            }
        },

        //  Новая функциональность.
        dblclick: 'onDoubleClick'
    },

    'onDoubleClick': function() {
        ...
    }

//  Последним параметром указываем базовый класс.
}, 'foo');
<div data-nb="bar">foobar</div>
<div class="_disabled" data-nb="bar">disabled foobar</div>

Замена

Пока не реализовано. Полностью заменяет реакцию на событие. Нужно ли это вообще?

Непонятно, каким образом задавать этот вариант. Вариант:

nb.define('bar', {

    events: {

        //  Даже если этот обработчик не возвращает false,
        //  родительский обработчик не вызывается.
        '! click': function() {
            ...
        }
    }

}, 'foo');

Стандартные методы и свойства блоков

Получение экземпляра блока

Функция nb.block(node) принимает html-ноду и возвращает блок, созданный на этой ноде.

var block = nb.block( document.getElementsByClassName('.popup')[0] );

var block = nb.block( document.getElementById('my-block') );
var block = nb.find('my-block'); // Тоже самое.

Функция nb.find(id) сперва ищет в документе ноду с заданным id и создает на ней блок. Лучше ей пока не пользоваться, видимо, т.к. я планирую ее расширить, чтобы она принимала селектор, а не id.

Блочные события

Все блоки имеют методы on, off и trigger:

var block = nb.block(...);

var handler = block.on('foo', function(e, params) {
    console.log(e, params);
});

block.trigger('foo', 42);

block.off('foo', handler);

Работа с модификаторами

Сейчас есть методы getMod, setMod, delMod. Но они, видимо, будут переделаны в отдельные функции, работающие с html-нодами, а не с блоками.

Разное

Свойство node:

var node = block.node; // html-нода, на которой инициализирован блок.

Метод data:

var foo = block.data('foo'); // тоже самое, что и block.node.getAttribute('data-nb-foo').
block.data('foo', 42); // тоже самое, что и block.node.setAttribute('data-nb-foo', 42).