Initial version

This commit is contained in:
Joey Yakimowich-Payne 2025-08-09 12:15:58 -06:00
commit eaaad96bb3
14 changed files with 9889 additions and 0 deletions

57
INSTALL.md Normal file
View 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
View 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
View 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
View 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

File diff suppressed because one or more lines are too long

7
html2canvas-pro.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

BIN
icons/icon128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

BIN
icons/icon16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 625 B

BIN
icons/icon32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

BIN
icons/icon48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

51
manifest.json Normal file
View 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
View 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
View 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);
}
}
});