| 1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
|
| 6 |
|
| 7 |
|
| 8 |
|
| 9 |
|
| 10 |
|
| 11 |
|
| 12 |
|
| 13 |
|
| 14 |
|
| 15 |
|
| 16 |
|
| 17 |
|
| 18 |
|
| 19 |
|
| 20 |
(() => { |
| 21 |
"use strict"; |
| 22 |
|
| 23 |
|
| 24 |
const FIELD = "data-fills"; |
| 25 |
|
| 26 |
const VALUE = "data-fill"; |
| 27 |
|
| 28 |
|
| 29 |
* The box on this document under that name. |
| 30 |
* |
| 31 |
* By `name` rather than by `id`, because `name` is what the description |
| 32 |
* carries: it is what a submit sends the value under and what `Field::writes` |
| 33 |
* names. An id is scoped per form instance by the emitter and is therefore |
| 34 |
* not the string the description wrote. |
| 35 |
* |
| 36 |
* Searched from the pressed control outward: the nearest enclosing form |
| 37 |
* first, then the document. A screen showing the same form twice -- an |
| 38 |
* edit modal over a list -- otherwise gets the first copy in the document |
| 39 |
* whichever one the reader is in. |
| 40 |
|
| 41 |
const box = (control, name) => { |
| 42 |
const selector = `[name="${CSS.escape(name)}"]`; |
| 43 |
return control.closest("form")?.querySelector(selector) |
| 44 |
?? document.querySelector(selector); |
| 45 |
}; |
| 46 |
|
| 47 |
|
| 48 |
* Put `text` where the caret is, and leave the caret after it. |
| 49 |
* |
| 50 |
* `selectionStart` is null on an input whose type has no text selection -- |
| 51 |
* a colour or a date -- and on anything that is not a text control at all. |
| 52 |
* Appending is the honest fallback there: the value still arrives, which is |
| 53 |
* what the description asked for, and the position was never described. |
| 54 |
|
| 55 |
const insert = (target, text) => { |
| 56 |
const value = target.value ?? ""; |
| 57 |
const at = typeof target.selectionStart === "number" ? target.selectionStart : value.length; |
| 58 |
const stop = typeof target.selectionEnd === "number" ? target.selectionEnd : at; |
| 59 |
|
| 60 |
target.value = value.slice(0, at) + text + value.slice(stop); |
| 61 |
const after = at + text.length; |
| 62 |
try { |
| 63 |
target.setSelectionRange(after, after); |
| 64 |
} catch { |
| 65 |
|
| 66 |
|
| 67 |
} |
| 68 |
|
| 69 |
|
| 70 |
|
| 71 |
|
| 72 |
target.dispatchEvent(new Event("input", { bubbles: true })); |
| 73 |
}; |
| 74 |
|
| 75 |
|
| 76 |
|
| 77 |
|
| 78 |
|
| 79 |
document.addEventListener("click", (event) => { |
| 80 |
const control = event.target?.closest?.(`[${FIELD}]`); |
| 81 |
if (!control) { |
| 82 |
return; |
| 83 |
} |
| 84 |
const target = box(control, control.getAttribute(FIELD)); |
| 85 |
if (!target) { |
| 86 |
|
| 87 |
|
| 88 |
|
| 89 |
|
| 90 |
return; |
| 91 |
} |
| 92 |
target.focus(); |
| 93 |
insert(target, control.getAttribute(VALUE) ?? ""); |
| 94 |
}); |
| 95 |
})(); |
| 96 |
|