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 searchResults = document.getElementById('search-results');
|
||||||
const searchTagsBtn = document.getElementById('search-tags-btn');
|
const searchTagsBtn = document.getElementById('search-tags-btn');
|
||||||
const searchAllBtn = document.getElementById('search-all-btn');
|
const searchAllBtn = document.getElementById('search-all-btn');
|
||||||
|
const searchNotesBtn = document.getElementById('search-notes-btn');
|
||||||
const searchForm = document.getElementById('search-form');
|
const searchForm = document.getElementById('search-form');
|
||||||
|
|
||||||
let searchMode = 'search'; // 'search' or 'tags'
|
let searchMode = 'search'; // 'search' | 'tags' | 'notes'
|
||||||
let selectedIndex = -1;
|
let selectedIndex = -1;
|
||||||
let searchTimeout = null;
|
let searchTimeout = null;
|
||||||
let cachedBookmarks = null;
|
let cachedBookmarks = null;
|
||||||
@ -167,9 +168,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
if (linkElements.length > 0) {
|
if (linkElements.length > 0) {
|
||||||
cachedBookmarks = Array.from(linkElements).map(el => ({
|
cachedBookmarks = Array.from(linkElements).map(el => ({
|
||||||
id: el.dataset.id,
|
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 || '',
|
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 || '',
|
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 || ''
|
description: el.querySelector('.link-description')?.textContent || ''
|
||||||
}));
|
}));
|
||||||
return cachedBookmarks;
|
return cachedBookmarks;
|
||||||
@ -181,21 +183,32 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render search results (tags or bookmarks)
|
function getSearchHint(mode) {
|
||||||
function renderResults(results, query, isTagMode = false) {
|
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 (!searchResults) return;
|
||||||
|
|
||||||
if (results.length === 0) {
|
if (results.length === 0) {
|
||||||
if (query && query.length > 0) {
|
if (query && query.length > 0) {
|
||||||
|
const scopeLabel = mode === 'tags' ? 'tags' : (mode === 'notes' ? 'notes' : 'bookmarks/notes');
|
||||||
searchResults.innerHTML = `
|
searchResults.innerHTML = `
|
||||||
<div class="search-no-results">
|
<div class="search-no-results">
|
||||||
No results found for "<strong>${escapeHtml(query)}</strong>"
|
No ${scopeLabel} found for "<strong>${escapeHtml(query)}</strong>"
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
searchResults.innerHTML = `
|
searchResults.innerHTML = `
|
||||||
<div class="search-results-empty">
|
<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>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -203,7 +216,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let html;
|
let html;
|
||||||
if (isTagMode) {
|
if (mode === 'tags') {
|
||||||
// Render tags
|
// Render tags
|
||||||
html = results.slice(0, 15).map((tag, index) => {
|
html = results.slice(0, 15).map((tag, index) => {
|
||||||
const highlightedTag = highlightMatch(escapeHtml(tag), query);
|
const highlightedTag = highlightMatch(escapeHtml(tag), query);
|
||||||
@ -219,18 +232,32 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
`;
|
`;
|
||||||
}).join('');
|
}).join('');
|
||||||
} else {
|
} else {
|
||||||
// Render bookmarks
|
// Render bookmarks or notes
|
||||||
html = results.slice(0, 10).map((item, index) => {
|
html = results.slice(0, 12).map((item, index) => {
|
||||||
const highlightedTitle = highlightMatch(escapeHtml(item.title), query);
|
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 `
|
return `
|
||||||
<div class="search-result-item${index === selectedIndex ? ' selected' : ''}"
|
<div class="search-result-item${index === selectedIndex ? ' selected' : ''}"
|
||||||
data-index="${index}"
|
data-index="${index}"
|
||||||
data-url="${escapeHtml(item.url)}"
|
data-url="${escapeHtml(item.url)}"
|
||||||
data-id="${item.id}">
|
data-id="${item.id}">
|
||||||
<div class="search-result-item-content">
|
<div class="search-result-item-content">
|
||||||
<i class="mdi mdi-bookmark-outline search-result-icon"></i>
|
<i class="mdi ${typeIcon} search-result-icon"></i>
|
||||||
<span class="search-result-text">${highlightedTitle}</span>
|
<div class="search-result-main">
|
||||||
|
<span class="search-result-text">${highlightedTitle}</span>
|
||||||
|
<span class="search-result-kind">${typeLabel}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
${highlightedDescription ? `<div class="search-result-sub">${highlightedDescription}</div>` : ''}
|
||||||
|
${highlightedTags ? `<div class="search-result-tags">${highlightedTags}</div>` : ''}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}).join('');
|
}).join('');
|
||||||
@ -241,7 +268,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
// Add click handlers to results
|
// Add click handlers to results
|
||||||
searchResults.querySelectorAll('.search-result-item').forEach(item => {
|
searchResults.querySelectorAll('.search-result-item').forEach(item => {
|
||||||
item.addEventListener('click', () => {
|
item.addEventListener('click', () => {
|
||||||
if (isTagMode) {
|
if (mode === 'tags') {
|
||||||
const tag = item.dataset.tag;
|
const tag = item.dataset.tag;
|
||||||
if (tag) {
|
if (tag) {
|
||||||
// Navigate to tag search
|
// 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
|
// Perform live search
|
||||||
async function performSearch(query) {
|
async function performSearch(query) {
|
||||||
const tags = fetchTags();
|
const normalizedQuery = (query || '').trim();
|
||||||
|
|
||||||
if (!query || query.length === 0) {
|
if (searchMode === 'tags') {
|
||||||
// Show all tags when empty
|
const tags = fetchTags();
|
||||||
renderResults(tags.slice(0, 15), '', true);
|
if (!normalizedQuery) {
|
||||||
|
renderResults(tags.slice(0, 15), '', 'tags');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tagResults = tags.filter(tag => fuzzyMatch(tag, normalizedQuery));
|
||||||
|
renderResults(tagResults, normalizedQuery, 'tags');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter tags with fuzzy matching
|
const bookmarks = await fetchBookmarks();
|
||||||
const results = tags.filter(tag => fuzzyMatch(tag, query));
|
const searchPool = searchMode === 'notes' ? bookmarks.filter(item => item.isNote) : bookmarks;
|
||||||
renderResults(results, query, true);
|
|
||||||
|
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
|
// Update selected result
|
||||||
@ -318,15 +361,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function openSearch() {
|
function openSearch(mode = 'search') {
|
||||||
searchOverlay?.classList.add('show');
|
searchOverlay?.classList.add('show');
|
||||||
selectedIndex = -1;
|
selectedIndex = -1;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
setSearchMode(mode);
|
||||||
searchModalInput?.focus();
|
searchModalInput?.focus();
|
||||||
// Trigger initial search if there's existing text
|
|
||||||
if (searchModalInput?.value) {
|
|
||||||
performSearch(searchModalInput.value);
|
|
||||||
}
|
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -337,20 +377,30 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
// Toggle search mode (tags vs search)
|
// Toggle search mode (tags vs search)
|
||||||
function setSearchMode(mode) {
|
function setSearchMode(mode) {
|
||||||
|
if (!['search', 'tags', 'notes'].includes(mode)) {
|
||||||
|
mode = 'search';
|
||||||
|
}
|
||||||
|
|
||||||
searchMode = mode;
|
searchMode = mode;
|
||||||
|
|
||||||
searchTagsBtn?.classList.toggle('active', mode === 'tags');
|
|
||||||
searchAllBtn?.classList.toggle('active', mode === 'search');
|
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) {
|
if (searchModalInput) {
|
||||||
searchModalInput.name = mode === 'tags' ? 'searchtags' : 'searchterm';
|
searchModalInput.name = mode === 'tags' ? 'searchtags' : 'searchterm';
|
||||||
|
searchModalInput.placeholder = getSearchHint(mode);
|
||||||
// Re-run search with new mode
|
// Re-run search with new mode
|
||||||
performSearch(searchModalInput.value);
|
performSearch(searchModalInput.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
searchToggleBtns.forEach((btn) => {
|
searchToggleBtns.forEach((btn) => {
|
||||||
btn.addEventListener('click', openSearch);
|
btn.addEventListener('click', () => openSearch('search'));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Close search on overlay click
|
// Close search on overlay click
|
||||||
@ -367,7 +417,25 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
searchAllBtn?.addEventListener('click', (e) => {
|
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) {
|
if (selectedIndex >= 0) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
navigateToSelected();
|
navigateToSelected();
|
||||||
@ -425,6 +493,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
// If an item is selected, navigate to it
|
// If an item is selected, navigate to it
|
||||||
if (selectedIndex >= 0 && navigateToSelected()) {
|
if (selectedIndex >= 0 && navigateToSelected()) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
} else if (searchMode === 'notes') {
|
||||||
|
e.preventDefault();
|
||||||
|
updateSelection(0);
|
||||||
|
navigateToSelected();
|
||||||
}
|
}
|
||||||
// Otherwise, submit the form normally
|
// Otherwise, submit the form normally
|
||||||
break;
|
break;
|
||||||
@ -435,14 +507,19 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
// S to open search (when not typing)
|
// S to open search (when not typing)
|
||||||
if (!isTyping && !e.ctrlKey && !e.metaKey && !e.altKey && (e.key === 's' || e.key === 'S')) {
|
if (!isTyping && !e.ctrlKey && !e.metaKey && !e.altKey && (e.key === 's' || e.key === 'S')) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
openSearch();
|
openSearch('search');
|
||||||
}
|
}
|
||||||
|
|
||||||
// T to open search directly in tags mode (when not typing)
|
// 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')) {
|
if (!isTyping && !e.ctrlKey && !e.metaKey && !e.altKey && (e.key === 't' || e.key === 'T')) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
openSearch();
|
openSearch('tags');
|
||||||
setSearchMode('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"}
|
{loop="$plugin_start_zone"}
|
||||||
{$value}
|
{$value}
|
||||||
{/loop}
|
{/loop}
|
||||||
|
{$active_search_tags=[]}
|
||||||
|
{if="!empty($search_tags)"}
|
||||||
|
{$active_search_tags=tags_str2array($search_tags, $tags_separator)}
|
||||||
|
{/if}
|
||||||
<!-- {* ----- toolbar ----- *} -->
|
<!-- {* ----- toolbar ----- *} -->
|
||||||
<div class="content-toolbar">
|
<div class="content-toolbar">
|
||||||
<div class="toolbar-left">{include="linklist.paging"}</div>
|
<div class="toolbar-left">{include="linklist.paging"}</div>
|
||||||
@ -32,8 +36,7 @@
|
|||||||
{if="!empty($search_tags)"}
|
{if="!empty($search_tags)"}
|
||||||
<div class="search-results-header">
|
<div class="search-results-header">
|
||||||
<span class="search-count">{$result_count} résultat(s) tagué(s)</span>
|
<span class="search-count">{$result_count} résultat(s) tagué(s)</span>
|
||||||
{$exploded_tags=tags_str2array($search_tags, $tags_separator)}
|
{loop="$active_search_tags"}
|
||||||
{loop="$exploded_tags"}
|
|
||||||
<span class="search-tag-chip">{$value} <a href="{$base_path}/remove-tag/{function="
|
<span class="search-tag-chip">{$value} <a href="{$base_path}/remove-tag/{function="
|
||||||
urlencode($value)"}" class="search-tag-close" title="Retirer le tag"
|
urlencode($value)"}" class="search-tag-close" title="Retirer le tag"
|
||||||
aria-label="Retirer le tag {$value}"><i class="mdi mdi-close"
|
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}
|
{if="$value.description"}<div class="link-description">{$value.description}</div>{/if}
|
||||||
<div class="link-footer">
|
<div class="link-footer">
|
||||||
<div class="link-tag-list">
|
<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}
|
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>
|
||||||
<div class="link-actions">
|
<div class="link-actions">
|
||||||
|
|||||||
@ -267,11 +267,15 @@ Bookmarklet detection logic
|
|||||||
<input type="text" name="searchterm" class="search-modal-input" id="search-modal-input"
|
<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}">
|
placeholder="Type to search..." autocomplete="off" value="{if="isset($search_term)"}{$search_term}{/if}">
|
||||||
<div class="search-modal-actions">
|
<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">
|
<button type="button" class="search-pill-btn search-pill-tags" id="search-tags-btn">
|
||||||
<i class="mdi mdi-tag-outline"></i>
|
<i class="mdi mdi-tag-outline"></i>
|
||||||
<span>tags</span>
|
<span>tags</span>
|
||||||
</button>
|
</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>
|
<i class="mdi mdi-magnify"></i>
|
||||||
<span>search</span>
|
<span>search</span>
|
||||||
</button>
|
</button>
|
||||||
@ -286,7 +290,9 @@ Bookmarklet detection logic
|
|||||||
</div>
|
</div>
|
||||||
<div class="search-footer">
|
<div class="search-footer">
|
||||||
<span class="search-footer-hint">
|
<span class="search-footer-hint">
|
||||||
|
<kbd class="search-kbd">S</kbd> global
|
||||||
<kbd class="search-kbd">T</kbd> tags
|
<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">↑</kbd><kbd class="search-kbd">↓</kbd> to navigate
|
||||||
<kbd class="search-kbd">Enter</kbd> to select
|
<kbd class="search-kbd">Enter</kbd> to select
|
||||||
<kbd class="search-kbd">ESC</kbd> to close
|
<kbd class="search-kbd">ESC</kbd> to close
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user