Add optional active-element preservation during morphing
Introduce preserveActiveElement to protect the focused element from moves, replacement, and removal so typing state can stay stable when requested without changing default behavior.
···46464747```javascript
4848morph(currentNode, newNode, {
4949+ preserveActiveElement: false,
4950 preserveChanges: true,
5051 beforeNodeAdded: (parent, node, insertionPoint) => {
5152 console.log("Adding node:", node)
···5758### Available Options
58595960- **`preserveChanges`**: When `true`, preserves modified form inputs during morphing. This prevents user-entered data from being overwritten. Default: `false`
6161+6262+- **`preserveActiveElement`**: When `true`, preserves the current `document.activeElement` during morphing. This prevents focused elements from being moved, replaced or updated. Default: `false`
60636164- **`beforeNodeVisited`**: Called before a node is visited during morphing. Return `false` to skip morphing this node.
6265
+36-5
src/morphlex.ts
···4848 preserveChanges?: boolean
49495050 /**
5151+ * When `true`, preserves the active element during morphing.
5252+ * This prevents focused elements from being moved, replaced or updated.
5353+ * @default false
5454+ */
5555+ preserveActiveElement?: boolean
5656+5757+ /**
5158 * Called before a node is visited during morphing.
5259 * @param fromNode The existing node in the DOM
5360 * @param toNode The new node to morph to
···155162 if (typeof to === "string") to = parseFragment(to).childNodes
156163157164 if (!options.preserveChanges && isParentNode(from)) flagDirtyInputs(from)
158158-159159- new Morph(options).morph(from, to)
165165+ new Morph(options, getActiveElement(from, options)).morph(from, to)
160166}
161167162168/**
···187193 (from as Element).localName === (to as Element).localName
188194 ) {
189195 if (isParentNode(from)) flagDirtyInputs(from)
190190- new Morph(options).visitChildNodes(from as Element, to as Element)
196196+ new Morph(options, getActiveElement(from, options)).visitChildNodes(from as Element, to as Element)
191197 } else {
192198 throw new Error("[Morphlex] You can only do an inner morph with matching elements.")
193199 }
194200}
195201202202+function getActiveElement(from: ChildNode, options: Options): Element | null {
203203+ if (!options.preserveActiveElement) return null
204204+205205+ const activeElement = document.activeElement
206206+ if (!(activeElement instanceof Element)) return null
207207+ if (from === activeElement) return activeElement
208208+209209+ if (from.nodeType !== ELEMENT_NODE_TYPE) return null
210210+ return (from as Element).contains(activeElement) ? activeElement : null
211211+}
212212+196213function flagDirtyInputs(node: ParentNode): void {
197214 for (const input of node.querySelectorAll("input")) {
198215 if ((input.name && input.value !== input.defaultValue) || input.checked !== input.defaultChecked) {
···241258class Morph {
242259 readonly #idArrayMap: IdArrayMap = new WeakMap()
243260 readonly #idSetMap: IdSetMap = new WeakMap()
261261+ readonly #activeElement: Element | null
244262 readonly #options: Options
245263246246- constructor(options: Options = {}) {
264264+ constructor(options: Options = {}, activeElement: Element | null = null) {
247265 this.#options = options
266266+ this.#activeElement = activeElement
267267+ }
268268+269269+ #isPinnedActiveElement(node: Node): boolean {
270270+ return !!this.#options.preserveActiveElement && node === this.#activeElement
248271 }
249272250273 morph(from: ChildNode, to: ChildNode | NodeListOf<ChildNode>): void {
···302325303326 #morphMatchingElements(from: Element, to: Element): void {
304327 if (!(this.#options.beforeNodeVisited?.(from, to) ?? true)) return
328328+ if (this.#isPinnedActiveElement(from)) {
329329+ this.#options.afterNodeVisited?.(from, to)
330330+ return
331331+ }
305332306333 if (from.hasAttributes() || to.hasAttributes()) {
307334 this.#visitAttributes(from, to)
···650677 const match = fromChildNodes[matchInd]!
651678 const operation = op[i]!
652679653653- if (!shouldNotMove[matchInd]) {
680680+ if (!shouldNotMove[matchInd] && !this.#isPinnedActiveElement(match)) {
654681 moveBefore(parent, match, insertionPoint)
655682 }
656683···676703 }
677704678705 #replaceNode(node: ChildNode, newNode: ChildNode): void {
706706+ if (this.#isPinnedActiveElement(node)) return
707707+679708 const parent = node.parentNode || document
680709 const insertionPoint = node
681710 // Check if both removal and addition are allowed before starting the replacement
···691720 }
692721693722 #removeNode(node: ChildNode): void {
723723+ if (this.#isPinnedActiveElement(node)) return
724724+694725 if (this.#options.beforeNodeRemoved?.(node) ?? true) {
695726 node.remove()
696727 this.#options.afterNodeRemoved?.(node)