Commit | Line | Data |
---|---|---|
1e7a45c4 VSB |
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, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>') || ''; | |
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._); |