theme:
css/node.preview.css: false
-# Information added by Drupal.org packaging script on 2018-03-11
-version: '8.x-3.11'
+# Information added by Drupal.org packaging script on 2018-11-12
+version: '8.x-3.14'
core: '8.x'
project: 'bootstrap'
-datestamp: 1520792888
+datestamp: 1542006206
identifier = identifier.toLowerCase();
if (filter['__'] === void 0) {
- identifier = identifier.replace('__', '#DOUBLE_UNDERSCORE#', identifier);
+ identifier = identifier.replace('__', '#DOUBLE_UNDERSCORE#');
}
- identifier = identifier.replace(Object.keys(filter), Object.keys(filter).map(function(key) { return filter[key]; }), identifier);
+ 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 = 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])|^(--)/'], ['_', '__'], identifier);
+ identifier = identifier.replace(['/^[0-9]/', '/^(-[0-9])|^(--)/'], ['_', '__']);
return identifier;
};
/**
* Simulates a native event on an element in the browser.
*
- * Note: This is a pretty complete modern implementation. If things are quite
- * working 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 it.
+ * 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} element
- * A DOM element to dispatch event on.
+ * @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} type
* The type of event to simulate.
* @param {Object} [options]
* 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.
+ if (element instanceof $) {
+ var ret = true;
+ 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;
+ return true;
}
+
var event;
var ctor;
for (var name in this.eventMap) {
}
if (typeof window[ctor] === 'function') {
event = new window[ctor](type, opts);
- element.dispatchEvent(event);
+ return element.dispatchEvent(event);
}
else if (document.createEvent) {
event = document.createEvent(ctor);
event.initEvent(type, opts.bubbles, opts.cancelable);
- element.dispatchEvent(event);
+ return element.dispatchEvent(event);
}
else if (typeof element.fireEvent === 'function') {
event = $.extend(document.createEventObject(), opts);
- element.fireEvent('on' + type, event);
+ return element.fireEvent('on' + type, event);
}
else if (typeof element[type]) {
element[type]();
+ return true;
+ }
+ };
+
+ /**
+ * 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]*$/g, '');
};
/**
* 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('Unsupported Drupal Bootstrap Modal @type: @name -> @value', {
- '@type': type,
- '@name': name,
- '@value': typeof value === 'object' ? JSON.stringify(value) : value
- }));
+ console.warn(Drupal.formatString(message, args));
}
};
* @file
* dialog.ajax.js
*/
-(function ($, Drupal) {
+(function ($, Drupal, Bootstrap) {
- var dialogAjaxCurrentButton;
- var dialogAjaxOriginalButton;
+ Drupal.behaviors.dialog.ajaxCurrentButton = null;
+ Drupal.behaviors.dialog.ajaxOriginalButton = null;
+
+ /**
+ * 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.ajaxCurrentButton.prop('disabled', this.ajaxOriginalButton.prop('disabled'));
+ }
+ if (reset) {
+ this.ajaxCurrentButton = null;
+ this.ajaxOriginalButton = null;
+ }
+ };
$(document)
.ajaxSend(function () {
- if (dialogAjaxCurrentButton && dialogAjaxOriginalButton) {
- dialogAjaxCurrentButton.html(dialogAjaxOriginalButton.html());
- dialogAjaxCurrentButton.prop('disabled', dialogAjaxOriginalButton.prop('disabled'));
- }
+ Drupal.behaviors.dialog.ajaxUpdateButtons();
})
.ajaxComplete(function () {
- if (dialogAjaxCurrentButton && dialogAjaxOriginalButton) {
- dialogAjaxCurrentButton.html(dialogAjaxOriginalButton.html());
- dialogAjaxCurrentButton.prop('disabled', dialogAjaxOriginalButton.prop('disabled'));
- }
- dialogAjaxCurrentButton = null;
- dialogAjaxOriginalButton = null;
+ Drupal.behaviors.dialog.ajaxUpdateButtons(true);
})
;
* {@inheritdoc}
*/
Drupal.behaviors.dialog.prepareDialogButtons = function prepareDialogButtons($dialog) {
+ var _that = this;
var buttons = [];
var $buttons = $dialog.find('.form-actions').find('button, input[type=submit], .form-actions a.button');
$buttons.each(function () {
- var $originalButton = $(this).css({
- display: 'block',
- width: 0,
- height: 0,
- padding: 0,
- border: 0,
- overflow: 'hidden'
- });
- var classes = $originalButton.attr('class').replace('use-ajax-submit', '');
+ 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({
- text: $originalButton.html() || $originalButton.attr('value'),
- class: classes,
+ // 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),
+ class: $originalButton.attr('class').replace('use-ajax-submit', ''),
click: function click(e) {
- dialogAjaxCurrentButton = $(e.target);
- dialogAjaxOriginalButton = $originalButton;
- if ($originalButton.is('a')) {
- $originalButton[0].click();
- }
- else {
- $originalButton.trigger('mousedown').trigger('mouseup').trigger('click');
- e.preventDefault();
- }
+ e.preventDefault();
+ e.stopPropagation();
+ _that.ajaxCurrentButton = $(e.target);
+ _that.ajaxOriginalButton = $originalButton;
+ Bootstrap.simulate($originalButton, 'click');
+ },
+ create: function () {
+ _that.ajaxCurrentButton = $(this);
+ _that.ajaxOriginalButton = $originalButton;
+ _that.ajaxUpdateButtons(true);
}
});
});
+
return buttons;
};
(function ($, window, Drupal, drupalSettings) {
"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
return;
}
+ /**
+ * Binds a listener to handle fragment link clicks and URL hash changes.
+ */
+ $('body').once('vertical-tabs-fragments').on('formFragmentLinkClickOrHashChange.verticalTabs', handleFragmentLinkClickOrHashChange);
+
$(context).find('[data-vertical-tabs-panes]').once('vertical-tabs').each(function () {
var $this = $(this).addClass('tab-content vertical-tabs-panes');
*/
createButtons: function () {
this.$footer.find('.modal-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);
+ $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']();
},
/**
this.$footer = $(Drupal.theme('bootstrapModalFooter', {}, true)).insertAfter(this.$dialogBody);
}
- // Create buttons.
- this.createButtons();
-
- // Hide the footer if there are no children.
- if (!this.$footer.children()[0]) {
- this.$footer.hide();
- }
-
// Now call the parent init method.
this.super();
if (value) {
dialogOptions[prop] = 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 (options.autoResize && (prop === 'height' || prop === 'maxHeight')) {
+ styles.display = 'flex';
+ styles.flexDirection = 'column';
+ this.$dialogBody.css('overflow', 'scroll');
+ }
}
}
}
delete options.dialogClass;
}
+ // Add jQuery UI classes to elements in case developers target them
+ // in callbacks.
+ for (var k in classesMap) {
+ this.$element.find('.' + classesMap[k]).addClass(k);
+ }
// Bind events.
var events = [
this.$element.on('dialog' + event, options[event]);
}
+ // Support title attribute on the modal.
+ var title;
+ if ((options.title === null || options.title === void 0) && (title = this.$element.attr('title'))) {
+ options.title = title;
+ }
+
// Handle the reset of the options.
for (var name in options) {
if (!options.hasOwnProperty(name) || options[name] === void 0) continue;
// Merge in the cloned mapped options.
$.extend(true, this.options, this.mapDialogOptions(clone.options));
+
+ // Update buttons.
+ this.createButtons();
},
position: function(position) {
var icon = '';
var iconPosition = button.iconPosition || 'beginning';
iconPosition = (iconPosition === 'end' && !rtl) || (iconPosition === 'beginning' && rtl) ? 'after' : 'before';
- if (button.icon) {
+
+ // 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>';
}
- // Value.
- var value = button.text;
+ // 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.
if (button['class']) {
attributes.addClass(button['class']);
}
+ if (button.primary) {
+ attributes.addClass('btn-primary');
+ }
return Drupal.theme('button', 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');
+ };
+
+ $document.on('shown.bs.modal', function (e) {
+ var $modal = $(e.target);
+ var modal = $modal.data('bs.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)
+ }
+ }
+ }
+ });
+
/**
* Only process this once.
*/
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,
show: !!settings.modal_show,
size: settings.modal_size
var data = $this.data('bs.modal');
var initialize = false;
+ // Immediately return if there's no instance to invoke a valid method.
+ if (!data && method && method !== 'open') {
+ return;
+ }
+
options = $.extend({}, Modal.DEFAULTS, data && data.options, $this.data(), options);
if (!data) {
// When initializing the Bootstrap Modal, only pass the "supported"
// 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)
+ $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 = $this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, '')); // strip for ie7
+ var $target = $document.find(target);
var option = $target.data('bs.modal') ? 'toggle' : $.extend({ remote: !/#/.test(href) && href }, $target.data(), $this.data());
if ($this.is('a')) e.preventDefault();
(function ($, Drupal, Bootstrap) {
"use strict";
+ var $document = $(document);
+
/**
* Extend the Bootstrap Popover plugin constructor class.
*/
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,
- triggerAutoclose: !!settings.popover_trigger_autoclose,
title: settings.popover_title,
content: settings.popover_content,
delay: parseInt(settings.popover_delay, 10),
* @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;
}
- // Popover autoclose.
- if ($.fn.popover.Constructor.DEFAULTS.triggerAutoclose) {
- var $currentPopover = null;
- $(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 ($currentPopover && !$currentPopover.is($trigger)) {
- $currentPopover.popover('hide');
- }
- $currentPopover = $trigger;
- }
- })
- .on('click', function (e) {
- var $target = $(e.target);
- var popover = $target.is('[data-toggle=popover]') && $target.data('bs.popover');
- if ($currentPopover && !$target.is('[data-toggle=popover]') && !$target.closest('.popover.in')[0]) {
- $currentPopover.popover('hide');
- $currentPopover = null;
+ 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;
+ }
+ })
+ .on('focus.bs.popover', ':focusable', 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++) {
}
// Retrieve content from a target element.
- var $target = $(options.target || $element.is('a[href^="#"]') && $element.attr('href')).clone();
+ 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']() || '';
.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 || {}).options);
+ if (options[name] !== void 0) {
+ return options[name];
+ }
+ return defaultValue !== void 0 ? defaultValue : void 0;
}
};
return $cache->get($this->hook);
}
- // Uppercase each theme hook suggestion to be used in the method name.
- $hook_suggestions = $this->hookSuggestions;
- foreach ($hook_suggestions as $key => $suggestion) {
- $hook_suggestions[$key] = Unicode::ucfirst($suggestion);
- }
-
+ // Convert snake_cased hook suggestions into lowerCamelCase alter methods.
$methods = [];
+ $hook_suggestions = array_map('\Drupal\Component\Utility\Unicode::ucfirst', $this->hookSuggestions);
while ($hook_suggestions) {
- $method = 'alter' . implode('', $hook_suggestions);
- if (method_exists($this, $method)) {
- $methods[] = $method;
+ // In order to provide backwards compatibility with sub-themes that used
+ // the previous malformed method names, both of the method names need to
+ // be checked.
+ // @see https://www.drupal.org/project/bootstrap/issues/3008004
+ // @todo Only use the last method name and remove array in 8.x-4.x.
+ $methodNames = [
+ 'alter' . implode('', $hook_suggestions),
+ 'alter' . implode('', array_map('\Drupal\Component\Utility\Unicode::ucfirst', explode('_', implode('', $hook_suggestions)))),
+ ];
+ foreach (array_unique($methodNames) as $method) {
+ if (method_exists($this, $method)) {
+ $methods[] = $method;
+ }
}
array_pop($hook_suggestions);
}
namespace Drupal\bootstrap\Plugin\Form;
use Drupal\bootstrap\Bootstrap;
+use Drupal\bootstrap\Plugin\Setting\DeprecatedSettingInterface;
use Drupal\bootstrap\Utility\Element;
use Drupal\Core\Form\FormStateInterface;
// Iterate over all setting plugins and manually save them since core's
// process is severely limiting and somewhat broken.
foreach ($theme->getSettingPlugin() as $name => $setting) {
+ // Skip saving deprecated settings.
+ if ($setting instanceof DeprecatedSettingInterface) {
+ $form_state->unsetValue($name);
+ continue;
+ }
+
// Allow the setting to participate in the form submission process.
// Must call the "submitForm" method in case any setting actually uses it.
// It should, in turn, invoke "submitFormElement", if the setting that
$wrapper_attributes['hreflang'] = $element['language']->getId();
// Ensure the Url language is set on the object itself.
- // @todo Possibly a core bug?
- if (empty($element['url']->getOption('language'))) {
- $element['url']->setOption('language', $element['language']);
- }
+ // @todo Revisit, possibly a core bug?
+ // @see https://www.drupal.org/project/bootstrap/issues/2868100
+ $element['url']->setOption('language', $element['language']);
}
// Preserve query parameters (if any)
--- /dev/null
+<?php
+
+namespace Drupal\bootstrap\Plugin\Preprocess;
+
+use Drupal\bootstrap\Utility\Attributes;
+use Drupal\bootstrap\Utility\Variables;
+use Drupal\Core\Template\Attribute;
+use Drupal\Core\Url;
+
+/**
+ * Pre-processes variables for the "menu" theme hook.
+ *
+ * @ingroup plugins_preprocess
+ *
+ * @BootstrapPreprocess("menu")
+ */
+class Menu extends PreprocessBase implements PreprocessInterface {
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function preprocessVariables(Variables $variables) {
+ foreach ($variables->items as &$item) {
+ $wrapperAttributes = new Attributes();
+ $linkAttributes = new Attributes();
+ if ($item['attributes'] instanceof Attribute || $item['attributes'] instanceof Attributes) {
+ $wrapperAttributes->setAttributes($item['attributes']->getIterator()->getArrayCopy());
+ }
+ if ($item['url'] instanceof Url) {
+ $wrapperAttributes->setAttributes($item['url']->getOption('wrapper_attributes') ?: []);
+ $wrapperAttributes->setAttributes($item['url']->getOption('container_attributes') ?: []);
+ $linkAttributes->setAttributes($item['url']->getOption('attributes') ?: []);
+ }
+
+ // Unfortunately, in newer core/Twig versions, only certain classes are
+ // allowed to be invoked due to stricter sandboxing policies. To get
+ // around this, just rewrap attributes in core's native Attribute class.
+ $item['attributes'] = new Attribute($wrapperAttributes->getArrayCopy());
+ $item['link_attributes'] = new Attribute($linkAttributes->getArrayCopy());
+ }
+ }
+
+}
* {@inheritdoc}
*/
public function processApi(array $json, array &$definition) {
- $definition['description'] = t('<p style="background:#EB4C36"><a href=":jsdelivr" target="_blank"><img src="//www.jsdelivr.com/img/logo.png" alt="jsDelivr Logo"/></a></p><p><a href=":jsdelivr" target="_blank">jsDelivr</a> is a free multi-CDN infrastructure that uses <a href=":maxcdn" target="_blank">MaxCDN</a>, <a href=":cloudflare" target="_blank">Cloudflare</a> and many others to combine their powers for the good of the open source community... <a href=":jsdelivr_about" target="_blank">read more</a></p>', [
+ $definition['description'] = t('<p><a href=":jsdelivr" target="_blank">jsDelivr</a> is a free multi-CDN infrastructure that uses <a href=":maxcdn" target="_blank">MaxCDN</a>, <a href=":cloudflare" target="_blank">Cloudflare</a> and many others to combine their powers for the good of the open source community... <a href=":jsdelivr_about" target="_blank">read more</a></p>', [
':jsdelivr' => 'https://www.jsdelivr.com',
':jsdelivr_about' => 'https://www.jsdelivr.com/about',
':maxcdn' => 'https://www.maxcdn.com',
--- /dev/null
+<?php
+
+namespace Drupal\bootstrap\Plugin\Setting;
+
+/**
+ * Interface DeprecatedSettingInterface.
+ */
+interface DeprecatedSettingInterface {
+}
--- /dev/null
+<?php
+
+namespace Drupal\bootstrap\Plugin\Setting\JavaScript\Modals;
+
+use Drupal\bootstrap\Plugin\Setting\SettingBase;
+use Drupal\bootstrap\Utility\Element;
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * The "modal_focus_input" theme setting.
+ *
+ * @ingroup plugins_setting
+ *
+ * @BootstrapSetting(
+ * id = "modal_focus_input",
+ * type = "checkbox",
+ * title = @Translation("focusInput"),
+ * description = @Translation("Enabling this focuses on the first available and visible input found in the modal after it's opened."),
+ * defaultValue = 1,
+ * groups = {
+ * "javascript" = @Translation("JavaScript"),
+ * "modals" = @Translation("Modals"),
+ * "options" = @Translation("Options"),
+ * },
+ * )
+ */
+class ModalFocusInput extends SettingBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function alterFormElement(Element $form, FormStateInterface $form_state, $form_id = NULL) {
+ parent::alterFormElement($form, $form_state, $form_id);
+ $setting = $this->getSettingElement($form, $form_state);
+ $setting->setProperty('states', [
+ 'visible' => [
+ ':input[name="modal_enabled"]' => ['checked' => TRUE],
+ ],
+ ]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function drupalSettings() {
+ return !!$this->theme->getSetting('modal_enabled');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCacheTags() {
+ return ['rendered', 'library_info'];
+ }
+
+}
--- /dev/null
+<?php
+
+namespace Drupal\bootstrap\Plugin\Setting\JavaScript\Modals;
+
+use Drupal\bootstrap\Plugin\Setting\SettingBase;
+use Drupal\bootstrap\Utility\Element;
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * The "modal_select_text" theme setting.
+ *
+ * @ingroup plugins_setting
+ *
+ * @BootstrapSetting(
+ * id = "modal_select_text",
+ * type = "checkbox",
+ * title = @Translation("selectText"),
+ * description = @Translation("Enabling this selects the text of the first available and visible input found after it has been focused."),
+ * defaultValue = 1,
+ * groups = {
+ * "javascript" = @Translation("JavaScript"),
+ * "modals" = @Translation("Modals"),
+ * "options" = @Translation("Options"),
+ * },
+ * )
+ */
+class ModalSelectText extends SettingBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function alterFormElement(Element $form, FormStateInterface $form_state, $form_id = NULL) {
+ parent::alterFormElement($form, $form_state, $form_id);
+ $setting = $this->getSettingElement($form, $form_state);
+ $setting->setProperty('states', [
+ 'visible' => [
+ ':input[name="modal_enabled"]' => ['checked' => TRUE],
+ ':input[name="modal_focus_input"]' => ['checked' => TRUE],
+ ],
+ ]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function drupalSettings() {
+ return !!$this->theme->getSetting('modal_enabled');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCacheTags() {
+ return ['rendered', 'library_info'];
+ }
+
+}
--- /dev/null
+<?php
+
+namespace Drupal\bootstrap\Plugin\Setting\JavaScript\Popovers;
+
+use Drupal\bootstrap\Plugin\Setting\SettingBase;
+
+/**
+ * The "popover_trigger_autoclose" theme setting.
+ *
+ * @ingroup plugins_setting
+ *
+ * @BootstrapSetting(
+ * id = "popover_auto_close",
+ * type = "checkbox",
+ * title = @Translation("autoClose"),
+ * description = @Translation("If enabled, the active popover will automatically close when it loses focus, when a click occurs anywhere in the DOM (outside the popover), the escape key (ESC) is pressed or when another popover is opened."),
+ * defaultValue = 1,
+ * groups = {
+ * "javascript" = @Translation("JavaScript"),
+ * "popovers" = @Translation("Popovers"),
+ * "options" = @Translation("Options"),
+ * },
+ * )
+ */
+class PopoverAutoClose extends SettingBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function drupalSettings() {
+ return !!$this->theme->getSetting('popover_enabled');
+ }
+
+}
namespace Drupal\bootstrap\Plugin\Setting\JavaScript\Popovers;
-use Drupal\bootstrap\Plugin\Setting\SettingBase;
+use Drupal\bootstrap\Plugin\Setting\DeprecatedSettingInterface;
/**
* The "popover_trigger_autoclose" theme setting.
* "options" = @Translation("Options"),
* },
* )
+ *
+ * @deprecated Since 8.x-3.14. Will be removed in a future release.
+ *
+ * @see \Drupal\bootstrap\Plugin\Setting\JavaScript\Popovers\PopoverAutoClose
*/
-class PopoverTriggerAutoclose extends SettingBase {
-
- /**
- * {@inheritdoc}
- */
- public function drupalSettings() {
- return !!$this->theme->getSetting('popover_enabled');
- }
-
+class PopoverTriggerAutoclose extends PopoverAutoClose implements DeprecatedSettingInterface {
}
$group->$plugin_id->setProperty('description', $description);
}
}
+
+ // Hide the setting if is been deprecated.
+ if ($this instanceof DeprecatedSettingInterface) {
+ $group->$plugin_id->access(FALSE);
+ }
+
return $group->$plugin_id;
}
parent::__unset($name);
}
+ /**
+ * Sets the #access property on an element.
+ *
+ * @param bool|\Drupal\Core\Access\AccessResultInterface $access
+ * The value to assign to #access.
+ *
+ * @return static
+ */
+ public function access($access = NULL) {
+ return $this->setProperty('access', $access);
+ }
+
/**
* Appends a property with a value.
*
{% if items %}
<ul{{ attributes.addClass(menu_level == 0 ? classes : 'dropdown-menu') }}>
{% for item in items %}
+ {%
+ set item_classes = item.url.getOption('container_attributes').class | split(" ")
+ %}
{%
set item_classes = [
- item.is_expanded and item.below ? 'expanded',
- item.is_expanded and menu_level == 0 and item.below ? 'dropdown',
- item.in_active_trail ? 'active',
+ item.is_expanded and item.below ? 'expanded dropdown',
+ item.in_active_trail ? 'active-trail',
+ loop.first ? 'first',
+ loop.last ? 'last',
]
%}
+ <li{{ item.attributes.addClass(item_classes) }}>
{% if menu_level == 0 and item.is_expanded and item.below %}
- <li{{ item.attributes.addClass(item_classes) }}>
- <a href="{{ item.url }}" class="dropdown-toggle" data-toggle="dropdown">{{ item.title }} <span class="caret"></span></a>
+ <a{{ item.link_attributes.addClass(['dropdown-toggle', item.in_active_trail ? 'active-trail']) | without('data-toggle') }} href="{{ item.url }}" data-toggle="dropdown">{{ item.title }}<span class="caret"></span></a>
{% else %}
- <li{{ item.attributes.addClass(item_classes) }}>
- {{ link(item.title, item.url) }}
+ <a{{ item.link_attributes.addClass(item.in_active_trail ? 'active-trail') }} href="{{ item.url }}">{{ item.title }}</a>
{% endif %}
{% if item.below %}
{{ _self.menu_links(item.below, attributes.removeClass(classes), menu_level + 1, classes) }}
{# Print first item if we are not on the first page. #}
{% if items.first %}
<li class="pager__item pager__item--first">
- <a href="{{ items.first.href }}" title="{{ 'Go to first page'|t }}" rel="prev"{{ items.first.attributes }}>
+ <a href="{{ items.first.href }}" title="{{ 'Go to first page'|t }}" rel="first"{{ items.first.attributes }}>
<span class="visually-hidden">{{ 'First page'|t }}</span>
<span aria-hidden="true">{{ items.first.text|default('first'|t) }}</span>
</a>