Skip to content

Instantly share code, notes, and snippets.

@porqz
Created May 11, 2017 09:52
Show Gist options
  • Save porqz/8f2d61d965ce7a3b1c1291858832c046 to your computer and use it in GitHub Desktop.
Save porqz/8f2d61d965ce7a3b1c1291858832c046 to your computer and use it in GitHub Desktop.
Simple BEM implementation (my old code)
/**
* Реализует Блок в терминах БЭМ
*
* @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 () {}
};
/**
* Коллекция методов для удобной работы с объектами
*/
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