feat: refactorer le système de couleurs des notes avec palette unifiée pour thème clair/sombre, ajout de support pour images de fond, amélioration du popup de palette avec grille responsive et accessibilité, et correction du z-index et overflow pour permettre l'affichage correct des popups
This commit is contained in:
+709
-53
@@ -6,6 +6,10 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
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") {
|
||||
@@ -21,6 +25,263 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
}
|
||||
});
|
||||
|
||||
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
|
||||
*/
|
||||
@@ -305,10 +566,29 @@ function initNoteView(linkList, container) {
|
||||
const modalOverlay = document.createElement("div");
|
||||
modalOverlay.className = "note-modal-overlay";
|
||||
modalOverlay.innerHTML = `
|
||||
<div class="note-modal">
|
||||
<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">
|
||||
<button class="btn btn-primary" id="note-modal-close">Fermer</button>
|
||||
<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>
|
||||
`;
|
||||
@@ -337,6 +617,103 @@ function initNoteView(linkList, container) {
|
||||
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) {
|
||||
@@ -364,12 +741,20 @@ function parseNoteFromLink(linkEl) {
|
||||
|
||||
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);
|
||||
color = potentialColor;
|
||||
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);
|
||||
}
|
||||
@@ -385,30 +770,34 @@ function parseNoteFromLink(linkEl) {
|
||||
// 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 };
|
||||
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
|
||||
notes.sort((a, b) => {
|
||||
visibleNotes.sort((a, b) => {
|
||||
const aPinned = a.tags.includes("shaarli-pin");
|
||||
const bPinned = b.tags.includes("shaarli-pin");
|
||||
return bPinned - aPinned;
|
||||
});
|
||||
|
||||
notes.forEach((note) => {
|
||||
visibleNotes.forEach((note) => {
|
||||
const card = document.createElement("div");
|
||||
card.className = `note-card note-color-${note.color}`;
|
||||
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);
|
||||
});
|
||||
|
||||
@@ -488,18 +877,14 @@ function renderNotes(container, notes, viewMode) {
|
||||
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");
|
||||
});
|
||||
|
||||
// Close palette when clicking outside
|
||||
// (Handled globally or card based? simple: card mouseleave?)
|
||||
card.addEventListener("mouseleave", () => {
|
||||
palettePopup.classList.remove("open");
|
||||
positionPalettePopup(palettePopup);
|
||||
});
|
||||
|
||||
inner.appendChild(actions);
|
||||
@@ -509,60 +894,203 @@ function renderNotes(container, notes, viewMode) {
|
||||
});
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
// 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)
|
||||
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);
|
||||
|
||||
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.
|
||||
function generateModalPaletteButtons(note) {
|
||||
const currentColor = note.color || "default";
|
||||
const currentBackground = note.background || "none";
|
||||
|
||||
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("");
|
||||
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) {
|
||||
// Remove all color classes
|
||||
card.classList.forEach((cls) => {
|
||||
if (cls.startsWith("note-color-")) card.classList.remove(cls);
|
||||
});
|
||||
card.classList.add(`note-color-${color}`);
|
||||
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
|
||||
@@ -592,7 +1120,11 @@ window.setNoteColor = function (noteId, color, editUrl) {
|
||||
let tagsArray = currentTags.split(/[\s,]+/).filter((t) => t.trim() !== "");
|
||||
|
||||
// Remove existing color tags
|
||||
tagsArray = tagsArray.filter((t) => !t.startsWith("note-"));
|
||||
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") {
|
||||
@@ -624,6 +1156,130 @@ window.setNoteColor = function (noteId, color, editUrl) {
|
||||
});
|
||||
};
|
||||
|
||||
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)
|
||||
========================================================== */
|
||||
|
||||
Reference in New Issue
Block a user