Layout von Websiten mit Bootstrap und Foundation
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

tooltip.js 19KB

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