UNPKG

vue-css-modifiers

Version:

A tiny Vue directive to simplify the manipulation of CSS modifier classes.

210 lines (176 loc) 6.29 kB
/** * vue-css-modifiers * * A tiny Vue directive to simplify the manipulation of CSS modifier classes. * * Copyright (c) 2019-present, cheap glitch * * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR * PERFORMANCE OF THIS SOFTWARE. */ // The directive will call the function on the 'bind' and 'update' hooks module.exports = function(el, binding, vnode, oldVnode) { const name = `v-${binding.name}`; const value = binding.value; if (value === undefined || value === null) { logError(`"${name}" is null or undefined`); return -1; } if (!['string', 'object'].includes(typeof value)) { logError(`"${name}" must be a string, an array or an object`); return -1; } if (Array.isArray(value) && !value.every(elem => typeof elem == 'string')) { logError(`all the values of array "${name}" must be strings`); return -1; } if (Object.prototype.toString.call(value) == '[object Object]' && !Object.keys(value).every(key => typeof value[key] == 'boolean')) { logError(`all the values of object "${name}" must be booleans`); return -1; } /** * String value * --------------------------------------------------------------------- */ if (typeof value == 'string') return setClassByName(value, el, binding, vnode); /** * Array value * --------------------------------------------------------------------- */ if (Array.isArray(value)) { for (const val of value) if (setClassByName(val, el, binding, vnode) == -1) return -1; return 0; } /** * Object value * --------------------------------------------------------------------- */ const isBEM = ('bem' in binding.modifiers || binding.name == 'bem'); const sameClasses = checkIfSameClasses(vnode, oldVnode); // Add a class to the element for every key whose value is true Object.keys(value).forEach(function(key) { // Ignore unchanged values if (binding.oldValue && value[key] === binding.oldValue[key] && (!isBEM || sameClasses)) return; setElemClass(camel2Kebab(key), value[key], el, binding, vnode); }); return 0; } /** * Add/remove the class 'class-name' depending on the value of the property 'className' */ function setClassByName(className, el, binding, vnode) { const prop = kebab2Camel(className); const value = vnode.context._data[prop]; // Check that the corresponding property is defined if (value === undefined || value === null) { logError(`property "${prop}" is undefined or null`); return -1; } // Add/remove the class on the element depending on the value of the prop setElemClass(className, value, el, binding, vnode); return 0; } /** * Add/remove a class on a DOM element */ function setElemClass(className, add, el, binding, vnode) { let baseClass = ''; // Get the mode from the directive modifiers let mode = 'default'; if ('is' in binding.modifiers) mode = 'force-is-prefix'; if ('bem' in binding.modifiers) mode = 'bem'; // Enforce a mode if the name of the directive is that of the corresponding directive modifier if (binding.name == 'is') mode = 'force-is-prefix'; if (binding.name == 'bem') mode = 'bem'; switch (mode) { /** * Add 'is-' at the beginning of every modifier * class name (unless the name already starts with '-is') */ case 'force-is-prefix': className = /^is-/.test(className) ? className : `is-${className}`; break; /** * Make the modifiers suffixes of a base class name */ case 'bem': // If a base class is provided as an argument if ('arg' in binding) { baseClass = binding.arg; // Don't add the modifier if the base class is absent from the element if (!vnode.elm._prevClass || !vnode.elm._prevClass.split(' ').includes(baseClass)) return; } else { // Get all the class names that aren't modifiers themselves const classes = vnode.elm._prevClass ? vnode.elm._prevClass.split(' ').filter(prevClassName => !prevClassName.includes('--')) : []; // If there is no base class, don't add the modifier if (!classes.length) return; // Else, take the first class of the list baseClass = classes[0]; } // Duplicate the name of the base class and add the modifier as its suffix className = `${baseClass}--${className.replace(/^is-/, '')}` break; } if (add) el.classList.add(className); else el.classList.remove(className); } /** * Return 'true' if the classes are the same between the two VNodes, 'false' otherwise */ function checkIfSameClasses(vnode, oldVnode) { if (!oldVnode || !vnode.data.class || !oldVnode.data.class) return false; const classes = vnode.data.class; const oldClasses = oldVnode.data.class; if (!['string', 'object'].includes(typeof classes) || typeof classes != typeof oldClasses) return false; // Single class if (typeof classes == 'string') return classes == oldClasses; // Array of classes if (Array.isArray(classes)) return (classes.length == oldClasses.length) && classes.every(className => oldClasses.includes(className)); // Hashmap const keys = Object.keys(classes); const oldKeys = Object.keys(oldClasses); return (keys.length == oldKeys.length) && keys.every(key => oldKeys.includes(key) && classes[key] == oldClasses[key]) } /** * Convert a kebab-cased string to camel case and conversely */ function kebab2Camel(str) { return str.replace(/-[a-z0-9]/g, match => match.toUpperCase()[1]); } function camel2Kebab(str) { return str.replace(/(?:[A-Z]|\d+)/g, match => `-${match.toLowerCase()}`); } /** * Output an error in the dev console */ function logError(msg) { console.error(`[vue-css-modifiers]: ${msg}`); }