first commit

This commit is contained in:
2024-07-15 12:33:27 +02:00
commit ce50ae282b
22084 changed files with 2623791 additions and 0 deletions

View File

@@ -0,0 +1,380 @@
(function ($, _) {
/**
* @class Attributes
*
* Modifies attributes.
*
* @param {Object|Attributes} attributes
* An object to initialize attributes with.
*/
var Attributes = function (attributes) {
this.data = {};
this.data['class'] = [];
this.merge(attributes);
};
/**
* Renders the attributes object as a string to inject into an HTML element.
*
* @return {String}
* A rendered string suitable for inclusion in HTML markup.
*/
Attributes.prototype.toString = function () {
var output = '';
var name, value;
var checkPlain = function (str) {
return str && str.toString().replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;') || '';
};
var data = this.getData();
for (name in data) {
if (!data.hasOwnProperty(name)) continue;
value = data[name];
if (_.isFunction(value)) value = value();
if (_.isObject(value)) value = _.values(value);
if (_.isArray(value)) value = value.join(' ');
output += ' ' + checkPlain(name) + '="' + checkPlain(value) + '"';
}
return output;
};
/**
* Renders the Attributes object as a plain object.
*
* @return {Object}
* A plain object suitable for inclusion in DOM elements.
*/
Attributes.prototype.toPlainObject = function () {
var object = {};
var name, value;
var data = this.getData();
for (name in data) {
if (!data.hasOwnProperty(name)) continue;
value = data[name];
if (_.isFunction(value)) value = value();
if (_.isObject(value)) value = _.values(value);
if (_.isArray(value)) value = value.join(' ');
object[name] = value;
}
return object;
};
/**
* Add class(es) to the array.
*
* @param {string|Array} value
* An individual class or an array of classes to add.
*
* @return {Attributes}
*
* @chainable
*/
Attributes.prototype.addClass = function (value) {
var args = Array.prototype.slice.call(arguments);
this.data['class'] = this.sanitizeClasses(this.data['class'].concat(args));
return this;
};
/**
* Returns whether the requested attribute exists.
*
* @param {string} name
* An attribute name to check.
*
* @return {boolean}
* TRUE or FALSE
*/
Attributes.prototype.exists = function (name) {
return this.data[name] !== void(0) && this.data[name] !== null;
};
/**
* Retrieve a specific attribute from the array.
*
* @param {string} name
* The specific attribute to retrieve.
* @param {*} defaultValue
* (optional) The default value to set if the attribute does not exist.
*
* @return {*}
* A specific attribute value, passed by reference.
*/
Attributes.prototype.get = function (name, defaultValue) {
if (!this.exists(name)) this.data[name] = defaultValue;
return this.data[name];
};
/**
* Retrieves a cloned copy of the internal attributes data object.
*
* @return {Object}
*/
Attributes.prototype.getData = function () {
return _.extend({}, this.data);
};
/**
* Retrieves classes from the array.
*
* @return {Array}
* The classes array.
*/
Attributes.prototype.getClasses = function () {
return this.get('class', []);
};
/**
* Indicates whether a class is present in the array.
*
* @param {string|Array} className
* The class(es) to search for.
*
* @return {boolean}
* TRUE or FALSE
*/
Attributes.prototype.hasClass = function (className) {
className = this.sanitizeClasses(Array.prototype.slice.call(arguments));
var classes = this.getClasses();
for (var i = 0, l = className.length; i < l; i++) {
// If one of the classes fails, immediately return false.
if (_.indexOf(classes, className[i]) === -1) {
return false;
}
}
return true;
};
/**
* Merges multiple values into the array.
*
* @param {Attributes|Node|jQuery|Object} object
* An Attributes object with existing data, a Node DOM element, a jQuery
* instance or a plain object where the key is the attribute name and the
* value is the attribute value.
* @param {boolean} [recursive]
* Flag determining whether or not to recursively merge key/value pairs.
*
* @return {Attributes}
*
* @chainable
*/
Attributes.prototype.merge = function (object, recursive) {
// Immediately return if there is nothing to merge.
if (!object) {
return this;
}
// Get attributes from a jQuery element.
if (object instanceof $) {
object = object[0];
}
// Get attributes from a DOM element.
if (object instanceof Node) {
object = Array.prototype.slice.call(object.attributes).reduce(function (attributes, attribute) {
attributes[attribute.name] = attribute.value;
return attributes;
}, {});
}
// Get attributes from an Attributes instance.
else if (object instanceof Attributes) {
object = object.getData();
}
// Otherwise, clone the object.
else {
object = _.extend({}, object);
}
// By this point, there should be a valid plain object.
if (!$.isPlainObject(object)) {
setTimeout(function () {
throw new Error('Passed object is not supported: ' + object);
});
return this;
}
// Handle classes separately.
if (object && object['class'] !== void 0) {
this.addClass(object['class']);
delete object['class'];
}
if (recursive === void 0 || recursive) {
this.data = $.extend(true, {}, this.data, object);
}
else {
this.data = $.extend({}, this.data, object);
}
return this;
};
/**
* Removes an attribute from the array.
*
* @param {string} name
* The name of the attribute to remove.
*
* @return {Attributes}
*
* @chainable
*/
Attributes.prototype.remove = function (name) {
if (this.exists(name)) delete this.data[name];
return this;
};
/**
* Removes a class from the attributes array.
*
* @param {...string|Array} className
* An individual class or an array of classes to remove.
*
* @return {Attributes}
*
* @chainable
*/
Attributes.prototype.removeClass = function (className) {
var remove = this.sanitizeClasses(Array.prototype.slice.apply(arguments));
this.data['class'] = _.without(this.getClasses(), remove);
return this;
};
/**
* Replaces a class in the attributes array.
*
* @param {string} oldValue
* The old class to remove.
* @param {string} newValue
* The new class. It will not be added if the old class does not exist.
*
* @return {Attributes}
*
* @chainable
*/
Attributes.prototype.replaceClass = function (oldValue, newValue) {
var classes = this.getClasses();
var i = _.indexOf(this.sanitizeClasses(oldValue), classes);
if (i >= 0) {
classes[i] = newValue;
this.set('class', classes);
}
return this;
};
/**
* Ensures classes are flattened into a single is an array and sanitized.
*
* @param {...String|Array} classes
* The class or classes to sanitize.
*
* @return {Array}
* A sanitized array of classes.
*/
Attributes.prototype.sanitizeClasses = function (classes) {
return _.chain(Array.prototype.slice.call(arguments))
// Flatten in case there's a mix of strings and arrays.
.flatten()
// Split classes that may have been added with a space as a separator.
.map(function (string) {
return string.split(' ');
})
// Flatten again since it was just split into arrays.
.flatten()
// Filter out empty items.
.filter()
// Clean the class to ensure it's a valid class name.
.map(function (value) {
return Attributes.cleanClass(value);
})
// Ensure classes are unique.
.uniq()
// Retrieve the final value.
.value();
};
/**
* Sets an attribute on the array.
*
* @param {string} name
* The name of the attribute to set.
* @param {*} value
* The value of the attribute to set.
*
* @return {Attributes}
*
* @chainable
*/
Attributes.prototype.set = function (name, value) {
var obj = $.isPlainObject(name) ? name : {};
if (typeof name === 'string') {
obj[name] = value;
}
return this.merge(obj);
};
/**
* Prepares a string for use as a CSS identifier (element, class, or ID name).
*
* Note: this is essentially a direct copy from
* \Drupal\Component\Utility\Html::cleanCssIdentifier
*
* @param {string} identifier
* The identifier to clean.
* @param {Object} [filter]
* An object of string replacements to use on the identifier.
*
* @return {string}
* The cleaned identifier.
*/
Attributes.cleanClass = function (identifier, filter) {
filter = filter || {
' ': '-',
'_': '-',
'/': '-',
'[': '-',
']': ''
};
identifier = identifier.toLowerCase();
if (filter['__'] === void 0) {
identifier = identifier.replace('__', '#DOUBLE_UNDERSCORE#');
}
identifier = identifier.replace(Object.keys(filter), Object.keys(filter).map(function(key) { return filter[key]; }));
if (filter['__'] === void 0) {
identifier = identifier.replace('#DOUBLE_UNDERSCORE#', '__');
}
identifier = identifier.replace(/[^\u002D\u0030-\u0039\u0041-\u005A\u005F\u0061-\u007A\u00A1-\uFFFF]/g, '');
identifier = identifier.replace(['/^[0-9]/', '/^(-[0-9])|^(--)/'], ['_', '__']);
return identifier;
};
/**
* Creates an Attributes instance.
*
* @param {object|Attributes} [attributes]
* An object to initialize attributes with.
*
* @return {Attributes}
* An Attributes instance.
*
* @constructor
*/
Attributes.create = function (attributes) {
return new Attributes(attributes);
};
window.Attributes = Attributes;
})(window.jQuery, window._);

View File

@@ -0,0 +1,172 @@
/**
* @file
* dialog.js
*/
(function ($, Drupal, Bootstrap, Attributes) {
Bootstrap.Dialog = Bootstrap.Dialog || {};
/**
* A collection of Drupal dialog handlers.
*
* @type {Object<String, Drupal.bootstrap.Dialog.Handler>}
*/
Bootstrap.Dialog.handlers = {};
/**
* @class Drupal.bootstrap.Dialog.Handler
*
* @param type
* @param data
*/
Bootstrap.Dialog.Handler = function (type, data) {
this.ctor = $.fn.modal;
this.extend = null;
this.plugin = 'modal';
this.prefix = 'modal';
this.themeHooks = {
modal: 'bootstrapModal',
dialog: 'bootstrapModalDialog',
header: 'bootstrapModalHeader',
title: 'bootstrapModalTitle',
close: 'bootstrapModalClose',
content: 'bootstrapModalContent',
body: 'bootstrapModalBody',
footer: 'bootstrapModalFooter',
};
this.type = type;
this.selectors = {
dialog: '.modal-dialog',
header: '.modal-header',
title: '.modal-title',
close: '.close',
content: '.modal-content',
body: '.modal-body',
footer: '.modal-footer',
buttons: '.modal-buttons'
};
// Extend the object with subclassed data.
$.extend(this, data);
// Extend the jQuery plugin.
if (this.extend) {
Bootstrap.extend(this.plugin, this.extend);
}
};
/**
* Retrieves a Drupal dialog type handler.
*
* @param {String|HTMLElement|jQuery} type
* The dialog type to retrieve.
*
* @return {Drupal.bootstrap.Dialog.Handler}
* A Bootstrap.Dialog.Handler instance.
*/
Bootstrap.Dialog.Handler.get = function (type) {
if (type instanceof $) {
type = type[0];
}
if (type instanceof HTMLElement) {
type = type.dialogType;
}
if (!type) {
type = 'modal';
}
if (!Bootstrap.Dialog.handlers[type]) {
Bootstrap.Dialog.handlers[type] = new Bootstrap.Dialog.Handler();
}
return Bootstrap.Dialog.handlers[type];
};
/**
* Registers a Drupal dialog type handler.
*
* @param {String} type
* The dialog type to
* @param {Object} [data]
* Optional. Additional data to use to create the dialog handler. By
* default, this assumes values relative to the Bootstrap Modal plugin.
*/
Bootstrap.Dialog.Handler.register = function (type, data) {
Bootstrap.Dialog.handlers[type] = new Bootstrap.Dialog.Handler(type, data);
};
Bootstrap.Dialog.Handler.prototype.invoke = function (context) {
var args = Array.prototype.slice.call(arguments);
return this.ctor.apply(context, args.slice(1));
};
Bootstrap.Dialog.Handler.prototype.theme = function (hook) {
var args = Array.prototype.slice.call(arguments);
return $(Drupal.theme.apply(Drupal.theme, [this.themeHooks[hook]].concat(args.slice(1))));
};
/**
* Ensures a DOM element has the appropriate structure for a modal.
*
* Note: this can get a little tricky. Core potentially already
* semi-processes a "dialog" if was created using an Ajax command
* (i.e. prepareDialogButtons in drupal.ajax.js). Because of this, the
* contents (HTML) of the existing element cannot simply be dumped into a
* newly created modal. This would destroy any existing event bindings.
* Instead, the contents must be "moved" (appended) to the new modal and
* then "moved" again back to the to the existing container as needed.
*
* @param {HTMLElement|jQuery} element
* The element to ensure is a modal structure.
* @param {Object} options
* THe dialog options to use to construct the modal.
*/
Bootstrap.Dialog.Handler.prototype.ensureModalStructure = function (element, options) {
var $element = $(element);
// Immediately return if the modal was already converted into a proper modal.
if ($element.is('[data-drupal-theme="' + this.themeHooks.modal + '"]')) {
return;
}
var attributes = Attributes.create(element).remove('style').set('data-drupal-theme', this.themeHooks.modal);
// Merge in trigger data attributes.
if (options.$trigger && options.$trigger[0]) {
/** @var {HTMLElement} trigger */
var trigger = options.$trigger[0];
var data = {};
for (var i = 0, l = trigger.attributes.length; i < l; i++) {
var name = trigger.attributes[i].name;
if (name && name.substring(0, 5) === 'data-') {
data[name] = trigger.getAttribute(name);
}
}
attributes.merge(data);
}
options = $.extend(true, {}, options, {
attributes: attributes,
});
// Create a new modal.
var $modal = this.theme('modal', options);
// Store a reference to the content inside the existing element container.
// This references the actual DOM node elements which will allow
// jQuery to "move" then when appending below. Using $.fn.children()
// does not return any text nodes present and $.fn.html() only returns
// a string representation of the content, which effectively destroys
// any prior event bindings or processing.
var $body = $element.find(this.selectors.body);
var $existing = $body[0] ? $body.contents() : $element.contents();
// Set the attributes of the dialog to that of the newly created modal.
$element.attr(Attributes.create($modal).toPlainObject());
// Append the newly created modal markup.
$element.append($modal.html());
// Move the existing HTML into the modal markup that was just appended.
$element.find(this.selectors.body).append($existing);
};
})(jQuery, Drupal, Drupal.bootstrap, Attributes);

View File

