| 476 | 479 | |
| 477 | 480 | // Bind dialog listeners manually, so we can be sure context is preserved |
| 478 | 481 | const cancelListener = async (evt) => { |
| 482 | + // If neither cancel button nor close button is visible or present, don't allow escape to close the popup |
| 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(); |
| 487 | + evt.stopPropagation(); |
| 488 | + // Set flag so closeListener also blocks the close event (browser may fire it after multiple Escape presses) |
| 489 | + this.#isClosingPrevented = true; |
| 490 | + |
| 491 | + // Check for double-escape within 500ms to allow force-closing |
| 492 | + const now = Date.now(); |
| 493 | + const timeSinceLastEscape = now - this.#lastEscapePress; |
| 494 | + this.#lastEscapePress = now; |
| 495 | + |
| 496 | + if (timeSinceLastEscape < 500 && !this.#isShowingForceCloseConfirm) { |
| 497 | + this.#isShowingForceCloseConfirm = true; |
| 498 | + |
| 499 | + // Defer to next frame to escape the current event context, |
| 500 | + // allowing the confirmation popup to stack properly on top |
| 501 | + requestAnimationFrame(async () => { |
| 502 | + const confirmPopup = new Popup( |
| 503 | + PopupUtils.BuildTextWithHeader( |
| 504 | + t`Force-close Blocking Popup`, ` |
| 505 | + <p>${t`This action is blocking and not meant to be closed manually.`}</p> |
| 506 | + <p>${t`Force-closing may leave the application in an inconsistent state.`}</p> |
| 507 | + <p><strong>${t`Are you sure you want to force-close?`}</strong></p>`), |
| 508 | + POPUP_TYPE.CONFIRM, |
| 509 | + '', |
| 510 | + { okButton: t`Force Close`, cancelButton: t`Cancel` }); |
| 511 | + |
| 512 | + // If the the main popup closes while the force-close popup is still being displayed, we gracefully cancel that. |
| 513 | + const originalOnClose = this.onClose; |
| 514 | + this.onClose = async (x) => { |
| 515 | + if (originalOnClose) await originalOnClose; |
| 516 | + await confirmPopup.completeCancelled(); |
| 517 | + }; |
| 518 | + |
| 519 | + |
| 520 | + const result = await confirmPopup.show(); |
| 521 | + this.#isShowingForceCloseConfirm = false; |
| 522 | + if (result === POPUP_RESULT.AFFIRMATIVE) { |
| 523 | + // Force-close by bypassing the normal close prevention |
| 524 | + this.#isClosingPrevented = false; |
| 525 | + await this.complete(POPUP_RESULT.CANCELLED); |
| 526 | + } |
| 527 | + }); |
| 528 | + } |
| 529 | + return; |
| 530 | + } |
| 531 | + |
| 479 | 532 | evt.preventDefault(); |
| 480 | 533 | evt.stopPropagation(); |
| 481 | 534 | await this.complete(POPUP_RESULT.CANCELLED); |