Defer active-element direct morph updates until blur
Queue direct morphs for the focused element and flush them when focus moves away, while preserving user-typed input value by updating only default value/attribute during the deferred pass.
···59596060- **`preserveChanges`**: When `true`, preserves modified form inputs during morphing. This prevents user-entered data from being overwritten. Default: `false`
61616262-- **`preserveActiveElement`**: When `true`, preserves the current `document.activeElement` during morphing. This prevents the focused element itself from being replaced or updated. Default: `false`
6262+- **`preserveActiveElement`**: When `true`, preserves the current `document.activeElement` during morphing. Direct updates and replacement of the focused element are deferred until it blurs. Default: `false`
63636464- **`beforeNodeVisited`**: Called before a node is visited during morphing. Return `false` to skip morphing this node.
6565
+51-3
src/morphlex.ts
···2929type IdSetMap = WeakMap<Node, Set<string>>
3030type IdArrayMap = WeakMap<Node, Array<string>>
31313232+const queuedActiveElementTargets: WeakMap<Element, ChildNode> = new WeakMap()
3333+const queuedActiveElementOptions: WeakMap<Element, Options> = new WeakMap()
3434+const queuedActiveElementListeners: WeakSet<Element> = new WeakSet()
3535+3236/**
3337 * Configuration options for morphing operations.
3438 */
···42464347 /**
4448 * When `true`, preserves the active element during morphing.
4545- * This prevents the focused element itself from being replaced or updated.
4949+ * This defers direct updates/replacement of the focused element until blur.
4650 * @default false
4751 */
4852 preserveActiveElement?: boolean
···253257 readonly #idSetMap: IdSetMap = new WeakMap()
254258 readonly #activeElement: Element | null
255259 readonly #options: Options
260260+ readonly #skipValuePropertyUpdateFor: Element | null
256261257257- constructor(options: Options = {}, activeElement: Element | null = null) {
262262+ constructor(options: Options = {}, activeElement: Element | null = null, skipValuePropertyUpdateFor: Element | null = null) {
258263 this.#options = options
259264 this.#activeElement = activeElement
265265+ this.#skipValuePropertyUpdateFor = skipValuePropertyUpdateFor
260266 }
261267262268 #isPinnedActiveElement(node: Node): boolean {
···305311 if (from === to) return
306312 if (from.isEqualNode(to)) return
307313314314+ if (this.#isPinnedActiveElement(from)) {
315315+ queueActiveElementMorph(from as Element, to, this.#options)
316316+ return
317317+ }
318318+308319 if (from.nodeType === ELEMENT_NODE_TYPE && to.nodeType === ELEMENT_NODE_TYPE) {
309320 if ((from as Element).localName === (to as Element).localName) {
310321 this.#morphMatchingElements(from as Element, to as Element)
···319330 #morphMatchingElements(from: Element, to: Element): void {
320331 if (!(this.#options.beforeNodeVisited?.(from, to) ?? true)) return
321332 if (this.#isPinnedActiveElement(from)) {
333333+ queueActiveElementMorph(from, to, this.#options)
322334 this.#options.afterNodeVisited?.(from, to)
323335 return
324336 }
···365377 for (const { name, value } of to.attributes) {
366378 if (name === "value") {
367379 if (isInputElement(from) && from.value !== value) {
368368- if (!this.#options.preserveChanges || from.value === from.defaultValue) {
380380+ if (from !== this.#skipValuePropertyUpdateFor && (!this.#options.preserveChanges || from.value === from.defaultValue)) {
369381 from.value = value
370382 }
371383 }
···835847 }
836848 }
837849 }
850850+}
851851+852852+function queueActiveElementMorph(from: Element, to: ChildNode, options: Options): void {
853853+ queuedActiveElementTargets.set(from, to.cloneNode(true) as ChildNode)
854854+ queuedActiveElementOptions.set(from, options)
855855+856856+ if (queuedActiveElementListeners.has(from)) return
857857+ queuedActiveElementListeners.add(from)
858858+859859+ from.addEventListener(
860860+ "blur",
861861+ () => {
862862+ queuedActiveElementListeners.delete(from)
863863+ flushQueuedActiveElementMorph(from)
864864+ },
865865+ { once: true },
866866+ )
867867+}
868868+869869+function flushQueuedActiveElementMorph(from: Element): void {
870870+ const to = queuedActiveElementTargets.get(from)
871871+ if (!to) return
872872+873873+ const options = queuedActiveElementOptions.get(from) ?? {}
874874+875875+ queuedActiveElementTargets.delete(from)
876876+ queuedActiveElementOptions.delete(from)
877877+878878+ if (!from.isConnected) return
879879+880880+ const flushOptions: Options = {
881881+ ...options,
882882+ preserveActiveElement: false,
883883+ }
884884+885885+ new Morph(flushOptions, null, from).morph(from as ChildNode, to)
838886}
839887840888function nodeListToArray(nodeList: NodeListOf<ChildNode>): Array<ChildNode>