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( * $('
') * ); * // 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: * //
* //
* //
* * 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} 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); });