init teams and toc

This commit is contained in:
Anthony Stirling 2025-05-14 12:11:06 +01:00
parent 1471f80199
commit 097134af2d
8 changed files with 760 additions and 22 deletions

21
LICENSE
View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2024 Stirling Tools
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,262 @@
package stirling.software.SPDF.controller.api;
import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDDocumentOutline;
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlineItem;
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlineNode;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.EditTableOfContentsRequest;
import stirling.software.SPDF.service.CustomPDFDocumentFactory;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@RequestMapping("/api/v1/general")
@Slf4j
@Tag(name = "General", description = "General APIs")
@RequiredArgsConstructor
public class EditTableOfContentsController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final ObjectMapper objectMapper;
@PostMapping(value = "/extract-bookmarks", consumes = "multipart/form-data")
@Operation(
summary = "Extract PDF Bookmarks",
description = "Extracts bookmarks/table of contents from a PDF document as JSON.")
@ResponseBody
public List<Map<String, Object>> extractBookmarks(@RequestParam("file") MultipartFile file)
throws Exception {
PDDocument document = null;
try {
document = pdfDocumentFactory.load(file);
PDDocumentOutline outline = document.getDocumentCatalog().getDocumentOutline();
if (outline == null) {
log.info("No outline/bookmarks found in PDF");
return new ArrayList<>();
}
return extractBookmarkItems(document, outline);
} finally {
if (document != null) {
document.close();
}
}
}
private List<Map<String, Object>> extractBookmarkItems(PDDocument document, PDDocumentOutline outline) throws Exception {
List<Map<String, Object>> bookmarks = new ArrayList<>();
PDOutlineItem current = outline.getFirstChild();
while (current != null) {
Map<String, Object> bookmark = new HashMap<>();
// Get bookmark title
String title = current.getTitle();
bookmark.put("title", title);
// Get page number (1-based for UI purposes)
PDPage page = current.findDestinationPage(document);
if (page != null) {
int pageIndex = document.getPages().indexOf(page);
bookmark.put("pageNumber", pageIndex + 1);
} else {
bookmark.put("pageNumber", 1);
}
// Process children if any
PDOutlineItem child = current.getFirstChild();
if (child != null) {
List<Map<String, Object>> children = new ArrayList<>();
PDOutlineNode parent = current;
while (child != null) {
// Recursively process child items
Map<String, Object> childBookmark = processChild(document, child);
children.add(childBookmark);
child = child.getNextSibling();
}
bookmark.put("children", children);
} else {
bookmark.put("children", new ArrayList<>());
}
bookmarks.add(bookmark);
current = current.getNextSibling();
}
return bookmarks;
}
private Map<String, Object> processChild(PDDocument document, PDOutlineItem item) throws Exception {
Map<String, Object> bookmark = new HashMap<>();
// Get bookmark title
String title = item.getTitle();
bookmark.put("title", title);
// Get page number (1-based for UI purposes)
PDPage page = item.findDestinationPage(document);
if (page != null) {
int pageIndex = document.getPages().indexOf(page);
bookmark.put("pageNumber", pageIndex + 1);
} else {
bookmark.put("pageNumber", 1);
}
// Process children if any
PDOutlineItem child = item.getFirstChild();
if (child != null) {
List<Map<String, Object>> children = new ArrayList<>();
while (child != null) {
// Recursively process child items
Map<String, Object> childBookmark = processChild(document, child);
children.add(childBookmark);
child = child.getNextSibling();
}
bookmark.put("children", children);
} else {
bookmark.put("children", new ArrayList<>());
}
return bookmark;
}
@PostMapping(value = "/edit-table-of-contents", consumes = "multipart/form-data")
@Operation(
summary = "Edit Table of Contents",
description = "Add or edit bookmarks/table of contents in a PDF document.")
public ResponseEntity<byte[]> editTableOfContents(@ModelAttribute EditTableOfContentsRequest request)
throws Exception {
MultipartFile file = request.getFileInput();
PDDocument document = null;
try {
document = pdfDocumentFactory.load(file);
// Parse the bookmark data from JSON
List<BookmarkItem> bookmarks = objectMapper.readValue(
request.getBookmarkData(),
new TypeReference<List<BookmarkItem>>() {});
// Create a new document outline
PDDocumentOutline outline = new PDDocumentOutline();
document.getDocumentCatalog().setDocumentOutline(outline);
// Add bookmarks to the outline
addBookmarksToOutline(document, outline, bookmarks);
// Save the document to a byte array
ByteArrayOutputStream baos = new ByteArrayOutputStream();
document.save(baos);
String filename = file.getOriginalFilename().replaceFirst("[.][^.]+$", "");
return WebResponseUtils.bytesToWebResponse(
baos.toByteArray(), filename + "_with_toc.pdf", MediaType.APPLICATION_PDF);
} finally {
if (document != null) {
document.close();
}
}
}
private void addBookmarksToOutline(PDDocument document, PDDocumentOutline outline, List<BookmarkItem> bookmarks) {
for (BookmarkItem bookmark : bookmarks) {
PDOutlineItem item = createOutlineItem(document, bookmark);
outline.addLast(item);
if (bookmark.getChildren() != null && !bookmark.getChildren().isEmpty()) {
addChildBookmarks(document, item, bookmark.getChildren());
}
}
}
private void addChildBookmarks(PDDocument document, PDOutlineItem parent, List<BookmarkItem> children) {
for (BookmarkItem child : children) {
PDOutlineItem item = createOutlineItem(document, child);
parent.addLast(item);
if (child.getChildren() != null && !child.getChildren().isEmpty()) {
addChildBookmarks(document, item, child.getChildren());
}
}
}
private PDOutlineItem createOutlineItem(PDDocument document, BookmarkItem bookmark) {
PDOutlineItem item = new PDOutlineItem();
item.setTitle(bookmark.getTitle());
// Get the target page - adjust for 0-indexed pages in PDFBox
int pageIndex = bookmark.getPageNumber() - 1;
if (pageIndex < 0) {
pageIndex = 0;
} else if (pageIndex >= document.getNumberOfPages()) {
pageIndex = document.getNumberOfPages() - 1;
}
PDPage page = document.getPage(pageIndex);
item.setDestination(page);
return item;
}
// Inner class to represent bookmarks in JSON
public static class BookmarkItem {
private String title;
private int pageNumber;
private List<BookmarkItem> children = new ArrayList<>();
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public int getPageNumber() {
return pageNumber;
}
public void setPageNumber(int pageNumber) {
this.pageNumber = pageNumber;
}
public List<BookmarkItem> getChildren() {
return children;
}
public void setChildren(List<BookmarkItem> children) {
this.children = children;
}
}
}

