├── icon.png ├── icon-open.png ├── thumbnail.png ├── .gitignore ├── popup.css ├── LICENSE ├── manifest.json ├── _locales ├── zh │ └── messages.json └── en │ └── messages.json ├── .eslintrc.json ├── CHANGELOG.md ├── README.md ├── src ├── modules │ ├── i18n.js │ ├── colorUtils.js │ ├── fontDetection.js │ ├── listeners.js │ └── tooltips.js └── app.js ├── CONTRIBUTING.md ├── background.js └── contentScript.js /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zanwei/FontDetector/HEAD/icon.png -------------------------------------------------------------------------------- /icon-open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zanwei/FontDetector/HEAD/icon-open.png -------------------------------------------------------------------------------- /thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zanwei/FontDetector/HEAD/thumbnail.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS generated files 2 | .DS_Store 3 | .DS_Store? 4 | ._* 5 | .Spotlight-V100 6 | .Trashes 7 | ehthumbs.db 8 | Thumbs.db 9 | 10 | # IDE directories 11 | .idea/ 12 | .vscode/ 13 | *.sublime-project 14 | *.sublime-workspace 15 | 16 | # Node modules (if used in future) 17 | node_modules/ 18 | npm-debug.log 19 | yarn-error.log 20 | package-lock.json 21 | yarn.lock 22 | 23 | # Distribution directories 24 | dist/ 25 | build/ 26 | 27 | # Testing 28 | coverage/ 29 | .nyc_output/ 30 | 31 | # Logs 32 | logs 33 | *.log -------------------------------------------------------------------------------- /popup.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;700&display=swap'); 2 | 3 | /** 4 | * Basic styles for the popup 5 | */ 6 | body { 7 | font-family: 'Poppins', Arial, sans-serif; 8 | margin: 0; 9 | padding: 0; 10 | color: initial; 11 | } 12 | 13 | .container { 14 | padding: 0; 15 | } 16 | 17 | h1 { 18 | margin-top: 0; 19 | margin-bottom: 10px; 20 | color: #A8A8A8; 21 | font-size: 16px; 22 | } 23 | 24 | /** 25 | * Font detector specific styles 26 | */ 27 | .font-detector { 28 | color: #A8A8A8; 29 | } 30 | 31 | .font-detector span { 32 | color: #fff; 33 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023-2024 FontDetector Contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "FontDetector", 4 | "version": "1.2.1", 5 | "description": "Display font information on hover over text elements.", 6 | "default_locale": "en", 7 | "permissions": [ 8 | "tabs", 9 | "activeTab", 10 | "scripting" 11 | ], 12 | "host_permissions": [ 13 | "" 14 | ], 15 | "background": { 16 | "service_worker": "background.js" 17 | }, 18 | "action": { 19 | "default_icon": "icon.png" 20 | }, 21 | "content_scripts": [ 22 | { 23 | "matches": [ 24 | "" 25 | ], 26 | "exclude_matches": [ 27 | "*://chrome.google.com/webstore/*", 28 | "*://*.chrome.google.com/*" 29 | ], 30 | "js": [ 31 | "contentScript.js" 32 | ], 33 | "all_frames": true, 34 | "run_at": "document_idle" 35 | } 36 | ], 37 | "web_accessible_resources": [ 38 | { 39 | "resources": ["Satoshi-Medium.otf"], 40 | "matches": [""] 41 | } 42 | ], 43 | "icons": { 44 | "128": "icon.png" 45 | }, 46 | "commands": { 47 | "toggle_font_detector": { 48 | "suggested_key": { 49 | "default": "Shift+Alt+X", 50 | "mac": "Shift+Command+X" 51 | }, 52 | "description": "Toggle font detector" 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /_locales/zh/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "appName": { 3 | "message": "字体检测器", 4 | "description": "扩展程序名称" 5 | }, 6 | "appDesc": { 7 | "message": "悬停在文本元素上显示字体信息。", 8 | "description": "扩展程序描述" 9 | }, 10 | "fontFamily": { 11 | "message": "字体系列", 12 | "description": "字体系列标签" 13 | }, 14 | "fontWeight": { 15 | "message": "字体粗细", 16 | "description": "字体粗细标签" 17 | }, 18 | "fontSize": { 19 | "message": "字体大小", 20 | "description": "字体大小标签" 21 | }, 22 | "lineHeight": { 23 | "message": "行高", 24 | "description": "行高标签" 25 | }, 26 | "letterSpacing": { 27 | "message": "字间距", 28 | "description": "字间距标签" 29 | }, 30 | "textAlign": { 31 | "message": "文本对齐", 32 | "description": "文本对齐标签" 33 | }, 34 | "color": { 35 | "message": "颜色", 36 | "description": "颜色标签" 37 | }, 38 | "colorHex": { 39 | "message": "十六进制", 40 | "description": "十六进制颜色格式标签" 41 | }, 42 | "colorLCH": { 43 | "message": "LCH", 44 | "description": "LCH颜色格式标签" 45 | }, 46 | "colorHCL": { 47 | "message": "HCL", 48 | "description": "HCL颜色格式标签" 49 | }, 50 | "copied": { 51 | "message": "已复制", 52 | "description": "复制后显示的文本" 53 | }, 54 | "noFontFound": { 55 | "message": "没有可用的字体信息", 56 | "description": "未找到字体信息时的消息" 57 | }, 58 | "clickToCopy": { 59 | "message": "点击复制", 60 | "description": "复制按钮的提示" 61 | } 62 | } -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "webextensions": true 6 | }, 7 | "extends": "eslint:recommended", 8 | "parserOptions": { 9 | "ecmaVersion": 2022, 10 | "sourceType": "module" 11 | }, 12 | "rules": { 13 | "indent": [ 14 | "error", 15 | 2, 16 | { "SwitchCase": 1 } 17 | ], 18 | "linebreak-style": [ 19 | "error", 20 | "unix" 21 | ], 22 | "quotes": [ 23 | "error", 24 | "single", 25 | { "avoidEscape": true } 26 | ], 27 | "semi": [ 28 | "error", 29 | "always" 30 | ], 31 | "no-unused-vars": [ 32 | "warn", 33 | { "vars": "all", "args": "after-used", "ignoreRestSiblings": false } 34 | ], 35 | "no-console": [ 36 | "warn", 37 | { "allow": ["warn", "error", "log"] } 38 | ], 39 | "max-len": [ 40 | "warn", 41 | { "code": 100 } 42 | ], 43 | "curly": ["error", "all"], 44 | "brace-style": ["error", "1tbs"], 45 | "no-var": "error", 46 | "prefer-const": "warn", 47 | "no-trailing-spaces": "error", 48 | "eqeqeq": ["error", "always"], 49 | "comma-dangle": ["error", "never"], 50 | "arrow-spacing": "error", 51 | "no-multi-spaces": "error", 52 | "space-before-blocks": "error", 53 | "keyword-spacing": "error", 54 | "space-infix-ops": "error", 55 | "no-multiple-empty-lines": ["error", { "max": 2, "maxEOF": 1 }], 56 | "object-curly-spacing": ["error", "always"], 57 | "array-bracket-spacing": ["error", "never"], 58 | "key-spacing": ["error", { "beforeColon": false, "afterColon": true }], 59 | "comma-spacing": ["error", { "before": false, "after": true }] 60 | } 61 | } -------------------------------------------------------------------------------- /_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "appName": { 3 | "message": "FontDetector", 4 | "description": "The name of the extension" 5 | }, 6 | "appDesc": { 7 | "message": "Display font information on hover over text elements.", 8 | "description": "The description of the extension" 9 | }, 10 | "fontFamily": { 11 | "message": "Font Family", 12 | "description": "Label for font family" 13 | }, 14 | "fontWeight": { 15 | "message": "Font Weight", 16 | "description": "Label for font weight" 17 | }, 18 | "fontSize": { 19 | "message": "Font Size", 20 | "description": "Label for font size" 21 | }, 22 | "lineHeight": { 23 | "message": "Line Height", 24 | "description": "Label for line height" 25 | }, 26 | "letterSpacing": { 27 | "message": "Letter Spacing", 28 | "description": "Label for letter spacing" 29 | }, 30 | "textAlign": { 31 | "message": "Text Align", 32 | "description": "Label for text alignment" 33 | }, 34 | "color": { 35 | "message": "Color", 36 | "description": "Label for color" 37 | }, 38 | "colorHex": { 39 | "message": "HEX", 40 | "description": "Label for HEX color format" 41 | }, 42 | "colorLCH": { 43 | "message": "LCH", 44 | "description": "Label for LCH color format" 45 | }, 46 | "colorHCL": { 47 | "message": "HCL", 48 | "description": "Label for HCL color format" 49 | }, 50 | "copied": { 51 | "message": "Copied", 52 | "description": "Text shown after copying" 53 | }, 54 | "noFontFound": { 55 | "message": "No font information available", 56 | "description": "Message when no font information is found" 57 | }, 58 | "clickToCopy": { 59 | "message": "Click to copy", 60 | "description": "Tooltip for copy button" 61 | } 62 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [1.1.9] - 2024-05-25 9 | 10 | ### Added 11 | - Improved error handling and stability 12 | - Better performance on dynamic websites 13 | - Added keyboard shortcut (Shift+Alt+X) for toggling detector 14 | - Enhanced color detection accuracy 15 | 16 | ### Fixed 17 | - Fixed memory leaks in long-running sessions 18 | - Fixed tooltip positioning on scrolling 19 | - Improved detector accuracy for nested elements 20 | 21 | ## [1.0.0] - 2024-05-08 22 | 23 | ### Added 24 | - Basic font detection functionality 25 | - Support font information detection via mouse hover 26 | - Automatic parsing and display of font name, size, weight and other properties 27 | - Color detection functionality 28 | - Support for multiple color formats: HEX, LCH, HCL 29 | - Color preview and copy functionality 30 | - Fixed tooltip functionality 31 | - Create fixed tooltips after selecting text 32 | - Support creating multiple tooltips for comparison 33 | - Copy functionality 34 | - One-click copy of font information and color values 35 | - Visual feedback after successful copying 36 | - Font search functionality 37 | - Click font name to search directly 38 | - Dynamic extension icon update 39 | - Dedicated icon for active state 40 | 41 | ### Fixed 42 | - Optimized text selection area recognition algorithm 43 | - Fixed special character handling issues 44 | - Resolved font detection inaccuracies in multi-level nested elements 45 | 46 | ### Changed 47 | - Improved UI design using blur background effect 48 | - Optimized floating tooltip positioning logic 49 | - Adjusted font information display order and format 50 | 51 | ## [0.2.0] - 2024-04-15 52 | 53 | ### Added 54 | - ESC key support: Press ESC key to quickly hide floating tooltips while preserving fixed tooltips 55 | - Added visual feedback for copy functionality 56 | - Added support for multiple color formats 57 | 58 | ### Fixed 59 | - Resolved issues with incorrect font detection on certain websites 60 | - Fixed inaccurate font weight display 61 | - Optimized resource cleanup mechanism to reduce memory usage 62 | 63 | ## [0.1.0] - 2024-03-20 64 | 65 | ### Added 66 | - First test version released 67 | - Basic font information detection functionality 68 | - Simple floating tooltip UI -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![FontDetector Preview](thumbnail.png) 2 | 3 | # FontDetector 4 | 5 | FontDetector is a powerful browser extension that helps designers, developers, and users quickly identify fonts and color information on web pages. With a simple mouse hover, you can instantly get font names, sizes, colors, and other style attributes of text elements. 6 | 7 | ## Key Features 8 | 9 | - **Real-time Detection**: Hover over any text element to view font information 10 | - **Mini Tooltip**: A lightweight tooltip follows your cursor, indicating when text selection is available 11 | - **Fixed Tooltips**: Create fixed information tooltips by selecting text for comparing different elements 12 | - **Long Press Support**: Press and hold to trigger text selection mode 13 | - **Color Information**: Display text colors in HEX, LCH, and HCL formats 14 | - **One-click Copy**: Easily copy font information and color values 15 | - **Font Search**: Search for detected fonts directly to learn more 16 | - **Keyboard Shortcuts**: Use ESC key to quickly close floating tooltips while preserving fixed ones 17 | 18 | ## Installation 19 | 20 | ### Chrome Web Store Installation 21 | 1. Visit the [Chrome Web Store](https://chromewebstore.google.com/detail/fontdetector/jphgedmdokkhlllaibcbndaccmdcckfe) 22 | 2. Click "Add to Chrome" button 23 | 24 | ### Manual Installation (Development Version) 25 | 1. Download or clone this repository locally 26 | 2. Open Chrome browser and navigate to the extensions management page (`chrome://extensions/`) 27 | 3. Enable "Developer Mode" (toggle button in top right) 28 | 4. Click "Load unpacked extension" button 29 | 5. Select the root directory of this repository 30 | 31 | ## How to Use 32 | 33 | ### Basic Usage 34 | 1. Click the FontDetector icon in the toolbar to activate the extension (icon turns blue to indicate activation) 35 | 2. Hover your mouse over any text element on the webpage to see the mini tooltip 36 | 3. Select text to create a fixed tooltip with detailed font information 37 | 4. Click the extension icon again or press ESC key to deactivate the extension 38 | 39 | ### Creating Fixed Tooltips 40 | 1. With the extension active, select a piece of text or use long press 41 | 2. After selection, a fixed information tooltip will be automatically created 42 | 3. Repeat this operation to create multiple fixed tooltips for comparison 43 | 4. Click the close button in the top-right corner of a tooltip to close it 44 | 45 | ### Using Long Press 46 | 1. Press and hold your mouse button on any text element 47 | 2. After about 300ms, the mini tooltip will hide, indicating long press mode 48 | 3. Release the mouse button to create a fixed tooltip 49 | 4. For quick clicks (less than 300ms), the mini tooltip remains visible 50 | 51 | ### Copying Information 52 | - Click the copy icon in the tooltip to copy the corresponding value to clipboard 53 | - A blue checkmark icon indicates successful copying 54 | 55 | ## Technical Details 56 | 57 | FontDetector is written in pure JavaScript with no external dependencies. It can detect and display the following information: 58 | 59 | - Font Family 60 | - Font Weight 61 | - Font Size 62 | - Letter Spacing 63 | - Line Height 64 | - Text Alignment 65 | - Text Color (multiple formats) 66 | 67 | ## Contributing 68 | 69 | We welcome all forms of contribution! Please check [CONTRIBUTING.md](CONTRIBUTING.md) to learn how to participate. 70 | 71 | ## License 72 | 73 | This project is licensed under the [MIT License](LICENSE). 74 | 75 | ## Download 76 | 77 | [Download FontDetector](https://chromewebstore.google.com/detail/fontdetector/jphgedmdokkhlllaibcbndaccmdcckfe) 78 | -------------------------------------------------------------------------------- /src/modules/i18n.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Internationalization module for the FontDetector extension 3 | */ 4 | 5 | // Default messages in English (fallback) 6 | const messages = { 7 | fontFamily: 'Font Family', 8 | fontWeight: 'Font Weight', 9 | fontSize: 'Font Size', 10 | lineHeight: 'Line Height', 11 | letterSpacing: 'Letter Spacing', 12 | textAlign: 'Text Align', 13 | color: { 14 | title: 'Color', 15 | hex: 'HEX', 16 | lch: 'LCH', 17 | hcl: 'HCL' 18 | }, 19 | copied: 'Copied', 20 | noFontFound: 'No font information available', 21 | clickToCopy: 'Click to copy', 22 | weightMap: { 23 | '100': 'Thin', 24 | '200': 'Extra Light', 25 | '300': 'Light', 26 | '400': 'Regular', 27 | '500': 'Medium', 28 | '600': 'Semi Bold', 29 | '700': 'Bold', 30 | '800': 'Extra Bold', 31 | '900': 'Black' 32 | } 33 | }; 34 | 35 | // Map of dot notation keys to Chrome i18n message keys 36 | const messageKeyMap = { 37 | 'fontFamily': 'fontFamily', 38 | 'fontWeight': 'fontWeight', 39 | 'fontSize': 'fontSize', 40 | 'lineHeight': 'lineHeight', 41 | 'letterSpacing': 'letterSpacing', 42 | 'textAlign': 'textAlign', 43 | 'color.title': 'color', 44 | 'color.hex': 'colorHex', 45 | 'color.lch': 'colorLCH', 46 | 'color.hcl': 'colorHCL', 47 | 'copied': 'copied', 48 | 'noFontFound': 'noFontFound', 49 | 'clickToCopy': 'clickToCopy' 50 | }; 51 | 52 | /** 53 | * Get localized message 54 | * @param {string} key - Message key (dot notation supported for nested properties) 55 | * @returns {string} - Localized message 56 | */ 57 | export function getMessage(key) { 58 | try { 59 | // First try to get message from Chrome i18n API if available 60 | if (chrome && chrome.i18n && chrome.i18n.getMessage) { 61 | // Convert dot notation key to Chrome i18n message key if needed 62 | const chromeKey = messageKeyMap[key] || key; 63 | const chromeMessage = chrome.i18n.getMessage(chromeKey); 64 | if (chromeMessage) { 65 | return chromeMessage; 66 | } 67 | } 68 | } catch (e) { 69 | console.warn('Chrome i18n API not available, falling back to default messages', e); 70 | // Chrome i18n API not available, fallback to default messages 71 | } 72 | 73 | // Fallback to default messages 74 | return getNestedProperty(messages, key) || key; 75 | } 76 | 77 | /** 78 | * Get nested property from object using dot notation 79 | * @param {Object} obj - Object to get property from 80 | * @param {string} path - Property path using dot notation (e.g. 'color.hex') 81 | * @returns {*} - Property value or undefined 82 | */ 83 | function getNestedProperty(obj, path) { 84 | return path.split('.').reduce((prev, curr) => { 85 | return prev ? prev[curr] : undefined; 86 | }, obj); 87 | } 88 | 89 | /** 90 | * Format font weight with human-readable label 91 | * @param {string|number} weight - Font weight value 92 | * @returns {string} - Formatted font weight 93 | */ 94 | export function formatFontWeight(weight) { 95 | // Normalize weight to string for lookup 96 | const weightStr = String(weight); 97 | 98 | // Weight map for different weights 99 | const weightLabels = { 100 | '100': 'Thin', 101 | '200': 'Extra Light', 102 | '300': 'Light', 103 | '400': 'Regular', 104 | '500': 'Medium', 105 | '600': 'Semi Bold', 106 | '700': 'Bold', 107 | '800': 'Extra Bold', 108 | '900': 'Black' 109 | }; 110 | 111 | // Get the label from Chrome i18n if available, otherwise use default 112 | const label = weightLabels[weightStr] || weightStr; 113 | 114 | // Return formatted weight 115 | return `${weightStr} (${label})`; 116 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for considering contributing to the FontDetector project! This document will guide you on how to participate in the development and improvement of this project. 4 | 5 | ## Development Environment Setup 6 | 7 | 1. **Clone the repository** 8 | ```bash 9 | git clone https://github.com/yourusername/FontDetector.git 10 | cd FontDetector 11 | ``` 12 | 13 | 2. **Load the extension in your browser** 14 | - Open Chrome browser and navigate to the extensions management page (`chrome://extensions/`) 15 | - Enable "Developer Mode" (toggle button in top right) 16 | - Click "Load unpacked extension" button 17 | - Select the root directory of the project 18 | 19 | 3. **Test your changes** 20 | - After modifying the code, click the refresh icon in the extensions management page to reload the extension 21 | - Test the functionality on different web pages to ensure it works properly 22 | 23 | ## Commit Guidelines 24 | 25 | 1. **Create a branch** 26 | ```bash 27 | git checkout -b feature/your-feature-name 28 | ``` 29 | 30 | 2. **Commit message format** 31 | Please use clear commit messages in the following format: 32 | ``` 33 | type(module): brief description 34 | 35 | detailed description (if necessary) 36 | ``` 37 | 38 | Types can be: 39 | - `feat`: New feature 40 | - `fix`: Bug fix 41 | - `docs`: Documentation update 42 | - `style`: Code style change (not affecting functionality) 43 | - `refactor`: Code refactoring 44 | - `perf`: Performance optimization 45 | - `test`: Add or modify tests 46 | - `chore`: Build process or auxiliary tool changes 47 | 48 | 3. **Pull Request Process** 49 | - Ensure your code passes all tests 50 | - Submit a Pull Request to the `main` branch 51 | - Provide detailed description of your changes and their purpose in the PR description 52 | - Wait for maintainers to review 53 | 54 | ## Code Style Guide 55 | 56 | 1. **JavaScript** 57 | - Use 2 spaces for indentation 58 | - Use semicolons to end statements 59 | - Use single quotes for string delimiters 60 | - Use camelCase for variable names 61 | - Use descriptive verb prefixes for function names, such as `getElement`, `createTooltip`, etc. 62 | 63 | 2. **Comments** 64 | - Use JSDoc format for function comments 65 | - Add concise comments for complex logic 66 | - Keep comments synchronized with code updates 67 | 68 | ## Feature Requests and Bug Reports 69 | 70 | 1. **Submitting Bug Reports** 71 | - Use GitHub Issues to submit bug reports 72 | - Include environment information such as browser version, operating system, extension version 73 | - Provide detailed steps to reproduce the issue 74 | - If possible, attach screenshots or screen recordings 75 | 76 | 2. **Feature Requests** 77 | - Clearly describe the feature you want 78 | - Explain why this feature would be valuable to the project 79 | - Optionally provide implementation ideas or design sketches 80 | 81 | ## Release Process 82 | 83 | 1. **Version Number Convention** 84 | We use [Semantic Versioning](https://semver.org/): 85 | - Major version: Incompatible API changes 86 | - Minor version: Backwards-compatible functionality additions 87 | - Patch version: Backwards-compatible bug fixes 88 | 89 | 2. **Pre-release Checklist** 90 | - All tests pass 91 | - Update CHANGELOG.md 92 | - Update version number (in manifest.json) 93 | - Ensure documentation is consistent with the latest changes 94 | 95 | ## Code of Conduct 96 | 97 | - Respect all contributors regardless of experience level, gender, race, ethnicity, religion, or nationality 98 | - Use friendly and inclusive language 99 | - Accept constructive criticism 100 | - Focus on project goals and community interests 101 | 102 | ## Contact 103 | 104 | If you have any questions, please contact the project maintainers: 105 | - Ask questions in GitHub Issues 106 | - Send emails to: zanwei.guo@outlook.com 107 | 108 | Thank you for your contributions to the FontDetector project! 109 | -------------------------------------------------------------------------------- /src/modules/colorUtils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Color manipulation utilities for the FontDetector extension 3 | */ 4 | 5 | /** 6 | * Convert hex color to RGB 7 | * @param {string} hex - Hex color string 8 | * @returns {Array} - RGB values as array [r, g, b] 9 | */ 10 | export function hexToRgb(hex) { 11 | hex = hex.replace(/^#/, ''); 12 | let r = parseInt(hex.substring(0, 2), 16); 13 | let g = parseInt(hex.substring(2, 4), 16); 14 | let b = parseInt(hex.substring(4, 6), 16); 15 | return [r, g, b]; 16 | } 17 | 18 | /** 19 | * Convert RGB to hex color 20 | * @param {number} r - Red value (0-255) 21 | * @param {number} g - Green value (0-255) 22 | * @param {number} b - Blue value (0-255) 23 | * @returns {string} - Hex color string 24 | */ 25 | export function rgbToHex(r, g, b) { 26 | return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1); 27 | } 28 | 29 | /** 30 | * Convert RGB to LCH color space 31 | * @param {number} r - Red value (0-255) 32 | * @param {number} g - Green value (0-255) 33 | * @param {number} b - Blue value (0-255) 34 | * @returns {Object} - LCH values {l, c, h} 35 | */ 36 | export function rgbToLCH(r, g, b) { 37 | // Convert to sRGB 38 | r /= 255; 39 | g /= 255; 40 | b /= 255; 41 | 42 | // Convert to XYZ 43 | let x = r * 0.4124 + g * 0.3576 + b * 0.1805; 44 | let y = r * 0.2126 + g * 0.7152 + b * 0.0722; 45 | let z = r * 0.0193 + g * 0.1192 + b * 0.9505; 46 | 47 | // XYZ to Lab 48 | const xRef = 0.95047; 49 | const yRef = 1.0; 50 | const zRef = 1.08883; 51 | 52 | x = x / xRef; 53 | y = y / yRef; 54 | z = z / zRef; 55 | 56 | x = x > 0.008856 ? Math.pow(x, 1/3) : (7.787 * x) + 16/116; 57 | y = y > 0.008856 ? Math.pow(y, 1/3) : (7.787 * y) + 16/116; 58 | z = z > 0.008856 ? Math.pow(z, 1/3) : (7.787 * z) + 16/116; 59 | 60 | const l = (116 * y) - 16; 61 | const a = 500 * (x - y); 62 | const b2 = 200 * (y - z); 63 | 64 | // Lab to LCh 65 | const c = Math.sqrt(a * a + b2 * b2); 66 | let h = Math.atan2(b2, a) * (180 / Math.PI); 67 | if (h < 0) h += 360; 68 | 69 | return { 70 | l: Math.round(l), 71 | c: Math.round(c), 72 | h: Math.round(h) 73 | }; 74 | } 75 | 76 | /** 77 | * Convert RGB to HCL color space (HCL is LCH with reordered components) 78 | * @param {number} r - Red value (0-255) 79 | * @param {number} g - Green value (0-255) 80 | * @param {number} b - Blue value (0-255) 81 | * @returns {Object} - HCL values {h, c, l} 82 | */ 83 | export function rgbToHCL(r, g, b) { 84 | const lch = rgbToLCH(r, g, b); 85 | return { 86 | h: lch.h, 87 | c: lch.c, 88 | l: lch.l 89 | }; 90 | } 91 | 92 | /** 93 | * Get color information from an element 94 | * @param {Element} element - DOM element 95 | * @returns {Object|null} - Color information or null if not available 96 | */ 97 | export function getColorFromElement(element) { 98 | try { 99 | const style = getComputedStyle(element); 100 | const color = style.color; 101 | 102 | // Create a temporary element to parse any color format 103 | const tempEl = document.createElement('div'); 104 | tempEl.style.color = color; 105 | tempEl.style.display = 'none'; 106 | document.body.appendChild(tempEl); 107 | 108 | // Get the computed color value (browser will convert various formats to rgb or rgba) 109 | const computedColor = getComputedStyle(tempEl).color; 110 | document.body.removeChild(tempEl); 111 | 112 | // Parse RGB or RGBA color 113 | const rgbMatch = computedColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/); 114 | if (rgbMatch) { 115 | const r = parseInt(rgbMatch[1]); 116 | const g = parseInt(rgbMatch[2]); 117 | const b = parseInt(rgbMatch[3]); 118 | 119 | // Convert to different formats 120 | const hex = rgbToHex(r, g, b); 121 | const lch = rgbToLCH(r, g, b); 122 | const hcl = rgbToHCL(r, g, b); 123 | 124 | return { 125 | rgb: { r, g, b }, 126 | hex: hex, 127 | lch: lch, 128 | hcl: hcl 129 | }; 130 | } 131 | 132 | return null; 133 | } catch (err) { 134 | console.error('Error getting color from element:', err); 135 | return null; 136 | } 137 | } -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * FontDetector main application 3 | * 4 | * This is the entry point for the FontDetector extension content script. 5 | * It ties together all the modules and handles the extension lifecycle. 6 | */ 7 | 8 | import { initTooltips, removeAllFixedTooltips } from './modules/tooltips.js'; 9 | import { initListeners, removeListeners } from './modules/listeners.js'; 10 | 11 | // Extension state 12 | window.fontDetectorState = { 13 | isActive: false, 14 | isReinitializing: false 15 | }; 16 | 17 | /** 18 | * Initialize the font detector 19 | */ 20 | function initializeDetector() { 21 | try { 22 | if (window.fontDetectorState.isReinitializing) { 23 | return; 24 | } 25 | 26 | console.log('Initializing FontDetector...'); 27 | 28 | // Initialize tooltips 29 | initTooltips(); 30 | 31 | // Initialize listeners 32 | initListeners(window.fontDetectorState); 33 | 34 | // Add error handler 35 | setupErrorHandling(); 36 | 37 | console.log('FontDetector initialized successfully'); 38 | } catch (err) { 39 | console.error('Error initializing FontDetector:', err); 40 | cleanupResources(); 41 | } 42 | } 43 | 44 | /** 45 | * Deinitialize the font detector 46 | * @param {boolean} preserveFixedTooltips - Whether to preserve fixed tooltips 47 | */ 48 | function deinitializeDetector(preserveFixedTooltips = false) { 49 | try { 50 | console.log('Deinitializing FontDetector...'); 51 | 52 | // Clean up listeners 53 | removeListeners(); 54 | 55 | // Remove tooltips if needed 56 | if (!preserveFixedTooltips) { 57 | removeAllFixedTooltips(); 58 | } 59 | 60 | window.fontDetectorState.isActive = false; 61 | console.log('FontDetector deinitialized successfully'); 62 | } catch (err) { 63 | console.error('Error deinitializing FontDetector:', err); 64 | } 65 | } 66 | 67 | /** 68 | * Clean up all resources 69 | */ 70 | function cleanupResources() { 71 | try { 72 | if (!window.fontDetectorState.isReinitializing) { 73 | window.fontDetectorState.isReinitializing = true; 74 | console.log('Cleaning up FontDetector resources...'); 75 | 76 | // Deactivate 77 | window.fontDetectorState.isActive = false; 78 | 79 | // Clean up 80 | deinitializeDetector(false); 81 | 82 | window.fontDetectorState.isReinitializing = false; 83 | } 84 | } catch (err) { 85 | console.error('Error cleaning up resources:', err); 86 | window.fontDetectorState.isReinitializing = false; 87 | } 88 | } 89 | 90 | /** 91 | * Set up error handling 92 | */ 93 | function setupErrorHandling() { 94 | window.addEventListener('error', function(event) { 95 | if (event.error && event.error.message && event.error.message.includes('Extension context invalidated')) { 96 | console.warn('Captured Extension context invalidated error, preparing to clean up resources...'); 97 | cleanupResources(); 98 | } 99 | }); 100 | 101 | // Chrome connection listener 102 | chrome.runtime.onConnect.addListener(function(port) { 103 | port.onDisconnect.addListener(function() { 104 | if (chrome.runtime.lastError) { 105 | console.warn('Port disconnected due to error:', chrome.runtime.lastError); 106 | cleanupResources(); 107 | } 108 | }); 109 | }); 110 | } 111 | 112 | // Listen for messages from the background script 113 | chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { 114 | if (message.action === 'toggleExtension') { 115 | console.log('Toggle extension message received'); 116 | 117 | window.fontDetectorState.isActive = !window.fontDetectorState.isActive; 118 | 119 | if (window.fontDetectorState.isActive) { 120 | console.log('Activating font detector'); 121 | } else { 122 | console.log('Deactivating font detector'); 123 | deinitializeDetector(false); 124 | } 125 | 126 | // Send response 127 | if (sendResponse) { 128 | sendResponse({ success: true }); 129 | } 130 | } 131 | }); 132 | 133 | // Initialize detector 134 | initializeDetector(); 135 | 136 | // Export functions for use in modules 137 | export { 138 | initializeDetector, 139 | deinitializeDetector, 140 | cleanupResources 141 | }; -------------------------------------------------------------------------------- /src/modules/fontDetection.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Font detection module for the FontDetector extension 3 | */ 4 | 5 | import { formatFontWeight } from './i18n.js'; 6 | 7 | /** 8 | * Get font information from an element 9 | * @param {Element} element - The DOM element to get font information from 10 | * @returns {Object|null} - Font information object or null if not available 11 | */ 12 | export function getFontInfo(element) { 13 | if (!element || !element.nodeType || element.nodeType !== Node.ELEMENT_NODE) { 14 | return null; 15 | } 16 | 17 | try { 18 | const style = getComputedStyle(element); 19 | const fontFamily = style.fontFamily.replace(/['"]/g, ''); 20 | const fontSize = style.fontSize; 21 | const fontWeight = formatFontWeight(style.fontWeight); 22 | const lineHeight = style.lineHeight; 23 | const letterSpacing = style.letterSpacing; 24 | const textAlign = style.textAlign; 25 | 26 | return { 27 | fontFamily, 28 | fontSize, 29 | fontWeight, 30 | lineHeight, 31 | letterSpacing, 32 | textAlign 33 | }; 34 | } catch (err) { 35 | console.error('Error getting font info:', err); 36 | return null; 37 | } 38 | } 39 | 40 | /** 41 | * Check if an element contains text or is a text-containing element 42 | * @param {Element} element - The element to check 43 | * @returns {boolean} - True if the element contains text 44 | */ 45 | export function hasTextContent(element) { 46 | // Check if element is empty 47 | if (!element) { 48 | console.debug('Element is empty'); 49 | return false; 50 | } 51 | 52 | // Extended non-text tag list - added more tags that should not display tooltips 53 | const nonTextTags = [ 54 | 'HTML', 'BODY', 'SCRIPT', 'STYLE', 'SVG', 'PATH', 'IMG', 'VIDEO', 'AUDIO', 'CANVAS', 'IFRAME', 55 | 'OBJECT', 'EMBED', 'NAV', 'UL', 'OL', 'HR', 'BR', 'WBR', 'NOSCRIPT', 'INPUT', 'SELECT', 'OPTION', 56 | 'OPTGROUP', 'DATALIST', 'OUTPUT', 'MENU', 'ASIDE', 'FIGURE', 'FIGCAPTION', 'MAP', 'AREA', 57 | 'SOURCE', 'TRACK', 'META', 'LINK', 'BASE', 'PARAM', 'PROGRESS', 'METER', 'TIME', 'HEADER', 58 | 'FOOTER', 'MAIN', 'SECTION', 'ARTICLE', 'DIALOG', 'DETAILS', 'SUMMARY', 'PICTURE', 'TEMPLATE' 59 | ]; 60 | 61 | if (nonTextTags.includes(element.tagName)) { 62 | console.debug('Non-text tag', element.tagName); 63 | return false; 64 | } 65 | 66 | // Get element text content (remove spaces) 67 | const rawText = element.textContent || ''; 68 | const text = rawText.trim(); 69 | 70 | // Check element computed style 71 | const style = getComputedStyle(element); 72 | 73 | // Check if element is hidden 74 | if (style.display === 'none' || style.visibility === 'hidden' || parseFloat(style.opacity) === 0) { 75 | console.debug('Hidden element', element.tagName); 76 | return false; 77 | } 78 | 79 | // Get direct text content (excluding child elements) 80 | let directTextLength = 0; 81 | for (let i = 0; i < element.childNodes.length; i++) { 82 | if (element.childNodes[i].nodeType === Node.TEXT_NODE) { 83 | directTextLength += element.childNodes[i].textContent.trim().length; 84 | } 85 | } 86 | 87 | // Check if it is meaningful text content 88 | // Must contain letters, numbers, or Chinese, and at least 3 characters 89 | const meaningfulTextPattern = /[a-zA-Z0-9\u4e00-\u9fa5]{3,}/; 90 | if (!meaningfulTextPattern.test(text)) { 91 | console.debug('Does not contain meaningful text', `${element.tagName}: ${text}`); 92 | return false; 93 | } 94 | 95 | // Check if it is a clear text element 96 | const textElements = ['P', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'BLOCKQUOTE', 'PRE', 'CODE']; 97 | if (textElements.includes(element.tagName) && directTextLength >= 3) { 98 | console.debug('Clear text element', `${element.tagName}: ${directTextLength} characters`); 99 | return true; 100 | } 101 | 102 | // Check inline text elements 103 | const inlineTextElements = ['SPAN', 'A', 'STRONG', 'EM', 'B', 'I', 'U', 'SUP', 'SUB', 'MARK', 'SMALL', 'DEL', 'INS', 'Q', 'ABBR', 'CITE', 'DFN', 'LABEL']; 104 | if (inlineTextElements.includes(element.tagName) && directTextLength >= 3) { 105 | console.debug('Inline text element', `${element.tagName}: ${directTextLength} characters`); 106 | return true; 107 | } 108 | 109 | // Check table cell elements 110 | if (['TD', 'TH'].includes(element.tagName) && directTextLength >= 3) { 111 | console.debug('Table cell text', `${element.tagName}: ${directTextLength} characters`); 112 | return true; 113 | } 114 | 115 | // Check list elements 116 | if (['LI', 'DT', 'DD'].includes(element.tagName) && directTextLength >= 3) { 117 | console.debug('List element text', `${element.tagName}: ${directTextLength} characters`); 118 | return true; 119 | } 120 | 121 | // Check form elements 122 | if (['BUTTON', 'TEXTAREA'].includes(element.tagName) && directTextLength >= 3) { 123 | console.debug('Form element text', `${element.tagName}: ${directTextLength} characters`); 124 | return true; 125 | } 126 | 127 | // Additional check for DIV elements - stricter requirements 128 | if (element.tagName === 'DIV') { 129 | // Only accept DIVs with a lot of text (at least 20 characters) 130 | if (directTextLength >= 20) { 131 | console.debug('Text-rich DIV', `Direct text length: ${directTextLength} characters`); 132 | return true; 133 | } 134 | 135 | // Check DIV's style to see if it looks like a text container 136 | if (style.fontFamily !== 'inherit' && style.textAlign !== 'start' && directTextLength >= 5) { 137 | console.debug('Style similar to text container DIV', `${element.tagName}: ${directTextLength} characters`); 138 | return true; 139 | } 140 | 141 | console.debug('Regular DIV does not meet text requirements', `Direct text length: ${directTextLength} characters`); 142 | return false; 143 | } 144 | 145 | // By default, if it doesn't meet any of the above conditions, it's not considered a text element 146 | console.debug('Does not meet any text element conditions', element.tagName); 147 | return false; 148 | } -------------------------------------------------------------------------------- /src/modules/listeners.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Event listeners module for the FontDetector extension 3 | */ 4 | 5 | import { showTooltip, hideTooltip, createFixedTooltip, removeAllFixedTooltips } from './tooltips.js'; 6 | import { hasTextContent } from './fontDetection.js'; 7 | 8 | // Track current target element 9 | let currentTarget = null; 10 | let animationFrameId = null; 11 | let selectionTimeout = null; 12 | 13 | /** 14 | * Initialize all event listeners 15 | * @param {Object} state - The application state 16 | */ 17 | export function initListeners(state) { 18 | addMouseListeners(); 19 | addSelectionListener(); 20 | document.addEventListener('keydown', (event) => handleKeyDown(event, state)); 21 | 22 | // Listen for extension toggle message from background script 23 | chrome.runtime.onMessage.addListener((message) => { 24 | if (message.action === 'toggleExtension') { 25 | state.isActive = !state.isActive; 26 | 27 | if (state.isActive) { 28 | // If activated, initialize 29 | console.log('Extension activated'); 30 | } else { 31 | // If deactivated, clean up 32 | console.log('Extension deactivated'); 33 | hideTooltip(); 34 | 35 | // Cancel any pending animation frame 36 | if (animationFrameId) { 37 | cancelAnimationFrame(animationFrameId); 38 | animationFrameId = null; 39 | } 40 | } 41 | } 42 | }); 43 | } 44 | 45 | /** 46 | * Remove all event listeners 47 | */ 48 | export function removeListeners() { 49 | removeMouseListeners(); 50 | removeSelectionListener(); 51 | document.removeEventListener('keydown', handleKeyDown); 52 | 53 | // Cancel any pending animation frame 54 | if (animationFrameId) { 55 | cancelAnimationFrame(animationFrameId); 56 | animationFrameId = null; 57 | } 58 | } 59 | 60 | /** 61 | * Add mouse event listeners 62 | */ 63 | function addMouseListeners() { 64 | document.addEventListener('mousemove', handleMouseMove); 65 | document.addEventListener('mouseover', handleMouseOver); 66 | document.addEventListener('mouseout', handleMouseOut); 67 | } 68 | 69 | /** 70 | * Remove mouse event listeners 71 | */ 72 | function removeMouseListeners() { 73 | document.removeEventListener('mousemove', handleMouseMove); 74 | document.removeEventListener('mouseover', handleMouseOver); 75 | document.removeEventListener('mouseout', handleMouseOut); 76 | } 77 | 78 | /** 79 | * Add text selection listener 80 | */ 81 | function addSelectionListener() { 82 | document.addEventListener('mouseup', handleTextSelection); 83 | } 84 | 85 | /** 86 | * Remove text selection listener 87 | */ 88 | function removeSelectionListener() { 89 | document.removeEventListener('mouseup', handleTextSelection); 90 | } 91 | 92 | /** 93 | * Handle mouse move event 94 | * @param {Event} event - The mouse event 95 | */ 96 | function handleMouseMove(event) { 97 | // Use request animation frame to throttle tooltip updates 98 | if (animationFrameId) { 99 | cancelAnimationFrame(animationFrameId); 100 | } 101 | 102 | animationFrameId = requestAnimationFrame(() => { 103 | if (!currentTarget) return; 104 | 105 | const x = event.clientX; 106 | const y = event.clientY; 107 | 108 | showTooltip(x, y, currentTarget); 109 | }); 110 | } 111 | 112 | /** 113 | * Handle mouseover event 114 | * @param {Event} event - The mouse event 115 | */ 116 | function handleMouseOver(event) { 117 | if (!window.fontDetectorState || !window.fontDetectorState.isActive) return; 118 | 119 | let targetElement = event.target; 120 | 121 | // If it's a text node, use its parent element 122 | if (targetElement.nodeType === Node.TEXT_NODE) { 123 | targetElement = targetElement.parentElement; 124 | } 125 | 126 | // If the cursor is at the edge of the window or in a blank area, don't display the tooltip 127 | const mouseX = event.clientX; 128 | const mouseY = event.clientY; 129 | const windowWidth = window.innerWidth; 130 | const windowHeight = window.innerHeight; 131 | 132 | // Check if the mouse is at the edge of the window 133 | const edgeThreshold = 15; // Edge threshold (pixels) 134 | if (mouseX < edgeThreshold || mouseX > windowWidth - edgeThreshold || 135 | mouseY < edgeThreshold || mouseY > windowHeight - edgeThreshold) { 136 | if (currentTarget) { 137 | console.debug('Mouse at window edge', `${mouseX},${mouseY}`); 138 | currentTarget = null; 139 | hideTooltip(); 140 | } 141 | return; 142 | } 143 | 144 | // Check if the target element is the root or body element of the document (possibly a blank area) 145 | if (targetElement === document.documentElement || targetElement === document.body) { 146 | if (currentTarget) { 147 | console.debug('Mouse over root element', targetElement.tagName); 148 | currentTarget = null; 149 | hideTooltip(); 150 | } 151 | return; 152 | } 153 | 154 | // Ignore tooltips and other extension elements 155 | if (targetElement.classList.contains('font-detector')) { 156 | return; 157 | } 158 | 159 | // Check if it's a valid text element 160 | if (targetElement && hasTextContent(targetElement)) { 161 | currentTarget = targetElement; 162 | 163 | // Show tooltip at current mouse position 164 | showTooltip(mouseX, mouseY, targetElement); 165 | } else if (currentTarget) { 166 | // Hide tooltip when moving over non-text element 167 | currentTarget = null; 168 | hideTooltip(); 169 | } 170 | } 171 | 172 | /** 173 | * Handle mouseout event 174 | * @param {Event} event - The mouse event 175 | */ 176 | function handleMouseOut(event) { 177 | // Check if mouseout is from the current target or its ancestors 178 | let relatedTarget = event.relatedTarget; 179 | let isChildOfCurrentTarget = false; 180 | 181 | // Check if the related target is child of current target 182 | if (currentTarget && relatedTarget) { 183 | isChildOfCurrentTarget = currentTarget.contains(relatedTarget); 184 | } 185 | 186 | // If moving from current target to non-child element, hide tooltip 187 | if (currentTarget && !isChildOfCurrentTarget) { 188 | currentTarget = null; 189 | hideTooltip(); 190 | } 191 | } 192 | 193 | /** 194 | * Handle text selection event 195 | * @param {Event} event - The mouse event 196 | */ 197 | function handleTextSelection(event) { 198 | if (!window.fontDetectorState || !window.fontDetectorState.isActive) return; 199 | 200 | try { 201 | // Debounce: cancel previous timeout 202 | if (selectionTimeout) { 203 | clearTimeout(selectionTimeout); 204 | } 205 | 206 | // Set new timeout, ensure tooltip created after selection completes 207 | selectionTimeout = setTimeout(() => { 208 | try { 209 | const selection = window.getSelection(); 210 | if (selection && selection.toString().trim().length > 0) { 211 | // Get the element to extract font info from 212 | let element = event.target; 213 | if (element.nodeType === Node.TEXT_NODE) { 214 | element = element.parentElement; 215 | } 216 | 217 | // Only proceed if we have a valid element 218 | if (element && element.nodeType === Node.ELEMENT_NODE) { 219 | createFixedTooltip(event, element); 220 | } 221 | } 222 | } catch (err) { 223 | console.error('Error handling text selection:', err); 224 | } 225 | }, 100); // 100ms delay, reduce multiple triggers 226 | } catch (err) { 227 | console.error('Error handling text selection:', err); 228 | } 229 | } 230 | 231 | /** 232 | * Handle keyboard events 233 | * @param {Event} event - The keyboard event 234 | * @param {Object} state - The application state 235 | */ 236 | function handleKeyDown(event, state) { 237 | if (event.key === 'Escape' && state.isActive) { 238 | // Hide floating tooltip 239 | hideTooltip(); 240 | 241 | // Disable extension functionality but preserve fixed tooltips 242 | state.isActive = false; 243 | 244 | // Notify background extension state changed 245 | chrome.runtime.sendMessage({ action: 'toggleExtension' }); 246 | 247 | // Send message to background script to restore icon to default state 248 | chrome.runtime.sendMessage({ action: 'updateIcon', iconState: 'inactive' }); 249 | } 250 | } -------------------------------------------------------------------------------- /src/modules/tooltips.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Tooltip management module for the FontDetector extension 3 | */ 4 | 5 | import { getColorFromElement } from './colorUtils.js'; 6 | import { getMessage } from './i18n.js'; 7 | import { getFontInfo } from './fontDetection.js'; 8 | 9 | // Tooltip elements 10 | let tooltip = null; 11 | let fixedTooltips = []; 12 | let fixedTooltipPositions = new Set(); 13 | let lastTooltipContent = ''; 14 | 15 | /** 16 | * Initialize tooltip system 17 | */ 18 | export function initTooltips() { 19 | injectCSS(); 20 | tooltip = createTooltip(); 21 | document.body.appendChild(tooltip); 22 | } 23 | 24 | /** 25 | * Display the tooltip at the specified position with font information 26 | * @param {number} x - X coordinate 27 | * @param {number} y - Y coordinate 28 | * @param {Element} element - Target element to extract font info from 29 | */ 30 | export function showTooltip(x, y, element) { 31 | if (!tooltip) return; 32 | 33 | // Update position 34 | updateTooltipPosition(tooltip, x, y); 35 | 36 | // Update content 37 | updateTooltipContent(tooltip, element); 38 | 39 | // Show tooltip 40 | tooltip.style.display = 'block'; 41 | 42 | // Trigger fade-in animation 43 | requestAnimationFrame(() => { 44 | tooltip.style.opacity = '1'; 45 | }); 46 | } 47 | 48 | /** 49 | * Hide the tooltip 50 | */ 51 | export function hideTooltip() { 52 | if (!tooltip) return; 53 | 54 | tooltip.style.transition = ''; // Reset transition 55 | tooltip.style.opacity = '0'; 56 | tooltip.style.display = 'none'; 57 | } 58 | 59 | /** 60 | * Create a fixed tooltip at the specified position 61 | * @param {Event} event - Mouse event that triggered this 62 | * @param {Element} element - Element to extract font info from 63 | */ 64 | export function createFixedTooltip(event, element) { 65 | const x = event.clientX; 66 | const y = event.clientY; 67 | 68 | // Create a position key to avoid duplicates in same area 69 | const positionKey = `${Math.round(x/10)},${Math.round(y/10)}`; 70 | 71 | // Check if we already have a tooltip at this position 72 | if (fixedTooltipPositions.has(positionKey)) { 73 | return; 74 | } 75 | 76 | // Create tooltip element 77 | const fixedTooltip = document.createElement('div'); 78 | fixedTooltip.classList.add('font-detector', 'fixed-tooltip'); 79 | 80 | // Add close button 81 | const closeButton = document.createElement('div'); 82 | closeButton.classList.add('close-button'); 83 | closeButton.innerHTML = ``; 84 | closeButton.addEventListener('click', (e) => { 85 | e.stopPropagation(); 86 | removeFixedTooltip(fixedTooltip); 87 | }); 88 | fixedTooltip.appendChild(closeButton); 89 | 90 | // Add to DOM and position 91 | document.body.appendChild(fixedTooltip); 92 | 93 | // Position tooltip 94 | fixedTooltip.style.left = `${x}px`; 95 | fixedTooltip.style.top = `${y}px`; 96 | 97 | // Update tooltip content 98 | updateTooltipContent(fixedTooltip, element); 99 | 100 | // Add event listeners for copy functionality 101 | const copyIcons = fixedTooltip.querySelectorAll('.copy-icon'); 102 | copyIcons.forEach(icon => { 103 | icon.addEventListener('click', handleCopyClick); 104 | }); 105 | 106 | // Add to tracking collections 107 | fixedTooltips.push(fixedTooltip); 108 | fixedTooltipPositions.add(positionKey); 109 | 110 | // Return the tooltip element 111 | return fixedTooltip; 112 | } 113 | 114 | /** 115 | * Remove a fixed tooltip 116 | * @param {Element} tooltip - The tooltip element to remove 117 | */ 118 | export function removeFixedTooltip(tooltip) { 119 | if (!tooltip) return; 120 | 121 | // Remove from DOM 122 | tooltip.remove(); 123 | 124 | // Remove from tracking arrays 125 | const index = fixedTooltips.indexOf(tooltip); 126 | if (index !== -1) { 127 | fixedTooltips.splice(index, 1); 128 | } 129 | 130 | // Note: we can't easily remove the position from fixedTooltipPositions 131 | // since we don't store the reverse lookup, but that's fine as it's just for duplication prevention 132 | } 133 | 134 | /** 135 | * Remove all fixed tooltips 136 | */ 137 | export function removeAllFixedTooltips() { 138 | try { 139 | const tooltipsToRemove = [...fixedTooltips]; // Create a copy to iterate 140 | fixedTooltipPositions = new Set(); // Reset positions tracking 141 | fixedTooltips = []; 142 | 143 | // Remove each tooltip 144 | tooltipsToRemove.forEach(t => { 145 | try { 146 | if (t && t.parentNode) { 147 | t.remove(); 148 | } 149 | } catch (err) { 150 | console.error('Error removing fixed tooltip:', err); 151 | } 152 | }); 153 | 154 | // Ensure all elements with .fixed-tooltip class are removed, in case of any missed 155 | try { 156 | const remainingTooltips = document.querySelectorAll('.fixed-tooltip'); 157 | remainingTooltips.forEach(t => { 158 | try { 159 | t.remove(); 160 | } catch (err) {} 161 | }); 162 | } catch (err) {} 163 | } catch (err) { 164 | console.error('Error removing all fixed tooltips:', err); 165 | } 166 | } 167 | 168 | /** 169 | * Update tooltip position 170 | * @param {Element} tooltip - The tooltip element 171 | * @param {number} x - X coordinate 172 | * @param {number} y - Y coordinate 173 | */ 174 | function updateTooltipPosition(tooltip, x, y) { 175 | const padding = 15; // Padding from cursor 176 | const tooltipWidth = 250; // Width from CSS 177 | const tooltipHeight = tooltip.getBoundingClientRect().height || 200; 178 | 179 | // Check if tooltip would go outside viewport 180 | const viewportWidth = window.innerWidth; 181 | const viewportHeight = window.innerHeight; 182 | 183 | // Adjust X position if needed 184 | let tooltipX = x + padding; 185 | if (tooltipX + tooltipWidth > viewportWidth) { 186 | tooltipX = x - tooltipWidth - padding; 187 | } 188 | 189 | // Adjust Y position if needed 190 | let tooltipY = y + padding; 191 | if (tooltipY + tooltipHeight > viewportHeight) { 192 | tooltipY = y - tooltipHeight - padding; 193 | } 194 | 195 | // Make sure we don't go negative 196 | tooltipX = Math.max(0, tooltipX); 197 | tooltipY = Math.max(0, tooltipY); 198 | 199 | // Update position 200 | tooltip.style.left = `${tooltipX}px`; 201 | tooltip.style.top = `${tooltipY}px`; 202 | } 203 | 204 | /** 205 | * Update tooltip content with font information 206 | * @param {Element} tooltip - The tooltip element 207 | * @param {Element} element - The element to extract font info from 208 | */ 209 | function updateTooltipContent(tooltip, element) { 210 | const fontInfo = getFontInfo(element); 211 | const colorInfo = getColorFromElement(element); 212 | 213 | // Caching: Only update if content would change 214 | const now = Date.now(); 215 | const contentKey = JSON.stringify({fontInfo, colorInfo}); 216 | 217 | if (contentKey !== lastTooltipContent || !tooltip.lastContentUpdate || (now - tooltip.lastContentUpdate > 1000)) { 218 | lastTooltipContent = contentKey; 219 | 220 | let html = ''; 221 | 222 | if (fontInfo) { 223 | if (fontInfo.fontFamily) { 224 | html += createInfoSection(getMessage('fontFamily'), fontInfo.fontFamily, true); 225 | } 226 | 227 | if (fontInfo.fontWeight) { 228 | html += createInfoSection(getMessage('fontWeight'), fontInfo.fontWeight); 229 | } 230 | 231 | if (fontInfo.fontSize) { 232 | html += createInfoSection(getMessage('fontSize'), fontInfo.fontSize); 233 | } 234 | 235 | if (fontInfo.lineHeight) { 236 | html += createInfoSection(getMessage('lineHeight'), fontInfo.lineHeight); 237 | } 238 | 239 | if (fontInfo.letterSpacing) { 240 | html += createInfoSection(getMessage('letterSpacing'), fontInfo.letterSpacing); 241 | } 242 | 243 | if (fontInfo.textAlign) { 244 | html += createInfoSection(getMessage('textAlign'), fontInfo.textAlign); 245 | } 246 | } else { 247 | html += `
${getMessage('noFontFound')}
`; 248 | } 249 | 250 | // Add color information 251 | if (colorInfo) { 252 | const {hex, lch, hcl} = colorInfo; 253 | 254 | // HEX Color 255 | html += createColorInfoSection(getMessage('color.hex'), hex, hex); 256 | 257 | // LCH Color 258 | const lchValue = `L: ${lch.l}, C: ${lch.c}, H: ${lch.h}`; 259 | html += createColorInfoSection(getMessage('color.lch'), lchValue, hex); 260 | 261 | // HCL Color 262 | const hclValue = `H: ${hcl.h}, C: ${hcl.c}, L: ${hcl.l}`; 263 | html += createColorInfoSection(getMessage('color.hcl'), hclValue, hex); 264 | } 265 | 266 | tooltip.innerHTML = html; 267 | 268 | // Add copy icon click event 269 | const copyIcons = tooltip.querySelectorAll('.copy-icon'); 270 | copyIcons.forEach(icon => { 271 | icon.addEventListener('click', handleCopyClick); 272 | }); 273 | 274 | tooltip.lastContentUpdate = now; 275 | } 276 | } 277 | 278 | /** 279 | * Create an info section HTML for the tooltip 280 | * @param {string} label - Section label 281 | * @param {string} value - Section value 282 | * @param {boolean} isLink - Whether the value should be a link 283 | * @returns {string} - HTML for the section 284 | */ 285 | function createInfoSection(label, value, isLink = false) { 286 | const valueHtml = isLink 287 | ? `${value}` 288 | : value; 289 | 290 | return ` 291 |
292 | ${label} 293 |
294 | ${valueHtml} 295 | ${createCopyButton(value)} 296 |
297 |
298 | `; 299 | } 300 | 301 | /** 302 | * Create a color info section HTML for the tooltip 303 | * @param {string} label - Section label 304 | * @param {string} value - Section value 305 | * @param {string} color - Color value for preview 306 | * @returns {string} - HTML for the section 307 | */ 308 | function createColorInfoSection(label, value, color) { 309 | return ` 310 |
311 | ${label} 312 |
313 | 314 | 315 | ${value} 316 | 317 | ${createCopyButton(value)} 318 |
319 |
320 | `; 321 | } 322 | 323 | /** 324 | * Create copy button HTML 325 | * @param {string} value - Value to copy 326 | * @returns {string} - HTML for the copy button 327 | */ 328 | function createCopyButton(value) { 329 | return ` 330 |
331 | 332 | 333 | 334 | 335 |
336 | `; 337 | } 338 | 339 | /** 340 | * Handle click on copy icon 341 | * @param {Event} event - Click event 342 | */ 343 | function handleCopyClick(event) { 344 | event.stopPropagation(); 345 | const copyText = event.currentTarget.getAttribute('data-copy'); 346 | 347 | if (copyText) { 348 | // Copy to clipboard 349 | navigator.clipboard.writeText(copyText).then(() => { 350 | // Change icon to checkmark 351 | event.currentTarget.innerHTML = ` 352 | 353 | 354 | 355 | `; 356 | 357 | // Change back after 2 seconds 358 | setTimeout(() => { 359 | event.currentTarget.innerHTML = ` 360 | 361 | 362 | 363 | 364 | `; 365 | }, 2000); 366 | }).catch(err => { 367 | console.error('Failed to copy:', err); 368 | }); 369 | } 370 | } 371 | 372 | /** 373 | * Create the tooltip element 374 | * @returns {Element} - Tooltip DOM element 375 | */ 376 | function createTooltip() { 377 | // If existing tooltip, remove first 378 | const existingTooltip = document.getElementById('fontInfoTooltip'); 379 | if (existingTooltip) { 380 | existingTooltip.remove(); 381 | } 382 | 383 | const tooltip = document.createElement('div'); 384 | tooltip.classList.add('font-detector'); 385 | tooltip.setAttribute('id', 'fontInfoTooltip'); 386 | tooltip.style.position = 'fixed'; // Use fixed positioning 387 | tooltip.style.display = 'none'; 388 | tooltip.style.opacity = '0'; 389 | tooltip.style.zIndex = '2147483647'; // Max z-index 390 | tooltip.style.transition = 'opacity 0.15s ease'; // Fade animation 391 | 392 | return tooltip; 393 | } 394 | 395 | /** 396 | * Inject CSS styles for the font detector 397 | */ 398 | function injectCSS() { 399 | const fontImport = "@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;700&display=swap');"; 400 | 401 | const css = ` 402 | .font-detector { 403 | color: #A8A8A8; 404 | z-index: 2147483647 !important; 405 | } 406 | 407 | .font-detector span { 408 | color: #fff; 409 | } 410 | 411 | #fontInfoTooltip, .fixed-tooltip { 412 | backdrop-filter: blur(50px); 413 | border: 1px solid #2F2F2F; 414 | background-color: rgba(30, 30, 30, 0.85); 415 | font-family: 'Poppins', Arial, sans-serif; 416 | padding: 16px 16px; 417 | border-radius: 16px; 418 | width: 250px; 419 | word-wrap: break-word; 420 | position: relative; 421 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); 422 | transition: opacity 0.15s ease; 423 | opacity: 1; 424 | } 425 | 426 | #fontInfoTooltip h1, .fixed-tooltip h1 { 427 | display: none; /* Remove Font Information */ 428 | } 429 | 430 | #fontInfoTooltip div, .fixed-tooltip div { 431 | display: flex; 432 | flex-direction: column; /* Vertical arrangement of title and content */ 433 | color: #A8A8A8; 434 | font-size: 13px; /* Title font size */ 435 | margin-bottom: 6px; 436 | gap: 2px; 437 | } 438 | 439 | #fontInfoTooltip div span, .fixed-tooltip div span { 440 | color: #FFFFFF; 441 | font-size: 14px; /* Content font size */ 442 | margin-left: 0px; /* Remove spacing between title and content */ 443 | font-weight: 500; /* Medium font weight for content */ 444 | } 445 | 446 | #fontInfoTooltip a, .fixed-tooltip a { 447 | text-decoration: none; 448 | color: inherit; 449 | } 450 | 451 | .color-preview { 452 | width: 12px; 453 | height: 12px; 454 | border-radius: 50%; 455 | display: inline-block; 456 | margin-right: 8px; 457 | border: 1px solid rgba(255, 255, 255, 0.2); 458 | vertical-align: middle; 459 | } 460 | 461 | .color-value-container { 462 | display: flex; 463 | flex-direction: row; 464 | align-items: center; 465 | justify-content: flex-start; 466 | } 467 | 468 | .close-button { 469 | position: absolute; 470 | top: 10px; 471 | right: 10px; 472 | width: 16px; 473 | height: 16px; 474 | border-radius: 50%; 475 | background-color: rgba(60, 60, 60, 0.7); 476 | display: flex; 477 | align-items: center; 478 | justify-content: center; 479 | cursor: pointer; 480 | transition: background-color 0.2s; 481 | } 482 | 483 | .close-button:hover { 484 | background-color: rgba(80, 80, 80, 0.9); 485 | } 486 | 487 | .close-button svg { 488 | width: 16px; 489 | height: 16px; 490 | } 491 | 492 | .fixed-tooltip { 493 | position: absolute; 494 | z-index: 2147483647 !important; 495 | } 496 | 497 | /* CSS for copy button and checkmark */ 498 | .copy-icon { 499 | width: 24px; 500 | height: 24px; 501 | margin-left: 8px; 502 | display: inline-flex; 503 | align-items: center; 504 | justify-content: center; 505 | cursor: pointer; 506 | position: relative; 507 | background-color: transparent; 508 | border-radius: 4px; 509 | transition: background-color 0.2s; 510 | } 511 | 512 | .copy-icon:hover { 513 | background-color: rgba(255, 255, 255, 0.1); 514 | } 515 | 516 | .copy-icon svg { 517 | width: 14px; 518 | height: 14px; 519 | display: block; /* Ensure SVG has no extra space */ 520 | } 521 | 522 | .value-with-copy { 523 | display: flex; 524 | align-items: center; 525 | justify-content: space-between; 526 | } 527 | 528 | #fontInfoTooltip { 529 | pointer-events: none; 530 | } 531 | 532 | #fontInfoTooltip .copy-icon { 533 | pointer-events: auto; 534 | } 535 | `; 536 | 537 | const style = document.createElement('style'); 538 | style.textContent = fontImport + css; 539 | document.head.appendChild(style); 540 | } -------------------------------------------------------------------------------- /background.js: -------------------------------------------------------------------------------- 1 | const TOGGLE_ACTION = 'toggleExtension'; 2 | const TOGGLE_COMMAND = 'toggle_font_detector'; 3 | let extensionContextValid = true; 4 | 5 | /** 6 | * Error handling for extension context invalidation 7 | */ 8 | function setupErrorHandling() { 9 | try { 10 | // Listen for runtime errors 11 | chrome.runtime.onError.addListener((error) => { 12 | console.error('Runtime error:', error); 13 | if (error && error.message && error.message.includes('Extension context invalidated')) { 14 | extensionContextValid = false; 15 | console.warn('Extension context invalidated, preparing for recovery...'); 16 | } 17 | }); 18 | 19 | // Periodic check for extension context validity 20 | function checkExtensionContext() { 21 | try { 22 | // This will throw if extension context is invalidated 23 | const extensionId = chrome.runtime.id; 24 | setTimeout(checkExtensionContext, 10000); // Check every 10 seconds 25 | } catch (e) { 26 | console.warn('Extension context check failed:', e); 27 | extensionContextValid = false; 28 | } 29 | } 30 | 31 | // Start periodic checking 32 | setTimeout(checkExtensionContext, 10000); 33 | } catch (e) { 34 | console.warn('Error setting up extension context monitoring:', e); 35 | } 36 | } 37 | 38 | // Initialize error handling 39 | setupErrorHandling(); 40 | 41 | /** 42 | * Listens for clicks on the extension icon 43 | * Toggles the extension on compatible pages 44 | */ 45 | chrome.action.onClicked.addListener((tab) => { 46 | if (!extensionContextValid) { 47 | console.warn('Extension context is invalid, cannot process click'); 48 | return; 49 | } 50 | 51 | if (tab.url.startsWith('http://') || tab.url.startsWith('https://')) { 52 | toggleExtension(tab); 53 | } else { 54 | console.error('Extension cannot run on this page (non-HTTP/HTTPS URL)'); 55 | } 56 | }); 57 | 58 | /** 59 | * Safely execute a function with error handling 60 | * @param {Function} fn - The function to execute 61 | * @param {Array} args - Arguments to pass to the function 62 | * @returns {Promise} - The result of the function 63 | */ 64 | async function safeExecute(fn, ...args) { 65 | try { 66 | return await fn(...args); 67 | } catch (error) { 68 | if (error && error.message && error.message.includes('Extension context invalidated')) { 69 | extensionContextValid = false; 70 | console.warn('Extension context invalidated during execution'); 71 | } 72 | 73 | // Add specific handling for 'No tab with id' errors 74 | if (error && error.message && error.message.includes('No tab with id')) { 75 | console.warn(`Tab operation failed: ${error.message} - Tab may have been closed or navigated away`); 76 | return null; // Return null instead of throwing for this specific error 77 | } 78 | 79 | console.error('Error executing ' + fn.name + ':', error); 80 | throw error; 81 | } 82 | } 83 | 84 | /** 85 | * Toggles the extension state on the specified tab 86 | * @param {Object} tab - The tab where the extension should be toggled 87 | */ 88 | async function toggleExtension(tab) { 89 | if (!tab || !tab.id) { 90 | console.error('Invalid tab: Cannot toggle extension on undefined tab'); 91 | return; 92 | } 93 | 94 | // Check if tab URL is supported 95 | if (tab.url) { 96 | const url = tab.url.toLowerCase(); 97 | if (url.startsWith('chrome:') || 98 | url.startsWith('chrome-extension:') || 99 | url.startsWith('about:') || 100 | url.startsWith('file:') || 101 | url.startsWith('view-source:') || 102 | url.startsWith('devtools:') || 103 | !url.startsWith('http')) { 104 | console.log(`Cannot toggle extension on unsupported URL: ${url}`); 105 | alert('FontDetector does not support usage on this page.'); 106 | return; 107 | } 108 | } 109 | 110 | try { 111 | // First check tab status 112 | let canExecute = true; 113 | try { 114 | await new Promise((resolve, reject) => { 115 | chrome.tabs.get(tab.id, function(currentTab) { 116 | if (chrome.runtime.lastError) { 117 | reject(new Error(chrome.runtime.lastError.message)); 118 | return; 119 | } 120 | 121 | if (currentTab.status === 'error' || !currentTab.url.startsWith('http')) { 122 | canExecute = false; 123 | reject(new Error('Tab is showing error page or is not an http page')); 124 | return; 125 | } 126 | 127 | resolve(); 128 | }); 129 | }); 130 | } catch (err) { 131 | console.warn('Cannot toggle extension:', err.message); 132 | alert('FontDetector cannot run on this page (error or restricted page).'); 133 | return; 134 | } 135 | 136 | if (!canExecute) return; 137 | 138 | // Check current extension status to determine activation or deactivation 139 | let currentStatus = false; 140 | 141 | try { 142 | currentStatus = await new Promise((resolve) => { 143 | chrome.tabs.sendMessage(tab.id, { action: 'checkExtensionStatus' }, (response) => { 144 | if (chrome.runtime.lastError) { 145 | console.warn('Error checking extension status:', chrome.runtime.lastError.message); 146 | resolve(false); // If error occurs, assume inactive state 147 | } else if (response && response.isActive !== undefined) { 148 | resolve(response.isActive); 149 | } else { 150 | resolve(false); 151 | } 152 | }); 153 | }); 154 | } catch (err) { 155 | console.warn('Error determining current extension status:', err); 156 | currentStatus = false; // If error occurs, assume inactive state 157 | } 158 | 159 | // Determine post-operation status: current false -> true (activate); current true -> false (deactivate) 160 | const willBeActive = !currentStatus; 161 | console.log(`Extension will be ${willBeActive ? 'activated' : 'deactivated'}`); 162 | 163 | // Update icon early to improve user experience responsiveness 164 | chrome.action.setIcon({ 165 | path: { 166 | "128": willBeActive ? "icon-open.png" : "icon.png" 167 | } 168 | }); 169 | 170 | // Inject content script 171 | try { 172 | await safeExecute(async () => { 173 | await chrome.scripting.executeScript({ 174 | target: { tabId: tab.id }, 175 | files: ['contentScript.js'] 176 | }); 177 | }); 178 | } catch (err) { 179 | // Handle specific errors 180 | if (err.message && ( 181 | err.message.includes('showing error page') || 182 | err.message.includes('cannot access a chrome') || 183 | err.message.includes('cannot be scripted due to'))) { 184 | console.log('Cannot inject script on error or restricted page:', err.message); 185 | alert('FontDetector cannot run on this page (error or restricted page).'); 186 | return; 187 | } 188 | // For other errors, continue to throw 189 | throw err; 190 | } 191 | 192 | // Add delay to ensure script is properly loaded 193 | await new Promise(resolve => setTimeout(resolve, 100)); 194 | 195 | // Send message to content script 196 | await safeExecute(async () => { 197 | chrome.tabs.sendMessage(tab.id, { action: TOGGLE_ACTION }, (response) => { 198 | if (chrome.runtime.lastError) { 199 | console.error('Error sending message:', chrome.runtime.lastError.message); 200 | 201 | // Check if this is a context invalidated error 202 | if (chrome.runtime.lastError.message.includes('Extension context invalidated')) { 203 | extensionContextValid = false; 204 | } 205 | 206 | // Restore icon state due to operation failure 207 | chrome.action.setIcon({ 208 | path: { 209 | "128": currentStatus ? "icon-open.png" : "icon.png" 210 | } 211 | }); 212 | } else { 213 | console.log('Extension toggled successfully', response); 214 | } 215 | }); 216 | }); 217 | 218 | // Subsequent verification of extension status and final icon confirmation 219 | setTimeout(() => { 220 | safeExecute(async () => { 221 | chrome.tabs.sendMessage(tab.id, { action: 'checkExtensionStatus' }, (response) => { 222 | if (chrome.runtime.lastError) { 223 | console.warn('Error verifying extension status:', chrome.runtime.lastError.message); 224 | } else if (response && response.isActive !== undefined) { 225 | // Set icon based on actual status 226 | const isActive = response.isActive; 227 | chrome.action.setIcon({ 228 | path: { 229 | "128": isActive ? "icon-open.png" : "icon.png" 230 | } 231 | }); 232 | console.log(`Final icon state set to ${isActive ? 'active' : 'inactive'}`); 233 | } 234 | }); 235 | }); 236 | }, 300); // Allow enough time for contentScript to complete state transition 237 | } catch (error) { 238 | console.error('Exception while executing script or sending message:', error?.message || error); 239 | } 240 | } 241 | 242 | /** 243 | * Checks if content script is loaded in the specified tab 244 | * @param {number} tabId - Tab ID to check 245 | * @returns {Promise} - True if content script is loaded 246 | */ 247 | async function checkContentScriptLoaded(tabId) { 248 | // First check tab status 249 | try { 250 | const tab = await new Promise((resolve, reject) => { 251 | chrome.tabs.get(tabId, (tab) => { 252 | if (chrome.runtime.lastError) { 253 | reject(new Error(chrome.runtime.lastError.message)); 254 | return; 255 | } 256 | resolve(tab); 257 | }); 258 | }); 259 | 260 | // Check if URL is supported 261 | const url = tab.url?.toLowerCase() || ''; 262 | if (url.startsWith('chrome:') || 263 | url.startsWith('chrome-extension:') || 264 | url.startsWith('about:') || 265 | url.startsWith('file:') || 266 | url.startsWith('view-source:') || 267 | url.startsWith('devtools:') || 268 | !url.startsWith('http') || 269 | tab.status === 'error') { 270 | console.log(`Skipping content script check for unsupported URL: ${url}`); 271 | return false; 272 | } 273 | } catch (err) { 274 | console.warn('Error checking tab before content script check:', err.message); 275 | return false; 276 | } 277 | 278 | try { 279 | const response = await new Promise((resolve) => { 280 | chrome.tabs.sendMessage(tabId, { action: 'checkContentScriptLoaded' }, (response) => { 281 | if (chrome.runtime.lastError) { 282 | const error = chrome.runtime.lastError.message; 283 | console.log('Content script check error:', error); 284 | 285 | // Check specific error types 286 | if (error.includes('showing error page') || 287 | error.includes('cannot access a chrome') || 288 | error.includes('cannot be scripted due to')) { 289 | console.log('Tab is showing error page or is restricted'); 290 | } 291 | 292 | resolve({ loaded: false }); 293 | } else { 294 | resolve(response || { loaded: false }); 295 | } 296 | }); 297 | }); 298 | return response.loaded === true; 299 | } catch (error) { 300 | console.error('Error checking content script loading status:', error); 301 | return false; 302 | } 303 | } 304 | 305 | /** 306 | * Listens for keyboard shortcuts 307 | * Activates the extension when the toggle command is triggered 308 | */ 309 | chrome.commands.onCommand.addListener((command) => { 310 | if (!extensionContextValid) { 311 | console.warn('Extension context is invalid, cannot process command'); 312 | return; 313 | } 314 | 315 | if (command === TOGGLE_COMMAND) { 316 | chrome.tabs.query({ active: true, currentWindow: true }, ([tab]) => { 317 | if (tab) toggleExtension(tab); 318 | }); 319 | } 320 | }); 321 | 322 | /** 323 | * Check if a tab exists and is accessible 324 | * @param {number} tabId - The tab ID to check 325 | * @returns {Promise} - Whether the tab exists and is accessible 326 | */ 327 | async function tabExists(tabId) { 328 | try { 329 | return await new Promise((resolve) => { 330 | chrome.tabs.get(tabId, (tab) => { 331 | if (chrome.runtime.lastError) { 332 | console.warn(`Tab check failed: ${chrome.runtime.lastError.message}`); 333 | resolve(false); 334 | return; 335 | } 336 | resolve(true); 337 | }); 338 | }); 339 | } catch (err) { 340 | console.warn('Error checking tab existence:', err); 341 | return false; 342 | } 343 | } 344 | 345 | /** 346 | * Listens for tab updates 347 | * Ensures the content script is loaded when a tab is updated 348 | */ 349 | chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { 350 | if (!extensionContextValid) { 351 | console.warn('Extension context is invalid, cannot process tab update'); 352 | return; 353 | } 354 | 355 | // Ensure page is fully loaded and URL is one we can operate on 356 | if (changeInfo.status === 'complete' && tab.url) { 357 | // Skip unsupported URL schemes and error pages 358 | const url = tab.url.toLowerCase(); 359 | if (url.startsWith('chrome:') || 360 | url.startsWith('chrome-extension:') || 361 | url.startsWith('about:') || 362 | url.startsWith('file:') || 363 | url.startsWith('view-source:') || 364 | url.startsWith('devtools:') || 365 | !url.startsWith('http')) { 366 | console.log(`Skipping tab update for unsupported URL: ${url}`); 367 | return; 368 | } 369 | 370 | // Check tab status 371 | tabExists(tabId).then(exists => { 372 | if (!exists) { 373 | console.log(`Tab ${tabId} no longer exists, skipping update`); 374 | return; 375 | } 376 | 377 | chrome.tabs.get(tabId, function(currentTab) { 378 | if (chrome.runtime.lastError) { 379 | console.warn('Error getting tab:', chrome.runtime.lastError.message); 380 | return; 381 | } 382 | 383 | // Ignore error pages 384 | if (currentTab.status === 'error' || !currentTab.url.startsWith('http')) { 385 | console.log('Skipping error page or non-http page'); 386 | return; 387 | } 388 | 389 | // Safely execute content script injection 390 | safeExecute(async () => { 391 | try { 392 | // Verify tab still exists before proceeding 393 | if (!(await tabExists(tabId))) { 394 | console.log(`Tab ${tabId} no longer exists, aborting script injection`); 395 | return; 396 | } 397 | 398 | const isLoaded = await checkContentScriptLoaded(tabId); 399 | if (!isLoaded) { 400 | console.log('Content script not loaded after tab update, injecting now...'); 401 | 402 | // Final check before executing script 403 | if (await tabExists(tabId)) { 404 | await chrome.scripting.executeScript({ 405 | target: { tabId: tabId }, 406 | files: ['contentScript.js'] 407 | }); 408 | } else { 409 | console.log(`Tab ${tabId} disappeared before script injection`); 410 | } 411 | } 412 | } catch (err) { 413 | // Handle "Frame with ID 0 is showing error page" errors 414 | if (err.message && ( 415 | err.message.includes('showing error page') || 416 | err.message.includes('cannot access a chrome') || 417 | err.message.includes('cannot be scripted due to') || 418 | err.message.includes('No tab with id'))) { 419 | console.log('Skipping scripting on error, restricted page or closed tab:', err.message); 420 | return; 421 | } 422 | throw err; // Re-throw other errors 423 | } 424 | }).catch(error => { 425 | console.error('Error handling tab update:', error); 426 | }); 427 | }); 428 | }).catch(err => { 429 | console.warn('Tab existence check failed:', err); 430 | }); 431 | } 432 | }); 433 | 434 | /** 435 | * Listens for messages from content script 436 | * Handles search requests and deactivation notifications 437 | */ 438 | chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { 439 | if (!extensionContextValid) { 440 | console.warn('Extension context is invalid, cannot process message'); 441 | sendResponse({ error: 'Extension context invalid' }); 442 | return true; 443 | } 444 | 445 | try { 446 | if (request.action === 'searchFontFamily') { 447 | const fontFamily = request.fontFamily; 448 | const formattedFontFamily = fontFamily.replace(/['"]/g, '').split(',')[0].trim(); 449 | 450 | const url = `https://www.google.com/search?q=${encodeURIComponent(formattedFontFamily + ' font')}`; 451 | chrome.tabs.create({ url }); 452 | } else if (request.action === 'deactivateExtension') { 453 | // Handle extension deactivation request from content script 454 | console.log('Received extension deactivation request'); 455 | } else if (request.action === 'updateIcon') { 456 | // Update icon based on extension state 457 | if (request.iconState === 'active') { 458 | // Use active state icon when extension is active 459 | chrome.action.setIcon({ 460 | path: { 461 | "128": "icon-open.png" 462 | } 463 | }); 464 | } else { 465 | // Use default icon when extension is inactive 466 | chrome.action.setIcon({ 467 | path: { 468 | "128": "icon.png" 469 | } 470 | }); 471 | } 472 | // Send response 473 | sendResponse({ success: true }); 474 | } else if (request.action === 'checkExtensionContext') { 475 | // Allow content script to check if background context is valid 476 | sendResponse({ valid: extensionContextValid }); 477 | } 478 | } catch (error) { 479 | console.error('Error processing message:', error); 480 | sendResponse({ error: error.message }); 481 | } 482 | 483 | return true; // Keep message channel open 484 | }); -------------------------------------------------------------------------------- /contentScript.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | const TOGGLE_ACTION = 'toggleExtension'; 3 | let isActive = false; 4 | let currentTarget; 5 | let tooltip; // tooltip element 6 | let fixedTooltips = []; // array of fixed tooltips 7 | let animationFrameId; // for requestAnimationFrame 8 | let lastTooltipContent = ''; // cache tooltip content 9 | let selectionTimeout = null; // for preventing multiple selection events 10 | let isReinitializing = false; // prevent repeated initialization 11 | let fixedTooltipPositions = new Set(); // track positions of created fixed tooltips 12 | let isExtensionContextValid = true; // track extension context validity 13 | let lastMouseX = 0, lastMouseY = 0; // for storing mouse position 14 | let isCreatingFixedTooltip = false; // flag to prevent mouse events from interfering with newly created tooltip 15 | let lastTargetHash = ''; // Add global variable for caching 16 | let miniTooltip; // Add mini tooltip variable 17 | let isLongPress = false; // Add long press state tracking 18 | 19 | // Try to capture all unhandled errors 20 | window.addEventListener('error', function(event) { 21 | if (event.error && event.error.message && event.error.message.includes('Extension context invalidated')) { 22 | console.warn('Captured Extension context invalidated error, preparing to clean up resources...'); 23 | isExtensionContextValid = false; 24 | cleanupResources(true); // Force cleanup 25 | } 26 | }); 27 | 28 | // Add a more robust error handling for Chrome extension context 29 | try { 30 | // Check if chrome runtime is available 31 | if (chrome && chrome.runtime) { 32 | // Add a listener for runtime connection to detect disconnection 33 | chrome.runtime.onConnect.addListener(function(port) { 34 | port.onDisconnect.addListener(function() { 35 | if (chrome.runtime.lastError) { 36 | console.warn('Port disconnected due to error:', chrome.runtime.lastError); 37 | isExtensionContextValid = false; 38 | cleanupResources(true); // Force cleanup 39 | } 40 | }); 41 | }); 42 | 43 | // Add a listener for runtime.id to detect when extension is reloaded 44 | function checkExtensionContext() { 45 | try { 46 | // This will throw if extension context is invalidated 47 | const extensionId = chrome.runtime.id; 48 | setTimeout(checkExtensionContext, 5000); // Check every 5 seconds 49 | } catch (e) { 50 | console.warn('Extension context check failed:', e); 51 | isExtensionContextValid = false; 52 | cleanupResources(true); // Force cleanup 53 | } 54 | } 55 | 56 | // Start periodic checking 57 | setTimeout(checkExtensionContext, 5000); 58 | } 59 | } catch (e) { 60 | console.warn('Error setting up extension context monitoring:', e); 61 | } 62 | 63 | /** 64 | * Clean up all resources 65 | * @param {boolean} force - Force cleanup even if already reinitializing 66 | */ 67 | function cleanupResources(force = false) { 68 | try { 69 | // Only clean up resources if not already reinitializing or if forced 70 | if (!isReinitializing || force) { 71 | isReinitializing = true; 72 | console.log('Cleaning up FontDetector resources...'); 73 | 74 | isActive = false; 75 | 76 | // Safe removal of tooltip 77 | if (tooltip) { 78 | try { 79 | if (tooltip.parentNode) { 80 | tooltip.parentNode.removeChild(tooltip); 81 | } else { 82 | tooltip.remove(); 83 | } 84 | } catch(e) { 85 | console.warn('Error removing tooltip:', e); 86 | } 87 | tooltip = null; 88 | } 89 | 90 | // Clean up all fixed tooltips 91 | try { 92 | removeAllFixedTooltips(); 93 | } catch(e) { 94 | console.warn('Error removing fixed tooltips:', e); 95 | 96 | // Fallback cleanup for fixed tooltips 97 | try { 98 | for (let i = 0; i < fixedTooltips.length; i++) { 99 | try { 100 | const t = fixedTooltips[i]; 101 | if (t && t.parentNode) { 102 | t.parentNode.removeChild(t); 103 | } 104 | } catch(e) {} 105 | } 106 | fixedTooltips = []; 107 | 108 | // Also try to remove any elements with font-detector class 109 | const detectorElements = document.querySelectorAll('.font-detector'); 110 | for (let i = 0; i < detectorElements.length; i++) { 111 | try { 112 | const el = detectorElements[i]; 113 | if (el && el.parentNode) { 114 | el.parentNode.removeChild(el); 115 | } 116 | } catch(e) {} 117 | } 118 | } catch(e) {} 119 | } 120 | 121 | // Remove all event listeners safely 122 | try { removeMouseListeners(); } catch(e) { 123 | console.warn('Error removing mouse listeners:', e); 124 | } 125 | 126 | try { removeSelectionListener(); } catch(e) { 127 | console.warn('Error removing selection listener:', e); 128 | } 129 | 130 | try { document.removeEventListener('keydown', handleKeyDown); } catch(e) { 131 | console.warn('Error removing keydown listener:', e); 132 | } 133 | 134 | // Cancel all animation frame requests 135 | if (animationFrameId) { 136 | try { cancelAnimationFrame(animationFrameId); } catch(e) { 137 | console.warn('Error canceling animation frame:', e); 138 | } 139 | animationFrameId = null; 140 | } 141 | 142 | // Clear selection timeout 143 | if (selectionTimeout) { 144 | try { clearTimeout(selectionTimeout); } catch(e) { 145 | console.warn('Error clearing selection timeout:', e); 146 | } 147 | selectionTimeout = null; 148 | } 149 | 150 | // Clear position set 151 | try { fixedTooltipPositions.clear(); } catch(e) { 152 | console.warn('Error clearing position set:', e); 153 | } 154 | 155 | console.log('FontDetector resource cleanup completed'); 156 | 157 | // Allow reinitialization after delay 158 | setTimeout(() => { 159 | isReinitializing = false; 160 | }, 2000); 161 | } 162 | } catch (e) { 163 | console.error('Error occurred while cleaning up resources:', e); 164 | // Reset reinitialization flag in case of error 165 | setTimeout(() => { 166 | isReinitializing = false; 167 | }, 2000); 168 | } 169 | } 170 | 171 | /** 172 | * Use requestAnimationFrame for smooth animations 173 | * @param {Function} callback - The function to call 174 | */ 175 | function requestUpdate(callback) { 176 | // Cancel any pending animation frame 177 | if (animationFrameId) { 178 | cancelAnimationFrame(animationFrameId); 179 | } 180 | 181 | // Schedule new animation frame 182 | animationFrameId = requestAnimationFrame(callback); 183 | } 184 | 185 | /** 186 | * Convert hex color to RGB 187 | * @param {string} hex - Hex color string 188 | * @returns {Array} - RGB values as array [r, g, b] 189 | */ 190 | function hexToRgb(hex) { 191 | hex = hex.replace(/^#/, ''); 192 | let r = parseInt(hex.substring(0, 2), 16); 193 | let g = parseInt(hex.substring(2, 4), 16); 194 | let b = parseInt(hex.substring(4, 6), 16); 195 | return [r, g, b]; 196 | } 197 | 198 | /** 199 | * Convert RGB to hex color 200 | * @param {number} r - Red value (0-255) 201 | * @param {number} g - Green value (0-255) 202 | * @param {number} b - Blue value (0-255) 203 | * @returns {string} - Hex color string 204 | */ 205 | function rgbToHex(r, g, b) { 206 | return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1); 207 | } 208 | 209 | /** 210 | * Convert RGB to LCH color space 211 | * @param {number} r - Red value (0-255) 212 | * @param {number} g - Green value (0-255) 213 | * @param {number} b - Blue value (0-255) 214 | * @returns {Object} - LCH values {l, c, h} 215 | */ 216 | function rgbToLCH(r, g, b) { 217 | // Convert to sRGB 218 | r /= 255; 219 | g /= 255; 220 | b /= 255; 221 | 222 | // Convert to XYZ 223 | let x = r * 0.4124 + g * 0.3576 + b * 0.1805; 224 | let y = r * 0.2126 + g * 0.7152 + b * 0.0722; 225 | let z = r * 0.0193 + g * 0.1192 + b * 0.9505; 226 | 227 | // XYZ to Lab 228 | const xRef = 0.95047; 229 | const yRef = 1.0; 230 | const zRef = 1.08883; 231 | 232 | x = x / xRef; 233 | y = y / yRef; 234 | z = z / zRef; 235 | 236 | x = x > 0.008856 ? Math.pow(x, 1/3) : (7.787 * x) + 16/116; 237 | y = y > 0.008856 ? Math.pow(y, 1/3) : (7.787 * y) + 16/116; 238 | z = z > 0.008856 ? Math.pow(z, 1/3) : (7.787 * z) + 16/116; 239 | 240 | const l = (116 * y) - 16; 241 | const a = 500 * (x - y); 242 | const b2 = 200 * (y - z); 243 | 244 | // Lab to LCh 245 | const c = Math.sqrt(a * a + b2 * b2); 246 | let h = Math.atan2(b2, a) * (180 / Math.PI); 247 | if (h < 0) h += 360; 248 | 249 | return { 250 | l: Math.round(l), 251 | c: Math.round(c), 252 | h: Math.round(h) 253 | }; 254 | } 255 | 256 | /** 257 | * Convert RGB to HCL color space (HCL is LCH with reordered components) 258 | * @param {number} r - Red value (0-255) 259 | * @param {number} g - Green value (0-255) 260 | * @param {number} b - Blue value (0-255) 261 | * @returns {Object} - HCL values {h, c, l} 262 | */ 263 | function rgbToHCL(r, g, b) { 264 | const lch = rgbToLCH(r, g, b); 265 | return { 266 | h: lch.h, 267 | c: lch.c, 268 | l: lch.l 269 | }; 270 | } 271 | 272 | /** 273 | * Get color information from an element 274 | * @param {Element} element - DOM element 275 | * @returns {Object|null} - Color information or null if not available 276 | */ 277 | function getColorFromElement(element) { 278 | try { 279 | const style = getComputedStyle(element); 280 | const color = style.color; 281 | 282 | // Create a temporary element to parse any color format 283 | const tempEl = document.createElement('div'); 284 | tempEl.style.color = color; 285 | tempEl.style.display = 'none'; 286 | 287 | // Add null check before appendChild 288 | if (document.body) { 289 | document.body.appendChild(tempEl); 290 | 291 | // Get the computed color value 292 | const computedColor = getComputedStyle(tempEl).color; 293 | document.body.removeChild(tempEl); 294 | 295 | // Parse RGB or RGBA color 296 | const rgbMatch = computedColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/); 297 | if (rgbMatch) { 298 | const r = parseInt(rgbMatch[1]); 299 | const g = parseInt(rgbMatch[2]); 300 | const b = parseInt(rgbMatch[3]); 301 | 302 | // Return color info in multiple formats 303 | return { 304 | rgb: { r, g, b }, 305 | hex: rgbToHex(r, g, b), 306 | hcl: rgbToHCL(r, g, b), 307 | lch: rgbToLCH(r, g, b) 308 | }; 309 | } 310 | } else { 311 | // Fallback for when document.body is null 312 | console.error('Cannot append temp element: document.body is null'); 313 | 314 | // Try to extract color info without using DOM 315 | const rgbMatch = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/); 316 | if (rgbMatch) { 317 | const r = parseInt(rgbMatch[1]); 318 | const g = parseInt(rgbMatch[2]); 319 | const b = parseInt(rgbMatch[3]); 320 | 321 | return { 322 | rgb: { r, g, b }, 323 | hex: rgbToHex(r, g, b), 324 | hcl: rgbToHCL(r, g, b), 325 | lch: rgbToLCH(r, g, b) 326 | }; 327 | } 328 | } 329 | 330 | return null; 331 | } catch (err) { 332 | console.error('Error getting color from element:', err); 333 | return null; 334 | } 335 | } 336 | 337 | /** 338 | * Toggle extension state 339 | */ 340 | function toggleExtension() { 341 | isActive = !isActive; 342 | if (isActive) { 343 | // Initialize detector and reset state 344 | currentTarget = null; 345 | lastTooltipContent = ''; 346 | 347 | // Fully initialize detector 348 | initializeDetector(); 349 | 350 | // Ensure mini tooltip is visible 351 | if (miniTooltip) { 352 | miniTooltip.style.display = 'block'; 353 | requestAnimationFrame(() => { 354 | miniTooltip.classList.add('visible'); 355 | miniTooltip.style.opacity = '1'; 356 | }); 357 | } 358 | 359 | // Send message to background script to update icon to active state 360 | chrome.runtime.sendMessage({ action: 'updateIcon', iconState: 'active' }); 361 | console.log('Extension activated'); 362 | } else { 363 | // Deactivate detector but preserve fixed tooltips 364 | deinitializeDetector(true); // true means preserve fixed tooltips 365 | 366 | // Hide mini tooltip 367 | if (miniTooltip) { 368 | miniTooltip.classList.remove('visible'); 369 | miniTooltip.style.opacity = '0'; 370 | setTimeout(() => { 371 | miniTooltip.style.display = 'none'; 372 | }, 200); 373 | } 374 | 375 | // Send message to background script to restore icon to default state 376 | chrome.runtime.sendMessage({ action: 'updateIcon', iconState: 'inactive' }); 377 | console.log('Extension deactivated'); 378 | } 379 | } 380 | 381 | /** 382 | * Debug function to log information about the current state 383 | * @param {string} message - Debug message 384 | * @param {any} data - Debug data 385 | */ 386 | function debug(message, data) { 387 | // Users can enable debugging by setting localStorage.fontDetectorDebug = 'true' 388 | // or by directly running window.fontDetectorDebug = true; 389 | const debugMode = window.fontDetectorDebug === true || localStorage.getItem('fontDetectorDebug') === 'true'; 390 | if (debugMode) { 391 | console.log(`[FontDetector] ${message}`, data || ''); 392 | } 393 | } 394 | 395 | /** 396 | * Initialize the font detector 397 | */ 398 | function initializeDetector() { 399 | // Ensure cleanup of existing resources 400 | if (tooltip) { 401 | try { 402 | tooltip.remove(); 403 | tooltip = null; 404 | } catch (e) { 405 | console.warn('Error cleaning up existing tooltip:', e); 406 | } 407 | } 408 | 409 | if (miniTooltip) { 410 | try { 411 | miniTooltip.remove(); 412 | miniTooltip = null; 413 | } catch (e) { 414 | console.warn('Error cleaning up existing mini tooltip:', e); 415 | } 416 | } 417 | 418 | injectCSS(); 419 | tooltip = createTooltip(); 420 | miniTooltip = createMiniTooltip(); 421 | 422 | // Ensure tooltip is initialized correctly 423 | if (tooltip) { 424 | tooltip.style.display = 'none'; 425 | tooltip.style.opacity = '0'; 426 | console.log('Tooltip element created and initialized'); 427 | } 428 | 429 | // Ensure mini tooltip is initialized correctly and visible 430 | if (miniTooltip) { 431 | miniTooltip.style.display = 'block'; 432 | requestAnimationFrame(() => { 433 | miniTooltip.classList.add('visible'); 434 | miniTooltip.style.opacity = '1'; 435 | }); 436 | console.log('Mini tooltip created and initialized'); 437 | } 438 | 439 | document.addEventListener('keydown', handleKeyDown); 440 | addMouseListeners(); 441 | addSelectionListener(); 442 | console.log('Font detector initialized - all event listeners added'); 443 | } 444 | 445 | /** 446 | * Deinitialize the font detector 447 | * @param {boolean} preserveFixedTooltips - whether to preserve fixed tooltips 448 | */ 449 | function deinitializeDetector(preserveFixedTooltips = false) { 450 | console.log(`Deactivating font detector (preserve fixed tooltips: ${preserveFixedTooltips})`); 451 | document.removeEventListener('keydown', handleKeyDown); 452 | 453 | // Safely remove following tooltip 454 | if (tooltip) { 455 | try { 456 | // Hide first 457 | console.log('Hiding mouse-following tooltip'); 458 | hideTooltip(tooltip); 459 | 460 | // Then remove 461 | tooltip.remove(); 462 | tooltip = null; 463 | console.log('Successfully removed mouse-following tooltip'); 464 | } catch (e) { 465 | console.warn('Error cleaning up tooltip:', e); 466 | // Fallback handling 467 | if (tooltip && tooltip.parentNode) { 468 | tooltip.parentNode.removeChild(tooltip); 469 | } 470 | tooltip = null; 471 | } 472 | } 473 | 474 | // Only remove fixed tooltips if not preserving 475 | if (!preserveFixedTooltips) { 476 | console.log('Not preserving fixed tooltips, removing all fixed tooltips'); 477 | removeAllFixedTooltips(); 478 | } else { 479 | console.log('Preserving all fixed tooltips'); 480 | } 481 | 482 | // Remove all event listeners 483 | removeMouseListeners(); 484 | removeSelectionListener(); 485 | 486 | // Cancel any pending animation frames 487 | if (animationFrameId) { 488 | cancelAnimationFrame(animationFrameId); 489 | animationFrameId = null; 490 | } 491 | 492 | // Clear all timeouts 493 | if (selectionTimeout) { 494 | clearTimeout(selectionTimeout); 495 | selectionTimeout = null; 496 | } 497 | 498 | // Clear state variables 499 | currentTarget = null; 500 | 501 | console.log('Font detector has been deactivated' + (preserveFixedTooltips ? ' (fixed tooltips preserved)' : '')); 502 | } 503 | 504 | /** 505 | * Remove all fixed tooltips 506 | */ 507 | function removeAllFixedTooltips() { 508 | try { 509 | console.log('Removing all fixed tooltips...'); 510 | 511 | // If creating a tooltip, avoid deleting protected tooltips 512 | if (isCreatingFixedTooltip) { 513 | console.log('Creating tooltip, only removing unprotected tooltips'); 514 | // Clean up unprotected tooltips 515 | const tooltipsToRemove = document.querySelectorAll('.fixed-tooltip:not([data-protected="true"])'); 516 | for (let i = 0; i < tooltipsToRemove.length; i++) { 517 | try { 518 | const t = tooltipsToRemove[i]; 519 | if (t && t.parentNode) { 520 | fixedTooltipPositions.delete(t.dataset.positionKey); 521 | t.parentNode.removeChild(t); 522 | fixedTooltips = fixedTooltips.filter(tooltip => tooltip !== t); 523 | } 524 | } catch (err) { 525 | console.warn('Error removing unprotected tooltip:', err); 526 | } 527 | } 528 | console.log('Finished removing unprotected tooltips'); 529 | return; 530 | } 531 | 532 | // Standard deletion logic when no creation is in progress 533 | // Clean up position records collection 534 | try { 535 | fixedTooltipPositions.clear(); 536 | } catch (err) { 537 | console.warn('Error clearing fixedTooltipPositions:', err); 538 | } 539 | 540 | // Save current tooltip array and reset 541 | let tooltipsToRemove = []; 542 | try { 543 | tooltipsToRemove = [...fixedTooltips]; 544 | fixedTooltips = []; // Reset array first 545 | } catch (err) { 546 | console.warn('Error copying fixedTooltips array:', err); 547 | // Fallback: try to get tooltips from DOM 548 | try { 549 | tooltipsToRemove = Array.from(document.querySelectorAll('.fixed-tooltip')); 550 | } catch (err2) { 551 | console.warn('Error getting tooltips from DOM:', err2); 552 | tooltipsToRemove = []; 553 | } 554 | } 555 | 556 | // Remove each tooltip 557 | for (let i = 0; i < tooltipsToRemove.length; i++) { 558 | try { 559 | const t = tooltipsToRemove[i]; 560 | if (t) { 561 | if (t.parentNode) { 562 | t.parentNode.removeChild(t); 563 | } else { 564 | t.remove(); 565 | } 566 | } 567 | } catch (err) { 568 | console.warn('Error removing fixed tooltip:', err); 569 | } 570 | } 571 | 572 | // Double check to ensure all .fixed-tooltip elements are removed 573 | setTimeout(() => { 574 | try { 575 | const remainingTooltips = document.querySelectorAll('.fixed-tooltip'); 576 | if (remainingTooltips.length > 0) { 577 | console.log(`Found ${remainingTooltips.length} remaining tooltips, cleaning up...`); 578 | 579 | for (let i = 0; i < remainingTooltips.length; i++) { 580 | try { 581 | const t = remainingTooltips[i]; 582 | if (t) { 583 | if (t.parentNode) { 584 | t.parentNode.removeChild(t); 585 | } else { 586 | t.remove(); 587 | } 588 | } 589 | } catch (err) { 590 | console.warn('Error removing remaining tooltip:', err); 591 | } 592 | } 593 | } 594 | } catch (err) { 595 | console.warn('Error getting remaining tooltips:', err); 596 | } 597 | }, 10); 598 | 599 | // As a last resort, remove any elements with font-detector class 600 | setTimeout(() => { 601 | try { 602 | const detectorElements = document.querySelectorAll('.font-detector:not(#fontInfoTooltip)'); 603 | if (detectorElements.length > 0) { 604 | console.log(`Found ${detectorElements.length} remaining detector elements, cleaning up...`); 605 | 606 | for (let i = 0; i < detectorElements.length; i++) { 607 | try { 608 | const el = detectorElements[i]; 609 | if (el && el.parentNode) { 610 | el.parentNode.removeChild(el); 611 | } 612 | } catch (err) { 613 | console.warn('Error removing detector element:', err); 614 | } 615 | } 616 | } 617 | } catch (err) { 618 | console.warn('Error getting detector elements:', err); 619 | } 620 | }, 20); 621 | 622 | console.log('All fixed tooltips removed'); 623 | } catch (err) { 624 | console.error('Error removing all fixed tooltips:', err); 625 | } 626 | } 627 | 628 | /** 629 | * Inject CSS styles for the font detector 630 | */ 631 | function injectCSS() { 632 | const css = ` 633 | @font-face { 634 | font-family: 'Satoshi'; 635 | src: url('chrome-extension://${chrome.runtime.id}/Satoshi-Medium.otf') format('opentype'); 636 | font-weight: 500; 637 | font-style: normal; 638 | font-display: swap; 639 | } 640 | 641 | .font-detector { 642 | color: #A8A8A8; 643 | z-index: 2147483647 !important; 644 | } 645 | 646 | .font-detector span { 647 | color: #fff; 648 | } 649 | 650 | #miniTooltip { 651 | position: fixed; 652 | padding: 4px 8px; 653 | background-color: rgba(30, 30, 30, 0.95); 654 | border: 1px solid #2F2F2F; 655 | border-radius: 4px; 656 | font-size: 13px; 657 | color: #fff; 658 | pointer-events: none; 659 | font-family: 'Satoshi', Arial, sans-serif; 660 | white-space: nowrap; 661 | opacity: 0; 662 | display: none; 663 | transition: opacity 0.15s ease-out; 664 | backdrop-filter: blur(8px); 665 | -webkit-backdrop-filter: blur(8px); 666 | z-index: 2147483647; 667 | } 668 | 669 | #miniTooltip.visible { 670 | opacity: 1; 671 | display: block; 672 | } 673 | 674 | #fontInfoTooltip, .fixed-tooltip { 675 | border: 1px solid #2F2F2F; 676 | background-color: rgba(30, 30, 30, 0.85); 677 | font-family: 'Satoshi', Arial, sans-serif; 678 | padding: 16px 16px; 679 | border-radius: 16px; 680 | word-wrap: break-word; 681 | position: relative; 682 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); 683 | transition: all 0.2s ease-in-out; 684 | opacity: 0; 685 | backdrop-filter: blur(8px); 686 | -webkit-backdrop-filter: blur(8px); 687 | width: 250px; 688 | transform: translateY(-5px); 689 | pointer-events: none; 690 | } 691 | 692 | #fontInfoTooltip { 693 | position: fixed; 694 | pointer-events: none; 695 | transform: translate3d(0, 0, 0); 696 | will-change: transform; 697 | backface-visibility: hidden; 698 | transition: opacity 0.2s ease-in-out; 699 | opacity: 0; 700 | display: none; 701 | } 702 | 703 | #fontInfoTooltip.visible { 704 | opacity: 1; 705 | display: block; 706 | } 707 | 708 | .fixed-tooltip { 709 | position: absolute; 710 | z-index: 2147483647 !important; 711 | pointer-events: auto !important; 712 | } 713 | 714 | .fixed-tooltip[data-is-selection-tooltip="true"] { 715 | /* width setting removed */ 716 | } 717 | 718 | #fontInfoTooltip h1, .fixed-tooltip h1 { 719 | display: none; 720 | } 721 | 722 | #fontInfoTooltip div, .fixed-tooltip div { 723 | display: flex; 724 | flex-direction: column; 725 | color: #A8A8A8; 726 | font-size: 13px; 727 | margin-bottom: 6px; 728 | gap: 2px; 729 | font-family: 'Satoshi', Arial, sans-serif; 730 | } 731 | 732 | #fontInfoTooltip div span, .fixed-tooltip div span { 733 | color: #FFFFFF; 734 | font-size: 14px; 735 | margin-left: 0px; 736 | font-weight: 500; 737 | font-family: 'Satoshi', Arial, sans-serif; 738 | } 739 | 740 | #fontInfoTooltip a, .fixed-tooltip a { 741 | text-decoration: none; 742 | color: inherit; 743 | font-family: 'Satoshi', Arial, sans-serif; 744 | } 745 | 746 | .color-preview { 747 | width: 12px; 748 | height: 12px; 749 | border-radius: 50%; 750 | display: inline-block; 751 | margin-right: 8px; 752 | border: 1px solid rgba(255, 255, 255, 0.2); 753 | vertical-align: middle; 754 | } 755 | 756 | .color-value-container { 757 | display: flex; 758 | flex-direction: row; 759 | align-items: center; 760 | justify-content: flex-start; 761 | } 762 | 763 | .close-button { 764 | position: absolute; 765 | top: 14px; 766 | right: 16px; 767 | width: 14px; 768 | height: 14px; 769 | border-radius: 50%; 770 | display: flex; 771 | align-items: center; 772 | justify-content: center; 773 | cursor: pointer; 774 | transition: background-color 0.2s; 775 | pointer-events: auto !important; 776 | } 777 | 778 | .close-button:hover { 779 | background-color: rgba(80, 80, 80, 0.9); 780 | } 781 | 782 | .close-button svg { 783 | width: 16px; 784 | height: 16px; 785 | display: block; 786 | } 787 | 788 | .copy-icon { 789 | width: 24px; 790 | height: 24px; 791 | margin-left: 8px; 792 | display: inline-flex; 793 | align-items: center; 794 | justify-content: center; 795 | cursor: pointer; 796 | position: relative; 797 | background-color: transparent; 798 | border-radius: 4px; 799 | transition: background-color 0.2s; 800 | font-family: 'Satoshi', Arial, sans-serif; 801 | } 802 | 803 | .copy-icon:hover { 804 | background-color: rgba(255, 255, 255, 0.1); 805 | } 806 | 807 | .copy-icon svg { 808 | width: 14px; 809 | height: 14px; 810 | display: block; 811 | } 812 | 813 | .value-with-copy { 814 | display: flex; 815 | align-items: center; 816 | justify-content: space-between; 817 | } 818 | 819 | #fontInfoTooltip { 820 | pointer-events: none; 821 | } 822 | 823 | #fontInfoTooltip .copy-icon { 824 | pointer-events: auto; 825 | } 826 | `; 827 | 828 | const style = document.createElement('style'); 829 | style.textContent = css; 830 | 831 | if (document.head) { 832 | document.head.appendChild(style); 833 | } else if (document.documentElement) { 834 | document.documentElement.appendChild(style); 835 | } else { 836 | console.error('Cannot inject CSS: document.head and document.documentElement are null'); 837 | } 838 | } 839 | 840 | /** 841 | * Create the tooltip element 842 | * @returns {Element} - Tooltip DOM element 843 | */ 844 | function createTooltip() { 845 | // Remove existing tooltip 846 | const existingTooltip = document.getElementById('fontInfoTooltip'); 847 | if (existingTooltip) { 848 | existingTooltip.remove(); 849 | } 850 | 851 | const tooltip = document.createElement('div'); 852 | tooltip.classList.add('font-detector'); 853 | tooltip.setAttribute('id', 'fontInfoTooltip'); 854 | 855 | // Set basic styles 856 | tooltip.style.position = 'fixed'; 857 | tooltip.style.display = 'none'; 858 | tooltip.style.opacity = '0'; 859 | tooltip.style.zIndex = '2147483647'; 860 | tooltip.style.pointerEvents = 'none'; 861 | tooltip.style.backgroundColor = 'rgba(30, 30, 30, 0.95)'; 862 | tooltip.style.backdropFilter = 'blur(10px)'; 863 | tooltip.style.webkitBackdropFilter = 'blur(10px)'; 864 | tooltip.style.transform = 'translate3d(0, 0, 0)'; 865 | tooltip.style.willChange = 'transform'; 866 | tooltip.style.backfaceVisibility = 'hidden'; 867 | tooltip.style.transition = 'opacity 0.2s ease-in-out'; 868 | 869 | // Add to document 870 | if (document.body) { 871 | document.body.appendChild(tooltip); 872 | 873 | // Ensure initial state is correct 874 | requestAnimationFrame(() => { 875 | tooltip.style.display = 'none'; 876 | tooltip.style.opacity = '0'; 877 | tooltip.classList.remove('visible'); 878 | }); 879 | } 880 | 881 | return tooltip; 882 | } 883 | 884 | /** 885 | * Create a fixed tooltip at the selected text 886 | * @param {Event} event - The event object 887 | * @param {Element} element - The element to get font info from 888 | * @returns {Element} - The created fixed tooltip 889 | */ 890 | function createFixedTooltip(event, element) { 891 | try { 892 | console.log('===== Creating fixed tooltip [START] ====='); 893 | 894 | // Get current selection 895 | const selection = window.getSelection(); 896 | if (!selection || selection.rangeCount === 0) { 897 | console.warn('No valid text selection'); 898 | return null; 899 | } 900 | 901 | const range = selection.getRangeAt(0); 902 | const selectedText = selection.toString().trim(); 903 | 904 | // Output debug info 905 | console.log(`Selected text: "${selectedText.substring(0, 30)}${selectedText.length > 30 ? '...' : ''}"`); 906 | 907 | // Check if tooltip with same or similar text content already exists (new code) 908 | const existingTooltips = document.querySelectorAll('.fixed-tooltip'); 909 | for (let i = 0; i < existingTooltips.length; i++) { 910 | const existingTooltip = existingTooltips[i]; 911 | // Check if tooltip with same text content exists 912 | if (existingTooltip.dataset.selectedText === selectedText) { 913 | console.log('Tooltip with same text content already exists, using existing tooltip'); 914 | return existingTooltip; 915 | } 916 | } 917 | 918 | // Declare position variables 919 | let tooltipLeft = null; 920 | let tooltipTop = null; 921 | let positionKey = null; 922 | let positionMethod = null; 923 | 924 | // Generate unique ID, but include text content information (modified code) 925 | // Use text content hash as part of ID, instead of pure random 926 | const textHash = hashCode(selectedText).toString(); 927 | const uniqueId = Date.now() + '_' + textHash; 928 | 929 | // Method 1: Using getClientRects() - most precise method 930 | console.log('Trying method 1: getClientRects()'); 931 | try { 932 | const rects = range.getClientRects(); 933 | if (rects && rects.length > 0) { 934 | const lastRect = rects[rects.length - 1]; 935 | if (lastRect && lastRect.width > 0 && lastRect.height > 0) { 936 | console.log('Success: Using getClientRects() to get position', lastRect); 937 | 938 | // Precisely set 4px below selected text 939 | tooltipLeft = window.pageXOffset + lastRect.left; 940 | tooltipTop = window.pageYOffset + lastRect.bottom + 4; // ensure 4px 941 | positionKey = `rects-${textHash}-${Math.round(tooltipLeft)},${Math.round(tooltipTop)}`; 942 | positionMethod = 'getClientRects'; 943 | } else { 944 | console.log('Method 1 failed: Invalid rectangle dimensions'); 945 | } 946 | } else { 947 | console.log('Method 1 failed: No rectangles obtained'); 948 | } 949 | } catch (err) { 950 | console.warn('Method 1 error:', err); 951 | } 952 | 953 | // Method 2: Using getBoundingClientRect() 954 | if (!tooltipLeft || !tooltipTop) { 955 | console.log('Trying method 2: getBoundingClientRect()'); 956 | try { 957 | const rect = range.getBoundingClientRect(); 958 | if (rect && rect.width > 0 && rect.height > 0) { 959 | console.log('Success: Using getBoundingClientRect() to get position', rect); 960 | 961 | tooltipLeft = window.pageXOffset + rect.left; 962 | tooltipTop = window.pageYOffset + rect.bottom + 4; // ensure 4px 963 | positionKey = `rect-${uniqueId}-${Math.round(tooltipLeft)},${Math.round(tooltipTop)}`; 964 | positionMethod = 'getBoundingClientRect'; 965 | } else { 966 | console.log('Method 2 failed: Invalid rectangle dimensions'); 967 | } 968 | } catch (err) { 969 | console.warn('Method 2 error:', err); 970 | } 971 | } 972 | 973 | // Method 3: Using mouse position (most reliable fallback) 974 | if (!tooltipLeft || !tooltipTop) { 975 | console.log('Trying method 3: Using mouse position'); 976 | if (event && ('clientX' in event || 'pageX' in event)) { 977 | console.log('Success: Using mouse event position'); 978 | 979 | tooltipLeft = event.pageX !== undefined ? event.pageX : 980 | (event.clientX !== undefined ? event.clientX + window.pageXOffset : 0); 981 | tooltipTop = (event.pageY !== undefined ? event.pageY : 982 | (event.clientY !== undefined ? event.clientY + window.pageYOffset : 0)) + 4; // ensure 4px 983 | positionKey = `mouse-${uniqueId}-${Math.round(tooltipLeft)},${Math.round(tooltipTop)}`; 984 | positionMethod = 'mouseEvent'; 985 | } else if (lastMouseX !== undefined && lastMouseY !== undefined) { 986 | console.log('Success: Using last recorded mouse position'); 987 | 988 | tooltipLeft = lastMouseX + window.pageXOffset; 989 | tooltipTop = lastMouseY + window.pageYOffset + 4; // ensure 4px 990 | positionKey = `lastmouse-${uniqueId}-${Math.round(tooltipLeft)},${Math.round(tooltipTop)}`; 991 | positionMethod = 'lastMousePosition'; 992 | } else { 993 | console.log('Method 3 failed: Cannot get mouse position'); 994 | } 995 | } 996 | 997 | // Method 4: Final fallback - Using viewport center 998 | if (!tooltipLeft || !tooltipTop) { 999 | console.log('Trying method 4: Using viewport center position'); 1000 | 1001 | tooltipLeft = window.innerWidth / 2 + window.pageXOffset; 1002 | tooltipTop = window.innerHeight / 2 + window.pageYOffset; 1003 | positionKey = `center-${uniqueId}-${Math.round(tooltipLeft)},${Math.round(tooltipTop)}`; 1004 | positionMethod = 'viewportCenter'; 1005 | } 1006 | 1007 | // Basic position validation 1008 | if (tooltipLeft < 0 || tooltipTop < 0 || tooltipLeft > 50000 || tooltipTop > 50000) { 1009 | console.warn('Calculated tooltip position exceeds reasonable range:', tooltipLeft, tooltipTop); 1010 | 1011 | // Use safer position as fallback 1012 | tooltipLeft = window.innerWidth / 2 + window.pageXOffset; 1013 | tooltipTop = window.innerHeight / 2 + window.pageYOffset; 1014 | positionKey = `safe-${uniqueId}-${Math.round(tooltipLeft)},${Math.round(tooltipTop)}`; 1015 | positionMethod = 'safePosition'; 1016 | } 1017 | 1018 | // Ensure positions are valid numbers 1019 | tooltipLeft = Math.round(tooltipLeft); 1020 | tooltipTop = Math.round(tooltipTop); 1021 | 1022 | // Check if tooltip already exists at nearby position (new code, check similar positions) 1023 | if (tooltipLeft !== null && tooltipTop !== null) { 1024 | const proximityThreshold = 20; // Consider positions within 20px as same 1025 | for (let i = 0; i < existingTooltips.length; i++) { 1026 | const existingTooltip = existingTooltips[i]; 1027 | const existingLeft = parseFloat(existingTooltip.style.left); 1028 | const existingTop = parseFloat(existingTooltip.style.top); 1029 | 1030 | // If positions are very close, consider it a duplicate tooltip 1031 | if (!isNaN(existingLeft) && !isNaN(existingTop)) { 1032 | const distanceX = Math.abs(tooltipLeft - existingLeft); 1033 | const distanceY = Math.abs(tooltipTop - existingTop); 1034 | 1035 | if (distanceX < proximityThreshold && distanceY < proximityThreshold) { 1036 | console.log('Tooltip already exists at nearby position, using existing tooltip'); 1037 | return existingTooltip; 1038 | } 1039 | } 1040 | } 1041 | } 1042 | 1043 | console.log(`Final tooltip position: left=${tooltipLeft}, top=${tooltipTop}, method=${positionMethod}`); 1044 | console.log('===== Creating fixed tooltip [END] ====='); 1045 | 1046 | // Create positioned tooltip, pass selected text info 1047 | return createPositionedTooltip(positionKey, tooltipLeft, tooltipTop, element, selectedText); 1048 | } catch (err) { 1049 | console.error('Error creating fixed tooltip:', err); 1050 | return null; 1051 | } 1052 | } 1053 | 1054 | /** 1055 | * Calculate string hash value (new function) 1056 | * @param {string} str - String to hash 1057 | * @returns {number} - Hash value 1058 | */ 1059 | function hashCode(str) { 1060 | let hash = 0; 1061 | if (str.length === 0) return hash; 1062 | for (let i = 0; i < str.length; i++) { 1063 | const char = str.charCodeAt(i); 1064 | hash = ((hash << 5) - hash) + char; 1065 | hash = hash & hash; // Convert to 32bit integer 1066 | } 1067 | return Math.abs(hash); 1068 | } 1069 | 1070 | /** 1071 | * Create a positioned tooltip with the given position and content 1072 | * @param {string} positionKey - Unique position identifier 1073 | * @param {number} left - Left position 1074 | * @param {number} top - Top position 1075 | * @param {Element} element - Element to get font info from 1076 | * @param {string} selectedText - Selected text content (new parameter) 1077 | * @returns {Element} - Created tooltip or null 1078 | */ 1079 | function createPositionedTooltip(positionKey, left, top, element, selectedText = '') { 1080 | console.log(`Creating positioned tooltip: key=${positionKey}, left=${left}, top=${top}`); 1081 | 1082 | // Check if tooltip with exactly same position key already exists 1083 | const existingTooltips = document.querySelectorAll('.fixed-tooltip'); 1084 | for (let i = 0; i < existingTooltips.length; i++) { 1085 | const existingTooltip = existingTooltips[i]; 1086 | if (existingTooltip.dataset.positionKey === positionKey) { 1087 | console.log('Tooltip with same position already exists, skipping creation'); 1088 | return existingTooltip; 1089 | } 1090 | 1091 | // Check if tooltip with same text content exists (new check) 1092 | if (selectedText && existingTooltip.dataset.selectedText === selectedText) { 1093 | console.log('Tooltip with same text content already exists, skipping creation'); 1094 | return existingTooltip; 1095 | } 1096 | } 1097 | 1098 | // Record new position 1099 | fixedTooltipPositions.add(positionKey); 1100 | 1101 | // Create new fixed tooltip 1102 | const fixedTooltip = document.createElement('div'); 1103 | fixedTooltip.classList.add('font-detector', 'fixed-tooltip'); 1104 | fixedTooltip.dataset.positionKey = positionKey; 1105 | fixedTooltip.dataset.creationTime = Date.now().toString(); 1106 | fixedTooltip.dataset.isSelectionTooltip = 'true'; // Mark as selection-created tooltip 1107 | 1108 | // Save selected text content for duplicate detection (new code) 1109 | if (selectedText) { 1110 | fixedTooltip.dataset.selectedText = selectedText; 1111 | } 1112 | 1113 | // Get viewport dimensions for boundary checks 1114 | const viewportWidth = window.innerWidth; 1115 | const viewportHeight = window.innerHeight; 1116 | 1117 | // Estimate tooltip dimensions (will adjust as needed after DOM insertion) 1118 | const estimatedWidth = 250; // Width set in CSS 1119 | 1120 | // Adjust position to ensure tooltip stays within viewport, but respect original position 1121 | let adjustedLeft = left; 1122 | let adjustedTop = top; 1123 | 1124 | // Only check and adjust right boundary - if it would overflow, adjust left 1125 | if (adjustedLeft + estimatedWidth > viewportWidth - 10) { 1126 | adjustedLeft = Math.max(10, viewportWidth - estimatedWidth - 10); 1127 | console.log(`Right boundary adjustment: ${left} -> ${adjustedLeft}`); 1128 | } 1129 | 1130 | // For vertical direction, no adjustments, always maintain position 4px below text 1131 | console.log(`Keeping tooltip 4px below text: top=${adjustedTop}`); 1132 | 1133 | // Set precise position 1134 | fixedTooltip.style.left = `${adjustedLeft}px`; 1135 | fixedTooltip.style.top = `${adjustedTop}px`; 1136 | 1137 | // Set fixed width for consistency 1138 | fixedTooltip.style.width = '250px'; 1139 | 1140 | // Fill content 1141 | populateTooltipContent(fixedTooltip, element); 1142 | 1143 | // Add close button 1144 | const closeButton = document.createElement('div'); 1145 | closeButton.classList.add('close-button'); 1146 | closeButton.innerHTML = ``; 1147 | closeButton.style.pointerEvents = 'auto'; 1148 | closeButton.addEventListener('click', (e) => { 1149 | e.preventDefault(); 1150 | e.stopPropagation(); 1151 | 1152 | // Remove position from Set when closing 1153 | fixedTooltipPositions.delete(positionKey); 1154 | fixedTooltip.remove(); 1155 | fixedTooltips = fixedTooltips.filter(t => t !== fixedTooltip); 1156 | }); 1157 | 1158 | fixedTooltip.appendChild(closeButton); 1159 | 1160 | // Ensure only added to one parent element 1161 | let parentElement = null; 1162 | if (document.body) { 1163 | parentElement = document.body; // Prefer body as parent 1164 | } else if (document.documentElement) { 1165 | parentElement = document.documentElement; 1166 | } else { 1167 | console.error('Cannot add fixed tooltip: document.body and document.documentElement are both null'); 1168 | return null; 1169 | } 1170 | 1171 | // Add to DOM and record 1172 | parentElement.appendChild(fixedTooltip); 1173 | fixedTooltips.push(fixedTooltip); 1174 | 1175 | // Ensure created tooltip is visible 1176 | fixedTooltip.style.display = 'block'; 1177 | fixedTooltip.style.opacity = '1'; 1178 | // Add flicker protection mechanism, but use shorter duration 1179 | fixedTooltip.dataset.protected = 'true'; 1180 | 1181 | // After DOM addition, fine-tune position (if needed) - only adjust horizontal, not vertical 1182 | setTimeout(() => { 1183 | try { 1184 | // Prevent errors for removed elements 1185 | if (!fixedTooltip.isConnected) return; 1186 | 1187 | // Get actual tooltip dimensions 1188 | const tooltipRect = fixedTooltip.getBoundingClientRect(); 1189 | if (!tooltipRect) return; 1190 | 1191 | let needsAdjustment = false; 1192 | let newLeft = parseFloat(fixedTooltip.style.left); 1193 | 1194 | // Only check and adjust horizontal direction, ensure not exceeding right edge 1195 | if (tooltipRect.right > viewportWidth - 5) { 1196 | newLeft = Math.max(5, viewportWidth - tooltipRect.width - 5); 1197 | needsAdjustment = true; 1198 | console.log(`Fine-tuning right boundary: ${parseFloat(fixedTooltip.style.left)} -> ${newLeft}`); 1199 | 1200 | // Only update left position, don't adjust vertical 1201 | fixedTooltip.style.left = `${newLeft}px`; 1202 | console.log(`Horizontal position adjustment complete: left=${newLeft}`); 1203 | } 1204 | 1205 | } catch (err) { 1206 | console.warn('Error fine-tuning tooltip position:', err); 1207 | } 1208 | }, 0); 1209 | 1210 | // Cancel protection status after a while, using shorter time 1211 | setTimeout(() => { 1212 | if (fixedTooltip.isConnected) { 1213 | fixedTooltip.dataset.protected = 'false'; 1214 | } 1215 | }, 800); // Reduce protection time to avoid blocking other functions for too long 1216 | 1217 | console.log(`Fixed tooltip created: position=${positionKey} (${adjustedLeft}, ${adjustedTop})`); 1218 | return fixedTooltip; 1219 | } 1220 | 1221 | /** 1222 | * Fill tooltip with content 1223 | * @param {Element} tooltip - Tooltip element 1224 | * @param {Element} element - Element to get font info from 1225 | */ 1226 | function populateTooltipContent(tooltip, element) { 1227 | if (!tooltip || !element) return; 1228 | 1229 | try { 1230 | // Use cache to check if content needs updating 1231 | const targetHash = element.outerHTML; 1232 | if (targetHash === tooltip.dataset.lastTargetHash) { 1233 | return; // If content hasn't changed, return directly 1234 | } 1235 | 1236 | // Generate new content 1237 | const content = generateTooltipContent(element); 1238 | if (content && tooltip.innerHTML !== content) { 1239 | tooltip.innerHTML = content; 1240 | tooltip.dataset.lastTargetHash = targetHash; 1241 | 1242 | // Set up copy functionality 1243 | const copyIcons = tooltip.querySelectorAll('.copy-icon'); 1244 | copyIcons.forEach(icon => { 1245 | icon.addEventListener('click', function(e) { 1246 | e.preventDefault(); 1247 | e.stopPropagation(); 1248 | 1249 | const valueToCopy = this.dataset.value; 1250 | if (!valueToCopy) return; 1251 | 1252 | if (navigator.clipboard && navigator.clipboard.writeText) { 1253 | navigator.clipboard.writeText(valueToCopy) 1254 | .then(() => { 1255 | const originalSvg = this.innerHTML; 1256 | this.innerHTML = ``; 1257 | 1258 | setTimeout(() => { 1259 | this.innerHTML = originalSvg; 1260 | }, 1500); 1261 | }); 1262 | } 1263 | }); 1264 | }); 1265 | 1266 | // Set up font family links 1267 | const fontFamilyLinks = tooltip.querySelectorAll('.fontFamilyLink'); 1268 | fontFamilyLinks.forEach(link => { 1269 | link.addEventListener('click', function(e) { 1270 | e.preventDefault(); 1271 | e.stopPropagation(); 1272 | 1273 | const fontName = this.dataset.font; 1274 | if (!fontName) return; 1275 | 1276 | if (navigator.clipboard && navigator.clipboard.writeText) { 1277 | navigator.clipboard.writeText(fontName) 1278 | .then(() => { 1279 | const span = this.querySelector('span'); 1280 | if (span) { 1281 | const originalText = span.textContent; 1282 | span.textContent = 'Copied!'; 1283 | 1284 | setTimeout(() => { 1285 | span.textContent = originalText; 1286 | }, 1500); 1287 | } 1288 | }); 1289 | } 1290 | }); 1291 | }); 1292 | } 1293 | } catch (err) { 1294 | console.error('Error updating tooltip content:', err); 1295 | tooltip.innerHTML = '
Font information Unable to display details
'; 1296 | } 1297 | } 1298 | 1299 | /** 1300 | * Generate tooltip HTML content 1301 | * @param {Element} element - The element to get font info from 1302 | * @returns {string} - HTML content for tooltip 1303 | */ 1304 | function generateTooltipContent(element) { 1305 | const style = getComputedStyle(element); 1306 | const fontFamily = style.fontFamily; 1307 | const fontSize = style.fontSize; 1308 | const letterSpacing = style.letterSpacing; 1309 | const lineHeight = style.lineHeight; 1310 | const textAlign = style.textAlign; 1311 | const fontWeight = style.fontWeight; 1312 | 1313 | // Get color information 1314 | const colorInfo = getColorFromElement(element); 1315 | 1316 | // Fix SVG icon definitions 1317 | const copySvg = ``; 1318 | const checkSvg = ``; 1319 | 1320 | let content = ` 1321 |
Font family ${fontFamily}
1322 |
Font weight ${fontWeight}
1323 |
Font size ${fontSize}
1324 |
Letter Spacing ${letterSpacing}
1325 |
Line height ${lineHeight}
1326 |
Text alignment ${textAlign}
1327 | `; 1328 | 1329 | // Add color information 1330 | if (colorInfo) { 1331 | // Format LCH and HCL values as readable strings 1332 | const lchFormatted = `L: ${colorInfo.lch.l}, C: ${colorInfo.lch.c}, H: ${colorInfo.lch.h}`; 1333 | const hclFormatted = `H: ${colorInfo.hcl.h}, C: ${colorInfo.hcl.c}, L: ${colorInfo.hcl.l}`; 1334 | 1335 | content += ` 1336 |
Color 1337 | 1338 | ${colorInfo.hex} 1339 | 1340 | 1341 | ${copySvg} 1342 | 1343 |
1344 |
LCH 1345 | ${lchFormatted} 1346 | 1347 | ${copySvg} 1348 | 1349 |
1350 |
HCL 1351 | ${hclFormatted} 1352 | 1353 | ${copySvg} 1354 | 1355 |
1356 | `; 1357 | } 1358 | 1359 | return content; 1360 | } 1361 | 1362 | /** 1363 | * Update tooltip position only 1364 | * @param {Element} tooltip - The tooltip element 1365 | * @param {number} x - X position 1366 | * @param {number} y - Y position 1367 | */ 1368 | function updateTooltipPosition(tooltip, x, y) { 1369 | if (!tooltip) return; 1370 | 1371 | try { 1372 | // Directly set left and top instead of using transform 1373 | tooltip.style.left = `${x}px`; 1374 | tooltip.style.top = `${y}px`; 1375 | } catch (err) { 1376 | console.error('Error updating tooltip position:', err); 1377 | } 1378 | } 1379 | 1380 | /** 1381 | * Handle text selection event 1382 | * @param {Event} event - The mouse event 1383 | */ 1384 | function handleTextSelection(event) { 1385 | if (!isActive) { 1386 | console.log('Extension not active, ignoring text selection'); 1387 | return; 1388 | } 1389 | 1390 | try { 1391 | // Hide mini tooltip at the start of selection 1392 | if (miniTooltip && event.type === 'mousedown') { 1393 | miniTooltip.classList.remove('visible'); 1394 | miniTooltip.style.opacity = '0'; 1395 | isLongPress = false; // Reset long press state 1396 | 1397 | // Add mouse long press detection 1398 | const longPressTimeout = setTimeout(() => { 1399 | // If still pressed after 300ms, consider it a long press 1400 | isLongPress = true; 1401 | // Let the selection end logic handle showing the mini tooltip 1402 | }, 300); 1403 | 1404 | // If released before 300ms, clear long press detection 1405 | document.addEventListener('mouseup', function clearLongPress() { 1406 | if (!isLongPress) { 1407 | clearTimeout(longPressTimeout); 1408 | } 1409 | // Show mini tooltip when mouse is released 1410 | if (miniTooltip && !isCreatingFixedTooltip) { 1411 | setTimeout(() => { 1412 | miniTooltip.classList.add('visible'); 1413 | miniTooltip.style.opacity = '1'; 1414 | }, 100); 1415 | } 1416 | document.removeEventListener('mouseup', clearLongPress); 1417 | }, { once: true }); 1418 | 1419 | return; 1420 | } 1421 | 1422 | // If not a mouseup event, don't continue processing 1423 | if (event.type !== 'mouseup') { 1424 | return; 1425 | } 1426 | 1427 | // Hide mini tooltip 1428 | if (miniTooltip) { 1429 | miniTooltip.classList.remove('visible'); 1430 | miniTooltip.style.opacity = '0'; 1431 | } 1432 | 1433 | // Record that text selection event was triggered 1434 | console.log('Text selection event triggered ✨'); 1435 | 1436 | // Immediately record mouse position for more accurate positioning 1437 | if (event && 'clientX' in event && 'clientY' in event) { 1438 | lastMouseX = event.clientX; 1439 | lastMouseY = event.clientY; 1440 | console.log(`Recorded mouse position: x=${lastMouseX}, y=${lastMouseY}`); 1441 | } 1442 | 1443 | // Get selection object 1444 | const selection = window.getSelection(); 1445 | if (!selection) { 1446 | console.log('Unable to get selection object'); 1447 | // If no selection exists, show mini tooltip 1448 | if (miniTooltip) { 1449 | miniTooltip.classList.add('visible'); 1450 | miniTooltip.style.opacity = '1'; 1451 | } 1452 | return; 1453 | } 1454 | 1455 | // Don't process if selection is empty 1456 | const text = selection.toString().trim(); 1457 | if (!text) { 1458 | console.log('Selected text is empty'); 1459 | // If no text is selected, show mini tooltip 1460 | if (miniTooltip) { 1461 | miniTooltip.classList.add('visible'); 1462 | miniTooltip.style.opacity = '1'; 1463 | } 1464 | return; 1465 | } 1466 | 1467 | console.log(`Selected text: "${text.substring(0, 30)}${text.length > 30 ? '...' : ''}"`); 1468 | 1469 | // Temporarily block following tooltip while processing current selection 1470 | isCreatingFixedTooltip = true; 1471 | 1472 | // Avoid multiple processing for the same selection 1473 | if (selectionTimeout) { 1474 | clearTimeout(selectionTimeout); 1475 | } 1476 | 1477 | selectionTimeout = setTimeout(() => { 1478 | try { 1479 | // Check again if selection still exists 1480 | if (!selection || selection.rangeCount === 0) { 1481 | console.warn('Selection has disappeared'); 1482 | isCreatingFixedTooltip = false; // Reset flag 1483 | return; 1484 | } 1485 | 1486 | const range = selection.getRangeAt(0); 1487 | if (!range) { 1488 | console.warn('Unable to get selection range'); 1489 | isCreatingFixedTooltip = false; // Reset flag 1490 | return; 1491 | } 1492 | 1493 | // Get target element 1494 | let element = null; 1495 | 1496 | // Prioritize the common ancestor of the selection 1497 | if (range.commonAncestorContainer) { 1498 | element = range.commonAncestorContainer; 1499 | // If it's a text node, get its parent element 1500 | if (element.nodeType === Node.TEXT_NODE) { 1501 | element = element.parentElement; 1502 | } 1503 | } 1504 | // If no common ancestor, try using event target 1505 | else if (event && event.target) { 1506 | element = event.target; 1507 | // If it's a text node, get its parent element 1508 | if (element.nodeType === Node.TEXT_NODE) { 1509 | element = element.parentElement; 1510 | } 1511 | } 1512 | 1513 | // If still no element, don't create tooltip 1514 | if (!element || element.nodeType !== Node.ELEMENT_NODE) { 1515 | console.warn('Unable to get valid element for tooltip creation'); 1516 | isCreatingFixedTooltip = false; // Reset flag 1517 | return; 1518 | } 1519 | 1520 | console.log(`Preparing to create tooltip, element: ${element.tagName}`); 1521 | 1522 | // Use delay to ensure DOM is updated 1523 | setTimeout(() => { 1524 | try { 1525 | // Use original event or build event object 1526 | const tooltipEvent = event || { 1527 | target: element, 1528 | clientX: lastMouseX, 1529 | clientY: lastMouseY, 1530 | pageX: lastMouseX + window.pageXOffset, 1531 | pageY: lastMouseY + window.pageYOffset 1532 | }; 1533 | 1534 | // Directly call creation function 1535 | console.log('Creating new fixed tooltip'); 1536 | const tooltip = createFixedTooltip(tooltipEvent, element); 1537 | 1538 | // Validate result 1539 | if (tooltip) { 1540 | console.log('✅ Tooltip creation succeeded'); 1541 | 1542 | // Ensure tooltip remains visible 1543 | tooltip.style.display = 'block'; 1544 | tooltip.style.opacity = '1'; 1545 | tooltip.style.visibility = 'visible'; 1546 | 1547 | // Force update tooltip size and position to ensure content displays correctly 1548 | setTimeout(() => { 1549 | if (tooltip.isConnected) { 1550 | // Ensure fixed width is maintained 1551 | tooltip.style.width = '250px'; 1552 | } 1553 | 1554 | // Show mini tooltip immediately after tooltip creation 1555 | if (miniTooltip) { 1556 | miniTooltip.style.display = 'block'; 1557 | miniTooltip.classList.add('visible'); 1558 | miniTooltip.style.opacity = '1'; 1559 | } 1560 | }, 50); 1561 | } else { 1562 | console.warn('❌ Tooltip creation failed'); 1563 | // If creation fails, show mini tooltip 1564 | if (miniTooltip) { 1565 | miniTooltip.style.display = 'block'; 1566 | miniTooltip.classList.add('visible'); 1567 | miniTooltip.style.opacity = '1'; 1568 | } 1569 | } 1570 | 1571 | // Immediately reset creation flag 1572 | isCreatingFixedTooltip = false; 1573 | console.log('Creation process complete, resetting flag'); 1574 | 1575 | } catch (err) { 1576 | console.error('Error creating tooltip:', err); 1577 | isCreatingFixedTooltip = false; // Ensure flag is reset on error 1578 | // Show mini tooltip on error 1579 | if (miniTooltip) { 1580 | miniTooltip.style.display = 'block'; 1581 | miniTooltip.classList.add('visible'); 1582 | miniTooltip.style.opacity = '1'; 1583 | } 1584 | } 1585 | }, 10); 1586 | } catch (err) { 1587 | console.error('Error in selection delay callback:', err); 1588 | isCreatingFixedTooltip = false; // Ensure flag is reset on error 1589 | // Show mini tooltip on error 1590 | if (miniTooltip) { 1591 | miniTooltip.style.display = 'block'; 1592 | miniTooltip.classList.add('visible'); 1593 | miniTooltip.style.opacity = '1'; 1594 | } 1595 | } 1596 | }, 100); 1597 | 1598 | } catch (err) { 1599 | console.error('Error handling text selection:', err); 1600 | // Ensure mini tooltip is shown again 1601 | if (miniTooltip) { 1602 | miniTooltip.classList.add('visible'); 1603 | miniTooltip.style.opacity = '1'; 1604 | } 1605 | } 1606 | } 1607 | 1608 | /** 1609 | * Check if an element contains text or is a text-containing element 1610 | * @param {Element} element - The element to check 1611 | * @returns {boolean} - True if the element contains text 1612 | */ 1613 | function hasTextContent(element) { 1614 | // Check if element is empty 1615 | if (!element) { 1616 | debug('Element is empty', null); 1617 | return false; 1618 | } 1619 | 1620 | // Update non-text tag list - remove some tags that might contain text 1621 | const nonTextTags = [ 1622 | 'SCRIPT', 'STYLE', 'SVG', 'PATH', 'IMG', 'VIDEO', 'AUDIO', 'CANVAS', 'IFRAME', 1623 | 'OBJECT', 'EMBED', 'HR', 'BR', 'WBR', 'NOSCRIPT', 'INPUT', 'SELECT', 1624 | 'SOURCE', 'TRACK', 'META', 'LINK', 'BASE', 'PARAM' 1625 | ]; 1626 | 1627 | if (nonTextTags.includes(element.tagName)) { 1628 | debug('Non-text tag', element.tagName); 1629 | return false; 1630 | } 1631 | 1632 | // Get element text content 1633 | const rawText = element.textContent || ''; 1634 | const text = rawText.trim(); 1635 | 1636 | // Check element computed style 1637 | const style = getComputedStyle(element); 1638 | 1639 | // Check if element is hidden 1640 | if (style.display === 'none' || style.visibility === 'hidden' || parseFloat(style.opacity) === 0) { 1641 | debug('Hidden element', element.tagName); 1642 | return false; 1643 | } 1644 | 1645 | // Check if element is empty 1646 | if (!/\S/.test(rawText)) { 1647 | debug('Blank element', element.tagName); 1648 | return false; 1649 | } 1650 | 1651 | // Reduce element size requirement 1652 | const rect = element.getBoundingClientRect(); 1653 | if (rect.width < 5 || rect.height < 5) { 1654 | debug('Element too small', `${element.tagName} ${rect.width}x${rect.height}`); 1655 | return false; 1656 | } 1657 | 1658 | // Check direct text child nodes 1659 | let hasDirectTextNode = false; 1660 | let directTextLength = 0; 1661 | 1662 | for (let i = 0; i < element.childNodes.length; i++) { 1663 | const node = element.childNodes[i]; 1664 | if (node.nodeType === Node.TEXT_NODE) { 1665 | const nodeText = node.textContent.trim(); 1666 | if (nodeText.length > 0) { 1667 | hasDirectTextNode = true; 1668 | directTextLength += nodeText.length; 1669 | } 1670 | } 1671 | } 1672 | 1673 | // Reduce text length requirement 1674 | if (text.length < 1) { 1675 | debug('Text too short', `${element.tagName}: ${text}`); 1676 | return false; 1677 | } 1678 | 1679 | // Relax special character check 1680 | const punctuationOnlyPattern = /^[\s\.,;:!?()[\]{}'"\/\\-_+=<>|&$#@%^*]+$/; 1681 | if (punctuationOnlyPattern.test(text)) { 1682 | debug('Contains only special characters', `${element.tagName}: ${text}`); 1683 | return false; 1684 | } 1685 | 1686 | // Relax meaningful text content check 1687 | const meaningfulTextPattern = /[a-zA-Z0-9\u4e00-\u9fa5]/; 1688 | if (!meaningfulTextPattern.test(text)) { 1689 | debug('Does not contain meaningful text', `${element.tagName}: ${text}`); 1690 | return false; 1691 | } 1692 | 1693 | // Expand text element list 1694 | const textElements = ['P', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'BLOCKQUOTE', 'PRE', 'CODE', 'TD', 'TH', 'LI', 'DT', 'DD', 'SPAN', 'A', 'DIV']; 1695 | if (textElements.includes(element.tagName) && text.length > 0) { 1696 | debug('Text element', `${element.tagName}: ${text.length} characters`); 1697 | return true; 1698 | } 1699 | 1700 | // Check inline text elements 1701 | const inlineTextElements = ['STRONG', 'EM', 'B', 'I', 'U', 'SUP', 'SUB', 'MARK', 'SMALL', 'DEL', 'INS', 'Q', 'ABBR', 'CITE', 'DFN', 'LABEL']; 1702 | if (inlineTextElements.includes(element.tagName) && text.length > 0) { 1703 | debug('Inline text element', `${element.tagName}: ${text.length} characters`); 1704 | return true; 1705 | } 1706 | 1707 | // Special handling for DIV elements 1708 | if (element.tagName === 'DIV') { 1709 | // Reduce DIV text length requirement 1710 | if (directTextLength > 0) { 1711 | debug('Text-rich DIV', `Direct text length: ${directTextLength} characters`); 1712 | return true; 1713 | } 1714 | 1715 | // Check DIV style 1716 | if (style.fontFamily !== 'inherit' && text.length > 0) { 1717 | debug('Style similar to text container DIV', `${element.tagName}: ${text.length} characters`); 1718 | return true; 1719 | } 1720 | } 1721 | 1722 | return hasDirectTextNode && directTextLength > 0; 1723 | } 1724 | 1725 | /** 1726 | * Handle mousemove event using requestAnimationFrame 1727 | * @param {Event} event - The mouse event 1728 | */ 1729 | function handleMouseMove(event) { 1730 | if (!isActive || !miniTooltip) return; 1731 | 1732 | // If creating fixed tooltip or in long press, don't show mini tooltip 1733 | if (isCreatingFixedTooltip || isLongPress) { 1734 | miniTooltip.classList.remove('visible'); 1735 | miniTooltip.style.opacity = '0'; 1736 | return; 1737 | } 1738 | 1739 | // Update mouse position 1740 | lastMouseX = event.clientX; 1741 | lastMouseY = event.clientY; 1742 | 1743 | // Use requestAnimationFrame for throttling 1744 | if (animationFrameId) { 1745 | cancelAnimationFrame(animationFrameId); 1746 | } 1747 | 1748 | animationFrameId = requestAnimationFrame(() => { 1749 | try { 1750 | // Check window edges 1751 | const edgeThreshold = 15; 1752 | if (lastMouseX < edgeThreshold || lastMouseX > window.innerWidth - edgeThreshold || 1753 | lastMouseY < edgeThreshold || lastMouseY > window.innerHeight - edgeThreshold) { 1754 | miniTooltip.classList.remove('visible'); 1755 | miniTooltip.style.opacity = '0'; 1756 | return; 1757 | } 1758 | 1759 | // Update mini tooltip position 1760 | updateTooltipPosition(miniTooltip, lastMouseX + 15, lastMouseY + 15); 1761 | 1762 | // Ensure mini tooltip is visible (only when not in long press) 1763 | if (!isLongPress) { 1764 | miniTooltip.style.display = 'block'; 1765 | miniTooltip.classList.add('visible'); 1766 | miniTooltip.style.opacity = '1'; 1767 | } 1768 | 1769 | } catch (err) { 1770 | console.error('Error handling mouse movement:', err); 1771 | } finally { 1772 | animationFrameId = null; 1773 | } 1774 | }); 1775 | } 1776 | 1777 | /** 1778 | * Add mouse event listeners 1779 | */ 1780 | function addMouseListeners() { 1781 | document.addEventListener('mousemove', handleMouseMove); 1782 | } 1783 | 1784 | /** 1785 | * Remove mouse event listeners 1786 | */ 1787 | function removeMouseListeners() { 1788 | document.removeEventListener('mousemove', handleMouseMove); 1789 | } 1790 | 1791 | /** 1792 | * Handle keyboard events 1793 | * @param {Event} event - The keyboard event 1794 | */ 1795 | function handleKeyDown(event) { 1796 | if (!isActive) return; 1797 | 1798 | if (event.key === 'Escape') { 1799 | console.log('ESC key pressed, preparing to deactivate extension...'); 1800 | 1801 | // Hide floating tooltip 1802 | if (tooltip) { 1803 | hideTooltip(tooltip); 1804 | } 1805 | 1806 | // Hide mini tooltip 1807 | if (miniTooltip) { 1808 | miniTooltip.classList.remove('visible'); 1809 | miniTooltip.style.opacity = '0'; 1810 | setTimeout(() => { 1811 | miniTooltip.style.display = 'none'; 1812 | }, 200); 1813 | } 1814 | 1815 | // Deactivate extension functionality but preserve fixed tooltips 1816 | isActive = false; 1817 | 1818 | // Deactivate detector but preserve fixed tooltips 1819 | deinitializeDetector(true); // Pass true to preserve fixed tooltips 1820 | 1821 | // Clear current target element 1822 | currentTarget = null; 1823 | 1824 | // Notify background script to update icon state to inactive 1825 | chrome.runtime.sendMessage({ action: 'updateIcon', iconState: 'inactive' }); 1826 | 1827 | console.log('Extension deactivated via ESC key (fixed tooltips preserved)'); 1828 | } 1829 | } 1830 | 1831 | /** 1832 | * Add selection event listeners 1833 | */ 1834 | function addSelectionListener() { 1835 | console.log('Adding text selection listeners'); 1836 | document.addEventListener('mouseup', handleTextSelection); 1837 | document.addEventListener('mousedown', handleTextSelection); 1838 | document.addEventListener('selectionchange', function() { 1839 | // Record selection change event for more accurate tooltip positioning 1840 | const selection = window.getSelection(); 1841 | if (selection && selection.toString().trim().length > 0) { 1842 | // Selection content exists, prepare for possible mouseup event 1843 | console.log('Selection content detected, preparing for mouseup event'); 1844 | } 1845 | }); 1846 | } 1847 | 1848 | /** 1849 | * Remove selection event listeners 1850 | */ 1851 | function removeSelectionListener() { 1852 | console.log('Removing text selection listeners'); 1853 | document.removeEventListener('mouseup', handleTextSelection); 1854 | document.removeEventListener('mousedown', handleTextSelection); 1855 | document.removeEventListener('selectionchange', function() {}); 1856 | } 1857 | 1858 | // Set up message listener for extension communication 1859 | try { 1860 | chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { 1861 | try { 1862 | // Ensure request object exists 1863 | if (!request) { 1864 | console.warn('Received invalid message request'); 1865 | sendResponse({ success: false, error: 'Invalid request' }); 1866 | return true; 1867 | } 1868 | 1869 | // Check if extension running in a valid DOM context 1870 | if (!document || !document.documentElement) { 1871 | console.warn('Extension running in invalid DOM context, cannot process messages'); 1872 | sendResponse({ success: false, error: 'Invalid DOM context' }); 1873 | return true; 1874 | } 1875 | 1876 | // Check request action property 1877 | if (!request.action) { 1878 | console.warn('Message request missing action property'); 1879 | sendResponse({ success: false, error: 'Missing action property' }); 1880 | return true; 1881 | } 1882 | 1883 | if (request.action === TOGGLE_ACTION) { 1884 | toggleExtension(); 1885 | sendResponse({ success: true }); 1886 | } else if (request.action === 'checkContentScriptLoaded') { 1887 | sendResponse({ loaded: true }); 1888 | } else if (request.action === 'checkExtensionStatus') { 1889 | // Return the current activation state of the extension 1890 | sendResponse({ isActive: isActive }); 1891 | } else { 1892 | // Unknown action 1893 | console.warn(`Received unknown action: ${request.action}`); 1894 | sendResponse({ success: false, error: `Unknown action: ${request.action}` }); 1895 | } 1896 | } catch (err) { 1897 | console.error('Error in message handler:', err); 1898 | // Check if extension context is invalidated 1899 | if (err.message && err.message.includes("Extension context invalidated")) { 1900 | console.warn('Extension context was invalidated in message handler'); 1901 | cleanupResources(); 1902 | } 1903 | try { 1904 | sendResponse({ success: false, error: err.message }); 1905 | } catch (responseErr) { 1906 | console.error('Error sending response:', responseErr); 1907 | } 1908 | } 1909 | return true; // Keep message channel open 1910 | }); 1911 | } catch (err) { 1912 | console.error('Setting message listener failed:', err); 1913 | if (err.message && err.message.includes("Extension context invalidated")) { 1914 | cleanupResources(); 1915 | } 1916 | } 1917 | 1918 | // Add debug helper to global, convenient for turning on in console 1919 | window.fontDetectorDebug = false; 1920 | window.toggleFontDetectorDebug = function() { 1921 | window.fontDetectorDebug = !window.fontDetectorDebug; 1922 | console.log(`FontDetector debug mode ${window.fontDetectorDebug ? 'enabled' : 'disabled'}`); 1923 | return window.fontDetectorDebug; 1924 | }; 1925 | 1926 | /** 1927 | * Show tooltip at mouse position 1928 | * @param {Event} event - The mouse event 1929 | * @param {Element} tooltip - The tooltip element 1930 | */ 1931 | function showTooltip(event, tooltip) { 1932 | if (!event || !tooltip || !currentTarget) return; 1933 | 1934 | try { 1935 | // Update position 1936 | updateTooltipPosition(tooltip, event.clientX + 15, event.clientY + 15); 1937 | 1938 | // Use cache to check if content needs updating 1939 | const targetHash = currentTarget.outerHTML; 1940 | if (targetHash !== tooltip.dataset.lastTargetHash) { 1941 | const content = generateTooltipContent(currentTarget); 1942 | if (content && tooltip.innerHTML !== content) { 1943 | tooltip.innerHTML = content; 1944 | tooltip.dataset.lastTargetHash = targetHash; 1945 | } 1946 | } 1947 | 1948 | // Show tooltip 1949 | if (tooltip.style.display !== 'block') { 1950 | tooltip.style.display = 'block'; 1951 | requestAnimationFrame(() => { 1952 | tooltip.classList.add('visible'); 1953 | }); 1954 | } 1955 | } catch (err) { 1956 | console.error('Error showing tooltip:', err); 1957 | hideTooltip(tooltip); 1958 | } 1959 | } 1960 | 1961 | /** 1962 | * Hide tooltip 1963 | * @param {Element} tooltip - The tooltip element 1964 | */ 1965 | function hideTooltip(tooltip) { 1966 | if (!tooltip) return; 1967 | 1968 | try { 1969 | tooltip.classList.remove('visible'); 1970 | 1971 | // Wait for transition animation to complete before hiding 1972 | setTimeout(() => { 1973 | if (!tooltip.classList.contains('visible')) { 1974 | tooltip.style.display = 'none'; 1975 | // Don't clear content, keep cache 1976 | // tooltip.innerHTML = ''; 1977 | } 1978 | }, 200); 1979 | } catch (err) { 1980 | tooltip.style.display = 'none'; 1981 | } 1982 | } 1983 | 1984 | function createMiniTooltip() { 1985 | // Remove existing mini tooltip 1986 | const existingMiniTooltip = document.getElementById('miniTooltip'); 1987 | if (existingMiniTooltip) { 1988 | existingMiniTooltip.remove(); 1989 | } 1990 | 1991 | const miniTooltip = document.createElement('div'); 1992 | miniTooltip.setAttribute('id', 'miniTooltip'); 1993 | miniTooltip.textContent = 'Select to view font info'; 1994 | 1995 | // Set basic styles 1996 | miniTooltip.style.position = 'fixed'; 1997 | miniTooltip.style.display = 'block'; 1998 | miniTooltip.style.opacity = '0'; 1999 | miniTooltip.style.zIndex = '2147483647'; 2000 | miniTooltip.style.left = '0'; 2001 | miniTooltip.style.top = '0'; 2002 | miniTooltip.style.pointerEvents = 'none'; 2003 | 2004 | // Add to document 2005 | if (document.body) { 2006 | document.body.appendChild(miniTooltip); 2007 | // Use requestAnimationFrame to ensure styles are applied correctly 2008 | requestAnimationFrame(() => { 2009 | miniTooltip.style.opacity = '1'; 2010 | miniTooltip.classList.add('visible'); 2011 | 2012 | // Immediately update position to mouse position 2013 | if (lastMouseX && lastMouseY) { 2014 | updateTooltipPosition(miniTooltip, lastMouseX + 15, lastMouseY + 15); 2015 | } 2016 | }); 2017 | } 2018 | 2019 | return miniTooltip; 2020 | } 2021 | })(); --------------------------------------------------------------------------------