4581 lines
162 KiB
JavaScript
4581 lines
162 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");
|
|
|
|
const restoreDefaultListView = () => {
|
|
try {
|
|
document.body.classList.remove("view-todo", "view-notes", "view-archive");
|
|
} catch (e) {
|
|
// noop
|
|
}
|
|
|
|
const toolbar = document.querySelector(".content-toolbar");
|
|
if (toolbar) toolbar.style.display = "";
|
|
|
|
const list = document.getElementById("links-list");
|
|
if (list) list.style.display = "";
|
|
|
|
document.querySelectorAll(".notes-wrapper.todo-wrapper").forEach((el) => el.remove());
|
|
};
|
|
|
|
const startViewInitialization = function () {
|
|
if (typeof initBookmarkPaletteButtons === "function") {
|
|
initBookmarkPaletteButtons();
|
|
}
|
|
|
|
if (typeof organizePinnedBookmarks === "function") {
|
|
window.requestAnimationFrame(organizePinnedBookmarks);
|
|
}
|
|
|
|
// 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 === "shaarli-todo") {
|
|
try {
|
|
initTodoView(linkList, container);
|
|
} catch (err) {
|
|
console.error("Erreur lors de l'initialisation de la vue Todo:", err);
|
|
restoreDefaultListView();
|
|
}
|
|
} else if (searchTags === "note") {
|
|
// Pour la vue notes, parser les notes AVANT de supprimer les tags techniques
|
|
// afin que les propriétés visuelles (couleur, fond, etc.) soient correctement extraites
|
|
initNoteView(linkList, container);
|
|
// Puis supprimer les tags techniques de l'affichage
|
|
if (typeof initTagDisplayAndRemoval === "function") {
|
|
initTagDisplayAndRemoval();
|
|
}
|
|
} else if (searchTags === "shaarli-archive") {
|
|
// Vue Archive - similaire à Notes mais pour les notes archivées
|
|
initArchiveView(linkList, container);
|
|
// Puis supprimer les tags techniques de l'affichage
|
|
if (typeof initTagDisplayAndRemoval === "function") {
|
|
initTagDisplayAndRemoval();
|
|
}
|
|
} else {
|
|
// Vue standard : supprimer les tags techniques
|
|
if (typeof initTagDisplayAndRemoval === "function") {
|
|
initTagDisplayAndRemoval();
|
|
}
|
|
}
|
|
};
|
|
|
|
// Charger les backgrounds dynamiquement, puis initialiser les vues
|
|
initThemeModeBackgroundSync();
|
|
|
|
if (typeof loadBackgroundOptions === "function") {
|
|
loadBackgroundOptions()
|
|
.then(() => {
|
|
startViewInitialization();
|
|
})
|
|
.catch((err) => {
|
|
console.error("Erreur lors du chargement des backgrounds:", err);
|
|
// Initialiser quand même même si le chargement échoue
|
|
startViewInitialization();
|
|
});
|
|
} else {
|
|
startViewInitialization();
|
|
}
|
|
});
|
|
|
|
const NOTE_COLOR_OPTIONS = [
|
|
{
|
|
key: "default",
|
|
label: "Par défaut",
|
|
light: "#ffffff",
|
|
dark: "#20293A"
|
|
},
|
|
{
|
|
key: "red",
|
|
label: "Rouge",
|
|
light: "#f28b82",
|
|
dark: "#9c2116"
|
|
},
|
|
{
|
|
key: "orange",
|
|
label: "Orange",
|
|
light: "#fbbc04",
|
|
dark: "#9c7a16"
|
|
},
|
|
{
|
|
key: "yellow",
|
|
label: "Jaune",
|
|
light: "#fff475",
|
|
dark: "#9c9116"
|
|
},
|
|
{
|
|
key: "green",
|
|
label: "Vert",
|
|
light: "#ccff90",
|
|
dark: "#5e9c16"
|
|
},
|
|
{
|
|
key: "teal",
|
|
label: "Menthe",
|
|
light: "#a7ffeb",
|
|
dark: "#169c7d"
|
|
},
|
|
{
|
|
key: "blue",
|
|
label: "Bleu clair",
|
|
light: "#cbf0f8",
|
|
dark: "#16849c"
|
|
},
|
|
{
|
|
key: "darkblue",
|
|
label: "Bleu",
|
|
light: "#aecbfa",
|
|
dark: "#16499c"
|
|
},
|
|
{
|
|
key: "purple",
|
|
label: "Violet",
|
|
light: "#d7aefb",
|
|
dark: "#5d169c"
|
|
},
|
|
{
|
|
key: "pink",
|
|
label: "Rose",
|
|
light: "#fdcfe8",
|
|
dark: "#9c165f"
|
|
},
|
|
{
|
|
key: "brown",
|
|
label: "Beige",
|
|
light: "#e6c9a8",
|
|
dark: "#9c5d16"
|
|
},
|
|
{
|
|
key: "grey",
|
|
label: "Gris",
|
|
light: "#e8eaed",
|
|
dark: "#4e5764"
|
|
},
|
|
{
|
|
key: "custom",
|
|
label: "Personnalisé",
|
|
light: "#custom",
|
|
dark: "#custom"
|
|
}
|
|
];
|
|
|
|
const NOTE_FILTER_OPTIONS = [
|
|
{ key: "none", label: "Aucun", icon: "mdi-close-circle-outline" },
|
|
{ key: "glass", label: "Glass", icon: "mdi-blur" },
|
|
{ key: "vignette", label: "Vignette", icon: "mdi-vignette" },
|
|
{ key: "lined", label: "Ligné", icon: "mdi-format-align-justify" },
|
|
{ key: "grid", label: "Quadrillé", icon: "mdi-grid" },
|
|
{ key: "noise", label: "Noise", icon: "mdi-grain" },
|
|
{ key: "dots", label: "Points", icon: "mdi-dots-grid" },
|
|
{ key: "stripes", label: "Rayures", icon: "mdi-slash-forward" },
|
|
];
|
|
|
|
const NOTE_FONT_COLOR_OPTIONS = [
|
|
{ key: "auto", label: "Auto", value: "auto" },
|
|
{ key: "light", label: "Clair", value: "#f5f7fb" },
|
|
{ key: "dark", label: "Sombre", value: "#202124" },
|
|
{ key: "white", label: "Blanc", value: "#ffffff" },
|
|
{ key: "black", label: "Noir", value: "#000000" },
|
|
{ key: "custom", label: "Personnalisé", value: "custom" }
|
|
];
|
|
|
|
const NOTE_FONT_COLOR_TAG_PREFIX = "font-";
|
|
const NOTE_COLOR_TAG_PREFIX = "note-color-";
|
|
|
|
const NOTE_FILTER_TAG_PREFIX = "notefilter-";
|
|
const NOTE_BACKGROUND_TAG_PREFIX = "notebg-";
|
|
|
|
function isTechnicalTag(tag) {
|
|
if (typeof tag !== "string") return false;
|
|
const t = tag.trim();
|
|
if (!t) return false;
|
|
|
|
if (t === "note") return true;
|
|
if (t === "shaarli-todo") return true;
|
|
if (t === "shaarli-pin") return true;
|
|
if (t.startsWith(NOTE_FONT_COLOR_TAG_PREFIX)) return true;
|
|
if (t.startsWith(NOTE_COLOR_TAG_PREFIX)) return true;
|
|
if (t.startsWith(NOTE_FILTER_TAG_PREFIX)) return true;
|
|
if (t.startsWith(NOTE_BACKGROUND_TAG_PREFIX)) return true;
|
|
if (t.startsWith("notefilter-")) return true;
|
|
if (t.startsWith("notebg-")) return true;
|
|
if (t.startsWith("note-color-")) return true;
|
|
|
|
// Legacy note color tags: note-<color>
|
|
if (t.startsWith("note-")) {
|
|
const candidate = t.substring(5);
|
|
if (NOTE_COLOR_OPTIONS.some((opt) => opt.key === candidate)) return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function createTagPill({ tag, onRemoveClass = "tag-remove-btn", tagClass = "tag-pill" }) {
|
|
const wrapper = document.createElement("span");
|
|
wrapper.className = tagClass;
|
|
wrapper.dataset.tag = tag;
|
|
|
|
const text = document.createElement("span");
|
|
text.className = `${tagClass}-text`;
|
|
text.textContent = tag;
|
|
wrapper.appendChild(text);
|
|
|
|
const canRemove = arguments[0] && Object.prototype.hasOwnProperty.call(arguments[0], "canRemove") ? !!arguments[0].canRemove : true;
|
|
if (!canRemove) return wrapper;
|
|
|
|
const btn = document.createElement("button");
|
|
btn.type = "button";
|
|
btn.className = onRemoveClass;
|
|
btn.dataset.tag = tag;
|
|
btn.setAttribute("aria-label", `Supprimer le tag ${tag}`);
|
|
btn.title = "Supprimer";
|
|
btn.innerHTML = "×";
|
|
wrapper.appendChild(btn);
|
|
|
|
return wrapper;
|
|
}
|
|
|
|
function removeTagFromEntity(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, getFormFieldValue(input));
|
|
}
|
|
});
|
|
|
|
let currentTags = formData.get("lf_tags") || "";
|
|
let tagsArray = currentTags.split(/[\s,]+/).filter((t) => t.trim() !== "");
|
|
tagsArray = tagsArray.filter((t) => t !== 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;
|
|
});
|
|
}
|
|
|
|
let tagDisplayRemovalInitialized = false;
|
|
function initTagDisplayAndRemoval() {
|
|
if (tagDisplayRemovalInitialized) return;
|
|
|
|
// Filter technical tags on bookmarks list (standard view).
|
|
// Keep shaarli-pin in DOM (hidden) so initPinnedItems can still detect it.
|
|
document.querySelectorAll(".link-tag-list .link-tag").forEach((el) => {
|
|
const tag = (el.dataset.tag || el.textContent || "").trim();
|
|
if (!tag) return;
|
|
if (tag === "shaarli-pin") {
|
|
el.classList.add("is-tech-tag");
|
|
return;
|
|
}
|
|
if (isTechnicalTag(tag)) {
|
|
el.classList.add("is-tech-tag");
|
|
}
|
|
});
|
|
|
|
// Delegate remove button clicks (bookmarks + notes + modal)
|
|
document.addEventListener("click", function (e) {
|
|
const btn = e.target.closest(".tag-remove-btn, .note-tag-remove-btn");
|
|
if (!btn) return;
|
|
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
const tag = (btn.dataset.tag || "").trim();
|
|
if (!tag) return;
|
|
|
|
const tagEl = btn.closest(".link-tag, .note-tag");
|
|
const card = btn.closest(".link-outer, .note-card, .note-modal");
|
|
|
|
let editUrl = "";
|
|
if (card && card.classList.contains("note-card")) {
|
|
editUrl = card.dataset.editUrl || "";
|
|
} else if (card && card.classList.contains("note-modal")) {
|
|
editUrl = card.dataset.editUrl || "";
|
|
} else {
|
|
const actionsEl = card ? card.querySelector(".link-actions") : null;
|
|
const editLink = actionsEl ? actionsEl.querySelector('a[href*="admin/shaare"]') : null;
|
|
editUrl = editLink ? editLink.href : "";
|
|
}
|
|
|
|
if (!editUrl) return;
|
|
|
|
removeTagFromEntity(editUrl, tag)
|
|
.then(() => {
|
|
if (tagEl) tagEl.remove();
|
|
|
|
// Sync note datasets + modal tag list if applicable
|
|
if (card && card.classList.contains("note-card")) {
|
|
let tags = (card.dataset.tags || "").split("||").filter((t) => t);
|
|
tags = tags.filter((t) => t !== tag);
|
|
card.dataset.tags = tags.join("||");
|
|
|
|
const modal = getOpenModalOverlay();
|
|
const entity = getModalCurrentEntity(modal);
|
|
if (modal && entity && String(entity.id) === String(card.dataset.id)) {
|
|
entity.tags = (entity.tags || []).filter((t) => t !== tag);
|
|
const tagsContainer = modal.querySelector(".note-modal-tags");
|
|
renderModalTags(tagsContainer, entity.tags);
|
|
}
|
|
}
|
|
|
|
if (card && card.classList.contains("note-modal")) {
|
|
let tags = (card.dataset.tags || "").split("||").filter((t) => t);
|
|
tags = tags.filter((t) => t !== tag);
|
|
card.dataset.tags = tags.join("||");
|
|
|
|
const modal = getOpenModalOverlay();
|
|
const entity = getModalCurrentEntity(modal);
|
|
if (modal && entity) {
|
|
entity.tags = (entity.tags || []).filter((t) => t !== tag);
|
|
const tagsContainer = modal.querySelector(".note-modal-tags");
|
|
renderModalTags(tagsContainer, entity.tags);
|
|
}
|
|
|
|
// Also update corresponding card if visible
|
|
const entityId = card.dataset.noteId || card.dataset.todoId || (entity && entity.id ? String(entity.id) : "");
|
|
const entityCard = entityId ? document.querySelector(`.note-card[data-id="${entityId}"]`) : null;
|
|
if (entityCard) {
|
|
const pill = entityCard.querySelector(`.note-tag[data-tag="${CSS.escape(tag)}"]`);
|
|
if (pill) pill.remove();
|
|
let entityTags = (entityCard.dataset.tags || "").split("||").filter((t) => t);
|
|
entityTags = entityTags.filter((t) => t !== tag);
|
|
entityCard.dataset.tags = entityTags.join("||");
|
|
}
|
|
}
|
|
})
|
|
.catch((err) => {
|
|
console.error("Error removing tag:", err);
|
|
alert("Erreur lors de la suppression du tag.");
|
|
});
|
|
});
|
|
|
|
tagDisplayRemovalInitialized = true;
|
|
}
|
|
|
|
function getOpenModalOverlay() {
|
|
return document.querySelector(".note-modal-overlay.open");
|
|
}
|
|
|
|
function getModalCurrentEntity(modal) {
|
|
if (!modal) return null;
|
|
return modal.currentTodo || modal.currentNote || null;
|
|
}
|
|
|
|
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_ASSET_ROOT = `${resolveThemeAssetBasePath().replace(/\/$/, "")}/img`;
|
|
|
|
// Liste des backgrounds - chargée dynamiquement depuis le serveur
|
|
let NOTE_BACKGROUND_OPTIONS = [];
|
|
let isBackgroundOptionsLoaded = false;
|
|
let backgroundOptionsLoadPromise = null;
|
|
|
|
window.__shaarliCustomViewsVersion = "1.0.5-inline-bg-manifest";
|
|
if (typeof console !== "undefined" && console && typeof console.info === "function") {
|
|
console.info("[custom_views] loaded", window.__shaarliCustomViewsVersion);
|
|
}
|
|
|
|
function organizePinnedBookmarks() {
|
|
const linksList = document.getElementById("links-list");
|
|
if (!linksList) return;
|
|
|
|
const pinnedHead = linksList.querySelector(".pinned-section-head");
|
|
const normalHead = linksList.querySelector(".normal-section-head");
|
|
const compactHead = linksList.querySelector(".compact-table-head");
|
|
if (!pinnedHead || !normalHead || !compactHead) return;
|
|
|
|
const allBookmarks = Array.from(linksList.getElementsByClassName("link-outer"));
|
|
if (!allBookmarks.length) return;
|
|
|
|
const isPinnedBookmark = (bookmark) => {
|
|
if (bookmark.classList.contains("is-sticky") || bookmark.classList.contains("is-pinned-tag")) {
|
|
return true;
|
|
}
|
|
|
|
const tags = bookmark.querySelectorAll(".link-tag-list a, .link-tag-list .link-tag-link, .note-tag");
|
|
for (const tag of tags) {
|
|
const value = (tag.textContent || "").trim().toLowerCase();
|
|
if (value === "shaarli-pin") {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
const pinnedBookmarks = allBookmarks.filter((bookmark) => isPinnedBookmark(bookmark));
|
|
const normalBookmarks = allBookmarks.filter((bookmark) => !isPinnedBookmark(bookmark));
|
|
|
|
linksList.classList.toggle("has-pinned", pinnedBookmarks.length > 0);
|
|
linksList.classList.toggle("has-normal", normalBookmarks.length > 0);
|
|
|
|
const fragment = document.createDocumentFragment();
|
|
|
|
fragment.appendChild(pinnedHead);
|
|
pinnedBookmarks.forEach((bookmark) => fragment.appendChild(bookmark));
|
|
|
|
fragment.appendChild(normalHead);
|
|
fragment.appendChild(compactHead);
|
|
normalBookmarks.forEach((bookmark) => fragment.appendChild(bookmark));
|
|
|
|
linksList.replaceChildren(fragment);
|
|
}
|
|
|
|
function initPinnedBookmarksSeparation() {
|
|
const linksList = document.getElementById("links-list");
|
|
if (!linksList) return;
|
|
|
|
organizePinnedBookmarks();
|
|
|
|
const viewButtons = ["view-grid-btn", "view-list-btn", "view-compact-btn"];
|
|
viewButtons.forEach((id) => {
|
|
const button = document.getElementById(id);
|
|
if (!button) return;
|
|
button.addEventListener("click", () => {
|
|
window.requestAnimationFrame(organizePinnedBookmarks);
|
|
});
|
|
});
|
|
|
|
linksList.addEventListener("click", (event) => {
|
|
const pinLink = event.target.closest('a[href*="/pin?token="]');
|
|
if (!pinLink) return;
|
|
|
|
window.setTimeout(() => {
|
|
window.location.reload();
|
|
}, 500);
|
|
});
|
|
}
|
|
|
|
document.addEventListener("DOMContentLoaded", initPinnedBookmarksSeparation);
|
|
|
|
const NOTE_BACKGROUND_MANIFEST_INLINE = [
|
|
{ key: "cafe", label: "Café", files: { light: "bg-light-cafe.jpg", dark: "bg-dark-cafe.jpg" } },
|
|
{ key: "codes", label: "Codes", files: { light: "bg-light-codes.jpg", dark: "bg-dark-codes.jpg" } },
|
|
{ key: "dune", label: "Dune", files: { light: "bg-light-dune.jpg", dark: "bg-dark-dune.jpg" } },
|
|
{ key: "feuille-ligne", label: "Feuille Ligne", files: { light: "bg-light-feuilleLigne.jpg", dark: "bg-dark-feuilleLigne.jpg" } },
|
|
{ key: "feuille-quadrillage", label: "Feuille Quadrillage", files: { light: "bg-light-feuilleQuadrille.jpg", dark: "bg-dark-feuilleQuadrille.jpg" } },
|
|
{ key: "fleurs", label: "Fleurs", files: { light: "bg-light-fleurs.jpg", dark: "bg-dark-fleurs.jpg" } },
|
|
{ key: "foret", label: "Forêt", files: { light: "bg-light-foret.jpg", dark: "bg-dark-foret.jpg" } },
|
|
{ key: "grid", label: "Grid", files: { light: "bg-light-grid.jpg", dark: "bg-dark-grid.jpg" } },
|
|
{ key: "journal", label: "Journal", files: { light: "bg-light-journal.jpg", dark: "bg-dark-journal.jpg" } },
|
|
{ key: "lecture", label: "Lecture", files: { light: "bg-light-lecture.jpg", dark: "bg-dark-lecture.jpg" } },
|
|
{ key: "legumes", label: "Légumes", files: { light: "bg-light-legumes.jpg", dark: "bg-dark-legumes.jpg" } },
|
|
{ key: "montagnes", label: "Montagnes", files: { light: "bg-light-montagnes.jpg", dark: "bg-dark-montagnes.jpg" } },
|
|
{ key: "ocean", label: "Océan", files: { light: "bg-light-ocean.jpg", dark: "bg-dark-ocean.jpg" } },
|
|
{ key: "radio", label: "Radio", files: { light: "bg-light-radio.jpg", dark: "bg-dark-radio.jpg" } },
|
|
{ key: "sports", label: "Sports", files: { light: "bg-light-sports.jpg", dark: "bg-dark-sports.jpg" } },
|
|
{ key: "vague1", label: "Vague 1", files: { light: "bg-light-vague1.jpg", dark: "bg-dark-vague1.jpg" } },
|
|
{ key: "vague2", label: "Vague 2", files: { light: "bg-light-vague2.jpg", dark: "bg-dark-vague2.jpg" } },
|
|
{ key: "ville", label: "Ville", files: { light: "bg-light-ville.jpg", dark: "bg-dark-ville.jpg" } },
|
|
{ key: "voyage", label: "Voyage", files: { light: "bg-light-voyage.jpg", dark: "bg-dark-voyage.jpg" } },
|
|
];
|
|
|
|
/**
|
|
* Charge la liste des backgrounds depuis le endpoint PHP
|
|
* @returns {Promise<Array>} Liste des backgrounds
|
|
*/
|
|
function loadBackgroundOptions() {
|
|
if (isBackgroundOptionsLoaded) {
|
|
return Promise.resolve(NOTE_BACKGROUND_OPTIONS);
|
|
}
|
|
|
|
if (backgroundOptionsLoadPromise) {
|
|
return backgroundOptionsLoadPromise;
|
|
}
|
|
|
|
NOTE_BACKGROUND_OPTIONS = normalizeDynamicBackgroundOptions(NOTE_BACKGROUND_MANIFEST_INLINE);
|
|
isBackgroundOptionsLoaded = true;
|
|
backgroundOptionsLoadPromise = Promise.resolve(NOTE_BACKGROUND_OPTIONS);
|
|
return backgroundOptionsLoadPromise;
|
|
}
|
|
|
|
function normalizeBackgroundKey(key) {
|
|
if (typeof key !== "string") return "";
|
|
|
|
return key
|
|
.trim()
|
|
.toLowerCase()
|
|
.replace(/[\s_]+/g, "-")
|
|
.replace(/[^a-z0-9-]/g, "-")
|
|
.replace(/-+/g, "-")
|
|
.replace(/^-|-$/g, "");
|
|
}
|
|
|
|
function formatBackgroundLabel(rawLabel, fallbackKey) {
|
|
const base = (typeof rawLabel === "string" && rawLabel.trim() !== "" ? rawLabel : fallbackKey)
|
|
.replace(/[_-]+/g, " ")
|
|
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
.replace(/\s+/g, " ")
|
|
.trim();
|
|
|
|
return base
|
|
.split(" ")
|
|
.filter((part) => part !== "")
|
|
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
.join(" ");
|
|
}
|
|
|
|
function getCurrentThemeMode() {
|
|
return document.documentElement.getAttribute("data-theme") === "dark" ? "dark" : "light";
|
|
}
|
|
|
|
function getColorOption(colorKey) {
|
|
const normalizedKey = typeof colorKey === "string" ? colorKey.trim().toLowerCase() : "";
|
|
|
|
return NOTE_COLOR_OPTIONS.find((opt) => opt.key === normalizedKey) || NOTE_COLOR_OPTIONS.find((opt) => opt.key === "default") || null;
|
|
}
|
|
|
|
function getThemeColorValue(option, mode = getCurrentThemeMode()) {
|
|
if (!option || typeof option !== "object") return "";
|
|
|
|
const safeMode = mode === "dark" ? "dark" : "light";
|
|
const preferred = typeof option[safeMode] === "string" ? option[safeMode].trim() : "";
|
|
const fallbackLight = typeof option.light === "string" ? option.light.trim() : "";
|
|
const fallbackDark = typeof option.dark === "string" ? option.dark.trim() : "";
|
|
|
|
return preferred || fallbackLight || fallbackDark;
|
|
}
|
|
|
|
function parseHexColor(colorValue) {
|
|
if (typeof colorValue !== "string") return null;
|
|
|
|
const safeValue = colorValue.trim();
|
|
const shortHexMatch = /^#([0-9a-f]{3})$/i.exec(safeValue);
|
|
if (shortHexMatch) {
|
|
return {
|
|
r: parseInt(shortHexMatch[1].charAt(0).repeat(2), 16),
|
|
g: parseInt(shortHexMatch[1].charAt(1).repeat(2), 16),
|
|
b: parseInt(shortHexMatch[1].charAt(2).repeat(2), 16),
|
|
};
|
|
}
|
|
|
|
const longHexMatch = /^#([0-9a-f]{6})$/i.exec(safeValue);
|
|
if (longHexMatch) {
|
|
return {
|
|
r: parseInt(longHexMatch[1].slice(0, 2), 16),
|
|
g: parseInt(longHexMatch[1].slice(2, 4), 16),
|
|
b: parseInt(longHexMatch[1].slice(4, 6), 16),
|
|
};
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function getReadableForegroundForBackground(colorValue, mode = getCurrentThemeMode()) {
|
|
const rgb = parseHexColor(colorValue);
|
|
if (!rgb) {
|
|
return mode === "dark" ? "#e8eaed" : "#202124";
|
|
}
|
|
|
|
const luminance = (0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b) / 255;
|
|
return luminance > 0.56 ? "#202124" : "#f5f7fb";
|
|
}
|
|
|
|
function normalizeDynamicBackgroundOptions(rawOptions) {
|
|
const rawList = Array.isArray(rawOptions)
|
|
? rawOptions
|
|
: rawOptions && Array.isArray(rawOptions.backgrounds)
|
|
? rawOptions.backgrounds
|
|
: [];
|
|
|
|
const mergedByKey = {};
|
|
|
|
rawList.forEach((option) => {
|
|
if (!option || typeof option !== "object") return;
|
|
|
|
const key = normalizeBackgroundKey(typeof option.key === "string" ? option.key : option.label || "");
|
|
if (!key) return;
|
|
|
|
const files = option.files && typeof option.files === "object" ? option.files : {};
|
|
const lightFile = typeof files.light === "string" ? files.light.trim() : "";
|
|
const darkFile = typeof files.dark === "string" ? files.dark.trim() : "";
|
|
if (!lightFile && !darkFile) return;
|
|
|
|
if (!mergedByKey[key]) {
|
|
mergedByKey[key] = {
|
|
key,
|
|
label: formatBackgroundLabel(option.label || "", key),
|
|
paths: {
|
|
light: "",
|
|
dark: "",
|
|
},
|
|
};
|
|
}
|
|
|
|
if (lightFile) {
|
|
mergedByKey[key].paths.light = `note-bg-light/${lightFile}`;
|
|
}
|
|
if (darkFile) {
|
|
mergedByKey[key].paths.dark = `note-bg-dark/${darkFile}`;
|
|
}
|
|
});
|
|
|
|
return Object.values(mergedByKey).sort((a, b) => a.label.localeCompare(b.label, "fr", { sensitivity: "base" }));
|
|
}
|
|
|
|
function getAvailableBackgroundOptionsForMode(mode = getCurrentThemeMode()) {
|
|
const safeMode = mode === "dark" ? "dark" : "light";
|
|
return NOTE_BACKGROUND_OPTIONS.filter((option) => option.paths && option.paths[safeMode]);
|
|
}
|
|
|
|
function getNoteBackgroundUrl(backgroundKey, mode = getCurrentThemeMode()) {
|
|
const normalizedKey = normalizeBackgroundKey(backgroundKey);
|
|
if (!normalizedKey) return "";
|
|
|
|
const found = NOTE_BACKGROUND_OPTIONS.find((bg) => bg.key === normalizedKey);
|
|
if (!found || !found.paths) return "";
|
|
|
|
const safeMode = mode === "dark" ? "dark" : "light";
|
|
const selectedPath = found.paths[safeMode] || "";
|
|
|
|
if (!selectedPath) return "";
|
|
return `${NOTE_BACKGROUND_ASSET_ROOT}/${selectedPath}`;
|
|
}
|
|
|
|
function getElementVisualColor(element) {
|
|
if (!element) return "default";
|
|
|
|
if (element.dataset.color === "custom" && element.dataset.customColor) {
|
|
return `custom:${element.dataset.customColor}`;
|
|
}
|
|
|
|
const colorClass = Array.from(element.classList).find((cls) => cls.startsWith("note-color-"));
|
|
if (colorClass) return colorClass.replace("note-color-", "");
|
|
|
|
return element.dataset.color || "default";
|
|
}
|
|
|
|
function getElementVisualBackground(element) {
|
|
if (!element) return "none";
|
|
|
|
const normalizedBackground = normalizeBackgroundKey(element.dataset.background || "");
|
|
return normalizedBackground || "none";
|
|
}
|
|
|
|
function normalizeFilterKey(key) {
|
|
if (typeof key !== "string") return "none";
|
|
const normalized = key.trim().toLowerCase();
|
|
if (NOTE_FILTER_OPTIONS.some((opt) => opt.key === normalized)) {
|
|
return normalized;
|
|
}
|
|
return "none";
|
|
}
|
|
|
|
function getElementVisualFilter(element) {
|
|
if (!element) return "none";
|
|
return normalizeFilterKey(element.dataset.filter || "");
|
|
}
|
|
|
|
function getElementVisualFontColor(element) {
|
|
if (!element) return "auto";
|
|
return element.dataset.fontColor || "auto";
|
|
}
|
|
|
|
function refreshNoteFilterVisuals() {
|
|
document.querySelectorAll(".note-card, .note-modal, .link-outer").forEach((element) => {
|
|
applyNoteVisualState(element, {
|
|
color: getElementVisualColor(element),
|
|
filter: getElementVisualFilter(element),
|
|
background: getElementVisualBackground(element),
|
|
fontColor: getElementVisualFontColor(element),
|
|
});
|
|
});
|
|
}
|
|
|
|
function refreshBackgroundPalettes() {
|
|
document.querySelectorAll(".note-card").forEach((card) => {
|
|
const palettePopup = card.querySelector(".note-hover-actions .palette-popup");
|
|
if (!palettePopup) return;
|
|
|
|
const noteId = card.dataset.id || "";
|
|
if (!noteId) return;
|
|
|
|
const editLink = card.querySelector(
|
|
'.note-hover-actions a[href*="/admin/shaare/"], .note-hover-actions a[href*="do=editlink"], .note-hover-actions a[title="Edit"]',
|
|
);
|
|
|
|
palettePopup.innerHTML = generatePaletteButtons({
|
|
id: noteId,
|
|
editUrl: editLink && editLink.href ? editLink.href : "#",
|
|
color: getElementVisualColor(card),
|
|
background: getElementVisualBackground(card),
|
|
});
|
|
});
|
|
|
|
document.querySelectorAll(".link-outer").forEach((card) => {
|
|
const palettePopup = card.querySelector(".bookmark-palette .palette-popup");
|
|
if (!palettePopup) return;
|
|
|
|
const noteId = card.dataset.id || card.getAttribute("data-id") || card.id;
|
|
if (!noteId) return;
|
|
|
|
const actions = card.querySelector(".link-actions");
|
|
const editLink = actions
|
|
? actions.querySelector('a[title="Modifier"], a[aria-label="Modifier ce bookmark"], a[href*="/admin/shaare/"]')
|
|
: null;
|
|
|
|
palettePopup.innerHTML = generateBookmarkPaletteButtons(
|
|
noteId,
|
|
editLink && editLink.href ? editLink.href : "#",
|
|
getElementVisualColor(card),
|
|
getElementVisualBackground(card),
|
|
);
|
|
});
|
|
|
|
const modal = getOpenModalOverlay();
|
|
const entity = getModalCurrentEntity(modal);
|
|
if (!modal || !entity) return;
|
|
|
|
const modalCard = modal.querySelector(".note-modal");
|
|
if (modalCard) {
|
|
entity.color = getElementVisualColor(modalCard);
|
|
entity.background = getElementVisualBackground(modalCard);
|
|
}
|
|
|
|
const modalColorPopup = modal.querySelector(".note-modal-palette");
|
|
if (modalColorPopup) {
|
|
modalColorPopup.innerHTML = generateModalPaletteButtons(entity);
|
|
}
|
|
}
|
|
|
|
function parseBackgroundManifestPayload(rawPayload) {
|
|
const payload = typeof rawPayload === "string" ? rawPayload.trim() : "";
|
|
if (!payload) return [];
|
|
|
|
try {
|
|
return JSON.parse(payload);
|
|
} catch (parseError) {
|
|
const firstArrayBracket = payload.indexOf("[");
|
|
const lastArrayBracket = payload.lastIndexOf("]");
|
|
if (firstArrayBracket === -1 || lastArrayBracket === -1 || lastArrayBracket <= firstArrayBracket) {
|
|
throw parseError;
|
|
}
|
|
|
|
const jsonSlice = payload.slice(firstArrayBracket, lastArrayBracket + 1);
|
|
return JSON.parse(jsonSlice);
|
|
}
|
|
}
|
|
|
|
|
|
let isThemeModeBackgroundSyncInitialized = false;
|
|
|
|
function initThemeModeBackgroundSync() {
|
|
if (isThemeModeBackgroundSyncInitialized) return;
|
|
|
|
const root = document.documentElement;
|
|
if (!root) return;
|
|
|
|
let lastTheme = getCurrentThemeMode();
|
|
const observer = new MutationObserver((mutations) => {
|
|
const themeChanged = mutations.some((mutation) => mutation.attributeName === "data-theme");
|
|
if (!themeChanged) return;
|
|
|
|
const nextTheme = getCurrentThemeMode();
|
|
if (nextTheme === lastTheme) return;
|
|
|
|
lastTheme = nextTheme;
|
|
refreshNoteBackgroundVisuals();
|
|
refreshBackgroundPalettes();
|
|
});
|
|
|
|
observer.observe(root, { attributes: true, attributeFilter: ["data-theme"] });
|
|
isThemeModeBackgroundSyncInitialized = true;
|
|
}
|
|
|
|
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";
|
|
}
|
|
}
|
|
|
|
let backgroundStudioPanelInitialized = false;
|
|
|
|
function ensureBackgroundStudioPanel() {
|
|
if (backgroundStudioPanelInitialized) return;
|
|
if (document.getElementById("shaarli-bg-studio")) {
|
|
backgroundStudioPanelInitialized = true;
|
|
return;
|
|
}
|
|
|
|
const panel = document.createElement("div");
|
|
panel.id = "shaarli-bg-studio";
|
|
panel.className = "bg-studio-panel";
|
|
panel.setAttribute("role", "dialog");
|
|
panel.setAttribute("aria-modal", "false");
|
|
panel.setAttribute("aria-hidden", "true");
|
|
panel.style.display = "none";
|
|
document.body.appendChild(panel);
|
|
|
|
document.addEventListener("click", (e) => {
|
|
const p = document.getElementById("shaarli-bg-studio");
|
|
if (!p || !p.classList.contains("open")) return;
|
|
if (e.target.closest("#shaarli-bg-studio")) return;
|
|
const anchor = p.__anchorEl;
|
|
if (anchor && (e.target === anchor || e.target.closest(`#${anchor.id}`))) return;
|
|
closeBackgroundStudioPanel();
|
|
});
|
|
|
|
document.addEventListener("keydown", (e) => {
|
|
if (e.key !== "Escape") return;
|
|
const p = document.getElementById("shaarli-bg-studio");
|
|
if (!p || !p.classList.contains("open")) return;
|
|
closeBackgroundStudioPanel();
|
|
});
|
|
|
|
panel.addEventListener("click", (e) => {
|
|
const actionBtn = e.target.closest("button[data-bg-studio-action]");
|
|
if (!actionBtn) return;
|
|
|
|
const panelEl = document.getElementById("shaarli-bg-studio");
|
|
if (!panelEl) return;
|
|
|
|
const mode = panelEl.dataset.mode || "entity";
|
|
const entityId = panelEl.dataset.entityId || "";
|
|
const editUrl = panelEl.dataset.editUrl || "";
|
|
|
|
const action = actionBtn.dataset.bgStudioAction;
|
|
if (action === "close") {
|
|
closeBackgroundStudioPanel();
|
|
return;
|
|
}
|
|
|
|
if (action === "set-color") {
|
|
const key = actionBtn.dataset.colorKey || "default";
|
|
if (mode === "modal") setModalNoteColor(key);
|
|
else setNoteColor(entityId, key, editUrl);
|
|
return;
|
|
}
|
|
|
|
if (action === "set-background") {
|
|
const key = actionBtn.dataset.bgKey || "none";
|
|
if (mode === "modal") setModalNoteBackground(key);
|
|
else setNoteBackground(entityId, key, editUrl);
|
|
return;
|
|
}
|
|
|
|
if (action === "set-filter") {
|
|
const filterKey = actionBtn.dataset.filter || "none";
|
|
if (mode === "modal") setModalNoteFilter(filterKey);
|
|
else setNoteFilter(entityId, filterKey, editUrl);
|
|
return;
|
|
}
|
|
|
|
if (action === "set-query") {
|
|
panelEl.dataset.query = "";
|
|
renderBackgroundStudioPanel(panelEl);
|
|
const input = panelEl.querySelector(".bg-studio-search-input");
|
|
if (input) input.focus();
|
|
return;
|
|
}
|
|
|
|
if (action === "set-defaults") {
|
|
if (mode === "modal") {
|
|
setModalNoteColor("default");
|
|
setModalNoteFilter("none");
|
|
} else {
|
|
setNoteColor(entityId, "default", editUrl);
|
|
setNoteFilter(entityId, "none", editUrl);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (action === "reset-font-color") {
|
|
if (mode === "modal") {
|
|
setModalNoteFontColor("auto");
|
|
} else {
|
|
setNoteFontColor(entityId, "auto", editUrl);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (action === "open-color-picker") {
|
|
openColorPickerPanel({ mode, entityId, editUrl, type: "background" });
|
|
return;
|
|
}
|
|
|
|
if (action === "open-font-color-picker") {
|
|
openColorPickerPanel({ mode, entityId, editUrl, type: "font" });
|
|
return;
|
|
}
|
|
|
|
if (action === "set-font-color") {
|
|
const fontColorKey = actionBtn.dataset.fontColorKey || "auto";
|
|
if (mode === "modal") setModalNoteFontColor(fontColorKey);
|
|
else setNoteFontColor(entityId, fontColorKey, editUrl);
|
|
return;
|
|
}
|
|
});
|
|
|
|
panel.addEventListener("keydown", (e) => {
|
|
const targetBtn = e.target && e.target.closest ? e.target.closest("button") : null;
|
|
if (!targetBtn) return;
|
|
|
|
if (e.key === "Enter" || e.key === " ") {
|
|
if (targetBtn.hasAttribute("data-bg-studio-action") || targetBtn.classList.contains("bg-studio-thumb") || targetBtn.classList.contains("bg-studio-swatch")) {
|
|
e.preventDefault();
|
|
targetBtn.click();
|
|
}
|
|
return;
|
|
}
|
|
|
|
const moveFocus = (buttons, nextIndex) => {
|
|
if (!buttons || buttons.length === 0) return;
|
|
const idx = Math.max(0, Math.min(buttons.length - 1, nextIndex));
|
|
const el = buttons[idx];
|
|
if (el && typeof el.focus === "function") el.focus();
|
|
};
|
|
|
|
if (targetBtn.classList.contains("bg-studio-thumb")) {
|
|
const buttons = Array.from(panel.querySelectorAll(".bg-studio-gallery .bg-studio-thumb"));
|
|
const currentIndex = buttons.indexOf(targetBtn);
|
|
if (currentIndex === -1) return;
|
|
|
|
const colSize = 2;
|
|
if (e.key === "ArrowRight") {
|
|
e.preventDefault();
|
|
moveFocus(buttons, currentIndex + colSize);
|
|
} else if (e.key === "ArrowLeft") {
|
|
e.preventDefault();
|
|
moveFocus(buttons, currentIndex - colSize);
|
|
} else if (e.key === "ArrowDown") {
|
|
e.preventDefault();
|
|
moveFocus(buttons, currentIndex + 1);
|
|
} else if (e.key === "ArrowUp") {
|
|
e.preventDefault();
|
|
moveFocus(buttons, currentIndex - 1);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (targetBtn.classList.contains("bg-studio-swatch")) {
|
|
const buttons = Array.from(panel.querySelectorAll(".bg-studio-swatches .bg-studio-swatch"));
|
|
const currentIndex = buttons.indexOf(targetBtn);
|
|
if (currentIndex === -1) return;
|
|
|
|
if (e.key === "ArrowRight") {
|
|
e.preventDefault();
|
|
moveFocus(buttons, currentIndex + 1);
|
|
} else if (e.key === "ArrowLeft") {
|
|
e.preventDefault();
|
|
moveFocus(buttons, currentIndex - 1);
|
|
}
|
|
return;
|
|
}
|
|
});
|
|
|
|
panel.addEventListener("input", (e) => {
|
|
const input = e.target && e.target.matches(".bg-studio-search-input") ? e.target : null;
|
|
if (!input) return;
|
|
|
|
const panelEl = document.getElementById("shaarli-bg-studio");
|
|
if (!panelEl) return;
|
|
panelEl.dataset.query = input.value || "";
|
|
|
|
window.clearTimeout(panelEl.__searchTimer);
|
|
panelEl.__searchTimer = window.setTimeout(() => {
|
|
renderBackgroundStudioPanel(panelEl);
|
|
}, 150);
|
|
});
|
|
|
|
backgroundStudioPanelInitialized = true;
|
|
}
|
|
|
|
function closeBackgroundStudioPanel() {
|
|
const panel = document.getElementById("shaarli-bg-studio");
|
|
if (!panel) return;
|
|
panel.classList.remove("open");
|
|
panel.style.display = "none";
|
|
panel.setAttribute("aria-hidden", "true");
|
|
panel.__anchorEl = null;
|
|
}
|
|
|
|
function openBackgroundStudioPanel({
|
|
anchorEl,
|
|
mode,
|
|
entityId,
|
|
editUrl,
|
|
currentColor,
|
|
currentFilter,
|
|
currentBackground,
|
|
currentFontColor,
|
|
title,
|
|
}) {
|
|
ensureBackgroundStudioPanel();
|
|
const panel = document.getElementById("shaarli-bg-studio");
|
|
if (!panel) return;
|
|
|
|
panel.dataset.mode = mode || "entity";
|
|
panel.dataset.entityId = entityId || "";
|
|
panel.dataset.editUrl = editUrl || "";
|
|
panel.dataset.color = currentColor || "default";
|
|
panel.dataset.filter = normalizeFilterKey(currentFilter || "") || "none";
|
|
panel.dataset.background = normalizeBackgroundKey(currentBackground || "") || "none";
|
|
panel.dataset.fontColor = currentFontColor || panel.dataset.fontColor || "auto";
|
|
panel.dataset.query = panel.dataset.query || "";
|
|
panel.dataset.title = title || "Mes images & couleurs";
|
|
panel.__anchorEl = anchorEl || null;
|
|
|
|
renderBackgroundStudioPanel(panel);
|
|
|
|
panel.style.display = "block";
|
|
panel.classList.add("open");
|
|
panel.setAttribute("aria-hidden", "false");
|
|
|
|
positionBackgroundStudioPanel(panel, anchorEl);
|
|
|
|
window.requestAnimationFrame(() => {
|
|
const input = panel.querySelector(".bg-studio-search-input");
|
|
if (input && typeof input.focus === "function") {
|
|
input.focus();
|
|
input.select();
|
|
}
|
|
});
|
|
}
|
|
|
|
function positionBackgroundStudioPanel(panel, anchorEl) {
|
|
if (!panel) return;
|
|
|
|
const viewportPadding = 12;
|
|
const rect = anchorEl && anchorEl.getBoundingClientRect ? anchorEl.getBoundingClientRect() : null;
|
|
|
|
panel.style.left = "";
|
|
panel.style.top = "";
|
|
panel.style.right = "";
|
|
panel.style.bottom = "";
|
|
|
|
const panelRect = panel.getBoundingClientRect();
|
|
const preferredTop = rect ? rect.top - panelRect.height - 10 : viewportPadding;
|
|
const preferredLeft = rect ? rect.left : viewportPadding;
|
|
|
|
let top = preferredTop;
|
|
if (top < viewportPadding) {
|
|
top = rect ? rect.bottom + 10 : viewportPadding;
|
|
}
|
|
|
|
let left = preferredLeft;
|
|
if (left + panelRect.width > window.innerWidth - viewportPadding) {
|
|
left = window.innerWidth - viewportPadding - panelRect.width;
|
|
}
|
|
if (left < viewportPadding) left = viewportPadding;
|
|
|
|
if (top + panelRect.height > window.innerHeight - viewportPadding) {
|
|
top = window.innerHeight - viewportPadding - panelRect.height;
|
|
}
|
|
if (top < viewportPadding) top = viewportPadding;
|
|
|
|
panel.style.left = `${Math.round(left)}px`;
|
|
panel.style.top = `${Math.round(top)}px`;
|
|
}
|
|
|
|
function normalizeSearchText(value) {
|
|
return String(value || "")
|
|
.toLowerCase()
|
|
.normalize("NFD")
|
|
.replace(/[\u0300-\u036f]/g, "")
|
|
.trim();
|
|
}
|
|
|
|
function categorizeBackgroundKey(key, label) {
|
|
const hay = normalizeSearchText(`${key || ""} ${label || ""}`);
|
|
|
|
if (!key || key === "none") return "solid";
|
|
if (hay.includes("degrade") || hay.includes("gradient")) return "gradient";
|
|
if (hay.includes("grid") || hay.includes("quadrill") || hay.includes("lign") || hay.includes("feuille")) return "grid";
|
|
if (hay.includes("damier") || hay.includes("checker") || hay.includes("texture")) return "texture";
|
|
|
|
return "image";
|
|
}
|
|
|
|
function getBackgroundStudioItems() {
|
|
const colors = NOTE_COLOR_OPTIONS.map((opt) => ({
|
|
type: "color",
|
|
key: opt.key,
|
|
label: opt.label,
|
|
color: getThemeColorValue(opt),
|
|
keywords: [opt.key, opt.label],
|
|
}));
|
|
|
|
const backgrounds = [
|
|
{
|
|
type: "background",
|
|
category: "solid",
|
|
key: "none",
|
|
label: "Couleur unie",
|
|
keywords: ["none", "sans image", "solid", "uni"],
|
|
},
|
|
...getAvailableBackgroundOptionsForMode().map((bg) => ({
|
|
type: "background",
|
|
category: categorizeBackgroundKey(bg.key, bg.label),
|
|
key: bg.key,
|
|
label: bg.label,
|
|
url: getNoteBackgroundUrl(bg.key),
|
|
keywords: [bg.key, bg.label, "image", "background"],
|
|
})),
|
|
];
|
|
|
|
return { colors, backgrounds };
|
|
}
|
|
|
|
function renderBackgroundStudioPanel(panel) {
|
|
if (!panel) return;
|
|
|
|
const activeEl = document.activeElement;
|
|
const wasSearchFocused = !!(
|
|
activeEl &&
|
|
panel.contains(activeEl) &&
|
|
activeEl.classList &&
|
|
activeEl.classList.contains("bg-studio-search-input")
|
|
);
|
|
const caretStart = wasSearchFocused && typeof activeEl.selectionStart === "number" ? activeEl.selectionStart : null;
|
|
const caretEnd = wasSearchFocused && typeof activeEl.selectionEnd === "number" ? activeEl.selectionEnd : null;
|
|
|
|
const title = panel.dataset.title || "Mes images & couleurs";
|
|
const color = panel.dataset.color || "default";
|
|
const isCustomColor = typeof color === "string" && color.startsWith("custom:");
|
|
const currentFilter = panel.dataset.filter || "none";
|
|
const currentFontColor = panel.dataset.fontColor || "auto";
|
|
const query = normalizeSearchText(panel.dataset.query || "");
|
|
|
|
const { colors, backgrounds } = getBackgroundStudioItems();
|
|
|
|
// Filter backgrounds based on search query
|
|
const filteredBackgrounds = backgrounds.filter((b) => {
|
|
if (!query) return true;
|
|
const hay = normalizeSearchText([b.label, b.key, "image", "background"].join(" "));
|
|
return hay.includes(query);
|
|
});
|
|
|
|
// Generate gallery HTML for backgrounds
|
|
const galleryHtml = filteredBackgrounds
|
|
.map((item) => {
|
|
const isActive = panel.dataset.background === item.key;
|
|
const common = `class="bg-studio-thumb ${isActive ? "is-active" : ""}" type="button"`;
|
|
const thumb =
|
|
item.key === "none"
|
|
? `<button ${common} data-bg-studio-action="set-background" data-bg-key="none" title="${item.label}" aria-label="${item.label}" aria-pressed="${isActive}"><span class="bg-studio-thumb-solid"></span></button>`
|
|
: `<button ${common} data-bg-studio-action="set-background" data-bg-key="${item.key}" title="${item.label}" aria-label="${item.label}" aria-pressed="${isActive}" style="background-image:url('${item.url || ""}')"></button>`;
|
|
|
|
return `<div class="bg-studio-tile">${thumb}<div class="bg-studio-thumb-label">${item.label}</div></div>`;
|
|
})
|
|
.join("");
|
|
|
|
// Colors row with custom color picker button
|
|
const colorsRowHtml = colors
|
|
.filter((c) => c.key !== "default" && c.key !== "custom")
|
|
.map((c) => {
|
|
const isActive = color === c.key;
|
|
const hex = c.color || "";
|
|
return `<button class="bg-studio-swatch ${isActive ? "is-active" : ""}" type="button" data-bg-studio-action="set-color" data-color-key="${c.key}" title="${hex}" aria-label="${c.label}" aria-pressed="${isActive}" style="background-color:${hex}"></button>`;
|
|
})
|
|
.join("") +
|
|
// Add custom color button (opens color picker)
|
|
`<button class="bg-studio-swatch bg-studio-swatch-custom ${isCustomColor ? "is-active" : ""}" type="button" data-bg-studio-action="open-color-picker" title="Couleur personnalisée" aria-label="Couleur personnalisée" aria-pressed="${isCustomColor}">
|
|
<i class="mdi mdi-plus" aria-hidden="true"></i>
|
|
</button>`;
|
|
|
|
// Font colors row
|
|
const isCustomFontColor = typeof currentFontColor === "string" && currentFontColor.startsWith("custom:");
|
|
const fontColorsRowHtml = NOTE_FONT_COLOR_OPTIONS
|
|
.filter((c) => c.key !== "custom")
|
|
.map((c) => {
|
|
const isActive = currentFontColor === c.key;
|
|
const value = c.value || "auto";
|
|
const style =
|
|
value === "auto"
|
|
? "background: linear-gradient(135deg, #f5f7fb 50%, #202124 50%);"
|
|
: `background-color: ${value};`;
|
|
return `<button class="bg-studio-swatch ${isActive ? "is-active" : ""}" type="button" data-bg-studio-action="set-font-color" data-font-color-key="${c.key}" title="${c.label}" aria-label="${c.label}" aria-pressed="${isActive}" style="${style}"></button>`;
|
|
})
|
|
.join("") +
|
|
// Add custom font color button
|
|
`<button class="bg-studio-swatch bg-studio-swatch-custom ${isCustomFontColor ? "is-active" : ""}" type="button" data-bg-studio-action="open-font-color-picker" title="Couleur de texte personnalisée" aria-label="Couleur de texte personnalisée" aria-pressed="${isCustomFontColor}">
|
|
<i class="mdi mdi-plus" aria-hidden="true"></i>
|
|
</button>`;
|
|
|
|
const filterBtn = (key, icon, label) => {
|
|
const active = currentFilter === key;
|
|
return `<button class="bg-studio-filter-btn ${active ? "is-active" : ""}" type="button" data-bg-studio-action="set-filter" data-filter="${key}" title="${label}" aria-label="${label}" aria-pressed="${active}"><i class="mdi ${icon}" aria-hidden="true"></i></button>`;
|
|
};
|
|
|
|
const filtersHtml = NOTE_FILTER_OPTIONS.map((f) => filterBtn(f.key, f.icon, f.label)).join("");
|
|
|
|
const showClear = !!(panel.dataset.query || "").trim();
|
|
|
|
panel.innerHTML = `
|
|
<div class="bg-studio-header">
|
|
<div class="bg-studio-title">${title}</div>
|
|
<button class="bg-studio-close" type="button" data-bg-studio-action="close" title="Fermer" aria-label="Fermer"><i class="mdi mdi-close" aria-hidden="true"></i></button>
|
|
</div>
|
|
<div class="bg-studio-gallery" role="list">${galleryHtml}</div>
|
|
<div class="bg-studio-rows">
|
|
<div class="bg-studio-row">
|
|
<div class="bg-studio-row-heading">
|
|
<div class="bg-studio-row-label">Colors:</div>
|
|
<button type="button" class="bg-studio-reset" data-bg-studio-action="set-defaults" title="Réinitialiser" aria-label="Réinitialiser"><i class="mdi mdi-refresh" aria-hidden="true"></i></button>
|
|
</div>
|
|
<div class="bg-studio-swatches" role="list">${colorsRowHtml}</div>
|
|
</div>
|
|
<div class="bg-studio-row">
|
|
<div class="bg-studio-row-heading">
|
|
<div class="bg-studio-row-label">Font:</div>
|
|
<button type="button" class="bg-studio-reset" data-bg-studio-action="reset-font-color" title="Réinitialiser" aria-label="Réinitialiser"><i class="mdi mdi-refresh" aria-hidden="true"></i></button>
|
|
</div>
|
|
<div class="bg-studio-swatches bg-studio-swatches-font" role="list">${fontColorsRowHtml}</div>
|
|
</div>
|
|
<div class="bg-studio-row">
|
|
<div class="bg-studio-row-label">Filtres:</div>
|
|
<div class="bg-studio-filters">${filtersHtml}</div>
|
|
</div>
|
|
</div>
|
|
<div class="bg-studio-search">
|
|
<i class="mdi mdi-magnify" aria-hidden="true"></i>
|
|
<input class="bg-studio-search-input" type="search" placeholder="Rechercher mes images… (filtrer par couleur ou filtre)" value="${panel.dataset.query || ""}" />
|
|
<button class="bg-studio-clear" type="button" data-bg-studio-action="set-query" title="Effacer" aria-label="Effacer" style="${showClear ? "" : "display:none"}"><i class="mdi mdi-close-circle-outline" aria-hidden="true"></i></button>
|
|
</div>
|
|
`;
|
|
|
|
if (wasSearchFocused) {
|
|
const input = panel.querySelector(".bg-studio-search-input");
|
|
if (input && typeof input.focus === "function") {
|
|
input.focus({ preventScroll: true });
|
|
if (typeof input.setSelectionRange === "function") {
|
|
const len = (input.value || "").length;
|
|
const start = typeof caretStart === "number" ? Math.max(0, Math.min(len, caretStart)) : len;
|
|
const end = typeof caretEnd === "number" ? Math.max(0, Math.min(len, caretEnd)) : len;
|
|
try {
|
|
input.setSelectionRange(start, end);
|
|
} catch (e) {
|
|
// Ignore selection errors for non-text inputs or unsupported browsers.
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function applyNoteVisualState(element, note) {
|
|
if (!element || !note) return;
|
|
|
|
const rawColor = note.color || "default";
|
|
const isCustomColor = typeof rawColor === "string" && rawColor.startsWith("custom:");
|
|
const customHex = isCustomColor ? rawColor.substring(7) : "";
|
|
|
|
const resolvedColorOption = isCustomColor ? null : getColorOption(rawColor);
|
|
const color = isCustomColor ? "custom" : (resolvedColorOption ? resolvedColorOption.key : "default");
|
|
const colorValue = isCustomColor ? customHex : getThemeColorValue(resolvedColorOption);
|
|
const foregroundColor = getReadableForegroundForBackground(colorValue);
|
|
const filter = normalizeFilterKey(note.filter || "none");
|
|
const normalizedBackground = normalizeBackgroundKey(note.background || "");
|
|
const background = normalizedBackground || "none";
|
|
const fontColor = note.fontColor || "auto";
|
|
|
|
element.classList.forEach((cls) => {
|
|
if (cls.startsWith("note-color-")) element.classList.remove(cls);
|
|
if (cls.startsWith("note-filter-")) element.classList.remove(cls);
|
|
});
|
|
element.classList.add(`note-color-${color}`);
|
|
if (filter !== "none") {
|
|
element.classList.add(`note-filter-${filter}`);
|
|
}
|
|
|
|
if (colorValue) {
|
|
element.style.backgroundColor = colorValue;
|
|
element.style.borderColor = "transparent";
|
|
} else {
|
|
element.style.removeProperty("background-color");
|
|
}
|
|
|
|
if (foregroundColor) {
|
|
element.style.setProperty("--note-card-fg", foregroundColor);
|
|
}
|
|
|
|
if (fontColor && fontColor !== "auto") {
|
|
let fontValue = null;
|
|
if (typeof fontColor === "string" && fontColor.startsWith("custom:")) {
|
|
fontValue = fontColor.substring(7);
|
|
} else {
|
|
const opt = NOTE_FONT_COLOR_OPTIONS.find((o) => o.key === fontColor);
|
|
if (opt && opt.value && opt.value !== "auto") fontValue = opt.value;
|
|
}
|
|
if (fontValue) {
|
|
element.style.setProperty("--note-card-fg", fontValue);
|
|
}
|
|
}
|
|
|
|
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;
|
|
if (isCustomColor && customHex) {
|
|
element.dataset.customColor = customHex;
|
|
} else {
|
|
element.dataset.customColor = "";
|
|
}
|
|
element.dataset.filter = filter;
|
|
element.dataset.fontColor = fontColor;
|
|
}
|
|
|
|
function extractNoteVisualStateFromTags(tags) {
|
|
const safeTags = Array.isArray(tags) ? tags : [];
|
|
|
|
let color = "default";
|
|
const foundColorTag = safeTags.find((t) => typeof t === "string" && t.startsWith(NOTE_COLOR_TAG_PREFIX));
|
|
if (foundColorTag) {
|
|
const candidate = foundColorTag.substring(NOTE_COLOR_TAG_PREFIX.length);
|
|
if (/^[0-9A-Fa-f]{6}$/.test(candidate)) {
|
|
color = `custom:#${candidate.toUpperCase()}`;
|
|
} else if (NOTE_COLOR_OPTIONS.some((opt) => opt.key === candidate)) {
|
|
color = candidate;
|
|
}
|
|
} else {
|
|
// Backward compat: legacy note-custom-<hex>
|
|
const legacyCustomTag = safeTags.find((t) => typeof t === "string" && t.startsWith("note-custom-"));
|
|
if (legacyCustomTag) {
|
|
const candidate = legacyCustomTag.substring("note-custom-".length);
|
|
if (/^[0-9A-Fa-f]{6}$/.test(candidate)) {
|
|
color = `custom:#${candidate.toUpperCase()}`;
|
|
}
|
|
}
|
|
|
|
// Backward compat: legacy note-<color>
|
|
const legacyColorTag = safeTags.find((t) => typeof t === "string" && t.startsWith("note-"));
|
|
if (legacyColorTag) {
|
|
const candidate = legacyColorTag.substring(5);
|
|
if (NOTE_COLOR_OPTIONS.some((opt) => opt.key === candidate)) {
|
|
color = candidate;
|
|
}
|
|
}
|
|
}
|
|
|
|
let filter = "none";
|
|
const foundFilterTag = safeTags.find((t) => typeof t === "string" && t.startsWith(NOTE_FILTER_TAG_PREFIX));
|
|
if (foundFilterTag) {
|
|
const candidate = normalizeFilterKey(foundFilterTag.substring(NOTE_FILTER_TAG_PREFIX.length));
|
|
if (candidate && candidate !== "none") {
|
|
filter = candidate;
|
|
}
|
|
}
|
|
|
|
let background = "none";
|
|
const foundBgTag = safeTags.find((t) => typeof t === "string" && t.startsWith(NOTE_BACKGROUND_TAG_PREFIX));
|
|
if (foundBgTag) {
|
|
const candidate = normalizeBackgroundKey(foundBgTag.substring(NOTE_BACKGROUND_TAG_PREFIX.length));
|
|
if (candidate) {
|
|
background = candidate;
|
|
}
|
|
}
|
|
|
|
let fontColor = "auto";
|
|
const foundFontTag = safeTags.find((t) => typeof t === "string" && t.startsWith(NOTE_FONT_COLOR_TAG_PREFIX));
|
|
if (foundFontTag) {
|
|
const raw = foundFontTag.substring(NOTE_FONT_COLOR_TAG_PREFIX.length);
|
|
const hex = raw.startsWith("#") ? raw : `#${raw}`;
|
|
if (/^#[0-9A-Fa-f]{6}$/.test(hex)) {
|
|
fontColor = `custom:${hex.toUpperCase()}`;
|
|
}
|
|
}
|
|
|
|
return { color, filter, background, fontColor };
|
|
}
|
|
|
|
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, filter, background, fontColor } = extractNoteVisualStateFromTags(tags);
|
|
applyNoteVisualState(card, { color, filter, background, fontColor });
|
|
|
|
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>
|
|
`;
|
|
|
|
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}`);
|
|
|
|
if (!paletteBtn) return;
|
|
|
|
paletteBtn.addEventListener("click", (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
openBackgroundStudioPanel({
|
|
anchorEl: paletteBtn,
|
|
mode: "entity",
|
|
entityId: cardId,
|
|
editUrl,
|
|
currentColor: getElementVisualColor(card),
|
|
currentFilter: getElementVisualFilter(card),
|
|
currentBackground: getElementVisualBackground(card),
|
|
currentFontColor: getElementVisualFontColor(card),
|
|
title: "Mes images & couleurs",
|
|
});
|
|
});
|
|
});
|
|
|
|
document.addEventListener("click", (e) => {
|
|
if (e.target.closest(".bookmark-palette")) return;
|
|
if (e.target.closest("#shaarli-bg-studio")) return;
|
|
closeBackgroundStudioPanel();
|
|
});
|
|
}
|
|
|
|
function generateBookmarkPaletteButtons(bookmarkId, editUrl, currentColor, currentFilter) {
|
|
return generateUnifiedPaletteMenu({
|
|
entityId: bookmarkId,
|
|
editUrl,
|
|
currentColor,
|
|
currentFilter,
|
|
mode: "entity",
|
|
});
|
|
}
|
|
|
|
function generateUnifiedPaletteMenu({ entityId, editUrl, currentColor, currentFilter, mode }) {
|
|
const color = currentColor || "default";
|
|
const filter = normalizeFilterKey(currentFilter || "") || "none";
|
|
|
|
const onColorClick =
|
|
mode === "modal"
|
|
? (c) => `setModalNoteColor('${c}')`
|
|
: (c) => `setNoteColor('${entityId}', '${c}', '${editUrl}')`;
|
|
|
|
const onFilterClick =
|
|
mode === "modal"
|
|
? (k) => `setModalNoteFilter('${k}')`
|
|
: (k) => `setNoteFilter('${entityId}', '${k}', '${editUrl}')`;
|
|
|
|
const colorButtons = [
|
|
`<button class="palette-btn palette-btn-default ${color === "default" ? "is-active" : ""}" type="button" title="Par défaut" aria-label="Par défaut" aria-pressed="${color === "default"}" onclick="${onColorClick("default")}"><i class="mdi mdi-format-color-reset" aria-hidden="true"></i></button>`,
|
|
...NOTE_COLOR_OPTIONS.filter((opt) => opt.key !== "default").map((opt) => {
|
|
const swatchColor = getThemeColorValue(opt);
|
|
return `<button class="palette-btn note-color-${opt.key} ${color === opt.key ? "is-active" : ""}" type="button" title="${opt.label}" aria-label="${opt.label}" aria-pressed="${color === opt.key}" onclick="${onColorClick(opt.key)}" style="background-color:${swatchColor}"></button>`;
|
|
}),
|
|
].join("");
|
|
|
|
const filterButtons = [
|
|
`<button class="palette-btn palette-btn-filter-none ${filter === "none" ? "is-active" : ""}" type="button" title="Aucun" aria-label="Aucun" aria-pressed="${filter === "none"}" onclick="${onFilterClick("none")}"><i class="mdi mdi-close-circle-outline" aria-hidden="true"></i></button>`,
|
|
...NOTE_FILTER_OPTIONS.filter((opt) => opt.key !== "none").map((opt) => {
|
|
return `<button class="palette-btn palette-btn-filter ${filter === opt.key ? "is-active" : ""}" type="button" title="${opt.label}" aria-label="${opt.label}" aria-pressed="${filter === opt.key}" onclick="${onFilterClick(opt.key)}"><i class="mdi ${opt.icon}" aria-hidden="true"></i></button>`;
|
|
}),
|
|
].join("");
|
|
|
|
return `
|
|
<div class="palette-section">
|
|
<div class="palette-section-title">Couleurs</div>
|
|
<div class="palette-row palette-row-colors">${colorButtons}</div>
|
|
</div>
|
|
<div class="palette-divider"></div>
|
|
<div class="palette-section">
|
|
<div class="palette-section-title">Filtres</div>
|
|
<div class="palette-row palette-row-filters">${filterButtons}</div>
|
|
</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 filter = card.dataset.filter;
|
|
note.filter = filter && filter !== "none" ? filter : "none";
|
|
|
|
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", "view-notes");
|
|
|
|
const basePath = typeof shaarli !== "undefined" && shaarli.basePath ? shaarli.basePath : "";
|
|
|
|
const toolbar = document.querySelector(".content-toolbar");
|
|
if (toolbar) toolbar.style.display = "none";
|
|
|
|
const wrapper = document.createElement("div");
|
|
wrapper.className = "notes-wrapper todo-wrapper";
|
|
|
|
const topBar = document.createElement("div");
|
|
topBar.className = "notes-top-bar";
|
|
|
|
const inputContainer = document.createElement("div");
|
|
inputContainer.className = "note-input-container";
|
|
inputContainer.innerHTML = `
|
|
<div class="note-input-collapsed" onclick="window.location.href='${basePath}/admin/shaare?post=http%3A%2F%2Fshaarli-todo&tags=shaarli-todo&title=%E2%9C%85%20'">
|
|
<span class="note-input-placeholder">Créer une tâche...</span>
|
|
<div class="note-input-actions">
|
|
<button title="Créer une tâche" onclick="event.stopPropagation(); window.location.href='${basePath}/admin/shaare?post=http%3A%2F%2Fshaarli-todo&tags=shaarli-todo&title=%E2%9C%85%20'"><i class="mdi mdi-check-circle-outline"></i></button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
topBar.appendChild(inputContainer);
|
|
|
|
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);
|
|
|
|
const contentArea = document.createElement("div");
|
|
contentArea.className = "notes-content-area";
|
|
|
|
const links = Array.from(linkList.querySelectorAll(".link-outer"));
|
|
const todos = links.map((link) => parseTodoFromLink(link)).filter((t) => t);
|
|
|
|
wrapper.appendChild(contentArea);
|
|
|
|
linkList.style.display = "none";
|
|
if (linkList.parentNode) {
|
|
linkList.parentNode.insertBefore(wrapper, linkList);
|
|
} else {
|
|
container.appendChild(wrapper);
|
|
}
|
|
|
|
renderTodos(contentArea, todos, "grid");
|
|
|
|
const modalOverlay = document.createElement("div");
|
|
modalOverlay.className = "note-modal-overlay todo-modal-overlay";
|
|
modalOverlay.innerHTML = `
|
|
<div class="note-modal note-color-default todo-modal">
|
|
<div class="note-modal-header">
|
|
<h2 class="note-title" id="todo-modal-title"></h2>
|
|
<button type="button" class="note-modal-pin-toggle" id="todo-modal-pin" title="Épingler">
|
|
<i class="mdi mdi-pin-outline"></i>
|
|
</button>
|
|
</div>
|
|
<div class="note-modal-content">
|
|
<div class="todo-checklist" id="todo-modal-checklist"></div>
|
|
<button type="button" class="todo-add-item-btn" id="todo-modal-add-item"><i class="mdi mdi-plus"></i> Élément de liste</button>
|
|
</div>
|
|
<div class="note-modal-tags is-empty" id="todo-modal-tags"></div>
|
|
<div class="note-modal-actions">
|
|
<div class="note-modal-actions-left">
|
|
<div class="note-modal-color-picker">
|
|
<button type="button" id="todo-modal-color-btn" title="Couleur"><i class="mdi mdi-palette-outline"></i></button>
|
|
<div class="palette-popup note-modal-palette" id="todo-modal-color-popup"></div>
|
|
</div>
|
|
<a href="#" id="todo-modal-edit" title="Modifier"><i class="mdi mdi-pencil-outline"></i></a>
|
|
<button type="button" id="todo-modal-delete" title="Supprimer"><i class="mdi mdi-dots-vertical"></i></button>
|
|
</div>
|
|
<button type="button" class="note-modal-close-btn" id="todo-modal-close">Fermer</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
document.body.appendChild(modalOverlay);
|
|
|
|
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");
|
|
renderTodos(contentArea, todos, "grid");
|
|
});
|
|
|
|
btnList.addEventListener("click", () => {
|
|
btnList.classList.add("active");
|
|
btnGrid.classList.remove("active");
|
|
renderTodos(contentArea, todos, "list");
|
|
});
|
|
|
|
modalOverlay.querySelector("#todo-modal-close").addEventListener("click", () => {
|
|
modalOverlay.classList.remove("open");
|
|
});
|
|
modalOverlay.addEventListener("click", (e) => {
|
|
if (e.target === modalOverlay) modalOverlay.classList.remove("open");
|
|
});
|
|
|
|
const modalPinBtn = modalOverlay.querySelector("#todo-modal-pin");
|
|
const modalColorBtn = modalOverlay.querySelector("#todo-modal-color-btn");
|
|
const modalColorPopup = modalOverlay.querySelector("#todo-modal-color-popup");
|
|
|
|
modalColorBtn.addEventListener("click", (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
const modalCard = modalOverlay.querySelector(".note-modal");
|
|
openBackgroundStudioPanel({
|
|
anchorEl: modalColorBtn,
|
|
mode: "modal",
|
|
entityId: modalCard ? modalCard.dataset.todoId || "" : "",
|
|
editUrl: modalCard ? modalCard.dataset.editUrl || "" : "",
|
|
currentColor: modalCard ? getElementVisualColor(modalCard) : "default",
|
|
currentFilter: modalCard ? getElementVisualFilter(modalCard) : "none",
|
|
currentBackground: modalCard ? getElementVisualBackground(modalCard) : "none",
|
|
currentFontColor: modalCard ? getElementVisualFontColor(modalCard) : "auto",
|
|
title: "Mes images & couleurs",
|
|
});
|
|
});
|
|
|
|
modalPinBtn.addEventListener("click", (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
const modalCard = modalOverlay.querySelector(".note-modal");
|
|
const todoId = modalCard.dataset.todoId;
|
|
const editUrl = modalCard.dataset.editUrl;
|
|
if (!todoId || !editUrl) return;
|
|
|
|
togglePinTag(todoId, 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("#todo-modal-tags"), tags);
|
|
|
|
if (modalOverlay.currentTodo) {
|
|
modalOverlay.currentTodo.isPinned = isPinned;
|
|
modalOverlay.currentTodo.tags = tags;
|
|
}
|
|
});
|
|
|
|
modalOverlay.querySelector("#todo-modal-delete").addEventListener("click", () => {
|
|
const modalCard = modalOverlay.querySelector(".note-modal");
|
|
const deleteUrl = modalCard.dataset.deleteUrl;
|
|
if (deleteUrl && deleteUrl !== "#") {
|
|
window.location.href = deleteUrl;
|
|
}
|
|
});
|
|
|
|
modalOverlay.addEventListener("click", (e) => {
|
|
if (!e.target.closest(".note-modal-color-picker")) {
|
|
modalColorPopup.classList.remove("open");
|
|
}
|
|
});
|
|
|
|
modalOverlay.querySelector("#todo-modal-add-item").addEventListener("click", (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
if (!modalOverlay.currentTodo) return;
|
|
addTodoItem(modalOverlay, modalOverlay.currentTodo);
|
|
});
|
|
|
|
if (typeof initTagDisplayAndRemoval === "function") {
|
|
initTagDisplayAndRemoval();
|
|
}
|
|
}
|
|
|
|
function parseTodoFromLink(linkEl) {
|
|
if (!linkEl) return null;
|
|
|
|
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 : "";
|
|
|
|
const rawTags = [];
|
|
linkEl.querySelectorAll(".link-tag-list a").forEach((tag) => {
|
|
const t = (tag.textContent || "").trim();
|
|
if (t) rawTags.push(t);
|
|
});
|
|
|
|
const { color, filter, background, fontColor } = extractNoteVisualStateFromTags(rawTags);
|
|
|
|
const tags = rawTags.filter((t) => {
|
|
if (t.startsWith(NOTE_COLOR_TAG_PREFIX)) return false;
|
|
if (t.startsWith("note-custom-")) return false;
|
|
if (t.startsWith(NOTE_FILTER_TAG_PREFIX)) return false;
|
|
if (t.startsWith(NOTE_BACKGROUND_TAG_PREFIX)) return false;
|
|
if (t.startsWith(NOTE_FONT_COLOR_TAG_PREFIX)) return false;
|
|
return true;
|
|
});
|
|
|
|
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 : "#";
|
|
const isPinned = tags.includes("shaarli-pin");
|
|
|
|
const items = extractChecklistItemsFromDescription(descEl, descText);
|
|
const parsed = parseTodoMarkdown(descText);
|
|
|
|
return {
|
|
id,
|
|
title,
|
|
descHtml,
|
|
descText,
|
|
items,
|
|
_todoHeaderLines: parsed.headerLines,
|
|
_todoFooterLines: parsed.footerLines,
|
|
tags,
|
|
color,
|
|
filter,
|
|
background,
|
|
fontColor,
|
|
editUrl,
|
|
deleteUrl,
|
|
pinUrl,
|
|
isPinned,
|
|
};
|
|
}
|
|
|
|
function extractChecklistItemsFromDescription(descEl, descText) {
|
|
const items = [];
|
|
|
|
if (descEl) {
|
|
const checkboxEls = Array.from(descEl.querySelectorAll('input[type="checkbox"]'));
|
|
if (checkboxEls.length > 0) {
|
|
checkboxEls.forEach((cb) => {
|
|
const li = cb.closest("li") || cb.parentElement;
|
|
let text = "";
|
|
if (li) {
|
|
const clone = li.cloneNode(true);
|
|
clone.querySelectorAll('input[type="checkbox"]').forEach((i) => i.remove());
|
|
text = (clone.textContent || "").trim();
|
|
}
|
|
items.push({ checked: !!cb.checked, text });
|
|
});
|
|
return items;
|
|
}
|
|
}
|
|
|
|
const lines = String(descText || "").split(/\r?\n/);
|
|
const fallbackSource = descEl ? descEl.innerHTML : lines.join("\n");
|
|
const parsed = parseTodoMarkdown(fallbackSource);
|
|
return parsed.items;
|
|
}
|
|
|
|
function getFormFieldValue(input) {
|
|
if (!input) return "";
|
|
const tag = (input.tagName || "").toUpperCase();
|
|
if (tag === "TEXTAREA") {
|
|
const v = typeof input.value === "string" ? input.value : "";
|
|
if (v && v.trim() !== "") return v;
|
|
const t = typeof input.textContent === "string" ? input.textContent : "";
|
|
return t;
|
|
}
|
|
return input.value;
|
|
}
|
|
|
|
async function hydrateTodoFromEditForm(todo) {
|
|
if (!todo || !todo.editUrl || todo.editUrl === "#") return;
|
|
if (todo._todoHydrated) return;
|
|
if (todo._todoHydratePromise) return todo._todoHydratePromise;
|
|
|
|
todo._todoHydratePromise = (async () => {
|
|
try {
|
|
const response = await fetch(todo.editUrl, { credentials: "same-origin" });
|
|
if (!response.ok) {
|
|
console.error("Todo hydration failed (edit form fetch not ok).", { id: todo && todo.id, url: todo && todo.editUrl, status: response && response.status });
|
|
return;
|
|
}
|
|
const html = await response.text();
|
|
const parser = new DOMParser();
|
|
const doc = parser.parseFromString(html, "text/html");
|
|
const form = doc.querySelector('form[name="linkform"]');
|
|
if (!form) {
|
|
console.error("Todo hydration failed (edit form not found).", { id: todo && todo.id, url: todo && todo.editUrl, status: response && response.status });
|
|
return;
|
|
}
|
|
|
|
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, getFormFieldValue(input));
|
|
}
|
|
});
|
|
|
|
todo._todoEditAction = form.action;
|
|
todo._todoEditBaseData = formData;
|
|
|
|
const rawMarkdown = formData.get("lf_description") || "";
|
|
const parsed = parseTodoMarkdown(rawMarkdown);
|
|
todo._todoHeaderLines = parsed.headerLines;
|
|
todo._todoFooterLines = parsed.footerLines;
|
|
todo.items = parsed.items;
|
|
|
|
if (!todo._todoDebugLogged && rawMarkdown && String(rawMarkdown).trim() && (!parsed.items || parsed.items.length === 0)) {
|
|
todo._todoDebugLogged = true;
|
|
console.warn("Todo parsed 0 items from lf_description; sample follows.", {
|
|
id: todo && todo.id,
|
|
url: todo && todo.editUrl,
|
|
sample: String(rawMarkdown).slice(0, 240),
|
|
});
|
|
}
|
|
|
|
updateTodoCardPreview(todo);
|
|
todo._todoHydrated = true;
|
|
} catch (e) {
|
|
console.error("Error hydrating todo markdown:", e);
|
|
} finally {
|
|
todo._todoHydratePromise = null;
|
|
}
|
|
})();
|
|
|
|
return todo._todoHydratePromise;
|
|
}
|
|
|
|
function renderTodos(container, todos, viewMode) {
|
|
if (!container) return;
|
|
container.innerHTML = "";
|
|
container.className = viewMode === "grid" ? "notes-masonry" : "notes-list-view";
|
|
|
|
const visibleTodos = (todos || []).slice();
|
|
visibleTodos.sort((a, b) => {
|
|
const aPinned = (a.tags || []).includes("shaarli-pin");
|
|
const bPinned = (b.tags || []).includes("shaarli-pin");
|
|
return bPinned - aPinned;
|
|
});
|
|
|
|
visibleTodos.forEach((todo) => {
|
|
const card = document.createElement("div");
|
|
card.className = "note-card todo-card";
|
|
card.dataset.id = todo.id;
|
|
card.dataset.editUrl = todo.editUrl || "";
|
|
card.dataset.tags = (todo.tags || []).filter((t) => t).join("||");
|
|
todo._todoCardEl = card;
|
|
applyNoteVisualState(card, todo);
|
|
if (viewMode === "list") card.classList.add("list-mode");
|
|
|
|
card.addEventListener("click", (e) => {
|
|
if (e.target.closest("button") || e.target.closest("a") || e.target.closest(".note-hover-actions")) return;
|
|
syncNoteFromCardElement(todo, card);
|
|
openTodoModal(todo);
|
|
});
|
|
|
|
const inner = document.createElement("div");
|
|
inner.className = "note-inner";
|
|
|
|
if (todo.title) {
|
|
const h3 = document.createElement("h3");
|
|
h3.className = "note-title";
|
|
h3.textContent = todo.title;
|
|
inner.appendChild(h3);
|
|
}
|
|
|
|
const body = document.createElement("div");
|
|
body.className = "note-body todo-checklist-preview-wrap";
|
|
body.innerHTML = buildTodoPreviewHtml(todo.items || []);
|
|
inner.appendChild(body);
|
|
|
|
if ((todo.tags || []).length > 0) {
|
|
const tagContainer = document.createElement("div");
|
|
tagContainer.className = "note-tags";
|
|
todo.tags.forEach((t) => {
|
|
if (isTechnicalTag(t)) return;
|
|
const pill = createTagPill({ tag: t, onRemoveClass: "note-tag-remove-btn", tagClass: "note-tag", canRemove: !!todo.editUrl && todo.editUrl !== "#" });
|
|
tagContainer.appendChild(pill);
|
|
});
|
|
inner.appendChild(tagContainer);
|
|
}
|
|
|
|
const actions = document.createElement("div");
|
|
actions.className = "note-hover-actions";
|
|
|
|
const paletteBtnId = `palette-${todo.id}`;
|
|
|
|
actions.innerHTML = `
|
|
<div style="position:relative;">
|
|
<button title="Couleur" id="${paletteBtnId}"><i class="mdi mdi-palette-outline"></i></button>
|
|
</div>
|
|
<div class="spacer"></div>
|
|
<a href="${todo.pinUrl}" title="${todo.isPinned ? "Unpin" : "Pin"}" class="${todo.isPinned ? "active" : ""}"><i class="mdi mdi-pin${todo.isPinned ? "" : "-outline"}"></i></a>
|
|
<a href="${todo.editUrl}" title="Edit"><i class="mdi mdi-pencil-outline"></i></a>
|
|
<button title="Plus" onclick="window.location.href='${todo.deleteUrl}'"><i class="mdi mdi-dots-vertical"></i></button>
|
|
`;
|
|
|
|
const paletteBtn = actions.querySelector(`#${paletteBtnId}`);
|
|
paletteBtn.addEventListener("click", (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
openBackgroundStudioPanel({
|
|
anchorEl: paletteBtn,
|
|
mode: "entity",
|
|
entityId: todo.id,
|
|
editUrl: todo.editUrl,
|
|
currentColor: getElementVisualColor(card),
|
|
currentFilter: getElementVisualFilter(card),
|
|
currentBackground: getElementVisualBackground(card),
|
|
title: "Mes images & couleurs",
|
|
});
|
|
});
|
|
|
|
inner.appendChild(actions);
|
|
card.appendChild(inner);
|
|
container.appendChild(card);
|
|
|
|
hydrateTodoFromEditForm(todo);
|
|
});
|
|
}
|
|
|
|
function buildTodoPreviewHtml(items) {
|
|
const safeItems = Array.isArray(items) ? items : [];
|
|
const maxItems = 8;
|
|
const visible = safeItems.slice(0, maxItems);
|
|
const hasMore = safeItems.length > maxItems;
|
|
|
|
const rows = visible
|
|
.map((it) => {
|
|
const checked = !!it.checked;
|
|
const text = escapeHtml(String(it.text || ""));
|
|
return `<li class="todo-checklist-preview-item${checked ? " is-checked" : ""}"><span class="todo-checklist-preview-box"><i class="mdi mdi-check"></i></span><span class="todo-checklist-preview-text">${text}</span></li>`;
|
|
})
|
|
.join("");
|
|
|
|
return `<ul class="todo-checklist-preview">${rows}${hasMore ? '<li class="todo-checklist-preview-more">...</li>' : ""}</ul>`;
|
|
}
|
|
|
|
function escapeHtml(str) {
|
|
return String(str)
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/\"/g, """)
|
|
.replace(/'/g, "'");
|
|
}
|
|
|
|
function parseTodoMarkdown(markdown) {
|
|
const raw = String(markdown || "");
|
|
const normalized = raw
|
|
.replace(/<br\s*\/?\s*>/gi, "\n")
|
|
.replace(/<\/(p|div|li|h\d)\s*>/gi, "\n");
|
|
|
|
let text = normalized;
|
|
try {
|
|
const tmp = document.createElement("div");
|
|
tmp.innerHTML = normalized;
|
|
const checkboxEls = Array.from(tmp.querySelectorAll('input[type="checkbox"]'));
|
|
if (checkboxEls.length > 0) {
|
|
const items = [];
|
|
checkboxEls.forEach((cb) => {
|
|
const li = cb.closest("li") || cb.parentElement;
|
|
let itemText = "";
|
|
if (li) {
|
|
const clone = li.cloneNode(true);
|
|
clone.querySelectorAll('input[type="checkbox"]').forEach((i) => i.remove());
|
|
itemText = (clone.textContent || "").trim();
|
|
}
|
|
items.push({ checked: !!cb.checked, text: itemText });
|
|
});
|
|
return { headerLines: [], footerLines: [], items };
|
|
}
|
|
|
|
const listItemEls = Array.from(tmp.querySelectorAll("li"));
|
|
if (listItemEls.length > 0) {
|
|
const items = listItemEls
|
|
.map((li) => ({ checked: false, text: (li.textContent || "").trim() }))
|
|
.filter((it) => it.text);
|
|
if (items.length > 0) {
|
|
return { headerLines: [], footerLines: [], items };
|
|
}
|
|
}
|
|
text = tmp.textContent || normalized;
|
|
} catch (e) {
|
|
text = normalized;
|
|
}
|
|
|
|
const lines = text.split(/\r?\n/);
|
|
const taskRegexes = [
|
|
/^\s*[-*+]\s*\[([ xX])\]\s*(.*)$/,
|
|
/^\s*\[([ xX])\]\s*(.*)$/,
|
|
/^\s*☐\s*(.*)$/,
|
|
/^\s*☑\s*(.*)$/,
|
|
];
|
|
|
|
let firstTaskIndex = -1;
|
|
let lastTaskIndex = -1;
|
|
const items = [];
|
|
|
|
lines.forEach((line, idx) => {
|
|
let matched = false;
|
|
|
|
for (let i = 0; i < taskRegexes.length; i++) {
|
|
const rx = taskRegexes[i];
|
|
const m = line.match(rx);
|
|
if (!m) continue;
|
|
|
|
matched = true;
|
|
if (firstTaskIndex === -1) firstTaskIndex = idx;
|
|
lastTaskIndex = idx;
|
|
|
|
if (i === 2) {
|
|
items.push({ checked: false, text: (m[1] || "").trim() });
|
|
} else if (i === 3) {
|
|
items.push({ checked: true, text: (m[1] || "").trim() });
|
|
} else {
|
|
items.push({ checked: String(m[1] || "").toLowerCase() === "x", text: (m[2] || "").trim() });
|
|
}
|
|
break;
|
|
}
|
|
|
|
if (!matched) return;
|
|
});
|
|
|
|
const headerLines = firstTaskIndex === -1 ? lines.slice() : lines.slice(0, firstTaskIndex);
|
|
const footerLines = lastTaskIndex === -1 ? [] : lines.slice(lastTaskIndex + 1);
|
|
|
|
return { headerLines, footerLines, items };
|
|
}
|
|
|
|
function buildTodoMarkdown(todo) {
|
|
const headerLines = Array.isArray(todo._todoHeaderLines) ? todo._todoHeaderLines : [];
|
|
const footerLines = Array.isArray(todo._todoFooterLines) ? todo._todoFooterLines : [];
|
|
const items = Array.isArray(todo.items) ? todo.items : [];
|
|
const taskLines = items.map((it) => `- [${it.checked ? "x" : " "}] ${String(it.text || "").trim()}`);
|
|
|
|
const out = [];
|
|
headerLines.forEach((l) => out.push(l));
|
|
taskLines.forEach((l) => out.push(l));
|
|
footerLines.forEach((l) => out.push(l));
|
|
|
|
return out.join("\n").replace(/\s+$/g, "");
|
|
}
|
|
|
|
function renderTodoModalChecklist(modal, todo) {
|
|
const list = modal.querySelector("#todo-modal-checklist");
|
|
if (!list) return;
|
|
|
|
list.innerHTML = "";
|
|
const items = Array.isArray(todo.items) ? todo.items : [];
|
|
|
|
items.forEach((item, idx) => {
|
|
const row = document.createElement("div");
|
|
row.className = "todo-checklist-row";
|
|
row.dataset.index = String(idx);
|
|
row.setAttribute("draggable", "true");
|
|
row.classList.toggle("is-checked", !!item.checked);
|
|
row.innerHTML = `
|
|
<button type="button" class="todo-drag-handle" aria-label="Déplacer"><i class="mdi mdi-dots-grid"></i></button>
|
|
<label class="todo-checklist-box">
|
|
<input type="checkbox" class="todo-item-checkbox" ${item.checked ? "checked" : ""}>
|
|
<span class="todo-checklist-box-ui"></span>
|
|
</label>
|
|
<input type="text" class="todo-item-text" value="${escapeHtml(item.text || "")}" placeholder="">
|
|
<button type="button" class="todo-item-delete" aria-label="Supprimer"><i class="mdi mdi-close"></i></button>
|
|
`;
|
|
|
|
const cb = row.querySelector(".todo-item-checkbox");
|
|
const textInput = row.querySelector(".todo-item-text");
|
|
const delBtn = row.querySelector(".todo-item-delete");
|
|
|
|
cb.addEventListener("change", () => {
|
|
todo.items[idx].checked = cb.checked;
|
|
scheduleTodoSave(modal, todo);
|
|
row.classList.toggle("is-checked", cb.checked);
|
|
});
|
|
|
|
textInput.addEventListener("input", () => {
|
|
todo.items[idx].text = textInput.value;
|
|
scheduleTodoSave(modal, todo);
|
|
});
|
|
|
|
delBtn.addEventListener("click", (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
todo.items.splice(idx, 1);
|
|
renderTodoModalChecklist(modal, todo);
|
|
scheduleTodoSave(modal, todo);
|
|
});
|
|
|
|
row.addEventListener("dragstart", (e) => {
|
|
row.classList.add("is-dragging");
|
|
e.dataTransfer.effectAllowed = "move";
|
|
e.dataTransfer.setData("text/plain", String(idx));
|
|
});
|
|
row.addEventListener("dragend", () => {
|
|
row.classList.remove("is-dragging");
|
|
});
|
|
|
|
list.appendChild(row);
|
|
});
|
|
|
|
if (!list.dataset.dndInit) {
|
|
list.addEventListener(
|
|
"dragover",
|
|
(e) => {
|
|
e.preventDefault();
|
|
const dragging = list.querySelector(".todo-checklist-row.is-dragging");
|
|
if (!dragging) return;
|
|
|
|
const afterElement = getDragAfterElement(list, e.clientY);
|
|
if (afterElement == null) {
|
|
list.appendChild(dragging);
|
|
} else {
|
|
list.insertBefore(dragging, afterElement);
|
|
}
|
|
},
|
|
{ passive: false },
|
|
);
|
|
|
|
list.addEventListener("drop", (e) => {
|
|
e.preventDefault();
|
|
const current = modal && modal.currentTodo ? modal.currentTodo : null;
|
|
if (!current) return;
|
|
const rows = Array.from(list.querySelectorAll(".todo-checklist-row"));
|
|
const newItems = [];
|
|
rows.forEach((r) => {
|
|
const oldIndex = Number(r.dataset.index);
|
|
if (!Number.isNaN(oldIndex) && current.items && current.items[oldIndex]) {
|
|
newItems.push(current.items[oldIndex]);
|
|
}
|
|
});
|
|
current.items = newItems;
|
|
renderTodoModalChecklist(modal, current);
|
|
scheduleTodoSave(modal, current);
|
|
});
|
|
|
|
list.dataset.dndInit = "1";
|
|
}
|
|
}
|
|
|
|
function getDragAfterElement(container, y) {
|
|
const draggableElements = [...container.querySelectorAll(".todo-checklist-row:not(.is-dragging)")];
|
|
|
|
return draggableElements.reduce(
|
|
(closest, child) => {
|
|
const box = child.getBoundingClientRect();
|
|
const offset = y - box.top - box.height / 2;
|
|
if (offset < 0 && offset > closest.offset) {
|
|
return { offset, element: child };
|
|
}
|
|
return closest;
|
|
},
|
|
{ offset: Number.NEGATIVE_INFINITY, element: null },
|
|
).element;
|
|
}
|
|
|
|
async function openTodoModal(todo) {
|
|
const modal = document.querySelector(".todo-modal-overlay");
|
|
if (!modal) return;
|
|
|
|
const modalCard = modal.querySelector(".note-modal");
|
|
const title = modal.querySelector("#todo-modal-title");
|
|
const tagsContainer = modal.querySelector("#todo-modal-tags");
|
|
const editLink = modal.querySelector("#todo-modal-edit");
|
|
const pinButton = modal.querySelector("#todo-modal-pin");
|
|
const modalColorPopup = modal.querySelector("#todo-modal-color-popup");
|
|
|
|
modal.currentTodo = todo;
|
|
|
|
modalCard.className = "note-modal todo-modal";
|
|
applyNoteVisualState(modalCard, todo);
|
|
modalCard.dataset.todoId = todo.id || "";
|
|
modalCard.dataset.editUrl = todo.editUrl || "";
|
|
modalCard.dataset.deleteUrl = todo.deleteUrl || "";
|
|
modalCard.dataset.background = todo.background || "none";
|
|
|
|
const visibleTags = (todo.tags || []).filter((tag) => tag && !isTechnicalTag(tag));
|
|
modalCard.dataset.tags = visibleTags.join("||");
|
|
|
|
title.textContent = todo.title || "Sans titre";
|
|
renderModalTags(tagsContainer, visibleTags);
|
|
|
|
if (editLink) {
|
|
editLink.href = todo.editUrl || "#";
|
|
}
|
|
|
|
if (modalColorPopup) {
|
|
modalColorPopup.innerHTML = generateUnifiedPaletteMenu({
|
|
entityId: todo && todo.id ? todo.id : "",
|
|
editUrl: todo && todo.editUrl ? todo.editUrl : "",
|
|
currentColor: todo && todo.color ? todo.color : "default",
|
|
currentFilter: todo && todo.filter ? todo.filter : "none",
|
|
mode: "modal",
|
|
});
|
|
modalColorPopup.classList.remove("open");
|
|
}
|
|
|
|
setModalPinButtonState(pinButton, !!todo.isPinned);
|
|
|
|
modal.classList.add("open");
|
|
renderTodoModalChecklist(modal, todo);
|
|
|
|
const loadKey = `${todo.id}-${Date.now()}`;
|
|
modal._todoLoadKey = loadKey;
|
|
|
|
if (todo.editUrl && todo.editUrl !== "#") {
|
|
try {
|
|
const response = await fetch(todo.editUrl, { credentials: "same-origin" });
|
|
if (!response.ok) {
|
|
console.error("Todo modal load failed (edit form fetch not ok).", { id: todo && todo.id, url: todo && todo.editUrl, status: response && response.status });
|
|
return;
|
|
}
|
|
const html = await response.text();
|
|
if (modal._todoLoadKey !== loadKey) return;
|
|
|
|
const parser = new DOMParser();
|
|
const doc = parser.parseFromString(html, "text/html");
|
|
const form = doc.querySelector('form[name="linkform"]');
|
|
if (!form) {
|
|
console.error("Todo modal load failed (edit form not found).", { id: todo && todo.id, url: todo && todo.editUrl, status: response && response.status });
|
|
return;
|
|
}
|
|
|
|
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, getFormFieldValue(input));
|
|
}
|
|
});
|
|
|
|
todo._todoEditAction = form.action;
|
|
todo._todoEditBaseData = formData;
|
|
|
|
const rawMarkdown = formData.get("lf_description") || "";
|
|
const parsed = parseTodoMarkdown(rawMarkdown);
|
|
todo._todoHeaderLines = parsed.headerLines;
|
|
todo._todoFooterLines = parsed.footerLines;
|
|
todo.items = parsed.items;
|
|
|
|
renderTodoModalChecklist(modal, todo);
|
|
} catch (e) {
|
|
console.error("Error loading todo markdown:", e);
|
|
}
|
|
}
|
|
}
|
|
|
|
function addTodoItem(modal, todo) {
|
|
if (!todo.items) todo.items = [];
|
|
todo.items.push({ checked: false, text: "" });
|
|
renderTodoModalChecklist(modal, todo);
|
|
const inputs = modal.querySelectorAll(".todo-item-text");
|
|
const last = inputs && inputs.length ? inputs[inputs.length - 1] : null;
|
|
if (last) last.focus();
|
|
scheduleTodoSave(modal, todo);
|
|
}
|
|
|
|
function scheduleTodoSave(modal, todo) {
|
|
if (!modal || !todo) return;
|
|
if (modal._todoSaveTimer) {
|
|
clearTimeout(modal._todoSaveTimer);
|
|
}
|
|
|
|
updateTodoCardPreview(todo);
|
|
|
|
modal._todoSaveTimer = setTimeout(() => {
|
|
persistTodoChanges(todo).catch((err) => {
|
|
console.error("Error saving todo:", err);
|
|
alert("Erreur lors de la sauvegarde de la tâche. Veuillez rafraîchir la page.");
|
|
});
|
|
}, 600);
|
|
}
|
|
|
|
function updateTodoCardPreview(todo) {
|
|
if (!todo || !todo.id) return;
|
|
let card = todo._todoCardEl && todo._todoCardEl.isConnected ? todo._todoCardEl : null;
|
|
if (!card) {
|
|
card = document.querySelector(`.note-card.todo-card[data-id="${String(todo.id)}"]`);
|
|
}
|
|
if (!card) {
|
|
const all = Array.from(document.querySelectorAll(".note-card.todo-card"));
|
|
card = all.find((c) => String(c.dataset.id || "") === String(todo.id)) || null;
|
|
}
|
|
if (!card) return;
|
|
const wrap = card.querySelector(".todo-checklist-preview-wrap");
|
|
if (!wrap) return;
|
|
wrap.innerHTML = buildTodoPreviewHtml(todo.items || []);
|
|
}
|
|
|
|
async function persistTodoChanges(todo, retryCount = 0) {
|
|
if (!todo || !todo.editUrl || todo.editUrl === "#") return;
|
|
|
|
const refreshEditForm = async () => {
|
|
const response = await fetch(todo.editUrl, { credentials: "same-origin" });
|
|
const html = await response.text();
|
|
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 baseData = new URLSearchParams();
|
|
const inputs = form.querySelectorAll("input, textarea");
|
|
inputs.forEach((input) => {
|
|
if (input.type === "checkbox") {
|
|
if (input.checked) baseData.append(input.name, input.value || "on");
|
|
} else if (input.name) {
|
|
baseData.append(input.name, getFormFieldValue(input));
|
|
}
|
|
});
|
|
|
|
todo._todoEditAction = form.action;
|
|
todo._todoEditBaseData = baseData;
|
|
|
|
const rawMarkdown = baseData.get("lf_description") || "";
|
|
const parsed = parseTodoMarkdown(rawMarkdown);
|
|
todo._todoHeaderLines = parsed.headerLines;
|
|
todo._todoFooterLines = parsed.footerLines;
|
|
};
|
|
|
|
if (!todo._todoEditAction || !todo._todoEditBaseData || !(todo._todoEditBaseData.get && todo._todoEditBaseData.get("token"))) {
|
|
await refreshEditForm();
|
|
}
|
|
|
|
const description = buildTodoMarkdown(todo);
|
|
const formData = new URLSearchParams(todo._todoEditBaseData.toString());
|
|
formData.set("lf_description", description);
|
|
formData.append("save_edit", "1");
|
|
|
|
const response = await fetch(todo._todoEditAction, {
|
|
method: "POST",
|
|
credentials: "same-origin",
|
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
body: formData.toString(),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
if (response.status === 403 && retryCount < 1) {
|
|
todo._todoEditAction = "";
|
|
todo._todoEditBaseData = null;
|
|
await refreshEditForm();
|
|
return persistTodoChanges(todo, retryCount + 1);
|
|
}
|
|
throw new Error("Failed to save todo");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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='/admin/shaare?post=&tags=note'">
|
|
<span class="note-input-placeholder">Créer une note...</span>
|
|
<div class="note-input-actions">
|
|
<button title="Créer une note" onclick="event.stopPropagation(); window.location.href='/admin/shaare?post=&tags=note'"><i class="mdi mdi-note-outline"></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);
|
|
}
|
|
|
|
// Check for hash parameter to auto-open a specific note
|
|
const hash = window.location.hash;
|
|
if (hash && hash.startsWith('#open-note-')) {
|
|
const noteId = hash.replace('#open-note-', '');
|
|
const targetNote = notes.find(n => String(n.id) === String(noteId));
|
|
if (targetNote) {
|
|
// Clear the hash to avoid reopening on refresh
|
|
history.replaceState(null, null, window.location.pathname + window.location.search);
|
|
// Open the note modal after a short delay to ensure DOM is ready
|
|
setTimeout(() => {
|
|
openNoteModal(targetNote);
|
|
}, 100);
|
|
}
|
|
}
|
|
|
|
// 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();
|
|
const modalCard = modalOverlay.querySelector(".note-modal");
|
|
openBackgroundStudioPanel({
|
|
anchorEl: modalColorBtn,
|
|
mode: "modal",
|
|
entityId: modalCard ? modalCard.dataset.noteId || "" : "",
|
|
editUrl: modalCard ? modalCard.dataset.editUrl || "" : "",
|
|
currentColor: modalCard ? getElementVisualColor(modalCard) : "default",
|
|
currentFilter: modalCard ? getElementVisualFilter(modalCard) : "none",
|
|
currentBackground: modalCard ? getElementVisualBackground(modalCard) : "none",
|
|
currentFontColor: modalCard ? getElementVisualFontColor(modalCard) : "auto",
|
|
title: "Mes images & couleurs",
|
|
});
|
|
});
|
|
|
|
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-archive")
|
|
.then(() => {
|
|
if (modalOverlay.currentNote) {
|
|
if (!modalOverlay.currentNote.tags.includes("shaarli-archive")) {
|
|
modalOverlay.currentNote.tags.push("shaarli-archive");
|
|
}
|
|
}
|
|
|
|
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");
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Initialize the Archive view (similar to Notes but for archived notes)
|
|
*/
|
|
function initArchiveView(linkList, container) {
|
|
document.body.classList.add("view-notes", "view-archive");
|
|
|
|
// 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 archive-wrapper";
|
|
|
|
// 2. Create Title Area (Top)
|
|
const topBar = document.createElement("div");
|
|
topBar.className = "notes-top-bar archive-top-bar";
|
|
|
|
// Title for archive view
|
|
const titleContainer = document.createElement("div");
|
|
titleContainer.className = "archive-title-container";
|
|
titleContainer.innerHTML = `
|
|
<h1 class="archive-title"><i class="mdi mdi-archive-arrow-down-outline"></i> Archive</h1>
|
|
<p class="archive-subtitle">Notes archivées</p>
|
|
`;
|
|
topBar.appendChild(titleContainer);
|
|
|
|
// 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));
|
|
|
|
// Filter only archived notes
|
|
const archivedNotes = notes.filter((note) => (note.tags || []).includes("shaarli-archive"));
|
|
|
|
// Initial Render (Grid)
|
|
renderNotes(contentArea, archivedNotes, "grid", true); // true = archive mode
|
|
|
|
wrapper.appendChild(contentArea);
|
|
|
|
// Replace original list
|
|
linkList.style.display = "none";
|
|
if (linkList.parentNode) {
|
|
linkList.parentNode.insertBefore(wrapper, linkList);
|
|
} else {
|
|
container.appendChild(wrapper);
|
|
}
|
|
|
|
// Modal Container (reuse the same modal as notes)
|
|
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-unarchive" title="Désarchiver"><i class="mdi mdi-archive-arrow-up-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, archivedNotes, "grid", true);
|
|
});
|
|
|
|
btnList.addEventListener("click", () => {
|
|
btnList.classList.add("active");
|
|
btnGrid.classList.remove("active");
|
|
renderNotes(contentArea, archivedNotes, "list", true);
|
|
});
|
|
|
|
// 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();
|
|
const modalCard = modalOverlay.querySelector(".note-modal");
|
|
openBackgroundStudioPanel({
|
|
anchorEl: modalColorBtn,
|
|
mode: "modal",
|
|
entityId: modalCard ? modalCard.dataset.noteId || "" : "",
|
|
editUrl: modalCard ? modalCard.dataset.editUrl || "" : "",
|
|
currentColor: modalCard ? getElementVisualColor(modalCard) : "default",
|
|
currentFilter: modalCard ? getElementVisualFilter(modalCard) : "none",
|
|
currentBackground: modalCard ? getElementVisualBackground(modalCard) : "none",
|
|
currentFontColor: modalCard ? getElementVisualFontColor(modalCard) : "auto",
|
|
title: "Mes images & couleurs",
|
|
});
|
|
});
|
|
|
|
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;
|
|
}
|
|
});
|
|
|
|
// Unarchive button in modal
|
|
modalOverlay.querySelector("#note-modal-unarchive").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;
|
|
|
|
removeTagFromEntity(editUrl, "shaarli-archive")
|
|
.then(() => {
|
|
if (modalOverlay.currentNote) {
|
|
modalOverlay.currentNote.tags = (modalOverlay.currentNote.tags || []).filter((t) => t !== "shaarli-archive");
|
|
}
|
|
|
|
const noteCard = document.querySelector(`.note-card[data-id="${noteId}"]`);
|
|
if (noteCard) noteCard.remove();
|
|
|
|
const index = archivedNotes.findIndex((n) => String(n.id) === String(noteId));
|
|
if (index > -1) archivedNotes.splice(index, 1);
|
|
|
|
modalOverlay.classList.remove("open");
|
|
})
|
|
.catch((err) => {
|
|
console.error("Error unarchiving note:", err);
|
|
alert("Erreur lors du désarchivage 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 rawTags = [];
|
|
linkEl.querySelectorAll(".link-tag-list a").forEach((tag) => {
|
|
const t = (tag.textContent || "").trim();
|
|
if (t) rawTags.push(t);
|
|
});
|
|
|
|
const { color, filter, background, fontColor } = extractNoteVisualStateFromTags(rawTags);
|
|
|
|
const isLegacyNoteColorTag = (t) => {
|
|
if (typeof t !== "string") return false;
|
|
if (!t.startsWith("note-")) return false;
|
|
const candidate = t.substring(5);
|
|
return NOTE_COLOR_OPTIONS.some((opt) => opt.key === candidate);
|
|
};
|
|
|
|
const tags = rawTags.filter((t) => {
|
|
if (t.startsWith(NOTE_COLOR_TAG_PREFIX)) return false;
|
|
if (t.startsWith("note-custom-")) return false;
|
|
if (isLegacyNoteColorTag(t)) return false;
|
|
if (t.startsWith(NOTE_FILTER_TAG_PREFIX)) return false;
|
|
if (t.startsWith(NOTE_BACKGROUND_TAG_PREFIX)) return false;
|
|
if (t.startsWith(NOTE_FONT_COLOR_TAG_PREFIX)) return false;
|
|
return true;
|
|
});
|
|
|
|
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, filter, background, fontColor, editUrl, deleteUrl, pinUrl, isPinned };
|
|
}
|
|
|
|
function renderNotes(container, notes, viewMode, isArchiveMode = false) {
|
|
container.innerHTML = "";
|
|
container.className = viewMode === "grid" ? "notes-masonry" : "notes-list-view";
|
|
|
|
// Filter notes based on archive mode
|
|
let visibleNotes;
|
|
if (isArchiveMode) {
|
|
// In archive mode: show only notes with shaarli-archive tag
|
|
visibleNotes = notes.filter((note) => (note.tags || []).includes("shaarli-archive"));
|
|
} else {
|
|
// In normal notes mode: hide archived notes
|
|
visibleNotes = notes.filter((note) => !(note.tags || []).includes("shaarli-archive"));
|
|
}
|
|
|
|
// 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;
|
|
card.dataset.editUrl = note.editUrl || "";
|
|
card.dataset.tags = (note.tags || []).filter((t) => t).join("||");
|
|
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 (isTechnicalTag(t)) return;
|
|
const pill = createTagPill({ tag: t, onRemoveClass: "note-tag-remove-btn", tagClass: "note-tag", canRemove: !!note.editUrl && note.editUrl !== "#" });
|
|
tagContainer.appendChild(pill);
|
|
});
|
|
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}`;
|
|
const archiveBtnId = `archive-${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>
|
|
<button title="Image"><i class="mdi mdi-image-outline"></i></button>
|
|
<button title="Archiver" id="${archiveBtnId}"><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}`);
|
|
paletteBtn.addEventListener("click", (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
openBackgroundStudioPanel({
|
|
anchorEl: paletteBtn,
|
|
mode: "entity",
|
|
entityId: note.id,
|
|
editUrl: note.editUrl,
|
|
currentColor: getElementVisualColor(card),
|
|
currentFilter: getElementVisualFilter(card),
|
|
currentBackground: getElementVisualBackground(card),
|
|
title: "Mes images & couleurs",
|
|
});
|
|
});
|
|
|
|
// Archive button handler
|
|
const archiveBtn = actions.querySelector(`#${archiveBtnId}`);
|
|
archiveBtn.addEventListener("click", (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
if (!note.editUrl || note.editUrl === "#") return;
|
|
|
|
addTagToNote(note.editUrl, "shaarli-archive")
|
|
.then(() => {
|
|
// Remove the card from the view
|
|
card.remove();
|
|
console.log(`Note ${note.id} archived`);
|
|
})
|
|
.catch((err) => {
|
|
console.error("Error archiving note:", err);
|
|
alert("Erreur lors de l'archivage de la note.");
|
|
});
|
|
});
|
|
|
|
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 && !isTechnicalTag(tag));
|
|
|
|
const modal = container.closest(".note-modal-overlay") || getOpenModalOverlay();
|
|
const entity = getModalCurrentEntity(modal);
|
|
const canRemove = !!(entity && entity.editUrl && entity.editUrl !== "#");
|
|
|
|
if (visibleTags.length === 0) {
|
|
container.classList.add("is-empty");
|
|
return;
|
|
}
|
|
|
|
container.classList.remove("is-empty");
|
|
visibleTags.forEach((tag) => {
|
|
const pill = createTagPill({ tag, onRemoveClass: "note-tag-remove-btn", tagClass: "note-tag", canRemove });
|
|
container.appendChild(pill);
|
|
});
|
|
}
|
|
|
|
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 && !isTechnicalTag(tag));
|
|
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) {
|
|
return generateUnifiedPaletteMenu({
|
|
entityId: note && note.id ? note.id : "",
|
|
editUrl: note && note.editUrl ? note.editUrl : "",
|
|
currentColor: note && note.color ? note.color : "default",
|
|
currentFilter: note && note.filter ? note.filter : "none",
|
|
mode: "modal",
|
|
});
|
|
}
|
|
|
|
window.setModalNoteColor = function (color) {
|
|
const modal = getOpenModalOverlay();
|
|
const entity = getModalCurrentEntity(modal);
|
|
if (!modal || !entity) return;
|
|
|
|
setNoteColor(entity.id, color, entity.editUrl);
|
|
|
|
entity.color = color;
|
|
const modalCard = modal.querySelector(".note-modal");
|
|
if (modalCard) {
|
|
applyNoteVisualState(modalCard, entity);
|
|
}
|
|
|
|
const modalColorPopup = modal.querySelector(".note-modal-palette");
|
|
if (modalColorPopup) {
|
|
modalColorPopup.innerHTML = generateModalPaletteButtons(entity);
|
|
modalColorPopup.classList.add("open");
|
|
positionPalettePopup(modalColorPopup);
|
|
}
|
|
};
|
|
|
|
window.setModalNoteFontColor = function (fontColorKey) {
|
|
const modal = getOpenModalOverlay();
|
|
const entity = getModalCurrentEntity(modal);
|
|
if (!modal || !entity) return;
|
|
|
|
setNoteFontColor(entity.id, fontColorKey, entity.editUrl);
|
|
|
|
entity.fontColor = fontColorKey;
|
|
const modalCard = modal.querySelector(".note-modal");
|
|
if (modalCard) {
|
|
applyNoteVisualState(modalCard, entity);
|
|
}
|
|
|
|
const modalColorPopup = modal.querySelector(".note-modal-palette");
|
|
if (modalColorPopup) {
|
|
modalColorPopup.innerHTML = generateModalPaletteButtons(entity);
|
|
modalColorPopup.classList.add("open");
|
|
positionPalettePopup(modalColorPopup);
|
|
}
|
|
};
|
|
|
|
window.setModalNoteFilter = function (filterKey) {
|
|
const modal = getOpenModalOverlay();
|
|
const entity = getModalCurrentEntity(modal);
|
|
if (!modal || !entity) return;
|
|
|
|
const normalizedFilterKey = normalizeFilterKey(filterKey) || "none";
|
|
setNoteFilter(entity.id, normalizedFilterKey, entity.editUrl);
|
|
|
|
entity.filter = normalizedFilterKey;
|
|
const modalCard = modal.querySelector(".note-modal");
|
|
if (modalCard) {
|
|
applyNoteVisualState(modalCard, entity);
|
|
}
|
|
|
|
const modalColorPopup = modal.querySelector(".note-modal-palette");
|
|
if (modalColorPopup) {
|
|
modalColorPopup.innerHTML = generateModalPaletteButtons(entity);
|
|
modalColorPopup.classList.add("open");
|
|
positionPalettePopup(modalColorPopup);
|
|
}
|
|
};
|
|
|
|
function generatePaletteButtons(note) {
|
|
return generateUnifiedPaletteMenu({
|
|
entityId: note && note.id ? note.id : "",
|
|
editUrl: note && note.editUrl ? note.editUrl : "",
|
|
currentColor: note && note.color ? note.color : "default",
|
|
currentFilter: note && note.filter ? note.filter : "none",
|
|
mode: "entity",
|
|
});
|
|
}
|
|
|
|
window.setNoteColor = function (noteId, color, editUrl) {
|
|
// 1. Visual Update (Immediate feedback)
|
|
setNoteColorVisual(noteId, color);
|
|
|
|
const bookmarkCard = document.querySelector(`.link-outer[data-id="${noteId}"]`);
|
|
if (bookmarkCard) {
|
|
const palettePopup = bookmarkCard.querySelector(".bookmark-palette .palette-popup");
|
|
if (palettePopup) {
|
|
const currentColor = getElementVisualColor(bookmarkCard);
|
|
const currentFilter = getElementVisualFilter(bookmarkCard);
|
|
palettePopup.innerHTML = generateBookmarkPaletteButtons(noteId, editUrl, currentColor, currentFilter);
|
|
}
|
|
}
|
|
|
|
const modal = getOpenModalOverlay();
|
|
const entity = getModalCurrentEntity(modal);
|
|
if (modal && entity && String(entity.id) === String(noteId)) {
|
|
entity.color = color;
|
|
const modalCard = modal.querySelector(".note-modal");
|
|
if (modalCard) {
|
|
applyNoteVisualState(modalCard, entity);
|
|
}
|
|
const modalColorPopup = modal.querySelector(".note-modal-palette");
|
|
if (modalColorPopup) {
|
|
modalColorPopup.innerHTML = generateModalPaletteButtons(entity);
|
|
}
|
|
}
|
|
|
|
// 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, getFormFieldValue(input));
|
|
}
|
|
});
|
|
|
|
// Update Tags
|
|
let currentTags = formData.get("lf_tags") || "";
|
|
let tagsArray = currentTags.split(/[\s,]+/).filter((t) => t.trim() !== "");
|
|
|
|
// Remove existing color tags (only one note-color-* allowed)
|
|
tagsArray = tagsArray.filter((t) => !t.startsWith(NOTE_COLOR_TAG_PREFIX));
|
|
|
|
// Backward compat cleanup: remove legacy note-custom-<hex>
|
|
tagsArray = tagsArray.filter((t) => !t.startsWith("note-custom-"));
|
|
|
|
// Backward compat cleanup: remove legacy note-<color>
|
|
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") {
|
|
if (typeof color === "string" && color.startsWith("custom:")) {
|
|
const cleanColor = color.substring(7).replace("#", "");
|
|
tagsArray.push(`${NOTE_COLOR_TAG_PREFIX}${cleanColor}`);
|
|
} else {
|
|
tagsArray.push(`${NOTE_COLOR_TAG_PREFIX}${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.setNoteFilter = function (noteId, filterKey, editUrl) {
|
|
const normalizedFilterKey = normalizeFilterKey(filterKey) || "none";
|
|
|
|
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, filter: normalizedFilterKey });
|
|
}
|
|
|
|
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, filter: normalizedFilterKey });
|
|
const palettePopup = bookmarkCard.querySelector(".bookmark-palette .palette-popup");
|
|
if (palettePopup) {
|
|
palettePopup.innerHTML = generateBookmarkPaletteButtons(noteId, editUrl, color, normalizedFilterKey);
|
|
}
|
|
}
|
|
|
|
const modal = getOpenModalOverlay();
|
|
const entity = getModalCurrentEntity(modal);
|
|
if (modal && entity && String(entity.id) === String(noteId)) {
|
|
entity.filter = normalizedFilterKey;
|
|
const modalCard = modal.querySelector(".note-modal");
|
|
if (modalCard) {
|
|
applyNoteVisualState(modalCard, entity);
|
|
}
|
|
const modalColorPopup = modal.querySelector(".note-modal-palette");
|
|
if (modalColorPopup) {
|
|
modalColorPopup.innerHTML = generateModalPaletteButtons(entity);
|
|
}
|
|
}
|
|
|
|
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, getFormFieldValue(input));
|
|
}
|
|
});
|
|
|
|
let currentTags = formData.get("lf_tags") || "";
|
|
let tagsArray = currentTags.split(/[\s,]+/).filter((t) => t.trim() !== "");
|
|
|
|
tagsArray = tagsArray.filter((t) => !t.startsWith(NOTE_FILTER_TAG_PREFIX));
|
|
if (normalizedFilterKey && normalizedFilterKey !== "none") {
|
|
tagsArray.push(`${NOTE_FILTER_TAG_PREFIX}${normalizedFilterKey}`);
|
|
}
|
|
|
|
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 filter");
|
|
}
|
|
})
|
|
.catch((err) => {
|
|
console.error("Error saving note filter:", err);
|
|
alert("Erreur lors de la sauvegarde du filtre. Veuillez rafraîchir la page.");
|
|
});
|
|
};
|
|
|
|
window.setNoteBackground = function (noteId, backgroundKey, editUrl) {
|
|
const normalizedBackgroundKey = backgroundKey === "none" ? "none" : normalizeBackgroundKey(backgroundKey) || "none";
|
|
|
|
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";
|
|
const filter = card.dataset.filter || "none";
|
|
applyNoteVisualState(card, { color, filter, background: normalizedBackgroundKey });
|
|
}
|
|
|
|
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";
|
|
const filter = bookmarkCard.dataset.filter || "none";
|
|
applyNoteVisualState(bookmarkCard, { color, filter, background: normalizedBackgroundKey });
|
|
}
|
|
|
|
const modal = getOpenModalOverlay();
|
|
const entity = getModalCurrentEntity(modal);
|
|
if (modal && entity && String(entity.id) === String(noteId)) {
|
|
entity.background = normalizedBackgroundKey;
|
|
const modalCard = modal.querySelector(".note-modal");
|
|
if (modalCard) {
|
|
applyNoteVisualState(modalCard, entity);
|
|
}
|
|
const modalColorPopup = modal.querySelector(".note-modal-palette");
|
|
if (modalColorPopup) {
|
|
modalColorPopup.innerHTML = generateModalPaletteButtons(entity);
|
|
}
|
|
}
|
|
|
|
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, getFormFieldValue(input));
|
|
}
|
|
});
|
|
|
|
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 (normalizedBackgroundKey && normalizedBackgroundKey !== "none") {
|
|
tagsArray.push(`${NOTE_BACKGROUND_TAG_PREFIX}${normalizedBackgroundKey}`);
|
|
}
|
|
|
|
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.");
|
|
});
|
|
};
|
|
|
|
window.setModalNoteBackground = function (backgroundKey) {
|
|
const modal = getOpenModalOverlay();
|
|
const entity = getModalCurrentEntity(modal);
|
|
if (!modal || !entity) return;
|
|
|
|
const normalizedBackgroundKey = backgroundKey === "none" ? "none" : normalizeBackgroundKey(backgroundKey) || "none";
|
|
setNoteBackground(entity.id, normalizedBackgroundKey, entity.editUrl);
|
|
|
|
entity.background = normalizedBackgroundKey;
|
|
const modalCard = modal.querySelector(".note-modal");
|
|
if (modalCard) {
|
|
applyNoteVisualState(modalCard, entity);
|
|
}
|
|
|
|
const modalColorPopup = modal.querySelector(".note-modal-palette");
|
|
if (modalColorPopup) {
|
|
modalColorPopup.innerHTML = generateModalPaletteButtons(entity);
|
|
modalColorPopup.classList.add("open");
|
|
positionPalettePopup(modalColorPopup);
|
|
}
|
|
};
|
|
|
|
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, getFormFieldValue(input));
|
|
}
|
|
});
|
|
|
|
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]);
|
|
}
|
|
}
|
|
|
|
if (typeof organizePinnedBookmarks === "function") {
|
|
window.requestAnimationFrame(organizePinnedBookmarks);
|
|
}
|
|
|
|
// 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) {
|
|
card.classList.toggle("is-pinned-tag", isPinning);
|
|
|
|
// 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, getFormFieldValue(input));
|
|
}
|
|
});
|
|
|
|
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");
|
|
if (typeof organizePinnedBookmarks === "function") {
|
|
window.requestAnimationFrame(organizePinnedBookmarks);
|
|
}
|
|
}
|
|
})
|
|
.catch((err) => console.error(err));
|
|
}
|
|
|
|
/* ==========================================================
|
|
COLOR PICKER PANEL
|
|
========================================================== */
|
|
let colorPickerPanelInitialized = false;
|
|
|
|
function ensureColorPickerPanel() {
|
|
if (colorPickerPanelInitialized) return;
|
|
if (document.getElementById("shaarli-color-picker")) {
|
|
colorPickerPanelInitialized = true;
|
|
return;
|
|
}
|
|
|
|
const panel = document.createElement("div");
|
|
panel.id = "shaarli-color-picker";
|
|
panel.className = "color-picker-panel";
|
|
panel.setAttribute("role", "dialog");
|
|
panel.setAttribute("aria-modal", "false");
|
|
panel.setAttribute("aria-hidden", "true");
|
|
panel.style.display = "none";
|
|
document.body.appendChild(panel);
|
|
|
|
document.addEventListener("click", (e) => {
|
|
const p = document.getElementById("shaarli-color-picker");
|
|
if (!p || !p.classList.contains("open")) return;
|
|
if (e.target.closest("#shaarli-color-picker")) return;
|
|
if (e.target.closest("#shaarli-bg-studio")) return;
|
|
closeColorPickerPanel();
|
|
});
|
|
|
|
document.addEventListener("keydown", (e) => {
|
|
if (e.key !== "Escape") return;
|
|
const p = document.getElementById("shaarli-color-picker");
|
|
if (!p || !p.classList.contains("open")) return;
|
|
closeColorPickerPanel();
|
|
});
|
|
|
|
colorPickerPanelInitialized = true;
|
|
}
|
|
|
|
function closeColorPickerPanel() {
|
|
const panel = document.getElementById("shaarli-color-picker");
|
|
if (!panel) return;
|
|
|
|
const type = panel.dataset.type || "background";
|
|
const skipRestore = panel.dataset.skipRestore === "1";
|
|
if (!skipRestore && type === "font") {
|
|
const prevFontColorKey = panel.dataset.prevFontColorKey || "auto";
|
|
const mode = panel.dataset.mode || "entity";
|
|
const entityId = panel.dataset.entityId || "";
|
|
if (mode === "modal") {
|
|
setModalNoteFontColorVisual(prevFontColorKey);
|
|
} else {
|
|
setNoteFontColorVisual(entityId, prevFontColorKey);
|
|
}
|
|
}
|
|
|
|
if (!skipRestore && type === "background") {
|
|
const prevColorKey = panel.dataset.prevColorKey || "default";
|
|
const mode = panel.dataset.mode || "entity";
|
|
const entityId = panel.dataset.entityId || "";
|
|
if (mode === "modal") {
|
|
setModalNoteColorVisual(prevColorKey);
|
|
} else {
|
|
setNoteColorVisual(entityId, prevColorKey);
|
|
}
|
|
}
|
|
|
|
panel.dataset.skipRestore = "0";
|
|
panel.classList.remove("open");
|
|
panel.style.display = "none";
|
|
panel.setAttribute("aria-hidden", "true");
|
|
}
|
|
|
|
function openColorPickerPanel({ mode, entityId, editUrl, type }) {
|
|
ensureColorPickerPanel();
|
|
const panel = document.getElementById("shaarli-color-picker");
|
|
if (!panel) return;
|
|
|
|
panel.dataset.mode = mode || "entity";
|
|
panel.dataset.entityId = entityId || "";
|
|
panel.dataset.editUrl = editUrl || "";
|
|
panel.dataset.type = type || "background";
|
|
|
|
const isDark = getCurrentThemeMode() === "dark";
|
|
const defaultColor = isDark ? "#20293A" : "#ffffff";
|
|
|
|
if ((type || "background") === "font") {
|
|
let prevFontColorKey = "auto";
|
|
if ((mode || "entity") === "modal") {
|
|
const modal = getOpenModalOverlay();
|
|
const modalCard = modal ? modal.querySelector(".note-modal") : null;
|
|
prevFontColorKey = (modalCard && modalCard.dataset.fontColor) ? modalCard.dataset.fontColor : "auto";
|
|
} else {
|
|
const noteCard = document.querySelector(`.note-card[data-id="${entityId}"]`);
|
|
const bookmarkCard = document.querySelector(`.link-outer[data-id="${entityId}"]`);
|
|
prevFontColorKey = (noteCard && noteCard.dataset.fontColor) ? noteCard.dataset.fontColor : ((bookmarkCard && bookmarkCard.dataset.fontColor) ? bookmarkCard.dataset.fontColor : "auto");
|
|
}
|
|
panel.dataset.prevFontColorKey = prevFontColorKey;
|
|
} else {
|
|
panel.dataset.prevFontColorKey = "";
|
|
}
|
|
|
|
if ((type || "background") === "background") {
|
|
let prevColorKey = "default";
|
|
if ((mode || "entity") === "modal") {
|
|
const modal = getOpenModalOverlay();
|
|
const modalCard = modal ? modal.querySelector(".note-modal") : null;
|
|
if (modalCard) {
|
|
const isCustom = modalCard.dataset.color === "custom";
|
|
const cc = modalCard.dataset.customColor || "";
|
|
prevColorKey = isCustom && cc ? `custom:${cc}` : getElementVisualColor(modalCard);
|
|
}
|
|
} else {
|
|
const noteCard = document.querySelector(`.note-card[data-id="${entityId}"]`);
|
|
const bookmarkCard = document.querySelector(`.link-outer[data-id="${entityId}"]`);
|
|
const el = noteCard || bookmarkCard;
|
|
if (el) {
|
|
const isCustom = el.dataset.color === "custom";
|
|
const cc = el.dataset.customColor || "";
|
|
prevColorKey = isCustom && cc ? `custom:${cc}` : getElementVisualColor(el);
|
|
}
|
|
}
|
|
panel.dataset.prevColorKey = prevColorKey;
|
|
} else {
|
|
panel.dataset.prevColorKey = "";
|
|
}
|
|
|
|
panel.innerHTML = `
|
|
<div class="color-picker-header">
|
|
<div class="color-picker-title">${type === "font" ? "Couleur du texte" : "Couleur personnalisée"}</div>
|
|
<button class="color-picker-close" type="button" data-color-picker-action="close" title="Fermer" aria-label="Fermer"><i class="mdi mdi-close" aria-hidden="true"></i></button>
|
|
</div>
|
|
<div class="color-picker-body">
|
|
<div class="color-picker-gradient">
|
|
<div class="color-picker-gradient-inner" id="color-gradient">
|
|
<div class="color-picker-cursor" id="color-cursor"></div>
|
|
</div>
|
|
</div>
|
|
<div class="color-picker-hue">
|
|
<input type="range" min="0" max="360" value="210" class="color-picker-hue-slider" id="hue-slider">
|
|
</div>
|
|
<div class="color-picker-preview-row">
|
|
<div class="color-picker-preview" id="color-preview"></div>
|
|
<div class="color-picker-inputs">
|
|
<div class="color-picker-input-group">
|
|
<label>Hex</label>
|
|
<input type="text" id="hex-input" value="${defaultColor}" maxlength="7">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="color-picker-footer">
|
|
<button type="button" class="color-picker-btn color-picker-btn-secondary" data-color-picker-action="close">Annuler</button>
|
|
<button type="button" class="color-picker-btn color-picker-btn-primary" data-color-picker-action="apply">Appliquer</button>
|
|
</div>
|
|
`;
|
|
|
|
// Initialize color picker state
|
|
let currentHue = 210;
|
|
let currentSaturation = 50;
|
|
let currentValue = 50;
|
|
let currentHex = defaultColor;
|
|
|
|
const gradient = panel.querySelector("#color-gradient");
|
|
const cursor = panel.querySelector("#color-cursor");
|
|
const hueSlider = panel.querySelector("#hue-slider");
|
|
const hexInput = panel.querySelector("#hex-input");
|
|
const preview = panel.querySelector("#color-preview");
|
|
|
|
function hsvToHex(h, s, v) {
|
|
const c = v * s;
|
|
const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
|
|
const m = v - c;
|
|
let r, g, b;
|
|
|
|
if (h < 60) { r = c; g = x; b = 0; }
|
|
else if (h < 120) { r = x; g = c; b = 0; }
|
|
else if (h < 180) { r = 0; g = c; b = x; }
|
|
else if (h < 240) { r = 0; g = x; b = c; }
|
|
else if (h < 300) { r = x; g = 0; b = c; }
|
|
else { r = c; g = 0; b = x; }
|
|
|
|
const toHex = (n) => Math.round((n + m) * 255).toString(16).padStart(2, "0");
|
|
return `#${toHex(r)}${toHex(g)}${toHex(b)}`.toUpperCase();
|
|
}
|
|
|
|
function hexToHsv(hex) {
|
|
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
if (!result) return { h: 0, s: 0, v: 0 };
|
|
|
|
const r = parseInt(result[1], 16) / 255;
|
|
const g = parseInt(result[2], 16) / 255;
|
|
const b = parseInt(result[3], 16) / 255;
|
|
|
|
const max = Math.max(r, g, b);
|
|
const min = Math.min(r, g, b);
|
|
const d = max - min;
|
|
|
|
let h = 0;
|
|
if (d !== 0) {
|
|
if (max === r) h = ((g - b) / d + 6) % 6 * 60;
|
|
else if (max === g) h = ((b - r) / d + 2) * 60;
|
|
else h = ((r - g) / d + 4) * 60;
|
|
}
|
|
|
|
const s = max === 0 ? 0 : d / max;
|
|
const v = max;
|
|
|
|
return { h, s, v };
|
|
}
|
|
|
|
function updateColor() {
|
|
currentHex = hsvToHex(currentHue, currentSaturation / 100, currentValue / 100);
|
|
gradient.style.background = `linear-gradient(to top, #000, transparent), linear-gradient(to right, #fff, hsl(${currentHue}, 100%, 50%))`;
|
|
cursor.style.left = `${currentSaturation}%`;
|
|
cursor.style.top = `${100 - currentValue}%`;
|
|
preview.style.backgroundColor = currentHex;
|
|
hexInput.value = currentHex;
|
|
|
|
if ((panel.dataset.type || "background") === "font") {
|
|
const previewKey = `custom:${currentHex}`;
|
|
const pMode = panel.dataset.mode || "entity";
|
|
const pEntityId = panel.dataset.entityId || "";
|
|
if (pMode === "modal") {
|
|
setModalNoteFontColorVisual(previewKey);
|
|
} else {
|
|
setNoteFontColorVisual(pEntityId, previewKey);
|
|
}
|
|
}
|
|
|
|
if ((panel.dataset.type || "background") === "background") {
|
|
const previewKey = `custom:${currentHex}`;
|
|
const pMode = panel.dataset.mode || "entity";
|
|
const pEntityId = panel.dataset.entityId || "";
|
|
if (pMode === "modal") {
|
|
setModalNoteColorVisual(previewKey);
|
|
} else {
|
|
setNoteColorVisual(pEntityId, previewKey);
|
|
}
|
|
}
|
|
}
|
|
|
|
function setFromHex(hex) {
|
|
const hsv = hexToHsv(hex);
|
|
currentHue = hsv.h;
|
|
currentSaturation = hsv.s * 100;
|
|
currentValue = hsv.v * 100;
|
|
hueSlider.value = currentHue;
|
|
updateColor();
|
|
}
|
|
|
|
// Event listeners
|
|
let isDragging = false;
|
|
|
|
gradient.addEventListener("mousedown", (e) => {
|
|
isDragging = true;
|
|
const rect = gradient.getBoundingClientRect();
|
|
currentSaturation = Math.max(0, Math.min(100, ((e.clientX - rect.left) / rect.width) * 100));
|
|
currentValue = Math.max(0, Math.min(100, 100 - ((e.clientY - rect.top) / rect.height) * 100));
|
|
updateColor();
|
|
});
|
|
|
|
document.addEventListener("mousemove", (e) => {
|
|
if (!isDragging) return;
|
|
const rect = gradient.getBoundingClientRect();
|
|
currentSaturation = Math.max(0, Math.min(100, ((e.clientX - rect.left) / rect.width) * 100));
|
|
currentValue = Math.max(0, Math.min(100, 100 - ((e.clientY - rect.top) / rect.height) * 100));
|
|
updateColor();
|
|
});
|
|
|
|
document.addEventListener("mouseup", () => {
|
|
isDragging = false;
|
|
});
|
|
|
|
hueSlider.addEventListener("input", (e) => {
|
|
currentHue = parseInt(e.target.value);
|
|
updateColor();
|
|
});
|
|
|
|
hexInput.addEventListener("input", (e) => {
|
|
const hex = e.target.value;
|
|
if (/^#[0-9A-F]{6}$/i.test(hex)) {
|
|
setFromHex(hex);
|
|
}
|
|
});
|
|
|
|
// Initialize
|
|
if ((panel.dataset.type || "background") === "font") {
|
|
const prevFontColorKey = panel.dataset.prevFontColorKey || "auto";
|
|
let initialHex = defaultColor;
|
|
if (typeof prevFontColorKey === "string" && prevFontColorKey.startsWith("custom:")) {
|
|
initialHex = prevFontColorKey.substring(7);
|
|
} else if (prevFontColorKey !== "auto") {
|
|
const opt = NOTE_FONT_COLOR_OPTIONS.find((o) => o.key === prevFontColorKey);
|
|
if (opt && opt.value && opt.value !== "auto") initialHex = opt.value;
|
|
}
|
|
setFromHex(initialHex);
|
|
} else if ((panel.dataset.type || "background") === "background") {
|
|
const prevColorKey = panel.dataset.prevColorKey || "default";
|
|
let initialHex = defaultColor;
|
|
if (typeof prevColorKey === "string" && prevColorKey.startsWith("custom:")) {
|
|
initialHex = prevColorKey.substring(7);
|
|
}
|
|
setFromHex(initialHex);
|
|
} else {
|
|
setFromHex(defaultColor);
|
|
}
|
|
|
|
// Position the color picker panel
|
|
positionColorPickerPanel(panel, mode);
|
|
|
|
panel.style.display = "block";
|
|
panel.classList.add("open");
|
|
panel.setAttribute("aria-hidden", "false");
|
|
}
|
|
|
|
function positionColorPickerPanel(panel, mode) {
|
|
if (!panel) return;
|
|
|
|
const bgPanel = document.getElementById("shaarli-bg-studio");
|
|
const viewportPadding = 12;
|
|
|
|
// Default position if bgPanel is not available
|
|
let anchorRect = null;
|
|
|
|
if (bgPanel && bgPanel.classList.contains("open")) {
|
|
const bgRect = bgPanel.getBoundingClientRect();
|
|
// Position to the right of the bg-studio panel
|
|
let left = bgRect.right + 10;
|
|
let top = bgRect.top;
|
|
|
|
// Check if it fits on the right
|
|
if (left + panel.offsetWidth > window.innerWidth - viewportPadding) {
|
|
// Position to the left instead
|
|
left = bgRect.left - panel.offsetWidth - 10;
|
|
}
|
|
|
|
// Ensure it stays within viewport
|
|
if (left < viewportPadding) left = viewportPadding;
|
|
if (top + panel.offsetHeight > window.innerHeight - viewportPadding) {
|
|
top = window.innerHeight - viewportPadding - panel.offsetHeight;
|
|
}
|
|
if (top < viewportPadding) top = viewportPadding;
|
|
|
|
panel.style.left = `${Math.round(left)}px`;
|
|
panel.style.top = `${Math.round(top)}px`;
|
|
panel.style.right = "";
|
|
panel.style.bottom = "";
|
|
} else {
|
|
// Center in viewport if bg panel not available
|
|
const panelRect = panel.getBoundingClientRect();
|
|
const left = Math.max(viewportPadding, (window.innerWidth - panelRect.width) / 2);
|
|
const top = Math.max(viewportPadding, (window.innerHeight - panelRect.height) / 2);
|
|
panel.style.left = `${Math.round(left)}px`;
|
|
panel.style.top = `${Math.round(top)}px`;
|
|
}
|
|
}
|
|
|
|
// Handle color picker actions
|
|
document.addEventListener("click", (e) => {
|
|
const actionBtn = e.target.closest("[data-color-picker-action]");
|
|
if (!actionBtn) return;
|
|
|
|
const panel = document.getElementById("shaarli-color-picker");
|
|
if (!panel) return;
|
|
|
|
const action = actionBtn.dataset.colorPickerAction;
|
|
|
|
if (action === "close") {
|
|
closeColorPickerPanel();
|
|
return;
|
|
}
|
|
|
|
if (action === "apply") {
|
|
const mode = panel.dataset.mode || "entity";
|
|
const entityId = panel.dataset.entityId || "";
|
|
const editUrl = panel.dataset.editUrl || "";
|
|
const type = panel.dataset.type || "background";
|
|
const hexInput = panel.querySelector("#hex-input");
|
|
const color = hexInput ? hexInput.value : "#ffffff";
|
|
|
|
if (type === "font") {
|
|
if (mode === "modal") {
|
|
setModalCustomFontColor(color);
|
|
} else {
|
|
setNoteFontColor(entityId, `custom:${color}`, editUrl);
|
|
}
|
|
} else {
|
|
if (mode === "modal") {
|
|
setModalCustomNoteColor(color);
|
|
} else {
|
|
setNoteColor(entityId, `custom:${color}`, editUrl);
|
|
}
|
|
}
|
|
|
|
panel.dataset.skipRestore = "1";
|
|
closeColorPickerPanel();
|
|
}
|
|
});
|
|
|
|
function setNoteFontColorVisual(noteId, fontColorKey) {
|
|
const card = document.querySelector(`.note-card[data-id="${noteId}"]`);
|
|
const bookmarkCard = document.querySelector(`.link-outer[data-id="${noteId}"]`);
|
|
|
|
let colorValue = "auto";
|
|
if (typeof fontColorKey === "string" && fontColorKey.startsWith("custom:")) {
|
|
colorValue = fontColorKey.substring(7);
|
|
} else if (fontColorKey !== "auto") {
|
|
const option = NOTE_FONT_COLOR_OPTIONS.find((opt) => opt.key === fontColorKey);
|
|
colorValue = option ? option.value : "auto";
|
|
}
|
|
|
|
const applyTo = (element) => {
|
|
if (!element) return;
|
|
if (colorValue === "auto") element.style.removeProperty("--note-card-fg");
|
|
else element.style.setProperty("--note-card-fg", colorValue);
|
|
element.dataset.fontColor = fontColorKey;
|
|
};
|
|
|
|
applyTo(card);
|
|
applyTo(bookmarkCard);
|
|
|
|
const modal = getOpenModalOverlay();
|
|
const entity = getModalCurrentEntity(modal);
|
|
if (modal && entity && String(entity.id) === String(noteId)) {
|
|
const modalCard = modal.querySelector(".note-modal");
|
|
applyTo(modalCard);
|
|
}
|
|
}
|
|
|
|
function setModalNoteFontColorVisual(fontColorKey) {
|
|
const modal = getOpenModalOverlay();
|
|
const entity = getModalCurrentEntity(modal);
|
|
if (!modal || !entity) return;
|
|
setNoteFontColorVisual(entity.id, fontColorKey);
|
|
}
|
|
|
|
function setNoteColorVisual(noteId, colorKey) {
|
|
const card = document.querySelector(`.note-card[data-id="${noteId}"]`);
|
|
const bookmarkCard = document.querySelector(`.link-outer[data-id="${noteId}"]`);
|
|
|
|
const applyTo = (element) => {
|
|
if (!element) return;
|
|
const currentState = {
|
|
color: getElementVisualColor(element),
|
|
filter: getElementVisualFilter(element),
|
|
background: getElementVisualBackground(element),
|
|
fontColor: getElementVisualFontColor(element),
|
|
};
|
|
|
|
if (typeof colorKey === "string" && colorKey.startsWith("custom:")) {
|
|
const hex = colorKey.substring(7);
|
|
element.classList.forEach((cls) => {
|
|
if (cls.startsWith("note-color-")) element.classList.remove(cls);
|
|
});
|
|
element.classList.add("note-color-custom");
|
|
element.style.backgroundColor = hex;
|
|
element.style.borderColor = "transparent";
|
|
element.dataset.color = "custom";
|
|
element.dataset.customColor = hex;
|
|
const fg = getReadableForegroundForBackground(hex);
|
|
if (fg) element.style.setProperty("--note-card-fg", fg);
|
|
return;
|
|
}
|
|
|
|
element.dataset.customColor = "";
|
|
applyNoteVisualState(element, { ...currentState, color: colorKey });
|
|
};
|
|
|
|
applyTo(card);
|
|
applyTo(bookmarkCard);
|
|
|
|
const modal = getOpenModalOverlay();
|
|
const entity = getModalCurrentEntity(modal);
|
|
if (modal && entity && String(entity.id) === String(noteId)) {
|
|
const modalCard = modal.querySelector(".note-modal");
|
|
applyTo(modalCard);
|
|
}
|
|
}
|
|
|
|
function setModalNoteColorVisual(colorKey) {
|
|
const modal = getOpenModalOverlay();
|
|
const entity = getModalCurrentEntity(modal);
|
|
if (!modal || !entity) return;
|
|
setNoteColorVisual(entity.id, colorKey);
|
|
}
|
|
/* ==========================================================
|
|
FONT COLOR FUNCTIONS
|
|
========================================================== */
|
|
function setNoteFontColor(noteId, fontColorKey, editUrl) {
|
|
// 1. Visual Update (Immediate feedback)
|
|
setNoteFontColorVisual(noteId, fontColorKey);
|
|
|
|
// 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");
|
|
|
|
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, getFormFieldValue(input));
|
|
}
|
|
});
|
|
|
|
let currentTags = formData.get("lf_tags") || "";
|
|
let tagsArray = currentTags.split(/[\s,]+/).filter((t) => t.trim() !== "");
|
|
|
|
// Remove existing font color tags
|
|
tagsArray = tagsArray.filter((t) => !t.startsWith(NOTE_FONT_COLOR_TAG_PREFIX));
|
|
|
|
// Add new font color tag (unless auto)
|
|
if (fontColorKey !== "auto") {
|
|
if (fontColorKey.startsWith("custom:")) {
|
|
tagsArray.push(`${NOTE_FONT_COLOR_TAG_PREFIX}${fontColorKey.substring(7).replace("#", "")}`);
|
|
} else {
|
|
const option = NOTE_FONT_COLOR_OPTIONS.find((opt) => opt.key === fontColorKey);
|
|
if (option && option.value !== "auto") {
|
|
tagsArray.push(`${NOTE_FONT_COLOR_TAG_PREFIX}${option.value.substring(1)}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
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) {
|
|
console.log(`Font color ${fontColorKey} saved for note ${noteId}`);
|
|
} else {
|
|
throw new Error("Failed to save font color");
|
|
}
|
|
})
|
|
.catch((err) => {
|
|
console.error("Error saving note font color:", err);
|
|
});
|
|
}
|
|
|
|
function setModalNoteFontColor(fontColorKey) {
|
|
const modal = getOpenModalOverlay();
|
|
const entity = getModalCurrentEntity(modal);
|
|
if (!modal || !entity) return;
|
|
|
|
setNoteFontColor(entity.id, fontColorKey, entity.editUrl);
|
|
|
|
entity.fontColor = fontColorKey;
|
|
const modalCard = modal.querySelector(".note-modal");
|
|
if (modalCard) {
|
|
let colorValue = "auto";
|
|
if (String(fontColorKey || "").startsWith("custom:")) {
|
|
colorValue = String(fontColorKey).substring(7);
|
|
} else if (fontColorKey !== "auto") {
|
|
const option = NOTE_FONT_COLOR_OPTIONS.find((opt) => opt.key === fontColorKey);
|
|
colorValue = option ? option.value : "auto";
|
|
}
|
|
|
|
if (colorValue === "auto") {
|
|
modalCard.style.removeProperty("--note-card-fg");
|
|
} else {
|
|
modalCard.style.setProperty("--note-card-fg", colorValue);
|
|
}
|
|
modalCard.dataset.fontColor = fontColorKey;
|
|
}
|
|
}
|
|
|
|
function setModalCustomFontColor(color) {
|
|
setModalNoteFontColor(`custom:${color}`);
|
|
}
|
|
|
|
/* ==========================================================
|
|
CUSTOM NOTE COLOR FUNCTIONS
|
|
========================================================== */
|
|
function setModalCustomNoteColor(color) {
|
|
const modal = getOpenModalOverlay();
|
|
const entity = getModalCurrentEntity(modal);
|
|
if (!modal || !entity || !entity.editUrl) return;
|
|
|
|
// Apply custom color visually
|
|
const modalCard = modal.querySelector(".note-modal");
|
|
if (modalCard) {
|
|
modalCard.style.backgroundColor = color;
|
|
modalCard.style.borderColor = "transparent";
|
|
modalCard.dataset.color = "custom";
|
|
modalCard.dataset.customColor = color;
|
|
}
|
|
|
|
// Save via AJAX
|
|
fetch(entity.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, getFormFieldValue(input));
|
|
}
|
|
});
|
|
|
|
let currentTags = formData.get("lf_tags") || "";
|
|
let tagsArray = currentTags.split(/[\s,]+/).filter((t) => t.trim() !== "");
|
|
|
|
// Remove existing color tags (only one note-color-* allowed)
|
|
tagsArray = tagsArray.filter((t) => !t.startsWith(NOTE_COLOR_TAG_PREFIX));
|
|
|
|
// Backward compat cleanup: remove legacy note-custom-<hex>
|
|
tagsArray = tagsArray.filter((t) => !t.startsWith("note-custom-"));
|
|
|
|
// Backward compat cleanup: remove legacy note-<color>
|
|
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 custom color tag
|
|
const cleanColor = color.replace("#", "");
|
|
tagsArray.push(`${NOTE_COLOR_TAG_PREFIX}${cleanColor}`);
|
|
|
|
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 custom color");
|
|
console.log(`Custom color ${color} saved for note ${entity.id}`);
|
|
})
|
|
.catch((err) => {
|
|
console.error("Error saving custom color:", err);
|
|
});
|
|
}
|