ES module imports are read-only live bindings — can't reassign
imported let/const variables. Replace individual 'export let' with
single 'export const state = {...}' mutable object.
All modules updated: import { state } from './state.js'
All state access changed to state.xxx pattern.
Fixes cascade of 'Assignment to constant variable' errors.
1092 lines
48 KiB
JavaScript
1092 lines
48 KiB
JavaScript
import { state } from './state.js';
|
||
2|
|
||
3|// ---------------------------------------------------------------------------
|
||
4|// Vault context switching
|
||
5|// ---------------------------------------------------------------------------
|
||
6|function initVaultContext() {
|
||
7| const filter = document.getElementById("vault-filter");
|
||
8| const quickSelect = document.getElementById("vault-quick-select");
|
||
9| if (!filter || !quickSelect) return;
|
||
10|
|
||
11| filter.addEventListener("change", async () => {
|
||
12| await setSelectedVaultContext(filter.value, { focusVault: filter.value !== "all" });
|
||
13| });
|
||
14|
|
||
15| quickSelect.addEventListener("change", async () => {
|
||
16| await setSelectedVaultContext(quickSelect.value, { focusVault: quickSelect.value !== "all" });
|
||
17| });
|
||
18|}
|
||
19|
|
||
20|async function setSelectedVaultContext(vaultName, options) {
|
||
21| state.selectedContextVault = vaultName;
|
||
22| state.showingSource = false;
|
||
23| state.cachedRawSource = null;
|
||
24| syncVaultSelectors();
|
||
25| await refreshSidebarForContext();
|
||
26| await refreshTagsForContext();
|
||
27|
|
||
28| // Synchroniser le dashboard et les fichiers récents
|
||
29| if (typeof DashboardRecentWidget !== "undefined" && DashboardRecentWidget.load) {
|
||
30| DashboardRecentWidget.load(vaultName);
|
||
31| }
|
||
32| if (typeof DashboardBookmarkWidget !== "undefined" && DashboardBookmarkWidget.load) {
|
||
33| DashboardBookmarkWidget.load(vaultName);
|
||
34| }
|
||
35| if (state.activeSidebarTab === "recent") {
|
||
36| loadRecentFiles(vaultName === "all" ? null : vaultName);
|
||
37| }
|
||
38|
|
||
39| showWelcome();
|
||
40| if (options && options.focusVault && vaultName !== "all") {
|
||
41| await focusVaultInSidebar(vaultName);
|
||
42| }
|
||
43|}
|
||
44|
|
||
45|function syncVaultSelectors() {
|
||
46| const filter = document.getElementById("vault-filter");
|
||
47| const quickSelect = document.getElementById("vault-quick-select");
|
||
48| const recentFilter = document.getElementById("recent-vault-filter");
|
||
49| const dashboardFilter = document.getElementById("dashboard-vault-filter");
|
||
50| const contextText = document.getElementById("vault-context-text");
|
||
51|
|
||
52| if (filter) filter.value = state.selectedContextVault;
|
||
53| if (quickSelect) quickSelect.value = state.selectedContextVault;
|
||
54| if (recentFilter) recentFilter.value = state.selectedContextVault === "all" ? "" : state.selectedContextVault;
|
||
55| if (dashboardFilter) dashboardFilter.value = state.selectedContextVault;
|
||
56|
|
||
57| // Mise à jour visuelle des dropdowns personnalisés
|
||
58| updateCustomDropdownVisual("vault-filter-dropdown", state.selectedContextVault);
|
||
59| updateCustomDropdownVisual("vault-quick-select-dropdown", state.selectedContextVault);
|
||
60|
|
||
61| // Update vault context indicator
|
||
62| if (contextText) {
|
||
63| contextText.textContent = state.selectedContextVault === "all" ? "All" : state.selectedContextVault;
|
||
64| }
|
||
65|}
|
||
66|
|
||
67|/**
|
||
68| * Updates the visual state of a custom dropdown based on its current value.
|
||
69| */
|
||
70|function updateCustomDropdownVisual(dropdownId, value) {
|
||
71| const dropdown = document.getElementById(dropdownId);
|
||
72| if (!dropdown) return;
|
||
73|
|
||
74| const selectedText = dropdown.querySelector(".custom-dropdown-selected");
|
||
75| const options = dropdown.querySelectorAll(".custom-dropdown-option");
|
||
76|
|
||
77| options.forEach((opt) => {
|
||
78| const optValue = opt.getAttribute("data-value");
|
||
79| if (optValue === value) {
|
||
80| opt.classList.add("selected");
|
||
81| if (selectedText) selectedText.textContent = opt.textContent;
|
||
82| } else {
|
||
83| opt.classList.remove("selected");
|
||
84| }
|
||
85| });
|
||
86|}
|
||
87|
|
||
88|function scrollTreeItemIntoView(element, alignToTop) {
|
||
89| if (!element) return;
|
||
90| const scrollContainer = document.getElementById("sidebar-panel-vaults");
|
||
91| if (!scrollContainer) return;
|
||
92|
|
||
93| const containerRect = scrollContainer.getBoundingClientRect();
|
||
94| const elementRect = element.getBoundingClientRect();
|
||
95| const isAbove = elementRect.top < containerRect.top;
|
||
96| const isBelow = elementRect.bottom > containerRect.bottom;
|
||
97|
|
||
98| if (!isAbove && !isBelow && !alignToTop) return;
|
||
99|
|
||
100| const currentTop = scrollContainer.scrollTop;
|
||
101| const offsetTop = element.offsetTop;
|
||
102| const shouldCenter = alignToTop === "center";
|
||
103| const centeredTop = Math.max(0, currentTop + (elementRect.top - containerRect.top) - (containerRect.height / 2) + (elementRect.height / 2));
|
||
104| const targetTop = shouldCenter
|
||
105| ? centeredTop
|
||
106| : alignToTop
|
||
107| ? Math.max(0, offsetTop - 60)
|
||
108| : Math.max(0, currentTop + (elementRect.top - containerRect.top) - containerRect.height * 0.35);
|
||
109|
|
||
110| scrollContainer.scrollTo({
|
||
111| top: targetTop,
|
||
112| behavior: "smooth",
|
||
113| });
|
||
114|}
|
||
115|
|
||
116|async function refreshSidebarForContext() {
|
||
117| const container = document.getElementById("vault-tree");
|
||
118| container.innerHTML = "";
|
||
119|
|
||
120| const vaultsToShow = state.selectedContextVault === "all" ? state.allVaults : state.allVaults.filter((v) => v.name === state.selectedContextVault);
|
||
121|
|
||
122| vaultsToShow.forEach((v) => {
|
||
123| const vaultItem = el("div", { class: "tree-item vault-item", "data-vault": v.name }, [icon("chevron-right", 14), getVaultIcon(v.name, 16), el("span", { class: "tree-item-text" }, [document.createTextNode(v.name)]), smallBadge(v.file_count)]);
|
||
124| vaultItem.addEventListener("click", () => toggleVault(vaultItem, v.name));
|
||
125|
|
||
126| vaultItem.addEventListener("contextmenu", (e) => {
|
||
127| e.preventDefault();
|
||
128| const isReadonly = false;
|
||
129| ContextMenuManager.show(e.clientX, e.clientY, v.name, '', 'vault', isReadonly);
|
||
130| });
|
||
131| attachTreeItemActionButton(vaultItem, v.name, "", "vault", false);
|
||
132| attachTreeItemLongPress(vaultItem, () => ({ vault: v.name, path: "", type: "vault", isReadonly: false }));
|
||
133|
|
||
134| container.appendChild(vaultItem);
|
||
135|
|
||
136| const childContainer = el("div", { class: "tree-children collapsed", id: `vault-children-${v.name}` });
|
||
137| container.appendChild(childContainer);
|
||
138| });
|
||
139|
|
||
140| safeCreateIcons();
|
||
141|}
|
||
142|
|
||
143|async function focusVaultInSidebar(vaultName) {
|
||
144| switchSidebarTab("vaults");
|
||
145| const vaultItem = document.querySelector(`.vault-item[data-vault="${CSS.escape(vaultName)}"]`);
|
||
146| if (!vaultItem) return;
|
||
147| document.querySelectorAll(".vault-item.focused").forEach((el) => el.classList.remove("focused"));
|
||
148| vaultItem.classList.add("focused");
|
||
149| const childContainer = document.getElementById(`vault-children-${vaultName}`);
|
||
150| if (childContainer && childContainer.classList.contains("collapsed")) {
|
||
151| await toggleVault(vaultItem, vaultName, true);
|
||
152| }
|
||
153| scrollTreeItemIntoView(vaultItem, false);
|
||
154|}
|
||
155|
|
||
156|async function refreshTagsForContext() {
|
||
157| const vaultParam = state.selectedContextVault === "all" ? "" : `?vault=${encodeURIComponent(state.selectedContextVault)}`;
|
||
158| const data = await api(`/api/tags${vaultParam}`);
|
||
159| const filteredTags = TagFilterService.filterTags(data.tags);
|
||
160| renderTagCloud(filteredTags);
|
||
161|}
|
||
162|
|
||
163|// ---------------------------------------------------------------------------
|
||
164|// Helper: Check if path should be displayed based on hideHiddenFiles setting
|
||
165|// ---------------------------------------------------------------------------
|
||
166|function shouldDisplayPath(path, vaultName) {
|
||
167| // Get hideHiddenFiles setting for this vault (default: false = show all)
|
||
168| const settings = state.vaultSettings[vaultName] || { hideHiddenFiles: false };
|
||
169|
|
||
170| if (!settings.hideHiddenFiles) {
|
||
171| // Show all files
|
||
172| return true;
|
||
173| }
|
||
174|
|
||
175| // Check if any segment of the path starts with a dot (hidden)
|
||
176| const segments = path.split("/").filter(Boolean);
|
||
177| for (const segment of segments) {
|
||
178| if (segment.startsWith(".")) {
|
||
179| return false; // Hide this path
|
||
180| }
|
||
181| }
|
||
182|
|
||
183| return true; // Show this path
|
||
184|}
|
||
185|
|
||
186|async function loadVaultSettings() {
|
||
187| try {
|
||
188| const settings = await api("/api/vaults/settings/all");
|
||
189| state.vaultSettings = settings;
|
||
190| } catch (err) {
|
||
191| console.error("Failed to load vault settings:", err);
|
||
192| state.vaultSettings = {};
|
||
193| }
|
||
194|}
|
||
195|
|
||
196|// ---------------------------------------------------------------------------
|
||
197|// Sidebar — Vault tree
|
||
198|// ---------------------------------------------------------------------------
|
||
199|async function loadVaults() {
|
||
200| const vaults = await api("/api/vaults");
|
||
201| state.allVaults = vaults;
|
||
202| const container = document.getElementById("vault-tree");
|
||
203| container.innerHTML = "";
|
||
204|
|
||
205| // Prepare dropdown options
|
||
206| const dropdownOptions = [{ value: "all", text: "Tous les vaults" }, ...vaults.map((v) => ({ value: v.name, text: v.name }))];
|
||
207|
|
||
208| // Populate custom dropdowns
|
||
209| populateCustomDropdown("vault-filter-dropdown", dropdownOptions, "all");
|
||
210| populateCustomDropdown("vault-quick-select-dropdown", dropdownOptions, "all");
|
||
211|
|
||
212| // Populate standard selects
|
||
213| _populateRecentVaultFilter();
|
||
214| if (typeof DashboardRecentWidget !== "undefined" && DashboardRecentWidget.populateVaultFilter) {
|
||
215| DashboardRecentWidget.populateVaultFilter();
|
||
216| }
|
||
217|
|
||
218| vaults.forEach((v) => {
|
||
219| // Sidebar tree entry
|
||
220| const vaultItem = el("div", { class: "tree-item vault-item", "data-vault": v.name }, [icon("chevron-right", 14), getVaultIcon(v.name, 16), el("span", { class: "tree-item-text" }, [document.createTextNode(v.name)]), smallBadge(v.file_count)]);
|
||
221| vaultItem.addEventListener("click", () => toggleVault(vaultItem, v.name));
|
||
222|
|
||
223| vaultItem.addEventListener("contextmenu", (e) => {
|
||
224| e.preventDefault();
|
||
225| const isReadonly = false;
|
||
226| ContextMenuManager.show(e.clientX, e.clientY, v.name, '', 'vault', isReadonly);
|
||
227| });
|
||
228| attachTreeItemActionButton(vaultItem, v.name, "", "vault", false);
|
||
229| attachTreeItemLongPress(vaultItem, () => ({ vault: v.name, path: "", type: "vault", isReadonly: false }));
|
||
230|
|
||
231| container.appendChild(vaultItem);
|
||
232|
|
||
233| const childContainer = el("div", { class: "tree-children collapsed", id: `vault-children-${v.name}` });
|
||
234| container.appendChild(childContainer);
|
||
235| });
|
||
236|
|
||
237| syncVaultSelectors();
|
||
238| safeCreateIcons();
|
||
239|}
|
||
240|
|
||
241|/**
|
||
242| * Refreshes the sidebar tree while preserving the expanded state of vaults and folders.
|
||
243| * Optimized to avoid a full sidebar wipe and minimize visible loading states.
|
||
244| */
|
||
245|/**
|
||
246| * Incrementally update a directory container without wiping existing DOM.
|
||
247| * Only adds new items, removes deleted ones, and updates changed ones.
|
||
248| */
|
||
249|async function incrementalLoadDirectory(vaultName, dirPath, container) {
|
||
250| let data;
|
||
251| try {
|
||
252| const url = `/api/browse/${encodeURIComponent(vaultName)}?path=${encodeURIComponent(dirPath)}`;
|
||
253| data = await api(url);
|
||
254| } catch (err) {
|
||
255| // Server unavailable — keep existing content
|
||
256| return;
|
||
257| }
|
||
258|
|
||
259| // Build a map of existing DOM elements by path
|
||
260| const existingItems = {};
|
||
261| const existingChildren = {}; // path -> child container (for directories)
|
||
262| for (let i = 0; i < container.children.length; i++) {
|
||
263| const child = container.children[i];
|
||
264| if (child.classList.contains("tree-item") && child.dataset.path) {
|
||
265| existingItems[child.dataset.path] = child;
|
||
266| // The next sibling should be the tree-children container for this directory
|
||
267| if (i + 1 < container.children.length) {
|
||
268| const next = container.children[i + 1];
|
||
269| if (next.classList.contains("tree-children")) {
|
||
270| existingChildren[child.dataset.path] = next;
|
||
271| }
|
||
272| }
|
||
273| }
|
||
274| }
|
||
275|
|
||
276| const fragment = document.createDocumentFragment();
|
||
277|
|
||
278| data.items.forEach((item) => {
|
||
279| if (!shouldDisplayPath(item.path, vaultName)) return;
|
||
280|
|
||
281| const existing = existingItems[item.path];
|
||
282|
|
||
283| if (existing) {
|
||
284| // Item already exists — reuse it, but update text/badge if needed
|
||
285| const textEl = existing.querySelector(".tree-item-text");
|
||
286| const displayName = item.type === "file" && item.name.match(/\.md$/i)
|
||
287| ? item.name.replace(/\.md$/i, "")
|
||
288| : item.name;
|
||
289| if (textEl && textEl.textContent !== displayName) {
|
||
290| textEl.textContent = displayName;
|
||
291| }
|
||
292| // Update badge for directories
|
||
293| if (item.type === "directory") {
|
||
294| const badge = existing.querySelector(".badge-small");
|
||
295| const newBadge = `(${item.children_count})`;
|
||
296| if (badge && badge.textContent !== newBadge) {
|
||
297| badge.textContent = newBadge;
|
||
298| } else if (!badge) {
|
||
299| existing.appendChild(smallBadge(item.children_count));
|
||
300| }
|
||
301| }
|
||
302| fragment.appendChild(existing);
|
||
303| // Also re-add the child container for directories
|
||
304| if (item.type === "directory" && existingChildren[item.path]) {
|
||
305| fragment.appendChild(existingChildren[item.path]);
|
||
306| } else if (item.type === "directory") {
|
||
307| // Directory existed but no child container — create one
|
||
308| const subContainer = el("div", { class: "tree-children collapsed", id: `dir-${vaultName}-${item.path}` });
|
||
309| fragment.appendChild(subContainer);
|
||
310| }
|
||
311| } else {
|
||
312| // New item — create it
|
||
313| if (item.type === "directory") {
|
||
314| const dirItem = el("div", { class: "tree-item", "data-vault": vaultName, "data-path": item.path }, [icon("chevron-right", 14), icon("folder", 16), el("span", { class: "tree-item-text" }, [document.createTextNode(item.name)]), smallBadge(item.children_count)]);
|
||
315| attachTreeItemActionButton(dirItem, vaultName, item.path, "directory", false);
|
||
316| attachTreeItemLongPress(dirItem, () => ({ vault: vaultName, path: item.path, type: "directory", isReadonly: false }));
|
||
317| fragment.appendChild(dirItem);
|
||
318|
|
||
319| const subContainer = el("div", { class: "tree-children collapsed", id: `dir-${vaultName}-${item.path}` });
|
||
320| fragment.appendChild(subContainer);
|
||
321|
|
||
322| dirItem.addEventListener("click", async () => {
|
||
323| scrollTreeItemIntoView(dirItem, false);
|
||
324| if (subContainer.classList.contains("collapsed")) {
|
||
325| if (subContainer.children.length === 0) {
|
||
326| await loadDirectory(vaultName, item.path, subContainer);
|
||
327| }
|
||
328| subContainer.classList.remove("collapsed");
|
||
329| const chev = dirItem.querySelector("[data-lucide]");
|
||
330| if (chev) chev.setAttribute("data-lucide", "chevron-down");
|
||
331| safeCreateIcons();
|
||
332| } else {
|
||
333| subContainer.classList.add("collapsed");
|
||
334| const chev = dirItem.querySelector("[data-lucide]");
|
||
335| if (chev) chev.setAttribute("data-lucide", "chevron-right");
|
||
336| safeCreateIcons();
|
||
337| }
|
||
338| });
|
||
339|
|
||
340| dirItem.addEventListener("contextmenu", (e) => {
|
||
341| e.preventDefault();
|
||
342| ContextMenuManager.show(e.clientX, e.clientY, vaultName, item.path, "directory", false);
|
||
343| });
|
||
344| } else {
|
||
345| const fileIconName = getFileIcon(item.name);
|
||
346| const displayName = item.name.match(/\.md$/i) ? item.name.replace(/\.md$/i, "") : item.name;
|
||
347| const fileItem = el("div", { class: "tree-item", "data-vault": vaultName, "data-path": item.path }, [icon(fileIconName, 16), el("span", { class: "tree-item-text" }, [document.createTextNode(displayName)])]);
|
||
348| attachTreeItemActionButton(fileItem, vaultName, item.path, "file", false);
|
||
349| attachTreeItemLongPress(fileItem, () => ({ vault: vaultName, path: item.path, type: "file", isReadonly: false }));
|
||
350| fileItem.addEventListener("click", () => {
|
||
351| scrollTreeItemIntoView(fileItem, false);
|
||
352| TabManager.openPreview(vaultName, item.path);
|
||
353| closeMobileSidebar();
|
||
354| });
|
||
355|
|
||
356| fileItem.addEventListener("dblclick", (e) => {
|
||
357| e.preventDefault();
|
||
358| TabManager.openPersistent(vaultName, item.path);
|
||
359| });
|
||
360|
|
||
361| fileItem.addEventListener("contextmenu", (e) => {
|
||
362| e.preventDefault();
|
||
363| ContextMenuManager.show(e.clientX, e.clientY, vaultName, item.path, "file", false);
|
||
364| });
|
||
365|
|
||
366| fragment.appendChild(fileItem);
|
||
367| }
|
||
368| }
|
||
369| });
|
||
370|
|
||
371| // Replace container content in a single batch operation to avoid flash
|
||
372| container.textContent = "";
|
||
373| container.appendChild(fragment);
|
||
374|}
|
||
375|
|
||
376|async function refreshSidebarTreePreservingState() {
|
||
377| // 1. Capture expanded states
|
||
378| const expandedVaults = Array.from(document.querySelectorAll(".vault-item"))
|
||
379| .filter((v) => {
|
||
380| const children = document.getElementById(`vault-children-${v.dataset.vault}`);
|
||
381| return children && !children.classList.contains("collapsed");
|
||
382| })
|
||
383| .map((v) => v.dataset.vault);
|
||
384|
|
||
385| const expandedDirs = Array.from(document.querySelectorAll(".tree-item[data-path]"))
|
||
386| .filter((item) => {
|
||
387| const vault = item.dataset.vault;
|
||
388| const path = item.dataset.path;
|
||
389| const children = document.getElementById(`dir-${vault}-${path}`);
|
||
390| return children && !children.classList.contains("collapsed");
|
||
391| })
|
||
392| .map((item) => ({ vault: item.dataset.vault, path: item.dataset.path }));
|
||
393|
|
||
394| const selectedItem = document.querySelector(".tree-item.path-selected");
|
||
395| const selectedState = selectedItem ? { vault: selectedItem.dataset.vault, path: selectedItem.dataset.path } : null;
|
||
396|
|
||
397| // 2. Soft update: vault names/counts without wiping the tree
|
||
398| try {
|
||
399| const vaults = await api("/api/vaults");
|
||
400| state.allVaults = vaults;
|
||
401| vaults.forEach((v) => {
|
||
402| const vItem = document.querySelector(`.vault-item[data-vault="${CSS.escape(v.name)}"]`);
|
||
403| if (vItem) {
|
||
404| const badge = vItem.querySelector(".badge-small");
|
||
405| if (badge) badge.textContent = `(${v.file_count})`;
|
||
406| }
|
||
407| });
|
||
408| } catch (e) {
|
||
409| console.warn("Soft vault refresh failed, falling back to full reload", e);
|
||
410| await loadVaults();
|
||
411| return;
|
||
412| }
|
||
413|
|
||
414| // 3. Incrementally update expanded vaults (no DOM wipe)
|
||
415| for (const vName of expandedVaults) {
|
||
416| const container = document.getElementById(`vault-children-${vName}`);
|
||
417| if (container) {
|
||
418| await incrementalLoadDirectory(vName, "", container);
|
||
419| }
|
||
420| }
|
||
421|
|
||
422| // 4. Incrementally update expanded directories (parents first, no DOM wipe)
|
||
423| expandedDirs.sort((a, b) => a.path.split("/").length - b.path.split("/").length);
|
||
424| for (const dir of expandedDirs) {
|
||
425| const container = document.getElementById(`dir-${dir.vault}-${dir.path}`);
|
||
426| if (container) {
|
||
427| try {
|
||
428| await incrementalLoadDirectory(dir.vault, dir.path, container);
|
||
429| container.classList.remove("collapsed");
|
||
430| const dItem = document.querySelector(`.tree-item[data-vault="${CSS.escape(dir.vault)}"][data-path="${CSS.escape(dir.path)}"]`);
|
||
431| if (dItem) {
|
||
432| const chev = dItem.querySelector("[data-lucide]");
|
||
433| if (chev) chev.setAttribute("data-lucide", "chevron-down");
|
||
434| }
|
||
435| } catch (e) {
|
||
436| console.error(`Failed to refresh directory ${dir.vault}/${dir.path}`, e);
|
||
437| }
|
||
438| }
|
||
439| }
|
||
440|
|
||
441| // 5. Restore selection
|
||
442| if (selectedState) {
|
||
443| await focusPathInSidebar(selectedState.vault, selectedState.path, { alignToTop: false });
|
||
444| }
|
||
445|
|
||
446| safeCreateIcons();
|
||
447|}
|
||
448|
|
||
449|async function toggleVault(itemEl, vaultName, forceExpand) {
|
||
450| const childContainer = document.getElementById(`vault-children-${vaultName}`);
|
||
451| if (!childContainer) return;
|
||
452|
|
||
453| scrollTreeItemIntoView(itemEl, false);
|
||
454|
|
||
455| const shouldExpand = forceExpand || childContainer.classList.contains("collapsed");
|
||
456|
|
||
457| if (shouldExpand) {
|
||
458| // Expand — load children if empty
|
||
459| if (childContainer.children.length === 0) {
|
||
460| await loadDirectory(vaultName, "", childContainer);
|
||
461| }
|
||
462| childContainer.classList.remove("collapsed");
|
||
463| // Swap chevron
|
||
464| const chevron = itemEl.querySelector("[data-lucide]");
|
||
465| if (chevron) chevron.setAttribute("data-lucide", "chevron-down");
|
||
466| safeCreateIcons();
|
||
467| } else {
|
||
468| childContainer.classList.add("collapsed");
|
||
469| const chevron = itemEl.querySelector("[data-lucide]");
|
||
470| if (chevron) chevron.setAttribute("data-lucide", "chevron-right");
|
||
471| safeCreateIcons();
|
||
472| }
|
||
473|}
|
||
474|
|
||
475|async function expandDirectoryInSidebar(vaultName, dirPath, dirItem) {
|
||
476| const subContainer = document.getElementById(`dir-${vaultName}-${dirPath}`);
|
||
477| if (!subContainer) return null;
|
||
478|
|
||
479| if (subContainer.children.length === 0) {
|
||
480| await loadDirectory(vaultName, dirPath, subContainer);
|
||
481| }
|
||
482|
|
||
483| subContainer.classList.remove("collapsed");
|
||
484| if (dirItem) {
|
||
485| const chevron = dirItem.querySelector("[data-lucide]");
|
||
486| if (chevron) chevron.setAttribute("data-lucide", "chevron-down");
|
||
487| }
|
||
488| safeCreateIcons();
|
||
489| return subContainer;
|
||
490|}
|
||
491|
|
||
492|async function focusPathInSidebar(vaultName, targetPath, options) {
|
||
493| switchSidebarTab("vaults");
|
||
494|
|
||
495| const vaultItem = document.querySelector(`.vault-item[data-vault="${CSS.escape(vaultName)}"]`);
|
||
496| if (!vaultItem) return;
|
||
497|
|
||
498| document.querySelectorAll(".vault-item.focused").forEach((el) => el.classList.remove("focused"));
|
||
499| vaultItem.classList.add("focused");
|
||
500|
|
||
501| const vaultContainer = document.getElementById(`vault-children-${vaultName}`);
|
||
502| if (!vaultContainer) return;
|
||
503|
|
||
504| if (vaultContainer.classList.contains("collapsed")) {
|
||
505| await toggleVault(vaultItem, vaultName, true);
|
||
506| }
|
||
507|
|
||
508| if (!targetPath) {
|
||
509| // Clear any previous path selection
|
||
510| document.querySelectorAll(".tree-item.path-selected").forEach((el) => el.classList.remove("path-selected"));
|
||
511| scrollTreeItemIntoView(vaultItem, options && options.alignToTop);
|
||
512| return;
|
||
513| }
|
||
514|
|
||
515| const segments = targetPath.split("/").filter(Boolean);
|
||
516| let currentContainer = vaultContainer;
|
||
517| let cumulativePath = "";
|
||
518| let lastTargetItem = null;
|
||
519|
|
||
520| for (let index = 0; index < segments.length; index++) {
|
||
521| cumulativePath += (cumulativePath ? "/" : "") + segments[index];
|
||
522|
|
||
523| let targetItem = null;
|
||
524| try {
|
||
525| targetItem = currentContainer.querySelector(`.tree-item[data-vault="${CSS.escape(vaultName)}"][data-path="${CSS.escape(cumulativePath)}"]`);
|
||
526| } catch (e) {
|
||
527| targetItem = null;
|
||
528| }
|
||
529|
|
||
530| if (!targetItem) {
|
||
531| return;
|
||
532| }
|
||
533|
|
||
534| lastTargetItem = targetItem;
|
||
535|
|
||
536| const isLastSegment = index === segments.length - 1;
|
||
537| if (!isLastSegment) {
|
||
538| const nextContainer = await expandDirectoryInSidebar(vaultName, cumulativePath, targetItem);
|
||
539| if (nextContainer) {
|
||
540| currentContainer = nextContainer;
|
||
541| }
|
||
542| }
|
||
543| }
|
||
544|
|
||
545| if (lastTargetItem && options && options.expandTarget) {
|
||
546| await expandDirectoryInSidebar(vaultName, targetPath, lastTargetItem);
|
||
547| }
|
||
548|
|
||
549| // Clear previous path selections and highlight the final target
|
||
550| document.querySelectorAll(".tree-item.path-selected").forEach((el) => el.classList.remove("path-selected"));
|
||
551| if (lastTargetItem) {
|
||
552| lastTargetItem.classList.add("path-selected");
|
||
553| }
|
||
554|
|
||
555| scrollTreeItemIntoView(lastTargetItem, options && options.alignToTop);
|
||
556|}
|
||
557|
|
||
558|function getParentDirectoryPath(filePath) {
|
||
559| if (!filePath) return "";
|
||
560| const segments = filePath.split("/").filter(Boolean);
|
||
561| if (segments.length <= 1) return "";
|
||
562| segments.pop();
|
||
563| return segments.join("/");
|
||
564|}
|
||
565|
|
||
566|function syncActiveFileTreeItem(vaultName, filePath) {
|
||
567| document.querySelectorAll(".tree-item.active").forEach((el) => el.classList.remove("active"));
|
||
568| if (!vaultName || !filePath) return;
|
||
569| const selector = `.tree-item[data-vault="${CSS.escape(vaultName)}"][data-path="${CSS.escape(filePath)}"]`;
|
||
570| try {
|
||
571| const active = document.querySelector(selector);
|
||
572| if (active) active.classList.add("active");
|
||
573| } catch (e) {
|
||
574| /* selector might fail on special chars */
|
||
575| }
|
||
576|}
|
||
577|
|
||
578|async function loadDirectory(vaultName, dirPath, container) {
|
||
579| // Only show the loading spinner if the container is currently empty
|
||
580| const isEmpty = container.children.length === 0;
|
||
581| if (isEmpty) {
|
||
582| container.innerHTML = '<div class="tree-loading"><div class="loading-spinner" style="width:16px;height:16px;border-width:2px"></div></div>';
|
||
583| }
|
||
584|
|
||
585| var data;
|
||
586| try {
|
||
587| const url = `/api/browse/${encodeURIComponent(vaultName)}?path=${encodeURIComponent(dirPath)}`;
|
||
588| data = await api(url);
|
||
589| } catch (err) {
|
||
590| container.innerHTML = '<div class="tree-loading" style="color:var(--text-muted);font-size:0.75rem;padding:4px 16px">Erreur de chargement</div>';
|
||
591| return;
|
||
592| }
|
||
593| container.innerHTML = "";
|
||
594|
|
||
595| const fragment = document.createDocumentFragment();
|
||
596|
|
||
597| data.items.forEach((item) => {
|
||
598| // Apply client-side filtering for hidden files
|
||
599| if (!shouldDisplayPath(item.path, vaultName)) {
|
||
600| return; // Skip this item
|
||
601| }
|
||
602|
|
||
603| if (item.type === "directory") {
|
||
604| const dirItem = el("div", { class: "tree-item", "data-vault": vaultName, "data-path": item.path }, [icon("chevron-right", 14), icon("folder", 16), el("span", { class: "tree-item-text" }, [document.createTextNode(item.name)]), smallBadge(item.children_count)]);
|
||
605| attachTreeItemActionButton(dirItem, vaultName, item.path, "directory", false);
|
||
606| attachTreeItemLongPress(dirItem, () => ({ vault: vaultName, path: item.path, type: "directory", isReadonly: false }));
|
||
607| fragment.appendChild(dirItem);
|
||
608|
|
||
609| const subContainer = el("div", { class: "tree-children collapsed", id: `dir-${vaultName}-${item.path}` });
|
||
610| fragment.appendChild(subContainer);
|
||
611|
|
||
612| dirItem.addEventListener("click", async () => {
|
||
613| scrollTreeItemIntoView(dirItem, false);
|
||
614| if (subContainer.classList.contains("collapsed")) {
|
||
615| if (subContainer.children.length === 0) {
|
||
616| await loadDirectory(vaultName, item.path, subContainer);
|
||
617| }
|
||
618| subContainer.classList.remove("collapsed");
|
||
619| const chev = dirItem.querySelector("[data-lucide]");
|
||
620| if (chev) chev.setAttribute("data-lucide", "chevron-down");
|
||
621| safeCreateIcons();
|
||
622| } else {
|
||
623| subContainer.classList.add("collapsed");
|
||
624| const chev = dirItem.querySelector("[data-lucide]");
|
||
625| if (chev) chev.setAttribute("data-lucide", "chevron-right");
|
||
626| safeCreateIcons();
|
||
627| }
|
||
628| });
|
||
629|
|
||
630| dirItem.addEventListener("contextmenu", (e) => {
|
||
631| e.preventDefault();
|
||
632| const isReadonly = false;
|
||
633| ContextMenuManager.show(e.clientX, e.clientY, vaultName, item.path, 'directory', isReadonly);
|
||
634| });
|
||
635| } else {
|
||
636| const fileIconName = getFileIcon(item.name);
|
||
637| const displayName = item.name.match(/\.md$/i) ? item.name.replace(/\.md$/i, "") : item.name;
|
||
638| const fileItem = el("div", { class: "tree-item", "data-vault": vaultName, "data-path": item.path }, [icon(fileIconName, 16), el("span", { class: "tree-item-text" }, [document.createTextNode(displayName)])]);
|
||
639| attachTreeItemActionButton(fileItem, vaultName, item.path, "file", false);
|
||
640| attachTreeItemLongPress(fileItem, () => ({ vault: vaultName, path: item.path, type: "file", isReadonly: false }));
|
||
641| fileItem.addEventListener("click", () => {
|
||
642| scrollTreeItemIntoView(fileItem, false);
|
||
643| TabManager.openPreview(vaultName, item.path);
|
||
644| closeMobileSidebar();
|
||
645| });
|
||
646|
|
||
647| fileItem.addEventListener("dblclick", (e) => {
|
||
648| e.preventDefault();
|
||
649| TabManager.openPersistent(vaultName, item.path);
|
||
650| });
|
||
651|
|
||
652| fileItem.addEventListener("contextmenu", (e) => {
|
||
653| e.preventDefault();
|
||
654| const isReadonly = false;
|
||
655| ContextMenuManager.show(e.clientX, e.clientY, vaultName, item.path, 'file', isReadonly);
|
||
656| });
|
||
657|
|
||
658| fragment.appendChild(fileItem);
|
||
659| }
|
||
660| });
|
||
661|
|
||
662| container.appendChild(fragment);
|
||
663| safeCreateIcons();
|
||
664|}
|
||
665|
|
||
666|// ---------------------------------------------------------------------------
|
||
667|// Sidebar filter
|
||
668|// ---------------------------------------------------------------------------
|
||
669|function initSidebarFilter() {
|
||
670| const input = document.getElementById("sidebar-filter-input");
|
||
671| const caseBtn = document.getElementById("sidebar-filter-case-btn");
|
||
672| const clearBtn = document.getElementById("sidebar-filter-clear-btn");
|
||
673|
|
||
674| input.addEventListener("input", () => {
|
||
675| const hasText = input.value.length > 0;
|
||
676| clearBtn.style.display = hasText ? "flex" : "none";
|
||
677| clearTimeout(state.filterDebounce);
|
||
678| state.filterDebounce = setTimeout(async () => {
|
||
679| const q = sidebarFilterCaseSensitive ? input.value.trim() : input.value.trim().toLowerCase();
|
||
680| if (hasText) {
|
||
681| if (state.activeSidebarTab === "vaults") {
|
||
682| await performTreeSearch(q);
|
||
683| } else {
|
||
684| filterTagCloud(q);
|
||
685| }
|
||
686| } else {
|
||
687| if (state.activeSidebarTab === "vaults") {
|
||
688| await restoreSidebarTree();
|
||
689| } else {
|
||
690| filterTagCloud("");
|
||
691| }
|
||
692| }
|
||
693| }, 220);
|
||
694| });
|
||
695|
|
||
696| caseBtn.addEventListener("click", async () => {
|
||
697| state.sidebarFilterCaseSensitive = !state.sidebarFilterCaseSensitive;
|
||
698| caseBtn.classList.toggle("active");
|
||
699| const q = sidebarFilterCaseSensitive ? input.value.trim() : input.value.trim().toLowerCase();
|
||
700| if (input.value.trim()) {
|
||
701| if (state.activeSidebarTab === "vaults") {
|
||
702| await performTreeSearch(q);
|
||
703| } else {
|
||
704| filterTagCloud(q);
|
||
705| }
|
||
706| }
|
||
707| });
|
||
708|
|
||
709| clearBtn.addEventListener("click", async () => {
|
||
710| input.value = "";
|
||
711| clearBtn.style.display = "none";
|
||
712| state.sidebarFilterCaseSensitive = false;
|
||
713| caseBtn.classList.remove("active");
|
||
714| clearTimeout(state.filterDebounce);
|
||
715| if (state.activeSidebarTab === "vaults") {
|
||
716| await restoreSidebarTree();
|
||
717| } else {
|
||
718| filterTagCloud("");
|
||
719| }
|
||
720| });
|
||
721|
|
||
722| clearBtn.style.display = "none";
|
||
723|}
|
||
724|
|
||
725|async function performTreeSearch(query) {
|
||
726| if (!query) {
|
||
727| await restoreSidebarTree();
|
||
728| return;
|
||
729| }
|
||
730|
|
||
731| try {
|
||
732| const vaultParam = state.selectedContextVault === "all" ? "all" : state.selectedContextVault;
|
||
733| const url = `/api/tree-search?q=${encodeURIComponent(query)}&vault=${encodeURIComponent(vaultParam)}`;
|
||
734| const data = await api(url);
|
||
735| renderFilteredSidebarResults(query, data.results);
|
||
736| } catch (err) {
|
||
737| console.error("Tree search error:", err);
|
||
738| renderFilteredSidebarResults(query, []);
|
||
739| }
|
||
740|}
|
||
741|
|
||
742|async function restoreSidebarTree() {
|
||
743| await refreshSidebarForContext();
|
||
744| if (state.currentVault) {
|
||
745| focusPathInSidebar(state.currentVault, currentPath || "", { alignToTop: false }).catch(() => {});
|
||
746| }
|
||
747|}
|
||
748|
|
||
749|function renderFilteredSidebarResults(query, results) {
|
||
750| const container = document.getElementById("vault-tree");
|
||
751| container.innerHTML = "";
|
||
752|
|
||
753| const grouped = new Map();
|
||
754| results.forEach((result) => {
|
||
755| if (!grouped.has(result.vault)) {
|
||
756| grouped.set(result.vault, []);
|
||
757| }
|
||
758| grouped.get(result.vault).push(result);
|
||
759| });
|
||
760|
|
||
761| if (grouped.size === 0) {
|
||
762| container.appendChild(el("div", { class: "sidebar-filter-empty" }, [document.createTextNode("Aucun répertoire ou fichier correspondant.")]));
|
||
763| return;
|
||
764| }
|
||
765|
|
||
766| grouped.forEach((entries, vaultName) => {
|
||
767| entries.sort((a, b) => a.path.localeCompare(b.path, undefined, { sensitivity: "base" }));
|
||
768|
|
||
769| const vaultHeader = el("div", { class: "tree-item vault-item filter-results-header", "data-vault": vaultName }, [getVaultIcon(vaultName, 16), document.createTextNode(` ${vaultName} `), smallBadge(entries.length)]);
|
||
770| container.appendChild(vaultHeader);
|
||
771|
|
||
772| const resultsWrapper = el("div", { class: "filter-results-group" });
|
||
773| entries.forEach((entry) => {
|
||
774| const resultItem = el(
|
||
775| "div",
|
||
776| {
|
||
777| class: `tree-item filter-result-item filter-result-${entry.type}`,
|
||
778| "data-vault": entry.vault,
|
||
779| "data-path": entry.path,
|
||
780| "data-type": entry.type,
|
||
781| },
|
||
782| [icon(entry.type === "directory" ? "folder" : getFileIcon(entry.name), 16)],
|
||
783| );
|
||
784|
|
||
785| const textWrap = el("div", { class: "filter-result-text" });
|
||
786| const primary = el("div", { class: "filter-result-primary" });
|
||
787| appendHighlightedText(primary, entry.name, query, state.sidebarFilterCaseSensitive);
|
||
788| const secondary = el("div", { class: "filter-result-secondary" });
|
||
789| appendHighlightedText(secondary, entry.path, query, state.sidebarFilterCaseSensitive);
|
||
790| textWrap.appendChild(primary);
|
||
791| textWrap.appendChild(secondary);
|
||
792| resultItem.appendChild(textWrap);
|
||
793|
|
||
794| resultItem.addEventListener("click", async () => {
|
||
795| const input = document.getElementById("sidebar-filter-input");
|
||
796| const clearBtn = document.getElementById("sidebar-filter-clear-btn");
|
||
797| if (input) input.value = "";
|
||
798| if (clearBtn) clearBtn.style.display = "none";
|
||
799| await restoreSidebarTree();
|
||
800| if (entry.type === "directory") {
|
||
801| await focusPathInSidebar(entry.vault, entry.path, { alignToTop: true, expandTarget: true });
|
||
802| } else {
|
||
803| await TabManager.openPreview(entry.vault, entry.path);
|
||
804| await focusPathInSidebar(entry.vault, getParentDirectoryPath(entry.path), { alignToTop: true, expandTarget: true });
|
||
805| syncActiveFileTreeItem(entry.vault, entry.path);
|
||
806| }
|
||
807| closeMobileSidebar();
|
||
808| });
|
||
809|
|
||
810| resultsWrapper.appendChild(resultItem);
|
||
811| });
|
||
812|
|
||
813| container.appendChild(resultsWrapper);
|
||
814| });
|
||
815|
|
||
816| flushIcons();
|
||
817|}
|
||
818|
|
||
819|function filterSidebarTree(query) {
|
||
820| const tree = document.getElementById("vault-tree");
|
||
821| const items = tree.querySelectorAll(".tree-item");
|
||
822| const containers = tree.querySelectorAll(".tree-children");
|
||
823|
|
||
824| if (!query) {
|
||
825| items.forEach((item) => item.classList.remove("filtered-out"));
|
||
826| containers.forEach((c) => {
|
||
827| c.classList.remove("filtered-out");
|
||
828| // Keep current collapsed state when clearing filter
|
||
829| });
|
||
830| return;
|
||
831| }
|
||
832|
|
||
833| // First pass: mark all as filtered out
|
||
834| items.forEach((item) => item.classList.add("filtered-out"));
|
||
835| containers.forEach((c) => c.classList.add("filtered-out"));
|
||
836|
|
||
837| // Second pass: find matching items and mark them + ancestors + descendants
|
||
838| const matchingItems = new Set();
|
||
839|
|
||
840| items.forEach((item) => {
|
||
841| const text = sidebarFilterCaseSensitive ? item.textContent : item.textContent.toLowerCase();
|
||
842| const searchQuery = sidebarFilterCaseSensitive ? query : query.toLowerCase();
|
||
843| if (text.includes(searchQuery)) {
|
||
844| matchingItems.add(item);
|
||
845| item.classList.remove("filtered-out");
|
||
846|
|
||
847| // Show all ancestor containers
|
||
848| let parent = item.parentElement;
|
||
849| while (parent && parent !== tree) {
|
||
850| parent.classList.remove("filtered-out");
|
||
851| if (parent.classList.contains("tree-children")) {
|
||
852| parent.classList.remove("collapsed");
|
||
853| }
|
||
854| parent = parent.parentElement;
|
||
855| }
|
||
856|
|
||
857| // If this is a directory (has a children container after it), show all descendants
|
||
858| const nextEl = item.nextElementSibling;
|
||
859| if (nextEl && nextEl.classList.contains("tree-children")) {
|
||
860| nextEl.classList.remove("filtered-out");
|
||
861| nextEl.classList.remove("collapsed");
|
||
862| // Recursively show all children in this container
|
||
863| showAllDescendants(nextEl);
|
||
864| }
|
||
865| }
|
||
866| });
|
||
867|
|
||
868| // Third pass: show items that are descendants of matching directories
|
||
869| // and ensure their containers are visible
|
||
870| matchingItems.forEach((item) => {
|
||
871| const nextEl = item.nextElementSibling;
|
||
872| if (nextEl && nextEl.classList.contains("tree-children")) {
|
||
873| const children = nextEl.querySelectorAll(".tree-item");
|
||
874| children.forEach((child) => child.classList.remove("filtered-out"));
|
||
875| }
|
||
876| });
|
||
877|}
|
||
878|
|
||
879|function showAllDescendants(container) {
|
||
880| const items = container.querySelectorAll(".tree-item");
|
||
881| items.forEach((item) => {
|
||
882| item.classList.remove("filtered-out");
|
||
883| // If this item has children, also show them
|
||
884| const nextEl = item.nextElementSibling;
|
||
885| if (nextEl && nextEl.classList.contains("tree-children")) {
|
||
886| nextEl.classList.remove("filtered-out");
|
||
887| nextEl.classList.remove("collapsed");
|
||
888| }
|
||
889| });
|
||
890| // Also ensure all nested containers are visible
|
||
891| const nestedContainers = container.querySelectorAll(".tree-children");
|
||
892| nestedContainers.forEach((c) => {
|
||
893| c.classList.remove("filtered-out");
|
||
894| c.classList.remove("collapsed");
|
||
895| });
|
||
896|}
|
||
897|
|
||
898|function filterTagCloud(query) {
|
||
899| const tags = document.querySelectorAll("#tag-cloud .tag-item");
|
||
900| tags.forEach((tag) => {
|
||
901| const text = sidebarFilterCaseSensitive ? tag.textContent : tag.textContent.toLowerCase();
|
||
902| const searchQuery = sidebarFilterCaseSensitive ? query : query.toLowerCase();
|
||
903| if (!query || text.includes(searchQuery)) {
|
||
904| tag.classList.remove("filtered-out");
|
||
905| } else {
|
||
906| tag.classList.add("filtered-out");
|
||
907| }
|
||
908| });
|
||
909|}
|
||
910|
|
||
911|// ---------------------------------------------------------------------------
|
||
912|// Tag Filter Service
|
||
913|// ---------------------------------------------------------------------------
|
||
914|const TagFilterService = {
|
||
915| defaultFilters: [
|
||
916| { pattern: "#<% ... %>", regex: "#<%.*%>", enabled: true },
|
||
917| { pattern: "#{{ ... }}", regex: "#\\{\\{.*\\}\\}", enabled: true },
|
||
918| { pattern: "#{ ... }", regex: "#\\{.*\\}", enabled: true },
|
||
919| ],
|
||
920|
|
||
921| getConfig() {
|
||
922| const stored = localStorage.getItem("obsigate-tag-filters");
|
||
923| if (stored) {
|
||
924| try {
|
||
925| return JSON.parse(stored);
|
||
926| } catch (e) {
|
||
927| return { tagFilters: this.defaultFilters };
|
||
928| }
|
||
929| }
|
||
930| return { tagFilters: this.defaultFilters };
|
||
931| },
|
||
932|
|
||
933| saveConfig(config) {
|
||
934| localStorage.setItem("obsigate-tag-filters", JSON.stringify(config));
|
||
935| },
|
||
936|
|
||
937| patternToRegex(pattern) {
|
||
938| // 1. Escape ALL special regex characters
|
||
939| // We use a broader set including * and .
|
||
940| let regex = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||
941|
|
||
942| // 2. Convert escaped '*' to '.*' (wildcard)
|
||
943| regex = regex.replace(/\\\*/g, ".*");
|
||
944|
|
||
945| // 3. Convert escaped '...' (or any sequence of 2+ dots like ..) to '.*'
|
||
946| // We also handle optional whitespace around it to make it more user-friendly
|
||
947| regex = regex.replace(/\s*\\\.{2,}\s*/g, ".*");
|
||
948|
|
||
949| return regex;
|
||
950| },
|
||
951|
|
||
952| isTagFiltered(tag) {
|
||
953| const config = this.getConfig();
|
||
954| const filters = config.tagFilters || this.defaultFilters;
|
||
955| const tagWithHash = `#${tag}`;
|
||
956|
|
||
957| for (const filter of filters) {
|
||
958| if (!filter.enabled) continue;
|
||
959| try {
|
||
960| // Robustly handle regex with or without ^/$
|
||
961| let patternStr = filter.regex;
|
||
962| if (!patternStr.startsWith("^")) patternStr = "^" + patternStr;
|
||
963| if (!patternStr.endsWith("$")) patternStr = patternStr + "$";
|
||
964|
|
||
965| const regex = new RegExp(patternStr);
|
||
966| if (regex.test(tagWithHash)) {
|
||
967| return true;
|
||
968| }
|
||
969| } catch (e) {
|
||
970| console.warn("Invalid regex:", filter.regex, e);
|
||
971| }
|
||
972| }
|
||
973| return false;
|
||
974| },
|
||
975|
|
||
976| filterTags(tags) {
|
||
977| const filtered = {};
|
||
978| Object.entries(tags).forEach(([tag, count]) => {
|
||
979| if (!this.isTagFiltered(tag)) {
|
||
980| filtered[tag] = count;
|
||
981| }
|
||
982| });
|
||
983| return filtered;
|
||
984| },
|
||
985|};
|
||
986|
|
||
987|// ---------------------------------------------------------------------------
|
||
988|// Tags
|
||
989|// ---------------------------------------------------------------------------
|
||
990|async function loadTags() {
|
||
991| const data = await api("/api/tags");
|
||
992| const filteredTags = TagFilterService.filterTags(data.tags);
|
||
993| renderTagCloud(filteredTags);
|
||
994|}
|
||
995|
|
||
996|function renderTagCloud(tags) {
|
||
997| const cloud = document.getElementById("tag-cloud");
|
||
998| cloud.innerHTML = "";
|
||
999|
|
||
1000| const counts = Object.values(tags);
|
||
1001| if (counts.length === 0) return;
|
||
1002|
|
||
1003| const maxCount = Math.max(...counts);
|
||
1004| const minSize = 0.7;
|
||
1005| const maxSize = 1.25;
|
||
1006|
|
||
1007| Object.entries(tags).forEach(([tag, count]) => {
|
||
1008| const ratio = maxCount > 1 ? (count - 1) / (maxCount - 1) : 0;
|
||
1009| const size = minSize + ratio * (maxSize - minSize);
|
||
1010| const tagEl = el("span", { class: "tag-item", style: `font-size:${size}rem` }, [document.createTextNode(`#${tag}`)]);
|
||
1011| tagEl.addEventListener("click", () => searchByTag(tag));
|
||
1012| cloud.appendChild(tagEl);
|
||
1013| });
|
||
1014|}
|
||
1015|
|
||
1016|function addTagFilter(tag) {
|
||
1017| if (!state.selectedTags.includes(tag)) {
|
||
1018| state.selectedTags.push(tag);
|
||
1019| performTagSearch();
|
||
1020| }
|
||
1021|}
|
||
1022|
|
||
1023|function removeTagFilter(tag) {
|
||
1024| state.selectedTags = state.selectedTags.filter((t) => t !== tag);
|
||
1025| if (state.selectedTags.length > 0) {
|
||
1026| performTagSearch();
|
||
1027| } else {
|
||
1028| const input = document.getElementById("search-input");
|
||
1029| if (input.value.trim()) {
|
||
1030| performAdvancedSearch(input.value.trim(), document.getElementById("vault-filter").value, null);
|
||
1031| } else {
|
||
1032| showWelcome();
|
||
1033| }
|
||
1034| }
|
||
1035|}
|
||
1036|
|
||
1037|function performTagSearch() {
|
||
1038| const input = document.getElementById("search-input");
|
||
1039| const query = input.value.trim();
|
||
1040| const vault = document.getElementById("vault-filter").value;
|
||
1041| performAdvancedSearch(query, vault, state.selectedTags.length > 0 ? state.selectedTags.join(",") : null);
|
||
1042|}
|
||
1043|
|
||
1044|function buildSearchResultsHeader(data, query, tagFilter) {
|
||
1045| const header = el("div", { class: "search-results-header" });
|
||
1046| const summaryText = el("span", { class: "search-results-summary-text" });
|
||
1047|
|
||
1048| if (query && tagFilter) {
|
||
1049| summaryText.textContent = `${data.count} résultat(s) pour "${query}" avec les tags`;
|
||
1050| } else if (query) {
|
||
1051| summaryText.textContent = `${data.count} résultat(s) pour "${query}"`;
|
||
1052| } else if (tagFilter) {
|
||
1053| summaryText.textContent = `${data.count} fichier(s) avec les tags`;
|
||
1054| } else {
|
||
1055| summaryText.textContent = `${data.count} résultat(s)`;
|
||
1056| }
|
||
1057|
|
||
1058| header.appendChild(summaryText);
|
||
1059|
|
||
1060| if (state.selectedTags.length > 0) {
|
||
1061| const activeTags = el("div", { class: "search-results-active-tags" });
|
||
1062| state.selectedTags.forEach((tag) => {
|
||
1063| const removeBtn = el(
|
||
1064| "button",
|
||
1065| {
|
||
1066| class: "search-results-active-tag-remove",
|
||
1067| title: `Retirer ${tag} du filtre`,
|
||
1068| "aria-label": `Retirer ${tag} du filtre`,
|
||
1069| },
|
||
1070| [document.createTextNode("×")],
|
||
1071| );
|
||
1072| removeBtn.addEventListener("click", (e) => {
|
||
1073| e.stopPropagation();
|
||
1074| removeTagFilter(tag);
|
||
1075| });
|
||
1076|
|
||
1077| const chip = el("span", { class: "search-results-active-tag" }, [document.createTextNode(`#${tag}`), removeBtn]);
|
||
1078| activeTags.appendChild(chip);
|
||
1079| });
|
||
1080| header.appendChild(activeTags);
|
||
1081| }
|
||
1082|
|
||
1083| return header;
|
||
1084|}
|
||
1085|
|
||
1086|function searchByTag(tag) {
|
||
1087| addTagFilter(tag);
|
||
1088|}
|
||
1089|
|
||
1090|
|
||
1091|export { initVaultContext, setSelectedVaultContext, syncVaultSelectors, shouldDisplayPath, loadVaults, initSidebarFilter, TagFilterService, loadTags, scrollTreeItemIntoView, refreshSidebarForContext, focusVaultInSidebar, refreshTagsForContext };
|
||
1092| |