hocmai-feedback-widget 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +78 -113
  2. package/dist/capture/device-detection.d.ts +13 -0
  3. package/dist/capture/fonts.d.ts +4 -0
  4. package/dist/capture/index.d.ts +14 -0
  5. package/dist/capture/mathjax-fonts.d.ts +5 -0
  6. package/dist/capture/types.d.ts +25 -0
  7. package/dist/ckeditor.mjs +113 -0
  8. package/dist/ckeditor.mjs.map +1 -0
  9. package/dist/components/CkEditor.d.ts +14 -0
  10. package/dist/components/FeedbackWidget.d.ts +13 -1
  11. package/dist/context/FeedbackContext.d.ts +14 -2
  12. package/dist/hocmai-feedback-widget.mjs +7550 -0
  13. package/dist/hocmai-feedback-widget.mjs.map +1 -0
  14. package/dist/index.d.ts +4 -2
  15. package/package.json +39 -29
  16. package/src/App.tsx +147 -58
  17. package/src/capture/device-detection.ts +52 -0
  18. package/src/capture/fonts.ts +34 -0
  19. package/src/capture/index.ts +361 -0
  20. package/src/capture/mathjax-fonts.ts +143 -0
  21. package/src/capture/types.ts +34 -0
  22. package/src/ckeditor.d.ts +14 -0
  23. package/src/components/CkEditor.tsx +130 -0
  24. package/src/components/CropOverlay.tsx +29 -47
  25. package/src/components/FeedbackWidget.tsx +105 -93
  26. package/src/context/FeedbackContext.tsx +49 -9
  27. package/src/index.ts +9 -3
  28. package/src/main.tsx +7 -9
  29. package/src/utils/device.ts +14 -14
  30. package/dist/support-feedback-lib.es.js +0 -3946
  31. package/dist/support-feedback-lib.umd.js +0 -84
  32. package/dist/test-mobile-screenshot.html +0 -211
  33. package/dist/test-simple-capture.html +0 -107
  34. package/dist/utils/screenshot/background-renderer.d.ts +0 -3
  35. package/dist/utils/screenshot/canvas-renderer.d.ts +0 -9
  36. package/dist/utils/screenshot/cleanup.d.ts +0 -3
  37. package/dist/utils/screenshot/device-detection.d.ts +0 -16
  38. package/dist/utils/screenshot/element-collector.d.ts +0 -10
  39. package/dist/utils/screenshot/image-renderer.d.ts +0 -21
  40. package/dist/utils/screenshot/performance.d.ts +0 -3
  41. package/dist/utils/screenshot/svg-renderer.d.ts +0 -24
  42. package/dist/utils/screenshot/text-renderer.d.ts +0 -23
  43. package/dist/utils/screenshot/types.d.ts +0 -35
  44. package/dist/utils/screenshot.d.ts +0 -9
  45. package/dist/vite.svg +0 -1
  46. package/src/App.css +0 -42
  47. package/src/assets/react.svg +0 -1
  48. package/src/index.css +0 -68
  49. package/src/utils/screenshot/background-renderer.ts +0 -54
  50. package/src/utils/screenshot/canvas-renderer.ts +0 -343
  51. package/src/utils/screenshot/cleanup.ts +0 -32
  52. package/src/utils/screenshot/device-detection.ts +0 -90
  53. package/src/utils/screenshot/element-collector.ts +0 -134
  54. package/src/utils/screenshot/image-renderer.ts +0 -148
  55. package/src/utils/screenshot/performance.ts +0 -57
  56. package/src/utils/screenshot/svg-renderer.ts +0 -149
  57. package/src/utils/screenshot/text-renderer.ts +0 -212
  58. package/src/utils/screenshot/types.ts +0 -40
  59. package/src/utils/screenshot.ts +0 -83
