UI Scraper : Element Text Extractor

Advanced UI element text extractor with keyboard shortcuts, export formats, undo functionality, and optimized performance

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         UI Scraper : Element Text Extractor
// @namespace    http://tampermonkey.net/
// @version      2.1
// @license      MIT
// @description  Advanced UI element text extractor with keyboard shortcuts, export formats, undo functionality, and optimized performance
// @author       MakMak
// @match        http://*/*
// @match        https://*/*
// @icon         https://i.ibb.co/Fk364jmW/table.png
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    // --- STATE MANAGEMENT ---
    let isSelecting = false;
    let currentMode = 'list'; // 'list' or 'table'
    let listData = [];
    let tableData = [];
    let tableHeaders = [];
    let currentRow = [];
    let lastHoveredElement = null;
    let extractorEnabled = false;
    let panelEl = null;
    let stylesInjected = false;
    let abortController = null;
    let hoverDebounceTimer = null;
    let autoSaveTimer = null;
    let actionHistory = []; // For undo functionality
    let previewMode = false;
    let menuCommandText = ""; // To store the current menu command text for toggling

    // Enhanced table batch mode state
    let firstRowElements = []; // Store DOM elements from first complete row
    let columnSelectors = []; // Store selectors for each column
    let firstRowCompleted = false; // Track if first row is complete

    // Analyze first row elements to create column-specific selectors
    function analyzeColumnSelectors(elements) {
        const selectors = [];

        elements.forEach((element, index) => {
            const selector = generateElementSelector(element);
            selectors.push({
                columnIndex: index,
                columnName: tableHeaders[index],
                selector: selector,
                element: element,
                tagName: element.tagName.toLowerCase(),
                className: element.className,
                attributes: getRelevantAttributes(element)
            });
        });

        return selectors;
    }

    // Generate a specific selector for an element
    function generateElementSelector(element) {
        const tagName = element.tagName.toLowerCase();
        let selector = tagName;

        // Add class selector if available (excluding our highlight classes)
        const cleanClassName = element.className
            .split(' ')
            .filter(c => !c.startsWith('extractor-'))
            .join('.');

        if (cleanClassName) {
            selector += '.' + cleanClassName;
        }

        // Add attribute selectors for more specificity
        const relevantAttrs = getRelevantAttributes(element);
        relevantAttrs.forEach(attr => {
            if (attr.value && attr.value.length < 50) { // Avoid very long attribute values
                selector += `[${attr.name}="${attr.value}"]`;
            }
        });

        return selector;
    }

    // Get relevant attributes for selector generation
    function getRelevantAttributes(element) {
        const relevantAttrs = ['data-*', 'role', 'type', 'name', 'id'];
        const attrs = [];

        for (let attr of element.attributes) {
            if (relevantAttrs.some(pattern =>
                pattern.includes('*') ? attr.name.startsWith(pattern.replace('*', '')) : attr.name === pattern
            )) {
                attrs.push({ name: attr.name, value: attr.value });
            }
        }

        return attrs;
    }

    // Performance constants
    const HOVER_DEBOUNCE_DELAY = 50;
    const AUTO_SAVE_DELAY = 2000;
    const MAX_HISTORY_SIZE = 50;

    // Smart element selection - elements to ignore
    const IGNORED_ELEMENTS = new Set([
        'SCRIPT', 'STYLE', 'NOSCRIPT', 'META', 'LINK', 'TITLE', 'HEAD',
        'BR', 'HR', 'IMG', 'INPUT', 'BUTTON', 'SELECT', 'TEXTAREA'
    ]);

    // --- UI PANEL HTML ---
    const panelHTML = `
        <div id="extractor-panel" class="extractor-panel-hidden">
            <div class="ex-header">
                📝 Text Extractor <span id="transparency-indicator" style="display: none;">👻</span>
                <div class="ex-header-controls">
                    <button class="ex-minimize-btn" title="Minimize">−</button>
                    <button class="ex-close-btn" title="Close">×</button>
                </div>
            </div>
            <div class="ex-body">
                <div class="ex-controls">
                    <button id="ex-toggle-selection" class="ex-primary-btn">Start Selecting</button>
                    <div class="ex-modes">
                        <label><input type="radio" name="ex-mode" value="list" checked> List</label>
                        <label><input type="radio" name="ex-mode" value="table"> Table</label>
                    </div>
                </div>
                <!-- Manual selector input for List mode -->
                <div id="ex-selector-input" class="ex-selector-controls" style="display:none; margin-top: 8px;">
                    <label for="ex-manual-selector" style="font-size: 12px; font-weight: 600;">CSS Selector:</label>
                    <div class="ex-selector-input-group" style="display:flex; gap: 4px; margin-top: 4px;">
                        <input type="text" id="ex-manual-selector" placeholder="e.g., .product-name, h2.title" title="Enter CSS selector and press Enter" style="flex:1; padding: 6px 8px; font-size: 13px; border: 1px solid #ced4da; border-radius: 6px;">
                        <button id="ex-apply-selector" class="ex-small-btn" title="Apply Selector" style="align-self: center; padding: 6px 10px;">→</button>
                    </div>
                    <div class="ex-selector-info" style="margin-top: 4px; font-size: 11px; color: #6c757d;">
                        <span id="ex-selector-count">0 elements found</span>
                    </div>
                </div>
                <div id="ex-table-setup" style="display: none;">
                    <label for="ex-column-names">Column Names (comma-separated):</label>
                    <input type="text" id="ex-column-names" placeholder="e.g., Name, Price, SKU">
                </div>
                <div class="ex-results">
                    <div class="ex-results-header">
                        <label>Collected Data:</label>
                        <div class="ex-data-controls">
                            <button id="ex-preview-btn" class="ex-small-btn" title="Toggle Preview">👁</button>
                            <button id="ex-undo-btn" class="ex-small-btn" title="Undo Last (Ctrl+Z)">↶</button>
                        </div>
                    </div>
                    <textarea id="ex-extracted-data" rows="8" readonly></textarea>
                </div>
                <div class="ex-actions">
                    <div class="ex-export-group">
                        <button id="ex-copy-btn">Copy</button>
                        <div class="ex-export-dropdown">
                            <button id="ex-export-btn" class="ex-dropdown-btn">Export ▼</button>
                            <div class="ex-export-menu">
                                <button data-format="text">Plain Text</button>
                                <button data-format="json">JSON</button>
                                <button data-format="csv">CSV</button>
                                <button data-format="html">HTML Table</button>
                            </div>
                        </div>
                    </div>
                    <button id="ex-clear-btn">Clear All</button>
                    <button id="ex-batch-btn" title="Select Similar Elements">Batch</button>
                </div>
                <div class="ex-status">
                    <span id="ex-status-text">Mode: List | Ready</span>
                    <span id="ex-shortcuts-hint">Ctrl+K: Toggle | Esc: Stop | Ctrl+Z: Undo | Ctrl+T: Transparency</span>
                </div>
            </div>
        </div>
    `;

    // --- CSS STYLES (Lazy loaded) ---
    const panelCSS = `
        #extractor-panel {
            position: fixed;
            top: 20px;
            right: 20px;
            width: 360px;
            background-color: #ffffff;
            border: 1px solid #e0e0e0;
            border-radius: 12px;
            box-shadow: 0 8px 32px rgba(0,0,0,0.15);
            z-index: 999999;
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            font-size: 14px;
            color: #333;
            user-select: none;
            transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
            backdrop-filter: blur(10px);
        }

        #extractor-panel * {
            box-sizing: border-box;
        }

        .extractor-panel-hidden {
            transform: translateX(100%);
            opacity: 0;
        }

        /* Enhanced dragging styles */
        #extractor-panel.dragging {
            box-shadow: 0 12px 48px rgba(0,0,0,0.25);
            z-index: 9999999;
        }

        #extractor-panel.dragging .ex-header {
            cursor: grabbing;
        }

        /* Semi-transparent mode for better element selection behind panel */
        #extractor-panel.semi-transparent {
            opacity: 0.7;
            pointer-events: none;
        }

        #extractor-panel.semi-transparent .ex-header,
        #extractor-panel.semi-transparent .ex-body {
            pointer-events: auto;
        }

        .ex-header {
            padding: 12px 16px;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            font-weight: 600;
            cursor: move;
            border-top-left-radius: 12px;
            border-top-right-radius: 12px;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }

        .ex-header-controls {
            display: flex;
            gap: 8px;
        }

        /* Reworked buttons to be perfect circles with centered icons */
        .ex-minimize-btn, .ex-close-btn {
            width: 22px;
            height: 22px;
            border-radius: 50%;
            background: rgba(255,255,255,0.2);
            border: none;
            color: white;
            cursor: pointer;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 20px;
            transition: background-color 0.2s;
            padding: 0; /* Remove default padding that skews the circle shape */
            line-height: 1; /* Normalize line-height for better centering */
        }
        .ex-minimize-btn {
            font-weight: bold; /* A bold minus/plus looks better */
        }
        .ex-close-btn {
            font-weight: normal; /* A normal weight '×' is cleaner */
        }
        .ex-minimize-btn:hover, .ex-close-btn:hover {
            background: rgba(255,255,255,0.3);
        }

        .ex-body {
            padding: 16px;
            display: flex;
            flex-direction: column;
            gap: 16px;
        }

        .ex-controls {
            display: flex;
            align-items: center;
            justify-content: space-between;
            gap: 12px;
        }

        .ex-modes {
            display: flex;
            gap: 12px;
            background: #f8f9fa;
            padding: 4px;
            border-radius: 8px;
        }

        .ex-modes label {
            padding: 6px 12px;
            border-radius: 6px;
            cursor: pointer;
            transition: all 0.2s;
            font-size: 13px;
        }

        .ex-modes label:has(input:checked) {
            background: #667eea;
            color: white;
        }

        .ex-modes input {
            display: none;
        }

        #ex-table-setup {
            display: flex;
            flex-direction: column;
            gap: 8px;
            padding: 12px;
            background: #f8f9fa;
            border-radius: 8px;
        }

        #ex-column-names {
            padding: 8px 12px;
            border: 2px solid #e9ecef;
            border-radius: 6px;
            font-size: 14px;
            transition: border-color 0.2s;
        }

        #ex-column-names:focus {
            outline: none;
            border-color: #667eea;
        }

        .ex-results {
            display: flex;
            flex-direction: column;
            gap: 8px;
        }

        .ex-results-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
        }

        .ex-data-controls {
            display: flex;
            gap: 4px;
        }

        #ex-extracted-data {
            width: 100%;
            border: 2px solid #e9ecef;
            border-radius: 8px;
            background-color: #fff;
            resize: vertical;
            min-height: 120px;
            font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
            font-size: 13px;
            padding: 12px;
            line-height: 1.5;
            transition: border-color 0.2s;
        }

        #ex-extracted-data:focus {
            outline: none;
            border-color: #667eea;
        }

        .ex-actions {
            display: flex;
            gap: 8px;
            align-items: center;
        }

        .ex-export-group {
            display: flex;
            position: relative;
        }

        .ex-export-dropdown {
            position: relative;
        }

        .ex-export-menu {
            position: absolute;
            top: 100%;
            left: 0;
            background: white;
            border: 1px solid #e0e0e0;
            border-radius: 8px;
            box-shadow: 0 4px 12px rgba(0,0,0,0.15);
            display: none;
            min-width: 120px;
            z-index: 1000000;
        }

        .ex-export-menu.show {
            display: block;
        }

        .ex-export-menu button {
            width: 100%;
            text-align: left;
            padding: 8px 12px;
            border: none;
            background: none;
            cursor: pointer;
            font-size: 13px;
            transition: background-color 0.2s;
        }

        .ex-export-menu button:hover {
            background: #f8f9fa;
        }

        .ex-export-menu button:first-child {
            border-top-left-radius: 8px;
            border-top-right-radius: 8px;
        }

        .ex-export-menu button:last-child {
            border-bottom-left-radius: 8px;
            border-bottom-right-radius: 8px;
        }

        button {
            padding: 8px 16px;
            border: 2px solid #e9ecef;
            border-radius: 8px;
            background-color: #ffffff;
            cursor: pointer;
            transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
            font-weight: 500;
            font-size: 13px;
        }

        button:hover {
            border-color: #667eea;
            transform: translateY(-1px);
        }

        .ex-primary-btn {
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            border: none;
            font-weight: 600;
        }

        .ex-primary-btn:hover {
            transform: translateY(-2px);
            box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
        }

        .ex-primary-btn.active {
            background: linear-gradient(135deg, #ff6b6b 0%, #ee5a24 100%);
        }

        .ex-small-btn {
            padding: 6px 8px;
            font-size: 12px;
            min-width: auto;
        }

        .ex-dropdown-btn {
            border-left: none;
            border-top-left-radius: 0;
            border-bottom-left-radius: 0;
            padding: 8px 12px;
        }

        #ex-copy-btn {
            border-top-right-radius: 0;
            border-bottom-right-radius: 0;
        }

        .ex-status {
            font-size: 11px;
            color: #6c757d;
            text-align: center;
            border-top: 1px solid #e9ecef;
            padding-top: 12px;
            display: flex;
            flex-direction: column;
            gap: 4px;
        }

        #ex-shortcuts-hint {
            font-size: 10px;
            opacity: 0.7;
        }

        /* Element Highlighting with improved animations */
        .extractor-hover-highlight {
            outline: 2px solid #667eea !important;
            outline-offset: 2px;
            cursor: crosshair !important;
            animation: pulse-hover 1.5s infinite;
        }

        .extractor-selected-highlight {
            outline: 2px solid #28a745 !important;
            outline-offset: 2px;
            background-color: rgba(40, 167, 69, 0.1) !important;
            animation: flash-select 0.6s ease-out;
        }

        .extractor-preview-highlight {
            outline: 2px dashed #ffc107 !important;
            outline-offset: 2px;
            background-color: rgba(255, 193, 7, 0.1) !important;
        }

        @keyframes pulse-hover {
            0%, 100% { outline-color: #667eea; }
            50% { outline-color: #764ba2; }
        }

        @keyframes flash-select {
            0% { background-color: rgba(40, 167, 69, 0.3) !important; }
            100% { background-color: rgba(40, 167, 69, 0.1) !important; }
        }

        /* Minimized state */
        .ex-body.minimized {
            display: none;
        }

        #extractor-panel.minimized {
            width: 200px;
        }

        /* Responsive adjustments */
        @media (max-width: 480px) {
            #extractor-panel {
                width: calc(100vw - 40px);
                right: 20px;
                left: 20px;
            }
        }

        /* Styles for manual selector input */
        .ex-selector-controls {
            margin: 12px 0;
            padding: 12px;
            background: #f8f9fa;
            border-radius: 6px;
            border: 1px solid #e9ecef;
        }

        .ex-selector-controls label {
            display: block;
            margin-bottom: 6px;
            font-weight: 500;
            color: #495057;
            font-size: 12px;
        }

        .ex-selector-input-group {
            display: flex;
            gap: 6px;
            margin-bottom: 6px;
        }

        #ex-manual-selector {
            flex: 1;
            padding: 8px 10px;
            border: 1px solid #ced4da;
            border-radius: 4px;
            font-size: 12px;
            font-family: 'Courier New', monospace;
            background: white;
        }

        #ex-manual-selector:focus {
            outline: none;
            border-color: #667eea;
            box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2);
        }

        #ex-apply-selector {
            padding: 8px 12px;
            background: #667eea;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 12px;
            min-width: 32px;
        }

        #ex-apply-selector:hover {
            background: #5a67d8;
        }

        .ex-selector-info {
            font-size: 11px;
            color: #6c757d;
        }

        #ex-selector-count {
            font-weight: 500;
        }

        /* Hide selector input in table mode */
        .ex-selector-controls.hidden {
            display: none;
        }
    `;

    // --- UTILITY FUNCTIONS ---
    function debounce(func, wait) {
        let timeout;
        return function executedFunction(...args) {
            const later = () => {
                clearTimeout(timeout);
                func(...args);
            };
            clearTimeout(timeout);
            timeout = setTimeout(later, wait);
        };
    }

    // Update selector input with current element's selector
    function updateSelectorInput(element) {
        const manualSelectorInput = document.getElementById('ex-manual-selector');
        const selectorCountSpan = document.getElementById('ex-selector-count');

        if (!manualSelectorInput || !selectorCountSpan) return;

        // Generate selector using same logic as batch mode
        const tagName = element.tagName.toLowerCase();
        const className = element.className;

        let selector = tagName;
        if (className) {
            const cleanClassName = className.split(' ').filter(c => !c.startsWith('extractor-')).join('.');
            if (cleanClassName) {
                selector += '.' + cleanClassName;
            }
        }

        // Update input field
        manualSelectorInput.value = selector;

        // Update count
        try {
            const elements = Array.from(document.querySelectorAll(selector))
                .filter(el => !isIgnorableElement(el) && el.innerText.trim().length > 0);
            const count = elements.length;
            selectorCountSpan.textContent = `${count} element${count !== 1 ? 's' : ''} found`;

            // Change color based on count
            if (count === 0) {
                selectorCountSpan.style.color = '#dc3545';
            } else if (count === 1) {
                selectorCountSpan.style.color = '#28a745';
            } else {
                selectorCountSpan.style.color = '#007bff';
            }
        } catch (err) {
            selectorCountSpan.textContent = 'Invalid selector';
            selectorCountSpan.style.color = '#dc3545';
        }
    }

    function isIgnorableElement(element) {
        if (!element || !element.tagName) return true;

        // Check if it's an ignored element type
        if (IGNORED_ELEMENTS.has(element.tagName)) return true;

        // Check if it's part of our extractor UI
        if (element.closest('#extractor-panel')) return true;

        // Check if element has no visible text content
        const text = element.innerText?.trim();
        if (!text || text.length === 0) return true;

        // Check if element is hidden
        const style = window.getComputedStyle(element);
        if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return true;

        return false;
    }

    function saveData() {
        const data = {
            listData,
            tableData,
            tableHeaders,
            currentRow,
            currentMode,
            timestamp: Date.now()
        };
        GM_setValue('extractorData', JSON.stringify(data));
    }

    function loadData() {
        try {
            const saved = GM_getValue('extractorData', null);
            if (saved) {
                const data = JSON.parse(saved);
                // Only load if data is recent (within 24 hours)
                if (Date.now() - data.timestamp < 24 * 60 * 60 * 1000) {
                    listData = data.listData || [];
                    tableData = data.tableData || [];
                    tableHeaders = data.tableHeaders || [];
                    currentRow = data.currentRow || [];
                    currentMode = data.currentMode || 'list';
                    return true;
                }
            }
        } catch (e) {
            console.warn('Failed to load saved data:', e);
        }
        return false;
    }

    function addToHistory(action) {
        actionHistory.push({
            action,
            timestamp: Date.now(),
            listData: [...listData],
            tableData: tableData.map(row => [...row]),
            tableHeaders: [...tableHeaders],
            currentRow: [...currentRow],
            // Include table batch state in history
            firstRowCompleted,
            columnSelectors: [...columnSelectors]
        });

        // Limit history size
        if (actionHistory.length > MAX_HISTORY_SIZE) {
            actionHistory.shift();
        }
    }

    function scheduleAutoSave() {
        if (autoSaveTimer) {
            clearTimeout(autoSaveTimer);
        }
        autoSaveTimer = setTimeout(saveData, AUTO_SAVE_DELAY);
    }

    // --- EXPORT FUNCTIONS ---
    function exportData(format) {
        let content = '';
        let filename = `extracted-data-${new Date().toISOString().split('T')[0]}`;
        let mimeType = 'text/plain';

        switch (format) {
            case 'json':
                if (currentMode === 'list') {
                    content = JSON.stringify({ items: listData }, null, 2);
                } else {
                    const tableObj = tableData.map(row => {
                        const obj = {};
                        tableHeaders.forEach((header, index) => {
                            obj[header] = row[index] || '';
                        });
                        return obj;
                    });
                    content = JSON.stringify({ headers: tableHeaders, data: tableObj }, null, 2);
                }
                filename += '.json';
                mimeType = 'application/json;charset=utf-8;';
                break;

            case 'csv':
                if (currentMode === 'list') {
                    content = listData.map(item => `"${item.replace(/"/g, '""')}"`).join('\n');
                } else {
                    const csvHeaders = tableHeaders.map(h => `"${h.replace(/"/g, '""')}"`).join(',');
                    const csvRows = tableData.map(row =>
                        row.map(cell => `"${String(cell).replace(/"/g, '""')}"`).join(',')
                    ).join('\n');
                    content = csvHeaders + '\n' + csvRows;
                }
                filename += '.csv';
                // Set charset and prepend BOM for UTF-8 compatibility (e.g., in Excel)
                mimeType = 'text/csv;charset=utf-8;';
                content = '\uFEFF' + content; // Add UTF-8 Byte Order Mark
                break;

            case 'html':
                if (currentMode === 'list') {
                    const listItems = listData.map(item => `<li>${escapeHtml(item)}</li>`).join('\n');
                    content = `<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Extracted Data</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 20px; }
        ul { list-style-type: disc; padding-left: 20px; }
        li { margin: 5px 0; }
    </style>
</head>
<body>
    <h1>Extracted List Data</h1>
    <ul>
${listItems}
    </ul>
</body>
</html>`;
                } else {
                    const headerRow = tableHeaders.map(h => `<th>${escapeHtml(h)}</th>`).join('');
                    const dataRows = tableData.map(row =>
                        '<tr>' + row.map(cell => `<td>${escapeHtml(cell)}</td>`).join('') + '</tr>'
                    ).join('\n');
                    content = `<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Extracted Data</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 20px; }
        table { border-collapse: collapse; width: 100%; }
        th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
        th { background-color: #f2f2f2; font-weight: bold; }
        tr:nth-child(even) { background-color: #f9f9f9; }
    </style>
</head>
<body>
    <h1>Extracted Table Data</h1>
    <table>
        <thead>
            <tr>${headerRow}</tr>
        </thead>
        <tbody>
${dataRows}
        </tbody>
    </table>
</body>
</html>`;
                }
                filename += '.html';
                mimeType = 'text/html;charset=utf-8;';
                break;

            default: // text
                if (currentMode === 'list') {
                    content = listData.join('\n');
                } else {
                    content = tableHeaders.join('\t') + '\n';
                    content += tableData.map(row => row.join('\t')).join('\n');
                }
                filename += '.txt';
                mimeType = 'text/plain;charset=utf-8;';
                break;
        }

        // Create and trigger download
        const blob = new Blob([content], { type: mimeType });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = filename;
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        URL.revokeObjectURL(url);
    }

    function escapeHtml(text) {
        const div = document.createElement('div');
        div.textContent = text;
        return div.innerHTML;
    }

    // --- GM MENU FUNCTIONS ---
    function toggleExtractor() {
        extractorEnabled = !extractorEnabled;

        if (extractorEnabled) {
            initExtractor();
        } else {
            destroyExtractor();
        }

        updateMenuCommand();
    }

    // This function now handles a single toggling menu command.
    function updateMenuCommand() {
        // Unregister the previous command to prevent duplicates in some script managers
        if (menuCommandText) {
            GM_unregisterMenuCommand(menuCommandText);
        }

        // Set the new command text based on the extractor's state
        menuCommandText = extractorEnabled ? "🟢 Disable Extractor" : "⚪ Enable Extractor";
        GM_registerMenuCommand(menuCommandText, toggleExtractor);
    }

    function initExtractor() {
        if (panelEl) return; // Already initialized

        // Lazy inject CSS
        if (!stylesInjected) {
            GM_addStyle(panelCSS);
            stylesInjected = true;
        }

        // Create UI
        const container = document.createElement('div');
        container.innerHTML = panelHTML;
        document.body.appendChild(container);

        // Get the actual panel element
        panelEl = document.getElementById('extractor-panel');

        // Load saved data
        if (loadData()) {
            // Update UI to reflect loaded data
            const modeRadio = document.querySelector(`input[name="ex-mode"][value="${currentMode}"]`);
            if (modeRadio) {
                modeRadio.checked = true;
                const tableSetupDiv = document.getElementById('ex-table-setup');
                const selectorInputDiv = document.getElementById('ex-selector-input');
                tableSetupDiv.style.display = currentMode === 'table' ? 'block' : 'none';
                // Initialize selector input visibility based on mode
                selectorInputDiv.style.display = currentMode === 'list' ? 'block' : 'none';
            }
            updateDisplay();
            updateStatus();
        }

        setupEventListeners();

        // Restore saved panel position
        const savedPosition = GM_getValue('panel_position', null);
        if (savedPosition) {
            // Ensure position is still within viewport bounds
            const maxTop = window.innerHeight - panelEl.offsetHeight - 10;
            const maxLeft = window.innerWidth - panelEl.offsetWidth - 10;

            const top = Math.max(10, Math.min(savedPosition.top, maxTop));
            const left = Math.max(10, Math.min(savedPosition.left, maxLeft));

            panelEl.style.top = top + 'px';
            panelEl.style.left = left + 'px';
        }

        // Make panel draggable
        makeDraggable(panelEl);

        // Show panel with animation
        setTimeout(() => {
            if (panelEl) panelEl.classList.remove('extractor-panel-hidden');
        }, 100);
    }

    function destroyExtractor() {
        if (!panelEl) return;

        // Stop any active selection
        if (isSelecting) {
            toggleSelection();
        }

        // Clear timers
        if (hoverDebounceTimer) clearTimeout(hoverDebounceTimer);
        if (autoSaveTimer) clearTimeout(autoSaveTimer);

        // Abort all event listeners
        if (abortController) abortController.abort();

        // Remove UI
        panelEl.parentElement.remove();
        panelEl = null;

        // Clear highlights
        document.querySelectorAll('.extractor-hover-highlight, .extractor-selected-highlight, .extractor-preview-highlight').forEach(el => {
            el.classList.remove('extractor-hover-highlight', 'extractor-selected-highlight', 'extractor-preview-highlight');
        });

        // Reset state
        isSelecting = false;
        lastHoveredElement = null;
        actionHistory = [];
    }

    function setupEventListeners() {
        // Create new AbortController for better event management
        abortController = new AbortController();
        const { signal } = abortController;

        // Get UI Elements
        const toggleBtn = document.getElementById('ex-toggle-selection');
        const modeRadios = document.querySelectorAll('input[name="ex-mode"]');
        const tableSetupDiv = document.getElementById('ex-table-setup');
        const selectorInputDiv = document.getElementById('ex-selector-input');
        const manualSelectorInput = document.getElementById('ex-manual-selector');
        const applySelectorBtn = document.getElementById('ex-apply-selector');
        const selectorCountSpan = document.getElementById('ex-selector-count');
        const copyBtn = document.getElementById('ex-copy-btn');
        const clearBtn = document.getElementById('ex-clear-btn');
        const closeBtn = document.querySelector('.ex-close-btn');
        const minimizeBtn = document.querySelector('.ex-minimize-btn');
        const undoBtn = document.getElementById('ex-undo-btn');
        const previewBtn = document.getElementById('ex-preview-btn');
        const batchBtn = document.getElementById('ex-batch-btn');
        const exportBtn = document.getElementById('ex-export-btn');
        const exportMenu = document.querySelector('.ex-export-menu');

        // Panel event listeners
        toggleBtn.addEventListener('click', toggleSelection, { signal });
        copyBtn.addEventListener('click', copyToClipboard, { signal });
        clearBtn.addEventListener('click', clearData, { signal });
        undoBtn.addEventListener('click', undoLastAction, { signal });
        previewBtn.addEventListener('click', togglePreview, { signal });
        batchBtn.addEventListener('click', toggleBatchMode, { signal });

        closeBtn.addEventListener('click', () => {
            // Automatically disable extractor when closing instead of just hiding
            extractorEnabled = false;
            destroyExtractor();
            updateMenuCommand();
        }, { signal });

        minimizeBtn.addEventListener('click', () => {
            const body = document.querySelector('.ex-body');
            const isMinimized = body.classList.contains('minimized');
            body.classList.toggle('minimized');
            panelEl.classList.toggle('minimized');
            minimizeBtn.textContent = isMinimized ? '−' : '+';
        }, { signal });

        // Export dropdown
        exportBtn.addEventListener('click', (e) => {
            e.stopPropagation();
            exportMenu.classList.toggle('show');
        }, { signal });

        // Close export menu when clicking outside
        document.addEventListener('click', () => {
            if (exportMenu.classList.contains('show')) {
                 exportMenu.classList.remove('show');
            }
        }, { signal });

        // Export format buttons
        exportMenu.querySelectorAll('button').forEach(btn => {
            btn.addEventListener('click', (e) => {
                e.stopPropagation();
                const format = btn.dataset.format;
                exportData(format);
                exportMenu.classList.remove('show');

                // Visual feedback
                const originalText = exportBtn.textContent;
                exportBtn.textContent = 'Exported!';
                setTimeout(() => {
                    exportBtn.textContent = originalText;
                }, 1500);
            }, { signal });
        });

        // Mode change listeners
        modeRadios.forEach(radio => {
            radio.addEventListener('change', (e) => {
                currentMode = e.target.value;
                tableSetupDiv.style.display = currentMode === 'table' ? 'block' : 'none';

                // Show or hide manual selector input for list mode
                if (currentMode === 'list') {
                    selectorInputDiv.style.display = 'block';
                } else {
                    selectorInputDiv.style.display = 'none';
                    manualSelectorInput.value = '';
                    selectorCountSpan.textContent = '0 elements found';
                }

                clearData(); // Clear data when switching modes
                updateStatus();
                scheduleAutoSave();
            }, { signal });
        });

        // Manual selector input: apply selector button click
        applySelectorBtn.addEventListener('click', () => {
            applyManualSelector();
        }, { signal });

        // Manual selector input: enter key triggers apply
        manualSelectorInput.addEventListener('keydown', (e) => {
            if (e.key === 'Enter') {
                e.preventDefault();
                applyManualSelector();
            }
        }, { signal });

        // Real-time validation as user types
        manualSelectorInput.addEventListener('input', () => {
            const selector = manualSelectorInput.value.trim();
            if (!selector) {
                selectorCountSpan.textContent = '0 elements found';
                selectorCountSpan.style.color = '#6c757d';
                clearSelectorPreview();
                return;
            }

            try {
                const elements = Array.from(document.querySelectorAll(selector))
                    .filter(el => !isIgnorableElement(el) && el.innerText.trim().length > 0);
                const count = elements.length;
                selectorCountSpan.textContent = `${count} element${count !== 1 ? 's' : ''} found`;

                // Change color based on count
                if (count === 0) {
                    selectorCountSpan.style.color = '#dc3545';
                } else if (count === 1) {
                    selectorCountSpan.style.color = '#28a745';
                } else {
                    selectorCountSpan.style.color = '#007bff';
                }

                // Preview matching elements
                previewSelectorElements(elements);
            } catch (err) {
                selectorCountSpan.textContent = 'Invalid selector';
                selectorCountSpan.style.color = '#dc3545';
                clearSelectorPreview();
            }
        }, { signal });

        // Clear preview when input loses focus
        manualSelectorInput.addEventListener('blur', () => {
            setTimeout(() => clearSelectorPreview(), 200); // Small delay to allow clicking apply button
        }, { signal });

        // Show preview when input gains focus
        manualSelectorInput.addEventListener('focus', () => {
            const selector = manualSelectorInput.value.trim();
            if (selector) {
                try {
                    const elements = Array.from(document.querySelectorAll(selector))
                        .filter(el => !isIgnorableElement(el) && el.innerText.trim().length > 0);
                    previewSelectorElements(elements);
                } catch (err) {
                    // Ignore errors on focus
                }
            }
        }, { signal });

        // Keyboard shortcuts
        document.addEventListener('keydown', handleKeyboardShortcuts, { signal });

        // Global listeners for selection (with debouncing)
        const debouncedMouseOver = debounce(handleMouseOver, HOVER_DEBOUNCE_DELAY);
        document.addEventListener('mouseover', debouncedMouseOver, { signal });
        document.addEventListener('mouseout', handleMouseOut, { signal });
        document.addEventListener('click', handleElementClick, { capture: true, signal });

        // Apply manual selector function
        function applyManualSelector() {
            const selector = manualSelectorInput.value.trim();
            if (!selector) {
                alert('Please enter a CSS selector.');
                return;
            }
            let elements;
            try {
                elements = Array.from(document.querySelectorAll(selector))
                    .filter(el => !isIgnorableElement(el) && el.innerText.trim().length > 0);
            } catch (err) {
                alert('Invalid CSS selector.');
                return;
            }
            const count = elements.length;
            selectorCountSpan.textContent = `${count} element${count !== 1 ? 's' : ''} found`;

            if (count === 0) {
                alert('No matching elements found for the given selector.');
                return;
            }

            // Confirm adding elements to listData
            if (!confirm(`Found ${count} matching elements. Add all to the list?`)) {
                return;
            }

            addToHistory('manual_selector_add');

            elements.forEach(el => {
                const text = el.innerText.trim();
                if (!listData.includes(text)) {
                    listData.push(text);
                    el.classList.add('extractor-selected-highlight');
                }
            });

            updateDisplay();
            updateStatus();
            scheduleAutoSave();
        }
    }

    // Preview matching elements with highlight
    function previewSelectorElements(elements) {
        clearSelectorPreview();
        elements.forEach(el => {
            el.classList.add('extractor-preview-highlight');
        });
    }

    // Clear selector preview highlights
    function clearSelectorPreview() {
        document.querySelectorAll('.extractor-preview-highlight').forEach(el => {
            el.classList.remove('extractor-preview-highlight');
        });
    }

    // --- KEYBOARD SHORTCUTS ---
    function handleKeyboardShortcuts(e) {
        // Ctrl+K: Toggle selection
        if (e.ctrlKey && e.key === 'k') {
            e.preventDefault();
            toggleSelection();
            return;
        }

        // Escape: Stop selecting
        if (e.key === 'Escape' && isSelecting) {
            e.preventDefault();
            toggleSelection();
            return;
        }

        // Ctrl+Z: Undo
        if (e.ctrlKey && e.key === 'z' && !e.shiftKey) {
            e.preventDefault();
            undoLastAction();
            return;
        }

        // Ctrl+E: Toggle extractor
        if (e.ctrlKey && e.key === 'e') {
            e.preventDefault();
            toggleExtractor();
            return;
        }

        // Ctrl+T: Toggle panel transparency for better element selection
        if (e.ctrlKey && e.key === 't' && panelEl) {
            e.preventDefault();
            togglePanelTransparency();
            return;
        }

        // Ctrl+Shift+C: Copy data
        if (e.ctrlKey && e.shiftKey && (e.key === 'C' || e.key === 'c')) {
            e.preventDefault();
            copyToClipboard();
            return;
        }
    }

    // --- CORE LOGIC ---
    function toggleSelection() {
        isSelecting = !isSelecting;
        const toggleBtn = document.getElementById('ex-toggle-selection');

        if (isSelecting) {
            toggleBtn.textContent = 'Stop Selecting';
            toggleBtn.classList.add('active');
        } else {
            toggleBtn.textContent = 'Start Selecting';
            toggleBtn.classList.remove('active');
            if (lastHoveredElement) {
                lastHoveredElement.classList.remove('extractor-hover-highlight');
                lastHoveredElement = null;
            }
        }
        updateStatus();
    }

    function handleMouseOver(e) {
        if (!isSelecting || isIgnorableElement(e.target)) return;

        // Clear previous hover highlight
        if (lastHoveredElement && lastHoveredElement !== e.target) {
            lastHoveredElement.classList.remove('extractor-hover-highlight');
        }

        lastHoveredElement = e.target;
        lastHoveredElement.classList.add('extractor-hover-highlight');

        // Auto-populate selector input in List mode
        if (currentMode === 'list') {
            updateSelectorInput(e.target);
        }
    }

    function handleMouseOut(e) {
        if (!isSelecting || !e.target.classList) return;
        e.target.classList.remove('extractor-hover-highlight');
    }

    function handleElementClick(e) {
        if (!isSelecting || isIgnorableElement(e.target)) return;

        e.preventDefault();
        e.stopPropagation();

        const target = e.target;
        target.classList.remove('extractor-hover-highlight');

        const text = target.innerText.trim();
        if (!text) return;

        // Add to history before making changes
        addToHistory('add_element');

        target.classList.add('extractor-selected-highlight');

        if (currentMode === 'list') {
            listData.push(text);
        } else { // Table mode
            if (tableHeaders.length === 0) {
                const columnNames = document.getElementById('ex-column-names').value;
                if (!columnNames) {
                    alert('Please set the column names for the table first!');
                    target.classList.remove('extractor-selected-highlight');
                    // Re-enable selection for this element
                    isSelecting = true;
                    return;
                }
                tableHeaders = columnNames.split(',').map(h => h.trim());
            }

            // Track elements for first row to build column selectors
            if (!firstRowCompleted) {
                firstRowElements.push(target);
            }

            currentRow.push(text);
            if (currentRow.length === tableHeaders.length) {
                tableData.push([...currentRow]);

                // When first row is complete, analyze column selectors
                if (!firstRowCompleted) {
                    firstRowCompleted = true;
                    columnSelectors = analyzeColumnSelectors(firstRowElements);

                    // Update batch button to indicate it's ready for intelligent selection
                    const batchBtn = document.getElementById('ex-batch-btn');
                    if (batchBtn) {
                        batchBtn.title = 'Smart Batch: Select similar table rows';
                        batchBtn.style.background = '#28a745';
                        batchBtn.style.color = 'white';
                    }

                    console.log('First row completed. Column selectors ready:', columnSelectors);
                }

                currentRow = [];
                firstRowElements = []; // Reset for potential next row
            }
        }

        updateDisplay();
        updateStatus();
        scheduleAutoSave();
    }

    function togglePreview() {
        previewMode = !previewMode;
        const previewBtn = document.getElementById('ex-preview-btn');

        if (previewMode) {
            previewBtn.style.background = '#ffc107';
            previewBtn.style.color = 'black';
            showPreview();
        } else {
            previewBtn.style.background = '';
            previewBtn.style.color = '';
            hidePreview();
        }
    }

    function showPreview() {
        // Highlight all selected elements with preview style
        document.querySelectorAll('.extractor-selected-highlight').forEach(el => {
            el.classList.add('extractor-preview-highlight');
        });
    }

    function hidePreview() {
        document.querySelectorAll('.extractor-preview-highlight').forEach(el => {
            el.classList.remove('extractor-preview-highlight');
        });
    }

    // Toggle panel transparency for better element selection
    function togglePanelTransparency() {
        if (!panelEl) return;

        const isTransparent = panelEl.classList.contains('semi-transparent');

        if (isTransparent) {
            panelEl.classList.remove('semi-transparent');
            // Update status to show normal mode
            const statusText = document.getElementById('ex-status-text');
            if (statusText) {
                statusText.textContent = statusText.textContent.replace(' | Transparent', '');
            }
        } else {
            panelEl.classList.add('semi-transparent');
            // Update status to show transparent mode
            const statusText = document.getElementById('ex-status-text');
            if (statusText) {
                statusText.textContent += ' | Transparent';
            }
        }
    }

    function toggleBatchMode() {
        // Enhanced batch mode with different logic for list vs table mode
        if (currentMode === 'list') {
            // Original list mode batch logic
            if (!lastHoveredElement) {
                alert('Hover over an element first to select similar elements in batch mode.');
                return;
            }

            const tagName = lastHoveredElement.tagName;
            const className = lastHoveredElement.className;

            // Find similar elements
            let selector = tagName.toLowerCase();
            if (className) {
                // Filter out script-injected classes from the selector
                const cleanClassName = className.split(' ').filter(c => !c.startsWith('extractor-')).join('.');
                if(cleanClassName) {
                    selector += '.' + cleanClassName;
                }
            }

            const similarElements = Array.from(document.querySelectorAll(selector))
                .filter(el => !isIgnorableElement(el) && el.innerText.trim());

            if (similarElements.length === 0) {
                alert('No similar elements found.');
                return;
            }

            const confirmMsg = `Found ${similarElements.length} similar elements. Add all to selection?`;
            if (!confirm(confirmMsg)) return;

            addToHistory('batch_add');

            similarElements.forEach(el => {
                const text = el.innerText.trim();
                if (!text) return;

                el.classList.add('extractor-selected-highlight');
                if (!listData.includes(text)) {
                    listData.push(text);
                }
            });

        } else {
            // Simplified table mode batch logic using proven List mode approach
            if (!firstRowCompleted) {
                alert('Please complete the first table row manually before using batch mode.\\n\\nThis helps the system understand your table structure and find similar rows intelligently.');
                return;
            }

            if (columnSelectors.length === 0) {
                alert('Column selectors not available. Please try selecting the first row again.');
                return;
            }

            // Use the same proven logic as List mode for each column
            const allSimilarElements = [];

            columnSelectors.forEach((cs, columnIndex) => {
                const tagName = cs.element.tagName;
                const className = cs.element.className;

                // Build selector using same logic as List mode
                let selector = tagName.toLowerCase();
                if (className) {
                    const cleanClassName = className.split(' ').filter(c => !c.startsWith('extractor-')).join('.');
                    if(cleanClassName) {
                        selector += '.' + cleanClassName;
                    }
                }

                // Find all similar elements for this column
                const similarElements = Array.from(document.querySelectorAll(selector))
                    .filter(el => !isIgnorableElement(el) && el.innerText.trim())
                    .filter(el => el !== cs.element); // Exclude the original element

                console.log(`Column ${columnIndex + 1} (${cs.columnName}): Found ${similarElements.length} similar elements with selector "${selector}"`);

                allSimilarElements.push({
                    columnIndex,
                    columnName: cs.columnName,
                    elements: similarElements,
                    selector
                });
            });

            // Find the minimum number of elements across all columns to ensure complete rows
            const minElements = Math.min(...allSimilarElements.map(col => col.elements.length));

            if (minElements === 0) {
                alert('No similar elements found for table batch mode.\\n\\nTry hovering over elements from the same table structure.');
                return;
            }

            const totalElements = minElements * tableHeaders.length;
            const confirmMsg = `Found ${minElements} similar rows with ${tableHeaders.length} columns each.\\n\\nThis will add ${totalElements} elements to your table.\\n\\nContinue?`;
            if (!confirm(confirmMsg)) return;

            addToHistory('batch_add_table_rows');

            // Add elements row by row
            for (let rowIndex = 0; rowIndex < minElements; rowIndex++) {
                const rowData = [];

                allSimilarElements.forEach(columnData => {
                    const element = columnData.elements[rowIndex];
                    if (element) {
                        const text = element.innerText.trim();
                        if (text) {
                            element.classList.add('extractor-selected-highlight');
                            rowData.push(text);
                        }
                    }
                });

                // Only add complete rows
                if (rowData.length === tableHeaders.length) {
                    tableData.push(rowData);
                }
            }
        }

        updateDisplay();
        updateStatus();
        scheduleAutoSave();
    }

    // Find similar table rows based on column selectors from first row


    function undoLastAction() {
        if (actionHistory.length === 0) {
            alert('Nothing to undo.');
            return;
        }

        const lastState = actionHistory.pop();

        // Restore previous state
        listData = [...lastState.listData];
        tableData = lastState.tableData.map(row => [...row]);
        tableHeaders = [...lastState.tableHeaders];
        currentRow = [...lastState.currentRow];

        // Restore table batch mode state from history if available
        if (currentMode === 'table' && lastState.firstRowCompleted !== undefined) {
            firstRowCompleted = lastState.firstRowCompleted;
            columnSelectors = lastState.columnSelectors ? [...lastState.columnSelectors] : [];

            // Update batch button appearance based on restored state
            const batchBtn = document.getElementById('ex-batch-btn');
            if (batchBtn) {
                if (firstRowCompleted && columnSelectors.length > 0) {
                    batchBtn.title = 'Smart Batch: Select similar table rows';
                    batchBtn.style.background = '#28a745';
                    batchBtn.style.color = 'white';
                } else {
                    batchBtn.title = 'Select Similar Elements';
                    batchBtn.style.background = '';
                    batchBtn.style.color = '';
                }
            }
        } else if (currentMode === 'table') {
            // Reset if no history data available
            firstRowElements = [];
            columnSelectors = [];
            firstRowCompleted = false;

            const batchBtn = document.getElementById('ex-batch-btn');
            if (batchBtn) {
                batchBtn.title = 'Select Similar Elements';
                batchBtn.style.background = '';
                batchBtn.style.color = '';
            }
        }

        // This is complex, so for now we just clear all highlights
        // and let the user re-highlight if needed. A more advanced implementation
        // would track individual elements.
        document.querySelectorAll('.extractor-selected-highlight').forEach(el => {
            el.classList.remove('extractor-selected-highlight');
        });

        updateDisplay();
        updateStatus();
        scheduleAutoSave();

        // Visual feedback
        const undoBtn = document.getElementById('ex-undo-btn');
        const originalText = undoBtn.textContent;
        undoBtn.textContent = '✓';
        setTimeout(() => {
            undoBtn.textContent = originalText;
        }, 1000);
    }

    function updateDisplay() {
        const textarea = document.getElementById('ex-extracted-data');
        if (!textarea) return;

        let output = '';
        if (currentMode === 'list') {
            output = listData.join('\n');
        } else {
            if (tableHeaders.length > 0) {
                output = tableHeaders.join('\t') + '\n';
                output += tableData.map(row => row.join('\t')).join('\n');

                // Show current row progress
                if (currentRow.length > 0) {
                    output += (output.length > 0 ? '\n' : '') + 'Next row: ' + currentRow.join('\t');
                }
            }
        }
        textarea.value = output;
        textarea.scrollTop = textarea.scrollHeight;
    }

    function updateStatus() {
        const statusText = document.getElementById('ex-status-text');
        if (!statusText) return;
        let status = `Mode: ${currentMode.charAt(0).toUpperCase() + currentMode.slice(1)}`;

        if (isSelecting) {
            if (currentMode === 'table') {
                if (tableHeaders.length > 0) {
                    status += ` | Row ${tableData.length + 1}, Col ${currentRow.length + 1}/${tableHeaders.length}`;
                } else {
                    status += ` | Set column names first`;
                }
            } else {
                status += ` | Item ${listData.length + 1}`;
            }
        } else {
            const count = currentMode === 'list' ? listData.length : tableData.length;
            status += ` | ${count} items collected`;
        }
        statusText.textContent = status;
    }

    function clearData() {
        // Add to history before clearing
        if (listData.length > 0 || tableData.length > 0) {
            addToHistory('clear_all');
        }

        listData = [];
        tableData = [];
        tableHeaders = [];
        currentRow = [];

        // Reset table batch mode state
        firstRowElements = [];
        columnSelectors = [];
        firstRowCompleted = false;

        // Reset batch button appearance
        const batchBtn = document.getElementById('ex-batch-btn');
        if (batchBtn) {
            batchBtn.title = 'Select Similar Elements';
            batchBtn.style.background = '';
            batchBtn.style.color = '';
        }

        if (isSelecting) {
            toggleSelection();
        }

        document.querySelectorAll('.extractor-selected-highlight, .extractor-preview-highlight').forEach(el => {
            el.classList.remove('extractor-selected-highlight', 'extractor-preview-highlight');
        });

        if (panelEl) {
            document.getElementById('ex-extracted-data').value = '';
            document.getElementById('ex-column-names').value = '';
        }

        updateStatus();
        scheduleAutoSave();
    }

    function copyToClipboard() {
        const textarea = document.getElementById('ex-extracted-data');
        const copyBtn = document.getElementById('ex-copy-btn');
        if (!textarea || !copyBtn) return;

        if (!textarea.value) {
            alert('No data to copy!');
            return;
        }

        navigator.clipboard.writeText(textarea.value).then(() => {
            const originalText = copyBtn.textContent;
            copyBtn.textContent = 'Copied!';
            copyBtn.style.background = '#28a745';
            copyBtn.style.color = 'white';

            setTimeout(() => {
                copyBtn.textContent = originalText;
                copyBtn.style.background = '';
                copyBtn.style.color = '';
            }, 2000);
        }).catch(err => {
            console.error('Failed to copy text: ', err);

            // Fallback: select text for manual copy
            textarea.select();
            textarea.setSelectionRange(0, 99999);
            alert('Could not copy automatically. Text has been selected - press Ctrl+C to copy.');
        });
    }

    function makeDraggable(element) {
        let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
        let isDragging = false;
        const header = element.querySelector(".ex-header");
        if (!header) return;

        // Enhanced dragging with better UX
        header.style.cursor = 'move';
        header.onmousedown = dragMouseDown;

        function dragMouseDown(e) {
            // Prevent dragging when clicking on buttons
            if (e.target.classList.contains('ex-close-btn') || e.target.classList.contains('ex-minimize-btn')) {
                return;
            }

            e.preventDefault();
            isDragging = true;
            pos3 = e.clientX;
            pos4 = e.clientY;

            // Add visual feedback during drag
            element.style.transition = 'none';
            element.style.opacity = '0.9';
            element.style.transform = 'scale(1.02)';

            // Temporarily reduce pointer events on body to prevent interference
            document.body.style.pointerEvents = 'none';
            element.style.pointerEvents = 'auto';

            document.onmouseup = closeDragElement;
            document.onmousemove = elementDrag;

            // Add class for styling during drag
            element.classList.add('dragging');
        }

        function elementDrag(e) {
            if (!isDragging) return;

            e.preventDefault();
            pos1 = pos3 - e.clientX;
            pos2 = pos4 - e.clientY;
            pos3 = e.clientX;
            pos4 = e.clientY;

            const newTop = element.offsetTop - pos2;
            const newLeft = element.offsetLeft - pos1;

            // Better viewport boundary detection with padding
            const padding = 10;
            const maxTop = window.innerHeight - element.offsetHeight - padding;
            const maxLeft = window.innerWidth - element.offsetWidth - padding;

            element.style.top = Math.max(padding, Math.min(newTop, maxTop)) + "px";
            element.style.left = Math.max(padding, Math.min(newLeft, maxLeft)) + "px";
        }

        function closeDragElement() {
            isDragging = false;
            document.onmouseup = null;
            document.onmousemove = null;

            // Restore visual state and pointer events
            element.style.transition = 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)';
            element.style.opacity = '1';
            element.style.transform = 'scale(1)';
            document.body.style.pointerEvents = 'auto';

            // Remove drag class
            element.classList.remove('dragging');

            // Save position for next session
            const rect = element.getBoundingClientRect();
            GM_setValue('panel_position', {
                top: rect.top,
                left: rect.left
            });
        }

        // Restore last position if available
        const savedPosition = GM_getValue('panel_position', null);
        if (savedPosition) {
            // Use padding equal to that in drag to avoid stuck edges if window resized
            const padding = 10;
            const maxTop = window.innerHeight - element.offsetHeight - padding;
            const maxLeft = window.innerWidth - element.offsetWidth - padding;

            element.style.top = `${Math.min(Math.max(padding, savedPosition.top), maxTop)}px`;
            element.style.left = `${Math.min(Math.max(padding, savedPosition.left), maxLeft)}px`;
        }
    }

    // Toggle panel transparency for better element selection
    function togglePanelTransparency() {
        if (!panelEl) return;

        const isTransparent = panelEl.classList.contains('semi-transparent');
        const transparencyIndicator = document.getElementById('transparency-indicator');

        if (isTransparent) {
            panelEl.classList.remove('semi-transparent');
            if (transparencyIndicator) transparencyIndicator.style.display = 'none';
            // Update status to show normal mode
            const statusText = document.getElementById('ex-status-text');
            if (statusText) {
                statusText.textContent = statusText.textContent.replace(' | Transparent', '');
            }
        } else {
            panelEl.classList.add('semi-transparent');
            if (transparencyIndicator) transparencyIndicator.style.display = 'inline';
            // Update status to show transparent mode
            const statusText = document.getElementById('ex-status-text');
            if (statusText) {
                statusText.textContent += ' | Transparent';
            }
        }
    }

    // --- SCRIPT INITIALIZATION ---
    function init() {
        // Load saved state
        extractorEnabled = false;

        // Register menu command
        updateMenuCommand();

        // Initialize if enabled
        if (extractorEnabled) {
            // Delay initialization to ensure page is fully loaded
            setTimeout(initExtractor, 500);
        }
    }

    // Initialize when DOM is ready
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }

})();