1557 lines
57 KiB
JavaScript
1557 lines
57 KiB
JavaScript
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");
|
|
|
|
if (typeof initBookmarkPaletteButtons === "function") {
|
|
initBookmarkPaletteButtons();
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
});
|
|
|
|
const NOTE_COLOR_OPTIONS = [
|
|
{ key: "default", label: "Par défaut", hex: "#20293A" },
|
|
{ key: "red", label: "Rouge", hex: "#f28b82" },
|
|
{ key: "orange", label: "Orange", hex: "#fbbc04" },
|
|
{ key: "yellow", label: "Jaune", hex: "#fff475" },
|
|
{ key: "green", label: "Vert", hex: "#ccff90" },
|
|
{ key: "teal", label: "Menthe", hex: "#a7ffeb" },
|
|
{ key: "blue", label: "Bleu clair", hex: "#cbf0f8" },
|
|
{ key: "darkblue", label: "Bleu", hex: "#aecbfa" },
|
|
{ key: "purple", label: "Violet", hex: "#d7aefb" },
|
|
{ key: "pink", label: "Rose", hex: "#fdcfe8" },
|
|
{ key: "brown", label: "Beige", hex: "#e6c9a8" },
|
|
{ key: "grey", label: "Gris", hex: "#e8eaed" },
|
|
];
|
|
|
|
const NOTE_BACKGROUND_TAG_PREFIX = "notebg-";
|
|
|
|
function resolveThemeAssetBasePath() {
|
|
const cssLink = Array.from(document.querySelectorAll('link[rel="stylesheet"]')).find(
|
|
(link) => link.href && link.href.includes("/custom_views.css"),
|
|
);
|
|
if (cssLink && cssLink.href) {
|
|
const cssUrl = new URL(cssLink.href, window.location.origin);
|
|
const cssPath = cssUrl.pathname.replace(/\/css\/custom_views\.css$/, "");
|
|
if (cssPath) return cssPath;
|
|
}
|
|
|
|
const jsScript = Array.from(document.querySelectorAll("script[src]")).find(
|
|
(script) => script.src && script.src.includes("/custom_views.js"),
|
|
);
|
|
if (jsScript && jsScript.src) {
|
|
const jsUrl = new URL(jsScript.src, window.location.origin);
|
|
const jsPath = jsUrl.pathname.replace(/\/js\/custom_views\.js$/, "");
|
|
if (jsPath) return jsPath;
|
|
}
|
|
|
|
if (window.shaarli && typeof window.shaarli.assetPath === "string" && window.shaarli.assetPath.trim() !== "") {
|
|
return window.shaarli.assetPath.replace(/\/$/, "");
|
|
}
|
|
|
|
return "/tpl/shaarli-pro";
|
|
}
|
|
|
|
const NOTE_BACKGROUND_BASE_PATH = `${resolveThemeAssetBasePath().replace(/\/$/, "")}/img/note-backgrounds`;
|
|
const NOTE_BACKGROUND_OPTIONS = [
|
|
{ key: "bg-canyon", label: "Canyon", file: "bg-canyon.png" },
|
|
{ key: "bg-leaves", label: "Feuilles", file: "bg-leaves.png" },
|
|
{ key: "bg-shore", label: "Rivage", file: "bg-shore.png" },
|
|
{ key: "bg-sunset", label: "Coucher", file: "bg-sunset.png" },
|
|
{ key: "bg-planet", label: "Planète", file: "bg-planet.png" },
|
|
{ key: "bg-crystal", label: "Cristal", file: "bg-crystal.png" },
|
|
{ key: "bg-orchid", label: "Orchidée", file: "bg-orchid.png" },
|
|
{ key: "bg-lake", label: "Lac", file: "bg-lake.png" },
|
|
{ key: "bg-ladder", label: "Échelle", file: "bg-ladder.png" },
|
|
{ key: "bg-burst", label: "Étoile", file: "bg-burst.png" },
|
|
];
|
|
|
|
function getNoteBackgroundUrl(backgroundKey) {
|
|
const found = NOTE_BACKGROUND_OPTIONS.find((bg) => bg.key === backgroundKey);
|
|
if (!found) return "";
|
|
|
|
return `${NOTE_BACKGROUND_BASE_PATH}/${found.file}`;
|
|
}
|
|
|
|
function positionPalettePopup(popup) {
|
|
if (!popup || !popup.classList.contains("open")) return;
|
|
|
|
popup.classList.remove("open-down");
|
|
popup.style.left = "";
|
|
popup.style.right = "";
|
|
|
|
const viewportPadding = 8;
|
|
const upRect = popup.getBoundingClientRect();
|
|
if (upRect.top < viewportPadding) {
|
|
popup.classList.add("open-down");
|
|
|
|
const downRect = popup.getBoundingClientRect();
|
|
if (downRect.bottom > window.innerHeight - viewportPadding) {
|
|
popup.classList.remove("open-down");
|
|
}
|
|
}
|
|
|
|
let rect = popup.getBoundingClientRect();
|
|
if (rect.right > window.innerWidth - viewportPadding) {
|
|
popup.style.left = "auto";
|
|
popup.style.right = "0";
|
|
rect = popup.getBoundingClientRect();
|
|
}
|
|
|
|
if (rect.left < viewportPadding) {
|
|
popup.style.left = "0";
|
|
popup.style.right = "auto";
|
|
}
|
|
}
|
|
|
|
function applyNoteVisualState(element, note) {
|
|
if (!element || !note) return;
|
|
|
|
const color = note.color || "default";
|
|
const background = note.background || "none";
|
|
|
|
element.classList.forEach((cls) => {
|
|
if (cls.startsWith("note-color-")) element.classList.remove(cls);
|
|
});
|
|
element.classList.add(`note-color-${color}`);
|
|
|
|
if (background && background !== "none") {
|
|
const bgUrl = getNoteBackgroundUrl(background);
|
|
if (bgUrl) {
|
|
element.classList.add("note-has-bg");
|
|
element.style.setProperty("--note-bg-image", `url('${bgUrl}')`);
|
|
element.dataset.background = background;
|
|
} else {
|
|
element.classList.remove("note-has-bg");
|
|
element.style.removeProperty("--note-bg-image");
|
|
element.dataset.background = "none";
|
|
}
|
|
} else {
|
|
element.classList.remove("note-has-bg");
|
|
element.style.removeProperty("--note-bg-image");
|
|
element.dataset.background = "none";
|
|
}
|
|
|
|
element.dataset.color = color;
|
|
}
|
|
|
|
function extractNoteVisualStateFromTags(tags) {
|
|
const safeTags = Array.isArray(tags) ? tags : [];
|
|
|
|
let color = "default";
|
|
const foundColorTag = safeTags.find((t) => typeof t === "string" && t.startsWith("note-"));
|
|
if (foundColorTag) {
|
|
const candidate = foundColorTag.substring(5);
|
|
if (NOTE_COLOR_OPTIONS.some((opt) => opt.key === candidate)) {
|
|
color = candidate;
|
|
}
|
|
}
|
|
|
|
let background = "none";
|
|
const foundBgTag = safeTags.find((t) => typeof t === "string" && t.startsWith(NOTE_BACKGROUND_TAG_PREFIX));
|
|
if (foundBgTag) {
|
|
const candidate = foundBgTag.substring(NOTE_BACKGROUND_TAG_PREFIX.length);
|
|
if (candidate && NOTE_BACKGROUND_OPTIONS.some((bg) => bg.key === candidate)) {
|
|
background = candidate;
|
|
}
|
|
}
|
|
|
|
return { color, background };
|
|
}
|
|
|
|
function initBookmarkPaletteButtons() {
|
|
const linkCards = Array.from(document.querySelectorAll(".link-outer"));
|
|
if (linkCards.length === 0) return;
|
|
|
|
linkCards.forEach((card) => {
|
|
const cardId = card.dataset.id || card.getAttribute("data-id") || card.id;
|
|
if (!cardId) return;
|
|
|
|
const tags = Array.from(card.querySelectorAll(".link-tag a")).map((a) => (a.textContent || "").trim());
|
|
const { color, background } = extractNoteVisualStateFromTags(tags);
|
|
applyNoteVisualState(card, { color, background });
|
|
|
|
const actions = card.querySelector(".link-actions");
|
|
if (!actions) return;
|
|
if (actions.querySelector(".bookmark-palette")) return;
|
|
|
|
const editLink = actions.querySelector('a[title="Modifier"], a[aria-label="Modifier ce bookmark"], a[href*="/admin/shaare/"]');
|
|
if (!editLink || !editLink.href) return;
|
|
const editUrl = editLink.href;
|
|
|
|
const paletteBtnId = `bookmark-palette-${cardId}`;
|
|
const wrapper = document.createElement("div");
|
|
wrapper.className = "bookmark-palette";
|
|
wrapper.style.position = "relative";
|
|
wrapper.innerHTML = `
|
|
<button type="button" title="Couleur" aria-label="Couleur" id="${paletteBtnId}"><i class="mdi mdi-palette-outline" aria-hidden="true"></i></button>
|
|
<div class="palette-popup" id="popup-${paletteBtnId}">
|
|
${generateBookmarkPaletteButtons(cardId, editUrl, color, background)}
|
|
</div>
|
|
`;
|
|
|
|
const pinLink = Array.from(actions.querySelectorAll('a[href*="/pin"], a[aria-label*="Épingler"], a[title*="Épingler"]'))[0];
|
|
if (pinLink && pinLink.parentNode === actions) {
|
|
actions.insertBefore(wrapper, pinLink);
|
|
} else if (editLink && editLink.parentNode === actions) {
|
|
actions.insertBefore(wrapper, editLink.nextSibling);
|
|
} else {
|
|
actions.appendChild(wrapper);
|
|
}
|
|
|
|
const paletteBtn = wrapper.querySelector(`#${paletteBtnId}`);
|
|
const palettePopup = wrapper.querySelector(`#popup-${paletteBtnId}`);
|
|
|
|
if (!paletteBtn || !palettePopup) return;
|
|
|
|
paletteBtn.addEventListener("click", (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
document.querySelectorAll(".palette-popup.open").forEach((p) => {
|
|
if (p !== palettePopup) {
|
|
p.classList.remove("open");
|
|
const parentCard = p.closest(".link-outer");
|
|
if (parentCard) parentCard.classList.remove("palette-open");
|
|
}
|
|
});
|
|
const nextOpenState = !palettePopup.classList.contains("open");
|
|
palettePopup.classList.toggle("open");
|
|
card.classList.toggle("palette-open", nextOpenState);
|
|
positionPalettePopup(palettePopup);
|
|
});
|
|
});
|
|
|
|
document.addEventListener("click", (e) => {
|
|
if (e.target.closest(".bookmark-palette")) return;
|
|
document.querySelectorAll(".palette-popup.open").forEach((p) => p.classList.remove("open"));
|
|
document.querySelectorAll(".link-outer.palette-open").forEach((el) => el.classList.remove("palette-open"));
|
|
});
|
|
}
|
|
|
|
function generateBookmarkPaletteButtons(bookmarkId, editUrl, currentColor, currentBackground) {
|
|
const color = currentColor || "default";
|
|
const background = currentBackground || "none";
|
|
|
|
const colorButtons = [
|
|
`<button class="palette-btn palette-btn-default ${color === "default" ? "is-active" : ""}" title="Par défaut" onclick="setNoteColor('${bookmarkId}', 'default', '${editUrl}')"><i class="mdi mdi-format-color-reset"></i></button>`,
|
|
...NOTE_COLOR_OPTIONS.filter((opt) => opt.key !== "default").map(
|
|
(opt) =>
|
|
`<button class="palette-btn note-color-${opt.key} ${color === opt.key ? "is-active" : ""}" title="${opt.label}" onclick="setNoteColor('${bookmarkId}', '${opt.key}', '${editUrl}')" style="background-color:${opt.hex}"></button>`,
|
|
),
|
|
].join("");
|
|
|
|
const backgroundButtons = [
|
|
`<button class="palette-btn palette-btn-bg-none ${background === "none" ? "is-active" : ""}" title="Sans image" onclick="setNoteBackground('${bookmarkId}', 'none', '${editUrl}')"><i class="mdi mdi-image-off-outline"></i></button>`,
|
|
...NOTE_BACKGROUND_OPTIONS.map((bg) => {
|
|
const bgUrl = getNoteBackgroundUrl(bg.key);
|
|
return `<button class="palette-btn palette-btn-bg ${background === bg.key ? "is-active" : ""}" title="${bg.label}" onclick="setNoteBackground('${bookmarkId}', '${bg.key}', '${editUrl}')" style="background-image:url('${bgUrl}')"></button>`;
|
|
}),
|
|
].join("");
|
|
|
|
return `
|
|
<div class="palette-row palette-row-colors">${colorButtons}</div>
|
|
<div class="palette-row palette-row-backgrounds">${backgroundButtons}</div>
|
|
`;
|
|
}
|
|
|
|
function syncNoteFromCardElement(note, card) {
|
|
if (!note || !card) return;
|
|
|
|
const colorClass = Array.from(card.classList).find((cls) => cls.startsWith("note-color-"));
|
|
if (colorClass) {
|
|
note.color = colorClass.replace("note-color-", "") || "default";
|
|
}
|
|
|
|
const background = card.dataset.background;
|
|
note.background = background && background !== "none" ? background : "none";
|
|
}
|
|
|
|
/**
|
|
* 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);">TÂCHES</div>
|
|
<div class="todo-list-item active" onclick="filterTasksByGroup('all')">
|
|
<i class="mdi mdi-inbox" aria-hidden="true"></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;" aria-label="${tasks.length} tâches">${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 note-color-default">
|
|
<div class="note-modal-header">
|
|
<h2 class="note-title" id="note-modal-title"></h2>
|
|
<button type="button" class="note-modal-pin-toggle" id="note-modal-pin" title="Épingler">
|
|
<i class="mdi mdi-pin-outline"></i>
|
|
</button>
|
|
</div>
|
|
<div class="note-modal-content"></div>
|
|
<div class="note-modal-tags is-empty" id="note-modal-tags"></div>
|
|
<div class="note-modal-actions">
|
|
<div class="note-modal-actions-left">
|
|
<div class="note-modal-color-picker">
|
|
<button type="button" id="note-modal-color-btn" title="Couleur"><i class="mdi mdi-palette-outline"></i></button>
|
|
<div class="palette-popup note-modal-palette" id="note-modal-color-popup"></div>
|
|
</div>
|
|
<button type="button" title="Rappel"><i class="mdi mdi-bell-outline"></i></button>
|
|
<button type="button" title="Collaborateur"><i class="mdi mdi-account-plus-outline"></i></button>
|
|
<button type="button" title="Image"><i class="mdi mdi-image-outline"></i></button>
|
|
<button type="button" id="note-modal-archive" title="Archiver"><i class="mdi mdi-archive-arrow-down-outline"></i></button>
|
|
<a href="#" id="note-modal-edit" title="Modifier"><i class="mdi mdi-pencil-outline"></i></a>
|
|
<button type="button" id="note-modal-delete" title="Supprimer"><i class="mdi mdi-dots-vertical"></i></button>
|
|
</div>
|
|
<button type="button" class="note-modal-close-btn" 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");
|
|
});
|
|
|
|
const modalPinBtn = modalOverlay.querySelector("#note-modal-pin");
|
|
const modalColorBtn = modalOverlay.querySelector("#note-modal-color-btn");
|
|
const modalColorPopup = modalOverlay.querySelector("#note-modal-color-popup");
|
|
|
|
modalColorBtn.addEventListener("click", (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
modalColorPopup.classList.toggle("open");
|
|
positionPalettePopup(modalColorPopup);
|
|
});
|
|
|
|
modalPinBtn.addEventListener("click", (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
const modalCard = modalOverlay.querySelector(".note-modal");
|
|
const noteId = modalCard.dataset.noteId;
|
|
const editUrl = modalCard.dataset.editUrl;
|
|
|
|
if (!noteId || !editUrl) return;
|
|
|
|
togglePinTag(noteId, editUrl, modalPinBtn);
|
|
|
|
const isPinned = modalPinBtn.classList.contains("active");
|
|
let tags = (modalCard.dataset.tags || "")
|
|
.split("||")
|
|
.filter((t) => t);
|
|
|
|
if (isPinned) {
|
|
if (!tags.includes("shaarli-pin")) tags.push("shaarli-pin");
|
|
} else {
|
|
tags = tags.filter((t) => t !== "shaarli-pin");
|
|
}
|
|
|
|
modalCard.dataset.tags = tags.join("||");
|
|
renderModalTags(modalOverlay.querySelector("#note-modal-tags"), tags);
|
|
|
|
if (modalOverlay.currentNote) {
|
|
modalOverlay.currentNote.isPinned = isPinned;
|
|
modalOverlay.currentNote.tags = tags;
|
|
}
|
|
});
|
|
|
|
modalOverlay.querySelector("#note-modal-delete").addEventListener("click", () => {
|
|
const modalCard = modalOverlay.querySelector(".note-modal");
|
|
const deleteUrl = modalCard.dataset.deleteUrl;
|
|
if (deleteUrl && deleteUrl !== "#") {
|
|
window.location.href = deleteUrl;
|
|
}
|
|
});
|
|
|
|
modalOverlay.querySelector("#note-modal-archive").addEventListener("click", (e) => {
|
|
e.preventDefault();
|
|
|
|
const modalCard = modalOverlay.querySelector(".note-modal");
|
|
const noteId = modalCard.dataset.noteId;
|
|
const editUrl = modalCard.dataset.editUrl;
|
|
if (!noteId || !editUrl) return;
|
|
|
|
addTagToNote(editUrl, "shaarli-archiver")
|
|
.then(() => {
|
|
if (modalOverlay.currentNote) {
|
|
if (!modalOverlay.currentNote.tags.includes("shaarli-archiver")) {
|
|
modalOverlay.currentNote.tags.push("shaarli-archiver");
|
|
}
|
|
}
|
|
|
|
const noteCard = document.querySelector(`.note-card[data-id="${noteId}"]`);
|
|
if (noteCard) noteCard.remove();
|
|
|
|
const index = notes.findIndex((n) => String(n.id) === String(noteId));
|
|
if (index > -1) notes.splice(index, 1);
|
|
|
|
modalOverlay.classList.remove("open");
|
|
})
|
|
.catch((err) => {
|
|
console.error("Error archiving note:", err);
|
|
alert("Erreur lors de l'archivage de la note.");
|
|
});
|
|
});
|
|
|
|
modalOverlay.addEventListener("click", (e) => {
|
|
if (!e.target.closest(".note-modal-color-picker")) {
|
|
modalColorPopup.classList.remove("open");
|
|
}
|
|
});
|
|
|
|
document.addEventListener("click", (e) => {
|
|
if (e.target.closest(".note-hover-actions .palette-popup") || e.target.closest('.note-hover-actions [id^="palette-"]')) {
|
|
return;
|
|
}
|
|
|
|
document.querySelectorAll(".note-hover-actions .palette-popup.open").forEach((p) => {
|
|
p.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";
|
|
let background = "none";
|
|
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);
|
|
const knownColor = NOTE_COLOR_OPTIONS.some((opt) => opt.key === potentialColor);
|
|
if (knownColor) {
|
|
color = potentialColor;
|
|
} else {
|
|
tags.push(t);
|
|
}
|
|
} else if (t.startsWith(NOTE_BACKGROUND_TAG_PREFIX)) {
|
|
background = t.substring(NOTE_BACKGROUND_TAG_PREFIX.length) || "none";
|
|
} 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, background, editUrl, deleteUrl, pinUrl, isPinned };
|
|
}
|
|
|
|
function renderNotes(container, notes, viewMode) {
|
|
container.innerHTML = "";
|
|
container.className = viewMode === "grid" ? "notes-masonry" : "notes-list-view";
|
|
|
|
const visibleNotes = notes.filter((note) => !(note.tags || []).includes("shaarli-archiver"));
|
|
|
|
// Sort: Pinned items first
|
|
visibleNotes.sort((a, b) => {
|
|
const aPinned = a.tags.includes("shaarli-pin");
|
|
const bPinned = b.tags.includes("shaarli-pin");
|
|
return bPinned - aPinned;
|
|
});
|
|
|
|
visibleNotes.forEach((note) => {
|
|
const card = document.createElement("div");
|
|
card.className = "note-card";
|
|
card.dataset.id = note.id;
|
|
applyNoteVisualState(card, note);
|
|
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;
|
|
syncNoteFromCardElement(note, card);
|
|
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.preventDefault();
|
|
e.stopPropagation();
|
|
// Close others?
|
|
document.querySelectorAll(".palette-popup.open").forEach((p) => {
|
|
if (p !== palettePopup) p.classList.remove("open");
|
|
});
|
|
palettePopup.classList.toggle("open");
|
|
positionPalettePopup(palettePopup);
|
|
});
|
|
|
|
inner.appendChild(actions);
|
|
card.appendChild(inner);
|
|
|
|
container.appendChild(card);
|
|
});
|
|
}
|
|
|
|
function setModalPinButtonState(button, isPinned) {
|
|
if (!button) return;
|
|
|
|
button.classList.toggle("active", isPinned);
|
|
button.title = isPinned ? "Désépingler" : "Épingler";
|
|
|
|
const icon = button.querySelector("i");
|
|
if (icon) {
|
|
icon.className = `mdi ${isPinned ? "mdi-pin" : "mdi-pin-outline"}`;
|
|
}
|
|
}
|
|
|
|
function renderModalTags(container, tags) {
|
|
if (!container) return;
|
|
|
|
container.innerHTML = "";
|
|
const visibleTags = (tags || []).filter((tag) => tag && tag !== "note");
|
|
|
|
if (visibleTags.length === 0) {
|
|
container.classList.add("is-empty");
|
|
return;
|
|
}
|
|
|
|
container.classList.remove("is-empty");
|
|
visibleTags.forEach((tag) => {
|
|
const tagEl = document.createElement("span");
|
|
tagEl.className = "note-tag";
|
|
tagEl.textContent = tag;
|
|
container.appendChild(tagEl);
|
|
});
|
|
}
|
|
|
|
function openNoteModal(note) {
|
|
const modal = document.querySelector(".note-modal-overlay");
|
|
if (!modal) return;
|
|
|
|
const modalCard = modal.querySelector(".note-modal");
|
|
const title = modal.querySelector("#note-modal-title");
|
|
const content = modal.querySelector(".note-modal-content");
|
|
const tagsContainer = modal.querySelector("#note-modal-tags");
|
|
const editLink = modal.querySelector("#note-modal-edit");
|
|
const pinButton = modal.querySelector("#note-modal-pin");
|
|
const modalColorPopup = modal.querySelector("#note-modal-color-popup");
|
|
|
|
modal.currentNote = note;
|
|
|
|
modalCard.className = "note-modal";
|
|
applyNoteVisualState(modalCard, note);
|
|
modalCard.dataset.noteId = note.id || "";
|
|
modalCard.dataset.editUrl = note.editUrl || "";
|
|
modalCard.dataset.deleteUrl = note.deleteUrl || "";
|
|
modalCard.dataset.background = note.background || "none";
|
|
|
|
const visibleTags = (note.tags || []).filter((tag) => tag && tag !== "note");
|
|
modalCard.dataset.tags = visibleTags.join("||");
|
|
|
|
title.textContent = note.title || "Sans titre";
|
|
content.innerHTML = `<div class="note-body">${note.descHtml || ""}</div>`;
|
|
renderModalTags(tagsContainer, visibleTags);
|
|
|
|
if (editLink) {
|
|
editLink.href = note.editUrl || "#";
|
|
}
|
|
|
|
if (modalColorPopup) {
|
|
modalColorPopup.innerHTML = generateModalPaletteButtons(note);
|
|
modalColorPopup.classList.remove("open");
|
|
}
|
|
|
|
setModalPinButtonState(pinButton, !!note.isPinned);
|
|
|
|
modal.classList.add("open");
|
|
}
|
|
|
|
function generateModalPaletteButtons(note) {
|
|
const currentColor = note.color || "default";
|
|
const currentBackground = note.background || "none";
|
|
|
|
const colorButtons = [
|
|
`<button class="palette-btn palette-btn-default ${currentColor === "default" ? "is-active" : ""}" title="Par défaut" onclick="setModalNoteColor('default')"><i class="mdi mdi-format-color-reset"></i></button>`,
|
|
...NOTE_COLOR_OPTIONS.filter((opt) => opt.key !== "default").map(
|
|
(opt) =>
|
|
`<button class="palette-btn note-color-${opt.key} ${currentColor === opt.key ? "is-active" : ""}" title="${opt.label}" onclick="setModalNoteColor('${opt.key}')" style="background-color:${opt.hex}"></button>`,
|
|
),
|
|
].join("");
|
|
|
|
const backgroundButtons = [
|
|
`<button class="palette-btn palette-btn-bg-none ${currentBackground === "none" ? "is-active" : ""}" title="Sans image" onclick="setModalNoteBackground('none')"><i class="mdi mdi-image-off-outline"></i></button>`,
|
|
...NOTE_BACKGROUND_OPTIONS.map((bg) => {
|
|
const bgUrl = getNoteBackgroundUrl(bg.key);
|
|
return `<button class="palette-btn palette-btn-bg ${currentBackground === bg.key ? "is-active" : ""}" title="${bg.label}" onclick="setModalNoteBackground('${bg.key}')" style="background-image:url('${bgUrl}')"></button>`;
|
|
}),
|
|
].join("");
|
|
|
|
return `
|
|
<div class="palette-row palette-row-colors">${colorButtons}</div>
|
|
<div class="palette-row palette-row-backgrounds">${backgroundButtons}</div>
|
|
`;
|
|
}
|
|
|
|
window.setModalNoteColor = function (color) {
|
|
const modal = document.querySelector(".note-modal-overlay");
|
|
if (!modal || !modal.currentNote) return;
|
|
|
|
const currentNote = modal.currentNote;
|
|
setNoteColor(currentNote.id, color, currentNote.editUrl);
|
|
|
|
currentNote.color = color;
|
|
const modalCard = modal.querySelector(".note-modal");
|
|
if (modalCard) {
|
|
applyNoteVisualState(modalCard, currentNote);
|
|
}
|
|
|
|
const modalColorPopup = modal.querySelector("#note-modal-color-popup");
|
|
if (modalColorPopup) {
|
|
modalColorPopup.innerHTML = generateModalPaletteButtons(currentNote);
|
|
modalColorPopup.classList.add("open");
|
|
positionPalettePopup(modalColorPopup);
|
|
}
|
|
};
|
|
|
|
window.setModalNoteBackground = function (backgroundKey) {
|
|
const modal = document.querySelector(".note-modal-overlay");
|
|
if (!modal || !modal.currentNote) return;
|
|
|
|
const currentNote = modal.currentNote;
|
|
setNoteBackground(currentNote.id, backgroundKey, currentNote.editUrl);
|
|
|
|
currentNote.background = backgroundKey;
|
|
const modalCard = modal.querySelector(".note-modal");
|
|
if (modalCard) {
|
|
applyNoteVisualState(modalCard, currentNote);
|
|
}
|
|
|
|
const modalColorPopup = modal.querySelector("#note-modal-color-popup");
|
|
if (modalColorPopup) {
|
|
modalColorPopup.innerHTML = generateModalPaletteButtons(currentNote);
|
|
modalColorPopup.classList.add("open");
|
|
positionPalettePopup(modalColorPopup);
|
|
}
|
|
};
|
|
|
|
function generatePaletteButtons(note) {
|
|
const currentColor = note.color || "default";
|
|
const currentBackground = note.background || "none";
|
|
|
|
const colorButtons = [
|
|
`<button class="palette-btn palette-btn-default ${currentColor === "default" ? "is-active" : ""}" title="Par défaut" onclick="setNoteColor('${note.id}', 'default', '${note.editUrl}')"><i class="mdi mdi-format-color-reset"></i></button>`,
|
|
...NOTE_COLOR_OPTIONS.filter((opt) => opt.key !== "default").map(
|
|
(opt) =>
|
|
`<button class="palette-btn note-color-${opt.key} ${currentColor === opt.key ? "is-active" : ""}" title="${opt.label}" onclick="setNoteColor('${note.id}', '${opt.key}', '${note.editUrl}')" style="background-color:${opt.hex}"></button>`,
|
|
),
|
|
].join("");
|
|
|
|
const backgroundButtons = [
|
|
`<button class="palette-btn palette-btn-bg-none ${currentBackground === "none" ? "is-active" : ""}" title="Sans image" onclick="setNoteBackground('${note.id}', 'none', '${note.editUrl}')"><i class="mdi mdi-image-off-outline"></i></button>`,
|
|
...NOTE_BACKGROUND_OPTIONS.map((bg) => {
|
|
const bgUrl = getNoteBackgroundUrl(bg.key);
|
|
return `<button class="palette-btn palette-btn-bg ${currentBackground === bg.key ? "is-active" : ""}" title="${bg.label}" onclick="setNoteBackground('${note.id}', '${bg.key}', '${note.editUrl}')" style="background-image:url('${bgUrl}')"></button>`;
|
|
}),
|
|
].join("");
|
|
|
|
return `
|
|
<div class="palette-row palette-row-colors">${colorButtons}</div>
|
|
<div class="palette-row palette-row-backgrounds">${backgroundButtons}</div>
|
|
`;
|
|
}
|
|
|
|
window.setNoteColor = function (noteId, color, editUrl) {
|
|
// 1. Visual Update (Immediate feedback)
|
|
const card = document.querySelector(`.note-card[data-id="${noteId}"]`);
|
|
if (card) {
|
|
const background = card.dataset.background || "none";
|
|
applyNoteVisualState(card, { color, background });
|
|
}
|
|
|
|
const bookmarkCard = document.querySelector(`.link-outer[data-id="${noteId}"]`);
|
|
if (bookmarkCard) {
|
|
const background = bookmarkCard.dataset.background || "none";
|
|
applyNoteVisualState(bookmarkCard, { color, background });
|
|
const palettePopup = bookmarkCard.querySelector(".bookmark-palette .palette-popup");
|
|
if (palettePopup) {
|
|
palettePopup.innerHTML = generateBookmarkPaletteButtons(noteId, editUrl, color, background);
|
|
}
|
|
}
|
|
|
|
const modal = document.querySelector(".note-modal-overlay");
|
|
if (modal && modal.currentNote && String(modal.currentNote.id) === String(noteId)) {
|
|
modal.currentNote.color = color;
|
|
const modalCard = modal.querySelector(".note-modal");
|
|
if (modalCard) {
|
|
applyNoteVisualState(modalCard, modal.currentNote);
|
|
}
|
|
const modalColorPopup = modal.querySelector("#note-modal-color-popup");
|
|
if (modalColorPopup) {
|
|
modalColorPopup.innerHTML = generateModalPaletteButtons(modal.currentNote);
|
|
}
|
|
}
|
|
|
|
// 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) => {
|
|
if (!t.startsWith("note-")) return true;
|
|
const colorKey = t.substring(5);
|
|
return !NOTE_COLOR_OPTIONS.some((opt) => opt.key === colorKey);
|
|
});
|
|
|
|
// 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.");
|
|
});
|
|
};
|
|
|
|
window.setNoteBackground = function (noteId, backgroundKey, editUrl) {
|
|
const card = document.querySelector(`.note-card[data-id="${noteId}"]`);
|
|
if (card) {
|
|
const colorClass = Array.from(card.classList).find((cls) => cls.startsWith("note-color-"));
|
|
const color = colorClass ? colorClass.replace("note-color-", "") : card.dataset.color || "default";
|
|
applyNoteVisualState(card, { color, background: backgroundKey });
|
|
}
|
|
|
|
const bookmarkCard = document.querySelector(`.link-outer[data-id="${noteId}"]`);
|
|
if (bookmarkCard) {
|
|
const colorClass = Array.from(bookmarkCard.classList).find((cls) => cls.startsWith("note-color-"));
|
|
const color = colorClass ? colorClass.replace("note-color-", "") : bookmarkCard.dataset.color || "default";
|
|
applyNoteVisualState(bookmarkCard, { color, background: backgroundKey });
|
|
const palettePopup = bookmarkCard.querySelector(".bookmark-palette .palette-popup");
|
|
if (palettePopup) {
|
|
palettePopup.innerHTML = generateBookmarkPaletteButtons(noteId, editUrl, color, backgroundKey);
|
|
}
|
|
}
|
|
|
|
const modal = document.querySelector(".note-modal-overlay");
|
|
if (modal && modal.currentNote && String(modal.currentNote.id) === String(noteId)) {
|
|
modal.currentNote.background = backgroundKey;
|
|
const modalCard = modal.querySelector(".note-modal");
|
|
if (modalCard) {
|
|
applyNoteVisualState(modalCard, modal.currentNote);
|
|
}
|
|
const modalColorPopup = modal.querySelector("#note-modal-color-popup");
|
|
if (modalColorPopup) {
|
|
modalColorPopup.innerHTML = generateModalPaletteButtons(modal.currentNote);
|
|
}
|
|
}
|
|
|
|
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() !== "");
|
|
|
|
tagsArray = tagsArray.filter((t) => !t.startsWith(NOTE_BACKGROUND_TAG_PREFIX));
|
|
if (backgroundKey && backgroundKey !== "none") {
|
|
tagsArray.push(`${NOTE_BACKGROUND_TAG_PREFIX}${backgroundKey}`);
|
|
}
|
|
|
|
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((response) => {
|
|
if (!response.ok) {
|
|
throw new Error("Failed to save background");
|
|
}
|
|
})
|
|
.catch((err) => {
|
|
console.error("Error saving note background:", err);
|
|
alert("Erreur lors de la sauvegarde du fond. Veuillez rafraîchir la page.");
|
|
});
|
|
};
|
|
|
|
function addTagToNote(editUrl, tag) {
|
|
return 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() !== "");
|
|
|
|
if (!tagsArray.includes(tag)) tagsArray.push(tag);
|
|
|
|
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((response) => {
|
|
if (!response.ok) throw new Error("Failed to update tags");
|
|
return response;
|
|
});
|
|
}
|
|
|
|
/* ==========================================================
|
|
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));
|
|
}
|