Skip to content

Commit 30e9c40

Browse files
committed
wip: cropping box follows mouse, need to remove screenshot editor & help once submitting works
1 parent b03f61a commit 30e9c40

File tree

4 files changed

+541
-11
lines changed

4 files changed

+541
-11
lines changed

packages/feedback-screenshot/src/screenshotButton.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { h, render } from 'preact';
22
import { useState, useCallback, useEffect } from 'preact/hooks';
33
import { useTakeScreenshot } from './useTakeScreenshot';
44
import type { VNode } from 'preact';
5-
import { ScreenshotWidget } from './screenshotEditor';
5+
import { ScreenshotWidget } from './screenshotWidget';
66

77
type Props = {
88
croppingRef: HTMLDivElement;
Lines changed: 221 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,233 @@
1+
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
12
import { h, render } from 'preact';
23
import type { VNode } from 'preact';
4+
import { ScreenshotEditorHelp } from './screenshotEditorHelp';
5+
import { useEffect, useRef, useState } from 'preact/hooks';
6+
import { GLOBAL_OBJ } from '@sentry/utils';
7+
8+
// exporting a separate copy of `WINDOW` rather than exporting the one from `@sentry/browser`
9+
// prevents the browser package from being bundled in the CDN bundle, and avoids a
10+
// circular dependency between the browser and feedback packages
11+
export const WINDOW = GLOBAL_OBJ as typeof GLOBAL_OBJ & Window;
312

413
type Props = {
514
screenshotImage: HTMLCanvasElement | null;
615
setScreenshotImage: (screenshot: HTMLCanvasElement | null) => void;
716
};
817
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
18+
19+
export interface Rect {
20+
height: number;
21+
width: number;
22+
x: number;
23+
y: number;
24+
}
925
export function ScreenshotWidget({ screenshotImage, setScreenshotImage }: Props): VNode | null {
10-
const image = screenshotImage;
11-
return image ? (
26+
// const image = screenshotImage;
27+
// return image ? (
28+
// <div style="padding-right: 16px;">
29+
// <img
30+
// type="image"
31+
// src={image.toDataURL()}
32+
// id="screenshot"
33+
// name="screenshot"
34+
// style="width:100%; height:100%;"
35+
// ></img>
36+
// </div>
37+
// ) : null;
38+
39+
// const Canvas = styled.canvas`
40+
// position: absolute;
41+
// cursor: crosshair;
42+
// max-width: 100vw;
43+
// max-height: 100vh;
44+
// `;
45+
// const Container = styled.div`
46+
// position: fixed;
47+
// z-index: 10000;
48+
// height: 100vh;
49+
// width: 100vw;
50+
// top: 0;
51+
// left: 0;
52+
// background-color: rgba(240, 236, 243, 1);
53+
// background-image: repeating-linear-gradient(
54+
// 45deg,
55+
// transparent,
56+
// transparent 5px,
57+
// rgba(0, 0, 0, 0.03) 5px,
58+
// rgba(0, 0, 0, 0.03) 10px
59+
// );
60+
// `;
61+
62+
const getCanvasRenderSize = (width: number, height: number) => {
63+
const maxWidth = WINDOW.innerWidth;
64+
const maxHeight = WINDOW.innerHeight;
65+
66+
if (width > maxWidth) {
67+
height = (maxWidth / width) * height;
68+
width = maxWidth;
69+
}
70+
71+
if (height > maxHeight) {
72+
width = (maxHeight / height) * width;
73+
height = maxHeight;
74+
}
75+
76+
return { width, height };
77+
};
78+
79+
interface Point {
80+
x: number;
81+
y: number;
82+
}
83+
84+
const constructRect = (start: Point, end: Point) => {
85+
return {
86+
x: Math.min(start.x, end.x),
87+
y: Math.min(start.y, end.y),
88+
width: Math.abs(start.x - end.x),
89+
height: Math.abs(start.y - end.y),
90+
};
91+
};
92+
93+
const canvasRef = useRef<HTMLCanvasElement>(screenshotImage);
94+
const [isDraggingState, setIsDraggingState] = useState(true);
95+
const currentRatio = useRef<number>(1);
96+
97+
useEffect(() => {
98+
const canvas = canvasRef.current;
99+
// eslint-disable-next-line @sentry-internal/sdk/no-optional-chaining
100+
const ctx = canvas?.getContext('2d');
101+
let img = new Image();
102+
const rectStart: { x: number; y: number } = { x: 0, y: 0 };
103+
const rectEnd: { x: number; y: number } = { x: canvas?.width ?? 0, y: canvas?.height ?? 0 };
104+
let isDragging = false;
105+
106+
function setCanvasSize() {
107+
const renderSize = getCanvasRenderSize(img.width, img.height);
108+
if (canvas) {
109+
canvas.style.width = `${renderSize.width}px`;
110+
canvas.style.height = `${renderSize.height}px`;
111+
canvas.style.top = `${(WINDOW.innerHeight - renderSize.height) / 2}px`;
112+
canvas.style.left = `${(WINDOW.innerWidth - renderSize.width) / 2}px`;
113+
console.log(WINDOW.innerWidth, WINDOW.innerHeight, renderSize.width, renderSize.height);
114+
}
115+
116+
// store it so we can translate the selection
117+
currentRatio.current = renderSize.width / img.width;
118+
}
119+
120+
function refreshCanvas() {
121+
if (canvas && ctx) {
122+
ctx.clearRect(0, 0, canvas.width, canvas.height);
123+
ctx.drawImage(img, 0, 0);
124+
}
125+
126+
if (!isDragging) {
127+
return;
128+
}
129+
130+
const rect = constructRect(rectStart, rectEnd);
131+
if (canvas && ctx) {
132+
// draw gray overlay around the selection
133+
ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
134+
ctx.fillRect(0, 0, canvas.width, rect.y);
135+
ctx.fillRect(0, rect.y, rect.x, rect.height);
136+
ctx.fillRect(rect.x + rect.width, rect.y, canvas.width, rect.height);
137+
ctx.fillRect(0, rect.y + rect.height, canvas.width, canvas.height);
138+
139+
// draw selection border
140+
ctx.strokeStyle = '#79628c';
141+
ctx.lineWidth = 6;
142+
ctx.strokeRect(rect.x, rect.y, rect.width, rect.height);
143+
}
144+
}
145+
146+
function submit(rect?: Rect) {
147+
if (!rect) {
148+
setScreenshotImage(canvas);
149+
return;
150+
}
151+
// eslint-disable-next-line no-restricted-globals
152+
const cutoutCanvas = document.createElement('canvas');
153+
cutoutCanvas.width = rect.width;
154+
cutoutCanvas.height = rect.height;
155+
const cutoutCtx = cutoutCanvas.getContext('2d');
156+
if (cutoutCtx && canvas) {
157+
cutoutCtx.drawImage(canvas, rect.x, rect.y, rect.width, rect.height, 0, 0, rect.width, rect.height);
158+
}
159+
160+
setScreenshotImage(cutoutCanvas);
161+
img.src = cutoutCanvas.toDataURL();
162+
}
163+
164+
function handleMouseDown(e: { offsetX: number; offsetY: number }) {
165+
rectStart.x = Math.floor(e.offsetX / currentRatio.current);
166+
rectStart.y = Math.floor(e.offsetY / currentRatio.current);
167+
isDragging = true;
168+
setIsDraggingState(true);
169+
}
170+
171+
function handleMouseMove(e: { offsetX: number; offsetY: number }) {
172+
rectEnd.x = Math.floor(e.offsetX / currentRatio.current);
173+
rectEnd.y = Math.floor(e.offsetY / currentRatio.current);
174+
refreshCanvas();
175+
}
176+
177+
async function handleMouseUp() {
178+
isDragging = false;
179+
setIsDraggingState(false);
180+
if (rectStart.x - rectEnd.x === 0 && rectStart.y - rectEnd.y === 0) {
181+
// no selection
182+
refreshCanvas();
183+
return;
184+
}
185+
await submit(constructRect(rectStart, rectEnd));
186+
}
187+
188+
async function handleEnterKey(e: { key: string }) {
189+
if (e.key === 'Enter') {
190+
await submit();
191+
}
192+
}
193+
194+
img.onload = () => {
195+
if (canvas && ctx) {
196+
canvas.width = img.width;
197+
canvas.height = img.height;
198+
setCanvasSize();
199+
ctx.drawImage(img, 0, 0);
200+
}
201+
};
202+
203+
if (screenshotImage) {
204+
img.src = screenshotImage.toDataURL();
205+
}
206+
207+
WINDOW.addEventListener('resize', setCanvasSize, { passive: true });
208+
canvas?.addEventListener('mousedown', handleMouseDown);
209+
canvas?.addEventListener('mousemove', handleMouseMove);
210+
canvas?.addEventListener('mouseup', handleMouseUp);
211+
WINDOW.addEventListener('keydown', handleEnterKey);
212+
213+
return () => {
214+
WINDOW.removeEventListener('resize', setCanvasSize);
215+
canvas?.removeEventListener('mousedown', handleMouseDown);
216+
canvas?.removeEventListener('mousemove', handleMouseMove);
217+
canvas?.removeEventListener('mouseup', handleMouseUp);
218+
WINDOW.removeEventListener('keydown', handleEnterKey);
219+
};
220+
}, [screenshotImage]);
221+
222+
return (
12223
<div style="padding-right: 16px;">
13-
<img
14-
type="image"
15-
src={image.toDataURL()}
16-
id="screenshot"
17-
name="screenshot"
18-
style="width:100%; height:100%;"
19-
></img>
224+
<canvas
225+
style="
226+
width: 100%;
227+
height: 100%;"
228+
ref={canvasRef}
229+
/>
230+
{/* <ScreenshotEditorHelp hide={isDraggingState} /> */}
20231
</div>
21-
) : null;
232+
);
22233
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/* eslint-disable @sentry-internal/sdk/no-optional-chaining */
2+
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
3+
import { h, render } from 'preact';
4+
import { useEffect, useState, useRef } from 'preact/hooks';
5+
import { GLOBAL_OBJ } from '@sentry/utils';
6+
7+
// exporting a separate copy of `WINDOW` rather than exporting the one from `@sentry/browser`
8+
// prevents the browser package from being bundled in the CDN bundle, and avoids a
9+
// circular dependency between the browser and feedback packages
10+
export const WINDOW = GLOBAL_OBJ as typeof GLOBAL_OBJ & Window;
11+
12+
// const Wrapper = styled.div`
13+
// position: fixed;
14+
// width: 100vw;
15+
// padding-top: 8px;
16+
// left: 0;
17+
// pointer-events: none;
18+
// display: flex;
19+
// justify-content: center;
20+
// transition: transform 0.2s ease-in-out;
21+
// transition-delay: 0.5s;
22+
// transform: translateY(0);
23+
// &[data-hide='true'] {
24+
// transition-delay: 0s;
25+
// transform: translateY(-100%);
26+
// }
27+
// `;
28+
29+
// const Content = styled.div`
30+
// background-color: #231c3d;
31+
// border: 1px solid #ccc;
32+
// border-radius: 20px;
33+
// color: #fff;
34+
// font-size: 14px;
35+
// padding: 6px 24px;
36+
// box-shadow:
37+
// 0 0 0 1px rgba(0, 0, 0, 0.05),
38+
// 0 4px 16px rgba(0, 0, 0, 0.2);
39+
// `;
40+
41+
export function ScreenshotEditorHelp({ hide }: { hide: boolean }) {
42+
const [isHidden, setIsHidden] = useState(false);
43+
const contentRef = useRef<HTMLDivElement>(null);
44+
45+
useEffect(() => {
46+
let boundingRect: DOMRect;
47+
if (contentRef.current) {
48+
boundingRect = contentRef.current?.getBoundingClientRect();
49+
}
50+
const handleMouseMove = (e: MouseEvent) => {
51+
const { clientX, clientY } = e;
52+
const { left, bottom, right } = boundingRect;
53+
const threshold = 50;
54+
const isNearContent = clientX > left - threshold && clientX < right + threshold && clientY < bottom + threshold;
55+
if (isNearContent) {
56+
setIsHidden(true);
57+
} else {
58+
setIsHidden(false);
59+
}
60+
};
61+
62+
function handleResize() {
63+
if (contentRef.current) {
64+
boundingRect = contentRef.current?.getBoundingClientRect();
65+
}
66+
}
67+
68+
WINDOW.addEventListener('resize', handleResize);
69+
WINDOW.addEventListener('mousemove', handleMouseMove);
70+
return () => {
71+
WINDOW.removeEventListener('resize', handleResize);
72+
WINDOW.removeEventListener('mousemove', handleMouseMove);
73+
};
74+
}, []);
75+
76+
return (
77+
<div
78+
style=" position: fixed;
79+
width: 100vw;
80+
padding-top: 8px;
81+
left: 0;
82+
pointer-events: none;
83+
display: flex;
84+
justify-content: center;
85+
transition: transform 0.2s ease-in-out;
86+
transition-delay: 0.5s;
87+
transform: translateY(0);
88+
&[data-hide='true'] {
89+
transition-delay: 0s;
90+
transform: translateY(-100%);
91+
}"
92+
data-hide={isHidden || hide}
93+
>
94+
<div
95+
style=" background-color: #231c3d;
96+
border: 1px solid #ccc;
97+
border-radius: 20px;
98+
color: #fff;
99+
font-size: 14px;
100+
padding: 6px 24px;
101+
box-shadow:
102+
0 0 0 1px rgba(0, 0, 0, 0.05),
103+
0 4px 16px rgba(0, 0, 0, 0.2);"
104+
ref={contentRef}
105+
>
106+
{'Mark the problem on the screen (press "Enter" to skip)'}
107+
<button>Cancel</button>
108+
<button>Confirm</button>
109+
</div>
110+
</div>
111+
);
112+
}

0 commit comments

Comments
 (0)