UX: Refonte complète - Notes en expansion inline (Google Keep style)

Implémentation d'un système d'expansion inline des notes au lieu de modals:
- Les notes s'agrandissent directement dans la grille au clic
- Mode compact: prévisualisation avec titre, contenu, image, premiers 3 todos
- Mode étendu: édition complète avec titre, contenu, todos, image, actions
- Auto-save avec debounce (1s) sur les modifications
- Filtres Actives/Archivées pour organiser les notes
- Système d'archivage avec endpoint API dédié

Fonctionnalités inline:
- expandNote(noteId): agrandir une note
- collapseNote(): réduire la note étendue
- saveNoteInline(noteId): sauvegarde auto avec debounce
- archiveNote(noteId, archived): archiver/désarchiver
- deleteNoteInline(noteId): supprimer depuis l'inline
- Gestion complète des todos inline (add, toggle, delete)
- Upload/suppression d'images inline
- Fermeture avec Escape

Event delegation pour performances:
- Tous les événements gérés via delegation sur #notesGrid
- Suppression de tous les attributs onclick/onchange (CSP compliant)
- Click handlers: expand, collapse, archive, delete, image, todos
- Change handlers: checkbox todos inline

CSS moderne avec shadows:
- .note-card.expanded: carte agrandie sur toute la largeur
- Styles inline pour titre, contenu, todos, image, footer
- Transitions fluides et hover effects
- View filters avec background pills
- Responsive grid layout

Backend prêt:
- GET /api/notes?archived=true/false
- PUT /api/notes/:id/archive
- Todos chargés avec chaque note
- Migration auto colonne 'archived'
This commit is contained in:
Claude committed 2025-11-11 15:26:19 +00:00
1 parent c6484702e2
commit 17a088ec05
3 files changed
+797 -26

No files matched your search

