| 5 |
5 |
|
|
| 6 |
6 |
|
(function() {
|
| 7 |
7 |
|
'use strict';
|
| 8 |
|
- |
const escAttr = GoingsOn.utils.escapeAttrValue;
|
| 9 |
|
- |
const esc = GoingsOn.utils.escapeHtml;
|
|
8 |
+ |
// Straight from the escape.js seal rather than the utils.js re-export, so this
|
|
9 |
+ |
// module loads in the standalone compose window (escape.js, no utils.js) as
|
|
10 |
+ |
// well as the main app. Same functions either way.
|
|
11 |
+ |
const escAttr = GoingsOn.escape.escapeAttrValue;
|
|
12 |
+ |
const esc = GoingsOn.escape.escapeHtml;
|
| 10 |
13 |
|
|
| 11 |
14 |
|
// Modal
|
| 12 |
15 |
|
|
| 18 |
21 |
|
* @param {boolean} [options.large] - Use large modal size (nearly full screen)
|
| 19 |
22 |
|
*/
|
| 20 |
23 |
|
function openModal(title, content, options = {}) {
|
|
24 |
+ |
// A modal that replaces another without closing it settles the outgoing
|
|
25 |
+ |
// dialog as dismissed rather than leaving its promise pending.
|
|
26 |
+ |
const superseded = onModalDismiss;
|
|
27 |
+ |
onModalDismiss = null;
|
|
28 |
+ |
if (superseded) superseded();
|
|
29 |
+ |
|
| 21 |
30 |
|
const overlay = document.getElementById('modal-overlay');
|
| 22 |
31 |
|
const container = overlay.querySelector('.modal-container');
|
| 23 |
32 |
|
const titleEl = document.getElementById('modal-title');
|
| 47 |
56 |
|
trapFocus(overlay);
|
| 48 |
57 |
|
}
|
| 49 |
58 |
|
|
|
59 |
+ |
/**
|
|
60 |
+ |
* Called once when the open modal closes by any path, then cleared. Lets an
|
|
61 |
+ |
* awaited dialog settle its promise when the user dismisses it with Escape, an
|
|
62 |
+ |
* overlay click, or the close button instead of one of its own buttons; without
|
|
63 |
+ |
* it those promises stay pending forever and the caller never resumes.
|
|
64 |
+ |
*/
|
|
65 |
+ |
let onModalDismiss = null;
|
|
66 |
+ |
|
|
67 |
+ |
/**
|
|
68 |
+ |
* Registers a dismissal callback for the currently open modal.
|
|
69 |
+ |
* @param {Function|null} fn - Called with no arguments when the modal closes
|
|
70 |
+ |
*/
|
|
71 |
+ |
function setModalDismissHandler(fn) {
|
|
72 |
+ |
onModalDismiss = fn;
|
|
73 |
+ |
}
|
|
74 |
+ |
|
| 50 |
75 |
|
/**
|
| 51 |
76 |
|
* Close the modal dialog.
|
| 52 |
77 |
|
*/
|
| 56 |
81 |
|
overlay.classList.add('hidden');
|
| 57 |
82 |
|
overlay.setAttribute('aria-hidden', 'true');
|
| 58 |
83 |
|
releaseFocusTrap();
|
|
84 |
+ |
|
|
85 |
+ |
// Cleared before the call so a handler that reopens a modal keeps its own.
|
|
86 |
+ |
const dismiss = onModalDismiss;
|
|
87 |
+ |
onModalDismiss = null;
|
|
88 |
+ |
if (dismiss) dismiss();
|
| 59 |
89 |
|
}
|
| 60 |
90 |
|
|
| 61 |
91 |
|
// Focus trap for modal accessibility
|
| 342 |
372 |
|
`;
|
| 343 |
373 |
|
|
| 344 |
374 |
|
openModal(title, content);
|
|
375 |
+ |
// Dismissing without answering is a decline.
|
|
376 |
+ |
setModalDismissHandler(() => resolve(false));
|
| 345 |
377 |
|
|
| 346 |
378 |
|
// Attach event handlers after modal is opened
|
| 347 |
379 |
|
setTimeout(() => {
|
| 348 |
380 |
|
const confirmBtn = document.getElementById('confirm-dialog-confirm');
|
| 349 |
381 |
|
const cancelBtn = document.getElementById('confirm-dialog-cancel');
|
| 350 |
382 |
|
|
| 351 |
|
- |
if (confirmBtn) {
|
| 352 |
|
- |
confirmBtn.onclick = () => {
|
| 353 |
|
- |
closeModal();
|
| 354 |
|
- |
resolve(true);
|
| 355 |
|
- |
};
|
| 356 |
|
- |
}
|
|
383 |
+ |
// Answering clears the dismiss handler first: closeModal() runs it
|
|
384 |
+ |
// otherwise, and its resolve would win the race against this one.
|
|
385 |
+ |
const answer = (value) => {
|
|
386 |
+ |
setModalDismissHandler(null);
|
|
387 |
+ |
closeModal();
|
|
388 |
+ |
resolve(value);
|
|
389 |
+ |
};
|
| 357 |
390 |
|
|
| 358 |
|
- |
if (cancelBtn) {
|
| 359 |
|
- |
cancelBtn.onclick = () => {
|
| 360 |
|
- |
closeModal();
|
| 361 |
|
- |
resolve(false);
|
| 362 |
|
- |
};
|
| 363 |
|
- |
}
|
|
391 |
+ |
if (confirmBtn) confirmBtn.onclick = () => answer(true);
|
|
392 |
+ |
if (cancelBtn) cancelBtn.onclick = () => answer(false);
|
| 364 |
393 |
|
}, 50);
|
| 365 |
394 |
|
});
|
| 366 |
395 |
|
}
|
| 419 |
448 |
|
`;
|
| 420 |
449 |
|
|
| 421 |
450 |
|
openModal(title, content);
|
|
451 |
+ |
// Dismissing without answering is a cancel.
|
|
452 |
+ |
setModalDismissHandler(() => resolve(null));
|
| 422 |
453 |
|
|
| 423 |
454 |
|
setTimeout(() => {
|
| 424 |
455 |
|
const input = document.getElementById(inputId);
|
| 426 |
457 |
|
const confirmBtn = document.getElementById('prompt-dialog-confirm');
|
| 427 |
458 |
|
const cancelBtn = document.getElementById('prompt-dialog-cancel');
|
| 428 |
459 |
|
|
|
460 |
+ |
// Answering clears the dismiss handler first: closeModal() runs it
|
|
461 |
+ |
// otherwise, and its resolve would win the race against this one.
|
|
462 |
+ |
const answer = (value) => {
|
|
463 |
+ |
setModalDismissHandler(null);
|
|
464 |
+ |
closeModal();
|
|
465 |
+ |
resolve(value);
|
|
466 |
+ |
};
|
|
467 |
+ |
|
| 429 |
468 |
|
const submit = () => {
|
| 430 |
469 |
|
const value = (input?.value || '').trim();
|
| 431 |
470 |
|
if (validate) {
|
| 438 |
477 |
|
return;
|
| 439 |
478 |
|
}
|
| 440 |
479 |
|
}
|
| 441 |
|
- |
closeModal();
|
| 442 |
|
- |
resolve(value);
|
|
480 |
+ |
answer(value);
|
| 443 |
481 |
|
};
|
| 444 |
482 |
|
|
| 445 |
483 |
|
if (confirmBtn) confirmBtn.onclick = submit;
|
| 446 |
|
- |
if (cancelBtn) cancelBtn.onclick = () => { closeModal(); resolve(null); };
|
|
484 |
+ |
if (cancelBtn) cancelBtn.onclick = () => answer(null);
|
| 447 |
485 |
|
if (input) {
|
| 448 |
486 |
|
input.addEventListener('keydown', (e) => {
|
| 449 |
487 |
|
if (e.key === 'Enter') { e.preventDefault(); submit(); }
|