linkify images in search results
[ryf-theme.git] / 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._);