Skip to content

Commit 6b52d60

Browse files
committed
fix: refresh data source makes images reload and flash
1 parent 6a67f42 commit 6b52d60

File tree

1 file changed

+171
-31
lines changed

1 file changed

+171
-31
lines changed

src/view/canvas.ts

Lines changed: 171 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -57,21 +57,122 @@ function updateNodeContent(oldNode: Node, newNode: Node): void {
5757
const oldChildren = Array.from(oldEl.childNodes)
5858
const newChildren = Array.from(newEl.childNodes)
5959

60-
// Update existing children
61-
const minLength = Math.min(oldChildren.length, newChildren.length)
62-
for (let i = 0; i < minLength; i++) {
63-
updateNodeContent(oldChildren[i], newChildren[i])
64-
}
60+
// Build a map of old children by ID for efficient lookup
61+
// Note: Multiple children can have the same ID (loop clones), so store arrays
62+
const oldChildrenById = new Map<string, Element[]>()
63+
const oldChildrenWithoutId: Node[] = []
64+
65+
oldChildren.forEach(child => {
66+
if (child.nodeType === Node.ELEMENT_NODE) {
67+
const id = (child as Element).id
68+
if (id) {
69+
if (!oldChildrenById.has(id)) {
70+
oldChildrenById.set(id, [])
71+
}
72+
oldChildrenById.get(id)!.push(child as Element)
73+
} else {
74+
oldChildrenWithoutId.push(child)
75+
}
76+
} else {
77+
oldChildrenWithoutId.push(child)
78+
}
79+
})
6580

66-
// Remove extra old children
67-
for (let i = oldChildren.length - 1; i >= minLength; i--) {
68-
oldEl.removeChild(oldChildren[i])
69-
}
7081

71-
// Add new children
72-
for (let i = minLength; i < newChildren.length; i++) {
73-
oldEl.appendChild(newChildren[i].cloneNode(true))
74-
}
82+
// Track which old children have been matched
83+
const matchedOldChildren = new Set<Node>()
84+
85+
// Process new children in order
86+
let lastInsertedNode: Node | null = null
87+
88+
newChildren.forEach(newChild => {
89+
if (newChild.nodeType === Node.ELEMENT_NODE) {
90+
const newId = (newChild as Element).id
91+
92+
// Try to find matching old child by ID
93+
if (newId && oldChildrenById.has(newId)) {
94+
const candidates = oldChildrenById.get(newId)!
95+
const oldChild = candidates.shift() // Take first unmatched element
96+
97+
if (!oldChild) {
98+
// All elements with this ID already matched, treat as no match
99+
const clonedNode = newChild.cloneNode(true)
100+
if (lastInsertedNode) {
101+
oldEl.insertBefore(clonedNode, lastInsertedNode.nextSibling)
102+
} else {
103+
oldEl.insertBefore(clonedNode, oldEl.firstChild)
104+
}
105+
lastInsertedNode = clonedNode
106+
return
107+
}
108+
109+
matchedOldChildren.add(oldChild)
110+
111+
// Update the existing element in place
112+
updateNodeContent(oldChild, newChild)
113+
114+
// Ensure it's in the correct position
115+
if (lastInsertedNode) {
116+
if (lastInsertedNode.nextSibling !== oldChild) {
117+
oldEl.insertBefore(oldChild, lastInsertedNode.nextSibling)
118+
}
119+
} else {
120+
if (oldEl.firstChild !== oldChild) {
121+
oldEl.insertBefore(oldChild, oldEl.firstChild)
122+
}
123+
}
124+
125+
lastInsertedNode = oldChild
126+
} else {
127+
// No matching old child found, insert new element
128+
const clonedNode = newChild.cloneNode(true)
129+
if (lastInsertedNode) {
130+
oldEl.insertBefore(clonedNode, lastInsertedNode.nextSibling)
131+
} else {
132+
oldEl.insertBefore(clonedNode, oldEl.firstChild)
133+
}
134+
lastInsertedNode = clonedNode
135+
}
136+
} else {
137+
// Text node or other - try to match with old children without IDs
138+
const matchIndex = oldChildrenWithoutId.findIndex(oldChild =>
139+
!matchedOldChildren.has(oldChild) && oldChild.nodeType === newChild.nodeType
140+
)
141+
142+
if (matchIndex >= 0) {
143+
const oldChild = oldChildrenWithoutId[matchIndex]
144+
matchedOldChildren.add(oldChild)
145+
updateNodeContent(oldChild, newChild)
146+
147+
if (lastInsertedNode) {
148+
if (lastInsertedNode.nextSibling !== oldChild) {
149+
oldEl.insertBefore(oldChild, lastInsertedNode.nextSibling)
150+
}
151+
} else {
152+
if (oldEl.firstChild !== oldChild) {
153+
oldEl.insertBefore(oldChild, oldEl.firstChild)
154+
}
155+
}
156+
157+
lastInsertedNode = oldChild
158+
} else {
159+
const clonedNode = newChild.cloneNode(true)
160+
if (lastInsertedNode) {
161+
oldEl.insertBefore(clonedNode, lastInsertedNode.nextSibling)
162+
} else {
163+
oldEl.insertBefore(clonedNode, oldEl.firstChild)
164+
}
165+
lastInsertedNode = clonedNode
166+
}
167+
}
168+
})
169+
170+
// Remove old children that weren't matched
171+
oldChildren.forEach(oldChild => {
172+
if (!matchedOldChildren.has(oldChild)) {
173+
oldEl.removeChild(oldChild)
174+
}
175+
})
75176
}
76177
}
77178

