aboutsummaryrefslogtreecommitdiff
path: root/js/src
diff options
context:
space:
mode:
Diffstat (limited to 'js/src')
-rw-r--r--js/src/dropdown.js177
-rw-r--r--js/src/popover.js17
-rw-r--r--js/src/tooltip.js185
3 files changed, 275 insertions, 104 deletions
diff --git a/js/src/dropdown.js b/js/src/dropdown.js
index eb536dc7d..acc3ed453 100644
--- a/js/src/dropdown.js
+++ b/js/src/dropdown.js
@@ -1,3 +1,5 @@
+/* global Popper */
+
import Util from './util'
@@ -10,6 +12,13 @@ import Util from './util'
const Dropdown = (($) => {
+ /**
+ * Check for Popper dependency
+ * Popper - https://popper.js.org
+ */
+ if (typeof Popper === 'undefined') {
+ throw new Error('Bootstrap dropdown require Popper.js (https://popper.js.org)')
+ }
/**
* ------------------------------------------------------------------------
@@ -43,8 +52,11 @@ const Dropdown = (($) => {
}
const ClassName = {
- DISABLED : 'disabled',
- SHOW : 'show'
+ DISABLED : 'disabled',
+ SHOW : 'show',
+ DROPUP : 'dropup',
+ MENURIGHT : 'dropdown-menu-right',
+ MENULEFT : 'dropdown-menu-left'
}
const Selector = {
@@ -55,6 +67,25 @@ const Dropdown = (($) => {
VISIBLE_ITEMS : '.dropdown-menu .dropdown-item:not(.disabled)'
}
+ const AttachmentMap = {
+ TOP : 'top-start',
+ TOPEND : 'top-end',
+ BOTTOM : 'bottom-start',
+ BOTTOMEND : 'bottom-end'
+ }
+
+ const Default = {
+ placement : AttachmentMap.BOTTOM,
+ offset : 0,
+ flip : true
+ }
+
+ const DefaultType = {
+ placement : 'string',
+ offset : '(number|string)',
+ flip : 'boolean'
+ }
+
/**
* ------------------------------------------------------------------------
@@ -64,8 +95,11 @@ const Dropdown = (($) => {
class Dropdown {
- constructor(element) {
+ constructor(element, config) {
this._element = element
+ this._popper = null
+ this._config = this._getConfig(config)
+ this._menu = this._getMenuElement()
this._addEventListeners()
}
@@ -77,34 +111,60 @@ const Dropdown = (($) => {
return VERSION
}
+ static get Default() {
+ return Default
+ }
+
+ static get DefaultType() {
+ return DefaultType
+ }
// public
toggle() {
- if (this.disabled || $(this).hasClass(ClassName.DISABLED)) {
- return false
+ if (this._element.disabled || $(this._element).hasClass(ClassName.DISABLED)) {
+ return
}
- const parent = Dropdown._getParentFromElement(this)
- const isActive = $(parent).hasClass(ClassName.SHOW)
+ const parent = Dropdown._getParentFromElement(this._element)
+ const isActive = $(this._menu).hasClass(ClassName.SHOW)
Dropdown._clearMenus()
if (isActive) {
- return false
+ return
}
const relatedTarget = {
- relatedTarget : this
+ relatedTarget : this._element
}
- const showEvent = $.Event(Event.SHOW, relatedTarget)
+ const showEvent = $.Event(Event.SHOW, relatedTarget)
$(parent).trigger(showEvent)
if (showEvent.isDefaultPrevented()) {
- return false
+ return
}
+ let element = this._element
+ // for dropup with alignment we use the parent as popper container
+ if ($(parent).hasClass(ClassName.DROPUP)) {
+ if ($(this._menu).hasClass(ClassName.MENULEFT) || $(this._menu).hasClass(ClassName.MENURIGHT)) {
+ element = parent
+ }
+ }
+ this._popper = new Popper(element, this._menu, {
+ placement : this._getPlacement(),
+ modifiers : {
+ offset : {
+ offset : this._config.offset
+ },
+ flip : {
+ enabled : this._config.flip
+ }
+ }
+ })
+
// if this is a touch-enabled device we add extra
// empty mouseover listeners to the body's immediate children;
// only needed because of broken event delegation on iOS
@@ -114,37 +174,100 @@ const Dropdown = (($) => {
$('body').children().on('mouseover', null, $.noop)
}
- this.focus()
- this.setAttribute('aria-expanded', true)
-
- $(parent).toggleClass(ClassName.SHOW)
- $(parent).trigger($.Event(Event.SHOWN, relatedTarget))
+ this._element.focus()
+ this._element.setAttribute('aria-expanded', true)
- return false
+ $(this._menu).toggleClass(ClassName.SHOW)
+ $(parent)
+ .toggleClass(ClassName.SHOW)
+ .trigger($.Event(Event.SHOWN, relatedTarget))
}
dispose() {
$.removeData(this._element, DATA_KEY)
$(this._element).off(EVENT_KEY)
this._element = null
+ this._menu = null
+ if (this._popper !== null) {
+ this._popper.destroy()
+ }
+ this._popper = null
}
+ update() {
+ if (this._popper !== null) {
+ this._popper.scheduleUpdate()
+ }
+ }
// private
_addEventListeners() {
- $(this._element).on(Event.CLICK, this.toggle)
+ $(this._element).on(Event.CLICK, (event) => {
+ event.preventDefault()
+ event.stopPropagation()
+ this.toggle()
+ })
+ }
+
+ _getConfig(config) {
+ const elementData = $(this._element).data()
+ if (elementData.placement !== undefined) {
+ elementData.placement = AttachmentMap[elementData.placement.toUpperCase()]
+ }
+
+ config = $.extend(
+ {},
+ this.constructor.Default,
+ $(this._element).data(),
+ config
+ )
+
+ Util.typeCheckConfig(
+ NAME,
+ config,
+ this.constructor.DefaultType
+ )
+
+ return config
+ }
+
+ _getMenuElement() {
+ if (!this._menu) {
+ const parent = Dropdown._getParentFromElement(this._element)
+ this._menu = $(parent).find(Selector.MENU)[0]
+ }
+ return this._menu
}
+ _getPlacement() {
+ const $parentDropdown = $(this._element).parent()
+ let placement = this._config.placement
+
+ // Handle dropup
+ if ($parentDropdown.hasClass(ClassName.DROPUP) || this._config.placement === AttachmentMap.TOP) {
+ placement = AttachmentMap.TOP
+ if ($(this._menu).hasClass(ClassName.MENURIGHT)) {
+ placement = AttachmentMap.TOPEND
+ }
+ }
+ else {
+ if ($(this._menu).hasClass(ClassName.MENURIGHT)) {
+ placement = AttachmentMap.BOTTOMEND
+ }
+ }
+ return placement
+ }
// static
static _jQueryInterface(config) {
return this.each(function () {
let data = $(this).data(DATA_KEY)
+ const _config = typeof config === 'object' ? config : null
if (!data) {
- data = new Dropdown(this)
+ data = new Dropdown(this, _config)
$(this).data(DATA_KEY, data)
}
@@ -152,7 +275,7 @@ const Dropdown = (($) => {
if (data[config] === undefined) {
throw new Error(`No method named "${config}"`)
}
- data[config].call(this)
+ data[config]()
}
})
}
@@ -164,13 +287,18 @@ const Dropdown = (($) => {
}
const toggles = $.makeArray($(Selector.DATA_TOGGLE))
-
for (let i = 0; i < toggles.length; i++) {
const parent = Dropdown._getParentFromElement(toggles[i])
+ const context = $(toggles[i]).data(DATA_KEY)
const relatedTarget = {
relatedTarget : toggles[i]
}
+ if (!context) {
+ continue
+ }
+
+ const dropdownMenu = context._menu
if (!$(parent).hasClass(ClassName.SHOW)) {
continue
}
@@ -195,6 +323,7 @@ const Dropdown = (($) => {
toggles[i].setAttribute('aria-expanded', 'false')
+ $(dropdownMenu).removeClass(ClassName.SHOW)
$(parent)
.removeClass(ClassName.SHOW)
.trigger($.Event(Event.HIDDEN, relatedTarget))
@@ -276,7 +405,11 @@ const Dropdown = (($) => {
.on(Event.KEYDOWN_DATA_API, Selector.DATA_TOGGLE, Dropdown._dataApiKeydownHandler)
.on(Event.KEYDOWN_DATA_API, Selector.MENU, Dropdown._dataApiKeydownHandler)
.on(`${Event.CLICK_DATA_API} ${Event.KEYUP_DATA_API}`, Dropdown._clearMenus)
- .on(Event.CLICK_DATA_API, Selector.DATA_TOGGLE, Dropdown.prototype.toggle)
+ .on(Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) {
+ event.preventDefault()
+ event.stopPropagation()
+ Dropdown._jQueryInterface.call($(this), 'toggle')
+ })
.on(Event.CLICK_DATA_API, Selector.FORM_CHILD, (e) => {
e.stopPropagation()
})
diff --git a/js/src/popover.js b/js/src/popover.js
index b68b47998..a068420d6 100644
--- a/js/src/popover.js
+++ b/js/src/popover.js
@@ -22,12 +22,15 @@ const Popover = (($) => {
const DATA_KEY = 'bs.popover'
const EVENT_KEY = `.${DATA_KEY}`
const JQUERY_NO_CONFLICT = $.fn[NAME]
+ const CLASS_PREFIX = 'bs-popover'
+ const BSCLS_PREFIX_REGEX = new RegExp(`(^|\\s)${CLASS_PREFIX}\\S+`, 'g')
const Default = $.extend({}, Tooltip.Default, {
placement : 'right',
trigger : 'click',
content : '',
template : '<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/tooltip.js b/js/src/tooltip.js
index 47c3d8d05..1d53b0470 100644
--- a/js/src/tooltip.js
+++ b/js/src/tooltip.js
@@ -1,4 +1,4 @@
-/* global Tether */
+/* global Popper */
import Util from './util'
@@ -13,11 +13,11 @@ import Util from './util'
const Tooltip = (($) => {
/**
- * Check for Tether dependency
- * Tether - http://tether.io/
+ * Check for Popper dependency
+ * Popper - https://popper.js.org
*/
- if (typeof Tether === 'undefined') {
- throw new Error('Bootstrap tooltips require Tether (http://tether.io/)')
+ if (typeof Popper === 'undefined') {
+ throw new Error('Bootstrap tooltips require Popper.js (https://popper.js.org)')
}
@@ -33,43 +33,45 @@ const Tooltip = (($) => {
const EVENT_KEY = `.${DATA_KEY}`
const JQUERY_NO_CONFLICT = $.fn[NAME]
const TRANSITION_DURATION = 150
- const CLASS_PREFIX = 'bs-tether'
- const TETHER_PREFIX_REGEX = new RegExp(`(^|\\s)${CLASS_PREFIX}\\S+`, 'g')
-
- const Default = {
- animation : true,
- template : '<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 = {
@@ -100,11 +102,6 @@ const Tooltip = (($) => {
TOOLTIP_INNER : '.tooltip-inner'
}
- const TetherClass = {
- element : false,
- enabled : false
- }
-
const Trigger = {
HOVER : 'hover',
FOCUS : 'focus',
@@ -128,7 +125,7 @@ const Tooltip = (($) => {
this._timeout = 0
this._hoverState = ''
this._activeTrigger = {}
- this._tether = null
+ this._popper = null
// protected
this.element = element
@@ -220,8 +217,6 @@ const Tooltip = (($) => {
dispose() {
clearTimeout(this._timeout)
- this.cleanupTether()
-
$.removeData(this.element, this.constructor.DATA_KEY)
$(this.element).off(this.constructor.EVENT_KEY)
@@ -235,7 +230,10 @@ const Tooltip = (($) => {
this._timeout = null
this._hoverState = null
this._activeTrigger = null
- this._tether = null
+ if (this._popper !== null) {
+ this._popper.destroy()
+ }
+ this._popper = null
this.element = null
this.config = null
@@ -277,6 +275,7 @@ const Tooltip = (($) => {
this.config.placement
const attachment = this._getAttachment(placement)
+ this.addAttachmentClass(attachment)
const container = this.config.container === false ? document.body : $(this.config.container)
@@ -288,20 +287,26 @@ const Tooltip = (($) => {
$(this.element).trigger(this.constructor.Event.INSERTED)
- this._tether = new Tether({
- attachment,
- element : tip,
- target : this.element,
- classes : TetherClass,
- classPrefix : CLASS_PREFIX,
- offset : this.config.offset,
- constraints : this.config.constraints,
- addTargetClasses: false
+ this._popper = new Popper(this.element, tip, {
+ placement : attachment,
+ modifiers : {
+ offset : {
+ offset : this.config.offset
+ },
+ flip : {
+ behavior : this.config.fallbackPlacement
+ }
+ },
+ onCreate : (data) => {
+ if (data.originalPlacement !== data.placement) {
+ this._handlePopperPlacementChange(data)
+ }
+ },
+ onUpdate : (data) => {
+ this._handlePopperPlacementChange(data)
+ }
})
- Util.reflow(tip)
- this._tether.position()
-
$(tip).addClass(ClassName.SHOW)
// if this is a touch-enabled device we add extra
@@ -313,6 +318,9 @@ const Tooltip = (($) => {
}
const complete = () => {
+ if (this.config.animation) {
+ this._fixTransition()
+ }
const prevHoverState = this._hoverState
this._hoverState = null
@@ -327,10 +335,9 @@ const Tooltip = (($) => {
$(this.tip)
.one(Util.TRANSITION_END, complete)
.emulateTransitionEnd(Tooltip._TRANSITION_DURATION)
- return
+ } else {
+ complete()
}
-
- complete()
}
}
@@ -345,7 +352,9 @@ const Tooltip = (($) => {
this._cleanTipClass()
this.element.removeAttribute('aria-describedby')
$(this.element).trigger(this.constructor.Event.HIDDEN)
- this.cleanupTether()
+ if (this._popper !== null) {
+ this._popper.destroy()
+ }
if (callback) {
callback()
@@ -385,6 +394,11 @@ const Tooltip = (($) => {
}
+ update() {
+ if (this._popper !== null) {
+ this._popper.scheduleUpdate()
+ }
+ }
// protected
@@ -392,18 +406,18 @@ const Tooltip = (($) => {
return Boolean(this.getTitle())
}
+ addAttachmentClass(attachment) {
+ $(this.getTipElement()).addClass(`${CLASS_PREFIX}-${attachment}`)
+ }
+
getTipElement() {
return this.tip = this.tip || $(this.config.template)[0]
}
setContent() {
const $tip = $(this.getTipElement())
-
this.setElementContent($tip.find(Selector.TOOLTIP_INNER), this.getTitle())
-
$tip.removeClass(`${ClassName.FADE} ${ClassName.SHOW}`)
-
- this.cleanupTether()
}
setElementContent($element, content) {
@@ -434,12 +448,6 @@ const Tooltip = (($) => {
return title
}
- cleanupTether() {
- if (this._tether) {
- this._tether.destroy()
- }
- }
-
// private
@@ -447,14 +455,6 @@ const Tooltip = (($) => {
return AttachmentMap[placement.toUpperCase()]
}
- _cleanTipClass() {
- const $tip = $(this.getTipElement())
- const tabClass = $tip.attr('class').match(TETHER_PREFIX_REGEX)
- if (tabClass !== null && tabClass.length > 0) {
- $tip.removeClass(tabClass.join(''))
- }
- }
-
_setListeners() {
const triggers = this.config.trigger.split(' ')
@@ -651,6 +651,31 @@ const Tooltip = (($) => {
return config
}
+ _cleanTipClass() {
+ const $tip = $(this.getTipElement())
+ const tabClass = $tip.attr('class').match(BSCLS_PREFIX_REGEX)
+ if (tabClass !== null && tabClass.length > 0) {
+ $tip.removeClass(tabClass.join(''))
+ }
+ }
+
+ _handlePopperPlacementChange(data) {
+ this._cleanTipClass()
+ this.addAttachmentClass(this._getAttachment(data.placement))
+ }
+
+ _fixTransition() {
+ const tip = this.getTipElement()
+ const initConfigAnimation = this.config.animation
+ if (tip.getAttribute('x-placement') !== null) {
+ return
+ }
+ $(tip).removeClass(ClassName.FADE)
+ this.config.animation = false
+ this.hide()
+ this.show()
+ this.config.animation = initConfigAnimation
+ }
// static