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

Xiao Ling @yushulx

Joined:
Mar 10, 2021

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

Publish Date: Aug 5
1 0

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

Comments 0 total

    Add comment