@@ -308,7 +409,6 @@ function renderContent(comp: Component, deep: number) {
308409
const innerHtml = renderInnerHTML(comp)
309410

310411
if (innerHtml === null) {
311-
comp.view!.render()
312412
comp.components()
313413
.forEach(c => renderPreview(c, deep+1))
314414
} else {
@@ -367,6 +467,15 @@ export function renderPreview(comp: Component, deep = 0) {
367467
if (__data.length === 0) {
368468
el.remove()
369469
} else {
470+
// Remove all existing loop clones (siblings with same ID from previous render)
471+
const componentId = el.id
472+
let nextSibling = el.nextElementSibling as HTMLElement | null
473+
while (nextSibling && nextSibling.id === componentId) {
474+
const toRemove = nextSibling
475+
nextSibling = nextSibling.nextElementSibling as HTMLElement | null
476+
toRemove.remove()
477+
}
478+
370479
const initialPreviewIndex = getPreviewIndex(comp) || 0
371480

372481
// Render each loop iteration
@@ -387,11 +496,37 @@ export function renderPreview(comp: Component, deep = 0) {
387496

388497
// For subsequent iterations: clone first, then render into original (without diffing), then repeat
389498
for (let idx = fromIdx - 1; idx >= toIdx; idx--) {
499+
// Check if this iteration should be visible
500+
setPreviewIndexToLoopData(comp, idx)
501+
const isVisibleAtIdx = isComponentVisible(comp)
502+
503+
// Skip invisible iterations - don't create a clone
504+
if (!isVisibleAtIdx) {
505+
continue
506+
}
507+
390508
// Clone the current state (with previous iteration's content)
391509
const clone = el.cloneNode(true) as HTMLElement
392510

393511
clone.classList.remove('gjs-selected')
394512

513+
// Remove hidden elements from the clone to prevent them from appearing in the output
514+
const hiddenInClone = clone.querySelectorAll('[style*="display: none"]')
515+
hiddenInClone.forEach(hiddenEl => hiddenEl.remove())
516+
517+
// Reset visibility on the ORIGINAL element's children before rendering next iteration
518+
// This ensures the next iteration starts with all elements visible
519+
const hiddenInOriginal = el.querySelectorAll('[style*="display: none"]')
520+
hiddenInOriginal.forEach(hiddenEl => {
521+
if (hiddenEl instanceof HTMLElement && hiddenEl.style) {
522+
hiddenEl.style.removeProperty('display')
523+
// Remove empty style attribute
524+
if (hiddenEl.style.length === 0) {
525+
hiddenEl.removeAttribute('style')
526+
}
527+
}
528+
})
529+
395530
// Keep the selection mechanism - use GrapesJS component API
396531
clone.addEventListener('click', (event) => {
397532
const clickedElement = event.target as HTMLElement
@@ -469,37 +604,42 @@ export function renderPreview(comp: Component, deep = 0) {
469604
// Add the clone to the canvas
470605
el.insertAdjacentElement('afterend', clone)
471606

472-
// Set preview index for the next iteration and render into original element
607+
// Render the current iteration into the original element
473608
// NOTE: We skip the diffing here because we're in a loop creating initial elements
474609
// Diffing only helps on re-renders, not initial renders
475-
setPreviewIndexToLoopData(comp, idx)
476-
const isVisibleAtIdx = isComponentVisible(comp)
477-
478-
if (isVisibleAtIdx) {
479-
const innerHtml = renderInnerHTML(comp)
480-
if (innerHtml === null) {
481-
comp.view!.render()
482-
comp.components()
483-
.forEach(c => renderPreview(c, deep+1))
484-
} else {
485-
// Just set innerHTML directly without diffing in the loop
486-
el.innerHTML = innerHtml
487-
}
488-
renderAttributes(comp)
610+
const innerHtml = renderInnerHTML(comp)
611+
if (innerHtml === null) {
612+
comp.components()
613+
.forEach(c => renderPreview(c, deep+1))
489614
} else {
490-
el.remove()
615+
// Just set innerHTML directly without diffing in the loop
616+
el.innerHTML = innerHtml
491617
}
618+
renderAttributes(comp)
492619
}
493620
setPreviewIndexToLoopData(comp, initialPreviewIndex)
494621
}
495622
} else {
496623
const isVisible = isComponentVisible(comp)
497624

498625
if(isVisible) {
626+
// Make sure the element is visible (in case it was hidden before)
627+
if (el.style && el.style.display === 'none') {
628+
el.style.removeProperty('display')
629+
// Remove empty style attribute
630+
if (el.style.length === 0) {
631+
el.removeAttribute('style')
632+
}
633+
}
499634
renderContent(comp, deep)
500635
renderAttributes(comp)
501636
} else {
502-
el.remove()
637+
// Don't remove the element, just hide it
638+
// This prevents breaking loop rendering where the same element
639+
// is referenced across multiple iterations
640+
if (el.parentElement && el.style) {
641+
el.style.display = 'none'
642+
}
503643
}
504644
}
505645
}

0 commit comments

Comments
 (0)