| 476 | | 479 | |
| 477 | // Bind dialog listeners manually, so we can be sure context is preserved | 480 | // Bind dialog listeners manually, so we can be sure context is preserved |
| 478 | const cancelListener = async (evt) => { | 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 | evt.preventDefault(); | 532 | evt.preventDefault(); |
| 480 | evt.stopPropagation(); | 533 | evt.stopPropagation(); |
| 481 | await this.complete(POPUP_RESULT.CANCELLED); | 534 | await this.complete(POPUP_RESULT.CANCELLED); |