dropdown-copy-helper/content.js

702 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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();
}
}