Commit | Line | Data |
---|---|---|
1e7a45c4 VSB |
1 | /** |
2 | * @file | |
3 | * Drupal Bootstrap object. | |
4 | */ | |
5 | ||
6 | /** | |
7 | * All Drupal Bootstrap JavaScript APIs are contained in this namespace. | |
8 | * | |
9 | * @namespace | |
10 | */ | |
11 | (function (_, $, Drupal, drupalSettings) { | |
12 | 'use strict'; | |
13 | ||
14 | var Bootstrap = { | |
15 | processedOnce: {}, | |
16 | settings: drupalSettings.bootstrap || {} | |
17 | }; | |
18 | ||
19 | /** | |
20 | * Wraps Drupal.checkPlain() to ensure value passed isn't empty. | |
21 | * | |
22 | * Encodes special characters in a plain-text string for display as HTML. | |
23 | * | |
24 | * @param {string} str | |
25 | * The string to be encoded. | |
26 | * | |
27 | * @return {string} | |
28 | * The encoded string. | |
29 | * | |
30 | * @ingroup sanitization | |
31 | */ | |
32 | Bootstrap.checkPlain = function (str) { | |
33 | return str && Drupal.checkPlain(str) || ''; | |
34 | }; | |
35 | ||
36 | /** | |
37 | * Creates a jQuery plugin. | |
38 | * | |
39 | * @param {String} id | |
40 | * A jQuery plugin identifier located in $.fn. | |
41 | * @param {Function} plugin | |
42 | * A constructor function used to initialize the for the jQuery plugin. | |
43 | * @param {Boolean} [noConflict] | |
44 | * Flag indicating whether or not to create a ".noConflict()" helper method | |
45 | * for the plugin. | |
46 | */ | |
47 | Bootstrap.createPlugin = function (id, plugin, noConflict) { | |
48 | // Immediately return if plugin doesn't exist. | |
49 | if ($.fn[id] !== void 0) { | |
50 | return this.fatal('Specified jQuery plugin identifier already exists: @id. Use Drupal.bootstrap.replacePlugin() instead.', {'@id': id}); | |
51 | } | |
52 | ||
53 | // Immediately return if plugin isn't a function. | |
54 | if (typeof plugin !== 'function') { | |
55 | return this.fatal('You must provide a constructor function to create a jQuery plugin "@id": @plugin', {'@id': id, '@plugin': plugin}); | |
56 | } | |
57 | ||
58 | // Add a ".noConflict()" helper method. | |
59 | this.pluginNoConflict(id, plugin, noConflict); | |
60 | ||
61 | $.fn[id] = plugin; | |
62 | }; | |
63 | ||
64 | /** | |
65 | * Diff object properties. | |
66 | * | |
67 | * @param {...Object} objects | |
68 | * Two or more objects. The first object will be used to return properties | |
69 | * values. | |
70 | * | |
71 | * @return {Object} | |
72 | * Returns the properties of the first passed object that are not present | |
73 | * in all other passed objects. | |
74 | */ | |
75 | Bootstrap.diffObjects = function (objects) { | |
76 | var args = Array.prototype.slice.call(arguments); | |
77 | return _.pick(args[0], _.difference.apply(_, _.map(args, function (obj) { | |
78 | return Object.keys(obj); | |
79 | }))); | |
80 | }; | |
81 | ||
82 | /** | |
83 | * Map of supported events by regular expression. | |
84 | * | |
85 | * @type {Object<Event|MouseEvent|KeyboardEvent|TouchEvent,RegExp>} | |
86 | */ | |
87 | Bootstrap.eventMap = { | |
88 | Event: /^(?:load|unload|abort|error|select|change|submit|reset|focus|blur|resize|scroll)$/, | |
89 | MouseEvent: /^(?:click|dblclick|mouse(?:down|enter|leave|up|over|move|out))$/, | |
90 | KeyboardEvent: /^(?:key(?:down|press|up))$/, | |
91 | TouchEvent: /^(?:touch(?:start|end|move|cancel))$/ | |
92 | }; | |
93 | ||
94 | /** | |
95 | * Extends a jQuery Plugin. | |
96 | * | |
97 | * @param {String} id | |
98 | * A jQuery plugin identifier located in $.fn. | |
99 | * @param {Function} callback | |
100 | * A constructor function used to initialize the for the jQuery plugin. | |
101 | * | |
102 | * @return {Function|Boolean} | |
103 | * The jQuery plugin constructor or FALSE if the plugin does not exist. | |
104 | */ | |
105 | Bootstrap.extendPlugin = function (id, callback) { | |
106 | // Immediately return if plugin doesn't exist. | |
107 | if (typeof $.fn[id] !== 'function') { | |
108 | return this.fatal('Specified jQuery plugin identifier does not exist: @id', {'@id': id}); | |
109 | } | |
110 | ||
111 | // Immediately return if callback isn't a function. | |
112 | if (typeof callback !== 'function') { | |
113 | return this.fatal('You must provide a callback function to extend the jQuery plugin "@id": @callback', {'@id': id, '@callback': callback}); | |
114 | } | |
115 | ||
116 | // Determine existing plugin constructor. | |
117 | var constructor = $.fn[id] && $.fn[id].Constructor || $.fn[id]; | |
118 | var plugin = callback.apply(constructor, [this.settings]); | |
119 | if (!$.isPlainObject(plugin)) { | |
120 | return this.fatal('Returned value from callback is not a plain object that can be used to extend the jQuery plugin "@id": @obj', {'@obj': plugin}); | |
121 | } | |
122 | ||
123 | this.wrapPluginConstructor(constructor, plugin, true); | |
124 | ||
125 | return $.fn[id]; | |
126 | }; | |
127 | ||
128 | Bootstrap.superWrapper = function (parent, fn) { | |
129 | return function () { | |
130 | var previousSuper = this.super; | |
131 | this.super = parent; | |
132 | var ret = fn.apply(this, arguments); | |
133 | if (previousSuper) { | |
134 | this.super = previousSuper; | |
135 | } | |
136 | else { | |
137 | delete this.super; | |
138 | } | |
139 | return ret; | |
140 | }; | |
141 | }; | |
142 | ||
143 | /** | |
144 | * Provide a helper method for displaying when something is went wrong. | |
145 | * | |
146 | * @param {String} message | |
147 | * The message to display. | |
148 | * @param {Object} [args] | |
149 | * An arguments to use in message. | |
150 | * | |
151 | * @return {Boolean} | |
152 | * Always returns FALSE. | |
153 | */ | |
154 | Bootstrap.fatal = function (message, args) { | |
155 | if (this.settings.dev && console.warn) { | |
156 | for (var name in args) { | |
157 | if (args.hasOwnProperty(name) && typeof args[name] === 'object') { | |
158 | args[name] = JSON.stringify(args[name]); | |
159 | } | |
160 | } | |
161 | Drupal.throwError(new Error(Drupal.formatString(message, args))); | |
162 | } | |
163 | return false; | |
164 | }; | |
165 | ||
166 | /** | |
167 | * Intersects object properties. | |
168 | * | |
169 | * @param {...Object} objects | |
170 | * Two or more objects. The first object will be used to return properties | |
171 | * values. | |
172 | * | |
173 | * @return {Object} | |
174 | * Returns the properties of first passed object that intersects with all | |
175 | * other passed objects. | |
176 | */ | |
177 | Bootstrap.intersectObjects = function (objects) { | |
178 | var args = Array.prototype.slice.call(arguments); | |
179 | return _.pick(args[0], _.intersection.apply(_, _.map(args, function (obj) { | |
180 | return Object.keys(obj); | |
181 | }))); | |
182 | }; | |
183 | ||
184 | /** | |
185 | * Normalizes an object's values. | |
186 | * | |
187 | * @param {Object} obj | |
188 | * The object to normalize. | |
189 | * | |
190 | * @return {Object} | |
191 | * The normalized object. | |
192 | */ | |
193 | Bootstrap.normalizeObject = function (obj) { | |
194 | if (!$.isPlainObject(obj)) { | |
195 | return obj; | |
196 | } | |
197 | ||
198 | for (var k in obj) { | |
199 | if (typeof obj[k] === 'string') { | |
200 | if (obj[k] === 'true') { | |
201 | obj[k] = true; | |
202 | } | |
203 | else if (obj[k] === 'false') { | |
204 | obj[k] = false; | |
205 | } | |
206 | else if (obj[k].match(/^[\d-.]$/)) { | |
207 | obj[k] = parseFloat(obj[k]); | |
208 | } | |
209 | } | |
210 | else if ($.isPlainObject(obj[k])) { | |
211 | obj[k] = Bootstrap.normalizeObject(obj[k]); | |
212 | } | |
213 | } | |
214 | ||
215 | return obj; | |
216 | }; | |
217 | ||
218 | /** | |
219 | * An object based once plugin (similar to jquery.once, but without the DOM). | |
220 | * | |
221 | * @param {String} id | |
222 | * A unique identifier. | |
223 | * @param {Function} callback | |
224 | * The callback to invoke if the identifier has not yet been seen. | |
225 | * | |
226 | * @return {Bootstrap} | |
227 | */ | |
228 | Bootstrap.once = function (id, callback) { | |
229 | // Immediately return if identifier has already been processed. | |
230 | if (this.processedOnce[id]) { | |
231 | return this; | |
232 | } | |
233 | callback.call(this, this.settings); | |
234 | this.processedOnce[id] = true; | |
235 | return this; | |
236 | }; | |
237 | ||
238 | /** | |
239 | * Provide jQuery UI like ability to get/set options for Bootstrap plugins. | |
240 | * | |
241 | * @param {string|object} key | |
242 | * A string value of the option to set, can be dot like to a nested key. | |
243 | * An object of key/value pairs. | |
244 | * @param {*} [value] | |
245 | * (optional) A value to set for key. | |
246 | * | |
247 | * @returns {*} | |
248 | * - Returns nothing if key is an object or both key and value parameters | |
249 | * were provided to set an option. | |
250 | * - Returns the a value for a specific setting if key was provided. | |
251 | * - Returns an object of key/value pairs of all the options if no key or | |
252 | * value parameter was provided. | |
253 | * | |
254 | * @see https://github.com/jquery/jquery-ui/blob/master/ui/widget.js | |
255 | */ | |
256 | Bootstrap.option = function (key, value) { | |
257 | var options = $.isPlainObject(key) ? $.extend({}, key) : {}; | |
258 | ||
259 | // Get all options (clone so it doesn't reference the internal object). | |
260 | if (arguments.length === 0) { | |
261 | return $.extend({}, this.options); | |
262 | } | |
263 | ||
264 | // Get/set single option. | |
265 | if (typeof key === "string") { | |
266 | // Handle nested keys in dot notation. | |
267 | // e.g., "foo.bar" => { foo: { bar: true } } | |
268 | var parts = key.split('.'); | |
269 | key = parts.shift(); | |
270 | var obj = options; | |
271 | if (parts.length) { | |
272 | for (var i = 0; i < parts.length - 1; i++) { | |
273 | obj[parts[i]] = obj[parts[i]] || {}; | |
274 | obj = obj[parts[i]]; | |
275 | } | |
276 | key = parts.pop(); | |
277 | } | |
278 | ||
279 | // Get. | |
280 | if (arguments.length === 1) { | |
281 | return obj[key] === void 0 ? null : obj[key]; | |
282 | } | |
283 | ||
284 | // Set. | |
285 | obj[key] = value; | |
286 | } | |
287 | ||
288 | // Set multiple options. | |
289 | $.extend(true, this.options, options); | |
290 | }; | |
291 | ||
292 | /** | |
293 | * Adds a ".noConflict()" helper method if needed. | |
294 | * | |
295 | * @param {String} id | |
296 | * A jQuery plugin identifier located in $.fn. | |
297 | * @param {Function} plugin | |
298 | * @param {Function} plugin | |
299 | * A constructor function used to initialize the for the jQuery plugin. | |
300 | * @param {Boolean} [noConflict] | |
301 | * Flag indicating whether or not to create a ".noConflict()" helper method | |
302 | * for the plugin. | |
303 | */ | |
304 | Bootstrap.pluginNoConflict = function (id, plugin, noConflict) { | |
305 | if (plugin.noConflict === void 0 && (noConflict === void 0 || noConflict)) { | |
306 | var old = $.fn[id]; | |
307 | plugin.noConflict = function () { | |
308 | $.fn[id] = old; | |
309 | return this; | |
310 | }; | |
311 | } | |
312 | }; | |
313 | ||
314 | /** | |
315 | * Replaces a Bootstrap jQuery plugin definition. | |
316 | * | |
317 | * @param {String} id | |
318 | * A jQuery plugin identifier located in $.fn. | |
319 | * @param {Function} callback | |
320 | * A callback function that is immediately invoked and must return a | |
321 | * function that will be used as the plugin constructor. | |
322 | * @param {Boolean} [noConflict] | |
323 | * Flag indicating whether or not to create a ".noConflict()" helper method | |
324 | * for the plugin. | |
325 | */ | |
326 | Bootstrap.replacePlugin = function (id, callback, noConflict) { | |
327 | // Immediately return if plugin doesn't exist. | |
328 | if (typeof $.fn[id] !== 'function') { | |
329 | return this.fatal('Specified jQuery plugin identifier does not exist: @id', {'@id': id}); | |
330 | } | |
331 | ||
332 | // Immediately return if callback isn't a function. | |
333 | if (typeof callback !== 'function') { | |
334 | return this.fatal('You must provide a valid callback function to replace a jQuery plugin: @callback', {'@callback': callback}); | |
335 | } | |
336 | ||
337 | // Determine existing plugin constructor. | |
338 | var constructor = $.fn[id] && $.fn[id].Constructor || $.fn[id]; | |
339 | var plugin = callback.apply(constructor, [this.settings]); | |
340 | ||
341 | // Immediately return if plugin isn't a function. | |
342 | if (typeof plugin !== 'function') { | |
343 | return this.fatal('Returned value from callback is not a usable function to replace a jQuery plugin "@id": @plugin', {'@id': id, '@plugin': plugin}); | |
344 | } | |
345 | ||
346 | this.wrapPluginConstructor(constructor, plugin); | |
347 | ||
348 | // Add a ".noConflict()" helper method. | |
349 | this.pluginNoConflict(id, plugin, noConflict); | |
350 | ||
351 | $.fn[id] = plugin; | |
352 | }; | |
353 | ||
354 | /** | |
355 | * Simulates a native event on an element in the browser. | |
356 | * | |
357 | * Note: This is a fairly complete modern implementation. If things aren't | |
358 | * working quite the way you intend (in older browsers), you may wish to use | |
359 | * the jQuery.simulate plugin. If it's available, this method will defer to | |
360 | * that plugin. | |
361 | * | |
362 | * @see https://github.com/jquery/jquery-simulate | |
363 | * | |
364 | * @param {HTMLElement|jQuery} element | |
365 | * A DOM element to dispatch event on. Note: this may be a jQuery object, | |
366 | * however be aware that this will trigger the same event for each element | |
367 | * inside the jQuery collection; use with caution. | |
368 | * @param {String|String[]} type | |
369 | * The type(s) of event to simulate. | |
370 | * @param {Object} [options] | |
371 | * An object of options to pass to the event constructor. Typically, if | |
372 | * an event is being proxied, you should just pass the original event | |
373 | * object here. This allows, if the browser supports it, to be a truly | |
374 | * simulated event. | |
375 | * | |
376 | * @return {Boolean} | |
377 | * The return value is false if event is cancelable and at least one of the | |
378 | * event handlers which handled this event called Event.preventDefault(). | |
379 | * Otherwise it returns true. | |
380 | */ | |
381 | Bootstrap.simulate = function (element, type, options) { | |
382 | // Handle jQuery object wrappers so it triggers on each element. | |
383 | var ret = true; | |
384 | if (element instanceof $) { | |
385 | element.each(function () { | |
386 | if (!Bootstrap.simulate(this, type, options)) { | |
387 | ret = false; | |
388 | } | |
389 | }); | |
390 | return ret; | |
391 | } | |
392 | ||
393 | if (!(element instanceof HTMLElement)) { | |
394 | this.fatal('Passed element must be an instance of HTMLElement, got "@type" instead.', { | |
395 | '@type': typeof element, | |
396 | }); | |
397 | } | |
398 | ||
399 | // Defer to the jQuery.simulate plugin, if it's available. | |
400 | if (typeof $.simulate === 'function') { | |
401 | new $.simulate(element, type, options); | |
402 | return true; | |
403 | } | |
404 | ||
405 | var event; | |
406 | var ctor; | |
407 | var types = [].concat(type); | |
408 | for (var i = 0, l = types.length; i < l; i++) { | |
409 | type = types[i]; | |
410 | for (var name in this.eventMap) { | |
411 | if (this.eventMap[name].test(type)) { | |
412 | ctor = name; | |
413 | break; | |
414 | } | |
415 | } | |
416 | if (!ctor) { | |
417 | throw new SyntaxError('Only rudimentary HTMLEvents, KeyboardEvents and MouseEvents are supported: ' + type); | |
418 | } | |
419 | var opts = {bubbles: true, cancelable: true}; | |
420 | if (ctor === 'KeyboardEvent' || ctor === 'MouseEvent') { | |
421 | $.extend(opts, {ctrlKey: !1, altKey: !1, shiftKey: !1, metaKey: !1}); | |
422 | } | |
423 | if (ctor === 'MouseEvent') { | |
424 | $.extend(opts, {button: 0, pointerX: 0, pointerY: 0, view: window}); | |
425 | } | |
426 | if (options) { | |
427 | $.extend(opts, options); | |
428 | } | |
429 | if (typeof window[ctor] === 'function') { | |
430 | event = new window[ctor](type, opts); | |
431 | if (!element.dispatchEvent(event)) { | |
432 | ret = false; | |
433 | } | |
434 | } | |
435 | else if (document.createEvent) { | |
436 | event = document.createEvent(ctor); | |
437 | event.initEvent(type, opts.bubbles, opts.cancelable); | |
438 | if (!element.dispatchEvent(event)) { | |
439 | ret = false; | |
440 | } | |
441 | } | |
442 | else if (typeof element.fireEvent === 'function') { | |
443 | event = $.extend(document.createEventObject(), opts); | |
444 | if (!element.fireEvent('on' + type, event)) { | |
445 | ret = false; | |
446 | } | |
447 | } | |
448 | else if (typeof element[type]) { | |
449 | element[type](); | |
450 | } | |
451 | } | |
452 | return ret; | |
453 | }; | |
454 | ||
455 | /** | |
456 | * Strips HTML and returns just text. | |
457 | * | |
458 | * @param {String|Element|jQuery} html | |
459 | * A string of HTML content, an Element DOM object or a jQuery object. | |
460 | * | |
461 | * @return {String} | |
462 | * The text without HTML tags. | |
463 | * | |
464 | * @todo Replace with http://locutus.io/php/strings/strip_tags/ | |
465 | */ | |
466 | Bootstrap.stripHtml = function (html) { | |
467 | if (html instanceof $) { | |
468 | html = html.html(); | |
469 | } | |
470 | else if (html instanceof Element) { | |
471 | html = html.innerHTML; | |
472 | } | |
473 | var tmp = document.createElement('DIV'); | |
474 | tmp.innerHTML = html; | |
475 | return (tmp.textContent || tmp.innerText || '').replace(/^[\s\n\t]*|[\s\n\t]*$/, ''); | |
476 | }; | |
477 | ||
478 | /** | |
479 | * Provide a helper method for displaying when something is unsupported. | |
480 | * | |
481 | * @param {String} type | |
482 | * The type of unsupported object, e.g. method or option. | |
483 | * @param {String} name | |
484 | * The name of the unsupported object. | |
485 | * @param {*} [value] | |
486 | * The value of the unsupported object. | |
487 | */ | |
488 | Bootstrap.unsupported = function (type, name, value) { | |
489 | Bootstrap.warn('Unsupported by Drupal Bootstrap: (@type) @name -> @value', { | |
490 | '@type': type, | |
491 | '@name': name, | |
492 | '@value': typeof value === 'object' ? JSON.stringify(value) : value | |
493 | }); | |
494 | }; | |
495 | ||
496 | /** | |
497 | * Provide a helper method to display a warning. | |
498 | * | |
499 | * @param {String} message | |
500 | * The message to display. | |
501 | * @param {Object} [args] | |
502 | * Arguments to use as replacements in Drupal.formatString. | |
503 | */ | |
504 | Bootstrap.warn = function (message, args) { | |
505 | if (this.settings.dev && console.warn) { | |
506 | console.warn(Drupal.formatString(message, args)); | |
507 | } | |
508 | }; | |
509 | ||
510 | /** | |
511 | * Wraps a plugin with common functionality. | |
512 | * | |
513 | * @param {Function} constructor | |
514 | * A plugin constructor being wrapped. | |
515 | * @param {Object|Function} plugin | |
516 | * The plugin being wrapped. | |
517 | * @param {Boolean} [extend = false] | |
518 | * Whether to add super extensibility. | |
519 | */ | |
520 | Bootstrap.wrapPluginConstructor = function (constructor, plugin, extend) { | |
521 | var proto = constructor.prototype; | |
522 | ||
523 | // Add a jQuery UI like option getter/setter method. | |
524 | var option = this.option; | |
525 | if (proto.option === void(0)) { | |
526 | proto.option = function () { | |
527 | return option.apply(this, arguments); | |
528 | }; | |
529 | } | |
530 | ||
531 | if (extend) { | |
532 | // Handle prototype properties separately. | |
533 | if (plugin.prototype !== void 0) { | |
534 | for (var key in plugin.prototype) { | |
535 | if (!plugin.prototype.hasOwnProperty(key)) continue; | |
536 | var value = plugin.prototype[key]; | |
537 | if (typeof value === 'function') { | |
538 | proto[key] = this.superWrapper(proto[key] || function () {}, value); | |
539 | } | |
540 | else { | |
541 | proto[key] = $.isPlainObject(value) ? $.extend(true, {}, proto[key], value) : value; | |
542 | } | |
543 | } | |
544 | } | |
545 | delete plugin.prototype; | |
546 | ||
547 | // Handle static properties. | |
548 | for (key in plugin) { | |
549 | if (!plugin.hasOwnProperty(key)) continue; | |
550 | value = plugin[key]; | |
551 | if (typeof value === 'function') { | |
552 | constructor[key] = this.superWrapper(constructor[key] || function () {}, value); | |
553 | } | |
554 | else { | |
555 | constructor[key] = $.isPlainObject(value) ? $.extend(true, {}, constructor[key], value) : value; | |
556 | } | |
557 | } | |
558 | } | |
559 | }; | |
560 | ||
561 | /** | |
562 | * Add Bootstrap to the global Drupal object. | |
563 | * | |
564 | * @type {Bootstrap} | |
565 | */ | |
566 | Drupal.bootstrap = Drupal.bootstrap || Bootstrap; | |
567 | ||
568 | })(window._, window.jQuery, window.Drupal, window.drupalSettings); |