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>

e52c035fcd63e737b9086e7dca89283312232d2b

Wolfsblvt <wolfsblvt@gmail.com>

Signed
2 files changed, +38 -6Showing whitespace changes
public/scripts/action-loader.js+7 -1
@@ -488,7 +488,13 @@ function showOverlay(customContent = null) {
488488
489 const content = getOverlayContent(customContent);489 const content = getOverlayContent(customContent);
490490
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 });
492498
493 // No close button, loaders are not closable499 // No close button, loaders are not closable
494 loaderPopup.closeButton.style.display = 'none';500 loaderPopup.closeButton.style.display = 'none';
public/scripts/popup.js+31 -5
@@ -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 close58 * @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 up59 * @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 opens60 * @property {(popup: Popup) => Promise<void?>|void?} [onOpen=null] - Handler called after the popup opens
@@ -173,6 +174,8 @@ export class Popup {
173174
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 field188 * @param {string} [inputValue=''] - The initial value of the input field
186 * @param {PopupOptions} [options={}] - Additional options for the popup189 * @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);
190216
191 // Make this popup uniquely identifiable217 // Make this popup uniquely identifiable
192 this.id = uuidv4();218 this.id = uuidv4();
193 this.type = type;219 this.type = type;
194220
221 // Setup some args being passed in as private properties
222 this.#allowEscapeClose = allowEscapeClose;
223
195 // Utilize event handlers being passed in224 // 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 {
479508
480 // Bind dialog listeners manually, so we can be sure context is preserved509 // 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 popup511 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)