- ${task.isCompleted ? '
' : ''}
+ el.innerHTML = `
+
+ ${task.isCompleted ? '' : ""}
`;
- // 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;
- });
+ // 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;
+ return el;
}
function isOverdue(dateStr) {
- try {
- return new Date(dateStr) < new Date();
- } catch (e) { return false; }
+ 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; }
+ try {
+ const d = new Date(dateStr);
+ return d.toLocaleDateString();
+ } catch (e) {
+ return dateStr;
+ }
}
/**
@@ -238,24 +241,24 @@ function formatDate(dateStr) {
* Initialize the Google Keep-like view
*/
function initNoteView(linkList, container) {
- document.body.classList.add('view-notes');
+ document.body.classList.add("view-notes");
- // Hide standard toolbar
- const toolbar = document.querySelector('.content-toolbar');
- if (toolbar) toolbar.style.display = 'none';
+ // 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';
+ // 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';
+ // 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 = `
+ // Custom Input "Take a note..."
+ const inputContainer = document.createElement("div");
+ inputContainer.className = "note-input-container";
+ inputContainer.innerHTML = `
`;
- topBar.appendChild(inputContainer);
+ topBar.appendChild(inputContainer);
- // View Toggle and other tools
- const tools = document.createElement('div');
- tools.className = 'notes-tools';
- tools.innerHTML = `
+ // View Toggle and other tools
+ const tools = document.createElement("div");
+ tools.className = "notes-tools";
+ tools.innerHTML = `
`;
- topBar.appendChild(tools);
+ topBar.appendChild(tools);
- wrapper.appendChild(topBar);
+ wrapper.appendChild(topBar);
- // 3. Content Area
- const contentArea = document.createElement('div');
- contentArea.className = 'notes-content-area';
+ // 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));
+ const links = Array.from(linkList.querySelectorAll(".link-outer"));
+ const notes = links.map((link) => parseNoteFromLink(link));
- // Initial Render (Grid)
- renderNotes(contentArea, notes, 'grid');
+ // Initial Render (Grid)
+ renderNotes(contentArea, notes, "grid");
- wrapper.appendChild(contentArea);
+ wrapper.appendChild(contentArea);
- // Replace original list
- linkList.style.display = 'none';
- if (linkList.parentNode) {
- linkList.parentNode.insertBefore(wrapper, linkList);
- } else {
- container.appendChild(wrapper);
- }
+ // 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 = `
+ // Modal Container
+ const modalOverlay = document.createElement("div");
+ modalOverlay.className = "note-modal-overlay";
+ modalOverlay.innerHTML = `
@@ -309,159 +312,160 @@ function initNoteView(linkList, container) {
`;
- document.body.appendChild(modalOverlay);
+ document.body.appendChild(modalOverlay);
- // Event Listeners for Toggles
- const btnGrid = wrapper.querySelector('#btn-view-grid');
- const btnList = wrapper.querySelector('#btn-view-list');
+ // 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');
- });
+ 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');
- });
+ 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');
- });
+ // 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 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 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
- }
+ // 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 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 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 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 : '#';
+ 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');
+ // User requested "availability of the tag 'shaarli-pin' as the main source"
+ const isPinned = tags.includes("shaarli-pin");
- return { id, title, descHtml, descText, coverImage, url, tags, color, editUrl, deleteUrl, pinUrl, isPinned };
+ return { id, title, descHtml, descText, coverImage, url, tags, color, editUrl, deleteUrl, pinUrl, isPinned };
}
function renderNotes(container, notes, viewMode) {
- container.innerHTML = '';
- container.className = viewMode === 'grid' ? 'notes-masonry' : 'notes-list-view';
+ 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;
+ // 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);
});
- 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');
+ // Cover Image
+ if (note.coverImage && viewMode === "grid") {
+ // Show cover mainly in grid
+ const imgContainer = document.createElement("div");
+ imgContainer.className = "note-cover";
+ imgContainer.innerHTML = `

`;
+ card.appendChild(imgContainer);
+ }
- // 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);
- });
+ // Inner Content
+ const inner = document.createElement("div");
+ inner.className = "note-inner";
- // Cover Image
- if (note.coverImage && viewMode === 'grid') { // Show cover mainly in grid
- const imgContainer = document.createElement('div');
- imgContainer.className = 'note-cover';
- imgContainer.innerHTML = `

`;
- card.appendChild(imgContainer);
+ // 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);
+ }
- // Inner Content
- const inner = document.createElement('div');
- inner.className = 'note-inner';
+ // Hover Actions (Keep style: at bottom, visible on hover)
+ const actions = document.createElement("div");
+ actions.className = "note-hover-actions";
- // Title
- if (note.title) {
- const h3 = document.createElement('h3');
- h3.className = 'note-title';
- h3.textContent = note.title;
- inner.appendChild(h3);
- }
+ // Palette Button Logic
+ const paletteBtnId = `palette-${note.id}`;
- // 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 = `
+ actions.innerHTML = `
@@ -474,411 +478,423 @@ function renderNotes(container, notes, viewMode) {
-
+
`;
- // Palette Toggle
- const paletteBtn = actions.querySelector(`#${paletteBtnId}`);
- const palettePopup = actions.querySelector(`#popup-${paletteBtnId}`);
+ // 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);
+ 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');
+ const modal = document.querySelector(".note-modal-overlay");
+ const content = modal.querySelector(".note-modal-content");
- // Build full content
- let html = `
+ // Build full content
+ let html = `
${note.title}
${note.descHtml}
`;
- // Add images if not in desc? (desc usually has it)
+ // Add images if not in desc? (desc usually has it)
- content.innerHTML = html;
- modal.classList.add('open');
+ 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.
+ 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 `
`;
- }).join('');
+ 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 `
`;
+ })
+ .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}`);
- }
+ // 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"]');
+ // 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');
+ if (!form) throw new Error("Could not find edit form");
- // Extract all necessary fields
- const formData = new URLSearchParams();
- const inputs = form.querySelectorAll('input, textarea');
+ // 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);
- }
- });
+ 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() !== '');
+ // 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-'));
+ // Remove existing color tags
+ tagsArray = tagsArray.filter((t) => !t.startsWith("note-"));
- // Add new color tag (unless default)
- if (color !== 'default') {
- tagsArray.push(`note-${color}`);
- }
+ // 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
+ 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.");
- });
+ // 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 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 = [];
+ 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;
+ 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
+ // 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();
- }
- }
+ 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;
}
- });
+ }
- // 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]);
+ // 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();
+ }
+ }
}
+ });
- // 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.
+ // 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]);
+ }
+ }
- // 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();
+ // 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.
- const card = btn.closest('.link-outer, .note-card');
- const id = card ? card.dataset.id : null;
+ // 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();
- // 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;
- }
+ const card = btn.closest(".link-outer, .note-card");
+ const id = card ? card.dataset.id : null;
- if (id && editUrl) {
- togglePinTag(id, editUrl, btn);
- }
+ // 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;
}
- }, { once: false }); // Listener is permanent
+
+ 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;
+ 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;
+ 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 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);
- }
+ // 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 {
- if (titleIcon) titleIcon.remove();
+ existingTagElement = el;
}
+ break;
+ }
}
+ }
- // 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 (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";
- if (tagContainer) {
- // Check if tag exists already
- let existingTagElement = null;
+ const link = document.createElement("a");
+ link.href = "?searchtags=shaarli-pin";
+ link.textContent = "shaarli-pin";
- // 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();
- }
- }
+ 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');
+ 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);
- }
- });
+ 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';
+ 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);
- }
+ 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');
+ 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));
+ 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));
}
diff --git a/shaarli-pro/js/script.js b/shaarli-pro/js/script.js
index 4b10ba6..4d4e85f 100644
--- a/shaarli-pro/js/script.js
+++ b/shaarli-pro/js/script.js
@@ -59,14 +59,24 @@ document.addEventListener('DOMContentLoaded', () => {
let cachedBookmarks = null;
let cachedTags = null;
+ // Escape HTML to prevent XSS
+ function escapeHtml(text) {
+ if (typeof text !== 'string') return '';
+ const div = document.createElement('div');
+ div.textContent = text;
+ return div.innerHTML;
+ }
+
// Highlight matching text with
tags
function highlightMatch(text, query) {
- if (!query || query.length === 0) return text;
+ if (!query || query.length === 0) return escapeHtml(text);
+ // Escape HTML first to prevent XSS
+ const escapedText = escapeHtml(text);
// Escape special regex characters in query
const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const regex = new RegExp(`(${escapedQuery})`, 'gi');
- return text.replace(regex, '$1');
+ return escapedText.replace(regex, '$1');
}
// Fuzzy search - matches substring anywhere in text
diff --git a/shaarli-pro/linklist.html b/shaarli-pro/linklist.html
index b7ee8a3..5258ec4 100644
--- a/shaarli-pro/linklist.html
+++ b/shaarli-pro/linklist.html
@@ -1,114 +1,148 @@
-
-
-{$pageName="linklist"}
-{include="includes"}
-
-
-{include="page.header"}
-
-{loop="$plugin_start_zone"}
-{$value}
-{/loop}
-
-
-
{include="linklist.paging"}
-
-
+
-
-{if="!empty($search_tags)"}
-
-{/if}
+
+ {$pageName="linklist"}
+ {include="includes"}
+
-
-{if="count($links)==0"}
-
-
-
No bookmarks found
-
{if="!empty($search_term)"}No results for: {$search_term}{else}Start adding bookmarks to see them here.{/if}
-
-{else}
-
-
-{loop="$links"}
-
-{if="$is_logged_in"}
-
-{/if}
-{if="$value.thumbnail !== false"}
{/if}
-
{if="$value.private"}{else}{/if}
-
-
+
+ {include="page.header"}
+
+ {loop="$plugin_start_zone"}
+ {$value}
+ {/loop}
+
+
+
{include="linklist.paging"}
+
+
-{if="$value.description"}
{$value.description}
{/if}
-
-
-
-{/loop}
-
-{include="linklist.paging"}
-{/if}
-{loop="$plugin_end_zone"}
-{$value}
-{/loop}
-
+
+ {if="!empty($search_tags)"}
+
+ {/if}
-
-
+
+ {if="count($links)==0"}
+
+
+
Aucun bookmark trouvé
+
{if="!empty($search_term)"}Aucun résultat pour : {$search_term}{else}Commencez à ajouter des bookmarks pour les voir apparaître ici.{/if}
+
+ {else}
+
+
+ {loop="$links"}
+
+ {if="$is_logged_in"}
+
+ {/if}
+ {if="$value.thumbnail !== false"}
{/if}
+
{if="$value.private"}{else}{/if}
+
+
-
-
+ {if="$value.description"}
{$value.description}
{/if}
+
+
+
+ {/loop}
+
+ {include="linklist.paging"}
+ {/if}
+ {loop="$plugin_end_zone"}
+ {$value}
+ {/loop}
+
-{include="page.footer"}
-
+
+
-
\ No newline at end of file
+
+
+
+ {include="page.footer"}
+
+
+
\ No newline at end of file
diff --git a/shaarli-pro/linklist.paging.html b/shaarli-pro/linklist.paging.html
index a9081b5..801a783 100644
--- a/shaarli-pro/linklist.paging.html
+++ b/shaarli-pro/linklist.paging.html
@@ -13,7 +13,7 @@
{/if}
{$from=($page_current - 1) * $links_per_page + 1}
{$to=min($total, ($page_current - 1) * $links_per_page + $links_per_page)}
-
+
{if="$page_max > 1"}
{if="$next_page_url"}
{/if}
diff --git a/shaarli-pro/page.footer.html b/shaarli-pro/page.footer.html
index 287ad7a..1d334ca 100644
--- a/shaarli-pro/page.footer.html
+++ b/shaarli-pro/page.footer.html
@@ -18,7 +18,7 @@
{/loop}
{loop="$plugins_footer.js"}
-
+
{/loop}
{if="$pageName=='editlink' || $pageName=='addlink' || $pageName=='editlinkbatch'"}