Files
simple-bookreader/client/islets/core/y-block/y-block.js
Oleg Mokhov f3546ef3a5 Release
2015-06-20 14:48:34 +05:00

1201 lines
49 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
modules.define(
'y-block',
[
'inherit',
'y-event-emitter',
'y-event-manager',
'y-block-event',
'jquery',
'vow',
'bt',
'y-extend'
],
function (
provide,
inherit,
YEventEmitter,
YEventManager,
YBlockEvent,
$,
vow,
bt,
extend
) {
/**
* @name YBlock
* @augments YEventEmitter
*/
var YBlock = inherit(YEventEmitter, /** @lends YBlock.prototype */ {
/**
* Конструктор базового блока.
* Его следует вызывать с помощью `this.__base` в наследующих классах.
*
* @constructor
* @param {jQuery} [domNode] Элемент, на котором следует инициализировать блок.
* @param {Object} [options] Опции блока. Содержит все декларированные опции BH-шаблона блока.
*
* @example
* modules.define('y-control', ['y-block'], function (provide, YBlock) {
* var YControl = inherit(YBlock, {
* __constructor: function () {
* this.__base.apply(this, arguments);
* // Дополнительные действия по инициализации
* }
* }, {
* getBlockName: function () {
* return 'y-control';
* }
* }));
*
* provide(YControl);
* });
*/
__constructor: function (domNode, options) {
if (domNode !== null && !(domNode instanceof $)) {
options = domNode;
domNode = null;
}
if (!domNode) {
options = options || {};
domNode = this._createDomElement(options);
}
// Если параметры не переданы, извлекаем их из DOM-ноды.
if (!options) {
options = this.__self._getDomNodeOptions(domNode).options || {};
} else if (!options.__complete) {
options = extend(options, this.__self._getDomNodeOptions(domNode).options || {});
}
domNode.addClass(this.__self._autoInitCssClass);
// Сохраняем ссылку на экземпляр блока в jQuery-хранилище ноды.
this.__self._getDomNodeDataStorage(domNode).block = this;
this._initOptions = options;
this._node = domNode;
this._eventManager = new YEventManager(this);
this._stateCache = null;
this.__self._liveInitIfRequired();
this._cachedViewName = null;
},
/**
* Уничтожает блок. При уничтожении блок автоматически отвязывает все обработчики событий,
* которые были привязаны к инстанции блока или привязаны внутри блока, используя метод `_bindTo()`.
*
* После уничтожения блока удаляет его из DOM-дерева.
*
* Этот метод следует перекрывать, если необходимы дополнительные действия при уничтожении блока.
* При этом необходимо вызывать базовую реализацию деструктора с помощью `this.__base()`.
*
* @example
* destruct: function () {
* this._cache.drop();
* this.__base();
* }
*/
destruct: function () {
var nodeStorage;
if (this._node) {
nodeStorage = this.__self._getDomNodeDataStorage(this._node);
}
if (!nodeStorage || !nodeStorage.block) {
throw new Error('Block `' + this.__self.getBlockName() + '` was already destroyed');
}
delete nodeStorage.block;
this.__self.destructDomTree(this.getDomNode());
this.offAll();
this._eventManager.unbindAll();
this._eventManager = null;
this._node.remove();
this._node = null;
this._initOptions = null;
this._stateCache = null;
},
/**
* Возвращает DOM-элемент данного блока.
*
* @returns {jQuery}
*/
getDomNode: function () {
return this._node;
},
/**
* Добавляет обработчик события `event` объекта `emitter`. Контекстом обработчика
* является экземпляр данного блока. Обработчик события автоматически удалится при вызове
* `YBlock.prototype.destruct()`.
*
* @protected
* @param {jQuery|YBlock} emitter
* @param {String} event
* @param {Function} callback
* @returns {YBlock}
*
* @example
* var View = inherit(YBlock, {
* __constructor: function (model) {
* this.__base();
*
* var hide = this._findElement('hide');
* this._bindTo(hide, 'click', this._onHideClick);
*
* this._bindTo(model, 'change-attr', this._onAttrChange);
* }
* });
*/
_bindTo: function (emitter, event, callback) {
this._eventManager.bindTo(emitter, event, callback);
return this;
},
/**
* Удаляет обработчик события `event` объекта `emitter`, добавленный с помощью
* `YBlock.prototype._bindTo()`.
*
* @protected
* @param {jQuery|YBlock} emitter
* @param {String} event
* @param {Function} callback
* @returns {YBlock}
*/
_unbindFrom: function (emitter, event, callback) {
this._eventManager.unbindFrom(emitter, event, callback);
return this;
},
/**
* Исполняет обработчики события `blockEvent` блока. Первым аргументом в обработчики события будет
* передан экземпляр класса `YBlockEvent`.
*
* @param {String|YBlockEvent} blockEvent Имя события или экземпляр класса `YBlockEvent`.
* @param {Object} [data] Дополнительные данные, которые можно получить через `e.data` в обработчике.
* @returns {YBlock}
*
* @example
* var block = new YBlock();
* block.on('click', function (e) {
* console.log(e.type);
* });
*
* block.emit('click'); // => 'click'
*
* var event = new YBlockEvent('click');
* block.emit(event); // => 'click'
*/
emit: function (blockEvent, data) {
if (typeof blockEvent === 'string') {
blockEvent = new YBlockEvent(blockEvent);
}
blockEvent.data = data;
blockEvent.target = this;
this.__base(blockEvent.type, blockEvent);
if (!blockEvent.isPropagationStopped()) {
// Если событие блока надо распространять, кидаем специальное событие на DOM ноде блока.
var jqEvent = $.Event(this.__self._getPropagationEventName(blockEvent.type));
blockEvent._jqEvent = jqEvent;
var domNode = this.getDomNode();
if (domNode) {
this.getDomNode().trigger(jqEvent, blockEvent);
}
}
return this;
},
/**
* Возвращает имя отображения данного блока.
*
* @returns {String|undefined}
*/
getView: function () {
if (this._cachedViewName === null) {
var cls = this.getDomNode().attr('class');
if (cls) {
this._cachedViewName = cls.split(' ').shift().split('_')[1];
} else {
this._cachedViewName = undefined;
}
}
return this._cachedViewName;
},
/**
* Устанавливает CSS-класс по имени и значению состояния.
* Например, для блока `y-button` вызов `this._setState('pressed', 'yes')`
* добавляет CSS-класс с именем `pressed_yes`.
*
* С точки зрения `BEM` похож на метод `setMod`, но не вызывает каких-либо событий.
*
* @protected
* @param {String} stateName Имя состояния.
* @param {String|Boolean} [stateVal=true] Значение.
* Если указан `false` или пустая строка, то CSS-класс удаляется.
* @returns {YBlock}
*/
_setState: function (stateName, stateVal) {
if (arguments.length === 1) {
stateVal = true;
}
stateVal = getStateValue(stateVal);
var domElem = this.getDomNode();
if (!this._stateCache) {
this._stateCache = this._parseStateCssClasses(domElem);
}
var prevStateVal = this._stateCache[stateName] || false;
if (stateVal !== prevStateVal) {
this._stateCache[stateName] = stateVal;
if (prevStateVal) {
domElem.removeClass('_' + stateName + (prevStateVal === true ? '' : '_' + prevStateVal));
}
if (stateVal) {
domElem.addClass('_' + stateName + (stateVal === true ? '' : '_' + stateVal));
}
}
return this;
},
/**
* Удаляет CSS-класс состояния с заданным именем.
* Например, для блока `y-button` вызов `this._removeState('side')`
* удалит CSS-классы с именами `side_left`, `side_right` и т.п.
*
* С точки зрения `BEM` похож на метод `delMod`, но не вызывает каких-либо событий.
*
* @protected
* @param {String} stateName
* @returns {YBlock}
*/
_removeState: function (stateName) {
return this._setState(stateName, false); // false удаляет состояние с указанным именем
},
/**
* Возвращает значение состояния на основе CSS-классов блока.
* Например, для блока `y-button`, у которого на DOM-элементе висит класс `pressed_yes`,
* вызов `this._getState('pressed')` возвратит значение `yes`.
*
* С точки зрения `BEM` похож на метод `getMod`.
*
* @protected
* @param {String} stateName
* @returns {String|Boolean}
*/
_getState: function (stateName) {
if (!this._stateCache) {
this._stateCache = this._parseStateCssClasses(this.getDomNode());
}
return this._stateCache[stateName] || false;
},
/**
* Переключает значение состояния блока (полученное на основе CSS-классов) между двумя значениями.
* Например, для блока `y-button`, у которого на DOM-элементе висит класс `pressed_yes`,
* вызов `this._toggleState('pressed', 'yes', '')` удалит класс `pressed_yes`,
* а повторный вызов — вернет на место.
*
* С точки зрения `BEM` похож на метод `toggleMod`, но не вызывает каких-либо событий.
*
* @protected
* @param {String} stateName
* @param {String|Boolean} stateVal1
* @param {String|Boolean} stateVal2
* @returns {YBlock}
*/
_toggleState: function (stateName, stateVal1, stateVal2) {
stateVal1 = getStateValue(stateVal1);
stateVal2 = getStateValue(stateVal2);
var currentModVal = this._getState(stateName);
if (currentModVal === stateVal1) {
this._setState(stateName, stateVal2);
} else if (currentModVal === stateVal2) {
this._setState(stateName, stateVal1);
}
return this;
},
/**
* Устанавливает CSS-класс для элемента по имени и значению состояния.
* Например, для элемента `text` блока `y-button` вызов
* `this._setElementState(this._findElement('text'), 'pressed', 'yes')`
* добавляет CSS-класс с именем `pressed_yes`.
*
* С точки зрения `BEM` похож на метод `setElemMod`.
*
* @protected
* @param {HTMLElement|jQuery} domNode
* @param {String} stateName Имя состояния.
* @param {String|Boolean} [stateVal=true] Значение.
* Если указан `false` или пустая строка, то CSS-класс удаляется.
* @returns {YBlock}
*/
_setElementState: function (domNode, stateName, stateVal) {
if (domNode) {
domNode = $(domNode);
if (arguments.length === 2) {
stateVal = true;
}
stateVal = getStateValue(stateVal);
var parsedMods = this._parseStateCssClasses(domNode);
var prevModVal = parsedMods[stateName];
if (prevModVal) {
domNode.removeClass('_' + stateName + (prevModVal === true ? '' : '_' + prevModVal));
}
if (stateVal) {
domNode.addClass('_' + stateName + (stateVal === true ? '' : '_' + stateVal));
}
} else {
throw new Error('`domNode` should be specified for `_setElementState` method.');
}
return this;
},
/**
* Удаляет CSS-класс состояния с заданным именем для элемента.
* Например, для элемента `text` блока `y-button` вызов
* `this._removeElementState(this._findElement('text'), 'side')`
* удалит CSS-классы с именами `side_left`, `side_right` и т.п.
*
* С точки зрения `BEM` похож на метод `delElemMod`.
*
* @protected
* @param {HTMLElement|jQuery} domNode
* @param {String} stateName
* @returns {YBlock}
*/
_removeElementState: function (domNode, stateName) {
// false удаляет состояние с указанным именем
return this._setElementState(domNode, stateName, false);
},
/**
* Возвращает значение состояния на основе CSS-классов элемента.
* Например, для элемента `text` блока `y-button`,
* у которого на DOM-элементе висит класс `pressed_yes`, вызов
* `this._getElementState(this._findElement('text'), 'pressed')` возвратит значение `yes`.
*
* С точки зрения `BEM` похож на метод `getElemMod`.
*
* @protected
* @param {HTMLElement|jQuery} domNode
* @param {String} stateName
* @returns {String}
*/
_getElementState: function (domNode, stateName) {
if (domNode) {
domNode = $(domNode);
return this._parseStateCssClasses(domNode)[stateName] || false;
} else {
throw new Error('`domNode` should be specified for `_getElementState` method.');
}
},
/**
* Переключает значение состояния элемента блока (полученное на основе CSS-классов) между двумя значениями.
* Например, для элемента `text` блока `y-button`,
* у которого на DOM-элементе висит класс `pressed_yes`, вызов
* `this._toggleElementState(this._findElement('text'), 'pressed', 'yes', '')`
* удалит класс `pressed_yes`, а повторный вызов — вернет на место.
*
* С точки зрения `BEM` похож на метод `toggleElemMod`.
*
* @protected
* @param {HTMLElement|jQuery} domNode
* @param {String} stateName
* @param {String} stateVal1
* @param {String} stateVal2
* @returns {YBlock}
*/
_toggleElementState: function (domNode, stateName, stateVal1, stateVal2) {
stateVal1 = getStateValue(stateVal1);
stateVal2 = getStateValue(stateVal2);
var currentModVal = this._getElementState(domNode, stateName);
if (currentModVal === stateVal1) {
this._setElementState(domNode, stateName, stateVal2);
} else if (currentModVal === stateVal2) {
this._setElementState(domNode, stateName, stateVal1);
}
return this;
},
/**
* Возвращает первый элемент с указанным именем.
*
* @protected
* @param {String} elementName Имя элемента.
* @param {HTMLElement|jQuery} [parentElement] Элемент в котором необходимо произвести поиск. Если не указан,
* то используется результат `this.getDomNode()`.
* @returns {jQuery|undefined}
*
* @example
* var title = this._findElement('title');
* title.text('Hello World');
*/
_findElement: function (elementName, parentElement) {
return this._findAllElements(elementName, parentElement)[0];
},
/**
* Возвращает все элементы по указанному имени.
*
* @protected
* @param {String} elementName Имя элемента.
* @param {HTMLElement|jQuery} [parentElement] Элемент в котором необходимо произвести поиск. Если не указан,
* то используется результат `this.getDomNode()`.
* @returns {jQuery[]}
*
* @example
* this._findAllElements('item').forEach(function (item) {
* item.text('Item');
* });
*/
_findAllElements: function (elementName, parentElement) {
parentElement = parentElement ? $(parentElement) : this.getDomNode();
var view = this.getView();
var elems = parentElement.find(
'.' + this.__self.getBlockName() + (view ? '_' + view : '') + '__' + elementName
);
var result = [];
var l = elems.length;
for (var i = 0; i < l; i++) {
result.push($(elems[i]));
}
return result;
},
/**
* Возвращает все родительские элементы с заданным именем.
*
* @protected
* @param {String} elementName Имя элемента.
* @param {HTMLElement|jQuery} childElement Элемент, среди родителей которого необходимо произвести поиск.
* @returns {jQuery[]}
*
* @example
* var branches = this._findAllParentElements('branch', item);
*/
_findAllParentElements: function (elementName, childElement) {
if (childElement) {
childElement = $(childElement);
var view = this.getView();
var elems = childElement.parents(
'.' + this.__self.getBlockName() + (view ? '_' + view : '') + '__' + elementName
);
var result = [];
var l = elems.length;
for (var i = 0; i < l; i++) {
result.push($(elems[i]));
}
return result;
} else {
throw new Error('`childElement` should be specified for `_findAllParentElements` method.');
}
},
/**
* Возвращает первый родительский элемент с заданным именем.
*
* @protected
* @param {String} elementName Имя элемента.
* @param {HTMLElement|jQuery} childElement Элемент, среди родителей которого необходимо произвести поиск.
* @returns {jQuery|undefined}
*
* @example
* var branch = this._findParentElement('branch', item);
*/
_findParentElement: function (elementName, childElement) {
if (childElement) {
return this._findAllParentElements(elementName, childElement)[0];
} else {
throw new Error('`childElement` should be specified for `_findParentElement` method.');
}
},
/**
* Возвращает параметры, которые были переданы блоку при инициализации.
*
* @protected
* @returns {Object}
*
* @example
* var control = YControl.fromDomNode(
* $('<div class="y-control _init" onclick="return {\'y-control\':{level:5}}"></div>')
* );
* // control:
* inherit(YBlock, {
* myMethod: function() {
* console.log(this._getOptions().level);
* }
* }, {
* getBlockName: function() {
* return 'y-control';
* }
* });
*/
_getOptions: function () {
return this._initOptions;
},
/**
* Возвращает параметры, которые были переданы элементу блока при инициализации.
*
* @protected
* @param {HTMLElement|jQuery} domNode
* @returns {Object}
*
* @example
* // HTML:
* // <div class="y-control _init">
* // <div class="y-control__text" data-options="{options:{level:5}}"></div>
* // </div>
*
* provide(inherit(YBlock, {
* __constructor: function() {
* this.__base.apply(this, arguments);
* this._textParams = this._getElementOptions(this._findElement('text'));
* }
* }, { getBlockName: function() { return 'y-control'; } }));
*/
_getElementOptions: function (domNode) {
if (domNode) {
domNode = $(domNode);
var elemName = this._getElementName(domNode);
if (elemName) {
return this.__self._getDomNodeOptions(domNode).options || {};
} else {
throw new Error('Unable to get BEM Element name from DOM Node.');
}
} else {
throw new Error('`domNode` should be specified for `_getElementOptions` method.');
}
},
/**
* Создает и возвращает DOM-элемент на основе BH-опций.
* Создание нового элемента осуществляется с помощью применения BH-шаблонов.
*
* @protected
* @param {Object} params
* @returns {jQuery}
*/
_createDomElement: function (params) {
return $(bt.apply(extend({}, params, {block: this.__self.getBlockName()})));
},
/**
* Разбирает состояния DOM-элемента, возвращает объект вида `{stateName: stateVal, ...}`.
*
* @param {jQuery} domNode
* @returns {Object}
*/
_parseStateCssClasses: function (domNode) {
var result = {};
var classAttr = domNode.attr('class');
if (classAttr) {
var classNames = classAttr.split(' ');
for (var i = classNames.length - 1; i >= 0; i--) {
if (classNames[i].charAt(0) === '_') {
var classNameParts = classNames[i].substr(1).split('_');
if (classNameParts.length === 2) {
result[classNameParts[0]] = classNameParts[1];
} else {
result[classNameParts[0]] = true;
}
}
}
}
return result;
},
/**
* Возвращает имя элемента блока на основе DOM-элемента.
*
* @param {jQuery} domNode
* @returns {String|null}
*/
_getElementName: function (domNode) {
var view = this.getView();
var match = (domNode[0].className || '').match(
new RegExp(this.__self.getBlockName() + (view ? '_' + view : '') + '__([a-zA-Z0-9-]+)(?:\\s|$)')
);
return match ? match[1] : null;
}
}, {
/**
* Возвращает имя блока.
* Этот метод следует перекрывать при создании новых блоков.
*
* @static
* @returns {String|null}
*
* @example
* provide(inherit(YBlock, {}, {
* getBlockName: function() {
* return 'my-button';
* }
* });
*/
getBlockName: function () {
return 'y-block';
},
/**
* Возвращает инстанцию блока для переданного DOM-элемента.
*
* @static
* @param {HTMLElement|jQuery} domNode
* @param {Object} [params]
* @returns {YBlock}
*
* @example
* var page = YPage.fromDomNode(document.body);
*/
fromDomNode: function (domNode, params) {
if (!domNode) {
throw new Error('`domNode` should be specified for `findDomNode` method');
}
var blockName = this.getBlockName();
domNode = $(domNode);
if (!domNode.length) {
throw new Error('Cannot initialize "' + blockName + '" from empty jQuery object');
}
var instance = this._getDomNodeDataStorage(domNode).block;
if (!instance) {
if (params === undefined) {
params = this._getDomNodeOptions(domNode).options || {};
}
params.__complete = true;
var BlockClass = this;
instance = new BlockClass(domNode, params);
}
return instance;
},
/**
* Инициализирует блок, если это необходимо.
* Возвращает `null` для блоков с отложенной (`live`) инициализацией и инстанцию блока для прочих.
*
* @static
* @param {HTMLElement|jQuery} domNode
* @param {Object} params
* @returns {YBlock|null}
*/
initOnDomNode: function (domNode, params) {
var initBlock;
if (this._liveInit) {
this._liveInitIfRequired();
initBlock = false;
if (this._instantInitHandlers) {
for (var i = 0, l = this._instantInitHandlers.length; i < l; i++) {
if (this._instantInitHandlers[i](params, domNode)) {
initBlock = true;
break;
}
}
}
} else {
initBlock = true;
}
if (initBlock) {
domNode = $(domNode);
return this.fromDomNode(domNode, params);
} else {
return null;
}
},
/**
* Запускает `live`-инициализацию, если она определена для блока и не была выполнена ранее.
*
* @static
* @protected
*/
_liveInitIfRequired: function () {
var blockName = this.getBlockName();
if (this._liveInit && (!this._liveInitialized || !this._liveInitialized[blockName])) {
this._liveInit();
(this._liveInitialized = this._liveInitialized || {})[blockName] = true;
}
},
/**
* Если для блока требуется отложенная (`live`) инициализация,
* следует перекрыть это свойство статическим методом.
*
* Этот выполняется лишь однажды, при инициализации первого блока на странице.
*
* В рамках `_liveInit` можно пользоваться методами `_liveBind` и `_liveBindToElement` для того,
* чтобы глобально слушать события на блоке и элементе соответственно.
*
* @static
* @protected
* @type {Function|null}
*
* @example
* var MyBlock = inherit(YBlock, {}, {
* _liveInit: function () {
* this._liveBind('click', function(e) {
* this._setState('clicked', 'yes');
* });
* this._liveBindToElement('title', 'click', function(e) {
* this._setElementState($(e.currentTarget), 'clicked', 'yes');
* });
* }
* });
*/
_liveInit: null,
/**
* Отменяет отложенную инициализацию блока по определенному условию.
* Условием служит функция, которая принимает параметры и DOM-элемент блока. Если функция возвращает true,
* то блок инициализируется сразу.
* Рекомендуется для таких случаев передавать нужные параметры, которые сигнализируют о том,
* что блок необходимо инициализировать блок сразу.
*
* @static
* @protected
* @param {Function<Object,jQuery>} condition
*/
_instantInitIf: function (condition) {
if (!this._instantInitHandlers) {
this._instantInitHandlers = [];
}
this._instantInitHandlers.push(condition);
},
/**
* Глобально слушает событие на блоке. Используется при отложенной инициализации.
* Обработчик события выполнится в контексте инстанции блока.
*
* @static
* @protected
* @param {String} eventName
* @param {Function} handler
*/
_liveBind: function (eventName, handler) {
var blockClass = this;
this._getLiveEventsScopeElement().on(eventName, '[data-block="' + this.getBlockName() + '"]', function (e) {
handler.call(blockClass.fromDomNode(e.currentTarget), e);
});
},
/**
* Глобально слушает событие на элементе блока. Используется при отложенной инициализации.
* Обработчик события выполнится в контексте инстанции блока.
*
* @static
* @protected
* @param {String} elementName
* @param {String} eventName
* @param {Function} handler
*/
_liveBindToElement: function (elementName, eventName, handler) {
var blockClass = this;
var blockName = this.getBlockName();
var selectors = [
'[class^="' + blockName + '_"][class$="__' + elementName + '"]',
'[class^="' + blockName + '_"][class*="__' + elementName + ' "]'
];
this._getLiveEventsScopeElement().on(
eventName,
selectors.join(', '),
function (e) {
handler.call(
blockClass.fromDomNode($(e.currentTarget).closest('[data-block="' + blockName + '"]')),
e
);
}
);
},
/**
* Возвращает элемент, на котором будут слушаться глобальные (`live`) события.
*
* @static
* @protected
* @returns {jQuery}
*/
_getLiveEventsScopeElement: function () {
return $(document.body);
},
/**
* Возвращает первую инстанцию блока внутри переданного фрагмента DOM-дерева.
*
* @static
* @param {jQuery|HTMLElement|YBlock} parentElement
* @returns {YBlock|undefined}
*
* @example
* var input = YInput.find(document.body);
* if (input) {
* input.setValue('Hello World');
* } else {
* throw new Error('Input wasn\'t found in "y-control".');
* }
*/
find: function (parentElement) {
return this.findAll(parentElement)[0];
},
/**
* Возвращает все инстанции блока внутри переданного фрагмента DOM-дерева.
*
* @static
* @param {jQuery|HTMLElement|YBlock} parentElement
* @returns {YBlock[]}
*
* @example
* var inputs = YInput.findAll(document.body);
* inputs.forEach(function (input) {
* input.setValue("Input here");
* });
*/
findAll: function (parentElement) {
if (!parentElement) {
throw new Error('`parentElement` should be specified for `findAll` method');
}
parentElement = this._getDomNodeFrom(parentElement);
var domNodes = parentElement.find('[data-block=' + this.getBlockName() + ']');
if (domNodes.length) {
var result = [];
var l = domNodes.length;
for (var i = 0; i < l; i++) {
var domNode = $(domNodes[i]);
result.push(this.fromDomNode(domNode));
}
return result;
} else {
return [];
}
},
/**
* Инициализирует все блоки на переданном фрагменте DOM-дерева.
*
* @static
* @param {HTMLElement|jQuery|YBlock} domNode
* @returns {Promise}
*
* @example
* YBlock.initDomTree(document.body).done(function () {
* YButton.getEmitter(document.body).on('click', function () {
* alert("Button is clicked");
* });
* });
*/
initDomTree: function (domNode) {
if (!domNode) {
throw new Error('`domNode` should be specified for `initDomTree` method');
}
domNode = this._getDomNodeFrom(domNode);
var selector = '.' + this._autoInitCssClass;
var classesToLoad = {};
var nodes = domNode.find(selector);
if (domNode.is(selector)) {
Array.prototype.unshift.call(nodes, domNode);
}
var tasks = [];
var l = nodes.length;
for (var i = 0; i < l; i++) {
var node = $(nodes[i]);
var params = this._getDomNodeOptions(node) || {};
var blockName = node.attr('data-block');
if (blockName) {
tasks.push({
node: node,
className: blockName,
options: params.options || {},
isMixin: false
});
classesToLoad[blockName] = null;
var mixins = params.mixins;
if (mixins) {
for (var j = 0, jl = mixins.length; j < jl; j++) {
var mixinData = mixins[j];
if (mixinData && mixinData.name) {
tasks.push({
node: node,
className: mixinData.name,
blockName: blockName,
options: mixinData,
isMixin: true
});
classesToLoad[mixinData.name] = null;
}
}
}
}
}
function loadModule(moduleName) {
var deferred = vow.defer();
if (modules.isDefined(moduleName)) {
modules.require([moduleName], function (moduleClass) {
classesToLoad[moduleName] = moduleClass;
deferred.resolve();
});
return deferred.promise();
} else {
return null;
}
}
return vow.fulfill().then(function () {
return vow.all(Object.keys(classesToLoad).map(function (className) {
return loadModule(className);
})).then(function () {
var l = tasks.length;
for (var i = 0; i < l; i++) {
var task = tasks[i];
var node = task.node;
var className = task.className;
var options = task.options;
var classDef = classesToLoad[className];
if (classDef) {
try {
if (task.isMixin) {
var blockClass = classesToLoad[task.blockName];
if (blockClass) {
classDef.fromBlock(blockClass.fromDomNode(node), options);
}
} else {
classDef.initOnDomNode(node, options);
}
} catch (e) {
e.message = className + ' init error: ' + e.message;
throw e;
}
}
}
});
});
},
/**
* Уничтожает все инстанции блоков на переданном фрагменте DOM-дерева.
*
* @static
* @param {HTMLElement|jQuery|YBlock} domNode
*/
destructDomTree: function (domNode) {
if (!domNode) {
throw new Error('`domNode` should be specified for `destructDomTree` method');
}
domNode = this._getDomNodeFrom(domNode);
var selector = '.' + this._autoInitCssClass + ',.' + this._delegateEventsCssClass;
var nodes = domNode.find(selector);
if (domNode.is(selector)) {
Array.prototype.unshift.call(nodes, domNode);
}
for (var i = 0; i < nodes.length; i++) {
var node = $(nodes[i]);
var nodeStorage = this._getDomNodeDataStorage(node, true);
if (nodeStorage) {
if (nodeStorage.block) {
nodeStorage.block.destruct();
}
var blockEvents = nodeStorage.blockEvents;
var blockName;
for (blockName in blockEvents) {
if (blockEvents.hasOwnProperty(blockName)) {
blockEvents[blockName].offAll();
}
}
nodeStorage.blockEvents = {};
}
}
},
/**
* Возвращает эмиттер событий блока для переданного DOM-элемента.
* На полученном эмиттере можно слушать блочные события, которые будут всплывать до этого DOM-элемента.
*
* @static
* @param {HTMLElement|jQuery|YBlock} domNode
* @returns {YEventEmitter}
*
* @example
* YButton.getEmitter(document.body).on('click', function () {
* alert('Button is clicked');
* });
*/
getEmitter: function (domNode) {
domNode = this._getDomNodeFrom(domNode);
var nodeStorage = this._getDomNodeDataStorage(domNode);
var blockName = this.getBlockName();
var emitter = nodeStorage.blockEvents[blockName];
if (!emitter) {
domNode.addClass(this._delegateEventsCssClass);
emitter = new YBlockEventEmitter(this, domNode);
nodeStorage.blockEvents[blockName] = emitter;
}
return emitter;
},
/**
* Возвращает jQuery DOM-элемент используя HTMLElement, инстанцию блока или другой jQuery-элемент.
*
* @static
* @protected
* @param {jQuery|HTMLElement|YBlock} domNode
* @returns {YBlock}
*/
_getDomNodeFrom: function (domNode) {
if (domNode) {
if (domNode instanceof YBlock) {
domNode = domNode.getDomNode();
}
domNode = $(domNode);
} else {
throw new Error('jQuery element, DOM Element or YBlock instance should be specified');
}
return domNode;
},
/**
* Возвращает опции блока или элемента на указанном DOM-элементе.
*
* @static
* @param {jQuery} domNode
*/
_getDomNodeOptions: function (domNode) {
var options = domNode.attr('data-options');
return options ? JSON.parse(options) : {};
},
/**
* Возвращает хранилище данных для DOM-элемента.
*
* @static
* @param {jQuery} domNode
* @param {Boolean} [skipCreating]
* @returns {Object}
*/
_getDomNodeDataStorage: function (domNode, skipCreating) {
var data = domNode.data('y-block');
if (!data && !skipCreating) {
data = {
blockEvents: {}
};
domNode.data('y-block', data);
}
return data;
},
/**
* Возвращает специальное имя события, которое используется для распространения события блока по DOM дереву.
*
* @static
* @param {String} eventName Имя события блока.
* @returns {String}
*/
_getPropagationEventName: function (eventName) {
return 'y-block/' + this.getBlockName() + '/' + eventName;
},
/**
* CSS-класс для автоматической инициализации.
*
* @static
* @type {String}
*/
_autoInitCssClass: '_init',
/**
* CSS-класс для делегирования событий.
*
* @static
* @type {String}
*/
_delegateEventsCssClass: '_live-events'
});
/**
* Эмиттер, используемый для делегирования событий блока.
*
* Делегирование событий блока происходит следующим образом:
* - Когда блок инициирует событие `eventName`, он также инциирует событие `y-block/blockName/eventName`
* на DOM ноде блока. Это событие распространяется вверх по DOM дереву.
*
* - При добавлении нового события в `YBlockEventEmitter`, для переданной DOM ноды добавляется обработчик события
* `y-block/blockName/eventName`, который инициирует в эмиттере событие `eventName`.
*
* - При удалении события из `YBlockEventEmitter`, соответствующий обработчик удаляется из DOM ноды. Тем самым
* прекращается делегирование.
*/
var YBlockEventEmitter = inherit(YEventEmitter, {
/**
* Создает эмиттер событий, который позволяет слушать события экземпляров блока `blockClass`
* на DOM ноде `domNode`.
*
* @param {Function} blockClass
* @param {jQuery} domNode
*/
__constructor: function (blockClass, domNode) {
this._blockClass = blockClass;
this._domNode = domNode;
this._listeners = {};
},
_onAddEvent: function (eventName) {
var _this = this;
function listener(jqEvent, blockEvent) {
_this.emit(eventName, blockEvent);
if (blockEvent.isPropagationStopped()) {
jqEvent.stopPropagation();
}
}
var propagationEventName = this._blockClass._getPropagationEventName(eventName);
this._domNode.on(propagationEventName, listener);
this._listeners[eventName] = listener;
},
_onRemoveEvent: function (eventName) {
var propagationEventName = this._blockClass._getPropagationEventName(eventName);
this._domNode.off(propagationEventName, this._listeners[eventName]);
delete this._listeners[eventName];
}
});
function getStateValue(stateVal) {
if (typeof stateVal === 'string') {
if (stateVal === '') {
stateVal = false;
}
} else {
if (typeof stateVal === 'number') {
stateVal = String(stateVal);
} else {
stateVal = Boolean(stateVal);
}
}
return stateVal;
}
provide(YBlock);
});