DEV Community

Xiao Ling
Xiao Ling

Posted on • Originally published at dynamsoft.com

Building a Document Management App with Split, Merge, and PDF Export using HTML5 and JavaScript

A modern web-based document management application is essential for organizations looking to digitize their paper-based workflows and streamline document processing. In this tutorial, we'll build a professional document management application that demonstrates the four core features essential for modern document processing: scan, split, merge through drag-and-drop, and save as PDF. Our app will feature a modern UI design and be built entirely with HTML5 and JavaScript using the Dynamic Web TWAIN.

Demo Video

Online Demo

https://yushulx.me/web-twain-document-scan-management/examples/split_merge_document/

Prerequisites

Primary Features We'll Build

Our document management application focuses on four essential document processing capabilities:

1. Document Scanning

  • Direct scanning from TWAIN-compatible scanners
  • Automatic thumbnail generation

2. Document Splitting

  • Split multi-page documents at any point
  • Create separate document groups

3. Drag-and-Drop Merging

  • Intuitive page reordering within documents
  • Cross-document page movement for merging
  • Visual feedback during drag operations

4. PDF Generation

  • Save documents as multi-page PDFs

Step 1: Set Up the HTML Foundation

First, let's create our main HTML file with the basic structure:

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document Management App - Dynamsoft</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <script type="text/javascript" src="https://unpkg.com/dwt/dist/dynamsoft.webtwain.min.js"></script> <link href="css/index.css" rel="stylesheet" /> </head> <body> <!-- License Activation Overlay --> <div id="licenseOverlay" class="license-overlay"> </div> <div id="appContainer" class="app-container"> </div> </body> </html> 
Enter fullscreen mode Exit fullscreen mode

Key Points:

  • We're loading the Dynamic Web TWAIN from CDN
  • The structure separates license activation from the main app

Step 2: Create the License Activation Interface

Add the license activation overlay inside the licenseOverlay div:

<div id="licenseOverlay" class="license-overlay"> <div class="license-card"> <div class="license-header"> <h1>🚀 Activate Your License</h1> <p>Enter your Dynamsoft license key to get started with full features</p> </div> <div class="trial-info"> <h3>📝 Need a License Key?</h3> <p>Get a free trial license key to unlock all features and start scanning documents right away.</p> <a href="https://www.dynamsoft.com/customer/license/trialLicense/?product=dcv&package=cross-platform" target="_blank" rel="noopener noreferrer" class="trial-link">Apply for Free Trial License →</a> </div> <form class="license-form" id="licenseForm"> <div class="form-group"> <label for="licenseKey" class="form-label">License Key</label> <input type="text" id="licenseKey" class="form-input" placeholder="Enter your license key here..." spellcheck="false"> </div> <div class="button-group"> <button type="submit" class="btn btn-primary" id="activateBtn"> <span id="activateText">Activate License</span> <span id="activateSpinner" class="loading-spinner ds-hidden"></span> </button> <button type="button" class="btn btn-secondary" id="useTrialBtn"> Use Trial License </button> </div> </form> </div> </div> 
Enter fullscreen mode Exit fullscreen mode

Step 3: Build the Main Application Interface

Create the main application interface:

