Updated the Bootstrap theme.
[yaffs-website] / web / themes / contrib / bootstrap / js / attributes.js
1 (function ($, _) {
2
3   /**
4    * @class Attributes
5    *
6    * Modifies attributes.
7    *
8    * @param {Object|Attributes} attributes
9    *   An object to initialize attributes with.
10    */
11   var Attributes = function (attributes) {
12     this.data = {};
13     this.data['class'] = [];
14     this.merge(attributes);
15   };
16
17   /**
18    * Renders the attributes object as a string to inject into an HTML element.
19    *
20    * @return {String}
21    *   A rendered string suitable for inclusion in HTML markup.
22    */
23   Attributes.prototype.toString = function () {
24     var output = '';
25     var name, value;
26     var checkPlain = function (str) {
27       return str && str.toString().replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;') || '';
28     };
29     var data = this.getData();
30     for (name in data) {
31       if (!data.hasOwnProperty(name)) continue;
32       value = data[name];
33       if (_.isFunction(value)) value = value();
34       if (_.isObject(value)) value = _.values(value);
35       if (_.isArray(value)) value = value.join(' ');
36       output += ' ' + checkPlain(name) + '="' + checkPlain(value) + '"';
37     }
38     return output;
39   };
40
41   /**
42    * Renders the Attributes object as a plain object.
43    *
44    * @return {Object}
45    *   A plain object suitable for inclusion in DOM elements.
46    */
47   Attributes.prototype.toPlainObject = function () {
48     var object = {};
49     var name, value;
50     var data = this.getData();
51     for (name in data) {
52       if (!data.hasOwnProperty(name)) continue;
53       value = data[name];
54       if (_.isFunction(value)) value = value();
55       if (_.isObject(value)) value = _.values(value);
56       if (_.isArray(value)) value = value.join(' ');
57       object[name] = value;
58     }
59     return object;
60   };
61
62   /**
63    * Add class(es) to the array.
64    *
65    * @param {string|Array} value
66    *   An individual class or an array of classes to add.
67    *
68    * @return {Attributes}
69    *
70    * @chainable
71    */
72   Attributes.prototype.addClass = function (value) {
73     var args = Array.prototype.slice.call(arguments);
74     this.data['class'] = this.sanitizeClasses(this.data['class'].concat(args));
75     return this;
76   };
77
78   /**
79    * Returns whether the requested attribute exists.
80    *
81    * @param {string} name
82    *   An attribute name to check.
83    *
84    * @return {boolean}
85    *   TRUE or FALSE
86    */
87   Attributes.prototype.exists = function (name) {
88     return this.data[name] !== void(0) && this.data[name] !== null;
89   };
90
91   /**
92    * Retrieve a specific attribute from the array.
93    *
94    * @param {string} name
95    *   The specific attribute to retrieve.
96    * @param {*} defaultValue
97    *   (optional) The default value to set if the attribute does not exist.
98    *
99    * @return {*}
100    *   A specific attribute value, passed by reference.
101    */
102   Attributes.prototype.get = function (name, defaultValue) {
103     if (!this.exists(name)) this.data[name] = defaultValue;
104     return this.data[name];
105   };
106
107   /**
108    * Retrieves a cloned copy of the internal attributes data object.
109    *
110    * @return {Object}
111    */
112   Attributes.prototype.getData = function () {
113     return _.extend({}, this.data);
114   };
115
116   /**
117    * Retrieves classes from the array.
118    *
119    * @return {Array}
120    *   The classes array.
121    */
122   Attributes.prototype.getClasses = function () {
123     return this.get('class', []);
124   };
125
126   /**
127    * Indicates whether a class is present in the array.
128    *
129    * @param {string|Array} className
130    *   The class(es) to search for.
131    *
132    * @return {boolean}
133    *   TRUE or FALSE
134    */
135   Attributes.prototype.hasClass = function (className) {
136     className = this.sanitizeClasses(Array.prototype.slice.call(arguments));
137     var classes = this.getClasses();
138     for (var i = 0, l = className.length; i < l; i++) {
139       // If one of the classes fails, immediately return false.
140       if (_.indexOf(classes, className[i]) === -1) {
141         return false;
142       }
143     }
144     return true;
145   };
146
147   /**
148    * Merges multiple values into the array.
149    *
150    * @param {Attributes|Node|jQuery|Object} object
151    *   An Attributes object with existing data, a Node DOM element, a jQuery
152    *   instance or a plain object where the key is the attribute name and the
153    *   value is the attribute value.
154    * @param {boolean} [recursive]
155    *   Flag determining whether or not to recursively merge key/value pairs.
156    *
157    * @return {Attributes}
158    *
159    * @chainable
160    */
161   Attributes.prototype.merge = function (object, recursive) {
162     // Immediately return if there is nothing to merge.
163     if (!object) {
164       return this;
165     }
166
167     // Get attributes from a jQuery element.
168     if (object instanceof $) {
169       object = object[0];
170     }
171
172     // Get attributes from a DOM element.
173     if (object instanceof Node) {
174       object = Array.prototype.slice.call(object.attributes).reduce(function (attributes, attribute) {
175         attributes[attribute.name] = attribute.value;
176         return attributes;
177       }, {});
178     }
179     // Get attributes from an Attributes instance.
180     else if (object instanceof Attributes) {
181       object = object.getData();
182     }
183     // Otherwise, clone the object.
184     else {
185       object = _.extend({}, object);
186     }
187
188     // By this point, there should be a valid plain object.
189     if (!$.isPlainObject(object)) {
190       setTimeout(function () {
191         throw new Error('Passed object is not supported: ' + object);
192       });
193       return this;
194     }
195
196     // Handle classes separately.
197     if (object && object['class'] !== void 0) {
198       this.addClass(object['class']);
199       delete object['class'];
200     }
201
202     if (recursive === void 0 || recursive) {
203       this.data = $.extend(true, {}, this.data, object);
204     }
205     else {
206       this.data = $.extend({}, this.data, object);
207     }
208
209     return this;
210   };
211
212   /**
213    * Removes an attribute from the array.
214    *
215    * @param {string} name
216    *   The name of the attribute to remove.
217    *
218    * @return {Attributes}
219    *
220    * @chainable
221    */
222   Attributes.prototype.remove = function (name) {
223     if (this.exists(name)) delete this.data[name];
224     return this;
225   };
226
227   /**
228    * Removes a class from the attributes array.
229    *
230    * @param {...string|Array} className
231    *   An individual class or an array of classes to remove.
232    *
233    * @return {Attributes}
234    *
235    * @chainable
236    */
237   Attributes.prototype.removeClass = function (className) {
238     var remove = this.sanitizeClasses(Array.prototype.slice.apply(arguments));
239     this.data['class'] = _.without(this.getClasses(), remove);
240     return this;
241   };
242
243   /**
244    * Replaces a class in the attributes array.
245    *
246    * @param {string} oldValue
247    *   The old class to remove.
248    * @param {string} newValue
249    *   The new class. It will not be added if the old class does not exist.
250    *
251    * @return {Attributes}
252    *
253    * @chainable
254    */
255   Attributes.prototype.replaceClass = function (oldValue, newValue) {
256     var classes = this.getClasses();
257     var i = _.indexOf(this.sanitizeClasses(oldValue), classes);
258     if (i >= 0) {
259       classes[i] = newValue;
260       this.set('class', classes);
261     }
262     return this;
263   };
264
265   /**
266    * Ensures classes are flattened into a single is an array and sanitized.
267    *
268    * @param {...String|Array} classes
269    *   The class or classes to sanitize.
270    *
271    * @return {Array}
272    *   A sanitized array of classes.
273    */
274   Attributes.prototype.sanitizeClasses = function (classes) {
275     return _.chain(Array.prototype.slice.call(arguments))
276       // Flatten in case there's a mix of strings and arrays.
277       .flatten()
278
279       // Split classes that may have been added with a space as a separator.
280       .map(function (string) {
281         return string.split(' ');
282       })
283
284       // Flatten again since it was just split into arrays.
285       .flatten()
286
287       // Filter out empty items.
288       .filter()
289
290       // Clean the class to ensure it's a valid class name.
291       .map(function (value) {
292         return Attributes.cleanClass(value);
293       })
294
295       // Ensure classes are unique.
296       .uniq()
297
298       // Retrieve the final value.
299       .value();
300   };
301
302   /**
303    * Sets an attribute on the array.
304    *
305    * @param {string} name
306    *   The name of the attribute to set.
307    * @param {*} value
308    *   The value of the attribute to set.
309    *
310    * @return {Attributes}
311    *
312    * @chainable
313    */
314   Attributes.prototype.set = function (name, value) {
315     var obj = $.isPlainObject(name) ? name : {};
316     if (typeof name === 'string') {
317       obj[name] = value;
318     }
319     return this.merge(obj);
320   };
321
322   /**
323    * Prepares a string for use as a CSS identifier (element, class, or ID name).
324    *
325    * Note: this is essentially a direct copy from
326    * \Drupal\Component\Utility\Html::cleanCssIdentifier
327    *
328    * @param {string} identifier
329    *   The identifier to clean.
330    * @param {Object} [filter]
331    *   An object of string replacements to use on the identifier.
332    *
333    * @return {string}
334    *   The cleaned identifier.
335    */
336   Attributes.cleanClass = function (identifier, filter) {
337     filter = filter || {
338       ' ': '-',
339       '_': '-',
340       '/': '-',
341       '[': '-',
342       ']': ''
343     };
344
345     identifier = identifier.toLowerCase();
346
347     if (filter['__'] === void 0) {
348       identifier = identifier.replace('__', '#DOUBLE_UNDERSCORE#');
349     }
350
351     identifier = identifier.replace(Object.keys(filter), Object.keys(filter).map(function(key) { return filter[key]; }));
352
353     if (filter['__'] === void 0) {
354       identifier = identifier.replace('#DOUBLE_UNDERSCORE#', '__');
355     }
356
357     identifier = identifier.replace(/[^\u002D\u0030-\u0039\u0041-\u005A\u005F\u0061-\u007A\u00A1-\uFFFF]/g, '');
358     identifier = identifier.replace(['/^[0-9]/', '/^(-[0-9])|^(--)/'], ['_', '__']);
359
360     return identifier;
361   };
362
363   /**
364    * Creates an Attributes instance.
365    *
366    * @param {object|Attributes} [attributes]
367    *   An object to initialize attributes with.
368    *
369    * @return {Attributes}
370    *   An Attributes instance.
371    *
372    * @constructor
373    */
374   Attributes.create = function (attributes) {
375     return new Attributes(attributes);
376   };
377
378   window.Attributes = Attributes;
379
380 })(window.jQuery, window._);