mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-09-13 04:08:22 +02:00
Ensure Pixel gets disabled, PDF ToC support (#3659)
# Description of Changes Please provide a summary of the changes, including: - What was changed - Why the change was made - Any challenges encountered Closes #(issue_number) --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#6-testing) for more details. --------- Co-authored-by: Dario Ghunney Ware <[email protected]> Co-authored-by: Connor Yoh <[email protected]> Co-authored-by: a <a> Co-authored-by: Reece <[email protected]>
This commit is contained in:
co-authored by
Dario Ghunney Ware
Connor Yoh
a
Reece
parent
bdc35519da
commit
1f2365f03c
@@ -0,0 +1,276 @@
|
||||
/* 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, var(--md-sys-color-on-surface, #212529));
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .bookmark-title-preview {
|
||||
--text-primary: var(--md-sys-color-on-surface, #e9ecef);
|
||||
}
|
||||
|
||||
.bookmark-page-preview {
|
||||
font-size: 0.9em;
|
||||
color: var(--text-secondary, var(--md-sys-color-on-surface-variant, #6c757d));
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .bookmark-page-preview {
|
||||
--text-secondary: var(--md-sys-color-on-surface-variant, #adb5bd);
|
||||
}
|
||||
|
||||
/* Input text colors */
|
||||
.bookmark-content input,
|
||||
.bookmark-content label {
|
||||
color: var(--input-text, var(--md-sys-color-on-surface, #212529));
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .bookmark-content input,
|
||||
[data-bs-theme="dark"] .bookmark-content label {
|
||||
--input-text: var(--md-sys-color-on-surface, #e9ecef);
|
||||
background-color: var(--input-bg, var(--md-sys-color-surface-container-high, #3a424a));
|
||||
border-color: var(--input-border, var(--md-sys-color-outline, #495057));
|
||||
}
|
||||
|
||||
/* 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);
|
||||
}
|
||||
@@ -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 = '';
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user