Updated the Bootstrap theme.
[yaffs-website] / web / themes / contrib / bootstrap / js / drupal.bootstrap.js
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 proto = constructor.prototype;
119
120     var obj = callback.apply(constructor, [this.settings]);
121     if (!$.isPlainObject(obj)) {
122       return this.fatal('Returned value from callback is not a plain object that can be used to extend the jQuery plugin "@id": @obj', {'@obj':  obj});
123     }
124
125     // Add a jQuery UI like option getter/setter method.
126     var option = this.option;
127     if (proto.option === void(0)) {
128       proto.option = function () {
129         return option.apply(this, arguments);
130       };
131     }
132
133     // Handle prototype properties separately.
134     if (obj.prototype !== void 0) {
135       for (var key in obj.prototype) {
136         if (!obj.prototype.hasOwnProperty(key)) continue;
137         var value = obj.prototype[key];
138         if (typeof value === 'function') {
139           proto[key] = this.superWrapper(proto[key] || function () {}, value);
140         }
141         else {
142           proto[key] = $.isPlainObject(value) ? $.extend(true, {}, proto[key], value) : value;
143         }
144       }
145     }
146     delete obj.prototype;
147
148     // Handle static properties.
149     for (key in obj) {
150       if (!obj.hasOwnProperty(key)) continue;
151       value = obj[key];
152       if (typeof value === 'function') {
153         constructor[key] = this.superWrapper(constructor[key] || function () {}, value);
154       }
155       else {
156         constructor[key] = $.isPlainObject(value) ? $.extend(true, {}, constructor[key], value) : value;
157       }
158     }
159
160     return $.fn[id];
161   };
162
163   Bootstrap.superWrapper = function (parent, fn) {
164     return function () {
165       var previousSuper = this.super;
166       this.super = parent;
167       var ret = fn.apply(this, arguments);
168       if (previousSuper) {
169         this.super = previousSuper;
170       }
171       else {
172         delete this.super;
173       }
174       return ret;
175     };
176   };
177
178   /**
179    * Provide a helper method for displaying when something is went wrong.
180    *
181    * @param {String} message
182    *   The message to display.
183    * @param {Object} [args]
184    *   An arguments to use in message.
185    *
186    * @return {Boolean}
187    *   Always returns FALSE.
188    */
189   Bootstrap.fatal = function (message, args) {
190     if (this.settings.dev && console.warn) {
191       for (var name in args) {
192         if (args.hasOwnProperty(name) && typeof args[name] === 'object') {
193           args[name] = JSON.stringify(args[name]);
194         }
195       }
196       Drupal.throwError(new Error(Drupal.formatString(message, args)));
197     }
198     return false;
199   };
200
201   /**
202    * Intersects object properties.
203    *
204    * @param {...Object} objects
205    *   Two or more objects. The first object will be used to return properties
206    *   values.
207    *
208    * @return {Object}
209    *   Returns the properties of first passed object that intersects with all
210    *   other passed objects.
211    */
212   Bootstrap.intersectObjects = function (objects) {
213     var args = Array.prototype.slice.call(arguments);
214     return _.pick(args[0], _.intersection.apply(_, _.map(args, function (obj) {
215       return Object.keys(obj);
216     })));
217   };
218
219   /**
220    * An object based once plugin (similar to jquery.once, but without the DOM).
221    *
222    * @param {String} id
223    *   A unique identifier.
224    * @param {Function} callback
225    *   The callback to invoke if the identifier has not yet been seen.
226    *
227    * @return {Bootstrap}
228    */
229   Bootstrap.once = function (id, callback) {
230     // Immediately return if identifier has already been processed.
231     if (this.processedOnce[id]) {
232       return this;
233     }
234     callback.call(this, this.settings);
235     this.processedOnce[id] = true;
236     return this;
237   };
238
239   /**
240    * Provide jQuery UI like ability to get/set options for Bootstrap plugins.
241    *
242    * @param {string|object} key
243    *   A string value of the option to set, can be dot like to a nested key.
244    *   An object of key/value pairs.
245    * @param {*} [value]
246    *   (optional) A value to set for key.
247    *
248    * @returns {*}
249    *   - Returns nothing if key is an object or both key and value parameters
250    *   were provided to set an option.
251    *   - Returns the a value for a specific setting if key was provided.
252    *   - Returns an object of key/value pairs of all the options if no key or
253    *   value parameter was provided.
254    *
255    * @see https://github.com/jquery/jquery-ui/blob/master/ui/widget.js
256    */
257   Bootstrap.option = function (key, value) {
258     var options = $.isPlainObject(key) ? $.extend({}, key) : {};
259
260     // Get all options (clone so it doesn't reference the internal object).
261     if (arguments.length === 0) {
262       return $.extend({}, this.options);
263     }
264
265     // Get/set single option.
266     if (typeof key === "string") {
267       // Handle nested keys in dot notation.
268       // e.g., "foo.bar" => { foo: { bar: true } }
269       var parts = key.split('.');
270       key = parts.shift();
271       var obj = options;
272       if (parts.length) {
273         for (var i = 0; i < parts.length - 1; i++) {
274           obj[parts[i]] = obj[parts[i]] || {};
275           obj = obj[parts[i]];
276         }
277         key = parts.pop();
278       }
279
280       // Get.
281       if (arguments.length === 1) {
282         return obj[key] === void 0 ? null : obj[key];
283       }
284
285       // Set.
286       obj[key] = value;
287     }
288
289     // Set multiple options.
290     $.extend(true, this.options, options);
291   };
292
293   /**
294    * Adds a ".noConflict()" helper method if needed.
295    *
296    * @param {String} id
297    *   A jQuery plugin identifier located in $.fn.
298    * @param {Function} plugin
299    * @param {Function} plugin
300    *   A constructor function used to initialize the for the jQuery plugin.
301    * @param {Boolean} [noConflict]
302    *   Flag indicating whether or not to create a ".noConflict()" helper method
303    *   for the plugin.
304    */
305   Bootstrap.pluginNoConflict = function (id, plugin, noConflict) {
306     if (plugin.noConflict === void 0 && (noConflict === void 0 || noConflict)) {
307       var old = $.fn[id];
308       plugin.noConflict = function () {
309         $.fn[id] = old;
310         return this;
311       };
312     }
313   };
314
315   /**
316    * Replaces a Bootstrap jQuery plugin definition.
317    *
318    * @param {String} id
319    *   A jQuery plugin identifier located in $.fn.
320    * @param {Function} callback
321    *   A callback function that is immediately invoked and must return a
322    *   function that will be used as the plugin constructor.
323    * @param {Boolean} [noConflict]
324    *   Flag indicating whether or not to create a ".noConflict()" helper method
325    *   for the plugin.
326    */
327   Bootstrap.replacePlugin = function (id, callback, noConflict) {
328     // Immediately return if plugin doesn't exist.
329     if (typeof $.fn[id] !== 'function') {
330       return this.fatal('Specified jQuery plugin identifier does not exist: @id', {'@id':  id});
331     }
332
333     // Immediately return if callback isn't a function.
334     if (typeof callback !== 'function') {
335       return this.fatal('You must provide a valid callback function to replace a jQuery plugin: @callback', {'@callback': callback});
336     }
337
338     // Determine existing plugin constructor.
339     var constructor = $.fn[id] && $.fn[id].Constructor || $.fn[id];
340     var plugin = callback.apply(constructor, [this.settings]);
341
342     // Immediately return if plugin isn't a function.
343     if (typeof plugin !== 'function') {
344       return this.fatal('Returned value from callback is not a usable function to replace a jQuery plugin "@id": @plugin', {'@id': id, '@plugin': plugin});
345     }
346
347     // Add a ".noConflict()" helper method.
348     this.pluginNoConflict(id, plugin, noConflict);
349
350     $.fn[id] = plugin;
351   };
352
353   /**
354    * Simulates a native event on an element in the browser.
355    *
356    * Note: This is a fairly complete modern implementation. If things aren't
357    * working quite the way you intend (in older browsers), you may wish to use
358    * the jQuery.simulate plugin. If it's available, this method will defer to
359    * that plugin.
360    *
361    * @see https://github.com/jquery/jquery-simulate
362    *
363    * @param {HTMLElement|jQuery} element
364    *   A DOM element to dispatch event on. Note: this may be a jQuery object,
365    *   however be aware that this will trigger the same event for each element
366    *   inside the jQuery collection; use with caution.
367    * @param {String} type
368    *   The type of event to simulate.
369    * @param {Object} [options]
370    *   An object of options to pass to the event constructor. Typically, if
371    *   an event is being proxied, you should just pass the original event
372    *   object here. This allows, if the browser supports it, to be a truly
373    *   simulated event.
374    *
375    * @return {Boolean}
376    *   The return value is false if event is cancelable and at least one of the
377    *   event handlers which handled this event called Event.preventDefault().
378    *   Otherwise it returns true.
379    */
380   Bootstrap.simulate = function (element, type, options) {
381     // Handle jQuery object wrappers so it triggers on each element.
382     if (element instanceof $) {
383       var ret = true;
384       element.each(function () {
385         if (!Bootstrap.simulate(this, type, options)) {
386           ret = false;
387         }
388       });
389       return ret;
390     }
391
392     if (!(element instanceof HTMLElement)) {
393       this.fatal('Passed element must be an instance of HTMLElement, got "@type" instead.', {
394         '@type': typeof element,
395       });
396     }
397
398     // Defer to the jQuery.simulate plugin, if it's available.
399     if (typeof $.simulate === 'function') {
400       new $.simulate(element, type, options);
401       return true;
402     }
403
404     var event;
405     var ctor;
406     for (var name in this.eventMap) {
407       if (this.eventMap[name].test(type)) {
408         ctor = name;
409         break;
410       }
411     }
412     if (!ctor) {
413       throw new SyntaxError('Only rudimentary HTMLEvents, KeyboardEvents and MouseEvents are supported: ' + type);
414     }
415     var opts = {bubbles: true, cancelable: true};
416     if (ctor === 'KeyboardEvent' || ctor === 'MouseEvent') {
417       $.extend(opts, {ctrlKey: !1, altKey: !1, shiftKey: !1, metaKey: !1});
418     }
419     if (ctor === 'MouseEvent') {
420       $.extend(opts, {button: 0, pointerX: 0, pointerY: 0, view: window});
421     }
422     if (options) {
423       $.extend(opts, options);
424     }
425     if (typeof window[ctor] === 'function') {
426       event = new window[ctor](type, opts);
427       return element.dispatchEvent(event);
428     }
429     else if (document.createEvent) {
430       event = document.createEvent(ctor);
431       event.initEvent(type, opts.bubbles, opts.cancelable);
432       return element.dispatchEvent(event);
433     }
434     else if (typeof element.fireEvent === 'function') {
435       event = $.extend(document.createEventObject(), opts);
436       return element.fireEvent('on' + type, event);
437     }
438     else if (typeof element[type]) {
439       element[type]();
440       return true;
441     }
442   };
443
444   /**
445    * Strips HTML and returns just text.
446    *
447    * @param {String|Element|jQuery} html
448    *   A string of HTML content, an Element DOM object or a jQuery object.
449    *
450    * @return {String}
451    *   The text without HTML tags.
452    *
453    * @todo Replace with http://locutus.io/php/strings/strip_tags/
454    */
455   Bootstrap.stripHtml = function (html) {
456     if (html instanceof $) {
457       html = html.html();
458     }
459     else if (html instanceof Element) {
460       html = html.innerHTML;
461     }
462     var tmp = document.createElement('DIV');
463     tmp.innerHTML = html;
464     return (tmp.textContent || tmp.innerText || '').replace(/^[\s\n\t]*|[\s\n\t]*$/g, '');
465   };
466
467   /**
468    * Provide a helper method for displaying when something is unsupported.
469    *
470    * @param {String} type
471    *   The type of unsupported object, e.g. method or option.
472    * @param {String} name
473    *   The name of the unsupported object.
474    * @param {*} [value]
475    *   The value of the unsupported object.
476    */
477   Bootstrap.unsupported = function (type, name, value) {
478     Bootstrap.warn('Unsupported by Drupal Bootstrap: (@type) @name -> @value', {
479       '@type': type,
480       '@name': name,
481       '@value': typeof value === 'object' ? JSON.stringify(value) : value
482     });
483   };
484
485   /**
486    * Provide a helper method to display a warning.
487    *
488    * @param {String} message
489    *   The message to display.
490    * @param {Object} [args]
491    *   Arguments to use as replacements in Drupal.formatString.
492    */
493   Bootstrap.warn = function (message, args) {
494     if (this.settings.dev && console.warn) {
495       console.warn(Drupal.formatString(message, args));
496     }
497   };
498
499   /**
500    * Add Bootstrap to the global Drupal object.
501    *
502    * @type {Bootstrap}
503    */
504   Drupal.bootstrap = Drupal.bootstrap || Bootstrap;
505
506 })(window._, window.jQuery, window.Drupal, window.drupalSettings);