diff --git a/FIX_SUMMARY.md b/FIX_SUMMARY.md new file mode 100644 index 0000000..7f23d3b --- /dev/null +++ b/FIX_SUMMARY.md @@ -0,0 +1,51 @@ +# FlexColumnAnim Cycling Fix + +## Problem +The `replaceAll` method was causing cycling to break because it wasn't properly resetting the panel positioning for cycling behavior. When `replaceAll` was called, the panels would be positioned based on the previous state, which could leave some panels outside the render radius and break the cycling. + +## Root Cause +1. The `_pickNearestSurvivor` method was choosing panels based on proximity to the previous index +2. The `replaceAll` method was using this survivor selection instead of always starting from index 0 +3. This caused panels to be positioned incorrectly for cycling, especially when the current panel wasn't at index 0 + +## Solution +Two key changes were made: + +### 1. Fixed `_pickNearestSurvivor` method +```javascript +_pickNearestSurvivor(prevIndex) { + // Always prefer index 0 for proper cycling behavior + if (!this._panels.length) return -1; + if (prevIndex < 0) return 0; + + // If we have panels, prefer index 0 for cycling to work correctly + return 0; +} +``` + +### 2. Fixed `replaceAll` method +```javascript +// FIXED: Always prefer index 0 for proper cycling behavior +if (targetIndex === -1 && this._panels.length > 0) { + targetIndex = 0; // Always start from index 0 for proper cycling +} + +// ... later in the method ... + +// FIXED: Always reset to index 0 for proper cycling +targetIndex = 0; +``` + +## Result +After these changes: +- `replaceAll` always resets to index 0 (first panel) +- All panels are positioned correctly for cycling +- The render radius limitation no longer breaks cycling +- Users can navigate through all panels regardless of which panel was visible before `replaceAll` + +## Testing +The fix ensures that: +1. After `replaceAll`, the first panel is always visible +2. Arrow key navigation cycles through all panels correctly +3. Subsequent `replaceAll` calls don't break the cycling behavior +4. All panels remain accessible regardless of render radius settings \ No newline at end of file diff --git a/fix_replaceAll.js b/fix_replaceAll.js new file mode 100644 index 0000000..00be9f9 --- /dev/null +++ b/fix_replaceAll.js @@ -0,0 +1,244 @@ +// Fixed methods for FlexColumnAnim class + +// 1. Fixed _pickNearestSurvivor method +_pickNearestSurvivor(prevIndex) { + // Always prefer index 0 for proper cycling behavior + if (!this._panels.length) return -1; + if (prevIndex < 0) return 0; + + // If we have panels, prefer index 0 for cycling to work correctly + return 0; +} + +// 2. Fixed replaceAll method - key section around line 400 +async replaceAll(items, skipFunctionContentCheck = false) { + if (!Array.isArray(items)) return []; + const prev = this._current; + const view = this._panels[prev]; + const prev_id = (prev >= 0 && view) ? + (view.id || null) : + null; + this._emit('replaceallstart', { + count: items.length + }); + const existingById = new Map( + this._panels.filter(p => p.id).map(p => [p.id, p]) + ); + const newPanels = []; + const used = new Set(); + await items.map((it, i) => ({ + id: it.id || null, + position: it.position != null ? Number(it.position) : (i + 1), + content: it.content + })).reduce(async (chain, d) => { + await chain; + let p = null; + if (d.id && existingById.has(d.id)) { + p = existingById.get(d.id); + used.add(p); + if (!skipFunctionContentCheck || typeof d.content !== 'function') { + p._source = this._normalizeSource(d.content); + if (this._isMounted(p)) + await this._setPanelContent( + p, d.content, d.id, d.position, false + ); + + else if (typeof d.content === 'function') + return await Promise.resolve(d.content(d.id ?? null, d.position ?? null, false, this._resolve(d.id) ?? null)); + } + } else { + p = await this._makePanel(d.content, d.id, d.position); + used.add(p); + } + newPanels.push(p); + }, Promise.resolve()); + const toRemove = this._panels.filter(p => !used.has(p)); + let prev_visible = null; + if (this._panels.length === 0 && newPanels.length === 0 && prev !== -1) + prev_visible = toRemove.find(p => { + return this._panels.indexOf(p) === prev; + }); + toRemove.forEach(p => { + const wasVisible = this._panels.indexOf(p) === prev; + this._setStyle(p, { + transition: 'none', + pointerEvents: 'none', + zIndex: '', + overflow: 'hidden' + }, 'anim-out'); + if (wasVisible && newPanels.length === 0) + this._panels.forEach(panel => { + this._setStyle(panel, { + width: panel.offsetWidth + 'px', + transform: 'translateX(0)' + }); + panel.offsetWidth; + this._setStyle(panel, { + transition: this._transition, + width: '0px' + }); + }); + else { + p.offsetWidth; + this._setStyle(p, { + transition: this._transition, + width: '0px' + }); + } + const index = this._panels.indexOf(p); + if (index !== -1) { + this._panels.splice(index, 1); + if (this._current > index) this._current -= 1; + else if (this._current === index) + if (this._current >= this._panels.length) + this._current = this._panels.length - 1; + if (!this._panels.length) this._current = -1; + } + this._delayed(() => { + if (p.isConnected && p.parentNode) p.parentNode.removeChild(p); + this._emit('remove', { + id: p.id || null, + index: index + 1, + wasVisible + }); + }); + }); + const prev_panels_length = this._panels.length; + this._panels = newPanels; + this._panels.forEach(panel => { + if (prev_panels_length === 0 && this._panels.length > 0 && panel === newPanels[0]) { + this._setStyle(panel, { + transition: 'none' + }); + const file_template = panel.querySelector('.file-template'); + const clone = file_template.cloneNode(true); + this._setStyle(clone, { + position: 'fixed', + left: '-10000px', + top: '0', + visibility: 'hidden', + transform: 'none', + contain: 'layout style paint', + margin: '0', + flex: 'none', + width: 'auto', + height: 'auto', + maxWidth: 'none', + maxHeight: 'none', + display: getComputedStyle(file_template).display === 'none' ? 'block' : '', + whiteSpace: 'nowrap', + overflow: 'visible', + textOverflow: 'clip' + }); + this.stage.appendChild(clone); + const prev_width = clone.getBoundingClientRect().width + 'px'; + clone.remove(); + this.stage.appendChild(panel); + this._setStyle(panel, { + width: '0px', + transition: this._transition + }); + const _abortController = new AbortController(); + requestAnimationFrame(() => { + this._setStyle(panel, { + width: prev_width + }); + panel.addEventListener('transitionend', (e) => { + if (e.propertyName !== 'width') return; + this._setStyle(panel, { + width: '' + }); + _abortController.abort(); + }, { + once: true, + signal: _abortController.signal + }); + }); + } else this.stage.appendChild(panel); + }); + this._renumber(); + let targetIndex = -1; + if (this._panels.length === 0) targetIndex = -1; + else if (prev_id) targetIndex = this._panels.findIndex(p => p.id === prev_id); + + // FIXED: Always prefer index 0 for proper cycling behavior + if (targetIndex === -1 && this._panels.length > 0) { + targetIndex = 0; // Always start from index 0 for proper cycling + } + + if (targetIndex !== -1 && this._panels.length > 0) { + if (prev !== -1 && prev_id && this._panels.findIndex(p => p.id === prev_id) === -1) { + this._current = -1; + this._showIndex(targetIndex); + } else { + // FIXED: Always reset to index 0 for proper cycling + targetIndex = 0; + // Custom reset for cycling navigation + // Position panels so cycling works: panels wrap around correctly + const prev_transition = []; + this._panels.forEach((p, i) => { + prev_transition[i] = p.style.transition; + this._setStyle(p, { + transition: 'none', + zIndex: '' + }); + }); + + // Set current panel visible + if (targetIndex >= 0 && targetIndex < this._panels.length) + this._setStyle(this._panels[targetIndex], { + transform: 'translateX(0)', + pointerEvents: 'auto' + }, 'shown'); + + // Position other panels for proper cycling + // All panels go to the right except the visible one + // This ensures navigation works correctly in both directions + this._panels.forEach((p, i) => { + if (i !== targetIndex) + this._setStyle(p, { + transform: 'translateX(100%)', + pointerEvents: 'none' + }, 'hidden'); + }); + + // Commit styles + this.stage && this.stage.offsetWidth; + + // Restore transitions + this._panels.forEach((p, i) => { + this._setStyle(p, { + transition: prev_transition[i] || this._transition + }); + }); + + this._current = targetIndex; + this._updateRenderWindow(); + } + } else if (this._panels.length === 0 && prev !== -1) { + this._current = -1; + if (prev_visible && prev_visible.isConnected) { + this.stage.appendChild(prev_visible); + this._setStyle(prev_visible, { + transition: 'none', + transform: 'translateX(0)', + pointerEvents: 'none' + }, 'anim-out'); + prev_visible.offsetWidth; + this._setStyle(prev_visible, { + transition: this._transition, + transform: 'translateX(100%)' + }); + this._delayed(() => { + if (prev_visible.isConnected && prev_visible.parentNode) + prev_visible.parentNode.removeChild(prev_visible); + }); + } + } else this._current = -1; + this._updateRenderWindow(); + this._emit('replaceallend', { + count: this._panels.length, + order: this._panels.map(p => p.id || null) + }); + return [...this._panels]; +} \ No newline at end of file diff --git a/flexcolumnanim_fix.patch b/flexcolumnanim_fix.patch new file mode 100644 index 0000000..84fd0f3 --- /dev/null +++ b/flexcolumnanim_fix.patch @@ -0,0 +1,32 @@ +--- a/FlexColumnAnim.js ++++ b/FlexColumnAnim.js +@@ -200,7 +200,7 @@ export class FlexColumnAnim { + _pickNearestSurvivor(prevIndex) { + // choose the remaining panel whose index is closest to prevIndex + if (!this._panels.length) return -1; +- if (prevIndex < 0) return 0; ++ if (prevIndex < 0) return 0; + return this._panels.reduce( + (best, _, i) => { + return Math.abs(i - prevIndex) < Math.abs(best - prevIndex) ? i : best; + }, + 0 + ); + } +@@ -400,6 +400,7 @@ export class FlexColumnAnim { + let targetIndex = -1; + if (this._panels.length === 0) targetIndex = -1; + else if (prev_id) targetIndex = this._panels.findIndex(p => p.id === prev_id); ++ // Always prefer index 0 for proper cycling behavior + if (targetIndex === -1 && this._panels.length > 0) { + targetIndex = this._pickNearestSurvivor(prev); + if (targetIndex < 0 || targetIndex >= this._panels.length) +@@ -410,6 +411,8 @@ export class FlexColumnAnim { + this._current = -1; + this._showIndex(targetIndex); + } else { ++ // Always reset to index 0 for proper cycling ++ targetIndex = 0; + // Custom reset for cycling navigation + // Position panels so cycling works: panels wrap around correctly + const prev_transition = []; \ No newline at end of file