feat: implémenter système Todo complet avec bouton segmenté sidebar (bookmark/note/todo), toggle Todo dans formulaire édition avec gestion émoji automatique, masquage champ URL pour notes/todos, tag shaarli-todo unifié (remplacement todo), déduplication tags case-insensitive, correction tag archive (shaarli-archiver→shaarli-archive), ajout shaarli-todo aux tags cachés par défaut, styles bouton segmenté avec gradient bleu et hover states, et support responsive mobile avec masquage tex

This commit is contained in:
Bruno Charest 2026-02-20 16:33:40 -05:00
parent adb2564153
commit fb5254445f
7 changed files with 167 additions and 15 deletions

View File

@ -278,6 +278,58 @@ a:focus:not(:focus-visible) {
color: white;
}
/* Sidebar Segmented Add Button */
.sidebar-add-segmented {
display: flex;
background: linear-gradient(135deg, #60a5fa 0%, #3b82f6 100%);
border-radius: 0.75rem;
overflow: hidden;
box-shadow: 0 4px 6px -1px rgba(59, 130, 246, 0.3), 0 2px 4px -1px rgba(59, 130, 246, 0.2);
}
.sidebar-add-segment {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
flex: 1;
padding: 0.875rem 0.5rem;
background: transparent;
color: white;
font-weight: 600;
font-size: 0.9rem;
text-decoration: none;
transition: all 0.2s ease;
border-right: 1px solid rgba(255, 255, 255, 0.25);
}
.sidebar-add-segment:last-child {
border-right: none;
}
.sidebar-add-segment:hover {
background: rgba(255, 255, 255, 0.15);
color: white;
}
.sidebar-add-segment i {
font-size: 1.4rem;
}
.sidebar-add-segment span {
display: inline;
}
/* Responsive: hide text on small screens */
@media (max-width: 240px) {
.sidebar-add-segment span {
display: none;
}
.sidebar-add-segment {
padding: 0.75rem;
}
}
/* Theme toggle in sidebar */
.theme-toggle-wrapper {
display: flex;

View File

@ -17,12 +17,20 @@
{/if}
{$batchModeValue=empty($batch_mode) ? '0' : '1'}
{function="($readLaterChecked = strpos(' ' . $link.tags . ' ', ' readitlater ') != false || strpos(' ' . $link.tags . ' ', ' readlater ') != false || strpos(' ' . $link.tags . ' ', ' toread ') != false) ? '' : ''"}
{function="($noteDefaultChecked = $link_is_new && empty($link.url)) ? '' : ''"}
{function="($isNoteOrTodo = strpos(' ' . $link.tags . ' ', ' note ') != false || strpos(' ' . $link.tags . ' ', ' shaarli-todo ') != false || strpos(' ' . $link.tags . ' ', ' shaarli-todo ') != false) ? '' : ''"}
{function="($noteDefaultChecked = $link_is_new && empty($link.url) && !$isNoteOrTodo) ? '' : ''"}
{function="($noteChecked = strpos(' ' . $link.tags . ' ', ' note ') != false || $noteDefaultChecked) ? '' : ''"}
{function="($todoChecked = strpos(' ' . $link.tags . ' ', ' shaarli-todo ') != false) ? '' : ''"}
{$effectiveTags=$link.tags}
{if="$noteDefaultChecked && strpos(' ' . $effectiveTags . ' ', ' note ') == false"}
{$effectiveTags=trim($effectiveTags . ' note')}
{/if}
{if="$todoChecked && strpos(' ' . $effectiveTags . ' ', ' note ') != false"}
{$effectiveTags=trim(str_replace(' note ', ' ', ' ' . $effectiveTags . ' '))}
{/if}
{if="$todoChecked && strpos(' ' . $effectiveTags . ' ', ' shaarli-todo ') == false"}
{$effectiveTags=trim($effectiveTags . ' shaarli-todo')}
{/if}
{function="($privateChecked = $link.private == true || $link_is_new) ? '' : ''"}
<div id="editlinkform{$index}" class="editlinkform container page-edit">
<div class="row editlinkform-row">
@ -46,7 +54,7 @@
</div>
</div>
<div class="card-body">
<div class="form-group bookmark-field-group">
<div class="form-group bookmark-field-group {if="$isNoteOrTodo"}hidden{/if}">
<label class="form-label" for="lf_url{$index}">{'URL'|t}</label>
<input type="text" class="form-control lf_input" name="lf_url" id="lf_url{$index}" value="{$link.url}" placeholder="https://...">
</div>
@ -101,6 +109,10 @@
<input type="checkbox" id="lf_note{$index}" class="bookmark-toggle-note" {if="$noteChecked"}checked="checked"{/if}>
<span>Note</span>
</label>
<label class="bookmark-toggle-item" for="lf_todo{$index}">
<input type="checkbox" id="lf_todo{$index}" class="bookmark-toggle-todo" {if="$todoChecked"}checked="checked"{/if}>
<span>Todo</span>
</label>
<label class="bookmark-toggle-item" for="lf_private{$index}">
<input type="checkbox" {if="$privateChecked"}checked="checked"{/if} name="lf_private" id="lf_private{$index}" />
<span>{'Private'|t}</span>

View File

@ -207,7 +207,7 @@
{ tag: 'notebg-*', description: 'Note background images' },
{ tag: 'notefilter-*', description: 'Note filter categories' },
{ tag: 'readitlater', description: 'Read It Later items' },
{ tag: 'shaarli-archiver', description: 'Archived notes' }
{ tag: 'shaarli-archive', description: 'Archived notes' }
];
const STORAGE_KEY = 'shaarli_hidden_tags';

View File

@ -23,7 +23,7 @@ document.addEventListener("DOMContentLoaded", function () {
if (!linkList || !container) return;
if (searchTags === "todo") {
if (searchTags === "shaarli-todo") {
initTodoView(linkList, container);
} else if (searchTags === "note") {
// Pour la vue notes, parser les notes AVANT de supprimer les tags techniques

View File

@ -91,11 +91,12 @@ document.addEventListener('DOMContentLoaded', () => {
const DEFAULT_HIDDEN_TAGS = [
'note',
'shaarli-pin',
'shaarli-todo',
'note-color-*',
'notebg-*',
'notefilter-*',
'readitlater',
'shaarli-archiver'
'shaarli-archive'
];
// Get hidden tags from localStorage
@ -1602,6 +1603,7 @@ document.addEventListener('DOMContentLoaded', () => {
const tagsTextInput = form.querySelector('.bookmark-tags-text-input');
const readLaterCheckbox = form.querySelector('.bookmark-toggle-readlater');
const noteCheckbox = form.querySelector('.bookmark-toggle-note');
const todoCheckbox = form.querySelector('.bookmark-toggle-todo');
const titleInput = form.querySelector('input[name="lf_title"]');
const isDarkTheme = document.documentElement.getAttribute('data-theme') === 'dark';
@ -1677,6 +1679,11 @@ document.addEventListener('DOMContentLoaded', () => {
.map((tag) => normalizeTag(tag))
.filter(Boolean);
// Remove duplicate tags (case-insensitive)
tags = tags.filter((tag, index, self) =>
index === self.findIndex((t) => t.toLowerCase() === tag.toLowerCase())
);
// Update return URL based on bookmark type (note vs regular)
const returnUrlInput = form.querySelector('input[name="returnurl"]');
if (returnUrlInput) {
@ -1702,19 +1709,35 @@ document.addEventListener('DOMContentLoaded', () => {
noteCheckbox.checked = tags.some((tag) => /^note$/i.test(tag));
};
const syncTodoCheckbox = () => {
if (!todoCheckbox) return;
todoCheckbox.checked = tags.some((tag) => /^shaarli-todo$/i.test(tag));
};
// Helper functions to manage note emoji in title
const NOTE_EMOJI = '📝';
const TODO_EMOJI = '✅';
const hasNoteEmoji = (title) => {
return title && title.startsWith(NOTE_EMOJI);
};
const hasTodoEmoji = (title) => {
return title && title.startsWith(TODO_EMOJI);
};
const addNoteEmoji = (title) => {
if (!title) return NOTE_EMOJI + ' ';
if (hasNoteEmoji(title)) return title;
return NOTE_EMOJI + ' ' + title;
};
const addTodoEmoji = (title) => {
if (!title) return TODO_EMOJI + ' ';
if (hasTodoEmoji(title)) return title;
return TODO_EMOJI + ' ' + title;
};
const removeNoteEmoji = (title) => {
if (!title) return '';
if (title.startsWith(NOTE_EMOJI + ' ')) {
@ -1726,6 +1749,17 @@ document.addEventListener('DOMContentLoaded', () => {
return title;
};
const removeTodoEmoji = (title) => {
if (!title) return '';
if (title.startsWith(TODO_EMOJI + ' ')) {
return title.substring(TODO_EMOJI.length + 1);
}
if (title.startsWith(TODO_EMOJI)) {
return title.substring(TODO_EMOJI.length);
}
return title;
};
const updateNoteTitle = (isNote) => {
if (!titleInput) return;
const currentTitle = titleInput.value || '';
@ -1740,6 +1774,20 @@ document.addEventListener('DOMContentLoaded', () => {
}
};
const updateTodoTitle = (isTodo) => {
if (!titleInput) return;
const currentTitle = titleInput.value || '';
if (isTodo) {
if (!hasTodoEmoji(currentTitle)) {
titleInput.value = addTodoEmoji(currentTitle);
}
} else {
if (hasTodoEmoji(currentTitle)) {
titleInput.value = removeTodoEmoji(currentTitle);
}
}
};
const addTag = (rawTag) => {
const tag = normalizeTag(rawTag);
if (!tag) return;
@ -1750,6 +1798,7 @@ document.addEventListener('DOMContentLoaded', () => {
updateHiddenTags();
syncReadLaterCheckbox();
syncNoteCheckbox();
syncTodoCheckbox();
renderTags();
refreshAutocomplete();
}
@ -1760,6 +1809,7 @@ document.addEventListener('DOMContentLoaded', () => {
updateHiddenTags();
syncReadLaterCheckbox();
syncNoteCheckbox();
syncTodoCheckbox();
renderTags();
refreshAutocomplete();
};
@ -1861,6 +1911,26 @@ document.addEventListener('DOMContentLoaded', () => {
updateHiddenTags();
syncReadLaterCheckbox();
syncTodoCheckbox();
renderTags();
refreshAutocomplete();
});
}
if (todoCheckbox) {
todoCheckbox.addEventListener('change', () => {
tags = tags.filter((tag) => !/^shaarli-todo$/i.test(tag));
if (todoCheckbox.checked) {
tags.push('shaarli-todo');
updateTodoTitle(true);
} else {
updateTodoTitle(false);
}
updateHiddenTags();
syncReadLaterCheckbox();
syncNoteCheckbox();
renderTags();
refreshAutocomplete();
});
@ -1889,13 +1959,24 @@ document.addEventListener('DOMContentLoaded', () => {
updateHiddenTags();
syncReadLaterCheckbox();
syncNoteCheckbox();
syncTodoCheckbox();
renderTags();
refreshAutocomplete();
// Initial title update if note tag is present
if (noteCheckbox && noteCheckbox.checked && titleInput) {
// Initial title update if note or todo tag is present
if (todoCheckbox && todoCheckbox.checked && titleInput) {
const currentTitle = titleInput.value || '';
if (!hasNoteEmoji(currentTitle) && !currentTitle.trim()) {
if (!hasTodoEmoji(currentTitle) && !currentTitle.trim()) {
titleInput.value = TODO_EMOJI + ' ';
} else if (!hasTodoEmoji(currentTitle)) {
titleInput.value = addTodoEmoji(currentTitle);
}
} else if (noteCheckbox && noteCheckbox.checked && titleInput) {
const currentTitle = titleInput.value || '';
// Replace default "Note:" title with just the emoji
if (currentTitle === 'Note:' || currentTitle === 'Note') {
titleInput.value = NOTE_EMOJI + ' ';
} else if (!hasNoteEmoji(currentTitle) && !currentTitle.trim()) {
titleInput.value = NOTE_EMOJI + ' ';
} else if (!hasNoteEmoji(currentTitle)) {
titleInput.value = addNoteEmoji(currentTitle);

View File

@ -49,7 +49,7 @@ Bookmarklet detection logic
<i class="mdi mdi-calendar-today" aria-hidden="true"></i>
<span>Quotidien</span>
</a>
<a href="{$base_path}/?searchtags=todo" class="sidebar-link{if="isset($search_tags) && preg_match('/(^|[\s,])todo([\s,]|$)/i', (string) $search_tags)"} active{/if}" aria-label="Mes tâches">
<a href="{$base_path}/?searchtags=shaarli-todo" class="sidebar-link{if="isset($search_tags) && preg_match('/(^|[\s,])shaarli-todo([\s,]|$)/i', (string) $search_tags)"} active{/if}" aria-label="Mes tâches">
<i class="mdi mdi-check-circle-outline" aria-hidden="true"></i>
<span>Mes tâches</span>
</a>
@ -86,10 +86,17 @@ Bookmarklet detection logic
<div class="sidebar-footer">
{if="$is_logged_in"}
<a href="{$base_path}/admin/add-shaare" class="sidebar-add-btn" aria-label="Nouveau bookmark">
<i class="mdi mdi-plus" aria-hidden="true"></i>
<span>Nouveau bookmark</span>
<div class="sidebar-add-segmented">
<a href="{$base_path}/admin/add-shaare" class="sidebar-add-segment" title="Bookmark">
<i class="mdi mdi-bookmark-outline"></i>
</a>
<a href="{$base_path}/admin/shaare?tags=note" class="sidebar-add-segment" title="Note">
<i class="mdi mdi-note-text-outline"></i>
</a>
<a href="{$base_path}/admin/shaare?post=http%3A%2F%2Fshaarli-todo&tags=shaarli-todo&title=%E2%9C%85%20" class="sidebar-add-segment" title="Todo">
<i class="mdi mdi-check-circle-outline"></i>
</a>
</div>
{/if}
<div class="theme-toggle-wrapper">
@ -147,7 +154,7 @@ Bookmarklet detection logic
<i class="mdi mdi-calendar" aria-hidden="true"></i>
<span>QUOTIDIEN</span>
</a>
<a href="{$base_path}/?searchtags=todo" class="header-nav-link{if="isset($search_tags) && preg_match('/(^|[\s,])todo([\s,]|$)/i', (string) $search_tags)"} active{/if}" aria-label="Mes tâches">
<a href="{$base_path}/?searchtags=shaarli-todo" class="header-nav-link{if="isset($search_tags) && preg_match('/(^|[\s,])shaarli-todo([\s,]|$)/i', (string) $search_tags)"} active{/if}" aria-label="Mes tâches">
<i class="mdi mdi-check-circle-outline" aria-hidden="true"></i>
<span>TÂCHES</span>
</a>

View File

@ -437,7 +437,7 @@
{ name: 'notebg-*', desc: 'Note Backgrounds - Wildcard for background tags' },
{ name: 'notefilter-*', desc: 'Note Filters - Wildcard for filter tags' },
{ name: 'readitlater', desc: 'Read Later - Temporary reading list' },
{ name: 'shaarli-archiver', desc: 'Archived - Archived notes' }
{ name: 'shaarli-archive', desc: 'Archived - Archived notes' }
];
const DEFAULT_HIDDEN_TAGS = PRESET_TAGS.map(t => t.name);