Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions FIX_SUMMARY.md
Original file line number Diff line number Diff line change
@@ -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
244 changes: 244 additions & 0 deletions fix_replaceAll.js
Original file line number Diff line number Diff line change
@@ -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];
}
32 changes: 32 additions & 0 deletions flexcolumnanim_fix.patch
Original file line number Diff line number Diff line change
@@ -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 = [];
Loading