aboutsummaryrefslogtreecommitdiff
path: root/js/src
diff options
context:
space:
mode:
authorGijs Boddeus <[email protected]>2017-08-15 23:43:36 +0200
committerGitHub <[email protected]>2017-08-15 23:43:36 +0200
commit06d4c6d273daf3eb84c9c5bb6306cecc9209304f (patch)
tree695d8fb5b2f414eae0bac032369f77285f594803 /js/src
parent1fb6d8c46a560e2e35295440721ba2929f9721b6 (diff)
parent7b873fa0a15c0fb62671f95e966656967c6fd9b4 (diff)
downloadbootstrap-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.js12
-rw-r--r--js/src/button.js16
-rw-r--r--js/src/carousel.js105
-rw-r--r--js/src/collapse.js69
-rw-r--r--js/src/dropdown.js266
-rw-r--r--js/src/modal.js103
-rw-r--r--js/src/popover.js35
-rw-r--r--js/src/scrollspy.js39
-rw-r--r--js/src/tab.js42
-rw-r--r--js/src/tooltip.js250
-rw-r--r--js/src/util.js13
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) {