Multi-Barcode Scanner (Sidebar Table) - AutoReload v3

Enhanced batch barcode scanner with resizable sidebar and session persistence

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Multi-Barcode Scanner (Sidebar Table) - AutoReload v3
// @description  Enhanced batch barcode scanner with resizable sidebar and session persistence
// @match        https://his.kaauh.org/lab/*
// @grant        none
// @version 0.0.1.20260101031721
// @namespace https://greasyfork.org/users/1396691
// ==/UserScript==

(function() {
    'use strict';

    // ============================================
    // CONFIGURATION - Easy to adjust timing/behavior
    // ============================================
    const CONFIG = {
        INACTIVITY_DELAY: 5000,
        FIRST_SUBMIT_WAIT: 2000,
        SUBSEQUENT_SUBMIT_WAIT: 1200,
        SCAN_INPUT_DEBOUNCE: 300,
        VISIBILITY_CHECK_INTERVAL: 1000,
        MIN_SIDEBAR_WIDTH: 320,
        MAX_SIDEBAR_WIDTH: 600,
        STORAGE_KEY: 'batchQueueState'  // sessionStorage key
    };

    // ============================================
    // STATE MANAGEMENT
    // ============================================
    const state = {
        barcodeQueue: [],
        inactivityTimer: null,
        isProcessing: false,
        userClosed: false,
        visibilityInterval: null,
        mutationObserver: null,
        resizeObserver: null,
        statusDiv: null,
        queueListDiv: null,
        clonedInput: null,
        originalInput: null,
        originalContainer: null
    };

    // ============================================
    // SESSION STORAGE HELPERS
    // ============================================
    function savePositionAndSize() {
        if (!state.queueListDiv) return;

        const data = {
            position: {
                top: state.queueListDiv.style.top,
                left: state.queueListDiv.style.left
            },
            size: {
                width: state.queueListDiv.style.width,
                height: state.queueListDiv.style.height
            },
            userClosed: state.userClosed
        };

        try {
            sessionStorage.setItem(CONFIG.STORAGE_KEY, JSON.stringify(data));
            console.log('💾 Saved position and size:', data);
        } catch (e) {
            console.warn('Failed to save to sessionStorage:', e);
        }
    }

    function loadPositionAndSize() {
        try {
            const saved = sessionStorage.getItem(CONFIG.STORAGE_KEY);
            if (!saved) return null;
            
            const data = JSON.parse(saved);
            console.log('📂 Loaded position and size:', data);
            return data;
        } catch (e) {
            console.warn('Failed to load from sessionStorage:', e);
            return null;
        }
    }

    function applyPositionAndSize() {
        const saved = loadPositionAndSize();
        if (!saved || !state.queueListDiv) return false;

        // Apply position
        if (saved.position.top) state.queueListDiv.style.top = saved.position.top;
        if (saved.position.left) state.queueListDiv.style.left = saved.position.left;

        // Apply size
        if (saved.size.width) {
            state.queueListDiv.style.width = saved.size.width;
            state.queueListDiv.dataset.manuallyResized = 'true';
        }
        if (saved.size.height) {
            state.queueListDiv.style.height = saved.size.height;
        }

        // Apply user closed state
        if (saved.userClosed) {
            state.userClosed = true;
        }

        return true;
    }

    // ============================================
    // CLEANUP FUNCTION
    // ============================================
    function cleanup() {
        console.log('🧹 Cleaning up previous instance...');

        if (state.inactivityTimer) {
            clearTimeout(state.inactivityTimer);
            state.inactivityTimer = null;
        }

        if (state.visibilityInterval) {
            clearInterval(state.visibilityInterval);
            state.visibilityInterval = null;
        }

        if (state.mutationObserver) {
            state.mutationObserver.disconnect();
            state.mutationObserver = null;
        }

        if (state.resizeObserver) {
            state.resizeObserver.disconnect();
            state.resizeObserver = null;
        }

        const elementsToRemove = ['batch-status', 'batch-queue-list', 'barcodecollection-clone'];
        elementsToRemove.forEach(id => {
            const el = document.getElementById(id);
            if (el && el.parentNode) {
                el.parentNode.removeChild(el);
            }
        });

        state.barcodeQueue.length = 0;
        state.isProcessing = false;
        state.userClosed = false;
        state.statusDiv = null;
        state.queueListDiv = null;
    }

    // ============================================
    // MUTATION OBSERVER
    // ============================================
    function setupMutationObserver() {
        if (state.mutationObserver) {
            state.mutationObserver.disconnect();
        }

        state.mutationObserver = new MutationObserver(() => {
            const originalInput = document.querySelector('#barcodecollection');
            const existingClone = document.getElementById('barcodecollection-clone');

            if (originalInput && !existingClone) {
                cleanup();
                initializeScript(originalInput);
            }
        });

        state.mutationObserver.observe(document.body, {
            childList: true,
            subtree: true
        });
    }

    // ============================================
    // LOCATION CALCULATION
    // ============================================
    function calculateLocation(index) {
        const letter = String.fromCharCode(65 + Math.floor(index / 10));
        const number = (index % 10) + 1;
        return `${letter}${number}`;
    }

    // ============================================
    // UI UPDATES
    // ============================================
    function updateStatus() {
        if (!state.statusDiv) return;

        const timeLeft = state.inactivityTimer ? Math.ceil(CONFIG.INACTIVITY_DELAY / 1000) : 0;
        const count = state.barcodeQueue.length;

        state.statusDiv.innerHTML = `
            <strong>📋 Batch Collector</strong><br>
            ${state.isProcessing ? '🔄 Processing...' : '✅ Collecting'}<br>
            📦 Pending: ${count}<br>
            ${!state.isProcessing && count > 0 ? `⏱️ Starting in ${timeLeft}s` : ''}
        `;

        const timerDisplay = document.getElementById('queue-timer-display');
        if (timerDisplay) {
            timerDisplay.innerText = (!state.isProcessing && count > 0) ? `Auto-run in ${timeLeft}s...` : '';
        }

        if (state.isProcessing) {
            state.statusDiv.style.background = '#ffc107';
            state.statusDiv.style.color = 'black';
        } else if (count > 0) {
            state.statusDiv.style.background = '#17a2b8';
            state.statusDiv.style.color = 'white';
        } else {
            state.statusDiv.style.background = '#28a745';
            state.statusDiv.style.color = 'white';
        }
    }

    function showNotification(message, type = 'info') {
        const colors = {
            success: '#28a745',
            error: '#dc3545',
            warning: '#ffc107',
            info: '#17a2b8'
        };

        const notif = document.createElement('div');
        notif.style.cssText = `
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: ${colors[type]};
            color: white;
            padding: 18px 35px;
            border-radius: 10px;
            z-index: 10002;
            font-size: 18px;
            font-weight: bold;
            box-shadow: 0 6px 25px rgba(0,0,0,0.5);
        `;
        notif.textContent = message;
        document.body.appendChild(notif);

        setTimeout(() => {
            if (notif.parentNode) {
                notif.parentNode.removeChild(notif);
            }
        }, 1500);
    }

    // ============================================
    // VISIBILITY MANAGEMENT
    // ============================================
    function checkContextVisibility() {
        if (state.userClosed || !state.queueListDiv || !state.statusDiv) return;

        const headers = Array.from(document.querySelectorAll('h1, h2, h3, h4, .modal-title, .panel-title, span.caption-subject'));
        const targetHeader = headers.find(h => h.innerText && h.innerText.includes('Sample Receive in WB'));
        const isVisible = targetHeader && targetHeader.offsetParent !== null;

        if (isVisible) {
            if (state.queueListDiv.style.display === 'none') {
                state.queueListDiv.style.display = 'flex';
                state.statusDiv.style.display = 'block';

                const container = targetHeader.closest('.portlet, .panel, .modal-content, .card');
                if (container && !state.queueListDiv.dataset.manuallyResized) {
                    const containerWidth = container.offsetWidth;
                    const newWidth = Math.max(
                        CONFIG.MIN_SIDEBAR_WIDTH,
                        Math.min(containerWidth, CONFIG.MAX_SIDEBAR_WIDTH)
                    );
                    state.queueListDiv.style.width = newWidth + 'px';
                }
            }
        } else {
            state.queueListDiv.style.display = 'none';
            state.statusDiv.style.display = 'none';
        }
    }

    // ============================================
    // TIMER MANAGEMENT
    // ============================================
    function resetInactivityTimer() {
        clearTimeout(state.inactivityTimer);
        state.inactivityTimer = null;

        if (state.barcodeQueue.length > 0 && !state.isProcessing) {
            state.inactivityTimer = setTimeout(() => {
                console.log('⏱️ Inactivity detected, starting batch processing...');
                processBatchQueue();
            }, CONFIG.INACTIVITY_DELAY);
        }
        updateStatus();
    }

    // ============================================
    // QUEUE MANAGEMENT
    // ============================================
    function addToQueue(barcode) {
        if (!barcode || barcode.length === 0) return;

        if (state.barcodeQueue.includes(barcode)) {
            showNotification(`⚠️ ${barcode} already in queue`, 'warning');
            return;
        }

        const emptyMsg = document.getElementById('empty-msg');
        if (emptyMsg && emptyMsg.parentNode) {
            emptyMsg.parentNode.removeChild(emptyMsg);
        }

        state.barcodeQueue.push(barcode);

        const tbody = document.getElementById('batch-table-body');
        if (!tbody) return;

        const rowIndex = state.barcodeQueue.length - 1;
        const rowNum = rowIndex + 1;
        const location = calculateLocation(rowIndex);

        const tr = document.createElement('tr');
        tr.id = `row-${barcode}`;
        tr.style.cssText = "border-bottom: 1px solid #f1f1f1;";
        tr.innerHTML = `
            <td style="text-align: center; padding: 4px;">${rowNum}</td>
            <td style="text-align: left; padding: 4px; font-weight: bold; color: #333;">${barcode}</td>
            <td style="text-align: center; padding: 4px; color: #17a2b8;">${location}</td>
            <td class="status-cell" style="text-align: center; padding: 4px;">⏳</td>
        `;
        tbody.appendChild(tr);

        const container = tbody.closest('div[style*="overflow"]');
        if (container) {
            container.scrollTop = container.scrollHeight;
        }

        console.log(`✅ Added to queue: ${barcode}`);
        showNotification(`✅ Added: ${barcode}`, 'success');
        updateStatus();
        resetInactivityTimer();
    }

    function clearQueue() {
        if (state.isProcessing) {
            showNotification('⚠️ Cannot clear while processing', 'warning');
            return;
        }

        state.barcodeQueue.length = 0;
        const tbody = document.getElementById('batch-table-body');
        if (tbody) {
            tbody.innerHTML = '<tr id="empty-msg"><td colspan="4" style="text-align:center; padding:15px; color:#999;">No barcodes scanned</td></tr>';
        }
        clearTimeout(state.inactivityTimer);
        state.inactivityTimer = null;
        updateStatus();
    }

    // ============================================
    // ANGULAR INTEGRATION
    // ============================================
    function getAngularModel() {
        try {
            if (typeof angular === 'undefined') return null;
            const ngElement = angular.element(state.originalInput);
            return ngElement.controller('ngModel');
        } catch(e) {
            console.warn('Angular model access failed:', e);
            return null;
        }
    }

    function safeAngularApply(scope) {
        try {
            if (!scope) return;
            if (scope.$root && scope.$root.$$phase) {
                return;
            }
            scope.$apply();
        } catch(e) {
            console.warn('Angular $apply failed (might be okay):', e);
        }
    }

    // ============================================
    // BARCODE SUBMISSION
    // ============================================
    async function submitSingleBarcode(barcode, waitTime) {
        return new Promise((resolve) => {
            if (!state.originalInput || !state.originalContainer) {
                resolve();
                return;
            }

            state.originalContainer.style.display = 'block';
            state.originalInput.value = barcode;

            const ngModel = getAngularModel();
            if (ngModel) {
                ngModel.$setViewValue(barcode);
                ngModel.$render();

                const scope = angular.element(state.originalInput).scope();
                safeAngularApply(scope);
            }

            state.originalInput.focus();

            setTimeout(() => {
                const eventProps = {
                    key: 'Enter',
                    code: 'Enter',
                    keyCode: 13,
                    which: 13,
                    bubbles: true,
                    cancelable: true
                };

                state.originalInput.dispatchEvent(new KeyboardEvent('keydown', eventProps));

                setTimeout(() => {
                    state.originalInput.dispatchEvent(new KeyboardEvent('keypress', eventProps));
                    state.originalInput.dispatchEvent(new KeyboardEvent('keyup', eventProps));

                    setTimeout(() => {
                        state.originalInput.value = '';
                        if (ngModel) {
                            ngModel.$setViewValue('');
                            ngModel.$render();
                        }
                        resolve();
                    }, waitTime);
                }, 50);
            }, 50);
        });
    }

    // ============================================
    // BATCH PROCESSING
    // ============================================
    async function processBatchQueue() {
        if (state.isProcessing || state.barcodeQueue.length === 0) return;

        state.isProcessing = true;
        clearTimeout(state.inactivityTimer);
        state.inactivityTimer = null;
        updateStatus();

        showNotification(`🚀 Processing ${state.barcodeQueue.length} barcodes...`, 'info');

        const processList = [...state.barcodeQueue];

        for (let i = 0; i < processList.length; i++) {
            const barcode = processList[i];
            const row = document.getElementById(`row-${barcode}`);
            const statusCell = row ? row.querySelector('.status-cell') : null;
            const waitTime = (i === 0) ? CONFIG.FIRST_SUBMIT_WAIT : CONFIG.SUBSEQUENT_SUBMIT_WAIT;

            if (statusCell) {
                statusCell.innerHTML = '<span style="color:#ffc107">⏳ Processing...</span>';
            }

            try {
                await submitSingleBarcode(barcode, waitTime);

                const errorAlert = document.querySelector("div.alert.alert-danger");

                if (statusCell) {
                    if (errorAlert && errorAlert.offsetParent !== null) {
                        const errorMsg = errorAlert.querySelector('strong')?.innerText || 'Error';
                        statusCell.innerHTML = `<span style="color:red; font-weight:bold; font-size:10px;" title="${errorMsg}">${errorMsg.substring(0, 15)}...</span>`;

                        const closeBtn = errorAlert.querySelector('.close');
                        if (closeBtn) closeBtn.click();
                    } else {
                        statusCell.innerHTML = '<span style="color:green; font-weight:bold; font-size:14px;">✔️</span>';
                    }
                }

                console.log(`✅ Completed: ${barcode}`);
            } catch(error) {
                console.error(`❌ Error processing ${barcode}:`, error);
                if (statusCell) {
                    statusCell.innerHTML = '<span style="color:red; font-size:10px;">❌ Error</span>';
                }
            }
        }

        state.barcodeQueue.length = 0;
        if (state.originalContainer) {
            state.originalContainer.style.display = 'none';
        }
        state.isProcessing = false;
        updateStatus();

        showNotification('✅ All barcodes processed!', 'success');
        console.log('✅ Batch processing complete!');

        if (state.clonedInput) {
            state.clonedInput.focus();
        }
    }

    // ============================================
    // DRAGGABLE FUNCTIONALITY WITH SAVE
    // ============================================
    function makeDraggable(element, handle) {
        let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;

        handle.onmousedown = dragMouseDown;

        function dragMouseDown(e) {
            e = e || window.event;
            e.preventDefault();
            pos3 = e.clientX;
            pos4 = e.clientY;
            document.onmouseup = closeDragElement;
            document.onmousemove = elementDrag;
        }

        function elementDrag(e) {
            e = e || window.event;
            e.preventDefault();
            pos1 = pos3 - e.clientX;
            pos2 = pos4 - e.clientY;
            pos3 = e.clientX;
            pos4 = e.clientY;
            element.style.top = (element.offsetTop - pos2) + "px";
            element.style.left = (element.offsetLeft - pos1) + "px";
        }

        function closeDragElement() {
            document.onmouseup = null;
            document.onmousemove = null;
            // Save position after drag ends
            savePositionAndSize();
        }
    }

    // ============================================
    // RESIZE OBSERVER SETUP
    // ============================================
    function setupResizeObserver() {
        if (state.resizeObserver) {
            state.resizeObserver.disconnect();
        }

        state.resizeObserver = new ResizeObserver((entries) => {
            for (let entry of entries) {
                if (entry.target === state.queueListDiv) {
                    state.queueListDiv.dataset.manuallyResized = 'true';
                    // Debounce save to avoid too many writes
                    clearTimeout(state.resizeSaveTimer);
                    state.resizeSaveTimer = setTimeout(() => {
                        savePositionAndSize();
                    }, 500);
                }
            }
        });

        state.resizeObserver.observe(state.queueListDiv);
    }

    // ============================================
    // MAIN INITIALIZATION
    // ============================================
    function initializeScript(originalInput) {
        console.log('🚀 Initializing Barcode Batch Collector v3...');

        state.originalInput = originalInput;
        state.originalContainer = originalInput.closest('.form-group');

        if (!state.originalContainer) {
            console.error('❌ Could not find original container');
            return;
        }

        // --- 1. Create Cloned Input ---
        const clonedContainer = state.originalContainer.cloneNode(true);
        const clonedInput = clonedContainer.querySelector('#barcodecollection');

        clonedInput.id = 'barcodecollection-clone';
        clonedInput.placeholder = '🔄 Scan barcodes rapidly here...';
        clonedInput.style.cssText = 'border: 3px solid #17a2b8; background-color: #e7f9ff; font-weight: bold;';
        clonedInput.value = '';

        const clonedLabel = clonedContainer.querySelector('label');
        if (clonedLabel) {
            clonedLabel.textContent = '⚡ Rapid Scan Input (Batch Mode):';
            clonedLabel.style.cssText = 'color: #17a2b8; font-weight: bold;';
        }

        state.originalContainer.parentNode.insertBefore(clonedContainer, state.originalContainer);
        state.originalContainer.style.display = 'none';
        state.clonedInput = clonedInput;

        const separator = document.createElement('div');
        separator.style.cssText = 'border-top: 2px dashed #ddd; margin: 15px 0; padding-top: 10px;';
        state.originalContainer.parentNode.insertBefore(separator, state.originalContainer);

        // --- 2. Create Status Display ---
        state.statusDiv = document.createElement('div');
        state.statusDiv.id = 'batch-status';
        state.statusDiv.style.cssText = `
            position: fixed;
            top: 10px;
            right: 10px;
            background: #28a745;
            color: white;
            padding: 12px 18px;
            border-radius: 8px;
            z-index: 10000;
            font-size: 13px;
            box-shadow: 0 4px 15px rgba(0,0,0,0.3);
            min-width: 220px;
            display: none;
        `;
        state.statusDiv.innerHTML = '<strong>📋 Batch Mode:</strong> Ready';
        document.body.appendChild(state.statusDiv);

        // --- 3. Create Queue List Display ---
        state.queueListDiv = document.createElement('div');
        state.queueListDiv.id = 'batch-queue-list';
        
        // Check if we have saved position, otherwise use default
        const hasSavedPosition = loadPositionAndSize();
        
        state.queueListDiv.style.cssText = `
            position: fixed;
            ${hasSavedPosition ? '' : 'top: 140px; left: calc(50% - 200px);'}
            background: white;
            border: 2px solid #17a2b8;
            padding: 10px;
            border-radius: 8px;
            z-index: 10000;
            max-height: 80vh;
            overflow: hidden;
            ${hasSavedPosition ? '' : 'width: 400px;'}
            min-width: ${CONFIG.MIN_SIDEBAR_WIDTH}px;
            font-size: 12px;
            box-shadow: 0 4px 15px rgba(0,0,0,0.2);
            display: none;
            flex-direction: column;
            resize: both;
        `;

        state.queueListDiv.innerHTML = `
            <div id="batch-queue-header" style="display:flex; justify-content:space-between; margin-bottom:5px; border-bottom: 2px solid #17a2b8; padding-bottom:5px; cursor: move; user-select: none;">
                <div style="display:flex; align-items:center;">
                    <span style="font-size: 14px; margin-right: 5px;">🤚</span>
                    <strong style="color: #17a2b8;">📋 Batch Queue</strong>
                </div>
                <div>
                    <span id="queue-timer-display" style="color: #888; font-size: 11px; margin-right: 10px;"></span>
                    <span id="btn-close-batch" style="cursor: pointer; font-weight: bold; color: #888; font-size: 16px;">&times;</span>
                </div>
            </div>
            <div style="overflow-y: auto; flex-grow: 1; max-height: calc(100% - 40px);">
                <table style="width: 100%; border-collapse: collapse; font-size: 11px; table-layout: fixed;">
                    <thead style="position: sticky; top: 0; background: white; box-shadow: 0 1px 2px rgba(0,0,0,0.1); z-index: 5;">
                        <tr style="color: #555;">
                            <th style="width: 15%; padding: 4px; text-align: center; border-bottom: 1px solid #ddd;">No.</th>
                            <th style="width: 40%; padding: 4px; text-align: left; border-bottom: 1px solid #ddd;">Barcode</th>
                            <th style="width: 20%; padding: 4px; text-align: center; border-bottom: 1px solid #ddd;">Loc</th>
                            <th style="width: 25%; padding: 4px; text-align: center; border-bottom: 1px solid #ddd;">Status</th>
                        </tr>
                    </thead>
                    <tbody id="batch-table-body">
                        <tr id="empty-msg"><td colspan="4" style="text-align:center; padding:15px; color:#999;">No barcodes scanned</td></tr>
                    </tbody>
                </table>
            </div>
            <div style="margin-top: 5px; font-size: 10px; color: #666; text-align: center; border-top: 1px solid #eee; padding-top: 5px;">
                📐 Drag header to move • Drag corner to resize | <a href="#" id="btn-clear-queue" style="color:#dc3545; text-decoration:none;">Clear</a>
            </div>
        `;
        document.body.appendChild(state.queueListDiv);

        // Apply saved position and size from session
        applyPositionAndSize();

        // Setup resize observer to track manual resizing
        setupResizeObserver();

        // --- 4. Event Listeners ---
        document.getElementById('btn-close-batch').onclick = () => {
            state.queueListDiv.style.display = 'none';
            state.statusDiv.style.display = 'none';
            state.userClosed = true;
            savePositionAndSize();
            showNotification("Batch Scanner hidden. Refresh page to restore.", "info");
        };

        document.getElementById('btn-clear-queue').onclick = (e) => {
            e.preventDefault();
            clearQueue();
        };

        const header = document.getElementById("batch-queue-header");
        makeDraggable(state.queueListDiv, header);

        clonedInput.addEventListener('keydown', (e) => {
            if (e.key === 'Enter') {
                e.preventDefault();
                const barcode = clonedInput.value.trim();
                if (barcode.length > 0) {
                    addToQueue(barcode);
                    clonedInput.value = '';
                }
            }
        });

        let scanTimer = null;
        clonedInput.addEventListener('input', (e) => {
            clearTimeout(scanTimer);
            const currentValue = e.target.value.trim();

            scanTimer = setTimeout(() => {
                if (currentValue.length > 0) {
                    addToQueue(currentValue);
                    clonedInput.value = '';
                }
            }, CONFIG.SCAN_INPUT_DEBOUNCE);
        });

        // --- 5. Start Visibility Monitoring ---
        state.visibilityInterval = setInterval(checkContextVisibility, CONFIG.VISIBILITY_CHECK_INTERVAL);

        console.log('✅ Barcode Batch Collector v3 initialized with FREE positioning');
        updateStatus();
        clonedInput.focus();
    }

    // ============================================
    // STARTUP
    // ============================================
    const initialInput = document.querySelector('#barcodecollection');
    if (initialInput && !document.getElementById('barcodecollection-clone')) {
        initializeScript(initialInput);
    }

    setupMutationObserver();

    console.log('👀 Barcode Scanner v3 watching for page changes...');
})();