- B + {% include bootstrap-icon.html %}

Bootstrap is the most popular HTML, CSS, and JS framework in the world for building responsive, mobile-first projects on the web.

Download Bootstrap @@ -29,11 +29,11 @@ layout: home

Bootstrap CDN

-

When you just need to include Bootstrap's compiled CSS and JS, use the Bootstrap CDN, free from the Max CDN folks.

+

When you just need to include Bootstrap's compiled CSS and JS, use the Bootstrap CDN, free from the MaxCDN folks.

{% highlight html %} - + {% endhighlight %}
diff --git a/docs/layout/grid.md b/docs/layout/grid.md index 7a16c52a7..97ffd3161 100644 --- a/docs/layout/grid.md +++ b/docs/layout/grid.md @@ -144,7 +144,7 @@ For example, here are two grid layouts that apply to every device and viewport, 1 of 2
- 1 of 2 + 2 of 2
@@ -152,10 +152,10 @@ For example, here are two grid layouts that apply to every device and viewport, 1 of 3
- 1 of 3 + 2 of 3
- 1 of 3 + 3 of 3
diff --git a/docs/migration.md b/docs/migration.md index 2584a22e8..6bbf754cb 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -97,6 +97,7 @@ New to Bootstrap 4 is the Reboot, a new stylesheet that builds on Normalize with - `.form-group` no longer applies styles from the `.row` via mixin, so `.row` is now required for horizontal grid layouts (e.g., `
`). - Added new `.form-control-label` class to vertically center labels with `.form-control`s. - Added custom forms support (for checkboxes, radios, selects, and file inputs). +- Renamed `.has-error` to `.has-danger`. ### Buttons @@ -196,14 +197,6 @@ Dropped entirely for the new card component. - `.panel-warning` to `.card-warning` and `.card-inverse` (or use `.bg-warning` on `.card-header`) - `.panel-danger` to `.card-danger` and `.card-inverse` (or use `.bg-danger` on `.card-header`) -### Tooltips - -- Removed support for `auto` placement options. - -### Popovers - -- Removed support for `auto` placement options. - ### Carousel - Overhauled the entire component to simplify design and styling. We have fewer styles for you to override, new indicators, and new icons. diff --git a/js/dist/button.js b/js/dist/button.js index 367e50495..cd82242c1 100644 --- a/js/dist/button.js +++ b/js/dist/button.js @@ -82,6 +82,9 @@ var Button = function ($) { } if (triggerChangeEvent) { + if (input.hasAttribute('disabled') || rootElement.hasAttribute('disabled') || input.classList.contains('disabled') || rootElement.classList.contains('disabled')) { + return; + } input.checked = !$(this._element).hasClass(ClassName.ACTIVE); $(input).trigger('change'); } diff --git a/js/dist/button.js.map b/js/dist/button.js.map index ae41dc503..899bcdaea 100644 Binary files a/js/dist/button.js.map and b/js/dist/button.js.map differ diff --git a/js/dist/collapse.js b/js/dist/collapse.js index d5105f442..0e2bc75e2 100644 --- a/js/dist/collapse.js +++ b/js/dist/collapse.js @@ -58,9 +58,8 @@ var Collapse = function ($) { }; var Selector = { - ACTIVES: '.card > .show, .card > .collapsing', - DATA_TOGGLE: '[data-toggle="collapse"]', - DATA_CHILDREN: 'data-children' + ACTIVES: '.show, .collapsing', + DATA_TOGGLE: '[data-toggle="collapse"]' }; /** @@ -77,20 +76,13 @@ var Collapse = function ($) { this._element = element; this._config = this._getConfig(config); this._triggerArray = $.makeArray($('[data-toggle="collapse"][href="#' + element.id + '"],' + ('[data-toggle="collapse"][data-target="#' + element.id + '"]'))); + this._parent = this._config.parent ? this._getParent() : null; if (!this._config.parent) { this._addAriaAndCollapsedClass(this._element, this._triggerArray); } - this._selectorActives = Selector.ACTIVES; - if (this._parent) { - var childrenSelector = this._parent.hasAttribute(Selector.DATA_CHILDREN) ? this._parent.getAttribute(Selector.DATA_CHILDREN) : null; - if (childrenSelector !== null) { - this._selectorActives = childrenSelector + ' > .show, ' + childrenSelector + ' > .collapsing'; - } - } - if (this._config.toggle) { this.toggle(); } @@ -119,7 +111,7 @@ var Collapse = function ($) { var activesData = void 0; if (this._parent) { - actives = $.makeArray($(this._parent).find(this._selectorActives)); + actives = $.makeArray($(this._parent).children().children(Selector.ACTIVES)); if (!actives.length) { actives = null; } diff --git a/js/dist/collapse.js.map b/js/dist/collapse.js.map index db9d13f6e..5fa9f74fa 100644 Binary files a/js/dist/collapse.js.map and b/js/dist/collapse.js.map differ diff --git a/js/dist/dropdown.js b/js/dist/dropdown.js index 62df3f341..a2593e162 100644 --- a/js/dist/dropdown.js +++ b/js/dist/dropdown.js @@ -1,3 +1,5 @@ +var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; + var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } @@ -11,6 +13,14 @@ function _classCallCheck(instance, Constructor) { if (!(instance instanceof Cons var Dropdown = function ($) { + /** + * Check for Popper dependency + * Popper - https://popper.js.org + */ + if (typeof Popper === 'undefined') { + throw new Error('Bootstrap dropdown require Popper.js (https://popper.js.org)'); + } + /** * ------------------------------------------------------------------------ * Constants @@ -44,7 +54,10 @@ var Dropdown = function ($) { var ClassName = { DISABLED: 'disabled', - SHOW: 'show' + SHOW: 'show', + DROPUP: 'dropup', + MENURIGHT: 'dropdown-menu-right', + MENULEFT: 'dropdown-menu-left' }; var Selector = { @@ -55,6 +68,25 @@ var Dropdown = function ($) { VISIBLE_ITEMS: '.dropdown-menu .dropdown-item:not(.disabled)' }; + var AttachmentMap = { + TOP: 'top-start', + TOPEND: 'top-end', + BOTTOM: 'bottom-start', + BOTTOMEND: 'bottom-end' + }; + + var Default = { + placement: AttachmentMap.BOTTOM, + offset: 0, + flip: true + }; + + var DefaultType = { + placement: 'string', + offset: '(number|string)', + flip: 'boolean' + }; + /** * ------------------------------------------------------------------------ * Class Definition @@ -62,10 +94,13 @@ var Dropdown = function ($) { */ var Dropdown = function () { - function Dropdown(element) { + function Dropdown(element, config) { _classCallCheck(this, Dropdown); this._element = element; + this._popper = null; + this._config = this._getConfig(config); + this._menu = this._getMenuElement(); this._addEventListeners(); } @@ -75,30 +110,49 @@ var Dropdown = function ($) { // public Dropdown.prototype.toggle = function toggle() { - if (this.disabled || $(this).hasClass(ClassName.DISABLED)) { - return false; + if (this._element.disabled || $(this._element).hasClass(ClassName.DISABLED)) { + return; } - var parent = Dropdown._getParentFromElement(this); - var isActive = $(parent).hasClass(ClassName.SHOW); + var parent = Dropdown._getParentFromElement(this._element); + var isActive = $(this._menu).hasClass(ClassName.SHOW); Dropdown._clearMenus(); if (isActive) { - return false; + return; } var relatedTarget = { - relatedTarget: this + relatedTarget: this._element }; var showEvent = $.Event(Event.SHOW, relatedTarget); $(parent).trigger(showEvent); if (showEvent.isDefaultPrevented()) { - return false; + return; } + var element = this._element; + // for dropup with alignment we use the parent as popper container + if ($(parent).hasClass(ClassName.DROPUP)) { + if ($(this._menu).hasClass(ClassName.MENULEFT) || $(this._menu).hasClass(ClassName.MENURIGHT)) { + element = parent; + } + } + this._popper = new Popper(element, this._menu, { + placement: this._getPlacement(), + modifiers: { + offset: { + offset: this._config.offset + }, + flip: { + enabled: this._config.flip + } + } + }); + // if this is a touch-enabled device we add extra // empty mouseover listeners to the body's immediate children; // only needed because of broken event delegation on iOS @@ -107,25 +161,79 @@ var Dropdown = function ($) { $('body').children().on('mouseover', null, $.noop); } - this.focus(); - this.setAttribute('aria-expanded', true); + this._element.focus(); + this._element.setAttribute('aria-expanded', true); - $(parent).toggleClass(ClassName.SHOW); - $(parent).trigger($.Event(Event.SHOWN, relatedTarget)); - - return false; + $(this._menu).toggleClass(ClassName.SHOW); + $(parent).toggleClass(ClassName.SHOW).trigger($.Event(Event.SHOWN, relatedTarget)); }; Dropdown.prototype.dispose = function dispose() { $.removeData(this._element, DATA_KEY); $(this._element).off(EVENT_KEY); this._element = null; + this._menu = null; + if (this._popper !== null) { + this._popper.destroy(); + } + this._popper = null; + }; + + Dropdown.prototype.update = function update() { + if (this._popper !== null) { + this._popper.scheduleUpdate(); + } }; // private Dropdown.prototype._addEventListeners = function _addEventListeners() { - $(this._element).on(Event.CLICK, this.toggle); + var _this = this; + + $(this._element).on(Event.CLICK, function (event) { + event.preventDefault(); + event.stopPropagation(); + _this.toggle(); + }); + }; + + Dropdown.prototype._getConfig = function _getConfig(config) { + var elementData = $(this._element).data(); + if (elementData.placement !== undefined) { + elementData.placement = AttachmentMap[elementData.placement.toUpperCase()]; + } + + config = $.extend({}, this.constructor.Default, $(this._element).data(), config); + + Util.typeCheckConfig(NAME, config, this.constructor.DefaultType); + + return config; + }; + + Dropdown.prototype._getMenuElement = function _getMenuElement() { + if (!this._menu) { + var parent = Dropdown._getParentFromElement(this._element); + this._menu = $(parent).find(Selector.MENU)[0]; + } + return this._menu; + }; + + Dropdown.prototype._getPlacement = function _getPlacement() { + var $parentDropdown = $(this._element).parent(); + var placement = this._config.placement; + + // Handle dropup + if ($parentDropdown.hasClass(ClassName.DROPUP) || this._config.placement === AttachmentMap.TOP) { + placement = AttachmentMap.TOP; + if ($(this._menu).hasClass(ClassName.MENURIGHT)) { + placement = AttachmentMap.TOPEND; + } + } else { + if ($(this._menu).hasClass(ClassName.MENURIGHT)) { + placement = AttachmentMap.BOTTOMEND; + } + } + return placement; }; // static @@ -133,9 +241,10 @@ var Dropdown = function ($) { Dropdown._jQueryInterface = function _jQueryInterface(config) { return this.each(function () { var data = $(this).data(DATA_KEY); + var _config = (typeof config === 'undefined' ? 'undefined' : _typeof(config)) === 'object' ? config : null; if (!data) { - data = new Dropdown(this); + data = new Dropdown(this, _config); $(this).data(DATA_KEY, data); } @@ -143,7 +252,7 @@ var Dropdown = function ($) { if (data[config] === undefined) { throw new Error('No method named "' + config + '"'); } - data[config].call(this); + data[config](); } }); }; @@ -154,13 +263,18 @@ var Dropdown = function ($) { } var toggles = $.makeArray($(Selector.DATA_TOGGLE)); - for (var i = 0; i < toggles.length; i++) { var parent = Dropdown._getParentFromElement(toggles[i]); + var context = $(toggles[i]).data(DATA_KEY); var relatedTarget = { relatedTarget: toggles[i] }; + if (!context) { + continue; + } + + var dropdownMenu = context._menu; if (!$(parent).hasClass(ClassName.SHOW)) { continue; } @@ -183,6 +297,7 @@ var Dropdown = function ($) { toggles[i].setAttribute('aria-expanded', 'false'); + $(dropdownMenu).removeClass(ClassName.SHOW); $(parent).removeClass(ClassName.SHOW).trigger($.Event(Event.HIDDEN, relatedTarget)); } }; @@ -254,6 +369,16 @@ var Dropdown = function ($) { get: function get() { return VERSION; } + }, { + key: 'Default', + get: function get() { + return Default; + } + }, { + key: 'DefaultType', + get: function get() { + return DefaultType; + } }]); return Dropdown; @@ -265,7 +390,11 @@ var Dropdown = function ($) { * ------------------------------------------------------------------------ */ - $(document).on(Event.KEYDOWN_DATA_API, Selector.DATA_TOGGLE, Dropdown._dataApiKeydownHandler).on(Event.KEYDOWN_DATA_API, Selector.MENU, Dropdown._dataApiKeydownHandler).on(Event.CLICK_DATA_API + ' ' + Event.KEYUP_DATA_API, Dropdown._clearMenus).on(Event.CLICK_DATA_API, Selector.DATA_TOGGLE, Dropdown.prototype.toggle).on(Event.CLICK_DATA_API, Selector.FORM_CHILD, function (e) { + $(document).on(Event.KEYDOWN_DATA_API, Selector.DATA_TOGGLE, Dropdown._dataApiKeydownHandler).on(Event.KEYDOWN_DATA_API, Selector.MENU, Dropdown._dataApiKeydownHandler).on(Event.CLICK_DATA_API + ' ' + Event.KEYUP_DATA_API, Dropdown._clearMenus).on(Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) { + event.preventDefault(); + event.stopPropagation(); + Dropdown._jQueryInterface.call($(this), 'toggle'); + }).on(Event.CLICK_DATA_API, Selector.FORM_CHILD, function (e) { e.stopPropagation(); }); @@ -283,5 +412,5 @@ var Dropdown = function ($) { }; return Dropdown; -}(jQuery); +}(jQuery); /* global Popper */ //# sourceMappingURL=dropdown.js.map \ No newline at end of file diff --git a/js/dist/dropdown.js.map b/js/dist/dropdown.js.map index c7b189283..bce6f0161 100644 Binary files a/js/dist/dropdown.js.map and b/js/dist/dropdown.js.map differ diff --git a/js/dist/popover.js b/js/dist/popover.js index d570fa372..1e82e8210 100644 --- a/js/dist/popover.js +++ b/js/dist/popover.js @@ -28,12 +28,14 @@ var Popover = function ($) { var DATA_KEY = 'bs.popover'; var EVENT_KEY = '.' + DATA_KEY; var JQUERY_NO_CONFLICT = $.fn[NAME]; + var CLASS_PREFIX = 'bs-popover'; + var BSCLS_PREFIX_REGEX = new RegExp('(^|\\s)' + CLASS_PREFIX + '\\S+', 'g'); var Default = $.extend({}, Tooltip.Default, { placement: 'right', trigger: 'click', content: '', - template: '' + template: '' }); var DefaultType = $.extend({}, Tooltip.DefaultType, { @@ -84,6 +86,10 @@ var Popover = function ($) { return this.getTitle() || this._getContent(); }; + Popover.prototype.addAttachmentClass = function addAttachmentClass(attachment) { + $(this.getTipElement()).addClass(CLASS_PREFIX + '-' + attachment); + }; + Popover.prototype.getTipElement = function getTipElement() { return this.tip = this.tip || $(this.config.template)[0]; }; @@ -96,8 +102,6 @@ var Popover = function ($) { this.setElementContent($tip.find(Selector.CONTENT), this._getContent()); $tip.removeClass(ClassName.FADE + ' ' + ClassName.SHOW); - - this.cleanupTether(); }; // private @@ -106,6 +110,14 @@ var Popover = function ($) { return this.element.getAttribute('data-content') || (typeof this.config.content === 'function' ? this.config.content.call(this.element) : this.config.content); }; + Popover.prototype._cleanTipClass = function _cleanTipClass() { + var $tip = $(this.getTipElement()); + var tabClass = $tip.attr('class').match(BSCLS_PREFIX_REGEX); + if (tabClass !== null && tabClass.length > 0) { + $tip.removeClass(tabClass.join('')); + } + }; + // static Popover._jQueryInterface = function _jQueryInterface(config) { diff --git a/js/dist/popover.js.map b/js/dist/popover.js.map index e8bf8e9d9..727ee815f 100644 Binary files a/js/dist/popover.js.map and b/js/dist/popover.js.map differ diff --git a/js/dist/tab.js b/js/dist/tab.js index 079f98a63..fb0749ed5 100644 --- a/js/dist/tab.js +++ b/js/dist/tab.js @@ -129,7 +129,7 @@ var Tab = function ($) { }; Tab.prototype.dispose = function dispose() { - $.removeClass(this._element, DATA_KEY); + $.removeData(this._element, DATA_KEY); this._element = null; }; diff --git a/js/dist/tab.js.map b/js/dist/tab.js.map index 80117e026..7e0d8179d 100644 Binary files a/js/dist/tab.js.map and b/js/dist/tab.js.map differ diff --git a/js/dist/tooltip.js b/js/dist/tooltip.js index af385927f..f470481d1 100644 --- a/js/dist/tooltip.js +++ b/js/dist/tooltip.js @@ -14,11 +14,11 @@ function _classCallCheck(instance, Constructor) { if (!(instance instanceof Cons var Tooltip = function ($) { /** - * Check for Tether dependency - * Tether - http://tether.io/ + * Check for Popper dependency + * Popper - https://popper.js.org */ - if (typeof Tether === 'undefined') { - throw new Error('Bootstrap tooltips require Tether (http://tether.io/)'); + if (typeof Popper === 'undefined') { + throw new Error('Bootstrap tooltips require Popper.js (https://popper.js.org)'); } /** @@ -33,22 +33,8 @@ var Tooltip = function ($) { var EVENT_KEY = '.' + DATA_KEY; var JQUERY_NO_CONFLICT = $.fn[NAME]; var TRANSITION_DURATION = 150; - var CLASS_PREFIX = 'bs-tether'; - var TETHER_PREFIX_REGEX = new RegExp('(^|\\s)' + CLASS_PREFIX + '\\S+', 'g'); - - var Default = { - animation: true, - template: '', - trigger: 'hover focus', - title: '', - delay: 0, - html: false, - selector: false, - placement: 'top', - offset: '0 0', - constraints: [], - container: false - }; + var CLASS_PREFIX = 'bs-tooltip'; + var BSCLS_PREFIX_REGEX = new RegExp('(^|\\s)' + CLASS_PREFIX + '\\S+', 'g'); var DefaultType = { animation: 'boolean', @@ -59,16 +45,31 @@ var Tooltip = function ($) { html: 'boolean', selector: '(string|boolean)', placement: '(string|function)', - offset: 'string', - constraints: 'array', - container: '(string|element|boolean)' + offset: '(number|string)', + container: '(string|element|boolean)', + fallbackPlacement: '(string|array)' }; var AttachmentMap = { - TOP: 'bottom center', - RIGHT: 'middle left', - BOTTOM: 'top center', - LEFT: 'middle right' + AUTO: 'auto', + TOP: 'top', + RIGHT: 'right', + BOTTOM: 'bottom', + LEFT: 'left' + }; + + var Default = { + animation: true, + template: '', + trigger: 'hover focus', + title: '', + delay: 0, + html: false, + selector: false, + placement: 'top', + offset: 0, + container: false, + fallbackPlacement: 'flip' }; var HoverState = { @@ -99,11 +100,6 @@ var Tooltip = function ($) { TOOLTIP_INNER: '.tooltip-inner' }; - var TetherClass = { - element: false, - enabled: false - }; - var Trigger = { HOVER: 'hover', FOCUS: 'focus', @@ -126,7 +122,7 @@ var Tooltip = function ($) { this._timeout = 0; this._hoverState = ''; this._activeTrigger = {}; - this._tether = null; + this._popper = null; // protected this.element = element; @@ -183,8 +179,6 @@ var Tooltip = function ($) { Tooltip.prototype.dispose = function dispose() { clearTimeout(this._timeout); - this.cleanupTether(); - $.removeData(this.element, this.constructor.DATA_KEY); $(this.element).off(this.constructor.EVENT_KEY); @@ -198,7 +192,10 @@ var Tooltip = function ($) { this._timeout = null; this._hoverState = null; this._activeTrigger = null; - this._tether = null; + if (this._popper !== null) { + this._popper.destroy(); + } + this._popper = null; this.element = null; this.config = null; @@ -237,6 +234,7 @@ var Tooltip = function ($) { var placement = typeof this.config.placement === 'function' ? this.config.placement.call(this, tip, this.element) : this.config.placement; var attachment = this._getAttachment(placement); + this.addAttachmentClass(attachment); var container = this.config.container === false ? document.body : $(this.config.container); @@ -248,20 +246,26 @@ var Tooltip = function ($) { $(this.element).trigger(this.constructor.Event.INSERTED); - this._tether = new Tether({ - attachment: attachment, - element: tip, - target: this.element, - classes: TetherClass, - classPrefix: CLASS_PREFIX, - offset: this.config.offset, - constraints: this.config.constraints, - addTargetClasses: false + this._popper = new Popper(this.element, tip, { + placement: attachment, + modifiers: { + offset: { + offset: this.config.offset + }, + flip: { + behavior: this.config.fallbackPlacement + } + }, + onCreate: function onCreate(data) { + if (data.originalPlacement !== data.placement) { + _this._handlePopperPlacementChange(data); + } + }, + onUpdate: function onUpdate(data) { + _this._handlePopperPlacementChange(data); + } }); - Util.reflow(tip); - this._tether.position(); - $(tip).addClass(ClassName.SHOW); // if this is a touch-enabled device we add extra @@ -273,6 +277,9 @@ var Tooltip = function ($) { } var complete = function complete() { + if (_this.config.animation) { + _this._fixTransition(); + } var prevHoverState = _this._hoverState; _this._hoverState = null; @@ -285,10 +292,9 @@ var Tooltip = function ($) { if (Util.supportsTransitionEnd() && $(this.tip).hasClass(ClassName.FADE)) { $(this.tip).one(Util.TRANSITION_END, complete).emulateTransitionEnd(Tooltip._TRANSITION_DURATION); - return; + } else { + complete(); } - - complete(); } }; @@ -305,7 +311,9 @@ var Tooltip = function ($) { _this2._cleanTipClass(); _this2.element.removeAttribute('aria-describedby'); $(_this2.element).trigger(_this2.constructor.Event.HIDDEN); - _this2.cleanupTether(); + if (_this2._popper !== null) { + _this2._popper.destroy(); + } if (callback) { callback(); @@ -340,24 +348,30 @@ var Tooltip = function ($) { this._hoverState = ''; }; + Tooltip.prototype.update = function update() { + if (this._popper !== null) { + this._popper.scheduleUpdate(); + } + }; + // protected Tooltip.prototype.isWithContent = function isWithContent() { return Boolean(this.getTitle()); }; + Tooltip.prototype.addAttachmentClass = function addAttachmentClass(attachment) { + $(this.getTipElement()).addClass(CLASS_PREFIX + '-' + attachment); + }; + Tooltip.prototype.getTipElement = function getTipElement() { return this.tip = this.tip || $(this.config.template)[0]; }; Tooltip.prototype.setContent = function setContent() { var $tip = $(this.getTipElement()); - this.setElementContent($tip.find(Selector.TOOLTIP_INNER), this.getTitle()); - $tip.removeClass(ClassName.FADE + ' ' + ClassName.SHOW); - - this.cleanupTether(); }; Tooltip.prototype.setElementContent = function setElementContent($element, content) { @@ -386,26 +400,12 @@ var Tooltip = function ($) { return title; }; - Tooltip.prototype.cleanupTether = function cleanupTether() { - if (this._tether) { - this._tether.destroy(); - } - }; - // private Tooltip.prototype._getAttachment = function _getAttachment(placement) { return AttachmentMap[placement.toUpperCase()]; }; - Tooltip.prototype._cleanTipClass = function _cleanTipClass() { - var $tip = $(this.getTipElement()); - var tabClass = $tip.attr('class').match(TETHER_PREFIX_REGEX); - if (tabClass !== null && tabClass.length > 0) { - $tip.removeClass(tabClass.join('')); - } - }; - Tooltip.prototype._setListeners = function _setListeners() { var _this3 = this; @@ -566,6 +566,32 @@ var Tooltip = function ($) { return config; }; + Tooltip.prototype._cleanTipClass = function _cleanTipClass() { + var $tip = $(this.getTipElement()); + var tabClass = $tip.attr('class').match(BSCLS_PREFIX_REGEX); + if (tabClass !== null && tabClass.length > 0) { + $tip.removeClass(tabClass.join('')); + } + }; + + Tooltip.prototype._handlePopperPlacementChange = function _handlePopperPlacementChange(data) { + this._cleanTipClass(); + this.addAttachmentClass(this._getAttachment(data.placement)); + }; + + Tooltip.prototype._fixTransition = function _fixTransition() { + var tip = this.getTipElement(); + var initConfigAnimation = this.config.animation; + if (tip.getAttribute('x-placement') !== null) { + return; + } + $(tip).removeClass(ClassName.FADE); + this.config.animation = false; + this.hide(); + this.show(); + this.config.animation = initConfigAnimation; + }; + // static Tooltip._jQueryInterface = function _jQueryInterface(config) { @@ -645,5 +671,5 @@ var Tooltip = function ($) { }; return Tooltip; -}(jQuery); /* global Tether */ +}(jQuery); /* global Popper */ //# sourceMappingURL=tooltip.js.map \ No newline at end of file diff --git a/js/dist/tooltip.js.map b/js/dist/tooltip.js.map index f697512d3..ce79d67d1 100644 Binary files a/js/dist/tooltip.js.map and b/js/dist/tooltip.js.map differ diff --git a/js/src/dropdown.js b/js/src/dropdown.js index eb536dc7d..acc3ed453 100644 --- a/js/src/dropdown.js +++ b/js/src/dropdown.js @@ -1,3 +1,5 @@ +/* global Popper */ + import Util from './util' @@ -10,6 +12,13 @@ import Util from './util' const Dropdown = (($) => { + /** + * Check for Popper dependency + * Popper - https://popper.js.org + */ + if (typeof Popper === 'undefined') { + throw new Error('Bootstrap dropdown require Popper.js (https://popper.js.org)') + } /** * ------------------------------------------------------------------------ @@ -43,8 +52,11 @@ const Dropdown = (($) => { } const ClassName = { - DISABLED : 'disabled', - SHOW : 'show' + DISABLED : 'disabled', + SHOW : 'show', + DROPUP : 'dropup', + MENURIGHT : 'dropdown-menu-right', + MENULEFT : 'dropdown-menu-left' } const Selector = { @@ -55,6 +67,25 @@ const Dropdown = (($) => { VISIBLE_ITEMS : '.dropdown-menu .dropdown-item:not(.disabled)' } + const AttachmentMap = { + TOP : 'top-start', + TOPEND : 'top-end', + BOTTOM : 'bottom-start', + BOTTOMEND : 'bottom-end' + } + + const Default = { + placement : AttachmentMap.BOTTOM, + offset : 0, + flip : true + } + + const DefaultType = { + placement : 'string', + offset : '(number|string)', + flip : 'boolean' + } + /** * ------------------------------------------------------------------------ @@ -64,8 +95,11 @@ const Dropdown = (($) => { class Dropdown { - constructor(element) { + constructor(element, config) { this._element = element + this._popper = null + this._config = this._getConfig(config) + this._menu = this._getMenuElement() this._addEventListeners() } @@ -77,34 +111,60 @@ const Dropdown = (($) => { return VERSION } + static get Default() { + return Default + } + + static get DefaultType() { + return DefaultType + } // public toggle() { - if (this.disabled || $(this).hasClass(ClassName.DISABLED)) { - return false + if (this._element.disabled || $(this._element).hasClass(ClassName.DISABLED)) { + return } - const parent = Dropdown._getParentFromElement(this) - const isActive = $(parent).hasClass(ClassName.SHOW) + const parent = Dropdown._getParentFromElement(this._element) + const isActive = $(this._menu).hasClass(ClassName.SHOW) Dropdown._clearMenus() if (isActive) { - return false + return } const relatedTarget = { - relatedTarget : this + relatedTarget : this._element } - const showEvent = $.Event(Event.SHOW, relatedTarget) + const showEvent = $.Event(Event.SHOW, relatedTarget) $(parent).trigger(showEvent) if (showEvent.isDefaultPrevented()) { - return false + return } + let element = this._element + // for dropup with alignment we use the parent as popper container + if ($(parent).hasClass(ClassName.DROPUP)) { + if ($(this._menu).hasClass(ClassName.MENULEFT) || $(this._menu).hasClass(ClassName.MENURIGHT)) { + element = parent + } + } + this._popper = new Popper(element, this._menu, { + placement : this._getPlacement(), + modifiers : { + offset : { + offset : this._config.offset + }, + flip : { + enabled : this._config.flip + } + } + }) + // if this is a touch-enabled device we add extra // empty mouseover listeners to the body's immediate children; // only needed because of broken event delegation on iOS @@ -114,37 +174,100 @@ const Dropdown = (($) => { $('body').children().on('mouseover', null, $.noop) } - this.focus() - this.setAttribute('aria-expanded', true) + this._element.focus() + this._element.setAttribute('aria-expanded', true) - $(parent).toggleClass(ClassName.SHOW) - $(parent).trigger($.Event(Event.SHOWN, relatedTarget)) - - return false + $(this._menu).toggleClass(ClassName.SHOW) + $(parent) + .toggleClass(ClassName.SHOW) + .trigger($.Event(Event.SHOWN, relatedTarget)) } dispose() { $.removeData(this._element, DATA_KEY) $(this._element).off(EVENT_KEY) this._element = null + this._menu = null + if (this._popper !== null) { + this._popper.destroy() + } + this._popper = null } + update() { + if (this._popper !== null) { + this._popper.scheduleUpdate() + } + } // private _addEventListeners() { - $(this._element).on(Event.CLICK, this.toggle) + $(this._element).on(Event.CLICK, (event) => { + event.preventDefault() + event.stopPropagation() + this.toggle() + }) } + _getConfig(config) { + const elementData = $(this._element).data() + if (elementData.placement !== undefined) { + elementData.placement = AttachmentMap[elementData.placement.toUpperCase()] + } + + config = $.extend( + {}, + this.constructor.Default, + $(this._element).data(), + config + ) + + Util.typeCheckConfig( + NAME, + config, + this.constructor.DefaultType + ) + + return config + } + + _getMenuElement() { + if (!this._menu) { + const parent = Dropdown._getParentFromElement(this._element) + this._menu = $(parent).find(Selector.MENU)[0] + } + return this._menu + } + + _getPlacement() { + const $parentDropdown = $(this._element).parent() + let placement = this._config.placement + + // Handle dropup + if ($parentDropdown.hasClass(ClassName.DROPUP) || this._config.placement === AttachmentMap.TOP) { + placement = AttachmentMap.TOP + if ($(this._menu).hasClass(ClassName.MENURIGHT)) { + placement = AttachmentMap.TOPEND + } + } + else { + if ($(this._menu).hasClass(ClassName.MENURIGHT)) { + placement = AttachmentMap.BOTTOMEND + } + } + return placement + } // static static _jQueryInterface(config) { return this.each(function () { let data = $(this).data(DATA_KEY) + const _config = typeof config === 'object' ? config : null if (!data) { - data = new Dropdown(this) + data = new Dropdown(this, _config) $(this).data(DATA_KEY, data) } @@ -152,7 +275,7 @@ const Dropdown = (($) => { if (data[config] === undefined) { throw new Error(`No method named "${config}"`) } - data[config].call(this) + data[config]() } }) } @@ -164,13 +287,18 @@ const Dropdown = (($) => { } const toggles = $.makeArray($(Selector.DATA_TOGGLE)) - for (let i = 0; i < toggles.length; i++) { const parent = Dropdown._getParentFromElement(toggles[i]) + const context = $(toggles[i]).data(DATA_KEY) const relatedTarget = { relatedTarget : toggles[i] } + if (!context) { + continue + } + + const dropdownMenu = context._menu if (!$(parent).hasClass(ClassName.SHOW)) { continue } @@ -195,6 +323,7 @@ const Dropdown = (($) => { toggles[i].setAttribute('aria-expanded', 'false') + $(dropdownMenu).removeClass(ClassName.SHOW) $(parent) .removeClass(ClassName.SHOW) .trigger($.Event(Event.HIDDEN, relatedTarget)) @@ -276,7 +405,11 @@ const Dropdown = (($) => { .on(Event.KEYDOWN_DATA_API, Selector.DATA_TOGGLE, Dropdown._dataApiKeydownHandler) .on(Event.KEYDOWN_DATA_API, Selector.MENU, Dropdown._dataApiKeydownHandler) .on(`${Event.CLICK_DATA_API} ${Event.KEYUP_DATA_API}`, Dropdown._clearMenus) - .on(Event.CLICK_DATA_API, Selector.DATA_TOGGLE, Dropdown.prototype.toggle) + .on(Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) { + event.preventDefault() + event.stopPropagation() + Dropdown._jQueryInterface.call($(this), 'toggle') + }) .on(Event.CLICK_DATA_API, Selector.FORM_CHILD, (e) => { e.stopPropagation() }) diff --git a/js/src/popover.js b/js/src/popover.js index b68b47998..a068420d6 100644 --- a/js/src/popover.js +++ b/js/src/popover.js @@ -22,12 +22,15 @@ const Popover = (($) => { const DATA_KEY = 'bs.popover' const EVENT_KEY = `.${DATA_KEY}` const JQUERY_NO_CONFLICT = $.fn[NAME] + const CLASS_PREFIX = 'bs-popover' + const BSCLS_PREFIX_REGEX = new RegExp(`(^|\\s)${CLASS_PREFIX}\\S+`, 'g') const Default = $.extend({}, Tooltip.Default, { placement : 'right', trigger : 'click', content : '', template : '' }) @@ -106,6 +109,10 @@ const Popover = (($) => { return this.getTitle() || this._getContent() } + addAttachmentClass(attachment) { + $(this.getTipElement()).addClass(`${CLASS_PREFIX}-${attachment}`) + } + getTipElement() { return this.tip = this.tip || $(this.config.template)[0] } @@ -118,8 +125,6 @@ const Popover = (($) => { this.setElementContent($tip.find(Selector.CONTENT), this._getContent()) $tip.removeClass(`${ClassName.FADE} ${ClassName.SHOW}`) - - this.cleanupTether() } // private @@ -131,6 +136,14 @@ const Popover = (($) => { this.config.content) } + _cleanTipClass() { + const $tip = $(this.getTipElement()) + const tabClass = $tip.attr('class').match(BSCLS_PREFIX_REGEX) + if (tabClass !== null && tabClass.length > 0) { + $tip.removeClass(tabClass.join('')) + } + } + // static diff --git a/js/src/tooltip.js b/js/src/tooltip.js index 47c3d8d05..1d53b0470 100644 --- a/js/src/tooltip.js +++ b/js/src/tooltip.js @@ -1,4 +1,4 @@ -/* global Tether */ +/* global Popper */ import Util from './util' @@ -13,11 +13,11 @@ import Util from './util' const Tooltip = (($) => { /** - * Check for Tether dependency - * Tether - http://tether.io/ + * Check for Popper dependency + * Popper - https://popper.js.org */ - if (typeof Tether === 'undefined') { - throw new Error('Bootstrap tooltips require Tether (http://tether.io/)') + if (typeof Popper === 'undefined') { + throw new Error('Bootstrap tooltips require Popper.js (https://popper.js.org)') } @@ -33,43 +33,45 @@ const Tooltip = (($) => { const EVENT_KEY = `.${DATA_KEY}` const JQUERY_NO_CONFLICT = $.fn[NAME] const TRANSITION_DURATION = 150 - const CLASS_PREFIX = 'bs-tether' - const TETHER_PREFIX_REGEX = new RegExp(`(^|\\s)${CLASS_PREFIX}\\S+`, 'g') - - const Default = { - animation : true, - template : '', - trigger : 'hover focus', - title : '', - delay : 0, - html : false, - selector : false, - placement : 'top', - offset : '0 0', - constraints : [], - container : false - } + const CLASS_PREFIX = 'bs-tooltip' + const BSCLS_PREFIX_REGEX = new RegExp(`(^|\\s)${CLASS_PREFIX}\\S+`, 'g') const DefaultType = { - animation : 'boolean', - template : 'string', - title : '(string|element|function)', - trigger : 'string', - delay : '(number|object)', - html : 'boolean', - selector : '(string|boolean)', - placement : '(string|function)', - offset : 'string', - constraints : 'array', - container : '(string|element|boolean)' + animation : 'boolean', + template : 'string', + title : '(string|element|function)', + trigger : 'string', + delay : '(number|object)', + html : 'boolean', + selector : '(string|boolean)', + placement : '(string|function)', + offset : '(number|string)', + container : '(string|element|boolean)', + fallbackPlacement : '(string|array)' } const AttachmentMap = { - TOP : 'bottom center', - RIGHT : 'middle left', - BOTTOM : 'top center', - LEFT : 'middle right' + AUTO : 'auto', + TOP : 'top', + RIGHT : 'right', + BOTTOM : 'bottom', + LEFT : 'left' + } + + const Default = { + animation : true, + template : '', + trigger : 'hover focus', + title : '', + delay : 0, + html : false, + selector : false, + placement : 'top', + offset : 0, + container : false, + fallbackPlacement : 'flip' } const HoverState = { @@ -100,11 +102,6 @@ const Tooltip = (($) => { TOOLTIP_INNER : '.tooltip-inner' } - const TetherClass = { - element : false, - enabled : false - } - const Trigger = { HOVER : 'hover', FOCUS : 'focus', @@ -128,7 +125,7 @@ const Tooltip = (($) => { this._timeout = 0 this._hoverState = '' this._activeTrigger = {} - this._tether = null + this._popper = null // protected this.element = element @@ -220,8 +217,6 @@ const Tooltip = (($) => { dispose() { clearTimeout(this._timeout) - this.cleanupTether() - $.removeData(this.element, this.constructor.DATA_KEY) $(this.element).off(this.constructor.EVENT_KEY) @@ -235,7 +230,10 @@ const Tooltip = (($) => { this._timeout = null this._hoverState = null this._activeTrigger = null - this._tether = null + if (this._popper !== null) { + this._popper.destroy() + } + this._popper = null this.element = null this.config = null @@ -277,6 +275,7 @@ const Tooltip = (($) => { this.config.placement const attachment = this._getAttachment(placement) + this.addAttachmentClass(attachment) const container = this.config.container === false ? document.body : $(this.config.container) @@ -288,20 +287,26 @@ const Tooltip = (($) => { $(this.element).trigger(this.constructor.Event.INSERTED) - this._tether = new Tether({ - attachment, - element : tip, - target : this.element, - classes : TetherClass, - classPrefix : CLASS_PREFIX, - offset : this.config.offset, - constraints : this.config.constraints, - addTargetClasses: false + this._popper = new Popper(this.element, tip, { + placement : attachment, + modifiers : { + offset : { + offset : this.config.offset + }, + flip : { + behavior : this.config.fallbackPlacement + } + }, + onCreate : (data) => { + if (data.originalPlacement !== data.placement) { + this._handlePopperPlacementChange(data) + } + }, + onUpdate : (data) => { + this._handlePopperPlacementChange(data) + } }) - Util.reflow(tip) - this._tether.position() - $(tip).addClass(ClassName.SHOW) // if this is a touch-enabled device we add extra @@ -313,6 +318,9 @@ const Tooltip = (($) => { } const complete = () => { + if (this.config.animation) { + this._fixTransition() + } const prevHoverState = this._hoverState this._hoverState = null @@ -327,10 +335,9 @@ const Tooltip = (($) => { $(this.tip) .one(Util.TRANSITION_END, complete) .emulateTransitionEnd(Tooltip._TRANSITION_DURATION) - return + } else { + complete() } - - complete() } } @@ -345,7 +352,9 @@ const Tooltip = (($) => { this._cleanTipClass() this.element.removeAttribute('aria-describedby') $(this.element).trigger(this.constructor.Event.HIDDEN) - this.cleanupTether() + if (this._popper !== null) { + this._popper.destroy() + } if (callback) { callback() @@ -385,6 +394,11 @@ const Tooltip = (($) => { } + update() { + if (this._popper !== null) { + this._popper.scheduleUpdate() + } + } // protected @@ -392,18 +406,18 @@ const Tooltip = (($) => { return Boolean(this.getTitle()) } + addAttachmentClass(attachment) { + $(this.getTipElement()).addClass(`${CLASS_PREFIX}-${attachment}`) + } + getTipElement() { return this.tip = this.tip || $(this.config.template)[0] } setContent() { const $tip = $(this.getTipElement()) - this.setElementContent($tip.find(Selector.TOOLTIP_INNER), this.getTitle()) - $tip.removeClass(`${ClassName.FADE} ${ClassName.SHOW}`) - - this.cleanupTether() } setElementContent($element, content) { @@ -434,12 +448,6 @@ const Tooltip = (($) => { return title } - cleanupTether() { - if (this._tether) { - this._tether.destroy() - } - } - // private @@ -447,14 +455,6 @@ const Tooltip = (($) => { return AttachmentMap[placement.toUpperCase()] } - _cleanTipClass() { - const $tip = $(this.getTipElement()) - const tabClass = $tip.attr('class').match(TETHER_PREFIX_REGEX) - if (tabClass !== null && tabClass.length > 0) { - $tip.removeClass(tabClass.join('')) - } - } - _setListeners() { const triggers = this.config.trigger.split(' ') @@ -651,6 +651,31 @@ const Tooltip = (($) => { return config } + _cleanTipClass() { + const $tip = $(this.getTipElement()) + const tabClass = $tip.attr('class').match(BSCLS_PREFIX_REGEX) + if (tabClass !== null && tabClass.length > 0) { + $tip.removeClass(tabClass.join('')) + } + } + + _handlePopperPlacementChange(data) { + this._cleanTipClass() + this.addAttachmentClass(this._getAttachment(data.placement)) + } + + _fixTransition() { + const tip = this.getTipElement() + const initConfigAnimation = this.config.animation + if (tip.getAttribute('x-placement') !== null) { + return + } + $(tip).removeClass(ClassName.FADE) + this.config.animation = false + this.hide() + this.show() + this.config.animation = initConfigAnimation + } // static diff --git a/js/tests/index.html b/js/tests/index.html index 81efd5876..d17608e4a 100644 --- a/js/tests/index.html +++ b/js/tests/index.html @@ -7,7 +7,7 @@ - + diff --git a/js/tests/unit/dropdown.js b/js/tests/unit/dropdown.js index a15eb5245..1dd675b0b 100644 --- a/js/tests/unit/dropdown.js +++ b/js/tests/unit/dropdown.js @@ -275,20 +275,20 @@ $(function () { $first.parent('.dropdown') .on('shown.bs.dropdown', function () { assert.strictEqual($first.parents('.show').length, 1, '"show" class added on click') - assert.strictEqual($('#qunit-fixture .show').length, 1, 'only one dropdown is shown') + assert.strictEqual($('#qunit-fixture .dropdown-menu.show').length, 1, 'only one dropdown is shown') $(document.body).trigger('click') }).on('hidden.bs.dropdown', function () { - assert.strictEqual($('#qunit-fixture .show').length, 0, '"show" class removed') + assert.strictEqual($('#qunit-fixture .dropdown-menu.show').length, 0, '"show" class removed') $last.trigger('click') }) $last.parent('.btn-group') .on('shown.bs.dropdown', function () { assert.strictEqual($last.parent('.show').length, 1, '"show" class added on click') - assert.strictEqual($('#qunit-fixture .show').length, 1, 'only one dropdown is shown') + assert.strictEqual($('#qunit-fixture .dropdown-menu.show').length, 1, 'only one dropdown is shown') $(document.body).trigger('click') }).on('hidden.bs.dropdown', function () { - assert.strictEqual($('#qunit-fixture .show').length, 0, '"show" class removed') + assert.strictEqual($('#qunit-fixture .dropdown-menu.show').length, 0, '"show" class removed') done() }) $first.trigger('click') @@ -321,24 +321,24 @@ $(function () { $first.parent('.dropdown') .on('shown.bs.dropdown', function () { assert.strictEqual($first.parents('.show').length, 1, '"show" class added on click') - assert.strictEqual($('#qunit-fixture .show').length, 1, 'only one dropdown is shown') + assert.strictEqual($('#qunit-fixture .dropdown-menu.show').length, 1, 'only one dropdown is shown') var e = $.Event('keyup') e.which = 9 // Tab $(document.body).trigger(e) }).on('hidden.bs.dropdown', function () { - assert.strictEqual($('#qunit-fixture .show').length, 0, '"show" class removed') + assert.strictEqual($('#qunit-fixture .dropdown-menu.show').length, 0, '"show" class removed') $last.trigger('click') }) $last.parent('.btn-group') .on('shown.bs.dropdown', function () { assert.strictEqual($last.parent('.show').length, 1, '"show" class added on click') - assert.strictEqual($('#qunit-fixture .show').length, 1, 'only one dropdown is shown') + assert.strictEqual($('#qunit-fixture .dropdown-menu.show').length, 1, 'only one dropdown is shown') var e = $.Event('keyup') e.which = 9 // Tab $(document.body).trigger(e) }).on('hidden.bs.dropdown', function () { - assert.strictEqual($('#qunit-fixture .show').length, 0, '"show" class removed') + assert.strictEqual($('#qunit-fixture .dropdown-menu.show').length, 0, '"show" class removed') done() }) $first.trigger('click') @@ -552,7 +552,7 @@ $(function () { }) QUnit.test('should not close the dropdown if the user clicks on a text field', function (assert) { - assert.expect(1) + assert.expect(2) var done = assert.async() var dropdownHTML = '