···727727 },
728728 },
729729 {
730730+ name: "Large List - Partial Reorder",
731731+ description: "Reordering some items in a list of 100 items while keeping many in place",
732732+ setup: () => {
733733+ const from = document.createElement("ul")
734734+ for (let i = 1; i <= 100; i++) {
735735+ const li = document.createElement("li")
736736+ li.id = `item-${i}`
737737+ li.textContent = `Item ${i}`
738738+ from.appendChild(li)
739739+ }
740740+ const to = document.createElement("ul")
741741+ // Partial reorder: move every 5th item to a different position
742742+ // [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,...]
743743+ // This keeps most items in order (good for LIS) while shuffling some
744744+ const items = []
745745+ for (let i = 1; i <= 100; i++) {
746746+ items.push(i)
747747+ }
748748+ const reordered = []
749749+ for (let i = 0; i < items.length; i += 5) {
750750+ if (i + 4 < items.length) {
751751+ // Move 5th item to front of group
752752+ reordered.push(items[i + 4])
753753+ reordered.push(items[i])
754754+ reordered.push(items[i + 1])
755755+ reordered.push(items[i + 2])
756756+ reordered.push(items[i + 3])
757757+ } else {
758758+ // Handle remaining items
759759+ for (let j = i; j < items.length; j++) {
760760+ reordered.push(items[j])
761761+ }
762762+ }
763763+ }
764764+ for (const num of reordered) {
765765+ const li = document.createElement("li")
766766+ li.id = `item-${num}`
767767+ li.textContent = `Item ${num}`
768768+ to.appendChild(li)
769769+ }
770770+ return { from, to }
771771+ },
772772+ },
773773+ {
730774 name: "Deep Nesting",
731775 description: "Morphing deeply nested structures",
732776 setup: () => {
+84-2
src/morphlex.ts
···108108 this.options = options
109109 }
110110111111+ // Find longest increasing subsequence to minimize moves during reordering
112112+ // Returns the indices in the sequence that form the LIS
113113+ private longestIncreasingSubsequence(sequence: Array<number>): Array<number> {
114114+ const n = sequence.length
115115+ if (n === 0) return []
116116+117117+ // smallestEnding[i] = smallest ending value of any increasing subsequence of length i+1
118118+ const smallestEnding: Array<number> = []
119119+ // indices[i] = index in sequence where smallestEnding[i] occurs
120120+ const indices: Array<number> = []
121121+ // prev[i] = previous index in the LIS ending at sequence[i]
122122+ const prev: Array<number> = Array.from({ length: n }, () => -1)
123123+124124+ // Build the LIS by processing each value
125125+ for (let i = 0; i < n; i++) {
126126+ const val = sequence[i]!
127127+ if (val === -1) continue // Skip new nodes (not in original sequence)
128128+129129+ // Binary search: find where this value fits in smallestEnding
130130+ let left = 0
131131+ let right = smallestEnding.length
132132+133133+ while (left < right) {
134134+ const mid = Math.floor((left + right) / 2)
135135+ if (smallestEnding[mid]! < val) left = mid + 1
136136+ else right = mid
137137+ }
138138+139139+ // Link this element to the previous one in the subsequence
140140+ if (left > 0) prev[i] = indices[left - 1]!
141141+142142+ // Either extend the sequence or update an existing position
143143+ if (left === smallestEnding.length) {
144144+ // Extend: this value is larger than all previous endings
145145+ smallestEnding.push(val)
146146+ indices.push(i)
147147+ } else {
148148+ // Update: found a better (smaller) ending for this length
149149+ smallestEnding[left] = val
150150+ indices[left] = i
151151+ }
152152+ }
153153+154154+ // Reconstruct the actual indices that form the LIS
155155+ const result: Array<number> = []
156156+ if (indices.length === 0) return result
157157+158158+ // Walk backwards through prev links to build the LIS
159159+ let curr: number | undefined = indices[indices.length - 1]
160160+ while (curr !== undefined && curr !== -1) {
161161+ result.unshift(curr)
162162+ curr = prev[curr]
163163+ }
164164+165165+ return result
166166+ }
167167+111168 morph(from: ChildNode, to: ChildNode | NodeListOf<ChildNode>): void {
112169 if (isParentNode(from)) {
113170 this.mapIdSets(from)
···286343 const candidateNodes: Set<ChildNode> = new Set()
287344 const candidateElements: Set<Element> = new Set()
288345289289- const matches: Array<ChildNode | null> = new Array(toChildNodes.length).fill(null)
346346+ const matches: Array<ChildNode | null> = Array.from({ length: toChildNodes.length }, () => null)
290347291348 for (const candidate of fromChildNodes) {
292349 if (isElement(candidate)) candidateElements.add(candidate)
···428485 }
429486 }
430487488488+ // Build sequence of current indices for LIS calculation
489489+ const fromIndex = new Map<ChildNode, number>()
490490+ Array.from(fromChildNodes).forEach((node, i) => fromIndex.set(node, i))
491491+492492+ const sequence: Array<number> = []
493493+ for (let i = 0; i < matches.length; i++) {
494494+ const match = matches[i]
495495+ if (match && fromIndex.has(match)) {
496496+ sequence.push(fromIndex.get(match)!)
497497+ } else {
498498+ sequence.push(-1) // New node, not in sequence
499499+ }
500500+ }
501501+502502+ // Find LIS - these nodes don't need to move
503503+ const lisIndices = this.longestIncreasingSubsequence(sequence)
504504+ const shouldNotMove = new Set<number>()
505505+ for (const idx of lisIndices) {
506506+ shouldNotMove.add(sequence[idx]!)
507507+ }
508508+431509 // Process nodes in forward order to maintain proper positioning
432510 let insertionPoint: ChildNode | null = parent.firstChild
433511 for (let i = 0; i < toChildNodes.length; i++) {
434512 const node = toChildNodes[i]!
435513 const match = matches[i]
436514 if (match) {
437437- moveBefore(parent, match, insertionPoint)
515515+ const matchIndex = fromIndex.get(match)!
516516+ // Only move if not in LIS
517517+ if (!shouldNotMove.has(matchIndex)) {
518518+ moveBefore(parent, match, insertionPoint)
519519+ }
438520 this.morphOneToOne(match, node)
439521 insertionPoint = match.nextSibling
440522 // Skip over any nodes that will be removed to avoid unnecessary moves