(function ($, _) { /** * @class Attributes * * Modifies attributes. * * @param {Object|Attributes} attributes * An object to initialize attributes with. */ var Attributes = function (attributes) { this.data = {}; this.data['class'] = []; this.merge(attributes); }; /** * Renders the attributes object as a string to inject into an HTML element. * * @return {String} * A rendered string suitable for inclusion in HTML markup. */ Attributes.prototype.toString = function () { var output = ''; var name, value; var checkPlain = function (str) { return str && str.toString().replace(/&/g, '&').replace(/"/g, '"').replace(//g, '>') || ''; }; var data = this.getData(); for (name in data) { if (!data.hasOwnProperty(name)) continue; value = data[name]; if (_.isFunction(value)) value = value(); if (_.isObject(value)) value = _.values(value); if (_.isArray(value)) value = value.join(' '); output += ' ' + checkPlain(name) + '="' + checkPlain(value) + '"'; } return output; }; /** * Renders the Attributes object as a plain object. * * @return {Object} * A plain object suitable for inclusion in DOM elements. */ Attributes.prototype.toPlainObject = function () { var object = {}; var name, value; var data = this.getData(); for (name in data) { if (!data.hasOwnProperty(name)) continue; value = data[name]; if (_.isFunction(value)) value = value(); if (_.isObject(value)) value = _.values(value); if (_.isArray(value)) value = value.join(' '); object[name] = value; } return object; }; /** * Add class(es) to the array. * * @param {string|Array} value * An individual class or an array of classes to add. * * @return {Attributes} * * @chainable */ Attributes.prototype.addClass = function (value) { var args = Array.prototype.slice.call(arguments); this.data['class'] = this.sanitizeClasses(this.data['class'].concat(args)); return this; }; /** * Returns whether the requested attribute exists. * * @param {string} name * An attribute name to check. * * @return {boolean} * TRUE or FALSE */ Attributes.prototype.exists = function (name) { return this.data[name] !== void(0) && this.data[name] !== null; }; /** * Retrieve a specific attribute from the array. * * @param {string} name * The specific attribute to retrieve. * @param {*} defaultValue * (optional) The default value to set if the attribute does not exist. * * @return {*} * A specific attribute value, passed by reference. */ Attributes.prototype.get = function (name, defaultValue) { if (!this.exists(name)) this.data[name] = defaultValue; return this.data[name]; }; /** * Retrieves a cloned copy of the internal attributes data object. * * @return {Object} */ Attributes.prototype.getData = function () { return _.extend({}, this.data); }; /** * Retrieves classes from the array. * * @return {Array} * The classes array. */ Attributes.prototype.getClasses = function () { return this.get('class', []); }; /** * Indicates whether a class is present in the array. * * @param {string|Array} className * The class(es) to search for. * * @return {boolean} * TRUE or FALSE */ Attributes.prototype.hasClass = function (className) { className = this.sanitizeClasses(Array.prototype.slice.call(arguments)); var classes = this.getClasses(); for (var i = 0, l = className.length; i < l; i++) { // If one of the classes fails, immediately return false. if (_.indexOf(classes, className[i]) === -1) { return false; } } return true; }; /** * Merges multiple values into the array. * * @param {Attributes|Node|jQuery|Object} object * An Attributes object with existing data, a Node DOM element, a jQuery * instance or a plain object where the key is the attribute name and the * value is the attribute value. * @param {boolean} [recursive] * Flag determining whether or not to recursively merge key/value pairs. * * @return {Attributes} * * @chainable */ Attributes.prototype.merge = function (object, recursive) { // Immediately return if there is nothing to merge. if (!object) { return this; } // Get attributes from a jQuery element. if (object instanceof $) { object = object[0]; } // Get attributes from a DOM element. if (object instanceof Node) { object = Array.prototype.slice.call(object.attributes).reduce(function (attributes, attribute) { attributes[attribute.name] = attribute.value; return attributes; }, {}); } // Get attributes from an Attributes instance. else if (object instanceof Attributes) { object = object.getData(); } // Otherwise, clone the object. else { object = _.extend({}, object); } // By this point, there should be a valid plain object. if (!$.isPlainObject(object)) { setTimeout(function () { throw new Error('Passed object is not supported: ' + object); }); return this; } // Handle classes separately. if (object && object['class'] !== void 0) { this.addClass(object['class']); delete object['class']; } if (recursive === void 0 || recursive) { this.data = $.extend(true, {}, this.data, object); } else { this.data = $.extend({}, this.data, object); } return this; }; /** * Removes an attribute from the array. * * @param {string} name * The name of the attribute to remove. * * @return {Attributes} * * @chainable */ Attributes.prototype.remove = function (name) { if (this.exists(name)) delete this.data[name]; return this; }; /** * Removes a class from the attributes array. * * @param {...string|Array} className * An individual class or an array of classes to remove. * * @return {Attributes} * * @chainable */ Attributes.prototype.removeClass = function (className) { var remove = this.sanitizeClasses(Array.prototype.slice.apply(arguments)); this.data['class'] = _.without(this.getClasses(), remove); return this; }; /** * Replaces a class in the attributes array. * * @param {string} oldValue * The old class to remove. * @param {string} newValue * The new class. It will not be added if the old class does not exist. * * @return {Attributes} * * @chainable */ Attributes.prototype.replaceClass = function (oldValue, newValue) { var classes = this.getClasses(); var i = _.indexOf(this.sanitizeClasses(oldValue), classes); if (i >= 0) { classes[i] = newValue; this.set('class', classes); } return this; }; /** * Ensures classes are flattened into a single is an array and sanitized. * * @param {...String|Array} classes * The class or classes to sanitize. * * @return {Array} * A sanitized array of classes. */ Attributes.prototype.sanitizeClasses = function (classes) { return _.chain(Array.prototype.slice.call(arguments)) // Flatten in case there's a mix of strings and arrays. .flatten() // Split classes that may have been added with a space as a separator. .map(function (string) { return string.split(' '); }) // Flatten again since it was just split into arrays. .flatten() // Filter out empty items. .filter() // Clean the class to ensure it's a valid class name. .map(function (value) { return Attributes.cleanClass(value); }) // Ensure classes are unique. .uniq() // Retrieve the final value. .value(); }; /** * Sets an attribute on the array. * * @param {string} name * The name of the attribute to set. * @param {*} value * The value of the attribute to set. * * @return {Attributes} * * @chainable */ Attributes.prototype.set = function (name, value) { var obj = $.isPlainObject(name) ? name : {}; if (typeof name === 'string') { obj[name] = value; } return this.merge(obj); }; /** * Prepares a string for use as a CSS identifier (element, class, or ID name). * * Note: this is essentially a direct copy from * \Drupal\Component\Utility\Html::cleanCssIdentifier * * @param {string} identifier * The identifier to clean. * @param {Object} [filter] * An object of string replacements to use on the identifier. * * @return {string} * The cleaned identifier. */ Attributes.cleanClass = function (identifier, filter) { filter = filter || { ' ': '-', '_': '-', '/': '-', '[': '-', ']': '' }; identifier = identifier.toLowerCase(); if (filter['__'] === void 0) { identifier = identifier.replace('__', '#DOUBLE_UNDERSCORE#'); } identifier = identifier.replace(Object.keys(filter), Object.keys(filter).map(function(key) { return filter[key]; })); if (filter['__'] === void 0) { identifier = identifier.replace('#DOUBLE_UNDERSCORE#', '__'); } identifier = identifier.replace(/[^\u002D\u0030-\u0039\u0041-\u005A\u005F\u0061-\u007A\u00A1-\uFFFF]/g, ''); identifier = identifier.replace(['/^[0-9]/', '/^(-[0-9])|^(--)/'], ['_', '__']); return identifier; }; /** * Creates an Attributes instance. * * @param {object|Attributes} [attributes] * An object to initialize attributes with. * * @return {Attributes} * An Attributes instance. * * @constructor */ Attributes.create = function (attributes) { return new Attributes(attributes); }; window.Attributes = Attributes; })(window.jQuery, window._);