@@ -0,0 +1,76 @@
/**
* @file
* Provides an event handler for hidden elements in dropdown menus.
*/
(function ($, Drupal, Bootstrap) {
'use strict';
/**
* The list of supported events to proxy.
*
* @type {Array}
*/
var events = [
// MouseEvent.
'click', 'dblclick', 'mousedown', 'mouseenter', 'mouseleave', 'mouseup', 'mouseover', 'mousemove', 'mouseout',
// KeyboardEvent.
'keypress', 'keydown', 'keyup'
];
/**
* Bootstrap dropdown behaviors.
*
* Proxy any dropdown element events that should actually be fired on the
* original target (e.g. button, submits, etc.). This allows any registered
* event callbacks to be fired as they were intended (despite the fact that
* the markup has been changed to work with Bootstrap).
*
* @see \Drupal\bootstrap\Plugin\Preprocess\BootstrapDropdown::preprocessLinks
*
* @type {Drupal~behavior#bootstrapDropdown}
*/
Drupal.behaviors.bootstrapDropdown = {
attach: function (context) {
var elements = context.querySelectorAll('.dropdown [data-dropdown-target]');
for (var k in elements) {
if (!elements.hasOwnProperty(k)) {
continue;
}
var element = elements[k];
for (var i = 0, l = events.length; i < l; i++) {
var event = events[i];
element.removeEventListener(event, this.proxyEvent);
element.addEventListener(event, this.proxyEvent);
}
}
},
/**
* Proxy event handler for bootstrap dropdowns.
*
* @param {Event} e
* The event object.
*/
proxyEvent: function (e) {
// Ignore tabbing.
if (e.type.match(/^key/) && (e.which === 9 || e.keyCode === 9)) {
return;
}
var target = e.currentTarget.dataset && e.currentTarget.dataset.dropdownTarget || e.currentTarget.getAttribute('data-dropdown-target');
if (target) {
e.preventDefault();
e.stopPropagation();
var element = target && target !== '#' && document.querySelectorAll(target)[0];
if (element) {
Bootstrap.simulate(element, e.type, e);
}
else if (Bootstrap.settings.dev && window.console && !e.type.match(/^mouse/)) {
window.console.debug('[Drupal Bootstrap] Could not find a the target:', target);
}
}
}
}
})(jQuery, Drupal, Drupal.bootstrap);

View File

@@ -0,0 +1,600 @@
/**
* @file
* Drupal Bootstrap object.
*/
/**
* All Drupal Bootstrap JavaScript APIs are contained in this namespace.
*
* @param {underscore} _
* @param {jQuery} $
* @param {Drupal} Drupal
* @param {drupalSettings} drupalSettings
*/
(function (_, $, Drupal, drupalSettings) {
'use strict';
/**
* @typedef Drupal.bootstrap
*/
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<Event|MouseEvent|KeyboardEvent|TouchEvent,RegExp>}
*/
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;
};
}
};
/**
* Creates a handler that relays to another event name.
*
* @param {HTMLElement|jQuery} target
* A target element.
* @param {String} name
* The name of the event to trigger.
* @param {Boolean} [stopPropagation=true]
* Flag indicating whether to stop the propagation of the event, defaults
* to true.
*
* @return {Function}
* An even handler callback function.
*/
Bootstrap.relayEvent = function (target, name, stopPropagation) {
return function (e) {
if (stopPropagation === void 0 || stopPropagation) {
e.stopPropagation();
}
var $target = $(target);
var parts = name.split('.').filter(Boolean);
var type = parts.shift();
e.target = $target[0];
e.currentTarget = $target[0];
e.namespace = parts.join('.');
e.type = type;
$target.trigger(e);
};
};
/**
* 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.
Drupal.bootstrap = Drupal.bootstrap || Bootstrap;
})(window._, window.jQuery, window.Drupal, window.drupalSettings);

View File

@@ -0,0 +1,131 @@
/**
* @file
* Extends methods from core/misc/ajax.js.
*/
(function ($, window, Drupal, drupalSettings) {
/**
* Attempts to find the closest glyphicon progress indicator.
*
* @param {jQuery|Element} element
* A DOM element.
*
* @returns {jQuery}
* A jQuery object.
*/
Drupal.Ajax.prototype.findGlyphicon = function (element) {
return $(element).closest('.form-item').find('.ajax-progress.glyphicon')
};
/**
* Starts the spinning of the glyphicon progress indicator.
*
* @param {jQuery|Element} element
* A DOM element.
* @param {string} [message]
* An optional message to display (tooltip) for the progress.
*
* @returns {jQuery}
* A jQuery object.
*/
Drupal.Ajax.prototype.glyphiconStart = function (element, message) {
var $glyphicon = this.findGlyphicon(element);
if ($glyphicon[0]) {
$glyphicon.addClass('glyphicon-spin');
// Add any message as a tooltip to the glyphicon.
if ($.fn.tooltip && drupalSettings.bootstrap.tooltip_enabled) {
$glyphicon
.removeAttr('data-toggle')
.removeAttr('data-original-title')
.removeAttr('title')
.tooltip('destroy')
;
if (message) {
$glyphicon.attr('data-toggle', 'tooltip').attr('title', message).tooltip();
}
}
// Append a message for screen readers.
if (message) {
$glyphicon.parent().append('<div class="sr-only message">' + message + '</div>');
}
}
return $glyphicon;
};
/**
* Stop the spinning of a glyphicon progress indicator.
*
* @param {jQuery|Element} element
* A DOM element.
*/
Drupal.Ajax.prototype.glyphiconStop = function (element) {
var $glyphicon = this.findGlyphicon(element);
if ($glyphicon[0]) {
$glyphicon.removeClass('glyphicon-spin');
if ($.fn.tooltip && drupalSettings.bootstrap.tooltip_enabled) {
$glyphicon
.removeAttr('data-toggle')
.removeAttr('data-original-title')
.removeAttr('title')
.tooltip('destroy')
;
}
}
};
/**
* Sets the throbber progress indicator.
*/
Drupal.Ajax.prototype.setProgressIndicatorThrobber = function () {
var $element = $(this.element);
// Find an existing glyphicon progress indicator.
var $glyphicon = this.glyphiconStart($element, this.progress.message);
if ($glyphicon[0]) {
this.progress.element = $glyphicon.parent();
this.progress.glyphicon = true;
return;
}
// Otherwise, add a glyphicon throbber after the element.
if (!this.progress.element) {
this.progress.element = $(Drupal.theme('ajaxThrobber'));
}
if (this.progress.message) {
this.progress.element.after('<div class="message">' + this.progress.message + '</div>');
}
// If element is an input DOM element type (not :input), append after.
if ($element.is('input') || $element.is('select')) {
$element.after(this.progress.element);
}
// Otherwise append the throbber inside the element.
else {
$element.append(this.progress.element);
}
};
/**
* Handler for the form redirection completion.
*
* @param {Array.<Drupal.AjaxCommands~commandDefinition>} response
* @param {number} status
*/
var success = Drupal.Ajax.prototype.success;
Drupal.Ajax.prototype.success = function (response, status) {
if (this.progress.element) {
// Remove any message set.
this.progress.element.parent().find('.message').remove();
}
// Invoke the original success handler.
return success.apply(this, [response, status]);
};
})(jQuery, this, Drupal, drupalSettings);

View File

@@ -0,0 +1,36 @@
/**
* @file
* Extends autocomplete based on jQuery UI.
*
* @todo Remove once jQuery UI is no longer used?
*/
(function ($, Drupal) {
'use strict';
// Ensure the input element has a "change" event triggered. This is important
// so that summaries in vertical tabs can be updated properly.
// @see Drupal.behaviors.formUpdated
$(document).on('autocompleteselect', '.form-autocomplete', function (e) {
$(e.target).trigger('change.formUpdated');
});
// Extend ui.autocomplete widget so it triggers the glyphicon throbber.
$.widget('ui.autocomplete', $.ui.autocomplete, {
_search: function (value) {
this.pending++;
Drupal.Ajax.prototype.glyphiconStart(this.element);
this.cancelSearch = false;
this.source({term: value}, this._response());
},
_response: function () {
var index = ++this.requestIndex;
return $.proxy(function (content) {
if (index === this.requestIndex) this.__response(content);
this.pending--;
if (!this.pending) Drupal.Ajax.prototype.glyphiconStop(this.element);
}, this);
}
});
})(jQuery, Drupal);

View File

@@ -0,0 +1,43 @@
/**
* @file
* Drupal's batch API.
*/
(function ($, Drupal, once) {
'use strict';
/**
* Attaches the batch behavior to progress bars.
*
* @type {Drupal~behavior}
*/
Drupal.behaviors.batch = {
attach: function (context, settings) {
var batch = settings.batch;
var $progress = $(once('batch', '[data-drupal-progress]', context));
var progressBar;
// Success: redirect to the summary.
function updateCallback(progress, status, pb) {
if (progress === '100') {
pb.stopMonitoring();
window.location = batch.uri + '&op=finished';
}
}
function errorCallback(pb) {
$progress.prepend($('<p class="error"></p>').html(batch.errorMessage));
$('#wait').hide();
}
if ($progress.length) {
var id = $progress.find('.progress').attr('id') || 'updateprogress';
progressBar = new Drupal.ProgressBar(id, updateCallback, 'POST', errorCallback);
$progress.replaceWith(progressBar.element);
progressBar.setProgress(-1, batch.initMessage);
progressBar.startMonitoring(batch.uri + '&op=do', 10);
}
}
};
})(jQuery, Drupal, once);

View File

@@ -0,0 +1,115 @@
/**
* @file
* dialog.ajax.js
*/
(function ($, Drupal, Bootstrap) {
Drupal.behaviors.dialog.ajaxCurrentButton = null;
Drupal.behaviors.dialog.ajaxOriginalButton = null;
// Intercept the success event to add the dialog type to commands.
var success = Drupal.Ajax.prototype.success;
Drupal.Ajax.prototype.success = function (response, status) {
if (this.dialogType) {
for (var i = 0, l = response.length; i < l; i++) {
if (response[i].dialogOptions) {
response[i].dialogType = response[i].dialogOptions.dialogType = this.dialogType;
response[i].$trigger = response[i].dialogOptions.$trigger = $(this.element);
}
}
}
return success.apply(this, [response, status]);
};
var beforeSerialize = Drupal.Ajax.prototype.beforeSerialize;
Drupal.Ajax.prototype.beforeSerialize = function (element, options) {
// Add the dialog type currently in use.
if (this.dialogType) {
options.data['ajax_page_state[dialogType]'] = this.dialogType;
// Add the dialog element ID if it can be found (useful for closing it).
var id = $(this.element).parents('.js-drupal-dialog:first').attr('id');
if (id) {
options.data['ajax_page_state[dialogId]'] = id;
}
}
return beforeSerialize.apply(this, arguments);
};
/**
* Synchronizes a faux button with its original counterpart.
*
* @param {Boolean} [reset = false]
* Whether to reset the current and original buttons after synchronizing.
*/
Drupal.behaviors.dialog.ajaxUpdateButtons = function (reset) {
if (this.ajaxCurrentButton && this.ajaxOriginalButton) {
this.ajaxCurrentButton.html(this.ajaxOriginalButton.html() || this.ajaxOriginalButton.attr('value'));
this.ajaxCurrentButton.prop('disabled', this.ajaxOriginalButton.prop('disabled'));
}
if (reset) {
this.ajaxCurrentButton = null;
this.ajaxOriginalButton = null;
}
};
$(document)
.ajaxSend(function () {
Drupal.behaviors.dialog.ajaxUpdateButtons();
})
.ajaxComplete(function () {
Drupal.behaviors.dialog.ajaxUpdateButtons(true);
})
;
/**
* {@inheritdoc}
*/
Drupal.behaviors.dialog.prepareDialogButtons = function prepareDialogButtons($dialog) {
var _this = this;
var buttons = [];
var $buttons = $dialog.find('.form-actions').find('button, input[type=submit], a.button, .btn');
$buttons.each(function () {
var $originalButton = $(this)
// Prevent original button from being tabbed to.
.attr('tabindex', -1)
// Visually make the original button invisible, but don't actually hide
// or remove it from the DOM because the click needs to be proxied from
// the faux button created in the footer to its original counterpart.
.css({
display: 'block',
width: 0,
height: 0,
padding: 0,
border: 0,
overflow: 'hidden'
});
buttons.push({
// Strip all HTML from the actual text value. This value is escaped.
// It actual HTML value will be synced with the original button's HTML
// below in the "create" method.
text: Bootstrap.stripHtml($originalButton) || $originalButton.attr('value'),
class: $originalButton.attr('class').replace('use-ajax-submit', ''),
click: function click(e) {
e.preventDefault();
e.stopPropagation();
_this.ajaxCurrentButton = $(e.target);
_this.ajaxOriginalButton = $originalButton;
// Some core JS binds dialog buttons to the mousedown or mouseup
// events instead of click; all three events must be simulated here.
// @see https://www.drupal.org/project/bootstrap/issues/3016254
Bootstrap.simulate($originalButton, ['mousedown', 'mouseup', 'click']);
},
create: function () {
_this.ajaxCurrentButton = $(this);
_this.ajaxOriginalButton = $originalButton;
_this.ajaxUpdateButtons(true);
}
});
});
return buttons;
};
})(window.jQuery, window.Drupal, window.Drupal.bootstrap);

View File

@@ -0,0 +1,30 @@
/**
* @file
* Extends methods from core/misc/form.js.
*/
(function ($, window, Drupal, drupalSettings, once) {
/**
* Behavior for "forms_has_error_value_toggle" theme setting.
*/
Drupal.behaviors.bootstrapForm = {
attach: function (context) {
if (drupalSettings.bootstrap && drupalSettings.bootstrap.forms_has_error_value_toggle) {
var $context = $(context);
$(once('error', '.form-item.has-error:not(.form-type-password.has-feedback)', context)).each(function () {
var $formItem = $(this);
var $input = $formItem.find(':input');
$input.on('keyup focus blur', function () {
if (this.defaultValue !== void 0) {
$formItem[this.defaultValue !== this.value ? 'removeClass' : 'addClass']('has-error');
$input[this.defaultValue !== this.value ? 'removeClass' : 'addClass']('error');
}
});
});
}
}
};
})(jQuery, this, Drupal, drupalSettings, once);

View File

