/** * @file * Drupal Bootstrap object. */ /** * All Drupal Bootstrap JavaScript APIs are contained in this namespace. * * @namespace */ (function (_, $, Drupal, drupalSettings) { 'use strict'; var Bootstrap = { processedOnce: {}, settings: drupalSettings.bootstrap || {} }; /** * Wraps Drupal.checkPlain() to ensure value passed isn't empty. * * Encodes special characters in a plain-text string for display as HTML. * * @param {string} str * The string to be encoded. * * @return {string} * The encoded string. * * @ingroup sanitization */ Bootstrap.checkPlain = function (str) { return str && Drupal.checkPlain(str) || ''; }; /** * Creates a jQuery plugin. * * @param {String} id * A jQuery plugin identifier located in $.fn. * @param {Function} plugin * A constructor function used to initialize the for the jQuery plugin. * @param {Boolean} [noConflict] * Flag indicating whether or not to create a ".noConflict()" helper method * for the plugin. */ Bootstrap.createPlugin = function (id, plugin, noConflict) { // Immediately return if plugin doesn't exist. if ($.fn[id] !== void 0) { return this.fatal('Specified jQuery plugin identifier already exists: @id. Use Drupal.bootstrap.replacePlugin() instead.', {'@id': id}); } // Immediately return if plugin isn't a function. if (typeof plugin !== 'function') { return this.fatal('You must provide a constructor function to create a jQuery plugin "@id": @plugin', {'@id': id, '@plugin': plugin}); } // Add a ".noConflict()" helper method. this.pluginNoConflict(id, plugin, noConflict); $.fn[id] = plugin; }; /** * Diff object properties. * * @param {...Object} objects * Two or more objects. The first object will be used to return properties * values. * * @return {Object} * Returns the properties of the first passed object that are not present * in all other passed objects. */ Bootstrap.diffObjects = function (objects) { var args = Array.prototype.slice.call(arguments); return _.pick(args[0], _.difference.apply(_, _.map(args, function (obj) { return Object.keys(obj); }))); }; /** * Map of supported events by regular expression. * * @type {Object} */ Bootstrap.eventMap = { Event: /^(?:load|unload|abort|error|select|change|submit|reset|focus|blur|resize|scroll)$/, MouseEvent: /^(?:click|dblclick|mouse(?:down|enter|leave|up|over|move|out))$/, KeyboardEvent: /^(?:key(?:down|press|up))$/, TouchEvent: /^(?:touch(?:start|end|move|cancel))$/ }; /** * Extends a jQuery Plugin. * * @param {String} id * A jQuery plugin identifier located in $.fn. * @param {Function} callback * A constructor function used to initialize the for the jQuery plugin. * * @return {Function|Boolean} * The jQuery plugin constructor or FALSE if the plugin does not exist. */ Bootstrap.extendPlugin = function (id, callback) { // Immediately return if plugin doesn't exist. if (typeof $.fn[id] !== 'function') { return this.fatal('Specified jQuery plugin identifier does not exist: @id', {'@id': id}); } // Immediately return if callback isn't a function. if (typeof callback !== 'function') { return this.fatal('You must provide a callback function to extend the jQuery plugin "@id": @callback', {'@id': id, '@callback': callback}); } // Determine existing plugin constructor. var constructor = $.fn[id] && $.fn[id].Constructor || $.fn[id]; var plugin = callback.apply(constructor, [this.settings]); if (!$.isPlainObject(plugin)) { 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}); } this.wrapPluginConstructor(constructor, plugin, true); return $.fn[id]; }; Bootstrap.superWrapper = function (parent, fn) { return function () { var previousSuper = this.super; this.super = parent; var ret = fn.apply(this, arguments); if (previousSuper) { this.super = previousSuper; } else { delete this.super; } return ret; }; }; /** * Provide a helper method for displaying when something is went wrong. * * @param {String} message * The message to display. * @param {Object} [args] * An arguments to use in message. * * @return {Boolean} * Always returns FALSE. */ Bootstrap.fatal = function (message, args) { if (this.settings.dev && console.warn) { for (var name in args) { if (args.hasOwnProperty(name) && typeof args[name] === 'object') { args[name] = JSON.stringify(args[name]); } } Drupal.throwError(new Error(Drupal.formatString(message, args))); } return false; }; /** * Intersects object properties. * * @param {...Object} objects * Two or more objects. The first object will be used to return properties * values. * * @return {Object} * Returns the properties of first passed object that intersects with all * other passed objects. */ Bootstrap.intersectObjects = function (objects) { var args = Array.prototype.slice.call(arguments); return _.pick(args[0], _.intersection.apply(_, _.map(args, function (obj) { return Object.keys(obj); }))); }; /** * Normalizes an object's values. * * @param {Object} obj * The object to normalize. * * @return {Object} * The normalized object. */ Bootstrap.normalizeObject = function (obj) { if (!$.isPlainObject(obj)) { return obj; } for (var k in obj) { if (typeof obj[k] === 'string') { if (obj[k] === 'true') { obj[k] = true; } else if (obj[k] === 'false') { obj[k] = false; } else if (obj[k].match(/^[\d-.]$/)) { obj[k] = parseFloat(obj[k]); } } else if ($.isPlainObject(obj[k])) { obj[k] = Bootstrap.normalizeObject(obj[k]); } } return obj; }; /** * An object based once plugin (similar to jquery.once, but without the DOM). * * @param {String} id * A unique identifier. * @param {Function} callback * The callback to invoke if the identifier has not yet been seen. * * @return {Bootstrap} */ Bootstrap.once = function (id, callback) { // Immediately return if identifier has already been processed. if (this.processedOnce[id]) { return this; } callback.call(this, this.settings); this.processedOnce[id] = true; return this; }; /** * Provide jQuery UI like ability to get/set options for Bootstrap plugins. * * @param {string|object} key * A string value of the option to set, can be dot like to a nested key. * An object of key/value pairs. * @param {*} [value] * (optional) A value to set for key. * * @returns {*} * - Returns nothing if key is an object or both key and value parameters * were provided to set an option. * - Returns the a value for a specific setting if key was provided. * - Returns an object of key/value pairs of all the options if no key or * value parameter was provided. * * @see https://github.com/jquery/jquery-ui/blob/master/ui/widget.js */ Bootstrap.option = function (key, value) { var options = $.isPlainObject(key) ? $.extend({}, key) : {}; // Get all options (clone so it doesn't reference the internal object). if (arguments.length === 0) { return $.extend({}, this.options); } // Get/set single option. if (typeof key === "string") { // Handle nested keys in dot notation. // e.g., "foo.bar" => { foo: { bar: true } } var parts = key.split('.'); key = parts.shift(); var obj = options; if (parts.length) { for (var i = 0; i < parts.length - 1; i++) { obj[parts[i]] = obj[parts[i]] || {}; obj = obj[parts[i]]; } key = parts.pop(); } // Get. if (arguments.length === 1) { return obj[key] === void 0 ? null : obj[key]; } // Set. obj[key] = value; } // Set multiple options. $.extend(true, this.options, options); }; /** * Adds a ".noConflict()" helper method if needed. * * @param {String} id * A jQuery plugin identifier located in $.fn. * @param {Function} plugin * @param {Function} plugin * A constructor function used to initialize the for the jQuery plugin. * @param {Boolean} [noConflict] * Flag indicating whether or not to create a ".noConflict()" helper method * for the plugin. */ Bootstrap.pluginNoConflict = function (id, plugin, noConflict) { if (plugin.noConflict === void 0 && (noConflict === void 0 || noConflict)) { var old = $.fn[id]; plugin.noConflict = function () { $.fn[id] = old; return this; }; } }; /** * Replaces a Bootstrap jQuery plugin definition. * * @param {String} id * A jQuery plugin identifier located in $.fn. * @param {Function} callback * A callback function that is immediately invoked and must return a * function that will be used as the plugin constructor. * @param {Boolean} [noConflict] * Flag indicating whether or not to create a ".noConflict()" helper method * for the plugin. */ Bootstrap.replacePlugin = function (id, callback, noConflict) { // Immediately return if plugin doesn't exist. if (typeof $.fn[id] !== 'function') { return this.fatal('Specified jQuery plugin identifier does not exist: @id', {'@id': id}); } // Immediately return if callback isn't a function. if (typeof callback !== 'function') { return this.fatal('You must provide a valid callback function to replace a jQuery plugin: @callback', {'@callback': callback}); } // Determine existing plugin constructor. var constructor = $.fn[id] && $.fn[id].Constructor || $.fn[id]; var plugin = callback.apply(constructor, [this.settings]); // Immediately return if plugin isn't a function. if (typeof plugin !== 'function') { return this.fatal('Returned value from callback is not a usable function to replace a jQuery plugin "@id": @plugin', {'@id': id, '@plugin': plugin}); } this.wrapPluginConstructor(constructor, plugin); // Add a ".noConflict()" helper method. this.pluginNoConflict(id, plugin, noConflict); $.fn[id] = plugin; }; /** * Simulates a native event on an element in the browser. * * Note: This is a fairly complete modern implementation. If things aren't * working quite the way you intend (in older browsers), you may wish to use * the jQuery.simulate plugin. If it's available, this method will defer to * that plugin. * * @see https://github.com/jquery/jquery-simulate * * @param {HTMLElement|jQuery} element * A DOM element to dispatch event on. Note: this may be a jQuery object, * however be aware that this will trigger the same event for each element * inside the jQuery collection; use with caution. * @param {String|String[]} type * The type(s) of event to simulate. * @param {Object} [options] * An object of options to pass to the event constructor. Typically, if * an event is being proxied, you should just pass the original event * object here. This allows, if the browser supports it, to be a truly * simulated event. * * @return {Boolean} * The return value is false if event is cancelable and at least one of the * event handlers which handled this event called Event.preventDefault(). * Otherwise it returns true. */ Bootstrap.simulate = function (element, type, options) { // Handle jQuery object wrappers so it triggers on each element. var ret = true; if (element instanceof $) { element.each(function () { if (!Bootstrap.simulate(this, type, options)) { ret = false; } }); return ret; } if (!(element instanceof HTMLElement)) { this.fatal('Passed element must be an instance of HTMLElement, got "@type" instead.', { '@type': typeof element, }); } // Defer to the jQuery.simulate plugin, if it's available. if (typeof $.simulate === 'function') { new $.simulate(element, type, options); return true; } var event; var ctor; var types = [].concat(type); for (var i = 0, l = types.length; i < l; i++) { type = types[i]; for (var name in this.eventMap) { if (this.eventMap[name].test(type)) { ctor = name; break; } } if (!ctor) { throw new SyntaxError('Only rudimentary HTMLEvents, KeyboardEvents and MouseEvents are supported: ' + type); } var opts = {bubbles: true, cancelable: true}; if (ctor === 'KeyboardEvent' || ctor === 'MouseEvent') { $.extend(opts, {ctrlKey: !1, altKey: !1, shiftKey: !1, metaKey: !1}); } if (ctor === 'MouseEvent') { $.extend(opts, {button: 0, pointerX: 0, pointerY: 0, view: window}); } if (options) { $.extend(opts, options); } if (typeof window[ctor] === 'function') { event = new window[ctor](type, opts); if (!element.dispatchEvent(event)) { ret = false; } } else if (document.createEvent) { event = document.createEvent(ctor); event.initEvent(type, opts.bubbles, opts.cancelable); if (!element.dispatchEvent(event)) { ret = false; } } else if (typeof element.fireEvent === 'function') { event = $.extend(document.createEventObject(), opts); if (!element.fireEvent('on' + type, event)) { ret = false; } } else if (typeof element[type]) { element[type](); } } return ret; }; /** * Strips HTML and returns just text. * * @param {String|Element|jQuery} html * A string of HTML content, an Element DOM object or a jQuery object. * * @return {String} * The text without HTML tags. * * @todo Replace with http://locutus.io/php/strings/strip_tags/ */ Bootstrap.stripHtml = function (html) { if (html instanceof $) { html = html.html(); } else if (html instanceof Element) { html = html.innerHTML; } var tmp = document.createElement('DIV'); tmp.innerHTML = html; return (tmp.textContent || tmp.innerText || '').replace(/^[\s\n\t]*|[\s\n\t]*$/, ''); }; /** * Provide a helper method for displaying when something is unsupported. * * @param {String} type * The type of unsupported object, e.g. method or option. * @param {String} name * The name of the unsupported object. * @param {*} [value] * The value of the unsupported object. */ Bootstrap.unsupported = function (type, name, value) { Bootstrap.warn('Unsupported by Drupal Bootstrap: (@type) @name -> @value', { '@type': type, '@name': name, '@value': typeof value === 'object' ? JSON.stringify(value) : value }); }; /** * Provide a helper method to display a warning. * * @param {String} message * The message to display. * @param {Object} [args] * Arguments to use as replacements in Drupal.formatString. */ Bootstrap.warn = function (message, args) { if (this.settings.dev && console.warn) { console.warn(Drupal.formatString(message, args)); } }; /** * Wraps a plugin with common functionality. * * @param {Function} constructor * A plugin constructor being wrapped. * @param {Object|Function} plugin * The plugin being wrapped. * @param {Boolean} [extend = false] * Whether to add super extensibility. */ Bootstrap.wrapPluginConstructor = function (constructor, plugin, extend) { var proto = constructor.prototype; // Add a jQuery UI like option getter/setter method. var option = this.option; if (proto.option === void(0)) { proto.option = function () { return option.apply(this, arguments); }; } if (extend) { // Handle prototype properties separately. if (plugin.prototype !== void 0) { for (var key in plugin.prototype) { if (!plugin.prototype.hasOwnProperty(key)) continue; var value = plugin.prototype[key]; if (typeof value === 'function') { proto[key] = this.superWrapper(proto[key] || function () {}, value); } else { proto[key] = $.isPlainObject(value) ? $.extend(true, {}, proto[key], value) : value; } } } delete plugin.prototype; // Handle static properties. for (key in plugin) { if (!plugin.hasOwnProperty(key)) continue; value = plugin[key]; if (typeof value === 'function') { constructor[key] = this.superWrapper(constructor[key] || function () {}, value); } else { constructor[key] = $.isPlainObject(value) ? $.extend(true, {}, constructor[key], value) : value; } } } }; /** * Add Bootstrap to the global Drupal object. * * @type {Bootstrap} */ Drupal.bootstrap = Drupal.bootstrap || Bootstrap; })(window._, window.jQuery, window.Drupal, window.drupalSettings);