mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-06-13 11:05:03 +00:00
table of contents fixes
This commit is contained in:
parent
85f4e1eb20
commit
b1f9bb671c
1
.gitignore
vendored
1
.gitignore
vendored
@ -197,4 +197,3 @@ id_ed25519.pub
|
|||||||
|
|
||||||
# node_modules
|
# node_modules
|
||||||
node_modules/
|
node_modules/
|
||||||
*.mjs
|
|
||||||
|
1
stirling-pdf/.gitignore
vendored
1
stirling-pdf/.gitignore
vendored
@ -194,4 +194,3 @@ id_ed25519.pub
|
|||||||
|
|
||||||
# node_modules
|
# node_modules
|
||||||
node_modules/
|
node_modules/
|
||||||
*.mjs
|
|
||||||
|
@ -130,6 +130,13 @@ public class GeneralWebController {
|
|||||||
return "view-pdf";
|
return "view-pdf";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/edit-table-of-contents")
|
||||||
|
@Hidden
|
||||||
|
public String editTableOfContents(Model model) {
|
||||||
|
model.addAttribute("currentPage", "edit-table-of-contents");
|
||||||
|
return "edit-table-of-contents";
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/multi-tool")
|
@GetMapping("/multi-tool")
|
||||||
@Hidden
|
@Hidden
|
||||||
public String multiToolForm(Model model) {
|
public String multiToolForm(Model model) {
|
||||||
|
@ -0,0 +1,653 @@
|
|||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const bookmarksContainer = document.getElementById('bookmarks-container');
|
||||||
|
const addBookmarkBtn = document.getElementById('addBookmarkBtn');
|
||||||
|
const bookmarkDataInput = document.getElementById('bookmarkData');
|
||||||
|
let bookmarks = [];
|
||||||
|
let counter = 0; // Used for generating unique IDs
|
||||||
|
|
||||||
|
// Add event listener to the file input to extract existing bookmarks
|
||||||
|
document.getElementById('fileInput-input').addEventListener('change', async function(e) {
|
||||||
|
if (!e.target.files || e.target.files.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset bookmarks initially
|
||||||
|
bookmarks = [];
|
||||||
|
updateBookmarksUI();
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', e.target.files[0]);
|
||||||
|
|
||||||
|
// Show loading indicator
|
||||||
|
showLoadingIndicator();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Call the API to extract bookmarks using fetchWithCsrf for CSRF protection
|
||||||
|
const response = await fetchWithCsrf('/api/v1/general/extract-bookmarks', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to extract bookmarks: ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const extractedBookmarks = await response.json();
|
||||||
|
|
||||||
|
// Convert extracted bookmarks to our format with IDs
|
||||||
|
if (extractedBookmarks && extractedBookmarks.length > 0) {
|
||||||
|
bookmarks = extractedBookmarks.map(convertExtractedBookmark);
|
||||||
|
} else {
|
||||||
|
showEmptyState();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Show error message
|
||||||
|
showErrorMessage('Failed to extract bookmarks. You can still create new ones.');
|
||||||
|
|
||||||
|
// Add a default bookmark if no bookmarks and error
|
||||||
|
if (bookmarks.length === 0) {
|
||||||
|
showEmptyState();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
// Remove loading indicator
|
||||||
|
removeLoadingIndicator();
|
||||||
|
|
||||||
|
// Update the UI
|
||||||
|
updateBookmarksUI();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function showLoadingIndicator() {
|
||||||
|
const loadingEl = document.createElement('div');
|
||||||
|
loadingEl.className = 'alert alert-info';
|
||||||
|
loadingEl.textContent = 'Loading bookmarks from PDF...';
|
||||||
|
loadingEl.id = 'loading-bookmarks';
|
||||||
|
bookmarksContainer.innerHTML = '';
|
||||||
|
bookmarksContainer.appendChild(loadingEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeLoadingIndicator() {
|
||||||
|
const loadingEl = document.getElementById('loading-bookmarks');
|
||||||
|
if (loadingEl) {
|
||||||
|
loadingEl.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showErrorMessage(message) {
|
||||||
|
const errorEl = document.createElement('div');
|
||||||
|
errorEl.className = 'alert alert-danger';
|
||||||
|
errorEl.textContent = message;
|
||||||
|
bookmarksContainer.appendChild(errorEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showEmptyState() {
|
||||||
|
const emptyStateEl = document.createElement('div');
|
||||||
|
emptyStateEl.className = 'empty-bookmarks mb-3';
|
||||||
|
emptyStateEl.innerHTML = `
|
||||||
|
<span class="material-symbols-rounded mb-2" style="font-size: 48px;">bookmark_add</span>
|
||||||
|
<h5>No bookmarks found</h5>
|
||||||
|
<p class="mb-3">This PDF doesn't have any bookmarks yet. Add your first bookmark to get started.</p>
|
||||||
|
<button type="button" class="btn btn-primary btn-add-first-bookmark">
|
||||||
|
<span class="material-symbols-rounded">add</span> Add First Bookmark
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Add event listener to the "Add First Bookmark" button
|
||||||
|
emptyStateEl.querySelector('.btn-add-first-bookmark').addEventListener('click', function() {
|
||||||
|
addBookmark(null, 'New Bookmark', 1);
|
||||||
|
emptyStateEl.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
bookmarksContainer.appendChild(emptyStateEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to convert extracted bookmarks to our format with IDs
|
||||||
|
function convertExtractedBookmark(bookmark) {
|
||||||
|
counter++;
|
||||||
|
const result = {
|
||||||
|
id: Date.now() + counter, // Generate a unique ID
|
||||||
|
title: bookmark.title || 'Untitled Bookmark',
|
||||||
|
pageNumber: bookmark.pageNumber || 1,
|
||||||
|
children: [],
|
||||||
|
expanded: false // All bookmarks start collapsed for better visibility
|
||||||
|
};
|
||||||
|
|
||||||
|
// Convert children recursively
|
||||||
|
if (bookmark.children && bookmark.children.length > 0) {
|
||||||
|
result.children = bookmark.children.map(child => {
|
||||||
|
return convertExtractedBookmark(child);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add bookmark button click handler
|
||||||
|
addBookmarkBtn.addEventListener('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
addBookmark();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add form submit handler to update JSON data
|
||||||
|
document.getElementById('editTocForm').addEventListener('submit', function() {
|
||||||
|
updateBookmarkData();
|
||||||
|
});
|
||||||
|
|
||||||
|
function addBookmark(parent = null, title = '', pageNumber = 1) {
|
||||||
|
counter++;
|
||||||
|
const newBookmark = {
|
||||||
|
id: Date.now() + counter,
|
||||||
|
title: title || 'New Bookmark',
|
||||||
|
pageNumber: pageNumber || 1,
|
||||||
|
children: [],
|
||||||
|
expanded: false // New bookmarks start collapsed
|
||||||
|
};
|
||||||
|
|
||||||
|
if (parent === null) {
|
||||||
|
bookmarks.push(newBookmark);
|
||||||
|
} else {
|
||||||
|
const parentBookmark = findBookmark(bookmarks, parent);
|
||||||
|
if (parentBookmark) {
|
||||||
|
parentBookmark.children.push(newBookmark);
|
||||||
|
parentBookmark.expanded = true; // Auto-expand the parent when adding a child
|
||||||
|
} else {
|
||||||
|
// Add to root level if parent not found
|
||||||
|
bookmarks.push(newBookmark);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateBookmarksUI();
|
||||||
|
|
||||||
|
// After updating UI, find and focus the new bookmark's title field
|
||||||
|
setTimeout(() => {
|
||||||
|
const newElement = document.querySelector(`[data-id="${newBookmark.id}"]`);
|
||||||
|
if (newElement) {
|
||||||
|
const titleInput = newElement.querySelector('.bookmark-title');
|
||||||
|
if (titleInput) {
|
||||||
|
titleInput.focus();
|
||||||
|
titleInput.select();
|
||||||
|
}
|
||||||
|
// Scroll to the new element
|
||||||
|
newElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
function findBookmark(bookmarkArray, id) {
|
||||||
|
for (const bookmark of bookmarkArray) {
|
||||||
|
if (bookmark.id === id) {
|
||||||
|
return bookmark;
|
||||||
|
}
|
||||||
|
if (bookmark.children.length > 0) {
|
||||||
|
const found = findBookmark(bookmark.children, id);
|
||||||
|
if (found) return found;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the parent bookmark of a given bookmark by ID
|
||||||
|
function findParentBookmark(bookmarkArray, id, parent = null) {
|
||||||
|
for (const bookmark of bookmarkArray) {
|
||||||
|
if (bookmark.id === id) {
|
||||||
|
return parent; // Return the parent ID (or null if top-level)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bookmark.children.length > 0) {
|
||||||
|
const found = findParentBookmark(bookmark.children, id, bookmark.id);
|
||||||
|
if (found !== undefined) return found;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined; // Not found at this level
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeBookmark(id) {
|
||||||
|
// Remove from top level
|
||||||
|
const index = bookmarks.findIndex(b => b.id === id);
|
||||||
|
if (index !== -1) {
|
||||||
|
bookmarks.splice(index, 1);
|
||||||
|
updateBookmarksUI();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove from children
|
||||||
|
function removeFromChildren(bookmarkArray, id) {
|
||||||
|
for (const bookmark of bookmarkArray) {
|
||||||
|
const childIndex = bookmark.children.findIndex(b => b.id === id);
|
||||||
|
if (childIndex !== -1) {
|
||||||
|
bookmark.children.splice(childIndex, 1);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (removeFromChildren(bookmark.children, id)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (removeFromChildren(bookmarks, id)) {
|
||||||
|
updateBookmarksUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no bookmarks left, show empty state
|
||||||
|
if (bookmarks.length === 0) {
|
||||||
|
showEmptyState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleBookmarkExpanded(id) {
|
||||||
|
const bookmark = findBookmark(bookmarks, id);
|
||||||
|
if (bookmark) {
|
||||||
|
bookmark.expanded = !bookmark.expanded;
|
||||||
|
updateBookmarksUI();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateBookmarkData() {
|
||||||
|
// Create a clean version without the IDs for submission
|
||||||
|
const cleanBookmarks = bookmarks.map(cleanBookmark);
|
||||||
|
bookmarkDataInput.value = JSON.stringify(cleanBookmarks);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanBookmark(bookmark) {
|
||||||
|
return {
|
||||||
|
title: bookmark.title,
|
||||||
|
pageNumber: bookmark.pageNumber,
|
||||||
|
children: bookmark.children.map(cleanBookmark)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateBookmarksUI() {
|
||||||
|
if (!bookmarksContainer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only clear the container if there are no error messages or loading indicators
|
||||||
|
if (!document.querySelector('#bookmarks-container .alert')) {
|
||||||
|
bookmarksContainer.innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there are bookmarks to display
|
||||||
|
if (bookmarks.length === 0 && !document.querySelector('.empty-bookmarks')) {
|
||||||
|
showEmptyState();
|
||||||
|
} else {
|
||||||
|
// Remove empty state if it exists and there are bookmarks
|
||||||
|
const emptyState = document.querySelector('.empty-bookmarks');
|
||||||
|
if (emptyState && bookmarks.length > 0) {
|
||||||
|
emptyState.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create bookmark elements
|
||||||
|
bookmarks.forEach(bookmark => {
|
||||||
|
const bookmarkElement = createBookmarkElement(bookmark);
|
||||||
|
bookmarksContainer.appendChild(bookmarkElement);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateBookmarkData();
|
||||||
|
|
||||||
|
// Initialize tooltips for dynamically added elements
|
||||||
|
if (typeof $ !== 'undefined') {
|
||||||
|
$('[data-bs-toggle="tooltip"]').tooltip();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the main bookmark element with collapsible interface
|
||||||
|
function createBookmarkElement(bookmark, level = 0) {
|
||||||
|
const bookmarkEl = document.createElement('div');
|
||||||
|
bookmarkEl.className = 'bookmark-item';
|
||||||
|
bookmarkEl.dataset.id = bookmark.id;
|
||||||
|
bookmarkEl.dataset.level = level;
|
||||||
|
|
||||||
|
// Create the header (always visible part)
|
||||||
|
const header = createBookmarkHeader(bookmark, level);
|
||||||
|
bookmarkEl.appendChild(header);
|
||||||
|
|
||||||
|
// Create the content (collapsible part)
|
||||||
|
const content = document.createElement('div');
|
||||||
|
content.className = 'bookmark-content';
|
||||||
|
if (!bookmark.expanded) {
|
||||||
|
content.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main input row
|
||||||
|
const inputRow = createInputRow(bookmark);
|
||||||
|
content.appendChild(inputRow);
|
||||||
|
bookmarkEl.appendChild(content);
|
||||||
|
|
||||||
|
// Add children container if has children and expanded
|
||||||
|
if (bookmark.children && bookmark.children.length > 0) {
|
||||||
|
const childrenContainer = createChildrenContainer(bookmark, level);
|
||||||
|
if (bookmark.expanded) {
|
||||||
|
bookmarkEl.appendChild(childrenContainer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bookmarkEl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the header that's always visible
|
||||||
|
function createBookmarkHeader(bookmark, level) {
|
||||||
|
const header = document.createElement('div');
|
||||||
|
header.className = 'bookmark-header';
|
||||||
|
if (!bookmark.expanded) {
|
||||||
|
header.classList.add('collapsed');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Left side of header with expand/collapse and info
|
||||||
|
const headerLeft = document.createElement('div');
|
||||||
|
headerLeft.className = 'd-flex align-items-center';
|
||||||
|
|
||||||
|
// Toggle expand/collapse icon with child count
|
||||||
|
const toggleContainer = document.createElement('div');
|
||||||
|
toggleContainer.className = 'd-flex align-items-center';
|
||||||
|
toggleContainer.style.marginRight = '8px';
|
||||||
|
|
||||||
|
// Only show toggle if has children
|
||||||
|
if (bookmark.children && bookmark.children.length > 0) {
|
||||||
|
// Create toggle icon
|
||||||
|
const toggleIcon = document.createElement('span');
|
||||||
|
toggleIcon.className = 'material-symbols-rounded toggle-icon me-1';
|
||||||
|
toggleIcon.textContent = 'expand_more';
|
||||||
|
toggleIcon.style.cursor = 'pointer';
|
||||||
|
toggleContainer.appendChild(toggleIcon);
|
||||||
|
|
||||||
|
// Add child count indicator
|
||||||
|
const childCount = document.createElement('span');
|
||||||
|
childCount.className = 'badge rounded-pill';
|
||||||
|
// Use theme-appropriate badge color
|
||||||
|
const isDarkMode = document.documentElement.getAttribute('data-bs-theme') === 'dark';
|
||||||
|
childCount.classList.add(isDarkMode ? 'bg-info' : 'bg-secondary');
|
||||||
|
childCount.style.fontSize = '0.7rem';
|
||||||
|
childCount.style.padding = '0.2em 0.5em';
|
||||||
|
childCount.textContent = bookmark.children.length;
|
||||||
|
childCount.setAttribute('data-bs-toggle', 'tooltip');
|
||||||
|
childCount.setAttribute('data-bs-placement', 'top');
|
||||||
|
childCount.title = `${bookmark.children.length} child bookmark${bookmark.children.length > 1 ? 's' : ''}`;
|
||||||
|
toggleContainer.appendChild(childCount);
|
||||||
|
} else {
|
||||||
|
// Add spacer if no children
|
||||||
|
const spacer = document.createElement('span');
|
||||||
|
spacer.style.width = '24px';
|
||||||
|
spacer.style.display = 'inline-block';
|
||||||
|
toggleContainer.appendChild(spacer);
|
||||||
|
}
|
||||||
|
|
||||||
|
headerLeft.appendChild(toggleContainer);
|
||||||
|
|
||||||
|
// Level indicator for nested items
|
||||||
|
if (level > 0) {
|
||||||
|
// Add relationship indicator visual line
|
||||||
|
const relationshipIndicator = document.createElement('div');
|
||||||
|
relationshipIndicator.className = 'bookmark-relationship-indicator';
|
||||||
|
|
||||||
|
const line = document.createElement('div');
|
||||||
|
line.className = 'relationship-line';
|
||||||
|
relationshipIndicator.appendChild(line);
|
||||||
|
|
||||||
|
const arrow = document.createElement('div');
|
||||||
|
arrow.className = 'relationship-arrow';
|
||||||
|
relationshipIndicator.appendChild(arrow);
|
||||||
|
|
||||||
|
header.appendChild(relationshipIndicator);
|
||||||
|
|
||||||
|
// Text indicator
|
||||||
|
const levelIndicator = document.createElement('span');
|
||||||
|
levelIndicator.className = 'bookmark-level-indicator';
|
||||||
|
levelIndicator.textContent = `Child`;
|
||||||
|
headerLeft.appendChild(levelIndicator);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Title preview
|
||||||
|
const titlePreview = document.createElement('span');
|
||||||
|
titlePreview.className = 'bookmark-title-preview';
|
||||||
|
titlePreview.textContent = bookmark.title;
|
||||||
|
headerLeft.appendChild(titlePreview);
|
||||||
|
|
||||||
|
// Page number preview
|
||||||
|
const pagePreview = document.createElement('span');
|
||||||
|
pagePreview.className = 'bookmark-page-preview';
|
||||||
|
pagePreview.textContent = `Page ${bookmark.pageNumber}`;
|
||||||
|
headerLeft.appendChild(pagePreview);
|
||||||
|
|
||||||
|
// Right side of header with action buttons
|
||||||
|
const headerRight = document.createElement('div');
|
||||||
|
headerRight.className = 'bookmark-actions-header';
|
||||||
|
|
||||||
|
// Quick add buttons with clear visual distinction - using Stirling-PDF's tooltip system
|
||||||
|
const quickAddChildButton = createButton('subdirectory_arrow_right', 'btn-add-child', 'Add child bookmark', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
addBookmark(bookmark.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
const quickAddSiblingButton = createButton('add', 'btn-add-sibling', 'Add sibling bookmark', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
// Find parent of current bookmark
|
||||||
|
const parentId = findParentBookmark(bookmarks, bookmark.id);
|
||||||
|
addBookmark(parentId, '', bookmark.pageNumber); // Same level as current bookmark
|
||||||
|
});
|
||||||
|
|
||||||
|
// Quick remove button
|
||||||
|
const quickRemoveButton = createButton('delete', 'btn-outline-danger', 'Remove bookmark', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
if (confirm('Are you sure you want to remove this bookmark' +
|
||||||
|
(bookmark.children.length > 0 ? ' and all its children?' : '?'))) {
|
||||||
|
removeBookmark(bookmark.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
headerRight.appendChild(quickAddChildButton);
|
||||||
|
headerRight.appendChild(quickAddSiblingButton);
|
||||||
|
headerRight.appendChild(quickRemoveButton);
|
||||||
|
|
||||||
|
// Assemble header
|
||||||
|
header.appendChild(headerLeft);
|
||||||
|
header.appendChild(headerRight);
|
||||||
|
|
||||||
|
// Add click handler for expansion toggle
|
||||||
|
header.addEventListener('click', function(e) {
|
||||||
|
// Only toggle if not clicking on buttons
|
||||||
|
if (!e.target.closest('button')) {
|
||||||
|
toggleBookmarkExpanded(bookmark.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return header;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createInputRow(bookmark) {
|
||||||
|
const row = document.createElement('div');
|
||||||
|
row.className = 'row';
|
||||||
|
|
||||||
|
// Title input
|
||||||
|
row.appendChild(createTitleInputElement(bookmark));
|
||||||
|
|
||||||
|
// Page input
|
||||||
|
row.appendChild(createPageInputElement(bookmark));
|
||||||
|
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTitleInputElement(bookmark) {
|
||||||
|
const titleCol = document.createElement('div');
|
||||||
|
titleCol.className = 'col-md-8';
|
||||||
|
|
||||||
|
const titleGroup = document.createElement('div');
|
||||||
|
titleGroup.className = 'mb-3';
|
||||||
|
|
||||||
|
const titleLabel = document.createElement('label');
|
||||||
|
titleLabel.textContent = 'Title';
|
||||||
|
titleLabel.className = 'form-label';
|
||||||
|
|
||||||
|
const titleInput = document.createElement('input');
|
||||||
|
titleInput.type = 'text';
|
||||||
|
titleInput.className = 'form-control bookmark-title';
|
||||||
|
titleInput.value = bookmark.title;
|
||||||
|
titleInput.addEventListener('input', function() {
|
||||||
|
bookmark.title = this.value;
|
||||||
|
updateBookmarkData();
|
||||||
|
|
||||||
|
// Also update the preview in the header
|
||||||
|
const header = titleInput.closest('.bookmark-item').querySelector('.bookmark-title-preview');
|
||||||
|
if (header) {
|
||||||
|
header.textContent = this.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
titleGroup.appendChild(titleLabel);
|
||||||
|
titleGroup.appendChild(titleInput);
|
||||||
|
titleCol.appendChild(titleGroup);
|
||||||
|
|
||||||
|
return titleCol;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createPageInputElement(bookmark) {
|
||||||
|
const pageCol = document.createElement('div');
|
||||||
|
pageCol.className = 'col-md-4';
|
||||||
|
|
||||||
|
const pageGroup = document.createElement('div');
|
||||||
|
pageGroup.className = 'mb-3';
|
||||||
|
|
||||||
|
const pageLabel = document.createElement('label');
|
||||||
|
pageLabel.textContent = 'Page';
|
||||||
|
pageLabel.className = 'form-label';
|
||||||
|
|
||||||
|
const pageInput = document.createElement('input');
|
||||||
|
pageInput.type = 'number';
|
||||||
|
pageInput.className = 'form-control bookmark-page';
|
||||||
|
pageInput.value = bookmark.pageNumber;
|
||||||
|
pageInput.min = 1;
|
||||||
|
pageInput.addEventListener('input', function() {
|
||||||
|
bookmark.pageNumber = parseInt(this.value) || 1;
|
||||||
|
updateBookmarkData();
|
||||||
|
|
||||||
|
// Also update the preview in the header
|
||||||
|
const header = pageInput.closest('.bookmark-item').querySelector('.bookmark-page-preview');
|
||||||
|
if (header) {
|
||||||
|
header.textContent = `Page ${bookmark.pageNumber}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
pageGroup.appendChild(pageLabel);
|
||||||
|
pageGroup.appendChild(pageInput);
|
||||||
|
pageCol.appendChild(pageGroup);
|
||||||
|
|
||||||
|
return pageCol;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createButton(icon, className, title, clickHandler) {
|
||||||
|
const button = document.createElement('button');
|
||||||
|
button.type = 'button';
|
||||||
|
button.className = `btn ${className} btn-bookmark-action`;
|
||||||
|
button.innerHTML = `<span class="material-symbols-rounded">${icon}</span>`;
|
||||||
|
|
||||||
|
// Use Bootstrap tooltips
|
||||||
|
button.setAttribute('data-bs-toggle', 'tooltip');
|
||||||
|
button.setAttribute('data-bs-placement', 'top');
|
||||||
|
button.title = title;
|
||||||
|
|
||||||
|
button.addEventListener('click', clickHandler);
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createChildrenContainer(bookmark, level) {
|
||||||
|
const childrenContainer = document.createElement('div');
|
||||||
|
childrenContainer.className = 'bookmark-children';
|
||||||
|
|
||||||
|
bookmark.children.forEach(child => {
|
||||||
|
childrenContainer.appendChild(createBookmarkElement(child, level + 1));
|
||||||
|
});
|
||||||
|
|
||||||
|
return childrenContainer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the add bookmark button appearance with clear visual cue
|
||||||
|
addBookmarkBtn.innerHTML = '<span class="material-symbols-rounded">add</span> Add Top-level Bookmark';
|
||||||
|
addBookmarkBtn.className = 'btn btn-primary btn-add-bookmark top-level';
|
||||||
|
|
||||||
|
// Use Bootstrap tooltips
|
||||||
|
addBookmarkBtn.setAttribute('data-bs-toggle', 'tooltip');
|
||||||
|
addBookmarkBtn.setAttribute('data-bs-placement', 'top');
|
||||||
|
addBookmarkBtn.title = 'Add a new top-level bookmark';
|
||||||
|
|
||||||
|
// Add icon to empty state button as well
|
||||||
|
const updateEmptyStateButton = function() {
|
||||||
|
const emptyStateBtn = document.querySelector('.btn-add-first-bookmark');
|
||||||
|
if (emptyStateBtn) {
|
||||||
|
emptyStateBtn.innerHTML = '<span class="material-symbols-rounded">add</span> Add First Bookmark';
|
||||||
|
emptyStateBtn.setAttribute('data-bs-toggle', 'tooltip');
|
||||||
|
emptyStateBtn.setAttribute('data-bs-placement', 'top');
|
||||||
|
emptyStateBtn.title = 'Add first bookmark';
|
||||||
|
|
||||||
|
// Initialize tooltips for the empty state button
|
||||||
|
if (typeof $ !== 'undefined') {
|
||||||
|
$('[data-bs-toggle="tooltip"]').tooltip();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize with an empty state if no bookmarks
|
||||||
|
if (bookmarks.length === 0) {
|
||||||
|
showEmptyState();
|
||||||
|
updateEmptyStateButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen for theme changes to update badge colors
|
||||||
|
const observer = new MutationObserver(function(mutations) {
|
||||||
|
mutations.forEach(function(mutation) {
|
||||||
|
if (mutation.attributeName === 'data-bs-theme') {
|
||||||
|
const isDarkMode = document.documentElement.getAttribute('data-bs-theme') === 'dark';
|
||||||
|
document.querySelectorAll('.badge').forEach(badge => {
|
||||||
|
badge.classList.remove('bg-secondary', 'bg-info');
|
||||||
|
badge.classList.add(isDarkMode ? 'bg-info' : 'bg-secondary');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(document.documentElement, { attributes: true });
|
||||||
|
|
||||||
|
// Add visual enhancement to clearly show the top-level/child relationship
|
||||||
|
document.addEventListener('mouseover', function(e) {
|
||||||
|
// When hovering over add buttons, highlight their relationship targets
|
||||||
|
const button = e.target.closest('.btn-add-child, .btn-add-sibling');
|
||||||
|
if (button) {
|
||||||
|
if (button.classList.contains('btn-add-child')) {
|
||||||
|
// Highlight parent-child relationship
|
||||||
|
const bookmarkItem = button.closest('.bookmark-item');
|
||||||
|
if (bookmarkItem) {
|
||||||
|
bookmarkItem.style.boxShadow = '0 0 0 2px var(--btn-add-child-border, #198754)';
|
||||||
|
}
|
||||||
|
} else if (button.classList.contains('btn-add-sibling')) {
|
||||||
|
// Highlight sibling relationship
|
||||||
|
const bookmarkItem = button.closest('.bookmark-item');
|
||||||
|
if (bookmarkItem) {
|
||||||
|
// Find siblings
|
||||||
|
const parent = bookmarkItem.parentElement;
|
||||||
|
const siblings = parent.querySelectorAll(':scope > .bookmark-item');
|
||||||
|
siblings.forEach(sibling => {
|
||||||
|
if (sibling !== bookmarkItem) {
|
||||||
|
sibling.style.boxShadow = '0 0 0 2px var(--btn-add-sibling-border, #0d6efd)';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('mouseout', function(e) {
|
||||||
|
// Remove highlights when not hovering
|
||||||
|
const button = e.target.closest('.btn-add-child, .btn-add-sibling');
|
||||||
|
if (button) {
|
||||||
|
// Remove all highlights
|
||||||
|
document.querySelectorAll('.bookmark-item').forEach(item => {
|
||||||
|
item.style.boxShadow = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
18511
stirling-pdf/src/main/resources/static/pdfjs-legacy/js/viewer.mjs
vendored
Normal file
18511
stirling-pdf/src/main/resources/static/pdfjs-legacy/js/viewer.mjs
vendored
Normal file
File diff suppressed because one or more lines are too long
24320
stirling-pdf/src/main/resources/static/pdfjs-legacy/pdf.mjs
vendored
Normal file
24320
stirling-pdf/src/main/resources/static/pdfjs-legacy/pdf.mjs
vendored
Normal file
File diff suppressed because it is too large
Load Diff
3796
stirling-pdf/src/main/resources/static/pdfjs-legacy/pdf.sandbox.mjs
vendored
Normal file
3796
stirling-pdf/src/main/resources/static/pdfjs-legacy/pdf.sandbox.mjs
vendored
Normal file
File diff suppressed because one or more lines are too long
61184
stirling-pdf/src/main/resources/static/pdfjs-legacy/pdf.worker.mjs
vendored
Normal file
61184
stirling-pdf/src/main/resources/static/pdfjs-legacy/pdf.worker.mjs
vendored
Normal file
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user