mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-08-02 13:48:15 +02:00
formatting
This commit is contained in:
parent
018bc69270
commit
7de5043656
263
src/main/resources/static/css/edit-table-of-contents.css
Normal file
263
src/main/resources/static/css/edit-table-of-contents.css
Normal 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);
|
||||||
|
}
|
599
src/main/resources/static/js/pages/edit-table-of-contents.js
Normal file
599
src/main/resources/static/js/pages/edit-table-of-contents.js
Normal 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 = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
@ -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>
|
Loading…
Reference in New Issue
Block a user