diff options
| author | Gijs Boddeus <[email protected]> | 2017-08-15 23:43:36 +0200 |
|---|---|---|
| committer | GitHub <[email protected]> | 2017-08-15 23:43:36 +0200 |
| commit | 06d4c6d273daf3eb84c9c5bb6306cecc9209304f (patch) | |
| tree | 695d8fb5b2f414eae0bac032369f77285f594803 /js/src | |
| parent | 1fb6d8c46a560e2e35295440721ba2929f9721b6 (diff) | |
| parent | 7b873fa0a15c0fb62671f95e966656967c6fd9b4 (diff) | |
| download | bootstrap-06d4c6d273daf3eb84c9c5bb6306cecc9209304f.tar.xz bootstrap-06d4c6d273daf3eb84c9c5bb6306cecc9209304f.zip | |
Merge pull request #1 from twbs/v4-dev
updating fork of BS to v4-beta1
Diffstat (limited to 'js/src')
| -rw-r--r-- | js/src/alert.js | 12 | ||||
| -rw-r--r-- | js/src/button.js | 16 | ||||
| -rw-r--r-- | js/src/carousel.js | 105 | ||||
| -rw-r--r-- | js/src/collapse.js | 69 | ||||
| -rw-r--r-- | js/src/dropdown.js | 266 | ||||
| -rw-r--r-- | js/src/modal.js | 103 | ||||
| -rw-r--r-- | js/src/popover.js | 35 | ||||
| -rw-r--r-- | js/src/scrollspy.js | 39 | ||||
| -rw-r--r-- | js/src/tab.js | 42 | ||||
| -rw-r--r-- | js/src/tooltip.js | 250 | ||||
| -rw-r--r-- | js/src/util.js | 13 |
11 files changed, 654 insertions, 296 deletions
diff --git a/js/src/alert.js b/js/src/alert.js index 8e5524950..b6b9336af 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-beta): 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-beta' const DATA_KEY = 'bs.alert' const EVENT_KEY = `.${DATA_KEY}` const DATA_API_KEY = '.data-api' @@ -36,9 +36,9 @@ const Alert = (($) => { } const ClassName = { - ALERT : 'alert', - FADE : 'fade', - ACTIVE : 'active' + ALERT : 'alert', + FADE : 'fade', + SHOW : 'show' } @@ -108,7 +108,7 @@ const Alert = (($) => { } _removeElement(element) { - $(element).removeClass(ClassName.ACTIVE) + $(element).removeClass(ClassName.SHOW) if (!Util.supportsTransitionEnd() || !$(element).hasClass(ClassName.FADE)) { diff --git a/js/src/button.js b/js/src/button.js index 45e1424ff..a8a72ef56 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-beta): 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-beta' 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 e56d4f0f2..a5d5f143a 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-beta): 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-beta' + 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, @@ -45,7 +46,9 @@ const Carousel = (($) => { const Direction = { NEXT : 'next', - PREVIOUS : 'prev' + PREV : 'prev', + LEFT : 'left', + RIGHT : 'right' } const Event = { @@ -54,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}` } @@ -62,8 +66,10 @@ const Carousel = (($) => { CAROUSEL : 'carousel', ACTIVE : 'active', SLIDE : 'slide', - RIGHT : 'right', - LEFT : 'left', + RIGHT : 'carousel-item-right', + LEFT : 'carousel-item-left', + NEXT : 'carousel-item-next', + PREV : 'carousel-item-prev', ITEM : 'carousel-item' } @@ -71,7 +77,7 @@ const Carousel = (($) => { ACTIVE : '.active', ACTIVE_ITEM : '.active.carousel-item', ITEM : '.carousel-item', - NEXT_PREV : '.next, .prev', + NEXT_PREV : '.carousel-item-next, .carousel-item-prev', INDICATORS : '.carousel-indicators', DATA_SLIDE : '[data-slide], [data-slide-to]', DATA_RIDE : '[data-ride="carousel"]' @@ -94,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] @@ -130,7 +138,7 @@ const Carousel = (($) => { prev() { if (!this._isSliding) { - this._slide(Direction.PREVIOUS) + this._slide(Direction.PREV) } } @@ -189,7 +197,7 @@ const Carousel = (($) => { const direction = index > activeIndex ? Direction.NEXT : - Direction.PREVIOUS + Direction.PREV this._slide(direction, this._items[index]) } @@ -223,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) + }) + } } } @@ -235,13 +258,14 @@ const Carousel = (($) => { if (/input|textarea/i.test(event.target.tagName)) { return } - event.preventDefault() switch (event.which) { case ARROW_LEFT_KEYCODE: + event.preventDefault() this.prev() break case ARROW_RIGHT_KEYCODE: + event.preventDefault() this.next() break default: @@ -256,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 || @@ -266,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 ? @@ -274,10 +298,14 @@ const Carousel = (($) => { } - _triggerSlideEvent(relatedTarget, directionalClassname) { + _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: directionalClassname + direction: eventDirectionName, + from: fromIndex, + to: targetIndex }) $(this._element).trigger(slideEvent) @@ -303,21 +331,32 @@ 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) - const directionalClassName = direction === Direction.NEXT ? - ClassName.LEFT : - ClassName.RIGHT + let directionalClassName + let orderClassName + let eventDirectionName + + if (direction === Direction.NEXT) { + directionalClassName = ClassName.LEFT + orderClassName = ClassName.NEXT + eventDirectionName = Direction.LEFT + } else { + directionalClassName = ClassName.RIGHT + orderClassName = ClassName.PREV + eventDirectionName = Direction.RIGHT + } if (nextElement && $(nextElement).hasClass(ClassName.ACTIVE)) { this._isSliding = false return } - const slideEvent = this._triggerSlideEvent(nextElement, directionalClassName) + const slideEvent = this._triggerSlideEvent(nextElement, eventDirectionName) if (slideEvent.isDefaultPrevented()) { return } @@ -337,13 +376,15 @@ const Carousel = (($) => { const slidEvent = $.Event(Event.SLID, { relatedTarget: nextElement, - direction: directionalClassName + direction: eventDirectionName, + from: activeElementIndex, + to: nextElementIndex }) if (Util.supportsTransitionEnd() && $(this._element).hasClass(ClassName.SLIDE)) { - $(nextElement).addClass(direction) + $(nextElement).addClass(orderClassName) Util.reflow(nextElement) @@ -353,10 +394,10 @@ const Carousel = (($) => { $(activeElement) .one(Util.TRANSITION_END, () => { $(nextElement) - .removeClass(`${directionalClassName} ${direction}`) + .removeClass(`${directionalClassName} ${orderClassName}`) .addClass(ClassName.ACTIVE) - $(activeElement).removeClass(`${ClassName.ACTIVE} ${direction} ${directionalClassName}`) + $(activeElement).removeClass(`${ClassName.ACTIVE} ${orderClassName} ${directionalClassName}`) this._isSliding = false diff --git a/js/src/collapse.js b/js/src/collapse.js index ebc3e24cf..2f00b98cb 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-beta): 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-beta' const DATA_KEY = 'bs.collapse' const EVENT_KEY = `.${DATA_KEY}` const DATA_API_KEY = '.data-api' @@ -44,7 +44,7 @@ const Collapse = (($) => { } const ClassName = { - ACTIVE : 'active', + SHOW : 'show', COLLAPSE : 'collapse', COLLAPSING : 'collapsing', COLLAPSED : 'collapsed' @@ -56,7 +56,7 @@ const Collapse = (($) => { } const Selector = { - ACTIVES : '.card > .active, .card > .collapsing', + ACTIVES : '.show, .collapsing', DATA_TOGGLE : '[data-toggle="collapse"]' } @@ -77,6 +77,14 @@ const Collapse = (($) => { `[data-toggle="collapse"][href="#${element.id}"],` + `[data-toggle="collapse"][data-target="#${element.id}"]` )) + const tabToggles = $(Selector.DATA_TOGGLE) + for (let i = 0; i < tabToggles.length; i++) { + const elem = tabToggles[i] + const selector = Util.getSelectorFromElement(elem) + if (selector !== null && $(selector).filter(element).length > 0) { + this._triggerArray.push(elem) + } + } this._parent = this._config.parent ? this._getParent() : null @@ -104,7 +112,7 @@ const Collapse = (($) => { // public toggle() { - if ($(this._element).hasClass(ClassName.ACTIVE)) { + if ($(this._element).hasClass(ClassName.SHOW)) { this.hide() } else { this.show() @@ -113,7 +121,7 @@ const Collapse = (($) => { show() { if (this._isTransitioning || - $(this._element).hasClass(ClassName.ACTIVE)) { + $(this._element).hasClass(ClassName.SHOW)) { return } @@ -121,7 +129,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 } @@ -154,7 +162,6 @@ const Collapse = (($) => { .addClass(ClassName.COLLAPSING) this._element.style[dimension] = 0 - this._element.setAttribute('aria-expanded', true) if (this._triggerArray.length) { $(this._triggerArray) @@ -168,7 +175,7 @@ const Collapse = (($) => { $(this._element) .removeClass(ClassName.COLLAPSING) .addClass(ClassName.COLLAPSE) - .addClass(ClassName.ACTIVE) + .addClass(ClassName.SHOW) this._element.style[dimension] = '' @@ -194,7 +201,7 @@ const Collapse = (($) => { hide() { if (this._isTransitioning || - !$(this._element).hasClass(ClassName.ACTIVE)) { + !$(this._element).hasClass(ClassName.SHOW)) { return } @@ -205,24 +212,28 @@ 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) $(this._element) .addClass(ClassName.COLLAPSING) .removeClass(ClassName.COLLAPSE) - .removeClass(ClassName.ACTIVE) - - this._element.setAttribute('aria-expanded', false) + .removeClass(ClassName.SHOW) if (this._triggerArray.length) { - $(this._triggerArray) - .addClass(ClassName.COLLAPSED) - .attr('aria-expanded', false) + for (let i = 0; i < this._triggerArray.length; i++) { + const trigger = this._triggerArray[i] + const selector = Util.getSelectorFromElement(trigger) + if (selector !== null) { + const $elem = $(selector) + if (!$elem.hasClass(ClassName.SHOW)) { + $(trigger).addClass(ClassName.COLLAPSED) + .attr('aria-expanded', false) + } + } + } } this.setTransitioning(true) @@ -293,8 +304,7 @@ const Collapse = (($) => { _addAriaAndCollapsedClass(element, triggerArray) { if (element) { - const isOpen = $(element).hasClass(ClassName.ACTIVE) - element.setAttribute('aria-expanded', isOpen) + const isOpen = $(element).hasClass(ClassName.SHOW) if (triggerArray.length) { $(triggerArray) @@ -351,13 +361,18 @@ const Collapse = (($) => { */ $(document).on(Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) { - event.preventDefault() - - const target = Collapse._getTargetFromElement(this) - const data = $(target).data(DATA_KEY) - const config = data ? 'toggle' : $(this).data() + if (!/input|textarea/i.test(event.target.tagName)) { + event.preventDefault() + } - Collapse._jQueryInterface.call($(target), config) + const $trigger = $(this) + const selector = Util.getSelectorFromElement(this) + $(selector).each(function () { + const $target = $(this) + const data = $target.data(DATA_KEY) + const config = data ? 'toggle' : $trigger.data() + Collapse._jQueryInterface.call($target, config) + }) }) diff --git a/js/src/dropdown.js b/js/src/dropdown.js index 8b2164aa9..5e792a527 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-beta): 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-beta' 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', - ACTIVE : 'active' + 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,12 @@ const Dropdown = (($) => { class Dropdown { - constructor(element) { - this._element = element + constructor(element, config) { + this._element = element + this._popper = null + this._config = this._getConfig(config) + this._menu = this._getMenuElement() + this._inNavbar = this._detectNavbar() this._addEventListeners() } @@ -77,75 +112,177 @@ 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.ACTIVE) + 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, this._getPopperConfig()) - $(parent).toggleClass(ClassName.ACTIVE) - $(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() { + this._inNavbar = this._detectNavbar() + 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 + } + + _detectNavbar() { + return $(this._element).closest('.navbar').length > 0 + } + + _getPopperConfig() { + const popperConfig = { + placement : this._getPlacement(), + modifiers : { + offset : { + offset : this._config.offset + }, + flip : { + enabled : this._config.flip + } + } + } + + // Disable Popper.js for Dropdown in Navbar + if (this._inNavbar) { + popperConfig.modifiers.applyStyle = { + enabled: !this._inNavbar + } + } + return popperConfig + } // 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 +290,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 (!$(parent).hasClass(ClassName.ACTIVE)) { + if (!context) { continue } - if (event && event.type === 'click' && - /input|textarea/i.test(event.target.tagName) && - $.contains(parent, event.target)) { + const dropdownMenu = context._menu + if (!$(parent).hasClass(ClassName.SHOW)) { + continue + } + + 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,10 +330,17 @@ 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.ACTIVE) + .removeClass(ClassName.SHOW) .trigger($.Event(Event.HIDDEN, relatedTarget)) } } @@ -212,7 +357,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 } @@ -225,10 +370,10 @@ const Dropdown = (($) => { } const parent = Dropdown._getParentFromElement(this) - const isActive = $(parent).hasClass(ClassName.ACTIVE) + 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 +418,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 61a28dbf5..9f17fafc8 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-beta): 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-beta' const DATA_KEY = 'bs.modal' const EVENT_KEY = `.${DATA_KEY}` const DATA_API_KEY = '.data-api' @@ -60,14 +60,15 @@ const Modal = (($) => { BACKDROP : 'modal-backdrop', OPEN : 'modal-open', FADE : 'fade', - ACTIVE : 'active' + SHOW : 'show' } const Selector = { 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' } @@ -110,6 +111,14 @@ const Modal = (($) => { } show(relatedTarget) { + if (this._isTransitioning) { + return + } + + if (Util.supportsTransitionEnd() && $(this._element).hasClass(ClassName.FADE)) { + this._isTransitioning = true + } + const showEvent = $.Event(Event.SHOW, { relatedTarget }) @@ -152,6 +161,16 @@ const Modal = (($) => { event.preventDefault() } + if (this._isTransitioning || !this._isShown) { + return + } + + const transition = Util.supportsTransitionEnd() && $(this._element).hasClass(ClassName.FADE) + + if (transition) { + this._isTransitioning = true + } + const hideEvent = $.Event(Event.HIDE) $(this._element).trigger(hideEvent) @@ -167,13 +186,12 @@ const Modal = (($) => { $(document).off(Event.FOCUSIN) - $(this._element).removeClass(ClassName.ACTIVE) + $(this._element).removeClass(ClassName.SHOW) $(this._element).off(Event.CLICK_DISMISS) $(this._dialog).off(Event.MOUSEDOWN_DISMISS) - if (Util.supportsTransitionEnd() && - $(this._element).hasClass(ClassName.FADE)) { + if (transition) { $(this._element) .one(Util.TRANSITION_END, (event) => this._hideModal(event)) @@ -195,10 +213,12 @@ const Modal = (($) => { this._isShown = null this._isBodyOverflowing = null this._ignoreBackdropClick = null - this._originalBodyPadding = null this._scrollbarWidth = null } + handleUpdate() { + this._adjustDialog() + } // private @@ -226,7 +246,7 @@ const Modal = (($) => { Util.reflow(this._element) } - $(this._element).addClass(ClassName.ACTIVE) + $(this._element).addClass(ClassName.SHOW) if (this._config.focus) { this._enforceFocus() @@ -240,6 +260,7 @@ const Modal = (($) => { if (this._config.focus) { this._element.focus() } + this._isTransitioning = false $(this._element).trigger(shownEvent) } @@ -268,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() } }) @@ -279,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) } @@ -288,6 +310,7 @@ const Modal = (($) => { _hideModal() { this._element.style.display = 'none' this._element.setAttribute('aria-hidden', true) + this._isTransitioning = false this._showBackdrop(() => { $(document.body).removeClass(ClassName.OPEN) this._resetAdjustments() @@ -338,7 +361,7 @@ const Modal = (($) => { Util.reflow(this._backdrop) } - $(this._backdrop).addClass(ClassName.ACTIVE) + $(this._backdrop).addClass(ClassName.SHOW) if (!callback) { return @@ -354,7 +377,7 @@ const Modal = (($) => { .emulateTransitionEnd(BACKDROP_TRANSITION_DURATION) } else if (!this._isShown && this._backdrop) { - $(this._backdrop).removeClass(ClassName.ACTIVE) + $(this._backdrop).removeClass(ClassName.SHOW) const callbackRemove = () => { this._removeBackdrop() @@ -383,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 @@ -411,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 a08ed4de9..0e8953f77 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-beta): popover.js * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * -------------------------------------------------------------------------- */ @@ -18,18 +18,21 @@ const Popover = (($) => { */ const NAME = 'popover' - const VERSION = '4.0.0-alpha.5' + const VERSION = '4.0.0-beta' 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">' - + '<h3 class="popover-title"></h3>' - + '<div class="popover-content"></div></div>' + + '<div class="arrow"></div>' + + '<h3 class="popover-header"></h3>' + + '<div class="popover-body"></div></div>' }) const DefaultType = $.extend({}, Tooltip.DefaultType, { @@ -37,13 +40,13 @@ const Popover = (($) => { }) const ClassName = { - FADE : 'fade', - ACTIVE : 'active' + FADE : 'fade', + SHOW : 'show' } const Selector = { - TITLE : '.popover-title', - CONTENT : '.popover-content' + TITLE : '.popover-header', + CONTENT : '.popover-body' } const Event = { @@ -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] } @@ -117,9 +124,7 @@ const Popover = (($) => { this.setElementContent($tip.find(Selector.TITLE), this.getTitle()) this.setElementContent($tip.find(Selector.CONTENT), this._getContent()) - $tip.removeClass(`${ClassName.FADE} ${ClassName.ACTIVE}`) - - this.cleanupTether() + $tip.removeClass(`${ClassName.FADE} ${ClassName.SHOW}`) } // 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 9cb1438ca..b70b7e477 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-beta): 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-beta' 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 e7e8f550f..4c3091495 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-beta): 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-beta' const DATA_KEY = 'bs.tab' const EVENT_KEY = `.${DATA_KEY}` const DATA_API_KEY = '.data-api' @@ -36,19 +36,17 @@ const Tab = (($) => { const ClassName = { DROPDOWN_MENU : 'dropdown-menu', ACTIVE : 'active', + DISABLED : 'disabled', FADE : 'fade', - IN : 'in' + SHOW : 'show' } 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"]', + ACTIVE_UL : '> li > .active', + DATA_TOGGLE : '[data-toggle="tab"], [data-toggle="pill"], [data-toggle="list"]', DROPDOWN_TOGGLE : '.dropdown-toggle', DROPDOWN_ACTIVE_CHILD : '> .dropdown-menu .active' } @@ -79,17 +77,19 @@ const Tab = (($) => { show() { if (this._element.parentNode && this._element.parentNode.nodeType === Node.ELEMENT_NODE && - $(this._element).hasClass(ClassName.ACTIVE)) { + $(this._element).hasClass(ClassName.ACTIVE) || + $(this._element).hasClass(ClassName.DISABLED)) { return } 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) { - previous = $.makeArray($(listElement).find(Selector.ACTIVE)) + const itemSelector = listElement.nodeName === 'UL' ? Selector.ACTIVE_UL : Selector.ACTIVE + previous = $.makeArray($(listElement).find(itemSelector)) previous = previous[previous.length - 1] } @@ -142,7 +142,7 @@ const Tab = (($) => { } dispose() { - $.removeClass(this._element, DATA_KEY) + $.removeData(this._element, DATA_KEY) this._element = null } @@ -150,11 +150,17 @@ const Tab = (($) => { // private _activate(element, container, callback) { - const active = $(container).find(Selector.ACTIVE_CHILD)[0] + let activeElements + if (container.nodeName === 'UL') { + activeElements = $(container).find(Selector.ACTIVE_UL) + } else { + activeElements = $(container).children(Selector.ACTIVE) + } + + const active = activeElements[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, @@ -173,7 +179,7 @@ const Tab = (($) => { } if (active) { - $(active).removeClass(ClassName.IN) + $(active).removeClass(ClassName.SHOW) } } @@ -197,7 +203,7 @@ const Tab = (($) => { if (isTransitioning) { Util.reflow(element) - $(element).addClass(ClassName.IN) + $(element).addClass(ClassName.SHOW) } else { $(element).removeClass(ClassName.FADE) } diff --git a/js/src/tooltip.js b/js/src/tooltip.js index 2b659b885..2060cebbb 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-beta): 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 (window.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,52 +28,55 @@ const Tooltip = (($) => { */ const NAME = 'tooltip' - const VERSION = '4.0.0-alpha.5' + const VERSION = '4.0.0-beta' 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"></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 = { - ACTIVE : 'active', - OUT : 'out' + SHOW : 'show', + OUT : 'out' } const Event = { @@ -90,18 +93,14 @@ const Tooltip = (($) => { } const ClassName = { - FADE : 'fade', - ACTIVE : 'active' + FADE : 'fade', + SHOW : 'show' } const Selector = { TOOLTIP : '.tooltip', - TOOLTIP_INNER : '.tooltip-inner' - } - - const TetherClass = { - element : false, - enabled : false + TOOLTIP_INNER : '.tooltip-inner', + ARROW : '.arrow' } const Trigger = { @@ -127,7 +126,7 @@ const Tooltip = (($) => { this._timeout = 0 this._hoverState = '' this._activeTrigger = {} - this._tether = null + this._popper = null // protected this.element = element @@ -207,7 +206,7 @@ const Tooltip = (($) => { } else { - if ($(this.getTipElement()).hasClass(ClassName.ACTIVE)) { + if ($(this.getTipElement()).hasClass(ClassName.SHOW)) { this._leave(null, this) return } @@ -219,8 +218,6 @@ const Tooltip = (($) => { dispose() { clearTimeout(this._timeout) - this.cleanupTether() - $.removeData(this.element, this.constructor.DATA_KEY) $(this.element).off(this.constructor.EVENT_KEY) @@ -234,7 +231,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 @@ -245,8 +245,8 @@ const Tooltip = (($) => { if ($(this.element).css('display') === 'none') { throw new Error('Please use show on visible elements') } - const showEvent = $.Event(this.constructor.Event.SHOW) + const showEvent = $.Event(this.constructor.Event.SHOW) if (this.isWithContent() && this._isEnabled) { $(this.element).trigger(showEvent) @@ -276,32 +276,55 @@ 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 + }, + arrow: { + element: Selector.ARROW + } + }, + 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) - $(tip).addClass(ClassName.ACTIVE) + // 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 @@ -316,10 +339,9 @@ const Tooltip = (($) => { $(this.tip) .one(Util.TRANSITION_END, complete) .emulateTransitionEnd(Tooltip._TRANSITION_DURATION) - return + } else { + complete() } - - complete() } } @@ -327,13 +349,16 @@ const Tooltip = (($) => { const tip = this.getTipElement() const hideEvent = $.Event(this.constructor.Event.HIDE) const complete = () => { - if (this._hoverState !== HoverState.ACTIVE && tip.parentNode) { + 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.cleanupTether() + if (this._popper !== null) { + this._popper.destroy() + } if (callback) { callback() @@ -346,7 +371,17 @@ const Tooltip = (($) => { return } - $(tip).removeClass(ClassName.ACTIVE) + $(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)) { @@ -360,8 +395,14 @@ const Tooltip = (($) => { } this._hoverState = '' + } + update() { + if (this._popper !== null) { + this._popper.scheduleUpdate() + } + } // protected @@ -369,18 +410,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.ACTIVE}`) - - this.cleanupTether() + $tip.removeClass(`${ClassName.FADE} ${ClassName.SHOW}`) } setElementContent($element, content) { @@ -411,12 +452,6 @@ const Tooltip = (($) => { return title } - cleanupTether() { - if (this._tether) { - this._tether.destroy() - } - } - // private @@ -503,15 +538,15 @@ const Tooltip = (($) => { ] = true } - if ($(context.getTipElement()).hasClass(ClassName.ACTIVE) || - context._hoverState === HoverState.ACTIVE) { - context._hoverState = HoverState.ACTIVE + if ($(context.getTipElement()).hasClass(ClassName.SHOW) || + context._hoverState === HoverState.SHOW) { + context._hoverState = HoverState.SHOW return } clearTimeout(context._timeout) - context._hoverState = HoverState.ACTIVE + context._hoverState = HoverState.SHOW if (!context.config.delay || !context.config.delay.show) { context.show() @@ -519,7 +554,7 @@ const Tooltip = (($) => { } context._timeout = setTimeout(() => { - if (context._hoverState === HoverState.ACTIVE) { + if (context._hoverState === HoverState.SHOW) { context.show() } }, context.config.delay.show) @@ -589,6 +624,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, @@ -612,6 +655,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..387c7d2ed 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-beta): 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) { |
