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:
@@ -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}">×</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>
|
||||
Reference in New Issue
Block a user