commit d87e02004e6ee9ec019f06f0856b621aac403955 Author: songtianlun Date: Wed Jul 23 13:57:36 2025 +0800 finish function, with claude and augument diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3037f60 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +claude + diff --git a/INSTALLATION.md b/INSTALLATION.md new file mode 100644 index 0000000..ee272d4 --- /dev/null +++ b/INSTALLATION.md @@ -0,0 +1,99 @@ +# Installation Guide / 安装指南 + +## Prerequisites / 前置要求 + +- Google Chrome browser / Google Chrome 浏览器 +- Developer mode enabled in Chrome extensions / Chrome 扩展开发者模式已启用 + +## Installation Steps / 安装步骤 + +### 1. Verify Files / 验证文件 + +All required files are already included in the project: +- ✅ `manifest.json` - Extension configuration +- ✅ `background.js` - Service worker +- ✅ `content.js` - Main functionality +- ✅ `styles.css` - Styling +- ✅ `popup.html` - Popup interface +- ✅ `icons/icon16.png` - 16x16 icon +- ✅ `icons/icon32.png` - 32x32 icon +- ✅ `icons/icon48.png` - 48x48 icon +- ✅ `icons/icon128.png` - 128x128 icon + +### 2. Load Extension / 加载扩展 + +1. Open Chrome and navigate to `chrome://extensions/` / 打开 Chrome 并访问扩展页面 +2. Enable "Developer mode" in the top right corner / 在右上角启用"开发者模式" +3. Click "Load unpacked" button / 点击"加载已解压的扩展程序"按钮 +4. Select the `dropdown-copy-helper` directory / 选择项目目录 +5. The extension should now appear in your extensions list / 扩展应该出现在扩展列表中 + +### 3. Verify Installation / 验证安装 + +1. Check that the extension icon appears in the Chrome toolbar / 检查扩展图标是否出现在工具栏中 +2. Visit google.com or youtube.com / 访问 google.com 或 youtube.com +3. The extension should be active on these sites / 扩展应该在这些网站上激活 + +## Testing / 测试 + +### Test on Google Search / 在 Google 搜索上测试 + +1. Go to https://www.google.com / 访问 Google 搜索 +2. Click on the search input field / 点击搜索输入框 +3. Type a few characters to trigger search suggestions / 输入几个字符触发搜索建议 +4. Right-click on the search input field / 在搜索输入框上右键点击 +5. Select "Copy All Dropdown Items / 复制所有下拉项" from the context menu / 从右键菜单选择复制选项 +6. Check if a success toast notification appears / 检查是否出现成功提示 +7. Paste (Ctrl+V) to verify the copied content / 粘贴验证复制的内容 + +### Test on YouTube / 在 YouTube 上测试 + +1. Go to https://www.youtube.com / 访问 YouTube +2. Click on the search input field / 点击搜索输入框 +3. Type a few characters to trigger search suggestions / 输入几个字符触发搜索建议 +4. Right-click on the search input field / 在搜索输入框上右键点击 +5. Select "Copy All Dropdown Items / 复制所有下拉项" from the context menu / 从右键菜单选择复制选项 +6. Check if a success toast notification appears / 检查是否出现成功提示 +7. Paste (Ctrl+V) to verify the copied content / 粘贴验证复制的内容 + +## Troubleshooting / 故障排除 + +### Extension not loading / 扩展无法加载 +- Make sure all required files are present / 确保所有必需文件都存在 +- Check that icon files are in the `icons/` directory / 检查图标文件是否在 `icons/` 目录中 +- Verify manifest.json syntax is correct / 验证 manifest.json 语法正确 + +### Context menu not appearing / 右键菜单不出现 +- Make sure you're on a supported website (Google or YouTube) / 确保在支持的网站上 +- Try refreshing the page / 尝试刷新页面 +- Check that you're right-clicking on the correct input field / 检查是否在正确的输入框上右键点击 + +### "No input element found" error / "未找到输入元素"错误 +- **First, click on the search input field** to focus it / 首先点击搜索输入框以聚焦 +- Try typing something in the search box / 尝试在搜索框中输入内容 +- Open browser console (F12) to see debug messages / 打开浏览器控制台查看调试信息 +- Use the test page `input-detection-test.html` to verify input detection / 使用测试页面验证输入检测 + +### Copy function not working / 复制功能不工作 +- Check browser console for error messages / 检查浏览器控制台的错误信息 +- Make sure clipboard permissions are granted / 确保剪贴板权限已授予 +- Try typing in the input field first to trigger suggestions / 先在输入框中输入以触发建议 + +### No dropdown suggestions / 没有下拉建议 +- Type more characters in the search field / 在搜索框中输入更多字符 +- Wait a moment for suggestions to load / 等待建议加载 +- Check your internet connection / 检查网络连接 + +### Debug Steps / 调试步骤 +1. Open `input-detection-test.html` to test input detection / 打开测试页面检测输入框识别 +2. Open browser console (F12) and look for extension messages / 打开控制台查看扩展消息 +3. Check if the extension content script is loaded / 检查内容脚本是否加载 +4. Verify that the search input is being detected / 验证搜索输入框是否被检测到 + +## Development / 开发 + +To modify the extension: +1. Make changes to the source files / 修改源文件 +2. Go to `chrome://extensions/` / 访问扩展页面 +3. Click the refresh button on the extension card / 点击扩展卡片上的刷新按钮 +4. Test your changes / 测试更改 diff --git a/README.md b/README.md new file mode 100644 index 0000000..cdcef0d --- /dev/null +++ b/README.md @@ -0,0 +1,95 @@ +# Dropdown Copy Helper / 下拉复制助手 + +A Chrome extension that helps you copy all dropdown options from input fields on supported websites like Google Search and YouTube. + +## ✨ Features / 功能 + +- **🔍 Smart Detection**: Automatically detects dropdown menus associated with search input fields +- **📋 Right-click Menu**: Adds a context menu option "Copy All Dropdown Items / 复制所有下拉项" +- **📢 Toast Notifications**: Shows success/failure notifications with item count +- **🌐 Multi-site Support**: Currently supports Google Search and YouTube +- **🎯 Precise Targeting**: Only activates on supported websites for better performance +- **🐛 Debug Support**: Comprehensive logging for troubleshooting + +## 🌍 Supported Websites / 支持的网站 + +- **Google Search** (google.com) - Main search suggestions +- **YouTube** (youtube.com) - Video search suggestions + +## 📦 Installation / 安装 + +### Quick Start / 快速开始 + +1. **Ready to Use / 即开即用** + - All required files including icons are already included / 所有必需文件包括图标都已包含 + - No additional setup required / 无需额外设置 + +2. **Load Extension / 加载扩展** + - Open Chrome and go to `chrome://extensions/` / 打开Chrome扩展页面 + - Enable "Developer mode" / 启用开发者模式 + - Click "Load unpacked" and select this directory / 加载此目录 + +3. **Verify Installation / 验证安装** + - Extension icon should appear in Chrome toolbar / 工具栏应显示扩展图标 + - Visit google.com or youtube.com to test / 访问支持的网站测试 + +For detailed installation instructions, see [INSTALLATION.md](INSTALLATION.md) + +## 🚀 Usage / 使用方法 + +1. **Navigate** to Google Search or YouTube / 访问Google搜索或YouTube +2. **Click** on the search input field / 点击搜索输入框 +3. **Type** a few characters to trigger dropdown suggestions / 输入字符触发下拉建议 +4. **Right-click** on the search input field / 在搜索框上右键点击 +5. **Select** "Copy All Dropdown Items / 复制所有下拉项" / 选择复制选项 +6. **Success!** All suggestions are copied to clipboard, one per line / 成功复制所有建议到剪贴板 + +## 🧪 Testing / 测试 + +Open `test.html` in your browser for a comprehensive testing guide with step-by-step instructions. + +## 📁 Project Structure / 项目结构 + +``` +dropdown-copy-helper/ +├── manifest.json # Extension configuration / 扩展配置 +├── background.js # Service worker for context menus / 后台服务 +├── content.js # Main functionality / 主要功能实现 +├── styles.css # Toast notification styles / 通知样式 +├── popup.html # Extension popup interface / 弹窗界面 +├── icons/ # Extension icons / 扩展图标 +├── generate-icons.html # Icon generator tool / 图标生成工具 +├── test.html # Testing guide / 测试指南 +├── INSTALLATION.md # Detailed installation guide / 详细安装指南 +└── README.md # This file / 说明文档 +``` + +## 🔧 Development / 开发 + +### Key Components / 核心组件 + +- **`manifest.json`**: Defines permissions, content scripts, and extension metadata +- **`background.js`**: Handles context menu creation and clipboard operations +- **`content.js`**: Core functionality for dropdown detection and text extraction +- **`styles.css`**: Styling for toast notifications with responsive design +- **`popup.html`**: User-friendly popup with usage instructions + +### Debugging / 调试 + +The extension includes comprehensive logging. Open browser console (F12) to see: +- Content script loading status +- Input element detection +- Dropdown item discovery +- Copy operation results + +## 🤝 Contributing / 贡献 + +1. Fork the repository / 分叉仓库 +2. Create a feature branch / 创建功能分支 +3. Make your changes / 进行更改 +4. Test thoroughly using `test.html` / 使用测试页面充分测试 +5. Submit a pull request / 提交拉取请求 + +## 📄 License / 许可证 + +MIT License - see LICENSE file for details diff --git a/background.js b/background.js new file mode 100644 index 0000000..1b4c0e1 --- /dev/null +++ b/background.js @@ -0,0 +1,107 @@ +// Background script for Dropdown Copy Helper +// 下拉复制助手后台脚本 + +// Context menu item ID +const CONTEXT_MENU_ID = 'copy-dropdown-items'; + +// Create context menu when extension is installed +chrome.runtime.onInstalled.addListener(() => { + chrome.contextMenus.create({ + id: CONTEXT_MENU_ID, + title: 'Copy All Dropdown Items\n复制所有下拉项', + contexts: ['editable'], + documentUrlPatterns: [ + 'https://www.google.com/*', + 'https://google.com/*', + 'https://www.youtube.com/*', + 'https://youtube.com/*' + ] + }); +}); + +// Handle context menu clicks +chrome.contextMenus.onClicked.addListener((info, tab) => { + if (info.menuItemId === CONTEXT_MENU_ID) { + // Send message to content script to copy dropdown items + chrome.tabs.sendMessage(tab.id, { + action: 'copyDropdownItems', + frameId: info.frameId + }, (response) => { + // Handle response or connection errors + if (chrome.runtime.lastError) { + console.log('Connection error:', chrome.runtime.lastError.message); + // Try to inject content script and retry + injectContentScriptAndRetry(tab.id); + } else if (response) { + console.log('Copy operation result:', response); + } + }); + } +}); + +// Function to inject content script and retry the operation +function injectContentScriptAndRetry(tabId) { + chrome.scripting.executeScript({ + target: { tabId: tabId }, + files: ['content.js'] + }, () => { + if (chrome.runtime.lastError) { + console.error('Failed to inject content script:', chrome.runtime.lastError.message); + } else { + // Retry after a short delay + setTimeout(() => { + chrome.tabs.sendMessage(tabId, { + action: 'copyDropdownItems' + }, (response) => { + if (chrome.runtime.lastError) { + console.error('Retry failed:', chrome.runtime.lastError.message); + } + }); + }, 500); + } + }); +} + +// Handle messages from content script +chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { + if (request.action === 'ping') { + sendResponse({ status: 'ready' }); + } + // Note: Clipboard operations are now handled directly in content script +}); + +// Handle extension startup +chrome.runtime.onStartup.addListener(() => { + console.log('Dropdown Copy Helper extension started'); +}); + +// Handle tab updates to ensure content script is ready +chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { + if (changeInfo.status === 'complete' && tab.url) { + const supportedSites = [ + 'https://www.google.com/', + 'https://google.com/', + 'https://www.youtube.com/', + 'https://youtube.com/' + ]; + + const isSupported = supportedSites.some(site => tab.url.startsWith(site)); + if (isSupported) { + // Ping to check if content script is ready + chrome.tabs.sendMessage(tabId, { action: 'ping' }, (response) => { + if (chrome.runtime.lastError) { + // Content script not ready, inject it + console.log('Injecting content script for tab:', tabId); + chrome.scripting.executeScript({ + target: { tabId: tabId }, + files: ['content.js'] + }).catch(error => { + console.error('Failed to inject content script:', error); + }); + } else { + console.log('Content script ready for tab:', tabId); + } + }); + } + } +}); diff --git a/content.js b/content.js new file mode 100644 index 0000000..5fe73cf --- /dev/null +++ b/content.js @@ -0,0 +1,701 @@ +// Content script for Dropdown Copy Helper +// 下拉复制助手内容脚本 + +class DropdownCopyHelper { + constructor() { + this.currentInputElement = null; + this.init(); + } + + init() { + // Prevent multiple initialization + if (window.dropdownCopyHelperInitialized) { + console.log('⚠️ Dropdown Copy Helper already initialized'); + return; + } + window.dropdownCopyHelperInitialized = true; + + // Listen for messages from background script + chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { + try { + if (request.action === 'copyDropdownItems') { + this.handleCopyRequest(sendResponse); + return true; // Keep message channel open for async response + } else if (request.action === 'ping') { + sendResponse({ status: 'ready', timestamp: Date.now() }); + return false; // Synchronous response + } + } catch (error) { + console.error('❌ Message handling error:', error); + sendResponse({ success: false, error: error.message }); + } + }); + + // Track focused input elements + this.trackInputFocus(); + + console.log('✅ Dropdown Copy Helper loaded on:', window.location.hostname); + } + + // Get dropdown items using simplified strategy + getDropdownItems() { + const hostname = window.location.hostname; + let items = []; + + if (hostname.includes('google.com')) { + items = this.getGoogleDropdownItems(); + } else if (hostname.includes('youtube.com')) { + items = this.getYouTubeDropdownItems(); + } + + // If no items found with standard methods, try emergency fallback + if (items.length === 0) { + items = this.getEmergencyFallbackItems(); + } + + // Filter and clean items + const cleanItems = items + .filter(item => item && item.trim().length > 0) + .filter(item => !this.isUIElement(item)) + .map(item => this.cleanSearchSuggestionText(item)) + .filter(item => item && item.length > 0 && item.length < 150); + + return [...new Set(cleanItems)]; // Remove duplicates + } + + + // Get Google search suggestions + getGoogleDropdownItems() { + const items = []; + + // Comprehensive list of selectors from most modern to legacy + const selectors = [ + // Most modern Google selectors (2024+) + 'ul[role="listbox"] li[role="option"]', + 'div[role="listbox"] div[role="option"]', + '[role="listbox"] [role="option"]', + + // Alternative modern structures + '.G43f7e li[role="option"]', + '.G43f7e div[role="option"]', + '.aajZCb li[role="option"]', + '.aajZCb div[role="option"]', + '.erkvQe li[role="option"]', + '.erkvQe li', + '.erkvQe div', + + // Google suggestions container variations + '.sbsb_b li', + '.sbsb_b div', + '.sbsb_c', + '.sbqs_c', + '.gsq_a', + '.pcTkSc', + '.sbtc', + '.sbl1', + + // Broader searches for any suggestion-like elements + 'li[data-ved]', + 'div[data-ved]', + 'span[data-ved]', + + // Last resort - any li under potential containers + '.sbsb_b li', + 'ul li', + 'div[jsname] li', + 'div[jsname] div' + ]; + + for (const selector of selectors) { + const elements = document.querySelectorAll(selector); + + if (elements.length > 0) { + let foundInThisSelector = 0; + elements.forEach(el => { + if (this.isVisible(el)) { + const text = this.extractCleanText(el); + if (text && text.length > 0 && text.length < 200) { + if (this.looksLikeSearchSuggestion(text)) { + items.push(text); + foundInThisSelector++; + } + } + } + }); + + // If we found items with this selector, stop trying others + if (foundInThisSelector > 0) { + break; + } + } + } + + return items; + } + + // Check if text looks like a search suggestion rather than UI element + looksLikeSearchSuggestion(text) { + // Reject obviously non-suggestion text + const rejectPatterns = [ + /^(搜索|查看更多|删除|更多|清除|历史记录)$/i, + /^(search|view more|delete|more|clear|history)$/i, + /^[×✕]$/, + /^[\s]*$/, + /^[0-9]+$/, + /^(OM|中国国际航空|Airbnb)$/ + ]; + + return !rejectPatterns.some(pattern => pattern.test(text)); + } + + // Extract clean text from element - simplified version + extractCleanText(element) { + if (!element) return ''; + + // Get the text content + let text = element.textContent || element.innerText || ''; + + // Basic cleaning + text = text.trim(); + + // Remove obvious UI elements by splitting and taking the first part + const parts = text.split(/[,,·•\|\n]/); + text = parts[0].trim(); + + // Remove common trailing UI text + text = text.replace(/(查看更多|删除|更多|OM|View more|Delete|Remove)$/gi, '').trim(); + + return text; + } + + // Check if text appears to be a UI element rather than search suggestion + isUIElement(text) { + const uiPatterns = [ + /^查看更多/, + /删除$/, + /^更多$/, + /^搜索$/, + /^OM$/, + /^×$/, + /^✕$/, + /^\s*$/ // empty or whitespace only + ]; + + return uiPatterns.some(pattern => pattern.test(text)); + } + + // Get YouTube search suggestions + getYouTubeDropdownItems() { + const items = []; + + const selectors = [ + // Modern YouTube selectors + '[role="listbox"] [role="option"]', + '.ytd-searchbox [role="option"]', + + // Traditional YouTube selectors + '.sbsb_c', + '.sbsb_a', + '.sbqs_c', + + // Container-based searches + '.ytd-searchbox .sbsb_c', + '#search-container [role="option"]', + '.search-container [role="option"]', + + // Broader searches + 'li[data-ved]', + 'div[data-ved]', + + // Last resort + '.ytd-searchbox li', + '.ytd-searchbox div', + '#search li', + '#search div' + ]; + + for (const selector of selectors) { + const elements = document.querySelectorAll(selector); + + if (elements.length > 0) { + let foundInThisSelector = 0; + elements.forEach(el => { + if (this.isVisible(el)) { + const text = this.extractCleanText(el); + if (text && text.length > 0 && text.length < 200) { + if (this.looksLikeSearchSuggestion(text)) { + items.push(text); + foundInThisSelector++; + } + } + } + }); + + if (foundInThisSelector > 0) { + break; + } + } + } + + return items; + } + + // Emergency fallback - try to find ANY dropdown-like elements on the page + getEmergencyFallbackItems() { + const items = []; + + // Look for any elements that might contain suggestions + const emergencySelectors = [ + // Any list items anywhere + 'li', + // Any divs with suggestion-like attributes + 'div[role]', + 'span[role]', + // Elements with data attributes (common in modern web apps) + '[data-ved]', + '[data-value]', + '[data-suggestion]', + // Any elements that might be in a dropdown + 'ul > *', + 'ol > *', + '.dropdown *', + '.suggestions *', + '.autocomplete *' + ]; + + for (const selector of emergencySelectors) { + const elements = document.querySelectorAll(selector); + if (elements.length > 0) { + let foundCount = 0; + elements.forEach(el => { + if (foundCount >= 10) return; // Limit to prevent spam + + if (this.isVisible(el)) { + const text = this.extractCleanText(el); + if (text && text.length > 2 && text.length < 100) { + if (this.looksLikeSearchSuggestion(text) && !items.includes(text)) { + items.push(text); + foundCount++; + } + } + } + }); + + if (foundCount > 0) { + break; + } + } + } + + return items.slice(0, 10); // Limit to 10 items max + } + + // Track input element focus + trackInputFocus() { + // Track right-click context menu + document.addEventListener('contextmenu', (event) => { + this.handleContextMenu(event); + }); + + // Also track focus and input events to better detect active search inputs + document.addEventListener('focusin', (event) => { + if (this.isSearchInput(event.target)) { + this.currentInputElement = event.target; + console.log('✅ Search input focused:', event.target); + } + }); + + // Track input events to detect when user is typing + document.addEventListener('input', (event) => { + if (this.isSearchInput(event.target)) { + this.currentInputElement = event.target; + console.log('✅ Search input active:', event.target); + } + }); + } + + // Handle context menu event + handleContextMenu(event) { + // Check if right-clicked element is a search input + if (this.isSearchInput(event.target)) { + this.currentInputElement = event.target; + console.log('✅ Search input detected via context menu:', event.target); + return; + } + + // Check if right-clicked near a search input + const nearbyInput = this.findNearbySearchInput(event.target); + if (nearbyInput) { + this.currentInputElement = nearbyInput; + console.log('✅ Nearby search input detected:', nearbyInput); + return; + } + + // Fallback: find main search input on page + this.currentInputElement = this.findMainSearchInput(); + console.log('🔍 Using main search input fallback:', this.currentInputElement); + } + + // Find search input near the clicked element + findNearbySearchInput(element) { + // Check parent elements up to 5 levels + let current = element; + for (let i = 0; i < 5 && current; i++) { + const searchInput = current.querySelector('input, textarea'); + if (searchInput && this.isSearchInput(searchInput)) { + return searchInput; + } + current = current.parentElement; + } + + // Check sibling elements + if (element.parentElement) { + const siblings = element.parentElement.querySelectorAll('input, textarea'); + for (const sibling of siblings) { + if (this.isSearchInput(sibling)) { + return sibling; + } + } + } + + return null; + } + + // Find the main search input on the page + findMainSearchInput() { + const hostname = window.location.hostname; + + if (hostname.includes('google.com')) { + const selectors = [ + 'input[name="q"]', + 'textarea[name="q"]', + '.gLFyf', + 'input[role="combobox"]', + 'input[aria-label*="Search"]', + 'input[aria-label*="搜索"]', + 'input[title*="Search"]', + 'input[placeholder*="Search"]' + ]; + + for (const selector of selectors) { + const input = document.querySelector(selector); + if (input && this.isVisible(input)) { + return input; + } + } + } + + if (hostname.includes('youtube.com')) { + const selectors = [ + 'input[name="search_query"]', + 'input#search', + 'input[aria-label*="Search"]', + 'input[placeholder*="Search"]', + '.ytd-searchbox input', + '#search-input input' + ]; + + for (const selector of selectors) { + const input = document.querySelector(selector); + if (input && this.isVisible(input)) { + return input; + } + } + } + + // Generic fallback - look for any search-related input + const genericSelectors = [ + 'input[name="search"]', + 'input[name="query"]', + 'input[name="q"]', + 'input[type="search"]', + 'input[aria-label*="search" i]', + 'input[placeholder*="search" i]', + 'input[title*="search" i]' + ]; + + for (const selector of genericSelectors) { + const input = document.querySelector(selector); + if (input && this.isVisible(input)) { + return input; + } + } + + return null; + } + + // Enhanced search input detection + isSearchInput(element) { + if (!element || (element.tagName !== 'INPUT' && element.tagName !== 'TEXTAREA')) return false; + + // Check if element is visible + if (!this.isVisible(element)) return false; + + const hostname = window.location.hostname; + const name = element.name?.toLowerCase() || ''; + const id = element.id?.toLowerCase() || ''; + const className = element.className?.toLowerCase() || ''; + const placeholder = element.placeholder?.toLowerCase() || ''; + const ariaLabel = element.getAttribute('aria-label')?.toLowerCase() || ''; + const title = element.title?.toLowerCase() || ''; + const type = element.type?.toLowerCase() || ''; + + // Site-specific checks + if (hostname.includes('google.com')) { + return name === 'q' || + className.includes('glfyf') || + element.getAttribute('role') === 'combobox' || + ariaLabel.includes('search') || + ariaLabel.includes('搜索'); + } + + if (hostname.includes('youtube.com')) { + return name === 'search_query' || + id === 'search' || + className.includes('search') || + ariaLabel.includes('search'); + } + + // Generic search input detection + const searchTerms = ['search', 'query', 'find', '搜索', '查找']; + const searchFields = [name, id, className, placeholder, ariaLabel, title]; + + // Check if type is search + if (type === 'search') return true; + + // Check if any field contains search terms + return searchTerms.some(term => + searchFields.some(field => field.includes(term)) + ) || name === 'q'; + } + + // Handle copy request from context menu - simplified and fast + async handleCopyRequest(sendResponse) { + let responseSent = false; + + const safeResponse = (data) => { + if (!responseSent) { + responseSent = true; + try { + sendResponse(data); + } catch (error) { + console.error('Failed to send response:', error); + } + } + }; + + try { + + // Find search input if not already set + if (!this.currentInputElement) { + this.currentInputElement = this.findMainSearchInput(); + } + + if (!this.currentInputElement) { + const errorMsg = 'No search input found. Please click on a search input field first.\n未找到搜索输入框。请先点击搜索框,然后输入内容显示下拉建议。'; + this.showToast(`❌ ${errorMsg}`, 'error'); + safeResponse({ success: false, error: errorMsg }); + return; + } + + // Focus input briefly to ensure dropdown is active + this.currentInputElement.focus(); + + // Get dropdown items immediately - no complex waiting + let dropdownItems = this.getDropdownItems(); + + // If no items found, wait a very short time and try once more + if (dropdownItems.length === 0) { + await new Promise(resolve => setTimeout(resolve, 300)); + dropdownItems = this.getDropdownItems(); + } + + if (dropdownItems.length === 0) { + const errorMsg = 'No dropdown suggestions found. Please type something to show suggestions first.\n未找到下拉建议。请在搜索框中输入内容以显示建议列表。'; + this.showToast(`❌ ${errorMsg}`, 'error'); + safeResponse({ success: false, error: errorMsg }); + return; + } + + // Copy to clipboard + const textToCopy = dropdownItems.join('\n'); + await this.copyToClipboard(textToCopy); + + this.showToast(`✅ Successfully copied ${dropdownItems.length} items!\n成功复制 ${dropdownItems.length} 条建议!`, 'success'); + safeResponse({ success: true, count: dropdownItems.length, items: dropdownItems }); + + } catch (error) { + console.error('❌ Copy failed:', error); + this.showToast(`❌ ${error.message}`, 'error'); + safeResponse({ success: false, error: error.message }); + } + } + + + + + // Extract clean text from element with filtering + extractTextFromElement(element) { + // Remove script and style elements + const clone = element.cloneNode(true); + const scripts = clone.querySelectorAll('script, style'); + scripts.forEach(script => script.remove()); + + let text = clone.textContent || clone.innerText || ''; + + // Clean up the text + text = this.cleanSearchSuggestionText(text); + + return text; + } + + // Clean search suggestion text - simplified version + cleanSearchSuggestionText(text) { + if (!text) return ''; + + // Basic cleaning + text = text.trim().replace(/\s+/g, ' '); + + // Remove common UI elements at the end + text = text.replace(/(查看更多删除|查看更多|删除|更多|OM|View more|Delete|Remove)$/gi, '').trim(); + + // Remove trailing dots and clean up + text = text.replace(/\.{2,}$/, '').trim(); + + return text; + } + + // Check if element is visible + isVisible(element) { + if (!element) return false; + + const style = window.getComputedStyle(element); + return style.display !== 'none' && + style.visibility !== 'hidden' && + style.opacity !== '0' && + element.offsetWidth > 0 && + element.offsetHeight > 0; + } + + // Copy text to clipboard + async copyToClipboard(text) { + if (navigator.clipboard) { + await navigator.clipboard.writeText(text); + } else { + // Fallback + const textArea = document.createElement('textarea'); + textArea.value = text; + textArea.style.position = 'fixed'; + textArea.style.left = '-999999px'; + document.body.appendChild(textArea); + textArea.select(); + document.execCommand('copy'); + document.body.removeChild(textArea); + } + } + + + + // Show enhanced toast notification + showToast(message, type = 'info') { + // Remove existing toast + const existingToast = document.getElementById('dropdown-copy-toast'); + if (existingToast) existingToast.remove(); + + const toast = document.createElement('div'); + toast.id = 'dropdown-copy-toast'; + toast.className = `dropdown-copy-toast ${type}`; + + // Handle multi-line messages (bilingual support) + if (message.includes('\n')) { + const lines = message.split('\n'); + lines.forEach((line, index) => { + const lineDiv = document.createElement('div'); + lineDiv.textContent = line; + if (index > 0) { + lineDiv.style.fontSize = '12px'; + lineDiv.style.opacity = '0.9'; + lineDiv.style.marginTop = '2px'; + } + toast.appendChild(lineDiv); + }); + } else { + toast.textContent = message; + } + + // Enhanced styling + Object.assign(toast.style, { + position: 'fixed', + top: '20px', + right: '20px', + padding: '16px 24px', + borderRadius: '8px', + color: 'white', + backgroundColor: this.getToastColor(type), + fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif', + fontSize: '14px', + fontWeight: '500', + zIndex: '2147483647', // Maximum z-index + boxShadow: '0 8px 32px rgba(0,0,0,0.2)', + backdropFilter: 'blur(10px)', + border: '1px solid rgba(255,255,255,0.1)', + maxWidth: '400px', + wordBreak: 'break-word', + transition: 'all 0.3s ease', + opacity: '0', + transform: 'translateX(100%)' + }); + + document.body.appendChild(toast); + + // Animate in + requestAnimationFrame(() => { + toast.style.opacity = '1'; + toast.style.transform = 'translateX(0)'; + }); + + // Auto-remove with fade out animation + setTimeout(() => { + toast.style.opacity = '0'; + toast.style.transform = 'translateX(100%)'; + setTimeout(() => { + if (toast.parentNode) { + toast.parentNode.removeChild(toast); + } + }, 300); + }, 4000); + + // Click to dismiss + toast.addEventListener('click', () => { + toast.style.opacity = '0'; + toast.style.transform = 'translateX(100%)'; + setTimeout(() => { + if (toast.parentNode) { + toast.parentNode.removeChild(toast); + } + }, 300); + }); + } + + // Get toast background color based on type + getToastColor(type) { + switch (type) { + case 'success': return '#10b981'; + case 'error': return '#ef4444'; + case 'warning': return '#f59e0b'; + case 'info': + default: return '#3b82f6'; + } + } +} + +// Initialize the helper when DOM is ready (only once) +if (!window.dropdownCopyHelper) { + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', () => { + window.dropdownCopyHelper = new DropdownCopyHelper(); + }); + } else { + window.dropdownCopyHelper = new DropdownCopyHelper(); + } +} diff --git a/create-icons.js b/create-icons.js new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/create-icons.js @@ -0,0 +1 @@ + diff --git a/icons/README.md b/icons/README.md new file mode 100644 index 0000000..4ad8fec --- /dev/null +++ b/icons/README.md @@ -0,0 +1,39 @@ +# Icons for Dropdown Copy Helper + +This directory contains all the required icon files for the Chrome extension: + +## ✅ Available Icon Files: +- ✅ `icon16.png` (16x16 pixels) - Toolbar icon +- ✅ `icon32.png` (32x32 pixels) - Extension management page +- ✅ `icon48.png` (48x48 pixels) - Extension management page +- ✅ `icon128.png` (128x128 pixels) - Chrome Web Store and installation +- ✅ `icon.png` (1080x1080 pixels) - Original high-resolution source +- ✅ `icon.svg` (Vector format) - Scalable vector source + +## Icon Creation Process: + +The icons were automatically generated from your original `icon.png` (1080x1080) using ImageMagick: + +```bash +magick icons/icon.png -resize 16x16 icons/icon16.png +magick icons/icon.png -resize 32x32 icons/icon32.png +magick icons/icon.png -resize 48x48 icons/icon48.png +magick icons/icon.png -resize 128x128 icons/icon128.png +``` + +## Icon Usage in Extension: + +- **icon16.png**: Displayed in the Chrome toolbar +- **icon32.png**: Used in extension management pages +- **icon48.png**: Used in extension management pages and details +- **icon128.png**: Used in Chrome Web Store and during installation + +## File Verification: + +All icon files have been verified: +- ✅ Correct dimensions +- ✅ PNG format with transparency support +- ✅ Proper color depth +- ✅ Ready for Chrome extension use + +The extension is now ready to be loaded into Chrome with proper icon support! diff --git a/icons/icon.png b/icons/icon.png new file mode 100644 index 0000000..4f84003 Binary files /dev/null and b/icons/icon.png differ diff --git a/icons/icon.svg b/icons/icon.svg new file mode 100644 index 0000000..f7a1442 --- /dev/null +++ b/icons/icon.svg @@ -0,0 +1,182 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/icons/icon128.png b/icons/icon128.png new file mode 100644 index 0000000..94a2412 Binary files /dev/null and b/icons/icon128.png differ diff --git a/icons/icon16.png b/icons/icon16.png new file mode 100644 index 0000000..2ac2021 Binary files /dev/null and b/icons/icon16.png differ diff --git a/icons/icon32.png b/icons/icon32.png new file mode 100644 index 0000000..829f658 Binary files /dev/null and b/icons/icon32.png differ diff --git a/icons/icon48.png b/icons/icon48.png new file mode 100644 index 0000000..22427a1 Binary files /dev/null and b/icons/icon48.png differ diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..5ba6314 --- /dev/null +++ b/manifest.json @@ -0,0 +1,58 @@ +{ + "manifest_version": 3, + "name": "Dropdown Copy Helper / 下拉复制助手", + "version": "1.0.0", + "description": "A Chrome extension to copy all dropdown options from input fields on supported websites like Google and YouTube. 一个Chrome插件,用于从Google和YouTube等支持的网站的输入框下拉选项中复制所有内容。", + "permissions": [ + "contextMenus", + "activeTab", + "clipboardWrite", + "scripting" + ], + "host_permissions": [ + "https://www.google.com/*", + "https://google.com/*", + "https://www.youtube.com/*", + "https://youtube.com/*" + ], + "background": { + "service_worker": "background.js" + }, + "content_scripts": [ + { + "matches": [ + "https://www.google.com/*", + "https://google.com/*", + "https://www.youtube.com/*", + "https://youtube.com/*" + ], + "js": [ + "content.js" + ], + "css": [ + "styles.css" + ], + "run_at": "document_end" + } + ], + "action": { + "default_popup": "popup.html", + "default_title": "Dropdown Copy Helper" + }, + "icons": { + "16": "icons/icon16.png", + "32": "icons/icon32.png", + "48": "icons/icon48.png", + "128": "icons/icon128.png" + }, + "web_accessible_resources": [ + { + "resources": [ + "toast.html" + ], + "matches": [ + "" + ] + } + ] +} \ No newline at end of file diff --git a/popup.html b/popup.html new file mode 100644 index 0000000..c8b0836 --- /dev/null +++ b/popup.html @@ -0,0 +1,154 @@ + + + + + + + +
+
Dropdown Copy Helper
+
下拉复制助手
+
+ +
+
+
📋
+
Right-click input fields to copy dropdown items
+
+ +
+
🔍
+
Automatically detects dropdown suggestions
+
+ +
+
📢
+
Shows toast notifications for copy status
+
+ +
+
Supported Sites / 支持的网站:
+
+
+ Google Search +
+
+
+ YouTube +
+
+ +
+
How to use / 使用方法:
+
+ 1. Visit a supported website
+ 2. Click on a search input field
+ 3. Right-click and select "Copy All Dropdown Items"
+ 4. All suggestions will be copied to clipboard +
+
+
+ + diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..23a4c01 --- /dev/null +++ b/styles.css @@ -0,0 +1,98 @@ +/* Styles for Dropdown Copy Helper */ +/* 下拉复制助手样式 */ + +/* Toast notification styles */ +.dropdown-copy-toast { + position: fixed !important; + top: 20px !important; + right: 20px !important; + padding: 12px 20px !important; + border-radius: 6px !important; + color: white !important; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important; + font-size: 14px !important; + font-weight: 500 !important; + z-index: 10000 !important; + box-shadow: 0 4px 12px rgba(0,0,0,0.15) !important; + max-width: 300px !important; + word-wrap: break-word !important; + transition: all 0.3s ease !important; + opacity: 1 !important; + transform: translateX(0) !important; +} + +.dropdown-copy-toast.success { + background-color: #4caf50 !important; +} + +.dropdown-copy-toast.error { + background-color: #f44336 !important; +} + +.dropdown-copy-toast.info { + background-color: #2196f3 !important; +} + +/* Animation for toast appearance */ +@keyframes slideInRight { + from { + opacity: 0; + transform: translateX(100%); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +.dropdown-copy-toast { + animation: slideInRight 0.3s ease !important; +} + +/* Ensure toast is above all other elements */ +.dropdown-copy-toast { + position: fixed !important; + z-index: 2147483647 !important; /* Maximum z-index value */ +} + +/* Responsive design for mobile */ +@media (max-width: 768px) { + .dropdown-copy-toast { + top: 10px !important; + right: 10px !important; + left: 10px !important; + max-width: none !important; + font-size: 13px !important; + padding: 10px 16px !important; + } +} + +/* High contrast mode support */ +@media (prefers-contrast: high) { + .dropdown-copy-toast { + border: 2px solid white !important; + } +} + +/* Reduced motion support */ +@media (prefers-reduced-motion: reduce) { + .dropdown-copy-toast { + animation: none !important; + transition: none !important; + } +} + +/* Dark mode support */ +@media (prefers-color-scheme: dark) { + .dropdown-copy-toast.success { + background-color: #2e7d32 !important; + } + + .dropdown-copy-toast.error { + background-color: #c62828 !important; + } + + .dropdown-copy-toast.info { + background-color: #1565c0 !important; + } +}