Compare commits

...

3 Commits

Author SHA1 Message Date
Denis Lopatin
e029f3c790
Merge ad443baf3a19598b3c5ac61159bd8f259ddc5ddd into 380a1d738b221fecc964260add053997399be4d4 2025-09-27 10:12:11 -05:00
Denis
ad443baf3a docs(modal): Update examples for dynamic configuration 2025-09-02 00:14:59 +03:00
Denis
9c526253b4 feature(modal): add dynamic config
- adds the ability to change the configuration settings of the modal window during script execution
- adds tests for new functionality
2025-09-01 21:35:59 +03:00
4 changed files with 219 additions and 12 deletions

View File

@ -12,7 +12,7 @@ import Backdrop from './util/backdrop.js'
import { enableDismissTrigger } from './util/component-functions.js'
import FocusTrap from './util/focustrap.js'
import {
defineJQueryPlugin, isRTL, isVisible, reflow
defineJQueryPlugin, execute, isRTL, isVisible, reflow
} from './util/index.js'
import ScrollBarHelper from './util/scrollbar.js'
@ -54,9 +54,9 @@ const Default = {
}
const DefaultType = {
backdrop: '(boolean|string)',
focus: 'boolean',
keyboard: 'boolean'
backdrop: '(boolean|string|function)',
focus: '(boolean|function)',
keyboard: '(boolean|function)'
}
/**
@ -157,7 +157,7 @@ class Modal extends BaseComponent {
// Private
_initializeBackDrop() {
return new Backdrop({
isVisible: Boolean(this._config.backdrop), // 'static' option will be translated to true, and booleans will keep their value,
isVisible: Boolean(this._resolvePossibleFunction(this._config.backdrop)), // 'static' option will be translated to true, and booleans will keep their value
isAnimated: this._isAnimated()
})
}
@ -190,7 +190,7 @@ class Modal extends BaseComponent {
this._element.classList.add(CLASS_NAME_SHOW)
const transitionComplete = () => {
if (this._config.focus) {
if (this._resolvePossibleFunction(this._config.focus)) {
this._focustrap.activate()
}
@ -209,7 +209,7 @@ class Modal extends BaseComponent {
return
}
if (this._config.keyboard) {
if (this._resolvePossibleFunction(this._config.keyboard)) {
this.hide()
return
}
@ -230,12 +230,14 @@ class Modal extends BaseComponent {
return
}
if (this._config.backdrop === 'static') {
const backdrop = this._resolvePossibleFunction(this._config.backdrop)
if (backdrop === 'static') {
this._triggerBackdropTransition()
return
}
if (this._config.backdrop) {
if (backdrop) {
this.hide()
}
})
@ -314,6 +316,10 @@ class Modal extends BaseComponent {
this._element.style.paddingRight = ''
}
_resolvePossibleFunction(arg) {
return execute(arg, [this])
}
// Static
static jQueryInterface(config, relatedTarget) {
return this.each(function () {

View File

@ -237,6 +237,7 @@ describe('Modal', () => {
modal.show()
})
})
it('should set is transitioning if fade class is present', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<div class="modal fade"><div class="modal-dialog"></div></div>'
@ -550,6 +551,7 @@ describe('Modal', () => {
modal.show()
})
})
it('should close modal when escape key is pressed with keyboard = true and backdrop is static', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
@ -677,6 +679,44 @@ describe('Modal', () => {
modal.show()
})
})
it('should call .focus() when config function returns true', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
const modalEl = fixtureEl.querySelector('.modal')
const focusSpy = spyOn(modalEl, 'focus')
const modal = new Modal(modalEl, {
focus: () => true
})
modalEl.addEventListener('shown.bs.modal', () => {
expect(focusSpy).toHaveBeenCalled()
resolve()
})
modal.show()
})
})
it('should NOT call .focus() when config function returns false', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
const modalEl = fixtureEl.querySelector('.modal')
const focusSpy = spyOn(modalEl, 'focus')
const modal = new Modal(modalEl, {
focus: () => false
})
modalEl.addEventListener('shown.bs.modal', () => {
expect(focusSpy).not.toHaveBeenCalled()
resolve()
})
modal.show()
})
})
})
describe('hide', () => {
@ -766,6 +806,129 @@ describe('Modal', () => {
})
})
it('should not close on Escape when "keyboard" option is dynamically changed to false', () => {
return new Promise((resolve, reject) => {
fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
const config = { keyboard: 'closing' }
const modalEl = fixtureEl.querySelector('.modal')
const modal = new Modal(modalEl, {
keyboard: () => config.keyboard === 'closing'
})
modalEl.addEventListener('shown.bs.modal', () => {
config.keyboard = 'nothing'
const keydownEscape = createEvent('keydown')
keydownEscape.key = 'Escape'
modalEl.dispatchEvent(keydownEscape)
expect(modal._isShown).toBeTrue()
resolve()
})
modalEl.addEventListener('hidden.bs.modal', () => {
reject(new Error('Should not hide a modal'))
})
modal.show()
})
})
it('should close on Escape when "keyboard" option is dynamically changed to true', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
const config = { keyboard: 'nothing' }
const modalEl = fixtureEl.querySelector('.modal')
const modal = new Modal(modalEl, {
keyboard: () => config.keyboard === 'closing'
})
modalEl.addEventListener('shown.bs.modal', () => {
config.keyboard = 'closing'
const keydownEscape = createEvent('keydown')
keydownEscape.key = 'Escape'
modalEl.dispatchEvent(keydownEscape)
})
modalEl.addEventListener('hidden.bs.modal', () => {
resolve()
})
modal.show()
})
})
it('should close by backdrop click when option dynamically changed to true', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
const config = { backdrop: 'static' }
const modalEl = fixtureEl.querySelector('.modal')
const modal = new Modal(modalEl, {
backdrop: () => config.backdrop
})
const backdropSpy = spyOn(modal._backdrop, 'hide').and.callThrough()
EventHandler.one(modalEl, 'click', () => {
if (config.backdrop === false) {
modal.hide()
}
})
modalEl.addEventListener('shown.bs.modal', () => {
config.backdrop = false
modalEl.click()
})
modalEl.addEventListener('hidden.bs.modal', () => {
expect(backdropSpy).toHaveBeenCalled()
resolve()
})
modal.show()
})
})
it('should not close by backdrop click when option dynamically changed to "static"', () => {
return new Promise((resolve, reject) => {
fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
const config = { backdrop: false }
const modalEl = fixtureEl.querySelector('.modal')
const modal = new Modal(modalEl, {
backdrop: () => config.backdrop
})
const backdropSpy = spyOn(modal._backdrop, 'hide').and.callThrough()
EventHandler.one(modalEl, 'click', () => {
if (config.backdrop === false) {
modal.hide()
}
})
modalEl.addEventListener('shown.bs.modal', () => {
config.backdrop = 'static'
modalEl.click()
expect(backdropSpy).not.toHaveBeenCalled()
resolve()
})
modalEl.addEventListener('hidden.bs.modal', () => {
reject(new Error('Should not hide a modal'))
})
modal.show()
})
})
it('should do nothing is the modal is not shown', () => {
fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
@ -1077,6 +1240,7 @@ describe('Modal', () => {
modal.show()
})
})
it('should not focus the trigger if the modal is not visible', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = [
@ -1109,6 +1273,7 @@ describe('Modal', () => {
trigger.click()
})
})
it('should not focus the trigger if the modal is not shown', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = [
@ -1162,6 +1327,7 @@ describe('Modal', () => {
})
})
})
describe('jQueryInterface', () => {
it('should create a modal', () => {
fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'

View File

@ -3,3 +3,5 @@ As options can be passed via data attributes or JavaScript, you can append an op
As of Bootstrap 5.2.0, all components support an **experimental** reserved data attribute `data-bs-config` that can house simple component configuration as a JSON string. When an element has `data-bs-config='{"delay":0, "title":123}'` and `data-bs-title="456"` attributes, the final `title` value will be `456` and the separate data attributes will override values given on `data-bs-config`. In addition, existing data attributes are able to house JSON values like `data-bs-delay='{"show":0,"hide":150}'`.
The final configuration object is the merged result of `data-bs-config`, `data-bs-`, and `js object` where the latest given key-value overrides the others.
You can pass a callback function there, which undertakes to return the value type specified for the parameter. This will allow you to change the logic of the modal window in runtime mode after you have passed the configuration object.

View File

@ -782,11 +782,44 @@ const myModalAlternative = new bootstrap.Modal('#myModal', options)
<BsTable>
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `backdrop` | boolean, `static'` | `true` | Includes a modal-backdrop element. Alternatively, specify `static` for a backdrop which doesnt close the modal when clicked. |
| `focus` | boolean | `true` | Puts the focus on the modal when initialized. |
| `keyboard` | boolean | `true` | Closes the modal when escape key is pressed. |
| `backdrop` | true, `static'`, <br /> function | `true` | Includes a modal-backdrop element. Alternatively, specify `static` for a backdrop which doesnt close the modal when clicked. |
| `focus` | boolean, function | `true` | Puts the focus on the modal when initialized. |
| `keyboard` | boolean, function | `true` | Closes the modal when escape key is pressed. |
</BsTable>
For example, if you want to prevent the user from closing the modal window using the Escape key to avoid accidental loss
of important data if they are entered, you can use instead:
```js
const confirmInput = document.getElementById('confirmInput')
const modalElement = document.getElementById('confirmModal')
new bootstrap.Modal(modalElement, { keyboard: true })
const keydownBlocker = event => {
if (event.key === 'Escape' && confirmInput.value.toLowerCase() === 'save') {
event.stopPropagation()
}
}
modalElement.addEventListener('show.bs.modal', () => {
document.addEventListener('keydown', keydownBlocker, true)
})
modalElement.addEventListener('hide.bs.modal', () => {
document.removeEventListener('keydown', keydownBlocker, true)
})
```
The next option is to transfer the function:
```js
const confirmInput = document.getElementById('confirmInput')
const modalElement = document.getElementById('confirmModal')
new bootstrap.Modal(modalElement, { keyboard: () => confirmInput.value.toLowerCase() !== 'save' })
```
### Methods
<Callout name="danger-async-methods" type="danger" />