View File

@ -122,6 +122,13 @@ public class GeneralWebController {
model.addAttribute("currentPage", "split-pdf-by-chapters");
return "split-pdf-by-chapters";
}
@GetMapping("/edit-table-of-contents")
@Hidden
public String editTableOfContents(Model model) {
model.addAttribute("currentPage", "edit-table-of-contents");
return "edit-table-of-contents";
}
@GetMapping("/view-pdf")
@Hidden

View File

@ -0,0 +1,17 @@
package stirling.software.SPDF.model.api;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = false)
public class EditTableOfContentsRequest extends PDFFile {
@Schema(description = "Bookmark structure in JSON format", example = "[{\"title\":\"Chapter 1\",\"pageNumber\":1,\"children\":[{\"title\":\"Section 1.1\",\"pageNumber\":2}]}]")
private String bookmarkData;
@Schema(description = "Whether to replace existing bookmarks or append to them", example = "true")
private Boolean replaceExisting;
}

View File

@ -1462,3 +1462,19 @@ cookieBanner.preferencesModal.necessary.description=These cookies are essential
cookieBanner.preferencesModal.analytics.title=Analytics
cookieBanner.preferencesModal.analytics.description=These cookies help us understand how our tools are being used, so we can focus on building the features our community values most. Rest assured—Stirling PDF cannot and will never track the content of the documents you work with.
# Table of Contents Feature
home.editTableOfContents.title=Edit Table of Contents
home.editTableOfContents.desc=Add or edit bookmarks and table of contents in PDF documents
editTableOfContents.tags=bookmarks,toc,navigation,index,table of contents,chapters,sections,outline
editTableOfContents.title=Edit Table of Contents
editTableOfContents.header=Add or Edit PDF Table of Contents
editTableOfContents.replaceExisting=Replace existing bookmarks (uncheck to append to existing)
editTableOfContents.editorTitle=Bookmark Editor
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
editTableOfContents.addBookmark=Add New Bookmark
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
editTableOfContents.desc.3=Each bookmark requires a title and target page number.
editTableOfContents.submit=Apply Table of Contents
EOL < /dev/null

