formatting

This commit is contained in:
Anthony Stirling 2025-05-14 16:10:30 +01:00
parent 018bc69270
commit 7de5043656
3 changed files with 864 additions and 381 deletions

View File

@ -0,0 +1,263 @@
/* Main bookmark container styles */
.bookmark-editor {
margin-top: 20px;
padding: 20px;
border: 1px solid var(--border-color, #ced4da);
border-radius: 0.25rem;
background-color: var(--bg-container, var(--md-sys-color-surface, #edf0f5));
}
[data-bs-theme="dark"] .bookmark-editor {
--border-color: #495057;
--bg-container: var(--md-sys-color-surface, #15202a);
}
/* Bookmark item styles */
.bookmark-item {
margin-bottom: 12px;
border: 1px solid var(--border-item, #e9ecef);
border-radius: 0.5rem;
background-color: var(--bg-item, var(--md-sys-color-surface-container-lowest, #ffffff));
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
overflow: hidden;
}
[data-bs-theme="dark"] .bookmark-item {
--border-item: var(--md-sys-color-surface-variant, #444b53);
--bg-item: var(--md-sys-color-surface-container-low, #24282e);
}
/* Bookmark header (collapsible part) */
.bookmark-header {
padding: 12px 15px;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
background-color: var(--bg-header, var(--md-sys-color-surface-container, #f1f3f5));
border-bottom: 1px solid var(--border-header, var(--md-sys-color-outline-variant, #e9ecef));
transition: background-color 0.15s ease;
}
.bookmark-header:hover {
background-color: var(--bg-header-hover, var(--md-sys-state-hover-opacity, #e9ecef));
}
[data-bs-theme="dark"] .bookmark-header {
--bg-header: var(--md-sys-color-surface-container-high, #3a424a);
--bg-header-hover: var(--md-sys-color-surface-container-highest, #434a52);
--border-header: var(--md-sys-color-outline-variant, #444b53);
}
/* Bookmark content (inside accordion) */
.bookmark-content {
padding: 15px;
border-top: 0;
}
/* Children container */
.bookmark-children {
margin-top: 10px;
margin-left: 20px;
padding-left: 15px;
border-left: 2px solid var(--border-children, #6c757d);
}
[data-bs-theme="dark"] .bookmark-children {
--border-children: #6c757d;
}
/* Level indicators */
.bookmark-level-indicator {
font-size: 0.8em;
color: var(--text-muted, #6c757d);
font-weight: 500;
text-transform: uppercase;
margin-right: 8px;
}
[data-bs-theme="dark"] .bookmark-level-indicator {
--text-muted: #adb5bd;
}
/* Button styles */
.btn-bookmark-action {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
border-radius: 4px;
padding: 0;
margin-right: 6px;
transition: all 0.2s ease;
position: relative;
}
.btn-bookmark-action:hover {
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
/* Visual distinction for child vs sibling buttons using theme colors */
.btn-add-child {
background-color: var(--btn-add-child-bg, var(--md-sys-color-surface-container-low, #e9ecef));
color: var(--btn-add-child-color, var(--md-nav-section-color-other, #72bd54));
border-color: var(--btn-add-child-border, var(--md-nav-section-color-other, #72bd54));
}
.btn-add-child::after {
content: "";
position: absolute;
left: -2px;
top: 50%;
width: 5px;
height: 1px;
background-color: var(--btn-add-child-border, var(--md-nav-section-color-other, #72bd54));
}
[data-bs-theme="dark"] .btn-add-child {
--btn-add-child-bg: var(--md-sys-color-surface-container, #28323a);
--btn-add-child-color: var(--favourite-add, #9ed18c);
--btn-add-child-border: var(--favourite-add, #9ed18c);
}
.btn-add-sibling {
background-color: var(--btn-add-sibling-bg, var(--md-sys-color-surface-container-low, #e9ecef));
color: var(--btn-add-sibling-color, var(--md-sys-color-primary, #0060aa));
border-color: var(--btn-add-sibling-border, var(--md-sys-color-primary, #0060aa));
}
[data-bs-theme="dark"] .btn-add-sibling {
--btn-add-sibling-bg: var(--md-sys-color-surface-container, #28323a);
--btn-add-sibling-color: var(--md-sys-color-primary, #a2c9ff);
--btn-add-sibling-border: var(--md-sys-color-primary, #a2c9ff);
}
.bookmark-actions-header {
display: flex;
margin-left: 8px;
}
.bookmark-actions-content {
display: flex;
justify-content: space-between;
margin-top: 10px;
padding-top: 10px;
border-top: 1px dashed var(--border-subtle-color, #dee2e6);
}
[data-bs-theme="dark"] .bookmark-actions-content {
--border-subtle-color: #495057;
}
/* Main actions section */
.bookmark-actions {
margin-top: 20px;
display: flex;
justify-content: flex-start;
}
/* Collapse/expand icons */
.toggle-icon {
transition: transform 0.3s ease;
}
.collapsed .toggle-icon {
transform: rotate(-90deg);
}
/* Title and page display in header */
.bookmark-title-preview {
font-weight: 500;
margin-right: 10px;
color: var(--text-primary, #212529);
}
[data-bs-theme="dark"] .bookmark-title-preview {
--text-primary: #e9ecef;
}
.bookmark-page-preview {
font-size: 0.9em;
color: var(--text-secondary, #6c757d);
}
[data-bs-theme="dark"] .bookmark-page-preview {
--text-secondary: #adb5bd;
}
/* We've removed the drag handle since it's not functional */
/* Add button at the top level */
.btn-add-bookmark {
display: flex;
align-items: center;
gap: 8px;
font-weight: 500;
position: relative;
padding-left: 24px;
}
.btn-add-bookmark::before {
content: "";
position: absolute;
left: 12px;
top: 0;
width: 2px;
height: 50%;
background-color: currentColor;
}
.btn-add-bookmark.top-level::before {
display: none;
}
/* Relationship indicators */
.bookmark-relationship-indicator {
position: absolute;
left: -15px;
top: 0;
height: 100%;
width: 14px;
pointer-events: none;
}
.relationship-line {
position: absolute;
left: 7px;
top: 0;
height: 100%;
width: 2px;
background-color: var(--relationship-color, var(--md-nav-section-color-other, #72bd54));
}
.relationship-arrow {
position: absolute;
left: 5px;
top: 50%;
width: 8px;
height: 2px;
background-color: var(--relationship-color, var(--md-nav-section-color-other, #72bd54));
}
[data-bs-theme="dark"] .bookmark-relationship-indicator {
--relationship-color: var(--favourite-add, #9ed18c);
}
/* Empty state */
.empty-bookmarks {
padding: 30px;
text-align: center;
color: var(--text-muted, var(--md-sys-color-on-surface-variant, #6c757d));
background-color: var(--bg-empty, var(--md-sys-color-surface-container-lowest, #ffffff));
border-radius: 0.375rem;
border: 1px dashed var(--border-empty, var(--md-sys-color-outline, #ced4da));
}
[data-bs-theme="dark"] .empty-bookmarks {
--text-muted: var(--md-sys-color-on-surface-variant, #adb5bd);
--bg-empty: var(--md-sys-color-surface-container-low, #24282e);
--border-empty: var(--md-sys-color-outline, #495057);
}

View File

@ -0,0 +1,599 @@
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: true // All bookmarks start expanded
};
// 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: true
};
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();
}
// 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
const toggleIcon = document.createElement('span');
toggleIcon.className = 'material-symbols-rounded toggle-icon me-2';
toggleIcon.textContent = 'expand_more';
toggleIcon.style.cursor = 'pointer';
// Only show toggle if has children
if (bookmark.children && bookmark.children.length > 0) {
headerLeft.appendChild(toggleIcon);
} else {
// Add spacer if no children
const spacer = document.createElement('span');
spacer.style.width = '24px';
spacer.style.display = 'inline-block';
headerLeft.appendChild(spacer);
}
// 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
const quickAddChildButton = createButton('subdirectory_arrow_right', 'btn-add-child', 'Add child bookmark (nested under this)', function(e) {
e.preventDefault();
e.stopPropagation();
addBookmark(bookmark.id);
});
const quickAddSiblingButton = createButton('add', 'btn-add-sibling', 'Add sibling bookmark (same level)', 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>`;
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';
// Add tooltip to better explain it
addBookmarkBtn.title = 'Add a new top-level bookmark (not nested under any other 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.title = 'Add your first bookmark to the document';
}
};
// Initialize with an empty state if no bookmarks
if (bookmarks.length === 0) {
showEmptyState();
updateEmptyStateButton();
}
// 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 = '';
});
}
});
});

View File

@ -5,46 +5,7 @@
<head> <head>
<th:block th:insert="~{fragments/common :: head(title=#{editTableOfContents.title}, header=#{editTableOfContents.header})}"> <th:block th:insert="~{fragments/common :: head(title=#{editTableOfContents.title}, header=#{editTableOfContents.header})}">
</th:block> </th:block>
<style> <link rel="stylesheet" th:href="@{'/css/edit-table-of-contents.css'}">
.bookmark-item {
padding: 15px;
margin-bottom: 10px;
border: 1px solid #ced4da;
border-radius: 0.25rem;
background-color: rgba(0, 0, 0, 0.02);
}
.bookmark-children {
margin-left: 30px;
margin-top: 10px;
}
.bookmark-actions {
margin-top: 20px;
display: flex;
justify-content: flex-start;
}
.bookmark-editor {
margin-top: 20px;
padding: 20px;
border: 1px solid #ced4da;
border-radius: 0.25rem;
}
.btn-bookmark-action {
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
border-radius: 50%;
padding: 0;
margin-right: 8px;
}
.bookmark-item-header {
font-weight: bold;
margin-bottom: 10px;
color: #666;
}
</style>
</head> </head>
<body> <body>
@ -108,347 +69,7 @@
</div> </div>
<script th:src="@{'/js/fetch-utils.js'}"></script> <script th:src="@{'/js/fetch-utils.js'}"></script>
<script> <script th:src="@{'/js/pages/edit-table-of-contents.js'}"></script>
document.addEventListener('DOMContentLoaded', function() {
console.log('DOM fully loaded - initializing bookmark editor');
// For debugging: Print all DOM elements we'll interact with
console.log('Bookmarks container element:', document.getElementById('bookmarks-container'));
console.log('Add bookmark button element:', document.getElementById('addBookmarkBtn'));
console.log('Form element:', document.getElementById('editTocForm'));
const bookmarksContainer = document.getElementById('bookmarks-container');
const addBookmarkBtn = document.getElementById('addBookmarkBtn');
const bookmarkDataInput = document.getElementById('bookmarkData');
let bookmarks = [];
// Add event listener to the file input to extract existing bookmarks
document.getElementById('fileInput-input').addEventListener('change', async function(e) {
try {
if (!e.target.files || e.target.files.length === 0) {
console.log('No file selected');
return;
}
console.log('File selected, extracting bookmarks');
// Reset bookmarks initially
bookmarks = [];
updateBookmarksUI();
const formData = new FormData();
formData.append('file', e.target.files[0]);
// Show loading indicator
const loadingEl = document.createElement('div');
loadingEl.className = 'alert alert-info';
loadingEl.textContent = 'Loading bookmarks from PDF...';
loadingEl.id = 'loading-bookmarks';
bookmarksContainer.appendChild(loadingEl);
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();
console.log('Extracted bookmarks:', extractedBookmarks);
// Convert extracted bookmarks to our format with IDs
if (extractedBookmarks && extractedBookmarks.length > 0) {
bookmarks = extractedBookmarks.map(convertExtractedBookmark);
console.log('Converted bookmarks:', bookmarks);
} else {
console.log('No bookmarks found in the PDF');
// If no bookmarks found, add a default one
addBookmark(null, 'Bookmark 1', 1);
}
} catch (error) {
console.error('Error extracting bookmarks:', error);
// Show error message
const errorEl = document.createElement('div');
errorEl.className = 'alert alert-danger';
errorEl.textContent = 'Failed to extract bookmarks. You can still create new ones.';
bookmarksContainer.appendChild(errorEl);
// Add a default bookmark
addBookmark(null, 'Bookmark 1', 1);
} finally {
// Remove loading indicator
const loadingEl = document.getElementById('loading-bookmarks');
if (loadingEl) {
loadingEl.remove();
}
// Update the UI
updateBookmarksUI();
}
} catch (error) {
console.error('Error in file input change handler:', error);
}
});
// Function to convert extracted bookmarks to our format with IDs
function convertExtractedBookmark(bookmark) {
const result = {
id: Date.now() + Math.floor(Math.random() * 1000), // Generate a unique ID
title: bookmark.title || 'Untitled Bookmark',
pageNumber: bookmark.pageNumber || 1,
children: []
};
// Convert children recursively
if (bookmark.children && bookmark.children.length > 0) {
result.children = bookmark.children.map(child => {
// Add a small delay to each ID to ensure uniqueness
setTimeout(() => {}, 1);
return convertExtractedBookmark(child);
});
}
return result;
}
// Add bookmark button click handler
addBookmarkBtn.addEventListener('click', function(e) {
e.preventDefault(); // Prevent any default action
console.log('Add bookmark button clicked');
addBookmark();
});
// Add form submit handler to update JSON data
document.getElementById('editTocForm').addEventListener('submit', function(e) {
updateBookmarkData();
});
function addBookmark(parent = null, title = '', pageNumber = 1) {
try {
console.log('Adding bookmark:', { parent, title, pageNumber });
const newBookmark = {
id: Date.now(), // Generate a unique ID for the DOM element
title: title || 'New Bookmark', // Ensure we have a default title
pageNumber: pageNumber || 1, // Ensure we have a default page number
children: []
};
if (parent === null) {
bookmarks.push(newBookmark);
console.log('Added bookmark to root level:', newBookmark);
} else {
const parentBookmark = findBookmark(bookmarks, parent);
if (parentBookmark) {
parentBookmark.children.push(newBookmark);
console.log('Added bookmark as child of:', parentBookmark.title);
} else {
console.error('Parent bookmark not found with ID:', parent);
// Add to root level if parent not found
bookmarks.push(newBookmark);
console.log('Added bookmark to root level (parent not found):', newBookmark);
}
}
updateBookmarksUI();
} catch (error) {
console.error('Error adding bookmark:', error);
}
}
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;
}
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();
}
}
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() {
try {
console.log('Updating bookmarks UI with:', bookmarks);
if (!bookmarksContainer) {
console.error('bookmarksContainer element not found');
return;
}
bookmarksContainer.innerHTML = '';
bookmarks.forEach(bookmark => {
const bookmarkElement = createBookmarkElement(bookmark);
bookmarksContainer.appendChild(bookmarkElement);
console.log('Added bookmark element to container:', bookmark.title);
});
updateBookmarkData();
} catch (error) {
console.error('Error updating bookmarks UI:', error);
}
}
function createBookmarkElement(bookmark, level = 0) {
const bookmarkEl = document.createElement('div');
bookmarkEl.className = 'bookmark-item';
bookmarkEl.dataset.id = bookmark.id;
const bookmarkContent = document.createElement('div');
bookmarkContent.className = 'bookmark-content';
// Add a header for the bookmark
const header = document.createElement('div');
header.className = 'bookmark-item-header';
header.textContent = level === 0 ? 'Top-level Bookmark' : `Nested Bookmark (Level ${level})`;
bookmarkContent.appendChild(header);
const row = document.createElement('div');
row.className = 'row';
// Title input
const titleCol = document.createElement('div');
titleCol.className = 'col-md-6';
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();
});
titleGroup.appendChild(titleLabel);
titleGroup.appendChild(titleInput);
titleCol.appendChild(titleGroup);
row.appendChild(titleCol);
// Page number input
const pageCol = document.createElement('div');
pageCol.className = 'col-md-3';
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();
});
pageGroup.appendChild(pageLabel);
pageGroup.appendChild(pageInput);
pageCol.appendChild(pageGroup);
row.appendChild(pageCol);
// Buttons
const buttonCol = document.createElement('div');
buttonCol.className = 'col-md-3';
const buttonGroup = document.createElement('div');
buttonGroup.className = 'mb-3 d-flex align-items-end h-100';
const addChildBtn = document.createElement('button');
addChildBtn.type = 'button';
addChildBtn.className = 'btn btn-outline-success btn-bookmark-action';
addChildBtn.innerHTML = '<span class="material-symbols-rounded">add</span>';
addChildBtn.title = 'Add child bookmark';
addChildBtn.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
addBookmark(bookmark.id);
});
const removeBtn = document.createElement('button');
removeBtn.type = 'button';
removeBtn.className = 'btn btn-outline-danger btn-bookmark-action';
removeBtn.innerHTML = '<span class="material-symbols-rounded">close</span>';
removeBtn.title = 'Remove bookmark';
removeBtn.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
removeBookmark(bookmark.id);
});
buttonGroup.appendChild(addChildBtn);
buttonGroup.appendChild(removeBtn);
buttonCol.appendChild(buttonGroup);
row.appendChild(buttonCol);
bookmarkContent.appendChild(row);
bookmarkEl.appendChild(bookmarkContent);
// Children container
if (bookmark.children && bookmark.children.length > 0) {
const childrenContainer = document.createElement('div');
childrenContainer.className = 'bookmark-children';
bookmark.children.forEach(child => {
childrenContainer.appendChild(createBookmarkElement(child, level + 1));
});
bookmarkEl.appendChild(childrenContainer);
}
return bookmarkEl;
}
// Initialize with an empty bookmark if no file is loaded initially
if (bookmarks.length === 0) {
console.log('Initializing with an empty bookmark');
addBookmark(null, 'Bookmark 1', 1);
console.log('Bookmarks after initialization:', bookmarks);
}
});
</script>
</body> </body>
</html> </html>