feat: implémenter système de gestion des tags cachés avec panel de configuration, présets système (note/shaarli-pin/readitlater/archiver), support wildcards (note-color-*/notebg-*/notefilter-*), ajout/suppression tags personnalisés, filtrage automatique DOM avec MutationObserver, exclusion tags cachés dans recherche/bookmarks/tag cloud, amélioration responsive modale recherche mobile (95% width, 90vh height, padding 5vh), et styles modernes avec gradients bleus, chips interactifs, toggle

This commit is contained in:
Bruno Charest 2026-02-20 12:55:54 -05:00
parent f6862609c0
commit 3f9e481cf9
4 changed files with 991 additions and 3 deletions

View File

@ -898,8 +898,9 @@ input:checked+.theme-slider:before {
/* Results List */
.search-results {
max-height: 320px;
max-height: 60vh;
overflow-y: auto;
overflow-x: hidden;
padding: 0.5rem 0;
}
@ -4505,4 +4506,19 @@ table {
justify-content: center;
padding: 0.55rem 0.45rem;
}
/* Mobile search panel - almost full screen */
.search-overlay {
padding-top: 5vh;
padding-bottom: 5vh;
}
.search-modal {
width: 95%;
max-height: 90vh;
}
.search-results {
max-height: calc(90vh - 200px);
}
}

View File

