Added — Settings → Automations, pipeline visuel (API steps v7.0) :
- Cartes ordinees (trigger/condition/delay/action) : resume + edition
TYPÉE par kind/type (datalist evenements, 7 ops, 8 types d'action avec
leurs champs reels), ajout/edition/suppression/haut-bas via
POST/PUT/DELETE /workspace/automations[/steps]/...
- ✨ Convertir le JSON en pipeline (legacy → steps ordonnes) ; textareas
JSON masques des qu'un step existe.
- Gate E2E « editeur visuel de steps » : creation → edition → ajout →
carte « Action · webhook » sous CSP reel.
Fixed (trouve par le gate) — 51 expressions Alpine MULTI-INSTRUCTIONS
(`a=1; b()`) = interdites par le parseur CSP (une seule expression par
directive ; ';' = token inattendu) — INVISIBLE pour le scan par tokens :
- Conversion en methodes dans 11 fichiers : nav settings x8 (navTo),
menu section base x7 (closeAndSetCount/Move/...), parts+editeur x13
(setSharePerm, more*, markAndSave...), breadcrumb x5 (hover*/goClose),
library/local x6 (menus popup), board x3 (pickStatus/...), ctx-menu x2
(addTagAndClear), agent/card/gitea/workspaces x6.
- Scanner dedie scan_semi (inventaire ';' hors chaines) ajoute au lot.
Verifs : 39 templates Jinja parse OK · scan expressions = 0 incompatible
(4 faux positifs en chaines) · **E2E 8/8** · suite **1094/1094** ·
ruff OK · docs a jour
2093 lines
82 KiB
JavaScript
2093 lines
82 KiB
JavaScript
const LW=(()=>{try{const el=document.getElementById('lw-config');return el?JSON.parse(el.textContent):{}}catch{return {}}})();
|
|
|
|
console.log('FLOWDECK v2.4.6 — IIFE running, about to set _wsInitData');
|
|
const _wsInitData = (function() { // lexical : NON propriété globalThis →
|
|
// invisible au snapshot ji du build CSP (valeur bannie sinon)
|
|
return {
|
|
tree:[],
|
|
displayTree:[],
|
|
breadcrumb: [],
|
|
loaded:false,
|
|
showCreate:false,
|
|
showRename:false,
|
|
showDelete:false,
|
|
showAll:false,
|
|
createType:'file',
|
|
newName:'',
|
|
parentFolder:null,
|
|
target:null,
|
|
expanded: (function() {
|
|
try { return JSON.parse(localStorage.getItem('fd_ws_expanded') || '{}'); }
|
|
catch { return {}; }
|
|
})(),
|
|
currentFolder: LW.current_folder_id,
|
|
searchQuery: '',
|
|
filterType: '',
|
|
filterOpen: false,
|
|
editingPath: false,
|
|
folderStack: [],
|
|
|
|
// Inline rename
|
|
renamingId: null,
|
|
renamingName: '',
|
|
renameInputRef: null,
|
|
|
|
// Preview cache
|
|
previewCache: {},
|
|
|
|
// Clipboard for copy/paste
|
|
clipboard: [],
|
|
|
|
// Sort
|
|
sortBy: '',
|
|
viewMode: localStorage.getItem('fd_default_view') || 'tree',
|
|
viewMenuOpen: false,
|
|
sortOpen: false,
|
|
searchOpen: false,
|
|
viewIcons: {tree:'',list:'',details:'',title:'',content:''},
|
|
viewLabels: {tree:'Tree',list:'List',details:'Details',title:'Cards',content:'Content'},
|
|
|
|
// Context menu — state lives in the shared fdCtx Alpine store (_ctx_menu.html)
|
|
|
|
// Tags
|
|
workspaceTags: [],
|
|
tagFilter: '',
|
|
tagInputId: null,
|
|
tagsVersion: 0,
|
|
|
|
// Preview panel
|
|
previewItem: null,
|
|
previewPanelContent: '',
|
|
|
|
// Multi-select
|
|
selectedIds: {},
|
|
selectedIdsCount: 0,
|
|
lastSelectedId: null,
|
|
allSelected: false,
|
|
moreOpen: false,
|
|
|
|
// Move-to modal
|
|
moveModalOpen: false,
|
|
moveIds: [],
|
|
moveSearchQuery: '',
|
|
moveCandidates: [],
|
|
_moveAllCandidates: [],
|
|
|
|
// Undo
|
|
undoVisible: false,
|
|
undoMessage: '',
|
|
undoItemId: null,
|
|
undoTimeout: null,
|
|
|
|
// Upload tracking
|
|
uploadSpeed: '',
|
|
uploadStartTime: 0,
|
|
|
|
// Path bar
|
|
pathValue: '/workspace/',
|
|
|
|
async navigateToRoot() {
|
|
this.folderStack = [];
|
|
this.navIndex = 0;
|
|
this._saveExpandedState();
|
|
await this._loadFolder(0);
|
|
},
|
|
|
|
async navigateToBreadcrumb(idx) {
|
|
this.folderStack = this.folderStack.slice(0, idx + 1);
|
|
this.navIndex = idx;
|
|
this._saveExpandedState();
|
|
var entry = this.folderStack[idx];
|
|
await this._loadFolder(entry ? entry.id : 0);
|
|
},
|
|
|
|
startPathEdit() {
|
|
this.editingPath = true;
|
|
var self = this;
|
|
this.$nextTick(function() {
|
|
var inp = self.$refs.pathEdit;
|
|
if (inp) { inp.focus(); inp.select(); }
|
|
});
|
|
},
|
|
|
|
// Drag indicators
|
|
dragCount: 0,
|
|
dragOverParentId: null,
|
|
|
|
// Online/offline
|
|
online: navigator.onLine,
|
|
pendingSyncCount: 0,
|
|
pinnedIds: (function() {
|
|
try { return JSON.parse(localStorage.getItem('fd_ws_pinned') || '[]'); }
|
|
catch { return []; }
|
|
})(),
|
|
|
|
// Navigation history
|
|
navHistory: [],
|
|
navIndex: -1,
|
|
canGoBack: false,
|
|
canGoForward: false,
|
|
|
|
// Preview popup
|
|
previewVisible: false,
|
|
previewContent: '',
|
|
previewName: '',
|
|
previewX: 0,
|
|
previewY: 0,
|
|
|
|
// Drag & drop state
|
|
isDragging: false,
|
|
dropTarget: null,
|
|
dropTargetName: '',
|
|
uploading: false,
|
|
uploadStatus: '',
|
|
uploadProgress: 0,
|
|
draggedItemId: null,
|
|
|
|
async init() {
|
|
try {
|
|
// Expose globally for event handlers (BEFORE await)
|
|
window._wsData = this;
|
|
var url = '/api/local-workspace/tree';
|
|
if (this.currentFolder && this.currentFolder > 0) {
|
|
url += '?folder=' + this.currentFolder;
|
|
}
|
|
var r = await fetch(url);
|
|
var d = await r.json();
|
|
// Keep tree hierarchical, only roots in displayTree
|
|
var roots = d.tree || [];
|
|
this._markTree(roots, 0);
|
|
this.tree = roots;
|
|
this.displayTree = roots;
|
|
this.breadcrumb = d.breadcrumb || [];
|
|
this.loaded = true;
|
|
// Initialize folder stack from breadcrumb
|
|
this.folderStack = (this.breadcrumb || []).map(function(b) { return {id: b.id, name: b.name}; });
|
|
this.navIndex = this.folderStack.length > 0 ? this.folderStack.length - 1 : 0;
|
|
// Expose globally for renderChildren onclick handlers
|
|
window._wsData = this;
|
|
// Listen for online/offline
|
|
var self = this;
|
|
window.addEventListener('online', function() { self.online = true; });
|
|
window.addEventListener('offline', function() { self.online = false; });
|
|
// v6.0.0 PWA: reflect offline queue state from the client module
|
|
if (window.FlowOffline && window.FlowOffline.onChange) {
|
|
window.FlowOffline.onChange(function(s) {
|
|
self.online = s.online;
|
|
self.pendingSyncCount = s.pendingSyncCount;
|
|
});
|
|
}
|
|
// Side-peek editor posts renames here so the tree/peek title stay in sync.
|
|
window.addEventListener('message', function(ev) {
|
|
if (ev.origin !== window.location.origin) return;
|
|
var m = ev.data;
|
|
if (!m || m.type !== 'fd-page-renamed' || m.id == null) return;
|
|
var node = self._findNodeById(self.tree, m.id);
|
|
if (node) node.name = m.title || 'Untitled';
|
|
self.displayTree = self.displayTree.slice();
|
|
var t = document.getElementById('ws-preview-title');
|
|
if (t && self.previewItem && self.previewItem.id === m.id) t.textContent = m.title || 'Untitled';
|
|
var side = document.querySelector('.sidebar-item.ws-tree-item[data-id="' + m.id + '"] .page-name');
|
|
if (side) side.textContent = m.title || 'Untitled';
|
|
});
|
|
// Load workspace tags
|
|
this.loadWorkspaceTags();
|
|
} catch(e) { console.error('Tree load error:', e); this.loaded = true; }
|
|
this._recompute();
|
|
this._initResizeHandle();
|
|
},
|
|
|
|
_initResizeHandle() {
|
|
var panel = document.getElementById('ws-preview-panel');
|
|
var handle = document.getElementById('ws-preview-resize-handle');
|
|
if (!panel || !handle) return;
|
|
var startX, startWidth, dragging = false;
|
|
handle.addEventListener('pointerdown', function(e) {
|
|
e.preventDefault();
|
|
handle.setPointerCapture(e.pointerId);
|
|
startX = e.clientX;
|
|
startWidth = panel.offsetWidth;
|
|
dragging = true;
|
|
document.body.style.userSelect = 'none';
|
|
});
|
|
handle.addEventListener('pointermove', function(e) {
|
|
if (!dragging) return;
|
|
var deltaX = startX - e.clientX;
|
|
var newWidth = Math.max(300, Math.min(window.innerWidth * 0.9, startWidth + deltaX));
|
|
panel.style.width = newWidth + 'px';
|
|
});
|
|
handle.addEventListener('pointerup', function(e) {
|
|
if (!dragging) return;
|
|
document.body.style.userSelect = '';
|
|
var moved = Math.abs(e.clientX - startX);
|
|
if (moved < 3) {
|
|
window._wsData.closePreviewPanel();
|
|
}
|
|
dragging = false;
|
|
try { localStorage.setItem('fd_peek_width', panel.style.width); } catch { /* volontaire */ }
|
|
handle.releasePointerCapture(e.pointerId);
|
|
});
|
|
try {
|
|
var saved = localStorage.getItem('fd_peek_width');
|
|
if (saved) panel.style.width = saved;
|
|
} catch { /* volontaire */ }
|
|
},
|
|
|
|
_recompute() {
|
|
// Recompute all former getters as plain properties
|
|
this.canGoBack = this.navIndex > 0;
|
|
this.canGoForward = this.navIndex < this.folderStack.length - 1;
|
|
this.pathValue = '/workspace/' + this.folderStack.filter(function(b){return b.id > 0}).map(function(b){return b.name}).join('/');
|
|
// flatTree
|
|
var ft = [];
|
|
this._flattenTree(this.tree, ft);
|
|
this.flatTree = ft;
|
|
// filteredTableItems
|
|
this._computeFilteredTableItems();
|
|
this._syncSelection();
|
|
},
|
|
|
|
// ── Pin/Favorite items ──
|
|
togglePin(id) {
|
|
var idx = this.pinnedIds.indexOf(id);
|
|
if (idx === -1) this.pinnedIds.push(id);
|
|
else this.pinnedIds.splice(idx, 1);
|
|
try { localStorage.setItem('fd_ws_pinned', JSON.stringify(this.pinnedIds)); }
|
|
catch { /* volontaire */ }
|
|
this._haptic();
|
|
},
|
|
isPinned(id) {
|
|
return this.pinnedIds.indexOf(id) !== -1;
|
|
},
|
|
|
|
// ── Multi-select ──
|
|
toggleSelect(node, ev) {
|
|
var e = ev || {};
|
|
if (e.shiftKey && this.lastSelectedId) {
|
|
// Range select
|
|
var allIds = this._collectVisibleIds(this.displayTree);
|
|
var start = allIds.indexOf(this.lastSelectedId);
|
|
var end = allIds.indexOf(node.id);
|
|
if (start !== -1 && end !== -1) {
|
|
var from = Math.min(start, end), to = Math.max(start, end);
|
|
for (var i = from; i <= to; i++) this.selectedIds[allIds[i]] = true;
|
|
}
|
|
} else if (e.ctrlKey || e.metaKey) {
|
|
// Toggle single, keep others
|
|
if (!!this.selectedIds[node.id]) delete this.selectedIds[node.id];
|
|
else this.selectedIds[node.id] = true;
|
|
} else {
|
|
// Notion-style: toggle this item, keep the rest of the selection
|
|
if (!!this.selectedIds[node.id]) delete this.selectedIds[node.id];
|
|
else this.selectedIds[node.id] = true;
|
|
}
|
|
this.lastSelectedId = node.id;
|
|
// Force reactivity
|
|
this.selectedIds = Object.assign({}, this.selectedIds);
|
|
this._syncSelection();
|
|
},
|
|
_collectVisibleIds(nodes) {
|
|
var ids = [];
|
|
for (var i=0; i<nodes.length; i++) {
|
|
ids.push(nodes[i].id);
|
|
if (nodes[i].children) ids = ids.concat(this._collectVisibleIds(nodes[i].children));
|
|
}
|
|
return ids;
|
|
},
|
|
async bulkDelete() {
|
|
var ids = Object.keys(this.selectedIds);
|
|
if (ids.length === 0) return;
|
|
if (!confirm('Delete ' + ids.length + ' selected items?')) return;
|
|
var self = this;
|
|
// Soft-delete all selected items
|
|
for (var i=0; i<ids.length; i++) {
|
|
await fetch('/api/local-workspace/items/' + ids[i], {headers: {'X-CSRF-Token': getCsrf()}, method: 'DELETE' });
|
|
}
|
|
this.clearSelection();
|
|
this._reloadAfterAction();
|
|
// Show undo banner for bulk delete
|
|
this.undoItemId = ids.join(',');
|
|
this.undoMessage = 'Deleted ' + ids.length + ' item(s)';
|
|
this.undoVisible = true;
|
|
this.undoTimeout = setTimeout(function() {
|
|
self.undoVisible = false;
|
|
self.undoItemId = null;
|
|
}, 5000);
|
|
},
|
|
clearSelection() {
|
|
for (var k in this.selectedIds) delete this.selectedIds[k];
|
|
this.selectedIds = {};
|
|
this.moreOpen = false;
|
|
this._syncSelection();
|
|
},
|
|
selectAll(ev) {
|
|
// Toggle: select / unselect all currently visible items
|
|
if (this.allSelected) {
|
|
for (var k in this.selectedIds) delete this.selectedIds[k];
|
|
} else {
|
|
var all = this._collectVisibleIds(this.displayTree);
|
|
for (var i=0; i<all.length; i++) this.selectedIds[all[i]] = true;
|
|
}
|
|
this.selectedIds = Object.assign({}, this.selectedIds);
|
|
this._syncSelection();
|
|
},
|
|
|
|
// ── Table view data ──
|
|
flatTree: [],
|
|
_flattenTree(nodes, out) {
|
|
for (var i=0; i<nodes.length; i++) {
|
|
out.push(nodes[i]);
|
|
if (nodes[i].children) this._flattenTree(nodes[i].children, out);
|
|
}
|
|
},
|
|
|
|
// ── Filtered table items (for enhanced filter/search table) ──
|
|
filteredTableItems: [],
|
|
_computeFilteredTableItems() {
|
|
var self = this;
|
|
var q = (this.searchQuery || '').toLowerCase().trim();
|
|
var ft = this.filterType || '';
|
|
var tf = this.tagFilter || '';
|
|
// If no filter active, return empty (show regular views)
|
|
if (!q && !ft && !tf) { this.filteredTableItems = []; return; }
|
|
// Flatten entire tree
|
|
var all = [];
|
|
this._flattenTree(this.tree, all);
|
|
// Build path for each item
|
|
for (var i=0; i<all.length; i++) {
|
|
all[i]._path = self._getItemPath(all[i].id);
|
|
}
|
|
// Filter
|
|
this.filteredTableItems = all.filter(function(n) {
|
|
var nameMatch = !q || (n.name || '').toLowerCase().indexOf(q) !== -1;
|
|
var typeMatch = true;
|
|
if (ft) {
|
|
switch(ft) {
|
|
case 'folder': typeMatch = n.is_folder; break;
|
|
case 'pdf': typeMatch = /\.pdf$/.test((n.name||'').toLowerCase()); break;
|
|
case 'image': typeMatch = /\.(png|jpe?g|gif|webp|svg|bmp|ico)$/.test((n.name||'').toLowerCase()); break;
|
|
case 'code': typeMatch = /\.(py|js|jsx|ts|tsx|rs|go|java|rb|php|c|cpp|h|swift|kt|scala|r|sh|bash|ps1|sql)$/.test((n.name||'').toLowerCase()); break;
|
|
case 'text': typeMatch = /\.(txt|md|markdown|log)$/.test((n.name||'').toLowerCase()); break;
|
|
case 'page': typeMatch = !n.is_folder && !/\.(pdf|png|jpe?g|gif|webp|svg|bmp|ico|py|js|jsx|ts|tsx|rs|go|java|rb|php|c|cpp|h|swift|kt|scala|r|sh|bash|ps1|sql|txt|md|markdown|log)$/.test((n.name||'').toLowerCase()); break;
|
|
}
|
|
}
|
|
var tagMatch = true;
|
|
if (tf) {
|
|
tagMatch = (n.tags || []).some(function(t) { return t.name === tf; });
|
|
}
|
|
return nameMatch && typeMatch && tagMatch;
|
|
});
|
|
},
|
|
|
|
_getItemPath(id) {
|
|
// Build folder path by walking up parent chain via tree
|
|
var found = this._findInTree(this.tree, id);
|
|
if (!found) return '—';
|
|
return '/' + found.path.reverse().join('/');
|
|
},
|
|
|
|
_findInTree(nodes, targetId, currentPath) {
|
|
if (!currentPath) currentPath = [];
|
|
for (var i=0; i<nodes.length; i++) {
|
|
var n = nodes[i];
|
|
if (n.id === targetId) {
|
|
return {node: n, path: currentPath.concat([n.name])};
|
|
}
|
|
if (n.children && n.children.length > 0) {
|
|
var r = this._findInTree(n.children, targetId, currentPath.concat([n.name]));
|
|
if (r) return r;
|
|
}
|
|
}
|
|
return null;
|
|
},
|
|
|
|
// ── Preview panel (loads editor in iframe via /pages/{id}?embed=1) ──
|
|
selectForPreview(node) {
|
|
var panel = document.getElementById('ws-preview-panel');
|
|
if (!panel) return;
|
|
this.previewItem = node;
|
|
document.getElementById('ws-preview-icon').innerHTML = this._fileIcon(node.name, node.is_folder, node.content_format, node.page_icon);
|
|
document.getElementById('ws-preview-title').textContent = node.name;
|
|
var openBtn = document.getElementById('ws-preview-open-btn');
|
|
openBtn.onclick = function() { if (window.fdNavigate) window.fdNavigate('/pages/' + node.id); else window.location = '/pages/' + node.id; };
|
|
var favBtn = document.getElementById('ws-preview-fav-btn');
|
|
favBtn.onclick = function() { window.showToast('Favorite toggled', 'success'); };
|
|
var copyBtn = document.getElementById('ws-preview-copy-btn');
|
|
copyBtn.onclick = function() {
|
|
var url = window.location.origin + '/pages/' + node.id;
|
|
navigator.clipboard.writeText(url).then(function() {
|
|
window.showToast('Link copied', 'success');
|
|
}).catch(function() {
|
|
window.showToast('Failed to copy', 'error');
|
|
});
|
|
};
|
|
// Load editor in iframe
|
|
var iframe = document.getElementById('ws-preview-iframe');
|
|
if (iframe) iframe.src = '/pages/' + node.id + '?embed=1';
|
|
// Show panel
|
|
panel.style.display = 'flex';
|
|
requestAnimationFrame(function() {
|
|
requestAnimationFrame(function() {
|
|
panel.style.transform = 'translateX(0)';
|
|
});
|
|
});
|
|
},
|
|
|
|
closePreviewPanel() {
|
|
var panel = document.getElementById('ws-preview-panel');
|
|
if (!panel) return;
|
|
panel.style.transform = 'translateX(100%)';
|
|
var self = this;
|
|
setTimeout(function() {
|
|
panel.style.display = 'none';
|
|
self.previewItem = null;
|
|
}, 200);
|
|
},
|
|
|
|
async _loadPreviewContentVanilla(node) {
|
|
var body = document.getElementById('ws-preview-body');
|
|
if (!body) return;
|
|
if (node.is_folder) {
|
|
body.innerHTML = '<div style="text-align:center;padding:40px;color:var(--text-secondary)">'
|
|
+ '<span>' + getSvgIcon('folder',32) + '</span>'
|
|
+ '<div style="font-size:14px">' + (node.child_count||0) + ' item(s)</div>'
|
|
+ '<a href="/local-workspace?folder=' + node.id + '" class="btn btn-secondary" style="display:inline-block;text-decoration:none;margin-top:16px">Open folder</a>'
|
|
+ '</div>';
|
|
return;
|
|
}
|
|
try {
|
|
var name = (node.name || '').toLowerCase();
|
|
|
|
// Images: try direct file URL
|
|
if (/\.(png|jpe?g|gif|webp|svg|bmp|ico)$/.test(name)) {
|
|
body.innerHTML = '<div style="text-align:center;padding:12px;">'
|
|
+ '<img src="/api/files/1/' + encodeURIComponent(node.name) + '" style="max-width:100%;max-height:50vh;border-radius:8px;">'
|
|
+ '</div>';
|
|
return;
|
|
}
|
|
// PDF: show icon + open link
|
|
if (/\.pdf$/.test(name)) {
|
|
body.innerHTML = '<div style="text-align:center;padding:24px;color:var(--text-secondary);">'
|
|
+ '<span style="font-size:48px;display:block;margin-bottom:8px;">' + getSvgIcon('file',32) + '</span>'
|
|
+ '<div style="font-size:14px;margin-bottom:4px;">' + this._escHtml(node.name) + '</div>'
|
|
+ '<div style="font-size:12px;color:var(--text-tertiary);margin-bottom:16px;">' + (node.size_display || '') + '</div>'
|
|
+ '<a href="/pages/' + node.id + '" class="btn btn-secondary" style="display:inline-block;text-decoration:none;">Open in viewer</a>'
|
|
+ '</div>';
|
|
return;
|
|
}
|
|
|
|
// Fetch content from API
|
|
var r = await this._fetchRetry('/api/local-workspace/page-content/' + node.id, true);
|
|
var content = r.content || '';
|
|
var format = r.format || '';
|
|
var preview = '';
|
|
|
|
if (format === 'file') {
|
|
// Uploaded file — show metadata
|
|
preview = '<div style="text-align:center;padding:30px;color:var(--text-secondary)">'
|
|
+ '<div style="font-size:48px;margin-bottom:12px;">' + getSvgIcon('file',32) + '</div>'
|
|
+ '<div style="font-size:15px;font-weight:600;margin-bottom:4px;">' + this._escHtml(node.name) + '</div>'
|
|
+ '<div style="font-size:13px;color:var(--text-tertiary);margin-bottom:16px;">' + (node.size_display || '') + '</div>'
|
|
+ '<a href="/pages/' + node.id + '" class="btn btn-secondary" style="display:inline-block;text-decoration:none;">Open file</a>'
|
|
+ '</div>';
|
|
|
|
} else if (format === 'blocks') {
|
|
// FlowDeck internal page — blocks can be array or {blocks:[...]}
|
|
try {
|
|
var blocks = typeof content === 'string' ? JSON.parse(content) : content;
|
|
var blockList = Array.isArray(blocks) ? blocks : (blocks && blocks.blocks ? blocks.blocks : []);
|
|
if (blockList.length > 0) {
|
|
preview = blockList.slice(0, 50).map(function(b) {
|
|
var txt = b.content || b.text || (typeof b === 'string' ? b : '');
|
|
var tag = 'div';
|
|
var style = 'margin-bottom:6px;line-height:1.5;';
|
|
if (b.type && b.type.indexOf('heading') === 0) {
|
|
var lvl = parseInt(b.type.split('_')[1]) || 1;
|
|
tag = 'h' + Math.min(lvl + 2, 6);
|
|
style += 'font-weight:600;';
|
|
}
|
|
if (b.type === 'code' || b.type === 'code_block') {
|
|
tag = 'pre';
|
|
style += 'background:var(--bg-tertiary);padding:10px 14px;border-radius:6px;font-family:monospace;font-size:12px;white-space:pre-wrap;';
|
|
}
|
|
return '<' + tag + ' style="' + style + '">' + (txt ? this._escHtml(txt) : '<br>') + '</' + tag + '>';
|
|
}, this).join('');
|
|
} else {
|
|
preview = '<div style="text-align:center;padding:30px;color:var(--text-tertiary)">Empty document</div>';
|
|
}
|
|
} catch {
|
|
preview = '<pre style="background:var(--bg-tertiary);padding:14px;border-radius:6px;overflow:auto;font-size:12px;font-family:monospace;white-space:pre-wrap;">' + this._escHtml(String(content).substring(0, 3000)) + '</pre>';
|
|
}
|
|
|
|
} else if (format === 'markdown') {
|
|
// Markdown — render as preformatted for now
|
|
preview = '<div style="white-space:pre-wrap;line-height:1.6;font-size:13px;">' + this._escHtml(String(content).substring(0, 5000)) + '</div>';
|
|
|
|
} else {
|
|
// Unknown format — show as text
|
|
var display = String(content || '').trim();
|
|
if (display) {
|
|
preview = '<pre style="background:var(--bg-tertiary);padding:14px;border-radius:6px;overflow:auto;font-size:12px;font-family:monospace;white-space:pre-wrap;">' + this._escHtml(display.substring(0, 5000)) + '</pre>';
|
|
} else {
|
|
preview = '<div style="text-align:center;padding:30px;color:var(--text-tertiary)">No content</div>';
|
|
}
|
|
}
|
|
body.innerHTML = preview || '<div style="color:var(--text-tertiary);text-align:center;padding:20px">(empty)</div>';
|
|
} catch {
|
|
body.innerHTML = '<div style="text-align:center;padding:20px;color:var(--text-tertiary)">Preview not available</div>';
|
|
}
|
|
},
|
|
|
|
_escHtml(s) {
|
|
return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
},
|
|
|
|
// ── Content snippet for content view ──
|
|
contentSnippetMap: {},
|
|
|
|
// ── Context menu (menu unifié partagé via le store Alpine fdCtx) ──
|
|
onContextMenu(ev) {
|
|
var el = ev.target.closest('[data-ws-id]');
|
|
if (!el) return;
|
|
var id = parseInt(el.getAttribute('data-ws-id'));
|
|
if (!id) return;
|
|
var node = this._findNodeById(this.displayTree, id) || this._findNodeById(this.tree, id);
|
|
if (!node) return;
|
|
var store = window.Alpine && Alpine.store('fdCtx');
|
|
if (!store) return;
|
|
var self = this;
|
|
if (!this.workspaceTags || this.workspaceTags.length === 0) this.loadWorkspaceTags();
|
|
store.setAvail(this._ctxAvailTags(node));
|
|
var handlers = {
|
|
open: function (n) { self.openPage(n.id); },
|
|
openTab: function (n) { window.open('/pages/' + n.id, '_blank'); },
|
|
peek: function (n) { self.openSidePeek(n.id); },
|
|
folder: function (n) { if (n.is_folder) self.navigateToFolder(n.id); },
|
|
rename: function (n) { self.startRename(n); },
|
|
setIcon: function (icon) { self.setItemIcon(node, icon); },
|
|
duplicate: function (n) { self.duplicateItem(n); },
|
|
link: function (n) { self.copyItemLink(n); },
|
|
download: function (n) { self.downloadItem(n); },
|
|
copyContent: function (n) { self.copyItemContent(n); },
|
|
move: function (n) { self._ctxMove(n); },
|
|
fav: function (n) { self.toggleFavorite(n); },
|
|
delete: function (n) { self.deleteWithUndo(n); },
|
|
tagExisting: function (t) { self.ctxAddExistingTag(t); },
|
|
tagAdd: function (name, color) { self.ctxAddNewTag(name, color); },
|
|
tagRemove: function (tagId) { self.ctxRemoveTag(tagId); }
|
|
};
|
|
store.openMenu(ev, node, 'workspace', handlers);
|
|
},
|
|
|
|
downloadItem(node) {
|
|
if (!node || node.is_folder) return;
|
|
var url = '/api/pages/' + node.id + '/download';
|
|
var a = document.createElement('a');
|
|
a.href = url;
|
|
a.style.display = 'none';
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
if (window.showToast) window.showToast('Download started', 'success');
|
|
},
|
|
|
|
async copyItemContent(node) {
|
|
if (!node || node.is_folder) return;
|
|
try {
|
|
var r = await fetch('/api/pages/' + node.id + '/file-content');
|
|
var d = await r.json();
|
|
if (d.ok && d.content !== undefined) {
|
|
await navigator.clipboard.writeText(d.content);
|
|
if (window.showToast) window.showToast('Content copied to clipboard', 'success');
|
|
} else {
|
|
if (window.showToast) window.showToast('Not a text file', 'error');
|
|
}
|
|
} catch {
|
|
if (window.showToast) window.showToast('Copy failed', 'error');
|
|
}
|
|
},
|
|
|
|
_ctxAvailTags(node) {
|
|
return (this.workspaceTags || []).filter(function(t) {
|
|
return !((node && node.tags) || []).some(function(nt) { return nt.id === t.id; });
|
|
});
|
|
},
|
|
|
|
_ctxRefreshAvail() {
|
|
var store = window.Alpine && Alpine.store('fdCtx');
|
|
if (store && store.node) store.setAvail(this._ctxAvailTags(store.node));
|
|
},
|
|
|
|
copyItemLink(node) {
|
|
if (!node) return;
|
|
navigator.clipboard.writeText(window.location.origin + '/pages/' + node.id);
|
|
if (window.showToast) window.showToast('Link copied', 'success');
|
|
},
|
|
|
|
async toggleFavorite(node) {
|
|
if (!node) return;
|
|
try {
|
|
var method = node.favorited ? 'DELETE' : 'POST';
|
|
var r = await fetch('/board/api/favorites/' + node.id, { method: method });
|
|
if (r.ok) {
|
|
node.favorited = !node.favorited;
|
|
if (window.appState && window.appState.refreshFavorites) window.appState.refreshFavorites();
|
|
if (window.showToast) window.showToast(node.favorited ? 'Added to Favorites' : 'Removed from Favorites', 'success');
|
|
}
|
|
} catch { /* volontaire */ }
|
|
},
|
|
|
|
async setItemIcon(node, icon) {
|
|
if (!node || !node.id) return;
|
|
icon = (icon || '').trim();
|
|
try {
|
|
var r = await fetch('/board/api/pages/' + node.id + '/icon', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json',
|
|
'X-CSRF-Token': getCsrf()},
|
|
body: JSON.stringify({icon: icon})
|
|
});
|
|
if (!r.ok) throw new Error('icon update failed');
|
|
node.page_icon = icon;
|
|
this.displayTree = [...this.tree];
|
|
this._reloadAfterAction();
|
|
if (window.showToast) window.showToast('Icon updated', 'success');
|
|
} catch {
|
|
if (window.showToast) window.showToast('Failed to update icon', 'error');
|
|
}
|
|
},
|
|
|
|
_ctxMove(node) {
|
|
if (!node) return;
|
|
this.selectedIds = {};
|
|
this.selectedIds[node.id] = true;
|
|
this.openMovePicker();
|
|
},
|
|
|
|
// ── Long-press (mobile context menu) ──
|
|
_touchStartX: 0,
|
|
_touchStartY: 0,
|
|
_touchStartTime: 0,
|
|
_touchMoved: false,
|
|
_touchTarget: null,
|
|
onTouchStart(ev) {
|
|
if (!ev.touches || ev.touches.length !== 1) return;
|
|
this._touchStartX = ev.touches[0].clientX;
|
|
this._touchStartY = ev.touches[0].clientY;
|
|
this._touchStartTime = Date.now();
|
|
this._touchMoved = false;
|
|
this._touchTarget = ev.target;
|
|
},
|
|
onTouchMove(ev) {
|
|
if (!ev.touches || ev.touches.length !== 1) return;
|
|
var dx = Math.abs(ev.touches[0].clientX - this._touchStartX);
|
|
var dy = Math.abs(ev.touches[0].clientY - this._touchStartY);
|
|
if (dx > 10 || dy > 10) this._touchMoved = true;
|
|
},
|
|
onTouchEnd(ev) {
|
|
if (this._touchMoved) return;
|
|
var elapsed = Date.now() - this._touchStartTime;
|
|
if (elapsed < 600) return;
|
|
// Long press detected — find target element
|
|
var el = this._touchTarget;
|
|
if (!el) return;
|
|
var idEl = el.closest('[data-ws-id]');
|
|
if (!idEl) return;
|
|
// Prevent default context menu if it hasn't fired yet
|
|
ev.preventDefault();
|
|
var fakeEv = {
|
|
clientX: this._touchStartX,
|
|
clientY: this._touchStartY,
|
|
target: idEl,
|
|
preventDefault: function(){}
|
|
};
|
|
this.onContextMenu(fakeEv);
|
|
},
|
|
|
|
ctxAddExistingTag(tag) {
|
|
this.ctxAddNewTag(tag.name, tag.color);
|
|
},
|
|
|
|
ctxRemoveTag(tagId) {
|
|
var store = window.Alpine && Alpine.store('fdCtx');
|
|
var node = store && store.node;
|
|
if (!node) return;
|
|
this.removeTag(node.id, tagId);
|
|
node.tags = (node.tags || []).filter(function(t) { return t.id !== tagId; });
|
|
this._ctxRefreshAvail();
|
|
},
|
|
|
|
async ctxAddNewTag(tagName, color) {
|
|
tagName = (tagName || '').trim();
|
|
if (!tagName) return;
|
|
var store = window.Alpine && Alpine.store('fdCtx');
|
|
var node = store && store.node;
|
|
if (!node) return;
|
|
color = color || (store && store.newTagColor) || '#787774';
|
|
try {
|
|
var r = await fetch('/api/local-workspace/items/' + node.id + '/tags', {
|
|
method: 'POST', headers: {'X-CSRF-Token': getCsrf(), 'Content-Type':'application/json'},
|
|
body: JSON.stringify({name: tagName, color: color})
|
|
});
|
|
if (r.ok) {
|
|
var d = await r.json();
|
|
var cur = node.tags || [];
|
|
if (!cur.some(function(t) { return t.id === d.tag.id; })) node.tags = cur.concat([d.tag]);
|
|
this._updateNodeTags(this.tree, node.id, d.tag, 'add');
|
|
this.displayTree = [...this.tree];
|
|
this.tagsVersion++;
|
|
this._bumpTagCount(d.tag, 1);
|
|
await this.loadWorkspaceTags();
|
|
this._computeFilteredTableItems();
|
|
this._ctxRefreshAvail();
|
|
}
|
|
} catch { /* volontaire */ }
|
|
},
|
|
|
|
_markTree(nodes, depth) {
|
|
for (var i=0; i<nodes.length; i++) {
|
|
var n = nodes[i];
|
|
n.depth = depth;
|
|
n.db_id = n.id;
|
|
n._path = '/' + n.name;
|
|
if (n.children && n.children.length > 0) {
|
|
this._markTree(n.children, depth + 1);
|
|
}
|
|
}
|
|
},
|
|
|
|
renderChildren(children, depth) {
|
|
if (!children || children.length === 0) return '';
|
|
var html = '';
|
|
for (var i=0; i<children.length; i++) {
|
|
var c = children[i];
|
|
var hasKids = c.children && c.children.length > 0;
|
|
var icon = this._fileIcon(c.name, c.is_folder, c.content_format, c.page_icon);
|
|
var escName = this.esc(c.name);
|
|
var escId = c.id;
|
|
html += '<li><div class="ws-tree-item" style="padding-left:'+(depth*20)+'px" draggable="true"'
|
|
+ ' data-folder-id="'+(c.is_folder?c.id:'')+'"'
|
|
+ ' data-ws-id="'+escId+'"'
|
|
+ ' ondragstart="var d=window._wsData;if(d)d.onInternalDragStart(event,'+escId+')"'
|
|
+ ' ondragend="event.target.style.opacity=\'\'">';
|
|
// Checkbox
|
|
html += '<input type="checkbox" class="item-checkbox"'
|
|
+ ' data-ws-id="'+escId+'"'
|
|
+ ' onclick="event.stopPropagation();var d=window._wsData;if(d)d.toggleSelect({id:'+escId+',name:\''+escName+'\'},event)"'
|
|
+ ' title="Select item">';
|
|
// Drag handle
|
|
html += '<div class="drag-handle" 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>';
|
|
if (c.is_folder && hasKids) {
|
|
html += '<button class="ws-chevron ws-expand-btn" data-ws-id="'+escId+'" onclick="event.stopPropagation();var d=window._wsData;if(d)d.toggleExpand('+escId+',this)">\u25b6</button>';
|
|
} else {
|
|
html += '<span style="width:16px;flex-shrink:0;"></span>';
|
|
}
|
|
html += '<span class="icon">'+icon+'</span>';
|
|
if (c.is_folder) {
|
|
var countBadge = (c.child_count > 0) ? ' <span class="child-count">('+c.child_count+')</span>' : '';
|
|
html += '<span class="name folder" onclick="window._wsData.navigateToFolder('+escId+')">'+c.name+countBadge+'</span>';
|
|
} else {
|
|
// Click to rename, double-click to open
|
|
html += '<span class="name file" onclick="event.stopPropagation();window._wsData.startRenderChildRename('+escId+',\''+escName+'\')"'
|
|
+ ' ondblclick="window.fdNavigate?window.fdNavigate(\'/pages/'+escId+'\'):window.location=\'/pages/'+escId+'\'"'
|
|
+ ' title="Click to rename, double-click to open"'
|
|
+ ' onmouseenter="var d=window._wsData;if(d)d.showPreview(event,{id:'+escId+',name:\''+escName+'\',is_folder:false,content_format:\''+(c.content_format||'')+'\'})"'
|
|
+ ' onmouseleave="var d=window._wsData;if(d)d.hidePreview()">'
|
|
+ (c.is_shared ? '<span class="ws-shared-badge" title="Partag\u00e9" onclick="event.stopPropagation()">👥</span>' : '')
|
|
+ '<span id="rc-name-'+escId+'">'+c.name+'</span>'
|
|
+ '<input id="rc-input-'+escId+'" class="inline-rename-input" style="display:none"'
|
|
+ ' onkeydown="if(event.key===\'Enter\')window._wsData.commitRenderChildRename('+escId+')"'
|
|
+ ' onblur="window._wsData.commitRenderChildRename('+escId+')"'
|
|
+ ' onclick="event.stopPropagation()">'
|
|
+ '<a href="#" class="btn-open hover-only"'
|
|
+ ' onclick="event.stopPropagation();event.preventDefault();var d=window._wsData;if(d)d.openSidePeek('+escId+')"'
|
|
+ ' title="Open in side preview">↢ Open</a>'
|
|
+ '</span>';
|
|
}
|
|
// Open button for files — to the right of title (like Library)
|
|
html += '<div class="actions">';
|
|
if (c.is_folder) {
|
|
html += '<span style="display:flex;gap:2px">';
|
|
html += '<button class="ws-act" data-ws-action="new-file" data-ws-id="'+escId+'" data-ws-name="'+escName+'" title="New file">'+getSvgIcon('file',14)+'</button>';
|
|
html += '<button class="ws-act" data-ws-action="new-folder" data-ws-id="'+escId+'" data-ws-name="'+escName+'" title="New folder">'+getSvgIcon('folder',14)+'</button>';
|
|
html += '<a class="ws-act" href="/local-workspace" title="Open workspace page" style="text-decoration:none;display:inline-flex;align-items:center;">'+getSvgIcon('external-link',14)+'</a>';
|
|
html += '</span>';
|
|
}
|
|
html += '<button class="ws-act" data-ws-action="rename" data-ws-id="'+escId+'" data-ws-name="'+escName+'" title="Rename">'+getSvgIcon('edit',14)+'</button>';
|
|
html += '<button class="ws-act danger" data-ws-action="delete" data-ws-id="'+escId+'" data-ws-name="'+escName+'" title="Delete">'+getSvgIcon('trash',14)+'</button>';
|
|
// Tags
|
|
if (c.tags && c.tags.length > 0) {
|
|
html += '<span style="display:flex;gap:2px;margin-left:4px;">';
|
|
for (var ti=0; ti<c.tags.length; ti++) {
|
|
var t = c.tags[ti];
|
|
var tc = this.esc(t.color||'#787774');
|
|
var tn = this.esc(t.name);
|
|
html += '<span class="tag-chip" style="background:'+tc+'">'+tn+'</span>';
|
|
}
|
|
html += '</span>';
|
|
}
|
|
html += '</div></div>';
|
|
if (hasKids) {
|
|
html += '<ul class="ws-tree" style="display:none">'+this.renderChildren(c.children, depth+1)+'</ul>';
|
|
}
|
|
html += '</li>';
|
|
}
|
|
return html;
|
|
},
|
|
|
|
// Inline rename for renderChildren (static HTML)
|
|
startRenderChildRename(id, currentName) {
|
|
var nameSpan = document.getElementById('rc-name-' + id);
|
|
var input = document.getElementById('rc-input-' + id);
|
|
if (!nameSpan || !input) return;
|
|
nameSpan.style.display = 'none';
|
|
input.style.display = '';
|
|
input.value = currentName;
|
|
var w = this._fitNameWidth(currentName);
|
|
var avail = input.parentElement ? input.parentElement.clientWidth : 0;
|
|
if (avail > 60) input.style.width = Math.min(w, avail) + 'px'; else input.style.width = w + 'px';
|
|
input.focus();
|
|
input.select();
|
|
},
|
|
|
|
async commitRenderChildRename(id) {
|
|
var nameSpan = document.getElementById('rc-name-' + id);
|
|
var input = document.getElementById('rc-input-' + id);
|
|
if (!input) return;
|
|
var newName = (input.value || '').trim();
|
|
input.style.display = 'none';
|
|
if (nameSpan) nameSpan.style.display = '';
|
|
if (!newName) return;
|
|
try {
|
|
var r = await fetch('/api/local-workspace/items/' + id, {
|
|
method: 'PUT', headers: {'X-CSRF-Token': getCsrf(), 'Content-Type':'application/json'},
|
|
body: JSON.stringify({name: newName})
|
|
});
|
|
if (r.ok) {
|
|
if (nameSpan) nameSpan.textContent = newName;
|
|
// Update tree node
|
|
var node = this._findNodeById(this.tree, id);
|
|
if (node) { node.name = newName; }
|
|
this._reloadAfterAction();
|
|
}
|
|
} catch { /* volontaire */ }
|
|
},
|
|
|
|
esc(s) {
|
|
if (!s) return '';
|
|
return s.replace(/\\/g,'\\\\').replace(/'/g,"\\'").replace(/"/g,'"');
|
|
},
|
|
|
|
_fitNameWidth(text) {
|
|
var off = document.createElement('span');
|
|
off.style.cssText = 'position:absolute;visibility:hidden;white-space:pre;font-size:13px;';
|
|
off.textContent = (text || '').toString();
|
|
document.body.appendChild(off);
|
|
var w = off.offsetWidth;
|
|
document.body.removeChild(off);
|
|
return Math.max(w + 16, 60);
|
|
},
|
|
|
|
// ── Event delegation for renderChildren action buttons ──
|
|
handleTreeAction(ev) {
|
|
var btn = (ev.target.closest && ev.target.closest('[data-ws-action]'))
|
|
|| (ev.target.parentElement && ev.target.parentElement.closest('[data-ws-action]'));
|
|
if (!btn) return;
|
|
ev.stopPropagation();
|
|
var action = btn.getAttribute('data-ws-action');
|
|
var id = parseInt(btn.getAttribute('data-ws-id'));
|
|
var name = btn.getAttribute('data-ws-name') || '';
|
|
if (!id) return;
|
|
|
|
switch(action) {
|
|
case 'new-file':
|
|
window.FlowDeck.createPage(id);
|
|
break;
|
|
case 'new-folder':
|
|
window.FlowDeck.showCreateFolderModal(id);
|
|
break;
|
|
case 'rename':
|
|
this.startRename({db_id: id, name: name});
|
|
break;
|
|
case 'delete':
|
|
this.deleteWithUndo({db_id: id, name: name});
|
|
break;
|
|
}
|
|
},
|
|
|
|
toggleExpand(id, btn) {
|
|
this.expanded[id] = !(this.expanded[id] || false);
|
|
this.expanded = Object.assign({}, this.expanded);
|
|
this._saveExpandedState();
|
|
// For renderChildren static HTML: toggle chevron + sibling <ul>
|
|
if (btn) {
|
|
btn.classList.toggle('open', this.expanded[id]);
|
|
// Navigate: btn → .ws-tree-item → <li> → <ul.ws-tree>
|
|
var itemDiv = btn.closest('.ws-tree-item');
|
|
if (itemDiv && itemDiv.parentElement) {
|
|
var children = itemDiv.parentElement.children;
|
|
for (var i = 0; i < children.length; i++) {
|
|
if (children[i].tagName === 'UL' && children[i].classList.contains('ws-tree')) {
|
|
children[i].style.display = this.expanded[id] ? '' : 'none';
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
var self = this;
|
|
this.$nextTick(function() { self._syncCheckboxes(); });
|
|
},
|
|
|
|
async navigateToFolder(id) {
|
|
// Find the folder name for the stack
|
|
var folderNode = this._findNodeById(this.tree, id);
|
|
var folderName = folderNode ? folderNode.name : 'Folder';
|
|
// Push to folder stack for breadcrumb
|
|
if (this.navIndex < this.folderStack.length - 1) {
|
|
this.folderStack = this.folderStack.slice(0, this.navIndex + 1);
|
|
}
|
|
this.folderStack.push({id: id, name: folderName});
|
|
this.navIndex = this.folderStack.length - 1;
|
|
// Fetch children of this folder
|
|
await this._loadFolder(id);
|
|
},
|
|
|
|
async goToParent() {
|
|
if (this.folderStack.length <= 1) { await this._loadFolder(0); return; }
|
|
this.folderStack.pop();
|
|
this.navIndex = this.folderStack.length - 1;
|
|
var parentId = this.folderStack.length > 0 ? this.folderStack[this.folderStack.length - 1].id : 0;
|
|
await this._loadFolder(parentId);
|
|
},
|
|
|
|
async _loadFolder(folderId) {
|
|
var url = '/api/local-workspace/tree';
|
|
if (folderId && folderId > 0) url += '?folder=' + folderId;
|
|
try {
|
|
var r = await fetch(url);
|
|
var d = await r.json();
|
|
var roots = d.tree || [];
|
|
this._markTree(roots, 0);
|
|
this.tree = roots;
|
|
this.displayTree = [...roots];
|
|
this.showAll = false;
|
|
this.breadcrumb = d.breadcrumb || [];
|
|
this.currentFolder = folderId || 0;
|
|
this._saveExpandedState();
|
|
this.previewItem = null;
|
|
this._recompute();
|
|
var self = this;
|
|
this.$nextTick(function() { self._syncCheckboxes(); });
|
|
} catch(e) { console.error('Load folder error:', e); }
|
|
},
|
|
|
|
_findNodeById(nodes, id) {
|
|
for (var i=0; i<nodes.length; i++) {
|
|
if (nodes[i].id === id) return nodes[i];
|
|
if (nodes[i].children) {
|
|
var found = this._findNodeById(nodes[i].children, id);
|
|
if (found) return found;
|
|
}
|
|
}
|
|
return null;
|
|
},
|
|
|
|
async goBack() {
|
|
if (this.navIndex <= 0) return;
|
|
this.navIndex--;
|
|
var entry = this.folderStack[this.navIndex];
|
|
this._saveExpandedState();
|
|
await this._loadFolder(entry ? entry.id : 0);
|
|
},
|
|
async goForward() {
|
|
if (this.navIndex >= this.folderStack.length - 1) return;
|
|
this.navIndex++;
|
|
var entry = this.folderStack[this.navIndex];
|
|
this._saveExpandedState();
|
|
await this._loadFolder(entry ? entry.id : 0);
|
|
},
|
|
|
|
_saveExpandedState() {
|
|
try { localStorage.setItem('fd_ws_expanded', JSON.stringify(this.expanded)); }
|
|
catch { /* volontaire */ }
|
|
},
|
|
_reloadAfterAction() {
|
|
this._saveExpandedState();
|
|
this._loadFolder(this.currentFolder);
|
|
// Directly refresh sidebar tree
|
|
if (window.appState && window.appState.refreshSidebarTree) {
|
|
window.appState.refreshSidebarTree();
|
|
}
|
|
},
|
|
|
|
// ── Inline Rename ──
|
|
startInlineRename(node, ev) {
|
|
this.renamingId = node.id;
|
|
this.renamingName = node.name;
|
|
var self = this;
|
|
this.$nextTick(function() {
|
|
var inp = self.$el.querySelector('.inline-rename-input:not([style*="display: none"])');
|
|
if (inp) {
|
|
var w = self._fitNameWidth(self.renamingName);
|
|
var avail = inp.parentElement ? inp.parentElement.clientWidth : 0;
|
|
if (avail > 60) inp.style.width = Math.min(w, avail) + 'px'; else inp.style.width = w + 'px';
|
|
inp.focus(); inp.select();
|
|
}
|
|
});
|
|
},
|
|
async commitRename(node) {
|
|
var n = this.renamingName.trim();
|
|
this.renamingId = null;
|
|
if (!n || n === node.name) return;
|
|
var r = await fetch('/api/local-workspace/items/' + node.id, {
|
|
method: 'PUT', headers: {'X-CSRF-Token': getCsrf(), 'Content-Type':'application/json'},
|
|
body: JSON.stringify({name: n})
|
|
});
|
|
if (r.ok) {
|
|
node.name = n; node.db_name = n;
|
|
this._saveExpandedState();
|
|
if (window.appState && window.appState.refreshSidebarTree) window.appState.refreshSidebarTree();
|
|
if (window.FlowDeck && window.FlowDeck.syncPageTitle) window.FlowDeck.syncPageTitle(node.id, n);
|
|
if (this.folderStack && this.folderStack.length) {
|
|
this.folderStack.forEach(function(f) { if (String(f.id) === String(node.id)) f.name = n; });
|
|
}
|
|
if (this.breadcrumb && this.breadcrumb.length) {
|
|
this.breadcrumb.forEach(function(b) { if (String(b.id) === String(node.id)) b.name = n; });
|
|
}
|
|
if (this.previewItem && String(this.previewItem.id) === String(node.id)) {
|
|
this.previewItem.name = n;
|
|
var pt = document.getElementById('ws-preview-title');
|
|
if (pt) pt.textContent = n;
|
|
}
|
|
}
|
|
},
|
|
cancelRename() { this.renamingId = null; },
|
|
|
|
// ── Keyboard shortcuts ──
|
|
onKeydown(ev) {
|
|
var tag = ev.target && ev.target.tagName;
|
|
if (tag === 'INPUT' || tag === 'TEXTAREA') return; // Don't hijack typing
|
|
if (this.renamingId) return; // Don't intercept during rename
|
|
if (ev.key === 'Delete' && !ev.ctrlKey && !ev.metaKey) {
|
|
ev.preventDefault();
|
|
// Delete selected item — implement when multi-select exists
|
|
}
|
|
if (ev.key === 'F2' && !ev.ctrlKey) {
|
|
ev.preventDefault();
|
|
// Find hovered or first file item and rename
|
|
var hovered = document.querySelector('.ws-tree-item:hover');
|
|
if (hovered) {
|
|
var id = parseInt(hovered.getAttribute('data-ws-id') || hovered.getAttribute('data-folder-id'));
|
|
var name = (hovered.querySelector('.name') || {}).textContent || '';
|
|
if (id) this.startInlineRename({id: id, name: name}, ev);
|
|
}
|
|
}
|
|
if (ev.ctrlKey && ev.key === 'c') {
|
|
// Copy selected items to clipboard
|
|
ev.preventDefault();
|
|
this.clipboard = []; // Will implement with multi-select
|
|
}
|
|
if (ev.ctrlKey && ev.key === 'v' && this.clipboard.length > 0) {
|
|
ev.preventDefault();
|
|
// Paste items — move them to current folder
|
|
var self = this;
|
|
var targetId = this.currentFolder > 0 ? this.currentFolder : null;
|
|
this.clipboard.forEach(function(id) {
|
|
fetch('/api/local-workspace/items/' + id + '/move', {
|
|
method: 'PUT', headers: {'X-CSRF-Token': getCsrf(), 'Content-Type':'application/json'},
|
|
body: JSON.stringify({parent_id: targetId})
|
|
}).then(function(r) { if (r.ok) self._reloadAfterAction(); });
|
|
});
|
|
this.clipboard = [];
|
|
}
|
|
},
|
|
|
|
// ── Duplicate ──
|
|
async duplicateItem(node) {
|
|
var r = await fetch('/api/local-workspace/items', {
|
|
method: 'POST', headers: {'X-CSRF-Token': getCsrf(), 'Content-Type':'application/json'},
|
|
body: JSON.stringify({name: node.name + ' (copy)', type: node.type || 'page',
|
|
parent_id: this.currentFolder > 0 ? this.currentFolder : null})
|
|
});
|
|
if (r.ok) this._reloadAfterAction();
|
|
},
|
|
|
|
// ── Sort ──
|
|
doSort() {
|
|
var s = this.sortBy;
|
|
if (!s) { this.displayTree = [...this.tree]; this.showAll = false; return; }
|
|
var sorted = this.tree.slice();
|
|
if (s === 'name-asc') sorted.sort(function(a,b) { return (a.name||'').localeCompare(b.name||''); });
|
|
else if (s === 'name-desc') sorted.sort(function(a,b) { return (b.name||'').localeCompare(a.name||''); });
|
|
else if (s === 'date-asc') sorted.sort(function(a,b) { return (a.updated_at||'').localeCompare(b.updated_at||''); });
|
|
else if (s === 'date-desc') sorted.sort(function(a,b) { return (b.updated_at||'').localeCompare(a.updated_at||''); });
|
|
else if (s === 'type') sorted.sort(function(a,b) { return (a.is_folder===b.is_folder) ? 0 : a.is_folder ? -1 : 1; });
|
|
this.displayTree = sorted;
|
|
this._recompute();
|
|
},
|
|
|
|
// ── Undo delete ──
|
|
async deleteWithUndo(node) {
|
|
var self = this;
|
|
this.undoItemId = node.db_id || node.id;
|
|
this.undoMessage = 'Deleted "' + (node.name || 'item') + '"';
|
|
this.undoVisible = true;
|
|
// Delete via API
|
|
var r = await fetch('/api/local-workspace/items/' + this.undoItemId, {headers: {'X-CSRF-Token': getCsrf()}, method:'DELETE' });
|
|
if (!r.ok) { this.undoVisible = false; return; }
|
|
// Reload from API for reliability (sidebar refresh handled by _reloadAfterAction)
|
|
this._reloadAfterAction();
|
|
// Auto-dismiss undo banner after 5s
|
|
this.undoTimeout = setTimeout(function() {
|
|
self.undoVisible = false;
|
|
self.undoItemId = null;
|
|
}, 5000);
|
|
},
|
|
undoDelete() {
|
|
if (this.undoTimeout) clearTimeout(this.undoTimeout);
|
|
this.undoVisible = false;
|
|
var ids = (this.undoItemId || '').toString().split(',');
|
|
this.undoItemId = null;
|
|
if (!ids.length || !ids[0]) return;
|
|
// Restore via API (soft-delete undo) — single or bulk
|
|
var self = this;
|
|
var total = ids.length;
|
|
function restoreOne(i) {
|
|
if (i >= total) {
|
|
self._reloadAfterAction();
|
|
return;
|
|
}
|
|
fetch('/api/local-workspace/items/' + ids[i] + '/restore', {headers: {'X-CSRF-Token': getCsrf()}, method: 'POST' })
|
|
.finally(function() { restoreOne(i + 1); });
|
|
}
|
|
restoreOne(0);
|
|
},
|
|
_removeFromTree(nodes, id) {
|
|
for (var i=0; i<nodes.length; i++) {
|
|
if (nodes[i].id === id) { nodes.splice(i, 1); return true; }
|
|
if (nodes[i].children && this._removeFromTree(nodes[i].children, id)) {
|
|
nodes[i].child_count = nodes[i].children.length;
|
|
nodes[i].has_children = nodes[i].child_count > 0;
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
},
|
|
|
|
// ── Upload speed tracking ──
|
|
_trackUploadSpeed(totalSize) {
|
|
var self = this;
|
|
this.uploadStartTime = Date.now();
|
|
this.uploadSpeed = '';
|
|
var checkSpeed = setInterval(function() {
|
|
if (!self.uploading) { clearInterval(checkSpeed); return; }
|
|
var elapsed = (Date.now() - self.uploadStartTime) / 1000;
|
|
if (elapsed > 0) {
|
|
self.uploadSpeed = (totalSize / elapsed / 1024).toFixed(0) + ' KB/s';
|
|
}
|
|
}, 500);
|
|
},
|
|
|
|
// ── Path bar navigation ──
|
|
navigateToPath() {
|
|
var path = (this.$el.querySelector('.path-input') || {}).value || '';
|
|
var parts = path.replace(/^\/workspace\/?/, '').split('/').filter(Boolean);
|
|
if (parts.length === 0) { if (window.fdNavigate) window.fdNavigate('/local-workspace'); else window.location = '/local-workspace'; return; }
|
|
// Walk tree to find matching folder
|
|
var targetId = this._findFolderByPath(this.tree, parts, 0);
|
|
if (targetId) {
|
|
if (window.fdNavigate) window.fdNavigate('/local-workspace?folder=' + targetId);
|
|
else window.location = '/local-workspace?folder=' + targetId;
|
|
} else {
|
|
window.showToast('Path not found: ' + parts.join('/'), 'error');
|
|
}
|
|
},
|
|
_findFolderByPath(nodes, parts, idx) {
|
|
if (idx >= parts.length) return null;
|
|
for (var i=0; i<nodes.length; i++) {
|
|
if (nodes[i].is_folder && nodes[i].name === parts[idx]) {
|
|
if (idx === parts.length - 1) return nodes[i].id;
|
|
if (nodes[i].children) {
|
|
var found = this._findFolderByPath(nodes[i].children, parts, idx+1);
|
|
if (found) return found;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
},
|
|
|
|
// ── Cache-aware preview ──
|
|
showPreview(ev, node) {
|
|
if (node.is_folder) return;
|
|
this.previewName = node.name;
|
|
this.previewX = ev.clientX + 12;
|
|
this.previewY = ev.clientY + 12;
|
|
this.previewVisible = true;
|
|
var self = this;
|
|
var wsId = LW.workspace_id || 1;
|
|
var cacheKey = 'prev_' + node.id;
|
|
// Return cached immediately
|
|
if (this.previewCache[cacheKey]) {
|
|
this.previewContent = this.previewCache[cacheKey];
|
|
return;
|
|
}
|
|
// Determine type
|
|
var imgExts = ['.png','.jpg','.jpeg','.gif','.webp','.svg','.bmp','.ico'];
|
|
var isImg = false, isPdf = false;
|
|
for (var i=0; i<imgExts.length; i++) {
|
|
if ((node.name||'').toLowerCase().endsWith(imgExts[i])) { isImg = true; break; }
|
|
}
|
|
isPdf = (node.name||'').toLowerCase().endsWith('.pdf');
|
|
|
|
if (node.content_format === 'file') {
|
|
var url = '/api/files/' + wsId + '/' + encodeURIComponent(node.name);
|
|
if (isImg) {
|
|
var html = '<img src="'+url+'" style="max-width:380px;max-height:180px;display:block;">';
|
|
this.previewContent = html; this.previewCache[cacheKey] = html;
|
|
} else if (isPdf) {
|
|
var html = '<div style="text-align:center;padding:16px;">'+getSvgIcon('file',48)+'<br><span style="color:var(--text-secondary);font-size:12px;">PDF Document</span><br><a href="/pages/'+node.id+'" style="color:var(--accent);font-size:11px;">Open viewer →</a></div>';
|
|
this.previewContent = html; this.previewCache[cacheKey] = html;
|
|
} else {
|
|
this.previewContent = 'Loading...';
|
|
this._fetchRetry(url).then(function(t) {
|
|
var content = t.substring(0, 500);
|
|
self.previewContent = content; self.previewCache[cacheKey] = content;
|
|
}).catch(function() { self.previewContent = '(preview not available)'; });
|
|
}
|
|
} else {
|
|
this.previewContent = 'Loading...';
|
|
this._fetchRetry('/api/local-workspace/page-content/' + node.id, true)
|
|
.then(function(d) {
|
|
var content = self._extractPreview(d.content || '', d.format);
|
|
self.previewContent = content; self.previewCache[cacheKey] = content;
|
|
}).catch(function() { self.previewContent = '(preview not available)'; });
|
|
}
|
|
},
|
|
|
|
// ── Fetch with retry ──
|
|
_fetchRetry(url, json) {
|
|
var maxRetries = 2;
|
|
var delay = 300;
|
|
return new Promise(function(resolve, reject) {
|
|
function attempt(n) {
|
|
fetch(url).then(function(r) {
|
|
if (!r.ok) throw new Error('HTTP ' + r.status);
|
|
return json ? r.json() : r.text();
|
|
}).then(resolve).catch(function(e) {
|
|
if (n < maxRetries) {
|
|
setTimeout(function() { attempt(n+1); }, delay * (n+1));
|
|
} else { reject(e); }
|
|
});
|
|
}
|
|
attempt(0);
|
|
});
|
|
},
|
|
|
|
// ── Touch haptic feedback ──
|
|
_haptic() {
|
|
if (navigator.vibrate) navigator.vibrate(10);
|
|
},
|
|
|
|
// ── Expand / Collapse all ──
|
|
expandAll() {
|
|
var self = this;
|
|
this._walkTree(this.tree, function(n) {
|
|
if (n.is_folder && n.children && n.children.length > 0) {
|
|
self.expanded[n.id] = true;
|
|
}
|
|
});
|
|
this.expanded = Object.assign({}, this.expanded);
|
|
// Toggle static HTML for renderChildren
|
|
this._toggleAllUl(this.$el, true);
|
|
},
|
|
collapseAll() {
|
|
this.expanded = {};
|
|
this._toggleAllUl(this.$el, false);
|
|
},
|
|
_walkTree(nodes, fn) {
|
|
for (var i=0; i<nodes.length; i++) {
|
|
fn(nodes[i]);
|
|
if (nodes[i].children) this._walkTree(nodes[i].children, fn);
|
|
}
|
|
},
|
|
_toggleAllUl(el, show) {
|
|
var uls = el.querySelectorAll('ul.ws-tree');
|
|
for (var i=0; i<uls.length; i++) {
|
|
uls[i].style.display = show ? '' : 'none';
|
|
}
|
|
// Also toggle chevrons
|
|
var chevs = el.querySelectorAll('.ws-chevron');
|
|
for (var j=0; j<chevs.length; j++) {
|
|
if (show) chevs[j].classList.add('open');
|
|
else chevs[j].classList.remove('open');
|
|
}
|
|
},
|
|
|
|
// ── File type icon ──
|
|
_fileIcon(name, isFolder, contentFormat, customIcon) {
|
|
if (customIcon) {
|
|
var html = window.fdIconHtml ? window.fdIconHtml(customIcon, 14) : this.esc(customIcon);
|
|
return '<span class="page-icon-svg" style="font-size:14px;line-height:1;display:inline-flex;align-items:center;">'+html+'</span>';
|
|
}
|
|
if (isFolder) return '<span class="page-icon-svg">'+getSvgIcon('folder',14)+'</span>';
|
|
// FlowDeck internal pages (no extension, blocks/markdown format)
|
|
if (contentFormat && contentFormat !== 'file') return '<span class="page-icon-svg">'+getSvgIcon('edit',14)+'</span>';
|
|
var n = (name || '').toLowerCase();
|
|
if (/\.(png|jpe?g|gif|webp|svg|bmp|ico)$/.test(n)) return '<span class="page-icon-svg">'+getSvgIcon('image',14)+'</span>';
|
|
if (/\.pdf$/.test(n)) return '<span class="page-icon-svg">'+getSvgIcon('file',14)+'</span>';
|
|
if (/\.(md|markdown)$/.test(n)) return '<span class="page-icon-svg">'+getSvgIcon('edit',14)+'</span>';
|
|
if (/\.(py)$/.test(n)) return '<span class="page-icon-svg">'+getSvgIcon('file',14)+'</span>';
|
|
if (/\.(js|jsx|ts|tsx)$/.test(n)) return '<span class="page-icon-svg">'+getSvgIcon('file',14)+'</span>';
|
|
if (/\.(html?|xml)$/.test(n)) return '<span class="page-icon-svg">'+getSvgIcon('file',14)+'</span>';
|
|
if (/\.css$/.test(n)) return '<span class="page-icon-svg">'+getSvgIcon('file',14)+'</span>';
|
|
if (/\.json$/.test(n)) return '<span class="page-icon-svg">'+getSvgIcon('file',14)+'</span>';
|
|
if (/\.sql$/.test(n)) return '<span class="page-icon-svg">'+getSvgIcon('file',14)+'</span>';
|
|
if (/\.(sh|bash|zsh)$/.test(n)) return '<span class="page-icon-svg">'+getSvgIcon('file',14)+'</span>';
|
|
if (/\.ps1$/.test(n)) return '<span class="page-icon-svg">'+getSvgIcon('file',14)+'</span>';
|
|
if (/\.(rs|go|java|rb|php|c|cpp|h|swift|kt|scala|r)$/.test(n)) return '<span class="page-icon-svg">'+getSvgIcon('file',14)+'</span>';
|
|
if (/\.(txt|log)$/.test(n)) return '<span class="page-icon-svg">'+getSvgIcon('file',14)+'</span>';
|
|
if (/\.(zip|tar|gz|rar|7z)$/.test(n)) return '<span class="page-icon-svg">'+getSvgIcon('file',14)+'</span>';
|
|
return '<span class="page-icon-svg">'+getSvgIcon('file',14)+'</span>';
|
|
},
|
|
|
|
// ── Search / Filter ──
|
|
doFilter() {
|
|
var q = (this.searchQuery || '').toLowerCase().trim();
|
|
var ft = this.filterType || '';
|
|
if (!q && !ft && !this.tagFilter) { this.displayTree = [...this.tree]; this.showAll = false; this._computeFilteredTableItems(); return; }
|
|
// Apply all filters (search + type + tag) to tree view
|
|
this.displayTree = this._filterTree([...this.tree], q, ft);
|
|
if (this.tagFilter) {
|
|
this.displayTree = this._filterTreeByTag(this.displayTree, this.tagFilter);
|
|
}
|
|
this._computeFilteredTableItems();
|
|
},
|
|
_filterTreeByTag(nodes, tagName) {
|
|
var result = [];
|
|
for (var i=0; i<nodes.length; i++) {
|
|
var n = nodes[i];
|
|
var hasTag = (n.tags || []).some(function(t) { return t.name === tagName; });
|
|
var filteredChildren = n.children ? this._filterTreeByTag(n.children, tagName) : [];
|
|
if (hasTag || filteredChildren.length > 0) {
|
|
var clone = Object.assign({}, n);
|
|
clone.children = filteredChildren;
|
|
result.push(clone);
|
|
}
|
|
}
|
|
return result;
|
|
},
|
|
filterByType(type) {
|
|
this.filterType = (this.filterType === type) ? '' : type;
|
|
this.doFilter();
|
|
},
|
|
_filterTree(nodes, query, fileType) {
|
|
var result = [];
|
|
for (var i=0; i<nodes.length; i++) {
|
|
var n = nodes[i];
|
|
var filteredChildren = n.children ? this._filterTree(n.children, query, fileType) : [];
|
|
var selfMatches = (n.name || '').toLowerCase().indexOf(query || '') !== -1;
|
|
// File type filter
|
|
var typeMatch = true;
|
|
if (fileType && !n.is_folder) {
|
|
var nm = (n.name || '').toLowerCase();
|
|
switch(fileType) {
|
|
case 'folder': typeMatch = n.is_folder; break;
|
|
case 'pdf': typeMatch = /\.pdf$/.test(nm); break;
|
|
case 'image': typeMatch = /\.(png|jpe?g|gif|webp|svg|bmp|ico)$/.test(nm); break;
|
|
case 'code': typeMatch = /\.(py|js|jsx|ts|tsx|rs|go|java|rb|php|c|cpp|h|swift|kt|scala|r|sh|bash|ps1|sql)$/.test(nm); break;
|
|
case 'text': typeMatch = /\.(txt|md|markdown|log)$/.test(nm); break;
|
|
case 'page': typeMatch = !/\.(pdf|png|jpe?g|gif|webp|svg|bmp|ico|py|js|jsx|ts|tsx|rs|go|java|rb|php|c|cpp|h|swift|kt|scala|r|sh|bash|ps1|sql|txt|md|markdown|log)$/.test(nm); break;
|
|
}
|
|
}
|
|
if ((selfMatches || filteredChildren.length > 0) && typeMatch) {
|
|
var clone = Object.assign({}, n);
|
|
clone.children = filteredChildren;
|
|
result.push(clone);
|
|
}
|
|
}
|
|
return result;
|
|
},
|
|
|
|
// ── Preview on hover ──
|
|
_extractPreview(content, format) {
|
|
if (!content) return '(empty)';
|
|
// Try to extract text from Notion block JSON
|
|
if (format === 'blocks' || format === 'markdown') {
|
|
try {
|
|
var blocks = typeof content === 'string' ? JSON.parse(content) : content;
|
|
if (blocks && blocks.blocks && Array.isArray(blocks.blocks)) {
|
|
var lines = [];
|
|
for (var i=0; i<blocks.blocks.length && lines.length<20; i++) {
|
|
var b = blocks.blocks[i];
|
|
if (b.content) lines.push(b.content);
|
|
else if (b.text) lines.push(b.text);
|
|
else if (typeof b === 'string') lines.push(b);
|
|
}
|
|
return lines.join('\n').substring(0, 500) || '(empty)';
|
|
}
|
|
} catch { /* volontaire */ }
|
|
}
|
|
// Fallback: raw text
|
|
return String(content).substring(0, 500);
|
|
},
|
|
hidePreview() {
|
|
this.previewVisible = false;
|
|
},
|
|
|
|
openModal(type, parent) {
|
|
this.createType = type;
|
|
this.parentFolder = parent||null;
|
|
this.newName = '';
|
|
this.showCreate = true;
|
|
var self = this;
|
|
this.$nextTick(function() {
|
|
var el = self.$refs.cinp;
|
|
if (el) setTimeout(function() { el.focus(); }, 100);
|
|
});
|
|
},
|
|
|
|
async doCreate() {
|
|
var n = this.newName.trim();
|
|
if (!n) return;
|
|
var body = { name:n, type:this.createType };
|
|
if (this.parentFolder) body.parent_id = this.parentFolder.db_id;
|
|
var r = await fetch('/api/local-workspace/items', {
|
|
method:'POST',
|
|
headers:{'X-CSRF-Token': getCsrf(), 'Content-Type':'application/json'},
|
|
body:JSON.stringify(body)
|
|
});
|
|
if (r.ok) {
|
|
var d = await r.json();
|
|
// Save parent ref BEFORE clearing so we can insert into tree
|
|
var parent = this.parentFolder;
|
|
this.showCreate = false;
|
|
this.parentFolder = null;
|
|
this.newName = '';
|
|
// Insert new node into tree without reload
|
|
var isFolder = this.createType === 'folder';
|
|
var newNode = {
|
|
id: d.id, db_id: d.id, name: d.name || n,
|
|
is_folder: isFolder, type: isFolder ? 'folder' : 'page',
|
|
content_format: isFolder ? null : 'blocks',
|
|
children: [], has_children: false, depth: 0
|
|
};
|
|
if (parent && parent.db_id) {
|
|
// Insert as child of parent folder
|
|
this._insertIntoParent(this.tree, parent.db_id, newNode);
|
|
// Auto-expand parent so the new item is visible
|
|
this.expanded[parent.db_id] = true;
|
|
this.expanded = Object.assign({}, this.expanded);
|
|
} else {
|
|
// Insert at root
|
|
this.tree.unshift(newNode);
|
|
}
|
|
// Force new reference so Alpine x-for re-renders
|
|
if (!this.searchQuery && !this.filterType) {
|
|
this.displayTree = [...this.tree];
|
|
} else if (this.sortBy) {
|
|
// Re-sort with the new item
|
|
this.doSort();
|
|
} else {
|
|
// Re-apply filter with updated tree
|
|
this.doFilter();
|
|
}
|
|
this._saveExpandedState();
|
|
// Directly refresh sidebar tree
|
|
if (window.appState && window.appState.refreshSidebarTree) {
|
|
window.appState.refreshSidebarTree();
|
|
}
|
|
}
|
|
},
|
|
|
|
_insertIntoParent(nodes, parentId, newNode) {
|
|
for (var i=0; i<nodes.length; i++) {
|
|
if (nodes[i].id === parentId) {
|
|
if (!nodes[i].children) nodes[i].children = [];
|
|
nodes[i].children.unshift(newNode);
|
|
nodes[i].has_children = true;
|
|
newNode.depth = (nodes[i].depth || 0) + 1;
|
|
return true;
|
|
}
|
|
if (nodes[i].children && this._insertIntoParent(nodes[i].children, parentId, newNode)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
},
|
|
|
|
openPage(id) { if (window.fdNavigate) window.fdNavigate('/pages/' + id); else window.location = '/pages/' + id; },
|
|
openSidePeek(id) {
|
|
var node = this._findNodeById(this.tree, id) || this._findNodeById(this.displayTree, id);
|
|
if (node) this.selectForPreview(node);
|
|
},
|
|
|
|
startRename(node) {
|
|
this.target = node;
|
|
this.newName = node.name.trim();
|
|
this.showRename = true;
|
|
var self = this;
|
|
this.$nextTick(function() {
|
|
var el = self.$refs.rinp;
|
|
if (el) setTimeout(function() { el.focus(); el.select(); }, 100);
|
|
});
|
|
},
|
|
|
|
async doRename() {
|
|
var n = this.newName.trim();
|
|
if (!n||!this.target) return;
|
|
var r = await fetch('/api/local-workspace/items/'+this.target.db_id, {
|
|
method:'PUT',
|
|
headers:{'X-CSRF-Token': getCsrf(), 'Content-Type':'application/json'},
|
|
body:JSON.stringify({name:n})
|
|
});
|
|
if (r.ok) { this._reloadAfterAction(); }
|
|
},
|
|
|
|
startDelete(node) {
|
|
this.target = node;
|
|
this.showDelete = true;
|
|
},
|
|
|
|
async doDelete() {
|
|
if (!this.target) return;
|
|
var r = await fetch('/api/local-workspace/items/'+this.target.db_id, {headers: {'X-CSRF-Token': getCsrf()}, method:'DELETE' });
|
|
if (r.ok) { this._reloadAfterAction(); }
|
|
},
|
|
|
|
// ── Drag & Drop ──
|
|
onDragOver(ev) {
|
|
this.isDragging = true;
|
|
// Detect if hovering over a folder item
|
|
var folderEl = ev.target.closest('.ws-tree-item');
|
|
var parentFolder = folderEl ? folderEl.querySelector('.name.folder') : null;
|
|
// Clear previous highlight
|
|
document.querySelectorAll('.drop-target-highlight').forEach(function(el){ el.classList.remove('drop-target-highlight'); });
|
|
if (parentFolder) {
|
|
var name = parentFolder.textContent.trim();
|
|
this.dropTargetName = name;
|
|
// Find parent node id from the onclick attribute or data
|
|
var li = parentFolder.closest('.ws-tree-item');
|
|
if (li) {
|
|
// Try to extract ID from onclick or the onclick handler
|
|
// Use data attributes if available, otherwise fall back
|
|
}
|
|
folderEl.classList.add('drop-target-highlight');
|
|
} else {
|
|
this.dropTargetName = 'root';
|
|
}
|
|
},
|
|
|
|
onDragLeave(ev) {
|
|
// Only reset if leaving the layout entirely
|
|
if (!ev.currentTarget.contains(ev.relatedTarget)) {
|
|
this.isDragging = false;
|
|
this.dropTargetName = '';
|
|
document.querySelectorAll('.drop-target-highlight').forEach(function(el){ el.classList.remove('drop-target-highlight'); });
|
|
}
|
|
},
|
|
|
|
onInternalDragStart(ev, id) {
|
|
this.draggedItemId = id;
|
|
ev.dataTransfer.effectAllowed = 'move';
|
|
ev.dataTransfer.setData('text/plain', String(id));
|
|
// Add a visual class to the dragged element
|
|
ev.target.style.opacity = '0.5';
|
|
},
|
|
|
|
onDrop(ev) {
|
|
this.isDragging = false;
|
|
document.querySelectorAll('.drop-target-highlight').forEach(function(el){ el.classList.remove('drop-target-highlight'); });
|
|
// Reset any opacity from drag
|
|
document.querySelectorAll('.ws-tree-item').forEach(function(el){ el.style.opacity = ''; });
|
|
|
|
// ── Internal drag (move item to another folder) ──
|
|
if (this.draggedItemId !== null) {
|
|
var draggedId = this.draggedItemId;
|
|
this.draggedItemId = null;
|
|
// Determine target parent
|
|
var folderEl = ev.target.closest('.ws-tree-item');
|
|
var targetParentId = this.currentFolder > 0 ? this.currentFolder : null;
|
|
if (folderEl) {
|
|
var folderLink = folderEl.querySelector('.name.folder');
|
|
if (folderLink) {
|
|
var fid = folderEl.getAttribute('data-folder-id');
|
|
if (fid) targetParentId = parseInt(fid);
|
|
}
|
|
}
|
|
// Don't drop on self or same parent
|
|
if (draggedId === targetParentId) return;
|
|
this._moveItem(draggedId, targetParentId);
|
|
return;
|
|
}
|
|
|
|
// ── External drop (files from computer) ──
|
|
var folderEl = ev.target.closest('.ws-tree-item');
|
|
var parentId = this.currentFolder > 0 ? this.currentFolder : null;
|
|
var parentName = 'root';
|
|
|
|
if (folderEl) {
|
|
var folderLink = folderEl.querySelector('.name.folder');
|
|
if (folderLink) {
|
|
parentName = folderLink.textContent.trim();
|
|
// Use data-folder-id attribute (works for both x-for and renderChildren)
|
|
var fid = folderEl.getAttribute('data-folder-id');
|
|
if (fid) parentId = parseInt(fid);
|
|
}
|
|
}
|
|
|
|
var items = ev.dataTransfer.items;
|
|
if (!items) return;
|
|
|
|
var dtFiles = ev.dataTransfer.files;
|
|
|
|
// Check if any item is a directory (WebKit only)
|
|
var hasDirectory = false;
|
|
for (var i = 0; i < items.length; i++) {
|
|
var entry = items[i].webkitGetAsEntry ? items[i].webkitGetAsEntry() : null;
|
|
if (entry && entry.isDirectory) { hasDirectory = true; break; }
|
|
}
|
|
|
|
if (hasDirectory) {
|
|
// Walk directory tree and upload structure
|
|
this._processDirectoryDrop(items, parentId, parentName);
|
|
} else if (dtFiles.length > 0) {
|
|
// Simple file upload
|
|
this._uploadFiles(dtFiles, parentId, parentName);
|
|
}
|
|
},
|
|
|
|
_processDirectoryDrop(items, parentId, parentName) {
|
|
var self = this;
|
|
var allItems = []; // {path: string, type: 'file'|'folder', entry: FileSystemEntry}
|
|
var pending = items.length;
|
|
|
|
function walkEntry(entry, basePath) {
|
|
var path = basePath ? basePath + '/' + entry.name : entry.name;
|
|
if (entry.isFile) {
|
|
allItems.push({path: path, type: 'file', entry: entry});
|
|
pending--;
|
|
if (pending === 0) doUpload();
|
|
} else if (entry.isDirectory) {
|
|
allItems.push({path: path, type: 'folder', entry: entry});
|
|
var reader = entry.createReader();
|
|
reader.readEntries(function(entries) {
|
|
pending += entries.length;
|
|
for (var i = 0; i < entries.length; i++) {
|
|
walkEntry(entries[i], path);
|
|
}
|
|
pending--;
|
|
if (pending === 0) doUpload();
|
|
});
|
|
}
|
|
}
|
|
|
|
for (var i = 0; i < items.length; i++) {
|
|
var entry = items[i].webkitGetAsEntry ? items[i].webkitGetAsEntry() : null;
|
|
if (entry) walkEntry(entry, '');
|
|
else pending--;
|
|
}
|
|
if (pending === 0) doUpload();
|
|
|
|
function doUpload() {
|
|
// Build structure
|
|
var structure = [];
|
|
for (var i = 0; i < allItems.length; i++) {
|
|
structure.push({path: allItems[i].path, type: allItems[i].type});
|
|
}
|
|
|
|
// Resolve file entries to actual File objects
|
|
var fileEntries = [];
|
|
for (var i = 0; i < allItems.length; i++) {
|
|
if (allItems[i].type === 'file') fileEntries.push(allItems[i]);
|
|
}
|
|
|
|
var formData = new FormData();
|
|
formData.append('structure', JSON.stringify(structure));
|
|
if (parentId) formData.append('parent_id', parentId);
|
|
|
|
function addFile(idx) {
|
|
if (idx >= fileEntries.length) {
|
|
// All resolved, upload
|
|
self._doUpload(formData, parentId, parentName);
|
|
return;
|
|
}
|
|
var fe = fileEntries[idx];
|
|
fe.entry.file(function(file) {
|
|
formData.append('files', file, fe.path);
|
|
addFile(idx + 1);
|
|
}, function() {
|
|
addFile(idx + 1);
|
|
});
|
|
}
|
|
addFile(0);
|
|
}
|
|
},
|
|
|
|
_uploadFiles(fileList, parentId, parentName) {
|
|
var formData = new FormData();
|
|
for (var i = 0; i < fileList.length; i++) {
|
|
formData.append('files', fileList[i]);
|
|
}
|
|
if (parentId) formData.append('parent_id', parentId);
|
|
this._doUpload(formData, parentId, parentName);
|
|
},
|
|
|
|
async _moveItem(itemId, newParentId) {
|
|
var body = { parent_id: newParentId || null };
|
|
try {
|
|
var r = await fetch('/api/local-workspace/items/' + itemId + '/move', {
|
|
method: 'PUT',
|
|
headers: {'X-CSRF-Token': getCsrf(), 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body)
|
|
});
|
|
if (r.ok) {
|
|
this._reloadAfterAction();
|
|
} else {
|
|
window.showToast('Move failed', 'error');
|
|
}
|
|
} catch(e) {
|
|
window.showToast('Move failed: ' + e.message, 'error');
|
|
}
|
|
},
|
|
|
|
async _doUpload(formData, parentId, parentName) {
|
|
var self = this;
|
|
this.uploading = true;
|
|
this.uploadStatus = 'Uploading to ' + parentName + '...';
|
|
this.uploadProgress = 10;
|
|
|
|
var folderHandled = formData.has('structure');
|
|
var url = folderHandled ? '/api/local-workspace/upload-folder' : '/api/local-workspace/upload';
|
|
|
|
try {
|
|
var r = await fetch(url, { method: 'POST', body: formData });
|
|
this.uploadProgress = 80;
|
|
var d = await r.json();
|
|
if (r.ok) {
|
|
this.uploadStatus = '✓ ' + d.items.length + ' item(s) uploaded';
|
|
this.uploadProgress = 100;
|
|
setTimeout(function() {
|
|
self.uploading = false;
|
|
self._reloadAfterAction();
|
|
}, 800);
|
|
} else {
|
|
this.uploadStatus = '✗ ' + (d.error || 'Upload failed');
|
|
this.uploadProgress = 0;
|
|
setTimeout(function() { self.uploading = false; }, 2000);
|
|
}
|
|
} catch {
|
|
this.uploadStatus = '✗ Network error';
|
|
this.uploadProgress = 0;
|
|
setTimeout(function() { self.uploading = false; }, 2000);
|
|
}
|
|
},
|
|
|
|
// ── Tag management ──
|
|
async loadWorkspaceTags() {
|
|
try {
|
|
var r = await fetch('/api/local-workspace/tags', { cache: 'no-store' });
|
|
var d = await r.json();
|
|
this.workspaceTags = d.tags || [];
|
|
} catch { this.workspaceTags = []; }
|
|
},
|
|
|
|
// Mise à jour OPTIMISTE du compteur d'un tag dans la barre de tags :
|
|
// rafraîchit immédiatement la pastille (delta +1/-1) sans attendre le
|
|
// re-fetch (qui viendra ensuite confirmer/surécrire avec les valeurs serveur).
|
|
_bumpTagCount(tag, delta) {
|
|
if (!tag) return;
|
|
var tags = (this.workspaceTags || []).slice();
|
|
var i = -1;
|
|
for (var k = 0; k < tags.length; k++) {
|
|
if (String(tags[k].id) === String(tag.id)) { i = k; break; }
|
|
}
|
|
if (i >= 0) {
|
|
tags[i] = Object.assign({}, tags[i], { count: Math.max(0, (tags[i].count || 0) + delta) });
|
|
} else if (delta > 0) {
|
|
tags.push({ id: tag.id, name: tag.name || '', color: tag.color || '#787774', count: delta });
|
|
}
|
|
// Réassigne un nouveau tableau → réactivité Alpine
|
|
this.workspaceTags = tags;
|
|
},
|
|
|
|
toggleTagFilter(tagName) {
|
|
this.tagFilter = (this.tagFilter === tagName) ? '' : tagName;
|
|
this.filterType = '';
|
|
this.searchQuery = '';
|
|
this.doFilter();
|
|
},
|
|
|
|
startTagInput(node) {
|
|
this.tagInputId = node.id;
|
|
var self = this;
|
|
this.$nextTick(function() {
|
|
var inp = self.$el.querySelector('.tag-input:not([style*="display: none"])');
|
|
if (inp) inp.focus();
|
|
});
|
|
},
|
|
|
|
async addTag(itemId, tagName) {
|
|
tagName = (tagName || '').trim();
|
|
if (!tagName) { this.tagInputId = null; return; }
|
|
try {
|
|
var r = await fetch('/api/local-workspace/items/' + itemId + '/tags', {
|
|
method: 'POST',
|
|
headers: {'X-CSRF-Token': getCsrf(), 'Content-Type': 'application/json'},
|
|
body: JSON.stringify({name: tagName})
|
|
});
|
|
if (r.ok) {
|
|
var d = await r.json();
|
|
// Update node tags in tree
|
|
this._updateNodeTags(this.tree, itemId, d.tag, 'add');
|
|
this.displayTree = [...this.tree];
|
|
this.tagsVersion++;
|
|
this._bumpTagCount(d.tag, 1);
|
|
await this.loadWorkspaceTags();
|
|
this._computeFilteredTableItems();
|
|
}
|
|
} catch { /* volontaire */ }
|
|
this.tagInputId = null;
|
|
},
|
|
|
|
async removeTag(itemId, tagId) {
|
|
try {
|
|
var r = await fetch('/api/local-workspace/items/' + itemId + '/tags/' + tagId, {headers: {'X-CSRF-Token': getCsrf()},
|
|
method: 'DELETE'
|
|
});
|
|
if (r.ok) {
|
|
this._updateNodeTags(this.tree, itemId, null, 'remove', tagId);
|
|
this.displayTree = [...this.tree];
|
|
this.tagsVersion++;
|
|
this._bumpTagCount({ id: tagId }, -1);
|
|
await this.loadWorkspaceTags();
|
|
this._computeFilteredTableItems();
|
|
}
|
|
} catch { /* volontaire */ }
|
|
},
|
|
|
|
_updateNodeTags(nodes, itemId, tag, action, removeTagId) {
|
|
for (var i=0; i<nodes.length; i++) {
|
|
if (nodes[i].id === itemId) {
|
|
if (action === 'add') {
|
|
// Réassigne un nouveau tableau → déclenche la réactivité Alpine
|
|
var cur = nodes[i].tags || [];
|
|
if (!cur.some(function(t) { return t.id === tag.id; })) {
|
|
nodes[i].tags = cur.concat([tag]);
|
|
}
|
|
} else if (action === 'remove') {
|
|
nodes[i].tags = (nodes[i].tags || []).filter(function(t) { return t.id !== removeTagId; });
|
|
}
|
|
return true;
|
|
}
|
|
if (nodes[i].children && this._updateNodeTags(nodes[i].children, itemId, tag, action, removeTagId)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
},
|
|
|
|
// ── Helpers ──
|
|
_formatDate(dt) {
|
|
if (!dt) return '—';
|
|
var d = new Date(dt.replace(' ', 'T') + 'Z');
|
|
if (isNaN(d.getTime())) return '—';
|
|
var now = new Date();
|
|
var diffMs = now - d;
|
|
var diffMin = Math.floor(diffMs / 60000);
|
|
var diffHrs = Math.floor(diffMs / 3600000);
|
|
var diffDays = Math.floor(diffMs / 86400000);
|
|
if (diffMin < 1) return 'Just now';
|
|
if (diffMin < 60) return diffMin + 'm ago';
|
|
if (diffHrs < 24) return diffHrs + 'h ago';
|
|
if (diffDays < 7) return diffDays + 'd ago';
|
|
return d.toLocaleDateString('en-CA');
|
|
},
|
|
|
|
_fileTypeLabel(name, contentFormat) {
|
|
if (contentFormat && contentFormat !== 'file') return 'Page';
|
|
var n = (name || '').toLowerCase();
|
|
if (/\.(png|jpe?g|gif|webp|svg|bmp|ico)$/.test(n)) return 'Image';
|
|
if (/\.pdf$/.test(n)) return 'PDF';
|
|
if (/\.(py|js|jsx|ts|tsx|rs|go|java|rb|php|c|cpp|h|swift|kt|scala|r|sh|bash|ps1|sql)$/.test(n)) return 'Code';
|
|
if (/\.(txt|md|markdown|log)$/.test(n)) return 'Text';
|
|
if (/\.(zip|tar|gz|rar|7z)$/.test(n)) return 'Archive';
|
|
if (/\.(mp3|wav|ogg|flac|aac)$/.test(n)) return 'Audio';
|
|
if (/\.(mp4|webm|mov|avi)$/.test(n)) return 'Video';
|
|
return 'File';
|
|
},
|
|
|
|
selectAllFiltered(ev) {
|
|
if (ev.target.checked) {
|
|
for (var i=0; i<this.filteredTableItems.length; i++) {
|
|
this.selectedIds[this.filteredTableItems[i].id] = true;
|
|
}
|
|
} else {
|
|
for (var k in this.selectedIds) delete this.selectedIds[k];
|
|
}
|
|
this.selectedIds = Object.assign({}, this.selectedIds);
|
|
this._syncSelection();
|
|
},
|
|
|
|
// ── Selection sync (keeps checkboxes & counters in sync) ──
|
|
_syncSelection() {
|
|
this.selectedIdsCount = Object.keys(this.selectedIds).length;
|
|
var total = this._collectVisibleIds(this.displayTree).length;
|
|
this.allSelected = total > 0 && this.selectedIdsCount >= total;
|
|
this._syncCheckboxes();
|
|
},
|
|
_syncCheckboxes() {
|
|
var boxes = document.querySelectorAll('input.item-checkbox[data-ws-id]');
|
|
for (var i = 0; i < boxes.length; i++) {
|
|
boxes[i].checked = !!this.selectedIds[parseInt(boxes[i].getAttribute('data-ws-id'))];
|
|
}
|
|
},
|
|
|
|
// ── Multi-select actions (Library-style select bar) ──
|
|
_selectedIds() {
|
|
return Object.keys(this.selectedIds).map(Number);
|
|
},
|
|
copySelectedLinks() {
|
|
this.moreOpen = false;
|
|
var ids = Object.keys(this.selectedIds);
|
|
if (!ids.length) return;
|
|
var links = ids.map(function(id) { return window.location.origin + '/pages/' + id; });
|
|
navigator.clipboard.writeText(links.join('\n'));
|
|
if (window.showToast) window.showToast('Links copied', 'success');
|
|
},
|
|
async openMovePicker() {
|
|
var ids = this._selectedIds();
|
|
if (!ids.length) return;
|
|
this.moreOpen = false;
|
|
this.moveIds = ids;
|
|
this.moveSearchQuery = '';
|
|
this.moveModalOpen = true;
|
|
try {
|
|
// Fetch the full workspace tree so every folder is a valid destination
|
|
var r = await fetch('/api/local-workspace/tree');
|
|
var d = await r.json();
|
|
var folders = [];
|
|
this._collectFolders(d.tree || [], folders, 0);
|
|
this._moveAllCandidates = folders;
|
|
this.moveCandidates = folders.slice(0, 50);
|
|
} catch {
|
|
this._moveAllCandidates = [];
|
|
this.moveCandidates = [];
|
|
}
|
|
},
|
|
_collectFolders(nodes, out, depth) {
|
|
for (var i = 0; i < nodes.length; i++) {
|
|
var n = nodes[i];
|
|
if (n.is_folder) out.push({ id: n.id, name: n.name, depth: depth });
|
|
if (n.children) this._collectFolders(n.children, out, depth + 1);
|
|
}
|
|
},
|
|
doMoveSearch() {
|
|
var q = (this.moveSearchQuery || '').toLowerCase().trim();
|
|
if (!q) { this.moveCandidates = this._moveAllCandidates.slice(0, 50); return; }
|
|
this.moveCandidates = this._moveAllCandidates.filter(function(it) {
|
|
return (it.name || '').toLowerCase().indexOf(q) !== -1;
|
|
}).slice(0, 50);
|
|
},
|
|
async confirmBulkMove(parentId) {
|
|
this.moveModalOpen = false;
|
|
var ids = this.moveIds;
|
|
this.moveIds = [];
|
|
if (!ids || !ids.length) return;
|
|
for (var i = 0; i < ids.length; i++) {
|
|
try {
|
|
await fetch('/api/local-workspace/items/' + ids[i] + '/move', {
|
|
method: 'PUT',
|
|
headers: {'X-CSRF-Token': getCsrf(), 'Content-Type':'application/json'},
|
|
body: JSON.stringify({ parent_id: parentId || null })
|
|
});
|
|
} catch { /* volontaire */ }
|
|
}
|
|
this.clearSelection();
|
|
this._reloadAfterAction();
|
|
if (window.showToast) window.showToast('Item' + (ids.length > 1 ? 's' : '') + ' moved', 'success');
|
|
},
|
|
|
|
// ── A20 ph3 : x-html interdit par le build CSP → x-init + Alpine.effect
|
|
// (réactivité conservée : lecture des données réactives dans l'effect) ──
|
|
toggleViewMenu() {
|
|
this.viewMenuOpen = !this.viewMenuOpen;
|
|
this.sortOpen = false;
|
|
this.searchOpen = false;
|
|
},
|
|
toggleSortMenu() {
|
|
this.sortOpen = !this.sortOpen;
|
|
this.viewMenuOpen = false;
|
|
},
|
|
toggleFilterMenu() {
|
|
this.filterOpen = !this.filterOpen;
|
|
this.viewMenuOpen = false;
|
|
this.sortOpen = false;
|
|
},
|
|
toggleSearch() {
|
|
this.searchOpen = !this.searchOpen;
|
|
if (this.searchOpen) Alpine.nextTick(() => {
|
|
const el = document.querySelector('[x-ref="searchInput"]');
|
|
if (el) el.focus();
|
|
});
|
|
},
|
|
createPageAt(node) { window.FlowDeck.createPage(node.id); },
|
|
createFolderAt(node) { window.FlowDeck.showCreateFolderModal(node.id); },
|
|
bindSvg(el, name, size) { el.innerHTML = getSvgIcon(name, size); },
|
|
bindFileIcon(el, node) {
|
|
Alpine.effect(() => {
|
|
el.innerHTML = this._fileIcon(node.name, node.is_folder, node.content_format, node.page_icon);
|
|
});
|
|
},
|
|
bindNodeIcon(el, node) {
|
|
Alpine.effect(() => {
|
|
el.innerHTML = node.is_folder
|
|
? getSvgIcon('folder', 24)
|
|
: this._fileIcon(node.name, node.is_folder, node.content_format, node.page_icon);
|
|
});
|
|
},
|
|
bindChildren(el, node) {
|
|
Alpine.effect(() => {
|
|
el.innerHTML = this.renderChildren(node.children, (node.depth || 0) + 1, this.tagsVersion);
|
|
});
|
|
},
|
|
bindPreview(el) { Alpine.effect(() => { el.innerHTML = this.previewContent; }); },
|
|
};
|
|
})();
|
|
// Injecter toutes les props comme globales pour Alpine (résout les 36 erreurs de scope)
|
|
var _wsKeys = Object.keys(_wsInitData);
|
|
for (var _i = 0; _i < _wsKeys.length; _i++) {
|
|
try { window[_wsKeys[_i]] = _wsInitData[_wsKeys[_i]]; } catch { /* volontaire */ }
|
|
}
|
|
console.log('FLOWDECK v2.4.6 — IIFE done, _wsInitData keys:', Object.keys(_wsInitData).length);
|
|
|
|
// A20 ph3 : registre Alpine.data (le build CSP ne résout que le registre ;
|
|
// x-data="wsInitData()" au lieu de l'identifiant global _wsInitData).
|
|
document.addEventListener('alpine:init', function () {
|
|
// objet partagé JS↔Alpine via closure LEXICALE (const hors window →
|
|
// non snapshoté par ji ; probe4 : factory retournant l'objet = accepté).
|
|
Alpine.data('wsInitData', function () { return _wsInitData; });
|
|
// Bloc preview : le parseur le place HORS de la div racine (structure
|
|
// pré-existante) → montage distinct DÉLÉGUANT vers l'objet partagé.
|
|
// Wrapper = objet unique (les magics $nextTick… ne sont redéfinissables
|
|
// qu'une fois) + lecture/écriture via Alpine.reactive(_wsInitData) pour
|
|
// garder la réactivité JS↔Alpine.
|
|
Alpine.data('wsPreview', function () {
|
|
const R = Alpine.reactive(_wsInitData);
|
|
const t = {};
|
|
['previewVisible', 'previewX', 'previewY', 'previewName'].forEach(function (k) {
|
|
Object.defineProperty(t, k, {
|
|
get: function () { return R[k]; },
|
|
set: function (v) { R[k] = v; },
|
|
configurable: true,
|
|
});
|
|
});
|
|
t.bindPreview = function (el) {
|
|
Alpine.effect(function () { el.innerHTML = R.previewContent; });
|
|
};
|
|
return t;
|
|
});
|
|
});
|