+307 -3
View File
@@ -8,18 +8,32 @@
background: var(--color-card);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
padding: var(--spacing-lg);
cursor: pointer;
transition: all var(--transition-base);
box-shadow: var(--shadow-sm);
}
.note-card:hover {
.note-card-compact {
padding: var(--spacing-lg);
cursor: pointer;
}
.note-card-compact:hover {
box-shadow: var(--shadow-hover);
transform: translateY(-2px);
border-color: var(--color-accent);
}
.note-card.expanded {
grid-column: 1 / -1;
cursor: default;
box-shadow: var(--shadow-lg);
border-color: var(--color-accent);
}
.note-card.expanded:hover {
transform: none;
}
.note-card-title {
font-size: var(--font-size-lg);
font-weight: var(--font-weight-semibold);
@@ -71,6 +85,296 @@
margin-bottom: var(--spacing-md);
}
.note-todos-preview {
margin-top: var(--spacing-md);
margin-bottom: var(--spacing-sm);
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
}
.todo-preview {
display: flex;
align-items: center;
gap: var(--spacing-sm);
font-size: var(--font-size-xs);
color: var(--color-text-secondary);
}
.todo-preview.completed .todo-text-preview {
text-decoration: line-through;
opacity: 0.6;
}
.todo-checkbox-preview {
font-size: var(--font-size-sm);
}
.todo-more {
font-size: var(--font-size-xs);
color: var(--color-text-secondary);
font-style: italic;
margin-top: var(--spacing-xs);
}
/* Inline expansion styles */
.note-card-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: var(--spacing-md);
padding: var(--spacing-lg);
border-bottom: 1px solid var(--color-border);
}
.note-title-input-inline {
flex: 1;
border: none;
font-size: var(--font-size-xl);
font-weight: var(--font-weight-bold);
color: var(--color-text-primary);
background: transparent;
padding: var(--spacing-sm) 0;
}
.note-title-input-inline:focus {
outline: none;
background: var(--color-background);
padding: var(--spacing-sm) var(--spacing-md);
border-radius: var(--radius-sm);
}
.note-actions {
display: flex;
gap: var(--spacing-xs);
flex-shrink: 0;
}
.btn-icon-small {
width: 32px;
height: 32px;
padding: 6px;
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
background: transparent;
color: var(--color-text-secondary);
cursor: pointer;
transition: all var(--transition-fast);
display: flex;
align-items: center;
justify-content: center;
}
.btn-icon-small:hover {
background: var(--color-hover);
color: var(--color-text-primary);
border-color: var(--color-accent);
}
.btn-icon-small svg {
width: 100%;
height: 100%;
}
.note-content-textarea-inline {
width: 100%;
min-height: 200px;
padding: var(--spacing-lg);
border: none;
font-size: var(--font-size-base);
color: var(--color-text-primary);
line-height: 1.8;
resize: vertical;
font-family: var(--font-family);
background: transparent;
}
.note-content-textarea-inline:focus {
outline: none;
background: var(--color-background);
}
.note-image-container-inline {
padding: 0 var(--spacing-lg);
position: relative;
margin-bottom: var(--spacing-md);
}
.btn-remove-image-inline {
position: absolute;
top: var(--spacing-sm);
right: calc(var(--spacing-lg) + var(--spacing-sm));
width: 28px;
height: 28px;
padding: 4px;
border: none;
border-radius: var(--radius-full);
background: rgba(0, 0, 0, 0.7);
color: white;
cursor: pointer;
font-size: var(--font-size-lg);
line-height: 1;
transition: all var(--transition-fast);
}
.btn-remove-image-inline:hover {
background: var(--color-danger);
}
.note-todos-inline {
padding: var(--spacing-lg);
border-top: 1px solid var(--color-border);
}
.note-todos-inline h4 {
font-size: var(--font-size-base);
font-weight: var(--font-weight-semibold);
color: var(--color-text-primary);
margin-bottom: var(--spacing-md);
}
.todos-list-inline {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
margin-bottom: var(--spacing-md);
}
.todo-item-inline {
display: flex;
align-items: center;
gap: var(--spacing-md);
padding: var(--spacing-sm);
background: var(--color-background);
border-radius: var(--radius-sm);
transition: all var(--transition-fast);
}
.todo-item-inline:hover {
background: var(--color-hover);
}
.todo-checkbox-inline {
width: 18px;
height: 18px;
border: 2px solid var(--color-border);
border-radius: 4px;
cursor: pointer;
transition: all var(--transition-fast);
flex-shrink: 0;
}
.todo-checkbox-inline:checked {
background: var(--color-accent);
border-color: var(--color-accent);
}
.todo-text-inline {
flex: 1;
font-size: var(--font-size-sm);
color: var(--color-text-primary);
line-height: 1.5;
}
.todo-text-inline.completed {
text-decoration: line-through;
color: var(--color-text-secondary);
}
.todo-delete-inline {
width: 24px;
height: 24px;
padding: 4px;
border: none;
border-radius: var(--radius-sm);
background: transparent;
color: var(--color-text-secondary);
cursor: pointer;
opacity: 0;
transition: all var(--transition-fast);
flex-shrink: 0;
}
.todo-item-inline:hover .todo-delete-inline {
opacity: 1;
}
.todo-delete-inline:hover {
background: var(--color-danger);
color: white;
}
.no-todos {
text-align: center;
color: var(--color-text-secondary);
font-size: var(--font-size-sm);
padding: var(--spacing-md);
font-style: italic;
}
.btn-add-todo-inline {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-sm) var(--spacing-md);
border: 1px dashed var(--color-border);
border-radius: var(--radius-sm);
background: transparent;
color: var(--color-text-secondary);
font-size: var(--font-size-sm);
font-weight: var(--font-weight-medium);
cursor: pointer;
transition: all var(--transition-fast);
width: 100%;
}
.btn-add-todo-inline:hover {
background: var(--color-hover);
border-color: var(--color-accent);
color: var(--color-text-primary);
}
.note-footer-inline {
display: flex;
align-items: center;
gap: var(--spacing-md);
padding: var(--spacing-lg);
border-top: 1px solid var(--color-border);
background: var(--color-background);
}
.note-meta-inline {
flex: 1;
font-size: var(--font-size-xs);
color: var(--color-text-secondary);
}
.btn-add-image-inline {
width: 36px;
height: 36px;
padding: 8px;
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
background: transparent;
color: var(--color-text-secondary);
cursor: pointer;
transition: all var(--transition-fast);
display: flex;
align-items: center;
justify-content: center;
}
.btn-add-image-inline:hover {
background: var(--color-hover);
color: var(--color-text-primary);
border-color: var(--color-accent);
}
.btn-add-image-inline svg {
width: 100%;
height: 100%;
}
/* ============================================
TODO BOX
============================================ */
+47
View File
@@ -244,6 +244,8 @@ body {
align-items: center;
justify-content: space-between;
margin-bottom: var(--spacing-lg);
gap: var(--spacing-lg);
flex-wrap: wrap;
}
.section-title {
@@ -252,6 +254,51 @@ body {
color: var(--color-text-primary);
}
.section-actions {
display: flex;
align-items: center;
gap: var(--spacing-md);
}
.view-filters {
display: flex;
gap: var(--spacing-xs);
background: var(--color-background);
border-radius: var(--radius-md);
padding: 4px;
}
.view-filter {
display: flex;
align-items: center;
gap: var(--spacing-xs);
padding: var(--spacing-sm) var(--spacing-md);
border: none;
border-radius: var(--radius-sm);
background: transparent;
color: var(--color-text-secondary);
font-size: var(--font-size-sm);
font-weight: var(--font-weight-medium);
cursor: pointer;
transition: all var(--transition-fast);
}
.view-filter svg {
width: 16px;
height: 16px;
}
.view-filter:hover {
background: var(--color-hover);
color: var(--color-text-primary);
}
.view-filter.active {
background: var(--color-card);
color: var(--color-accent);
box-shadow: var(--shadow-xs);
}
.btn-new-note {
display: flex;
align-items: center;
+443 -23
View File
@@ -174,8 +174,10 @@ const state = {
notes: [],
todos: [],
currentNote: null,
expandedNoteId: null,
filter: 'all',
searchQuery: '',
view: 'active',
user: null
};
@@ -241,7 +243,8 @@ async function initAuth() {
// ==================== NOTES ====================
async function loadNotes() {
try {
const response = await api.get('/api/notes');
const archived = state.view === 'archived';
const response = await api.get(`/api/notes?archived=${archived}`);
// S'assurer que response est un tableau
state.notes = Array.isArray(response) ? response : [];
renderNotes();
@@ -287,37 +290,369 @@ function renderNotes() {
function createNoteCard(note) {
const card = document.createElement('div');
card.className = 'note-card';
card.onclick = () => openNoteModal(note.id);
const isExpanded = state.expandedNoteId === note.id;
card.className = `note-card ${isExpanded ? 'expanded' : ''}`;
card.dataset.noteId = note.id;
let html = `<div class="note-card-title">${escapeHtml(note.title)}</div>`;
if (isExpanded) {
// Mode édition inline
card.innerHTML = `
<div class="note-card-header">
<input type="text" class="note-title-input-inline" value="${escapeHtml(note.title)}" placeholder="Titre" data-note-id="${note.id}">
<div class="note-actions">
<button class="btn-icon-small btn-archive-note" data-note-id="${note.id}" data-archived="${note.archived}" title="${note.archived ? 'Désarchiver' : 'Archiver'}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="21 8 21 21 3 21 3 8"></polyline>
<rect x="1" y="3" width="22" height="5"></rect>
</svg>
</button>
<button class="btn-icon-small btn-delete-note-inline" data-note-id="${note.id}" title="Supprimer">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="3 6 5 6 21 6"></polyline>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
</svg>
</button>
<button class="btn-icon-small btn-collapse-note" title="Fermer">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
</div>
<textarea class="note-content-textarea-inline" placeholder="Contenu..." data-note-id="${note.id}">${escapeHtml(note.content || '')}</textarea>
${note.image_filename ? `
<div class="note-image-container-inline">
<img src="/uploads/${note.image_filename}" class="note-card-image" alt="">
<button class="btn-remove-image-inline" data-note-id="${note.id}">✕</button>
</div>
` : ''}
<div class="note-todos-inline">
<h4>Todos</h4>
<div class="todos-list-inline" id="todos-${note.id}">
${renderInlineTodos(note.todos || [])}
</div>
<button class="btn-add-todo-inline" data-note-id="${note.id}">+ Ajouter une tâche</button>
</div>
<div class="note-footer-inline">
<button class="btn-add-image-inline" data-note-id="${note.id}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<circle cx="8.5" cy="8.5" r="1.5"></circle>
<polyline points="21 15 16 10 5 21"></polyline>
</svg>
</button>
<span class="note-meta-inline">${utils.formatDate(note.updated_at)}</span>
</div>
`;
if (note.content) {
html += `<div class="note-card-content">${escapeHtml(utils.truncate(note.content))}</div>`;
// Auto-save sur changement
setTimeout(() => {
const titleInput = card.querySelector('.note-title-input-inline');
const contentTextarea = card.querySelector('.note-content-textarea-inline');
if (titleInput) titleInput.oninput = utils.debounce(() => saveNoteInline(note.id), 1000);
if (contentTextarea) contentTextarea.oninput = utils.debounce(() => saveNoteInline(note.id), 1000);
}, 0);
} else {
// Mode compact
let html = `
<div class="note-card-compact" data-note-id="${note.id}">
<div class="note-card-title">${escapeHtml(note.title)}</div>
`;
if (note.content) {
html += `<div class="note-card-preview">${escapeHtml(utils.truncate(note.content, 100))}</div>`;
}
if (note.image_filename) {
html += `<img src="/uploads/${note.image_filename}" class="note-card-image" alt="">`;
}
// Afficher les todos
if (note.todos && note.todos.length > 0) {
const displayTodos = note.todos.slice(0, 3);
html += `<div class="note-todos-preview">`;
displayTodos.forEach(todo => {
html += `<div class="todo-preview ${todo.completed ? 'completed' : ''}">
<span class="todo-checkbox-preview">${todo.completed ? '☑' : '☐'}</span>
<span class="todo-text-preview">${escapeHtml(todo.text)}</span>
</div>`;
});
if (note.todos.length > 3) {
html += `<div class="todo-more">+${note.todos.length - 3} autre(s)</div>`;
}
html += `</div>`;
}
html += `
<div class="note-card-meta">
<span class="note-card-date">${utils.formatDate(note.updated_at)}</span>
${note.todos_count > 0 ? `<span class="note-card-todos-count">${note.todos_completed}/${note.todos_count}</span>` : ''}
</div>
</div>
`;
card.innerHTML = html;
}
if (note.image_filename) {
html += `<img src="/uploads/${note.image_filename}" class="note-card-image" alt="">`;
}
html += `<div class="note-card-meta">`;
html += `<span>${utils.formatDate(note.updated_at)}</span>`;
if (note.todos_count > 0) {
html += `<span class="note-card-badge">${note.todos_count} tasks</span>`;
}
html += `</div>`;
card.innerHTML = html;
return card;
}
function renderInlineTodos(todos) {
if (!todos || todos.length === 0) return '<p class="no-todos">Aucune tâche</p>';
return todos.map(todo => `
<div class="todo-item-inline" data-todo-inline-id="${todo.id}">
<input type="checkbox" class="todo-checkbox-inline" ${todo.completed ? 'checked' : ''} data-todo-inline-id="${todo.id}">
<span class="todo-text-inline ${todo.completed ? 'completed' : ''}">${escapeHtml(todo.text)}</span>
<button class="todo-delete-inline" data-todo-inline-id="${todo.id}">✕</button>
</div>
`).join('');
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// ==================== MODAL ====================
// ==================== INLINE NOTE EXPANSION ====================
function expandNote(noteId) {
state.expandedNoteId = noteId;
renderNotes();
}
function collapseNote() {
state.expandedNoteId = null;
renderNotes();
}
async function saveNoteInline(noteId) {
const card = document.querySelector(`.note-card[data-note-id="${noteId}"]`);
if (!card) return;
const titleInput = card.querySelector('.note-title-input-inline');
const contentTextarea = card.querySelector('.note-content-textarea-inline');
if (!titleInput) return;
try {
await api.put(`/api/notes/${noteId}`, {
title: titleInput.value,
content: contentTextarea ? contentTextarea.value : ''
});
// Update state without full reload
const noteIndex = state.notes.findIndex(n => n.id === noteId);
if (noteIndex !== -1) {
state.notes[noteIndex].title = titleInput.value;
state.notes[noteIndex].content = contentTextarea ? contentTextarea.value : '';
state.notes[noteIndex].updated_at = new Date().toISOString();
}
} catch (error) {
console.error('Erreur sauvegarde note:', error);
}
}
async function archiveNote(noteId, archived) {
try {
await api.put(`/api/notes/${noteId}/archive`, { archived });
collapseNote();
await loadNotes();
} catch (error) {
console.error('Erreur archivage note:', error);
alert('Erreur lors de l\'archivage de la note');
}
}
async function deleteNoteInline(noteId) {
const confirmed = await confirmDialog.show({
icon: '🗑️',
title: 'Supprimer cette note',
message: 'Êtes-vous sûr de vouloir supprimer cette note ?\n\nTous les todos associés seront également supprimés.',
okText: 'Supprimer'
});
if (!confirmed) return;
try {
await api.delete(`/api/notes/${noteId}`);
collapseNote();
await loadNotes();
} catch (error) {
console.error('Erreur suppression note:', error);
alert('Erreur lors de la suppression de la note');
}
}
async function addNoteTodoInline(noteId) {
const text = prompt('Texte du todo:');
if (!text || !text.trim()) return;
try {
await api.post(`/api/notes/${noteId}/todos`, { text: text.trim() });
// Recharger seulement cette note
const note = await api.get(`/api/notes/${noteId}`);
const noteIndex = state.notes.findIndex(n => n.id === noteId);
if (noteIndex !== -1) {
state.notes[noteIndex] = note;
}
// Re-render la liste des todos inline
const todosList = document.getElementById(`todos-${noteId}`);
if (todosList) {
todosList.innerHTML = renderInlineTodos(note.todos || []);
}
} catch (error) {
console.error('Erreur ajout todo:', error);
alert('Erreur lors de l\'ajout du todo');
}
}
async function toggleNoteTodoInline(todoId, completed) {
try {
await api.put(`/api/notes/todos/${todoId}`, { completed });
// Update UI optimistically
const todoItem = document.querySelector(`.todo-item-inline[data-todo-inline-id="${todoId}"]`);
if (todoItem) {
const textSpan = todoItem.querySelector('.todo-text-inline');
if (textSpan) {
if (completed) {
textSpan.classList.add('completed');
} else {
textSpan.classList.remove('completed');
}
}
}
} catch (error) {
console.error('Erreur toggle todo:', error);
}
}
async function deleteNoteTodoInline(todoId) {
const confirmed = await confirmDialog.show({
icon: '🗑️',
title: 'Supprimer ce todo',
message: 'Êtes-vous sûr de vouloir supprimer ce todo ?',
okText: 'Supprimer'
});
if (!confirmed) return;
try {
await api.delete(`/api/notes/todos/${todoId}`);
// Remove from DOM
const todoItem = document.querySelector(`.todo-item-inline[data-todo-inline-id="${todoId}"]`);
if (todoItem) {
todoItem.remove();
}
} catch (error) {
console.error('Erreur suppression todo:', error);
}
}
function triggerImageUpload(noteId) {
const imageInput = document.getElementById('imageInput');
if (imageInput) {
imageInput.dataset.noteId = noteId;
imageInput.click();
}
}
async function removeImageInline(noteId) {
const confirmed = await confirmDialog.show({
icon: '🖼️',
title: 'Supprimer l\'image',
message: 'Êtes-vous sûr de vouloir supprimer cette image ?',
okText: 'Supprimer'
});
if (!confirmed) return;
try {
await api.delete(`/api/notes/${noteId}/image`);
// Update state
const noteIndex = state.notes.findIndex(n => n.id === noteId);
if (noteIndex !== -1) {
state.notes[noteIndex].image_filename = null;
}
// Re-render expanded card
renderNotes();
} catch (error) {
console.error('Erreur suppression image:', error);
alert('Erreur lors de la suppression de l\'image');
}
}
async function handleImageUploadInline(event) {
const file = event.target.files[0];
const noteId = event.target.dataset.noteId;
if (!file || !noteId) return;
const formData = new FormData();
formData.append('image', file);
try {
const result = await api.uploadFile(`/api/notes/${noteId}/image`, formData);
// Update state
const noteIndex = state.notes.findIndex(n => n.id === parseInt(noteId));
if (noteIndex !== -1) {
state.notes[noteIndex].image_filename = result.filename;
}
// Re-render expanded card
renderNotes();
} catch (error) {
console.error('Erreur upload image:', error);
alert('Erreur lors de l\'upload de l\'image');
}
// Reset input
event.target.value = '';
event.target.dataset.noteId = '';
}
async function createNewNote() {
try {
const newNote = await api.post('/api/notes', {
title: 'Nouvelle note',
content: ''
});
// Reload notes
await loadNotes();
// Expand the new note
state.expandedNoteId = newNote.id;
renderNotes();
// Focus title input
setTimeout(() => {
const titleInput = document.querySelector('.note-title-input-inline');
if (titleInput) {
titleInput.focus();
titleInput.select();
}
}, 100);
} catch (error) {
console.error('Erreur création note:', error);
alert('Erreur lors de la création de la note');
}
}
function setNoteView(view) {
state.view = view;
document.querySelectorAll('.view-filter').forEach(btn => {
btn.classList.toggle('active', btn.dataset.view === view);
});
loadNotes();
}
// ==================== MODAL (OLD - DEPRECATED) ====================
let saveTimeout;
let currentNoteId = null;
@@ -838,9 +1173,14 @@ async function init() {
// Event listeners
const newNoteBtn = document.getElementById('newNoteBtn');
if (newNoteBtn) {
newNoteBtn.addEventListener('click', () => openNoteModal());
newNoteBtn.addEventListener('click', createNewNote);
}
// View filters (Active/Archived)
document.querySelectorAll('.view-filter').forEach(btn => {
btn.addEventListener('click', () => setNoteView(btn.dataset.view));
});
// Admin button
const adminBtn = document.getElementById('adminBtn');
if (adminBtn) {
@@ -891,7 +1231,7 @@ async function init() {
const imageInput = document.getElementById('imageInput');
if (imageInput) {
imageInput.addEventListener('change', handleImageUpload);
imageInput.addEventListener('change', handleImageUploadInline);
}
const removeImage = document.getElementById('removeImage');
@@ -986,11 +1326,91 @@ async function init() {
});
}
// Event delegation for notes grid
const notesGrid = document.getElementById('notesGrid');
if (notesGrid) {
// Click on compact note to expand
notesGrid.addEventListener('click', (e) => {
const compactNote = e.target.closest('.note-card-compact');
if (compactNote) {
const noteId = parseInt(compactNote.dataset.noteId);
expandNote(noteId);
return;
}
// Collapse note button
if (e.target.closest('.btn-collapse-note')) {
collapseNote();
return;
}
// Archive note button
const archiveBtn = e.target.closest('.btn-archive-note');
if (archiveBtn) {
const noteId = parseInt(archiveBtn.dataset.noteId);
const archived = archiveBtn.dataset.archived === 'true';
archiveNote(noteId, !archived);
return;
}
// Delete note button
const deleteBtn = e.target.closest('.btn-delete-note-inline');
if (deleteBtn) {
const noteId = parseInt(deleteBtn.dataset.noteId);
deleteNoteInline(noteId);
return;
}
// Add image button
const addImageBtn = e.target.closest('.btn-add-image-inline');
if (addImageBtn) {
const noteId = parseInt(addImageBtn.dataset.noteId);
triggerImageUpload(noteId);
return;
}
// Remove image button
const removeImageBtn = e.target.closest('.btn-remove-image-inline');
if (removeImageBtn) {
const noteId = parseInt(removeImageBtn.dataset.noteId);
removeImageInline(noteId);
return;
}
// Add todo button
const addTodoBtn = e.target.closest('.btn-add-todo-inline');
if (addTodoBtn) {
const noteId = parseInt(addTodoBtn.dataset.noteId);
addNoteTodoInline(noteId);
return;
}
// Delete todo inline button
if (e.target.classList.contains('todo-delete-inline')) {
const todoId = parseInt(e.target.dataset.todoInlineId);
deleteNoteTodoInline(todoId);
return;
}
});
// Checkbox change for inline todos
notesGrid.addEventListener('change', (e) => {
if (e.target.classList.contains('todo-checkbox-inline')) {
const todoId = parseInt(e.target.dataset.todoInlineId);
toggleNoteTodoInline(todoId, e.target.checked);
}
});
}
// Close modal on Escape
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closeNoteModal();
document.getElementById('adminModal').style.display = 'none';
if (state.expandedNoteId) {
collapseNote();
} else {
closeNoteModal();
document.getElementById('adminModal').style.display = 'none';
}
}
});
}