@ -0,0 +1,412 @@
<!DOCTYPE html>
<html{if="$language !=='auto'"} lang=" {$language}"{/if}>
<head>
{$pageName="hidden-tags"}
{include="includes"}
<style>
.hidden-tags-section {
margin-bottom: 2rem;
}
.hidden-tags-section h3 {
font-size: 1rem;
margin-bottom: 1rem;
color: var(--text-primary);
}
.preset-tags-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 0.75rem;
margin-bottom: 1.5rem;
}
.preset-tag-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
background: var(--bg-secondary);
border: 1px solid var(--border-light);
border-radius: 0.5rem;
cursor: pointer;
transition: all 0.15s ease;
}
.preset-tag-item:hover {
border-color: var(--primary);
background: var(--bg-card-hover);
}
.preset-tag-item input[type="checkbox"] {
margin: 0;
cursor: pointer;
}
.preset-tag-item label {
margin: 0;
cursor: pointer;
font-size: 0.875rem;
color: var(--text-primary);
flex: 1;
}
.preset-tag-item .tag-description {
font-size: 0.75rem;
color: var(--text-muted);
}
.custom-tags-section {
margin-top: 1.5rem;
padding-top: 1.5rem;
border-top: 1px solid var(--border);
}
.custom-tag-input-row {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
}
.custom-tag-input-row input {
flex: 1;
}
.custom-tags-list {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
min-height: 2rem;
}
.custom-tag-chip {
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.35rem 0.75rem;
background: var(--primary);
color: #fff;
border-radius: 999px;
font-size: 0.875rem;
}
.custom-tag-chip button {
background: none;
border: none;
color: inherit;
cursor: pointer;
padding: 0;
font-size: 1rem;
line-height: 1;
opacity: 0.8;
transition: opacity 0.15s ease;
}
.custom-tag-chip button:hover {
opacity: 1;
}
.hidden-tags-info {
background: var(--bg-secondary);
border-left: 3px solid var(--primary);
padding: 1rem;
border-radius: 0 0.5rem 0.5rem 0;
margin-bottom: 1.5rem;
}
.hidden-tags-info p {
margin: 0;
font-size: 0.875rem;
color: var(--text-secondary);
}
.hidden-tags-info strong {
color: var(--text-primary);
}
.save-status {
display: inline-flex;
align-items: center;
gap: 0.5rem;
margin-left: 1rem;
font-size: 0.875rem;
color: var(--success, #22c55e);
opacity: 0;
transition: opacity 0.3s ease;
}
.save-status.visible {
opacity: 1;
}
</style>
</head>
<body>
{include="page.header"}
<div class="container page-hidden-tags">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<div class="card">
<div class="card-header">
{'Hidden Tags'|t}
<span class="save-status" id="saveStatus">
<i class="mdi mdi-check-circle"></i>
{'Saved'|t}
</span>
</div>
<div class="card-body">
<div class="hidden-tags-info">
<p>
<strong>{'What are hidden tags?'|t}</strong><br>
{'Hidden tags are system tags that are used by the application but not displayed to users. They are still functional - bookmarks with these tags work normally, but the tags themselves are hidden from view in lists, clouds, and bookmark cards.'|t}
</p>
</div>
<div class="hidden-tags-section">
<h3>{'Preset System Tags'|t}</h3>
<div class="preset-tags-grid" id="presetTagsGrid">
<!-- Preset tags will be populated by JavaScript -->
</div>
</div>
<div class="custom-tags-section">
<h3>{'Custom Hidden Tags'|t}</h3>
<p style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 1rem;">
{'Add any additional tags you want to hide from display:'|t}
</p>
<div class="custom-tag-input-row">
<input type="text" id="customTagInput" class="form-control" placeholder="{'Enter tag name'|t}...">
<button type="button" id="addCustomTagBtn" class="btn btn-primary">
<i class="mdi mdi-plus"></i>
{'Add'|t}
</button>
</div>
<div class="custom-tags-list" id="customTagsList">
<!-- Custom tags will be populated by JavaScript -->
</div>
</div>
</div>
<div class="card-footer">
<button type="button" id="resetDefaultsBtn" class="btn btn-secondary">{'Reset to Defaults'|t}</button>
<a href="{$base_path}/admin/tools" class="btn btn-primary">{'Back to Tools'|t}</a>
</div>
</div>
</div>
</div>
</div>
<script>
(function() {
// Default system tags with descriptions
const PRESET_TAGS = [
{ tag: 'note', description: 'Note bookmarks' },
{ tag: 'shaarli-pin', description: 'Pinned items' },
{ tag: 'note-color-*', description: 'Note color variants' },
{ tag: 'notebg-*', description: 'Note background images' },
{ tag: 'notefilter-*', description: 'Note filter categories' },
{ tag: 'readitlater', description: 'Read It Later items' },
{ tag: 'shaarli-archiver', description: 'Archived notes' }
];
const STORAGE_KEY = 'shaarli_hidden_tags';
// Default hidden tags (all preset tags are hidden by default)
const DEFAULT_HIDDEN_TAGS = PRESET_TAGS.map(t => t.tag);
function getHiddenTags() {
try {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) {
return JSON.parse(stored);
}
} catch (e) {
console.error('Error loading hidden tags:', e);
}
return [...DEFAULT_HIDDEN_TAGS];
}
function saveHiddenTags(tags) {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(tags));
showSaveStatus();
return true;
} catch (e) {
console.error('Error saving hidden tags:', e);
return false;
}
}
function showSaveStatus() {
const status = document.getElementById('saveStatus');
if (status) {
status.classList.add('visible');
setTimeout(() => {
status.classList.remove('visible');
}, 2000);
}
}
function isWildcardTag(tag) {
return tag.includes('*');
}
function matchesWildcard(pattern, tag) {
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$', 'i');
return regex.test(tag);
}
function isTagHidden(tag, hiddenTags) {
const normalizedTag = tag.toLowerCase().trim();
return hiddenTags.some(hiddenTag => {
const normalizedHidden = hiddenTag.toLowerCase().trim();
if (isWildcardTag(normalizedHidden)) {
return matchesWildcard(normalizedHidden, normalizedTag);
}
return normalizedHidden === normalizedTag;
});
}
// Make isTagHidden globally available
window.isTagHidden = isTagHidden;
window.getHiddenTags = getHiddenTags;
function renderPresetTags(hiddenTags) {
const grid = document.getElementById('presetTagsGrid');
if (!grid) return;
grid.innerHTML = PRESET_TAGS.map(({ tag, description }) => {
const isChecked = hiddenTags.includes(tag);
const tagId = 'preset-' + tag.replace(/[^a-zA-Z0-9]/g, '_');
return `
<div class="preset-tag-item">
<input type="checkbox" id="${tagId}" data-tag="${tag}" ${isChecked ? 'checked' : ''}>
<div style="flex: 1;">
<label for="${tagId}">${tag}</label>
<div class="tag-description">${description}</div>
</div>
</div>
`;
}).join('');
// Add event listeners
grid.querySelectorAll('input[type="checkbox"]').forEach(checkbox => {
checkbox.addEventListener('change', function() {
const tag = this.dataset.tag;
let hiddenTags = getHiddenTags();
if (this.checked) {
if (!hiddenTags.includes(tag)) {
hiddenTags.push(tag);
}
} else {
hiddenTags = hiddenTags.filter(t => t !== tag);
}
saveHiddenTags(hiddenTags);
});
});
}
function renderCustomTags(hiddenTags) {
const list = document.getElementById('customTagsList');
if (!list) return;
// Filter out preset tags to show only custom ones
const customTags = hiddenTags.filter(tag => {
return !PRESET_TAGS.some(preset => preset.tag === tag);
});
if (customTags.length === 0) {
list.innerHTML = '<span style="color: var(--text-muted); font-size: 0.875rem;">{literal}No custom hidden tags{/literal}</span>';
return;
}
list.innerHTML = customTags.map(tag => `
<span class="custom-tag-chip">
${tag}
<button type="button" data-tag="${tag}" title="{literal}Remove{/literal}">&times;</button>
</span>
`).join('');
// Add event listeners for remove buttons
list.querySelectorAll('button').forEach(btn => {
btn.addEventListener('click', function() {
const tag = this.dataset.tag;
let hiddenTags = getHiddenTags();
hiddenTags = hiddenTags.filter(t => t !== tag);
saveHiddenTags(hiddenTags);
renderCustomTags(hiddenTags);
});
});
}
function addCustomTag() {
const input = document.getElementById('customTagInput');
if (!input) return;
const tag = input.value.trim();
if (!tag) return;
// Don't add if it's a preset tag
if (PRESET_TAGS.some(preset => preset.tag === tag)) {
alert('This tag is already managed in the preset list.');
return;
}
let hiddenTags = getHiddenTags();
if (!hiddenTags.includes(tag)) {
hiddenTags.push(tag);
saveHiddenTags(hiddenTags);
renderCustomTags(hiddenTags);
}
input.value = '';
input.focus();
}
function resetToDefaults() {
if (confirm('{literal}Reset to default hidden tags? This will restore all preset system tags.{/literal}')) {
saveHiddenTags([...DEFAULT_HIDDEN_TAGS]);
renderPresetTags(DEFAULT_HIDDEN_TAGS);
renderCustomTags(DEFAULT_HIDDEN_TAGS);
}
}
// Initialize
document.addEventListener('DOMContentLoaded', function() {
const hiddenTags = getHiddenTags();
renderPresetTags(hiddenTags);
renderCustomTags(hiddenTags);
// Add custom tag button
const addBtn = document.getElementById('addCustomTagBtn');
if (addBtn) {
addBtn.addEventListener('click', addCustomTag);
}
// Enter key in input
const input = document.getElementById('customTagInput');
if (input) {
input.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
addCustomTag();
}
});
}
// Reset button
const resetBtn = document.getElementById('resetDefaultsBtn');
if (resetBtn) {
resetBtn.addEventListener('click', resetToDefaults);
}
});
})();
</script>
{include="page.footer"}
</body>
</html>

