Add `allowEscapeClose` option to popup class for Escape key behavior override (#5340) * feat(popup): add allowEscapeClose option to override default escape key behavior Add `allowEscapeClose` parameter to PopupOptions to explicitly control whether Escape key closes the popup, overriding the default logic that checks for visible cancel/close buttons. When null (default), uses existing behavior; when true, allows escape even without buttons; when false, prevents escape even with buttons present. * feat(popup): enable Escape key closing for informational popups Add `allowEscapeClose: true` option to TEXT-type popups in export preset, persona lore, sampler select, stats, and world info assignment dialogs to allow users to dismiss these informational popups with the Escape key. * Improve Escape key interaction in popups * Adjust jsdoc for allowEscapeClose * Always return CANCELLED on Escape * fix(popup): correct jsdoc for allowEscapeClose property description --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>
Signed| @@ -488,7 +488,13 @@ function showOverlay(customContent = null) { | |||
| 488 | 488 | ||
| 489 | const content = getOverlayContent(customContent); | 489 | const content = getOverlayContent(customContent); |
| 490 | 490 | ||
| 491 | loaderPopup = new Popup(content, POPUP_TYPE.DISPLAY, null, { transparent: true, animation: 'none', wide: true, large: true }); | 491 | loaderPopup = new Popup(content, POPUP_TYPE.DISPLAY, null, { |
| 492 | allowEscapeClose: false, | ||
| 493 | transparent: true, | ||
| 494 | animation: 'none', | ||
| 495 | wide: true, | ||
| 496 | large: true, | ||
| 497 | }); | ||
| 492 | 498 | ||
| 493 | // No close button, loaders are not closable | 499 | // No close button, loaders are not closable |
| 494 | loaderPopup.closeButton.style.display = 'none'; | 500 | loaderPopup.closeButton.style.display = 'none'; |
| @@ -54,6 +54,7 @@ export const POPUP_RESULT = { | |||
| 54 | * @property {POPUP_RESULT|number?} [defaultResult=POPUP_RESULT.AFFIRMATIVE] - The default result of this popup when Enter is pressed. Can be changed from `POPUP_RESULT.AFFIRMATIVE`. | 54 | * @property {POPUP_RESULT|number?} [defaultResult=POPUP_RESULT.AFFIRMATIVE] - The default result of this popup when Enter is pressed. Can be changed from `POPUP_RESULT.AFFIRMATIVE`. |
| 55 | * @property {CustomPopupButton[]|string[]?} [customButtons=null] - Custom buttons to add to the popup. If only strings are provided, the buttons will be added with default options, and their result will be in order from `2` onward. | 55 | * @property {CustomPopupButton[]|string[]?} [customButtons=null] - Custom buttons to add to the popup. If only strings are provided, the buttons will be added with default options, and their result will be in order from `2` onward. |
| 56 | * @property {CustomPopupInput[]?} [customInputs=null] - Custom inputs to add to the popup. The display below the content and the input box, one by one. | 56 | * @property {CustomPopupInput[]?} [customInputs=null] - Custom inputs to add to the popup. The display below the content and the input box, one by one. |
| 57 | * @property {boolean} [allowEscapeClose=true] - If true, allows closing the popup with the Escape key, returning `POPUP_RESULT.CANCELLED`. If false, requires double-escape to force close with a confirmation to prevent accidental closure. | ||
| 57 | * @property {(popup: Popup) => Promise<boolean?>|boolean?} [onClosing=null] - Handler called before the popup closes, return `false` to cancel the close | 58 | * @property {(popup: Popup) => Promise<boolean?>|boolean?} [onClosing=null] - Handler called before the popup closes, return `false` to cancel the close |
| 58 | * @property {(popup: Popup) => Promise<void?>|void?} [onClose=null] - Handler called after the popup closes, but before the DOM is cleaned up | 59 | * @property {(popup: Popup) => Promise<void?>|void?} [onClose=null] - Handler called after the popup closes, but before the DOM is cleaned up |
| 59 | * @property {(popup: Popup) => Promise<void?>|void?} [onOpen=null] - Handler called after the popup opens | 60 | * @property {(popup: Popup) => Promise<void?>|void?} [onOpen=null] - Handler called after the popup opens |
| @@ -173,6 +174,8 @@ export class Popup { | |||
| 173 | 174 | ||
| 174 | /** @type {Promise<any>} */ #promise; | 175 | /** @type {Promise<any>} */ #promise; |
| 175 | /** @type {(result: any) => any} */ #resolver; | 176 | /** @type {(result: any) => any} */ #resolver; |
| 177 | |||
| 178 | /** @type {boolean} */ #allowEscapeClose; | ||
| 176 | /** @type {boolean} */ #isClosingPrevented; | 179 | /** @type {boolean} */ #isClosingPrevented; |
| 177 | /** @type {number} */ #lastEscapePress = 0; | 180 | /** @type {number} */ #lastEscapePress = 0; |
| 178 | /** @type {boolean} */ #isShowingForceCloseConfirm = false; | 181 | /** @type {boolean} */ #isShowingForceCloseConfirm = false; |
| @@ -185,13 +188,39 @@ export class Popup { | |||
| 185 | * @param {string} [inputValue=''] - The initial value of the input field | 188 | * @param {string} [inputValue=''] - The initial value of the input field |
| 186 | * @param {PopupOptions} [options={}] - Additional options for the popup | 189 | * @param {PopupOptions} [options={}] - Additional options for the popup |
| 187 | */ | 190 | */ |
| 188 | constructor(content, type, inputValue = '', { okButton = null, cancelButton = null, rows = 1, placeholder = null, tooltip = null, wide = false, wider = false, large = false, transparent = false, allowHorizontalScrolling = false, allowVerticalScrolling = false, leftAlign = false, animation = 'fast', defaultResult = POPUP_RESULT.AFFIRMATIVE, customButtons = null, customInputs = null, onClosing = null, onClose = null, onOpen = null, cropAspect = null, cropImage = null } = {}) { | 191 | constructor(content, type, inputValue = '', { |
| 192 | okButton = null, | ||
| 193 | cancelButton = null, | ||
| 194 | rows = 1, | ||
| 195 | placeholder = null, | ||
| 196 | tooltip = null, | ||
| 197 | wide = false, | ||
| 198 | wider = false, | ||
| 199 | large = false, | ||
| 200 | transparent = false, | ||
| 201 | allowHorizontalScrolling = false, | ||
| 202 | allowVerticalScrolling = false, | ||
| 203 | leftAlign = false, | ||
| 204 | animation = 'fast', | ||
| 205 | defaultResult = POPUP_RESULT.AFFIRMATIVE, | ||
| 206 | customButtons = null, | ||
| 207 | customInputs = null, | ||
| 208 | allowEscapeClose = true, | ||
| 209 | onClosing = null, | ||
| 210 | onClose = null, | ||
| 211 | onOpen = null, | ||
| 212 | cropAspect = null, | ||
| 213 | cropImage = null, | ||
| 214 | } = {}) { | ||
| 189 | Popup.util.popups.push(this); | 215 | Popup.util.popups.push(this); |
| 190 | 216 | ||
| 191 | // Make this popup uniquely identifiable | 217 | // Make this popup uniquely identifiable |
| 192 | this.id = uuidv4(); | 218 | this.id = uuidv4(); |
| 193 | this.type = type; | 219 | this.type = type; |
| 194 | 220 | ||
| 221 | // Setup some args being passed in as private properties | ||
| 222 | this.#allowEscapeClose = allowEscapeClose; | ||
| 223 | |||
| 195 | // Utilize event handlers being passed in | 224 | // Utilize event handlers being passed in |
| 196 | this.onClosing = onClosing; | 225 | this.onClosing = onClosing; |
| 197 | this.onClose = onClose; | 226 | this.onClose = onClose; |
| @@ -479,10 +508,7 @@ export class Popup { | |||
| 479 | 508 | ||
| 480 | // Bind dialog listeners manually, so we can be sure context is preserved | 509 | // Bind dialog listeners manually, so we can be sure context is preserved |
| 481 | const cancelListener = async (evt) => { | 510 | const cancelListener = async (evt) => { |
| 482 | // If neither cancel button nor close button is visible or present, don't allow escape to close the popup | 511 | if (!this.#allowEscapeClose) { |
| 483 | const hasCancelButton = this.cancelButton?.offsetParent !== null && this.buttonControls?.offsetParent !== null; | ||
| 484 | const hasCloseButton = this.closeButton?.offsetParent !== null; | ||
| 485 | if (!hasCancelButton && !hasCloseButton) { | ||
| 486 | evt.preventDefault(); | 512 | evt.preventDefault(); |
| 487 | evt.stopPropagation(); | 513 | evt.stopPropagation(); |
| 488 | // Set flag so closeListener also blocks the close event (browser may fire it after multiple Escape presses) | 514 | // Set flag so closeListener also blocks the close event (browser may fire it after multiple Escape presses) |