bem
Version:
1,648 lines (1,322 loc) • 48.5 kB
JavaScript
/** @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) + '$3') :
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) {
if($.isFunction(_this.live)) {
res = _this.live() !== false;
_this.live = function() {};
} else {
res = _this.live;
}
}
}
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);
},
/**
* 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;
while(fn = fns[j++]) fn.fn.call($(node), e);
if(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);
block && (invokeOnInit || (callback && 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);