@@ -0,0 +1,181 @@
/**
* @file
* message.js
*/
(function ($, Drupal) {
/**
* Retrieves the classes for a specific message type.
*
* @param {String} type
* The type of message.
*
* @return {String}
* The classes to add, space separated.
*/
Drupal.Message.getMessageTypeClass = function (type) {
var classes = this.getMessageTypeClasses();
return 'alert alert-' + (classes[type] || 'success');
};
/**
* Helper function to map Drupal types to Bootstrap classes.
*
* @return {Object<String, String>}
* A map of classes, keyed by message type.
*/
Drupal.Message.getMessageTypeClasses = function () {
return {
status: 'success',
error: 'danger',
warning: 'warning',
info: 'info',
};
};
/**
* Retrieves a label for a specific message type.
*
* @param {String} type
* The type of message.
*
* @return {String}
* The message type label.
*/
Drupal.Message.getMessageTypeLabel = function (type) {
var labels = this.getMessageTypeLabels();
return labels[type];
};
/**
* @inheritDoc
*/
Drupal.Message.getMessageTypeLabels = function () {
return {
status: Drupal.t('Status message'),
error: Drupal.t('Error message'),
warning: Drupal.t('Warning message'),
info: Drupal.t('Informative message'),
};
};
/**
* Retrieves the aria-role for a specific message type.
*
* @param {String} type
* The type of message.
*
* @return {String}
* The message type role.
*/
Drupal.Message.getMessageTypeRole = function (type) {
var labels = this.getMessageTypeRoles();
return labels[type];
};
/**
* Map of the message type aria-role values.
*
* @return {Object<String, String>}
* A map of roles, keyed by message type.
*/
Drupal.Message.getMessageTypeRoles = function () {
return {
status: 'status',
error: 'alert',
warning: 'alert',
info: 'status',
};
};
/**
* @inheritDoc
*/
Drupal.theme.message = function (message, options) {
options = options || {};
var wrapper = Drupal.theme('messageWrapper', options.id || (new Date()).getTime(), options.type || 'status');
if (options.dismissible === void 0 || !!options.dismissible) {
wrapper.classList.add('alert-dismissible');
wrapper.appendChild(Drupal.theme('messageClose'));
}
wrapper.appendChild(Drupal.theme('messageContents', message && message.text));
return wrapper;
};
/**
* Themes the message container.
*
* @param {String} id
* The message identifier.
* @param {String} type
* The type of message.
*
* @return {HTMLElement}
* A constructed HTMLElement.
*/
Drupal.theme.messageWrapper = function (id, type) {
var wrapper = document.createElement('div');
var label = Drupal.Message.getMessageTypeLabel(type);
wrapper.setAttribute('class', Drupal.Message.getMessageTypeClass(type));
wrapper.setAttribute('role', Drupal.Message.getMessageTypeRole(type));
wrapper.setAttribute('aria-label', label);
wrapper.setAttribute('data-drupal-message-id', id);
wrapper.setAttribute('data-drupal-message-type', type);
if (label) {
wrapper.appendChild(Drupal.theme('messageLabel', label));
}
return wrapper;
};
/**
* Themes the message close button.
*
* @return {HTMLElement}
* A constructed HTMLElement.
*/
Drupal.theme.messageClose = function () {
var element = document.createElement('button');
element.setAttribute('class', 'close');
element.setAttribute('type', 'button');
element.setAttribute('role', 'button');
element.setAttribute('data-dismiss', 'alert');
element.setAttribute('aria-label', Drupal.t('Close'));
element.innerHTML = '<span aria-hidden="true">&times;</span>';
return element;
};
/**
* Themes the message container.
*
* @param {String} label
* The message label.
*
* @return {HTMLElement}
* A constructed HTMLElement.
*/
Drupal.theme.messageLabel = function (label) {
var element = document.createElement('h2');
element.setAttribute('class', 'sr-only');
element.innerHTML = label;
return element;
};
/**
* Themes the message contents.
*
* @param {String} html
* The message identifier.
*
* @return {HTMLElement}
* A constructed HTMLElement.
*/
Drupal.theme.messageContents = function (html) {
var element = document.createElement('p');
element.innerHTML = '' + html;
return element;
}
})(window.jQuery, window.Drupal);

View File

@@ -0,0 +1,73 @@
/**
* @file
* Extends methods from core/misc/progress.js.
*/
(function ($, Drupal) {
'use strict';
/**
* Theme function for the progress bar.
*
* @param {string} id
*
* @return {string}
* The HTML for the progress bar.
*/
Drupal.theme.progressBar = function (id) {
return '<div class="progress-wrapper" aria-live="polite">' +
'<div class="message"></div>'+
'<div id ="' + id + '" class="progress progress-striped active">' +
'<div class="progress-bar" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0">' +
'<span class="percentage"></span>' +
'</div>' +
'</div>' +
'<div class="progress-label"></div>' +
'</div>';
};
$.extend(Drupal.ProgressBar.prototype, /** @lends Drupal.ProgressBar */{
/**
* Set the percentage and status message for the progressbar.
*
* @param {number} percentage
* @param {string} message
* @param {string} label
*/
setProgress: function (percentage, message, label) {
if (percentage >= 0 && percentage <= 100) {
$(this.element).find('.progress-bar').css('width', percentage + '%').attr('aria-valuenow', percentage);
$(this.element).find('.percentage').html(percentage + '%');
}
if (message) {
// Remove the unnecessary whitespace at the end of the message.
message = message.replace(/<br\/>&nbsp;|\s*$/, '');
$('.message', this.element).html(message);
}
if (label) {
$('.progress-label', this.element).html(label);
}
if (this.updateCallback) {
this.updateCallback(percentage, message, this);
}
},
/**
* Display errors on the page.
*
* @param {string} string
*/
displayError: function (string) {
var error = $('<div class="alert alert-block alert-error"><button class="close" data-dismiss="alert">&times;</button><h4>' + Drupal.t('Error message') + '</h4></div>').append(string);
$(this.element).before(error).hide();
if (this.errorCallback) {
this.errorCallback(this);
}
}
});
})(jQuery, Drupal);

View File

@@ -0,0 +1,30 @@
/**
* @file
* Extends core/misc/states.js.
*/
(function($) {
// Unbind core state.js from document first so we can then override below.
$(document).unbind('state:disabled');
/**
* Global state change handlers. These are bound to "document" to cover all
* elements whose state changes. Events sent to elements within the page
* bubble up to these handlers. We use this system so that themes and modules
* can override these state change handlers for particular parts of a page.
*/
$(document).bind('state:disabled', function(e) {
// Only act when this change was triggered by a dependency and not by the
// element monitoring itself.
if (e.trigger) {
$(e.target)
.attr('disabled', e.value)
.closest('.form-item, .form-submit, .form-wrapper').toggleClass('form-disabled', e.value)
.find(':input').attr('disabled', e.value);
// Note: WebKit nightlies don't reflect that change correctly.
// See https://bugs.webkit.org/show_bug.cgi?id=23789
}
});
})(jQuery);

View File

@@ -0,0 +1,463 @@
/**
* @file
* Extends methods from core/misc/tabledrag.js.
*/
(function ($) {
// Save the original prototype.
var prototype = Drupal.tableDrag.prototype;
/**
* Provides table and field manipulation.
*
* @constructor
*
* @param {HTMLElement} table
* DOM object for the table to be made draggable.
* @param {object} tableSettings
* Settings for the table added via drupal_add_dragtable().
*/
Drupal.tableDrag = function (table, tableSettings) {
var self = this;
var $table = $(table);
/**
* @type {jQuery}
*/
this.$table = $(table);
/**
*
* @type {HTMLElement}
*/
this.table = table;
/**
* @type {object}
*/
this.tableSettings = tableSettings;
/**
* Used to hold information about a current drag operation.
*
* @type {?HTMLElement}
*/
this.dragObject = null;
/**
* Provides operations for row manipulation.
*
* @type {?HTMLElement}
*/
this.rowObject = null;
/**
* Remember the previous element.
*
* @type {?HTMLElement}
*/
this.oldRowElement = null;
/**
* Used to determine up or down direction from last mouse move.
*
* @type {number}
*/
this.oldY = 0;
/**
* Whether anything in the entire table has changed.
*
* @type {bool}
*/
this.changed = false;
/**
* Maximum amount of allowed parenting.
*
* @type {number}
*/
this.maxDepth = 0;
/**
* Direction of the table.
*
* @type {number}
*/
this.rtl = $(this.table).css('direction') === 'rtl' ? -1 : 1;
/**
*
* @type {bool}
*/
this.striping = $(this.table).data('striping') === 1;
/**
* Configure the scroll settings.
*
* @type {object}
*
* @prop {number} amount
* @prop {number} interval
* @prop {number} trigger
*/
this.scrollSettings = {amount: 4, interval: 50, trigger: 70};
/**
*
* @type {?number}
*/
this.scrollInterval = null;
/**
*
* @type {number}
*/
this.scrollY = 0;
/**
*
* @type {number}
*/
this.windowHeight = 0;
/**
* @type {?HTMLElement}
*/
this.$toggleWeightButton = null;
/**
* Check this table's settings to see if there are parent relationships in
* this table. For efficiency, large sections of code can be skipped if we
* don't need to track horizontal movement and indentations.
*
* @type {bool}
*/
this.indentEnabled = false;
for (var group in tableSettings) {
if (tableSettings.hasOwnProperty(group)) {
for (var n in tableSettings[group]) {
if (tableSettings[group].hasOwnProperty(n)) {
if (tableSettings[group][n].relationship === 'parent') {
this.indentEnabled = true;
}
if (tableSettings[group][n].limit > 0) {
this.maxDepth = tableSettings[group][n].limit;
}
}
}
}
}
if (this.indentEnabled) {
/**
* Total width of indents, set in makeDraggable.
*
* @type {number}
*/
this.indentCount = 1;
// Find the width of indentations to measure mouse movements against.
// Because the table doesn't need to start with any indentations, we
// manually append 2 indentations in the first draggable row, measure
// the offset, then remove.
var indent = Drupal.theme('tableDragIndentation');
var testRow = $('<tr/>').addClass('draggable').appendTo(table);
var testCell = $('<td/>').appendTo(testRow).prepend(indent).prepend(indent);
var $indentation = testCell.find('.js-indentation');
/**
*
* @type {number}
*/
this.indentAmount = $indentation.get(1).offsetLeft - $indentation.get(0).offsetLeft;
testRow.remove();
}
// Make each applicable row draggable.
// Match immediate children of the parent element to allow nesting.
$table.find('> tr.draggable, > tbody > tr.draggable').each(function () { self.makeDraggable(this); });
// Add a link before the table for users to show or hide weight columns.
self.$toggleWeightButton = $(Drupal.theme('btn-sm', {
'class': ['tabledrag-toggle-weight'],
'data-drupal-selector': ['tabledrag-toggle-weight'],
title: Drupal.t('Re-order rows by numerical weight instead of dragging.'),
'data-toggle': 'tooltip'
}));
self.$toggleWeightButton
.on('click', $.proxy(function (e) {
e.preventDefault();
this.toggleColumns();
}, this))
.wrap('<div class="tabledrag-toggle-weight-wrapper"></div>')
.parent();
$table.before(self.$toggleWeightButton);
// Initialize the specified columns (for example, weight or parent columns)
// to show or hide according to user preference. This aids accessibility
// so that, e.g., screen reader users can choose to enter weight values and
// manipulate form elements directly, rather than using drag-and-drop..
self.initColumns();
// Add event bindings to the document. The self variable is passed along
// as event handlers do not have direct access to the tableDrag object.
$(document).on('touchmove', function (event) { return self.dragRow(event.originalEvent.touches[0], self); });
$(document).on('touchend', function (event) { return self.dropRow(event.originalEvent.touches[0], self); });
$(document).on('mousemove pointermove', function (event) { return self.dragRow(event, self); });
$(document).on('mouseup pointerup', function (event) { return self.dropRow(event, self); });
// React to localStorage event showing or hiding weight columns.
$(window).on('storage', $.proxy(function (e) {
// Only react to 'Drupal.tableDrag.showWeight' value change.
if (e.originalEvent.key === 'Drupal.tableDrag.showWeight') {
// This was changed in another window, get the new value for this
// window.
showWeight = JSON.parse(e.originalEvent.newValue);
this.displayColumns(showWeight);
}
}, this));
};
// Restore the original prototype.
Drupal.tableDrag.prototype = prototype;
/**
* Take an item and add event handlers to make it become draggable.
*
* @param {HTMLElement} item
*/
Drupal.tableDrag.prototype.makeDraggable = function (item) {
var self = this;
var $item = $(item);
// Add a class to the title link
$item.find('td:first-of-type').find('a').addClass('menu-item__link');
// Create the handle.
var handle = $('<a href="#" class="tabledrag-handle"/>');
// Insert the handle after indentations (if any).
var $indentationLast = $item.find('td:first-of-type').find('.js-indentation').eq(-1);
if ($indentationLast.length) {
$indentationLast.after(handle);
// Update the total width of indentation in this entire table.
self.indentCount = Math.max($item.find('.js-indentation').length, self.indentCount);
}
else {
$item.find('td').eq(0).prepend(handle);
}
// Add the glyphicon to the handle.
handle
.attr('title', Drupal.t('Drag to re-order'))
.attr('data-toggle', 'tooltip')
.append(Drupal.theme('bootstrapIcon', 'move'))
;
handle.on('mousedown touchstart pointerdown', function (event) {
event.preventDefault();
if (event.originalEvent.type === 'touchstart') {
event = event.originalEvent.touches[0];
}
self.dragStart(event, self, item);
});
// Prevent the anchor tag from jumping us to the top of the page.
handle.on('click', function (e) {
e.preventDefault();
});
// Set blur cleanup when a handle is focused.
handle.on('focus', function () {
self.safeBlur = true;
});
// On blur, fire the same function as a touchend/mouseup. This is used to
// update values after a row has been moved through the keyboard support.
handle.on('blur', function (event) {
if (self.rowObject && self.safeBlur) {
self.dropRow(event, self);
}
});
// Add arrow-key support to the handle.
handle.on('keydown', function (event) {
// If a rowObject doesn't yet exist and this isn't the tab key.
if (event.keyCode !== 9 && !self.rowObject) {
self.rowObject = new self.row(item, 'keyboard', self.indentEnabled, self.maxDepth, true);
}
var keyChange = false;
var groupHeight;
switch (event.keyCode) {
// Left arrow.
case 37:
// Safari left arrow.
case 63234:
keyChange = true;
self.rowObject.indent(-1 * self.rtl);
break;
// Up arrow.
case 38:
// Safari up arrow.
case 63232:
var $previousRow = $(self.rowObject.element).prev('tr:first-of-type');
var previousRow = $previousRow.get(0);
while (previousRow && $previousRow.is(':hidden')) {
$previousRow = $(previousRow).prev('tr:first-of-type');
previousRow = $previousRow.get(0);
}
if (previousRow) {
// Do not allow the onBlur cleanup.
self.safeBlur = false;
self.rowObject.direction = 'up';
keyChange = true;
if ($(item).is('.tabledrag-root')) {
// Swap with the previous top-level row.
groupHeight = 0;
while (previousRow && $previousRow.find('.js-indentation').length) {
$previousRow = $(previousRow).prev('tr:first-of-type');
previousRow = $previousRow.get(0);
groupHeight += $previousRow.is(':hidden') ? 0 : previousRow.offsetHeight;
}
if (previousRow) {
self.rowObject.swap('before', previousRow);
// No need to check for indentation, 0 is the only valid one.
window.scrollBy(0, -groupHeight);
}
}
else if (self.table.tBodies[0].rows[0] !== previousRow || $previousRow.is('.draggable')) {
// Swap with the previous row (unless previous row is the first
// one and undraggable).
self.rowObject.swap('before', previousRow);
self.rowObject.interval = null;
self.rowObject.indent(0);
window.scrollBy(0, -parseInt(item.offsetHeight, 10));
}
// Regain focus after the DOM manipulation.
handle.trigger('focus');
}
break;
// Right arrow.
case 39:
// Safari right arrow.
case 63235:
keyChange = true;
self.rowObject.indent(self.rtl);
break;
// Down arrow.
case 40:
// Safari down arrow.
case 63233:
var $nextRow = $(self.rowObject.group).eq(-1).next('tr:first-of-type');
var nextRow = $nextRow.get(0);
while (nextRow && $nextRow.is(':hidden')) {
$nextRow = $(nextRow).next('tr:first-of-type');
nextRow = $nextRow.get(0);
}
if (nextRow) {
// Do not allow the onBlur cleanup.
self.safeBlur = false;
self.rowObject.direction = 'down';
keyChange = true;
if ($(item).is('.tabledrag-root')) {
// Swap with the next group (necessarily a top-level one).
groupHeight = 0;
var nextGroup = new self.row(nextRow, 'keyboard', self.indentEnabled, self.maxDepth, false);
if (nextGroup) {
$(nextGroup.group).each(function () {
groupHeight += $(this).is(':hidden') ? 0 : this.offsetHeight;
});
var nextGroupRow = $(nextGroup.group).eq(-1).get(0);
self.rowObject.swap('after', nextGroupRow);
// No need to check for indentation, 0 is the only valid one.
window.scrollBy(0, parseInt(groupHeight, 10));
}
}
else {
// Swap with the next row.
self.rowObject.swap('after', nextRow);
self.rowObject.interval = null;
self.rowObject.indent(0);
window.scrollBy(0, parseInt(item.offsetHeight, 10));
}
// Regain focus after the DOM manipulation.
handle.trigger('focus');
}
break;
}
if (self.rowObject && self.rowObject.changed === true) {
$(item).addClass('drag');
if (self.oldRowElement) {
$(self.oldRowElement).removeClass('drag-previous');
}
self.oldRowElement = item;
if (self.striping === true) {
self.restripeTable();
}
self.onDrag();
}
// Returning false if we have an arrow key to prevent scrolling.
if (keyChange) {
return false;
}
});
// Compatibility addition, return false on keypress to prevent unwanted
// scrolling. IE and Safari will suppress scrolling on keydown, but all
// other browsers need to return false on keypress.
// http://www.quirksmode.org/js/keys.html
handle.on('keypress', function (event) {
switch (event.keyCode) {
// Left arrow.
case 37:
// Up arrow.
case 38:
// Right arrow.
case 39:
// Down arrow.
case 40:
return false;
}
});
};
/**
* Add an asterisk or other marker to the changed row.
*/
Drupal.tableDrag.prototype.row.prototype.markChanged = function () {
var $cell = $('td:first', this.element);
// Find the first appropriate place to insert the marker.
var $target = $($cell.find('.file-size').get(0) || $cell.find('.file').get(0) || $cell.find('.tabledrag-handle').get(0));
if (!$cell.find('.tabledrag-changed').length) {
$target.after(' ' + Drupal.theme('tableDragChangedMarker') + ' ');
}
};
$.extend(Drupal.theme, /** @lends Drupal.theme */{
/**
* @return {string}
*/
tableDragChangedMarker: function () {
return Drupal.theme('bootstrapIcon', 'warning-sign', {'class': ['tabledrag-changed', 'text-warning']});
},
/**
* @return {string}
*/
tableDragChangedWarning: function () {
return '<div class="tabledrag-changed-warning alert alert-sm alert-warning messages warning">' + Drupal.theme('tableDragChangedMarker') + ' ' + Drupal.t('You have unsaved changes.') + '</div>';
}
});
})(jQuery);

