901 lines
32 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");
// Always init Pinned Items logic (sorting and listeners)
// This function is defined at the end of the file
if (typeof initPinnedItems === "function") {
initPinnedItems();
}
if (!linkList || !container) return;
if (searchTags === "todo") {
initTodoView(linkList, container);
} else if (searchTags === "note") {
initNoteView(linkList, container);
}
});
/**
* Initialize the Google Tasks-like view
*/
function initTodoView(linkList, container) {
document.body.classList.add("view-todo");
// Extract task data from existing DOM
const rawLinks = Array.from(linkList.querySelectorAll(".link-outer"));
const tasks = rawLinks.map((link) => parseTaskFromLink(link)).filter((t) => t !== null);
// Create new Layout
const wrapper = document.createElement("div");
wrapper.className = "special-view-wrapper";
// 1. Sidebar
const sidebar = document.createElement("div");
sidebar.className = "todo-sidebar";
// Extract unique groups for the sidebar
const groups = new Set();
tasks.forEach((t) => {
if (t.group) groups.add(t.group);
});
const groupsList = Array.from(groups)
.map((g) => `<div class="todo-list-item" onclick="filterTasksByGroup('${g}')"><i class="mdi mdi-label-outline"></i> ${g}</div>`)
.join("");
sidebar.innerHTML = `
<div class="sidebar-section-title" style="padding: 1rem 1rem 0.5rem; font-weight:bold; color:var(--text-light);">TÂCHES</div>
<div class="todo-list-item active" onclick="filterTasksByGroup('all')">
<i class="mdi mdi-inbox" aria-hidden="true"></i> Mes tâches
<span style="margin-left:auto; font-size:0.8rem; background:rgba(0,0,0,0.1); padding:2px 6px; border-radius:10px;" aria-label="${tasks.length} tâches">${tasks.length}</span>
</div>
${groups.size > 0 ? `<div class="sidebar-section-title" style="padding: 1rem 1rem 0.5rem; font-weight:bold; color:var(--text-light);">LISTES</div>${groupsList}` : ""}
`;
// 2. Main Content
const main = document.createElement("div");
main.className = "todo-main";
const mainHeader = document.createElement("div");
mainHeader.className = "todo-main-header";
mainHeader.innerHTML = `
<h2 id="todo-list-title">Mes tâches</h2>
<div class="todo-actions">
<!-- Sorting/Menu could go here -->
</div>
`;
const itemsContainer = document.createElement("div");
itemsContainer.className = "todo-items-container";
// Sort Tasks: Pinned items first
tasks.sort((a, b) => {
const aPinned = a.tags && a.tags.includes("shaarli-pin");
const bPinned = b.tags && b.tags.includes("shaarli-pin");
return bPinned - aPinned;
});
// Render Tasks
tasks.forEach((task) => {
itemsContainer.appendChild(renderTaskItem(task));
});
main.appendChild(mainHeader);
main.appendChild(itemsContainer);
wrapper.appendChild(sidebar);
wrapper.appendChild(main);
// Inject and Hide original
linkList.style.display = "none";
// Remove pagination/toolbar if present to clean up view
const toolbar = document.querySelector(".content-toolbar");
if (toolbar) toolbar.style.display = "none";
if (linkList.parentNode) {
linkList.parentNode.insertBefore(wrapper, linkList);
} else {
container.appendChild(wrapper);
}
// Global filter function
window.filterTasksByGroup = function (group) {
const title = document.getElementById("todo-list-title");
const items = document.querySelectorAll(".todo-item");
// Update Sidebar Active State
document.querySelectorAll(".todo-list-item").forEach((el) => el.classList.remove("active"));
if (event && event.currentTarget) event.currentTarget.classList.add("active");
if (group === "all") {
title.textContent = "Mes tâches";
items.forEach((item) => (item.style.display = "flex"));
} else {
title.textContent = group;
items.forEach((item) => {
if (item.dataset.group === group) item.style.display = "flex";
else item.style.display = "none";
});
}
};
}
function parseTaskFromLink(linkEl) {
const id = linkEl.dataset.id;
const titleEl = linkEl.querySelector(".link-title"); // "Title" normally
// For Todos, the Bookmark Title is the Task Title?
// Or is the title inside the description?
// Mental model says: "Todo: reste un bookmark privé... LinkEntity.title" is used.
const title = titleEl ? titleEl.textContent.trim() : "Task";
const descEl = linkEl.querySelector(".link-description");
const rawDesc = descEl ? descEl.innerHTML : "";
const textDesc = descEl ? descEl.textContent : "";
// Check if it's really a todo (should be if we are in ?searchtags=todo, but double check)
// We assume yes.
// Parse Metadata from Description text
// Format: 📅 **Échéance :** <Instant ISO>
// Format: 🏷️ **Groupe :** <nom>
// Format: - [ ] Subtask or Main task status?
// Status
// If [x] is found in the first few lines, maybe completed?
// User says: "Puis une checkbox markdown: - [ ] Titre"
// Wait, if the Description contains the checkbox and title, then Bookmark Title is ignored?
// Let's assume Bookmark Title is the master Display Title.
// And "Checkbox status" can be parsed from description.
let isCompleted = false;
if (textDesc.includes("[x]")) isCompleted = true;
// Due Date
let dueDate = null;
const dateMatch = textDesc.match(/Échéance\s*:\s*\*+([^*]+)\*+/); // varies by markdown regex
// Text might be "📅 **Échéance :** 2023-10-10"
// Regex: Échéance\s*:\s*(.*?)(\n|$)
const dueMatch = textDesc.match(/Échéance\s*[:]\s*(.*?)(\n|$)/);
if (dueMatch) dueDate = dueMatch[1].trim();
// Group
let group = null;
const groupMatch = textDesc.match(/Groupe\s*[:]\s*(.*?)(\n|$)/);
if (groupMatch) group = groupMatch[1].trim();
return {
id,
title,
isCompleted,
dueDate,
group,
originalUrl: linkEl.querySelector(".link-url") ? linkEl.querySelector(".link-url").textContent : "",
editUrl: linkEl.querySelector('a[href*="admin/shaare/"]') ? linkEl.querySelector('a[href*="admin/shaare/"]').href : "#",
};
}
function renderTaskItem(task) {
const el = document.createElement("div");
el.className = `todo-item ${task.isCompleted ? "completed" : ""}`;
el.dataset.group = task.group || "";
/*
We cannot easily toggle state (AJAX) without a backend API that supports partial updates efficiently.
But we can provide a link to Edit.
Or simulate it.
*/
el.innerHTML = `
<div class="todo-checkbox ${task.isCompleted ? "checked" : ""}">
${task.isCompleted ? '<i class="mdi mdi-check"></i>' : ""}
</div>
<div class="todo-content">
<div class="todo-title">${task.title}</div>
<div class="todo-meta">
${task.group ? `<span class="todo-badge">${task.group}</span>` : ""}
${task.dueDate ? `<span class="due-date ${isOverdue(task.dueDate) ? "overdue" : ""}"><i class="mdi mdi-calendar"></i> ${formatDate(task.dueDate)}</span>` : ""}
<a href="${task.editUrl}" title="Edit Task" style="margin-left:auto;"><i class="mdi mdi-pencil" style="font-size:14px;"></i></a>
</div>
</div>
`;
// Simple click handler to open edit (since we can't sync state easily)
el.querySelector(".todo-checkbox").addEventListener("click", (e) => {
e.stopPropagation();
// Open edit page or toggle visually
// window.location.href = task.editUrl;
});
return el;
}
function isOverdue(dateStr) {
try {
return new Date(dateStr) < new Date();
} catch (e) {
return false;
}
}
function formatDate(dateStr) {
try {
const d = new Date(dateStr);
return d.toLocaleDateString();
} catch (e) {
return dateStr;
}
}
/**
* Initialize the Google Keep-like view
*/
/**
* Initialize the Google Keep-like view
*/
function initNoteView(linkList, container) {
document.body.classList.add("view-notes");
// Hide standard toolbar
const toolbar = document.querySelector(".content-toolbar");
if (toolbar) toolbar.style.display = "none";
// 1. Create Layout Wrapper
const wrapper = document.createElement("div");
wrapper.className = "notes-wrapper";
// 2. Create Search/Input Area (Top)
const topBar = document.createElement("div");
topBar.className = "notes-top-bar";
// Custom Input "Take a note..."
const inputContainer = document.createElement("div");
inputContainer.className = "note-input-container";
inputContainer.innerHTML = `
<div class="note-input-collapsed" onclick="window.location.href='?do=addlink&tags=note'">
<span class="note-input-placeholder">Créer une note...</span>
<div class="note-input-actions">
<button title="Nouvelle liste" onclick="event.stopPropagation(); window.location.href='?do=addlink&tags=note&description=- [ ] '"><i class="mdi mdi-checkbox-marked-outline"></i></button>
<button title="Nouveau dessin" onclick="event.stopPropagation(); window.location.href='?do=addlink&tags=note'"><i class="mdi mdi-brush"></i></button>
<button title="Nouvelle image" onclick="event.stopPropagation(); window.location.href='?do=addlink&tags=note'"><i class="mdi mdi-image"></i></button>
</div>
</div>
`;
topBar.appendChild(inputContainer);
// View Toggle and other tools
const tools = document.createElement("div");
tools.className = "notes-tools";
tools.innerHTML = `
<button class="icon-btn active" id="btn-view-grid" title="Vue grille"><i class="mdi mdi-view-dashboard-outline"></i></button>
<button class="icon-btn" id="btn-view-list" title="Vue liste"><i class="mdi mdi-view-agenda-outline"></i></button>
`;
topBar.appendChild(tools);
wrapper.appendChild(topBar);
// 3. Content Area
const contentArea = document.createElement("div");
contentArea.className = "notes-content-area";
const links = Array.from(linkList.querySelectorAll(".link-outer"));
const notes = links.map((link) => parseNoteFromLink(link));
// Initial Render (Grid)
renderNotes(contentArea, notes, "grid");
wrapper.appendChild(contentArea);
// Replace original list
linkList.style.display = "none";
if (linkList.parentNode) {
linkList.parentNode.insertBefore(wrapper, linkList);
} else {
container.appendChild(wrapper);
}
// Modal Container
const modalOverlay = document.createElement("div");
modalOverlay.className = "note-modal-overlay";
modalOverlay.innerHTML = `
<div class="note-modal">
<div class="note-modal-content"></div>
<div class="note-modal-actions">
<button class="btn btn-primary" id="note-modal-close">Fermer</button>
</div>
</div>
`;
document.body.appendChild(modalOverlay);
// Event Listeners for Toggles
const btnGrid = wrapper.querySelector("#btn-view-grid");
const btnList = wrapper.querySelector("#btn-view-list");
btnGrid.addEventListener("click", () => {
btnGrid.classList.add("active");
btnList.classList.remove("active");
renderNotes(contentArea, notes, "grid");
});
btnList.addEventListener("click", () => {
btnList.classList.add("active");
btnGrid.classList.remove("active");
renderNotes(contentArea, notes, "list");
});
// Close Modal
modalOverlay.querySelector("#note-modal-close").addEventListener("click", () => {
modalOverlay.classList.remove("open");
});
modalOverlay.addEventListener("click", (e) => {
if (e.target === modalOverlay) modalOverlay.classList.remove("open");
});
}
function parseNoteFromLink(linkEl) {
const id = linkEl.dataset.id;
const titleEl = linkEl.querySelector(".link-title");
const title = titleEl ? titleEl.textContent.trim() : "";
const descEl = linkEl.querySelector(".link-description");
const descHtml = descEl ? descEl.innerHTML : "";
const descText = descEl ? descEl.textContent : "";
// Extract Image from Description (First image as cover)
let coverImage = null;
if (descEl) {
const img = descEl.querySelector("img");
if (img) {
coverImage = img.src;
// Optionally remove img from body text if it's purely a cover
// But usually we keep it or hide it via CSS if we construct a custom card
}
}
const urlEl = linkEl.querySelector(".link-url");
const url = urlEl ? urlEl.textContent.trim() : "";
const tags = [];
let color = "default";
linkEl.querySelectorAll(".link-tag-list a").forEach((tag) => {
const t = tag.textContent.trim();
// Check for color tag
if (t.startsWith("note-")) {
const potentialColor = t.substring(5);
color = potentialColor;
} else {
tags.push(t);
}
});
const isPinnedByTag = tags.includes("shaarli-pin");
const actionsEl = linkEl.querySelector(".link-actions");
const editUrl = actionsEl && actionsEl.querySelector('a[href*="admin/shaare"]') ? actionsEl.querySelector('a[href*="admin/shaare"]').href : "#";
const deleteUrl = actionsEl && actionsEl.querySelector('a[href*="delete"]') ? actionsEl.querySelector('a[href*="delete"]').href : "#";
const pinUrl = actionsEl && actionsEl.querySelector('a[href*="pin"]') ? actionsEl.querySelector('a[href*="pin"]').href : "#";
// User requested "availability of the tag 'shaarli-pin' as the main source"
const isPinned = tags.includes("shaarli-pin");
return { id, title, descHtml, descText, coverImage, url, tags, color, editUrl, deleteUrl, pinUrl, isPinned };
}
function renderNotes(container, notes, viewMode) {
container.innerHTML = "";
container.className = viewMode === "grid" ? "notes-masonry" : "notes-list-view";
// Sort: Pinned items first
notes.sort((a, b) => {
const aPinned = a.tags.includes("shaarli-pin");
const bPinned = b.tags.includes("shaarli-pin");
return bPinned - aPinned;
});
notes.forEach((note) => {
const card = document.createElement("div");
card.className = `note-card note-color-${note.color}`;
card.dataset.id = note.id;
if (viewMode === "list") card.classList.add("list-mode");
// Main Click to Open Modal
card.addEventListener("click", (e) => {
// Prevent if clicking buttons
if (e.target.closest("button") || e.target.closest("a") || e.target.closest(".note-hover-actions")) return;
openNoteModal(note);
});
// Cover Image
if (note.coverImage && viewMode === "grid") {
// Show cover mainly in grid
const imgContainer = document.createElement("div");
imgContainer.className = "note-cover";
imgContainer.innerHTML = `<img src="${note.coverImage}" alt="Cover">`;
card.appendChild(imgContainer);
}
// Inner Content
const inner = document.createElement("div");
inner.className = "note-inner";
// Title
if (note.title) {
const h3 = document.createElement("h3");
h3.className = "note-title";
h3.textContent = note.title;
inner.appendChild(h3);
}
// Body (truncated in grid, maybe?)
if (note.descHtml) {
const body = document.createElement("div");
body.className = "note-body";
// Start simple: use innerHTML but maybe strip big images if we used cover?
// For now, let's just dump it and style images to fit or hide if first child
body.innerHTML = note.descHtml;
inner.appendChild(body);
}
// Tags (Labels)
if (note.tags.length > 0) {
const tagContainer = document.createElement("div");
tagContainer.className = "note-tags";
note.tags.forEach((t) => {
if (t !== "note") {
const span = document.createElement("span");
span.className = "note-tag";
span.textContent = t;
tagContainer.appendChild(span);
}
});
inner.appendChild(tagContainer);
}
// Hover Actions (Keep style: at bottom, visible on hover)
const actions = document.createElement("div");
actions.className = "note-hover-actions";
// Palette Button Logic
const paletteBtnId = `palette-${note.id}`;
actions.innerHTML = `
<button title="Rappel"><i class="mdi mdi-bell-outline"></i></button>
<button title="Collaborateur"><i class="mdi mdi-account-plus-outline"></i></button>
<div style="position:relative;">
<button title="Couleur" id="${paletteBtnId}"><i class="mdi mdi-palette-outline"></i></button>
<div class="palette-popup" id="popup-${paletteBtnId}">
${generatePaletteButtons(note)}
</div>
</div>
<button title="Image"><i class="mdi mdi-image-outline"></i></button>
<button title="Archiver"><i class="mdi mdi-archive-arrow-down-outline"></i></button>
<div class="spacer"></div>
<!-- Real Actions -->
<a href="${note.pinUrl}" title="${note.isPinned ? "Unpin" : "Pin"}" class="${note.isPinned ? "active" : ""}"><i class="mdi mdi-pin${note.isPinned ? "" : "-outline"}"></i></a>
<a href="${note.editUrl}" title="Edit"><i class="mdi mdi-pencil-outline"></i></a>
<button title="Plus" onclick="window.location.href='${note.deleteUrl}'"><i class="mdi mdi-dots-vertical"></i></button>
`;
// Palette Toggle
const paletteBtn = actions.querySelector(`#${paletteBtnId}`);
const palettePopup = actions.querySelector(`#popup-${paletteBtnId}`);
paletteBtn.addEventListener("click", (e) => {
e.stopPropagation();
// Close others?
document.querySelectorAll(".palette-popup.open").forEach((p) => {
if (p !== palettePopup) p.classList.remove("open");
});
palettePopup.classList.toggle("open");
});
// Close palette when clicking outside
// (Handled globally or card based? simple: card mouseleave?)
card.addEventListener("mouseleave", () => {
palettePopup.classList.remove("open");
});
inner.appendChild(actions);
card.appendChild(inner);
container.appendChild(card);
});
}
function openNoteModal(note) {
const modal = document.querySelector(".note-modal-overlay");
const content = modal.querySelector(".note-modal-content");
// Build full content
let html = `
<h2 class="note-title" style="margin-top:0;">${note.title}</h2>
<div class="note-body">${note.descHtml}</div>
`;
// Add images if not in desc? (desc usually has it)
content.innerHTML = html;
modal.classList.add("open");
}
function generatePaletteButtons(note) {
const colors = ["default", "red", "orange", "yellow", "green", "teal", "blue", "darkblue", "purple", "pink", "brown", "grey"];
// Map to hex for the button background
const colorMap = {
default: "#ffffff",
red: "#f28b82",
orange: "#fbbc04",
yellow: "#fff475",
green: "#ccff90",
teal: "#a7ffeb",
blue: "#cbf0f8",
darkblue: "#aecbfa",
purple: "#d7aefb",
pink: "#fdcfe8",
brown: "#e6c9a8",
grey: "#e8eaed",
};
// Dark mode mapping could be handled via CSS classes on buttons but inline styles are easier for the picker circles
// We will just use class names and let CSS handle the preview color if possible, or set style.
return colors
.map((c) => {
// We use style for the button background roughly.
// Actually, let's use the class on the button itself to pick up the color from CSS variables if defined,
// OR just hardcode the light mode preview for simplicity as the picker is usually on white/dark background.
return `<button class="palette-btn note-color-${c}" title="${c}" onclick="setNoteColor('${note.id}', '${c}', '${note.editUrl}')" style="background-color:${colorMap[c]}"></button>`;
})
.join("");
}
window.setNoteColor = function (noteId, color, editUrl) {
// 1. Visual Update (Immediate feedback)
const card = document.querySelector(`.note-card[data-id="${noteId}"]`);
if (card) {
// Remove all color classes
card.classList.forEach((cls) => {
if (cls.startsWith("note-color-")) card.classList.remove(cls);
});
card.classList.add(`note-color-${color}`);
}
// 2. Persistence via AJAX Form Submission
fetch(editUrl)
.then((response) => response.text())
.then((html) => {
const parser = new DOMParser();
const doc = parser.parseFromString(html, "text/html");
const form = doc.querySelector('form[name="linkform"]');
if (!form) throw new Error("Could not find edit form");
// Extract all necessary fields
const formData = new URLSearchParams();
const inputs = form.querySelectorAll("input, textarea");
inputs.forEach((input) => {
if (input.type === "checkbox") {
if (input.checked) formData.append(input.name, input.value || "on");
} else if (input.name) {
formData.append(input.name, input.value);
}
});
// Update Tags
let currentTags = formData.get("lf_tags") || "";
let tagsArray = currentTags.split(/[\s,]+/).filter((t) => t.trim() !== "");
// Remove existing color tags
tagsArray = tagsArray.filter((t) => !t.startsWith("note-"));
// Add new color tag (unless default)
if (color !== "default") {
tagsArray.push(`note-${color}`);
}
formData.set("lf_tags", tagsArray.join(" "));
formData.append("save_edit", "1"); // Trigger save action
// POST back to Shaarli
return fetch(form.action, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: formData.toString(),
});
})
.then((response) => {
if (response.ok) {
console.log(`Color ${color} saved for note ${noteId}`);
} else {
throw new Error("Failed to save color");
}
})
.catch((err) => {
console.error("Error saving note color:", err);
alert("Erreur lors de la sauvegarde de la couleur. Veuillez rafraîchir la page.");
});
};
/* ==========================================================
PINNED ITEMS LOGIC (Tag: shaarli-pin)
========================================================== */
function initPinnedItems() {
const container = document.querySelector(".links-list, .notes-masonry, .notes-list-view");
if (!container) return; // Exit if no container found (e.g. empty page or other view)
const items = Array.from(container.children);
const pinnedItems = [];
items.forEach((item) => {
// Support both Standard Link items and Note items
if (item.classList.contains("link-outer") || item.classList.contains("note-card")) {
let isPinned = false;
// 1. Check for Tag 'shaarli-pin'
// In note-card, we might need to check dataset or re-parse tags if not visible?
// But usually renderNotes puts tags in DOM or we rely on data attribute if we saved it?
// Let's rely on finding the tag text in the DOM for consistency with Standard View.
// For Note View, tags are in .note-tags > .note-tag
// For Standard View, tags are in .link-tag-list > a
const itemHtml = item.innerHTML; // Simple search in content (quick & dirty but effective for tag presence)
// Better: Select text content of tags specifically to avoid false positives in description
const tagElements = item.querySelectorAll(".link-tag-list a, .note-tag");
for (let t of tagElements) {
if (t.textContent.trim() === "shaarli-pin") {
isPinned = true;
break;
}
}
// 2. Enforce Visual State based on Tag Presence
const pinBtnIcon = item.querySelector(".mdi-pin-outline, .mdi-pin");
const titleArea = item.querySelector(".link-title, .note-title");
const titleIcon = titleArea ? titleArea.querySelector("i.mdi-pin") : null;
if (isPinned) {
// It IS Pinned: Ensure UI reflects this
pinnedItems.push(item);
item.classList.add("is-pinned-tag");
// Button -> Filled Pin
if (pinBtnIcon) {
pinBtnIcon.classList.remove("mdi-pin-outline");
pinBtnIcon.classList.add("mdi-pin");
if (pinBtnIcon.parentElement) pinBtnIcon.parentElement.classList.add("active");
}
// Title -> Add Icon if missing
if (titleArea && !titleIcon) {
const newIcon = document.createElement("i");
newIcon.className = "mdi mdi-pin";
newIcon.style.color = "var(--primary)";
newIcon.style.marginRight = "8px";
titleArea.prepend(newIcon);
}
} else {
// It is NOT Pinned: Ensure UI reflects this (Clean up native sticky or mismatches)
item.classList.remove("is-pinned-tag");
// Button -> Outline Pin
if (pinBtnIcon) {
pinBtnIcon.classList.remove("mdi-pin");
pinBtnIcon.classList.add("mdi-pin-outline");
if (pinBtnIcon.parentElement) pinBtnIcon.parentElement.classList.remove("active");
}
// Title -> Remove Icon if present
if (titleIcon) {
titleIcon.remove();
}
}
}
});
// 3. Move Pinned Items to Top (Only for standard list, renderNotes already sorts itself)
if (container.classList.contains("links-list")) {
for (let i = pinnedItems.length - 1; i >= 0; i--) {
container.prepend(pinnedItems[i]);
}
}
// 4. Click Listener for all Pin Buttons (Event Delegation)
// Avoid adding multiple listeners if init called multiple times?
// We'll rely on one global listener on document, but here we add it inside this function which is called once on load.
// To be safe, let's remove old one if we could, but anonymous function makes it hard.
// Better: Allow this to run, but ensure we don't duplicate.
// Since initPinnedItems is called on DOMContentLoaded, it runs once.
// Note: We already have the listener attached in previous version.
// We will just keep the listener logic here in the replacement.
document.addEventListener(
"click",
function (e) {
const btn = e.target.closest('a[href*="do=pin"], .note-hover-actions a[href*="pin"], .link-actions a[href*="pin"]');
if (btn) {
e.preventDefault();
e.stopPropagation();
const card = btn.closest(".link-outer, .note-card");
const id = card ? card.dataset.id : null;
// Re-derive edit URL if needed
let editUrl = btn.href.replace("do=pin", "do=editlink").replace("pin", "editlink");
if (card) {
const editBtn = card.querySelector('a[href*="edit_link"], a[href*="admin/shaare"]');
if (editBtn) editUrl = editBtn.href;
}
if (id && editUrl) {
togglePinTag(id, editUrl, btn);
}
}
},
{ once: false },
); // Listener is permanent
}
function togglePinTag(id, editUrl, btn) {
const icon = btn.querySelector("i");
let isPinning = false;
if (icon) {
if (icon.classList.contains("mdi-pin-outline")) {
icon.classList.remove("mdi-pin-outline");
icon.classList.add("mdi-pin");
btn.classList.add("active");
isPinning = true;
} else {
icon.classList.remove("mdi-pin");
icon.classList.add("mdi-pin-outline");
btn.classList.remove("active");
isPinning = false;
}
}
// Update Title Icon (The one "devant le titre")
const card = btn.closest(".link-outer, .note-card");
if (card) {
// Update Title Icon
const titleArea = card.querySelector(".link-title, .note-title");
if (titleArea) {
let titleIcon = titleArea.querySelector("i.mdi-pin");
if (isPinning) {
if (!titleIcon) {
const newIcon = document.createElement("i");
newIcon.className = "mdi mdi-pin";
newIcon.style.color = "var(--primary)";
newIcon.style.marginRight = "8px";
titleArea.prepend(newIcon);
}
} else {
if (titleIcon) titleIcon.remove();
}
}
// Update Tag List Visualization
// We need to handle both Note Cards (.note-tags) and Standard Links (.link-tag-list)
let tagContainer = card.querySelector(".note-tags");
if (!tagContainer) tagContainer = card.querySelector(".link-tag-list");
if (tagContainer) {
// Check if tag exists already
let existingTagElement = null;
// Search in children
// Notes: .note-tag
// Links: .label-tag > a OR just a
const allCandidates = tagContainer.querySelectorAll("*");
for (let el of allCandidates) {
if (el.textContent.trim() === "shaarli-pin") {
// We found the text.
// If note, it's the span.note-tag
if (el.classList.contains("note-tag")) {
existingTagElement = el;
break;
}
// If link, we want the anchor or its wrapper
if (el.tagName === "A") {
// Check if wrapped in .label-tag
if (el.parentElement.classList.contains("label-tag")) {
existingTagElement = el.parentElement;
} else {
existingTagElement = el;
}
break;
}
}
}
if (isPinning) {
if (!existingTagElement) {
if (card.classList.contains("note-card")) {
// Add Note Tag
const span = document.createElement("span");
span.className = "note-tag";
span.textContent = "shaarli-pin";
tagContainer.appendChild(span);
} else {
// Add Link Tag (Standard View)
// Structure: <span class="link-tag"><a ...>shaarli-pin</a></span>
const wrapper = document.createElement("span");
wrapper.className = "link-tag";
const link = document.createElement("a");
link.href = "?searchtags=shaarli-pin";
link.textContent = "shaarli-pin";
wrapper.appendChild(link);
// Append space first for separation if there are other tags
if (tagContainer.children.length > 0) {
tagContainer.appendChild(document.createTextNode(" "));
}
tagContainer.appendChild(wrapper);
}
}
} else {
if (existingTagElement) {
// Remove the element
const prev = existingTagElement.previousSibling;
existingTagElement.remove();
// Clean up trailing space/text if it was the last one or between tags
if (prev && prev.nodeType === Node.TEXT_NODE && !prev.textContent.trim()) {
prev.remove();
}
}
}
}
}
fetch(editUrl)
.then((response) => response.text())
.then((html) => {
const parser = new DOMParser();
const doc = parser.parseFromString(html, "text/html");
const form = doc.querySelector('form[name="linkform"]');
if (!form) throw new Error("Could not find edit form");
const formData = new URLSearchParams();
const inputs = form.querySelectorAll("input, textarea");
inputs.forEach((input) => {
if (input.type === "checkbox") {
if (input.checked) formData.append(input.name, input.value || "on");
} else if (input.name) {
formData.append(input.name, input.value);
}
});
let currentTags = formData.get("lf_tags") || "";
let tagsArray = currentTags.split(/[\s,]+/).filter((t) => t.trim() !== "");
const pinTag = "shaarli-pin";
if (isPinning) {
if (!tagsArray.includes(pinTag)) tagsArray.push(pinTag);
} else {
tagsArray = tagsArray.filter((t) => t !== pinTag);
}
formData.set("lf_tags", tagsArray.join(" "));
formData.append("save_edit", "1");
return fetch(form.action, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: formData.toString(),
});
})
.then((res) => {
if (res.ok) console.log("Pin toggled successfully");
})
.catch((err) => console.error(err));
}