/* exported libraryPage -- appeles depuis les attributs HTML des templates */ // A20 phase 3 : enregistrement Alpine.data — le build CSP ne résout que le // registre (probe : globale window → « Undefined variable: libraryPage »). if (window.Alpine) { Alpine.data('libraryPage', libraryPage); fdRefreshXData('libraryPage'); } else document.addEventListener('alpine:init', function () { Alpine.data('libraryPage', libraryPage); }); function libraryPage() { return { tab: 'recents', workspaceId: 0, isGiteaActive: false, giteaOwner: '', giteaRepo: '', items: [], loading: false, searchOpen: false, searchQuery: '', // Selection selected: {}, selectedCount: 0, allSelected: false, // Tree expanded: {}, renamingId: null, renameValue: '', // Toolbar dropdowns filterOpen: false, sortOpen: false, viewOpen: false, sourceFilter: 'all', shareDir: 'all', showAuthor: false, showSource: false, showEditedBy: true, showEditedTime: true, showVisited: true, // Tags workspaceTags: [], tagFilter: '', // Sort sortBy: '', // More menu moreOpen: false, // Move-to modal moveModalOpen: false, moveIds: [], moveSearchQuery: '', // No workspace guard noWorkspace: false, moveCandidates: [], _moveAllCandidates: [], // Drag & drop _dragId: null, // Side peek peekItem: null, peekContent: '', peekLoading: false, peekFavorited: false, peekMoreOpen: false, // ── Computed flat list ── flatItems: [], _renderTable() { var self = this; var body = document.getElementById('lib-table-body'); var table = document.getElementById('lib-table'); var emptyDiv = document.getElementById('lib-empty'); var loadingDiv = document.getElementById('lib-loading'); if (!body) return; // Show/hide table vs empty state if (this.flatItems.length === 0) { if (table) table.style.display = 'none'; if (emptyDiv) emptyDiv.style.display = ''; return; } if (table) table.style.display = ''; if (emptyDiv) emptyDiv.style.display = 'none'; if (loadingDiv) loadingDiv.style.display = 'none'; var html = ''; for (var i = 0; i < this.flatItems.length; i++) { var item = this.flatItems[i]; // ── En-tête de section de date (onglet AI Meeting Notes) ── if (item._isSection) { html += '
' + self._escHtml(item.title) + '
'; continue; } // ── Synthetic "+ Add new" placeholder row for an expanded parent ── if (item._isAddNew) { var pad = 30 + (item.depth || 0) * 20; html += '
'; html += ' Add new
'; continue; } var selClass = this.selected[item.id] ? ' selected' : ''; var indentPx = 4 + (item.depth || 0) * 20; var checkClass = this.selected[item.id] ? ' checked' : ''; var chevOpen = this.expanded[item.id] ? ' open' : ''; var isRenaming = this.renamingId === item.id; html += '
'; html += '
'; // Drag handle html += '
'; html += '
'; // Checkbox html += '
'; if (this.selected[item.id]) html += ''; html += '
'; // Chevron or spacer if (item.has_children) { html += ''; } else { html += ''; } // Icon — monochrome, comme local-workspace (emoji custom si défini) var isFolder = item.is_folder || item.has_children; html += '' + self._renderIcon(item) + ''; // Title or rename input if (isRenaming) { html += ''; } else { html += '' + self._escHtml(item.title) + ''; } // Tags if (item.tags && item.tags.length) { html += ''; for (var ti = 0; ti < item.tags.length; ti++) { var tg = item.tags[ti]; html += '' + self._escHtml(tg.name) + ''; } html += ''; } // OPEN button (side peek) — hidden for folders if (!isFolder) { html += ''; } html += '
'; // Author html += '
'; html += '
' + (item.author_initial || '?') + '
'; html += '' + (item.author || '\u2014') + '
'; // Last edited by html += '
'; html += '
' + (item.edited_by_initial || '?') + '
'; html += '' + (item.edited_by || '\u2014') + '
'; // Edited html += '
' + self.formatTime(item.updated_at) + '
'; // Last visited html += '
' + (item.visited_at ? self.formatTime(item.visited_at) : '\u2014') + '
'; // Source (icône de source + libellé) — en dernier, comme l'en-tête html += '
' + getSvgIcon(item.source_icon || 'file', 13) + '' + (item.source_label || 'Private') + '
'; html += '
'; } body.innerHTML = 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,'>'); }, openMoveSelected() { this.openMovePicker(Object.keys(this.selected).map(Number)); }, // ── A20 phase 3 (surface library) : formes compatibles build CSP ── // $nextTick / x-html ne passent pas par l'évaluateur maison du build CSP // (arrow inline = parse error ; x-html = interdit) → méthodes JS réelles, // réactivité via Alpine.effect — valable sous les deux builds. toggleFilterMenu() { this.filterOpen = !this.filterOpen; this.sortOpen = false; this.viewOpen = false; }, toggleSortMenu() { this.sortOpen = !this.sortOpen; this.filterOpen = false; this.viewOpen = false; }, toggleViewMenu() { this.viewOpen = !this.viewOpen; this.filterOpen = false; this.sortOpen = false; }, toggleSearch() { this.searchOpen = !this.searchOpen; if (this.searchOpen) Alpine.nextTick(() => { var el = document.getElementById('lib-search-input'); if (el) el.focus(); }); }, bindHtmlIcon(el) { Alpine.effect(() => { el.innerHTML = getSvgIcon(this.emptyIcon, 48); }); }, bindHtmlItem(el, item) { Alpine.effect(() => { el.innerHTML = this._renderIcon(item); }); }, _renderIcon(item) { if (!item) return getSvgIcon('file', 14); if (item.is_meeting) return getSvgIcon('calendar', 14); // Custom emoji (image URL), icon name, or unicode emoji if (item.page_icon) return '' + (window.fdIconHtml ? window.fdIconHtml(item.page_icon, 14) : this._escHtml(item.page_icon)) + ''; if (item.is_folder || item.has_children) return getSvgIcon('folder', 14); if (item.content_format && item.content_format !== 'file') return getSvgIcon('edit', 14); var n = (item.title || '').toLowerCase(); if (/\.(png|jpe?g|gif|webp|svg|bmp|ico)$/.test(n)) return getSvgIcon('image', 14); return getSvgIcon('file', 14); }, _recomputeFlatItems() { var result = []; this._flatten(this.items, result, 0); if (this.searchQuery.trim()) { var q = this.searchQuery.toLowerCase(); result = result.filter(function(r) { return (r.title || '').toLowerCase().indexOf(q) !== -1; }); } if (this.tagFilter) { var tf = this.tagFilter; result = result.filter(function(r) { return (r.tags || []).some(function(t) { return t.name === tf; }); }); } if (this.tab === 'meetings') result = this._groupMeetings(result); // Use splice for Alpine 3.14.9 reactivity (array reference reassign fails) this.flatItems.splice(0, this.flatItems.length, ...result); this.selectedCount = Object.keys(this.selected).length; var selectable = result.filter(function(r) { return !r._isSection; }); this.allSelected = selectable.length > 0 && this.selectedCount === selectable.length; this._renderTable(); }, // Regroupement par date pour l'onglet AI Meeting Notes : sections // Today / Yesterday / This week / Last week / This month / Older. _meetingBucket(updatedAt) { var d = new Date(String(updatedAt || '').replace(' ', 'T')); if (isNaN(d.getTime())) return 5; var now = new Date(); var start = new Date(now.getFullYear(), now.getMonth(), now.getDate()); var diff = Math.floor((start.getTime() - new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime()) / 86400000); if (diff <= 0) return 0; if (diff === 1) return 1; if (diff <= 7) return 2; if (diff <= 14) return 3; if (diff <= 30) return 4; return 5; }, _groupMeetings(items) { var labels = ['Today', 'Yesterday', 'This week', 'Last week', 'This month', 'Older']; var buckets = [[], [], [], [], [], []]; for (var i = 0; i < items.length; i++) { if (items[i]._isSection) continue; buckets[this._meetingBucket(items[i].updated_at)].push(items[i]); } var out = []; for (var b = 0; b < buckets.length; b++) { if (!buckets[b].length) continue; out.push({ _isSection: true, id: 'sec-' + b, title: labels[b] }); for (var j = 0; j < buckets[b].length; j++) out.push(buckets[b][j]); } return out; }, // ── 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._renderTable(); return; } // Read value from the input var inp = document.getElementById('rename-inp-' + id); if (inp) this.renameValue = inp.value; 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); }, cancelRename() { this.renamingId = null; this._renderTable(); }, _findItem(id) { for (var i = 0; i < this.flatItems.length; i++) { if (this.flatItems[i].id === id) return this.flatItems[i]; } return null; }, // ── Row click / context menu ── onRowClick(evt, id) { var item = this._findItem(id); if (!item) return; this.openItem(item); }, onRowContextMenu(evt, id) { evt.preventDefault(); var item = this._findItem(id); if (!item) return; if (!(window.Alpine && Alpine.store('fdCtx'))) return; /* MENU UNIFIÉ (partial _ctx_menu.html + store fdCtx) — handlers library complète l'union. Open folder/tags non prévus côté library : fdCtx les cache automatiquement via handlers absents. */ var self = this; var handlers = { open: function (n) { self.openItem(n); }, openTab: function (n) { self.openInNewTab(n); }, peek: function (n) { self.openPeek(n); }, rename: function (n) { self.startRename(n); }, setIcon: function (icon) { self.setItemIcon(item, icon); }, duplicate: function (n) { self.duplicateItem(n); }, link: function (n) { self.copyLink(n); }, download: function (n) { self.downloadItem(n); }, copyContent: function (n) { self.copyItemContent(n); }, move: function (n) { self.openMovePicker([n.id]); }, fav: function (n) { self.toggleFavoriteItem(n); }, delete: function (n) { self.deleteItem(n); }, tagExisting: function (t) { self.ctxAddExistingTag(t); }, tagAdd: function (name, color) { self.ctxAddNewTag(name, color); }, tagRemove: function (tagId) { self.ctxRemoveTag(tagId); } }; if (this.tab === 'recents') handlers.recent = function (n) { self.removeItemFromRecents(n); }; var store = Alpine.store('fdCtx'); store.setAvail(self._ctxAvailTags(item)); store.openMenu(evt, item, 'library', 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'); } }, // ── Drag & drop (move / reparent) ── onRowDragStart(evt, id) { if (!evt.target.closest('.drag-handle')) { evt.preventDefault(); return; } this._dragId = id; evt.dataTransfer.effectAllowed = 'move'; evt.dataTransfer.setData('text/plain', String(id)); evt.currentTarget.classList.add('dragging'); }, onRowDragOver(evt, id) { if (this._dragId === null || this._dragId === id) return; evt.preventDefault(); evt.currentTarget.classList.add('drag-over'); }, onRowDragLeave(evt, id) { evt.currentTarget.classList.remove('drag-over'); }, async onRowDrop(evt, id) { evt.preventDefault(); evt.currentTarget.classList.remove('drag-over'); var draggedId = this._dragId; this._dragId = null; if (!draggedId || draggedId === id) return; var target = this._findItem(id); var dragged = this._findItem(draggedId); if (!target || !dragged) return; // Prevent dropping a page onto one of its own (already-loaded) descendants if (this._isDescendant(dragged, id)) { if (window.showToast) window.showToast('Cannot move a page into its own subpage', 'error'); return; } await this._movePages([draggedId], id); if (window.showToast) window.showToast('Page moved', 'success'); }, onRowDragEnd(evt) { this._dragId = null; document.querySelectorAll('.lib-row.dragging').forEach(function(el){ el.classList.remove('dragging'); }); document.querySelectorAll('.lib-row.drag-over').forEach(function(el){ el.classList.remove('drag-over'); }); }, _flatten(nodes, out, depth) { if (!nodes) return; for (var i = 0; i < nodes.length; i++) { var n = nodes[i]; n.depth = depth || 0; out.push(n); if (n.has_children && this.expanded[n.id]) { this._flatten(n.children, out, depth + 1); if (this._canAddChild()) { out.push({ _isAddNew: true, _parentId: n.id, depth: (depth || 0) + 1, id: 'add-' + n.id }); } } } }, _canAddChild() { return this.tab !== 'favorites' && this.tab !== 'shared' && this.tab !== 'published' && this.tab !== 'repository' && this.tab !== 'meetings'; }, _getCsrf() { var m = getCsrf(); return m; }, _syncSidebar() { if (window.appState && window.appState.refreshSidebarTree) window.appState.refreshSidebarTree(); if (window.appState && window.appState.refreshFavorites) window.appState.refreshFavorites(); }, _isDescendant(node, targetId) { if (!node || !node.children) return false; for (var i = 0; i < node.children.length; i++) { if (node.children[i].id === targetId) return true; if (this._isDescendant(node.children[i], targetId)) return true; } return false; }, async _movePages(ids, parentId) { var csrf = this._getCsrf(); for (var i = 0; i < ids.length; i++) { try { await fetch('/board/api/pages/' + ids[i] + '/move', { method: 'PUT', headers: {'Content-Type':'application/json', 'X-CSRF-Token': csrf}, body: JSON.stringify({ parent_id: parentId || 0 }) }); } catch { /* volontaire */ } } this.clearSelection(); this.loadTab(); this._syncSidebar(); }, // ── Empty states ── emptyIcon: 'file', emptyTitle: 'No items', emptyText: '', _recomputeEmpty() { var icons = {recents:'clock',meetings:'calendar',favorites:'star',shared:'users',published:'globe',private:'lock',workspace:'folder',repository:'folder'}; this.emptyIcon = icons[this.tab] || 'file'; var titles = {recents:'No recent pages',meetings:'No meeting notes',favorites:'No favorites',shared:'No shared pages', published:'No published pages',private:'No private pages',workspace:'No workspace pages', repository:'Empty repository'}; this.emptyTitle = titles[this.tab] || 'No items'; var texts = {recents:'Pages you\'ve recently viewed or edited will appear here.', meetings:'Create a meeting note from the sidebar (+) — it will appear here, grouped by date.', favorites:'Right-click a page in the sidebar and select "Add to Favorites".', shared:'Shared pages from your team will appear here.', published:'Pages published to the web will appear here.', private:'Create a new page with the + button.', workspace:'Pages inside workspaces will appear here.', repository:'Repository files and pages will appear here.'}; this.emptyText = texts[this.tab] || ''; }, // ── 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; var giteaEl = document.getElementById('lib-gitea-active'); this.isGiteaActive = (giteaEl ? giteaEl.textContent : '0') === '1'; this.giteaOwner = (document.getElementById('lib-gitea-owner') || {}).textContent || ''; this.giteaRepo = (document.getElementById('lib-gitea-repo') || {}).textContent || ''; // Detect no workspace: no local wsId AND no Gitea active if (!this.workspaceId && !this.isGiteaActive) { this.noWorkspace = true; this.loading = false; // don't show loading spinner return; } console.log('[Library] workspaceId=' + this.workspaceId + ' isGitea=' + this.isGiteaActive); var params = new URLSearchParams(window.location.search); var t = params.get('tab'); if (t && ['recents','meetings','favorites','shared','published','private','workspace','repository'].indexOf(t) !== -1) { this.tab = t; } this._recomputeEmpty(); this.loadWorkspaceTags(); console.log('[Library] loading tab: ' + this.tab); this.loadTab(); }, switchTab(t) { if (this.tab === t) return; if (t === 'repository' && !this.isGiteaActive) return; this.tab = t; this.shareDir = 'all'; this.tagFilter = ''; this.items = []; this.selected = {}; this.selectedCount = 0; this.allSelected = false; this.expanded = {}; this.searchQuery = ''; this.searchOpen = false; this._recomputeEmpty(); this.updateUrl(); this.loadTab(); }, updateUrl() { var p = new URLSearchParams(); if (this.tab !== 'recents') p.set('tab', this.tab); var qs = p.toString(); window.history.replaceState({}, '', qs ? '/library?' + qs : '/library'); }, // ── Load ── async loadTab() { this.loading = true; try { var url = '/api/library/' + this.tab; var sep = '?'; if (this.workspaceId && (this.tab === 'recents' || this.tab === 'workspace')) { url += sep + 'workspace_id=' + this.workspaceId; sep = '&'; } if (this.tab === 'repository' && this.isGiteaActive) { url += sep + 'gitea_owner=' + encodeURIComponent(this.giteaOwner) + '&gitea_repo=' + encodeURIComponent(this.giteaRepo); sep = '&'; } else if (this.tab !== 'repository' && this.tab !== 'recents' && this.tab !== 'favorites' && this.tab !== 'meetings') { // Hierarchical tree view: show only root-level pages at the top url += sep + 'tree=1'; sep = '&'; } else if (this.tab !== 'repository' && this.tab !== 'workspace' && this.sourceFilter && this.sourceFilter !== 'all') { url += sep + 'source_type=' + encodeURIComponent(this.sourceFilter); sep = '&'; } if (this.tab === 'shared' && this.shareDir && this.shareDir !== 'all') { url += sep + 'dir=' + encodeURIComponent(this.shareDir); sep = '&'; } var r = await fetch(url); var d = await r.json(); // Backend already computes has_children correctly for each item (children // are loaded lazily on expand) — do NOT overwrite it here. this.items = d.items || []; this._recomputeFlatItems(); this._renderTable(); var self = this; this.$nextTick(function() { self.loading = false; }); } catch(e) { console.error('[Library] load error:', e); this.items = []; this.loading = false; } }, doSearch() { this._recomputeFlatItems(); }, setSourceFilter(v) { this.sourceFilter = v; this.filterOpen = false; this.loadTab(); }, setShareDir(v) { if (this.shareDir === v) return; this.shareDir = v; this.loadTab(); }, setSort(field) { this.sortBy = field; this.sortOpen = false; this.doSort(); }, toggleColumn(col) { if (col === 'author') this.showAuthor = !this.showAuthor; else if (col === 'source') this.showSource = !this.showSource; else if (col === 'editedby') this.showEditedBy = !this.showEditedBy; else if (col === 'editedtime') this.showEditedTime = !this.showEditedTime; else if (col === 'visited') this.showVisited = !this.showVisited; }, doSort() { var s = this.sortBy; if (!s) return; function sortNodes(nodes) { if (!nodes) return; nodes.sort(function(a,b){ if (s === 'name') return (a.title||'').localeCompare(b.title||''); if (s === 'edited') return (b.updated_at||'').localeCompare(a.updated_at||''); if (s === 'author') return (a.author||'').localeCompare(b.author||''); return 0; }); for (var i = 0; i < nodes.length; i++) { if (nodes[i].children && nodes[i].children.length) sortNodes(nodes[i].children); } } sortNodes(this.items); this._recomputeFlatItems(); }, // ── Tree expand ── async toggleExpand(item) { if (this.expanded[item.id]) { // Collapse delete this.expanded[item.id]; this.expanded = Object.assign({}, this.expanded); this._recomputeFlatItems(); this._renderTable(); return; } // Expand — load children if not loaded yet this.expanded[item.id] = true; this.expanded = Object.assign({}, this.expanded); if (!item.children || !item.children.length) { try { var r = await fetch('/api/library/children/' + item.id); var d = await r.json(); item.children = d.items || []; // Mark the tree for depth calculation this._markChildren(item.children, (item.depth || 0) + 1); } catch { item.children = []; } } this._recomputeFlatItems(); this._renderTable(); }, _markChildren(nodes, depth) { if (!nodes) return; for (var i = 0; i < nodes.length; i++) { nodes[i].depth = depth; if (nodes[i].children && nodes[i].children.length > 0) { this._markChildren(nodes[i].children, depth + 1); } } }, // ── Selection ── toggleItem(item) { if (this.selected[item.id]) { delete this.selected[item.id]; } else { this.selected[item.id] = true; } this.selected = Object.assign({}, this.selected); this._recomputeFlatItems(); this._renderTable(); }, toggleSelectAll() { if (this.allSelected) { this.selected = {}; } else { var s = {}; for (var i = 0; i < this.flatItems.length; i++) { if (this.flatItems[i]._isSection || this.flatItems[i]._isAddNew) continue; s[this.flatItems[i].id] = true; } this.selected = s; } this._recomputeFlatItems(); this._renderTable(); }, clearSelection() { this.selected = {}; this.moreOpen = false; this._recomputeFlatItems(); this._renderTable(); }, // ── Multi-select actions ── async deleteSelected() { var ids = Object.keys(this.selected); if (!ids.length) return; if (!confirm('Move ' + ids.length + ' item(s) to Trash?')) return; var csrf = this._getCsrf(); for (var i = 0; i < ids.length; i++) { await fetch('/board/api/pages/' + ids[i], {method:'DELETE', headers:{'X-CSRF-Token': csrf}}); } this.clearSelection(); this.loadTab(); this._syncSidebar(); if (window.showToast) window.showToast('Moved to trash', 'success'); }, async deleteItem(item) { if (!item) return; if (!confirm('Move "' + item.title + '" to Trash?')) return; var csrf = this._getCsrf(); await fetch('/board/api/pages/' + item.id, {method:'DELETE', headers:{'X-CSRF-Token': csrf}}); this.loadTab(); this._syncSidebar(); if (window.showToast) window.showToast('Moved to trash', 'success'); }, removeFromRecents() { // No server-side "visited" tracking exists yet — remove from the // current view only (session-scoped). this.moreOpen = false; var ids = Object.keys(this.selected).map(Number); if (!ids.length) return; this.items = this.items.filter(function(it) { return ids.indexOf(it.id) === -1; }); this.clearSelection(); this._recomputeFlatItems(); }, copyLinks() { this.moreOpen = false; var ids = Object.keys(this.selected); 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'); }, copyLink(item) { if (!item) return; navigator.clipboard.writeText(window.location.origin + '/pages/' + item.id); if (window.showToast) window.showToast('Link copied', 'success'); }, openInNewTab(item) { if (!item || !item.id) return; window.open('/pages/' + item.id, '_blank'); }, removeItemFromRecents(item) { if (!item) return; var id = item.id; this.items = this.items.filter(function(it) { return it.id !== id; }); this._recomputeFlatItems(); if (window.showToast) window.showToast('Removed from Recents', 'success'); }, async setItemIcon(item, icon) { if (!item || !item.id) return; icon = (icon || '').trim(); try { var r = await fetch('/board/api/pages/' + item.id + '/icon', { method: 'POST', headers: {'Content-Type': 'application/json', 'X-CSRF-Token': this._getCsrf()}, body: JSON.stringify({icon: icon}) }); if (!r.ok) throw new Error('icon update failed'); item.page_icon = icon; this._renderTable(); if (window.showToast) window.showToast('Icon updated', 'success'); } catch { if (window.showToast) window.showToast('Failed to update icon', 'error'); } }, // ── Tags ── async loadWorkspaceTags() { try { var r = await fetch('/api/settings/tags/all'); var d = await r.json(); this.workspaceTags = d.tags || []; } catch { this.workspaceTags = []; } }, toggleTagFilter(tagName) { this.tagFilter = (this.tagFilter === tagName) ? '' : tagName; this._recomputeFlatItems(); }, _ctxAvailTags(item) { return (this.workspaceTags || []).filter(function(t) { return !((item && item.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)); }, ctxAddExistingTag(tag) { this.ctxAddNewTag(tag.name, tag.color); }, ctxRemoveTag(tagId) { var store = window.Alpine && Alpine.store('fdCtx'); var item = store && store.node; if (!item) return; var self = this; fetch('/api/local-workspace/items/' + item.id + '/tags/' + tagId, {headers: {'X-CSRF-Token': getCsrf()}, method: 'DELETE' }) .then(function(r) { if (!r.ok) return; item.tags = (item.tags || []).filter(function(t) { return t.id !== tagId; }); self.loadWorkspaceTags(); self._ctxRefreshAvail(); self._recomputeFlatItems(); }); }, async ctxAddNewTag(tagName, color) { tagName = (tagName || '').trim(); if (!tagName) return; var store = window.Alpine && Alpine.store('fdCtx'); var item = store && store.node; if (!item) return; color = color || (store && store.newTagColor) || '#787774'; try { var r = await fetch('/api/local-workspace/items/' + item.id + '/tags', { method: 'POST', headers: {'Content-Type': 'application/json', 'X-CSRF-Token': getCsrf()}, body: JSON.stringify({name: tagName, color: color}) }); if (r.ok) { var d = await r.json(); var cur = item.tags || []; if (!cur.some(function(t) { return t.id === d.tag.id; })) item.tags = cur.concat([d.tag]); await this.loadWorkspaceTags(); this._ctxRefreshAvail(); this._recomputeFlatItems(); } } catch { /* volontaire */ } }, async toggleFavoriteItem(item) { if (!item) return; var csrf = this._getCsrf(); var method = item.favorited ? 'DELETE' : 'POST'; var r = await fetch('/board/api/favorites/' + item.id, { method: method, headers: {'X-CSRF-Token': csrf} }); if (r.ok) { item.favorited = !item.favorited; this._renderTable(); if (window.appState && window.appState.refreshFavorites) window.appState.refreshFavorites(); if (this.tab === 'favorites') this.loadTab(); } }, async duplicateItem(item) { if (!item) return; try { var r = await fetch('/board/api/pages/' + item.id); var page = await r.json(); var csrf = this._getCsrf(); var section = page.parent_section || 'Private'; var parentId = page.parent_id || 0; var ws = page.workspace || ''; var createUrl = '/board/api/pages?title=' + encodeURIComponent((page.title || 'Untitled') + ' (Copy)') + '§ion=' + encodeURIComponent(section) + '&project=' + encodeURIComponent(ws) + '&parent_id=' + parentId; var cr = await fetch(createUrl, { method: 'POST', headers: {'X-CSRF-Token': csrf} }); var created = await cr.json(); if (page.content) { var updUrl = '/board/api/pages/' + created.id + '?content=' + encodeURIComponent(page.content) + '&content_format=' + encodeURIComponent(page.content_format || ''); await fetch(updUrl, { method: 'PUT', headers: {'X-CSRF-Token': csrf} }); } this.loadTab(); this._syncSidebar(); if (window.showToast) window.showToast('Page duplicated', 'success'); } catch { if (window.showToast) window.showToast('Failed to duplicate page', 'error'); } }, // ── Move to (picker modal) ── async openMovePicker(ids) { this.moreOpen = false; this.peekMoreOpen = false; if (!ids || !ids.length) return; this.moveIds = ids; this.moveSearchQuery = ''; this.moveModalOpen = true; try { var r = await fetch('/api/library/recents?workspace_id=' + (this.workspaceId || 0)); var d = await r.json(); var all = (d.items || []).filter(function(it) { return ids.indexOf(it.id) === -1; }); this._moveAllCandidates = all; this.moveCandidates = all.slice(0, 30); } catch { this._moveAllCandidates = []; this.moveCandidates = []; } }, doMoveSearch() { var q = (this.moveSearchQuery || '').toLowerCase().trim(); if (!q) { this.moveCandidates = this._moveAllCandidates.slice(0, 30); return; } this.moveCandidates = this._moveAllCandidates.filter(function(it) { return (it.title || '').toLowerCase().indexOf(q) !== -1; }).slice(0, 30); }, async confirmMove(parentId) { this.moveModalOpen = false; var ids = this.moveIds; this.moveIds = []; if (!ids || !ids.length) return; await this._movePages(ids, parentId); if (window.showToast) window.showToast('Page moved', 'success'); }, // ── Item actions ── openItem(item) { if (!item || !item.id) return; if (window.fdNavigate) window.fdNavigate('/pages/' + item.id); else window.location.href = '/pages/' + item.id; }, startRename(item) { this.renamingId = item.id; this.renameValue = item.title; this._renderTable(); // Focus the rename input setTimeout(function() { var inp = document.getElementById('rename-inp-' + item.id); if (inp) { inp.focus(); inp.select(); } }, 50); }, async commitRename(item) { var newName = (this.renameValue || '').trim(); this.renamingId = null; if (!newName || newName === item.title) { this._renderTable(); return; } try { var csrf = this._getCsrf(); var r = await fetch('/board/api/pages/' + item.id + '?title=' + encodeURIComponent(newName), { method: 'PUT', headers: {'X-CSRF-Token': csrf} }); if (r.ok) { item.title = newName; } } catch { /* volontaire */ } this._renderTable(); this._syncSidebar(); }, // ── Add new ── async addNewPage(parentId) { var name = prompt('Page name:'); if (!name || !name.trim()) return; var csrf = this._getCsrf(); var section = 'Private'; try { var url = '/board/api/pages?title=' + encodeURIComponent(name.trim()) + '§ion=' + encodeURIComponent(section) + '&parent_id=' + (parentId || 0); var r = await fetch(url, { method: 'POST', headers: {'X-CSRF-Token': csrf} }); if (r.ok) { var d = await r.json(); if (parentId) { // Refresh the parent's children so the new page appears inline var parent = this._findItem(parentId); if (parent) { this.expanded[parentId] = true; try { var cr = await fetch('/api/library/children/' + parentId); var cd = await cr.json(); parent.children = cd.items || []; } catch { /* volontaire */ } this._recomputeFlatItems(); this._renderTable(); return; } } if (window.fdNavigate) window.fdNavigate('/pages/' + d.id); else window.location.href = '/pages/' + d.id; } } catch { if (window.showToast) window.showToast('Failed to create page', 'error'); } }, newPage() { this.addNewPage(); }, // ── Side peek ── // L'iframe est pilotée en JS : le build CSP d'Alpine n'évalue pas le // binding `:src` (l'iframe restait vide et aucun document n'apparaissait). // `about:blank` ferme explicitement le document précédent avant de charger // le nouveau : sans cela, même URL = pas de rechargement et l'ancien // document restait affiché. openPeek(item) { this.peekItem = item; this.peekFavorited = item.favorited || false; var frame = document.getElementById('lib-peek-iframe'); var loading = document.getElementById('lib-peek-loading'); if (!frame) return; if (loading) loading.style.display = ''; frame.onload = function () { if (loading) loading.style.display = 'none'; }; frame.src = 'about:blank'; // `?embed=1` : le peek affiche SEULEMENT le document en mode édition // (ni sidebar ni barre de navigation) — le cadre du panneau suffit. frame.src = '/pages/' + item.id + '?embed=1'; }, closePeek() { this.peekItem = null; var frame = document.getElementById('lib-peek-iframe'); // On décharge l'éditeur : il resterait en mémoire (et son autosave // tournerait) alors qu'il n'est plus visible. if (frame) { frame.onload = null; frame.src = 'about:blank'; } var loading = document.getElementById('lib-peek-loading'); if (loading) { loading.style.display = ''; loading.textContent = 'Chargement…'; } }, async toggleFavoritePeek() { if (!this.peekItem) return; var id = this.peekItem.id; var csrf = this._getCsrf(); var method = this.peekFavorited ? 'DELETE' : 'POST'; var r = await fetch('/board/api/favorites/' + id, { method: method, headers: {'X-CSRF-Token': csrf} }); if (r.ok) { this.peekFavorited = !this.peekFavorited; this.peekItem.favorited = this.peekFavorited; if (window.appState && window.appState.refreshFavorites) window.appState.refreshFavorites(); } }, copyPeekLink() { if (!this.peekItem) return; navigator.clipboard.writeText(window.location.origin + '/pages/' + this.peekItem.id); if (window.showToast) window.showToast('Link copied', 'success'); }, duplicatePeek() { this.peekMoreOpen = false; if (!this.peekItem) return; this.duplicateItem(this.peekItem); }, movePeekTo() { this.peekMoreOpen = false; if (!this.peekItem) return; this.openMovePicker([this.peekItem.id]); }, async deletePeek() { this.peekMoreOpen = false; if (!this.peekItem) return; if (!confirm('Move to Trash?')) return; var csrf = this._getCsrf(); await fetch('/board/api/pages/' + this.peekItem.id, {method:'DELETE', headers:{'X-CSRF-Token': csrf}}); this.closePeek(); this.loadTab(); this._syncSidebar(); if (window.showToast) window.showToast('Moved to trash', 'success'); }, // ── Time formatting ── formatTime(d) { if (!d) return '—'; try { var dt = new Date(d); if (isNaN(dt.getTime())) return d; var now = new Date(); var diff = now - dt; var mins = Math.floor(diff/60000); if (mins < 1) return 'Just now'; if (mins < 60) return mins + 'm ago'; var hours = Math.floor(mins/60); if (hours < 24) return hours + 'h ago'; var days = Math.floor(hours/24); if (days < 7) return days + 'd ago'; if (days < 30) return Math.floor(days/7) + 'w ago'; return dt.toLocaleDateString('en-US',{month:'short',day:'numeric',year:'numeric'}); } catch { return d; } } }; } // Init peek resize handle — helper partagé fdWirePeekResize (app.js, defer : // câblage différé jusqu'à sa disponibilité, ou immédiat si on est déjà prêt). (function() { function wire() { var panel = document.querySelector('.peek-overlay'); if (!panel || !window.fdWirePeekResize) return; window.fdWirePeekResize(panel, panel.querySelector('#lib-peek-resize-handle'), function() { if (panel.__x) panel.__x.$data.peekItem = null; }); } if (window.fdWirePeekResize || document.readyState !== 'loading') wire(); else document.addEventListener('DOMContentLoaded', wire); })();