View File

@@ -0,0 +1,15 @@
/**
* @file
* Overrides core/misc/vertical-tabs.js.
*/
(function ($, Drupal) {
"use strict";
var createSticky = Drupal.TableHeader.prototype.createSticky;
Drupal.TableHeader.prototype.createSticky = function () {
createSticky.call(this);
this.$stickyTable.addClass(this.$originalTable.attr('class')).removeClass('sticky-enabled sticky-table');
};
})(window.jQuery, window.Drupal);

View File

@@ -0,0 +1,289 @@
/**
* @file
* Overrides core/misc/vertical-tabs.js.
*/
(function ($, window, Drupal, drupalSettings, once) {
"use strict";
/**
* Show the parent vertical tab pane of a targeted page fragment.
*
* In order to make sure a targeted element inside a vertical tab pane is
* visible on a hash change or fragment link click, show all parent panes.
*
* @param {jQuery.Event} e
* The event triggered.
* @param {jQuery} $target
* The targeted node as a jQuery object.
*/
var handleFragmentLinkClickOrHashChange = function handleFragmentLinkClickOrHashChange(e, $target) {
$target.parents('.vertical-tabs-pane').each(function (index, pane) {
$(pane).data('verticalTab').focus();
});
};
/**
* This script transforms a set of details into a stack of vertical
* tabs. Another tab pane can be selected by clicking on the respective
* tab.
*
* Each tab may have a summary which can be updated by another
* script. For that to work, each details element has an associated
* 'verticalTabCallback' (with jQuery.data() attached to the details),
* which is called every time the user performs an update to a form
* element inside the tab pane.
*/
Drupal.behaviors.verticalTabs = {
attach: function (context) {
var width = drupalSettings.widthBreakpoint || 640;
var mq = '(max-width: ' + width + 'px)';
if (window.matchMedia(mq).matches) {
return;
}
/**
* Binds a listener to handle fragment link clicks and URL hash changes.
*/
$(once('vertical-tabs-fragments', 'body')).on('formFragmentLinkClickOrHashChange.verticalTabs', handleFragmentLinkClickOrHashChange);
$(once('vertical-tabs', '[data-vertical-tabs-panes]', context)).each(function () {
var $this = $(this).addClass('tab-content vertical-tabs-panes');
var focusID = $(':hidden.vertical-tabs__active-tab', this).val();
if (typeof focusID === 'undefined' || !focusID.length) {
focusID = false;
}
var tab_focus;
// Check if there are some details that can be converted to vertical-tabs
var $details = $this.find('> .panel');
if ($details.length === 0) {
return;
}
// Create the tab column.
var tab_list = $('<ul class="nav nav-tabs vertical-tabs-list"></ul>');
$this.wrap('<div class="tabbable tabs-left vertical-tabs clearfix"></div>').before(tab_list);
// Transform each details into a tab.
$details.each(function () {
var $that = $(this);
var vertical_tab = new Drupal.verticalTab({
title: $that.find('> .panel-heading > .panel-title, > .panel-heading').last().html(),
details: $that
});
tab_list.append(vertical_tab.item);
$that
.removeClass('collapsed')
// prop() can't be used on browsers not supporting details element,
// the style won't apply to them if prop() is used.
.attr('open', true)
.removeClass('collapsible collapsed panel panel-default')
.addClass('tab-pane vertical-tabs-pane')
.data('verticalTab', vertical_tab)
.find('> .panel-heading').remove();
if (this.id === focusID) {
tab_focus = $that;
}
});
$(tab_list).find('> li:first').addClass('first');
$(tab_list).find('> li:last').addClass('last');
if (!tab_focus) {
// If the current URL has a fragment and one of the tabs contains an
// element that matches the URL fragment, activate that tab.
var $locationHash = $this.find(window.location.hash);
if (window.location.hash && $locationHash.length) {
tab_focus = $locationHash.closest('.vertical-tabs-pane');
}
else {
tab_focus = $this.find('> .vertical-tabs-pane:first');
}
}
if (tab_focus.length) {
tab_focus.data('verticalTab').focus();
}
});
// Provide some Bootstrap tab/Drupal integration.
// @todo merge this into the above code from core.
$(once('bootstrap-tabs', '.tabbable', context)).each(function () {
var $wrapper = $(this);
var $tabs = $wrapper.find('.nav-tabs');
var $content = $wrapper.find('.tab-content');
var borderRadius = parseInt($content.css('borderBottomRightRadius'), 10);
var bootstrapTabResize = function() {
if ($wrapper.hasClass('tabs-left') || $wrapper.hasClass('tabs-right')) {
$content.css('min-height', $tabs.outerHeight());
}
};
// Add min-height on content for left and right tabs.
bootstrapTabResize();
// Detect tab switch.
if ($wrapper.hasClass('tabs-left') || $wrapper.hasClass('tabs-right')) {
$tabs.on('shown.bs.tab', 'a[data-toggle="tab"]', function (e) {
bootstrapTabResize();
if ($wrapper.hasClass('tabs-left')) {
if ($(e.target).parent().is(':first-child')) {
$content.css('borderTopLeftRadius', '0');
}
else {
$content.css('borderTopLeftRadius', borderRadius + 'px');
}
}
else {
if ($(e.target).parent().is(':first-child')) {
$content.css('borderTopRightRadius', '0');
}
else {
$content.css('borderTopRightRadius', borderRadius + 'px');
}
}
});
}
});
}
};
/**
* The vertical tab object represents a single tab within a tab group.
*
* @param settings
* An object with the following keys:
* - title: The name of the tab.
* - details: The jQuery object of the details element that is the tab pane.
*/
Drupal.verticalTab = function (settings) {
var self = this;
$.extend(this, settings, Drupal.theme('verticalTab', settings));
this.link.attr('href', '#' + settings.details.attr('id'));
this.link.on('click', function (e) {
e.preventDefault();
self.focus();
});
// Keyboard events added:
// Pressing the Enter key will open the tab pane.
this.link.on('keydown', function (event) {
if (event.keyCode === 13) {
event.preventDefault();
self.focus();
// Set focus on the first input field of the visible details/tab pane.
$(".vertical-tabs-pane :input:visible:enabled:first").trigger('focus');
}
});
this.details
.on('summaryUpdated', function () {
self.updateSummary();
})
.trigger('summaryUpdated');
};
Drupal.verticalTab.prototype = {
/**
* Displays the tab's content pane.
*/
focus: function () {
this.details
.siblings('.vertical-tabs-pane')
.each(function () {
$(this).removeClass('active').find('> div').removeClass('in');
var tab = $(this).data('verticalTab');
tab.item.removeClass('selected');
})
.end()
.addClass('active')
.siblings(':hidden.vertical-tabs-active-tab')
.val(this.details.attr('id'));
this.details.find('> div').addClass('in');
this.details.data('verticalTab').item.find('a').tab('show');
this.item.addClass('selected');
// Mark the active tab for screen readers.
$('#active-vertical-tab').remove();
this.link.append('<span id="active-vertical-tab" class="visually-hidden">' + Drupal.t('(active tab)') + '</span>');
},
/**
* Updates the tab's summary.
*/
updateSummary: function () {
this.summary.html(this.details.drupalGetSummary());
},
/**
* Shows a vertical tab pane.
*/
tabShow: function () {
// Display the tab.
this.item.show();
// Show the vertical tabs.
this.item.closest('.form-type-vertical-tabs').show();
// Update .first marker for items. We need recurse from parent to retain the
// actual DOM element order as jQuery implements sortOrder, but not as public
// method.
this.item.parent().children('.vertical-tab-button').removeClass('first')
.filter(':visible:first').addClass('first');
// Display the details element.
this.details.removeClass('vertical-tab-hidden').show();
// Focus this tab.
this.focus();
return this;
},
/**
* Hides a vertical tab pane.
*/
tabHide: function () {
// Hide this tab.
this.item.hide();
// Update .first marker for items. We need recurse from parent to retain the
// actual DOM element order as jQuery implements sortOrder, but not as public
// method.
this.item.parent().children('.vertical-tab-button').removeClass('first')
.filter(':visible:first').addClass('first');
// Hide the details element.
this.details.addClass('vertical-tab-hidden').hide();
// Focus the first visible tab (if there is one).
var $firstTab = this.details.siblings('.vertical-tabs-pane:not(.vertical-tab-hidden):first');
if ($firstTab.length) {
$firstTab.data('verticalTab').focus();
}
// Hide the vertical tabs (if no tabs remain).
else {
this.item.closest('.form-type-vertical-tabs').hide();
}
return this;
}
};
/**
* Theme function for a vertical tab.
*
* @param settings
* An object with the following keys:
* - title: The name of the tab.
* @return
* This function has to return an object with at least these keys:
* - item: The root tab jQuery element
* - link: The anchor tag that acts as the clickable area of the tab
* (jQuery version)
* - summary: The jQuery element that contains the tab summary
*/
Drupal.theme.verticalTab = function (settings) {
var tab = {};
tab.item = $('<li class="vertical-tab-button" tabindex="-1"></li>')
.append(tab.link = $('<a href="#' + settings.details[0].id + '" data-toggle="tab"></a>')
.append(tab.title = $('<span></span>').html(settings.title))
.append(tab.summary = $('<div class="summary"></div>')
)
);
return tab;
};
})(jQuery, this, Drupal, drupalSettings, once);

