diff options
| author | Mark Otto <[email protected]> | 2017-05-27 15:26:48 -0700 |
|---|---|---|
| committer | Mark Otto <[email protected]> | 2017-05-27 15:26:48 -0700 |
| commit | 8f67ac19a761b8d3ecc80485c3f54aa6ba4fa910 (patch) | |
| tree | d5a2aba4e2e762b283e7cfe60f00e1f239105bbe /js/src | |
| parent | 6c3f833076a9fa68601741e3e21bd07ad79b7d8a (diff) | |
| parent | db44e4b311317ef760f8412cc33c84146959b248 (diff) | |
| download | bootstrap-8f67ac19a761b8d3ecc80485c3f54aa6ba4fa910.tar.xz bootstrap-8f67ac19a761b8d3ecc80485c3f54aa6ba4fa910.zip | |
Merge branch 'v4-dev' into v4-docs-streamlined
Diffstat (limited to 'js/src')
| -rw-r--r-- | js/src/alert.js | 4 | ||||
| -rw-r--r-- | js/src/button.js | 16 | ||||
| -rw-r--r-- | js/src/carousel.js | 72 | ||||
| -rw-r--r-- | js/src/collapse.js | 34 | ||||
| -rw-r--r-- | js/src/dropdown.js | 243 | ||||
| -rw-r--r-- | js/src/modal.js | 89 | ||||
| -rw-r--r-- | js/src/popover.js | 21 | ||||
| -rw-r--r-- | js/src/scrollspy.js | 39 | ||||
| -rw-r--r-- | js/src/tab.js | 21 | ||||
| -rw-r--r-- | js/src/tooltip.js | 234 | ||||
| -rw-r--r-- | js/src/util.js | 13 |
11 files changed, 520 insertions, 266 deletions
diff --git a/js/src/alert.js b/js/src/alert.js index 884a5dec8..b30d0d3a0 100644 --- a/js/src/alert.js +++ b/js/src/alert.js @@ -3,7 +3,7 @@ import Util from './util' /** * -------------------------------------------------------------------------- - * Bootstrap (v4.0.0-alpha.5): alert.js + * Bootstrap (v4.0.0-alpha.6): alert.js * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * -------------------------------------------------------------------------- */ @@ -18,7 +18,7 @@ const Alert = (($) => { */ const NAME = 'alert' - const VERSION = '4.0.0-alpha.5' + const VERSION = '4.0.0-alpha.6' const DATA_KEY = 'bs.alert' const EVENT_KEY = `.${DATA_KEY}` const DATA_API_KEY = '.data-api' diff --git a/js/src/button.js b/js/src/button.js index 45e1424ff..722fd489d 100644 --- a/js/src/button.js +++ b/js/src/button.js @@ -1,6 +1,6 @@ /** * -------------------------------------------------------------------------- - * Bootstrap (v4.0.0-alpha.5): button.js + * Bootstrap (v4.0.0-alpha.6): button.js * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * -------------------------------------------------------------------------- */ @@ -15,7 +15,7 @@ const Button = (($) => { */ const NAME = 'button' - const VERSION = '4.0.0-alpha.5' + const VERSION = '4.0.0-alpha.6' const DATA_KEY = 'bs.button' const EVENT_KEY = `.${DATA_KEY}` const DATA_API_KEY = '.data-api' @@ -66,6 +66,7 @@ const Button = (($) => { toggle() { let triggerChangeEvent = true + let addAriaPressed = true const rootElement = $(this._element).closest( Selector.DATA_TOGGLE )[0] @@ -89,14 +90,23 @@ const Button = (($) => { } 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') } input.focus() + addAriaPressed = false } - } else { + } + + if (addAriaPressed) { this._element.setAttribute('aria-pressed', !$(this._element).hasClass(ClassName.ACTIVE)) } diff --git a/js/src/carousel.js b/js/src/carousel.js index 9a1a668b2..5993de256 100644 --- a/js/src/carousel.js +++ b/js/src/carousel.js @@ -3,7 +3,7 @@ import Util from './util' /** * -------------------------------------------------------------------------- - * Bootstrap (v4.0.0-alpha.5): carousel.js + * Bootstrap (v4.0.0-alpha.6): carousel.js * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * -------------------------------------------------------------------------- */ @@ -17,15 +17,16 @@ const Carousel = (($) => { * ------------------------------------------------------------------------ */ - const NAME = 'carousel' - const VERSION = '4.0.0-alpha.5' - const DATA_KEY = 'bs.carousel' - const EVENT_KEY = `.${DATA_KEY}` - const DATA_API_KEY = '.data-api' - const JQUERY_NO_CONFLICT = $.fn[NAME] - const TRANSITION_DURATION = 600 - const ARROW_LEFT_KEYCODE = 37 // KeyboardEvent.which value for left arrow key - const ARROW_RIGHT_KEYCODE = 39 // KeyboardEvent.which value for right arrow key + const NAME = 'carousel' + const VERSION = '4.0.0-alpha.6' + const DATA_KEY = 'bs.carousel' + const EVENT_KEY = `.${DATA_KEY}` + const DATA_API_KEY = '.data-api' + const JQUERY_NO_CONFLICT = $.fn[NAME] + const TRANSITION_DURATION = 600 + const ARROW_LEFT_KEYCODE = 37 // KeyboardEvent.which value for left arrow key + const ARROW_RIGHT_KEYCODE = 39 // KeyboardEvent.which value for right arrow key + const TOUCHEVENT_COMPAT_WAIT = 500 // Time for mouse compat events to fire after touch const Default = { interval : 5000, @@ -56,6 +57,7 @@ const Carousel = (($) => { KEYDOWN : `keydown${EVENT_KEY}`, MOUSEENTER : `mouseenter${EVENT_KEY}`, MOUSELEAVE : `mouseleave${EVENT_KEY}`, + TOUCHEND : `touchend${EVENT_KEY}`, LOAD_DATA_API : `load${EVENT_KEY}${DATA_API_KEY}`, CLICK_DATA_API : `click${EVENT_KEY}${DATA_API_KEY}` } @@ -98,6 +100,8 @@ const Carousel = (($) => { this._isPaused = false this._isSliding = false + this.touchTimeout = null + this._config = this._getConfig(config) this._element = $(element)[0] this._indicatorsElement = $(this._element).find(Selector.INDICATORS)[0] @@ -120,10 +124,9 @@ const Carousel = (($) => { // public next() { - if (this._isSliding) { - throw new Error('Carousel is sliding') + if (!this._isSliding) { + this._slide(Direction.NEXT) } - this._slide(Direction.NEXT) } nextWhenVisible() { @@ -134,10 +137,9 @@ const Carousel = (($) => { } prev() { - if (this._isSliding) { - throw new Error('Carousel is sliding') + if (!this._isSliding) { + this._slide(Direction.PREV) } - this._slide(Direction.PREVIOUS) } pause(event) { @@ -195,7 +197,7 @@ const Carousel = (($) => { const direction = index > activeIndex ? Direction.NEXT : - Direction.PREVIOUS + Direction.PREV this._slide(direction, this._items[index]) } @@ -229,11 +231,26 @@ const Carousel = (($) => { .on(Event.KEYDOWN, (event) => this._keydown(event)) } - if (this._config.pause === 'hover' && - !('ontouchstart' in document.documentElement)) { + if (this._config.pause === 'hover') { $(this._element) .on(Event.MOUSEENTER, (event) => this.pause(event)) .on(Event.MOUSELEAVE, (event) => this.cycle(event)) + if ('ontouchstart' in document.documentElement) { + // if it's a touch-enabled device, mouseenter/leave are fired as + // part of the mouse compatibility events on first tap - the carousel + // would stop cycling until user tapped out of it; + // here, we listen for touchend, explicitly pause the carousel + // (as if it's the second time we tap on it, mouseenter compat event + // is NOT fired) and after a timeout (to allow for mouse compatibility + // events to fire) we explicitly restart cycling + $(this._element).on(Event.TOUCHEND, () => { + this.pause() + if (this.touchTimeout) { + clearTimeout(this.touchTimeout) + } + this.touchTimeout = setTimeout((event) => this.cycle(event), TOUCHEVENT_COMPAT_WAIT + this._config.interval) + }) + } } } @@ -263,7 +280,7 @@ const Carousel = (($) => { _getItemByDirection(direction, activeElement) { const isNextDirection = direction === Direction.NEXT - const isPrevDirection = direction === Direction.PREVIOUS + const isPrevDirection = direction === Direction.PREV const activeIndex = this._getItemIndex(activeElement) const lastItemIndex = this._items.length - 1 const isGoingToWrap = isPrevDirection && activeIndex === 0 || @@ -273,7 +290,7 @@ const Carousel = (($) => { return activeElement } - const delta = direction === Direction.PREVIOUS ? -1 : 1 + const delta = direction === Direction.PREV ? -1 : 1 const itemIndex = (activeIndex + delta) % this._items.length return itemIndex === -1 ? @@ -282,9 +299,13 @@ const Carousel = (($) => { _triggerSlideEvent(relatedTarget, eventDirectionName) { + const targetIndex = this._getItemIndex(relatedTarget) + const fromIndex = this._getItemIndex($(this._element).find(Selector.ACTIVE_ITEM)[0]) const slideEvent = $.Event(Event.SLIDE, { relatedTarget, - direction: eventDirectionName + direction: eventDirectionName, + from: fromIndex, + to: targetIndex }) $(this._element).trigger(slideEvent) @@ -310,9 +331,10 @@ const Carousel = (($) => { _slide(direction, element) { const activeElement = $(this._element).find(Selector.ACTIVE_ITEM)[0] + const activeElementIndex = this._getItemIndex(activeElement) const nextElement = element || activeElement && this._getItemByDirection(direction, activeElement) - + const nextElementIndex = this._getItemIndex(nextElement) const isCycling = Boolean(this._interval) let directionalClassName @@ -354,7 +376,9 @@ const Carousel = (($) => { const slidEvent = $.Event(Event.SLID, { relatedTarget: nextElement, - direction: eventDirectionName + direction: eventDirectionName, + from: activeElementIndex, + to: nextElementIndex }) if (Util.supportsTransitionEnd() && diff --git a/js/src/collapse.js b/js/src/collapse.js index ad8815993..bf1c738f5 100644 --- a/js/src/collapse.js +++ b/js/src/collapse.js @@ -3,7 +3,7 @@ import Util from './util' /** * -------------------------------------------------------------------------- - * Bootstrap (v4.0.0-alpha.5): collapse.js + * Bootstrap (v4.0.0-alpha.6): collapse.js * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * -------------------------------------------------------------------------- */ @@ -18,7 +18,7 @@ const Collapse = (($) => { */ const NAME = 'collapse' - const VERSION = '4.0.0-alpha.5' + const VERSION = '4.0.0-alpha.6' const DATA_KEY = 'bs.collapse' const EVENT_KEY = `.${DATA_KEY}` const DATA_API_KEY = '.data-api' @@ -56,7 +56,7 @@ const Collapse = (($) => { } const Selector = { - ACTIVES : '.card > .show, .card > .collapsing', + ACTIVES : '.show, .collapsing', DATA_TOGGLE : '[data-toggle="collapse"]' } @@ -112,11 +112,8 @@ const Collapse = (($) => { } show() { - if (this._isTransitioning) { - throw new Error('Collapse is transitioning') - } - - if ($(this._element).hasClass(ClassName.SHOW)) { + if (this._isTransitioning || + $(this._element).hasClass(ClassName.SHOW)) { return } @@ -124,7 +121,7 @@ const Collapse = (($) => { let activesData if (this._parent) { - actives = $.makeArray($(this._parent).find(Selector.ACTIVES)) + actives = $.makeArray($(this._parent).children().children(Selector.ACTIVES)) if (!actives.length) { actives = null } @@ -157,7 +154,6 @@ const Collapse = (($) => { .addClass(ClassName.COLLAPSING) this._element.style[dimension] = 0 - this._element.setAttribute('aria-expanded', true) if (this._triggerArray.length) { $(this._triggerArray) @@ -196,11 +192,8 @@ const Collapse = (($) => { } hide() { - if (this._isTransitioning) { - throw new Error('Collapse is transitioning') - } - - if (!$(this._element).hasClass(ClassName.SHOW)) { + if (this._isTransitioning || + !$(this._element).hasClass(ClassName.SHOW)) { return } @@ -211,10 +204,8 @@ const Collapse = (($) => { } const dimension = this._getDimension() - const offsetDimension = dimension === Dimension.WIDTH ? - 'offsetWidth' : 'offsetHeight' - this._element.style[dimension] = `${this._element[offsetDimension]}px` + this._element.style[dimension] = `${this._element.getBoundingClientRect()[dimension]}px` Util.reflow(this._element) @@ -223,8 +214,6 @@ const Collapse = (($) => { .removeClass(ClassName.COLLAPSE) .removeClass(ClassName.SHOW) - this._element.setAttribute('aria-expanded', false) - if (this._triggerArray.length) { $(this._triggerArray) .addClass(ClassName.COLLAPSED) @@ -300,7 +289,6 @@ const Collapse = (($) => { _addAriaAndCollapsedClass(element, triggerArray) { if (element) { const isOpen = $(element).hasClass(ClassName.SHOW) - element.setAttribute('aria-expanded', isOpen) if (triggerArray.length) { $(triggerArray) @@ -357,7 +345,9 @@ const Collapse = (($) => { */ $(document).on(Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) { - event.preventDefault() + if (!/input|textarea/i.test(event.target.tagName)) { + event.preventDefault() + } const target = Collapse._getTargetFromElement(this) const data = $(target).data(DATA_KEY) diff --git a/js/src/dropdown.js b/js/src/dropdown.js index 97bba1c76..acc3ed453 100644 --- a/js/src/dropdown.js +++ b/js/src/dropdown.js @@ -1,15 +1,24 @@ +/* global Popper */ + import Util from './util' /** * -------------------------------------------------------------------------- - * Bootstrap (v4.0.0-alpha.5): dropdown.js + * Bootstrap (v4.0.0-alpha.6): dropdown.js * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * -------------------------------------------------------------------------- */ 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)') + } /** * ------------------------------------------------------------------------ @@ -18,15 +27,18 @@ const Dropdown = (($) => { */ const NAME = 'dropdown' - const VERSION = '4.0.0-alpha.5' + const VERSION = '4.0.0-alpha.6' const DATA_KEY = 'bs.dropdown' const EVENT_KEY = `.${DATA_KEY}` const DATA_API_KEY = '.data-api' const JQUERY_NO_CONFLICT = $.fn[NAME] const ESCAPE_KEYCODE = 27 // KeyboardEvent.which value for Escape (Esc) key + const SPACE_KEYCODE = 32 // KeyboardEvent.which value for space key + const TAB_KEYCODE = 9 // KeyboardEvent.which value for tab key const ARROW_UP_KEYCODE = 38 // KeyboardEvent.which value for up arrow key const ARROW_DOWN_KEYCODE = 40 // KeyboardEvent.which value for down arrow key const RIGHT_MOUSE_BUTTON_WHICH = 3 // MouseEvent.which value for the right button (assuming a right-handed mouse) + const REGEXP_KEYDOWN = new RegExp(`${ARROW_UP_KEYCODE}|${ARROW_DOWN_KEYCODE}|${ESCAPE_KEYCODE}`) const Event = { HIDE : `hide${EVENT_KEY}`, @@ -35,24 +47,43 @@ const Dropdown = (($) => { SHOWN : `shown${EVENT_KEY}`, CLICK : `click${EVENT_KEY}`, CLICK_DATA_API : `click${EVENT_KEY}${DATA_API_KEY}`, - KEYDOWN_DATA_API : `keydown${EVENT_KEY}${DATA_API_KEY}` + KEYDOWN_DATA_API : `keydown${EVENT_KEY}${DATA_API_KEY}`, + KEYUP_DATA_API : `keyup${EVENT_KEY}${DATA_API_KEY}` } const ClassName = { - BACKDROP : 'dropdown-backdrop', - DISABLED : 'disabled', - SHOW : 'show' + DISABLED : 'disabled', + SHOW : 'show', + DROPUP : 'dropup', + MENURIGHT : 'dropdown-menu-right', + MENULEFT : 'dropdown-menu-left' } const Selector = { - BACKDROP : '.dropdown-backdrop', DATA_TOGGLE : '[data-toggle="dropdown"]', FORM_CHILD : '.dropdown form', - ROLE_MENU : '[role="menu"]', - ROLE_LISTBOX : '[role="listbox"]', + MENU : '.dropdown-menu', NAVBAR_NAV : '.navbar-nav', - VISIBLE_ITEMS : '[role="menu"] li:not(.disabled) a, ' - + '[role="listbox"] li:not(.disabled) a' + 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,75 +111,163 @@ 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 - } - - if ('ontouchstart' in document.documentElement && - !$(parent).closest(Selector.NAVBAR_NAV).length) { - - // if mobile we use a backdrop because click events don't delegate - const dropdown = document.createElement('div') - dropdown.className = ClassName.BACKDROP - $(dropdown).insertBefore(this) - $(dropdown).on('click', Dropdown._clearMenus) + 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 } - this.focus() - this.setAttribute('aria-expanded', true) + 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 + } + } + }) - $(parent).toggleClass(ClassName.SHOW) - $(parent).trigger($.Event(Event.SHOWN, relatedTarget)) + // 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 + // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html + if ('ontouchstart' in document.documentElement && + !$(parent).closest(Selector.NAVBAR_NAV).length) { + $('body').children().on('mouseover', null, $.noop) + } - return false + this._element.focus() + this._element.setAttribute('aria-expanded', true) + + $(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) } @@ -153,36 +275,37 @@ const Dropdown = (($) => { if (data[config] === undefined) { throw new Error(`No method named "${config}"`) } - data[config].call(this) + data[config]() } }) } static _clearMenus(event) { - if (event && event.which === RIGHT_MOUSE_BUTTON_WHICH) { + if (event && (event.which === RIGHT_MOUSE_BUTTON_WHICH || + event.type === 'keyup' && event.which !== TAB_KEYCODE)) { return } - const backdrop = $(Selector.BACKDROP)[0] - if (backdrop) { - backdrop.parentNode.removeChild(backdrop) - } - 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 } - if (event && event.type === 'click' && - /input|textarea/i.test(event.target.tagName) && - $.contains(parent, event.target)) { + if (event && (event.type === 'click' && + /input|textarea/i.test(event.target.tagName) || event.type === 'keyup' && event.which === TAB_KEYCODE) + && $.contains(parent, event.target)) { continue } @@ -192,8 +315,15 @@ const Dropdown = (($) => { continue } + // if this is a touch-enabled device we remove the extra + // empty mouseover listeners we added for iOS support + if ('ontouchstart' in document.documentElement) { + $('body').children().off('mouseover', null, $.noop) + } + toggles[i].setAttribute('aria-expanded', 'false') + $(dropdownMenu).removeClass(ClassName.SHOW) $(parent) .removeClass(ClassName.SHOW) .trigger($.Event(Event.HIDDEN, relatedTarget)) @@ -212,7 +342,7 @@ const Dropdown = (($) => { } static _dataApiKeydownHandler(event) { - if (!/(38|40|27|32)/.test(event.which) || + if (!REGEXP_KEYDOWN.test(event.which) || /button/i.test(event.target.tagName) && event.which === SPACE_KEYCODE || /input|textarea/i.test(event.target.tagName)) { return } @@ -227,8 +357,8 @@ const Dropdown = (($) => { const parent = Dropdown._getParentFromElement(this) const isActive = $(parent).hasClass(ClassName.SHOW) - if (!isActive && event.which !== ESCAPE_KEYCODE || - isActive && event.which === ESCAPE_KEYCODE) { + if (!isActive && (event.which !== ESCAPE_KEYCODE || event.which !== SPACE_KEYCODE) || + isActive && (event.which === ESCAPE_KEYCODE || event.which === SPACE_KEYCODE)) { if (event.which === ESCAPE_KEYCODE) { const toggle = $(parent).find(Selector.DATA_TOGGLE)[0] @@ -273,10 +403,13 @@ const Dropdown = (($) => { $(document) .on(Event.KEYDOWN_DATA_API, Selector.DATA_TOGGLE, Dropdown._dataApiKeydownHandler) - .on(Event.KEYDOWN_DATA_API, Selector.ROLE_MENU, Dropdown._dataApiKeydownHandler) - .on(Event.KEYDOWN_DATA_API, Selector.ROLE_LISTBOX, Dropdown._dataApiKeydownHandler) - .on(Event.CLICK_DATA_API, Dropdown._clearMenus) - .on(Event.CLICK_DATA_API, Selector.DATA_TOGGLE, Dropdown.prototype.toggle) + .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, (e) => { e.stopPropagation() }) diff --git a/js/src/modal.js b/js/src/modal.js index 94abd19f4..02d463945 100644 --- a/js/src/modal.js +++ b/js/src/modal.js @@ -3,7 +3,7 @@ import Util from './util' /** * -------------------------------------------------------------------------- - * Bootstrap (v4.0.0-alpha.5): modal.js + * Bootstrap (v4.0.0-alpha.6): modal.js * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * -------------------------------------------------------------------------- */ @@ -18,7 +18,7 @@ const Modal = (($) => { */ const NAME = 'modal' - const VERSION = '4.0.0-alpha.5' + const VERSION = '4.0.0-alpha.6' const DATA_KEY = 'bs.modal' const EVENT_KEY = `.${DATA_KEY}` const DATA_API_KEY = '.data-api' @@ -67,7 +67,8 @@ const Modal = (($) => { DIALOG : '.modal-dialog', DATA_TOGGLE : '[data-toggle="modal"]', DATA_DISMISS : '[data-dismiss="modal"]', - FIXED_CONTENT : '.navbar-fixed-top, .navbar-fixed-bottom, .is-fixed' + FIXED_CONTENT : '.fixed-top, .fixed-bottom, .is-fixed, .sticky-top', + NAVBAR_TOGGLER : '.navbar-toggler' } @@ -87,7 +88,6 @@ const Modal = (($) => { this._isShown = false this._isBodyOverflowing = false this._ignoreBackdropClick = false - this._isTransitioning = false this._originalBodyPadding = 0 this._scrollbarWidth = 0 } @@ -112,13 +112,13 @@ const Modal = (($) => { show(relatedTarget) { if (this._isTransitioning) { - throw new Error('Modal is transitioning') + return } - if (Util.supportsTransitionEnd() && - $(this._element).hasClass(ClassName.FADE)) { + if (Util.supportsTransitionEnd() && $(this._element).hasClass(ClassName.FADE)) { this._isTransitioning = true } + const showEvent = $.Event(Event.SHOW, { relatedTarget }) @@ -161,17 +161,18 @@ const Modal = (($) => { event.preventDefault() } - if (this._isTransitioning) { - throw new Error('Modal is transitioning') + if (this._isTransitioning || !this._isShown) { + return } - const transition = Util.supportsTransitionEnd() && - $(this._element).hasClass(ClassName.FADE) + const transition = Util.supportsTransitionEnd() && $(this._element).hasClass(ClassName.FADE) + if (transition) { this._isTransitioning = true } const hideEvent = $.Event(Event.HIDE) + $(this._element).trigger(hideEvent) if (!this._isShown || hideEvent.isDefaultPrevented()) { @@ -191,6 +192,7 @@ const Modal = (($) => { $(this._dialog).off(Event.MOUSEDOWN_DISMISS) if (transition) { + $(this._element) .one(Util.TRANSITION_END, (event) => this._hideModal(event)) .emulateTransitionEnd(TRANSITION_DURATION) @@ -211,10 +213,12 @@ const Modal = (($) => { this._isShown = null this._isBodyOverflowing = null this._ignoreBackdropClick = null - this._originalBodyPadding = null this._scrollbarWidth = null } + handleUpdate() { + this._adjustDialog() + } // private @@ -285,6 +289,7 @@ const Modal = (($) => { if (this._isShown && this._config.keyboard) { $(this._element).on(Event.KEYDOWN_DISMISS, (event) => { if (event.which === ESCAPE_KEYCODE) { + event.preventDefault() this.hide() } }) @@ -296,7 +301,7 @@ const Modal = (($) => { _setResizeEvent() { if (this._isShown) { - $(window).on(Event.RESIZE, (event) => this._handleUpdate(event)) + $(window).on(Event.RESIZE, (event) => this.handleUpdate(event)) } else { $(window).off(Event.RESIZE) } @@ -304,7 +309,7 @@ const Modal = (($) => { _hideModal() { this._element.style.display = 'none' - this._element.setAttribute('aria-hidden', 'true') + this._element.setAttribute('aria-hidden', true) this._isTransitioning = false this._showBackdrop(() => { $(document.body).removeClass(ClassName.OPEN) @@ -401,10 +406,6 @@ const Modal = (($) => { // todo (fat): these should probably be refactored out of modal.js // ---------------------------------------------------------------------- - _handleUpdate() { - this._adjustDialog() - } - _adjustDialog() { const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight @@ -429,28 +430,60 @@ const Modal = (($) => { } _setScrollbar() { - const bodyPadding = parseInt( - $(Selector.FIXED_CONTENT).css('padding-right') || 0, - 10 - ) + if (this._isBodyOverflowing) { + // Note: DOMNode.style.paddingRight returns the actual value or '' if not set + // while $(DOMNode).css('padding-right') returns the calculated value or 0 if not set + + // Adjust fixed content padding + $(Selector.FIXED_CONTENT).each((index, element) => { + const actualPadding = $(element)[0].style.paddingRight + const calculatedPadding = $(element).css('padding-right') + $(element).data('padding-right', actualPadding).css('padding-right', `${parseFloat(calculatedPadding) + this._scrollbarWidth}px`) + }) - this._originalBodyPadding = document.body.style.paddingRight || '' + // Adjust navbar-toggler margin + $(Selector.NAVBAR_TOGGLER).each((index, element) => { + const actualMargin = $(element)[0].style.marginRight + const calculatedMargin = $(element).css('margin-right') + $(element).data('margin-right', actualMargin).css('margin-right', `${parseFloat(calculatedMargin) + this._scrollbarWidth}px`) + }) - if (this._isBodyOverflowing) { - document.body.style.paddingRight = - `${bodyPadding + this._scrollbarWidth}px` + // Adjust body padding + const actualPadding = document.body.style.paddingRight + const calculatedPadding = $('body').css('padding-right') + $('body').data('padding-right', actualPadding).css('padding-right', `${parseFloat(calculatedPadding) + this._scrollbarWidth}px`) } } _resetScrollbar() { - document.body.style.paddingRight = this._originalBodyPadding + // Restore fixed content padding + $(Selector.FIXED_CONTENT).each((index, element) => { + const padding = $(element).data('padding-right') + if (typeof padding !== 'undefined') { + $(element).css('padding-right', padding).removeData('padding-right') + } + }) + + // Restore navbar-toggler margin + $(Selector.NAVBAR_TOGGLER).each((index, element) => { + const margin = $(element).data('margin-right') + if (typeof margin !== 'undefined') { + $(element).css('margin-right', margin).removeData('margin-right') + } + }) + + // Restore body padding + const padding = $('body').data('padding-right') + if (typeof padding !== 'undefined') { + $('body').css('padding-right', padding).removeData('padding-right') + } } _getScrollbarWidth() { // thx d.walsh const scrollDiv = document.createElement('div') scrollDiv.className = ClassName.SCROLLBAR_MEASURER document.body.appendChild(scrollDiv) - const scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth + const scrollbarWidth = scrollDiv.getBoundingClientRect().width - scrollDiv.clientWidth document.body.removeChild(scrollDiv) return scrollbarWidth } diff --git a/js/src/popover.js b/js/src/popover.js index 33bc9e48c..a068420d6 100644 --- a/js/src/popover.js +++ b/js/src/popover.js @@ -3,7 +3,7 @@ import Tooltip from './tooltip' /** * -------------------------------------------------------------------------- - * Bootstrap (v4.0.0-alpha.5): popover.js + * Bootstrap (v4.0.0-alpha.6): popover.js * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * -------------------------------------------------------------------------- */ @@ -18,16 +18,19 @@ const Popover = (($) => { */ const NAME = 'popover' - const VERSION = '4.0.0-alpha.5' + const VERSION = '4.0.0-alpha.6' 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 : '<div class="popover" role="tooltip">' + + '<div class="arrow" x-arrow></div>' + '<h3 class="popover-title"></h3>' + '<div class="popover-content"></div></div>' }) @@ -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/scrollspy.js b/js/src/scrollspy.js index 0a4708bf9..7ea9f2483 100644 --- a/js/src/scrollspy.js +++ b/js/src/scrollspy.js @@ -3,7 +3,7 @@ import Util from './util' /** * -------------------------------------------------------------------------- - * Bootstrap (v4.0.0-alpha.5): scrollspy.js + * Bootstrap (v4.0.0-alpha.6): scrollspy.js * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * -------------------------------------------------------------------------- */ @@ -18,7 +18,7 @@ const ScrollSpy = (($) => { */ const NAME = 'scrollspy' - const VERSION = '4.0.0-alpha.5' + const VERSION = '4.0.0-alpha.6' const DATA_KEY = 'bs.scrollspy' const EVENT_KEY = `.${DATA_KEY}` const DATA_API_KEY = '.data-api' @@ -45,18 +45,15 @@ const ScrollSpy = (($) => { const ClassName = { DROPDOWN_ITEM : 'dropdown-item', DROPDOWN_MENU : 'dropdown-menu', - NAV_LINK : 'nav-link', - NAV : 'nav', ACTIVE : 'active' } const Selector = { DATA_SPY : '[data-spy="scroll"]', ACTIVE : '.active', - LIST_ITEM : '.list-item', - LI : 'li', - LI_DROPDOWN : 'li.dropdown', + NAV_LIST_GROUP : '.nav, .list-group', NAV_LINKS : '.nav-link', + LIST_ITEMS : '.list-group-item', DROPDOWN : '.dropdown', DROPDOWN_ITEMS : '.dropdown-item', DROPDOWN_TOGGLE : '.dropdown-toggle' @@ -81,6 +78,7 @@ const ScrollSpy = (($) => { this._scrollElement = element.tagName === 'BODY' ? window : element this._config = this._getConfig(config) this._selector = `${this._config.target} ${Selector.NAV_LINKS},` + + `${this._config.target} ${Selector.LIST_ITEMS},` + `${this._config.target} ${Selector.DROPDOWN_ITEMS}` this._offsets = [] this._targets = [] @@ -133,12 +131,15 @@ const ScrollSpy = (($) => { target = $(targetSelector)[0] } - if (target && (target.offsetWidth || target.offsetHeight)) { - // todo (fat): remove sketch reliance on jQuery position/offset - return [ - $(target)[offsetMethod]().top + offsetBase, - targetSelector - ] + if (target) { + const targetBCR = target.getBoundingClientRect() + if (targetBCR.width || targetBCR.height) { + // todo (fat): remove sketch reliance on jQuery position/offset + return [ + $(target)[offsetMethod]().top + offsetBase, + targetSelector + ] + } } return null }) @@ -186,7 +187,7 @@ const ScrollSpy = (($) => { _getScrollTop() { return this._scrollElement === window ? - this._scrollElement.scrollY : this._scrollElement.scrollTop + this._scrollElement.pageYOffset : this._scrollElement.scrollTop } _getScrollHeight() { @@ -198,7 +199,7 @@ const ScrollSpy = (($) => { _getOffsetHeight() { return this._scrollElement === window ? - window.innerHeight : this._scrollElement.offsetHeight + window.innerHeight : this._scrollElement.getBoundingClientRect().height } _process() { @@ -256,9 +257,11 @@ const ScrollSpy = (($) => { $link.closest(Selector.DROPDOWN).find(Selector.DROPDOWN_TOGGLE).addClass(ClassName.ACTIVE) $link.addClass(ClassName.ACTIVE) } else { - // todo (fat) this is kinda sus... - // recursively add actives to tested nav-links - $link.parents(Selector.LI).find(`> ${Selector.NAV_LINKS}`).addClass(ClassName.ACTIVE) + // Set triggered link as active + $link.addClass(ClassName.ACTIVE) + // Set triggered links parents as active + // With both <ul> and <nav> markup a parent is the previous sibling of any nav ancestor + $link.parents(Selector.NAV_LIST_GROUP).prev(`${Selector.NAV_LINKS}, ${Selector.LIST_ITEMS}`).addClass(ClassName.ACTIVE) } $(this._scrollElement).trigger(Event.ACTIVATE, { diff --git a/js/src/tab.js b/js/src/tab.js index 2f4e453e0..c7bc520df 100644 --- a/js/src/tab.js +++ b/js/src/tab.js @@ -3,7 +3,7 @@ import Util from './util' /** * -------------------------------------------------------------------------- - * Bootstrap (v4.0.0-alpha.5): tab.js + * Bootstrap (v4.0.0-alpha.6): tab.js * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * -------------------------------------------------------------------------- */ @@ -18,7 +18,7 @@ const Tab = (($) => { */ const NAME = 'tab' - const VERSION = '4.0.0-alpha.5' + const VERSION = '4.0.0-alpha.6' const DATA_KEY = 'bs.tab' const EVENT_KEY = `.${DATA_KEY}` const DATA_API_KEY = '.data-api' @@ -42,14 +42,10 @@ const Tab = (($) => { } const Selector = { - A : 'a', - LI : 'li', DROPDOWN : '.dropdown', - LIST : 'ul:not(.dropdown-menu), ol:not(.dropdown-menu)', - FADE_CHILD : '> .nav-item .fade, > .fade', + NAV_LIST_GROUP : '.nav, .list-group', ACTIVE : '.active', - ACTIVE_CHILD : '> .nav-item > .active, > .active', - DATA_TOGGLE : '[data-toggle="tab"], [data-toggle="pill"]', + DATA_TOGGLE : '[data-toggle="tab"], [data-toggle="pill"], [data-toggle="list"]', DROPDOWN_TOGGLE : '.dropdown-toggle', DROPDOWN_ACTIVE_CHILD : '> .dropdown-menu .active' } @@ -87,7 +83,7 @@ const Tab = (($) => { let target let previous - const listElement = $(this._element).closest(Selector.LIST)[0] + const listElement = $(this._element).closest(Selector.NAV_LIST_GROUP)[0] const selector = Util.getSelectorFromElement(this._element) if (listElement) { @@ -144,7 +140,7 @@ const Tab = (($) => { } dispose() { - $.removeClass(this._element, DATA_KEY) + $.removeData(this._element, DATA_KEY) this._element = null } @@ -152,11 +148,10 @@ const Tab = (($) => { // private _activate(element, container, callback) { - const active = $(container).find(Selector.ACTIVE_CHILD)[0] + const active = $(container).find(Selector.ACTIVE)[0] const isTransitioning = callback && Util.supportsTransitionEnd() - && (active && $(active).hasClass(ClassName.FADE) - || Boolean($(container).find(Selector.FADE_CHILD)[0])) + && (active && $(active).hasClass(ClassName.FADE)) const complete = () => this._transitionComplete( element, diff --git a/js/src/tooltip.js b/js/src/tooltip.js index 0c1d381b9..1d53b0470 100644 --- a/js/src/tooltip.js +++ b/js/src/tooltip.js @@ -1,11 +1,11 @@ -/* global Tether */ +/* global Popper */ import Util from './util' /** * -------------------------------------------------------------------------- - * Bootstrap (v4.0.0-alpha.5): tooltip.js + * Bootstrap (v4.0.0-alpha.6): tooltip.js * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * -------------------------------------------------------------------------- */ @@ -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)') } @@ -28,47 +28,50 @@ const Tooltip = (($) => { */ const NAME = 'tooltip' - const VERSION = '4.0.0-alpha.5' + const VERSION = '4.0.0-alpha.6' const DATA_KEY = 'bs.tooltip' const EVENT_KEY = `.${DATA_KEY}` const JQUERY_NO_CONFLICT = $.fn[NAME] const TRANSITION_DURATION = 150 - const CLASS_PREFIX = 'bs-tether' - - const Default = { - animation : true, - template : '<div class="tooltip" role="tooltip">' - + '<div class="tooltip-inner"></div></div>', - 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 : '<div class="tooltip" role="tooltip">' + + '<div class="arrow" x-arrow></div>' + + '<div class="tooltip-inner"></div></div>', + trigger : 'hover focus', + title : '', + delay : 0, + html : false, + selector : false, + placement : 'top', + offset : 0, + container : false, + fallbackPlacement : 'flip' } const HoverState = { @@ -99,11 +102,6 @@ const Tooltip = (($) => { TOOLTIP_INNER : '.tooltip-inner' } - const TetherClass = { - element : false, - enabled : false - } - const Trigger = { HOVER : 'hover', FOCUS : 'focus', @@ -123,12 +121,11 @@ const Tooltip = (($) => { constructor(element, config) { // private - this._isEnabled = true - this._timeout = 0 - this._hoverState = '' - this._activeTrigger = {} - this._isTransitioning = false - this._tether = null + this._isEnabled = true + this._timeout = 0 + this._hoverState = '' + this._activeTrigger = {} + 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 @@ -249,9 +247,6 @@ const Tooltip = (($) => { const showEvent = $.Event(this.constructor.Event.SHOW) if (this.isWithContent() && this._isEnabled) { - if (this._isTransitioning) { - throw new Error('Tooltip is transitioning') - } $(this.element).trigger(showEvent) const isInTheDom = $.contains( @@ -280,35 +275,54 @@ const Tooltip = (($) => { this.config.placement const attachment = this._getAttachment(placement) + this.addAttachmentClass(attachment) const container = this.config.container === false ? document.body : $(this.config.container) - $(tip) - .data(this.constructor.DATA_KEY, this) - .appendTo(container) + $(tip).data(this.constructor.DATA_KEY, this) + + if (!$.contains(this.element.ownerDocument.documentElement, this.tip)) { + $(tip).appendTo(container) + } $(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 + // empty mouseover listeners to the body's immediate children; + // only needed because of broken event delegation on iOS + // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html + if ('ontouchstart' in document.documentElement) { + $('body').children().on('mouseover', null, $.noop) + } + const complete = () => { + if (this.config.animation) { + this._fixTransition() + } const prevHoverState = this._hoverState - this._hoverState = null - this._isTransitioning = false + this._hoverState = null $(this.element).trigger(this.constructor.Event.SHOWN) @@ -318,32 +332,29 @@ const Tooltip = (($) => { } if (Util.supportsTransitionEnd() && $(this.tip).hasClass(ClassName.FADE)) { - this._isTransitioning = true $(this.tip) .one(Util.TRANSITION_END, complete) .emulateTransitionEnd(Tooltip._TRANSITION_DURATION) - return + } else { + complete() } - - complete() } } hide(callback) { const tip = this.getTipElement() const hideEvent = $.Event(this.constructor.Event.HIDE) - if (this._isTransitioning) { - throw new Error('Tooltip is transitioning') - } const complete = () => { if (this._hoverState !== HoverState.SHOW && tip.parentNode) { tip.parentNode.removeChild(tip) } + this._cleanTipClass() this.element.removeAttribute('aria-describedby') $(this.element).trigger(this.constructor.Event.HIDDEN) - this._isTransitioning = false - this.cleanupTether() + if (this._popper !== null) { + this._popper.destroy() + } if (callback) { callback() @@ -358,13 +369,19 @@ const Tooltip = (($) => { $(tip).removeClass(ClassName.SHOW) + // if this is a touch-enabled device we remove the extra + // empty mouseover listeners we added for iOS support + if ('ontouchstart' in document.documentElement) { + $('body').children().off('mouseover', null, $.noop) + } + this._activeTrigger[Trigger.CLICK] = false this._activeTrigger[Trigger.FOCUS] = false this._activeTrigger[Trigger.HOVER] = false if (Util.supportsTransitionEnd() && $(this.tip).hasClass(ClassName.FADE)) { - this._isTransitioning = true + $(tip) .one(Util.TRANSITION_END, complete) .emulateTransitionEnd(TRANSITION_DURATION) @@ -374,8 +391,14 @@ const Tooltip = (($) => { } this._hoverState = '' + } + update() { + if (this._popper !== null) { + this._popper.scheduleUpdate() + } + } // protected @@ -383,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) { @@ -425,12 +448,6 @@ const Tooltip = (($) => { return title } - cleanupTether() { - if (this._tether) { - this._tether.destroy() - } - } - // private @@ -603,6 +620,14 @@ const Tooltip = (($) => { } } + if (config.title && typeof config.title === 'number') { + config.title = config.title.toString() + } + + if (config.content && typeof config.content === 'number') { + config.content = config.content.toString() + } + Util.typeCheckConfig( NAME, config, @@ -626,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/src/util.js b/js/src/util.js index 06424fbfe..3c0d02251 100644 --- a/js/src/util.js +++ b/js/src/util.js @@ -1,6 +1,6 @@ /** * -------------------------------------------------------------------------- - * Bootstrap (v4.0.0-alpha.5): util.js + * Bootstrap (v4.0.0-alpha.6): util.js * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * -------------------------------------------------------------------------- */ @@ -112,13 +112,16 @@ const Util = (($) => { getSelectorFromElement(element) { let selector = element.getAttribute('data-target') - - if (!selector) { + if (!selector || selector === '#') { selector = element.getAttribute('href') || '' - selector = /^#[a-z]/i.test(selector) ? selector : null } - return selector + try { + const $selector = $(selector) + return $selector.length > 0 ? selector : null + } catch (error) { + return null + } }, reflow(element) { |
