Updated the Bootstrap theme.
authorJeff Veit <jeff.veit@gmail.com>
Tue, 13 Nov 2018 18:25:05 +0000 (18:25 +0000)
committerJeff Veit <jeff.veit@gmail.com>
Tue, 13 Nov 2018 18:25:05 +0000 (18:25 +0000)
31 files changed:
web/themes/contrib/bootstrap/bootstrap.info.yml
web/themes/contrib/bootstrap/js/attributes.js
web/themes/contrib/bootstrap/js/drupal.bootstrap.js
web/themes/contrib/bootstrap/js/misc/dialog.ajax.js
web/themes/contrib/bootstrap/js/misc/vertical-tabs.js
web/themes/contrib/bootstrap/js/modal.jquery.ui.bridge.js
web/themes/contrib/bootstrap/js/modal.js
web/themes/contrib/bootstrap/js/popover.js
web/themes/contrib/bootstrap/src/Plugin/Alter/ThemeSuggestions.php
web/themes/contrib/bootstrap/src/Plugin/Form/SystemThemeSettings.php
web/themes/contrib/bootstrap/src/Plugin/Preprocess/BootstrapDropdown.php
web/themes/contrib/bootstrap/src/Plugin/Preprocess/Menu.php [new file with mode: 0644]
web/themes/contrib/bootstrap/src/Plugin/Provider/JsDelivr.php
web/themes/contrib/bootstrap/src/Plugin/Setting/DeprecatedSettingInterface.php [new file with mode: 0644]
web/themes/contrib/bootstrap/src/Plugin/Setting/JavaScript/Modals/ModalFocusInput.php [new file with mode: 0644]
web/themes/contrib/bootstrap/src/Plugin/Setting/JavaScript/Modals/ModalSelectText.php [new file with mode: 0644]
web/themes/contrib/bootstrap/src/Plugin/Setting/JavaScript/Popovers/PopoverAutoClose.php [new file with mode: 0644]
web/themes/contrib/bootstrap/src/Plugin/Setting/JavaScript/Popovers/PopoverTriggerAutoclose.php
web/themes/contrib/bootstrap/src/Plugin/Setting/SettingBase.php
web/themes/contrib/bootstrap/src/Utility/Element.php
web/themes/contrib/bootstrap/starterkits/sass/README.md [changed mode: 0755->0644]
web/themes/contrib/bootstrap/starterkits/sass/THEMENAME.libraries.yml [changed mode: 0755->0644]
web/themes/contrib/bootstrap/starterkits/sass/THEMENAME.starterkit.yml [changed mode: 0755->0644]
web/themes/contrib/bootstrap/starterkits/sass/THEMENAME.theme [changed mode: 0755->0644]
web/themes/contrib/bootstrap/starterkits/sass/config/install/THEMENAME.settings.yml [changed mode: 0755->0644]
web/themes/contrib/bootstrap/starterkits/sass/config/schema/THEMENAME.schema.yml [changed mode: 0755->0644]
web/themes/contrib/bootstrap/starterkits/sass/favicon.ico [changed mode: 0755->0644]
web/themes/contrib/bootstrap/starterkits/sass/logo.svg [changed mode: 0755->0644]
web/themes/contrib/bootstrap/starterkits/sass/templates/README.md [changed mode: 0755->0644]
web/themes/contrib/bootstrap/templates/menu/menu.html.twig
web/themes/contrib/bootstrap/templates/system/pager.html.twig

index d613fe3bf5ba05d3ea29e44d48646ec153db5b92..b464980d9387c05b803d7ab9c6dc7eb6c4ef333c 100644 (file)
@@ -73,8 +73,8 @@ libraries-override:
       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
index e0c0c441fab5536564430f480da3cfd59c53abe0..ced3356b106b5dfe6237f1d7aa9a19bd1fcd8080 100644 (file)
     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;
   };
index 07af6a35c9f066384ec1e7d7cd3481004601958e..573afd37fdb857d974d23dd684e623c515190cd6 100644 (file)
   /**
    * 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));
     }
   };
 
index f5c853cb41b383ad4cbdcb9cb5ca3efc18f2626f..7bfdcd33a63032d28f768d219ab6883069432db8 100644 (file)
@@ -2,25 +2,34 @@
  * @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;
   };
 
index 0085c4db7f95a117c69aaeb38edb91ec4b0c83ab..83cfec446da94836d42af3c0dcf6c3b8ffb9b477 100644 (file)
@@ -6,6 +6,23 @@
 (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');
 
index 88d5ce480be5298a7c60f4c249f1f394202635f2..cd9509251baa9c321cd8951d900ff510e50ad44d 100644 (file)
            */
           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);
       }