<div id="appContainer" class="app-container"> <div class="app-header"> <h1 class="app-title">📄 Document Management</h1> <div class="license-status"> <span class="status-indicator"></span> <span id="licenseStatusText">License Active</span> </div> </div> <div class="container"> <div class="sidebar"> <div class="sidebar-section"> <h3 class="section-title">🎯 Actions</h3> <div class="action-buttons"> <button class="action-btn" id="btnAcquireImage" onclick="DWTManager.acquireImage();"> 📷 Scan Document </button> <button class="action-btn" id="btnLoadImage" onclick="DWTManager.loadImage();"> 📁 Load Images </button> </div> </div> <div class="sidebar-section files"> <h3 class="section-title">📋 Files</h3> <ul class="file-list"> <li data-group="group-1" class="initial-hidden"> <div class="file-info"> <div class="title-name"></div> <em><div class="page-number"></div></em> </div> <label class="checkbox-label"> <input type="checkbox" checked data-group="group-1"> <span class="visually-hidden">Show/hide document group</span> </label> </li> </ul> </div> </div> <div class="main"> <div class="ds-imagebox-wrapper"> <div id="imagebox-1" docID="1" class="doc initial-hidden" data-group="group-1"> <div class="doc-title"> <span class="title-name"></span> <div class="doc-buttons"> <button onclick="FileManager.save(this);">💾 Save File</button> <button onclick="FileManager.delete(this);">🗑️ Delete</button> </div> </div> <div class="ds-imagebox mt10 thumbnails"></div> </div> </div> <div class="dwt-container h600"> <div id="dwtcontrolContainer" class="h600"></div> </div> </div> </div> </div> 
Enter fullscreen mode Exit fullscreen mode

Step 4: Declare JavaScript Variables and Constants

Set up the JavaScript variables and constants that will be used throughout the application:

'use strict'; const DEFAULT_LICENSE = "YOUR_TRIAL_LICENSE_KEY_HERE"; const STORAGE_KEY = 'dynamsoft_license_key'; const NOTIFICATION_DURATION = 3000; let currentLicenseKey = null; let isLicenseActivated = false; let DWTObject = null; let imageCount = 0; let pdfName = ''; Dynamsoft.DWT.ResourcesPath = 'https://unpkg.com/dwt/dist/'; 
Enter fullscreen mode Exit fullscreen mode

Note: To load the Dynamic Web TWAIN SDK correctly, the Dynamsoft.DWT.ResourcesPath must point to the location of the SDK files.

Step 5: Build the License Manager

Create the license management module:

const LicenseManager = { init() { this.bindEvents(); this.checkStoredLicense(); }, bindEvents() { const licenseForm = document.getElementById('licenseForm'); const useTrialBtn = document.getElementById('useTrialBtn'); licenseForm.addEventListener('submit', this.handleLicenseSubmit.bind(this)); useTrialBtn.addEventListener('click', () => this.activateLicense(DEFAULT_LICENSE, true)); }, checkStoredLicense() { const storedLicense = localStorage.getItem(STORAGE_KEY); if (storedLicense && storedLicense !== DEFAULT_LICENSE) { document.getElementById('licenseKey').value = storedLicense; } }, async activateLicense(licenseKey, isTrial = false) { this.setLoadingState(true); try { Dynamsoft.DWT.ProductKey = licenseKey; currentLicenseKey = licenseKey; if (!isTrial) { localStorage.setItem(STORAGE_KEY, licenseKey); } document.getElementById('licenseOverlay').style.display = 'none'; document.getElementById('appContainer').classList.add('active'); this.updateLicenseStatus(isTrial); isLicenseActivated = true; DWTManager.initialize(); const message = isTrial ? 'Trial license activated successfully!' : 'License activated successfully!'; Utils.showNotification(message, 'success'); } catch (error) { console.error('License activation failed:', error); Utils.showNotification('License activation failed. Please check your license key.', 'error'); } finally { this.setLoadingState(false); } }, setLoadingState(loading) { const activateBtn = document.getElementById('activateBtn'); const useTrialBtn = document.getElementById('useTrialBtn'); const activateText = document.getElementById('activateText'); const activateSpinner = document.getElementById('activateSpinner'); if (loading) { activateBtn.disabled = true; useTrialBtn.disabled = true; activateText.textContent = 'Activating...'; activateSpinner.classList.remove('ds-hidden'); } else { activateBtn.disabled = false; useTrialBtn.disabled = false; activateText.textContent = 'Activate License'; activateSpinner.classList.add('ds-hidden'); } } }; 
Enter fullscreen mode Exit fullscreen mode

