(function ($, _) {
/**
- * Class to help modify attributes.
+ * @class Attributes
*
- * @param {object} object
- * An object to initialize attributes with.
+ * Modifies attributes.
*
- * @constructor
+ * @param {Object|Attributes} attributes
+ * An object to initialize attributes with.
*/
- var Attributes = function (object) {
- this.data = object && _.isObject(object) && _.clone(object) || {};
+ 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.
*
- * @returns {string}
+ * @return {String}
+ * A rendered string suitable for inclusion in HTML markup.
*/
Attributes.prototype.toString = function () {
var output = '';
var checkPlain = function (str) {
return str && str.toString().replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>') || '';
};
- for (name in this.data) {
- if (!this.data.hasOwnProperty(name)) continue;
- value = this.data[name];
+ 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(' ');
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.
*
* @chainable
*/
Attributes.prototype.addClass = function (value) {
- var classes = this.getClasses();
- value = [].concat(classes, value);
- this.set('class', _.uniq(value));
+ var args = Array.prototype.slice.call(arguments);
+ this.data['class'] = this.sanitizeClasses(this.data['class'].concat(args));
return this;
};
/**
* Retrieves a cloned copy of the internal attributes data object.
*
- * @returns {Object}
+ * @return {Object}
*/
Attributes.prototype.getData = function () {
- return _.clone(this.data);
+ return _.extend({}, this.data);
};
/**
* The classes array.
*/
Attributes.prototype.getClasses = function () {
- var classes = [].concat(this.get('class', []));
- return _.uniq(classes);
+ return this.get('class', []);
};
/**
* Indicates whether a class is present in the array.
*
- * @param {string|Array} name
+ * @param {string|Array} className
* The class(es) to search for.
*
* @return {boolean}
* TRUE or FALSE
*/
- Attributes.prototype.hasClass = function (name) {
- name = [].concat(name);
+ Attributes.prototype.hasClass = function (className) {
+ className = this.sanitizeClasses(Array.prototype.slice.call(arguments));
var classes = this.getClasses();
- var found = false;
- _.each(name, function (value) { if (_.indexOf(classes, value) !== -1) found = true; });
- return found;
+ 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 {object} values
- * An associative key/value 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.
*
*
* @chainable
*/
- Attributes.prototype.merge = function (values, recursive) {
- values = values instanceof Attributes ? values.getData() : values;
- if (recursive === void(0) || recursive) {
- this.data = $.extend(true, {}, this.data, values);
+ 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 {
- $.extend(this.data, values);
+ this.data = $.extend({}, this.data, object);
}
+
return this;
};
/**
* Removes a class from the attributes array.
*
- * @param {string|Array} value
+ * @param {...string|Array} className
* An individual class or an array of classes to remove.
*
* @return {Attributes}
*
* @chainable
*/
- Attributes.prototype.removeClass = function (value) {
- this.set('class', _.without(this.getClasses(), [].concat(value)));
+ Attributes.prototype.removeClass = function (className) {
+ var remove = this.sanitizeClasses(Array.prototype.slice.apply(arguments));
+ this.data['class'] = _.without(this.getClasses(), remove);
return this;
};
*/
Attributes.prototype.replaceClass = function (oldValue, newValue) {
var classes = this.getClasses();
- var i = _.indexOf(oldValue, classes);
+ 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.
*
* @chainable
*/
Attributes.prototype.set = function (name, value) {
- this.data[name] = value;
- return this;
+ 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 = identifier.replace(Object.keys(filter), Object.keys(filter).map(function(key) { return filter[key]; }), identifier);
+
+ if (filter['__'] === void 0) {
+ identifier = identifier.replace('#DOUBLE_UNDERSCORE#', '__', identifier);
+ }
+
+ identifier = identifier.replace(/[^\u002D\u0030-\u0039\u0041-\u005A\u005F\u0061-\u007A\u00A1-\uFFFF]/g, '');
+ identifier = identifier.replace(['/^[0-9]/', '/^(-[0-9])|^(--)/'], ['_', '__'], identifier);
+
+ return identifier;
};
/**
* Creates an Attributes instance.
*
- * @param {object|Attributes} object
+ * @param {object|Attributes} [attributes]
* An object to initialize attributes with.
*
- * @returns {Attributes}
- *
- * @global
+ * @return {Attributes}
+ * An Attributes instance.
*
* @constructor
*/
- window.Attributes = function (object) {
- return object instanceof Attributes ? object : new Attributes(object);
+ Attributes.create = function (attributes) {
+ return new Attributes(attributes);
};
+ window.Attributes = Attributes;
+
})(window.jQuery, window._);