tooltip.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636
  1. import Util from './util'
  2. /**
  3. * --------------------------------------------------------------------------
  4. * Bootstrap (v4.0.0): tooltip.js
  5. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
  6. * --------------------------------------------------------------------------
  7. */
  8. const Tooltip = (($) => {
  9. /**
  10. * ------------------------------------------------------------------------
  11. * Constants
  12. * ------------------------------------------------------------------------
  13. */
  14. const NAME = 'tooltip'
  15. const VERSION = '4.0.0'
  16. const DATA_KEY = 'bs.tooltip'
  17. const EVENT_KEY = `.${DATA_KEY}`
  18. const JQUERY_NO_CONFLICT = $.fn[NAME]
  19. const TRANSITION_DURATION = 150
  20. const CLASS_PREFIX = 'bs-tether'
  21. const Default = {
  22. animation : true,
  23. template : '<div class="tooltip" role="tooltip">'
  24. + '<div class="tooltip-arrow"></div>'
  25. + '<div class="tooltip-inner"></div></div>',
  26. trigger : 'hover focus',
  27. title : '',
  28. delay : 0,
  29. html : false,
  30. selector : false,
  31. placement : 'top',
  32. offset : '0 0',
  33. constraints : []
  34. }
  35. const DefaultType = {
  36. animation : 'boolean',
  37. template : 'string',
  38. title : '(string|function)',
  39. trigger : 'string',
  40. delay : '(number|object)',
  41. html : 'boolean',
  42. selector : '(string|boolean)',
  43. placement : '(string|function)',
  44. offset : 'string',
  45. constraints : 'array'
  46. }
  47. const AttachmentMap = {
  48. TOP : 'bottom center',
  49. RIGHT : 'middle left',
  50. BOTTOM : 'top center',
  51. LEFT : 'middle right'
  52. }
  53. const HoverState = {
  54. IN : 'in',
  55. OUT : 'out'
  56. }
  57. const Event = {
  58. HIDE : `hide${EVENT_KEY}`,
  59. HIDDEN : `hidden${EVENT_KEY}`,
  60. SHOW : `show${EVENT_KEY}`,
  61. SHOWN : `shown${EVENT_KEY}`,
  62. INSERTED : `inserted${EVENT_KEY}`,
  63. CLICK : `click${EVENT_KEY}`,
  64. FOCUSIN : `focusin${EVENT_KEY}`,
  65. FOCUSOUT : `focusout${EVENT_KEY}`,
  66. MOUSEENTER : `mouseenter${EVENT_KEY}`,
  67. MOUSELEAVE : `mouseleave${EVENT_KEY}`
  68. }
  69. const ClassName = {
  70. FADE : 'fade',
  71. IN : 'in'
  72. }
  73. const Selector = {
  74. TOOLTIP : '.tooltip',
  75. TOOLTIP_INNER : '.tooltip-inner'
  76. }
  77. const TetherClass = {
  78. element : false,
  79. enabled : false
  80. }
  81. const Trigger = {
  82. HOVER : 'hover',
  83. FOCUS : 'focus',
  84. CLICK : 'click',
  85. MANUAL : 'manual'
  86. }
  87. /**
  88. * ------------------------------------------------------------------------
  89. * Class Definition
  90. * ------------------------------------------------------------------------
  91. */
  92. class Tooltip {
  93. constructor(element, config) {
  94. // private
  95. this._isEnabled = true
  96. this._timeout = 0
  97. this._hoverState = ''
  98. this._activeTrigger = {}
  99. this._tether = null
  100. // protected
  101. this.element = element
  102. this.config = this._getConfig(config)
  103. this.tip = null
  104. this._setListeners()
  105. }
  106. // getters
  107. static get VERSION() {
  108. return VERSION
  109. }
  110. static get Default() {
  111. return Default
  112. }
  113. static get NAME() {
  114. return NAME
  115. }
  116. static get DATA_KEY() {
  117. return DATA_KEY
  118. }
  119. static get Event() {
  120. return Event
  121. }
  122. static get EVENT_KEY() {
  123. return EVENT_KEY
  124. }
  125. static get DefaultType() {
  126. return DefaultType
  127. }
  128. // public
  129. enable() {
  130. this._isEnabled = true
  131. }
  132. disable() {
  133. this._isEnabled = false
  134. }
  135. toggleEnabled() {
  136. this._isEnabled = !this._isEnabled
  137. }
  138. toggle(event) {
  139. if (event) {
  140. let dataKey = this.constructor.DATA_KEY
  141. let context = $(event.currentTarget).data(dataKey)
  142. if (!context) {
  143. context = new this.constructor(
  144. event.currentTarget,
  145. this._getDelegateConfig()
  146. )
  147. $(event.currentTarget).data(dataKey, context)
  148. }
  149. context._activeTrigger.click = !context._activeTrigger.click
  150. if (context._isWithActiveTrigger()) {
  151. context._enter(null, context)
  152. } else {
  153. context._leave(null, context)
  154. }
  155. } else {
  156. if ($(this.getTipElement()).hasClass(ClassName.IN)) {
  157. this._leave(null, this)
  158. return
  159. }
  160. this._enter(null, this)
  161. }
  162. }
  163. dispose() {
  164. clearTimeout(this._timeout)
  165. this.cleanupTether()
  166. $.removeData(this.element, this.constructor.DATA_KEY)
  167. $(this.element).off(this.constructor.EVENT_KEY)
  168. if (this.tip) {
  169. $(this.tip).remove()
  170. }
  171. this._isEnabled = null
  172. this._timeout = null
  173. this._hoverState = null
  174. this._activeTrigger = null
  175. this._tether = null
  176. this.element = null
  177. this.config = null
  178. this.tip = null
  179. }
  180. show() {
  181. let showEvent = $.Event(this.constructor.Event.SHOW)
  182. if (this.isWithContent() && this._isEnabled) {
  183. $(this.element).trigger(showEvent)
  184. let isInTheDom = $.contains(
  185. this.element.ownerDocument.documentElement,
  186. this.element
  187. )
  188. if (showEvent.isDefaultPrevented() || !isInTheDom) {
  189. return
  190. }
  191. let tip = this.getTipElement()
  192. let tipId = Util.getUID(this.constructor.NAME)
  193. tip.setAttribute('id', tipId)
  194. this.element.setAttribute('aria-describedby', tipId)
  195. this.setContent()
  196. if (this.config.animation) {
  197. $(tip).addClass(ClassName.FADE)
  198. }
  199. let placement = typeof this.config.placement === 'function' ?
  200. this.config.placement.call(this, tip, this.element) :
  201. this.config.placement
  202. let attachment = this._getAttachment(placement)
  203. $(tip)
  204. .data(this.constructor.DATA_KEY, this)
  205. .appendTo(document.body)
  206. $(this.element).trigger(this.constructor.Event.INSERTED)
  207. this._tether = new Tether({
  208. attachment,
  209. element : tip,
  210. target : this.element,
  211. classes : TetherClass,
  212. classPrefix : CLASS_PREFIX,
  213. offset : this.config.offset,
  214. constraints : this.config.constraints
  215. })
  216. Util.reflow(tip)
  217. this._tether.position()
  218. $(tip).addClass(ClassName.IN)
  219. let complete = () => {
  220. let prevHoverState = this._hoverState
  221. this._hoverState = null
  222. $(this.element).trigger(this.constructor.Event.SHOWN)
  223. if (prevHoverState === HoverState.OUT) {
  224. this._leave(null, this)
  225. }
  226. }
  227. if (Util.supportsTransitionEnd() && $(this.tip).hasClass(ClassName.FADE)) {
  228. $(this.tip)
  229. .one(Util.TRANSITION_END, complete)
  230. .emulateTransitionEnd(Tooltip._TRANSITION_DURATION)
  231. return
  232. }
  233. complete()
  234. }
  235. }
  236. hide(callback) {
  237. let tip = this.getTipElement()
  238. let hideEvent = $.Event(this.constructor.Event.HIDE)
  239. let complete = () => {
  240. if (this._hoverState !== HoverState.IN && tip.parentNode) {
  241. tip.parentNode.removeChild(tip)
  242. }
  243. this.element.removeAttribute('aria-describedby')
  244. $(this.element).trigger(this.constructor.Event.HIDDEN)
  245. this.cleanupTether()
  246. if (callback) {
  247. callback()
  248. }
  249. }
  250. $(this.element).trigger(hideEvent)
  251. if (hideEvent.isDefaultPrevented()) {
  252. return
  253. }
  254. $(tip).removeClass(ClassName.IN)
  255. if (Util.supportsTransitionEnd() &&
  256. ($(this.tip).hasClass(ClassName.FADE))) {
  257. $(tip)
  258. .one(Util.TRANSITION_END, complete)
  259. .emulateTransitionEnd(TRANSITION_DURATION)
  260. } else {
  261. complete()
  262. }
  263. this._hoverState = ''
  264. }
  265. // protected
  266. isWithContent() {
  267. return Boolean(this.getTitle())
  268. }
  269. getTipElement() {
  270. return (this.tip = this.tip || $(this.config.template)[0])
  271. }
  272. setContent() {
  273. let tip = this.getTipElement()
  274. let title = this.getTitle()
  275. let method = this.config.html ? 'innerHTML' : 'innerText'
  276. $(tip).find(Selector.TOOLTIP_INNER)[0][method] = title
  277. $(tip)
  278. .removeClass(ClassName.FADE)
  279. .removeClass(ClassName.IN)
  280. this.cleanupTether()
  281. }
  282. getTitle() {
  283. let title = this.element.getAttribute('data-original-title')
  284. if (!title) {
  285. title = typeof this.config.title === 'function' ?
  286. this.config.title.call(this.element) :
  287. this.config.title
  288. }
  289. return title
  290. }
  291. cleanupTether() {
  292. if (this._tether) {
  293. this._tether.destroy()
  294. // clean up after tether's junk classes
  295. // remove after they fix issue
  296. // (https://github.com/HubSpot/tether/issues/36)
  297. $(this.element).removeClass(this._removeTetherClasses)
  298. $(this.tip).removeClass(this._removeTetherClasses)
  299. }
  300. }
  301. // private
  302. _getAttachment(placement) {
  303. return AttachmentMap[placement.toUpperCase()]
  304. }
  305. _setListeners() {
  306. let triggers = this.config.trigger.split(' ')
  307. triggers.forEach((trigger) => {
  308. if (trigger === 'click') {
  309. $(this.element).on(
  310. this.constructor.Event.CLICK,
  311. this.config.selector,
  312. $.proxy(this.toggle, this)
  313. )
  314. } else if (trigger !== Trigger.MANUAL) {
  315. let eventIn = trigger === Trigger.HOVER ?
  316. this.constructor.Event.MOUSEENTER :
  317. this.constructor.Event.FOCUSIN
  318. let eventOut = trigger === Trigger.HOVER ?
  319. this.constructor.Event.MOUSELEAVE :
  320. this.constructor.Event.FOCUSOUT
  321. $(this.element)
  322. .on(
  323. eventIn,
  324. this.config.selector,
  325. $.proxy(this._enter, this)
  326. )
  327. .on(
  328. eventOut,
  329. this.config.selector,
  330. $.proxy(this._leave, this)
  331. )
  332. }
  333. })
  334. if (this.config.selector) {
  335. this.config = $.extend({}, this.config, {
  336. trigger : 'manual',
  337. selector : ''
  338. })
  339. } else {
  340. this._fixTitle()
  341. }
  342. }
  343. _removeTetherClasses(i, css) {
  344. return ((css.baseVal || css).match(
  345. new RegExp(`(^|\\s)${CLASS_PREFIX}-\\S+`, 'g')) || []
  346. ).join(' ')
  347. }
  348. _fixTitle() {
  349. let titleType = typeof this.element.getAttribute('data-original-title')
  350. if (this.element.getAttribute('title') ||
  351. (titleType !== 'string')) {
  352. this.element.setAttribute(
  353. 'data-original-title',
  354. this.element.getAttribute('title') || ''
  355. )
  356. this.element.setAttribute('title', '')
  357. }
  358. }
  359. _enter(event, context) {
  360. let dataKey = this.constructor.DATA_KEY
  361. context = context || $(event.currentTarget).data(dataKey)
  362. if (!context) {
  363. context = new this.constructor(
  364. event.currentTarget,
  365. this._getDelegateConfig()
  366. )
  367. $(event.currentTarget).data(dataKey, context)
  368. }
  369. if (event) {
  370. context._activeTrigger[
  371. event.type === 'focusin' ? Trigger.FOCUS : Trigger.HOVER
  372. ] = true
  373. }
  374. if ($(context.getTipElement()).hasClass(ClassName.IN) ||
  375. (context._hoverState === HoverState.IN)) {
  376. context._hoverState = HoverState.IN
  377. return
  378. }
  379. clearTimeout(context._timeout)
  380. context._hoverState = HoverState.IN
  381. if (!context.config.delay || !context.config.delay.show) {
  382. context.show()
  383. return
  384. }
  385. context._timeout = setTimeout(() => {
  386. if (context._hoverState === HoverState.IN) {
  387. context.show()
  388. }
  389. }, context.config.delay.show)
  390. }
  391. _leave(event, context) {
  392. let dataKey = this.constructor.DATA_KEY
  393. context = context || $(event.currentTarget).data(dataKey)
  394. if (!context) {
  395. context = new this.constructor(
  396. event.currentTarget,
  397. this._getDelegateConfig()
  398. )
  399. $(event.currentTarget).data(dataKey, context)
  400. }
  401. if (event) {
  402. context._activeTrigger[
  403. event.type === 'focusout' ? Trigger.FOCUS : Trigger.HOVER
  404. ] = false
  405. }
  406. if (context._isWithActiveTrigger()) {
  407. return
  408. }
  409. clearTimeout(context._timeout)
  410. context._hoverState = HoverState.OUT
  411. if (!context.config.delay || !context.config.delay.hide) {
  412. context.hide()
  413. return
  414. }
  415. context._timeout = setTimeout(() => {
  416. if (context._hoverState === HoverState.OUT) {
  417. context.hide()
  418. }
  419. }, context.config.delay.hide)
  420. }
  421. _isWithActiveTrigger() {
  422. for (let trigger in this._activeTrigger) {
  423. if (this._activeTrigger[trigger]) {
  424. return true
  425. }
  426. }
  427. return false
  428. }
  429. _getConfig(config) {
  430. config = $.extend(
  431. {},
  432. this.constructor.Default,
  433. $(this.element).data(),
  434. config
  435. )
  436. if (config.delay && typeof config.delay === 'number') {
  437. config.delay = {
  438. show : config.delay,
  439. hide : config.delay
  440. }
  441. }
  442. Util.typeCheckConfig(
  443. NAME,
  444. config,
  445. this.constructor.DefaultType
  446. )
  447. return config
  448. }
  449. _getDelegateConfig() {
  450. let config = {}
  451. if (this.config) {
  452. for (let key in this.config) {
  453. if (this.constructor.Default[key] !== this.config[key]) {
  454. config[key] = this.config[key]
  455. }
  456. }
  457. }
  458. return config
  459. }
  460. // static
  461. static _jQueryInterface(config) {
  462. return this.each(function () {
  463. let data = $(this).data(DATA_KEY)
  464. let _config = typeof config === 'object' ?
  465. config : null
  466. if (!data && /destroy|hide/.test(config)) {
  467. return
  468. }
  469. if (!data) {
  470. data = new Tooltip(this, _config)
  471. $(this).data(DATA_KEY, data)
  472. }
  473. if (typeof config === 'string') {
  474. data[config]()
  475. }
  476. })
  477. }
  478. }
  479. /**
  480. * ------------------------------------------------------------------------
  481. * jQuery
  482. * ------------------------------------------------------------------------
  483. */
  484. $.fn[NAME] = Tooltip._jQueryInterface
  485. $.fn[NAME].Constructor = Tooltip
  486. $.fn[NAME].noConflict = function () {
  487. $.fn[NAME] = JQUERY_NO_CONFLICT
  488. return Tooltip._jQueryInterface
  489. }
  490. return Tooltip
  491. })(jQuery)
  492. export default Tooltip