View File

@@ -0,0 +1,585 @@
/**
* @file
* Bootstrap Modals.
*
* @param {jQuery} $
* @param {Drupal} Drupal
* @param {Drupal.bootstrap} Bootstrap
* @param {Attributes} Attributes
* @param {drupalSettings} drupalSettings
*/
(function ($, Drupal, Bootstrap, Attributes, drupalSettings) {
'use strict';
/**
* Only process this once.
*/
Bootstrap.once('modal.jquery.ui.bridge', function (settings) {
// RTL support.
var rtl = document.documentElement.getAttribute('dir').toLowerCase() === 'rtl';
// Override drupal.dialog button classes. This must be done on DOM ready
// since core/drupal.dialog technically depends on this file and has not
// yet set their default settings.
$(function () {
drupalSettings.dialog.buttonClass = 'btn';
drupalSettings.dialog.buttonPrimaryClass = 'btn-primary';
});
// Create the "dialog" plugin bridge.
Bootstrap.Dialog.Bridge = function (options) {
var args = Array.prototype.slice.call(arguments);
var $element = $(this);
var type = options && options.dialogType || $element[0].dialogType || 'modal';
$element[0].dialogType = type;
var handler = Bootstrap.Dialog.Handler.get(type);
// When only options are passed, jQuery UI dialog treats this like a
// initialization method. Destroy any existing Bootstrap modal and
// recreate it using the contents of the dialog HTML.
if (args.length === 1 && typeof options === 'object') {
this.each(function () {
handler.ensureModalStructure(this, options);
});
// Proxy to the Bootstrap Modal plugin, indicating that this is a
// jQuery UI dialog bridge.
return handler.invoke(this, {
dialogOptions: options,
jQueryUiBridge: true
});
}
// Otherwise, proxy all arguments to the Bootstrap Modal plugin.
var ret;
try {
ret = handler.invoke.apply(handler, [this].concat(args));
}
catch (e) {
Bootstrap.warn(e);
}
// If just one element and there was a result returned for the option passed,
// then return the result. Otherwise, just return the jQuery object.
return this.length === 1 && ret !== void 0 ? ret : this;
};
// Assign the jQuery "dialog" plugin to use to the bridge.
Bootstrap.createPlugin('dialog', Bootstrap.Dialog.Bridge);
// Create the "modal" plugin bridge.
Bootstrap.Modal.Bridge = function () {
var Modal = this;
return {
DEFAULTS: {
// By default, this option is disabled. It's only flagged when a modal
// was created using $.fn.dialog above.
jQueryUiBridge: false
},
prototype: {
/**
* Handler for $.fn.dialog('close').
*/
close: function () {
var _this = this;
this.hide.apply(this, arguments);
// For some reason (likely due to the transition event not being
// registered properly), the backdrop doesn't always get removed
// after the above "hide" method is invoked . Instead, ensure the
// backdrop is removed after the transition duration by manually
// invoking the internal "hideModal" method shortly thereafter.
setTimeout(function () {
if (!_this.isShown && _this.$backdrop) {
_this.hideModal();
}
}, (Modal.TRANSITION_DURATION !== void 0 ? Modal.TRANSITION_DURATION : 300) + 10);
},
/**
* Creates any necessary buttons from dialog options.
*/
createButtons: function () {
var handler = Bootstrap.Dialog.Handler.get(this.$element);
this.$footer.find(handler.selectors.buttons).remove();
// jQuery UI supports both objects and arrays. Unfortunately
// developers have misunderstood and abused this by simply placing
// the objects that should be in an array inside an object with
// arbitrary keys (likely to target specific buttons as a hack).
var buttons = this.options.dialogOptions && this.options.dialogOptions.buttons || [];
if (!Array.isArray(buttons)) {
var array = [];
for (var k in buttons) {
// Support the proper object values: label => click callback.
if (typeof buttons[k] === 'function') {
array.push({
label: k,
click: buttons[k],
});
}
// Support nested objects, but log a warning.
else if (buttons[k].text || buttons[k].label) {
Bootstrap.warn('Malformed jQuery UI dialog button: @key. The button object should be inside an array.', {
'@key': k
});
array.push(buttons[k]);
}
else {
Bootstrap.unsupported('button', k, buttons[k]);
}
}
buttons = array;
}
if (buttons.length) {
var $buttons = $('<div class="modal-buttons"/>').appendTo(this.$footer);
for (var i = 0, l = buttons.length; i < l; i++) {
var button = buttons[i];
var $button = $(Drupal.theme('bootstrapModalDialogButton', button));
// Invoke the "create" method for jQuery UI buttons.
if (typeof button.create === 'function') {
button.create.call($button[0]);
}
// Bind the "click" method for jQuery UI buttons to the modal.
if (typeof button.click === 'function') {
$button.on('click', button.click.bind(this.$element));
}
$buttons.append($button);
}
}
// Toggle footer visibility based on whether it has child elements.
this.$footer[this.$footer.children()[0] ? 'show' : 'hide']();
},
/**
* Initializes the Bootstrap Modal.
*/
init: function () {
var handler = Bootstrap.Dialog.Handler.get(this.$element);
if (!this.$dialog) {
this.$dialog = this.$element.find(handler.selectors.dialog);
}
this.$dialog.addClass('js-drupal-dialog');
if (!this.$header) {
this.$header = this.$dialog.find(handler.selectors.header);
}
if (!this.$title) {
this.$title = this.$dialog.find(handler.selectors.title);
}
if (!this.$close) {
this.$close = this.$header.find(handler.selectors.close);
}
if (!this.$footer) {
this.$footer = this.$dialog.find(handler.selectors.footer);
}
if (!this.$content) {
this.$content = this.$dialog.find(handler.selectors.content);
}
if (!this.$dialogBody) {
this.$dialogBody = this.$dialog.find(handler.selectors.body);
}
// Relay necessary events.
if (this.options.jQueryUiBridge) {
this.$element.on('hide.bs.modal', Bootstrap.relayEvent(this.$element, 'dialogbeforeclose', false));
this.$element.on('hidden.bs.modal', Bootstrap.relayEvent(this.$element, 'dialogclose', false));
this.$element.on('show.bs.modal', Bootstrap.relayEvent(this.$element, 'dialogcreate', false));
this.$element.on('shown.bs.modal', Bootstrap.relayEvent(this.$element, 'dialogopen', false));
}
// Create a footer if one doesn't exist.
// This is necessary in case dialog.ajax.js decides to add buttons.
if (!this.$footer[0]) {
this.$footer = handler.theme('footer', {}, true).insertAfter(this.$dialogBody);
}
// Map the initial options.
$.extend(true, this.options, this.mapDialogOptions(this.options));
// Update buttons.
this.createButtons();
// Now call the parent init method.
this.super();
// Handle autoResize option (this is a drupal.dialog option).
if (this.options.dialogOptions && this.options.dialogOptions.autoResize && this.options.dialogOptions.position) {
this.position(this.options.dialogOptions.position);
}
// If show is enabled and currently not shown, show it.
if (this.options.jQueryUiBridge && this.options.show && !this.isShown) {
this.show();
}
},
/**
* Handler for $.fn.dialog('instance').
*/
instance: function () {
Bootstrap.unsupported('method', 'instance', arguments);
},
/**
* Handler for $.fn.dialog('isOpen').
*/
isOpen: function () {
return !!this.isShown;
},
/**
* Maps dialog options to the modal.
*
* @param {Object} options
* The options to map.
*/
mapDialogOptions: function (options) {
// Retrieve the dialog handler for this type.
var handler = Bootstrap.Dialog.Handler.get(this.$element);
var mappedOptions = {};
var dialogOptions = options.dialogOptions || {};
// Remove any existing dialog options.
delete options.dialogOptions;
// Separate Bootstrap modal options from jQuery UI dialog options.
for (var k in options) {
if (Modal.DEFAULTS.hasOwnProperty(k)) {
mappedOptions[k] = options[k];
}
else {
dialogOptions[k] = options[k];
}
}
// Handle CSS properties.
var cssUnitRegExp = /^([+-]?(?:\d+|\d*\.\d+))([a-z]*|%)?$/;
var parseCssUnit = function (value, defaultUnit) {
var parts = ('' + value).match(cssUnitRegExp);
return parts && parts[1] !== void 0 ? parts[1] + (parts[2] || defaultUnit || 'px') : null;
};
var styles = {};
var cssProperties = ['height', 'maxHeight', 'maxWidth', 'minHeight', 'minWidth', 'width'];
for (var i = 0, l = cssProperties.length; i < l; i++) {
var prop = cssProperties[i];
if (dialogOptions[prop] !== void 0) {
var value = parseCssUnit(dialogOptions[prop]);
if (value) {
styles[prop] = value;
// If there's a defined height of some kind, enforce the modal
// to use flex (on modern browsers). This will ensure that
// the core autoResize calculations don't cause the content
// to overflow.
if (dialogOptions.autoResize && (prop === 'height' || prop === 'maxHeight')) {
styles.display = 'flex';
styles.flexDirection = 'column';
this.$dialogBody.css('overflow', 'scroll');
}
}
}
}
// Apply mapped CSS styles to the modal-content container.
this.$content.css(styles);
// Handle deprecated "dialogClass" option by merging it with "classes".
var classesMap = {
'ui-dialog': 'modal-content',
'ui-dialog-titlebar': 'modal-header',
'ui-dialog-title': 'modal-title',
'ui-dialog-titlebar-close': 'close',
'ui-dialog-content': 'modal-body',
'ui-dialog-buttonpane': 'modal-footer'
};
if (dialogOptions.dialogClass) {
if (dialogOptions.classes === void 0) {
dialogOptions.classes = {};
}
if (dialogOptions.classes['ui-dialog'] === void 0) {
dialogOptions.classes['ui-dialog'] = '';
}
var dialogClass = dialogOptions.classes['ui-dialog'].split(' ');
dialogClass.push(dialogOptions.dialogClass);
dialogOptions.classes['ui-dialog'] = dialogClass.join(' ');
delete dialogOptions.dialogClass;
}
// Add jQuery UI classes to elements in case developers target them
// in callbacks.
for (k in classesMap) {
this.$element.find('.' + classesMap[k]).addClass(k);
}
// Bind events.
var events = [
'beforeClose', 'close',
'create',
'drag', 'dragStart', 'dragStop',
'focus',
'open',
'resize', 'resizeStart', 'resizeStop'
];
for (i = 0, l = events.length; i < l; i++) {
var event = events[i].toLowerCase();
if (dialogOptions[event] === void 0 || typeof dialogOptions[event] !== 'function') continue;
this.$element.on('dialog' + event, dialogOptions[event]);
}
// Support title attribute on the modal.
var title;
if ((dialogOptions.title === null || dialogOptions.title === void 0) && (title = this.$element.attr('title'))) {
dialogOptions.title = title;
}
// Handle the reset of the options.
for (var name in dialogOptions) {
if (!dialogOptions.hasOwnProperty(name) || dialogOptions[name] === void 0) continue;
switch (name) {
case 'appendTo':
Bootstrap.unsupported('option', name, dialogOptions.appendTo);
break;
case 'autoOpen':
mappedOptions.show = dialogOptions.show = !!dialogOptions.autoOpen;
break;
case 'classes':
if (dialogOptions.classes) {
for (var key in dialogOptions.classes) {
if (dialogOptions.classes.hasOwnProperty(key) && classesMap[key] !== void 0) {
// Run through Attributes to sanitize classes.
var attributes = Attributes.create().addClass(dialogOptions.classes[key]).toPlainObject();
var selector = '.' + classesMap[key];
this.$element.find(selector).addClass(attributes['class']);
}
}
}
break;
case 'closeOnEscape':
mappedOptions.keyboard = !!dialogOptions.closeOnEscape;
if (!dialogOptions.closeOnEscape && dialogOptions.modal) {
mappedOptions.backdrop = 'static';
}
break;
case 'closeText':
Bootstrap.unsupported('option', name, dialogOptions.closeText);
break;
case 'draggable':
this.$content
.draggable({
handle: handler.selectors.header,
drag: Bootstrap.relayEvent(this.$element, 'dialogdrag'),
start: Bootstrap.relayEvent(this.$element, 'dialogdragstart'),
end: Bootstrap.relayEvent(this.$element, 'dialogdragend')
})
.draggable(dialogOptions.draggable ? 'enable' : 'disable');
break;
case 'hide':
if (dialogOptions.hide === false || dialogOptions.hide === true) {
this.$element[dialogOptions.hide ? 'addClass' : 'removeClass']('fade');
mappedOptions.animation = dialogOptions.hide;
}
else {
Bootstrap.unsupported('option', name + ' (complex animation)', dialogOptions.hide);
}
break;
case 'modal':
if (!dialogOptions.closeOnEscape && dialogOptions.modal) {
mappedOptions.backdrop = 'static';
}
else {
mappedOptions.backdrop = dialogOptions.modal;
}
// If not a modal and no initial position, center it.
if (!dialogOptions.modal && !dialogOptions.position) {
this.position({ my: 'center', of: window });
}
break;
case 'position':
this.position(dialogOptions.position);
break;
// Resizable support (must initialize first).
case 'resizable':
this.$content
.resizable({
resize: Bootstrap.relayEvent(this.$element, 'dialogresize'),
start: Bootstrap.relayEvent(this.$element, 'dialogresizestart'),
end: Bootstrap.relayEvent(this.$element, 'dialogresizeend')
})
.resizable(dialogOptions.resizable ? 'enable' : 'disable');
break;
case 'show':
if (dialogOptions.show === false || dialogOptions.show === true) {
this.$element[dialogOptions.show ? 'addClass' : 'removeClass']('fade');
mappedOptions.animation = dialogOptions.show;
}
else {
Bootstrap.unsupported('option', name + ' (complex animation)', dialogOptions.show);
}
break;
case 'title':
this.$title.text(dialogOptions.title);
break;
}
}
// Add the supported dialog options to the mapped options.
mappedOptions.dialogOptions = dialogOptions;
return mappedOptions;
},
/**
* Handler for $.fn.dialog('moveToTop').
*/
moveToTop: function () {
Bootstrap.unsupported('method', 'moveToTop', arguments);
},
/**
* Handler for $.fn.dialog('option').
*/
option: function () {
var clone = {options: {}};
// Apply the parent option method to the clone of current options.
this.super.apply(clone, arguments);
// Merge in the cloned mapped options.
$.extend(true, this.options, this.mapDialogOptions(clone.options));
// Update buttons.
this.createButtons();
},
position: function(position) {
// Reset modal styling.
this.$element.css({
bottom: 'initial',
overflow: 'visible',
right: 'initial'
});
// Position the modal.
this.$element.position(position);
},
/**
* Handler for $.fn.dialog('open').
*/
open: function () {
this.show.apply(this, arguments);
},
/**
* Handler for $.fn.dialog('widget').
*/
widget: function () {
return this.$element;
}
}
};
};
// Extend the Bootstrap Modal plugin constructor class.
Bootstrap.extendPlugin('modal', Bootstrap.Modal.Bridge);
// Register default core dialog type handlers.
Bootstrap.Dialog.Handler.register('dialog');
Bootstrap.Dialog.Handler.register('modal');
/**
* Extend Drupal theming functions.
*/
$.extend(Drupal.theme, /** @lend Drupal.theme */ {
/**
* Renders a jQuery UI Dialog compatible button element.
*
* @param {Object} button
* The button object passed in the dialog options.
*
* @return {String}
* The modal dialog button markup.
*
* @see http://api.jqueryui.com/dialog/#option-buttons
* @see http://api.jqueryui.com/button/
*/
bootstrapModalDialogButton: function (button) {
var attributes = Attributes.create();
var icon = '';
var iconPosition = button.iconPosition || 'beginning';
iconPosition = (iconPosition === 'end' && !rtl) || (iconPosition === 'beginning' && rtl) ? 'after' : 'before';
// Handle Bootstrap icons differently.
if (button.bootstrapIcon) {
icon = Drupal.theme('icon', 'bootstrap', button.icon);
}
// Otherwise, assume it's a jQuery UI icon.
// @todo Map jQuery UI icons to Bootstrap icons?
else if (button.icon) {
var iconAttributes = Attributes.create()
.addClass(['ui-icon', button.icon])
.set('aria-hidden', 'true');
icon = '<span' + iconAttributes + '></span>';
}
// Label. Note: jQuery UI dialog has an inconsistency where it uses
// "text" instead of "label", so both need to be supported.
var value = button.label || button.text;
// Show/hide label.
if (icon && ((button.showLabel !== void 0 && !button.showLabel) || (button.text !== void 0 && !button.text))) {
value = '<span' + Attributes.create().addClass('sr-only') + '>' + value + '</span>';
}
attributes.set('value', iconPosition === 'before' ? icon + value : value + icon);
// Handle disabled.
attributes[button.disabled ? 'set' :'remove']('disabled', 'disabled');
if (button.classes) {
attributes.addClass(Object.keys(button.classes).map(function(key) { return button.classes[key]; }));
}
if (button['class']) {
attributes.addClass(button['class']);
}
if (button.primary) {
attributes.addClass('btn-primary');
}
return Drupal.theme('button', attributes);
}
});
});
})(window.jQuery, window.Drupal, window.Drupal.bootstrap, window.Attributes, window.drupalSettings);

