pushfeedback 0.1.67 → 0.1.69

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.
@@ -1,5 +1,4 @@
1
1
  import { h } from '@stencil/core';
2
- import html2canvas from 'html2canvas-pro';
3
2
  export class FeedbackModal {
4
3
  constructor() {
5
4
  this.onScrollDebounced = () => {
@@ -86,144 +85,711 @@ export class FeedbackModal {
86
85
  document.querySelectorAll('.feedback-modal-element-selected').forEach(el => {
87
86
  el.classList.remove('feedback-modal-element-selected');
88
87
  });
88
+ // Reset canvas editor states
89
89
  this.takingScreenshot = false;
90
- this.originalElement = null;
91
- this.selectedElementBounds = null;
90
+ this.showPreviewModal = false;
91
+ this.showCanvasEditor = false;
92
+ this.annotations = [];
93
+ this.currentAnnotation = null;
94
+ this.isDrawing = false;
95
+ this.canvasRef = null;
96
+ this.canvasContext = null;
97
+ this.originalImageData = null;
98
+ // Reset error states
99
+ this.showScreenshotError = false;
100
+ this.screenshotError = '';
101
+ // Reset resizing states
102
+ this.isResizing = false;
103
+ this.resizingAnnotation = null;
104
+ this.resizeStartSize = 16;
105
+ this.resizeStartDimensions = null;
106
+ this.hoveredAnnotation = null;
107
+ this.resizeHandle = false;
108
+ // Reset form states
92
109
  this.formSuccess = false;
93
110
  this.formError = false;
94
111
  this.formErrorStatus = 500;
95
112
  this.formMessage = '';
96
113
  this.formEmail = '';
97
- this.showPreviewModal = false;
98
114
  this.resetOverflow();
99
115
  }, 200);
100
116
  };
101
- this.openScreenShot = () => {
102
- this.hasSelectedElement = false;
117
+ this.openScreenShot = async () => {
118
+ // Show loading state immediately
119
+ this.takingScreenshot = true;
120
+ this.showScreenshotError = false;
121
+ // Clear any previous annotations when taking a new screenshot
122
+ this.annotations = [];
123
+ this.currentAnnotation = null;
124
+ this.isDrawing = false;
125
+ this.hoveredAnnotation = null;
126
+ // Hide the feedback modal temporarily to exclude it from screenshot
127
+ const wasModalVisible = this.showModal;
103
128
  this.showModal = false;
104
- this.showScreenshotMode = true;
105
- this.showScreenshotTopBar = true;
106
- // Clear previous screenshot and selection data
107
- this.encodedScreenshot = null;
108
- this.originalElement = null;
109
- this.selectedElementBounds = null;
110
- this.hoveredElement = null;
111
- this.hoveredElementBounds = null;
112
- // NO CSS CLASSES - they cause scroll jumping
129
+ // Also hide any feedback buttons on the page
130
+ this.hideAllFeedbackElements();
131
+ try {
132
+ // Wait a moment for UI to update before capturing
133
+ await new Promise(resolve => setTimeout(resolve, 100));
134
+ // Capture viewport screenshot using browser API
135
+ const dataUrl = await this.captureViewportScreenshot();
136
+ this.encodedScreenshot = dataUrl;
137
+ this.originalImageData = dataUrl;
138
+ // Reset loading state
139
+ this.takingScreenshot = false;
140
+ // Go directly to canvas editor (don't restore modal)
141
+ this.showCanvasEditor = true;
142
+ // Restore feedback elements visibility
143
+ this.showAllFeedbackElements();
144
+ // Initialize canvas after a short delay to ensure DOM is ready
145
+ setTimeout(() => {
146
+ this.initializeCanvas();
147
+ }, 100);
148
+ }
149
+ catch (error) {
150
+ console.error('Failed to capture screenshot:', error);
151
+ // Reset loading state on error
152
+ this.takingScreenshot = false;
153
+ // Restore modal and feedback elements on error
154
+ this.showModal = wasModalVisible;
155
+ this.showAllFeedbackElements();
156
+ // Show error message to user
157
+ this.handleScreenshotError(error);
158
+ }
113
159
  };
114
- this.closeScreenShot = () => {
115
- // Remove highlight from ALL selected elements
116
- document.querySelectorAll('.feedback-modal-element-selected').forEach(el => {
117
- el.classList.remove('feedback-modal-element-selected');
160
+ this.hideAllFeedbackElements = () => {
161
+ // Hide all feedback buttons and modals on the page
162
+ const feedbackElements = document.querySelectorAll('feedback-button, feedback-modal');
163
+ feedbackElements.forEach(element => {
164
+ element.style.visibility = 'hidden';
118
165
  });
119
- // Reset loading state
120
- this.takingScreenshot = false;
166
+ };
167
+ this.showAllFeedbackElements = () => {
168
+ // Show all feedback buttons and modals on the page
169
+ const feedbackElements = document.querySelectorAll('feedback-button, feedback-modal');
170
+ feedbackElements.forEach(element => {
171
+ element.style.visibility = 'visible';
172
+ });
173
+ };
174
+ this.handleScreenshotError = (error) => {
175
+ let errorMessage = 'Failed to capture screenshot. ';
176
+ if (error.name === 'NotAllowedError') {
177
+ errorMessage += 'Permission denied. Please allow screen sharing to take screenshots.';
178
+ }
179
+ else if (error.name === 'NotSupportedError') {
180
+ errorMessage += 'Screen capture is not supported in this browser.';
181
+ }
182
+ else if (error.name === 'NotFoundError') {
183
+ errorMessage += 'No screen sources available for capture.';
184
+ }
185
+ else if (error.name === 'AbortError') {
186
+ errorMessage += 'Screenshot capture was cancelled.';
187
+ }
188
+ else if (error.message && error.message.includes('not supported')) {
189
+ errorMessage += 'Your browser does not support screen capture. Please use a browser like Chrome, Firefox, or Safari.';
190
+ }
191
+ else {
192
+ errorMessage += 'An unexpected error occurred. Please try again.';
193
+ }
194
+ this.screenshotError = errorMessage;
195
+ this.showScreenshotError = true;
196
+ // Auto-hide error after 5 seconds
197
+ setTimeout(() => {
198
+ this.showScreenshotError = false;
199
+ }, 5000);
200
+ };
201
+ this.openCanvasEditor = (event) => {
202
+ if (event) {
203
+ event.stopPropagation();
204
+ }
121
205
  this.showModal = false;
122
- this.showScreenshotMode = false;
123
- this.showScreenshotTopBar = false;
206
+ this.showCanvasEditor = true;
207
+ // Initialize canvas after a short delay to ensure DOM is ready
208
+ setTimeout(() => {
209
+ this.initializeCanvas();
210
+ }, 100);
124
211
  };
125
- this.openPreviewModal = (event) => {
126
- event.stopPropagation(); // Prevent button click from firing
127
- this.showPreviewModal = true;
212
+ this.closeCanvasEditor = () => {
213
+ this.showCanvasEditor = false;
214
+ this.showModal = true;
128
215
  };
129
- this.closePreviewModal = () => {
130
- this.showPreviewModal = false;
216
+ this.saveAnnotations = () => {
217
+ if (this.canvasRef) {
218
+ // Create final image with annotations
219
+ const finalDataUrl = this.canvasRef.toDataURL('image/png');
220
+ this.encodedScreenshot = finalDataUrl;
221
+ }
222
+ this.showCanvasEditor = false;
223
+ this.showModal = true;
131
224
  };
132
- this.handleMouseOverScreenShot = (event) => {
133
- event.preventDefault();
134
- if (this.hasSelectedElement)
225
+ this.initializeCanvas = () => {
226
+ if (!this.canvasRef || !this.originalImageData)
135
227
  return;
136
- const borderOffset = 2;
137
- this.screenshotModal.style.display = 'none';
138
- const elementUnder = document.elementFromPoint(event.clientX, event.clientY);
139
- const rect = elementUnder.getBoundingClientRect();
140
- this.screenshotModal.style.display = '';
141
- // Store the hovered element and its bounds for later use
142
- this.hoveredElement = elementUnder;
143
- this.hoveredElementBounds = rect;
144
- // Get the bounding box of the element selected
145
- this.elementSelected.style.position = 'absolute';
146
- this.elementSelected.style.left = `${rect.left}px`;
147
- this.elementSelected.style.top = `${rect.top}px`;
148
- this.elementSelected.style.width = `${rect.width}px`;
149
- this.elementSelected.style.height = `${rect.height}px`;
150
- this.elementSelected.classList.add('feedback-modal-element-hover');
151
- // Set the background color of nonselected areas
152
- // Top
153
- this.topSide.style.position = 'absolute';
154
- this.topSide.style.left = `${rect.left}px`;
155
- this.topSide.style.top = '0px';
156
- this.topSide.style.width = `${rect.width + borderOffset}px`;
157
- this.topSide.style.height = `${rect.top}px`;
158
- this.topSide.style.backgroundColor = 'rgba(0, 0, 0, 0.4)';
159
- // Left
160
- this.leftSide.style.position = 'absolute';
161
- this.leftSide.style.left = '0px';
162
- this.leftSide.style.top = '0px';
163
- this.leftSide.style.width = `${rect.left}px`;
164
- this.leftSide.style.height = '100vh';
165
- this.leftSide.style.backgroundColor = 'rgba(0, 0, 0, 0.4)';
166
- // Bottom
167
- this.bottomSide.style.position = 'absolute';
168
- this.bottomSide.style.left = `${rect.left}px`;
169
- this.bottomSide.style.top = `${rect.bottom + borderOffset}px`;
170
- this.bottomSide.style.width = `${rect.width + borderOffset}px`;
171
- this.bottomSide.style.height = '100vh';
172
- this.bottomSide.style.backgroundColor = 'rgba(0, 0, 0, 0.4)';
173
- // Right
174
- this.rightSide.style.position = 'absolute';
175
- this.rightSide.style.left = `${rect.right + borderOffset}px`;
176
- this.rightSide.style.top = '0px';
177
- this.rightSide.style.width = '100%';
178
- this.rightSide.style.height = '100vh';
179
- this.rightSide.style.backgroundColor = 'rgba(0, 0, 0, 0.4)';
180
- // Restore the visibility of the screenshot-modal
181
- this.screenshotModal.style.backgroundColor = 'transparent';
228
+ this.canvasContext = this.canvasRef.getContext('2d');
229
+ const img = new Image();
230
+ img.onload = () => {
231
+ // Set canvas to original image dimensions
232
+ this.canvasRef.width = img.width;
233
+ this.canvasRef.height = img.height;
234
+ // Get available container dimensions
235
+ const containerWidth = this.canvasRef.parentElement.clientWidth - 32; // Account for reduced padding (16px * 2)
236
+ const containerHeight = this.canvasRef.parentElement.clientHeight - 32;
237
+ // Calculate scale factors for both dimensions
238
+ const scaleX = containerWidth / img.width;
239
+ const scaleY = containerHeight / img.height;
240
+ // Use the smaller scale to ensure complete image fits
241
+ const scale = Math.min(scaleX, scaleY, 1); // Never scale up, only down
242
+ // Calculate final display dimensions
243
+ const displayWidth = img.width * scale;
244
+ const displayHeight = img.height * scale;
245
+ // Set CSS size for display (this scales the canvas visually)
246
+ this.canvasRef.style.width = `${displayWidth}px`;
247
+ this.canvasRef.style.height = `${displayHeight}px`;
248
+ console.log('Canvas initialized with complete image fit:', {
249
+ originalWidth: img.width,
250
+ originalHeight: img.height,
251
+ displayWidth,
252
+ displayHeight,
253
+ scale,
254
+ scaleX,
255
+ scaleY,
256
+ containerWidth,
257
+ containerHeight,
258
+ usingScale: scale === scaleX ? 'width-limited' : 'height-limited'
259
+ });
260
+ // Draw the original image at full resolution
261
+ this.canvasContext.drawImage(img, 0, 0);
262
+ // Redraw existing annotations
263
+ this.redrawAnnotations();
264
+ };
265
+ img.src = this.originalImageData;
182
266
  };
183
- this.handleMouseClickedSelectedElement = async (event) => {
184
- event.preventDefault();
185
- if (!this.elementSelected || !this.hoveredElement) {
267
+ this.redrawAnnotations = () => {
268
+ if (!this.canvasContext)
269
+ return;
270
+ // Clear and redraw background image
271
+ const img = new Image();
272
+ img.onload = () => {
273
+ this.canvasContext.clearRect(0, 0, this.canvasRef.width, this.canvasRef.height);
274
+ this.canvasContext.drawImage(img, 0, 0);
275
+ // Draw all annotations
276
+ this.annotations.forEach(annotation => {
277
+ this.drawAnnotation(annotation);
278
+ });
279
+ };
280
+ img.src = this.originalImageData;
281
+ };
282
+ this.drawAnnotation = (annotation) => {
283
+ if (!this.canvasContext)
284
+ return;
285
+ this.canvasContext.strokeStyle = annotation.color;
286
+ this.canvasContext.lineWidth = annotation.lineWidth;
287
+ this.canvasContext.lineCap = 'round';
288
+ this.canvasContext.lineJoin = 'round';
289
+ switch (annotation.type) {
290
+ case 'rectangle':
291
+ this.canvasContext.strokeRect(annotation.startX, annotation.startY, annotation.width, annotation.height);
292
+ // Draw resize handle if this annotation is hovered
293
+ if (this.hoveredAnnotation === annotation) {
294
+ this.drawRectangleResizeHandles(annotation);
295
+ }
296
+ break;
297
+ case 'line':
298
+ this.canvasContext.beginPath();
299
+ this.canvasContext.moveTo(annotation.startX, annotation.startY);
300
+ this.canvasContext.lineTo(annotation.endX, annotation.endY);
301
+ this.canvasContext.stroke();
302
+ // Draw resize handles if this annotation is hovered
303
+ if (this.hoveredAnnotation === annotation) {
304
+ this.drawLineResizeHandles(annotation);
305
+ }
306
+ break;
307
+ case 'arrow':
308
+ this.drawArrow(annotation.startX, annotation.startY, annotation.endX, annotation.endY);
309
+ // Draw resize handles if this annotation is hovered
310
+ if (this.hoveredAnnotation === annotation) {
311
+ this.drawLineResizeHandles(annotation); // Same as line
312
+ }
313
+ break;
314
+ case 'text':
315
+ const fontSize = annotation.fontSize || 16;
316
+ this.canvasContext.fillStyle = annotation.color;
317
+ this.canvasContext.font = `${fontSize}px Arial`;
318
+ this.canvasContext.fillText(annotation.text, annotation.x, annotation.y);
319
+ // Draw resize handle if this annotation is hovered
320
+ if (this.hoveredAnnotation === annotation) {
321
+ this.drawTextResizeHandle(annotation);
322
+ }
323
+ break;
324
+ }
325
+ };
326
+ // Draw resize handle for text annotation
327
+ this.drawTextResizeHandle = (annotation) => {
328
+ if (!this.canvasContext || annotation.type !== 'text')
329
+ return;
330
+ const fontSize = annotation.fontSize || 16;
331
+ const textWidth = this.getTextWidth(annotation.text, fontSize);
332
+ const handleSize = 8;
333
+ const handleX = annotation.x + textWidth;
334
+ const handleY = annotation.y;
335
+ // Draw resize handle (small square) - using widget primary color
336
+ this.canvasContext.fillStyle = '#0070F4'; // var(--feedback-primary-color)
337
+ this.canvasContext.strokeStyle = '#ffffff';
338
+ this.canvasContext.lineWidth = 2;
339
+ this.canvasContext.fillRect(handleX - handleSize / 2, handleY - handleSize / 2, handleSize, handleSize);
340
+ this.canvasContext.strokeRect(handleX - handleSize / 2, handleY - handleSize / 2, handleSize, handleSize);
341
+ };
342
+ this.drawArrow = (fromX, fromY, toX, toY) => {
343
+ const headlen = 15; // Arrow head length
344
+ const angle = Math.atan2(toY - fromY, toX - fromX);
345
+ // Draw line
346
+ this.canvasContext.beginPath();
347
+ this.canvasContext.moveTo(fromX, fromY);
348
+ this.canvasContext.lineTo(toX, toY);
349
+ this.canvasContext.stroke();
350
+ // Draw arrow head
351
+ this.canvasContext.beginPath();
352
+ this.canvasContext.moveTo(toX, toY);
353
+ this.canvasContext.lineTo(toX - headlen * Math.cos(angle - Math.PI / 6), toY - headlen * Math.sin(angle - Math.PI / 6));
354
+ this.canvasContext.moveTo(toX, toY);
355
+ this.canvasContext.lineTo(toX - headlen * Math.cos(angle + Math.PI / 6), toY - headlen * Math.sin(angle + Math.PI / 6));
356
+ this.canvasContext.stroke();
357
+ };
358
+ this.undoLastAnnotation = () => {
359
+ this.annotations = this.annotations.slice(0, -1);
360
+ this.redrawAnnotations();
361
+ };
362
+ // Handle color slot editing
363
+ this.handleColorSlotClick = (colorIndex) => {
364
+ if (this.editingColorIndex === colorIndex) {
365
+ // If already editing this slot, just select the color
366
+ this.canvasDrawingColor = this.defaultColors[colorIndex];
367
+ this.showColorPicker = false;
368
+ this.editingColorIndex = -1;
369
+ }
370
+ else {
371
+ // Start editing this color slot
372
+ this.editingColorIndex = colorIndex;
373
+ this.showColorPicker = true;
374
+ this.canvasDrawingColor = this.defaultColors[colorIndex];
375
+ }
376
+ };
377
+ // Update color in slot
378
+ this.updateColorSlot = (newColor) => {
379
+ if (this.editingColorIndex >= 0 && this.editingColorIndex < this.defaultColors.length) {
380
+ this.defaultColors[this.editingColorIndex] = newColor;
381
+ this.canvasDrawingColor = newColor;
382
+ this.showColorPicker = false;
383
+ this.editingColorIndex = -1;
384
+ // Force reactivity
385
+ this.defaultColors = [...this.defaultColors];
386
+ }
387
+ };
388
+ // Handle color picker input without closing
389
+ this.handleColorPickerInput = (event) => {
390
+ event.stopPropagation();
391
+ const newColor = event.target.value;
392
+ if (this.editingColorIndex >= 0 && this.editingColorIndex < this.defaultColors.length) {
393
+ this.defaultColors[this.editingColorIndex] = newColor;
394
+ this.canvasDrawingColor = newColor;
395
+ // Force reactivity
396
+ this.defaultColors = [...this.defaultColors];
397
+ }
398
+ };
399
+ // Handle color picker click to prevent closing
400
+ this.handleColorPickerClick = (event) => {
401
+ event.stopPropagation();
402
+ };
403
+ // Close color picker
404
+ this.closeColorPicker = () => {
405
+ this.showColorPicker = false;
406
+ this.editingColorIndex = -1;
407
+ };
408
+ // Check if point is in resize handle for any annotation type
409
+ this.isPointInResizeHandle = (x, y, annotation) => {
410
+ const handleSize = 8;
411
+ switch (annotation.type) {
412
+ case 'text':
413
+ const textWidth = this.getTextWidth(annotation.text, annotation.fontSize || 16);
414
+ const handleX = annotation.x + textWidth;
415
+ const handleY = annotation.y;
416
+ return x >= handleX - handleSize / 2 && x <= handleX + handleSize / 2 &&
417
+ y >= handleY - handleSize / 2 && y <= handleY + handleSize / 2;
418
+ case 'rectangle':
419
+ const right = annotation.startX + annotation.width;
420
+ const bottom = annotation.startY + annotation.height;
421
+ // Only check bottom-right corner handle
422
+ return x >= right - handleSize / 2 && x <= right + handleSize / 2 &&
423
+ y >= bottom - handleSize / 2 && y <= bottom + handleSize / 2;
424
+ case 'line':
425
+ case 'arrow':
426
+ // Check both endpoint handles
427
+ const lineHandles = [
428
+ { x: annotation.startX, y: annotation.startY, point: 'start' },
429
+ { x: annotation.endX, y: annotation.endY, point: 'end' }
430
+ ];
431
+ for (const handle of lineHandles) {
432
+ if (x >= handle.x - handleSize / 2 && x <= handle.x + handleSize / 2 &&
433
+ y >= handle.y - handleSize / 2 && y <= handle.y + handleSize / 2) {
434
+ return handle.point; // Return which endpoint was clicked
435
+ }
436
+ }
437
+ return false;
438
+ default:
439
+ return false;
440
+ }
441
+ };
442
+ // Get text width for resize handle positioning
443
+ this.getTextWidth = (text, fontSize) => {
444
+ // Approximate text width calculation
445
+ return text.length * fontSize * 0.6;
446
+ };
447
+ // Start text resize
448
+ this.startTextResize = (annotation, startPos) => {
449
+ this.isResizing = true;
450
+ this.resizingAnnotation = annotation;
451
+ this.resizeStartSize = annotation.fontSize || 16;
452
+ this.dragStartPos = startPos;
453
+ };
454
+ // Handle text resize
455
+ this.handleTextResize = (currentPos) => {
456
+ if (!this.resizingAnnotation || !this.dragStartPos)
457
+ return;
458
+ const deltaX = currentPos.x - this.dragStartPos.x;
459
+ const deltaY = currentPos.y - this.dragStartPos.y;
460
+ const avgDelta = (deltaX + deltaY) / 2;
461
+ // Calculate new font size (minimum 8px, maximum 72px)
462
+ const newSize = Math.max(8, Math.min(72, this.resizeStartSize + avgDelta * 0.5));
463
+ // Update annotation font size
464
+ const index = this.annotations.findIndex(a => a === this.resizingAnnotation);
465
+ if (index !== -1) {
466
+ this.annotations[index] = Object.assign(Object.assign({}, this.resizingAnnotation), { fontSize: Math.round(newSize) });
467
+ this.resizingAnnotation = this.annotations[index];
468
+ }
469
+ this.redrawAnnotations();
470
+ };
471
+ // Start resize for any annotation type
472
+ this.startResize = (annotation, handle, startPos) => {
473
+ this.isResizing = true;
474
+ this.resizingAnnotation = annotation;
475
+ this.resizeHandle = handle;
476
+ this.dragStartPos = startPos;
477
+ // Store original values for different annotation types
478
+ if (annotation.type === 'text') {
479
+ this.resizeStartSize = annotation.fontSize || 16;
480
+ }
481
+ else if (annotation.type === 'rectangle') {
482
+ this.resizeStartDimensions = { width: annotation.width, height: annotation.height };
483
+ }
484
+ };
485
+ // Enhanced mouse down handler with resize detection for all annotation types
486
+ this.handleCanvasMouseDown = (event) => {
487
+ if (!this.canvasRef)
488
+ return;
489
+ // Disable drawing on mobile devices
490
+ if (window.innerWidth <= 768)
491
+ return;
492
+ // Close color picker if open
493
+ if (this.showColorPicker) {
494
+ this.closeColorPicker();
495
+ }
496
+ const coords = this.getCanvasCoordinates(event);
497
+ // Check if clicking on existing annotation first
498
+ const found = this.findAnnotationAt(coords.x, coords.y);
499
+ if (found) {
500
+ // Check if clicking on resize handle for any annotation type
501
+ const handle = this.isPointInResizeHandle(coords.x, coords.y, found.annotation);
502
+ if (handle) {
503
+ this.startResize(found.annotation, handle, coords);
504
+ this.canvasRef.style.cursor = 'nw-resize';
505
+ return;
506
+ }
507
+ // Start dragging existing annotation
508
+ if (!this.isDrawing) {
509
+ this.isDragging = true;
510
+ this.draggedAnnotation = found.annotation;
511
+ this.dragStartPos = coords;
512
+ this.canvasRef.style.cursor = 'grabbing';
513
+ return;
514
+ }
515
+ }
516
+ // Original drawing logic
517
+ this.isDrawing = true;
518
+ if (this.canvasDrawingTool === 'text') {
519
+ const text = prompt('Enter text:');
520
+ if (text) {
521
+ const annotation = {
522
+ type: 'text',
523
+ x: coords.x,
524
+ y: coords.y,
525
+ text,
526
+ color: this.canvasDrawingColor,
527
+ fontSize: 16
528
+ };
529
+ this.annotations = [...this.annotations, annotation];
530
+ this.redrawAnnotations();
531
+ }
532
+ this.isDrawing = false;
533
+ }
534
+ else {
535
+ this.currentAnnotation = {
536
+ type: this.canvasDrawingTool,
537
+ startX: coords.x,
538
+ startY: coords.y,
539
+ color: this.canvasDrawingColor,
540
+ lineWidth: this.canvasLineWidth
541
+ };
542
+ }
543
+ };
544
+ this.handleCanvasMouseMove = (event) => {
545
+ if (!this.canvasRef)
546
+ return;
547
+ // Disable drawing on mobile devices
548
+ if (window.innerWidth <= 768)
549
+ return;
550
+ const coords = this.getCanvasCoordinates(event);
551
+ // Handle resizing for any annotation type
552
+ if (this.isResizing && this.resizingAnnotation) {
553
+ this.handleResize(coords);
554
+ return;
555
+ }
556
+ // Handle dragging existing annotation
557
+ if (this.isDragging && this.draggedAnnotation && this.dragStartPos) {
558
+ const deltaX = coords.x - this.dragStartPos.x;
559
+ const deltaY = coords.y - this.dragStartPos.y;
560
+ // Update annotation position
561
+ const updatedAnnotation = Object.assign({}, this.draggedAnnotation);
562
+ switch (updatedAnnotation.type) {
563
+ case 'rectangle':
564
+ updatedAnnotation.startX += deltaX;
565
+ updatedAnnotation.startY += deltaY;
566
+ break;
567
+ case 'line':
568
+ case 'arrow':
569
+ updatedAnnotation.startX += deltaX;
570
+ updatedAnnotation.startY += deltaY;
571
+ updatedAnnotation.endX += deltaX;
572
+ updatedAnnotation.endY += deltaY;
573
+ break;
574
+ case 'text':
575
+ updatedAnnotation.x += deltaX;
576
+ updatedAnnotation.y += deltaY;
577
+ break;
578
+ }
579
+ // Update annotation in array
580
+ const index = this.annotations.findIndex(a => a === this.draggedAnnotation);
581
+ if (index !== -1) {
582
+ this.annotations[index] = updatedAnnotation;
583
+ this.draggedAnnotation = updatedAnnotation;
584
+ }
585
+ this.dragStartPos = coords;
586
+ this.redrawAnnotations();
587
+ return;
588
+ }
589
+ // Handle drawing new annotation
590
+ if (this.isDrawing && this.currentAnnotation) {
591
+ if (this.canvasDrawingTool === 'rectangle') {
592
+ this.currentAnnotation.width = coords.x - this.currentAnnotation.startX;
593
+ this.currentAnnotation.height = coords.y - this.currentAnnotation.startY;
594
+ }
595
+ else {
596
+ this.currentAnnotation.endX = coords.x;
597
+ this.currentAnnotation.endY = coords.y;
598
+ }
599
+ this.redrawAnnotations();
600
+ this.drawAnnotation(this.currentAnnotation);
601
+ return;
602
+ }
603
+ // Handle hover states and cursor changes
604
+ const found = this.findAnnotationAt(coords.x, coords.y);
605
+ if (found) {
606
+ // Check if hovering over resize handle for any annotation type
607
+ const handle = this.isPointInResizeHandle(coords.x, coords.y, found.annotation);
608
+ if (handle) {
609
+ this.canvasRef.style.cursor = 'nw-resize';
610
+ this.hoveredAnnotation = found.annotation;
611
+ this.redrawAnnotations();
612
+ return;
613
+ }
614
+ // Regular hover over annotation
615
+ this.canvasRef.style.cursor = 'grab';
616
+ if (this.hoveredAnnotation !== found.annotation) {
617
+ this.hoveredAnnotation = found.annotation;
618
+ this.redrawAnnotations();
619
+ }
620
+ }
621
+ else {
622
+ // No annotation under cursor
623
+ this.canvasRef.style.cursor = 'crosshair';
624
+ if (this.hoveredAnnotation) {
625
+ this.hoveredAnnotation = null;
626
+ this.redrawAnnotations();
627
+ }
628
+ }
629
+ };
630
+ this.handleCanvasMouseUp = () => {
631
+ // Disable drawing on mobile devices
632
+ if (window.innerWidth <= 768)
633
+ return;
634
+ // Handle end of resizing
635
+ if (this.isResizing) {
636
+ this.isResizing = false;
637
+ this.resizingAnnotation = null;
638
+ this.dragStartPos = null;
639
+ this.resizeHandle = false;
640
+ this.resizeStartDimensions = null;
641
+ if (this.canvasRef) {
642
+ this.canvasRef.style.cursor = 'crosshair';
643
+ }
186
644
  return;
187
645
  }
188
- this.hasSelectedElement = true;
189
- // Remove highlight from ALL previously selected elements
190
- document.querySelectorAll('.feedback-modal-element-selected').forEach(el => {
191
- el.classList.remove('feedback-modal-element-selected');
646
+ // Handle end of dragging
647
+ if (this.isDragging) {
648
+ this.isDragging = false;
649
+ this.draggedAnnotation = null;
650
+ this.dragStartPos = null;
651
+ if (this.canvasRef) {
652
+ this.canvasRef.style.cursor = 'crosshair';
653
+ }
654
+ return;
655
+ }
656
+ // Handle end of drawing
657
+ if (!this.isDrawing || !this.currentAnnotation)
658
+ return;
659
+ this.isDrawing = false;
660
+ this.annotations = [...this.annotations, this.currentAnnotation];
661
+ this.currentAnnotation = null;
662
+ this.redrawAnnotations();
663
+ };
664
+ // Draw resize handles for rectangle annotation (only bottom-right corner)
665
+ this.drawRectangleResizeHandles = (annotation) => {
666
+ if (!this.canvasContext || annotation.type !== 'rectangle')
667
+ return;
668
+ const handleSize = 8;
669
+ const right = annotation.startX + annotation.width;
670
+ const bottom = annotation.startY + annotation.height;
671
+ // Only draw bottom-right corner handle
672
+ const handle = { x: right, y: bottom };
673
+ // Draw the handle
674
+ this.canvasContext.fillStyle = '#0070F4'; // Primary color
675
+ this.canvasContext.strokeStyle = '#ffffff';
676
+ this.canvasContext.lineWidth = 2;
677
+ this.canvasContext.fillRect(handle.x - handleSize / 2, handle.y - handleSize / 2, handleSize, handleSize);
678
+ this.canvasContext.strokeRect(handle.x - handleSize / 2, handle.y - handleSize / 2, handleSize, handleSize);
679
+ };
680
+ // Draw resize handles for line/arrow annotation
681
+ this.drawLineResizeHandles = (annotation) => {
682
+ if (!this.canvasContext || (annotation.type !== 'line' && annotation.type !== 'arrow'))
683
+ return;
684
+ const handleSize = 8;
685
+ // Define handle positions (2 endpoints)
686
+ const handles = [
687
+ { x: annotation.startX, y: annotation.startY },
688
+ { x: annotation.endX, y: annotation.endY } // End point
689
+ ];
690
+ // Draw each handle
691
+ this.canvasContext.fillStyle = '#0070F4'; // Primary color
692
+ this.canvasContext.strokeStyle = '#ffffff';
693
+ this.canvasContext.lineWidth = 2;
694
+ handles.forEach(handle => {
695
+ this.canvasContext.fillRect(handle.x - handleSize / 2, handle.y - handleSize / 2, handleSize, handleSize);
696
+ this.canvasContext.strokeRect(handle.x - handleSize / 2, handle.y - handleSize / 2, handleSize, handleSize);
192
697
  });
193
- // Add highlight to newly selected element
194
- this.hoveredElement.classList.add('feedback-modal-element-selected');
195
- // Store element bounds in viewport coordinates
196
- this.selectedElementBounds = this.hoveredElementBounds;
197
- this.originalElement = this.hoveredElement;
198
- // Show loading state in top bar
199
- this.takingScreenshot = true;
200
- // Take screenshot FIRST while highlight is still visible
201
- try {
202
- const dataUrl = await this.captureScreenshot();
203
- console.log('Screenshot captured');
204
- this.encodedScreenshot = dataUrl;
205
- // Reset loading state
206
- this.takingScreenshot = false;
207
- // NOW hide screenshot interface after capturing
208
- this.showScreenshotTopBar = false;
209
- this.showScreenshotMode = false;
210
- // Restore normal page state
211
- this.resetOverflow();
212
- // Show modal with the captured screenshot
213
- this.showModal = true;
698
+ };
699
+ // Convert screen coordinates to canvas coordinates
700
+ this.getCanvasCoordinates = (event) => {
701
+ if (!this.canvasRef)
702
+ return { x: 0, y: 0 };
703
+ const rect = this.canvasRef.getBoundingClientRect();
704
+ // Calculate the scale factor between display size and actual canvas size
705
+ const scaleX = this.canvasRef.width / rect.width;
706
+ const scaleY = this.canvasRef.height / rect.height;
707
+ const x = (event.clientX - rect.left) * scaleX;
708
+ const y = (event.clientY - rect.top) * scaleY;
709
+ return { x, y };
710
+ };
711
+ // Find annotation under mouse cursor
712
+ this.findAnnotationAt = (x, y) => {
713
+ // Check in reverse order (top to bottom)
714
+ for (let i = this.annotations.length - 1; i >= 0; i--) {
715
+ const annotation = this.annotations[i];
716
+ if (this.isPointInAnnotation(x, y, annotation)) {
717
+ return { annotation, index: i };
718
+ }
214
719
  }
215
- catch (error) {
216
- console.error('Failed to capture screenshot:', error);
217
- this.hasSelectedElement = false;
218
- // Reset loading state on error
219
- this.takingScreenshot = false;
220
- // Still need to cleanup on error
221
- this.showScreenshotTopBar = false;
222
- this.showScreenshotMode = false;
223
- this.resetOverflow();
224
- this.showModal = true;
720
+ return null;
721
+ };
722
+ // Check if point is within annotation bounds
723
+ this.isPointInAnnotation = (x, y, annotation) => {
724
+ const tolerance = 10; // Click tolerance
725
+ switch (annotation.type) {
726
+ case 'rectangle':
727
+ const left = Math.min(annotation.startX, annotation.startX + annotation.width);
728
+ const right = Math.max(annotation.startX, annotation.startX + annotation.width);
729
+ const top = Math.min(annotation.startY, annotation.startY + annotation.height);
730
+ const bottom = Math.max(annotation.startY, annotation.startY + annotation.height);
731
+ return x >= left - tolerance && x <= right + tolerance &&
732
+ y >= top - tolerance && y <= bottom + tolerance;
733
+ case 'line':
734
+ case 'arrow':
735
+ // Distance from point to line
736
+ const A = annotation.endY - annotation.startY;
737
+ const B = annotation.startX - annotation.endX;
738
+ const C = annotation.endX * annotation.startY - annotation.startX * annotation.endY;
739
+ const distance = Math.abs(A * x + B * y + C) / Math.sqrt(A * A + B * B);
740
+ return distance <= tolerance;
741
+ case 'text':
742
+ // Simple bounding box for text
743
+ return x >= annotation.x - tolerance && x <= annotation.x + 100 &&
744
+ y >= annotation.y - 20 && y <= annotation.y + tolerance;
745
+ default:
746
+ return false;
225
747
  }
226
748
  };
749
+ // Handle resize for different annotation types
750
+ this.handleResize = (currentPos) => {
751
+ if (!this.resizingAnnotation || !this.dragStartPos)
752
+ return;
753
+ const annotation = this.resizingAnnotation;
754
+ const index = this.annotations.findIndex(a => a === annotation);
755
+ if (index === -1)
756
+ return;
757
+ let updatedAnnotation = Object.assign({}, annotation);
758
+ switch (annotation.type) {
759
+ case 'text':
760
+ // Text resize logic (existing)
761
+ const deltaX = currentPos.x - this.dragStartPos.x;
762
+ const deltaY = currentPos.y - this.dragStartPos.y;
763
+ const avgDelta = (deltaX + deltaY) / 2;
764
+ const newSize = Math.max(8, Math.min(72, this.resizeStartSize + avgDelta * 0.5));
765
+ updatedAnnotation.fontSize = Math.round(newSize);
766
+ break;
767
+ case 'rectangle':
768
+ // Rectangle resize logic - only bottom-right corner
769
+ const rectDeltaX = currentPos.x - this.dragStartPos.x;
770
+ const rectDeltaY = currentPos.y - this.dragStartPos.y;
771
+ // Update width and height based on original dimensions plus delta
772
+ updatedAnnotation.width = Math.max(10, this.resizeStartDimensions.width + rectDeltaX); // Minimum width of 10px
773
+ updatedAnnotation.height = Math.max(10, this.resizeStartDimensions.height + rectDeltaY); // Minimum height of 10px
774
+ break;
775
+ case 'line':
776
+ case 'arrow':
777
+ // Line/arrow resize logic - move endpoints
778
+ if (this.resizeHandle === 'start') {
779
+ updatedAnnotation.startX = currentPos.x;
780
+ updatedAnnotation.startY = currentPos.y;
781
+ }
782
+ else if (this.resizeHandle === 'end') {
783
+ updatedAnnotation.endX = currentPos.x;
784
+ updatedAnnotation.endY = currentPos.y;
785
+ }
786
+ break;
787
+ }
788
+ // Update annotation in array
789
+ this.annotations[index] = updatedAnnotation;
790
+ this.resizingAnnotation = updatedAnnotation;
791
+ this.redrawAnnotations();
792
+ };
227
793
  this.sending = false;
228
794
  this.formMessage = '';
229
795
  this.formEmail = '';
@@ -237,6 +803,29 @@ export class FeedbackModal {
237
803
  this.selectedRating = -1;
238
804
  this.overlayVisible = false;
239
805
  this.isAnimating = false;
806
+ this.takingScreenshot = false;
807
+ this.showPreviewModal = false;
808
+ this.screenshotError = '';
809
+ this.showScreenshotError = false;
810
+ this.showCanvasEditor = false;
811
+ this.canvasDrawingTool = 'rectangle';
812
+ this.canvasDrawingColor = '#ff0000';
813
+ this.canvasLineWidth = 3;
814
+ this.isDrawing = false;
815
+ this.annotations = [];
816
+ this.currentAnnotation = null;
817
+ this.isDragging = false;
818
+ this.draggedAnnotation = null;
819
+ this.dragStartPos = null;
820
+ this.showColorPicker = false;
821
+ this.editingColorIndex = -1;
822
+ this.isResizing = false;
823
+ this.resizingAnnotation = null;
824
+ this.resizeStartSize = 16;
825
+ this.resizeStartDimensions = null;
826
+ this.hoveredAnnotation = null;
827
+ this.resizeHandle = false;
828
+ this.defaultColors = ['#ff0000', '#00ff00', '#0000ff', '#000000'];
240
829
  this.customFont = false;
241
830
  this.emailAddress = '';
242
831
  this.hideEmail = false;
@@ -272,8 +861,9 @@ export class FeedbackModal {
272
861
  this.screenshotTakingText = 'Taking screenshot...';
273
862
  this.screenshotTopbarText = 'Select an element on this page';
274
863
  this.successMessage = '';
275
- this.takingScreenshot = false;
276
- this.showPreviewModal = false;
864
+ this.canvasEditorTitle = 'Edit screenshot';
865
+ this.canvasEditorCancelText = 'Cancel';
866
+ this.canvasEditorSaveText = 'Save';
277
867
  }
278
868
  componentWillLoad() {
279
869
  if (this.fetchData)
@@ -308,47 +898,62 @@ export class FeedbackModal {
308
898
  handleEmailInput(event) {
309
899
  this.formEmail = event.target.value;
310
900
  }
311
- captureScreenshot() {
312
- return new Promise((resolve, reject) => {
313
- // Add a small delay to ensure CSS highlight is applied
314
- setTimeout(() => {
315
- requestAnimationFrame(() => {
316
- if (!this.selectedElementBounds) {
317
- reject(new Error('No element selected'));
318
- return;
319
- }
320
- // Capture what's currently visible in the viewport
321
- html2canvas(document.body, {
322
- x: window.scrollX,
323
- y: window.scrollY,
324
- width: window.innerWidth,
325
- height: window.innerHeight,
326
- scrollX: window.scrollX,
327
- scrollY: window.scrollY,
328
- allowTaint: false,
329
- useCORS: true,
330
- scale: 1,
331
- backgroundColor: '#ffffff',
332
- logging: true,
333
- foreignObjectRendering: false,
334
- imageTimeout: 10000,
335
- ignoreElements: (element) => {
336
- // Only ignore screenshot UI, keep everything else including highlights
337
- return element.classList.contains('feedback-modal-screenshot-header') ||
338
- element.classList.contains('feedback-overlay');
901
+ async captureViewportScreenshot() {
902
+ try {
903
+ // Check if Screen Capture API is supported
904
+ if (!navigator.mediaDevices || !navigator.mediaDevices.getDisplayMedia) {
905
+ throw new Error('Screen Capture API is not supported in this browser');
906
+ }
907
+ // Request screen capture with preference for current tab
908
+ const stream = await navigator.mediaDevices.getDisplayMedia({
909
+ video: {
910
+ mediaSource: 'screen',
911
+ width: { ideal: window.innerWidth },
912
+ height: { ideal: window.innerHeight }
913
+ },
914
+ audio: false,
915
+ preferCurrentTab: true
916
+ });
917
+ // Create video element to capture frame
918
+ const video = document.createElement('video');
919
+ video.srcObject = stream;
920
+ video.autoplay = true;
921
+ video.muted = true;
922
+ return new Promise((resolve, reject) => {
923
+ video.onloadedmetadata = () => {
924
+ video.play();
925
+ // Wait a moment for video to stabilize
926
+ setTimeout(() => {
927
+ try {
928
+ // Create canvas to capture frame
929
+ const canvas = document.createElement('canvas');
930
+ canvas.width = video.videoWidth;
931
+ canvas.height = video.videoHeight;
932
+ const ctx = canvas.getContext('2d');
933
+ ctx.drawImage(video, 0, 0);
934
+ // Stop the stream
935
+ stream.getTracks().forEach(track => track.stop());
936
+ // Convert to data URL
937
+ const dataUrl = canvas.toDataURL('image/png');
938
+ console.log('Screenshot captured successfully using Screen Capture API');
939
+ resolve(dataUrl);
339
940
  }
340
- })
341
- .then((canvas) => {
342
- const dataUrl = canvas.toDataURL();
343
- resolve(dataUrl);
344
- })
345
- .catch((error) => {
346
- console.error('Failed to capture screenshot:', error);
347
- reject(error);
348
- });
349
- });
350
- }, 100); // Small delay to ensure CSS is applied
351
- });
941
+ catch (error) {
942
+ stream.getTracks().forEach(track => track.stop());
943
+ reject(error);
944
+ }
945
+ }, 100);
946
+ };
947
+ video.onerror = (_) => {
948
+ stream.getTracks().forEach(track => track.stop());
949
+ reject(new Error('Failed to load video for screenshot capture'));
950
+ };
951
+ });
952
+ }
953
+ catch (error) {
954
+ console.error('Screen capture failed:', error);
955
+ throw error;
956
+ }
352
957
  }
353
958
  handleCheckboxChange(event) {
354
959
  this.isPrivacyChecked = event.target.checked;
@@ -360,7 +965,7 @@ export class FeedbackModal {
360
965
  this.selectedRating = newRating;
361
966
  }
362
967
  render() {
363
- return (h("div", { class: `feedback-modal-wrapper ${this.customFont ? 'feedback-modal-wrapper--custom-font' : ''}` }, this.showScreenshotMode && (h("div", { class: "feedback-modal-screenshot", ref: (el) => (this.screenshotModal = el), onMouseMove: this.handleMouseOverScreenShot }, h("div", { class: "feedback-modal-screenshot-element-selected", ref: (el) => (this.elementSelected = el), onClick: this.handleMouseClickedSelectedElement }), h("div", { class: "top-side", ref: (el) => (this.topSide = el) }), h("div", { class: "left-side", ref: (el) => (this.leftSide = el) }), h("div", { class: "bottom-side", ref: (el) => (this.bottomSide = el) }), h("div", { class: "right-side", ref: (el) => (this.rightSide = el) }), this.showScreenshotTopBar && (h("div", { class: "feedback-modal-screenshot-header", onClick: this.closeScreenShot }, h("span", null, this.takingScreenshot ? this.screenshotTakingText : this.screenshotTopbarText), h("span", { class: "feedback-modal-screenshot-close" }, h("svg", { xmlns: "http://www.w3.org/2000/svg", width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", stroke: "#191919", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "feather feather-x" }, h("line", { x1: "18", y1: "6", x2: "6", y2: "18" }), h("line", { x1: "6", y1: "6", x2: "18", y2: "18" }))))))), this.showModal && (h("div", { class: `feedback-overlay ${this.isAnimating ? 'feedback-overlay--visible' : ''}` })), this.showModal && (h("div", { class: `feedback-modal-content feedback-modal-content--${this.modalPosition} ${this.isAnimating ? 'feedback-modal-content--open' : ''}`, ref: (el) => (this.modalContent = el) }, h("div", { class: "feedback-modal-header" }, !this.formSuccess && !this.formError ? (h("span", null, this.modalTitle)) : this.formSuccess ? (h("span", null, this.modalTitleSuccess)) : (h("span", null, this.modalTitleError)), h("button", { class: "feedback-modal-close", onClick: this.close }, h("svg", { xmlns: "http://www.w3.org/2000/svg", width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", stroke: "#191919", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "feather feather-x" }, h("line", { x1: "18", y1: "6", x2: "6", y2: "18" }), h("line", { x1: "6", y1: "6", x2: "18", y2: "18" })))), h("div", { class: "feedback-modal-body" }, !this.formSuccess && !this.formError ? (h("form", { onSubmit: this.handleSubmit }, !this.hideRating && (h("div", { class: "feedback-modal-rating" }, this.ratingMode === 'thumbs' ? (h("div", { class: "feedback-modal-rating-content" }, h("span", { class: "feedback-modal-input-heading" }, this.ratingPlaceholder), h("div", { class: "feedback-modal-rating-buttons feedback-modal-rating-buttons--thumbs" }, h("button", { title: "Yes", class: `feedback-modal-rating-button ${this.selectedRating === 1
968
+ return (h("div", { class: `feedback-modal-wrapper ${this.customFont ? 'feedback-modal-wrapper--custom-font' : ''}` }, this.showScreenshotError && (h("div", { class: "screenshot-error-notification" }, h("div", { class: "screenshot-error-content" }, h("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" }, h("circle", { cx: "12", cy: "12", r: "10" }), h("line", { x1: "15", y1: "9", x2: "9", y2: "15" }), h("line", { x1: "9", y1: "9", x2: "15", y2: "15" })), h("span", null, this.screenshotError), h("button", { class: "error-close-btn", onClick: () => this.showScreenshotError = false, title: "Close" }, h("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" }, h("line", { x1: "18", y1: "6", x2: "6", y2: "18" }), h("line", { x1: "6", y1: "6", x2: "18", y2: "18" })))))), this.showModal && (h("div", { class: `feedback-overlay ${this.isAnimating ? 'feedback-overlay--visible' : ''}` })), this.showModal && (h("div", { class: `feedback-modal-content feedback-modal-content--${this.modalPosition} ${this.isAnimating ? 'feedback-modal-content--open' : ''}`, ref: (el) => (this.modalContent = el) }, h("div", { class: "feedback-modal-header" }, !this.formSuccess && !this.formError ? (h("span", null, this.modalTitle)) : this.formSuccess ? (h("span", null, this.modalTitleSuccess)) : (h("span", null, this.modalTitleError)), h("button", { class: "feedback-modal-close", onClick: this.close }, h("svg", { xmlns: "http://www.w3.org/2000/svg", width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", stroke: "#191919", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "feather feather-x" }, h("line", { x1: "18", y1: "6", x2: "6", y2: "18" }), h("line", { x1: "6", y1: "6", x2: "18", y2: "18" })))), h("div", { class: "feedback-modal-body" }, !this.formSuccess && !this.formError ? (h("form", { onSubmit: this.handleSubmit }, !this.hideRating && (h("div", { class: "feedback-modal-rating" }, this.ratingMode === 'thumbs' ? (h("div", { class: "feedback-modal-rating-content" }, h("span", { class: "feedback-modal-input-heading" }, this.ratingPlaceholder), h("div", { class: "feedback-modal-rating-buttons feedback-modal-rating-buttons--thumbs" }, h("button", { title: "Yes", class: `feedback-modal-rating-button ${this.selectedRating === 1
364
969
  ? 'feedback-modal-rating-button--selected'
365
970
  : ''}`, onClick: (event) => {
366
971
  event.preventDefault();
@@ -375,7 +980,8 @@ export class FeedbackModal {
375
980
  : ''}`, onClick: (event) => {
376
981
  event.preventDefault();
377
982
  this.handleRatingChange(rating);
378
- } }, h("svg", { xmlns: "http://www.w3.org/2000/svg", width: "28", height: "28", viewBox: "0 0 24 24", fill: "none", stroke: "#5F6368", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" }, h("polygon", { points: "12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" })))))))))), h("div", { class: "feedback-modal-text" }, h("textarea", { placeholder: this.messagePlaceholder, value: this.formMessage, onInput: (event) => this.handleMessageInput(event) })), !this.hideEmail && (h("div", { class: "feedback-modal-email" }, h("input", { placeholder: this.emailPlaceholder, type: "email", onInput: (event) => this.handleEmailInput(event), value: this.formEmail, required: this.isEmailRequired }))), h("div", { class: "feedback-verification" }, h("input", { type: "text", name: "verification", style: { display: 'none' }, onInput: (event) => this.handleVerification(event), value: this.formVerification })), !this.hidePrivacyPolicy && (h("div", { class: "feedback-modal-privacy" }, h("input", { type: "checkbox", id: "privacyPolicy", onChange: (ev) => this.handleCheckboxChange(ev), required: true }), h("span", { innerHTML: this.privacyPolicyText }))), h("div", { class: `feedback-modal-buttons ${this.hideScreenshotButton ? 'single' : ''}` }, !this.hideScreenshotButton && (h("button", { type: "button", class: `feedback-modal-button feedback-modal-button--screenshot ${this.encodedScreenshot ? 'feedback-modal-button--active' : ''}`, onClick: this.openScreenShot, disabled: this.sending }, this.encodedScreenshot && (h("div", { class: "screenshot-preview", onClick: this.openPreviewModal }, h("img", { src: this.encodedScreenshot, alt: "Screenshot Preview" }))), !this.encodedScreenshot && (h("svg", { xmlns: "http://www.w3.org/2000/svg", height: "24", viewBox: "0 -960 960 960", width: "24" }, h("path", { d: "M680-80v-120H560v-80h120v-120h80v120h120v80H760v120h-80ZM200-200v-200h80v120h120v80H200Zm0-360v-200h200v80H280v120h-80Zm480 0v-120H560v-80h200v200h-80Z" }))), this.encodedScreenshot ? this.screenshotAttachedText : this.screenshotButtonText)), h("button", { class: "feedback-modal-button feedback-modal-button--submit", type: "submit", disabled: this.sending }, this.sendButtonText)))) : this.formSuccess && !this.formError ? (h("div", { class: "feedback-modal-success" }, h("p", { class: "feedback-modal-message" }, this.successMessage))) : this.formError && this.formErrorStatus == 404 ? (h("p", { class: "feedback-modal-message" }, this.errorMessage404)) : this.formError && this.formErrorStatus == 403 ? (h("p", { class: "feedback-modal-message" }, this.errorMessage403)) : this.formError ? (h("p", { class: "feedback-modal-message" }, this.errorMessage)) : (h("span", null))), h("div", { class: "feedback-modal-footer" }, h("div", { class: "feedback-logo", style: { display: this.whitelabel ? 'none' : 'block' } }, "Powered by", ' ', h("a", { target: "_blank", href: "https://pushfeedback.com" }, "PushFeedback.com")), this.footerText && (h("div", { class: "feedback-footer-text" }, h("span", { innerHTML: this.footerText })))))), this.showPreviewModal && (h("div", { class: "preview-modal-overlay", onClick: this.closePreviewModal }, h("div", { class: "preview-modal" }, h("img", { src: this.encodedScreenshot, alt: "Screenshot Preview", onClick: (e) => e.stopPropagation() }))))));
983
+ } }, h("svg", { xmlns: "http://www.w3.org/2000/svg", width: "28", height: "28", viewBox: "0 0 24 24", fill: "none", stroke: "#5F6368", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" }, h("polygon", { points: "12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" })))))))))), h("div", { class: "feedback-modal-text" }, h("textarea", { placeholder: this.messagePlaceholder, value: this.formMessage, onInput: (event) => this.handleMessageInput(event) })), !this.hideEmail && (h("div", { class: "feedback-modal-email" }, h("input", { placeholder: this.emailPlaceholder, type: "email", onInput: (event) => this.handleEmailInput(event), value: this.formEmail, required: this.isEmailRequired }))), h("div", { class: "feedback-verification" }, h("input", { type: "text", name: "verification", style: { display: 'none' }, onInput: (event) => this.handleVerification(event), value: this.formVerification })), !this.hidePrivacyPolicy && (h("div", { class: "feedback-modal-privacy" }, h("input", { type: "checkbox", id: "privacyPolicy", onChange: (ev) => this.handleCheckboxChange(ev), required: true }), h("span", { innerHTML: this.privacyPolicyText }))), h("div", { class: `feedback-modal-buttons ${this.hideScreenshotButton ? 'single' : ''}` }, !this.hideScreenshotButton && (h("button", { type: "button", class: `feedback-modal-button feedback-modal-button--screenshot ${this.encodedScreenshot ? 'feedback-modal-button--active' : ''}`, onClick: this.openScreenShot, disabled: this.sending || this.takingScreenshot }, this.encodedScreenshot && (h("div", { class: "screenshot-preview", onClick: this.openCanvasEditor }, h("img", { src: this.encodedScreenshot, alt: "Screenshot Preview" }))), !this.encodedScreenshot && !this.takingScreenshot && (h("svg", { xmlns: "http://www.w3.org/2000/svg", height: "24", viewBox: "0 -960 960 960", width: "24" }, h("path", { d: "M680-80v-120H560v-80h120v-120h80v120h120v80H760v120h-80ZM200-200v-200h80v120h120v80H200Zm0-360v-200h200v80H280v120h-80Zm480 0v-120H560v-80h200v200h-80Z" }))), this.takingScreenshot && (h("div", { class: "screenshot-loading" }, h("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "#666", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "feather-loader" }, h("line", { x1: "12", y1: "2", x2: "12", y2: "6" }), h("line", { x1: "12", y1: "18", x2: "12", y2: "22" }), h("line", { x1: "4.93", y1: "4.93", x2: "7.76", y2: "7.76" }), h("line", { x1: "16.24", y1: "16.24", x2: "19.07", y2: "19.07" }), h("line", { x1: "2", y1: "12", x2: "6", y2: "12" }), h("line", { x1: "18", y1: "12", x2: "22", y2: "12" }), h("line", { x1: "4.93", y1: "19.07", x2: "7.76", y2: "16.24" }), h("line", { x1: "16.24", y1: "7.76", x2: "19.07", y2: "4.93" })))), this.takingScreenshot ? this.screenshotTakingText :
984
+ this.encodedScreenshot ? this.screenshotAttachedText : this.screenshotButtonText)), h("button", { class: "feedback-modal-button feedback-modal-button--submit", type: "submit", disabled: this.sending }, this.sendButtonText)))) : this.formSuccess && !this.formError ? (h("div", { class: "feedback-modal-success" }, h("p", { class: "feedback-modal-message" }, this.successMessage))) : this.formError && this.formErrorStatus == 404 ? (h("p", { class: "feedback-modal-message" }, this.errorMessage404)) : this.formError && this.formErrorStatus == 403 ? (h("p", { class: "feedback-modal-message" }, this.errorMessage403)) : this.formError ? (h("p", { class: "feedback-modal-message" }, this.errorMessage)) : (h("span", null))), h("div", { class: "feedback-modal-footer" }, h("div", { class: "feedback-logo", style: { display: this.whitelabel ? 'none' : 'block' } }, "Powered by", ' ', h("a", { target: "_blank", href: "https://pushfeedback.com" }, "PushFeedback.com")), this.footerText && (h("div", { class: "feedback-footer-text" }, h("span", { innerHTML: this.footerText })))))), this.showCanvasEditor && (h("div", { class: "canvas-editor-overlay" }, h("div", { class: "canvas-editor-modal" }, h("div", { class: "canvas-editor-header" }, h("div", { class: "canvas-editor-title" }, h("h3", null, this.canvasEditorTitle)), h("div", { class: "canvas-editor-toolbar" }, h("div", { class: "toolbar-section" }, h("div", { class: "tool-group" }, h("button", { class: `tool-btn ${this.canvasDrawingTool === 'rectangle' ? 'active' : ''}`, onClick: () => this.canvasDrawingTool = 'rectangle', title: "Rectangle" }, h("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" }, h("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2", ry: "2" }))), h("button", { class: `tool-btn ${this.canvasDrawingTool === 'line' ? 'active' : ''}`, onClick: () => this.canvasDrawingTool = 'line', title: "Line" }, h("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" }, h("line", { x1: "5", y1: "12", x2: "19", y2: "12" }))), h("button", { class: `tool-btn ${this.canvasDrawingTool === 'arrow' ? 'active' : ''}`, onClick: () => this.canvasDrawingTool = 'arrow', title: "Arrow" }, h("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" }, h("line", { x1: "7", y1: "17", x2: "17", y2: "7" }), h("polyline", { points: "7,7 17,7 17,17" }))), h("button", { class: `tool-btn ${this.canvasDrawingTool === 'text' ? 'active' : ''}`, onClick: () => this.canvasDrawingTool = 'text', title: "Text" }, h("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" }, h("polyline", { points: "4,7 4,4 20,4 20,7" }), h("line", { x1: "9", y1: "20", x2: "15", y2: "20" }), h("line", { x1: "12", y1: "4", x2: "12", y2: "20" }))), h("div", { class: "toolbar-divider" }), h("button", { class: "tool-btn undo-btn", onClick: this.undoLastAnnotation, disabled: this.annotations.length === 0, title: "Undo" }, h("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" }, h("polyline", { points: "1,4 1,10 7,10" }), h("path", { d: "M3.51,15a9,9,0,0,0,14.85-3.36,9,9,0,0,0-9.19-10.15L1.83,10" }))))), h("div", { class: "toolbar-section" }, h("div", { class: "color-palette" }, this.defaultColors.map((color, index) => (h("div", { class: "color-slot-wrapper" }, h("button", { class: `color-btn ${this.canvasDrawingColor === color ? 'active' : ''} ${this.editingColorIndex === index ? 'editing' : ''}`, style: { backgroundColor: color }, onClick: () => this.handleColorSlotClick(index), title: `Color ${index + 1} - Click to customize` }, this.editingColorIndex === index && (h("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "white", "stroke-width": "2" }, h("path", { d: "M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z" })))), this.editingColorIndex === index && this.showColorPicker && (h("div", { class: "color-picker-dropdown" }, h("input", { type: "color", value: color, onInput: (e) => this.handleColorPickerInput(e), onClick: (e) => this.handleColorPickerClick(e) })))))))), h("div", { class: "toolbar-section" }, h("div", { class: "size-control" }, h("input", { type: "range", min: "1", max: "10", value: this.canvasLineWidth, onInput: (e) => this.canvasLineWidth = parseInt(e.target.value), class: "size-slider" }), h("span", { class: "size-value" }, this.canvasLineWidth, "px"))), h("div", { class: "toolbar-section" }, h("button", { class: "action-btn secondary", onClick: this.closeCanvasEditor }, this.canvasEditorCancelText), h("button", { class: "action-btn primary", onClick: this.saveAnnotations }, this.canvasEditorSaveText))), h("div", { class: "canvas-editor-content" }, h("canvas", { ref: (el) => this.canvasRef = el, class: "annotation-canvas", onMouseDown: this.handleCanvasMouseDown, onMouseMove: this.handleCanvasMouseMove, onMouseUp: this.handleCanvasMouseUp, onMouseLeave: this.handleCanvasMouseUp }))))))));
379
985
  }
380
986
  componentDidRender() {
381
987
  if (this.showModal) {
@@ -1033,6 +1639,60 @@ export class FeedbackModal {
1033
1639
  "attribute": "success-message",
1034
1640
  "reflect": false,
1035
1641
  "defaultValue": "''"
1642
+ },
1643
+ "canvasEditorTitle": {
1644
+ "type": "string",
1645
+ "mutable": false,
1646
+ "complexType": {
1647
+ "original": "string",
1648
+ "resolved": "string",
1649
+ "references": {}
1650
+ },
1651
+ "required": false,
1652
+ "optional": false,
1653
+ "docs": {
1654
+ "tags": [],
1655
+ "text": ""
1656
+ },
1657
+ "attribute": "canvas-editor-title",
1658
+ "reflect": false,
1659
+ "defaultValue": "'Edit screenshot'"
1660
+ },
1661
+ "canvasEditorCancelText": {
1662
+ "type": "string",
1663
+ "mutable": false,
1664
+ "complexType": {
1665
+ "original": "string",
1666
+ "resolved": "string",
1667
+ "references": {}
1668
+ },
1669
+ "required": false,
1670
+ "optional": false,
1671
+ "docs": {
1672
+ "tags": [],
1673
+ "text": ""
1674
+ },
1675
+ "attribute": "canvas-editor-cancel-text",
1676
+ "reflect": false,
1677
+ "defaultValue": "'Cancel'"
1678
+ },
1679
+ "canvasEditorSaveText": {
1680
+ "type": "string",
1681
+ "mutable": false,
1682
+ "complexType": {
1683
+ "original": "string",
1684
+ "resolved": "string",
1685
+ "references": {}
1686
+ },
1687
+ "required": false,
1688
+ "optional": false,
1689
+ "docs": {
1690
+ "tags": [],
1691
+ "text": ""
1692
+ },
1693
+ "attribute": "canvas-editor-save-text",
1694
+ "reflect": false,
1695
+ "defaultValue": "'Save'"
1036
1696
  }
1037
1697
  };
1038
1698
  }
@@ -1052,7 +1712,28 @@ export class FeedbackModal {
1052
1712
  "overlayVisible": {},
1053
1713
  "isAnimating": {},
1054
1714
  "takingScreenshot": {},
1055
- "showPreviewModal": {}
1715
+ "showPreviewModal": {},
1716
+ "screenshotError": {},
1717
+ "showScreenshotError": {},
1718
+ "showCanvasEditor": {},
1719
+ "canvasDrawingTool": {},
1720
+ "canvasDrawingColor": {},
1721
+ "canvasLineWidth": {},
1722
+ "isDrawing": {},
1723
+ "annotations": {},
1724
+ "currentAnnotation": {},
1725
+ "isDragging": {},
1726
+ "draggedAnnotation": {},
1727
+ "dragStartPos": {},
1728
+ "showColorPicker": {},
1729
+ "editingColorIndex": {},
1730
+ "isResizing": {},
1731
+ "resizingAnnotation": {},
1732
+ "resizeStartSize": {},
1733
+ "resizeStartDimensions": {},
1734
+ "hoveredAnnotation": {},
1735
+ "resizeHandle": {},
1736
+ "defaultColors": {}
1056
1737
  };
1057
1738
  }
1058
1739
  static get events() {