View File

@ -0,0 +1,454 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{editTableOfContents.title}, header=#{editTableOfContents.header})}">
</th:block>
<style>
.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>
<body>
<div id="page-container">
<div id="content-wrap">
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<br><br>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8 bg-card">
<div class="tool-header">
<span class="material-symbols-rounded tool-header-icon edit">bookmark_add</span>
<span class="tool-header-text" th:text="#{editTableOfContents.header}"></span>
</div>
<form th:action="@{'/api/v1/general/edit-table-of-contents'}" method="post" enctype="multipart/form-data" id="editTocForm">
<div
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}">
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="replaceExisting" name="replaceExisting" checked>
<label class="form-check-label" for="replaceExisting"
th:text="#{editTableOfContents.replaceExisting}"></label>
<input type="hidden" name="replaceExisting" value="false" />
</div>
<div class="bookmark-editor">
<h5 th:text="#{editTableOfContents.editorTitle}"></h5>
<p th:text="#{editTableOfContents.editorDesc}"></p>
<div id="bookmarks-container">
<!-- Bookmarks will be added here dynamically -->
</div>
<div class="bookmark-actions">
<button type="button" id="addBookmarkBtn" class="btn btn-outline-primary" th:text="#{editTableOfContents.addBookmark}"></button>
</div>
<!-- Hidden field to store JSON data -->
<input type="hidden" id="bookmarkData" name="bookmarkData" value="[]">
</div>
<p>
<a class="btn btn-outline-primary" data-bs-toggle="collapse" href="#info" role="button"
aria-expanded="false" aria-controls="info" th:text="#{info}"></a>
</p>
<div class="collapse" id="info">
<p th:text="#{editTableOfContents.desc.1}"></p>
<p th:text="#{editTableOfContents.desc.2}"></p>
<p th:text="#{editTableOfContents.desc.3}"></p>
</div>
<br>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{editTableOfContents.submit}"></button>
</form>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
<script th:src="@{'/js/fetch-utils.js'}"></script>
<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>
</html>

View File

@ -276,6 +276,9 @@
<div
th:replace="~{fragments/navbarEntryCustom :: navbarEntry('split-pdf-by-chapters', '/images/split-chapters.svg#icon-split-chapters', 'home.splitPdfByChapters.title', 'home.splitPdfByChapters.desc', 'splitPdfByChapters.tags', 'advance')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('edit-table-of-contents', 'bookmark_add', 'home.editTableOfContents.title', 'home.editTableOfContents.desc', 'editTableOfContents.tags', 'advance')}">
</div>
</div>
</div>
</th:block>

View File

@ -37,7 +37,7 @@
</div>
<div class="recent-features">
<div class="newfeature"
th:insert="~{fragments/navbarEntryCustom :: navbarEntry('redact', '/images/redact-manual.svg#icon-redact-manual', 'home.redact.title', 'home.redact.desc', 'redact.tags', 'security')}">
th:insert="~{fragments/navbarEntry :: navbarEntry('edit-table-of-contents', 'bookmark_add', 'home.editTableOfContents.title', 'home.editTableOfContents.desc', 'editTableOfContents.tags', 'advance')}">
</div>
<div class="newfeature"
th:insert="~{fragments/navbarEntry :: navbarEntry ('multi-tool', 'construction', 'home.multiTool.title', 'home.multiTool.desc', 'multiTool.tags', 'organize')}">