avec section Notes
This commit is contained in:
parent
eee850909a
commit
209614bb23
787
shaarli-pro/css/custom_views.css
Normal file
787
shaarli-pro/css/custom_views.css
Normal file
@ -0,0 +1,787 @@
|
|||||||
|
/* =========================================
|
||||||
|
Special Views for Todos and Notes
|
||||||
|
========================================= */
|
||||||
|
|
||||||
|
/* --- Layout Wrapper (injected by JS) --- */
|
||||||
|
.special-view-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
width: 100%;
|
||||||
|
min-height: calc(100vh - 60px);
|
||||||
|
/* Adjust for header */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- TODO VIEW --- */
|
||||||
|
body.view-todo .content-container {
|
||||||
|
max-width: 100%;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.view-todo #linklist {
|
||||||
|
display: none;
|
||||||
|
/* Hide default list when wrapper is active */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar */
|
||||||
|
.todo-sidebar {
|
||||||
|
width: 280px;
|
||||||
|
background-color: var(--background-secondary, #f8f9fa);
|
||||||
|
border-right: 1px solid var(--border-color, #e2e8f0);
|
||||||
|
padding: 1rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .todo-sidebar {
|
||||||
|
background-color: #1e293b;
|
||||||
|
border-color: #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-task-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
background-color: var(--primary-color, #2563eb);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 2rem;
|
||||||
|
font-weight: 500;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: transform 0.2s, background-color 0.2s;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-task-btn:hover {
|
||||||
|
background-color: var(--primary-dark, #1d4ed8);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-list-item {
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--text-color, #334155);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .todo-list-item {
|
||||||
|
color: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-list-item:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .todo-list-item:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-list-item.active {
|
||||||
|
background-color: rgba(37, 99, 235, 0.1);
|
||||||
|
color: var(--primary-color, #2563eb);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main Task Area */
|
||||||
|
.todo-main {
|
||||||
|
flex: 1;
|
||||||
|
padding: 2rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
background-color: var(--bg-body);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .todo-main {
|
||||||
|
background-color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-main-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-main-header h2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Todo Item */
|
||||||
|
.todo-item {
|
||||||
|
padding: 1rem;
|
||||||
|
border-bottom: 1px solid var(--border-color, #e2e8f0);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
/* keep aligned */
|
||||||
|
gap: 1rem;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .todo-item {
|
||||||
|
border-bottom-color: #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-item:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-checkbox {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: 2px solid var(--text-light, #94a3b8);
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-checkbox.checked {
|
||||||
|
background-color: var(--primary-color, #2563eb);
|
||||||
|
border-color: var(--primary-color, #2563eb);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-title {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: var(--text-color, #0f172a);
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .todo-title {
|
||||||
|
color: #f1f5f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-item.completed .todo-title {
|
||||||
|
text-decoration: line-through;
|
||||||
|
color: var(--text-light, #94a3b8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text-light, #64748b);
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-badge {
|
||||||
|
padding: 0.125rem 0.5rem;
|
||||||
|
border-radius: 1rem;
|
||||||
|
background-color: rgba(0, 0, 0, 0.05);
|
||||||
|
font-size: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .todo-badge {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.due-date {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.due-date.overdue {
|
||||||
|
color: var(--danger-color, #ef4444);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- NOTES VIEW --- */
|
||||||
|
|
||||||
|
/* Wrapper */
|
||||||
|
body.view-notes .content-container {
|
||||||
|
padding: 2rem;
|
||||||
|
background-color: var(--bg-body);
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] body.view-notes .content-container {
|
||||||
|
background-color: var(--bg-body);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tool Bar / Input Area */
|
||||||
|
.notes-top-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
position: relative;
|
||||||
|
padding-right: 60px;
|
||||||
|
/* Space for toggle */
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-input-container {
|
||||||
|
width: 600px;
|
||||||
|
max-width: 100%;
|
||||||
|
background-color: var(--background-secondary, #ffffff);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 1px 2px 0 rgba(60, 64, 67, 0.3), 0 2px 6px 2px rgba(60, 64, 67, 0.15);
|
||||||
|
transition: box-shadow 0.2s;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .note-input-container {
|
||||||
|
background-color: var(--bg-sidebar);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-input-collapsed {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 16px;
|
||||||
|
cursor: text;
|
||||||
|
color: var(--text-light, #80868b);
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-input-placeholder {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-input-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-input-actions button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--text-light, #80868b);
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: background-color 0.2s, color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-input-actions button:hover {
|
||||||
|
background-color: rgba(136, 136, 136, 0.1);
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tools (View Toggle) */
|
||||||
|
.notes-tools {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--text-light, #5f6368);
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn:hover {
|
||||||
|
background-color: rgba(136, 136, 136, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn.active {
|
||||||
|
color: var(--primary-color, #202124);
|
||||||
|
/* or specific active color */
|
||||||
|
background-color: rgba(136, 136, 136, 0.1);
|
||||||
|
/* Keep highlight style */
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .icon-btn {
|
||||||
|
color: #9aa0a6;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .icon-btn.active {
|
||||||
|
color: #e8eaed;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* --- LOGIC: Masonry vs List --- */
|
||||||
|
|
||||||
|
/* Masonry Grid */
|
||||||
|
.notes-masonry {
|
||||||
|
column-count: 4;
|
||||||
|
column-gap: 16px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.notes-masonry {
|
||||||
|
column-count: 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 800px) {
|
||||||
|
.notes-masonry {
|
||||||
|
column-count: 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 500px) {
|
||||||
|
.notes-masonry {
|
||||||
|
column-count: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* List View */
|
||||||
|
.notes-list-view {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
width: 600px;
|
||||||
|
max-width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notes-list-view .note-card {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* --- CARD STYLING --- */
|
||||||
|
.note-card {
|
||||||
|
background-color: var(--background-secondary, #ffffff);
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
/* spacing for masonry */
|
||||||
|
break-inside: avoid;
|
||||||
|
/* Prevent split */
|
||||||
|
position: relative;
|
||||||
|
transition: box-shadow 0.2s, transform 0.2s, background-color 0.2s;
|
||||||
|
overflow: hidden;
|
||||||
|
/* for cover image */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark Mode Card */
|
||||||
|
[data-theme="dark"] .note-card {
|
||||||
|
background-color: #202124;
|
||||||
|
border: 1px solid #5f6368;
|
||||||
|
color: #e8eaed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-card:hover {
|
||||||
|
box-shadow: 0 1px 2px 0 rgba(60, 64, 67, 0.3), 0 1px 3px 1px rgba(60, 64, 67, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .note-card:hover {
|
||||||
|
background-color: #202124;
|
||||||
|
/* Keep lightens on hover? usually same but controls appear */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cover Image */
|
||||||
|
.note-cover img {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Inner Content */
|
||||||
|
.note-inner {
|
||||||
|
padding: 12px 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Title */
|
||||||
|
.note-title {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.5rem;
|
||||||
|
color: var(--text-color, #202124);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .note-title {
|
||||||
|
color: #e8eaed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Body (Truncated) */
|
||||||
|
.note-body {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.25rem;
|
||||||
|
color: var(--text-color, #202124);
|
||||||
|
word-wrap: break-word;
|
||||||
|
/* Limit to ~12 lines */
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 12;
|
||||||
|
line-clamp: 12;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
max-height: 300px;
|
||||||
|
/* Fallback */
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .note-body {
|
||||||
|
color: #e8eaed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tags */
|
||||||
|
.note-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-tag {
|
||||||
|
background: rgba(0, 0, 0, 0.06);
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--text-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .note-tag {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hover Actions */
|
||||||
|
.note-hover-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0px;
|
||||||
|
/* evenly spaced */
|
||||||
|
margin-top: 8px;
|
||||||
|
margin-left: -8px;
|
||||||
|
/* Alignment fix */
|
||||||
|
opacity: 0;
|
||||||
|
/* Hidden by default */
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
position: relative;
|
||||||
|
/* For palette popup */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Show actions on hover */
|
||||||
|
.note-card:hover .note-hover-actions {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Always show actions on touch devices? (Can't detect easily here, but opacity 1 is safer for UX if no hover) */
|
||||||
|
@media (hover: none) {
|
||||||
|
.note-hover-actions {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-hover-actions button,
|
||||||
|
.note-hover-actions a {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
width: 34px;
|
||||||
|
/* Touch target */
|
||||||
|
height: 34px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--text-light, #5f6368);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
transition: background-color 0.2s, color 0.2s;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .note-hover-actions button,
|
||||||
|
[data-theme="dark"] .note-hover-actions a {
|
||||||
|
color: #9aa0a6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-hover-actions button:hover,
|
||||||
|
.note-hover-actions a:hover {
|
||||||
|
background-color: rgba(136, 136, 136, 0.2);
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .note-hover-actions button:hover,
|
||||||
|
[data-theme="dark"] .note-hover-actions a:hover {
|
||||||
|
color: #e8eaed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spacer {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pushes action buttons to left/right if needed, currently all left? Keep aligns left mostly. */
|
||||||
|
|
||||||
|
.note-body img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-top: 4px;
|
||||||
|
/* If we extracted cover, hide first img? */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- COLORS --- */
|
||||||
|
/* Reference: Keep Colors */
|
||||||
|
/* Default */
|
||||||
|
.note-card.note-color-default {
|
||||||
|
background-color: var(--background-secondary, #ffffff);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .note-card.note-color-default {
|
||||||
|
background-color: #202124;
|
||||||
|
border-color: #5f6368;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Red */
|
||||||
|
.note-card.note-color-red {
|
||||||
|
background-color: #f28b82;
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .note-card.note-color-red {
|
||||||
|
background-color: #5c2b29;
|
||||||
|
border-color: #5c2b29;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Orange */
|
||||||
|
.note-card.note-color-orange {
|
||||||
|
background-color: #fbbc04;
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .note-card.note-color-orange {
|
||||||
|
background-color: #614a19;
|
||||||
|
border-color: #614a19;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Yellow */
|
||||||
|
.note-card.note-color-yellow {
|
||||||
|
background-color: #fff475;
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .note-card.note-color-yellow {
|
||||||
|
background-color: #635d19;
|
||||||
|
border-color: #635d19;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Green */
|
||||||
|
.note-card.note-color-green {
|
||||||
|
background-color: #ccff90;
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .note-card.note-color-green {
|
||||||
|
background-color: #345920;
|
||||||
|
border-color: #345920;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Teal */
|
||||||
|
.note-card.note-color-teal {
|
||||||
|
background-color: #a7ffeb;
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .note-card.note-color-teal {
|
||||||
|
background-color: #16504b;
|
||||||
|
border-color: #16504b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Blue */
|
||||||
|
.note-card.note-color-blue {
|
||||||
|
background-color: #cbf0f8;
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .note-card.note-color-blue {
|
||||||
|
background-color: #2d555e;
|
||||||
|
border-color: #2d555e;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark Blue */
|
||||||
|
.note-card.note-color-darkblue {
|
||||||
|
background-color: #aecbfa;
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .note-card.note-color-darkblue {
|
||||||
|
background-color: #1e3a5f;
|
||||||
|
border-color: #1e3a5f;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Purple */
|
||||||
|
.note-card.note-color-purple {
|
||||||
|
background-color: #d7aefb;
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .note-card.note-color-purple {
|
||||||
|
background-color: #42275e;
|
||||||
|
border-color: #42275e;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pink */
|
||||||
|
.note-card.note-color-pink {
|
||||||
|
background-color: #fdcfe8;
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .note-card.note-color-pink {
|
||||||
|
background-color: #5b2245;
|
||||||
|
border-color: #5b2245;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Brown */
|
||||||
|
.note-card.note-color-brown {
|
||||||
|
background-color: #e6c9a8;
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .note-card.note-color-brown {
|
||||||
|
background-color: #442f19;
|
||||||
|
border-color: #442f19;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Grey */
|
||||||
|
.note-card.note-color-grey {
|
||||||
|
background-color: #e8eaed;
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .note-card.note-color-grey {
|
||||||
|
background-color: #3c3f43;
|
||||||
|
border-color: #3c3f43;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* --- PALETTE POPUP --- */
|
||||||
|
.palette-popup {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 100%;
|
||||||
|
left: 0;
|
||||||
|
width: 320px;
|
||||||
|
background: white;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 8px;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
z-index: 100;
|
||||||
|
display: none;
|
||||||
|
/* JS toggles this */
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .palette-popup {
|
||||||
|
background: #202124;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);
|
||||||
|
border: 1px solid #5f6368;
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette-popup.open {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette-btn {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette-btn:hover {
|
||||||
|
border-color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .palette-btn {
|
||||||
|
border-color: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- MODAL FOR NOTE VIEW --- */
|
||||||
|
.note-modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
z-index: 1000;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-modal-overlay.open {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-modal {
|
||||||
|
background: var(--background-secondary, #fff);
|
||||||
|
width: 600px;
|
||||||
|
max-width: 90%;
|
||||||
|
max-height: 90vh;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow-y: auto;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||||
|
padding: 24px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .note-modal {
|
||||||
|
background: var(--bg-card);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-modal .note-title {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-modal .note-body {
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
-webkit-line-clamp: unset;
|
||||||
|
line-clamp: unset;
|
||||||
|
max-height: none;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal actions at bottom? */
|
||||||
|
.note-modal-actions {
|
||||||
|
margin-top: 24px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
@ -23,6 +23,7 @@
|
|||||||
|
|
||||||
<!-- Professional Theme CSS -->
|
<!-- Professional Theme CSS -->
|
||||||
<link type="text/css" rel="stylesheet" href="{$asset_path}/css/style.css#" />
|
<link type="text/css" rel="stylesheet" href="{$asset_path}/css/style.css#" />
|
||||||
|
<link type="text/css" rel="stylesheet" href="{$asset_path}/css/custom_views.css#" />
|
||||||
{if="$pageName=='editlink' || $pageName=='addlink' || $pageName=='editlinkbatch'"}
|
{if="$pageName=='editlink' || $pageName=='addlink' || $pageName=='editlinkbatch'"}
|
||||||
<link type="text/css" rel="stylesheet" href="{$asset_path}/css/awesomplete.css#" />
|
<link type="text/css" rel="stylesheet" href="{$asset_path}/css/awesomplete.css#" />
|
||||||
<script src="{$asset_path}/js/awesomplete.min.js#" defer></script>
|
<script src="{$asset_path}/js/awesomplete.min.js#" defer></script>
|
||||||
@ -56,6 +57,7 @@ untaggedonly: {if="$untaggedonly"}true{else}false{/if}
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<script src="{$asset_path}/js/script.js#" defer></script>
|
<script src="{$asset_path}/js/script.js#" defer></script>
|
||||||
|
<script src="{$asset_path}/js/custom_views.js#" defer></script>
|
||||||
|
|
||||||
{if="file_exists('tpl/shaarli-pro/extra.html')"}
|
{if="file_exists('tpl/shaarli-pro/extra.html')"}
|
||||||
{include="extra"}
|
{include="extra"}
|
||||||
|
|||||||
884
shaarli-pro/js/custom_views.js
Normal file
884
shaarli-pro/js/custom_views.js
Normal file
@ -0,0 +1,884 @@
|
|||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
// Check URL parameters for custom views
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const searchTags = urlParams.get('searchtags');
|
||||||
|
|
||||||
|
const linkList = document.getElementById('links-list');
|
||||||
|
const container = document.querySelector('.content-container');
|
||||||
|
|
||||||
|
// Always init Pinned Items logic (sorting and listeners)
|
||||||
|
// This function is defined at the end of the file
|
||||||
|
if (typeof initPinnedItems === 'function') {
|
||||||
|
initPinnedItems();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!linkList || !container) return;
|
||||||
|
|
||||||
|
if (searchTags === 'todo') {
|
||||||
|
initTodoView(linkList, container);
|
||||||
|
} else if (searchTags === 'note') {
|
||||||
|
initNoteView(linkList, container);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the Google Tasks-like view
|
||||||
|
*/
|
||||||
|
function initTodoView(linkList, container) {
|
||||||
|
document.body.classList.add('view-todo');
|
||||||
|
|
||||||
|
// Extract task data from existing DOM
|
||||||
|
const rawLinks = Array.from(linkList.querySelectorAll('.link-outer'));
|
||||||
|
const tasks = rawLinks.map(link => parseTaskFromLink(link)).filter(t => t !== null);
|
||||||
|
|
||||||
|
// Create new Layout
|
||||||
|
const wrapper = document.createElement('div');
|
||||||
|
wrapper.className = 'special-view-wrapper';
|
||||||
|
|
||||||
|
// 1. Sidebar
|
||||||
|
const sidebar = document.createElement('div');
|
||||||
|
sidebar.className = 'todo-sidebar';
|
||||||
|
|
||||||
|
// Extract unique groups for the sidebar
|
||||||
|
const groups = new Set();
|
||||||
|
tasks.forEach(t => {
|
||||||
|
if (t.group) groups.add(t.group);
|
||||||
|
});
|
||||||
|
|
||||||
|
const groupsList = Array.from(groups).map(g =>
|
||||||
|
`<div class="todo-list-item" onclick="filterTasksByGroup('${g}')"><i class="mdi mdi-label-outline"></i> ${g}</div>`
|
||||||
|
).join('');
|
||||||
|
|
||||||
|
sidebar.innerHTML = `
|
||||||
|
<div class="sidebar-section-title" style="padding: 1rem 1rem 0.5rem; font-weight:bold; color:var(--text-light);">TASKS</div>
|
||||||
|
<div class="todo-list-item active" onclick="filterTasksByGroup('all')">
|
||||||
|
<i class="mdi mdi-inbox"></i> Mes tâches
|
||||||
|
<span style="margin-left:auto; font-size:0.8rem; background:rgba(0,0,0,0.1); padding:2px 6px; border-radius:10px;">${tasks.length}</span>
|
||||||
|
</div>
|
||||||
|
${groups.size > 0 ? `<div class="sidebar-section-title" style="padding: 1rem 1rem 0.5rem; font-weight:bold; color:var(--text-light);">LISTES</div>${groupsList}` : ''}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 2. Main Content
|
||||||
|
const main = document.createElement('div');
|
||||||
|
main.className = 'todo-main';
|
||||||
|
|
||||||
|
const mainHeader = document.createElement('div');
|
||||||
|
mainHeader.className = 'todo-main-header';
|
||||||
|
mainHeader.innerHTML = `
|
||||||
|
<h2 id="todo-list-title">Mes tâches</h2>
|
||||||
|
<div class="todo-actions">
|
||||||
|
<!-- Sorting/Menu could go here -->
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const itemsContainer = document.createElement('div');
|
||||||
|
itemsContainer.className = 'todo-items-container';
|
||||||
|
|
||||||
|
// Sort Tasks: Pinned items first
|
||||||
|
tasks.sort((a, b) => {
|
||||||
|
const aPinned = a.tags && a.tags.includes('shaarli-pin');
|
||||||
|
const bPinned = b.tags && b.tags.includes('shaarli-pin');
|
||||||
|
return bPinned - aPinned;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Render Tasks
|
||||||
|
tasks.forEach(task => {
|
||||||
|
itemsContainer.appendChild(renderTaskItem(task));
|
||||||
|
});
|
||||||
|
|
||||||
|
main.appendChild(mainHeader);
|
||||||
|
main.appendChild(itemsContainer);
|
||||||
|
|
||||||
|
wrapper.appendChild(sidebar);
|
||||||
|
wrapper.appendChild(main);
|
||||||
|
|
||||||
|
// Inject and Hide original
|
||||||
|
linkList.style.display = 'none';
|
||||||
|
|
||||||
|
// Remove pagination/toolbar if present to clean up view
|
||||||
|
const toolbar = document.querySelector('.content-toolbar');
|
||||||
|
if (toolbar) toolbar.style.display = 'none';
|
||||||
|
|
||||||
|
if (linkList.parentNode) {
|
||||||
|
linkList.parentNode.insertBefore(wrapper, linkList);
|
||||||
|
} else {
|
||||||
|
container.appendChild(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global filter function
|
||||||
|
window.filterTasksByGroup = function (group) {
|
||||||
|
const title = document.getElementById('todo-list-title');
|
||||||
|
const items = document.querySelectorAll('.todo-item');
|
||||||
|
|
||||||
|
// Update Sidebar Active State
|
||||||
|
document.querySelectorAll('.todo-list-item').forEach(el => el.classList.remove('active'));
|
||||||
|
if (event && event.currentTarget) event.currentTarget.classList.add('active');
|
||||||
|
|
||||||
|
if (group === 'all') {
|
||||||
|
title.textContent = 'Mes tâches';
|
||||||
|
items.forEach(item => item.style.display = 'flex');
|
||||||
|
} else {
|
||||||
|
title.textContent = group;
|
||||||
|
items.forEach(item => {
|
||||||
|
if (item.dataset.group === group) item.style.display = 'flex';
|
||||||
|
else item.style.display = 'none';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function parseTaskFromLink(linkEl) {
|
||||||
|
const id = linkEl.dataset.id;
|
||||||
|
const titleEl = linkEl.querySelector('.link-title'); // "Title" normally
|
||||||
|
// For Todos, the Bookmark Title is the Task Title?
|
||||||
|
// Or is the title inside the description?
|
||||||
|
// Mental model says: "Todo: reste un bookmark privé... LinkEntity.title" is used.
|
||||||
|
|
||||||
|
const title = titleEl ? titleEl.textContent.trim() : 'Task';
|
||||||
|
const descEl = linkEl.querySelector('.link-description');
|
||||||
|
const rawDesc = descEl ? descEl.innerHTML : '';
|
||||||
|
const textDesc = descEl ? descEl.textContent : '';
|
||||||
|
|
||||||
|
// Check if it's really a todo (should be if we are in ?searchtags=todo, but double check)
|
||||||
|
// We assume yes.
|
||||||
|
|
||||||
|
// Parse Metadata from Description text
|
||||||
|
// Format: 📅 **Échéance :** <Instant ISO>
|
||||||
|
// Format: 🏷️ **Groupe :** <nom>
|
||||||
|
// Format: - [ ] Subtask or Main task status?
|
||||||
|
|
||||||
|
// Status
|
||||||
|
// If [x] is found in the first few lines, maybe completed?
|
||||||
|
// User says: "Puis une checkbox markdown: - [ ] Titre"
|
||||||
|
// Wait, if the Description contains the checkbox and title, then Bookmark Title is ignored?
|
||||||
|
// Let's assume Bookmark Title is the master Display Title.
|
||||||
|
// And "Checkbox status" can be parsed from description.
|
||||||
|
|
||||||
|
let isCompleted = false;
|
||||||
|
if (textDesc.includes('[x]')) isCompleted = true;
|
||||||
|
|
||||||
|
// Due Date
|
||||||
|
let dueDate = null;
|
||||||
|
const dateMatch = textDesc.match(/Échéance\s*:\s*\*+([^*]+)\*+/); // varies by markdown regex
|
||||||
|
// Text might be "📅 **Échéance :** 2023-10-10"
|
||||||
|
// Regex: Échéance\s*:\s*(.*?)(\n|$)
|
||||||
|
const dueMatch = textDesc.match(/Échéance\s*[:]\s*(.*?)(\n|$)/);
|
||||||
|
if (dueMatch) dueDate = dueMatch[1].trim();
|
||||||
|
|
||||||
|
// Group
|
||||||
|
let group = null;
|
||||||
|
const groupMatch = textDesc.match(/Groupe\s*[:]\s*(.*?)(\n|$)/);
|
||||||
|
if (groupMatch) group = groupMatch[1].trim();
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
isCompleted,
|
||||||
|
dueDate,
|
||||||
|
group,
|
||||||
|
originalUrl: linkEl.querySelector('.link-url') ? linkEl.querySelector('.link-url').textContent : '',
|
||||||
|
editUrl: linkEl.querySelector('a[href*="admin/shaare/"]') ? linkEl.querySelector('a[href*="admin/shaare/"]').href : '#'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTaskItem(task) {
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.className = `todo-item ${task.isCompleted ? 'completed' : ''}`;
|
||||||
|
el.dataset.group = task.group || '';
|
||||||
|
|
||||||
|
/*
|
||||||
|
We cannot easily toggle state (AJAX) without a backend API that supports partial updates efficiently.
|
||||||
|
But we can provide a link to Edit.
|
||||||
|
Or simulate it.
|
||||||
|
*/
|
||||||
|
|
||||||
|
el.innerHTML = `
|
||||||
|
<div class="todo-checkbox ${task.isCompleted ? 'checked' : ''}">
|
||||||
|
${task.isCompleted ? '<i class="mdi mdi-check"></i>' : ''}
|
||||||
|
</div>
|
||||||
|
<div class="todo-content">
|
||||||
|
<div class="todo-title">${task.title}</div>
|
||||||
|
<div class="todo-meta">
|
||||||
|
${task.group ? `<span class="todo-badge">${task.group}</span>` : ''}
|
||||||
|
${task.dueDate ? `<span class="due-date ${isOverdue(task.dueDate) ? 'overdue' : ''}"><i class="mdi mdi-calendar"></i> ${formatDate(task.dueDate)}</span>` : ''}
|
||||||
|
<a href="${task.editUrl}" title="Edit Task" style="margin-left:auto;"><i class="mdi mdi-pencil" style="font-size:14px;"></i></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Simple click handler to open edit (since we can't sync state easily)
|
||||||
|
el.querySelector('.todo-checkbox').addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
// Open edit page or toggle visually
|
||||||
|
// window.location.href = task.editUrl;
|
||||||
|
});
|
||||||
|
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isOverdue(dateStr) {
|
||||||
|
try {
|
||||||
|
return new Date(dateStr) < new Date();
|
||||||
|
} catch (e) { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(dateStr) {
|
||||||
|
try {
|
||||||
|
const d = new Date(dateStr);
|
||||||
|
return d.toLocaleDateString();
|
||||||
|
} catch (e) { return dateStr; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the Google Keep-like view
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the Google Keep-like view
|
||||||
|
*/
|
||||||
|
function initNoteView(linkList, container) {
|
||||||
|
document.body.classList.add('view-notes');
|
||||||
|
|
||||||
|
// Hide standard toolbar
|
||||||
|
const toolbar = document.querySelector('.content-toolbar');
|
||||||
|
if (toolbar) toolbar.style.display = 'none';
|
||||||
|
|
||||||
|
// 1. Create Layout Wrapper
|
||||||
|
const wrapper = document.createElement('div');
|
||||||
|
wrapper.className = 'notes-wrapper';
|
||||||
|
|
||||||
|
// 2. Create Search/Input Area (Top)
|
||||||
|
const topBar = document.createElement('div');
|
||||||
|
topBar.className = 'notes-top-bar';
|
||||||
|
|
||||||
|
// Custom Input "Take a note..."
|
||||||
|
const inputContainer = document.createElement('div');
|
||||||
|
inputContainer.className = 'note-input-container';
|
||||||
|
inputContainer.innerHTML = `
|
||||||
|
<div class="note-input-collapsed" onclick="window.location.href='?do=addlink&tags=note'">
|
||||||
|
<span class="note-input-placeholder">Créer une note...</span>
|
||||||
|
<div class="note-input-actions">
|
||||||
|
<button title="Nouvelle liste" onclick="event.stopPropagation(); window.location.href='?do=addlink&tags=note&description=- [ ] '"><i class="mdi mdi-checkbox-marked-outline"></i></button>
|
||||||
|
<button title="Nouveau dessin" onclick="event.stopPropagation(); window.location.href='?do=addlink&tags=note'"><i class="mdi mdi-brush"></i></button>
|
||||||
|
<button title="Nouvelle image" onclick="event.stopPropagation(); window.location.href='?do=addlink&tags=note'"><i class="mdi mdi-image"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
topBar.appendChild(inputContainer);
|
||||||
|
|
||||||
|
// View Toggle and other tools
|
||||||
|
const tools = document.createElement('div');
|
||||||
|
tools.className = 'notes-tools';
|
||||||
|
tools.innerHTML = `
|
||||||
|
<button class="icon-btn active" id="btn-view-grid" title="Vue grille"><i class="mdi mdi-view-dashboard-outline"></i></button>
|
||||||
|
<button class="icon-btn" id="btn-view-list" title="Vue liste"><i class="mdi mdi-view-agenda-outline"></i></button>
|
||||||
|
`;
|
||||||
|
topBar.appendChild(tools);
|
||||||
|
|
||||||
|
wrapper.appendChild(topBar);
|
||||||
|
|
||||||
|
// 3. Content Area
|
||||||
|
const contentArea = document.createElement('div');
|
||||||
|
contentArea.className = 'notes-content-area';
|
||||||
|
|
||||||
|
const links = Array.from(linkList.querySelectorAll('.link-outer'));
|
||||||
|
const notes = links.map(link => parseNoteFromLink(link));
|
||||||
|
|
||||||
|
// Initial Render (Grid)
|
||||||
|
renderNotes(contentArea, notes, 'grid');
|
||||||
|
|
||||||
|
wrapper.appendChild(contentArea);
|
||||||
|
|
||||||
|
// Replace original list
|
||||||
|
linkList.style.display = 'none';
|
||||||
|
if (linkList.parentNode) {
|
||||||
|
linkList.parentNode.insertBefore(wrapper, linkList);
|
||||||
|
} else {
|
||||||
|
container.appendChild(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modal Container
|
||||||
|
const modalOverlay = document.createElement('div');
|
||||||
|
modalOverlay.className = 'note-modal-overlay';
|
||||||
|
modalOverlay.innerHTML = `
|
||||||
|
<div class="note-modal">
|
||||||
|
<div class="note-modal-content"></div>
|
||||||
|
<div class="note-modal-actions">
|
||||||
|
<button class="btn btn-primary" id="note-modal-close">Fermer</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
document.body.appendChild(modalOverlay);
|
||||||
|
|
||||||
|
// Event Listeners for Toggles
|
||||||
|
const btnGrid = wrapper.querySelector('#btn-view-grid');
|
||||||
|
const btnList = wrapper.querySelector('#btn-view-list');
|
||||||
|
|
||||||
|
btnGrid.addEventListener('click', () => {
|
||||||
|
btnGrid.classList.add('active');
|
||||||
|
btnList.classList.remove('active');
|
||||||
|
renderNotes(contentArea, notes, 'grid');
|
||||||
|
});
|
||||||
|
|
||||||
|
btnList.addEventListener('click', () => {
|
||||||
|
btnList.classList.add('active');
|
||||||
|
btnGrid.classList.remove('active');
|
||||||
|
renderNotes(contentArea, notes, 'list');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close Modal
|
||||||
|
modalOverlay.querySelector('#note-modal-close').addEventListener('click', () => {
|
||||||
|
modalOverlay.classList.remove('open');
|
||||||
|
});
|
||||||
|
modalOverlay.addEventListener('click', (e) => {
|
||||||
|
if (e.target === modalOverlay) modalOverlay.classList.remove('open');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseNoteFromLink(linkEl) {
|
||||||
|
const id = linkEl.dataset.id;
|
||||||
|
const titleEl = linkEl.querySelector('.link-title');
|
||||||
|
const title = titleEl ? titleEl.textContent.trim() : '';
|
||||||
|
|
||||||
|
const descEl = linkEl.querySelector('.link-description');
|
||||||
|
const descHtml = descEl ? descEl.innerHTML : '';
|
||||||
|
const descText = descEl ? descEl.textContent : '';
|
||||||
|
|
||||||
|
// Extract Image from Description (First image as cover)
|
||||||
|
let coverImage = null;
|
||||||
|
if (descEl) {
|
||||||
|
const img = descEl.querySelector('img');
|
||||||
|
if (img) {
|
||||||
|
coverImage = img.src;
|
||||||
|
// Optionally remove img from body text if it's purely a cover
|
||||||
|
// But usually we keep it or hide it via CSS if we construct a custom card
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const urlEl = linkEl.querySelector('.link-url');
|
||||||
|
const url = urlEl ? urlEl.textContent.trim() : '';
|
||||||
|
|
||||||
|
const tags = [];
|
||||||
|
let color = 'default';
|
||||||
|
linkEl.querySelectorAll('.link-tag-list a').forEach(tag => {
|
||||||
|
const t = tag.textContent.trim();
|
||||||
|
// Check for color tag
|
||||||
|
if (t.startsWith('note-')) {
|
||||||
|
const potentialColor = t.substring(5);
|
||||||
|
color = potentialColor;
|
||||||
|
} else {
|
||||||
|
tags.push(t);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const isPinnedByTag = tags.includes('shaarli-pin');
|
||||||
|
|
||||||
|
const actionsEl = linkEl.querySelector('.link-actions');
|
||||||
|
const editUrl = actionsEl && actionsEl.querySelector('a[href*="admin/shaare"]') ? actionsEl.querySelector('a[href*="admin/shaare"]').href : '#';
|
||||||
|
const deleteUrl = actionsEl && actionsEl.querySelector('a[href*="delete"]') ? actionsEl.querySelector('a[href*="delete"]').href : '#';
|
||||||
|
const pinUrl = actionsEl && actionsEl.querySelector('a[href*="pin"]') ? actionsEl.querySelector('a[href*="pin"]').href : '#';
|
||||||
|
|
||||||
|
// User requested "availability of the tag 'shaarli-pin' as the main source"
|
||||||
|
const isPinned = tags.includes('shaarli-pin');
|
||||||
|
|
||||||
|
return { id, title, descHtml, descText, coverImage, url, tags, color, editUrl, deleteUrl, pinUrl, isPinned };
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderNotes(container, notes, viewMode) {
|
||||||
|
container.innerHTML = '';
|
||||||
|
container.className = viewMode === 'grid' ? 'notes-masonry' : 'notes-list-view';
|
||||||
|
|
||||||
|
// Sort: Pinned items first
|
||||||
|
notes.sort((a, b) => {
|
||||||
|
const aPinned = a.tags.includes('shaarli-pin');
|
||||||
|
const bPinned = b.tags.includes('shaarli-pin');
|
||||||
|
return bPinned - aPinned;
|
||||||
|
});
|
||||||
|
|
||||||
|
notes.forEach(note => {
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.className = `note-card note-color-${note.color}`;
|
||||||
|
card.dataset.id = note.id;
|
||||||
|
if (viewMode === 'list') card.classList.add('list-mode');
|
||||||
|
|
||||||
|
// Main Click to Open Modal
|
||||||
|
card.addEventListener('click', (e) => {
|
||||||
|
// Prevent if clicking buttons
|
||||||
|
if (e.target.closest('button') || e.target.closest('a') || e.target.closest('.note-hover-actions')) return;
|
||||||
|
openNoteModal(note);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cover Image
|
||||||
|
if (note.coverImage && viewMode === 'grid') { // Show cover mainly in grid
|
||||||
|
const imgContainer = document.createElement('div');
|
||||||
|
imgContainer.className = 'note-cover';
|
||||||
|
imgContainer.innerHTML = `<img src="${note.coverImage}" alt="Cover">`;
|
||||||
|
card.appendChild(imgContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inner Content
|
||||||
|
const inner = document.createElement('div');
|
||||||
|
inner.className = 'note-inner';
|
||||||
|
|
||||||
|
// Title
|
||||||
|
if (note.title) {
|
||||||
|
const h3 = document.createElement('h3');
|
||||||
|
h3.className = 'note-title';
|
||||||
|
h3.textContent = note.title;
|
||||||
|
inner.appendChild(h3);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Body (truncated in grid, maybe?)
|
||||||
|
if (note.descHtml) {
|
||||||
|
const body = document.createElement('div');
|
||||||
|
body.className = 'note-body';
|
||||||
|
// Start simple: use innerHTML but maybe strip big images if we used cover?
|
||||||
|
// For now, let's just dump it and style images to fit or hide if first child
|
||||||
|
body.innerHTML = note.descHtml;
|
||||||
|
inner.appendChild(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tags (Labels)
|
||||||
|
if (note.tags.length > 0) {
|
||||||
|
const tagContainer = document.createElement('div');
|
||||||
|
tagContainer.className = 'note-tags';
|
||||||
|
note.tags.forEach(t => {
|
||||||
|
if (t !== 'note') {
|
||||||
|
const span = document.createElement('span');
|
||||||
|
span.className = 'note-tag';
|
||||||
|
span.textContent = t;
|
||||||
|
tagContainer.appendChild(span);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
inner.appendChild(tagContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hover Actions (Keep style: at bottom, visible on hover)
|
||||||
|
const actions = document.createElement('div');
|
||||||
|
actions.className = 'note-hover-actions';
|
||||||
|
|
||||||
|
// Palette Button Logic
|
||||||
|
const paletteBtnId = `palette-${note.id}`;
|
||||||
|
|
||||||
|
actions.innerHTML = `
|
||||||
|
<button title="Rappel"><i class="mdi mdi-bell-outline"></i></button>
|
||||||
|
<button title="Collaborateur"><i class="mdi mdi-account-plus-outline"></i></button>
|
||||||
|
<div style="position:relative;">
|
||||||
|
<button title="Couleur" id="${paletteBtnId}"><i class="mdi mdi-palette-outline"></i></button>
|
||||||
|
<div class="palette-popup" id="popup-${paletteBtnId}">
|
||||||
|
${generatePaletteButtons(note)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button title="Image"><i class="mdi mdi-image-outline"></i></button>
|
||||||
|
<button title="Archiver"><i class="mdi mdi-archive-arrow-down-outline"></i></button>
|
||||||
|
<div class="spacer"></div>
|
||||||
|
<!-- Real Actions -->
|
||||||
|
<a href="${note.pinUrl}" title="${note.isPinned ? 'Unpin' : 'Pin'}" class="${note.isPinned ? 'active' : ''}"><i class="mdi mdi-pin${note.isPinned ? '' : '-outline'}"></i></a>
|
||||||
|
<a href="${note.editUrl}" title="Edit"><i class="mdi mdi-pencil-outline"></i></a>
|
||||||
|
<button title="Plus" onclick="window.location.href='${note.deleteUrl}'"><i class="mdi mdi-dots-vertical"></i></button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Palette Toggle
|
||||||
|
const paletteBtn = actions.querySelector(`#${paletteBtnId}`);
|
||||||
|
const palettePopup = actions.querySelector(`#popup-${paletteBtnId}`);
|
||||||
|
|
||||||
|
paletteBtn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
// Close others?
|
||||||
|
document.querySelectorAll('.palette-popup.open').forEach(p => {
|
||||||
|
if (p !== palettePopup) p.classList.remove('open');
|
||||||
|
});
|
||||||
|
palettePopup.classList.toggle('open');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close palette when clicking outside
|
||||||
|
// (Handled globally or card based? simple: card mouseleave?)
|
||||||
|
card.addEventListener('mouseleave', () => {
|
||||||
|
palettePopup.classList.remove('open');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
inner.appendChild(actions);
|
||||||
|
card.appendChild(inner);
|
||||||
|
|
||||||
|
container.appendChild(card);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function openNoteModal(note) {
|
||||||
|
const modal = document.querySelector('.note-modal-overlay');
|
||||||
|
const content = modal.querySelector('.note-modal-content');
|
||||||
|
|
||||||
|
// Build full content
|
||||||
|
let html = `
|
||||||
|
<h2 class="note-title" style="margin-top:0;">${note.title}</h2>
|
||||||
|
<div class="note-body">${note.descHtml}</div>
|
||||||
|
`;
|
||||||
|
// Add images if not in desc? (desc usually has it)
|
||||||
|
|
||||||
|
content.innerHTML = html;
|
||||||
|
modal.classList.add('open');
|
||||||
|
}
|
||||||
|
|
||||||
|
function generatePaletteButtons(note) {
|
||||||
|
const colors = [
|
||||||
|
'default', 'red', 'orange', 'yellow', 'green', 'teal', 'blue', 'darkblue', 'purple', 'pink', 'brown', 'grey'
|
||||||
|
];
|
||||||
|
// Map to hex for the button background
|
||||||
|
const colorMap = {
|
||||||
|
'default': '#ffffff', 'red': '#f28b82', 'orange': '#fbbc04', 'yellow': '#fff475',
|
||||||
|
'green': '#ccff90', 'teal': '#a7ffeb', 'blue': '#cbf0f8', 'darkblue': '#aecbfa',
|
||||||
|
'purple': '#d7aefb', 'pink': '#fdcfe8', 'brown': '#e6c9a8', 'grey': '#e8eaed'
|
||||||
|
};
|
||||||
|
// Dark mode mapping could be handled via CSS classes on buttons but inline styles are easier for the picker circles
|
||||||
|
// We will just use class names and let CSS handle the preview color if possible, or set style.
|
||||||
|
|
||||||
|
return colors.map(c => {
|
||||||
|
// We use style for the button background roughly.
|
||||||
|
// Actually, let's use the class on the button itself to pick up the color from CSS variables if defined,
|
||||||
|
// OR just hardcode the light mode preview for simplicity as the picker is usually on white/dark background.
|
||||||
|
return `<button class="palette-btn note-color-${c}" title="${c}" onclick="setNoteColor('${note.id}', '${c}', '${note.editUrl}')" style="background-color:${colorMap[c]}"></button>`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
window.setNoteColor = function (noteId, color, editUrl) {
|
||||||
|
// 1. Visual Update (Immediate feedback)
|
||||||
|
const card = document.querySelector(`.note-card[data-id="${noteId}"]`);
|
||||||
|
if (card) {
|
||||||
|
// Remove all color classes
|
||||||
|
card.classList.forEach(cls => {
|
||||||
|
if (cls.startsWith('note-color-')) card.classList.remove(cls);
|
||||||
|
});
|
||||||
|
card.classList.add(`note-color-${color}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Persistence via AJAX Form Submission
|
||||||
|
fetch(editUrl)
|
||||||
|
.then(response => response.text())
|
||||||
|
.then(html => {
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const doc = parser.parseFromString(html, 'text/html');
|
||||||
|
const form = doc.querySelector('form[name="linkform"]');
|
||||||
|
|
||||||
|
if (!form) throw new Error('Could not find edit form');
|
||||||
|
|
||||||
|
// Extract all necessary fields
|
||||||
|
const formData = new URLSearchParams();
|
||||||
|
const inputs = form.querySelectorAll('input, textarea');
|
||||||
|
|
||||||
|
inputs.forEach(input => {
|
||||||
|
if (input.type === 'checkbox') {
|
||||||
|
if (input.checked) formData.append(input.name, input.value || 'on');
|
||||||
|
} else if (input.name) {
|
||||||
|
formData.append(input.name, input.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update Tags
|
||||||
|
let currentTags = formData.get('lf_tags') || '';
|
||||||
|
let tagsArray = currentTags.split(/[\s,]+/).filter(t => t.trim() !== '');
|
||||||
|
|
||||||
|
// Remove existing color tags
|
||||||
|
tagsArray = tagsArray.filter(t => !t.startsWith('note-'));
|
||||||
|
|
||||||
|
// Add new color tag (unless default)
|
||||||
|
if (color !== 'default') {
|
||||||
|
tagsArray.push(`note-${color}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
formData.set('lf_tags', tagsArray.join(' '));
|
||||||
|
formData.append('save_edit', '1'); // Trigger save action
|
||||||
|
|
||||||
|
// POST back to Shaarli
|
||||||
|
return fetch(form.action, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
body: formData.toString()
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (response.ok) {
|
||||||
|
console.log(`Color ${color} saved for note ${noteId}`);
|
||||||
|
} else {
|
||||||
|
throw new Error('Failed to save color');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error('Error saving note color:', err);
|
||||||
|
alert("Erreur lors de la sauvegarde de la couleur. Veuillez rafraîchir la page.");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ==========================================================
|
||||||
|
PINNED ITEMS LOGIC (Tag: shaarli-pin)
|
||||||
|
========================================================== */
|
||||||
|
function initPinnedItems() {
|
||||||
|
const container = document.querySelector('.links-list, .notes-masonry, .notes-list-view');
|
||||||
|
if (!container) return; // Exit if no container found (e.g. empty page or other view)
|
||||||
|
|
||||||
|
const items = Array.from(container.children);
|
||||||
|
const pinnedItems = [];
|
||||||
|
|
||||||
|
items.forEach(item => {
|
||||||
|
// Support both Standard Link items and Note items
|
||||||
|
if (item.classList.contains('link-outer') || item.classList.contains('note-card')) {
|
||||||
|
let isPinned = false;
|
||||||
|
|
||||||
|
// 1. Check for Tag 'shaarli-pin'
|
||||||
|
// In note-card, we might need to check dataset or re-parse tags if not visible?
|
||||||
|
// But usually renderNotes puts tags in DOM or we rely on data attribute if we saved it?
|
||||||
|
// Let's rely on finding the tag text in the DOM for consistency with Standard View.
|
||||||
|
// For Note View, tags are in .note-tags > .note-tag
|
||||||
|
// For Standard View, tags are in .link-tag-list > a
|
||||||
|
|
||||||
|
const itemHtml = item.innerHTML; // Simple search in content (quick & dirty but effective for tag presence)
|
||||||
|
// Better: Select text content of tags specifically to avoid false positives in description
|
||||||
|
const tagElements = item.querySelectorAll('.link-tag-list a, .note-tag');
|
||||||
|
for (let t of tagElements) {
|
||||||
|
if (t.textContent.trim() === 'shaarli-pin') {
|
||||||
|
isPinned = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Enforce Visual State based on Tag Presence
|
||||||
|
const pinBtnIcon = item.querySelector('.mdi-pin-outline, .mdi-pin');
|
||||||
|
const titleArea = item.querySelector('.link-title, .note-title');
|
||||||
|
const titleIcon = titleArea ? titleArea.querySelector('i.mdi-pin') : null;
|
||||||
|
|
||||||
|
if (isPinned) {
|
||||||
|
// It IS Pinned: Ensure UI reflects this
|
||||||
|
pinnedItems.push(item);
|
||||||
|
item.classList.add('is-pinned-tag');
|
||||||
|
|
||||||
|
// Button -> Filled Pin
|
||||||
|
if (pinBtnIcon) {
|
||||||
|
pinBtnIcon.classList.remove('mdi-pin-outline');
|
||||||
|
pinBtnIcon.classList.add('mdi-pin');
|
||||||
|
if (pinBtnIcon.parentElement) pinBtnIcon.parentElement.classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Title -> Add Icon if missing
|
||||||
|
if (titleArea && !titleIcon) {
|
||||||
|
const newIcon = document.createElement('i');
|
||||||
|
newIcon.className = 'mdi mdi-pin';
|
||||||
|
newIcon.style.color = 'var(--primary)';
|
||||||
|
newIcon.style.marginRight = '8px';
|
||||||
|
titleArea.prepend(newIcon);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// It is NOT Pinned: Ensure UI reflects this (Clean up native sticky or mismatches)
|
||||||
|
item.classList.remove('is-pinned-tag');
|
||||||
|
|
||||||
|
// Button -> Outline Pin
|
||||||
|
if (pinBtnIcon) {
|
||||||
|
pinBtnIcon.classList.remove('mdi-pin');
|
||||||
|
pinBtnIcon.classList.add('mdi-pin-outline');
|
||||||
|
if (pinBtnIcon.parentElement) pinBtnIcon.parentElement.classList.remove('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Title -> Remove Icon if present
|
||||||
|
if (titleIcon) {
|
||||||
|
titleIcon.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. Move Pinned Items to Top (Only for standard list, renderNotes already sorts itself)
|
||||||
|
if (container.classList.contains('links-list')) {
|
||||||
|
for (let i = pinnedItems.length - 1; i >= 0; i--) {
|
||||||
|
container.prepend(pinnedItems[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Click Listener for all Pin Buttons (Event Delegation)
|
||||||
|
// Avoid adding multiple listeners if init called multiple times?
|
||||||
|
// We'll rely on one global listener on document, but here we add it inside this function which is called once on load.
|
||||||
|
// To be safe, let's remove old one if we could, but anonymous function makes it hard.
|
||||||
|
// Better: Allow this to run, but ensure we don't duplicate.
|
||||||
|
// Since initPinnedItems is called on DOMContentLoaded, it runs once.
|
||||||
|
|
||||||
|
// Note: We already have the listener attached in previous version.
|
||||||
|
// We will just keep the listener logic here in the replacement.
|
||||||
|
document.addEventListener('click', function (e) {
|
||||||
|
const btn = e.target.closest('a[href*="do=pin"], .note-hover-actions a[href*="pin"], .link-actions a[href*="pin"]');
|
||||||
|
if (btn) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
const card = btn.closest('.link-outer, .note-card');
|
||||||
|
const id = card ? card.dataset.id : null;
|
||||||
|
|
||||||
|
// Re-derive edit URL if needed
|
||||||
|
let editUrl = btn.href.replace('do=pin', 'do=editlink').replace('pin', 'editlink');
|
||||||
|
if (card) {
|
||||||
|
const editBtn = card.querySelector('a[href*="edit_link"], a[href*="admin/shaare"]');
|
||||||
|
if (editBtn) editUrl = editBtn.href;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (id && editUrl) {
|
||||||
|
togglePinTag(id, editUrl, btn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, { once: false }); // Listener is permanent
|
||||||
|
}
|
||||||
|
|
||||||
|
function togglePinTag(id, editUrl, btn) {
|
||||||
|
const icon = btn.querySelector('i');
|
||||||
|
let isPinning = false;
|
||||||
|
|
||||||
|
if (icon) {
|
||||||
|
if (icon.classList.contains('mdi-pin-outline')) {
|
||||||
|
icon.classList.remove('mdi-pin-outline');
|
||||||
|
icon.classList.add('mdi-pin');
|
||||||
|
btn.classList.add('active');
|
||||||
|
isPinning = true;
|
||||||
|
} else {
|
||||||
|
icon.classList.remove('mdi-pin');
|
||||||
|
icon.classList.add('mdi-pin-outline');
|
||||||
|
btn.classList.remove('active');
|
||||||
|
isPinning = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update Title Icon (The one "devant le titre")
|
||||||
|
const card = btn.closest('.link-outer, .note-card');
|
||||||
|
if (card) {
|
||||||
|
// Update Title Icon
|
||||||
|
const titleArea = card.querySelector('.link-title, .note-title');
|
||||||
|
if (titleArea) {
|
||||||
|
let titleIcon = titleArea.querySelector('i.mdi-pin');
|
||||||
|
if (isPinning) {
|
||||||
|
if (!titleIcon) {
|
||||||
|
const newIcon = document.createElement('i');
|
||||||
|
newIcon.className = 'mdi mdi-pin';
|
||||||
|
newIcon.style.color = 'var(--primary)';
|
||||||
|
newIcon.style.marginRight = '8px';
|
||||||
|
titleArea.prepend(newIcon);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (titleIcon) titleIcon.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update Tag List Visualization
|
||||||
|
// We need to handle both Note Cards (.note-tags) and Standard Links (.link-tag-list)
|
||||||
|
let tagContainer = card.querySelector('.note-tags');
|
||||||
|
if (!tagContainer) tagContainer = card.querySelector('.link-tag-list');
|
||||||
|
|
||||||
|
if (tagContainer) {
|
||||||
|
// Check if tag exists already
|
||||||
|
let existingTagElement = null;
|
||||||
|
|
||||||
|
// Search in children
|
||||||
|
// Notes: .note-tag
|
||||||
|
// Links: .label-tag > a OR just a
|
||||||
|
const allCandidates = tagContainer.querySelectorAll('*');
|
||||||
|
for (let el of allCandidates) {
|
||||||
|
if (el.textContent.trim() === 'shaarli-pin') {
|
||||||
|
// We found the text.
|
||||||
|
// If note, it's the span.note-tag
|
||||||
|
if (el.classList.contains('note-tag')) {
|
||||||
|
existingTagElement = el;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// If link, we want the anchor or its wrapper
|
||||||
|
if (el.tagName === 'A') {
|
||||||
|
// Check if wrapped in .label-tag
|
||||||
|
if (el.parentElement.classList.contains('label-tag')) {
|
||||||
|
existingTagElement = el.parentElement;
|
||||||
|
} else {
|
||||||
|
existingTagElement = el;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPinning) {
|
||||||
|
if (!existingTagElement) {
|
||||||
|
if (card.classList.contains('note-card')) {
|
||||||
|
// Add Note Tag
|
||||||
|
const span = document.createElement('span');
|
||||||
|
span.className = 'note-tag';
|
||||||
|
span.textContent = 'shaarli-pin';
|
||||||
|
tagContainer.appendChild(span);
|
||||||
|
} else {
|
||||||
|
// Add Link Tag (Standard View)
|
||||||
|
// Structure: <span class="link-tag"><a ...>shaarli-pin</a></span>
|
||||||
|
const wrapper = document.createElement('span');
|
||||||
|
wrapper.className = 'link-tag';
|
||||||
|
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = '?searchtags=shaarli-pin';
|
||||||
|
link.textContent = 'shaarli-pin';
|
||||||
|
|
||||||
|
wrapper.appendChild(link);
|
||||||
|
|
||||||
|
// Append space first for separation if there are other tags
|
||||||
|
if (tagContainer.children.length > 0) {
|
||||||
|
tagContainer.appendChild(document.createTextNode(' '));
|
||||||
|
}
|
||||||
|
tagContainer.appendChild(wrapper);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (existingTagElement) {
|
||||||
|
// Remove the element
|
||||||
|
const prev = existingTagElement.previousSibling;
|
||||||
|
existingTagElement.remove();
|
||||||
|
// Clean up trailing space/text if it was the last one or between tags
|
||||||
|
if (prev && prev.nodeType === Node.TEXT_NODE && !prev.textContent.trim()) {
|
||||||
|
prev.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(editUrl)
|
||||||
|
.then(response => response.text())
|
||||||
|
.then(html => {
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const doc = parser.parseFromString(html, 'text/html');
|
||||||
|
const form = doc.querySelector('form[name="linkform"]');
|
||||||
|
if (!form) throw new Error('Could not find edit form');
|
||||||
|
|
||||||
|
const formData = new URLSearchParams();
|
||||||
|
const inputs = form.querySelectorAll('input, textarea');
|
||||||
|
inputs.forEach(input => {
|
||||||
|
if (input.type === 'checkbox') {
|
||||||
|
if (input.checked) formData.append(input.name, input.value || 'on');
|
||||||
|
} else if (input.name) {
|
||||||
|
formData.append(input.name, input.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let currentTags = formData.get('lf_tags') || '';
|
||||||
|
let tagsArray = currentTags.split(/[\s,]+/).filter(t => t.trim() !== '');
|
||||||
|
const pinTag = 'shaarli-pin';
|
||||||
|
|
||||||
|
if (isPinning) {
|
||||||
|
if (!tagsArray.includes(pinTag)) tagsArray.push(pinTag);
|
||||||
|
} else {
|
||||||
|
tagsArray = tagsArray.filter(t => t !== pinTag);
|
||||||
|
}
|
||||||
|
|
||||||
|
formData.set('lf_tags', tagsArray.join(' '));
|
||||||
|
formData.append('save_edit', '1');
|
||||||
|
|
||||||
|
return fetch(form.action, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
|
body: formData.toString()
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
if (res.ok) console.log("Pin toggled successfully");
|
||||||
|
})
|
||||||
|
.catch(err => console.error(err));
|
||||||
|
}
|
||||||
@ -17,16 +17,19 @@
|
|||||||
<nav class="sidebar-nav">
|
<nav class="sidebar-nav">
|
||||||
<div class="sidebar-section">
|
<div class="sidebar-section">
|
||||||
<div class="sidebar-section-title">Navigation</div>
|
<div class="sidebar-section-title">Navigation</div>
|
||||||
<a href="{$base_path}/" class="sidebar-link{if=" $pageName=='linklist'"} active{/if}">
|
<a href="{$base_path}/" class="sidebar-link{if="$pageName=='linklist' && empty($search_tags)"} active{/if}">
|
||||||
<i class="mdi mdi-bookmark-multiple-outline"></i>
|
<i class="mdi mdi-bookmark-multiple-outline"></i>
|
||||||
<span>All Bookmarks</span>
|
<span>All Bookmarks</span>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="{$base_path}/?searchtags=shaarli-pin" class="sidebar-link{if="isset($search_tags) && $search_tags == 'shaarli-pin'"} active{/if}">
|
||||||
|
<i class="mdi mdi-pin-outline"></i>
|
||||||
|
<span>Épinglés</span>
|
||||||
|
</a>
|
||||||
<a href="{$base_path}/tags/cloud" class="sidebar-link{if="$pageName=='tagcloud'"} active{/if}">
|
<a href="{$base_path}/tags/cloud" class="sidebar-link{if="$pageName=='tagcloud'"} active{/if}">
|
||||||
<i class="mdi mdi-tag-multiple-outline"></i>
|
<i class="mdi mdi-tag-multiple-outline"></i>
|
||||||
<span>Tag Cloud</span>
|
<span>Tag Cloud</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="{$base_path}/picture-wall?{function=" ltrim($searchcrits, '&' )"}" class="sidebar-link{if="
|
<a href="{$base_path}/picture-wall?{function="ltrim($searchcrits, '&')"}" class="sidebar-link{if="$pageName=='picwall'"} active{/if}">
|
||||||
$pageName=='picwall'"} active{/if}">
|
|
||||||
<i class="mdi mdi-image-multiple-outline"></i>
|
<i class="mdi mdi-image-multiple-outline"></i>
|
||||||
<span>Picture Wall</span>
|
<span>Picture Wall</span>
|
||||||
</a>
|
</a>
|
||||||
@ -34,16 +37,24 @@
|
|||||||
<i class="mdi mdi-calendar-today"></i>
|
<i class="mdi mdi-calendar-today"></i>
|
||||||
<span>Daily</span>
|
<span>Daily</span>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="{$base_path}/?searchtags=todo" class="sidebar-link{if="isset($search_tags) && $search_tags == 'todo'"} active{/if}">
|
||||||
|
<i class="mdi mdi-check-circle-outline"></i>
|
||||||
|
<span>Mes Tâches</span>
|
||||||
|
</a>
|
||||||
|
<a href="{$base_path}/?searchtags=note" class="sidebar-link{if="isset($search_tags) && $search_tags == 'note'"} active{/if}">
|
||||||
|
<i class="mdi mdi-note-text-outline"></i>
|
||||||
|
<span>Notes</span>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{if="$is_logged_in"}
|
{if="$is_logged_in"}
|
||||||
<div class="sidebar-section">
|
<div class="sidebar-section">
|
||||||
<div class="sidebar-section-title">Admin</div>
|
<div class="sidebar-section-title">Admin</div>
|
||||||
<a href="{$base_path}/admin/tools" class="sidebar-link">
|
<a href="{$base_path}/admin/tools" class="sidebar-link{if="$pageName == 'tools'"} active{/if}">
|
||||||
<i class="mdi mdi-tools"></i>
|
<i class="mdi mdi-tools"></i>
|
||||||
<span>Tools</span>
|
<span>Tools</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="{$base_path}/admin/configure" class="sidebar-link">
|
<a href="{$base_path}/admin/configure" class="sidebar-link{if="$pageName == 'configure'"} active{/if}">
|
||||||
<i class="mdi mdi-cog-outline"></i>
|
<i class="mdi mdi-cog-outline"></i>
|
||||||
<span>Settings</span>
|
<span>Settings</span>
|
||||||
</a>
|
</a>
|
||||||
@ -106,6 +117,14 @@
|
|||||||
<i class="mdi mdi-calendar"></i>
|
<i class="mdi mdi-calendar"></i>
|
||||||
<span>DAILY</span>
|
<span>DAILY</span>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="{$base_path}/?searchtags=todo" class="header-nav-link{if="isset($search_tags) && $search_tags == 'todo'"} active{/if}">
|
||||||
|
<i class="mdi mdi-check-circle-outline"></i>
|
||||||
|
<span>TÂCHES</span>
|
||||||
|
</a>
|
||||||
|
<a href="{$base_path}/?searchtags=note" class="header-nav-link{if="isset($search_tags) && $search_tags == 'note'"} active{/if}">
|
||||||
|
<i class="mdi mdi-note-text-outline"></i>
|
||||||
|
<span>NOTES</span>
|
||||||
|
</a>
|
||||||
<button class="header-nav-link" id="search-toggle-btn" title="Search (Press S)">
|
<button class="header-nav-link" id="search-toggle-btn" title="Search (Press S)">
|
||||||
<i class="mdi mdi-magnify"></i>
|
<i class="mdi mdi-magnify"></i>
|
||||||
<span>SEARCH</span>
|
<span>SEARCH</span>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user