Add tag filtering system with configurable pattern-based filters and persistent localStorage settings
This commit is contained in:
+222
-12
@@ -427,7 +427,8 @@
|
||||
async function refreshTagsForContext() {
|
||||
const vaultParam = selectedContextVault === "all" ? "" : `?vault=${encodeURIComponent(selectedContextVault)}`;
|
||||
const data = await api(`/api/tags${vaultParam}`);
|
||||
renderTagCloud(data.tags);
|
||||
const filteredTags = TagFilterService.filterTags(data.tags);
|
||||
renderTagCloud(filteredTags);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -673,12 +674,73 @@
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tag Filter Service
|
||||
// ---------------------------------------------------------------------------
|
||||
const TagFilterService = {
|
||||
defaultFilters: [
|
||||
{ pattern: "#<% ... %>", regex: "^#<%.*%>$", enabled: true },
|
||||
{ pattern: "#{{ ... }}", regex: "^#\\{\\{.*\\}\\}$", enabled: true }
|
||||
],
|
||||
|
||||
getConfig() {
|
||||
const stored = localStorage.getItem("obsigate-tag-filters");
|
||||
if (stored) {
|
||||
try {
|
||||
return JSON.parse(stored);
|
||||
} catch (e) {
|
||||
return { tagFilters: this.defaultFilters };
|
||||
}
|
||||
}
|
||||
return { tagFilters: this.defaultFilters };
|
||||
},
|
||||
|
||||
saveConfig(config) {
|
||||
localStorage.setItem("obsigate-tag-filters", JSON.stringify(config));
|
||||
},
|
||||
|
||||
patternToRegex(pattern) {
|
||||
let regex = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
|
||||
regex = regex.replace(/\\\.\\\.\\\./g, '.*');
|
||||
return regex;
|
||||
},
|
||||
|
||||
isTagFiltered(tag) {
|
||||
const config = this.getConfig();
|
||||
const filters = config.tagFilters || this.defaultFilters;
|
||||
|
||||
for (const filter of filters) {
|
||||
if (!filter.enabled) continue;
|
||||
try {
|
||||
const regex = new RegExp(`^${filter.regex}$`);
|
||||
if (regex.test(`#${tag}`)) {
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Invalid regex:", filter.regex, e);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
filterTags(tags) {
|
||||
const filtered = {};
|
||||
Object.entries(tags).forEach(([tag, count]) => {
|
||||
if (!this.isTagFiltered(tag)) {
|
||||
filtered[tag] = count;
|
||||
}
|
||||
});
|
||||
return filtered;
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tags
|
||||
// ---------------------------------------------------------------------------
|
||||
async function loadTags() {
|
||||
const data = await api("/api/tags");
|
||||
renderTagCloud(data.tags);
|
||||
const filteredTags = TagFilterService.filterTags(data.tags);
|
||||
renderTagCloud(filteredTags);
|
||||
}
|
||||
|
||||
function renderTagCloud(tags) {
|
||||
@@ -828,9 +890,11 @@
|
||||
// Tags
|
||||
const tagsDiv = el("div", { class: "file-tags" });
|
||||
(data.tags || []).forEach((tag) => {
|
||||
const t = el("span", { class: "file-tag" }, [document.createTextNode(`#${tag}`)]);
|
||||
t.addEventListener("click", () => searchByTag(tag));
|
||||
tagsDiv.appendChild(t);
|
||||
if (!TagFilterService.isTagFiltered(tag)) {
|
||||
const t = el("span", { class: "file-tag" }, [document.createTextNode(`#${tag}`)]);
|
||||
t.addEventListener("click", () => searchByTag(tag));
|
||||
tagsDiv.appendChild(t);
|
||||
}
|
||||
});
|
||||
|
||||
// Action buttons
|
||||
@@ -1026,6 +1090,147 @@
|
||||
if (modal) modal.classList.remove("active");
|
||||
}
|
||||
|
||||
function initConfigModal() {
|
||||
const openBtn = document.getElementById("config-open-btn");
|
||||
const closeBtn = document.getElementById("config-close");
|
||||
const modal = document.getElementById("config-modal");
|
||||
const addBtn = document.getElementById("config-add-btn");
|
||||
const patternInput = document.getElementById("config-pattern-input");
|
||||
|
||||
if (!openBtn || !closeBtn || !modal) return;
|
||||
|
||||
openBtn.addEventListener("click", () => {
|
||||
modal.classList.add("active");
|
||||
closeHeaderMenu();
|
||||
renderConfigFilters();
|
||||
safeCreateIcons();
|
||||
});
|
||||
|
||||
closeBtn.addEventListener("click", closeConfigModal);
|
||||
modal.addEventListener("click", (e) => {
|
||||
if (e.target === modal) {
|
||||
closeConfigModal();
|
||||
}
|
||||
});
|
||||
|
||||
addBtn.addEventListener("click", addConfigFilter);
|
||||
patternInput.addEventListener("keypress", (e) => {
|
||||
if (e.key === "Enter") {
|
||||
addConfigFilter();
|
||||
}
|
||||
});
|
||||
|
||||
patternInput.addEventListener("input", updateRegexPreview);
|
||||
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Escape" && modal.classList.contains("active")) {
|
||||
closeConfigModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function closeConfigModal() {
|
||||
const modal = document.getElementById("config-modal");
|
||||
if (modal) modal.classList.remove("active");
|
||||
}
|
||||
|
||||
function renderConfigFilters() {
|
||||
const config = TagFilterService.getConfig();
|
||||
const filters = config.tagFilters || TagFilterService.defaultFilters;
|
||||
const container = document.getElementById("config-filters-list");
|
||||
|
||||
container.innerHTML = "";
|
||||
|
||||
filters.forEach((filter, index) => {
|
||||
const badge = el("div", { class: `config-filter-badge ${!filter.enabled ? "disabled" : ""}` }, [
|
||||
el("span", {}, [document.createTextNode(filter.pattern)]),
|
||||
el("button", {
|
||||
class: "config-filter-toggle",
|
||||
title: filter.enabled ? "Désactiver" : "Activer",
|
||||
type: "button"
|
||||
}, [document.createTextNode(filter.enabled ? "✓" : "○")]),
|
||||
el("button", {
|
||||
class: "config-filter-remove",
|
||||
title: "Supprimer",
|
||||
type: "button"
|
||||
}, [document.createTextNode("×")]),
|
||||
]);
|
||||
|
||||
const toggleBtn = badge.querySelector(".config-filter-toggle");
|
||||
const removeBtn = badge.querySelector(".config-filter-remove");
|
||||
|
||||
toggleBtn.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
toggleConfigFilter(index);
|
||||
});
|
||||
|
||||
removeBtn.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
removeConfigFilter(index);
|
||||
});
|
||||
|
||||
container.appendChild(badge);
|
||||
});
|
||||
}
|
||||
|
||||
function toggleConfigFilter(index) {
|
||||
const config = TagFilterService.getConfig();
|
||||
const filters = config.tagFilters || TagFilterService.defaultFilters;
|
||||
if (filters[index]) {
|
||||
filters[index].enabled = !filters[index].enabled;
|
||||
config.tagFilters = filters;
|
||||
TagFilterService.saveConfig(config);
|
||||
renderConfigFilters();
|
||||
refreshTagsForContext().catch(err => console.error("Error refreshing tags:", err));
|
||||
}
|
||||
}
|
||||
|
||||
function removeConfigFilter(index) {
|
||||
const config = TagFilterService.getConfig();
|
||||
let filters = config.tagFilters || TagFilterService.defaultFilters;
|
||||
filters = filters.filter((_, i) => i !== index);
|
||||
config.tagFilters = filters;
|
||||
TagFilterService.saveConfig(config);
|
||||
renderConfigFilters();
|
||||
refreshTagsForContext().catch(err => console.error("Error refreshing tags:", err));
|
||||
}
|
||||
|
||||
function addConfigFilter() {
|
||||
const input = document.getElementById("config-pattern-input");
|
||||
const pattern = input.value.trim();
|
||||
|
||||
if (!pattern) return;
|
||||
|
||||
const regex = TagFilterService.patternToRegex(pattern);
|
||||
const config = TagFilterService.getConfig();
|
||||
const filters = config.tagFilters || TagFilterService.defaultFilters;
|
||||
|
||||
const newFilter = { pattern, regex, enabled: true };
|
||||
filters.push(newFilter);
|
||||
config.tagFilters = filters;
|
||||
TagFilterService.saveConfig(config);
|
||||
|
||||
input.value = "";
|
||||
renderConfigFilters();
|
||||
refreshTagsForContext().catch(err => console.error("Error refreshing tags:", err));
|
||||
updateRegexPreview();
|
||||
}
|
||||
|
||||
function updateRegexPreview() {
|
||||
const input = document.getElementById("config-pattern-input");
|
||||
const preview = document.getElementById("config-regex-preview");
|
||||
const code = document.getElementById("config-regex-code");
|
||||
const pattern = input.value.trim();
|
||||
|
||||
if (pattern) {
|
||||
const regex = TagFilterService.patternToRegex(pattern);
|
||||
code.textContent = `^${regex}$`;
|
||||
preview.style.display = "block";
|
||||
} else {
|
||||
preview.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Search
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1081,14 +1286,18 @@
|
||||
if (r.tags && r.tags.length > 0) {
|
||||
const tagsDiv = el("div", { class: "search-result-tags" });
|
||||
r.tags.forEach((tag) => {
|
||||
const tagEl = el("span", { class: "file-tag" }, [document.createTextNode(`#${tag}`)]);
|
||||
tagEl.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
addTagFilter(tag);
|
||||
});
|
||||
tagsDiv.appendChild(tagEl);
|
||||
if (!TagFilterService.isTagFiltered(tag)) {
|
||||
const tagEl = el("span", { class: "file-tag" }, [document.createTextNode(`#${tag}`)]);
|
||||
tagEl.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
addTagFilter(tag);
|
||||
});
|
||||
tagsDiv.appendChild(tagEl);
|
||||
}
|
||||
});
|
||||
item.appendChild(tagsDiv);
|
||||
if (tagsDiv.children.length > 0) {
|
||||
item.appendChild(tagsDiv);
|
||||
}
|
||||
}
|
||||
|
||||
item.addEventListener("click", () => openFile(r.vault, r.path));
|
||||
@@ -1473,6 +1682,7 @@
|
||||
initVaultContext();
|
||||
initCollapsiblePanels();
|
||||
initHelpModal();
|
||||
initConfigModal();
|
||||
initSidebarFilter();
|
||||
initSidebarResize();
|
||||
initTagResize();
|
||||
|
||||
Reference in New Issue
Block a user