tooltip.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725
  1. import $ from 'jquery'
  2. import Popper from 'popper.js'
  3. import Util from './util'
  4. /**
  5. * --------------------------------------------------------------------------
  6. * Bootstrap (v4.1.3): tooltip.js
  7. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
  8. * --------------------------------------------------------------------------
  9. */
  10. const Tooltip = (($) => {
  11. /**
  12. * ------------------------------------------------------------------------
  13. * Constants
  14. * ------------------------------------------------------------------------
  15. */
  16. const NAME = 'tooltip'
  17. const VERSION = '4.1.3'
  18. const DATA_KEY = 'bs.tooltip'
  19. const EVENT_KEY = `.${DATA_KEY}`
  20. const JQUERY_NO_CONFLICT = $.fn[NAME]
  21. const CLASS_PREFIX = 'bs-tooltip'
  22. const BSCLS_PREFIX_REGEX = new RegExp(`(^|\\s)${CLASS_PREFIX}\\S+`, 'g')
  23. const DefaultType = {
  24. animation : 'boolean',
  25. template : 'string',
  26. title : '(string|element|function)',
  27. trigger : 'string',
  28. delay : '(number|object)',
  29. html : 'boolean',
  30. selector : '(string|boolean)',
  31. placement : '(string|function)',
  32. offset : '(number|string)',
  33. container : '(string|element|boolean)',
  34. fallbackPlacement : '(string|array)',
  35. boundary : '(string|element)'
  36. }
  37. const AttachmentMap = {
  38. AUTO : 'auto',
  39. TOP : 'top',
  40. RIGHT : 'right',
  41. BOTTOM : 'bottom',
  42. LEFT : 'left'
  43. }
  44. const Default = {
  45. animation : true,
  46. template : '<div class="tooltip" role="tooltip">' +
  47. '<div class="arrow"></div>' +
  48. '<div class="tooltip-inner"></div></div>',
  49. trigger : 'hover focus',
  50. title : '',
  51. delay : 0,
  52. html : false,
  53. selector : false,
  54. placement : 'top',
  55. offset : 0,
  56. container : false,
  57. fallbackPlacement : 'flip',
  58. boundary : 'scrollParent'
  59. }
  60. const HoverState = {
  61. SHOW : 'show',
  62. OUT : 'out'
  63. }
  64. const Event = {
  65. HIDE : `hide${EVENT_KEY}`,
  66. HIDDEN : `hidden${EVENT_KEY}`,
  67. SHOW : `show${EVENT_KEY}`,
  68. SHOWN : `shown${EVENT_KEY}`,
  69. INSERTED : `inserted${EVENT_KEY}`,
  70. CLICK : `click${EVENT_KEY}`,
  71. FOCUSIN : `focusin${EVENT_KEY}`,
  72. FOCUSOUT : `focusout${EVENT_KEY}`,
  73. MOUSEENTER : `mouseenter${EVENT_KEY}`,
  74. MOUSELEAVE : `mouseleave${EVENT_KEY}`
  75. }
  76. const ClassName = {
  77. FADE : 'fade',
  78. SHOW : 'show'
  79. }
  80. const Selector = {
  81. TOOLTIP : '.tooltip',
  82. TOOLTIP_INNER : '.tooltip-inner',
  83. ARROW : '.arrow'
  84. }
  85. const Trigger = {
  86. HOVER : 'hover',
  87. FOCUS : 'focus',
  88. CLICK : 'click',
  89. MANUAL : 'manual'
  90. }
  91. /**
  92. * ------------------------------------------------------------------------
  93. * Class Definition
  94. * ------------------------------------------------------------------------
  95. */
  96. class Tooltip {
  97. constructor(element, config) {
  98. /**
  99. * Check for Popper dependency
  100. * Popper - https://popper.js.org
  101. */
  102. if (typeof Popper === 'undefined') {
  103. throw new TypeError('Bootstrap tooltips require Popper.js (https://popper.js.org)')
  104. }
  105. // private
  106. this._isEnabled = true
  107. this._timeout = 0
  108. this._hoverState = ''
  109. this._activeTrigger = {}
  110. this._popper = null
  111. // Protected
  112. this.element = element
  113. this.config = this._getConfig(config)
  114. this.tip = null
  115. this._setListeners()
  116. }
  117. // Getters
  118. static get VERSION() {
  119. return VERSION
  120. }
  121. static get Default() {
  122. return Default
  123. }
  124. static get NAME() {
  125. return NAME
  126. }
  127. static get DATA_KEY() {
  128. return DATA_KEY
  129. }
  130. static get Event() {
  131. return Event
  132. }
  133. static get EVENT_KEY() {
  134. return EVENT_KEY
  135. }
  136. static get DefaultType() {
  137. return DefaultType
  138. }
  139. // Public
  140. enable() {
  141. this._isEnabled = true
  142. }
  143. disable() {
  144. this._isEnabled = false
  145. }
  146. toggleEnabled() {
  147. this._isEnabled = !this._isEnabled
  148. }
  149. toggle(event) {
  150. if (!this._isEnabled) {
  151. return
  152. }
  153. if (event) {
  154. const dataKey = this.constructor.DATA_KEY
  155. let context = $(event.currentTarget).data(dataKey)
  156. if (!context) {
  157. context = new this.constructor(
  158. event.currentTarget,
  159. this._getDelegateConfig()
  160. )
  161. $(event.currentTarget).data(dataKey, context)
  162. }
  163. context._activeTrigger.click = !context._activeTrigger.click
  164. if (context._isWithActiveTrigger()) {
  165. context._enter(null, context)
  166. } else {
  167. context._leave(null, context)
  168. }
  169. } else {
  170. if ($(this.getTipElement()).hasClass(ClassName.SHOW)) {
  171. this._leave(null, this)
  172. return
  173. }
  174. this._enter(null, this)
  175. }
  176. }
  177. dispose() {
  178. clearTimeout(this._timeout)
  179. $.removeData(this.element, this.constructor.DATA_KEY)
  180. $(this.element).off(this.constructor.EVENT_KEY)
  181. $(this.element).closest('.modal').off('hide.bs.modal')
  182. if (this.tip) {
  183. $(this.tip).remove()
  184. }
  185. this._isEnabled = null
  186. this._timeout = null
  187. this._hoverState = null
  188. this._activeTrigger = null
  189. if (this._popper !== null) {
  190. this._popper.destroy()
  191. }
  192. this._popper = null
  193. this.element = null
  194. this.config = null
  195. this.tip = null
  196. }
  197. show() {
  198. if ($(this.element).css('display') === 'none') {
  199. throw new Error('Please use show on visible elements')
  200. }
  201. const showEvent = $.Event(this.constructor.Event.SHOW)
  202. if (this.isWithContent() && this._isEnabled) {
  203. $(this.element).trigger(showEvent)
  204. const isInTheDom = $.contains(
  205. this.element.ownerDocument.documentElement,
  206. this.element
  207. )
  208. if (showEvent.isDefaultPrevented() || !isInTheDom) {
  209. return
  210. }
  211. const tip = this.getTipElement()
  212. const tipId = Util.getUID(this.constructor.NAME)
  213. tip.setAttribute('id', tipId)
  214. this.element.setAttribute('aria-describedby', tipId)
  215. this.setContent()
  216. if (this.config.animation) {
  217. $(tip).addClass(ClassName.FADE)
  218. }
  219. const placement = typeof this.config.placement === 'function'
  220. ? this.config.placement.call(this, tip, this.element)
  221. : this.config.placement
  222. const attachment = this._getAttachment(placement)
  223. this.addAttachmentClass(attachment)
  224. const container = this.config.container === false ? document.body : $(document).find(this.config.container)
  225. $(tip).data(this.constructor.DATA_KEY, this)
  226. if (!$.contains(this.element.ownerDocument.documentElement, this.tip)) {
  227. $(tip).appendTo(container)
  228. }
  229. $(this.element).trigger(this.constructor.Event.INSERTED)
  230. this._popper = new Popper(this.element, tip, {
  231. placement: attachment,
  232. modifiers: {
  233. offset: {
  234. offset: this.config.offset
  235. },
  236. flip: {
  237. behavior: this.config.fallbackPlacement
  238. },
  239. arrow: {
  240. element: Selector.ARROW
  241. },
  242. preventOverflow: {
  243. boundariesElement: this.config.boundary
  244. }
  245. },
  246. onCreate: (data) => {
  247. if (data.originalPlacement !== data.placement) {
  248. this._handlePopperPlacementChange(data)
  249. }
  250. },
  251. onUpdate: (data) => {
  252. this._handlePopperPlacementChange(data)
  253. }
  254. })
  255. $(tip).addClass(ClassName.SHOW)
  256. // If this is a touch-enabled device we add extra
  257. // empty mouseover listeners to the body's immediate children;
  258. // only needed because of broken event delegation on iOS
  259. // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html
  260. if ('ontouchstart' in document.documentElement) {
  261. $(document.body).children().on('mouseover', null, $.noop)
  262. }
  263. const complete = () => {
  264. if (this.config.animation) {
  265. this._fixTransition()
  266. }
  267. const prevHoverState = this._hoverState
  268. this._hoverState = null
  269. $(this.element).trigger(this.constructor.Event.SHOWN)
  270. if (prevHoverState === HoverState.OUT) {
  271. this._leave(null, this)
  272. }
  273. }
  274. if ($(this.tip).hasClass(ClassName.FADE)) {
  275. const transitionDuration = Util.getTransitionDurationFromElement(this.tip)
  276. $(this.tip)
  277. .one(Util.TRANSITION_END, complete)
  278. .emulateTransitionEnd(transitionDuration)
  279. } else {
  280. complete()
  281. }
  282. }
  283. }
  284. hide(callback) {
  285. const tip = this.getTipElement()
  286. const hideEvent = $.Event(this.constructor.Event.HIDE)
  287. const complete = () => {
  288. if (this._hoverState !== HoverState.SHOW && tip.parentNode) {
  289. tip.parentNode.removeChild(tip)
  290. }
  291. this._cleanTipClass()
  292. this.element.removeAttribute('aria-describedby')
  293. $(this.element).trigger(this.constructor.Event.HIDDEN)
  294. if (this._popper !== null) {
  295. this._popper.destroy()
  296. }
  297. if (callback) {
  298. callback()
  299. }
  300. }
  301. $(this.element).trigger(hideEvent)
  302. if (hideEvent.isDefaultPrevented()) {
  303. return
  304. }
  305. $(tip).removeClass(ClassName.SHOW)
  306. // If this is a touch-enabled device we remove the extra
  307. // empty mouseover listeners we added for iOS support
  308. if ('ontouchstart' in document.documentElement) {
  309. $(document.body).children().off('mouseover', null, $.noop)
  310. }
  311. this._activeTrigger[Trigger.CLICK] = false
  312. this._activeTrigger[Trigger.FOCUS] = false
  313. this._activeTrigger[Trigger.HOVER] = false
  314. if ($(this.tip).hasClass(ClassName.FADE)) {
  315. const transitionDuration = Util.getTransitionDurationFromElement(tip)
  316. $(tip)
  317. .one(Util.TRANSITION_END, complete)
  318. .emulateTransitionEnd(transitionDuration)
  319. } else {
  320. complete()
  321. }
  322. this._hoverState = ''
  323. }
  324. update() {
  325. if (this._popper !== null) {
  326. this._popper.scheduleUpdate()
  327. }
  328. }
  329. // Protected
  330. isWithContent() {
  331. return Boolean(this.getTitle())
  332. }
  333. addAttachmentClass(attachment) {
  334. $(this.getTipElement()).addClass(`${CLASS_PREFIX}-${attachment}`)
  335. }
  336. getTipElement() {
  337. this.tip = this.tip || $(this.config.template)[0]
  338. return this.tip
  339. }
  340. setContent() {
  341. const tip = this.getTipElement()
  342. this.setElementContent($(tip.querySelectorAll(Selector.TOOLTIP_INNER)), this.getTitle())
  343. $(tip).removeClass(`${ClassName.FADE} ${ClassName.SHOW}`)
  344. }
  345. setElementContent($element, content) {
  346. const html = this.config.html
  347. if (typeof content === 'object' && (content.nodeType || content.jquery)) {
  348. // Content is a DOM node or a jQuery
  349. if (html) {
  350. if (!$(content).parent().is($element)) {
  351. $element.empty().append(content)
  352. }
  353. } else {
  354. $element.text($(content).text())
  355. }
  356. } else {
  357. $element[html ? 'html' : 'text'](content)
  358. }
  359. }
  360. getTitle() {
  361. let title = this.element.getAttribute('data-original-title')
  362. if (!title) {
  363. title = typeof this.config.title === 'function'
  364. ? this.config.title.call(this.element)
  365. : this.config.title
  366. }
  367. return title
  368. }
  369. // Private
  370. _getAttachment(placement) {
  371. return AttachmentMap[placement.toUpperCase()]
  372. }
  373. _setListeners() {
  374. const triggers = this.config.trigger.split(' ')
  375. triggers.forEach((trigger) => {
  376. if (trigger === 'click') {
  377. $(this.element).on(
  378. this.constructor.Event.CLICK,
  379. this.config.selector,
  380. (event) => this.toggle(event)
  381. )
  382. } else if (trigger !== Trigger.MANUAL) {
  383. const eventIn = trigger === Trigger.HOVER
  384. ? this.constructor.Event.MOUSEENTER
  385. : this.constructor.Event.FOCUSIN
  386. const eventOut = trigger === Trigger.HOVER
  387. ? this.constructor.Event.MOUSELEAVE
  388. : this.constructor.Event.FOCUSOUT
  389. $(this.element)
  390. .on(
  391. eventIn,
  392. this.config.selector,
  393. (event) => this._enter(event)
  394. )
  395. .on(
  396. eventOut,
  397. this.config.selector,
  398. (event) => this._leave(event)
  399. )
  400. }
  401. $(this.element).closest('.modal').on(
  402. 'hide.bs.modal',
  403. () => this.hide()
  404. )
  405. })
  406. if (this.config.selector) {
  407. this.config = {
  408. ...this.config,
  409. trigger: 'manual',
  410. selector: ''
  411. }
  412. } else {
  413. this._fixTitle()
  414. }
  415. }
  416. _fixTitle() {
  417. const titleType = typeof this.element.getAttribute('data-original-title')
  418. if (this.element.getAttribute('title') ||
  419. titleType !== 'string') {
  420. this.element.setAttribute(
  421. 'data-original-title',
  422. this.element.getAttribute('title') || ''
  423. )
  424. this.element.setAttribute('title', '')
  425. }
  426. }
  427. _enter(event, context) {
  428. const dataKey = this.constructor.DATA_KEY
  429. context = context || $(event.currentTarget).data(dataKey)
  430. if (!context) {
  431. context = new this.constructor(
  432. event.currentTarget,
  433. this._getDelegateConfig()
  434. )
  435. $(event.currentTarget).data(dataKey, context)
  436. }
  437. if (event) {
  438. context._activeTrigger[
  439. event.type === 'focusin' ? Trigger.FOCUS : Trigger.HOVER
  440. ] = true
  441. }
  442. if ($(context.getTipElement()).hasClass(ClassName.SHOW) ||
  443. context._hoverState === HoverState.SHOW) {
  444. context._hoverState = HoverState.SHOW
  445. return
  446. }
  447. clearTimeout(context._timeout)
  448. context._hoverState = HoverState.SHOW
  449. if (!context.config.delay || !context.config.delay.show) {
  450. context.show()
  451. return
  452. }
  453. context._timeout = setTimeout(() => {
  454. if (context._hoverState === HoverState.SHOW) {
  455. context.show()
  456. }
  457. }, context.config.delay.show)
  458. }
  459. _leave(event, context) {
  460. const dataKey = this.constructor.DATA_KEY
  461. context = context || $(event.currentTarget).data(dataKey)
  462. if (!context) {
  463. context = new this.constructor(
  464. event.currentTarget,
  465. this._getDelegateConfig()
  466. )
  467. $(event.currentTarget).data(dataKey, context)
  468. }
  469. if (event) {
  470. context._activeTrigger[
  471. event.type === 'focusout' ? Trigger.FOCUS : Trigger.HOVER
  472. ] = false
  473. }
  474. if (context._isWithActiveTrigger()) {
  475. return
  476. }
  477. clearTimeout(context._timeout)
  478. context._hoverState = HoverState.OUT
  479. if (!context.config.delay || !context.config.delay.hide) {
  480. context.hide()
  481. return
  482. }
  483. context._timeout = setTimeout(() => {
  484. if (context._hoverState === HoverState.OUT) {
  485. context.hide()
  486. }
  487. }, context.config.delay.hide)
  488. }
  489. _isWithActiveTrigger() {
  490. for (const trigger in this._activeTrigger) {
  491. if (this._activeTrigger[trigger]) {
  492. return true
  493. }
  494. }
  495. return false
  496. }
  497. _getConfig(config) {
  498. config = {
  499. ...this.constructor.Default,
  500. ...$(this.element).data(),
  501. ...typeof config === 'object' && config ? config : {}
  502. }
  503. if (typeof config.delay === 'number') {
  504. config.delay = {
  505. show: config.delay,
  506. hide: config.delay
  507. }
  508. }
  509. if (typeof config.title === 'number') {
  510. config.title = config.title.toString()
  511. }
  512. if (typeof config.content === 'number') {
  513. config.content = config.content.toString()
  514. }
  515. Util.typeCheckConfig(
  516. NAME,
  517. config,
  518. this.constructor.DefaultType
  519. )
  520. return config
  521. }
  522. _getDelegateConfig() {
  523. const config = {}
  524. if (this.config) {
  525. for (const key in this.config) {
  526. if (this.constructor.Default[key] !== this.config[key]) {
  527. config[key] = this.config[key]
  528. }
  529. }
  530. }
  531. return config
  532. }
  533. _cleanTipClass() {
  534. const $tip = $(this.getTipElement())
  535. const tabClass = $tip.attr('class').match(BSCLS_PREFIX_REGEX)
  536. if (tabClass !== null && tabClass.length) {
  537. $tip.removeClass(tabClass.join(''))
  538. }
  539. }
  540. _handlePopperPlacementChange(popperData) {
  541. const popperInstance = popperData.instance
  542. this.tip = popperInstance.popper
  543. this._cleanTipClass()
  544. this.addAttachmentClass(this._getAttachment(popperData.placement))
  545. }
  546. _fixTransition() {
  547. const tip = this.getTipElement()
  548. const initConfigAnimation = this.config.animation
  549. if (tip.getAttribute('x-placement') !== null) {
  550. return
  551. }
  552. $(tip).removeClass(ClassName.FADE)
  553. this.config.animation = false
  554. this.hide()
  555. this.show()
  556. this.config.animation = initConfigAnimation
  557. }
  558. // Static
  559. static _jQueryInterface(config) {
  560. return this.each(function () {
  561. let data = $(this).data(DATA_KEY)
  562. const _config = typeof config === 'object' && config
  563. if (!data && /dispose|hide/.test(config)) {
  564. return
  565. }
  566. if (!data) {
  567. data = new Tooltip(this, _config)
  568. $(this).data(DATA_KEY, data)
  569. }
  570. if (typeof config === 'string') {
  571. if (typeof data[config] === 'undefined') {
  572. throw new TypeError(`No method named "${config}"`)
  573. }
  574. data[config]()
  575. }
  576. })
  577. }
  578. }
  579. /**
  580. * ------------------------------------------------------------------------
  581. * jQuery
  582. * ------------------------------------------------------------------------
  583. */
  584. $.fn[NAME] = Tooltip._jQueryInterface
  585. $.fn[NAME].Constructor = Tooltip
  586. $.fn[NAME].noConflict = function () {
  587. $.fn[NAME] = JQUERY_NO_CONFLICT
  588. return Tooltip._jQueryInterface
  589. }
  590. return Tooltip
  591. })($, Popper)
  592. export default Tooltip