index 5bcce17846499037bd7ecb86c3403f81f160872d..009d630491c31969ac7a3b2b0ab4053d130014c8 100644 (file)
@@ -5,6 +5,49 @@
 (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.
    */
@@ -40,6 +83,8 @@
       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();
index 198cd3eb9522bfafb1ad875f18e6446843394827..1a1ffa42a9decb7e07138ee32b0815e084897493 100644 (file)
@@ -8,6 +8,8 @@ var Drupal = Drupal || {};
 (function ($, Drupal, Bootstrap) {
   "use strict";
 
+  var $document = $(document);
+
   /**
    * Extend the Bootstrap Popover plugin constructor class.
    */
@@ -15,12 +17,12 @@ var Drupal = Drupal || {};
     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),
@@ -35,38 +37,49 @@ var Drupal = Drupal || {};
    * @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++) {
@@ -82,7 +95,8 @@ var Drupal = Drupal || {};
         }
 
         // 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']() || '';
@@ -117,6 +131,14 @@ var Drupal = Drupal || {};
         .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;
     }
   };
 
index bf20a2307cdc977abb13a21c0c7377612413de30..db96e669956e99f7b4c84d442271dd9c3801aee4 100644 (file)
@@ -250,17 +250,23 @@ class ThemeSuggestions extends PluginBase implements AlterInterface {
       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);
     }
index 35bbcb15d3b563801db9e62388aa459fc522147c..298108856782f956967ec8843f9686f9a367e4a7 100644 (file)
@@ -3,6 +3,7 @@
 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;
 
@@ -122,6 +123,12 @@ class SystemThemeSettings extends FormBase implements FormInterface {
     // 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
index 8c0a6de217187346bec136c9df659a0c6ea53942..b6eb9094fb7505d2269943bdc48c78be142196fc 100644 (file)
@@ -77,10 +77,9 @@ class BootstrapDropdown extends PreprocessBase implements PreprocessInterface {
             $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)
diff --git a/web/themes/contrib/bootstrap/src/Plugin/Preprocess/Menu.php b/web/themes/contrib/bootstrap/src/Plugin/Preprocess/Menu.php
new file mode 100644 (file)
index 0000000..9af5dc6
--- /dev/null
@@ -0,0 +1,43 @@
+<?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());
+    }
+  }
+
+}
index 950a9d3e6609a42f35898027a381b5c34fc66b3f..2c0e7a5e3bb073c01e2490f151cdbd1a95d5011a 100644 (file)
@@ -94,7 +94,7 @@ class JsDelivr extends ProviderBase {
    * {@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',
diff --git a/web/themes/contrib/bootstrap/src/Plugin/Setting/DeprecatedSettingInterface.php b/web/themes/contrib/bootstrap/src/Plugin/Setting/DeprecatedSettingInterface.php
new file mode 100644 (file)
index 0000000..6c0ae9a
--- /dev/null
@@ -0,0 +1,9 @@
+<?php
+
+namespace Drupal\bootstrap\Plugin\Setting;
+
+/**
+ * Interface DeprecatedSettingInterface.
+ */
+interface DeprecatedSettingInterface {
+}
diff --git a/web/themes/contrib/bootstrap/src/Plugin/Setting/JavaScript/Modals/ModalFocusInput.php b/web/themes/contrib/bootstrap/src/Plugin/Setting/JavaScript/Modals/ModalFocusInput.php
new file mode 100644 (file)
index 0000000..27a9c1a
--- /dev/null
@@ -0,0 +1,56 @@
+<?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'];
+  }
+
+}
diff --git a/web/themes/contrib/bootstrap/src/Plugin/Setting/JavaScript/Modals/ModalSelectText.php b/web/themes/contrib/bootstrap/src/Plugin/Setting/JavaScript/Modals/ModalSelectText.php
new file mode 100644 (file)
index 0000000..9a8cb5f
--- /dev/null
@@ -0,0 +1,57 @@
+<?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'];
+  }
+
+}
diff --git a/web/themes/contrib/bootstrap/src/Plugin/Setting/JavaScript/Popovers/PopoverAutoClose.php b/web/themes/contrib/bootstrap/src/Plugin/Setting/JavaScript/Popovers/PopoverAutoClose.php
new file mode 100644 (file)
index 0000000..fd23b3f
--- /dev/null
@@ -0,0 +1,34 @@
+<?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');
+  }
+
+}
index 027e5c8b3f3187c7f29292f167eab4197b535343..8154772477e53ba54bfe7ed9130cf1ae76969683 100644 (file)
@@ -2,7 +2,7 @@
 
 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.
@@ -21,14 +21,10 @@ use Drupal\bootstrap\Plugin\Setting\SettingBase;
  *     "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 {
 }
index d8f85c0382f131ae4a192b5430fffcda6cae3967..c368451d5bff75c28482d5663a20ece9e8a425bc 100644 (file)
@@ -178,6 +178,12 @@ class SettingBase extends PluginBase implements SettingInterface {
         $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;
   }
 
index f0422265b661885816adb81a45bd28dba554696d..9fad8b887b30227e7cd93cb6059176b3a4beee86 100644 (file)
@@ -134,6 +134,18 @@ class Element extends DrupalAttributes {
     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.
    *
index 1f48ae6638d2939864538f15ac8284b0d246862d..3ded5ec342b8e1af52863d0005e3592cae19bdce 100644 (file)
   {% 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) }}
index e65b599a47d1db9651d5c9ffd2bfb7951c2c0d55..5e5bb287c354a5b7bf3aa2283f1f48bf851e932e 100644 (file)
@@ -39,7 +39,7 @@
       {# 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>