View File

@@ -0,0 +1,603 @@
/**
* @file
* Bootstrap Modals.
*
* @param {jQuery} $
* @param {Drupal} Drupal
* @param {Drupal.bootstrap} Bootstrap
* @param {Attributes} Attributes
*/
(function ($, Drupal, Bootstrap, Attributes) {
'use strict';
/**
* Document jQuery object.
*
* @type {jQuery}
*/
var $document = $(document);
/**
* Finds the first available and visible focusable input element.
*
* This is abstracted from the main code below so sub-themes can override
* this method to return their own element if desired.
*
* @param {Modal} modal
* The Bootstrap modal instance.
*
* @return {jQuery}
* A jQuery object containing the element that should be focused. Note: if
* this object contains multiple elements, only the first visible one will
* be used.
*/
Bootstrap.modalFindFocusableElement = function (modal) {
return modal.$dialogBody.find(':input,:button,.btn').not('.visually-hidden,.sr-only');
};
$document.on('shown.bs.modal', function (e) {
var $modal = $(e.target);
var modal = $modal.data('bs.modal');
// Check if there are any CKEditor 5 instances
var $ckeditor = $modal.find('[data-ckeditor5-id]');
if ($ckeditor.length) {
// Move the overlay wrapper inside the modal so it can be interacted with
$('.ck-body-wrapper').appendTo($modal);
}
// Focus the first input element found.
if (modal && modal.options.focusInput) {
var $focusable = Bootstrap.modalFindFocusableElement(modal);
if ($focusable && $focusable[0]) {
var $input = $focusable.filter(':visible:first').focus();
// Select text if input is text.
if (modal.options.selectText && $input.is(':text')) {
$input[0].setSelectionRange(0, $input[0].value.length)
}
}
else if (modal.$close.is(':visible')) {
modal.$close.focus();
}
}
});
/**
* Only process this once.
*/
Bootstrap.once('modal', function (settings) {
/**
* Replace the Bootstrap Modal jQuery plugin definition.
*
* This adds a little bit of functionality so it works better with Drupal.
*/
Bootstrap.replacePlugin('modal', function () {
var BootstrapModal = this;
// Override the Modal constructor.
Bootstrap.Modal = function (element, options) {
this.$body = $(document.body);
this.$element = $(element);
this.$dialog = this.$element.find('.modal-dialog');
this.$header = this.$dialog.find('.modal-header');
this.$title = this.$dialog.find('.modal-title');
this.$close = this.$header.find('.close');
this.$footer = this.$dialog.find('.modal-footer');
this.$content = this.$dialog.find('.modal-content');
this.$dialogBody = this.$dialog.find('.modal-body');
this.$backdrop = null;
this.isShown = null;
this.originalBodyPad = null;
this.scrollbarWidth = 0;
this.ignoreBackdropClick = false;
this.options = this.mapDialogOptions(options);
};
// Extend defaults to take into account for theme settings.
Bootstrap.Modal.DEFAULTS = $.extend({}, BootstrapModal.DEFAULTS, {
animation: !!settings.modal_animation,
backdrop: settings.modal_backdrop === 'static' ? 'static' : !!settings.modal_backdrop,
focusInput: !!settings.modal_focus_input,
selectText: !!settings.modal_select_text,
keyboard: !!settings.modal_keyboard,
remote: null,
show: !!settings.modal_show,
size: settings.modal_size
});
// Copy over the original prototype methods.
Bootstrap.Modal.prototype = BootstrapModal.prototype;
/**
* Handler for $.fn.modal('destroy').
*/
Bootstrap.Modal.prototype.destroy = function () {
this.hide();
Drupal.detachBehaviors(this.$element[0]);
this.$element.removeData('bs.modal').remove();
};
/**
* Initialize the modal.
*/
Bootstrap.Modal.prototype.init = function () {
if (this.options.remote) {
this.$content.load(this.options.remote, $.proxy(function () {
this.$element.trigger('loaded.bs.modal');
}, this));
}
};
/**
* Map dialog options.
*
* Note: this is primarily for use in modal.jquery.ui.bridge.js.
*
* @param {Object} options
* The passed options.
*/
Bootstrap.Modal.prototype.mapDialogOptions = function (options) {
return options || {};
}
// Modal jQuery Plugin Definition.
var Plugin = function () {
// Extract the arguments.
var args = Array.prototype.slice.call(arguments);
var method = args[0];
var options = args[1] || {};
var relatedTarget = args[2] || null;
// Move arguments down if no method was passed.
if ($.isPlainObject(method)) {
relatedTarget = options || null;
options = method;
method = null;
}
var ret = void 0;
this.each(function () {
var $this = $(this);
var data = $this.data('bs.modal');
var initialize = false;
// Immediately return if there's no instance to invoke a valid method.
var showMethods = ['open', 'show', 'toggle'];
if (!data && method && showMethods.indexOf(method) === -1) {
return;
}
options = Bootstrap.normalizeObject($.extend({}, Bootstrap.Modal.DEFAULTS, data && data.options, $this.data(), options));
delete options['bs.modal'];
if (!data) {
$this.data('bs.modal', (data = new Bootstrap.Modal(this, options)));
initialize = true;
}
// Initialize the modal.
if (initialize || (!method && !args.length)) {
data.init();
}
// Explicit method passed.
if (method) {
if (typeof data[method] === 'function') {
try {
ret = data[method].apply(data, args.slice(1));
}
catch (e) {
Drupal.throwError(e);
}
}
else {
Bootstrap.unsupported('method', method);
}
}
// No method, set options and open if necessary.
else {
data.option(options);
if (options.show && !data.isShown) {
data.show(relatedTarget);
}
}
});
// If just one element and there was a result returned for the option passed,
// then return the result. Otherwise, just return the jQuery object.
return this.length === 1 && ret !== void 0 ? ret : this;
};
// Replace the plugin constructor with the new Modal constructor.
Plugin.Constructor = Bootstrap.Modal;
// Replace the data API so that it calls $.fn.modal rather than Plugin.
// This allows sub-themes to replace the jQuery Plugin if they like with
// out having to redo all this boilerplate.
$document
.off('click.bs.modal.data-api')
.on('click.bs.modal.data-api', '[data-toggle="modal"]', function (e) {
var $this = $(this);
var href = $this.attr('href');
var target = $this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, '')); // strip for ie7
var $target = $document.find(target);
var options = $target.data('bs.modal') ? 'toggle' : $.extend({ remote: !/#/.test(href) && href }, $target.data(), $this.data());
if ($this.is('a')) e.preventDefault();
$target.one('show.bs.modal', function (showEvent) {
// Only register focus restorer if modal will actually get shown.
if (showEvent.isDefaultPrevented()) return;
$target.one('hidden.bs.modal', function () {
$this.is(':visible') && $this.trigger('focus');
});
});
$target.modal(options, this);
});
return Plugin;
});
/**
* Extend Drupal theming functions.
*/
$.extend(Drupal.theme, /** @lend Drupal.theme */ {
/**
* Theme function for a Bootstrap Modal.
*
* @param {Object} [variables]
* An object containing key/value pairs of variables.
*
* @return {string}
* The HTML for the modal.
*/
bootstrapModal: function (variables) {
var output = '';
var settings = drupalSettings.bootstrap || {};
var defaults = {
attributes: {
class: ['modal'],
tabindex: -1,
role: 'dialog'
},
body: '',
closeButton: true,
description: {
attributes: {
class: ['help-block']
},
content: null,
position: 'before'
},
footer: '',
id: 'drupal-modal',
size: settings.modal_size ? settings.modal_size : '',
title: {
attributes: {
class: ['modal-title']
},
content: Drupal.t('Loading...'),
html: false,
tag: 'h4'
}
};
variables = $.extend(true, {}, defaults, variables);
var attributes = Attributes.create(defaults.attributes).merge(variables.attributes);
attributes.set('id', attributes.get('id', variables.id));
if (settings.modal_animation) {
attributes.addClass('fade');
}
// Build the modal wrapper.
output += '<div' + attributes + '>';
// Build the modal-dialog wrapper.
output += Drupal.theme('bootstrapModalDialog', _.omit(variables, 'attributes'));
// Close the modal wrapper.
output += '</div>';
// Return the constructed modal.
return output;
},
/**
* Theme function for a Bootstrap Modal dialog markup.
*
* @param {Object} [variables]
* An object containing key/value pairs of variables.
*
* @return {string}
* The HTML for the modal close button.
*/
bootstrapModalDialog: function (variables) {
var output = '';
var defaults = {
attributes: {
class: ['modal-dialog'],
role: 'document'
},
id: 'drupal-modal'
};
variables = $.extend(true, {}, defaults, variables);
var attributes = Attributes.create(defaults.attributes).merge(variables.attributes);
attributes.set('id', attributes.get('id', variables.id + '--dialog'));
if (variables.size) {
attributes.addClass(variables.size);
}
output += '<div' + attributes + '>';
// Build the modal-content wrapper.
output += Drupal.theme('bootstrapModalContent', _.omit(variables, 'attributes'));
// Close the modal-dialog wrapper.
output += '</div>';
return output;
},
/**
* Theme function for a Bootstrap Modal content markup.
*
* @param {Object} [variables]
* An object containing key/value pairs of variables.
*
* @return {string}
* The HTML for the modal close button.
*/
bootstrapModalContent: function (variables) {
var output = '';
var defaults = {
attributes: {
class: ['modal-content']
},
id: 'drupal-modal'
};
variables = $.extend(true, {}, defaults, variables);
var attributes = Attributes.create(defaults.attributes).merge(variables.attributes);
attributes.set('id', attributes.get('id', variables.id + '--content'));
// Build the modal-content wrapper.
output += '<div' + attributes + '>';
variables = _.omit(variables, 'attributes');
// Build the header wrapper and title.
output += Drupal.theme('bootstrapModalHeader', variables);
// Build the body.
output += Drupal.theme('bootstrapModalBody', variables);
// Build the footer.
output += Drupal.theme('bootstrapModalFooter', variables);
// Close the modal-content wrapper.
output += '</div>';
return output;
},
/**
* Theme function for a Bootstrap Modal body markup.
*
* @param {Object} [variables]
* An object containing key/value pairs of variables.
*
* @return {string}
* The HTML for the modal close button.
*/
bootstrapModalBody: function (variables) {
var output = '';
var defaults = {
attributes: {
class: ['modal-body']
},
body: '',
description: {
attributes: {
class: ['help-block']
},
content: null,
position: 'before'
},
id: 'drupal-modal'
};
variables = $.extend(true, {}, defaults, variables);
var attributes = Attributes.create(defaults.attributes).merge(variables.attributes);
attributes.set('id', attributes.get('id', variables.id + '--body'));
output += '<div' + attributes + '>';
if (typeof variables.description === 'string') {
variables.description = $.extend({}, defaults.description, { content: variables.description });
}
var description = variables.description;
description.attributes = Attributes.create(defaults.description.attributes).merge(description.attributes);
if (description.content && description.position === 'invisible') {
description.attributes.addClass('sr-only');
}
if (description.content && description.position === 'before') {
output += '<p' + description.attributes + '>' + description.content + '</p>';
}
output += variables.body;
if (description.content && (description.position === 'after' || description.position === 'invisible')) {
output += '<p' + description.attributes + '>' + description.content + '</p>';
}
output += '</div>';
return output;
},
/**
* Theme function for a Bootstrap Modal close button.
*
* @param {Object} [variables]
* An object containing key/value pairs of variables.
*
* @return {string}
* The HTML for the modal close button.
*/
bootstrapModalClose: function (variables) {
var defaults = {
attributes: {
'aria-label': Drupal.t('Close'),
class: ['close'],
'data-dismiss': 'modal',
type: 'button'
}
};
variables = $.extend(true, {}, defaults, variables);
var attributes = Attributes.create(defaults.attributes).merge(variables.attributes);
return '<button' + attributes + '><span aria-hidden="true">&times;</span></button>';
},
/**
* Theme function for a Bootstrap Modal footer.
*
* @param {Object} [variables]
* An object containing key/value pairs of variables.
* @param {boolean} [force]
* Flag to force rendering the footer, regardless if there's content.
*
* @return {string}
* The HTML for the modal footer.
*/
bootstrapModalFooter: function (variables, force) {
var output = '';
var defaults = {
attributes: {
class: ['modal-footer']
},
footer: '',
id: 'drupal-modal'
};
variables = $.extend(true, {}, defaults, variables);
if (force || variables.footer) {
var attributes = Attributes.create(defaults.attributes).merge(variables.attributes);
attributes.set('id', attributes.get('id', variables.id + '--footer'));
output += '<div' + attributes + '>';
output += variables.footer;
output += '</div>';
}
return output;
},
/**
* Theme function for a Bootstrap Modal header.
*
* @param {Object} [variables]
* An object containing key/value pairs of variables.
*
* @return {string}
* The HTML for the modal header.
*/
bootstrapModalHeader: function (variables) {
var output = '';
var defaults = {
attributes: {
class: ['modal-header']
},
closeButton: true,
id: 'drupal-modal',
title: {
attributes: {
class: ['modal-title']
},
content: Drupal.t('Loading...'),
html: false,
tag: 'h4'
}
};
variables = $.extend(true, {}, defaults, variables);
if (typeof variables.title === 'string') {
variables.title = $.extend({}, defaults.title, { content: variables.title });
}
var title = Drupal.theme('bootstrapModalTitle', variables.title);
if (title) {
var attributes = Attributes.create(defaults.attributes).merge(variables.attributes);
attributes.set('id', attributes.get('id', variables.id + '--header'));
output += '<div' + attributes + '>';
if (variables.closeButton) {
output += Drupal.theme('bootstrapModalClose', _.omit(variables, 'attributes'));
}
output += title;
output += '</div>';
}
return output;
},
/**
* Theme function for a Bootstrap Modal title.
*
* @param {Object} [variables]
* An object containing key/value pairs of variables.
*
* @return {string}
* The HTML for the modal title.
*/
bootstrapModalTitle: function (variables) {
var output = '';
var defaults = {
attributes: {
class: ['modal-title']
},
closeButton: true,
id: 'drupal-modal',
content: Drupal.t('Loading...'),
html: false,
tag: 'h4'
};
if (typeof variables === 'string') {
variables = $.extend({}, defaults, { content: title });
}
variables = $.extend(true, {}, defaults, variables);
var attributes = Attributes.create(defaults.attributes).merge(variables.attributes);
attributes.set('id', attributes.get('id', variables.id + '--title'));
output += '<' + Drupal.checkPlain(variables.tag) + Attributes.create(defaults.attributes).merge(variables.attributes) + '>';
if (variables.closeButton) {
output += Drupal.theme('bootstrapModalClose', _.omit(variables, 'attributes'));
}
output += (variables.html ? variables.content : Drupal.checkPlain(variables.content));
output += '</' + Drupal.checkPlain(variables.tag) + '>';
return output;
}
})
});
})(window.jQuery, window.Drupal, window.Drupal.bootstrap, window.Attributes);