Step 6: DWT Manager for Scanner Integration

The DWT (Dynamic Web TWAIN) Manager is the heart of our scanning functionality. This module handles all interactions with the Dynamic Web TWAIN:

const DWTManager = { initialize() { if (!isLicenseActivated) { console.warn('Cannot initialize DWT: License not activated'); return; } pdfName = Utils.generateScanFilename(); this.updateTitles(); imageCount = 0; Dynamsoft.DWT.CreateDWTObjectEx({ WebTwainId: 'mydwt-' + Date.now() }, (obj) => { DWTObject = obj; this.registerEvents(); console.log('DWT Object initialized successfully'); }, (err) => { console.error('DWT initialization failed:', err); Utils.showNotification('Failed to initialize scanner. Please check your license.', 'error'); }); }, registerEvents() { DWTObject.RegisterEvent('OnBufferChanged', (bufferChangeInfo) => { if (bufferChangeInfo['action'] === 'add') { ImageManager.insert(); imageCount++; } }); }, acquireImage() { if (!DWTObject) { Utils.showNotification('Scanner not initialized. Please activate your license first.', 'error'); return; } DWTObject.SelectSourceAsync() .then(() => { return DWTObject.AcquireImageAsync({ IfCloseSourceAfterAcquire: true }); }) .then(() => { PageManager.showPages("group-1"); Utils.showNotification('Document scanned successfully!', 'success'); }) .catch((exp) => { console.error(exp.message); Utils.showNotification('Scanning failed: ' + exp.message, 'error'); }); }, loadImage() { if (!DWTObject) { Utils.showNotification('Scanner not initialized. Please activate your license first.', 'error'); return; } DWTObject.LoadImageEx('', -1, () => { console.log('Images loaded successfully'); PageManager.showPages("group-1"); Utils.showNotification('Images loaded successfully!', 'success'); }, (a, b, c) => { console.error([a, b, c, DWTObject.ErrorCause]); Utils.showNotification('Failed to load images', 'error'); } ); } }; 
Enter fullscreen mode Exit fullscreen mode

Key Concepts Explained:

  1. Buffer Management: DWT maintains an internal buffer of images. The OnBufferChanged event is crucial for detecting when new images are added.

  2. Asynchronous Operations: Scanner operations are asynchronous. We use promises to handle the scanning workflow properly.

  3. Error Handling: Always check if DWTObject exists before operations to prevent runtime errors.

Step 7: Image Management and Display

The ImageManager handles converting DWT buffer images into visible thumbnails in our UI. This is where scanned images become interactive elements:

const ImageManager = { insert() { if (!DWTObject) return; const currentImageIndex = imageCount; const currentImageUrl = DWTObject.GetImageURL(currentImageIndex); const currentImageID = DWTObject.IndexToImageID(currentImageIndex); const img = new Image(); img.className = "ds-dwt-image"; img.setAttribute("imageID", currentImageID); img.src = currentImageUrl; img.onload = () => { const wrapper = this.createImageWrapper(img); this.addToImageBox(wrapper); }; }, createImageWrapper(img) { const wrapper = document.createElement('div'); wrapper.className = "ds-image-wrapper"; wrapper.setAttribute("draggable", "true"); wrapper.appendChild(img); wrapper.addEventListener('dragstart', DragDrop.handleDragStart); wrapper.addEventListener('dragend', DragDrop.handleDragEnd); wrapper.addEventListener('mousedown', (e) => ImageInteraction.handleMouseDown(e)); return wrapper; }, addToImageBox(wrapper) { const imageBox = document.getElementById('imagebox-1'); if (imageBox.classList.contains('initial-hidden')) { imageBox.classList.remove('initial-hidden'); const fileListItem = document.querySelector('li[data-group="group-1"]'); if (fileListItem) { fileListItem.classList.remove('initial-hidden'); fileListItem.style.display = 'flex'; } } imageBox.lastElementChild.appendChild(wrapper); } }; 
Enter fullscreen mode Exit fullscreen mode

