您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Advanced UI element text extractor with keyboard shortcuts, export formats, undo functionality, and optimized performance
当前为
// ==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(); } })();