View File

@@ -0,0 +1,35 @@
/**
* @file
* Attaches behavior for the Filter module.
*/
(function ($, once) {
'use strict';
function updateFilterHelpLink () {
var $link = $(this).parents('.filter-wrapper').find('.filter-help > a');
var originalLink = $link.data('originalLink');
if (!originalLink) {
originalLink = $link.attr('href');
$link.data('originalLink', originalLink);
}
$link.attr('href', originalLink + '/' + $(this).find(':selected').val());
}
$(document).on('change', '.filter-wrapper select.filter-list', updateFilterHelpLink);
/**
* Displays the guidelines of the selected text format automatically.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches behavior for updating filter guidelines.
*/
Drupal.behaviors.filterGuidelines = {
attach: function (context) {
$(once('filter-list', '.filter-wrapper select.filter-list', context)).each(updateFilterHelpLink);
}
};
})(jQuery, once);

View File

@@ -0,0 +1,11 @@
/**
* @file
* Theme hooks for the Drupal Bootstrap base theme.
*/
(function ($, Drupal) {
if (Drupal.ImageWidgetCrop && Drupal.ImageWidgetCrop.prototype && Drupal.ImageWidgetCrop.prototype.selectors && Drupal.ImageWidgetCrop.prototype.selectors.summary) {
Drupal.ImageWidgetCrop.prototype.selectors.summary += ', > .panel-heading > .panel-title';
}
})(window.jQuery, window.Drupal);

View File

@@ -0,0 +1,24 @@
/**
* @file
* Extends core ajax_view.js.
*/
(function ($, Drupal) {
'use strict';
/**
* @method
*/
Drupal.views.ajaxView.prototype.attachExposedFormAjax = function () {
var that = this;
this.exposedFormAjax = [];
$('button[type=submit], input[type=submit], input[type=image]', this.$exposed_form).not('[data-drupal-selector=edit-reset]').each(function (index) {
var self_settings = $.extend({}, that.element_settings, {
base: $(this).attr('id'),
element: this
});
that.exposedFormAjax[index] = Drupal.ajax(self_settings);
});
};
})(jQuery, Drupal);

View File

@@ -0,0 +1,152 @@
/**
* @file
* Bootstrap Popovers.
*/
var Drupal = Drupal || {};
(function ($, Drupal, Bootstrap) {
"use strict";
var $document = $(document);
/**
* Extend the Bootstrap Popover plugin constructor class.
*/
Bootstrap.extendPlugin('popover', function (settings) {
return {
DEFAULTS: {
animation: !!settings.popover_animation,
autoClose: !!settings.popover_auto_close,
enabled: settings.popover_enabled,
html: !!settings.popover_html,
placement: settings.popover_placement,
selector: settings.popover_selector,
trigger: settings.popover_trigger,
title: settings.popover_title,
content: settings.popover_content,
delay: parseInt(settings.popover_delay, 10),
container: settings.popover_container
}
};
});
/**
* Bootstrap Popovers.
*
* @todo This should really be properly delegated if selector option is set.
*/
Drupal.behaviors.bootstrapPopovers = {
$activePopover: null,
attach: function (context) {
// Immediately return if popovers are not available.
if (!$.fn.popover || !$.fn.popover.Constructor.DEFAULTS.enabled) {
return;
}
var _this = this;
$document
.on('show.bs.popover', '[data-toggle=popover]', function () {
var $trigger = $(this);
var popover = $trigger.data('bs.popover');
// Only keep track of clicked triggers that we're manually handling.
if (popover.options.originalTrigger === 'click') {
if (_this.$activePopover && _this.getOption('autoClose') && !_this.$activePopover.is($trigger)) {
_this.$activePopover.popover('hide');
}
_this.$activePopover = $trigger;
}
})
// Unfortunately, :focusable is only made available when using jQuery
// UI. While this would be the most semantic pseudo selector to use
// here, jQuery UI may not always be loaded. Instead, just use :visible
// here as this just needs some sort of selector here. This activates
// delegate binding to elements in jQuery so it can work it's bubbling
// focus magic since elements don't really propagate their focus events.
// @see https://www.drupal.org/project/bootstrap/issues/3013236
.on('focus.bs.popover', ':visible', function (e) {
var $target = $(e.target);
if (_this.$activePopover && _this.getOption('autoClose') && !_this.$activePopover.is($target) && !$target.closest('.popover.in')[0]) {
_this.$activePopover.popover('hide');
_this.$activePopover = null;
}
})
.on('click.bs.popover', function (e) {
var $target = $(e.target);
if (_this.$activePopover && _this.getOption('autoClose') && !$target.is('[data-toggle=popover]') && !$target.closest('.popover.in')[0]) {
_this.$activePopover.popover('hide');
_this.$activePopover = null;
}
})
.on('keyup.bs.popover', function (e) {
if (_this.$activePopover && _this.getOption('autoClose') && e.which === 27) {
_this.$activePopover.popover('hide');
_this.$activePopover = null;
}
})
;
var elements = $(context).find('[data-toggle=popover]').toArray();
for (var i = 0; i < elements.length; i++) {
var $element = $(elements[i]);
var options = $.extend({}, $.fn.popover.Constructor.DEFAULTS, $element.data());
// Store the original trigger.
options.originalTrigger = options.trigger;
// If the trigger is "click", then we'll handle it manually here.
if (options.trigger === 'click') {
options.trigger = 'manual';
}
// Retrieve content from a target element.
var target = options.target || $element.is('a[href^="#"]') && $element.attr('href');
var $target = $document.find(target).clone();
if (!options.content && $target[0]) {
$target.removeClass('visually-hidden hidden').removeAttr('aria-hidden');
options.content = $target.wrap('<div/>').parent()[options.html ? 'html' : 'text']() || '';
}
// Initialize the popover.
$element.popover(options);
// Handle clicks manually.
if (options.originalTrigger === 'click') {
// To ensure the element is bound multiple times, remove any
// previously set event handler before adding another one.
$element
.off('click.drupal.bootstrap.popover')
.on('click.drupal.bootstrap.popover', function (e) {
$(this).popover('toggle');
e.preventDefault();
e.stopPropagation();
})
;
}
}
},
detach: function (context) {
// Immediately return if popovers are not available.
if (!$.fn.popover || !$.fn.popover.Constructor.DEFAULTS.enabled) {
return;
}
// Destroy all popovers.
$(context).find('[data-toggle="popover"]')
.off('click.drupal.bootstrap.popover')
.popover('destroy')
;
},
getOption: function(name, defaultValue, element) {
var $element = element ? $(element) : this.$activePopover;
var options = $.extend(true, {}, $.fn.popover.Constructor.DEFAULTS, ($element && $element.data('bs.popover') || {}).options);
if (options[name] !== void 0) {
return options[name];
}
return defaultValue !== void 0 ? defaultValue : void 0;
}
};
})(window.jQuery, window.Drupal, window.Drupal.bootstrap);

View File

@@ -0,0 +1,65 @@
/**
* @file
* Text behaviors.
*/
(function ($, Drupal, once) {
'use strict';
/**
* Auto-hide summary textarea if empty and show hide and unhide links.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches auto-hide behavior on `text-summary` events.
*/
Drupal.behaviors.textSummary = {
attach: function (context, settings) {
$(once('text-summary', '.js-text-summary', context)).each(function () {
var $widget = $(this).closest('.js-text-format-wrapper');
var $summary = $widget.find('.js-text-summary-wrapper');
var $summaryLabel = $summary.find('label').eq(0);
var $full = $widget.find('.js-text-full').closest('.js-form-item');
var $fullLabel = $full.find('label').eq(0);
// Create a placeholder label when the field cardinality is greater
// than 1.
if ($fullLabel.length === 0) {
$fullLabel = $('<label></label>').prependTo($full);
}
// Set up the edit/hide summary link.
var $link = $('<span class="field-edit-link"><button type="button" class="link link-edit-summary btn btn-default btn-xs pull-right" data-toggle="button" aria-pressed="false" autocomplete="off">' + Drupal.t('Hide summary') + '</button></span>');
var $button = $link.find('button');
var toggleClick = true;
$link.on('click', function (e) {
if (toggleClick) {
$summary.hide();
$button.html(Drupal.t('Edit summary'));
$fullLabel.before($link);
}
else {
$summary.show();
$button.html(Drupal.t('Hide summary'));
$summaryLabel.before($link);
}
e.preventDefault();
toggleClick = !toggleClick;
});
$summaryLabel.before($link);
// If no summary is set, hide the summary field.
if ($widget.find('.js-text-summary').val() === '') {
$link.trigger('click');
}
else {
$link.addClass('active');
}
});
}
};
})(jQuery, Drupal, once);

View File

