Created
May 11, 2017 09:52
-
-
Save porqz/8f2d61d965ce7a3b1c1291858832c046 to your computer and use it in GitHub Desktop.
Simple BEM implementation (my old code)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Реализует Блок в терминах БЭМ | |
* | |
* @requires jQuery | |
* @requires ObjectUtils | |
* | |
* @param {?String} blockName Имя блока | |
* @param {?jQuery} block jQuery-представление DOM-элемента блока | |
* | |
* @constructor | |
*/ | |
var Block = function (blockName, block, settings) { | |
var argumentsArray = Array.prototype.slice.apply(arguments); | |
this.init.apply(this, argumentsArray); | |
}; | |
Block.prototype = { | |
constructor: Block, | |
defaults: { | |
eventsNamespace: ".Block", | |
classes: {} | |
}, | |
subblocks: [], | |
/** | |
* Добавляет подблок с конструктором `constructor` в блок | |
* | |
* @param {Function} constructor Функция-конструктор блока-подблока | |
* | |
* @returns {Block} This | |
*/ | |
addSubblock: function (constructor /*, args */) { | |
var constructorArguments = Array.prototype.splice.apply(arguments, [1, arguments.length - 1]), | |
subblock; | |
if (typeof constructor === "function") { | |
function Subblock() { | |
return constructor.apply(this, constructorArguments); | |
} | |
Subblock.prototype = constructor.prototype; | |
subblock = new Subblock(); | |
this.subblocks.push(subblock); | |
} | |
return subblock; | |
}, | |
applyToSubblocks: function (callbackToApply) { | |
for (var i = this.subblocks.length - 1; i >= 0; i--) { | |
callbackToApply.apply(this.subblocks[i], [this]); | |
this.subblocks[i].applyToSubblocks(callbackToApply); | |
} | |
return this; | |
}, | |
/** | |
* Вычисляет CSS-класс элемента `elementName` | |
* | |
* @param {String} elementName Имя элемента | |
* | |
* @returns {String} CSS-класс | |
*/ | |
classOf: function (elementName) { | |
if (String(this.settings.classes[elementName]) === this.settings.classes[elementName]) { | |
return this.settings.classes[elementName].replace(/&/g, this.blockName); | |
} | |
else { | |
throw "Can’t calculate classname. There is no class for element with name \"" + elementName + "\" in the block " + this.blockName; | |
} | |
}, | |
/** | |
* Ищет элемент с именем `elementName` в блоке `block` | |
* | |
* @param {String} elementName Имя элемента | |
* @param {?jQuery} block Блок, который должен содержать элемент, если null, используется this.block | |
* | |
* @returns {jQuery} Найденный элемент | |
*/ | |
element: function (elementName, block) { | |
var elementClassname = this.classOf(elementName), | |
elementSelector = "." + elementClassname; | |
if (typeof block == "undefined") { | |
block = this.block; | |
} | |
else { | |
block = $(block); | |
} | |
if (elementClassname.length) { | |
return block.find(elementSelector); | |
} | |
else { | |
throw "Can’t find element. There is no element with name \"" + elementName + "\" in the block " + this.blockName; | |
} | |
}, | |
/** | |
* Удаляет CSS-модификатор `name` | |
* | |
* @param {String} name Имя модификатора | |
* @param {?String} elementName Элемент с модификатором, если null, используется this.blockName | |
* @param {?(jQuery|DOM)} element DOM-элемент, у которого удаляется модификатор | |
* | |
* @returns {Block} This | |
*/ | |
removeModifier: function (name, elementName, element) { | |
var modifierClassname; | |
if (elementName) { | |
if (elementName in this.ui) { | |
if (!element) { | |
element = this.ui[elementName]; | |
} | |
else { | |
element = $(element); | |
} | |
modifierClassname = this.classOf(elementName); | |
} | |
else { | |
throw "Can’t remove modifier. There is no element with name \"" + elementName + "\" in the block " + this.blockName; | |
} | |
} | |
else { | |
element = this.block; | |
modifierClassname = this.blockName; | |
} | |
if (name !== "") { | |
modifierClassname = modifierClassname + "_" + name; | |
} | |
var modifierRegexp = new RegExp("\\b" + modifierClassname + "_.+?\\b", "gi"); | |
element.each(function () { | |
this.className = this.className.replace(modifierRegexp, "").replace(/\s+/gi, " ").replace(/^\s/gi, "").replace(/\s$/gi, ""); | |
}); | |
return this; | |
}, | |
/** | |
* Добавляет модификатор `name` к элементу или блоку | |
* | |
* @param {String} name Имя модификатора | |
* @param {String} value Значение модификатора | |
* @param {?String} elementName Имя элемента, к которому добавляется модификатор, если null, используется this.blockName | |
* @param {?(jQuery|DOM)} element DOM-элемент, к которому добавляется модификатор | |
* | |
* @returns {Block} This | |
*/ | |
setModifier: function (name, value, elementName, element) { | |
var that = this, | |
modifierClassname; | |
if (elementName) { | |
if (elementName in this.ui) { | |
if (!element) { | |
element = this.ui[elementName]; | |
} | |
else { | |
element = $(element); | |
} | |
modifierClassname = this.classOf(elementName); | |
} | |
else { | |
throw "Can’t set modifier. There is no element with name \"" + elementName + "\" in the block " + this.blockName; | |
} | |
} | |
else { | |
element = this.block; | |
modifierClassname = this.blockName; | |
} | |
var modifierRegexp = new RegExp("\\b" + modifierClassname + "_" + name + ".+?\\b", "gi"); | |
if (value) { | |
element.each(function () { | |
this.className = this.className.replace(modifierRegexp, "").replace(/\s+/gi, " ").replace(/^\s/gi, "").replace(/\s$/gi, "") + " " + modifierClassname + "_" + name + (!!value === value ? "" : "_" + value); | |
}); | |
} | |
return this; | |
}, | |
/** | |
* Возвращает значение модификатора | |
* | |
* @param {String} name Имя модификатора | |
* @param {?String} elementName Имя элемента, если null, используется this.blockName | |
* @param {?(jQuery|DOM)} element DOM-элемент, к которому добавляется модификатор | |
* | |
* @returns {(String|Boolean)} Значение модификатора | |
*/ | |
getModifier: function (name, elementName, element) { | |
var that = this, | |
/** @type jQuery */ | |
element, | |
modifierClassname; | |
if (elementName) { | |
if (elementName in this.ui) { | |
if (!element) { | |
element = this.ui[elementName]; | |
} | |
else { | |
element = $(element); | |
} | |
modifierClassname = this.classOf(elementName); | |
} | |
else { | |
throw "Can’t get modifier. There is no element with name \"" + elementName + "\" in the block " + this.blockName; | |
} | |
} | |
else { | |
element = this.block; | |
modifierClassname = this.blockName; | |
} | |
var modifierRegexp = new RegExp("\\b" + modifierClassname + "_" + name + "(_(\\S+))?\\b", "gi"), | |
modifierValue = ""; | |
element.each(function () { | |
var matched = modifierRegexp.exec(this.className); | |
if (matched && matched.length) { | |
modifierValue = matched[matched.length - 1] ? matched[matched.length - 1].replace(/^_/gi, "") : true; | |
} | |
}); | |
return modifierValue; | |
}, | |
/** | |
* Ищет элементы в this.ui с именем `elementName`, у которых значение | |
* модификатора `modifierName` равно `modifierValue` | |
* | |
* @param {String} name Имя модификатора | |
* @param {String} value Значение модификатора | |
* @param {String} elementName Имя элемента | |
* | |
* @returns {jQuery} jQuery-объект | |
* | |
* @TODO Сделать возможность использования модификаторов, | |
* у которых ключ совпадает со значением, например: block__element_modified | |
* | |
*/ | |
getElementsWithModifier: function (name, value, elementName, elements) { | |
if (typeof elements === "undefined") { | |
elements = this.element(elementName); | |
} | |
else { | |
elements = $(elements); | |
} | |
var modifierClassname = this.classOf(elementName) + "_" + name + "_" + value, | |
elementsWithModifier = $(); | |
for (var i = elements.length - 1; i >= 0; i--) { | |
if (elements.eq(i).hasClass(modifierClassname)) { | |
elementsWithModifier = elementsWithModifier.add(elements.eq(i)); | |
} | |
} | |
return elementsWithModifier; | |
}, | |
/** | |
* Добавляет `element` к this.ui с именем `name` | |
* | |
* @param {String} name Имя элемента, которое будет использоваться в качестве ключа в this.ui | |
* @param {?(jQuery|String)} element jQuery-представление элемента или имя класса, если null, элемент ищется по имени, если строка, ищется по строке | |
* | |
* @returns {Block} This | |
*/ | |
addUIElement: function (name, element) { | |
if (typeof element != "undefined") { | |
if (String(element) === element) { | |
element = this.element(element); | |
} | |
} | |
else { | |
element = this.element(name); | |
} | |
if (!("ui" in this)) { | |
this.ui = {}; | |
} | |
if (name in this.ui && this.ui[name].length) { | |
this.ui[name] = this.ui[name].add(element); | |
} | |
else { | |
this.ui[name] = $(element); | |
} | |
return this; | |
}, | |
/** | |
* Удаляет элемент с именем `name` из this.ui | |
* | |
* @param {String} name Имя элемента | |
* @param {?jQuery} element Удаляемый элемент | |
*/ | |
removeUIElement: function (name, element) { | |
if (typeof this.ui[name] !== "undefined") { | |
this.ui[name] = this.ui[name].not(element); | |
} | |
return this; | |
}, | |
/** | |
* Инициализирует блок: инициализирует UI, навешивает поведение | |
* | |
* @param {String} blockName Имя блока | |
* @param {(jQuery|DOMElement)} block Элемент блока | |
*/ | |
init: function (blockName, block, settings) { | |
this.blockName = blockName || "page"; | |
this.block = $(block) || $("body"); | |
this.subblocks = []; | |
this.applySettings(settings || {}); | |
this.settings.classes["block"] = this.blockName; | |
if (!("ui" in this)) { | |
this.ui = {}; | |
} | |
this.ui["block"] = this.block; | |
this.block.data("block" + this.settings.eventsNamespace, this); | |
this.initUI(); | |
this.behavior(this); | |
}, | |
/** | |
* Применяет «настройки» `settings`. Значения по умолчанию берутся из this.defaults | |
* | |
* @param {Object} settings Настройки | |
* | |
*/ | |
applySettings: function (settings) { | |
this.settings = ObjectUtils.extend(ObjectUtils.clone(this.defaults), settings || {}); | |
return this; | |
}, | |
/** | |
* Инициализирует объект this.ui | |
* | |
* @interface | |
*/ | |
initUI: function () {}, | |
/** | |
* Удаляет «мёртвые» jQuery-объекты из this.ui | |
* | |
* @deprecated Использовать нельзя | |
* | |
* @returns {Block} This | |
*/ | |
cleanupUI: function () { | |
var element; | |
for (var elementName in this.ui) { | |
this.ui[elementName].each(function () { | |
$(this).length; | |
}); | |
} | |
}, | |
/** | |
* Навешивает поведение | |
* | |
* @interface | |
*/ | |
behavior: function () {}, | |
/** | |
* Удаляет все навешанные события с пространством имён `namespace` | |
* | |
* @param {String} namespace Пространство имён (обычно совпадает с именем JS-класса) | |
* | |
* @returns {Block} This | |
*/ | |
removeBehavior: function (namespace) { | |
namespace = "." + namespace.replace(/^\.+/, ""); | |
for (var elementName in this.ui) { | |
this.ui[elementName].unbind(namespace); | |
} | |
return this; | |
}, | |
/** | |
* Билдит jQuery-объект из строки `templateString` | |
* | |
* @param {String} templateString HTML-шаблон. Можно использовать символ амперсанда (&), | |
* который будет заменяться на имя блока (this.blockName) | |
* | |
* @returns {jQuery} jQuery-объект | |
*/ | |
buildElementFromString: function (templateString) { | |
var that = this, | |
preparedTemplateString = templateString.replace(/('|")([^\1]*?)\1/gi, function (attributeValue) { | |
return attributeValue.replace(/&/g, that.blockName); | |
}), | |
jqBuildedElement = $(preparedTemplateString), | |
domBuildedElement = jqBuildedElement.get(); | |
return jqBuildedElement; | |
}, | |
/** | |
* Билдит jQuery-объект из шаблона с именем `templateName`, | |
* шаблоны должны лежать в объекте this.settings.templates | |
* | |
* @param {String} templateName | |
* | |
* @returns {jQuery} jQuery-объект | |
*/ | |
buildElement: function (templateName) { | |
if ("templates" in this.settings && templateName in this.settings.templates) { | |
return this.buildElementFromString(this.settings.templates[templateName]); | |
} | |
}, | |
/** | |
* Перерисовывает подблоки | |
*/ | |
repaint: function () { | |
for (var i = this.subblocks.length - 1; i >= 0; i--) { | |
this.subblocks[i].repaint(); | |
} | |
this.block.trigger("update" + this.settings.eventsNamespace); | |
return this; | |
}, | |
/** | |
* Сбрасывает состояние блока до исходного | |
* | |
* @interface | |
*/ | |
reset: function () {} | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Коллекция методов для удобной работы с объектами | |
*/ | |
var ObjectUtils = { | |
/** | |
* Рекурсивно копирует объект `object` | |
* | |
* @param {Object} object Клонируемый объект | |
* | |
* @return {Object} Клон объекта | |
*/ | |
clone: function (object) { | |
return (function () { | |
var cloned = {}; | |
for (var propertyName in this) { | |
if (this.hasOwnProperty(propertyName)) { | |
var propertyValue = this[propertyName]; | |
if (propertyValue instanceof Object) { | |
cloned[propertyName] = ObjectUtils.clone(propertyValue); | |
} | |
else { | |
cloned[propertyName] = propertyValue; | |
} | |
} | |
} | |
return cloned; | |
}).apply(object); | |
}, | |
/** | |
* Дополняет объект `object` свойствами объекта anotherObject` | |
* | |
* @param {Object} object Расширяемый объект | |
* @param {Object} anotherObject Расширяющий объект | |
* @param {?Function} existsCallback Функция, которая вызывается, когда свойство объекта `anotherObject` уже есть у `object`. | |
* Должна возвращать значение скопированного свойства, если возвращает undefined, то новое свойство не будет скопировано | |
* | |
* @return {Object} Расширенный объект | |
*/ | |
extend: function (object, anotherObject, existsCallback) { | |
existsCallback = existsCallback || function (propertyName, propertyValue, newPropertyValue) { return newPropertyValue; }; | |
for (var key in anotherObject) { | |
if (anotherObject.hasOwnProperty(key)) { | |
if (key in object) { | |
// If property already exists and it is an object | |
if (typeof anotherObject[key] == "object") { | |
ObjectUtils.extend(object[key], anotherObject[key], existsCallback); | |
} | |
else { | |
var existsCallbackResult = existsCallback.apply(object, [key, object[key], anotherObject[key]]); | |
if (typeof existsCallbackResult != "undefined") { | |
object[key] = existsCallbackResult; | |
} | |
} | |
} | |
else { | |
object[key] = anotherObject[key]; | |
} | |
} | |
} | |
return object; | |
}, | |
/** | |
* Дополняет, объединив: если копируемое свойство уже существует, и это функция, | |
* оно заменится функцией, в которой вызываются обе функции (метод расширяемого и дополняющего) | |
* | |
* @override ObjectUtils.extend | |
*/ | |
extendMerged: function (object, anotherObject) { | |
return ObjectUtils.extend(object, anotherObject, function (propertyName, propertyValue, newPropertyValue) { | |
if ((typeof propertyValue == "function") && (typeof newPropertyValue == "function")) { | |
return (function () { | |
var method = propertyValue, | |
newMethod = newPropertyValue; | |
return function () { | |
var argumentsArray = Array.prototype.slice.apply(arguments), | |
originalMethodResult = method.apply(this, argumentsArray); | |
newMethod.apply(this, argumentsArray); | |
return originalMethodResult; | |
}; | |
})(); | |
} | |
else { | |
return newPropertyValue; | |
} | |
}); | |
} | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment