Add tag filtering system with configurable pattern-based filters and persistent localStorage settings

This commit is contained in:
2026-03-22 11:29:26 -04:00
parent 7a53e85e3d
commit 0ff888280a
3 changed files with 405 additions and 12 deletions
+222 -12
View File
@@ -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();