modal.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579
  1. import $ from 'jquery'
  2. import Util from './util'
  3. /**
  4. * --------------------------------------------------------------------------
  5. * Bootstrap (v4.1.3): modal.js
  6. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
  7. * --------------------------------------------------------------------------
  8. */
  9. const Modal = (($) => {
  10. /**
  11. * ------------------------------------------------------------------------
  12. * Constants
  13. * ------------------------------------------------------------------------
  14. */
  15. const NAME = 'modal'
  16. const VERSION = '4.1.3'
  17. const DATA_KEY = 'bs.modal'
  18. const EVENT_KEY = `.${DATA_KEY}`
  19. const DATA_API_KEY = '.data-api'
  20. const JQUERY_NO_CONFLICT = $.fn[NAME]
  21. const ESCAPE_KEYCODE = 27 // KeyboardEvent.which value for Escape (Esc) key
  22. const Default = {
  23. backdrop : true,
  24. keyboard : true,
  25. focus : true,
  26. show : true
  27. }
  28. const DefaultType = {
  29. backdrop : '(boolean|string)',
  30. keyboard : 'boolean',
  31. focus : 'boolean',
  32. show : 'boolean'
  33. }
  34. const Event = {
  35. HIDE : `hide${EVENT_KEY}`,
  36. HIDDEN : `hidden${EVENT_KEY}`,
  37. SHOW : `show${EVENT_KEY}`,
  38. SHOWN : `shown${EVENT_KEY}`,
  39. FOCUSIN : `focusin${EVENT_KEY}`,
  40. RESIZE : `resize${EVENT_KEY}`,
  41. CLICK_DISMISS : `click.dismiss${EVENT_KEY}`,
  42. KEYDOWN_DISMISS : `keydown.dismiss${EVENT_KEY}`,
  43. MOUSEUP_DISMISS : `mouseup.dismiss${EVENT_KEY}`,
  44. MOUSEDOWN_DISMISS : `mousedown.dismiss${EVENT_KEY}`,
  45. CLICK_DATA_API : `click${EVENT_KEY}${DATA_API_KEY}`
  46. }
  47. const ClassName = {
  48. SCROLLBAR_MEASURER : 'modal-scrollbar-measure',
  49. BACKDROP : 'modal-backdrop',
  50. OPEN : 'modal-open',
  51. FADE : 'fade',
  52. SHOW : 'show'
  53. }
  54. const Selector = {
  55. DIALOG : '.modal-dialog',
  56. DATA_TOGGLE : '[data-toggle="modal"]',
  57. DATA_DISMISS : '[data-dismiss="modal"]',
  58. FIXED_CONTENT : '.fixed-top, .fixed-bottom, .is-fixed, .sticky-top',
  59. STICKY_CONTENT : '.sticky-top'
  60. }
  61. /**
  62. * ------------------------------------------------------------------------
  63. * Class Definition
  64. * ------------------------------------------------------------------------
  65. */
  66. class Modal {
  67. constructor(element, config) {
  68. this._config = this._getConfig(config)
  69. this._element = element
  70. this._dialog = element.querySelector(Selector.DIALOG)
  71. this._backdrop = null
  72. this._isShown = false
  73. this._isBodyOverflowing = false
  74. this._ignoreBackdropClick = false
  75. this._scrollbarWidth = 0
  76. }
  77. // Getters
  78. static get VERSION() {
  79. return VERSION
  80. }
  81. static get Default() {
  82. return Default
  83. }
  84. // Public
  85. toggle(relatedTarget) {
  86. return this._isShown ? this.hide() : this.show(relatedTarget)
  87. }
  88. show(relatedTarget) {
  89. if (this._isTransitioning || this._isShown) {
  90. return
  91. }
  92. if ($(this._element).hasClass(ClassName.FADE)) {
  93. this._isTransitioning = true
  94. }
  95. const showEvent = $.Event(Event.SHOW, {
  96. relatedTarget
  97. })
  98. $(this._element).trigger(showEvent)
  99. if (this._isShown || showEvent.isDefaultPrevented()) {
  100. return
  101. }
  102. this._isShown = true
  103. this._checkScrollbar()
  104. this._setScrollbar()
  105. this._adjustDialog()
  106. $(document.body).addClass(ClassName.OPEN)
  107. this._setEscapeEvent()
  108. this._setResizeEvent()
  109. $(this._element).on(
  110. Event.CLICK_DISMISS,
  111. Selector.DATA_DISMISS,
  112. (event) => this.hide(event)
  113. )
  114. $(this._dialog).on(Event.MOUSEDOWN_DISMISS, () => {
  115. $(this._element).one(Event.MOUSEUP_DISMISS, (event) => {
  116. if ($(event.target).is(this._element)) {
  117. this._ignoreBackdropClick = true
  118. }
  119. })
  120. })
  121. this._showBackdrop(() => this._showElement(relatedTarget))
  122. }
  123. hide(event) {
  124. if (event) {
  125. event.preventDefault()
  126. }
  127. if (this._isTransitioning || !this._isShown) {
  128. return
  129. }
  130. const hideEvent = $.Event(Event.HIDE)
  131. $(this._element).trigger(hideEvent)
  132. if (!this._isShown || hideEvent.isDefaultPrevented()) {
  133. return
  134. }
  135. this._isShown = false
  136. const transition = $(this._element).hasClass(ClassName.FADE)
  137. if (transition) {
  138. this._isTransitioning = true
  139. }
  140. this._setEscapeEvent()
  141. this._setResizeEvent()
  142. $(document).off(Event.FOCUSIN)
  143. $(this._element).removeClass(ClassName.SHOW)
  144. $(this._element).off(Event.CLICK_DISMISS)
  145. $(this._dialog).off(Event.MOUSEDOWN_DISMISS)
  146. if (transition) {
  147. const transitionDuration = Util.getTransitionDurationFromElement(this._element)
  148. $(this._element)
  149. .one(Util.TRANSITION_END, (event) => this._hideModal(event))
  150. .emulateTransitionEnd(transitionDuration)
  151. } else {
  152. this._hideModal()
  153. }
  154. }
  155. dispose() {
  156. $.removeData(this._element, DATA_KEY)
  157. $(window, document, this._element, this._backdrop).off(EVENT_KEY)
  158. this._config = null
  159. this._element = null
  160. this._dialog = null
  161. this._backdrop = null
  162. this._isShown = null
  163. this._isBodyOverflowing = null
  164. this._ignoreBackdropClick = null
  165. this._scrollbarWidth = null
  166. }
  167. handleUpdate() {
  168. this._adjustDialog()
  169. }
  170. // Private
  171. _getConfig(config) {
  172. config = {
  173. ...Default,
  174. ...config
  175. }
  176. Util.typeCheckConfig(NAME, config, DefaultType)
  177. return config
  178. }
  179. _showElement(relatedTarget) {
  180. const transition = $(this._element).hasClass(ClassName.FADE)
  181. if (!this._element.parentNode ||
  182. this._element.parentNode.nodeType !== Node.ELEMENT_NODE) {
  183. // Don't move modal's DOM position
  184. document.body.appendChild(this._element)
  185. }
  186. this._element.style.display = 'block'
  187. this._element.removeAttribute('aria-hidden')
  188. this._element.scrollTop = 0
  189. if (transition) {
  190. Util.reflow(this._element)
  191. }
  192. $(this._element).addClass(ClassName.SHOW)
  193. if (this._config.focus) {
  194. this._enforceFocus()
  195. }
  196. const shownEvent = $.Event(Event.SHOWN, {
  197. relatedTarget
  198. })
  199. const transitionComplete = () => {
  200. if (this._config.focus) {
  201. this._element.focus()
  202. }
  203. this._isTransitioning = false
  204. $(this._element).trigger(shownEvent)
  205. }
  206. if (transition) {
  207. const transitionDuration = Util.getTransitionDurationFromElement(this._element)
  208. $(this._dialog)
  209. .one(Util.TRANSITION_END, transitionComplete)
  210. .emulateTransitionEnd(transitionDuration)
  211. } else {
  212. transitionComplete()
  213. }
  214. }
  215. _enforceFocus() {
  216. $(document)
  217. .off(Event.FOCUSIN) // Guard against infinite focus loop
  218. .on(Event.FOCUSIN, (event) => {
  219. if (document !== event.target &&
  220. this._element !== event.target &&
  221. $(this._element).has(event.target).length === 0) {
  222. this._element.focus()
  223. }
  224. })
  225. }
  226. _setEscapeEvent() {
  227. if (this._isShown && this._config.keyboard) {
  228. $(this._element).on(Event.KEYDOWN_DISMISS, (event) => {
  229. if (event.which === ESCAPE_KEYCODE) {
  230. event.preventDefault()
  231. this.hide()
  232. }
  233. })
  234. } else if (!this._isShown) {
  235. $(this._element).off(Event.KEYDOWN_DISMISS)
  236. }
  237. }
  238. _setResizeEvent() {
  239. if (this._isShown) {
  240. $(window).on(Event.RESIZE, (event) => this.handleUpdate(event))
  241. } else {
  242. $(window).off(Event.RESIZE)
  243. }
  244. }
  245. _hideModal() {
  246. this._element.style.display = 'none'
  247. this._element.setAttribute('aria-hidden', true)
  248. this._isTransitioning = false
  249. this._showBackdrop(() => {
  250. $(document.body).removeClass(ClassName.OPEN)
  251. this._resetAdjustments()
  252. this._resetScrollbar()
  253. $(this._element).trigger(Event.HIDDEN)
  254. })
  255. }
  256. _removeBackdrop() {
  257. if (this._backdrop) {
  258. $(this._backdrop).remove()
  259. this._backdrop = null
  260. }
  261. }
  262. _showBackdrop(callback) {
  263. const animate = $(this._element).hasClass(ClassName.FADE)
  264. ? ClassName.FADE : ''
  265. if (this._isShown && this._config.backdrop) {
  266. this._backdrop = document.createElement('div')
  267. this._backdrop.className = ClassName.BACKDROP
  268. if (animate) {
  269. this._backdrop.classList.add(animate)
  270. }
  271. $(this._backdrop).appendTo(document.body)
  272. $(this._element).on(Event.CLICK_DISMISS, (event) => {
  273. if (this._ignoreBackdropClick) {
  274. this._ignoreBackdropClick = false
  275. return
  276. }
  277. if (event.target !== event.currentTarget) {
  278. return
  279. }
  280. if (this._config.backdrop === 'static') {
  281. this._element.focus()
  282. } else {
  283. this.hide()
  284. }
  285. })
  286. if (animate) {
  287. Util.reflow(this._backdrop)
  288. }
  289. $(this._backdrop).addClass(ClassName.SHOW)
  290. if (!callback) {
  291. return
  292. }
  293. if (!animate) {
  294. callback()
  295. return
  296. }
  297. const backdropTransitionDuration = Util.getTransitionDurationFromElement(this._backdrop)
  298. $(this._backdrop)
  299. .one(Util.TRANSITION_END, callback)
  300. .emulateTransitionEnd(backdropTransitionDuration)
  301. } else if (!this._isShown && this._backdrop) {
  302. $(this._backdrop).removeClass(ClassName.SHOW)
  303. const callbackRemove = () => {
  304. this._removeBackdrop()
  305. if (callback) {
  306. callback()
  307. }
  308. }
  309. if ($(this._element).hasClass(ClassName.FADE)) {
  310. const backdropTransitionDuration = Util.getTransitionDurationFromElement(this._backdrop)
  311. $(this._backdrop)
  312. .one(Util.TRANSITION_END, callbackRemove)
  313. .emulateTransitionEnd(backdropTransitionDuration)
  314. } else {
  315. callbackRemove()
  316. }
  317. } else if (callback) {
  318. callback()
  319. }
  320. }
  321. // ----------------------------------------------------------------------
  322. // the following methods are used to handle overflowing modals
  323. // todo (fat): these should probably be refactored out of modal.js
  324. // ----------------------------------------------------------------------
  325. _adjustDialog() {
  326. const isModalOverflowing =
  327. this._element.scrollHeight > document.documentElement.clientHeight
  328. if (!this._isBodyOverflowing && isModalOverflowing) {
  329. this._element.style.paddingLeft = `${this._scrollbarWidth}px`
  330. }
  331. if (this._isBodyOverflowing && !isModalOverflowing) {
  332. this._element.style.paddingRight = `${this._scrollbarWidth}px`
  333. }
  334. }
  335. _resetAdjustments() {
  336. this._element.style.paddingLeft = ''
  337. this._element.style.paddingRight = ''
  338. }
  339. _checkScrollbar() {
  340. const rect = document.body.getBoundingClientRect()
  341. this._isBodyOverflowing = rect.left + rect.right < window.innerWidth
  342. this._scrollbarWidth = this._getScrollbarWidth()
  343. }
  344. _setScrollbar() {
  345. if (this._isBodyOverflowing) {
  346. // Note: DOMNode.style.paddingRight returns the actual value or '' if not set
  347. // while $(DOMNode).css('padding-right') returns the calculated value or 0 if not set
  348. const fixedContent = [].slice.call(document.querySelectorAll(Selector.FIXED_CONTENT))
  349. const stickyContent = [].slice.call(document.querySelectorAll(Selector.STICKY_CONTENT))
  350. // Adjust fixed content padding
  351. $(fixedContent).each((index, element) => {
  352. const actualPadding = element.style.paddingRight
  353. const calculatedPadding = $(element).css('padding-right')
  354. $(element)
  355. .data('padding-right', actualPadding)
  356. .css('padding-right', `${parseFloat(calculatedPadding) + this._scrollbarWidth}px`)
  357. })
  358. // Adjust sticky content margin
  359. $(stickyContent).each((index, element) => {
  360. const actualMargin = element.style.marginRight
  361. const calculatedMargin = $(element).css('margin-right')
  362. $(element)
  363. .data('margin-right', actualMargin)
  364. .css('margin-right', `${parseFloat(calculatedMargin) - this._scrollbarWidth}px`)
  365. })
  366. // Adjust body padding
  367. const actualPadding = document.body.style.paddingRight
  368. const calculatedPadding = $(document.body).css('padding-right')
  369. $(document.body)
  370. .data('padding-right', actualPadding)
  371. .css('padding-right', `${parseFloat(calculatedPadding) + this._scrollbarWidth}px`)
  372. }
  373. }
  374. _resetScrollbar() {
  375. // Restore fixed content padding
  376. const fixedContent = [].slice.call(document.querySelectorAll(Selector.FIXED_CONTENT))
  377. $(fixedContent).each((index, element) => {
  378. const padding = $(element).data('padding-right')
  379. $(element).removeData('padding-right')
  380. element.style.paddingRight = padding ? padding : ''
  381. })
  382. // Restore sticky content
  383. const elements = [].slice.call(document.querySelectorAll(`${Selector.STICKY_CONTENT}`))
  384. $(elements).each((index, element) => {
  385. const margin = $(element).data('margin-right')
  386. if (typeof margin !== 'undefined') {
  387. $(element).css('margin-right', margin).removeData('margin-right')
  388. }
  389. })
  390. // Restore body padding
  391. const padding = $(document.body).data('padding-right')
  392. $(document.body).removeData('padding-right')
  393. document.body.style.paddingRight = padding ? padding : ''
  394. }
  395. _getScrollbarWidth() { // thx d.walsh
  396. const scrollDiv = document.createElement('div')
  397. scrollDiv.className = ClassName.SCROLLBAR_MEASURER
  398. document.body.appendChild(scrollDiv)
  399. const scrollbarWidth = scrollDiv.getBoundingClientRect().width - scrollDiv.clientWidth
  400. document.body.removeChild(scrollDiv)
  401. return scrollbarWidth
  402. }
  403. // Static
  404. static _jQueryInterface(config, relatedTarget) {
  405. return this.each(function () {
  406. let data = $(this).data(DATA_KEY)
  407. const _config = {
  408. ...Default,
  409. ...$(this).data(),
  410. ...typeof config === 'object' && config ? config : {}
  411. }
  412. if (!data) {
  413. data = new Modal(this, _config)
  414. $(this).data(DATA_KEY, data)
  415. }
  416. if (typeof config === 'string') {
  417. if (typeof data[config] === 'undefined') {
  418. throw new TypeError(`No method named "${config}"`)
  419. }
  420. data[config](relatedTarget)
  421. } else if (_config.show) {
  422. data.show(relatedTarget)
  423. }
  424. })
  425. }
  426. }
  427. /**
  428. * ------------------------------------------------------------------------
  429. * Data Api implementation
  430. * ------------------------------------------------------------------------
  431. */
  432. $(document).on(Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) {
  433. let target
  434. const selector = Util.getSelectorFromElement(this)
  435. if (selector) {
  436. target = document.querySelector(selector)
  437. }
  438. const config = $(target).data(DATA_KEY)
  439. ? 'toggle' : {
  440. ...$(target).data(),
  441. ...$(this).data()
  442. }
  443. if (this.tagName === 'A' || this.tagName === 'AREA') {
  444. event.preventDefault()
  445. }
  446. const $target = $(target).one(Event.SHOW, (showEvent) => {
  447. if (showEvent.isDefaultPrevented()) {
  448. // Only register focus restorer if modal will actually get shown
  449. return
  450. }
  451. $target.one(Event.HIDDEN, () => {
  452. if ($(this).is(':visible')) {
  453. this.focus()
  454. }
  455. })
  456. })
  457. Modal._jQueryInterface.call($(target), config, this)
  458. })
  459. /**
  460. * ------------------------------------------------------------------------
  461. * jQuery
  462. * ------------------------------------------------------------------------
  463. */
  464. $.fn[NAME] = Modal._jQueryInterface
  465. $.fn[NAME].Constructor = Modal
  466. $.fn[NAME].noConflict = function () {
  467. $.fn[NAME] = JQUERY_NO_CONFLICT
  468. return Modal._jQueryInterface
  469. }
  470. return Modal
  471. })($)
  472. export default Modal