| 1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
|
| 6 |
|
| 7 |
|
| 8 |
|
| 9 |
|
| 10 |
|
| 11 |
|
| 12 |
|
| 13 |
|
| 14 |
|
| 15 |
|
| 16 |
|
| 17 |
|
| 18 |
|
| 19 |
|
| 20 |
|
| 21 |
|
| 22 |
|
| 23 |
(() => { |
| 24 |
"use strict"; |
| 25 |
|
| 26 |
|
| 27 |
|
| 28 |
|
| 29 |
let held = null; |
| 30 |
|
| 31 |
|
| 32 |
* How to find this element again in markup that has replaced it. |
| 33 |
* |
| 34 |
* An `id` is the description's own address for the thing and survives a |
| 35 |
* re-render, so it is preferred. A `name` is the next best: a form control |
| 36 |
* that carries one is being identified by the description too, since that |
| 37 |
* is the key the route reads it under. Anything with neither is not |
| 38 |
* addressable and is left alone -- guessing at a neighbour by position is |
| 39 |
* how focus lands on the wrong control, which is worse than losing it. |
| 40 |
|
| 41 |
const address = (element) => { |
| 42 |
if (element.id) return `#${CSS.escape(element.id)}`; |
| 43 |
const name = element.getAttribute?.("name"); |
| 44 |
if (name) { |
| 45 |
const tag = element.tagName.toLowerCase(); |
| 46 |
return `${tag}[name="${CSS.escape(name)}"]`; |
| 47 |
} |
| 48 |
return null; |
| 49 |
}; |
| 50 |
|
| 51 |
|
| 52 |
* The caret, for a control that has one. |
| 53 |
* |
| 54 |
* `selectionStart` throws on input types that have no text selection |
| 55 |
* (`email`, `number`, and others depending on the browser), which is a |
| 56 |
* `DOMException` rather than a `null`, so this is guarded rather than |
| 57 |
* checked: the list of which types support it differs between engines and |
| 58 |
* hard-coding it here would be a second thing to keep in step. |
| 59 |
|
| 60 |
const caret = (element) => { |
| 61 |
try { |
| 62 |
const { selectionStart, selectionEnd, selectionDirection } = element; |
| 63 |
if (selectionStart === null || selectionStart === undefined) return null; |
| 64 |
return { selectionStart, selectionEnd, selectionDirection }; |
| 65 |
} catch { |
| 66 |
return null; |
| 67 |
} |
| 68 |
}; |
| 69 |
|
| 70 |
|
| 71 |
const restoreCaret = (element, at) => { |
| 72 |
if (!at) return; |
| 73 |
try { |
| 74 |
element.setSelectionRange( |
| 75 |
at.selectionStart, |
| 76 |
at.selectionEnd, |
| 77 |
at.selectionDirection ?? "none", |
| 78 |
); |
| 79 |
} catch { |
| 80 |
|
| 81 |
|
| 82 |
} |
| 83 |
}; |
| 84 |
|
| 85 |
|
| 86 |
|
| 87 |
|
| 88 |
|
| 89 |
|
| 90 |
|
| 91 |
|
| 92 |
const remember = () => { |
| 93 |
const active = document.activeElement; |
| 94 |
if (!active || active === document.body) { |
| 95 |
held = null; |
| 96 |
return; |
| 97 |
} |
| 98 |
const selector = address(active); |
| 99 |
held = selector ? { selector, at: caret(active) } : null; |
| 100 |
}; |
| 101 |
|
| 102 |
|
| 103 |
|
| 104 |
|
| 105 |
const restore = () => { |
| 106 |
const record = held; |
| 107 |
held = null; |
| 108 |
if (!record) return; |
| 109 |
|
| 110 |
|
| 111 |
|
| 112 |
const active = document.activeElement; |
| 113 |
if (active && active !== document.body) return; |
| 114 |
|
| 115 |
const found = document.querySelector(record.selector); |
| 116 |
|
| 117 |
|
| 118 |
|
| 119 |
if (!found || typeof found.focus !== "function") return; |
| 120 |
|
| 121 |
|
| 122 |
|
| 123 |
|
| 124 |
|
| 125 |
found.focus({ preventScroll: true }); |
| 126 |
restoreCaret(found, record.at); |
| 127 |
}; |
| 128 |
|
| 129 |
document.addEventListener("htmx:before:request", remember); |
| 130 |
document.addEventListener("htmx:after:settle", restore); |
| 131 |
})(); |
| 132 |
|