@@ -1,32 +0,0 @@
1
- import type { CleanupTracker } from './types';
2
-
3
- export function createCleanupTracker(): CleanupTracker {
4
- return {
5
- objectUrls: [],
6
- tempCanvases: []
7
- };
8
- }
9
-
10
- export function cleanup(tracker: CleanupTracker): void {
11
- // Revoke all object URLs
12
- tracker.objectUrls.forEach(url => {
13
- try {
14
- URL.revokeObjectURL(url);
15
- } catch {
16
- console.warn('[Screenshot] Failed to revoke URL:', url);
17
- }
18
- });
19
-
20
- // Clear temp canvases
21
- tracker.tempCanvases.forEach(canvas => {
22
- const ctx = canvas.getContext('2d');
23
- if (ctx) {
24
- ctx.clearRect(0, 0, canvas.width, canvas.height);
25
- }
26
- canvas.width = 0;
27
- canvas.height = 0;
28
- });
29
-
30
- tracker.objectUrls.length = 0;
31
- tracker.tempCanvases.length = 0;
32
- }
@@ -1,90 +0,0 @@
1
- import type { DeviceCapabilities } from './types';
2
-
3
- /**
4
- * Detect if browser is Safari (including iOS Safari)
5
- */
6
- export function isSafari(): boolean {
7
- const userAgent = navigator.userAgent.toLowerCase();
8
- return /^((?!chrome|android).)*safari/i.test(navigator.userAgent) ||
9
- /iphone|ipad|ipod/.test(userAgent);
10
- }
11
-
12
- /**
13
- * Detect if browser is iOS Safari specifically
14
- * Must exclude Chrome/Chromium on iOS (Chrome DevTools emulation, Chrome iOS app)
15
- */
16
- export function isIOSSafari(): boolean {
17
- const userAgent = navigator.userAgent.toLowerCase();
18
- console.log('[DeviceDetection] User Agent:', userAgent);
19
-
20
- // Check for iOS device
21
- const isIOS = /iphone|ipad|ipod/.test(userAgent);
22
- console.log('[DeviceDetection] isIOS:', isIOS);
23
-
24
- // Check it's Safari (has 'safari' but NOT 'chrome' or 'crios' or 'crmo')
25
- const isSafariBrowser = /safari/.test(userAgent) &&
26
- !/chrome|crios|crmo/.test(userAgent);
27
- console.log('[DeviceDetection] isSafariBrowser:', isSafariBrowser);
28
- console.log('[DeviceDetection] Final isIOSSafari:', isIOS && isSafariBrowser);
29
-
30
- return isIOS && isSafariBrowser;
31
- }
32
-
33
- /**
34
- * Check if browser supports SVG foreignObject reliably
35
- * Safari iOS has known issues with foreignObject causing canvas tainting
36
- */
37
- export function supportsForeignObject(): boolean {
38
- // Safari iOS < 13 has poor foreignObject support
39
- // Even newer versions can cause canvas tainting issues
40
- if (isIOSSafari()) {
41
- console.warn('[DeviceDetection] Safari iOS detected - foreignObject may cause issues');
42
- return false;
43
- }
44
-
45
- // Desktop Safari also has some limitations
46
- if (isSafari() && !isIOSSafari()) {
47
- console.warn('[DeviceDetection] Desktop Safari detected - foreignObject may have limitations');
48
- // Desktop Safari works better, so we allow it
49
- return true;
50
- }
51
-
52
- return true;
53
- }
54
-
55
- export function getDeviceCapabilities(): DeviceCapabilities {
56
- const userAgent = navigator.userAgent.toLowerCase();
57
- const isMobile = /iphone|ipod|android.*mobile/.test(userAgent);
58
- const isTablet = /ipad|android(?!.*mobile)/.test(userAgent);
59
- const deviceMemory = (navigator as { deviceMemory?: number }).deviceMemory || 4;
60
- const isLowMemory = deviceMemory < 4;
61
-
62
- if (isMobile) {
63
- return {
64
- maxCanvasSize: 4096,
65
- recommendedScale: isLowMemory ? 0.8 : 1,
66
- recommendedQuality: isLowMemory ? 0.7 : 0.8,
67
- chunkSize: 30,
68
- yieldInterval: 16, // ~60fps
69
- maxTimeout: 5000
70
- };
71
- } else if (isTablet) {
72
- return {
73
- maxCanvasSize: 8192,
74
- recommendedScale: 1,
75
- recommendedQuality: 0.85,
76
- chunkSize: 50,
77
- yieldInterval: 16,
78
- maxTimeout: 4000
79
- };
80
- } else {
81
- return {
82
- maxCanvasSize: 16384,
83
- recommendedScale: 1,
84
- recommendedQuality: 0.9,
85
- chunkSize: 100,
86
- yieldInterval: 8,
87
- maxTimeout: 3000
88
- };
89
- }
90
- }
@@ -1,134 +0,0 @@
1
- import type { Rect, CaptureBounds } from './types';
2
-
3
- export function calculateCaptureBounds(
4
- cropRect: Rect | undefined,
5
- buffer: number,
6
- viewportWidth?: number,
7
- viewportHeight?: number
8
- ): CaptureBounds {
9
- // Use provided viewport dimensions or calculate them
10
- const getViewportWidth = () => {
11
- if (viewportWidth !== undefined) return viewportWidth;
12
- if (window.visualViewport) return window.visualViewport.width;
13
- return document.documentElement.clientWidth || window.innerWidth;
14
- };
15
-
16
- const getViewportHeight = () => {
17
- if (viewportHeight !== undefined) return viewportHeight;
18
- if (window.visualViewport) return window.visualViewport.height;
19
- return document.documentElement.clientHeight || window.innerHeight;
20
- };
21
-
22
- const targetX = cropRect ? cropRect.x : 0;
23
- const targetY = cropRect ? cropRect.y : 0;
24
- const targetWidth = cropRect ? cropRect.width : getViewportWidth();
25
- const targetHeight = cropRect ? cropRect.height : getViewportHeight();
26
-
27
- const bounds = {
28
- left: targetX - buffer,
29
- top: targetY - buffer,
30
- right: targetX + targetWidth + buffer,
31
- bottom: targetY + targetHeight + buffer
32
- };
33
-
34
- console.log('[ElementCollector] calculateCaptureBounds:', {
35
- viewportWidth: viewportWidth || 'auto',
36
- viewportHeight: viewportHeight || 'auto',
37
- targetWidth,
38
- targetHeight,
39
- bounds,
40
- scroll: { x: window.scrollX, y: window.scrollY }
41
- });
42
-
43
- return bounds;
44
- }
45
-
46
- export function isElementInViewport(
47
- element: HTMLElement,
48
- bounds: CaptureBounds
49
- ): boolean {
50
- const rect = element.getBoundingClientRect();
51
-
52
- // Convert bounds from document coordinates to viewport coordinates
53
- // getBoundingClientRect() returns viewport-relative coordinates
54
- // bounds are in document coordinates, so subtract scroll offset
55
- const scrollX = window.scrollX || window.pageXOffset;
56
- const scrollY = window.scrollY || window.pageYOffset;
57
-
58
- const viewportBounds = {
59
- left: bounds.left - scrollX,
60
- top: bounds.top - scrollY,
61
- right: bounds.right - scrollX,
62
- bottom: bounds.bottom - scrollY
63
- };
64
-
65
- // Outside viewport
66
- if (
67
- rect.bottom < viewportBounds.top ||
68
- rect.top > viewportBounds.bottom ||
69
- rect.right < viewportBounds.left ||
70
- rect.left > viewportBounds.right
71
- ) {
72
- return false;
73
- }
74
-
75
- // Hidden elements
76
- if (rect.width === 0 || rect.height === 0) {
77
- return false;
78
- }
79
-
80
- return true;
81
- }
82
-
83
- export function collectVisibleElements(bounds: CaptureBounds): Element[] {
84
- const all = Array.from(document.body.getElementsByTagName('*'));
85
-
86
- const filtered = all.filter(element => {
87
- // Skip non-HTML elements
88
- if (!(element instanceof HTMLElement)) return false;
89
-
90
- // Skip excluded tags
91
- const tag = element.tagName;
92
- if (['SCRIPT', 'STYLE', 'LINK', 'IFRAME', 'NOSCRIPT'].includes(tag)) {
93
- return false;
94
- }
95
-
96
- // Check viewport visibility (but keep HTML and BODY)
97
- if (tag === 'HTML' || tag === 'BODY') {
98
- return true;
99
- }
100
-
101
- return isElementInViewport(element as HTMLElement, bounds);
102
- });
103
-
104
- console.log('[ElementCollector] collectVisibleElements:', {
105
- total: all.length,
106
- filtered: filtered.length,
107
- bounds,
108
- sampleElements: filtered.slice(0, 5).map(el => ({
109
- tag: el.tagName,
110
- rect: el.getBoundingClientRect()
111
- }))
112
- });
113
-
114
- return filtered;
115
- }
116
-
117
- export function estimateDOMComplexity(element: HTMLElement): {
118
- nodeCount: number;
119
- imageCount: number;
120
- svgCount: number;
121
- isComplex: boolean;
122
- } {
123
- const allNodes = element.getElementsByTagName('*');
124
- const images = element.getElementsByTagName('img');
125
- const svgs = element.getElementsByTagName('svg');
126
- const nodeCount = allNodes.length;
127
- const imageCount = images.length;
128
- const svgCount = svgs.length;
129
-
130
- // Consider DOM complex if it has many nodes, images, or SVGs
131
- const isComplex = nodeCount > 1000 || imageCount > 50 || svgCount > 10;
132
-
133
- return { nodeCount, imageCount, svgCount, isComplex };
134
- }
@@ -1,148 +0,0 @@
1
- import { loadImageWithTimeout } from './performance';
2
-
3
- export interface ImageRenderResult {
4
- success: boolean;
5
- method?: 'canvas' | 'placeholder' | 'skip';
6
- error?: string;
7
- }
8
-
9
- /**
10
- * Strategy 1: Try canvas rendering with crossOrigin
11
- */
12
- async function tryCanvasRender(
13
- img: HTMLImageElement,
14
- ctx: CanvasRenderingContext2D,
15
- x: number,
16
- y: number,
17
- width: number,
18
- height: number
19
- ): Promise<ImageRenderResult> {
20
- try {
21
- // Create a test image with crossOrigin
22
- const testImg = new Image();
23
- testImg.crossOrigin = 'anonymous';
24
-
25
- await loadImageWithTimeout(testImg, img.src, 3000);
26
-
27
- // Draw to canvas
28
- ctx.drawImage(testImg, x, y, width, height);
29
-
30
- return { success: true, method: 'canvas' };
31
- } catch (error) {
32
- return {
33
- success: false,
34
- error: error instanceof Error ? error.message : 'Canvas render failed'
35
- };
36
- }
37
- }
38
-
39
- /**
40
- * Strategy 2: Use placeholder for CORS-blocked images
41
- */
42
- function renderPlaceholder(
43
- ctx: CanvasRenderingContext2D,
44
- x: number,
45
- y: number,
46
- width: number,
47
- height: number
48
- ): ImageRenderResult {
49
- // Draw gray background
50
- ctx.fillStyle = '#f0f0f0';
51
- ctx.fillRect(x, y, width, height);
52
-
53
- // Draw border
54
- ctx.strokeStyle = '#ccc';
55
- ctx.lineWidth = 1;
56
- ctx.strokeRect(x, y, width, height);
57
-
58
- // Draw "Image" text
59
- ctx.fillStyle = '#999';
60
- ctx.font = '12px sans-serif';
61
- ctx.textAlign = 'center';
62
- ctx.textBaseline = 'middle';
63
- ctx.fillText('Image', x + width / 2, y + height / 2);
64
-
65
- return { success: true, method: 'placeholder' };
66
- }
67
-
68
- /**
69
- * Strategy 3: Skip if very small or hidden
70
- */
71
- function shouldSkipImage(width: number, height: number): boolean {
72
- return width < 5 || height < 5;
73
- }
74
-
75
- /**
76
- * Main image rendering function with 3-tier strategy
77
- */
78
- export async function renderImage(
79
- img: HTMLImageElement,
80
- ctx: CanvasRenderingContext2D
81
- ): Promise<ImageRenderResult> {
82
- const rect = img.getBoundingClientRect();
83
-
84
- // Use viewport coordinates directly
85
- const x = rect.left;
86
- const y = rect.top;
87
- const width = rect.width;
88
- const height = rect.height;
89
-
90
- // Strategy 3: Skip very small images
91
- if (shouldSkipImage(width, height)) {
92
- return { success: true, method: 'skip' };
93
- }
94
-
95
- // Strategy 1: Try canvas rendering
96
- const canvasResult = await tryCanvasRender(img, ctx, x, y, width, height);
97
- if (canvasResult.success) {
98
- return canvasResult;
99
- }
100
-
101
- // Strategy 2: Use placeholder
102
- return renderPlaceholder(ctx, x, y, width, height);
103
- }
104
-
105
- /**
106
- * Render all images in the document
107
- */
108
- export async function renderImages(
109
- images: HTMLImageElement[],
110
- ctx: CanvasRenderingContext2D,
111
- onProgress?: (current: number, total: number) => void
112
- ): Promise<void> {
113
- for (let i = 0; i < images.length; i++) {
114
- await renderImage(images[i], ctx);
115
-
116
- if (onProgress) {
117
- onProgress(i + 1, images.length);
118
- }
119
- }
120
- }
121
-
122
- /**
123
- * Check if image is CORS-safe
124
- */
125
- export function isImageCORSSafe(img: HTMLImageElement): boolean {
126
- // Same origin images are always safe
127
- try {
128
- const imgUrl = new URL(img.src, window.location.href);
129
- return imgUrl.origin === window.location.origin;
130
- } catch {
131
- return false;
132
- }
133
- }
134
-
135
- /**
136
- * Get all renderable images from elements
137
- */
138
- export function getImageElements(elements: Element[]): HTMLImageElement[] {
139
- const images: HTMLImageElement[] = [];
140
-
141
- for (const element of elements) {
142
- if (element instanceof HTMLImageElement && element.src) {
143
- images.push(element);
144
- }
145
- }
146
-
147
- return images;
148
- }
@@ -1,57 +0,0 @@
1
- export function yieldToMainThread(): Promise<void> {
2
- return new Promise(resolve => {
3
- // Use Scheduler API if available (Chrome 94+)
4
- if ('scheduler' in window && 'yield' in (window as { scheduler?: { yield?: () => Promise<void> } }).scheduler!) {
5
- (window as { scheduler: { yield: () => Promise<void> } }).scheduler.yield().then(resolve);
6
- } else {
7
- // Fallback: setTimeout
8
- setTimeout(resolve, 0);
9
- }
10
- });
11
- }
12
-
13
- export async function processInChunks<T>(
14
- items: T[],
15
- chunkSize: number,
16
- processor: (chunk: T[]) => Promise<void>,
17
- onProgress?: (progress: number) => void
18
- ): Promise<void> {
19
- for (let i = 0; i < items.length; i += chunkSize) {
20
- const chunk = items.slice(i, i + chunkSize);
21
-
22
- // Process chunk
23
- await processor(chunk);
24
-
25
- // Report progress
26
- if (onProgress) {
27
- onProgress(Math.round(((i + chunk.length) / items.length) * 100));
28
- }
29
-
30
- // Yield to main thread
31
- await yieldToMainThread();
32
- }
33
- }
34
-
35
- export function loadImageWithTimeout(
36
- img: HTMLImageElement,
37
- url: string,
38
- timeout: number
39
- ): Promise<void> {
40
- return new Promise((resolve, reject) => {
41
- const timer = setTimeout(() => {
42
- reject(new Error('Image load timeout'));
43
- }, timeout);
44
-
45
- img.onload = () => {
46
- clearTimeout(timer);
47
- resolve();
48
- };
49
-
50
- img.onerror = () => {
51
- clearTimeout(timer);
52
- reject(new Error('Image load error'));
53
- };
54
-
55
- img.src = url;
56
- });
57
- }
@@ -1,149 +0,0 @@
1
- export interface SVGRenderResult {
2
- success: boolean;
3
- error?: string;
4
- }
5
-
6
- /**
7
- * Serialize SVG element to string
8
- */
9
- function serializeSVG(svg: SVGElement): string {
10
- const serializer = new XMLSerializer();
11
- let svgString = serializer.serializeToString(svg);
12
-
13
- // Ensure xmlns is present
14
- if (!svgString.includes('xmlns=')) {
15
- svgString = svgString.replace(
16
- '<svg',
17
- '<svg xmlns="http://www.w3.org/2000/svg"'
18
- );
19
- }
20
-
21
- return svgString;
22
- }
23
-
24
- /**
25
- * Check if SVG contains external resources that might cause CORS issues
26
- */
27
- function hasExternalResources(svgString: string): boolean {
28
- // Check for external image references
29
- if (svgString.includes('xlink:href="http') || svgString.includes('href="http')) {
30
- return true;
31
- }
32
-
33
- // Check for external font references
34
- if (svgString.includes('@font-face') && svgString.includes('url(http')) {
35
- return true;
36
- }
37
-
38
- return false;
39
- }
40
-
41
- /**
42
- * Render SVG element to canvas
43
- */
44
- export async function renderSVG(
45
- svg: SVGElement,
46
- ctx: CanvasRenderingContext2D
47
- ): Promise<SVGRenderResult> {
48
- try {
49
- const rect = svg.getBoundingClientRect();
50
-
51
- // Use viewport coordinates directly
52
- const x = rect.left;
53
- const y = rect.top;
54
- const width = rect.width;
55
- const height = rect.height;
56
-
57
- // Skip very small SVGs
58
- if (width < 1 || height < 1) {
59
- return { success: true };
60
- }
61
-
62
- // Serialize SVG to string
63
- const svgString = serializeSVG(svg);
64
-
65
- // Check for external resources that might cause CORS issues
66
- if (hasExternalResources(svgString)) {
67
- console.warn('[SVG] Skipping SVG with external resources to prevent canvas tainting');
68
- return { success: true }; // Skip but don't fail
69
- }
70
-
71
- // Create blob URL
72
- const blob = new Blob([svgString], { type: 'image/svg+xml;charset=utf-8' });
73
- const url = URL.createObjectURL(blob);
74
-
75
- try {
76
- // Load as image
77
- const img = new Image();
78
- await new Promise<void>((resolve, reject) => {
79
- img.onload = () => resolve();
80
- img.onerror = () => reject(new Error('SVG image load failed'));
81
- img.src = url;
82
- });
83
-
84
- // Draw to canvas
85
- ctx.drawImage(img, x, y, width, height);
86
-
87
- return { success: true };
88
- } finally {
89
- // Clean up blob URL
90
- URL.revokeObjectURL(url);
91
- }
92
- } catch (error) {
93
- return {
94
- success: false,
95
- error: error instanceof Error ? error.message : 'SVG render failed'
96
- };
97
- }
98
- }
99
-
100
- /**
101
- * Render all SVG elements
102
- */
103
- export async function renderSVGs(
104
- svgs: SVGElement[],
105
- ctx: CanvasRenderingContext2D,
106
- onProgress?: (current: number, total: number) => void
107
- ): Promise<void> {
108
- for (let i = 0; i < svgs.length; i++) {
109
- await renderSVG(svgs[i], ctx);
110
-
111
- if (onProgress) {
112
- onProgress(i + 1, svgs.length);
113
- }
114
- }
115
- }
116
-
117
- /**
118
- * Check if element is MathJax rendered
119
- */
120
- export function isMathJaxElement(element: Element): boolean {
121
- return (
122
- element.classList.contains('MathJax') ||
123
- element.classList.contains('MathJax_Display') ||
124
- element.classList.contains('MJX-TeXAtom-ORD') ||
125
- element.hasAttribute('data-mathml')
126
- );
127
- }
128
-
129
- /**
130
- * Get all SVG elements from the document
131
- */
132
- export function getSVGElements(elements: Element[]): SVGElement[] {
133
- const svgs: SVGElement[] = [];
134
-
135
- for (const element of elements) {
136
- if (element instanceof SVGElement) {
137
- svgs.push(element);
138
- }
139
- }
140
-
141
- return svgs;
142
- }
143
-
144
- /**
145
- * Get all MathJax elements from the document
146
- */
147
- export function getMathJaxElements(elements: Element[]): Element[] {
148
- return elements.filter(element => isMathJaxElement(element));
149
- }