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:
parent
1ea13ff16b
commit
83d9a7425e
File diff suppressed because it is too large
Load Diff
@ -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');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -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">×</button>{/if}</span>{/loop}
|
||||
</div>
|
||||
<div class="link-actions">
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user