added site license to ryf site footer
[ryf-theme.git] / js / drupal.bootstrap.js
CommitLineData
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);