···727 },
728 },
729 {
730+ name: "Large List - Partial Reorder",
731+ description: "Reordering some items in a list of 100 items while keeping many in place",
732+ setup: () => {
733+ const from = document.createElement("ul")
734+ for (let i = 1; i <= 100; i++) {
735+ const li = document.createElement("li")
736+ li.id = `item-${i}`
737+ li.textContent = `Item ${i}`
738+ from.appendChild(li)
739+ }
740+ const to = document.createElement("ul")
741+ // Partial reorder: move every 5th item to a different position
742+ // [1,2,3,4,5,6,7,8,9,10,...] → [5,1,2,3,4,10,6,7,8,9,15,11,12,13,14,20,...]
743+ // This keeps most items in order (good for LIS) while shuffling some
744+ const items = []
745+ for (let i = 1; i <= 100; i++) {
746+ items.push(i)
747+ }
748+ const reordered = []
749+ for (let i = 0; i < items.length; i += 5) {
750+ if (i + 4 < items.length) {
751+ // Move 5th item to front of group
752+ reordered.push(items[i + 4])
753+ reordered.push(items[i])
754+ reordered.push(items[i + 1])
755+ reordered.push(items[i + 2])
756+ reordered.push(items[i + 3])
757+ } else {
758+ // Handle remaining items
759+ for (let j = i; j < items.length; j++) {
760+ reordered.push(items[j])
761+ }
762+ }
763+ }
764+ for (const num of reordered) {
765+ const li = document.createElement("li")
766+ li.id = `item-${num}`
767+ li.textContent = `Item ${num}`
768+ to.appendChild(li)
769+ }
770+ return { from, to }
771+ },
772+ },
773+ {
774 name: "Deep Nesting",
775 description: "Morphing deeply nested structures",
776 setup: () => {
+84-2
src/morphlex.ts
···108 this.options = options
109 }
110000000000000000000000000000000000000000000000000000000000111 morph(from: ChildNode, to: ChildNode | NodeListOf<ChildNode>): void {
112 if (isParentNode(from)) {
113 this.mapIdSets(from)
···286 const candidateNodes: Set<ChildNode> = new Set()
287 const candidateElements: Set<Element> = new Set()
288289- const matches: Array<ChildNode | null> = new Array(toChildNodes.length).fill(null)
290291 for (const candidate of fromChildNodes) {
292 if (isElement(candidate)) candidateElements.add(candidate)
···428 }
429 }
430000000000000000000000431 // Process nodes in forward order to maintain proper positioning
432 let insertionPoint: ChildNode | null = parent.firstChild
433 for (let i = 0; i < toChildNodes.length; i++) {
434 const node = toChildNodes[i]!
435 const match = matches[i]
436 if (match) {
437- moveBefore(parent, match, insertionPoint)
0000438 this.morphOneToOne(match, node)
439 insertionPoint = match.nextSibling
440 // Skip over any nodes that will be removed to avoid unnecessary moves
···108 this.options = options
109 }
110111+ // Find longest increasing subsequence to minimize moves during reordering
112+ // Returns the indices in the sequence that form the LIS
113+ private longestIncreasingSubsequence(sequence: Array<number>): Array<number> {
114+ const n = sequence.length
115+ if (n === 0) return []
116+117+ // smallestEnding[i] = smallest ending value of any increasing subsequence of length i+1
118+ const smallestEnding: Array<number> = []
119+ // indices[i] = index in sequence where smallestEnding[i] occurs
120+ const indices: Array<number> = []
121+ // prev[i] = previous index in the LIS ending at sequence[i]
122+ const prev: Array<number> = Array.from({ length: n }, () => -1)
123+124+ // Build the LIS by processing each value
125+ for (let i = 0; i < n; i++) {
126+ const val = sequence[i]!
127+ if (val === -1) continue // Skip new nodes (not in original sequence)
128+129+ // Binary search: find where this value fits in smallestEnding
130+ let left = 0
131+ let right = smallestEnding.length
132+133+ while (left < right) {
134+ const mid = Math.floor((left + right) / 2)
135+ if (smallestEnding[mid]! < val) left = mid + 1
136+ else right = mid
137+ }
138+139+ // Link this element to the previous one in the subsequence
140+ if (left > 0) prev[i] = indices[left - 1]!
141+142+ // Either extend the sequence or update an existing position
143+ if (left === smallestEnding.length) {
144+ // Extend: this value is larger than all previous endings
145+ smallestEnding.push(val)
146+ indices.push(i)
147+ } else {
148+ // Update: found a better (smaller) ending for this length
149+ smallestEnding[left] = val
150+ indices[left] = i
151+ }
152+ }
153+154+ // Reconstruct the actual indices that form the LIS
155+ const result: Array<number> = []
156+ if (indices.length === 0) return result
157+158+ // Walk backwards through prev links to build the LIS
159+ let curr: number | undefined = indices[indices.length - 1]
160+ while (curr !== undefined && curr !== -1) {
161+ result.unshift(curr)
162+ curr = prev[curr]
163+ }
164+165+ return result
166+ }
167+168 morph(from: ChildNode, to: ChildNode | NodeListOf<ChildNode>): void {
169 if (isParentNode(from)) {
170 this.mapIdSets(from)
···343 const candidateNodes: Set<ChildNode> = new Set()
344 const candidateElements: Set<Element> = new Set()
345346+ const matches: Array<ChildNode | null> = Array.from({ length: toChildNodes.length }, () => null)
347348 for (const candidate of fromChildNodes) {
349 if (isElement(candidate)) candidateElements.add(candidate)
···485 }
486 }
487488+ // Build sequence of current indices for LIS calculation
489+ const fromIndex = new Map<ChildNode, number>()
490+ Array.from(fromChildNodes).forEach((node, i) => fromIndex.set(node, i))
491+492+ const sequence: Array<number> = []
493+ for (let i = 0; i < matches.length; i++) {
494+ const match = matches[i]
495+ if (match && fromIndex.has(match)) {
496+ sequence.push(fromIndex.get(match)!)
497+ } else {
498+ sequence.push(-1) // New node, not in sequence
499+ }
500+ }
501+502+ // Find LIS - these nodes don't need to move
503+ const lisIndices = this.longestIncreasingSubsequence(sequence)
504+ const shouldNotMove = new Set<number>()
505+ for (const idx of lisIndices) {
506+ shouldNotMove.add(sequence[idx]!)
507+ }
508+509 // Process nodes in forward order to maintain proper positioning
510 let insertionPoint: ChildNode | null = parent.firstChild
511 for (let i = 0; i < toChildNodes.length; i++) {
512 const node = toChildNodes[i]!
513 const match = matches[i]
514 if (match) {
515+ const matchIndex = fromIndex.get(match)!
516+ // Only move if not in LIS
517+ if (!shouldNotMove.has(matchIndex)) {
518+ moveBefore(parent, match, insertionPoint)
519+ }
520 this.morphOneToOne(match, node)
521 insertionPoint = match.nextSibling
522 // Skip over any nodes that will be removed to avoid unnecessary moves