fix(library): x-html au lieu de x-for — contourne bug Alpine 3.14.9
Le x-for d'Alpine 3.14.9 ne détecte pas les changements d'array. Remplacement complet par x-html avec génération HTML manuelle. Ajout de window._libData pour les onclick handlers globaux. Fonctions _byId pour toggle/expand/rename/open depuis le HTML.
This commit is contained in:
+111
-58
@@ -220,61 +220,7 @@
|
||||
<div class="th lib-col-edited sortable" @click="sortBy='edited';doSort()">Last edited</div>
|
||||
<div class="th lib-col-visited sortable" @click="sortBy='visited';doSort()">Last visited</div>
|
||||
</div>
|
||||
<div class="lib-table-body" id="lib-table-body">
|
||||
<template x-for="(item, idx) in flatItems" :key="item.id">
|
||||
<div class="lib-row" :class="{ selected: !!selected[item.id] }" :data-lib-id="item.id">
|
||||
<div class="row-cell lib-col-name" :class="'indent-' + (item.depth || 0)">
|
||||
<div class="row-name">
|
||||
<!-- Drag handle (hover-only) -->
|
||||
<div class="drag-handle hover-only" title="Drag to move">
|
||||
<span class="dot"></span><span class="dot"></span>
|
||||
<span class="dot"></span><span class="dot"></span>
|
||||
<span class="dot"></span><span class="dot"></span>
|
||||
</div>
|
||||
<!-- Checkbox (hover-only, always visible if checked) -->
|
||||
<div class="lib-checkbox hover-only" :class="{ checked: selected[item.id] }"
|
||||
@click.stop="toggleItem(item)"></div>
|
||||
<!-- Expand chevron -->
|
||||
<button x-show="item.has_children" class="tree-chevron"
|
||||
:class="{ open: expanded[item.id] }"
|
||||
@click.stop="toggleExpand(item)">▶</button>
|
||||
<span x-show="!item.has_children" style="width:18px;flex-shrink:0;"></span>
|
||||
<!-- Icon -->
|
||||
<span class="row-icon" x-text="item.icon || '📄'"></span>
|
||||
<!-- Title or inline rename -->
|
||||
<span class="row-title" x-show="renamingId !== item.id"
|
||||
@click.stop="startRename(item)"
|
||||
@dblclick.stop="openItem(item)"
|
||||
x-text="item.title"
|
||||
title="Click to rename, double-click to open"></span>
|
||||
<input x-show="renamingId === item.id"
|
||||
x-model="renameValue"
|
||||
@keydown.enter="commitRename(item)"
|
||||
@keydown.escape="renamingId=null"
|
||||
@blur="commitRename(item)"
|
||||
@click.stop
|
||||
style="flex:1;min-width:80px;">
|
||||
<!-- OPEN button (hover-only, right-aligned) -->
|
||||
<button class="btn-open hover-only" @click.stop="openPeek(item)" title="Open preview">⤢ Open</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row-cell lib-col-author">
|
||||
<div class="row-author">
|
||||
<div class="author-avatar" x-text="item.author_initial || '?'"></div>
|
||||
<span x-text="item.author || '—'"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row-cell lib-col-source">
|
||||
<span class="row-source" x-text="item.source_label || 'Private'"></span>
|
||||
</div>
|
||||
<div class="row-cell lib-col-edited">
|
||||
<span class="row-time" x-text="formatTime(item.updated_at)"></span>
|
||||
</div>
|
||||
<div class="row-cell lib-col-visited">
|
||||
<span class="row-time" x-text="formatTime(item.visited_at)"></span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="lib-table-body" id="lib-table-body" x-html="tableHtml">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -369,6 +315,61 @@ function libraryPage() {
|
||||
|
||||
// ── Computed flat list (NOT a getter — Alpine 3.14.9 bug) ──
|
||||
flatItems: [],
|
||||
tableHtml: '',
|
||||
|
||||
_buildTableHtml() {
|
||||
var self = this;
|
||||
var html = '';
|
||||
for (var i = 0; i < this.flatItems.length; i++) {
|
||||
var item = this.flatItems[i];
|
||||
var selClass = this.selected[item.id] ? ' selected' : '';
|
||||
var indent = 'indent-' + (item.depth || 0);
|
||||
var checkClass = this.selected[item.id] ? ' checked' : '';
|
||||
var chevOpen = this.expanded[item.id] ? ' open' : '';
|
||||
var isRenaming = this.renamingId === item.id;
|
||||
html += '<div class="lib-row' + selClass + '" data-lib-id="' + item.id + '">';
|
||||
html += '<div class="row-cell lib-col-name ' + indent + '"><div class="row-name">';
|
||||
// Drag handle
|
||||
html += '<div class="drag-handle hover-only" title="Drag to move">';
|
||||
html += '<span class="dot"></span><span class="dot"></span>';
|
||||
html += '<span class="dot"></span><span class="dot"></span>';
|
||||
html += '<span class="dot"></span><span class="dot"></span></div>';
|
||||
// Checkbox
|
||||
html += '<div class="lib-checkbox hover-only' + checkClass + '" onclick="window._libData.toggleItemById(' + item.id + ')"></div>';
|
||||
// Chevron or spacer
|
||||
if (item.has_children) {
|
||||
html += '<button class="tree-chevron' + chevOpen + '" onclick="window._libData.toggleExpandById(' + item.id + ')">\u25b6</button>';
|
||||
} else {
|
||||
html += '<span style="width:18px;flex-shrink:0;"></span>';
|
||||
}
|
||||
// Icon
|
||||
html += '<span class="row-icon">' + (item.icon || '\ud83d\udcc4') + '</span>';
|
||||
// Title or rename input
|
||||
if (isRenaming) {
|
||||
html += '<input value="' + self._escAttr(self.renameValue) + '" onkeydown="if(event.key===\'Enter\')window._libData.commitRenameById(' + item.id + ')" onblur="window._libData.commitRenameById(' + item.id + ')" style="flex:1;min-width:80px;font-size:13px;padding:2px 4px;background:var(--bg-primary);border:1px solid var(--accent);border-radius:4px;color:var(--text-primary);outline:none;" onclick="event.stopPropagation()">';
|
||||
} else {
|
||||
html += '<span class="row-title" onclick="window._libData.startRenameById(' + item.id + ')" ondblclick="window._libData.openItemById(' + item.id + ')" title="Click to rename, double-click to open">' + self._escHtml(item.title) + '</span>';
|
||||
}
|
||||
// OPEN button
|
||||
html += '<button class="btn-open hover-only" onclick="window._libData.openPeekById(' + item.id + ')" title="Open preview">\u2931 Open</button>';
|
||||
html += '</div></div>';
|
||||
// Author
|
||||
html += '<div class="row-cell lib-col-author"><div class="row-author">';
|
||||
html += '<div class="author-avatar">' + (item.author_initial || '?') + '</div>';
|
||||
html += '<span>' + (item.author || '\u2014') + '</span></div></div>';
|
||||
// Source
|
||||
html += '<div class="row-cell lib-col-source"><span class="row-source">' + (item.source_label || 'Private') + '</span></div>';
|
||||
// Edited
|
||||
html += '<div class="row-cell lib-col-edited"><span class="row-time">' + self.formatTime(item.updated_at) + '</span></div>';
|
||||
// Visited
|
||||
html += '<div class="row-cell lib-col-visited"><span class="row-time">' + self.formatTime(item.visited_at) + '</span></div>';
|
||||
html += '</div>';
|
||||
}
|
||||
this.tableHtml = html;
|
||||
},
|
||||
|
||||
_escHtml(s) { var d=document.createElement('div'); d.textContent = s||''; return d.innerHTML; },
|
||||
_escAttr(s) { return String(s||'').replace(/&/g,'&').replace(/"/g,'"').replace(/</g,'<').replace(/>/g,'>'); },
|
||||
|
||||
_recomputeFlatItems() {
|
||||
var result = [];
|
||||
@@ -383,6 +384,45 @@ function libraryPage() {
|
||||
this.flatItems.splice(0, this.flatItems.length, ...result);
|
||||
this.selectedCount = Object.keys(this.selected).length;
|
||||
this.allSelected = this.flatItems.length > 0 && this.selectedCount === this.flatItems.length;
|
||||
this._buildTableHtml();
|
||||
},
|
||||
|
||||
// ── Global dispatch (bypasses Alpine x-for reactivity issues) ──
|
||||
toggleItemById(id) {
|
||||
var item = this._findItem(id);
|
||||
if (!item) return;
|
||||
this.toggleItem(item);
|
||||
},
|
||||
toggleExpandById(id) {
|
||||
var item = this._findItem(id);
|
||||
if (!item) return;
|
||||
this.toggleExpand(item);
|
||||
},
|
||||
startRenameById(id) {
|
||||
var item = this._findItem(id);
|
||||
if (!item) return;
|
||||
this.startRename(item);
|
||||
},
|
||||
commitRenameById(id) {
|
||||
var item = this._findItem(id);
|
||||
if (!item) { this.renamingId = null; this._buildTableHtml(); return; }
|
||||
this.commitRename(item);
|
||||
},
|
||||
openItemById(id) {
|
||||
var item = this._findItem(id);
|
||||
if (!item) return;
|
||||
this.openItem(item);
|
||||
},
|
||||
openPeekById(id) {
|
||||
var item = this._findItem(id);
|
||||
if (!item) return;
|
||||
this.openPeek(item);
|
||||
},
|
||||
_findItem(id) {
|
||||
for (var i = 0; i < this.flatItems.length; i++) {
|
||||
if (this.flatItems[i].id === id) return this.flatItems[i];
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
_flatten(nodes, out, depth) {
|
||||
@@ -421,6 +461,7 @@ function libraryPage() {
|
||||
|
||||
// ── Init ──
|
||||
init() {
|
||||
window._libData = this; // expose for onclick handlers
|
||||
console.log('[Library] init start');
|
||||
var wsEl = document.getElementById('lib-workspace-id');
|
||||
this.workspaceId = parseInt(wsEl ? wsEl.textContent : '0') || 0;
|
||||
@@ -483,6 +524,7 @@ function libraryPage() {
|
||||
console.log('[Library] loaded ' + this.items.length + ' items');
|
||||
this._markTree(this.items, 0);
|
||||
this._recomputeFlatItems();
|
||||
this._buildTableHtml();
|
||||
console.log('[Library] flatItems after recompute: ' + this.flatItems.length);
|
||||
// Force Alpine to re-render (known issue with x-for in 3.14.9)
|
||||
var self = this;
|
||||
@@ -526,6 +568,7 @@ function libraryPage() {
|
||||
delete this.expanded[item.id];
|
||||
this.expanded = Object.assign({}, this.expanded);
|
||||
this._recomputeFlatItems();
|
||||
this._buildTableHtml();
|
||||
return;
|
||||
}
|
||||
// Expand — load children if not loaded yet
|
||||
@@ -543,6 +586,7 @@ function libraryPage() {
|
||||
}
|
||||
}
|
||||
this._recomputeFlatItems();
|
||||
this._buildTableHtml();
|
||||
},
|
||||
|
||||
_markChildren(nodes, depth) {
|
||||
@@ -564,6 +608,7 @@ function libraryPage() {
|
||||
}
|
||||
this.selected = Object.assign({}, this.selected);
|
||||
this._recomputeFlatItems();
|
||||
this._buildTableHtml();
|
||||
},
|
||||
|
||||
toggleSelectAll() {
|
||||
@@ -577,12 +622,14 @@ function libraryPage() {
|
||||
this.selected = s;
|
||||
}
|
||||
this._recomputeFlatItems();
|
||||
this._buildTableHtml();
|
||||
},
|
||||
|
||||
clearSelection() {
|
||||
this.selected = {};
|
||||
this.moreOpen = false;
|
||||
this._recomputeFlatItems();
|
||||
this._buildTableHtml();
|
||||
},
|
||||
|
||||
// ── Multi-select actions ──
|
||||
@@ -631,17 +678,22 @@ function libraryPage() {
|
||||
startRename(item) {
|
||||
this.renamingId = item.id;
|
||||
this.renameValue = item.title;
|
||||
this._buildTableHtml();
|
||||
var self = this;
|
||||
this.$nextTick(function() {
|
||||
var inp = document.querySelector('.row-title input');
|
||||
if (inp) { inp.focus(); inp.select(); }
|
||||
var inp = document.querySelector('.row-title input, [onkeydown*=\"commitRenameById\"]');
|
||||
// Try to find the rename input that was just created
|
||||
var inputs = document.querySelectorAll('#lib-table-body input');
|
||||
for (var i = 0; i < inputs.length; i++) {
|
||||
if (inputs[i].value === item.title) { inputs[i].focus(); inputs[i].select(); break; }
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
async commitRename(item) {
|
||||
var newName = (this.renameValue || '').trim();
|
||||
this.renamingId = null;
|
||||
if (!newName || newName === item.title) return;
|
||||
if (!newName || newName === item.title) { this._buildTableHtml(); return; }
|
||||
try {
|
||||
var r = await fetch('/api/pages/' + item.id + '/rename', {
|
||||
method:'PUT', headers:{'Content-Type':'application/json'},
|
||||
@@ -651,6 +703,7 @@ function libraryPage() {
|
||||
item.title = newName;
|
||||
}
|
||||
} catch(e) {}
|
||||
this._buildTableHtml();
|
||||
},
|
||||
|
||||
// ── Add new ──
|
||||
|
||||
Reference in New Issue
Block a user