1/** 2 * -------------------------------------------------------------------------- 3 * Bootstrap (v5.0.2): modal.js 4 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) 5 * -------------------------------------------------------------------------- 6 */ 7 8import { 9 defineJQueryPlugin, 10 getElementFromSelector, 11 isRTL, 12 isVisible, 13 reflow, 14 typeCheckConfig 15} from './util/index.js' 16import EventHandler from './dom/event-handler.js' 17import Manipulator from './dom/manipulator.js' 18import SelectorEngine from './dom/selector-engine.js' 19import ScrollBarHelper from './util/scrollbar.js' 20import BaseComponent from './base-component.js' 21import Backdrop from './util/backdrop.js' 22 23/** 24 * ------------------------------------------------------------------------ 25 * Constants 26 * ------------------------------------------------------------------------ 27 */ 28 29const NAME = 'modal' 30const DATA_KEY = 'bs.modal' 31const EVENT_KEY = `.${DATA_KEY}` 32const DATA_API_KEY = '.data-api' 33const ESCAPE_KEY = 'Escape' 34 35const Default = { 36 backdrop: true, 37 keyboard: true, 38 focus: true 39} 40 41const DefaultType = { 42 backdrop: '(boolean|string)', 43 keyboard: 'boolean', 44 focus: 'boolean' 45} 46 47const EVENT_HIDE = `hide${EVENT_KEY}` 48const EVENT_HIDE_PREVENTED = `hidePrevented${EVENT_KEY}` 49const EVENT_HIDDEN = `hidden${EVENT_KEY}` 50const EVENT_SHOW = `show${EVENT_KEY}` 51const EVENT_SHOWN = `shown${EVENT_KEY}` 52const EVENT_FOCUSIN = `focusin${EVENT_KEY}` 53const EVENT_RESIZE = `resize${EVENT_KEY}` 54const EVENT_CLICK_DISMISS = `click.dismiss${EVENT_KEY}` 55const EVENT_KEYDOWN_DISMISS = `keydown.dismiss${EVENT_KEY}` 56const EVENT_MOUSEUP_DISMISS = `mouseup.dismiss${EVENT_KEY}` 57const EVENT_MOUSEDOWN_DISMISS = `mousedown.dismiss${EVENT_KEY}` 58const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}` 59 60const CLASS_NAME_OPEN = 'modal-open' 61const CLASS_NAME_FADE = 'fade' 62const CLASS_NAME_SHOW = 'show' 63const CLASS_NAME_STATIC = 'modal-static' 64 65const SELECTOR_DIALOG = '.modal-dialog' 66const SELECTOR_MODAL_BODY = '.modal-body' 67const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="modal"]' 68const SELECTOR_DATA_DISMISS = '[data-bs-dismiss="modal"]' 69 70/** 71 * ------------------------------------------------------------------------ 72 * Class Definition 73 * ------------------------------------------------------------------------ 74 */ 75 76class Modal extends BaseComponent { 77 constructor(element, config) { 78 super(element) 79 80 this._config = this._getConfig(config) 81 this._dialog = SelectorEngine.findOne(SELECTOR_DIALOG, this._element) 82 this._backdrop = this._initializeBackDrop() 83 this._isShown = false 84 this._ignoreBackdropClick = false 85 this._isTransitioning = false 86 this._scrollBar = new ScrollBarHelper() 87 } 88 89 // Getters 90 91 static get Default() { 92 return Default 93 } 94 95 static get NAME() { 96 return NAME 97 } 98 99 // Public 100 101 toggle(relatedTarget) { 102 return this._isShown ? this.hide() : this.show(relatedTarget) 103 } 104 105 show(relatedTarget) { 106 if (this._isShown || this._isTransitioning) { 107 return 108 } 109 110 const showEvent = EventHandler.trigger(this._element, EVENT_SHOW, { 111 relatedTarget 112 }) 113 114 if (showEvent.defaultPrevented) { 115 return 116 } 117 118 this._isShown = true 119 120 if (this._isAnimated()) { 121 this._isTransitioning = true 122 } 123 124 this._scrollBar.hide() 125 126 document.body.classList.add(CLASS_NAME_OPEN) 127 128 this._adjustDialog() 129 130 this._setEscapeEvent() 131 this._setResizeEvent() 132 133 EventHandler.on(this._element, EVENT_CLICK_DISMISS, SELECTOR_DATA_DISMISS, event => this.hide(event)) 134 135 EventHandler.on(this._dialog, EVENT_MOUSEDOWN_DISMISS, () => { 136 EventHandler.one(this._element, EVENT_MOUSEUP_DISMISS, event => { 137 if (event.target === this._element) { 138 this._ignoreBackdropClick = true 139 } 140 }) 141 }) 142 143 this._showBackdrop(() => this._showElement(relatedTarget)) 144 } 145 146 hide(event) { 147 if (event && ['A', 'AREA'].includes(event.target.tagName)) { 148 event.preventDefault() 149 } 150 151 if (!this._isShown || this._isTransitioning) { 152 return 153 } 154 155 const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE) 156 157 if (hideEvent.defaultPrevented) { 158 return 159 } 160 161 this._isShown = false 162 const isAnimated = this._isAnimated() 163 164 if (isAnimated) { 165 this._isTransitioning = true 166 } 167 168 this._setEscapeEvent() 169 this._setResizeEvent() 170 171 EventHandler.off(document, EVENT_FOCUSIN) 172 173 this._element.classList.remove(CLASS_NAME_SHOW) 174 175 EventHandler.off(this._element, EVENT_CLICK_DISMISS) 176 EventHandler.off(this._dialog, EVENT_MOUSEDOWN_DISMISS) 177 178 this._queueCallback(() => this._hideModal(), this._element, isAnimated) 179 } 180 181 dispose() { 182 [window, this._dialog] 183 .forEach(htmlElement => EventHandler.off(htmlElement, EVENT_KEY)) 184 185 this._backdrop.dispose() 186 super.dispose() 187 188 /** 189 * `document` has 2 events `EVENT_FOCUSIN` and `EVENT_CLICK_DATA_API` 190 * Do not move `document` in `htmlElements` array 191 * It will remove `EVENT_CLICK_DATA_API` event that should remain 192 */ 193 EventHandler.off(document, EVENT_FOCUSIN) 194 } 195 196 handleUpdate() { 197 this._adjustDialog() 198 } 199 200 // Private 201 202 _initializeBackDrop() { 203 return new Backdrop({ 204 isVisible: Boolean(this._config.backdrop), // 'static' option will be translated to true, and booleans will keep their value 205 isAnimated: this._isAnimated() 206 }) 207 } 208 209 _getConfig(config) { 210 config = { 211 ...Default, 212 ...Manipulator.getDataAttributes(this._element), 213 ...(typeof config === 'object' ? config : {}) 214 } 215 typeCheckConfig(NAME, config, DefaultType) 216 return config 217 } 218 219 _showElement(relatedTarget) { 220 const isAnimated = this._isAnimated() 221 const modalBody = SelectorEngine.findOne(SELECTOR_MODAL_BODY, this._dialog) 222 223 if (!this._element.parentNode || this._element.parentNode.nodeType !== Node.ELEMENT_NODE) { 224 // Don't move modal's DOM position 225 document.body.appendChild(this._element) 226 } 227 228 this._element.style.display = 'block' 229 this._element.removeAttribute('aria-hidden') 230 this._element.setAttribute('aria-modal', true) 231 this._element.setAttribute('role', 'dialog') 232 this._element.scrollTop = 0 233 234 if (modalBody) { 235 modalBody.scrollTop = 0 236 } 237 238 if (isAnimated) { 239 reflow(this._element) 240 } 241 242 this._element.classList.add(CLASS_NAME_SHOW) 243 244 if (this._config.focus) { 245 this._enforceFocus() 246 } 247 248 const transitionComplete = () => { 249 if (this._config.focus) { 250 this._element.focus() 251 } 252 253 this._isTransitioning = false 254 EventHandler.trigger(this._element, EVENT_SHOWN, { 255 relatedTarget 256 }) 257 } 258 259 this._queueCallback(transitionComplete, this._dialog, isAnimated) 260 } 261 262 _enforceFocus() { 263 EventHandler.off(document, EVENT_FOCUSIN) // guard against infinite focus loop 264 EventHandler.on(document, EVENT_FOCUSIN, event => { 265 if (document !== event.target && 266 this._element !== event.target && 267 !this._element.contains(event.target)) { 268 this._element.focus() 269 } 270 }) 271 } 272 273 _setEscapeEvent() { 274 if (this._isShown) { 275 EventHandler.on(this._element, EVENT_KEYDOWN_DISMISS, event => { 276 if (this._config.keyboard && event.key === ESCAPE_KEY) { 277 event.preventDefault() 278 this.hide() 279 } else if (!this._config.keyboard && event.key === ESCAPE_KEY) { 280 this._triggerBackdropTransition() 281 } 282 }) 283 } else { 284 EventHandler.off(this._element, EVENT_KEYDOWN_DISMISS) 285 } 286 } 287 288 _setResizeEvent() { 289 if (this._isShown) { 290 EventHandler.on(window, EVENT_RESIZE, () => this._adjustDialog()) 291 } else { 292 EventHandler.off(window, EVENT_RESIZE) 293 } 294 } 295 296 _hideModal() { 297 this._element.style.display = 'none' 298 this._element.setAttribute('aria-hidden', true) 299 this._element.removeAttribute('aria-modal') 300 this._element.removeAttribute('role') 301 this._isTransitioning = false 302 this._backdrop.hide(() => { 303 document.body.classList.remove(CLASS_NAME_OPEN) 304 this._resetAdjustments() 305 this._scrollBar.reset() 306 EventHandler.trigger(this._element, EVENT_HIDDEN) 307 }) 308 } 309 310 _showBackdrop(callback) { 311 EventHandler.on(this._element, EVENT_CLICK_DISMISS, event => { 312 if (this._ignoreBackdropClick) { 313 this._ignoreBackdropClick = false 314 return 315 } 316 317 if (event.target !== event.currentTarget) { 318 return 319 } 320 321 if (this._config.backdrop === true) { 322 this.hide() 323 } else if (this._config.backdrop === 'static') { 324 this._triggerBackdropTransition() 325 } 326 }) 327 328 this._backdrop.show(callback) 329 } 330 331 _isAnimated() { 332 return this._element.classList.contains(CLASS_NAME_FADE) 333 } 334 335 _triggerBackdropTransition() { 336 const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED) 337 if (hideEvent.defaultPrevented) { 338 return 339 } 340 341 const { classList, scrollHeight, style } = this._element 342 const isModalOverflowing = scrollHeight > document.documentElement.clientHeight 343 344 // return if the following background transition hasn't yet completed 345 if ((!isModalOverflowing && style.overflowY === 'hidden') || classList.contains(CLASS_NAME_STATIC)) { 346 return 347 } 348 349 if (!isModalOverflowing) { 350 style.overflowY = 'hidden' 351 } 352 353 classList.add(CLASS_NAME_STATIC) 354 this._queueCallback(() => { 355 classList.remove(CLASS_NAME_STATIC) 356 if (!isModalOverflowing) { 357 this._queueCallback(() => { 358 style.overflowY = '' 359 }, this._dialog) 360 } 361 }, this._dialog) 362 363 this._element.focus() 364 } 365 366 // ---------------------------------------------------------------------- 367 // the following methods are used to handle overflowing modals 368 // ---------------------------------------------------------------------- 369
370 _adjustDialog() { 371 const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight 372 const scrollbarWidth = this._scrollBar.getWidth() 373 const isBodyOverflowing = scrollbarWidth > 0 374 375 if ((!isBodyOverflowing && isModalOverflowing && !isRTL()) || (isBodyOverflowing && !isModalOverflowing && isRTL())) { 376 this._element.style.paddingLeft = `${scrollbarWidth}px` 377 } 378 379 if ((isBodyOverflowing && !isModalOverflowing && !isRTL()) || (!isBodyOverflowing && isModalOverflowing && isRTL())) { 380 this._element.style.paddingRight = `${scrollbarWidth}px` 381 } 382 } 383 384 _resetAdjustments() { 385 this._element.style.paddingLeft = '' 386 this._element.style.paddingRight = '' 387 } 388 389 // Static 390 391 static jQueryInterface(config, relatedTarget) { 392 return this.each(function () { 393 const data = Modal.getOrCreateInstance(this, config) 394 395 if (typeof config !== 'string') { 396 return 397 } 398 399 if (typeof data[config] === 'undefined') { 400 throw new TypeError(`No method named "${config}"`) 401 } 402 403 data[config](relatedTarget) 404 }) 405 } 406} 407 408/** 409 * ------------------------------------------------------------------------ 410 * Data Api implementation 411 * ------------------------------------------------------------------------ 412 */ 413 414EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) { 415 const target = getElementFromSelector(this) 416 417 if (['A', 'AREA'].includes(this.tagName)) { 418 event.preventDefault() 419 } 420 421 EventHandler.one(target, EVENT_SHOW, showEvent => { 422 if (showEvent.defaultPrevented) { 423 // only register focus restorer if modal will actually get shown 424 return 425 } 426 427 EventHandler.one(target, EVENT_HIDDEN, () => { 428 if (isVisible(this)) { 429 this.focus() 430 } 431 }) 432 }) 433 434 const data = Modal.getOrCreateInstance(target) 435 436 data.toggle(this) 437}) 438 439/** 440 * ------------------------------------------------------------------------ 441 * jQuery 442 * ------------------------------------------------------------------------ 443 * add .Modal to jQuery only if jQuery is present 444 */ 445 446defineJQueryPlugin(Modal) 447 448export default Modal
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.