aboutsummaryrefslogtreecommitdiff
path: root/js/src
diff options
context:
space:
mode:
authorMark Otto <[email protected]>2017-05-27 15:26:48 -0700
committerMark Otto <[email protected]>2017-05-27 15:26:48 -0700
commit8f67ac19a761b8d3ecc80485c3f54aa6ba4fa910 (patch)
treed5a2aba4e2e762b283e7cfe60f00e1f239105bbe /js/src
parent6c3f833076a9fa68601741e3e21bd07ad79b7d8a (diff)
parentdb44e4b311317ef760f8412cc33c84146959b248 (diff)
downloadbootstrap-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.js4
-rw-r--r--js/src/button.js16
-rw-r--r--js/src/carousel.js72
-rw-r--r--js/src/collapse.js34
-rw-r--r--js/src/dropdown.js243
-rw-r--r--js/src/modal.js89
-rw-r--r--js/src/popover.js21
-rw-r--r--js/src/scrollspy.js39
-rw-r--r--js/src/tab.js21
-rw-r--r--js/src/tooltip.js234
-rw-r--r--js/src/util.js13
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) {