Understanding the Image Flow:

  1. DWT Buffer → URL: GetImageURL() creates a temporary URL for displaying images from the DWT buffer in the browser.

  2. Image ID Tracking: Each image gets a unique imageID that links the DOM element back to the DWT buffer. This is crucial for operations like saving and deleting.

  3. Wrapper Pattern: We wrap each image in a container div to handle drag-and-drop and interaction events without interfering with the image itself.

  4. Lazy Loading: Using img.onload ensures the image is fully loaded before adding to DOM, preventing layout issues.

Step 8: Implement Drag-and-Drop for Document Merging

The drag-and-drop system enables users to merge documents by moving pages between different document groups. This is one of the most complex but essential features:

const DragDrop = { handleDragStart(e) { draggedElement = this; draggedImageBox = this.closest('.ds-imagebox'); e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/html', this.outerHTML); this.style.opacity = '0.5'; }, handleDragEnd() { this.style.opacity = ''; draggedElement = null; draggedImageBox = null; document.querySelectorAll('.ds-imagebox').forEach(box => { box.classList.remove('drag-over'); }); }, handleDragOver(e) { if (e.preventDefault) { e.preventDefault(); } e.dataTransfer.dropEffect = 'move'; return false; }, handleDrop(e) { if (e.stopPropagation) { e.stopPropagation(); } const targetImageBox = e.currentTarget; targetImageBox.classList.remove('drag-over'); if (!draggedElement) return false; if (targetImageBox !== draggedImageBox) { DragDrop.insertAtPosition(targetImageBox, e.clientX); } else { DragDrop.reorderInSameBox(targetImageBox, e.clientX); } PageManager.updateAll(); return false; }, insertAtPosition(targetImageBox, clientX) { const rect = targetImageBox.getBoundingClientRect(); const x = clientX - rect.left; const children = Array.from(targetImageBox.children); let insertIndex = children.length; for (let i = 0; i < children.length; i++) { const child = children[i]; const childRect = child.getBoundingClientRect(); const childX = childRect.left - rect.left + childRect.width / 2; if (x < childX) { insertIndex = i; break; } } if (insertIndex >= children.length) { targetImageBox.appendChild(draggedElement); } else { targetImageBox.insertBefore(draggedElement, children[insertIndex]); } }, reorderInSameBox(targetImageBox, clientX) { const rect = targetImageBox.getBoundingClientRect(); const x = clientX - rect.left; const children = Array.from(targetImageBox.children).filter(child => child !== draggedElement); let insertIndex = children.length; for (let i = 0; i < children.length; i++) { const child = children[i]; const childRect = child.getBoundingClientRect(); const childX = childRect.left - rect.left + childRect.width / 2; if (x < childX) { insertIndex = i; break; } } if (insertIndex >= children.length) { targetImageBox.appendChild(draggedElement); } else { targetImageBox.insertBefore(draggedElement, children[insertIndex]); } } }; 
Enter fullscreen mode Exit fullscreen mode

Drag-and-Drop Logic Explained:

  1. Visual Feedback: We use opacity changes and CSS classes to show users what's happening during the drag operation.

  2. Position Calculation: The clientX coordinate tells us where the user dropped the item. We compare this to child element positions to determine insertion point.

  3. Merge vs. Reorder: The system automatically detects whether the user is merging documents (different containers) or just reordering pages (same container).

  4. DOM Manipulation: We use insertBefore() and appendChild() to physically move elements in the DOM, which immediately updates the visual order.

Step 9: File Management and PDF Generation

The FileManager handles the critical Save as PDF functionality, ensuring that the visual page order from drag-and-drop operations is preserved in the final PDF:

const FileManager = { save(button) { const docDiv = button.closest('.doc'); const imageTags = docDiv.querySelectorAll('img[imageid]'); const imageIndexes = Array.from(imageTags).map(img => { const imageid = img.getAttribute('imageid'); return DWTObject.ImageIDToIndex(imageid); }); console.log('Saving images in DOM order:', imageIndexes); DWTObject.SelectImages(imageIndexes); DWTObject.SaveSelectedImagesAsMultiPagePDF( pdfName, () => { console.log("PDF saved successfully"); Utils.showNotification('PDF saved successfully!', 'success'); }, (errorCode, errorString) => { console.error('Save error:', errorString); Utils.showNotification('Failed to save PDF: ' + errorString, 'error'); } ); }, delete(button) { const docDiv = button.closest('.doc'); const imageTags = docDiv.querySelectorAll('img[imageid]'); const imageIndexes = Array.from(imageTags).map(img => { const imageid = img.getAttribute('imageid'); return DWTObject.ImageIDToIndex(imageid); }); imageCount = imageCount - imageIndexes.length; DWTObject.SelectImages(imageIndexes); DWTObject.RemoveAllSelectedImages(); if (docDiv) { const docId = docDiv.getAttribute('docid'); if (docId === '1') { const wrappers = docDiv.querySelectorAll('.ds-image-wrapper'); wrappers.forEach(wrapper => wrapper.remove()); const remainingImages = docDiv.querySelectorAll('.ds-dwt-image'); if (remainingImages.length === 0) { docDiv.classList.add('initial-hidden'); const fileListItem = document.querySelector('li[data-group="group-1"]'); if (fileListItem) { fileListItem.classList.add('initial-hidden'); } } } else { docDiv.remove(); const group = docDiv.getAttribute('data-group'); const groupItem = document.querySelector(`li[data-group="${group}"]`); if (groupItem) { groupItem.remove(); } } } Utils.showNotification('Document deleted successfully!', 'success'); } }; 
Enter fullscreen mode Exit fullscreen mode

Step 10: Split Document

Document splitting allows users to break multi-page documents into separate groups at any point. This feature is essential for organizing scanned documents:

const DocumentSplitter = { splitImage(imageEl) { const imageWrapperDiv = imageEl.parentNode; const previousDivEl = imageWrapperDiv.previousSibling; if (previousDivEl) { this.createNextDocument(previousDivEl); } }, createNextDocument(divImageWrapperEl) { const imageboxWrapper = document.querySelector('.ds-imagebox-wrapper'); const currentDocumentEl = imageboxWrapper.lastElementChild; const documentID = 1 + parseInt(currentDocumentEl.getAttribute("docID")); const newDocGroup = this.createDocumentGroup(documentID); const newImageBox = this.createImageBox(); newDocGroup.appendChild(this.createDocTitle()); newDocGroup.appendChild(newImageBox); imageboxWrapper.appendChild(newDocGroup); while (divImageWrapperEl.nextElementSibling) { const siblingWrapper = divImageWrapperEl.nextElementSibling; this.prepareImageWrapper(siblingWrapper); newImageBox.appendChild(siblingWrapper); } this.createFileListItem(documentID); Utils.showNotification('Document split successfully!', 'success'); }, createDocumentGroup(documentID) { const newDivGroup = document.createElement('div'); newDivGroup.id = 'imagebox-' + documentID; newDivGroup.setAttribute('docID', documentID); newDivGroup.setAttribute('data-group', 'group-' + documentID); newDivGroup.className = "doc"; return newDivGroup; }, createDocTitle() { const docTitle = document.createElement('div'); docTitle.className = "doc-title"; const titleName = document.createElement('span'); titleName.className = "title-name"; titleName.innerText = pdfName; const buttons = document.createElement('div'); buttons.className = "doc-buttons"; const saveBtn = document.createElement('button'); saveBtn.textContent = '💾 Save File'; saveBtn.onclick = function () { FileManager.save(this); }; const deleteBtn = document.createElement('button'); deleteBtn.textContent = '🗑️ Delete'; deleteBtn.onclick = function () { FileManager.delete(this); }; buttons.appendChild(saveBtn); buttons.appendChild(deleteBtn); docTitle.appendChild(titleName); docTitle.appendChild(buttons); return docTitle; }, createImageBox() { const imageBox = document.createElement('div'); imageBox.className = "ds-imagebox mt10 thumbnails"; imageBox.addEventListener('drop', (e) => DragDrop.handleDrop.call(imageBox, e)); imageBox.addEventListener('dragover', (e) => DragDrop.handleDragOver.call(imageBox, e)); imageBox.addEventListener('dragenter', (e) => DragDrop.handleDragEnter.call(imageBox, e)); imageBox.addEventListener('dragleave', (e) => DragDrop.handleDragLeave.call(imageBox, e)); return imageBox; }, createFileListItem(documentID) { const ul = document.querySelector('ul.file-list'); const newLiGroup = document.createElement('li'); newLiGroup.setAttribute('data-group', 'group-' + documentID); const fileInfo = document.createElement('div'); fileInfo.className = 'file-info'; const titleName = document.createElement('div'); titleName.className = 'title-name'; titleName.innerText = pdfName; const pageNumber = document.createElement('div'); pageNumber.className = "page-number"; const checkboxLabel = document.createElement('label'); checkboxLabel.className = 'checkbox-label'; const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.checked = true; checkbox.setAttribute('data-group', 'group-' + documentID); checkbox.addEventListener('change', EventHandlers.changeCheckboxValue); fileInfo.appendChild(titleName); fileInfo.appendChild(document.createElement('em').appendChild(pageNumber)); checkboxLabel.appendChild(checkbox); newLiGroup.appendChild(fileInfo); newLiGroup.appendChild(checkboxLabel); ul.appendChild(newLiGroup); const newDocGroup = document.querySelector(`[data-group="group-${documentID}"].doc`); if (newDocGroup) { const images = newDocGroup.querySelectorAll('.ds-dwt-image'); pageNumber.textContent = `${images.length} pages`; } } }; 
Enter fullscreen mode Exit fullscreen mode

Document Splitting Workflow:

  1. User Action: Right-click on an image → Select "Split"
  2. Find Split Point: Locate the image wrapper and identify where to split
  3. Create New Document: Generate new container with unique ID
  4. Move Images: Transfer all images after split point to new document
  5. Update UI: Add new document to file list with page count
  6. Preserve Functionality: Ensure new document has save/delete buttons and drag-and-drop

Step 11: Utility Functions and Context Menu Integration

Add utility functions and context menu system to complete the splitting functionality:

const ContextMenu = { init() { this.bindEvents(); }, bindEvents() { document.addEventListener('contextmenu', this.handleContextMenu.bind(this)); document.addEventListener('click', this.handleClick.bind(this)); document.getElementById('liSplit').addEventListener('click', this.handleSplit.bind(this)); document.getElementById('liDelete').addEventListener('click', this.handleDelete.bind(this)); document.getElementById('liMultiDelete').addEventListener('click', this.handleMultiDelete.bind(this)); }, handleContextMenu(e) { e.preventDefault(); currentImgEl = null; const menu = document.querySelector('.ds-context-menu'); if (!menu) return; let targetElement = e.target; if (targetElement.classList.contains('ds-image-wrapper')) { targetElement = targetElement.querySelector('img'); } currentImgEl = targetElement; if (currentImgEl && currentImgEl.tagName === 'IMG') { menu.style.left = e.pageX + 'px'; menu.style.top = e.pageY + 'px'; menu.className = "ds-context-menu"; } else { menu.className = "ds-context-menu ds-hidden"; } }, handleSplit() { if (currentImgEl) { DocumentSplitter.splitImage(currentImgEl); } PageManager.updateAll(); } }; const Utils = { generateScanFilename() { const now = new Date(); const day = now.getDate().toString().padStart(2, '0'); const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; const month = monthNames[now.getMonth()]; const year = now.getFullYear(); let hour = now.getHours(); const minute = now.getMinutes().toString().padStart(2, '0'); const second = now.getSeconds().toString().padStart(2, '0'); const isAM = hour < 12; const period = isAM ? 'AM' : 'PM'; hour = hour % 12 || 12; hour = hour.toString().padStart(2, '0'); return `Scan - ${day} ${month} ${year} ${hour}_${minute}_${second} ${period}.pdf`; }, showNotification(message, type) { const notification = document.createElement('div'); notification.className = `notification notification-${type}`; notification.style.cssText = ` position: fixed; top: 20px; right: 20px; padding: 1rem 1.5rem; border-radius: 8px; color: white; font-weight: 600; z-index: 10000; animation: slideIn 0.3s ease; ${type === 'success' ? 'background: #10b981;' : 'background: #ef4444;'} `; notification.textContent = message; if (!document.getElementById('notification-styles')) { const style = document.createElement('style'); style.id = 'notification-styles'; style.textContent = ` @keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } } `; document.head.appendChild(style); } document.body.appendChild(notification); setTimeout(() => { notification.style.animation = 'slideIn 0.3s ease reverse'; setTimeout(() => { if (notification.parentNode) { notification.parentNode.removeChild(notification); } }, 300); }, NOTIFICATION_DURATION); } }; const PageManager = { updateAll() { document.querySelectorAll('.file-list input[type="checkbox"]').forEach(checkbox => { const group = checkbox.getAttribute('data-group'); this.showPages(group); }); }, showPages(group) { const groupItem = document.querySelector(`li[data-group="${group}"]`); if (!groupItem) return; const pageNumberEl = groupItem.querySelector('.page-number'); if (!pageNumberEl) return; const targetGroup = document.querySelector(`.doc[data-group="${group}"]`); if (targetGroup) { const images = targetGroup.querySelectorAll('.ds-dwt-image'); pageNumberEl.textContent = `${images.length} pages`; } } }; 
Enter fullscreen mode Exit fullscreen mode

Context Menu HTML Structure:

<div class="ds-context-menu ds-hidden"> <ul> <li id="liSplit">✂️ Split</li> <li id="liDelete">🗑️ Delete</li> <li id="liMultiDelete">🗑️ Multi Delete</li> </ul> </div> 
Enter fullscreen mode Exit fullscreen mode

Step 12: Application Initialization and Complete Integration

Finally, let's initialize the application and tie all modules together:

const App = { init() { LicenseManager.init(); ContextMenu.init(); EventHandlers.initializeCheckboxes(); } }; const EventHandlers = { changeCheckboxValue() { const group = this.getAttribute('data-group'); const targetGroup = document.querySelector(`.doc[data-group="${group}"]`); if (this.checked) { targetGroup.style.display = ""; } else { targetGroup.style.display = "none"; } }, initializeCheckboxes() { document.querySelectorAll('.file-list input[type="checkbox"]').forEach(checkbox => { checkbox.addEventListener('change', this.changeCheckboxValue); }); document.querySelectorAll('.file-list input[type="checkbox"]:checked').forEach(checkbox => { const group = checkbox.getAttribute('data-group'); const targetGroup = document.querySelector(`.doc[data-group="${group}"]`); if (targetGroup) { targetGroup.classList.add('active'); } }); } }; document.addEventListener('DOMContentLoaded', App.init); 
Enter fullscreen mode Exit fullscreen mode

Step 13: Test the Document Management Application

  1. Start a local server with Python's built-in HTTP server for quick testing:

    python -m http.server 8000 
  2. Navigate to http://localhost:8000 in your web browser.

    Document Management Features

Source Code

https://github.com/yushulx/web-twain-document-scan-management/tree/main/examples/split_merge_document

Top comments (0)