Compare commits

..

1 commit

Author SHA1 Message Date
Karthikeyan N
a018c36d67
Add copy to clipboard and save to pc toggle.
* escaped node modules from vcs

* feat: added copy to clipboard feature

* feat: added tests

* feat: added feature spec

* feat: added toggle for save to pc

* feat: added tests

* refactor: change toggle from '?' button to the 'switch' component

* Fix failing tests

* Update gitignore and remove unnecessary files

* Remove tooltips because they are unnecessary

---------

Co-authored-by: Joey Yakimowich-Payne <jyapayne@gmail.com>
2025-08-13 13:29:57 -06:00
8 changed files with 197 additions and 1557 deletions

1
.gitignore vendored
View file

@ -1,4 +1,3 @@
node_modules/ node_modules/
.kiro/ .kiro/
html-to-image/
.vscode/ .vscode/

View file

@ -411,42 +411,90 @@ class ScreenshotSelector {
element.classList.remove('screenshot-highlight'); element.classList.remove('screenshot-highlight');
this.removeScrollableIndicators(); 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 // Wait for fonts and external resources to load
await this.waitForFontsAndResources(element); await this.waitForFontsAndResources(element);
if (!window.htmlToImage || typeof window.htmlToImage.toCanvas !== 'function') { // Enhanced html2canvas configuration
throw new Error('Screenshot renderer not available: htmlToImage.toCanvas() missing'); const canvas = await html2canvas(element, {
} useCORS: true,
allowTaint: false,
backgroundColor: this.background === 'transparent' ? null :
this.background === 'white' ? '#ffffff' : '#000000',
const backgroundColor = // Improved rendering options
this.background === 'transparent' scale: window.devicePixelRatio || 1, // Use device pixel ratio for crisp images
? undefined logging: false, // Disable logging for cleaner console
: this.background === 'white'
? '#ffffff'
: '#000000';
// Use html-to-image to render the element to a canvas. // Better handling of external resources
// Important: avoid mutating the live DOM during capture (it can cause reflow/layout drift). imageTimeout: 15000, // Wait longer for images to load
const canvas = await window.htmlToImage.toCanvas(element, {
backgroundColor, // Font handling and style preservation
pixelRatio: window.devicePixelRatio || 1, onclone: (clonedDoc, clonedElementParam) => {
// Brightspace/D2L and many sites have CORS-protected stylesheets/fonts which cause noisy, // Ensure all stylesheets are loaded in cloned document
// expected DOMExceptions during CSS rule inspection. Keep logs clean by default. const originalStyleSheets = Array.from(document.styleSheets);
logLevel: 'silent', const clonedHead = clonedDoc.head;
// If the page heavily caches fonts/images, enabling this can help but may slow capture
cacheBust: false, // Copy all stylesheets to cloned document
// Capture full scrollable area without reflowing the live element originalStyleSheets.forEach(styleSheet => {
width: element.scrollWidth, try {
height: element.scrollHeight, if (styleSheet.href) {
style: { // External stylesheet
overflow: 'visible', const link = clonedDoc.createElement('link');
overflowX: 'visible', link.rel = 'stylesheet';
overflowY: 'visible', link.href = styleSheet.href;
height: `${element.scrollHeight}px`, link.type = 'text/css';
maxHeight: 'none', 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);
// Conditionally download the image based on saveToPc preference // Conditionally download the image based on saveToPc preference
if (this.saveToPc) { if (this.saveToPc) {
const link = document.createElement('a'); const link = document.createElement('a');
@ -549,7 +597,7 @@ class ScreenshotSelector {
} }
if (error.message) { if (error.message) {
if (error.message.includes('htmlToImage') || error.message.includes('html-to-image')) { if (error.message.includes('html2canvas')) {
errorMessage = `Screenshot rendering failed. Try selecting a different element or refresh the page.${operationContext}`; errorMessage = `Screenshot rendering failed. Try selecting a different element or refresh the page.${operationContext}`;
} else if (error.message.includes('timeout')) { } else if (error.message.includes('timeout')) {
errorMessage = `Screenshot capture timed out. Try selecting a smaller area or simpler element.${operationContext}`; errorMessage = `Screenshot capture timed out. Try selecting a smaller area or simpler element.${operationContext}`;

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -24,7 +24,7 @@
"content_scripts": [ "content_scripts": [
{ {
"matches": ["<all_urls>"], "matches": ["<all_urls>"],
"js": ["html-to-image.js", "content.js"], "js": ["html2canvas-pro.min.js", "content.js"],
"run_at": "document_idle" "run_at": "document_idle"
} }
], ],

View file

@ -53,10 +53,8 @@ global.window = {
}) })
}; };
// Mock html-to-image // Mock html2canvas
global.htmlToImage = { global.html2canvas = jest.fn();
toCanvas: jest.fn(),
};
describe('Integration Scenarios - Clipboard with Different Backgrounds', () => { describe('Integration Scenarios - Clipboard with Different Backgrounds', () => {
let screenshotSelector; let screenshotSelector;
@ -104,7 +102,7 @@ describe('Integration Scenarios - Clipboard with Different Backgrounds', () => {
toDataURL: jest.fn(() => 'data:image/png;base64,mockdata') toDataURL: jest.fn(() => 'data:image/png;base64,mockdata')
}; };
global.htmlToImage.toCanvas = jest.fn().mockResolvedValue(mockCanvas); global.html2canvas = jest.fn().mockResolvedValue(mockCanvas);
@ -185,17 +183,11 @@ describe('Integration Scenarios - Clipboard with Different Backgrounds', () => {
async captureElement(element) { async captureElement(element) {
try { try {
// Generate canvas with background-specific settings // Generate canvas with background-specific settings
const backgroundColor = const canvas = await global.html2canvas(element, {
this.background === 'transparent' backgroundColor: this.background === 'transparent' ? null :
? undefined this.background === 'white' ? '#ffffff' : '#000000',
: this.background === 'white' useCORS: true,
? '#ffffff' allowTaint: false
: '#000000';
const canvas = await global.htmlToImage.toCanvas(element, {
backgroundColor,
pixelRatio: global.window.devicePixelRatio || 1,
cacheBust: false
}); });
// Always download first // Always download first
@ -269,8 +261,10 @@ describe('Integration Scenarios - Clipboard with Different Backgrounds', () => {
const result = await screenshotSelector.captureElement(mockElement); const result = await screenshotSelector.captureElement(mockElement);
expect(global.htmlToImage.toCanvas).toHaveBeenCalledWith(mockElement, expect.objectContaining({ expect(global.html2canvas).toHaveBeenCalledWith(mockElement, expect.objectContaining({
backgroundColor: undefined, // Transparent background backgroundColor: null, // Transparent background
useCORS: true,
allowTaint: false
})); }));
expect(result.success).toBe(true); expect(result.success).toBe(true);
@ -284,8 +278,10 @@ describe('Integration Scenarios - Clipboard with Different Backgrounds', () => {
const result = await screenshotSelector.captureElement(mockElement); const result = await screenshotSelector.captureElement(mockElement);
expect(global.htmlToImage.toCanvas).toHaveBeenCalledWith(mockElement, expect.objectContaining({ expect(global.html2canvas).toHaveBeenCalledWith(mockElement, expect.objectContaining({
backgroundColor: '#000000', // Black background backgroundColor: '#000000', // Black background
useCORS: true,
allowTaint: false
})); }));
expect(result.success).toBe(true); expect(result.success).toBe(true);
@ -298,8 +294,10 @@ describe('Integration Scenarios - Clipboard with Different Backgrounds', () => {
const result = await screenshotSelector.captureElement(mockElement); const result = await screenshotSelector.captureElement(mockElement);
expect(global.htmlToImage.toCanvas).toHaveBeenCalledWith(mockElement, expect.objectContaining({ expect(global.html2canvas).toHaveBeenCalledWith(mockElement, expect.objectContaining({
backgroundColor: '#ffffff', // White background backgroundColor: '#ffffff', // White background
useCORS: true,
allowTaint: false
})); }));
expect(result.success).toBe(true); expect(result.success).toBe(true);

View file

@ -53,10 +53,8 @@ global.window = {
CSS: { escape: jest.fn(str => str) } CSS: { escape: jest.fn(str => str) }
}; };
// Mock html-to-image // Mock html2canvas
global.htmlToImage = { global.html2canvas = jest.fn();
toCanvas: jest.fn(),
};
// Mock ScreenshotSelector class for testing // Mock ScreenshotSelector class for testing
class MockScreenshotSelector { class MockScreenshotSelector {
@ -128,18 +126,10 @@ class MockScreenshotSelector {
toDataURL: jest.fn(() => 'data:image/png;base64,mockdata') toDataURL: jest.fn(() => 'data:image/png;base64,mockdata')
}; };
// Mock html-to-image // Mock html2canvas
global.htmlToImage.toCanvas.mockResolvedValue(mockCanvas); global.html2canvas.mockResolvedValue(mockCanvas);
const canvas = await global.htmlToImage.toCanvas(element, { const canvas = await global.html2canvas(element);
backgroundColor: this.background === 'transparent'
? undefined
: this.background === 'white'
? '#ffffff'
: '#000000',
pixelRatio: global.window.devicePixelRatio || 1,
cacheBust: false
});
// Conditionally download based on saveToPc setting // Conditionally download based on saveToPc setting
if (this.saveToPc) { if (this.saveToPc) {
@ -344,11 +334,8 @@ describe('Save to PC and Clipboard Combinations', () => {
test('should still create canvas even when both outputs are disabled', async () => { test('should still create canvas even when both outputs are disabled', async () => {
await screenshotSelector.captureElement(mockElement); await screenshotSelector.captureElement(mockElement);
// html-to-image should still be called (needed for potential future operations) // html2canvas should still be called (needed for potential future operations)
expect(global.htmlToImage.toCanvas).toHaveBeenCalledWith( expect(global.html2canvas).toHaveBeenCalledWith(mockElement);
mockElement,
expect.any(Object)
);
}); });
}); });
}); });

View file

@ -238,10 +238,8 @@ beforeEach(() => {
global.mockDOMAPIs(); global.mockDOMAPIs();
global.mockClipboardAPI('supported'); global.mockClipboardAPI('supported');
// Mock html-to-image // Mock html2canvas
global.htmlToImage = { global.html2canvas = jest.fn().mockResolvedValue(global.createMockCanvas());
toCanvas: jest.fn().mockResolvedValue(global.createMockCanvas()),
};
}); });
// Cleanup after each test // Cleanup after each test