summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMark Carver2017-01-05 19:54:17 (GMT)
committerMark Carver2017-01-05 19:54:17 (GMT)
commit5c1a0cf4a89a267a83cb6781fd9d758a67267cea (patch)
treec4ec94cd04cb9b576fbeb54f87e333655949dd43
parentae8c40966e8568dbc22c59128a6e84291dd44295 (diff)
Issue #2838956 by markcarver: Events bound to original dropdown elements are lostHEAD8.x-3.x
-rw-r--r--js/dropdown.js91
-rw-r--r--js/drupal.bootstrap.js83
-rw-r--r--src/Bootstrap.php2
-rw-r--r--src/Plugin/Alter/ThemeSuggestions.php5
-rw-r--r--src/Plugin/Preprocess/BootstrapDropdown.php100
-rw-r--r--src/Theme.php10
-rw-r--r--src/Utility/Attributes.php12
-rw-r--r--src/Utility/Element.php14
-rw-r--r--templates/bootstrap/bootstrap-dropdown.html.twig2
-rw-r--r--templates/input/input--button--dropdown.html.twig31
10 files changed, 254 insertions, 96 deletions
diff --git a/js/dropdown.js b/js/dropdown.js
index b9f6c56..1902475 100644
--- a/js/dropdown.js
+++ b/js/dropdown.js
@@ -1,28 +1,79 @@
/**
* @file
- * Provides a click handler for buttons in dropdown menus.
+ * Provides an event handler for hidden elements in dropdown menus.
*/
-(function ($) {
- "use strict";
+(function ($, Drupal, Bootstrap) {
+ 'use strict';
- // Delegates links that are really "actions" (buttons) in the dropdown menu
- // to the actual button so it can actually submit the form.
- $(document).on('click', 'a[data-target="dropdown-button"]', function (e) {
- e.preventDefault();
- e.stopPropagation();
- $(this).parent().find('button').trigger('click');
- });
+ /**
+ * The list of supported events to proxy.
+ *
+ * @type {Array}
+ */
+ var events = [
+ // MouseEvent.
+ 'click', 'dblclick', 'mousedown', 'mouseenter', 'mouseleave', 'mouseup', 'mouseover', 'mousemove', 'mouseout',
- // Handle buttons that used to be dropbutton links.
- // @see \Drupal\bootstrap\Plugin\Preprocess\BootstrapDropdown::preprocessLinks
- $(document).on('click', '.btn-group button[data-url], .dropdown button[data-url]', function (e) {
- var url = $(this).data('url');
- if (url) {
- e.preventDefault();
- e.stopPropagation();
- window.location = url;
+ // KeyboardEvent.
+ 'keypress', 'keydown', 'keyup',
+
+ // TouchEvent.
+ 'touchstart', 'touchend', 'touchmove', 'touchcancel'
+ ];
+
+ /**
+ * 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);
+})(jQuery, Drupal, Drupal.bootstrap);
diff --git a/js/drupal.bootstrap.js b/js/drupal.bootstrap.js
index 0ebc402..332fcdd 100644
--- a/js/drupal.bootstrap.js
+++ b/js/drupal.bootstrap.js
@@ -8,7 +8,7 @@
*
* @namespace
*/
-(function ($, Drupal) {
+(function ($, Drupal, drupalSettings) {
'use strict';
Drupal.bootstrap = {
@@ -86,12 +86,91 @@
plugin.Constructor = constructor;
var old = $.fn[id];
- plugin.noConflict = function () { $.fn[id] = old; return this; };
+ plugin.noConflict = function () {
+ $.fn[id] = old;
+ return this;
+ };
$.fn[id] = plugin;
}
};
/**
+ * Map of supported events by regular expression.
+ *
+ * @type {Object<Event|MouseEvent|KeyboardEvent|TouchEvent,RegExp>}
+ */
+ Drupal.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))$/
+ };
+
+ /**
+ * 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.
+ *
+ * @see https://github.com/jquery/jquery-simulate
+ *
+ * @param {HTMLElement} element
+ * A DOM element to dispatch event on.
+ * @param {String} type
+ * The type 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.
+ */
+ Drupal.bootstrap.simulate = function (element, type, options) {
+ // Defer to the jQuery.simulate plugin, if it's available.
+ if (typeof $.simulate === 'function') {
+ new $.simulate(element, type, options);
+ return;
+ }
+ var event;
+ var ctor;
+ for (var name in Drupal.bootstrap.eventMap) {
+ if (Drupal.bootstrap.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);
+ element.dispatchEvent(event);
+ }
+ else if (document.createEvent) {
+ event = document.createEvent(ctor);
+ event.initEvent(type, opts.bubbles, opts.cancelable);
+ element.dispatchEvent(event);
+ }
+ else if (typeof element.fireEvent === 'function') {
+ event = $.extend(document.createEventObject(), opts);
+ element.fireEvent('on' + type, event);
+ }
+ else if (typeof element[type]) {
+ element[type]();
+ }
+ };
+
+ /**
* Provide jQuery UI like ability to get/set options for Bootstrap plugins.
*
* @param {string|object} key
diff --git a/src/Bootstrap.php b/src/Bootstrap.php
index 414c299..e6a9547 100644
--- a/src/Bootstrap.php
+++ b/src/Bootstrap.php
@@ -524,7 +524,7 @@ class Bootstrap {
$hooks['bootstrap_dropdown'] = [
'variables' => [
- 'alignment' => NULL,
+ 'alignment' => 'down',
'attributes' => [],
'items' => [],
'split' => FALSE,
diff --git a/src/Plugin/Alter/ThemeSuggestions.php b/src/Plugin/Alter/ThemeSuggestions.php
index b570364..ce29ba6 100644
--- a/src/Plugin/Alter/ThemeSuggestions.php
+++ b/src/Plugin/Alter/ThemeSuggestions.php
@@ -107,10 +107,7 @@ class ThemeSuggestions extends PluginBase implements AlterInterface {
protected function alterInput() {
if ($this->element && $this->element->isButton()) {
$hook = 'input__button';
- if ($this->element->getProperty('dropbutton')) {
- $hook .= '__dropdown';
- }
- elseif ($this->element->getProperty('split')) {
+ if ($this->element->getProperty('split')) {
$hook .= '__split';
}
$this->addSuggestion($hook);
diff --git a/src/Plugin/Preprocess/BootstrapDropdown.php b/src/Plugin/Preprocess/BootstrapDropdown.php
index 9ca69cb..28e1662 100644
--- a/src/Plugin/Preprocess/BootstrapDropdown.php
+++ b/src/Plugin/Preprocess/BootstrapDropdown.php
@@ -10,7 +10,9 @@ use Drupal\bootstrap\Annotation\BootstrapPreprocess;
use Drupal\bootstrap\Utility\Element;
use Drupal\bootstrap\Utility\Unicode;
use Drupal\bootstrap\Utility\Variables;
+use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\Url;
/**
* Pre-processes variables for the "bootstrap_dropdown" theme hook.
@@ -53,50 +55,92 @@ class BootstrapDropdown extends PreprocessBase implements PreprocessInterface {
$operations = !!Unicode::strpos($variables->theme_hook_original, 'operations');
// Normal dropbutton links are not actually render arrays, convert them.
- foreach ($variables->links as &$link) {
- if (isset($link['title']) && $link['url']) {
+ foreach ($variables->links as &$element) {
+ if (isset($element['title']) && $element['url']) {
// Preserve query parameters (if any)
- if (!empty($link['query'])) {
- $url_query = $link['url']->getOption('query') ?: [];
- $link['url']->setOption('query', NestedArray::mergeDeep($url_query , $link['query']));
+ if (!empty($element['query'])) {
+ $url_query = $element['url']->getOption('query') ?: [];
+ $element['url']->setOption('query', NestedArray::mergeDeep($url_query , $element['query']));
}
// Build render array.
- $link = [
+ $element = [
'#type' => 'link',
- '#title' => $link['title'],
- '#url' => $link['url'],
+ '#title' => $element['title'],
+ '#url' => $element['url'],
];
}
}
- // Pop off the first link as the "toggle".
- $variables->toggle = array_shift($variables->links);
- $toggle = Element::create($variables->toggle);
-
- // Convert any toggle links to a proper button.
- if ($toggle->isType('link')) {
- $toggle->exchangeArray([
- '#type' => 'button',
- '#value' => $toggle->getProperty('title'),
- '#attributes' => [
- 'data-url' => $toggle->getProperty('url')->toString(),
- ],
- ]);
- if ($operations) {
- $toggle->setButtonSize('btn-xs');
+ $items = Element::createStandalone();
+
+ $primary_action = NULL;
+ $links = Element::create($variables->links);
+
+ // Iterate over all provided "links". The array may be associative, so
+ // this cannot rely on the key to be numeric, it must be tracked manually.
+ $i = -1;
+ foreach ($links->children(TRUE) as $key => $child) {
+ $i++;
+
+ // The first item is always the "primary link".
+ if ($i === 0) {
+ // Must generate an ID for this child because the toggle will use it.
+ $child->getProperty('id', $child->getAttribute('id', Html::getUniqueId('dropdown-item')));
+ $primary_action = $child->addClass('hidden');
+ }
+
+ // If actually a "link", add it to the items array directly.
+ if ($child->isType('link')) {
+ $items->$key->link = $child->getArrayCopy();
+ }
+ // Otherwise, convert into a proper link.
+ else {
+ // Hide the original element
+ $items->$key->element = $child->addClass('hidden')->getArrayCopy();
+
+ // Retrieve any set HTML identifier for the link, generating a new
+ // one if necessary.
+ $id = $child->getProperty('id', $child->getAttribute('id', Html::getUniqueId('dropdown-item')));
+ $items->$key->link = Element::createStandalone([
+ '#type' => 'link',
+ '#title' => $child->getProperty('value', $child->getProperty('title', $child->getProperty('text'))),
+ '#url' => Url::fromUserInput('#'),
+ '#attributes' => ['data-dropdown-target' => "#$id"],
+ ]);
+
+ // Also hide the real link if it's the primary action.
+ if ($i === 0) {
+ $items->$key->link->addClass('hidden');
+ }
}
}
- // Remove the dropbutton property.
- else {
- $toggle->unsetProperty('dropbutton');
+
+ // Create a toggle button, extracting relevant info from primary action.
+ $toggle = Element::createStandalone([
+ '#type' => 'button',
+ '#attributes' => $primary_action->getAttributes()->getArrayCopy(),
+ '#value' => $primary_action->getProperty('value', $primary_action->getProperty('title', $primary_action->getProperty('text'))),
+ ]);
+
+ // Remove the "hidden" class that was added to the primary action.
+ $toggle->removeClass('hidden')->removeAttribute('id')->setAttribute('data-dropdown-target', '#' . $primary_action->getAttribute('id'));
+
+ // Make operations smaller.
+ if ($operations) {
+ $toggle->setButtonSize('btn-xs', FALSE);
}
- $variables->items = array_values($variables->links);
+ // Add the toggle render array to the variables.
+ $variables->toggle = $toggle->getArrayCopy();
// Determine if toggle should be a split button.
- $variables->split = !!$variables->links;
+ $variables->split = count($items) > 1;
+
+ // Add the items variable for "bootstrap_dropdown".
+ $variables->items = $items->getArrayCopy();
+ // Remove the unnecessary "links" variable now.
unset($variables->links);
}
}
diff --git a/src/Theme.php b/src/Theme.php
index 480a0b0..aec2c60 100644
--- a/src/Theme.php
+++ b/src/Theme.php
@@ -247,7 +247,15 @@ class Theme {
}
$cache->setMultiple($drupal_settings);
}
- return array_intersect_key($this->settings()->get(), $drupal_settings);
+
+ $drupal_settings = array_intersect_key($this->settings()->get(), $drupal_settings);
+
+ // Indicate that theme is in dev mode.
+ if ($this->isDev()) {
+ $drupal_settings['dev'] = TRUE;
+ }
+
+ return $drupal_settings;
}
/**
diff --git a/src/Utility/Attributes.php b/src/Utility/Attributes.php
index 3352a7a..91bc2cb 100644
--- a/src/Utility/Attributes.php
+++ b/src/Utility/Attributes.php
@@ -82,16 +82,20 @@ class Attributes extends ArrayObject {
/**
* Indicates whether a class is present in the array.
*
- * @param string $class
- * The class to search for.
+ * @param string|array $class
+ * The class or array of classes to search for.
+ * @param bool $all
+ * Flag determining to check if all classes are present.
*
* @return bool
* TRUE or FALSE
*
* @see \Drupal\bootstrap\Utility\Attributes::getClasses()
*/
- public function hasClass($class) {
- return array_search($class, $this->getClasses()) !== FALSE;
+ public function hasClass($class, $all = FALSE) {
+ $classes = (array) $class;
+ $result = array_intersect($classes, $this->getClasses());
+ return $all ? $result && count($classes) === count($result) : !!$result;
}
/**
diff --git a/src/Utility/Element.php b/src/Utility/Element.php
index 2cff16c..84df3f7 100644
--- a/src/Utility/Element.php
+++ b/src/Utility/Element.php
@@ -206,9 +206,12 @@ class Element extends DrupalAttributes {
/**
* Adds a specific Bootstrap class to color a button based on its text value.
*
+ * @param bool $override
+ * Flag determining whether or not to override any existing set class.
+ *
* @return $this
*/
- public function colorize() {
+ public function colorize($override = TRUE) {
$button = $this->isButton();
// @todo refactor this more so it's not just "button" specific.
@@ -226,7 +229,7 @@ class Element extends DrupalAttributes {
$class = $button && !Bootstrap::getTheme()->getSetting('button_colorize') ? 'btn-default' : FALSE;
// Search for an existing class.
- if (!$class) {
+ if (!$class || !$override) {
foreach ($classes as $value) {
if ($this->hasClass($value)) {
$class = $value;
@@ -573,10 +576,13 @@ class Element extends DrupalAttributes {
* @param string $class
* The full button size class to add. If none is provided, it will default
* to any set theme setting.
+ * @param bool $override
+ * Flag indicating if the passed $class should be forcibly set. Setting
+ * this to FALSE allows any existing set class to persist.
*
* @return $this
*/
- public function setButtonSize($class = NULL) {
+ public function setButtonSize($class = NULL, $override = TRUE) {
// Immediately return if element is not a button.
if (!$this->isButton()) {
return $this;
@@ -592,7 +598,7 @@ class Element extends DrupalAttributes {
}
// Search for an existing class.
- if (!$class) {
+ if (!$class || !$override) {
foreach ($classes as $value) {
if ($this->hasClass($value)) {
$class = $value;
diff --git a/templates/bootstrap/bootstrap-dropdown.html.twig b/templates/bootstrap/bootstrap-dropdown.html.twig
index 4fc5dc0..e6ae2e4 100644
--- a/templates/bootstrap/bootstrap-dropdown.html.twig
+++ b/templates/bootstrap/bootstrap-dropdown.html.twig
@@ -16,7 +16,7 @@
{%
set classes = [
'btn-group',
- alignment == 'up' ? 'dropup',
+ alignment ? 'drop' ~ alignment|clean_class : 'dropdown',
]
%}
<div{{ attributes.addClass(classes) }}>
diff --git a/templates/input/input--button--dropdown.html.twig b/templates/input/input--button--dropdown.html.twig
deleted file mode 100644
index de7c4e6..0000000
--- a/templates/input/input--button--dropdown.html.twig
+++ /dev/null
@@ -1,31 +0,0 @@
-{% extends "input--button.html.twig" %}
-{#
-/**
- * @file
- * Theme suggestion for "button__dropdown" input form element.
- *
- * Available variables:
- * - attributes: A list of HTML attributes for the input element.
- * - children: Optional additional rendered elements.
- * - icon: An icon.
- * - icon_only: Flag to display only the icon and not the label.
- * - icon_position: Where an icon should be displayed.
- * - label: button label.
- * - prefix: Markup to display before the input element.
- * - suffix: Markup to display after the input element.
- * - type: The type of input.
- *
- * @ingroup templates
- *
- * @see \Drupal\bootstrap\Plugin\Preprocess\InputButton
- * @see \Drupal\bootstrap\Plugin\Preprocess\Input
- * @see template_preprocess_input()
- */
-#}
-{% spaceless %}
- {% block input %}
- <a href="#" data-target="dropdown-button">{{ label }}</a>
- <button{{ attributes.addClass('hidden') }}>{{ label }}</button>
- {{ children }}
- {% endblock %}
-{% endspaceless %}