View File

@ -84,6 +84,124 @@ document.addEventListener('DOMContentLoaded', () => {
}
});
// ===== Hidden Tags Management =====
const HIDDEN_TAGS_STORAGE_KEY = 'shaarli_hidden_tags';
// Default system tags that are hidden by default
const DEFAULT_HIDDEN_TAGS = [
'note',
'shaarli-pin',
'note-color-*',
'notebg-*',
'notefilter-*',
'readitlater',
'shaarli-archiver'
];
// Get hidden tags from localStorage
function getHiddenTags() {
try {
const stored = localStorage.getItem(HIDDEN_TAGS_STORAGE_KEY);
if (stored) {
return JSON.parse(stored);
}
} catch (e) {
console.error('Error loading hidden tags:', e);
}
return [...DEFAULT_HIDDEN_TAGS];
}
// Check if a tag matches a wildcard pattern
function matchesWildcard(pattern, tag) {
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$', 'i');
return regex.test(tag);
}
// Check if a tag should be hidden
function isTagHidden(tag) {
if (!tag) return false;
const normalizedTag = tag.toLowerCase().trim();
const hiddenTags = getHiddenTags();
return hiddenTags.some(hiddenTag => {
const normalizedHidden = hiddenTag.toLowerCase().trim();
if (normalizedHidden.includes('*')) {
return matchesWildcard(normalizedHidden, normalizedTag);
}
return normalizedHidden === normalizedTag;
});
}
// Filter out hidden tags from a list
function filterHiddenTags(tags) {
if (!Array.isArray(tags)) return [];
return tags.filter(tag => !isTagHidden(tag));
}
// Hide tag elements in the DOM
function hideHiddenTagElements() {
// Hide tag chips in bookmark cards
document.querySelectorAll('.link-tag[data-tag]').forEach(el => {
const tag = el.getAttribute('data-tag');
if (isTagHidden(tag)) {
el.style.display = 'none';
}
});
// Hide tags in tag list
document.querySelectorAll('#tagListContainer .list-group-item[data-tag-name]').forEach(el => {
const tagName = el.getAttribute('data-tag-name');
if (isTagHidden(tagName)) {
el.style.display = 'none';
}
});
// Hide tags in tag cloud
document.querySelectorAll('.tag-item[data-tag]').forEach(el => {
const tag = el.getAttribute('data-tag');
if (isTagHidden(tag)) {
el.style.display = 'none';
}
});
// Update visible tag count in tag list
const visibleTagCountEl = document.getElementById('visibleTagCount');
if (visibleTagCountEl) {
const visibleTags = document.querySelectorAll('#tagListContainer .list-group-item:not([style*="display: none"])');
visibleTagCountEl.textContent = visibleTags.length;
}
}
// Run on page load
hideHiddenTagElements();
// Set up MutationObserver to handle dynamically added tags
const tagObserver = new MutationObserver((mutations) => {
let shouldFilter = false;
mutations.forEach((mutation) => {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
// Check if added node contains tags
if (node.matches?.('.link-tag[data-tag], .tag-item[data-tag], [data-tag-name], .list-group-item[data-tag-name]') ||
node.querySelector?.('.link-tag[data-tag], .tag-item[data-tag], [data-tag-name], .list-group-item[data-tag-name]')) {
shouldFilter = true;
}
}
});
}
});
if (shouldFilter) {
hideHiddenTagElements();
}
});
// Start observing once DOM is ready
tagObserver.observe(document.body, {
childList: true,
subtree: true
});
// ===== Search Overlay (Spotlight Style) =====
try {
const searchOverlay = document.getElementById('search-overlay');
@ -127,7 +245,7 @@ document.addEventListener('DOMContentLoaded', () => {
return text.toLowerCase().includes(query.toLowerCase());
}
// Fetch all unique tags from the page
// Fetch all unique tags from the page (excluding hidden tags)
function fetchTags() {
if (cachedTags) return cachedTags;
try {
@ -137,6 +255,8 @@ document.addEventListener('DOMContentLoaded', () => {
if (typeof value !== 'string') return;
const cleaned = value.trim();
if (!cleaned || cleaned.includes('•')) return;
// Skip hidden tags
if (isTagHidden(cleaned)) return;
tagsSet.add(cleaned);
};
@ -172,7 +292,10 @@ document.addEventListener('DOMContentLoaded', () => {
isNote: (el.querySelector('.link-title')?.getAttribute('title') || '').toLowerCase().includes('note'),
title: el.querySelector('.link-title a')?.textContent || el.querySelector('.link-title')?.textContent || '',
url: el.querySelector('.link-url a')?.href || el.querySelector('.link-title a')?.href || '',
tags: Array.from(el.querySelectorAll('.link-tag[data-tag]')).map(t => (t.getAttribute('data-tag') || '').trim()).filter(Boolean),
// Filter out hidden tags
tags: filterHiddenTags(
Array.from(el.querySelectorAll('.link-tag[data-tag]')).map(t => (t.getAttribute('data-tag') || '').trim()).filter(Boolean)
),
description: el.querySelector('.link-description')?.textContent || ''
}));
return cachedBookmarks;

View File

@ -66,6 +66,16 @@
</div>
<i class="mdi mdi-chevron-right"></i>
</a>
<a class="list-group-item list-group-item-action ripple" href="#" id="hidden-tags-toggle">
<div class="list-sortable-handle">
<i class="mdi mdi-tag-off-outline"></i>
</div>
<div class="list-group-item-content">
<div class="list-group-item-label">{'Hidden Tags'|t}</div>
<div class="list-group-item-sublabel">{'Manage system tags hidden from display'|t}</div>
</div>
<i class="mdi mdi-chevron-right"></i>
</a>
<a class="list-group-item list-group-item-action ripple" href="{$base_path}/admin/import">
<div class="list-sortable-handle">
<i class="mdi mdi-file-import"></i>
@ -91,6 +101,59 @@
</div>
</div>
<!-- Hidden Tags Management Panel -->
<div class="row" id="hidden-tags-panel" style="display: none;">
<div class="col-md-8 col-md-offset-2">
<div class="card hidden-tags-card">
<div class="card-header">
<span><i class="mdi mdi-tag-off-outline"></i> {'Hidden Tags'|t}</span>
<button type="button" class="btn btn-sm btn-secondary" id="close-hidden-tags">
<i class="mdi mdi-close"></i>
</button>
</div>
<div class="card-body">
<!-- Info Banner -->
<div class="hidden-tags-info">
<i class="mdi mdi-information-outline"></i>
<span>{'Hidden tags are functional tags used by the application that remain invisible to users. They are still fully operational for internal features.'|t}</span>
</div>
<!-- Preset System Tags -->
<div class="hidden-tags-section">
<h4><i class="mdi mdi-tag-check-outline"></i> {'Preset System Tags'|t}</h4>
<p class="text-muted">{'Toggle visibility for built-in system tags'|t}</p>
<div id="preset-tags-grid" class="preset-tags-grid">
<!-- Filled by JavaScript -->
</div>
</div>
<!-- Custom Hidden Tags -->
<div class="hidden-tags-section">
<h4><i class="mdi mdi-tag-plus-outline"></i> {'Custom Hidden Tags'|t}</h4>
<p class="text-muted">{'Add your own tags to hide from display (supports wildcards like note-*)'|t}</p>
<div class="custom-tags-input-group">
<input type="text" id="custom-tag-input" class="form-control" placeholder="{'Enter tag name...'|t}">
<button type="button" class="btn btn-primary" id="add-custom-tag">
<i class="mdi mdi-plus"></i> {'Add'|t}
</button>
</div>
<div id="custom-tags-list" class="custom-tags-list">
<!-- Filled by JavaScript -->
</div>
</div>
<!-- Actions -->
<div class="hidden-tags-actions">
<button type="button" class="btn btn-secondary" id="reset-hidden-tags">
<i class="mdi mdi-refresh"></i> {'Reset to Defaults'|t}
</button>
<span id="save-status" class="save-status"></span>
</div>
</div>
</div>
</div>
</div>
{if="!empty($linkcount)"}
<div class="row">
<div class="col-md-8 col-md-offset-2">
@ -184,6 +247,380 @@
{/loop}
</div>
{include="page.footer"}
<style>
/* Hidden Tags Panel Styles */
.hidden-tags-card .card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.hidden-tags-info {
display: flex;
gap: 0.75rem;
padding: 1rem;
background: linear-gradient(135deg, rgba(59, 130, 246, 0.1) 0%, rgba(99, 102, 241, 0.1) 100%);
border: 1px solid rgba(99, 102, 241, 0.2);
border-radius: 0.75rem;
margin-bottom: 1.5rem;
}
.hidden-tags-info i {
font-size: 1.25rem;
color: #5b8def;
flex-shrink: 0;
}
.hidden-tags-info span {
font-size: 0.9rem;
color: var(--text-muted);
line-height: 1.5;
}
.hidden-tags-section {
margin-bottom: 2rem;
}
.hidden-tags-section h4 {
font-size: 1.1rem;
margin-bottom: 0.5rem;
color: var(--text-primary);
}
.hidden-tags-section h4 i {
color: #5b8def;
margin-right: 0.5rem;
}
.preset-tags-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 0.75rem;
margin-top: 1rem;
}
.preset-tag-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
background: var(--bg-card);
border: 1px solid var(--border-light);
border-radius: 0.5rem;
transition: all 0.2s ease;
}
.preset-tag-item:hover {
border-color: rgba(99, 102, 241, 0.3);
background: rgba(99, 102, 241, 0.05);
}
.preset-tag-checkbox {
width: 20px;
height: 20px;
accent-color: #5b8def;
cursor: pointer;
}
.preset-tag-info {
flex: 1;
}
.preset-tag-name {
font-weight: 500;
font-size: 0.95rem;
color: var(--text-primary);
font-family: 'Fira Code', monospace;
background: rgba(99, 102, 241, 0.1);
padding: 0.15rem 0.5rem;
border-radius: 0.25rem;
}
.preset-tag-desc {
font-size: 0.8rem;
color: var(--text-muted);
margin-top: 0.25rem;
}
.custom-tags-input-group {
display: flex;
gap: 0.5rem;
margin-top: 1rem;
}
.custom-tags-input-group input {
flex: 1;
}
.custom-tags-list {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 1rem;
min-height: 40px;
}
.custom-tag-chip {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.4rem 0.75rem;
background: linear-gradient(135deg, rgba(99, 102, 241, 0.15) 0%, rgba(91, 141, 239, 0.15) 100%);
border: 1px solid rgba(99, 102, 241, 0.3);
border-radius: 2rem;
font-size: 0.9rem;
font-family: 'Fira Code', monospace;
color: var(--text-primary);
}
.custom-tag-chip .remove-tag {
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
border-radius: 50%;
background: rgba(239, 68, 68, 0.2);
color: #ef4444;
font-size: 0.75rem;
cursor: pointer;
transition: all 0.2s ease;
}
.custom-tag-chip .remove-tag:hover {
background: #ef4444;
color: white;
}
.no-custom-tags {
color: var(--text-muted);
font-style: italic;
font-size: 0.9rem;
padding: 0.5rem 0;
}
.hidden-tags-actions {
display: flex;
align-items: center;
gap: 1rem;
margin-top: 1.5rem;
padding-top: 1.5rem;
border-top: 1px solid var(--border-light);
}
.save-status {
font-size: 0.9rem;
color: #10b981;
opacity: 0;
transition: opacity 0.3s ease;
}
.save-status.show {
opacity: 1;
}
[data-theme="light"] .hidden-tags-info {
background: linear-gradient(135deg, rgba(59, 130, 246, 0.08) 0%, rgba(99, 102, 241, 0.08) 100%);
}
</style>
<script>
// Hidden Tags Management
(function() {
const STORAGE_KEY = 'shaarli_hidden_tags';
const PRESET_TAGS = [
{ name: 'note', desc: 'Notes - Internal note identifier' },
{ name: 'shaarli-pin', desc: 'Pinned - Keeps bookmarks at top' },
{ name: 'note-color-*', desc: 'Note Colors - Wildcard for color tags' },
{ name: 'notebg-*', desc: 'Note Backgrounds - Wildcard for background tags' },
{ name: 'notefilter-*', desc: 'Note Filters - Wildcard for filter tags' },
{ name: 'readitlater', desc: 'Read Later - Temporary reading list' },
{ name: 'shaarli-archiver', desc: 'Archived - Archived notes' }
];
const DEFAULT_HIDDEN_TAGS = PRESET_TAGS.map(t => t.name);
function getHiddenTags() {
try {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) return JSON.parse(stored);
} catch (e) {
console.error('Error loading hidden tags:', e);
}
return [...DEFAULT_HIDDEN_TAGS];
}
function saveHiddenTags(tags) {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(tags));
showSaveStatus();
} catch (e) {
console.error('Error saving hidden tags:', e);
}
}
function showSaveStatus() {
const status = document.getElementById('save-status');
if (status) {
status.textContent = 'Saved!';
status.classList.add('show');
setTimeout(() => status.classList.remove('show'), 2000);
}
}
function isWildcardTag(tag) {
return tag.includes('*');
}
function matchesWildcard(pattern, tag) {
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$', 'i');
return regex.test(tag);
}
function renderPresetTags() {
const container = document.getElementById('preset-tags-grid');
if (!container) return;
const hiddenTags = getHiddenTags();
container.innerHTML = PRESET_TAGS.map(tag => {
const isChecked = hiddenTags.some(ht => ht.toLowerCase() === tag.name.toLowerCase());
return `
<div class="preset-tag-item">
<input type="checkbox" class="preset-tag-checkbox"
data-tag="${tag.name}" ${isChecked ? 'checked' : ''}>
<div class="preset-tag-info">
<div class="preset-tag-name">${tag.name}</div>
<div class="preset-tag-desc">${tag.desc}</div>
</div>
</div>
`;
}).join('');
// Add event listeners
container.querySelectorAll('.preset-tag-checkbox').forEach(cb => {
cb.addEventListener('change', function() {
const tagName = this.dataset.tag;
let hiddenTags = getHiddenTags();
if (this.checked) {
if (!hiddenTags.includes(tagName)) hiddenTags.push(tagName);
} else {
hiddenTags = hiddenTags.filter(t => t.toLowerCase() !== tagName.toLowerCase());
}
saveHiddenTags(hiddenTags);
});
});
}
function renderCustomTags() {
const container = document.getElementById('custom-tags-list');
if (!container) return;
const hiddenTags = getHiddenTags();
const customTags = hiddenTags.filter(tag =>
!PRESET_TAGS.some(pt => pt.name.toLowerCase() === tag.toLowerCase())
);
if (customTags.length === 0) {
container.innerHTML = '<div class="no-custom-tags">No custom hidden tags</div>';
return;
}
container.innerHTML = customTags.map(tag => `
<div class="custom-tag-chip">
<span>${tag}</span>
<span class="remove-tag" data-tag="${tag}">&times;</span>
</div>
`).join('');
// Add remove listeners
container.querySelectorAll('.remove-tag').forEach(btn => {
btn.addEventListener('click', function() {
const tagName = this.dataset.tag;
let hiddenTags = getHiddenTags();
hiddenTags = hiddenTags.filter(t => t.toLowerCase() !== tagName.toLowerCase());
saveHiddenTags(hiddenTags);
renderCustomTags();
});
});
}
function addCustomTag() {
const input = document.getElementById('custom-tag-input');
if (!input) return;
const tagName = input.value.trim().toLowerCase();
if (!tagName) return;
// Check if already exists
const hiddenTags = getHiddenTags();
if (hiddenTags.some(t => t.toLowerCase() === tagName)) {
input.value = '';
return;
}
hiddenTags.push(tagName);
saveHiddenTags(hiddenTags);
input.value = '';
renderCustomTags();
}
function resetToDefaults() {
if (confirm('Reset hidden tags to default settings?')) {
saveHiddenTags([...DEFAULT_HIDDEN_TAGS]);
renderPresetTags();
renderCustomTags();
}
}
// Initialize
document.addEventListener('DOMContentLoaded', function() {
const toggleBtn = document.getElementById('hidden-tags-toggle');
const panel = document.getElementById('hidden-tags-panel');
const closeBtn = document.getElementById('close-hidden-tags');
const addBtn = document.getElementById('add-custom-tag');
const resetBtn = document.getElementById('reset-hidden-tags');
const customInput = document.getElementById('custom-tag-input');
if (toggleBtn && panel) {
toggleBtn.addEventListener('click', function(e) {
e.preventDefault();
panel.style.display = panel.style.display === 'none' ? '' : 'none';
if (panel.style.display !== 'none') {
renderPresetTags();
renderCustomTags();
panel.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
});
}
if (closeBtn && panel) {
closeBtn.addEventListener('click', function() {
panel.style.display = 'none';
});
}
if (addBtn) {
addBtn.addEventListener('click', addCustomTag);
}
if (customInput) {
customInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
addCustomTag();
}
});
}
if (resetBtn) {
resetBtn.addEventListener('click', resetToDefaults);
}
});
})();
</script>
</body>
</html>