| 1 | // fixtures/link-dialog.mjs — Ctrl+K. The dialog is server-rendered |
| 2 | // (templates/editor.html: a labelled URL field and three named |
| 3 | // buttons, every string from the catalog); this only opens it, waits |
| 4 | // for the answer, and applies it to the view the key was pressed in. |
| 5 | // |
| 6 | // A <dialog> and not window.prompt: a prompt's label is the browser's, |
| 7 | // its one button cannot say "remove", and it cannot be styled to meet |
| 8 | // the focus and target rules the rest of the page meets. |
| 9 | import { linkAt, setLink, removeLink } from './text-keys.mjs' |
| 10 | import { safeHref } from './schema.mjs' |
| 11 | |
| 12 | /** |
| 13 | * Wraps the dialog. open(current) shows it with the field seeded and |
| 14 | * resolves to {action, href} when it closes: action is the value of |
| 15 | * the button that closed it ("insert", "remove"), or "cancel" for |
| 16 | * Cancel and Escape alike. A second open() while the dialog is up |
| 17 | * joins the first: both resolve to the one answer, and the field is |
| 18 | * not reseeded under the person typing in it. |
| 19 | */ |
| 20 | export function mountLinkDialog (dialog) { |
| 21 | if (!dialog) return null |
| 22 | const input = dialog.querySelector('input[name="href"]') |
| 23 | let pending = null |
| 24 | dialog.addEventListener('close', () => { |
| 25 | const done = pending |
| 26 | pending = null |
| 27 | if (done) done.resolve({ action: dialog.returnValue || 'cancel', href: input.value.trim() }) |
| 28 | }) |
| 29 | return { |
| 30 | open (current) { |
| 31 | if (pending) return pending.promise |
| 32 | input.value = current || '' |
| 33 | dialog.returnValue = '' |
| 34 | dialog.showModal() |
| 35 | input.select() |
| 36 | let resolve |
| 37 | const promise = new Promise(r => { resolve = r }) |
| 38 | pending = { promise, resolve } |
| 39 | return promise |
| 40 | }, |
| 41 | get isOpen () { return pending !== null } |
| 42 | } |
| 43 | } |
| 44 | |
| 45 | /** |
| 46 | * Opens the dialog for view's selection and applies what comes back. |
| 47 | * An address outside the allowlist is treated as no address: the |
| 48 | * dialog closes and the text is left as it was (setLink would refuse |
| 49 | * it anyway; checking here keeps the caret where it was too). |
| 50 | */ |
| 51 | export async function linkPrompt (view, dialog) { |
| 52 | if (!dialog) return |
| 53 | const { action, href } = await dialog.open(linkAt(view.state)) |
| 54 | if (view.isDestroyed) return |
| 55 | if (action === 'insert' && safeHref(href)) setLink(href)(view.state, view.dispatch) |
| 56 | else if (action === 'remove') removeLink(view.state, view.dispatch) |
| 57 | view.focus() |
| 58 | } |
| 59 | |