/** @requires BEM */ /** @requires BEM.INTERNAL */ (function(BEM, $, undefined) { var win = $(window), doc = $(document), /** * Storage for DOM elements by unique key * @private * @type Object */ uniqIdToDomElems = {}, /** * Storage for blocks by unique key * @static * @private * @type Object */ uniqIdToBlock = {}, /** * Storage for block parameters * @private * @type Object */ domElemToParams = {}, /** * Storage for liveCtx event handlers * @private * @type Object */ liveEventCtxStorage = {}, /** * Storage for liveClass event handlers * @private * @type Object */ liveClassEventStorage = {}, blocks = BEM.blocks, INTERNAL = BEM.INTERNAL, NAME_PATTERN = INTERNAL.NAME_PATTERN, MOD_DELIM = INTERNAL.MOD_DELIM, ELEM_DELIM = INTERNAL.ELEM_DELIM, buildModPostfix = INTERNAL.buildModPostfix, buildClass = INTERNAL.buildClass; /** * Initializes blocks on a DOM element * @private * @param {jQuery} domElem DOM element * @param {String} uniqInitId ID of the "initialization wave" */ function init(domElem, uniqInitId) { var domNode = domElem[0]; $.each(getParams(domNode), function(blockName, params) { processParams(params, domNode, blockName, uniqInitId); var block = uniqIdToBlock[params.uniqId]; if(block) { if(block.domElem.index(domNode) < 0) { block.domElem = block.domElem.add(domElem); $.extend(block._params, params); } } else { initBlock(blockName, domElem, params); } }); } /** * Initializes a specific block on a DOM element, or returns the existing block if it was already created * @private * @param {String} blockName Block name * @param {jQuery} domElem DOM element * @param {Object} [params] Initialization parameters * @param {Boolean} [forceLive] Force live initialization * @param {Function} [callback] Handler to call after complete initialization */ function initBlock(blockName, domElem, params, forceLive, callback) { if(typeof params == 'boolean') { callback = forceLive; forceLive = params; params = undefined; } var domNode = domElem[0]; params = processParams(params || getParams(domNode)[blockName], domNode, blockName); var uniqId = params.uniqId; if(uniqIdToBlock[uniqId]) { return uniqIdToBlock[uniqId]._init(); } uniqIdToDomElems[uniqId] = uniqIdToDomElems[uniqId]? uniqIdToDomElems[uniqId].add(domElem) : domElem; var parentDomNode = domNode.parentNode; if(!parentDomNode || parentDomNode.nodeType === 11) { // jquery doesn't unique disconnected node $.unique(uniqIdToDomElems[uniqId]); } var blockClass = blocks[blockName] || DOM.decl(blockName, {}, { live : true }); if(!(blockClass._liveInitable = !!blockClass._processLive()) || forceLive || params.live === false) { var block = new blockClass(uniqIdToDomElems[uniqId], params, !!forceLive); delete uniqIdToDomElems[uniqId]; callback && callback.apply(block, Array.prototype.slice.call(arguments, 4)); return block; } } /** * Processes and adds necessary block parameters * @private * @param {Object} params Initialization parameters * @param {HTMLElement} domNode DOM node * @param {String} blockName Block name * @param {String} [uniqInitId] ID of the "initialization wave" */ function processParams(params, domNode, blockName, uniqInitId) { (params || (params = {})).uniqId || (params.uniqId = (params.id? blockName + '-id-' + params.id : $.identify()) + (uniqInitId || $.identify())); var domUniqId = $.identify(domNode), domParams = domElemToParams[domUniqId] || (domElemToParams[domUniqId] = {}); domParams[blockName] || (domParams[blockName] = params); return params; } /** * Helper for searching for a DOM element using a selector inside the context, including the context itself * @private * @param {jQuery} ctx Context * @param {String} selector CSS selector * @param {Boolean} [excludeSelf=false] Exclude context from search * @returns {jQuery} */ function findDomElem(ctx, selector, excludeSelf) { var res = ctx.find(selector); return excludeSelf? res : res.add(ctx.filter(selector)); } /** * Returns parameters of a block's DOM element * @private * @param {HTMLElement} domNode DOM node * @returns {Object} */ function getParams(domNode) { var uniqId = $.identify(domNode); return domElemToParams[uniqId] || (domElemToParams[uniqId] = extractParams(domNode)); } /** * Retrieves block parameters from a DOM element * @private * @param {HTMLElement} domNode DOM node * @returns {Object} */ function extractParams(domNode) { var fn = domNode.onclick || domNode.ondblclick; if(!fn && domNode.tagName.toLowerCase() == 'body') { // LEGO-2027 in FF onclick doesn't work on body var elem = $(domNode), attr = elem.attr('onclick') || elem.attr('ondblclick'); attr && (fn = Function(attr)); } return fn? fn() : {}; } /** * Cleans up all the BEM storages associated with a DOM node * @private * @param {HTMLElement} domNode DOM node */ function cleanupDomNode(domNode) { delete domElemToParams[$.identify(domNode)]; } /** * Uncople DOM node from the block. If this is the last node, then destroys the block. * @private * @param {BEM.DOM} block block * @param {HTMLElement} domNode DOM node */ function removeDomNodeFromBlock(block, domNode) { block.domElem.length === 1? block.destruct(true) : block.domElem = block.domElem.not(domNode); } /** * Returns a DOM node for calculating the window size in IE * @returns {HTMLElement} */ function getClientNode() { return doc[0][$.support.boxModel? 'documentElement' : 'body']; } /** * Returns a block on a DOM element and initializes it if necessary * @param {String} blockName Block name * @param {Object} params Block parameters * @returns {BEM} */ $.fn.bem = function(blockName, params) { return initBlock(blockName, this, params, true); }; /** * @namespace * @name BEM.DOM */ var DOM = BEM.DOM = BEM.decl('i-bem__dom',/** @lends BEM.DOM.prototype */{ /** * @class Base block for creating BEM blocks that have DOM representation * @constructs * @private * @param {jQuery} domElem DOM element that the block is created on * @param {Object} params Block parameters * @param {Boolean} [initImmediately=true] */ __constructor : function(domElem, params, initImmediately) { var _this = this; /** * Block's DOM elements * @protected * @type jQuery */ _this.domElem = domElem; /** * Cache for names of events on DOM elements * @private * @type Object */ _this._eventNameCache = {}; /** * Cache for elements * @private * @type Object */ _this._elemCache = {}; /** * Unique block ID * @private * @type String */ uniqIdToBlock[_this._uniqId = params.uniqId || $.identify(_this)] = _this; /** * Flag for whether it's necessary to unbind from the document and window when destroying the block * @private * @type Boolean */ _this._needSpecialUnbind = false; _this.__base(null, params, initImmediately); }, /** * Finds blocks inside the current block or its elements (including context) * @protected * @param {String|jQuery} [elem] Block element * @param {String|Object} block Name or description (block,modName,modVal) of the block to find * @returns {BEM[]} */ findBlocksInside : function(elem, block) { return this._findBlocks('find', elem, block); }, /** * Finds the first block inside the current block or its elements (including context) * @protected * @param {String|jQuery} [elem] Block element * @param {String|Object} block Name or description (block,modName,modVal) of the block to find * @returns {BEM} */ findBlockInside : function(elem, block) { return this._findBlocks('find', elem, block, true); }, /** * Finds blocks outside the current block or its elements (including context) * @protected * @param {String|jQuery} [elem] Block element * @param {String|Object} block Name or description (block,modName,modVal) of the block to find * @returns {BEM[]} */ findBlocksOutside : function(elem, block) { return this._findBlocks('parents', elem, block); }, /** * Finds the first block outside the current block or its elements (including context) * @protected * @param {String|jQuery} [elem] Block element * @param {String|Object} block Name or description (block,modName,modVal) of the block to find * @returns {BEM} */ findBlockOutside : function(elem, block) { return this._findBlocks('closest', elem, block)[0] || null; }, /** * Finds blocks on DOM elements of the current block or its elements * @protected * @param {String|jQuery} [elem] Block element * @param {String|Object} block Name or description (block,modName,modVal) of the block to find * @returns {BEM[]} */ findBlocksOn : function(elem, block) { return this._findBlocks('', elem, block); }, /** * Finds the first block on DOM elements of the current block or its elements * @protected * @param {String|jQuery} [elem] Block element * @param {String|Object} block Name or description (block,modName,modVal) of the block to find * @returns {BEM} */ findBlockOn : function(elem, block) { return this._findBlocks('', elem, block, true); }, _findBlocks : function(select, elem, block, onlyFirst) { if(!block) { block = elem; elem = undefined; } var ctxElem = elem? (typeof elem == 'string'? this.findElem(elem) : elem) : this.domElem, isSimpleBlock = typeof block == 'string', blockName = isSimpleBlock? block : (block.block || block.blockName), selector = '.' + (isSimpleBlock? buildClass(blockName) : buildClass(blockName, block.modName, block.modVal)) + (onlyFirst? ':first' : ''), domElems = ctxElem.filter(selector); select && (domElems = domElems.add(ctxElem[select](selector))); if(onlyFirst) { return domElems[0]? initBlock(blockName, domElems.eq(0), true) : null; } var res = [], uniqIds = {}; $.each(domElems, function(i, domElem) { var block = initBlock(blockName, $(domElem), true); if(!uniqIds[block._uniqId]) { uniqIds[block._uniqId] = true; res.push(block); } }); return res; }, /** * Adds an event handler for any DOM element * @protected * @param {jQuery} domElem DOM element where the event will be listened for * @param {String|Object} event Event name or event object * @param {Function} fn Handler function, which will be executed in the block's context * @returns {BEM} */ bindToDomElem : function(domElem, event, fn) { var _this = this; fn? domElem.bind( _this._buildEventName(event), function(e) { (e.data || (e.data = {})).domElem = $(this); return fn.apply(_this, arguments); } ) : $.each(event, function(event, fn) { _this.bindToDomElem(domElem, event, fn); }); return _this; }, /** * Adds an event handler to the document * @protected * @param {String} event Event name * @param {Function} fn Handler function, which will be executed in the block's context * @returns {BEM} */ bindToDoc : function(event, fn) { this._needSpecialUnbind = true; return this.bindToDomElem(doc, event, fn); }, /** * Adds an event handler to the window * @protected * @param {String} event Event name * @param {Function} fn Handler function, which will be executed in the block's context * @returns {BEM} */ bindToWin : function(event, fn) { this._needSpecialUnbind = true; return this.bindToDomElem(win, event, fn); }, /** * Adds an event handler to the block's main DOM elements or its nested elements * @protected * @param {jQuery|String} [elem] Element * @param {String} event Event name * @param {Function} fn Handler function, which will be executed in the block's context * @returns {BEM} */ bindTo : function(elem, event, fn) { if(!event || $.isFunction(event)) { // if there is no element fn = event; event = elem; elem = this.domElem; } else if(typeof elem == 'string') { elem = this.elem(elem); } return this.bindToDomElem(elem, event, fn); }, /** * Removes event handlers from any DOM element * @protected * @param {jQuery} domElem DOM element where the event was being listened for * @param {String} event Event name * @returns {BEM} */ unbindFromDomElem : function(domElem, event) { domElem.unbind(this._buildEventName(event)); return this; }, /** * Removes event handler from document * @protected * @param {String} event Event name * @returns {BEM} */ unbindFromDoc : function(event) { return this.unbindFromDomElem(doc, event); }, /** * Removes event handler from window * @protected * @param {String} event Event name * @returns {BEM} */ unbindFromWin : function(event) { return this.unbindFromDomElem(win, event); }, /** * Removes event handlers from the block's main DOM elements or its nested elements * @protected * @param {jQuery|String} [elem] Nested element * @param {String} event Event name * @returns {BEM} */ unbindFrom : function(elem, event) { if(!event) { event = elem; elem = this.domElem; } else if(typeof elem == 'string') { elem = this.elem(elem); } return this.unbindFromDomElem(elem, event); }, /** * Builds a full name for an event * @private * @param {String} event Event name * @returns {String} */ _buildEventName : function(event) { var _this = this; return event.indexOf(' ') > 1? event.split(' ').map(function(e) { return _this._buildOneEventName(e); }).join(' ') : _this._buildOneEventName(event); }, /** * Builds a full name for a single event * @private * @param {String} event Event name * @returns {String} */ _buildOneEventName : function(event) { var _this = this, eventNameCache = _this._eventNameCache; if(event in eventNameCache) return eventNameCache[event]; var uniq = '.' + _this._uniqId; if(event.indexOf('.') < 0) return eventNameCache[event] = event + uniq; var lego = '.bem_' + _this.__self._name; return eventNameCache[event] = event.split('.').map(function(e, i) { return i == 0? e + lego : lego + '_' + e; }).join('') + uniq; }, /** * Triggers block event handlers and live event handlers * @protected * @param {String} e Event name * @param {Object} [data] Additional information * @returns {BEM} */ trigger : function(e, data) { this .__base(e = this.buildEvent(e), data) .domElem && this._ctxTrigger(e, data); return this; }, _ctxTrigger : function(e, data) { var _this = this, storage = liveEventCtxStorage[_this.__self._buildCtxEventName(e.type)], ctxIds = {}; storage && _this.domElem.each(function() { var ctx = this, counter = storage.counter; while(ctx && counter) { var ctxId = $.identify(ctx, true); if(ctxId) { if(ctxIds[ctxId]) break; var storageCtx = storage.ctxs[ctxId]; if(storageCtx) { $.each(storageCtx, function(uniqId, handler) { handler.fn.call( handler.ctx || _this, e, data); }); counter--; } ctxIds[ctxId] = true; } ctx = ctx.parentNode; } }); }, /** * Sets a modifier for a block/nested element * @protected * @param {jQuery} [elem] Nested element * @param {String} modName Modifier name * @param {String} modVal Modifier value * @returns {BEM} */ setMod : function(elem, modName, modVal) { if(elem && typeof modVal != 'undefined' && elem.length > 1) { var _this = this; elem.each(function() { var item = $(this); item.__bemElemName = elem.__bemElemName; _this.setMod(item, modName, modVal); }); return _this; } return this.__base(elem, modName, modVal); }, /** * Retrieves modifier value from the DOM node's CSS class * @private * @param {String} modName Modifier name * @param {jQuery} [elem] Nested element * @param {String} [elemName] Name of the nested element * @returns {String} Modifier value */ _extractModVal : function(modName, elem, elemName) { var domNode = (elem || this.domElem)[0], matches; domNode && (matches = domNode.className .match(this.__self._buildModValRE(modName, elemName || elem))); return matches? matches[2] : ''; }, /** * Retrieves a name/value list of modifiers * @private * @param {Array} [modNames] Names of modifiers * @param {Object} [elem] Element * @returns {Object} Hash of modifier values by names */ _extractMods : function(modNames, elem) { var res = {}, extractAll = !modNames.length, countMatched = 0; ((elem || this.domElem)[0].className .match(this.__self._buildModValRE( '(' + (extractAll? NAME_PATTERN : modNames.join('|')) + ')', elem, 'g')) || []).forEach(function(className) { var iModVal = (className = className.trim()).lastIndexOf(MOD_DELIM), iModName = className.substr(0, iModVal - 1).lastIndexOf(MOD_DELIM); res[className.substr(iModName + 1, iModVal - iModName - 1)] = className.substr(iModVal + 1); ++countMatched; }); // empty modifier values are not reflected in classes; they must be filled with empty values countMatched < modNames.length && modNames.forEach(function(modName) { modName in res || (res[modName] = ''); }); return res; }, /** * Sets a modifier's CSS class for a block's DOM element or nested element * @private * @param {String} modName Modifier name * @param {String} modVal Modifier value * @param {String} oldModVal Old modifier value * @param {jQuery} [elem] Element * @param {String} [elemName] Element name */ _afterSetMod : function(modName, modVal, oldModVal, elem, elemName) { var _self = this.__self, classPrefix = _self._buildModClassPrefix(modName, elemName), classRE = _self._buildModValRE(modName, elemName), needDel = modVal === ''; (elem || this.domElem).each(function() { var className = this.className; className.indexOf(classPrefix) > -1? this.className = className.replace( classRE, (needDel? '' : '$1' + classPrefix + modVal)) : needDel || $(this).addClass(classPrefix + modVal); }); elemName && this .dropElemCache(elemName, modName, oldModVal) .dropElemCache(elemName, modName, modVal); }, /** * Finds elements nested in a block * @protected * @param {String|jQuery} [ctx=this.domElem] Element where search is being performed * @param {String} names Nested element name (or names separated by spaces) * @param {String} [modName] Modifier name * @param {String} [modVal] Modifier value * @returns {jQuery} DOM elements */ findElem : function(ctx, names, modName, modVal) { if(arguments.length % 2) { // if the number of arguments is one or three modVal = modName; modName = names; names = ctx; ctx = this.domElem; } else if(typeof ctx == 'string') { ctx = this.findElem(ctx); } var _self = this.__self, selector = '.' + names.split(' ').map(function(name) { return buildClass(_self._name, name, modName, modVal); }).join(',.'); return findDomElem(ctx, selector); }, /** * Finds elements nested in a block * @protected * @param {String} name Nested element name * @param {String} [modName] Modifier name * @param {String} [modVal] Modifier value * @returns {jQuery} DOM elements */ _elem : function(name, modName, modVal) { var key = name + buildModPostfix(modName, modVal), res; if(!(res = this._elemCache[key])) { res = this._elemCache[key] = this.findElem(name, modName, modVal); res.__bemElemName = name; } return res; }, /** * Lazy search for elements nested in a block (caches results) * @protected * @param {String} names Nested element name (or names separated by spaces) * @param {String} [modName] Modifier name * @param {String} [modVal] Modifier value * @returns {jQuery} DOM elements */ elem : function(names, modName, modVal) { if(modName && typeof modName != 'string') { modName.__bemElemName = names; return modName; } if(names.indexOf(' ') < 0) { return this._elem(names, modName, modVal); } var res = $([]), _this = this; names.split(' ').forEach(function(name) { res = res.add(_this._elem(name, modName, modVal)); }); return res; }, /** * Clearing the cache for elements * @protected * @param {String} names Nested element name (or names separated by spaces) * @param {String} [modName] Modifier name * @param {String} [modVal] Modifier value * @returns {BEM} */ dropElemCache : function(names, modName, modVal) { if(names) { var _this = this, modPostfix = buildModPostfix(modName, modVal); names.indexOf(' ') < 0? delete _this._elemCache[names + modPostfix] : names.split(' ').forEach(function(name) { delete _this._elemCache[name + modPostfix]; }); } else { this._elemCache = {}; } return this; }, /** * Retrieves parameters of a block element * @param {String|jQuery} elem Element * @returns {Object} Parameters */ elemParams : function(elem) { var elemName; if(typeof elem == 'string') { elemName = elem; elem = this.elem(elem); } else { elemName = this.__self._extractElemNameFrom(elem); } return extractParams(elem[0])[buildClass(this.__self.getName(), elemName)] || {}; }, /** * Checks whether a DOM element is in a block * @protected * @param {jQuery} domElem DOM element * @returns {Boolean} */ containsDomElem : function(domElem) { var res = false; this.domElem.each(function() { return !(res = domElem.parents().andSelf().index(this) > -1); }); return res; }, /** * Builds a CSS selector corresponding to a block/element and modifier * @param {String} [elem] Element name * @param {String} [modName] Modifier name * @param {String} [modVal] Modifier value * @returns {String} */ buildSelector : function(elem, modName, modVal) { return this.__self.buildSelector(elem, modName, modVal); }, /** * Deletes a block * @param {Boolean} [keepDOM=false] Whether to keep the block's DOM nodes in the document */ destruct : function(keepDOM) { var _this = this, _self = _this.__self; _this._isDestructing = true; _this._needSpecialUnbind && _self.doc.add(_self.win).unbind('.' + _this._uniqId); _this.dropElemCache().domElem.each(function(i, domNode) { var params = getParams(domNode); $.each(params, function(blockName, blockParams) { var block = uniqIdToBlock[blockParams.uniqId]; if(block) { if(!block._isDestructing) { removeDomNodeFromBlock(block, domNode); delete params[blockName]; } } else { delete uniqIdToDomElems[blockParams.uniqId]; } }); $.isEmptyObject(params) && cleanupDomNode(domNode); }); keepDOM || _this.domElem.remove(); delete uniqIdToBlock[_this.un()._uniqId]; delete _this.domElem; delete _this._elemCache; _this.__base(); } }, /** @lends BEM.DOM */{ /** * Document shortcut * @protected * @type jQuery */ doc : doc, /** * Window shortcut * @protected * @type jQuery */ win : win, /** * Processes a block's live properties * @private * @param {Boolean} [heedLive=false] Whether to take into account that the block already processed its live properties * @returns {Boolean} Whether the block is a live block */ _processLive : function(heedLive) { var _this = this, res = _this._liveInitable; if('live' in _this) { var noLive = typeof res == 'undefined'; if(noLive ^ heedLive) { res = _this.live() !== false; _this.live = function() {}; } } return res; }, /** * Initializes blocks on a fragment of the DOM tree * @static * @protected * @param {jQuery} [ctx=document] Root DOM node * @returns {jQuery} ctx Initialization context */ init : function(ctx, callback, callbackCtx) { if(!ctx || $.isFunction(ctx)) { callbackCtx = callback; callback = ctx; ctx = doc; } var uniqInitId = $.identify(); findDomElem(ctx, '.i-bem').each(function() { init($(this), uniqInitId); }); callback && this.afterCurrentEvent( function() { callback.call(callbackCtx || this, ctx); }); // makes initialization completely synchronous this._runAfterCurrentEventFns(); return ctx; }, /** * Destroys blocks on a fragment of the DOM tree * @static * @protected * @param {Boolean} [keepDOM=false] Whether to keep DOM nodes in the document * @param {jQuery} ctx Root DOM node * @param {Boolean} [excludeSelf=false] Exclude the context */ destruct : function(keepDOM, ctx, excludeSelf) { if(typeof keepDOM != 'boolean') { excludeSelf = ctx; ctx = keepDOM; keepDOM = undefined; } findDomElem(ctx, '.i-bem', excludeSelf).each(function(i, domNode) { var params = getParams(this); $.each(params, function(blockName, blockParams) { if(blockParams.uniqId) { var block = uniqIdToBlock[blockParams.uniqId]; if(block) { removeDomNodeFromBlock(block, domNode); delete params[blockName]; } else { delete uniqIdToDomElems[blockParams.uniqId]; } } }); $.isEmptyObject(params) && cleanupDomNode(this); }); keepDOM || (excludeSelf? ctx.empty() : ctx.remove()); }, /** * Replaces a fragment of the DOM tree inside the context, destroying old blocks and intializing new ones * @static * @protected * @param {jQuery} ctx Root DOM node * @param {jQuery|String} content New content * @param {Function} [callback] Handler to be called after initialization * @param {Object} [callbackCtx] Handler's context */ update : function(ctx, content, callback, callbackCtx) { this.destruct(ctx, true); this.init(ctx.html(content), callback, callbackCtx); }, /** * Changes a fragment of the DOM tree including the context and initializes blocks. * @param {jQuery} ctx Root DOM node * @param {jQuery|String} content Content to be added */ replace : function(ctx, content) { this.destruct(true, ctx); this.init($(content).replaceAll(ctx)); }, /** * Adds a fragment of the DOM tree at the end of the context and initializes blocks * @param {jQuery} ctx Root DOM node * @param {jQuery|String} content Content to be added */ append : function(ctx, content) { this.init($(content).appendTo(ctx)); }, /** * Adds a fragment of the DOM tree at the beginning of the context and initializes blocks * @param {jQuery} ctx Root DOM node * @param {jQuery|String} content Content to be added */ prepend : function(ctx, content) { this.init($(content).prependTo(ctx)); }, /** * Adds a fragment of the DOM tree before the context and initializes blocks * @param {jQuery} ctx Contextual DOM node * @param {jQuery|String} content Content to be added */ before : function(ctx, content) { this.init($(content).insertBefore(ctx)); }, /** * Adds a fragment of the DOM tree after the context and initializes blocks * @param {jQuery} ctx Contextual DOM node * @param {jQuery|String} content Content to be added */ after : function(ctx, content) { this.init($(content).insertAfter(ctx)); }, /** * Builds a full name for a live event * @static * @private * @param {String} e Event name * @returns {String} */ _buildCtxEventName : function(e) { return this._name + ':' + e; }, _liveClassBind : function(className, e, callback, invokeOnInit) { var _this = this; if(e.indexOf(' ') > -1) { e.split(' ').forEach(function(e) { _this._liveClassBind(className, e, callback, invokeOnInit); }); } else { var storage = liveClassEventStorage[e], uniqId = $.identify(callback); if(!storage) { storage = liveClassEventStorage[e] = {}; doc.bind(e, _this.changeThis(_this._liveClassTrigger, _this)); } storage = storage[className] || (storage[className] = { uniqIds : {}, fns : [] }); if(!(uniqId in storage.uniqIds)) { storage.fns.push({ uniqId : uniqId, fn : _this._buildLiveEventFn(callback, invokeOnInit) }); storage.uniqIds[uniqId] = storage.fns.length - 1; } } return this; }, _liveClassUnbind : function(className, e, callback) { var storage = liveClassEventStorage[e]; if(storage) { if(callback) { if(storage = storage[className]) { var uniqId = $.identify(callback); if(uniqId in storage.uniqIds) { var i = storage.uniqIds[uniqId], len = storage.fns.length - 1; storage.fns.splice(i, 1); while(i < len) storage.uniqIds[storage.fns[i++].uniqId] = i - 1; delete storage.uniqIds[uniqId]; } } } else { delete storage[className]; } } return this; }, _liveClassTrigger : function(e) { var storage = liveClassEventStorage[e.type]; if(storage) { var node = e.target, classNames = []; for(var className in storage) storage.hasOwnProperty(className) && classNames.push(className); do { var nodeClassName = ' ' + node.className + ' ', i = 0; while(className = classNames[i++]) { if(nodeClassName.indexOf(' ' + className + ' ') > -1) { var j = 0, fns = storage[className].fns, fn, stopPropagationAndPreventDefault = false; while(fn = fns[j++]) if(fn.fn.call($(node), e) === false) stopPropagationAndPreventDefault = true; stopPropagationAndPreventDefault && e.preventDefault(); if(stopPropagationAndPreventDefault || e.isPropagationStopped()) return; classNames.splice(--i, 1); } } } while(classNames.length && (node = node.parentNode)); } }, _buildLiveEventFn : function(callback, invokeOnInit) { var _this = this; return function(e) { var args = [ _this._name, ((e.data || (e.data = {})).domElem = $(this)).closest(_this.buildSelector()), true ], block = initBlock.apply(null, invokeOnInit? args.concat([callback, e]) : args); if(block && !invokeOnInit && callback) return callback.apply(block, arguments); }; }, /** * Helper for live initialization for an event on DOM elements of a block or its elements * @static * @protected * @param {String} [elemName] Element name or names (separated by spaces) * @param {String} event Event name * @param {Function} [callback] Handler to call after successful initialization */ liveInitOnEvent : function(elemName, event, callback) { return this.liveBindTo(elemName, event, callback, true); }, /** * Helper for subscribing to live events on DOM elements of a block or its elements * @static * @protected * @param {String|Object} [to] Description (object with modName, modVal, elem) or name of the element or elements (space-separated) * @param {String} event Event name * @param {Function} [callback] Handler */ liveBindTo : function(to, event, callback, invokeOnInit) { if(!event || $.isFunction(event)) { callback = event; event = to; to = undefined; } if(!to || typeof to == 'string') { to = { elem : to }; } to.elemName && (to.elem = to.elemName); var _this = this; if(to.elem && to.elem.indexOf(' ') > 1) { to.elem.split(' ').forEach(function(elem) { _this._liveClassBind( buildClass(_this._name, elem, to.modName, to.modVal), event, callback, invokeOnInit); }); return _this; } return _this._liveClassBind( buildClass(_this._name, to.elem, to.modName, to.modVal), event, callback, invokeOnInit); }, /** * Helper for unsubscribing from live events on DOM elements of a block or its elements * @static * @protected * @param {String} [elem] Name of the element or elements (space-separated) * @param {String} event Event name * @param {Function} [callback] Handler */ liveUnbindFrom : function(elem, event, callback) { var _this = this; if(elem.indexOf(' ') > 1) { elem.split(' ').forEach(function(elem) { _this._liveClassUnbind( buildClass(_this._name, elem), event, callback); }); return _this; } return _this._liveClassUnbind( buildClass(_this._name, elem), event, callback); }, /** * Helper for live initialization when a different block is initialized * @static * @private * @param {String} event Event name * @param {String} blockName Name of the block that should trigger a reaction when initialized * @param {Function} callback Handler to be called after successful initialization in the new block's context * @param {String} findFnName Name of the method for searching */ _liveInitOnBlockEvent : function(event, blockName, callback, findFnName) { var name = this._name; blocks[blockName].on(event, function(e) { var args = arguments, blocks = e.block[findFnName](name); callback && blocks.forEach(function(block) { callback.apply(block, args); }); }); return this; }, /** * Helper for live initialization for a different block's event on the current block's DOM element * @static * @protected * @param {String} event Event name * @param {String} blockName Name of the block that should trigger a reaction when initialized * @param {Function} callback Handler to be called after successful initialization in the new block's context */ liveInitOnBlockEvent : function(event, blockName, callback) { return this._liveInitOnBlockEvent(event, blockName, callback, 'findBlocksOn'); }, /** * Helper for live initialization for a different block's event inside the current block * @static * @protected * @param {String} event Event name * @param {String} blockName Name of the block that should trigger a reaction when initialized * @param {Function} [callback] Handler to be called after successful initialization in the new block's context */ liveInitOnBlockInsideEvent : function(event, blockName, callback) { return this._liveInitOnBlockEvent(event, blockName, callback, 'findBlocksOutside'); }, /** * Helper for live initialization when a different block is initialized on a DOM element of the current block * @deprecated - use liveInitOnBlockEvent * @static * @protected * @param {String} blockName Name of the block that should trigger a reaction when initialized * @param {Function} callback Handler to be called after successful initialization in the new block's context */ liveInitOnBlockInit : function(blockName, callback) { return this.liveInitOnBlockEvent('init', blockName, callback); }, /** * Helper for live initialization when a different block is initialized inside the current block * @deprecated - use liveInitOnBlockInsideEvent * @static * @protected * @param {String} blockName Name of the block that should trigger a reaction when initialized * @param {Function} [callback] Handler to be called after successful initialization in the new block's context */ liveInitOnBlockInsideInit : function(blockName, callback) { return this.liveInitOnBlockInsideEvent('init', blockName, callback); }, /** * Adds a live event handler to a block, based on a specified element where the event will be listened for * @static * @protected * @param {jQuery} [ctx] The element in which the event will be listened for * @param {String} e Event name * @param {Object} [data] Additional information that the handler gets as e.data * @param {Function} fn Handler * @param {Object} [fnCtx] Handler's context */ on : function(ctx, e, data, fn, fnCtx) { return ctx.jquery? this._liveCtxBind(ctx, e, data, fn, fnCtx) : this.__base(ctx, e, data, fn); }, /** * Removes the live event handler from a block, based on a specified element where the event was being listened for * @static * @protected * @param {jQuery} [ctx] The element in which the event was being listened for * @param {String} e Event name * @param {Function} [fn] Handler * @param {Object} [fnCtx] Handler context */ un : function(ctx, e, fn, fnCtx) { return ctx.jquery? this._liveCtxUnbind(ctx, e, fn, fnCtx) : this.__base(ctx, e, fn); }, /** * Adds a live event handler to a block, based on a specified element where the event will be listened for * @deprecated Use on * @static * @protected * @param {jQuery} ctx The element in which the event will be listened for * @param {String} e Event name * @param {Object} [data] Additional information that the handler gets as e.data * @param {Function} fn Handler * @param {Object} [fnCtx] Handler context */ liveCtxBind : function(ctx, e, data, fn, fnCtx) { return this._liveCtxBind(ctx, e, data, fn, fnCtx); }, /** * Adds a live event handler to a block, based on a specified element where the event will be listened for * @static * @private * @param {jQuery} ctx The element in which the event will be listened for * @param {String} e Event name * @param {Object} [data] Additional information that the handler gets as e.data * @param {Function} fn Handler * @param {Object} [fnCtx] Handler context */ _liveCtxBind : function(ctx, e, data, fn, fnCtx) { var _this = this; if(typeof e == 'string') { if($.isFunction(data)) { fnCtx = fn; fn = data; data = undefined; } if(e.indexOf(' ') > -1) { e.split(' ').forEach(function(e) { _this._liveCtxBind(ctx, e, data, fn, fnCtx); }); } else { var ctxE = _this._buildCtxEventName(e), storage = liveEventCtxStorage[ctxE] || (liveEventCtxStorage[ctxE] = { counter : 0, ctxs : {} }); ctx.each(function() { var ctxId = $.identify(this), ctxStorage = storage.ctxs[ctxId]; if(!ctxStorage) { ctxStorage = storage.ctxs[ctxId] = {}; ++storage.counter; } ctxStorage[$.identify(fn) + (fnCtx? $.identify(fnCtx) : '')] = { fn : fn, data : data, ctx : fnCtx }; }); } } else { $.each(e, function(e, fn) { _this._liveCtxBind(ctx, e, fn, data); }); } return _this; }, /** * Removes a live event handler from a block, based on a specified element where the event was being listened for * @deprecated Use on * @static * @protected * @param {jQuery} ctx The element in which the event was being listened for * @param {String} e Event name * @param {Function} [fn] Handler * @param {Object} [fnCtx] Handler context */ liveCtxUnbind : function(ctx, e, fn, fnCtx) { return this._liveCtxUnbind(ctx, e, fn, fnCtx); }, /** * Removes a live event handler from a block, based on a specified element where the event was being listened for * @static * @private * @param {jQuery} ctx The element in which the event was being listened for * @param {String} e Event name * @param {Function} [fn] Handler * @param {Object} [fnCtx] Handler context */ _liveCtxUnbind : function(ctx, e, fn, fnCtx) { var _this = this, storage = liveEventCtxStorage[e =_this._buildCtxEventName(e)]; if(storage) { ctx.each(function() { var ctxId = $.identify(this, true), ctxStorage; if(ctxId && (ctxStorage = storage.ctxs[ctxId])) { fn && delete ctxStorage[$.identify(fn) + (fnCtx? $.identify(fnCtx) : '')]; if(!fn || $.isEmptyObject(ctxStorage)) { storage.counter--; delete storage.ctxs[ctxId]; } } }); storage.counter || delete liveEventCtxStorage[e]; } return _this; }, /** * Retrieves the name of an element nested in a block * @static * @private * @param {jQuery} elem Nested element * @returns {String|undefined} */ _extractElemNameFrom : function(elem) { if(elem.__bemElemName) return elem.__bemElemName; var matches = elem[0].className.match(this._buildElemNameRE()); return matches? matches[1] : undefined; }, /** * Retrieves block parameters from a DOM element * @static * @param {HTMLElement} domNode DOM node * @returns {Object} */ extractParams : extractParams, /** * Builds a prefix for the CSS class of a DOM element or nested element of the block, based on modifier name * @static * @private * @param {String} modName Modifier name * @param {jQuery|String} [elem] Element * @returns {String} */ _buildModClassPrefix : function(modName, elem) { return buildClass(this._name) + (elem? ELEM_DELIM + (typeof elem === 'string'? elem : this._extractElemNameFrom(elem)) : '') + MOD_DELIM + modName + MOD_DELIM; }, /** * Builds a regular expression for extracting modifier values from a DOM element or nested element of a block * @static * @private * @param {String} modName Modifier name * @param {jQuery|String} [elem] Element * @param {String} [quantifiers] Regular expression quantifiers * @returns {RegExp} */ _buildModValRE : function(modName, elem, quantifiers) { return new RegExp('(\\s|^)' + this._buildModClassPrefix(modName, elem) + '(' + NAME_PATTERN + ')(?=\\s|$)', quantifiers); }, /** * Builds a regular expression for extracting names of elements nested in a block * @static * @private * @returns {RegExp} */ _buildElemNameRE : function() { return new RegExp(this._name + ELEM_DELIM + '(' + NAME_PATTERN + ')(?:\\s|$)'); }, /** * Builds a CSS selector corresponding to the block/element and modifier * @param {String} [elem] Element name * @param {String} [modName] Modifier name * @param {String} [modVal] Modifier value * @returns {String} */ buildSelector : function(elem, modName, modVal) { return '.' + buildClass(this._name, elem, modName, modVal); }, /** * Returns a block instance by unique ID * @deprecated * @param {String} [uniqId] * @returns {BEM.DOM} */ getBlockByUniqId : function(uniqId) { return uniqIdToBlock[uniqId]; }, /** * Returns the size of the current window * @returns {Object} Object with width and height fields */ getWindowSize : function() { return { width : win.width(), height : win.height() }; } }); })(BEM, jQuery);