feat: refactorer la recherche avec modale moderne, pills interactives Notes/Tags/All, résultats avec badges kind et tags inline, chips de filtrage avec bouton close, layout responsive mobile optimisé, et simplification majeure du CSS avec suppression de 2500+ lignes de styles obsolètes (content-container, toolbar, pagination, link cards, compact table, etc.)

This commit is contained in:
Bruno Charest 2026-02-20 09:51:06 -05:00
parent 1ea13ff16b
commit 83d9a7425e
4 changed files with 266 additions and 2664 deletions

File diff suppressed because it is too large Load Diff

View File

@ -91,9 +91,10 @@ document.addEventListener('DOMContentLoaded', () => {
const searchResults = document.getElementById('search-results');
const searchTagsBtn = document.getElementById('search-tags-btn');
const searchAllBtn = document.getElementById('search-all-btn');
const searchNotesBtn = document.getElementById('search-notes-btn');
const searchForm = document.getElementById('search-form');
let searchMode = 'search'; // 'search' or 'tags'
let searchMode = 'search'; // 'search' | 'tags' | 'notes'
let selectedIndex = -1;
let searchTimeout = null;
let cachedBookmarks = null;
@ -167,9 +168,10 @@ document.addEventListener('DOMContentLoaded', () => {
if (linkElements.length > 0) {
cachedBookmarks = Array.from(linkElements).map(el => ({
id: el.dataset.id,
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')).map(t => t.textContent.trim()),
tags: 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;
@ -181,21 +183,32 @@ document.addEventListener('DOMContentLoaded', () => {
return [];
}
// Render search results (tags or bookmarks)
function renderResults(results, query, isTagMode = false) {
function getSearchHint(mode) {
if (mode === 'tags') {
return 'Type to search in tags...';
}
if (mode === 'notes') {
return 'Type to search in notes...';
}
return 'Type to search in bookmarks and notes...';
}
// Render search results (tags, global bookmarks, or notes)
function renderResults(results, query, mode = 'search') {
if (!searchResults) return;
if (results.length === 0) {
if (query && query.length > 0) {
const scopeLabel = mode === 'tags' ? 'tags' : (mode === 'notes' ? 'notes' : 'bookmarks/notes');
searchResults.innerHTML = `
<div class="search-no-results">
No results found for "<strong>${escapeHtml(query)}</strong>"
No ${scopeLabel} found for "<strong>${escapeHtml(query)}</strong>"
</div>
`;
} else {
searchResults.innerHTML = `
<div class="search-results-empty">
<span class="search-results-hint">Start typing to see tag suggestions...</span>
<span class="search-results-hint">${getSearchHint(mode)}</span>
</div>
`;
}
@ -203,7 +216,7 @@ document.addEventListener('DOMContentLoaded', () => {
}
let html;
if (isTagMode) {
if (mode === 'tags') {
// Render tags
html = results.slice(0, 15).map((tag, index) => {
const highlightedTag = highlightMatch(escapeHtml(tag), query);
@ -219,18 +232,32 @@ document.addEventListener('DOMContentLoaded', () => {
`;
}).join('');
} else {
// Render bookmarks
html = results.slice(0, 10).map((item, index) => {
// Render bookmarks or notes
html = results.slice(0, 12).map((item, index) => {
const highlightedTitle = highlightMatch(escapeHtml(item.title), query);
const highlightedDescription = item.description ? highlightMatch(escapeHtml(item.description), query) : '';
const typeLabel = item.isNote ? 'note' : 'bookmark';
const typeIcon = item.isNote ? 'mdi-note-text-outline' : 'mdi-bookmark-outline';
const highlightedTags = (item.tags || []).slice(0, 4).map((tag) => {
const tagValue = escapeHtml(tag);
return `<span class="search-result-tag">${highlightMatch(tagValue, query)}</span>`;
}).join('');
return `
<div class="search-result-item${index === selectedIndex ? ' selected' : ''}"
data-index="${index}"
data-url="${escapeHtml(item.url)}"
data-id="${item.id}">
<div class="search-result-item-content">
<i class="mdi mdi-bookmark-outline search-result-icon"></i>
<span class="search-result-text">${highlightedTitle}</span>
<i class="mdi ${typeIcon} search-result-icon"></i>
<div class="search-result-main">
<span class="search-result-text">${highlightedTitle}</span>
<span class="search-result-kind">${typeLabel}</span>
</div>
</div>
${highlightedDescription ? `<div class="search-result-sub">${highlightedDescription}</div>` : ''}
${highlightedTags ? `<div class="search-result-tags">${highlightedTags}</div>` : ''}
</div>
`;
}).join('');
@ -241,7 +268,7 @@ document.addEventListener('DOMContentLoaded', () => {
// Add click handlers to results
searchResults.querySelectorAll('.search-result-item').forEach(item => {
item.addEventListener('click', () => {
if (isTagMode) {
if (mode === 'tags') {
const tag = item.dataset.tag;
if (tag) {
// Navigate to tag search
@ -257,26 +284,42 @@ document.addEventListener('DOMContentLoaded', () => {
});
}
// Escape HTML to prevent XSS
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Perform live search
async function performSearch(query) {
const tags = fetchTags();
const normalizedQuery = (query || '').trim();
if (!query || query.length === 0) {
// Show all tags when empty
renderResults(tags.slice(0, 15), '', true);
if (searchMode === 'tags') {
const tags = fetchTags();
if (!normalizedQuery) {
renderResults(tags.slice(0, 15), '', 'tags');
return;
}
const tagResults = tags.filter(tag => fuzzyMatch(tag, normalizedQuery));
renderResults(tagResults, normalizedQuery, 'tags');
return;
}
// Filter tags with fuzzy matching
const results = tags.filter(tag => fuzzyMatch(tag, query));
renderResults(results, query, true);
const bookmarks = await fetchBookmarks();
const searchPool = searchMode === 'notes' ? bookmarks.filter(item => item.isNote) : bookmarks;
if (!normalizedQuery) {
renderResults(searchPool.slice(0, 12), '', searchMode);
return;
}
const results = searchPool.filter((item) => {
const haystack = [
item.title,
item.url,
item.description,
(item.tags || []).join(' ')
].join(' ').toLowerCase();
return haystack.includes(normalizedQuery.toLowerCase());
});
renderResults(results, normalizedQuery, searchMode);
}
// Update selected result
@ -318,15 +361,12 @@ document.addEventListener('DOMContentLoaded', () => {
return false;
}
function openSearch() {
function openSearch(mode = 'search') {
searchOverlay?.classList.add('show');
selectedIndex = -1;
setTimeout(() => {
setSearchMode(mode);
searchModalInput?.focus();
// Trigger initial search if there's existing text
if (searchModalInput?.value) {
performSearch(searchModalInput.value);
}
}, 100);
}
@ -337,20 +377,30 @@ document.addEventListener('DOMContentLoaded', () => {
// Toggle search mode (tags vs search)
function setSearchMode(mode) {
if (!['search', 'tags', 'notes'].includes(mode)) {
mode = 'search';
}
searchMode = mode;
searchTagsBtn?.classList.toggle('active', mode === 'tags');
searchAllBtn?.classList.toggle('active', mode === 'search');
searchTagsBtn?.classList.toggle('active', mode === 'tags');
searchNotesBtn?.classList.toggle('active', mode === 'notes');
searchAllBtn?.setAttribute('aria-pressed', String(mode === 'search'));
searchTagsBtn?.setAttribute('aria-pressed', String(mode === 'tags'));
searchNotesBtn?.setAttribute('aria-pressed', String(mode === 'notes'));
if (searchModalInput) {
searchModalInput.name = mode === 'tags' ? 'searchtags' : 'searchterm';
searchModalInput.placeholder = getSearchHint(mode);
// Re-run search with new mode
performSearch(searchModalInput.value);
}
}
searchToggleBtns.forEach((btn) => {
btn.addEventListener('click', openSearch);
btn.addEventListener('click', () => openSearch('search'));
});
// Close search on overlay click
@ -367,7 +417,25 @@ document.addEventListener('DOMContentLoaded', () => {
});
searchAllBtn?.addEventListener('click', (e) => {
// Only prevent default if not submitting
e.preventDefault();
setSearchMode('search');
});
searchNotesBtn?.addEventListener('click', (e) => {
e.preventDefault();
setSearchMode('notes');
});
searchForm?.addEventListener('submit', (e) => {
if (searchMode === 'notes') {
e.preventDefault();
if (selectedIndex < 0) {
updateSelection(0);
}
navigateToSelected();
return;
}
if (selectedIndex >= 0) {
e.preventDefault();
navigateToSelected();
@ -425,6 +493,10 @@ document.addEventListener('DOMContentLoaded', () => {
// If an item is selected, navigate to it
if (selectedIndex >= 0 && navigateToSelected()) {
e.preventDefault();
} else if (searchMode === 'notes') {
e.preventDefault();
updateSelection(0);
navigateToSelected();
}
// Otherwise, submit the form normally
break;
@ -435,14 +507,19 @@ document.addEventListener('DOMContentLoaded', () => {
// S to open search (when not typing)
if (!isTyping && !e.ctrlKey && !e.metaKey && !e.altKey && (e.key === 's' || e.key === 'S')) {
e.preventDefault();
openSearch();
openSearch('search');
}
// T to open search directly in tags mode (when not typing)
if (!isTyping && !e.ctrlKey && !e.metaKey && !e.altKey && (e.key === 't' || e.key === 'T')) {
e.preventDefault();
openSearch();
setSearchMode('tags');
openSearch('tags');
}
// N to open search directly in notes mode (when not typing)
if (!isTyping && !e.ctrlKey && !e.metaKey && !e.altKey && (e.key === 'n' || e.key === 'N')) {
e.preventDefault();
openSearch('notes');
}
});

View File

@ -12,6 +12,10 @@
{loop="$plugin_start_zone"}
{$value}
{/loop}
{$active_search_tags=[]}
{if="!empty($search_tags)"}
{$active_search_tags=tags_str2array($search_tags, $tags_separator)}
{/if}
<!-- {* ----- toolbar ----- *} -->
<div class="content-toolbar">
<div class="toolbar-left">{include="linklist.paging"}</div>
@ -32,8 +36,7 @@
{if="!empty($search_tags)"}
<div class="search-results-header">
<span class="search-count">{$result_count} résultat(s) tagué(s)</span>
{$exploded_tags=tags_str2array($search_tags, $tags_separator)}
{loop="$exploded_tags"}
{loop="$active_search_tags"}
<span class="search-tag-chip">{$value} <a href="{$base_path}/remove-tag/{function="
urlencode($value)"}" class="search-tag-close" title="Retirer le tag"
aria-label="Retirer le tag {$value}"><i class="mdi mdi-close"
@ -94,7 +97,7 @@
{if="$value.description"}<div class="link-description">{$value.description}</div>{/if}
<div class="link-footer">
<div class="link-tag-list">
{loop="$value.taglist"}<span class="link-tag" data-tag="{$value}"><a
{loop="$value.taglist"}<span class="link-tag{if="in_array($value, $active_search_tags)"} is-search-match{/if}" data-tag="{$value}"><a
class="link-tag-link" href="{$base_path}/add-tag/{$value|urlencode}">{$value}</a>{if="$is_logged_in"}<button type="button" class="tag-remove-btn" data-tag="{$value}" aria-label="Supprimer le tag {$value}" title="Supprimer">&times;</button>{/if}</span>{/loop}
</div>
<div class="link-actions">

View File

@ -267,11 +267,15 @@ Bookmarklet detection logic
<input type="text" name="searchterm" class="search-modal-input" id="search-modal-input"
placeholder="Type to search..." autocomplete="off" value="{if="isset($search_term)"}{$search_term}{/if}">
<div class="search-modal-actions">
<button type="button" class="search-pill-btn search-pill-notes" id="search-notes-btn" aria-pressed="false">
<i class="mdi mdi-note-text-outline"></i>
<span>notes</span>
</button>
<button type="button" class="search-pill-btn search-pill-tags" id="search-tags-btn">
<i class="mdi mdi-tag-outline"></i>
<span>tags</span>
</button>
<button type="submit" class="search-pill-btn search-pill-search active" id="search-all-btn">
<button type="button" class="search-pill-btn search-pill-search active" id="search-all-btn" aria-pressed="true">
<i class="mdi mdi-magnify"></i>
<span>search</span>
</button>
@ -286,7 +290,9 @@ Bookmarklet detection logic
</div>
<div class="search-footer">
<span class="search-footer-hint">
<kbd class="search-kbd">S</kbd> global
<kbd class="search-kbd">T</kbd> tags
<kbd class="search-kbd">N</kbd> notes
<kbd class="search-kbd"></kbd><kbd class="search-kbd"></kbd> to navigate
<kbd class="search-kbd">Enter</kbd> to select
<kbd class="search-kbd">ESC</kbd> to close