') || textToRender.trim().startsWith('<')) {
body.innerHTML = textToRender;
} else {
body.innerHTML = renderMarkdown(textToRender);
}
}
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 = `
`;
const openEditorBtn = actions.querySelector(".note-open-editor-btn");
openEditorBtn?.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
syncNoteFromCardElement(note, card);
openNoteModal(note);
});
// 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 titleInput = modal.querySelector("#note-modal-title");
const descriptionSource = modal.querySelector("#note-modal-description-source");
const descriptionPreview = modal.querySelector("#note-modal-description-preview");
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");
const formatToggleBtn = modal.querySelector("#note-modal-format-btn");
const formattingBar = modal.querySelector("#note-modal-formatting");
const setDescriptionEditMode = (isEditing) => {
if (!descriptionSource || !descriptionPreview) return;
if (isEditing) {
modal.classList.add("note-modal-editing");
descriptionPreview.setAttribute("aria-hidden", "true");
descriptionSource.removeAttribute("aria-hidden");
} else {
modal.classList.remove("note-modal-editing");
descriptionPreview.setAttribute("aria-hidden", "false");
descriptionSource.setAttribute("aria-hidden", "true");
}
};
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("||");
if (titleInput) titleInput.value = note.title || "";
if (descriptionSource) descriptionSource.value = (note._noteMarkdown || note.descText || "").trim();
if (descriptionPreview) {
descriptionPreview.innerHTML = renderMarkdown((note._noteMarkdown || note.descText || "").trim());
}
setDescriptionEditMode(false);
renderModalTags(tagsContainer, visibleTags);
if (editLink) {
editLink.href = note.editUrl || "#";
}
if (modalColorPopup) {
modalColorPopup.innerHTML = generateModalPaletteButtons(note);
modalColorPopup.classList.remove("open");
}
setModalPinButtonState(pinButton, !!note.isPinned);
if (!modal._noteEditorState) {
modal._noteEditorState = { hasChanges: false, isSaving: false, lastSavedTitle: "", lastSavedMarkdown: "" };
}
const state = modal._noteEditorState;
state.hasChanges = false;
state.lastSavedTitle = titleInput && typeof titleInput.value === "string" ? titleInput.value.trim() : "";
state.lastSavedMarkdown = descriptionSource && typeof descriptionSource.value === "string" ? descriptionSource.value.trim() : "";
if (!modal._noteEditorBound) {
modal._noteEditorBound = true;
titleInput?.addEventListener("input", () => {
if (modal._noteEditorState) modal._noteEditorState.hasChanges = true;
});
descriptionSource?.addEventListener("input", () => {
if (modal._noteEditorState) modal._noteEditorState.hasChanges = true;
if (descriptionPreview) {
descriptionPreview.innerHTML = renderMarkdown(descriptionSource.value || "");
}
});
descriptionPreview?.addEventListener("click", () => {
setDescriptionEditMode(true);
descriptionSource?.focus({ preventScroll: true });
});
descriptionPreview?.addEventListener("keydown", (e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
setDescriptionEditMode(true);
descriptionSource?.focus({ preventScroll: true });
}
});
descriptionSource?.addEventListener("blur", () => {
setDescriptionEditMode(false);
});
const setFormattingVisible = (visible) => {
const fb = modal.querySelector("#note-modal-formatting");
if (!fb) return;
if (visible) {
fb.classList.add("open");
fb.setAttribute("aria-hidden", "false");
modal.classList.add("show-formatting");
} else {
fb.classList.remove("open");
fb.setAttribute("aria-hidden", "true");
modal.classList.remove("show-formatting");
}
};
modal._setNoteModalFormattingVisible = setFormattingVisible;
const ftb = modal.querySelector("#note-modal-format-btn");
ftb?.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
const fb = modal.querySelector("#note-modal-formatting");
const isOpen = fb && fb.classList.contains("open");
setFormattingVisible(!isOpen);
});
modal.querySelectorAll(".note-format-btn").forEach((btnEl) => {
btnEl.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
const src = modal.querySelector("#note-modal-description-source");
if (!src) return;
setDescriptionEditMode(true);
applyKeepNoteFormatting(src, btnEl.dataset.noteFormat || "");
if (modal._noteEditorState) modal._noteEditorState.hasChanges = true;
});
});
}
if (typeof modal._setNoteModalFormattingVisible === "function") {
modal._setNoteModalFormattingVisible(false);
}
hydrateNoteFromEditForm(note).then(() => {
if (modal.currentNote !== note) return;
if (titleInput && note._noteTitle) {
titleInput.value = note._noteTitle;
}
if (descriptionSource) {
descriptionSource.value = (note._noteMarkdown || "").trim();
}
if (descriptionPreview) {
descriptionPreview.innerHTML = renderMarkdown(descriptionSource ? descriptionSource.value : "");
}
setDescriptionEditMode(false);
state.lastSavedTitle = titleInput && typeof titleInput.value === "string" ? titleInput.value.trim() : "";
state.lastSavedMarkdown = descriptionSource && typeof descriptionSource.value === "string" ? descriptionSource.value.trim() : "";
});
modal.classList.add("open");
document.body.classList.add("note-modal-open");
titleInput?.focus({ preventScroll: true });
}
async function saveOpenNoteEditorModal(modal) {
if (!modal || !modal.currentNote) return;
const note = modal.currentNote;
const titleInput = modal.querySelector("#note-modal-title");
const descriptionSource = modal.querySelector("#note-modal-description-source");
const title = titleInput && typeof titleInput.value === "string" ? titleInput.value.trim() : "";
const markdown = descriptionSource && typeof descriptionSource.value === "string" ? descriptionSource.value.trim() : "";
const state = modal._noteEditorState || { hasChanges: false, isSaving: false, lastSavedTitle: "", lastSavedMarkdown: "" };
if (!title && !markdown) {
return;
}
const changed =
state.hasChanges ||
title !== (state.lastSavedTitle || "") ||
markdown !== (state.lastSavedMarkdown || "");
if (!changed || state.isSaving) return;
state.isSaving = true;
modal._noteEditorState = state;
await hydrateNoteFromEditForm(note);
await persistNoteChanges(note, { title, markdown });
note.title = title;
note._noteTitle = title;
note._noteMarkdown = markdown;
note.descText = markdown;
const card = document.querySelector(`.note-card[data-id="${CSS.escape(String(note.id))}"]`);
if (card) {
const titleEl = card.querySelector(".note-inner .note-title");
if (titleEl) titleEl.textContent = title;
const bodyEl = card.querySelector(".note-inner .note-body");
if (bodyEl) {
bodyEl.innerHTML = renderMarkdown(markdown);
note.descHtml = bodyEl.innerHTML;
}
}
state.lastSavedTitle = title;
state.lastSavedMarkdown = markdown;
state.hasChanges = false;
state.isSaving = false;
}
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-
tagsArray = tagsArray.filter((t) => !t.startsWith("note-custom-"));
// Backward compat cleanup: remove legacy note-
tagsArray = tagsArray.filter((t) => {
if (!t.startsWith("note-")) return true;
const colorKey = t.substring(5);
return !NOTE_COLOR_OPTIONS.some((opt) => opt.key === colorKey);
});
// Add new color tag (unless default)
if (color !== "default") {
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: shaarli-pin
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";
const mode = panel.dataset.mode || "entity";
const entityId = panel.dataset.entityId || "";
if (!skipRestore && mode === "draft") {
panel.dataset.skipRestore = "0";
panel.classList.remove("open");
panel.style.display = "none";
panel.setAttribute("aria-hidden", "true");
return;
}
if (!skipRestore && type === "font") {
const prevFontColorKey = panel.dataset.prevFontColorKey || "auto";
if (mode === "modal") {
setModalNoteFontColorVisual(prevFontColorKey);
} else {
setNoteFontColorVisual(entityId, prevFontColorKey);
}
}
if (!skipRestore && type === "background") {
const prevColorKey = panel.dataset.prevColorKey || "default";
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") === "draft") {
const bgPanel = document.getElementById("shaarli-bg-studio");
prevFontColorKey = bgPanel ? (bgPanel.dataset.fontColor || "auto") : "auto";
} else
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") === "draft") {
const bgPanel = document.getElementById("shaarli-bg-studio");
if (bgPanel) {
prevColorKey = bgPanel.dataset.color || "default";
}
} else
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 = `
`;
// 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 === "draft") {
const bgPanel = document.getElementById("shaarli-bg-studio");
if (bgPanel && typeof bgPanel.__draftApply === "function") {
bgPanel.__draftApply({ fontColor: previewKey });
}
} else 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 === "draft") {
const bgPanel = document.getElementById("shaarli-bg-studio");
if (bgPanel && typeof bgPanel.__draftApply === "function") {
bgPanel.__draftApply({ color: previewKey });
}
} else 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 (mode === "draft") {
const bgPanel = document.getElementById("shaarli-bg-studio");
if (bgPanel && typeof bgPanel.__draftApply === "function") {
if (type === "font") {
bgPanel.dataset.fontColor = `custom:${color}`;
bgPanel.__draftApply({ fontColor: `custom:${color}` });
} else {
bgPanel.dataset.color = `custom:${color}`;
bgPanel.__draftApply({ color: `custom:${color}` });
}
renderBackgroundStudioPanel(bgPanel);
}
panel.dataset.skipRestore = "1";
closeColorPickerPanel();
return;
}
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);
Array.from(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-
tagsArray = tagsArray.filter((t) => !t.startsWith("note-custom-"));
// Backward compat cleanup: remove legacy note-
tagsArray = tagsArray.filter((t) => {
if (!t.startsWith("note-")) return true;
const colorKey = t.substring(5);
return !NOTE_COLOR_OPTIONS.some((opt) => opt.key === colorKey);
});
// Add 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);
});
}