@@ -0,0 +1,279 @@
(function ($, Drupal, Bootstrap, once) {
/*global jQuery:false */
/*global Drupal:false */
"use strict";
/**
* Provide vertical tab summaries for Bootstrap settings.
*/
Drupal.behaviors.bootstrapSettingSummaries = {
attach: function (context) {
var $context = $(context);
// General.
$context.find('[data-drupal-selector="edit-general"]').drupalSetSummary(function () {
var summary = [];
// Buttons.
var size = $context.find('select[name="button_size"] :selected');
if (size.val()) {
summary.push(Drupal.t('@size Buttons', {
'@size': size.text()
}));
}
// Images.
var shape = $context.find('select[name="image_shape"] :selected');
if (shape.val()) {
summary.push(Drupal.t('@shape Images', {
'@shape': shape.text()
}));
}
if ($context.find(':input[name="image_responsive"]').is(':checked')) {
summary.push(Drupal.t('Responsive Images'));
}
// Tables.
if ($context.find(':input[name="table_responsive"]').is(':checked')) {
summary.push(Drupal.t('Responsive Tables'));
}
return summary.join(', ');
});
// Components.
$context.find('[data-drupal-selector="edit-components"]').drupalSetSummary(function () {
var summary = [];
// Breadcrumbs.
var breadcrumb = parseInt($context.find('select[name="breadcrumb"]').val(), 10);
if (breadcrumb) {
summary.push(Drupal.t('Breadcrumbs'));
}
// Navbar.
var navbar = 'Navbar: ' + $context.find('select[name="navbar_position"] :selected').text();
if ($context.find('input[name="navbar_inverse"]').is(':checked')) {
navbar += ' (' + Drupal.t('Inverse') + ')';
}
summary.push(navbar);
return summary.join(', ');
});
// JavaScript.
var $jQueryUiBridge = $context.find('input[name="modal_jquery_ui_bridge"]');
$(once('bs.jquery.ui.dialog.bridge', 'input[name="modal_jquery_ui_bridge"]', context)).each(function () {
$jQueryUiBridge
.off('change.bs.jquery.ui.dialog.bridge')
.on('change.bs.jquery.ui.dialog.bridge', function (e) {
if ($jQueryUiBridge[0].checked) {
return;
}
var disable = false;
var title = Drupal.t('<p><strong>Warning: Disabling the jQuery UI Dialog bridge may have major consequences.</strong></p>');
var message = Drupal.t('<p>If you are unfamiliar with how to properly handle Bootstrap modals and jQuery UI dialogs together, it is highly recommended this remains <strong>enabled</strong>.</p> <p>Are you sure you want to disable this?</p>');
var callback = function () {
if (!disable) {
$jQueryUiBridge[0].checked = true;
$jQueryUiBridge.trigger('change');
}
};
if (!$.fn.dialog) {
disable = window.confirm(Bootstrap.stripHtml(title + ' ' + message));
callback();
}
else {
$('<div title="Disable jQuery UI Dialog Bridge?"><div class="alert alert-danger alert-sm">' + title + '</div>' + message + '</div>')
.appendTo('body')
.dialog({
modal: true,
close: callback,
buttons: [
{
text: Drupal.t('Disable'),
classes: {
'ui-button': 'btn-danger',
},
click: function () {
disable = true;
$(this).dialog('close');
}
},
{
text: 'Cancel',
primary: true,
click: function () {
$(this).dialog('close');
}
}
]
});
}
});
});
$context.find('[data-drupal-selector="edit-javascript"]').drupalSetSummary(function () {
var summary = [];
if ($context.find('input[name="modal_enabled"]').is(':checked')) {
if ($jQueryUiBridge.is(':checked')) {
summary.push(Drupal.t('Modals (Bridged)'));
}
else {
summary.push(Drupal.t('Modals'));
}
}
if ($context.find('input[name="popover_enabled"]').is(':checked')) {
summary.push(Drupal.t('Popovers'));
}
if ($context.find('input[name="tooltip_enabled"]').is(':checked')) {
summary.push(Drupal.t('Tooltips'));
}
return summary.join(', ');
});
// CDN.
$context.find('[data-drupal-selector="edit-cdn"]').drupalSetSummary(function () {
var summary = [];
var $cdnProvider = $context.find('select[name="cdn_provider"] :selected');
if ($cdnProvider.length) {
var provider = $cdnProvider.text();
var $version = $context.find('select[name="cdn_version"] :selected');
if ($version.length && $version.val().length) {
provider += ' - ' + $version.text();
var $theme = $context.find('select[name="cdn_theme"] :selected');
if ($theme.length) {
provider += ' (' + $theme.text() + ')';
}
}
else if ($cdnProvider.val() === 'custom') {
var $urls = $context.find('textarea[name="cdn_custom"]');
var urls = ($urls.val() + '').split(/\r\n|\n/).filter(Boolean);
provider += ' (' + Drupal.formatPlural(urls.length, '1 URL', '@count URLs') + ')';
}
summary.push(provider);
}
return summary.join(', ');
});
// Advanced.
$context.find('[data-drupal-selector="edit-advanced"]').drupalSetSummary(function () {
var summary = [];
var deprecations = [];
if ($context.find('input[name="include_deprecated"]').is(':checked')) {
deprecations.push(Drupal.t('Included'));
}
deprecations.push($context.find('input[name="suppress_deprecated_warnings"]').is(':checked') ? Drupal.t('Warnings Suppressed') : Drupal.t('Warnings Shown'));
summary.push(Drupal.t('Deprecations: @value', {
'@value': deprecations.join(', '),
}));
return summary.join(', ');
});
}
};
/**
* Provide Bootstrap Bootswatch preview.
*/
Drupal.behaviors.bootstrapBootswatchPreview = {
attach: function (context) {
var $context = $(context);
var $preview = $context.find('#bootstrap-theme-preview');
$(once('bootstrap-theme-preview', '#bootstrap-theme-preview', context)).each(function () {
// Construct the "Bootstrap Theme" preview here since it's not actually
// a Bootswatch theme, but rather one provided by Bootstrap itself.
// Unfortunately getbootstrap.com does not have HTTPS enabled, so the
// preview image cannot be protocol relative.
// @todo Make protocol relative if/when Bootstrap enables HTTPS.
$preview.append('<a id="bootstrap-theme-preview-bootstrap_theme" class="bootswatch-preview element-invisible" href="https://getbootstrap.com/docs/3.4/examples/theme/" target="_blank"><img class="img-responsive" src="//getbootstrap.com/docs/3.4/examples/screenshots/theme.jpg" alt="' + Drupal.t('Preview of the Bootstrap theme') + '" /></a>');
// Retrieve the Bootswatch theme preview images.
// @todo This should be moved into PHP.
$.ajax({
url: 'https://bootswatch.com/api/3.json',
dataType: 'json',
success: function (json) {
var themes = json.themes;
for (var i = 0, len = themes.length; i < len; i++) {
$preview.append('<a id="bootstrap-theme-preview-' + themes[i].name.toLowerCase() + '" class="bootswatch-preview element-invisible" href="' + themes[i].preview + '" target="_blank"><img class="img-responsive" src="' + themes[i].thumbnail.replace(/^http:/, 'https:') + '" alt="' + Drupal.t('Preview of the @title Bootswatch theme', { '@title': themes[i].name }) + '" /></a>');
}
},
complete: function () {
$preview.parent().find('select[name="cdn_theme"]').bind('change', function () {
$preview.find('.bootswatch-preview').addClass('visually-hidden');
var theme = $(this).val();
if (theme && theme.length) {
$preview.find('#bootstrap-theme-preview-' + theme).removeClass('visually-hidden');
}
}).change();
}
});
});
}
};
/**
* Provide Bootstrap navbar preview.
*/
Drupal.behaviors.bootstrapContainerPreview = {
attach: function (context) {
var $context = $(context);
var $container = $context.find('#edit-container');
$(once('container-preview', '#edit-container', context)).each(function () {
$container.find('[name="fluid_container"]').on('change.bootstrap', function () {
if ($(this).is(':checked')) {
$context.find('.container').removeClass('container').addClass('container-fluid');
}
else {
$context.find('.container-fluid').removeClass('container-fluid').addClass('container');
}
});
});
}
};
/**
* Provide Bootstrap navbar preview.
*/
Drupal.behaviors.bootstrapNavbarPreview = {
attach: function (context) {
var $context = $(context);
var $preview = $context.find('#edit-navbar');
$(once('navbar', '#edit-navbar', context)).each(function () {
var $body = $context.find('body');
var $navbar = $context.find('#navbar.navbar');
$preview.find('select[name="navbar_position"]').bind('change', function () {
var $position = $(this).find(':selected').val();
$navbar.removeClass('navbar-fixed-bottom navbar-fixed-top navbar-static-top container');
if ($position.length) {
$navbar.addClass('navbar-'+ $position);
}
else {
$navbar.addClass('container');
}
// Apply appropriate classes to body.
$body.removeClass('navbar-is-fixed-top navbar-is-fixed-bottom navbar-is-static-top');
switch ($position) {
case 'fixed-top':
$body.addClass('navbar-is-fixed-top');
break;
case 'fixed-bottom':
$body.addClass('navbar-is-fixed-bottom');
break;
case 'static-top':
$body.addClass('navbar-is-static-top');
break;
}
});
$preview.find('input[name="navbar_inverse"]').bind('change', function () {
$navbar.toggleClass('navbar-inverse navbar-default');
});
});
}
};
})(jQuery, Drupal, Drupal.bootstrap, once);

View File

@@ -0,0 +1,180 @@
/**
* @file
* Theme hooks for the Drupal Bootstrap base theme.
*/
(function ($, Drupal, Bootstrap, Attributes) {
/**
* Fallback for theming an icon if the Icon API module is not installed.
*/
if (!Drupal.icon) Drupal.icon = { bundles: {} };
if (!Drupal.theme.icon || Drupal.theme.prototype.icon) {
$.extend(Drupal.theme, /** @lends Drupal.theme */ {
/**
* Renders an icon.
*
* @param {string} bundle
* The bundle which the icon belongs to.
* @param {string} icon
* The name of the icon to render.
* @param {object|Attributes} [attributes]
* An object of attributes to also apply to the icon.
*
* @returns {string}
*/
icon: function (bundle, icon, attributes) {
if (!Drupal.icon.bundles[bundle]) return '';
attributes = Attributes.create(attributes).addClass('icon').set('aria-hidden', 'true');
icon = Drupal.icon.bundles[bundle](icon, attributes);
return '<span' + attributes + '></span>';
}
});
}
/**
* Callback for modifying an icon in the "bootstrap" icon bundle.
*
* @param {string} icon
* The icon being rendered.
* @param {Attributes} attributes
* Attributes object for the icon.
*/
Drupal.icon.bundles.bootstrap = function (icon, attributes) {
attributes.addClass(['glyphicon', 'glyphicon-' + icon]);
};
/**
* Add necessary theming hooks.
*/
$.extend(Drupal.theme, /** @lends Drupal.theme */ {
/**
* Renders a Bootstrap AJAX glyphicon throbber.
*
* @returns {string}
*/
ajaxThrobber: function () {
return Drupal.theme('bootstrapIcon', 'refresh', {'class': ['ajax-throbber', 'glyphicon-spin'] });
},
/**
* Renders a button element.
*
* @param {object|Attributes} attributes
* An object of attributes to apply to the button. If it contains one of:
* - value: The label of the button.
* - context: The context type of Bootstrap button, can be one of:
* - default
* - primary
* - success
* - info
* - warning
* - danger
* - link
*
* @returns {string}
*/
button: function (attributes) {
attributes = Attributes.create(attributes).addClass('btn');
var context = attributes.get('context', 'default');
var label = attributes.get('value', '');
attributes.remove('context').remove('value');
if (!attributes.hasClass(['btn-default', 'btn-primary', 'btn-success', 'btn-info', 'btn-warning', 'btn-danger', 'btn-link'])) {
attributes.addClass('btn-' + Bootstrap.checkPlain(context));
}
// Attempt to, intelligently, provide a default button "type".
if (!attributes.exists('type')) {
attributes.set('type', attributes.hasClass('form-submit') ? 'submit' : 'button');
}
return '<button' + attributes + '>' + label + '</button>';
},
/**
* Alias for "button" theme hook.
*
* @param {object|Attributes} attributes
* An object of attributes to apply to the button.
*
* @see Drupal.theme.button()
*
* @returns {string}
*/
btn: function (attributes) {
return Drupal.theme('button', attributes);
},
/**
* Renders a button block element.
*
* @param {object|Attributes} attributes
* An object of attributes to apply to the button.
*
* @see Drupal.theme.button()
*
* @returns {string}
*/
'btn-block': function (attributes) {
return Drupal.theme('button', Attributes.create(attributes).addClass('btn-block'));
},
/**
* Renders a large button element.
*
* @param {object|Attributes} attributes
* An object of attributes to apply to the button.
*
* @see Drupal.theme.button()
*
* @returns {string}
*/
'btn-lg': function (attributes) {
return Drupal.theme('button', Attributes.create(attributes).addClass('btn-lg'));
},
/**
* Renders a small button element.
*
* @param {object|Attributes} attributes
* An object of attributes to apply to the button.
*
* @see Drupal.theme.button()
*
* @returns {string}
*/
'btn-sm': function (attributes) {
return Drupal.theme('button', Attributes.create(attributes).addClass('btn-sm'));
},
/**
* Renders an extra small button element.
*
* @param {object|Attributes} attributes
* An object of attributes to apply to the button.
*
* @see Drupal.theme.button()
*
* @returns {string}
*/
'btn-xs': function (attributes) {
return Drupal.theme('button', Attributes.create(attributes).addClass('btn-xs'));
},
/**
* Renders a glyphicon.
*
* @param {string} name
* The name of the glyphicon.
* @param {object|Attributes} [attributes]
* An object of attributes to apply to the icon.
*
* @returns {string}
*/
bootstrapIcon: function (name, attributes) {
return Drupal.theme('icon', 'bootstrap', name, attributes);
}
});
})(window.jQuery, window.Drupal, window.Drupal.bootstrap, window.Attributes);

View File

@@ -0,0 +1,59 @@
/**
* @file
* Bootstrap Tooltips.
*/
var Drupal = Drupal || {};
(function ($, Drupal, Bootstrap) {
"use strict";
/**
* Extend the Bootstrap Tooltip plugin constructor class.
*/
Bootstrap.extendPlugin('tooltip', function (settings) {
return {
DEFAULTS: {
animation: !!settings.tooltip_animation,
enabled: settings.tooltip_enabled,
html: !!settings.tooltip_html,
placement: settings.tooltip_placement,
selector: settings.tooltip_selector,
trigger: settings.tooltip_trigger,
delay: parseInt(settings.tooltip_delay, 10),
container: settings.tooltip_container
}
};
});
/**
* Bootstrap Tooltips.
*
* @todo This should really be properly delegated if selector option is set.
*/
Drupal.behaviors.bootstrapTooltips = {
attach: function (context) {
// Immediately return if tooltips are not available.
if (!$.fn.tooltip || !$.fn.tooltip.Constructor.DEFAULTS.enabled) {
return;
}
var elements = $(context).find('[data-toggle="tooltip"]').toArray();
for (var i = 0; i < elements.length; i++) {
var $element = $(elements[i]);
var options = $.extend({}, $.fn.tooltip.Constructor.DEFAULTS, $element.data());
$element.tooltip(options);
}
},
detach: function (context) {
// Immediately return if tooltips are not available.
if (!$.fn.tooltip || !$.fn.tooltip.Constructor.DEFAULTS.enabled) {
return;
}
// Destroy all tooltips.
$(context).find('[data-toggle="tooltip"]').tooltip('destroy');
}
};
})(window.jQuery, window.Drupal, window.Drupal.bootstrap);