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:
+250
-2
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user