feat: refactorer l'interface d'édition de bookmarks avec UI sombre raffinée, éditeur Markdown Toast UI intégré, système de tags interactif avec pills et suppression, grille de toggles pour read later/note/private, variables CSS personnalisées pour thème cohérent, et amélioration responsive avec grid layouts optimisés et support mobile complet

This commit is contained in:
2026-02-18 13:29:45 -05:00
parent 6f58f6cd67
commit 5b6b0fd163
4 changed files with 651 additions and 19 deletions
+250 -2
View File
@@ -338,7 +338,16 @@ document.addEventListener('DOMContentLoaded', () => {
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
const isSearchOpen = searchOverlay?.classList.contains('show');
const isTyping = e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA';
const target = e.target;
const isTyping = Boolean(
target && (
target.tagName === 'INPUT' ||
target.tagName === 'TEXTAREA' ||
target.tagName === 'SELECT' ||
target.isContentEditable ||
(typeof target.closest === 'function' && target.closest('.toastui-editor-defaultUI, .toastui-editor-main, .toastui-editor-contents, .CodeMirror'))
)
);
// Handle ESC - always close search/filter
if (e.key === 'Escape') {
@@ -373,7 +382,7 @@ document.addEventListener('DOMContentLoaded', () => {
}
// S to open search (when not typing)
if (!isTyping && (e.key === 's' || e.key === 'S')) {
if (!isTyping && !e.ctrlKey && !e.metaKey && !e.altKey && (e.key === 's' || e.key === 'S')) {
e.preventDefault();
openSearch();
}
@@ -1145,6 +1154,245 @@ document.addEventListener('DOMContentLoaded', () => {
}
});
// ===== Bookmark Editor Form (Markdown + Tags UI) =====
function initBookmarkEditorForms() {
const forms = document.querySelectorAll('.bookmark-editor-form[data-batch-mode="0"]');
if (!forms.length) return;
const normalizeTag = (value) => value.trim().replace(/\s+/g, '-');
forms.forEach((form) => {
form.classList.add('is-enhanced');
const descriptionSource = form.querySelector('.bookmark-editor-source');
const editorMount = form.querySelector('.bookmark-markdown-editor');
const hiddenTagsInput = form.querySelector('.bookmark-tags-hidden-input');
const tagsInputContainer = form.querySelector('.bookmark-tags-input');
const tagsList = form.querySelector('.bookmark-tags-list');
const tagsTextInput = form.querySelector('.bookmark-tags-text-input');
const readLaterCheckbox = form.querySelector('.bookmark-toggle-readlater');
const noteCheckbox = form.querySelector('.bookmark-toggle-note');
const isDarkTheme = document.documentElement.getAttribute('data-theme') === 'dark';
// Markdown editor
if (descriptionSource && editorMount && window.toastui && window.toastui.Editor) {
const previewStyle = window.innerWidth < 992 ? 'tab' : 'vertical';
const markdownEditor = new window.toastui.Editor({
el: editorMount,
height: '320px',
initialEditType: 'markdown',
previewStyle,
initialValue: descriptionSource.value || '',
placeholder: descriptionSource.getAttribute('placeholder') || '',
theme: isDarkTheme ? 'dark' : undefined,
usageStatistics: false,
});
form.addEventListener('submit', () => {
descriptionSource.value = markdownEditor.getMarkdown();
});
} else if (editorMount) {
editorMount.style.display = 'none';
if (descriptionSource) {
descriptionSource.style.display = 'block';
}
}
// Pill tags
if (!hiddenTagsInput || !tagsList || !tagsTextInput) return;
const availableTags = (hiddenTagsInput.getAttribute('data-tag-options') || '')
.split(',')
.map((tag) => normalizeTag(tag))
.filter(Boolean);
const getSuggestionList = () => availableTags
.filter((tag) => !tags.some((existing) => existing.toLowerCase() === tag.toLowerCase()));
let tagsAutocomplete = null;
const refreshAutocomplete = () => {
if (!tagsAutocomplete) return;
tagsAutocomplete.list = getSuggestionList();
};
if (tagsInputContainer) {
tagsInputContainer.addEventListener('click', () => tagsTextInput.focus());
}
let tags = hiddenTagsInput.value
.split(/[\s,]+/)
.map((tag) => normalizeTag(tag))
.filter(Boolean);
const updateHiddenTags = () => {
hiddenTagsInput.value = tags.join(' ');
};
const syncReadLaterCheckbox = () => {
if (!readLaterCheckbox) return;
readLaterCheckbox.checked = tags.some((tag) => /^(readlater|toread)$/i.test(tag));
};
const syncNoteCheckbox = () => {
if (!noteCheckbox) return;
noteCheckbox.checked = tags.some((tag) => /^note$/i.test(tag));
};
const addTag = (rawTag) => {
const tag = normalizeTag(rawTag);
if (!tag) return;
const exists = tags.some((existing) => existing.toLowerCase() === tag.toLowerCase());
if (!exists) {
tags.push(tag);
updateHiddenTags();
syncReadLaterCheckbox();
syncNoteCheckbox();
renderTags();
refreshAutocomplete();
}
};
const removeTag = (tagToRemove) => {
tags = tags.filter((tag) => tag.toLowerCase() !== tagToRemove.toLowerCase());
updateHiddenTags();
syncReadLaterCheckbox();
syncNoteCheckbox();
renderTags();
refreshAutocomplete();
};
const commitInputValue = () => {
const value = tagsTextInput.value;
if (!value.trim()) return;
value
.split(/[\s,]+/)
.map((tag) => tag.trim())
.filter(Boolean)
.forEach(addTag);
tagsTextInput.value = '';
refreshAutocomplete();
};
function renderTags() {
tagsList.innerHTML = '';
tags.forEach((tag) => {
const pill = document.createElement('span');
pill.className = 'bookmark-tag-pill';
pill.setAttribute('data-tag', tag);
const label = document.createElement('span');
label.className = 'bookmark-tag-label';
label.textContent = tag;
const removeBtn = document.createElement('button');
removeBtn.type = 'button';
removeBtn.className = 'bookmark-tag-remove';
removeBtn.setAttribute('aria-label', `Remove tag ${tag}`);
removeBtn.innerHTML = '<i class="mdi mdi-close" aria-hidden="true"></i>';
removeBtn.addEventListener('click', () => removeTag(tag));
pill.appendChild(label);
pill.appendChild(removeBtn);
tagsList.appendChild(pill);
});
}
tagsTextInput.addEventListener('keydown', (event) => {
const hasOpenAutocomplete = Boolean(tagsAutocomplete && tagsAutocomplete.ul && !tagsAutocomplete.ul.hasAttribute('hidden'));
const shouldCommit = event.key === ' ' || event.key === ',' || (event.key === 'Enter' && !hasOpenAutocomplete);
if (shouldCommit) {
event.preventDefault();
commitInputValue();
return;
}
if (event.key === 'Backspace' && !tagsTextInput.value && tags.length) {
removeTag(tags[tags.length - 1]);
}
});
tagsTextInput.addEventListener('blur', commitInputValue);
tagsTextInput.addEventListener('paste', (event) => {
const pasted = event.clipboardData ? event.clipboardData.getData('text') : '';
if (!pasted) return;
event.preventDefault();
pasted
.split(/[\s,]+/)
.map((tag) => tag.trim())
.filter(Boolean)
.forEach(addTag);
tagsTextInput.value = '';
});
if (readLaterCheckbox) {
readLaterCheckbox.addEventListener('change', () => {
tags = tags.filter((tag) => !/^(readlater|toread)$/i.test(tag));
if (readLaterCheckbox.checked) {
tags.push('readlater');
}
updateHiddenTags();
syncNoteCheckbox();
renderTags();
refreshAutocomplete();
});
}
if (noteCheckbox) {
noteCheckbox.addEventListener('change', () => {
tags = tags.filter((tag) => !/^note$/i.test(tag));
if (noteCheckbox.checked) {
tags.push('note');
}
updateHiddenTags();
syncReadLaterCheckbox();
renderTags();
refreshAutocomplete();
});
}
if (window.Awesomplete && availableTags.length) {
tagsAutocomplete = new window.Awesomplete(tagsTextInput, {
list: getSuggestionList(),
minChars: 1,
maxItems: 8,
autoFirst: true,
sort: false,
filter: window.Awesomplete.FILTER_CONTAINS,
});
tagsTextInput.addEventListener('input', () => {
refreshAutocomplete();
tagsAutocomplete.evaluate();
});
tagsTextInput.addEventListener('awesomplete-selectcomplete', commitInputValue);
}
form.addEventListener('submit', commitInputValue);
updateHiddenTags();
syncReadLaterCheckbox();
syncNoteCheckbox();
renderTags();
refreshAutocomplete();
});
}
initBookmarkEditorForms();
// ===== Persistent Media Player (Popup via Blob URL) =====
// Audio plays in a separate popup window that survives page navigation.
// The popup HTML is generated as a Blob URL (no server file needed).