Initial version
This commit is contained in:
commit
eaaad96bb3
14 changed files with 9889 additions and 0 deletions
57
INSTALL.md
Normal file
57
INSTALL.md
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
# 🔵 Chrome Extension Installation Guide
|
||||
|
||||
## Quick Installation (Developer Mode)
|
||||
|
||||
1. **Open Chrome**
|
||||
2. **Navigate to**: `chrome://extensions/`
|
||||
3. **Enable**: "Developer mode" (toggle in top-right)
|
||||
4. **Click**: "Load unpacked"
|
||||
5. **Select**: This `chrome-extension` directory
|
||||
6. **Done!** Extension will appear in your toolbar
|
||||
|
||||
## 🚀 Usage
|
||||
|
||||
1. **Click** the extension icon in the toolbar
|
||||
2. **Or press** `Ctrl+Shift+S` (Cmd+Shift+S on Mac)
|
||||
3. **Hover** over any element on a webpage
|
||||
4. **Look for** red dotted borders showing scrollable content
|
||||
5. **Click** to capture the full element screenshot
|
||||
|
||||
## 📦 Chrome Web Store (Future)
|
||||
|
||||
To publish to Chrome Web Store:
|
||||
1. **ZIP** this entire directory
|
||||
2. **Upload** to [Chrome Developer Dashboard](https://chrome.google.com/webstore/devconsole)
|
||||
3. **Fill out** store listing details
|
||||
4. **Submit** for review
|
||||
|
||||
## ⚠️ Troubleshooting
|
||||
|
||||
- **Extension not loading**: Make sure to select the entire `chrome-extension` directory
|
||||
- **No screenshots**: Check that html2canvas.min.js is present
|
||||
- **Permissions error**: Make sure "Developer mode" is enabled
|
||||
- **Keyboard shortcut conflicts**: Check Chrome settings > Extensions > Keyboard shortcuts
|
||||
|
||||
## 📁 What's Included
|
||||
|
||||
```
|
||||
chrome-extension/
|
||||
├── manifest.json # Extension manifest (v3)
|
||||
├── content.js # Main functionality
|
||||
├── background.js # Service worker
|
||||
├── popup.html # Extension popup UI
|
||||
├── popup.js # Popup logic
|
||||
├── html2canvas.min.js # Screenshot library
|
||||
├── icons/ # Extension icons
|
||||
└── README.md # Full documentation
|
||||
```
|
||||
|
||||
## 🔄 Updates
|
||||
|
||||
To update the extension:
|
||||
1. **Modify** files in this directory
|
||||
2. **Go to** `chrome://extensions/`
|
||||
3. **Click** the refresh icon on your extension
|
||||
4. **Test** the changes
|
||||
|
||||
For complete documentation, see `README.md` in this directory.
|
||||
103
README.md
Normal file
103
README.md
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
# 📸 Full Screenshot Selector - Chrome Extension
|
||||
|
||||
A powerful Chrome extension that allows you to visually select and capture full screenshots of any element on a webpage, including hidden scrollable content.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- **Visual Element Selection**: Hover over any element and see it highlighted
|
||||
- **Full Content Capture**: Captures complete scrollable content, not just visible area
|
||||
- **Background Options**: Choose between black, transparent, or white backgrounds
|
||||
- **Keyboard Shortcut**: Quick access with `Ctrl+Shift+S` (or `Cmd+Shift+S` on Mac)
|
||||
- **Clean UI**: Non-intrusive interface with modern design
|
||||
- **Auto-Download**: Screenshots save automatically with timestamps
|
||||
|
||||
## 🚀 Installation
|
||||
|
||||
### Option 1: Developer Mode (Recommended for now)
|
||||
|
||||
1. **Download/Clone** this extension folder to your computer
|
||||
2. **Open Chrome** and go to `chrome://extensions/`
|
||||
3. **Enable Developer Mode** (toggle in top-right corner)
|
||||
4. **Click "Load unpacked"** and select the extension folder
|
||||
5. **Pin the extension** to your toolbar for easy access
|
||||
|
||||
### Option 2: From Chrome Web Store
|
||||
*Coming soon - will be published to Chrome Web Store*
|
||||
|
||||
## 🎯 How to Use
|
||||
|
||||
### Method 1: Extension Popup
|
||||
1. **Click the extension icon** in your toolbar
|
||||
2. **Choose background color** (Black/Transparent/White)
|
||||
3. **Click "Start Element Selector"**
|
||||
4. **Hover over elements** to see them highlighted
|
||||
5. **Click any element** to capture a full screenshot
|
||||
6. **Press ESC** or click the cancel button to exit
|
||||
|
||||
### Method 2: Keyboard Shortcut
|
||||
1. **Press `Ctrl+Shift+S`** (or `Cmd+Shift+S` on Mac)
|
||||
2. **Hover and click** elements to capture
|
||||
3. **Press ESC** to exit
|
||||
|
||||
## 🔧 Background Options
|
||||
|
||||
- **Black**: Solid black background (great for light content)
|
||||
- **Transparent**: No background (perfect for overlays)
|
||||
- **White**: Solid white background (ideal for dark content)
|
||||
|
||||
## 📱 Compatibility
|
||||
|
||||
- ✅ **Chrome 88+** (Manifest V3 support)
|
||||
- ✅ **All websites** (except Chrome internal pages)
|
||||
- ✅ **Cross-platform** (Windows, Mac, Linux)
|
||||
|
||||
## 🛠️ Technical Details
|
||||
|
||||
The extension uses several advanced techniques to ensure complete content capture:
|
||||
|
||||
- **Style Modification**: Temporarily expands elements to their full content height
|
||||
- **html2canvas-pro**: Uses the latest html2canvas library (included locally for CSP compliance)
|
||||
- **CSS Override**: Handles various overflow and height constraints
|
||||
- **Non-destructive**: Always restores original element styles
|
||||
|
||||
## 🔒 Privacy
|
||||
|
||||
- **No data collection**: Extension doesn't send any data to external servers
|
||||
- **Local processing**: All screenshot processing happens on your device
|
||||
- **No permissions abuse**: Only requests necessary permissions for functionality
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
**Screenshots not working on some sites?**
|
||||
- Some sites with strict Content Security Policy may block html2canvas
|
||||
- Try refreshing the page and trying again
|
||||
|
||||
**Element not highlighting properly?**
|
||||
- Some complex layouts may interfere with highlighting
|
||||
- The screenshot will still capture correctly
|
||||
|
||||
**Keyboard shortcut not working?**
|
||||
- Check Chrome's extension shortcuts: `chrome://extensions/shortcuts`
|
||||
- Make sure the shortcut isn't conflicting with other extensions
|
||||
|
||||
## 📄 File Structure
|
||||
|
||||
```
|
||||
chrome-extension/
|
||||
├── manifest.json # Extension configuration
|
||||
├── popup.html # Extension popup interface
|
||||
├── popup.js # Popup logic and communication
|
||||
├── content.js # Main screenshot functionality
|
||||
├── background.js # Service worker for shortcuts
|
||||
├── html2canvas.min.js # Screenshot library (local copy)
|
||||
├── icons/ # Extension icons (16, 32, 48, 128px)
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
Feel free to submit issues and enhancement requests!
|
||||
|
||||
## 📜 License
|
||||
|
||||
This project is open source and available under the MIT License.
|
||||
23
background.js
Normal file
23
background.js
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
// Background service worker for Full Screenshot Selector Chrome Extension
|
||||
|
||||
// Handle keyboard shortcut
|
||||
chrome.commands.onCommand.addListener(async (command) => {
|
||||
if (command === 'activate-selector') {
|
||||
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
||||
|
||||
if (tab && tab.url && !tab.url.startsWith('chrome://') && !tab.url.startsWith('chrome-extension://')) {
|
||||
try {
|
||||
await chrome.tabs.sendMessage(tab.id, { action: 'activateFromShortcut' });
|
||||
} catch (error) {
|
||||
console.log('Could not activate selector on this page:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle extension installation
|
||||
chrome.runtime.onInstalled.addListener((details) => {
|
||||
if (details.reason === 'install') {
|
||||
console.log('Full Screenshot Selector installed');
|
||||
}
|
||||
});
|
||||
575
content.js
Normal file
575
content.js
Normal file
|
|
@ -0,0 +1,575 @@
|
|||
// Content script for Full Screenshot Selector Chrome Extension
|
||||
class ScreenshotSelector {
|
||||
constructor() {
|
||||
this.isActive = false;
|
||||
this.currentHighlight = null;
|
||||
this.overlay = null;
|
||||
this.style = null;
|
||||
this.background = 'black';
|
||||
}
|
||||
|
||||
async init(background = 'black') {
|
||||
if (this.isActive) {
|
||||
console.log('Screenshot selector already active');
|
||||
return;
|
||||
}
|
||||
|
||||
this.background = background;
|
||||
this.isActive = true;
|
||||
|
||||
this.createUI();
|
||||
this.addEventListeners();
|
||||
}
|
||||
|
||||
createUI() {
|
||||
// Create overlay UI
|
||||
this.overlay = document.createElement('div');
|
||||
this.overlay.id = 'screenshot-selector-overlay';
|
||||
this.overlay.innerHTML = `
|
||||
<div style="position: fixed; top: 20px; left: 20px; background: rgba(0,0,0,0.9); color: white; padding: 12px 16px; border-radius: 8px; z-index: 2147483647; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 14px; box-shadow: 0 4px 20px rgba(0,0,0,0.3); backdrop-filter: blur(10px);">
|
||||
<div style="display: flex; align-items: center; gap: 10px;">
|
||||
<span>📸</span>
|
||||
<span>Hover over elements and click to capture</span>
|
||||
<button id="screenshot-cancel" style="margin-left: 10px; padding: 4px 8px; background: #ff4757; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px;">ESC</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Add hover highlighting styles
|
||||
this.style = document.createElement('style');
|
||||
this.style.textContent = `
|
||||
.screenshot-highlight {
|
||||
outline: 3px solid #00d2ff !important;
|
||||
outline-offset: 2px !important;
|
||||
cursor: crosshair !important;
|
||||
position: relative !important;
|
||||
}
|
||||
.screenshot-highlight::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
left: -5px;
|
||||
right: -5px;
|
||||
bottom: -5px;
|
||||
background: rgba(0, 210, 255, 0.1) !important;
|
||||
pointer-events: none !important;
|
||||
z-index: 2147483646 !important;
|
||||
}
|
||||
.screenshot-scrollable-indicator {
|
||||
position: absolute !important;
|
||||
border: 2px dotted #ff6b6b !important;
|
||||
background: rgba(255, 107, 107, 0.05) !important;
|
||||
pointer-events: none !important;
|
||||
z-index: 2147483645 !important;
|
||||
}
|
||||
#screenshot-selector-overlay * {
|
||||
cursor: default !important;
|
||||
}
|
||||
`;
|
||||
|
||||
document.head.appendChild(this.style);
|
||||
document.body.appendChild(this.overlay);
|
||||
|
||||
// Cancel button functionality
|
||||
document.getElementById('screenshot-cancel').onclick = () => this.cleanup();
|
||||
}
|
||||
|
||||
addEventListeners() {
|
||||
this.handleMouseOver = this.handleMouseOver.bind(this);
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
this.handleKeyPress = this.handleKeyPress.bind(this);
|
||||
|
||||
document.addEventListener('mouseover', this.handleMouseOver);
|
||||
document.addEventListener('click', this.handleClick, true);
|
||||
document.addEventListener('keydown', this.handleKeyPress);
|
||||
}
|
||||
|
||||
handleMouseOver(e) {
|
||||
if (e.target.closest('#screenshot-selector-overlay')) return;
|
||||
|
||||
// Clean up previous highlights and indicators
|
||||
if (this.currentHighlight) {
|
||||
this.currentHighlight.classList.remove('screenshot-highlight');
|
||||
this.removeScrollableIndicators();
|
||||
}
|
||||
|
||||
this.currentHighlight = e.target;
|
||||
e.target.classList.add('screenshot-highlight');
|
||||
|
||||
// Add scrollable content indicators
|
||||
this.addScrollableIndicators(e.target);
|
||||
}
|
||||
|
||||
handleClick(e) {
|
||||
if (e.target.closest('#screenshot-selector-overlay')) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const element = e.target;
|
||||
this.captureElement(element);
|
||||
}
|
||||
|
||||
handleKeyPress(e) {
|
||||
if (e.key === 'Escape') {
|
||||
this.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
addScrollableIndicators(element) {
|
||||
const rect = element.getBoundingClientRect();
|
||||
const hasVerticalScroll = element.scrollHeight > element.clientHeight;
|
||||
const hasHorizontalScroll = element.scrollWidth > element.clientWidth;
|
||||
|
||||
if (!hasVerticalScroll && !hasHorizontalScroll) return;
|
||||
|
||||
// Calculate the full content dimensions
|
||||
const fullHeight = element.scrollHeight;
|
||||
const fullWidth = element.scrollWidth;
|
||||
const visibleHeight = element.clientHeight;
|
||||
const visibleWidth = element.clientWidth;
|
||||
|
||||
// Create indicator for full scrollable area
|
||||
if (hasVerticalScroll || hasHorizontalScroll) {
|
||||
const indicator = document.createElement('div');
|
||||
indicator.className = 'screenshot-scrollable-indicator';
|
||||
indicator.setAttribute('data-screenshot-indicator', 'true');
|
||||
|
||||
// Position it relative to the viewport
|
||||
const style = {
|
||||
left: `${rect.left}px`,
|
||||
top: `${rect.top}px`,
|
||||
width: hasHorizontalScroll ? `${fullWidth}px` : `${rect.width}px`,
|
||||
height: hasVerticalScroll ? `${fullHeight}px` : `${rect.height}px`,
|
||||
};
|
||||
|
||||
Object.assign(indicator.style, style);
|
||||
document.body.appendChild(indicator);
|
||||
|
||||
// Add a label to show the full dimensions
|
||||
const label = document.createElement('div');
|
||||
label.setAttribute('data-screenshot-indicator', 'true');
|
||||
label.style.cssText = `
|
||||
position: fixed;
|
||||
top: ${rect.top - 25}px;
|
||||
left: ${rect.left}px;
|
||||
background: rgba(255, 107, 107, 0.9);
|
||||
color: white;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
font-family: monospace;
|
||||
z-index: 2147483647;
|
||||
pointer-events: none;
|
||||
`;
|
||||
|
||||
const scrollInfo = [];
|
||||
if (hasVerticalScroll) scrollInfo.push(`H: ${fullHeight}px (visible: ${visibleHeight}px)`);
|
||||
if (hasHorizontalScroll) scrollInfo.push(`W: ${fullWidth}px (visible: ${visibleWidth}px)`);
|
||||
label.textContent = `Full content - ${scrollInfo.join(', ')}`;
|
||||
|
||||
document.body.appendChild(label);
|
||||
}
|
||||
}
|
||||
|
||||
removeScrollableIndicators() {
|
||||
const indicators = document.querySelectorAll('[data-screenshot-indicator]');
|
||||
indicators.forEach(indicator => indicator.remove());
|
||||
}
|
||||
|
||||
// Helper method to generate a unique selector for an element
|
||||
getElementSelector(element) {
|
||||
// Escape helper for CSS identifiers (handles Tailwind classes like lg:pr-0)
|
||||
const escapeIdent = (ident) => {
|
||||
try {
|
||||
if (window.CSS && typeof window.CSS.escape === 'function') {
|
||||
return window.CSS.escape(ident);
|
||||
}
|
||||
} catch (_) {}
|
||||
// Fallback: escape most punctuation characters
|
||||
return String(ident).replace(/([ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g, '\\$1');
|
||||
};
|
||||
|
||||
if (element.id) {
|
||||
return `#${escapeIdent(element.id)}`;
|
||||
}
|
||||
|
||||
if (element.classList && element.classList.length) {
|
||||
const classSelector = Array.from(element.classList)
|
||||
.filter(Boolean)
|
||||
.map(cls => `.${escapeIdent(cls)}`)
|
||||
.join('');
|
||||
if (classSelector) {
|
||||
return `${element.tagName.toLowerCase()}${classSelector}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to tag name and position
|
||||
const siblings = Array.from(element.parentNode?.children || []);
|
||||
const index = siblings.indexOf(element);
|
||||
return `${element.tagName.toLowerCase()}:nth-child(${index + 1})`;
|
||||
}
|
||||
|
||||
// Preserve computed styles for better rendering
|
||||
preserveComputedStyles(clonedElement, originalElement) {
|
||||
if (!originalElement || !clonedElement) return;
|
||||
|
||||
const computedStyle = window.getComputedStyle(originalElement);
|
||||
|
||||
// Comprehensive style properties including layout and icon-related ones
|
||||
const importantStyles = [
|
||||
// Typography and fonts
|
||||
'font-family', 'font-size', 'font-weight', 'font-style', 'font-variant',
|
||||
'text-align', 'text-decoration', 'text-transform', 'text-indent',
|
||||
'line-height', 'letter-spacing', 'word-spacing', 'white-space',
|
||||
|
||||
// Colors and backgrounds
|
||||
'color', 'background-color', 'background-image', 'background-size',
|
||||
'background-position', 'background-repeat', 'background-attachment',
|
||||
|
||||
// Layout and positioning
|
||||
'display', 'position', 'top', 'left', 'right', 'bottom',
|
||||
'width', 'height', 'min-width', 'min-height', 'max-width', 'max-height',
|
||||
'margin', 'padding', 'border', 'border-radius',
|
||||
'float', 'clear', 'vertical-align',
|
||||
|
||||
// Flexbox and Grid
|
||||
'flex', 'flex-direction', 'flex-wrap', 'flex-basis', 'flex-grow', 'flex-shrink',
|
||||
'justify-content', 'align-items', 'align-self', 'align-content',
|
||||
'grid', 'grid-template', 'grid-area', 'grid-column', 'grid-row',
|
||||
|
||||
// Visual effects
|
||||
'opacity', 'visibility', 'overflow', 'overflow-x', 'overflow-y',
|
||||
'box-shadow', 'text-shadow', 'transform', 'filter',
|
||||
'z-index', 'cursor'
|
||||
];
|
||||
|
||||
// Apply computed styles
|
||||
importantStyles.forEach(property => {
|
||||
const value = computedStyle.getPropertyValue(property);
|
||||
if (value && value !== 'none' && value !== 'normal' && value !== 'auto' && value !== 'initial') {
|
||||
try {
|
||||
clonedElement.style.setProperty(property, value, 'important');
|
||||
} catch (e) {
|
||||
// Skip properties that can't be set
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle pseudo-elements (::before and ::after) which often contain icons
|
||||
this.preservePseudoElements(clonedElement, originalElement);
|
||||
|
||||
// Recursively apply to children
|
||||
const originalChildren = Array.from(originalElement.children);
|
||||
const clonedChildren = Array.from(clonedElement.children);
|
||||
|
||||
for (let i = 0; i < Math.min(originalChildren.length, clonedChildren.length); i++) {
|
||||
this.preserveComputedStyles(clonedChildren[i], originalChildren[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle pseudo-elements that often contain icons
|
||||
preservePseudoElements(clonedElement, originalElement) {
|
||||
try {
|
||||
['::before', '::after'].forEach(pseudo => {
|
||||
const pseudoStyle = window.getComputedStyle(originalElement, pseudo);
|
||||
const content = pseudoStyle.getPropertyValue('content');
|
||||
|
||||
// If pseudo-element has content, try to preserve it
|
||||
if (content && content !== 'none' && content !== '""') {
|
||||
const pseudoProperties = [
|
||||
'content', 'display', 'position', 'top', 'left', 'right', 'bottom',
|
||||
'width', 'height', 'font-family', 'font-size', 'color',
|
||||
'background-color', 'background-image', 'border', 'border-radius',
|
||||
'transform', 'opacity'
|
||||
];
|
||||
|
||||
// Create inline style for pseudo-element
|
||||
let selector = this.getElementSelector(clonedElement);
|
||||
let pseudoCSS = `${selector}${pseudo} {`;
|
||||
pseudoProperties.forEach(prop => {
|
||||
const value = pseudoStyle.getPropertyValue(prop);
|
||||
if (value && value !== 'none' && value !== 'normal') {
|
||||
pseudoCSS += `${prop}: ${value} !important;`;
|
||||
}
|
||||
});
|
||||
pseudoCSS += '}';
|
||||
|
||||
// Add to document head
|
||||
const style = document.createElement('style');
|
||||
style.textContent = pseudoCSS;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
// Pseudo-element handling failed, continue without it
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for fonts and resources to load properly
|
||||
async waitForFontsAndResources(element) {
|
||||
// Wait for document fonts to load
|
||||
if (document.fonts && document.fonts.ready) {
|
||||
try {
|
||||
await document.fonts.ready;
|
||||
} catch (e) {
|
||||
// Font loading API not available or failed
|
||||
}
|
||||
}
|
||||
|
||||
// Check for icon fonts (Font Awesome, Material Icons, etc.)
|
||||
const iconFontFamilies = [
|
||||
'FontAwesome', 'Font Awesome', 'Font Awesome 5', 'Font Awesome 6',
|
||||
'Material Icons', 'Material Icons Outlined', 'Material Icons Sharp',
|
||||
'Ionicons', 'Feather', 'Lucide', 'Tabler Icons'
|
||||
];
|
||||
|
||||
// Force load any icon fonts found in the element
|
||||
const allElements = [element, ...element.querySelectorAll('*')];
|
||||
const fontPromises = [];
|
||||
|
||||
allElements.forEach(el => {
|
||||
const computedStyle = window.getComputedStyle(el);
|
||||
const fontFamily = computedStyle.fontFamily;
|
||||
|
||||
iconFontFamilies.forEach(iconFont => {
|
||||
if (fontFamily.includes(iconFont)) {
|
||||
// Create a test element to ensure font is loaded
|
||||
const testEl = document.createElement('span');
|
||||
testEl.style.fontFamily = iconFont;
|
||||
testEl.style.position = 'absolute';
|
||||
testEl.style.left = '-9999px';
|
||||
testEl.textContent = '■'; // Use a test character
|
||||
document.body.appendChild(testEl);
|
||||
|
||||
const fontPromise = new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(testEl);
|
||||
resolve();
|
||||
}, 100);
|
||||
});
|
||||
fontPromises.push(fontPromise);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Wait for all font loading attempts
|
||||
await Promise.all(fontPromises);
|
||||
|
||||
// Additional wait for any remaining resources
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
}
|
||||
|
||||
|
||||
|
||||
async captureElement(element) {
|
||||
try {
|
||||
// Show loading state
|
||||
this.overlay.innerHTML = `
|
||||
<div style="position: fixed; top: 20px; left: 20px; background: rgba(0,0,0,0.9); color: white; padding: 12px 16px; border-radius: 8px; z-index: 2147483647; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 14px; box-shadow: 0 4px 20px rgba(0,0,0,0.3);">
|
||||
<div style="display: flex; align-items: center; gap: 10px;">
|
||||
<div style="width: 16px; height: 16px; border: 2px solid #fff; border-top: 2px solid transparent; border-radius: 50%; animation: spin 1s linear infinite;"></div>
|
||||
<span>Capturing screenshot...</span>
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
|
||||
// Remove highlight for clean capture first
|
||||
element.classList.remove('screenshot-highlight');
|
||||
this.removeScrollableIndicators();
|
||||
|
||||
// Store original styles
|
||||
const originalStyles = {
|
||||
overflow: element.style.overflow,
|
||||
overflowY: element.style.overflowY,
|
||||
overflowX: element.style.overflowX,
|
||||
height: element.style.height,
|
||||
maxHeight: element.style.maxHeight,
|
||||
position: element.style.position,
|
||||
zIndex: element.style.zIndex,
|
||||
};
|
||||
|
||||
// Temporarily modify element for full capture
|
||||
element.style.overflow = 'visible';
|
||||
element.style.overflowY = 'visible';
|
||||
element.style.overflowX = 'visible';
|
||||
element.style.height = `${element.scrollHeight}px`;
|
||||
element.style.maxHeight = 'none';
|
||||
|
||||
// Wait for fonts and external resources to load
|
||||
await this.waitForFontsAndResources(element);
|
||||
|
||||
// Enhanced html2canvas configuration
|
||||
const canvas = await html2canvas(element, {
|
||||
useCORS: true,
|
||||
allowTaint: false,
|
||||
backgroundColor: this.background === 'transparent' ? null :
|
||||
this.background === 'white' ? '#ffffff' : '#000000',
|
||||
|
||||
// Improved rendering options
|
||||
scale: window.devicePixelRatio || 1, // Use device pixel ratio for crisp images
|
||||
logging: false, // Disable logging for cleaner console
|
||||
|
||||
// Better handling of external resources
|
||||
imageTimeout: 15000, // Wait longer for images to load
|
||||
|
||||
// Font handling and style preservation
|
||||
onclone: (clonedDoc, clonedElementParam) => {
|
||||
// Ensure all stylesheets are loaded in cloned document
|
||||
const originalStyleSheets = Array.from(document.styleSheets);
|
||||
const clonedHead = clonedDoc.head;
|
||||
|
||||
// Copy all stylesheets to cloned document
|
||||
originalStyleSheets.forEach(styleSheet => {
|
||||
try {
|
||||
if (styleSheet.href) {
|
||||
// External stylesheet
|
||||
const link = clonedDoc.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = styleSheet.href;
|
||||
link.type = 'text/css';
|
||||
clonedHead.appendChild(link);
|
||||
} else if (styleSheet.ownerNode && styleSheet.ownerNode.tagName === 'STYLE') {
|
||||
// Inline stylesheet
|
||||
const style = clonedDoc.createElement('style');
|
||||
style.type = 'text/css';
|
||||
try {
|
||||
const cssText = Array.from(styleSheet.cssRules).map(rule => rule.cssText).join('\n');
|
||||
style.textContent = cssText;
|
||||
} catch (e) {
|
||||
// Fallback to original text content
|
||||
style.textContent = styleSheet.ownerNode.textContent;
|
||||
}
|
||||
clonedHead.appendChild(style);
|
||||
}
|
||||
} catch (e) {
|
||||
// Skip stylesheets that can't be accessed (CORS issues)
|
||||
console.log('Skipped stylesheet due to CORS:', e);
|
||||
}
|
||||
});
|
||||
|
||||
// Apply computed styles to preserve appearance
|
||||
const clonedElement = clonedElementParam;
|
||||
const originalElement = element; // from outer scope
|
||||
if (clonedElement && originalElement) {
|
||||
this.preserveComputedStyles(clonedElement, originalElement);
|
||||
}
|
||||
|
||||
return clonedDoc;
|
||||
}
|
||||
});
|
||||
|
||||
// Restore original styles
|
||||
Object.assign(element.style, originalStyles);
|
||||
|
||||
// Download image
|
||||
const link = document.createElement('a');
|
||||
link.href = canvas.toDataURL('image/png');
|
||||
link.download = `screenshot-${new Date().toISOString().slice(0, 19).replace(/[:.]/g, '-')}.png`;
|
||||
link.click();
|
||||
|
||||
// Success message
|
||||
this.overlay.innerHTML = `
|
||||
<div style="position: fixed; top: 20px; left: 20px; background: rgba(39, 174, 96, 0.95); color: white; padding: 12px 16px; border-radius: 8px; z-index: 2147483647; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 14px; box-shadow: 0 4px 20px rgba(0,0,0,0.3);">
|
||||
<div style="display: flex; align-items: center; gap: 10px;">
|
||||
<span>✅</span>
|
||||
<span>Screenshot saved successfully!</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Notify popup
|
||||
chrome.runtime.sendMessage({ action: 'selectorStopped', reason: 'screenshot-taken' });
|
||||
|
||||
setTimeout(() => this.cleanup(), 2000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Screenshot failed:', error);
|
||||
this.overlay.innerHTML = `
|
||||
<div style="position: fixed; top: 20px; left: 20px; background: rgba(231, 76, 60, 0.95); color: white; padding: 12px 16px; border-radius: 8px; z-index: 2147483647; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 14px; box-shadow: 0 4px 20px rgba(0,0,0,0.3);">
|
||||
<div style="display: flex; align-items: center; gap: 10px;">
|
||||
<span>❌</span>
|
||||
<span>Screenshot failed. Please try again.</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
setTimeout(() => this.cleanup(), 3000);
|
||||
}
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
document.removeEventListener('mouseover', this.handleMouseOver);
|
||||
document.removeEventListener('click', this.handleClick, true);
|
||||
document.removeEventListener('keydown', this.handleKeyPress);
|
||||
|
||||
if (this.currentHighlight) {
|
||||
this.currentHighlight.classList.remove('screenshot-highlight');
|
||||
}
|
||||
|
||||
// Remove any scrollable indicators
|
||||
this.removeScrollableIndicators();
|
||||
|
||||
if (this.overlay) {
|
||||
this.overlay.remove();
|
||||
this.overlay = null;
|
||||
}
|
||||
|
||||
if (this.style) {
|
||||
this.style.remove();
|
||||
this.style = null;
|
||||
}
|
||||
|
||||
this.isActive = false;
|
||||
this.currentHighlight = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Global instance
|
||||
let screenshotSelector = new ScreenshotSelector();
|
||||
|
||||
// Add load indicator
|
||||
console.log('Full Screenshot Selector content script loaded');
|
||||
|
||||
// Listen for messages from popup
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
try {
|
||||
switch (message.action) {
|
||||
case 'startSelector':
|
||||
screenshotSelector.init(message.background);
|
||||
sendResponse({ success: true });
|
||||
break;
|
||||
|
||||
case 'stopSelector':
|
||||
screenshotSelector.cleanup();
|
||||
sendResponse({ success: true });
|
||||
break;
|
||||
|
||||
case 'checkState':
|
||||
sendResponse({ isActive: screenshotSelector.isActive });
|
||||
break;
|
||||
|
||||
default:
|
||||
sendResponse({ error: 'Unknown action: ' + message.action });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Content script error:', error);
|
||||
sendResponse({ error: error.message });
|
||||
}
|
||||
|
||||
// Important: return true to indicate async response
|
||||
return true;
|
||||
});
|
||||
|
||||
// Handle keyboard shortcut
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
if (message.action === 'activateFromShortcut') {
|
||||
if (!screenshotSelector.isActive) {
|
||||
screenshotSelector.init('black'); // Default background for shortcut
|
||||
}
|
||||
}
|
||||
});
|
||||
8796
html2canvas-pro.js
Normal file
8796
html2canvas-pro.js
Normal file
File diff suppressed because one or more lines are too long
7
html2canvas-pro.min.js
vendored
Normal file
7
html2canvas-pro.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
html2canvas-pro.min.js.map
Normal file
1
html2canvas-pro.min.js.map
Normal file
File diff suppressed because one or more lines are too long
BIN
icons/icon128.png
Normal file
BIN
icons/icon128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.6 KiB |
BIN
icons/icon16.png
Normal file
BIN
icons/icon16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 625 B |
BIN
icons/icon32.png
Normal file
BIN
icons/icon32.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1 KiB |
BIN
icons/icon48.png
Normal file
BIN
icons/icon48.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
51
manifest.json
Normal file
51
manifest.json
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
{
|
||||
"manifest_version": 3,
|
||||
"name": "Full Screenshot Selector",
|
||||
"version": "1.0",
|
||||
"description": "Visually select and capture full screenshots of any element, including hidden scrollable content",
|
||||
|
||||
"permissions": [
|
||||
"activeTab",
|
||||
"storage"
|
||||
],
|
||||
|
||||
"action": {
|
||||
"default_popup": "popup.html",
|
||||
"default_title": "Full Screenshot Selector",
|
||||
"default_icon": {
|
||||
"16": "icons/icon16.png",
|
||||
"32": "icons/icon32.png",
|
||||
"48": "icons/icon48.png",
|
||||
"128": "icons/icon128.png"
|
||||
}
|
||||
},
|
||||
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["<all_urls>"],
|
||||
"js": ["html2canvas-pro.min.js", "content.js"],
|
||||
"run_at": "document_idle"
|
||||
}
|
||||
],
|
||||
|
||||
"background": {
|
||||
"service_worker": "background.js"
|
||||
},
|
||||
|
||||
"commands": {
|
||||
"activate-selector": {
|
||||
"suggested_key": {
|
||||
"default": "Ctrl+Shift+S",
|
||||
"mac": "Command+Shift+S"
|
||||
},
|
||||
"description": "Activate screenshot selector"
|
||||
}
|
||||
},
|
||||
|
||||
"icons": {
|
||||
"16": "icons/icon16.png",
|
||||
"32": "icons/icon32.png",
|
||||
"48": "icons/icon48.png",
|
||||
"128": "icons/icon128.png"
|
||||
}
|
||||
}
|
||||
143
popup.html
Normal file
143
popup.html
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body {
|
||||
width: 300px;
|
||||
padding: 20px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
margin: 0;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 18px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.header p {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.setting-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.setting-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
select {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
background: white;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #545b62;
|
||||
}
|
||||
|
||||
.shortcut-info {
|
||||
font-size: 11px;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
margin-top: 10px;
|
||||
padding: 8px;
|
||||
background: #e9ecef;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.status {
|
||||
text-align: center;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
margin: 10px 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.status.success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.status.error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>📸 Screenshot Selector</h1>
|
||||
<p>Capture full element screenshots</p>
|
||||
</div>
|
||||
|
||||
<div class="setting-group">
|
||||
<label for="background-select">Background Color:</label>
|
||||
<select id="background-select">
|
||||
<option value="black">Black</option>
|
||||
<option value="transparent">Transparent</option>
|
||||
<option value="white">White</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button id="start-selector" class="btn btn-primary">
|
||||
Start Element Selector
|
||||
</button>
|
||||
|
||||
<button id="stop-selector" class="btn btn-secondary" style="display: none;">
|
||||
Stop Selector
|
||||
</button>
|
||||
|
||||
<div id="status" class="status" style="display: none;"></div>
|
||||
|
||||
<div class="shortcut-info">
|
||||
💡 Tip: Use <strong>Ctrl+Shift+S</strong> (or Cmd+Shift+S on Mac) to quickly activate
|
||||
</div>
|
||||
|
||||
<script src="popup.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
133
popup.js
Normal file
133
popup.js
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
// Popup script for Chrome extension
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
const backgroundSelect = document.getElementById('background-select');
|
||||
const startBtn = document.getElementById('start-selector');
|
||||
const stopBtn = document.getElementById('stop-selector');
|
||||
const status = document.getElementById('status');
|
||||
|
||||
// Load saved background preference
|
||||
const saved = await chrome.storage.sync.get(['backgroundPreference']);
|
||||
if (saved.backgroundPreference) {
|
||||
backgroundSelect.value = saved.backgroundPreference;
|
||||
}
|
||||
|
||||
// Save background preference when changed
|
||||
backgroundSelect.addEventListener('change', () => {
|
||||
chrome.storage.sync.set({ backgroundPreference: backgroundSelect.value });
|
||||
});
|
||||
|
||||
// Start selector
|
||||
startBtn.addEventListener('click', async () => {
|
||||
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
||||
|
||||
// Check if page is compatible
|
||||
if (tab.url.startsWith('chrome://') ||
|
||||
tab.url.startsWith('chrome-extension://') ||
|
||||
tab.url.startsWith('edge://') ||
|
||||
tab.url.startsWith('about:') ||
|
||||
tab.url === 'chrome://newtab/' ||
|
||||
tab.url === 'edge://newtab/') {
|
||||
showStatus('Error: Cannot capture screenshots on this page type.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await chrome.tabs.sendMessage(tab.id, {
|
||||
action: 'startSelector',
|
||||
background: backgroundSelect.value
|
||||
});
|
||||
|
||||
showStatus('Selector activated! Hover over elements and click to capture.', 'success');
|
||||
toggleButtons(true);
|
||||
|
||||
// Auto-close popup after starting
|
||||
setTimeout(() => window.close(), 1500);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Full error details:', error);
|
||||
if (error.message.includes('Could not establish connection')) {
|
||||
showStatus('Error: Content script not loaded. Try refreshing the page.', 'error');
|
||||
} else {
|
||||
showStatus('Error: Could not activate on this page.', 'error');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Stop selector
|
||||
stopBtn.addEventListener('click', async () => {
|
||||
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
||||
|
||||
try {
|
||||
await chrome.tabs.sendMessage(tab.id, { action: 'stopSelector' });
|
||||
showStatus('Selector stopped.', 'success');
|
||||
toggleButtons(false);
|
||||
} catch (error) {
|
||||
console.error('Error stopping selector:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// Check current state
|
||||
checkSelectorState();
|
||||
|
||||
function toggleButtons(isActive) {
|
||||
if (isActive) {
|
||||
startBtn.style.display = 'none';
|
||||
stopBtn.style.display = 'block';
|
||||
} else {
|
||||
startBtn.style.display = 'block';
|
||||
stopBtn.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function showStatus(message, type = 'success') {
|
||||
status.textContent = message;
|
||||
status.className = `status ${type}`;
|
||||
status.style.display = 'block';
|
||||
|
||||
setTimeout(() => {
|
||||
status.style.display = 'none';
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
async function checkSelectorState() {
|
||||
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
||||
|
||||
// Skip check for incompatible pages
|
||||
if (tab.url.startsWith('chrome://') ||
|
||||
tab.url.startsWith('chrome-extension://') ||
|
||||
tab.url.startsWith('edge://') ||
|
||||
tab.url.startsWith('about:')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await chrome.tabs.sendMessage(tab.id, { action: 'checkState' });
|
||||
if (response && response.isActive) {
|
||||
toggleButtons(true);
|
||||
showStatus('Selector is currently active', 'success');
|
||||
}
|
||||
} catch (error) {
|
||||
// Content script not ready or selector not active
|
||||
console.log('Content script not ready or selector inactive:', error.message);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for messages from content script
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
if (message.action === 'selectorStopped') {
|
||||
const startBtn = document.getElementById('start-selector');
|
||||
const stopBtn = document.getElementById('stop-selector');
|
||||
const status = document.getElementById('status');
|
||||
|
||||
startBtn.style.display = 'block';
|
||||
stopBtn.style.display = 'none';
|
||||
|
||||
if (message.reason === 'screenshot-taken') {
|
||||
status.textContent = 'Screenshot captured!';
|
||||
status.className = 'status success';
|
||||
status.style.display = 'block';
|
||||
setTimeout(() => status.style.display = 'none', 2000);
|
||||
}
|
||||
}
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue