955 lines
36 KiB
JavaScript
955 lines
36 KiB
JavaScript
document.addEventListener('DOMContentLoaded', () => {
|
|
// ===== Theme Toggle =====
|
|
const themeCheckbox = document.getElementById('theme-toggle-checkbox');
|
|
const themeIconLight = document.getElementById('theme-icon-light');
|
|
const themeLabelSpan = document.querySelector('.theme-toggle-label span');
|
|
|
|
function updateTheme(theme) {
|
|
document.documentElement.setAttribute('data-theme', theme);
|
|
localStorage.setItem('theme', theme);
|
|
|
|
if (themeCheckbox) {
|
|
themeCheckbox.checked = theme === 'dark';
|
|
}
|
|
if (themeIconLight) {
|
|
themeIconLight.className = theme === 'dark' ? 'mdi mdi-weather-night' : 'mdi mdi-weather-sunny';
|
|
}
|
|
if (themeLabelSpan) {
|
|
themeLabelSpan.textContent = theme === 'dark' ? 'Dark Mode' : 'Light Mode';
|
|
}
|
|
}
|
|
|
|
// Init Theme
|
|
const savedTheme = localStorage.getItem('theme') ||
|
|
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
|
|
updateTheme(savedTheme);
|
|
|
|
if (themeCheckbox) {
|
|
themeCheckbox.addEventListener('change', () => {
|
|
const next = themeCheckbox.checked ? 'dark' : 'light';
|
|
updateTheme(next);
|
|
});
|
|
}
|
|
|
|
// ===== Mobile Sidebar Toggle =====
|
|
const sidebar = document.getElementById('sidebar');
|
|
const sidebarOverlay = document.getElementById('sidebar-overlay');
|
|
const mobileMenuBtn = document.getElementById('mobile-menu-btn');
|
|
|
|
function toggleSidebar() {
|
|
sidebar?.classList.toggle('show');
|
|
sidebarOverlay?.classList.toggle('show');
|
|
}
|
|
|
|
mobileMenuBtn?.addEventListener('click', toggleSidebar);
|
|
sidebarOverlay?.addEventListener('click', toggleSidebar);
|
|
|
|
// ===== Search Overlay (Spotlight Style) =====
|
|
const searchOverlay = document.getElementById('search-overlay');
|
|
const searchToggleBtn = document.getElementById('search-toggle-btn');
|
|
const searchModalInput = document.getElementById('search-modal-input');
|
|
const searchResults = document.getElementById('search-results');
|
|
const searchTagsBtn = document.getElementById('search-tags-btn');
|
|
const searchAllBtn = document.getElementById('search-all-btn');
|
|
const searchForm = document.getElementById('search-form');
|
|
|
|
let searchMode = 'search'; // 'search' or 'tags'
|
|
let selectedIndex = -1;
|
|
let searchTimeout = null;
|
|
let cachedBookmarks = null;
|
|
let cachedTags = null;
|
|
|
|
// Highlight matching text with <mark> tags
|
|
function highlightMatch(text, query) {
|
|
if (!query || query.length === 0) return text;
|
|
|
|
// Escape special regex characters in query
|
|
const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
const regex = new RegExp(`(${escapedQuery})`, 'gi');
|
|
return text.replace(regex, '<mark>$1</mark>');
|
|
}
|
|
|
|
// Fuzzy search - matches substring anywhere in text
|
|
function fuzzyMatch(text, query) {
|
|
if (!query || query.length === 0) return true;
|
|
return text.toLowerCase().includes(query.toLowerCase());
|
|
}
|
|
|
|
// Fetch all unique tags from the page
|
|
function fetchTags() {
|
|
if (cachedTags) return cachedTags;
|
|
try {
|
|
const tagElements = document.querySelectorAll('.link-tag, .tag-link, [class*="tag"]');
|
|
const tagsSet = new Set();
|
|
|
|
tagElements.forEach(el => {
|
|
const tagText = el.textContent.trim();
|
|
if (tagText && tagText.length > 0 && !tagText.includes('•')) {
|
|
tagsSet.add(tagText);
|
|
}
|
|
});
|
|
|
|
// Convert to array and sort alphabetically
|
|
cachedTags = Array.from(tagsSet).sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
|
|
return cachedTags;
|
|
} catch (e) {
|
|
console.error('Failed to fetch tags:', e);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// Fetch bookmarks for live search
|
|
async function fetchBookmarks() {
|
|
if (cachedBookmarks) return cachedBookmarks;
|
|
|
|
try {
|
|
// Try to get bookmarks from page (if already loaded)
|
|
const linkElements = document.querySelectorAll('.link-outer');
|
|
if (linkElements.length > 0) {
|
|
cachedBookmarks = Array.from(linkElements).map(el => ({
|
|
id: el.dataset.id,
|
|
title: el.querySelector('.link-title a')?.textContent || el.querySelector('.link-title')?.textContent || '',
|
|
url: el.querySelector('.link-url a')?.href || el.querySelector('.link-title a')?.href || '',
|
|
tags: Array.from(el.querySelectorAll('.link-tag')).map(t => t.textContent.trim()),
|
|
description: el.querySelector('.link-description')?.textContent || ''
|
|
}));
|
|
return cachedBookmarks;
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to fetch bookmarks:', e);
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
// Render search results (tags or bookmarks)
|
|
function renderResults(results, query, isTagMode = false) {
|
|
if (!searchResults) return;
|
|
|
|
if (results.length === 0) {
|
|
if (query && query.length > 0) {
|
|
searchResults.innerHTML = `
|
|
<div class="search-no-results">
|
|
No results found for "<strong>${escapeHtml(query)}</strong>"
|
|
</div>
|
|
`;
|
|
} else {
|
|
searchResults.innerHTML = `
|
|
<div class="search-results-empty">
|
|
<span class="search-results-hint">Start typing to see tag suggestions...</span>
|
|
</div>
|
|
`;
|
|
}
|
|
return;
|
|
}
|
|
|
|
let html;
|
|
if (isTagMode) {
|
|
// Render tags
|
|
html = results.slice(0, 15).map((tag, index) => {
|
|
const highlightedTag = highlightMatch(escapeHtml(tag), query);
|
|
return `
|
|
<div class="search-result-item${index === selectedIndex ? ' selected' : ''}"
|
|
data-index="${index}"
|
|
data-tag="${escapeHtml(tag)}">
|
|
<div class="search-result-item-content">
|
|
<i class="mdi mdi-tag-outline search-result-icon"></i>
|
|
<span class="search-result-text">${highlightedTag}</span>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
} else {
|
|
// Render bookmarks
|
|
html = results.slice(0, 10).map((item, index) => {
|
|
const highlightedTitle = highlightMatch(escapeHtml(item.title), query);
|
|
return `
|
|
<div class="search-result-item${index === selectedIndex ? ' selected' : ''}"
|
|
data-index="${index}"
|
|
data-url="${escapeHtml(item.url)}"
|
|
data-id="${item.id}">
|
|
<div class="search-result-item-content">
|
|
<i class="mdi mdi-bookmark-outline search-result-icon"></i>
|
|
<span class="search-result-text">${highlightedTitle}</span>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
searchResults.innerHTML = html;
|
|
|
|
// Add click handlers to results
|
|
searchResults.querySelectorAll('.search-result-item').forEach(item => {
|
|
item.addEventListener('click', () => {
|
|
if (isTagMode) {
|
|
const tag = item.dataset.tag;
|
|
if (tag) {
|
|
// Navigate to tag search
|
|
window.location.href = shaarli.basePath + '/?searchtags=' + encodeURIComponent(tag);
|
|
}
|
|
} else {
|
|
const url = item.dataset.url;
|
|
if (url) {
|
|
window.location.href = url;
|
|
}
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// Escape HTML to prevent XSS
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
// Perform live search
|
|
async function performSearch(query) {
|
|
const tags = fetchTags();
|
|
|
|
if (!query || query.length === 0) {
|
|
// Show all tags when empty
|
|
renderResults(tags.slice(0, 15), '', true);
|
|
return;
|
|
}
|
|
|
|
// Filter tags with fuzzy matching
|
|
const results = tags.filter(tag => fuzzyMatch(tag, query));
|
|
renderResults(results, query, true);
|
|
}
|
|
|
|
// Update selected result
|
|
function updateSelection(newIndex) {
|
|
const items = searchResults?.querySelectorAll('.search-result-item');
|
|
if (!items || items.length === 0) return;
|
|
|
|
// Clamp index
|
|
if (newIndex < 0) newIndex = items.length - 1;
|
|
if (newIndex >= items.length) newIndex = 0;
|
|
|
|
selectedIndex = newIndex;
|
|
|
|
items.forEach((item, index) => {
|
|
item.classList.toggle('selected', index === selectedIndex);
|
|
});
|
|
|
|
// Scroll into view
|
|
items[selectedIndex]?.scrollIntoView({ block: 'nearest' });
|
|
}
|
|
|
|
// Navigate to selected result
|
|
function navigateToSelected() {
|
|
const selected = searchResults?.querySelector('.search-result-item.selected');
|
|
if (selected) {
|
|
// Check if it's a tag
|
|
const tag = selected.dataset.tag;
|
|
if (tag) {
|
|
window.location.href = shaarli.basePath + '/?searchtags=' + encodeURIComponent(tag);
|
|
return true;
|
|
}
|
|
// Otherwise check for URL
|
|
const url = selected.dataset.url;
|
|
if (url) {
|
|
window.location.href = url;
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function openSearch() {
|
|
searchOverlay?.classList.add('show');
|
|
selectedIndex = -1;
|
|
setTimeout(() => {
|
|
searchModalInput?.focus();
|
|
// Trigger initial search if there's existing text
|
|
if (searchModalInput?.value) {
|
|
performSearch(searchModalInput.value);
|
|
}
|
|
}, 100);
|
|
}
|
|
|
|
function closeSearch() {
|
|
searchOverlay?.classList.remove('show');
|
|
selectedIndex = -1;
|
|
}
|
|
|
|
// Toggle search mode (tags vs search)
|
|
function setSearchMode(mode) {
|
|
searchMode = mode;
|
|
|
|
searchTagsBtn?.classList.toggle('active', mode === 'tags');
|
|
searchAllBtn?.classList.toggle('active', mode === 'search');
|
|
|
|
if (searchModalInput) {
|
|
searchModalInput.name = mode === 'tags' ? 'searchtags' : 'searchterm';
|
|
// Re-run search with new mode
|
|
performSearch(searchModalInput.value);
|
|
}
|
|
}
|
|
|
|
searchToggleBtn?.addEventListener('click', openSearch);
|
|
|
|
// Close search on overlay click
|
|
searchOverlay?.addEventListener('click', (e) => {
|
|
if (e.target === searchOverlay) {
|
|
closeSearch();
|
|
}
|
|
});
|
|
|
|
// Search mode toggle buttons
|
|
searchTagsBtn?.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
setSearchMode('tags');
|
|
});
|
|
|
|
searchAllBtn?.addEventListener('click', (e) => {
|
|
// Only prevent default if not submitting
|
|
if (selectedIndex >= 0) {
|
|
e.preventDefault();
|
|
navigateToSelected();
|
|
}
|
|
});
|
|
|
|
// Live search on input
|
|
searchModalInput?.addEventListener('input', (e) => {
|
|
const query = e.target.value;
|
|
|
|
// Debounce search
|
|
clearTimeout(searchTimeout);
|
|
searchTimeout = setTimeout(() => {
|
|
selectedIndex = -1;
|
|
performSearch(query);
|
|
}, 150);
|
|
});
|
|
|
|
// Keyboard shortcuts
|
|
document.addEventListener('keydown', (e) => {
|
|
const isSearchOpen = searchOverlay?.classList.contains('show');
|
|
const isTyping = e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA';
|
|
|
|
// Handle ESC - always close search/filter
|
|
if (e.key === 'Escape') {
|
|
if (isSearchOpen) {
|
|
closeSearch();
|
|
e.preventDefault();
|
|
}
|
|
closeFilterPanel();
|
|
return;
|
|
}
|
|
|
|
// If search is open, handle navigation
|
|
if (isSearchOpen && e.target === searchModalInput) {
|
|
switch (e.key) {
|
|
case 'ArrowDown':
|
|
e.preventDefault();
|
|
updateSelection(selectedIndex + 1);
|
|
break;
|
|
case 'ArrowUp':
|
|
e.preventDefault();
|
|
updateSelection(selectedIndex - 1);
|
|
break;
|
|
case 'Enter':
|
|
// If an item is selected, navigate to it
|
|
if (selectedIndex >= 0 && navigateToSelected()) {
|
|
e.preventDefault();
|
|
}
|
|
// Otherwise, submit the form normally
|
|
break;
|
|
}
|
|
return;
|
|
}
|
|
|
|
// S to open search (when not typing)
|
|
if (!isTyping && (e.key === 's' || e.key === 'S')) {
|
|
e.preventDefault();
|
|
openSearch();
|
|
}
|
|
});
|
|
|
|
// ===== Filter Panel =====
|
|
const filterToggleBtn = document.getElementById('filter-toggle-btn');
|
|
const filterPanel = document.getElementById('filter-panel');
|
|
const filterCloseBtn = document.getElementById('filter-close-btn');
|
|
const filterPrivate = document.getElementById('filter-private');
|
|
const filterPublic = document.getElementById('filter-public');
|
|
const filterUntagged = document.getElementById('filter-untagged');
|
|
|
|
function toggleFilterPanel() {
|
|
filterPanel?.classList.toggle('show');
|
|
}
|
|
|
|
function closeFilterPanel() {
|
|
filterPanel?.classList.remove('show');
|
|
}
|
|
|
|
filterToggleBtn?.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
toggleFilterPanel();
|
|
});
|
|
|
|
filterCloseBtn?.addEventListener('click', closeFilterPanel);
|
|
|
|
// Close filter when clicking outside
|
|
document.addEventListener('click', (e) => {
|
|
if (filterPanel?.classList.contains('show')) {
|
|
if (!filterPanel.contains(e.target) && e.target !== filterToggleBtn) {
|
|
closeFilterPanel();
|
|
}
|
|
}
|
|
});
|
|
|
|
// Handle filter toggle switches
|
|
// Logic:
|
|
// - Visibility: all (neither checked), private (only private checked), public (only public checked)
|
|
// - Untagged: can be combined with any visibility
|
|
function applyFilters() {
|
|
const isPrivate = filterPrivate?.checked || false;
|
|
const isPublic = filterPublic?.checked || false;
|
|
const isUntagged = filterUntagged?.checked || false;
|
|
|
|
let basePath = (typeof shaarli !== 'undefined' && shaarli.basePath) ? shaarli.basePath : '';
|
|
|
|
// Get current filter state from server-side rendered variables
|
|
const currentVisibility = (typeof shaarli !== 'undefined' && shaarli.visibility) ? shaarli.visibility : '';
|
|
const currentUntagged = (typeof shaarli !== 'undefined' && shaarli.untaggedonly) || false;
|
|
|
|
console.log('[Filter] Applying filters - private:', isPrivate, 'public:', isPublic, 'untagged:', isUntagged);
|
|
console.log('[Filter] Current state - visibility:', currentVisibility, 'untagged:', currentUntagged);
|
|
|
|
let url = basePath + '/';
|
|
|
|
// Determine desired visibility
|
|
let desiredVisibility = 'all';
|
|
if (isPrivate) {
|
|
desiredVisibility = 'private';
|
|
} else if (isPublic) {
|
|
desiredVisibility = 'public';
|
|
}
|
|
|
|
// Build URL based on desired state
|
|
if (desiredVisibility === 'private') {
|
|
url = basePath + '/admin/visibility/private';
|
|
} else if (desiredVisibility === 'public') {
|
|
url = basePath + '/admin/visibility/public';
|
|
} else {
|
|
// visibility = all - need to clear visibility if it was set
|
|
if (currentVisibility && currentVisibility !== '') {
|
|
url = basePath + '/admin/visibility/all';
|
|
} else if (isUntagged !== currentUntagged) {
|
|
// Untagged state changed - /untagged-only works as toggle
|
|
url = basePath + '/untagged-only';
|
|
} else {
|
|
url = basePath + '/';
|
|
}
|
|
}
|
|
|
|
console.log('[Filter] Navigating to:', url);
|
|
window.location.href = url;
|
|
}
|
|
|
|
// Clear all filters - go back to showing all bookmarks
|
|
function clearAllFilters() {
|
|
const basePath = (typeof shaarli !== 'undefined' && shaarli.basePath) ? shaarli.basePath : '';
|
|
const currentVisibility = (typeof shaarli !== 'undefined' && shaarli.visibility) ? shaarli.visibility : '';
|
|
const currentUntagged = (typeof shaarli !== 'undefined' && shaarli.untaggedonly) || false;
|
|
|
|
// If visibility is set, clear it first
|
|
if (currentVisibility && currentVisibility !== '') {
|
|
window.location.href = basePath + '/admin/visibility/all';
|
|
} else if (currentUntagged) {
|
|
// Toggle untagged off using /untagged-only
|
|
window.location.href = basePath + '/untagged-only';
|
|
} else {
|
|
window.location.href = basePath + '/';
|
|
}
|
|
}
|
|
|
|
// Private and public are mutually exclusive
|
|
filterPrivate?.addEventListener('change', (e) => {
|
|
if (e.target.checked && filterPublic) {
|
|
filterPublic.checked = false;
|
|
}
|
|
applyFilters();
|
|
});
|
|
|
|
filterPublic?.addEventListener('change', (e) => {
|
|
if (e.target.checked && filterPrivate) {
|
|
filterPrivate.checked = false;
|
|
}
|
|
applyFilters();
|
|
});
|
|
|
|
filterUntagged?.addEventListener('change', () => {
|
|
applyFilters();
|
|
});
|
|
|
|
// Initialize filter states from server-side variables (set in includes.html)
|
|
(function initFilterStates() {
|
|
console.log('[Filter Debug] ========================================');
|
|
|
|
// Get filter state from server-side rendered variables
|
|
const visibility = (typeof shaarli !== 'undefined' && shaarli.visibility) ? shaarli.visibility : '';
|
|
const untaggedonly = (typeof shaarli !== 'undefined' && shaarli.untaggedonly) ? shaarli.untaggedonly : false;
|
|
|
|
// Detect active filters from server variables
|
|
let isPrivateActive = visibility === 'private';
|
|
let isPublicActive = visibility === 'public';
|
|
let isUntaggedActive = untaggedonly === true;
|
|
|
|
console.log('[Filter Debug] shaarli.visibility:', visibility);
|
|
console.log('[Filter Debug] shaarli.untaggedonly:', untaggedonly);
|
|
|
|
console.log('[Filter Debug] isPrivateActive:', isPrivateActive);
|
|
console.log('[Filter Debug] isPublicActive:', isPublicActive);
|
|
console.log('[Filter Debug] isUntaggedActive:', isUntaggedActive);
|
|
|
|
// Set checkbox states
|
|
if (filterPrivate && isPrivateActive) {
|
|
filterPrivate.checked = true;
|
|
console.log('[Filter Debug] ✓ Set filterPrivate to CHECKED');
|
|
}
|
|
if (filterPublic && isPublicActive) {
|
|
filterPublic.checked = true;
|
|
console.log('[Filter Debug] ✓ Set filterPublic to CHECKED');
|
|
}
|
|
if (filterUntagged && isUntaggedActive) {
|
|
filterUntagged.checked = true;
|
|
console.log('[Filter Debug] ✓ Set filterUntagged to CHECKED');
|
|
}
|
|
|
|
const hasActiveFilter = isPrivateActive || isPublicActive || isUntaggedActive;
|
|
console.log('[Filter Debug] hasActiveFilter:', hasActiveFilter);
|
|
console.log('[Filter Debug] ========================================');
|
|
|
|
// Add/update filter indicator badge on the filter button
|
|
if (filterToggleBtn && hasActiveFilter) {
|
|
filterToggleBtn.classList.add('has-active-filter');
|
|
console.log('[Filter Debug] Added has-active-filter class to button');
|
|
|
|
// Add badge indicator if not exists
|
|
if (!filterToggleBtn.querySelector('.filter-badge')) {
|
|
const badge = document.createElement('span');
|
|
badge.className = 'filter-badge';
|
|
filterToggleBtn.appendChild(badge);
|
|
console.log('[Filter Debug] Added filter badge');
|
|
}
|
|
}
|
|
|
|
// Create and display the filter info banner
|
|
if (hasActiveFilter && !document.getElementById('filter-info-banner')) {
|
|
// Get result count from page - try multiple sources
|
|
let resultCount = 0;
|
|
const pagingStats = document.querySelector('.paging-stats strong:last-child');
|
|
const linkCount = document.querySelectorAll('.link-outer').length;
|
|
|
|
if (pagingStats) {
|
|
resultCount = parseInt(pagingStats.textContent) || 0;
|
|
} else if (linkCount > 0) {
|
|
resultCount = linkCount;
|
|
}
|
|
|
|
// Check if no results (empty state)
|
|
const emptyState = document.querySelector('.empty-state');
|
|
const isEmptyResults = emptyState !== null || resultCount === 0;
|
|
|
|
// Build the message like the examples:
|
|
// "5 results without any tag"
|
|
// "5 results with status private without any tag"
|
|
// "1 result with status public without any tag"
|
|
let statusPart = '';
|
|
let untaggedPart = '';
|
|
|
|
if (isPrivateActive) {
|
|
statusPart = '<strong>private</strong>';
|
|
} else if (isPublicActive) {
|
|
statusPart = '<strong>public</strong>';
|
|
}
|
|
|
|
if (isUntaggedActive) {
|
|
untaggedPart = '<strong>without any tag</strong>';
|
|
}
|
|
|
|
// Build the message
|
|
let message = '';
|
|
if (isEmptyResults) {
|
|
message = 'Nothing found.';
|
|
} else {
|
|
const resultWord = resultCount === 1 ? 'result' : 'results';
|
|
if (statusPart && untaggedPart) {
|
|
message = `${resultCount} ${resultWord} with status ${statusPart} ${untaggedPart}`;
|
|
} else if (statusPart) {
|
|
message = `${resultCount} ${resultWord} with status ${statusPart}`;
|
|
} else if (untaggedPart) {
|
|
message = `${resultCount} ${resultWord} ${untaggedPart}`;
|
|
}
|
|
}
|
|
|
|
console.log('[Filter Debug] resultCount:', resultCount);
|
|
console.log('[Filter Debug] isEmptyResults:', isEmptyResults);
|
|
console.log('[Filter Debug] message:', message);
|
|
|
|
if (message) {
|
|
// Get base path safely
|
|
const basePath = (typeof shaarli !== 'undefined' && shaarli.basePath) ? shaarli.basePath : '';
|
|
|
|
const banner = document.createElement('div');
|
|
banner.id = 'filter-info-banner';
|
|
banner.className = 'filter-info-banner' + (isEmptyResults ? ' empty-results' : '');
|
|
banner.innerHTML = `
|
|
<div class="filter-info-content">
|
|
<span>${message}</span>
|
|
</div>
|
|
<button type="button" class="filter-clear-btn" id="filter-clear-btn" title="Clear filters">
|
|
<i class="mdi mdi-close"></i>
|
|
<span>Clear</span>
|
|
</button>
|
|
`;
|
|
|
|
// Try to find the best insertion point
|
|
const contentToolbar = document.querySelector('.content-toolbar');
|
|
const linklist = document.getElementById('linklist');
|
|
const emptyStateDiv = document.querySelector('.empty-state');
|
|
|
|
console.log('[Filter Debug] contentToolbar:', contentToolbar);
|
|
console.log('[Filter Debug] linklist:', linklist);
|
|
|
|
// Insert after the content-toolbar (pagination)
|
|
if (contentToolbar && contentToolbar.parentNode) {
|
|
contentToolbar.parentNode.insertBefore(banner, contentToolbar.nextSibling);
|
|
console.log('[Filter Debug] Banner inserted after content-toolbar');
|
|
} else if (emptyStateDiv && emptyStateDiv.parentNode) {
|
|
emptyStateDiv.parentNode.insertBefore(banner, emptyStateDiv);
|
|
console.log('[Filter Debug] Banner inserted before empty-state');
|
|
} else if (linklist) {
|
|
linklist.insertBefore(banner, linklist.firstChild);
|
|
console.log('[Filter Debug] Banner inserted at beginning of linklist');
|
|
} else {
|
|
console.log('[Filter Debug] Could not find insertion point for banner');
|
|
}
|
|
|
|
// Add click handler to clear button
|
|
document.getElementById('filter-clear-btn')?.addEventListener('click', () => {
|
|
console.log('[Filter] Clear button clicked, clearing all filters');
|
|
// Clear based on what was active
|
|
if (isPrivateActive || isPublicActive) {
|
|
window.location.href = basePath + '/admin/visibility/all';
|
|
} else if (isUntaggedActive) {
|
|
// Toggle untagged off
|
|
window.location.href = basePath + '/untagged-only';
|
|
} else {
|
|
window.location.href = basePath + '/';
|
|
}
|
|
});
|
|
}
|
|
}
|
|
})();
|
|
|
|
// Handle links per page options
|
|
|
|
|
|
// Handle custom value form submission
|
|
const filterInput = document.querySelector('.filter-input');
|
|
if (filterInput) {
|
|
filterInput.closest('form')?.addEventListener('submit', (e) => {
|
|
e.preventDefault();
|
|
const value = filterInput.value;
|
|
if (value && value > 0) {
|
|
window.location.href = shaarli.basePath + '/links-per-page?nb=' + value;
|
|
}
|
|
});
|
|
}
|
|
|
|
// ===== View Toggle (Grid/List/Compact) =====
|
|
const linksList = document.getElementById('links-list');
|
|
const viewGridBtn = document.getElementById('view-grid-btn');
|
|
const viewListBtn = document.getElementById('view-list-btn');
|
|
const viewCompactBtn = document.getElementById('view-compact-btn');
|
|
|
|
function setView(view) {
|
|
if (!linksList) return;
|
|
|
|
// Remove all view classes
|
|
linksList.classList.remove('view-grid', 'view-list', 'view-compact');
|
|
|
|
// Remove active state from all buttons
|
|
viewGridBtn?.classList.remove('active');
|
|
viewListBtn?.classList.remove('active');
|
|
viewCompactBtn?.classList.remove('active');
|
|
|
|
// Apply selected view
|
|
if (view === 'list') {
|
|
linksList.classList.add('view-list');
|
|
viewListBtn?.classList.add('active');
|
|
} else if (view === 'compact') {
|
|
linksList.classList.add('view-compact');
|
|
viewCompactBtn?.classList.add('active');
|
|
} else {
|
|
// Default to grid
|
|
linksList.classList.add('view-grid');
|
|
viewGridBtn?.classList.add('active');
|
|
}
|
|
|
|
localStorage.setItem('linksView', view);
|
|
}
|
|
|
|
// Init view from localStorage
|
|
const savedView = localStorage.getItem('linksView') || 'grid';
|
|
setView(savedView);
|
|
|
|
viewGridBtn?.addEventListener('click', () => setView('grid'));
|
|
viewListBtn?.addEventListener('click', () => setView('list'));
|
|
viewCompactBtn?.addEventListener('click', () => setView('compact'));
|
|
|
|
// ===== Multi-Select Mode =====
|
|
const selectModeBtn = document.getElementById('select-mode-btn');
|
|
const bulkActionsBar = document.getElementById('bulk-actions-bar');
|
|
const bulkCount = document.getElementById('bulk-count');
|
|
const bulkSelectAll = document.getElementById('bulk-select-all');
|
|
const bulkCancel = document.getElementById('bulk-cancel');
|
|
const bulkDelete = document.getElementById('bulk-delete');
|
|
const bulkPublic = document.getElementById('bulk-public');
|
|
const bulkPrivate = document.getElementById('bulk-private');
|
|
|
|
let selectionMode = false;
|
|
let selectedIds = new Set();
|
|
|
|
function updateBulkUI() {
|
|
if (bulkCount) {
|
|
bulkCount.textContent = selectedIds.size;
|
|
}
|
|
|
|
// Update link cards visual state
|
|
document.querySelectorAll('.link-outer').forEach(card => {
|
|
const id = card.dataset.id;
|
|
if (selectedIds.has(id)) {
|
|
card.classList.add('selected');
|
|
} else {
|
|
card.classList.remove('selected');
|
|
}
|
|
});
|
|
|
|
// Update checkboxes
|
|
document.querySelectorAll('.link-checkbox').forEach(cb => {
|
|
cb.checked = selectedIds.has(cb.dataset.id);
|
|
});
|
|
}
|
|
|
|
function enterSelectionMode() {
|
|
selectionMode = true;
|
|
document.body.classList.add('selection-mode');
|
|
bulkActionsBar?.classList.add('show');
|
|
selectModeBtn?.classList.add('active');
|
|
}
|
|
|
|
function exitSelectionMode() {
|
|
selectionMode = false;
|
|
selectedIds.clear();
|
|
document.body.classList.remove('selection-mode');
|
|
bulkActionsBar?.classList.remove('show');
|
|
selectModeBtn?.classList.remove('active');
|
|
updateBulkUI();
|
|
}
|
|
|
|
function toggleSelection(id) {
|
|
if (selectedIds.has(id)) {
|
|
selectedIds.delete(id);
|
|
} else {
|
|
selectedIds.add(id);
|
|
}
|
|
|
|
if (selectedIds.size > 0 && !selectionMode) {
|
|
enterSelectionMode();
|
|
} else if (selectedIds.size === 0 && selectionMode) {
|
|
exitSelectionMode();
|
|
}
|
|
|
|
updateBulkUI();
|
|
}
|
|
|
|
selectModeBtn?.addEventListener('click', () => {
|
|
if (selectionMode) {
|
|
exitSelectionMode();
|
|
} else {
|
|
enterSelectionMode();
|
|
}
|
|
});
|
|
|
|
bulkCancel?.addEventListener('click', exitSelectionMode);
|
|
|
|
bulkSelectAll?.addEventListener('click', () => {
|
|
document.querySelectorAll('.link-outer').forEach(card => {
|
|
if (card.dataset.id) {
|
|
selectedIds.add(card.dataset.id);
|
|
}
|
|
});
|
|
updateBulkUI();
|
|
});
|
|
|
|
// Handle checkbox clicks
|
|
document.addEventListener('change', (e) => {
|
|
if (e.target.classList.contains('link-checkbox')) {
|
|
toggleSelection(e.target.dataset.id);
|
|
}
|
|
});
|
|
|
|
// Handle card clicks in selection mode
|
|
document.addEventListener('click', (e) => {
|
|
if (!selectionMode) return;
|
|
|
|
const card = e.target.closest('.link-outer');
|
|
if (card && card.dataset.id) {
|
|
// Don't toggle if clicking on actions or links
|
|
if (e.target.closest('.link-actions') ||
|
|
e.target.closest('.link-hover-actions') ||
|
|
e.target.tagName === 'A') {
|
|
return;
|
|
}
|
|
toggleSelection(card.dataset.id);
|
|
}
|
|
});
|
|
|
|
// Bulk actions
|
|
bulkDelete?.addEventListener('click', () => {
|
|
if (selectedIds.size === 0) return;
|
|
if (!confirm(`Delete ${selectedIds.size} bookmark(s)?`)) return;
|
|
|
|
// Submit form with selected IDs
|
|
const form = document.createElement('form');
|
|
form.method = 'POST';
|
|
form.action = shaarli.basePath + '/admin/shaare/delete';
|
|
|
|
selectedIds.forEach(id => {
|
|
const input = document.createElement('input');
|
|
input.type = 'hidden';
|
|
input.name = 'id[]';
|
|
input.value = id;
|
|
form.appendChild(input);
|
|
});
|
|
|
|
const tokenInput = document.createElement('input');
|
|
tokenInput.type = 'hidden';
|
|
tokenInput.name = 'token';
|
|
tokenInput.value = document.querySelector('input[name="token"]')?.value || '';
|
|
form.appendChild(tokenInput);
|
|
|
|
document.body.appendChild(form);
|
|
form.submit();
|
|
});
|
|
|
|
bulkPublic?.addEventListener('click', () => {
|
|
if (selectedIds.size === 0) return;
|
|
bulkVisibilityChange('public');
|
|
});
|
|
|
|
bulkPrivate?.addEventListener('click', () => {
|
|
if (selectedIds.size === 0) return;
|
|
bulkVisibilityChange('private');
|
|
});
|
|
|
|
function bulkVisibilityChange(visibility) {
|
|
const form = document.createElement('form');
|
|
form.method = 'POST';
|
|
form.action = shaarli.basePath + '/admin/shaare/visibility';
|
|
|
|
selectedIds.forEach(id => {
|
|
const input = document.createElement('input');
|
|
input.type = 'hidden';
|
|
input.name = 'id[]';
|
|
input.value = id;
|
|
form.appendChild(input);
|
|
});
|
|
|
|
const visInput = document.createElement('input');
|
|
visInput.type = 'hidden';
|
|
visInput.name = 'visibility';
|
|
visInput.value = visibility;
|
|
form.appendChild(visInput);
|
|
|
|
const tokenInput = document.createElement('input');
|
|
tokenInput.type = 'hidden';
|
|
tokenInput.name = 'token';
|
|
tokenInput.value = document.querySelector('input[name="token"]')?.value || '';
|
|
form.appendChild(tokenInput);
|
|
|
|
document.body.appendChild(form);
|
|
form.submit();
|
|
}
|
|
|
|
|
|
// ===== Thumbnail Update =====
|
|
const thumbnailsPage = document.querySelector('.page-thumbnails');
|
|
if (thumbnailsPage) {
|
|
const thumbnailPlaceholder = document.querySelector('.thumbnail-placeholder');
|
|
const thumbnailTitle = document.querySelector('.thumbnail-link-title');
|
|
const progressCurrent = document.querySelector('.progress-current');
|
|
const progressBarActual = document.querySelector('.progress-actual');
|
|
const idsInput = document.querySelector('input[name="ids"]');
|
|
|
|
if (idsInput && idsInput.value) {
|
|
const thumbnailsIdList = idsInput.value.split(',');
|
|
const total = thumbnailsIdList.length;
|
|
let i = 0;
|
|
|
|
const updateThumbnail = function (id) {
|
|
console.log('Updating thumbnail #' + i + ' with id ' + id);
|
|
|
|
fetch(shaarli.basePath + '/admin/shaare/' + id + '/update-thumbnail', {
|
|
method: 'PATCH',
|
|
headers: {
|
|
'Accept': 'application/json',
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
i++;
|
|
|
|
if (thumbnailTitle) {
|
|
thumbnailTitle.textContent = data.title || 'Processing...';
|
|
}
|
|
|
|
if (thumbnailPlaceholder) {
|
|
if (data.thumbnail) {
|
|
thumbnailPlaceholder.innerHTML = '<img title="Current thumbnail" src="' + data.thumbnail + '" style="max-width: 100%; height: auto;"/>';
|
|
} else {
|
|
thumbnailPlaceholder.innerHTML = '';
|
|
}
|
|
}
|
|
|
|
if (progressCurrent) {
|
|
progressCurrent.textContent = i;
|
|
}
|
|
|
|
if (progressBarActual) {
|
|
progressBarActual.style.width = ((i * 100) / total) + '%';
|
|
}
|
|
|
|
if (i < total) {
|
|
updateThumbnail(thumbnailsIdList[i]);
|
|
} else {
|
|
if (thumbnailTitle) {
|
|
thumbnailTitle.textContent = 'Thumbnail update done!';
|
|
}
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Failed to update thumbnail:', error);
|
|
if (thumbnailTitle) {
|
|
thumbnailTitle.textContent = 'Error updating thumbnail #' + i;
|
|
}
|
|
// Continue with next thumbnail even if one fails
|
|
i++;
|
|
if (i < total) {
|
|
updateThumbnail(thumbnailsIdList[i]);
|
|
}
|
|
});
|
|
};
|
|
|
|
// Start the update process
|
|
updateThumbnail(thumbnailsIdList[0]);
|
|
}
|
|
}
|
|
});
|