pushfeedback 0.1.67 → 0.1.68

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.
@@ -86,144 +86,634 @@ export class FeedbackModal {
86
86
  document.querySelectorAll('.feedback-modal-element-selected').forEach(el => {
87
87
  el.classList.remove('feedback-modal-element-selected');
88
88
  });
89
+ // Reset canvas editor states
89
90
  this.takingScreenshot = false;
90
- this.originalElement = null;
91
- this.selectedElementBounds = null;
91
+ this.showPreviewModal = false;
92
+ this.showCanvasEditor = false;
93
+ this.annotations = [];
94
+ this.currentAnnotation = null;
95
+ this.isDrawing = false;
96
+ this.canvasRef = null;
97
+ this.canvasContext = null;
98
+ this.originalImageData = null;
99
+ // Reset resizing states
100
+ this.isResizing = false;
101
+ this.resizingAnnotation = null;
102
+ this.resizeStartSize = 16;
103
+ this.hoveredAnnotation = null;
104
+ this.resizeHandle = false;
105
+ // Reset form states
92
106
  this.formSuccess = false;
93
107
  this.formError = false;
94
108
  this.formErrorStatus = 500;
95
109
  this.formMessage = '';
96
110
  this.formEmail = '';
97
- this.showPreviewModal = false;
98
111
  this.resetOverflow();
99
112
  }, 200);
100
113
  };
101
- this.openScreenShot = () => {
102
- this.hasSelectedElement = false;
103
- 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
113
- };
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');
118
- });
119
- // Reset loading state
120
- this.takingScreenshot = false;
121
- this.showModal = false;
122
- this.showScreenshotMode = false;
123
- this.showScreenshotTopBar = false;
124
- };
125
- this.openPreviewModal = (event) => {
126
- event.stopPropagation(); // Prevent button click from firing
127
- this.showPreviewModal = true;
128
- };
129
- this.closePreviewModal = () => {
130
- this.showPreviewModal = false;
131
- };
132
- this.handleMouseOverScreenShot = (event) => {
133
- event.preventDefault();
134
- if (this.hasSelectedElement)
135
- 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';
182
- };
183
- this.handleMouseClickedSelectedElement = async (event) => {
184
- event.preventDefault();
185
- if (!this.elementSelected || !this.hoveredElement) {
186
- return;
187
- }
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');
192
- });
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
114
+ this.openScreenShot = async () => {
115
+ // Show loading state immediately
199
116
  this.takingScreenshot = true;
200
- // Take screenshot FIRST while highlight is still visible
201
117
  try {
202
- const dataUrl = await this.captureScreenshot();
203
- console.log('Screenshot captured');
118
+ // Capture viewport screenshot immediately
119
+ const dataUrl = await this.captureViewportScreenshot();
204
120
  this.encodedScreenshot = dataUrl;
121
+ this.originalImageData = dataUrl;
205
122
  // Reset loading state
206
123
  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;
124
+ // Skip preview modal and go directly to canvas editor
125
+ this.showModal = false;
126
+ this.showCanvasEditor = true;
127
+ // Initialize canvas after a short delay to ensure DOM is ready
128
+ setTimeout(() => {
129
+ this.initializeCanvas();
130
+ }, 100);
214
131
  }
215
132
  catch (error) {
216
133
  console.error('Failed to capture screenshot:', error);
217
- this.hasSelectedElement = false;
218
134
  // Reset loading state on error
219
135
  this.takingScreenshot = false;
220
- // Still need to cleanup on error
221
- this.showScreenshotTopBar = false;
222
- this.showScreenshotMode = false;
223
- this.resetOverflow();
136
+ // Show modal anyway
224
137
  this.showModal = true;
225
138
  }
226
139
  };
140
+ this.openCanvasEditor = (event) => {
141
+ if (event) {
142
+ event.stopPropagation();
143
+ }
144
+ this.showModal = false;
145
+ this.showCanvasEditor = true;
146
+ // Initialize canvas after a short delay to ensure DOM is ready
147
+ setTimeout(() => {
148
+ this.initializeCanvas();
149
+ }, 100);
150
+ };
151
+ this.closeCanvasEditor = () => {
152
+ this.showCanvasEditor = false;
153
+ this.showModal = true;
154
+ };
155
+ this.saveAnnotations = () => {
156
+ if (this.canvasRef) {
157
+ // Create final image with annotations
158
+ const finalDataUrl = this.canvasRef.toDataURL('image/png');
159
+ this.encodedScreenshot = finalDataUrl;
160
+ }
161
+ this.showCanvasEditor = false;
162
+ this.showModal = true;
163
+ };
164
+ this.initializeCanvas = () => {
165
+ if (!this.canvasRef || !this.originalImageData)
166
+ return;
167
+ this.canvasContext = this.canvasRef.getContext('2d');
168
+ const img = new Image();
169
+ img.onload = () => {
170
+ // Set canvas to original image dimensions
171
+ this.canvasRef.width = img.width;
172
+ this.canvasRef.height = img.height;
173
+ // Get available container dimensions
174
+ const containerWidth = this.canvasRef.parentElement.clientWidth - 32; // Account for reduced padding (16px * 2)
175
+ const containerHeight = this.canvasRef.parentElement.clientHeight - 32;
176
+ // Calculate scale factors for both dimensions
177
+ const scaleX = containerWidth / img.width;
178
+ const scaleY = containerHeight / img.height;
179
+ // Use the smaller scale to ensure complete image fits
180
+ const scale = Math.min(scaleX, scaleY, 1); // Never scale up, only down
181
+ // Calculate final display dimensions
182
+ const displayWidth = img.width * scale;
183
+ const displayHeight = img.height * scale;
184
+ // Set CSS size for display (this scales the canvas visually)
185
+ this.canvasRef.style.width = `${displayWidth}px`;
186
+ this.canvasRef.style.height = `${displayHeight}px`;
187
+ console.log('Canvas initialized with complete image fit:', {
188
+ originalWidth: img.width,
189
+ originalHeight: img.height,
190
+ displayWidth,
191
+ displayHeight,
192
+ scale,
193
+ scaleX,
194
+ scaleY,
195
+ containerWidth,
196
+ containerHeight,
197
+ usingScale: scale === scaleX ? 'width-limited' : 'height-limited'
198
+ });
199
+ // Draw the original image at full resolution
200
+ this.canvasContext.drawImage(img, 0, 0);
201
+ // Redraw existing annotations
202
+ this.redrawAnnotations();
203
+ };
204
+ img.src = this.originalImageData;
205
+ };
206
+ this.redrawAnnotations = () => {
207
+ if (!this.canvasContext)
208
+ return;
209
+ // Clear and redraw background image
210
+ const img = new Image();
211
+ img.onload = () => {
212
+ this.canvasContext.clearRect(0, 0, this.canvasRef.width, this.canvasRef.height);
213
+ this.canvasContext.drawImage(img, 0, 0);
214
+ // Draw all annotations
215
+ this.annotations.forEach(annotation => {
216
+ this.drawAnnotation(annotation);
217
+ });
218
+ };
219
+ img.src = this.originalImageData;
220
+ };
221
+ this.drawAnnotation = (annotation) => {
222
+ if (!this.canvasContext)
223
+ return;
224
+ this.canvasContext.strokeStyle = annotation.color;
225
+ this.canvasContext.lineWidth = annotation.lineWidth;
226
+ this.canvasContext.lineCap = 'round';
227
+ this.canvasContext.lineJoin = 'round';
228
+ switch (annotation.type) {
229
+ case 'rectangle':
230
+ this.canvasContext.strokeRect(annotation.startX, annotation.startY, annotation.width, annotation.height);
231
+ // Rectangle resize handles disabled for now
232
+ break;
233
+ case 'line':
234
+ this.canvasContext.beginPath();
235
+ this.canvasContext.moveTo(annotation.startX, annotation.startY);
236
+ this.canvasContext.lineTo(annotation.endX, annotation.endY);
237
+ this.canvasContext.stroke();
238
+ // Draw resize handles if this annotation is hovered
239
+ if (this.hoveredAnnotation === annotation) {
240
+ this.drawLineResizeHandles(annotation);
241
+ }
242
+ break;
243
+ case 'arrow':
244
+ this.drawArrow(annotation.startX, annotation.startY, annotation.endX, annotation.endY);
245
+ // Draw resize handles if this annotation is hovered
246
+ if (this.hoveredAnnotation === annotation) {
247
+ this.drawLineResizeHandles(annotation); // Same as line
248
+ }
249
+ break;
250
+ case 'text':
251
+ const fontSize = annotation.fontSize || 16;
252
+ this.canvasContext.fillStyle = annotation.color;
253
+ this.canvasContext.font = `${fontSize}px Arial`;
254
+ this.canvasContext.fillText(annotation.text, annotation.x, annotation.y);
255
+ // Draw resize handle if this annotation is hovered
256
+ if (this.hoveredAnnotation === annotation) {
257
+ this.drawTextResizeHandle(annotation);
258
+ }
259
+ break;
260
+ }
261
+ };
262
+ // Draw resize handle for text annotation
263
+ this.drawTextResizeHandle = (annotation) => {
264
+ if (!this.canvasContext || annotation.type !== 'text')
265
+ return;
266
+ const fontSize = annotation.fontSize || 16;
267
+ const textWidth = this.getTextWidth(annotation.text, fontSize);
268
+ const handleSize = 8;
269
+ const handleX = annotation.x + textWidth;
270
+ const handleY = annotation.y;
271
+ // Draw resize handle (small square) - using widget primary color
272
+ this.canvasContext.fillStyle = '#0070F4'; // var(--feedback-primary-color)
273
+ this.canvasContext.strokeStyle = '#ffffff';
274
+ this.canvasContext.lineWidth = 2;
275
+ this.canvasContext.fillRect(handleX - handleSize / 2, handleY - handleSize / 2, handleSize, handleSize);
276
+ this.canvasContext.strokeRect(handleX - handleSize / 2, handleY - handleSize / 2, handleSize, handleSize);
277
+ };
278
+ this.drawArrow = (fromX, fromY, toX, toY) => {
279
+ const headlen = 15; // Arrow head length
280
+ const angle = Math.atan2(toY - fromY, toX - fromX);
281
+ // Draw line
282
+ this.canvasContext.beginPath();
283
+ this.canvasContext.moveTo(fromX, fromY);
284
+ this.canvasContext.lineTo(toX, toY);
285
+ this.canvasContext.stroke();
286
+ // Draw arrow head
287
+ this.canvasContext.beginPath();
288
+ this.canvasContext.moveTo(toX, toY);
289
+ this.canvasContext.lineTo(toX - headlen * Math.cos(angle - Math.PI / 6), toY - headlen * Math.sin(angle - Math.PI / 6));
290
+ this.canvasContext.moveTo(toX, toY);
291
+ this.canvasContext.lineTo(toX - headlen * Math.cos(angle + Math.PI / 6), toY - headlen * Math.sin(angle + Math.PI / 6));
292
+ this.canvasContext.stroke();
293
+ };
294
+ this.undoLastAnnotation = () => {
295
+ this.annotations = this.annotations.slice(0, -1);
296
+ this.redrawAnnotations();
297
+ };
298
+ // Handle color slot editing
299
+ this.handleColorSlotClick = (colorIndex) => {
300
+ if (this.editingColorIndex === colorIndex) {
301
+ // If already editing this slot, just select the color
302
+ this.canvasDrawingColor = this.defaultColors[colorIndex];
303
+ this.showColorPicker = false;
304
+ this.editingColorIndex = -1;
305
+ }
306
+ else {
307
+ // Start editing this color slot
308
+ this.editingColorIndex = colorIndex;
309
+ this.showColorPicker = true;
310
+ this.canvasDrawingColor = this.defaultColors[colorIndex];
311
+ }
312
+ };
313
+ // Update color in slot
314
+ this.updateColorSlot = (newColor) => {
315
+ if (this.editingColorIndex >= 0 && this.editingColorIndex < this.defaultColors.length) {
316
+ this.defaultColors[this.editingColorIndex] = newColor;
317
+ this.canvasDrawingColor = newColor;
318
+ this.showColorPicker = false;
319
+ this.editingColorIndex = -1;
320
+ // Force reactivity
321
+ this.defaultColors = [...this.defaultColors];
322
+ }
323
+ };
324
+ // Handle color picker input without closing
325
+ this.handleColorPickerInput = (event) => {
326
+ event.stopPropagation();
327
+ const newColor = event.target.value;
328
+ if (this.editingColorIndex >= 0 && this.editingColorIndex < this.defaultColors.length) {
329
+ this.defaultColors[this.editingColorIndex] = newColor;
330
+ this.canvasDrawingColor = newColor;
331
+ // Force reactivity
332
+ this.defaultColors = [...this.defaultColors];
333
+ }
334
+ };
335
+ // Handle color picker click to prevent closing
336
+ this.handleColorPickerClick = (event) => {
337
+ event.stopPropagation();
338
+ };
339
+ // Close color picker
340
+ this.closeColorPicker = () => {
341
+ this.showColorPicker = false;
342
+ this.editingColorIndex = -1;
343
+ };
344
+ // Check if point is in resize handle for any annotation type
345
+ this.isPointInResizeHandle = (x, y, annotation) => {
346
+ const handleSize = 8;
347
+ switch (annotation.type) {
348
+ case 'text':
349
+ const textWidth = this.getTextWidth(annotation.text, annotation.fontSize || 16);
350
+ const handleX = annotation.x + textWidth;
351
+ const handleY = annotation.y;
352
+ return x >= handleX - handleSize / 2 && x <= handleX + handleSize / 2 &&
353
+ y >= handleY - handleSize / 2 && y <= handleY + handleSize / 2;
354
+ case 'rectangle':
355
+ // Rectangle resizing disabled for now
356
+ return false;
357
+ case 'line':
358
+ case 'arrow':
359
+ // Check both endpoint handles
360
+ const lineHandles = [
361
+ { x: annotation.startX, y: annotation.startY, point: 'start' },
362
+ { x: annotation.endX, y: annotation.endY, point: 'end' }
363
+ ];
364
+ for (const handle of lineHandles) {
365
+ if (x >= handle.x - handleSize / 2 && x <= handle.x + handleSize / 2 &&
366
+ y >= handle.y - handleSize / 2 && y <= handle.y + handleSize / 2) {
367
+ return handle.point; // Return which endpoint was clicked
368
+ }
369
+ }
370
+ return false;
371
+ default:
372
+ return false;
373
+ }
374
+ };
375
+ // Get text width for resize handle positioning
376
+ this.getTextWidth = (text, fontSize) => {
377
+ // Approximate text width calculation
378
+ return text.length * fontSize * 0.6;
379
+ };
380
+ // Start text resize
381
+ this.startTextResize = (annotation, startPos) => {
382
+ this.isResizing = true;
383
+ this.resizingAnnotation = annotation;
384
+ this.resizeStartSize = annotation.fontSize || 16;
385
+ this.dragStartPos = startPos;
386
+ };
387
+ // Handle text resize
388
+ this.handleTextResize = (currentPos) => {
389
+ if (!this.resizingAnnotation || !this.dragStartPos)
390
+ return;
391
+ const deltaX = currentPos.x - this.dragStartPos.x;
392
+ const deltaY = currentPos.y - this.dragStartPos.y;
393
+ const avgDelta = (deltaX + deltaY) / 2;
394
+ // Calculate new font size (minimum 8px, maximum 72px)
395
+ const newSize = Math.max(8, Math.min(72, this.resizeStartSize + avgDelta * 0.5));
396
+ // Update annotation font size
397
+ const index = this.annotations.findIndex(a => a === this.resizingAnnotation);
398
+ if (index !== -1) {
399
+ this.annotations[index] = Object.assign(Object.assign({}, this.resizingAnnotation), { fontSize: Math.round(newSize) });
400
+ this.resizingAnnotation = this.annotations[index];
401
+ }
402
+ this.redrawAnnotations();
403
+ };
404
+ // Start resize for any annotation type
405
+ this.startResize = (annotation, handle, startPos) => {
406
+ this.isResizing = true;
407
+ this.resizingAnnotation = annotation;
408
+ this.resizeHandle = handle;
409
+ this.dragStartPos = startPos;
410
+ // Store original values for different annotation types
411
+ if (annotation.type === 'text') {
412
+ this.resizeStartSize = annotation.fontSize || 16;
413
+ }
414
+ };
415
+ // Enhanced mouse down handler with resize detection for all annotation types
416
+ this.handleCanvasMouseDown = (event) => {
417
+ if (!this.canvasRef)
418
+ return;
419
+ // Close color picker if open
420
+ if (this.showColorPicker) {
421
+ this.closeColorPicker();
422
+ }
423
+ const coords = this.getCanvasCoordinates(event);
424
+ // Check if clicking on existing annotation first
425
+ const found = this.findAnnotationAt(coords.x, coords.y);
426
+ if (found) {
427
+ // Check if clicking on resize handle for any annotation type
428
+ const handle = this.isPointInResizeHandle(coords.x, coords.y, found.annotation);
429
+ if (handle) {
430
+ this.startResize(found.annotation, handle, coords);
431
+ this.canvasRef.style.cursor = 'nw-resize';
432
+ return;
433
+ }
434
+ // Start dragging existing annotation
435
+ if (!this.isDrawing) {
436
+ this.isDragging = true;
437
+ this.draggedAnnotation = found.annotation;
438
+ this.dragStartPos = coords;
439
+ this.canvasRef.style.cursor = 'grabbing';
440
+ return;
441
+ }
442
+ }
443
+ // Original drawing logic
444
+ this.isDrawing = true;
445
+ if (this.canvasDrawingTool === 'text') {
446
+ const text = prompt('Enter text:');
447
+ if (text) {
448
+ const annotation = {
449
+ type: 'text',
450
+ x: coords.x,
451
+ y: coords.y,
452
+ text,
453
+ color: this.canvasDrawingColor,
454
+ fontSize: 16
455
+ };
456
+ this.annotations = [...this.annotations, annotation];
457
+ this.redrawAnnotations();
458
+ }
459
+ this.isDrawing = false;
460
+ }
461
+ else {
462
+ this.currentAnnotation = {
463
+ type: this.canvasDrawingTool,
464
+ startX: coords.x,
465
+ startY: coords.y,
466
+ color: this.canvasDrawingColor,
467
+ lineWidth: this.canvasLineWidth
468
+ };
469
+ }
470
+ };
471
+ this.handleCanvasMouseMove = (event) => {
472
+ if (!this.canvasRef)
473
+ return;
474
+ const coords = this.getCanvasCoordinates(event);
475
+ // Handle resizing for any annotation type
476
+ if (this.isResizing && this.resizingAnnotation) {
477
+ this.handleResize(coords);
478
+ return;
479
+ }
480
+ // Handle dragging existing annotation
481
+ if (this.isDragging && this.draggedAnnotation && this.dragStartPos) {
482
+ const deltaX = coords.x - this.dragStartPos.x;
483
+ const deltaY = coords.y - this.dragStartPos.y;
484
+ // Update annotation position
485
+ const updatedAnnotation = Object.assign({}, this.draggedAnnotation);
486
+ switch (updatedAnnotation.type) {
487
+ case 'rectangle':
488
+ updatedAnnotation.startX += deltaX;
489
+ updatedAnnotation.startY += deltaY;
490
+ break;
491
+ case 'line':
492
+ case 'arrow':
493
+ updatedAnnotation.startX += deltaX;
494
+ updatedAnnotation.startY += deltaY;
495
+ updatedAnnotation.endX += deltaX;
496
+ updatedAnnotation.endY += deltaY;
497
+ break;
498
+ case 'text':
499
+ updatedAnnotation.x += deltaX;
500
+ updatedAnnotation.y += deltaY;
501
+ break;
502
+ }
503
+ // Update annotation in array
504
+ const index = this.annotations.findIndex(a => a === this.draggedAnnotation);
505
+ if (index !== -1) {
506
+ this.annotations[index] = updatedAnnotation;
507
+ this.draggedAnnotation = updatedAnnotation;
508
+ }
509
+ this.dragStartPos = coords;
510
+ this.redrawAnnotations();
511
+ return;
512
+ }
513
+ // Handle drawing new annotation
514
+ if (this.isDrawing && this.currentAnnotation) {
515
+ if (this.canvasDrawingTool === 'rectangle') {
516
+ this.currentAnnotation.width = coords.x - this.currentAnnotation.startX;
517
+ this.currentAnnotation.height = coords.y - this.currentAnnotation.startY;
518
+ }
519
+ else {
520
+ this.currentAnnotation.endX = coords.x;
521
+ this.currentAnnotation.endY = coords.y;
522
+ }
523
+ this.redrawAnnotations();
524
+ this.drawAnnotation(this.currentAnnotation);
525
+ return;
526
+ }
527
+ // Handle hover states and cursor changes
528
+ const found = this.findAnnotationAt(coords.x, coords.y);
529
+ if (found) {
530
+ // Check if hovering over resize handle for any annotation type
531
+ const handle = this.isPointInResizeHandle(coords.x, coords.y, found.annotation);
532
+ if (handle) {
533
+ this.canvasRef.style.cursor = 'nw-resize';
534
+ this.hoveredAnnotation = found.annotation;
535
+ this.redrawAnnotations();
536
+ return;
537
+ }
538
+ // Regular hover over annotation
539
+ this.canvasRef.style.cursor = 'grab';
540
+ if (this.hoveredAnnotation !== found.annotation) {
541
+ this.hoveredAnnotation = found.annotation;
542
+ this.redrawAnnotations();
543
+ }
544
+ }
545
+ else {
546
+ // No annotation under cursor
547
+ this.canvasRef.style.cursor = 'crosshair';
548
+ if (this.hoveredAnnotation) {
549
+ this.hoveredAnnotation = null;
550
+ this.redrawAnnotations();
551
+ }
552
+ }
553
+ };
554
+ this.handleCanvasMouseUp = () => {
555
+ // Handle end of text resizing
556
+ if (this.isResizing) {
557
+ this.isResizing = false;
558
+ this.resizingAnnotation = null;
559
+ this.dragStartPos = null;
560
+ this.resizeHandle = false;
561
+ if (this.canvasRef) {
562
+ this.canvasRef.style.cursor = 'crosshair';
563
+ }
564
+ return;
565
+ }
566
+ // Handle end of dragging
567
+ if (this.isDragging) {
568
+ this.isDragging = false;
569
+ this.draggedAnnotation = null;
570
+ this.dragStartPos = null;
571
+ if (this.canvasRef) {
572
+ this.canvasRef.style.cursor = 'crosshair';
573
+ }
574
+ return;
575
+ }
576
+ // Handle end of drawing
577
+ if (!this.isDrawing || !this.currentAnnotation)
578
+ return;
579
+ this.isDrawing = false;
580
+ this.annotations = [...this.annotations, this.currentAnnotation];
581
+ this.currentAnnotation = null;
582
+ this.redrawAnnotations();
583
+ };
584
+ // Draw resize handles for rectangle annotation
585
+ this.drawRectangleResizeHandles = (annotation) => {
586
+ if (!this.canvasContext || annotation.type !== 'rectangle')
587
+ return;
588
+ const handleSize = 8;
589
+ const left = annotation.startX;
590
+ const top = annotation.startY;
591
+ const right = annotation.startX + annotation.width;
592
+ const bottom = annotation.startY + annotation.height;
593
+ // Define handle positions (4 corners)
594
+ const handles = [
595
+ { x: left, y: top },
596
+ { x: right, y: top },
597
+ { x: right, y: bottom },
598
+ { x: left, y: bottom } // Bottom-left
599
+ ];
600
+ // Draw each handle
601
+ this.canvasContext.fillStyle = '#0070F4'; // Primary color
602
+ this.canvasContext.strokeStyle = '#ffffff';
603
+ this.canvasContext.lineWidth = 2;
604
+ handles.forEach(handle => {
605
+ this.canvasContext.fillRect(handle.x - handleSize / 2, handle.y - handleSize / 2, handleSize, handleSize);
606
+ this.canvasContext.strokeRect(handle.x - handleSize / 2, handle.y - handleSize / 2, handleSize, handleSize);
607
+ });
608
+ };
609
+ // Draw resize handles for line/arrow annotation
610
+ this.drawLineResizeHandles = (annotation) => {
611
+ if (!this.canvasContext || (annotation.type !== 'line' && annotation.type !== 'arrow'))
612
+ return;
613
+ const handleSize = 8;
614
+ // Define handle positions (2 endpoints)
615
+ const handles = [
616
+ { x: annotation.startX, y: annotation.startY },
617
+ { x: annotation.endX, y: annotation.endY } // End point
618
+ ];
619
+ // Draw each handle
620
+ this.canvasContext.fillStyle = '#0070F4'; // Primary color
621
+ this.canvasContext.strokeStyle = '#ffffff';
622
+ this.canvasContext.lineWidth = 2;
623
+ handles.forEach(handle => {
624
+ this.canvasContext.fillRect(handle.x - handleSize / 2, handle.y - handleSize / 2, handleSize, handleSize);
625
+ this.canvasContext.strokeRect(handle.x - handleSize / 2, handle.y - handleSize / 2, handleSize, handleSize);
626
+ });
627
+ };
628
+ // Convert screen coordinates to canvas coordinates
629
+ this.getCanvasCoordinates = (event) => {
630
+ if (!this.canvasRef)
631
+ return { x: 0, y: 0 };
632
+ const rect = this.canvasRef.getBoundingClientRect();
633
+ // Calculate the scale factor between display size and actual canvas size
634
+ const scaleX = this.canvasRef.width / rect.width;
635
+ const scaleY = this.canvasRef.height / rect.height;
636
+ const x = (event.clientX - rect.left) * scaleX;
637
+ const y = (event.clientY - rect.top) * scaleY;
638
+ return { x, y };
639
+ };
640
+ // Find annotation under mouse cursor
641
+ this.findAnnotationAt = (x, y) => {
642
+ // Check in reverse order (top to bottom)
643
+ for (let i = this.annotations.length - 1; i >= 0; i--) {
644
+ const annotation = this.annotations[i];
645
+ if (this.isPointInAnnotation(x, y, annotation)) {
646
+ return { annotation, index: i };
647
+ }
648
+ }
649
+ return null;
650
+ };
651
+ // Check if point is within annotation bounds
652
+ this.isPointInAnnotation = (x, y, annotation) => {
653
+ const tolerance = 10; // Click tolerance
654
+ switch (annotation.type) {
655
+ case 'rectangle':
656
+ const left = Math.min(annotation.startX, annotation.startX + annotation.width);
657
+ const right = Math.max(annotation.startX, annotation.startX + annotation.width);
658
+ const top = Math.min(annotation.startY, annotation.startY + annotation.height);
659
+ const bottom = Math.max(annotation.startY, annotation.startY + annotation.height);
660
+ return x >= left - tolerance && x <= right + tolerance &&
661
+ y >= top - tolerance && y <= bottom + tolerance;
662
+ case 'line':
663
+ case 'arrow':
664
+ // Distance from point to line
665
+ const A = annotation.endY - annotation.startY;
666
+ const B = annotation.startX - annotation.endX;
667
+ const C = annotation.endX * annotation.startY - annotation.startX * annotation.endY;
668
+ const distance = Math.abs(A * x + B * y + C) / Math.sqrt(A * A + B * B);
669
+ return distance <= tolerance;
670
+ case 'text':
671
+ // Simple bounding box for text
672
+ return x >= annotation.x - tolerance && x <= annotation.x + 100 &&
673
+ y >= annotation.y - 20 && y <= annotation.y + tolerance;
674
+ default:
675
+ return false;
676
+ }
677
+ };
678
+ // Handle resize for different annotation types
679
+ this.handleResize = (currentPos) => {
680
+ if (!this.resizingAnnotation || !this.dragStartPos)
681
+ return;
682
+ const annotation = this.resizingAnnotation;
683
+ const index = this.annotations.findIndex(a => a === annotation);
684
+ if (index === -1)
685
+ return;
686
+ let updatedAnnotation = Object.assign({}, annotation);
687
+ switch (annotation.type) {
688
+ case 'text':
689
+ // Text resize logic (existing)
690
+ const deltaX = currentPos.x - this.dragStartPos.x;
691
+ const deltaY = currentPos.y - this.dragStartPos.y;
692
+ const avgDelta = (deltaX + deltaY) / 2;
693
+ const newSize = Math.max(8, Math.min(72, this.resizeStartSize + avgDelta * 0.5));
694
+ updatedAnnotation.fontSize = Math.round(newSize);
695
+ break;
696
+ case 'rectangle':
697
+ // Rectangle resizing disabled for now
698
+ return;
699
+ case 'line':
700
+ case 'arrow':
701
+ // Line/arrow resize logic - move endpoints
702
+ if (this.resizeHandle === 'start') {
703
+ updatedAnnotation.startX = currentPos.x;
704
+ updatedAnnotation.startY = currentPos.y;
705
+ }
706
+ else if (this.resizeHandle === 'end') {
707
+ updatedAnnotation.endX = currentPos.x;
708
+ updatedAnnotation.endY = currentPos.y;
709
+ }
710
+ break;
711
+ }
712
+ // Update annotation in array
713
+ this.annotations[index] = updatedAnnotation;
714
+ this.resizingAnnotation = updatedAnnotation;
715
+ this.redrawAnnotations();
716
+ };
227
717
  this.sending = false;
228
718
  this.formMessage = '';
229
719
  this.formEmail = '';
@@ -237,6 +727,26 @@ export class FeedbackModal {
237
727
  this.selectedRating = -1;
238
728
  this.overlayVisible = false;
239
729
  this.isAnimating = false;
730
+ this.takingScreenshot = false;
731
+ this.showPreviewModal = false;
732
+ this.showCanvasEditor = false;
733
+ this.canvasDrawingTool = 'rectangle';
734
+ this.canvasDrawingColor = '#ff0000';
735
+ this.canvasLineWidth = 3;
736
+ this.isDrawing = false;
737
+ this.annotations = [];
738
+ this.currentAnnotation = null;
739
+ this.isDragging = false;
740
+ this.draggedAnnotation = null;
741
+ this.dragStartPos = null;
742
+ this.showColorPicker = false;
743
+ this.editingColorIndex = -1;
744
+ this.isResizing = false;
745
+ this.resizingAnnotation = null;
746
+ this.resizeStartSize = 16;
747
+ this.hoveredAnnotation = null;
748
+ this.resizeHandle = false;
749
+ this.defaultColors = ['#ff0000', '#00ff00', '#0000ff', '#000000'];
240
750
  this.customFont = false;
241
751
  this.emailAddress = '';
242
752
  this.hideEmail = false;
@@ -272,8 +782,9 @@ export class FeedbackModal {
272
782
  this.screenshotTakingText = 'Taking screenshot...';
273
783
  this.screenshotTopbarText = 'Select an element on this page';
274
784
  this.successMessage = '';
275
- this.takingScreenshot = false;
276
- this.showPreviewModal = false;
785
+ this.canvasEditorTitle = 'Edit screenshot';
786
+ this.canvasEditorCancelText = 'Cancel';
787
+ this.canvasEditorSaveText = 'Save';
277
788
  }
278
789
  componentWillLoad() {
279
790
  if (this.fetchData)
@@ -308,46 +819,51 @@ export class FeedbackModal {
308
819
  handleEmailInput(event) {
309
820
  this.formEmail = event.target.value;
310
821
  }
311
- captureScreenshot() {
822
+ captureViewportScreenshot() {
312
823
  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;
824
+ requestAnimationFrame(() => {
825
+ // Get viewport dimensions and scroll position
826
+ const viewportWidth = window.innerWidth;
827
+ const viewportHeight = window.innerHeight;
828
+ const scrollX = window.scrollX || window.pageXOffset || 0;
829
+ const scrollY = window.scrollY || window.pageYOffset || 0;
830
+ // Capture exactly what the user sees in their viewport
831
+ html2canvas(document.documentElement, {
832
+ x: scrollX,
833
+ y: scrollY,
834
+ width: viewportWidth,
835
+ height: viewportHeight,
836
+ scrollX: 0,
837
+ scrollY: 0,
838
+ allowTaint: false,
839
+ useCORS: true,
840
+ scale: 1,
841
+ backgroundColor: '#ffffff',
842
+ logging: false,
843
+ foreignObjectRendering: false,
844
+ imageTimeout: 15000,
845
+ windowWidth: viewportWidth,
846
+ windowHeight: viewportHeight,
847
+ removeContainer: true,
848
+ ignoreElements: (element) => {
849
+ // Ignore all feedback modal elements
850
+ return element.closest('feedback-modal') !== null ||
851
+ element.classList.contains('feedback-overlay') ||
852
+ element.classList.contains('feedback-modal-screenshot-header') ||
853
+ element.tagName === 'FEEDBACK-MODAL' ||
854
+ element.tagName === 'FEEDBACK-BUTTON';
319
855
  }
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');
339
- }
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
- });
856
+ })
857
+ .then((canvas) => {
858
+ const dataUrl = canvas.toDataURL('image/png');
859
+ console.log('Screenshot captured successfully, size:', canvas.width, 'x', canvas.height);
860
+ resolve(dataUrl);
861
+ })
862
+ .catch((error) => {
863
+ console.error('Failed to capture viewport screenshot:', error);
864
+ reject(error);
349
865
  });
350
- }, 100); // Small delay to ensure CSS is applied
866
+ });
351
867
  });
352
868
  }
353
869
  handleCheckboxChange(event) {
@@ -360,7 +876,7 @@ export class FeedbackModal {
360
876
  this.selectedRating = newRating;
361
877
  }
362
878
  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
879
+ return (h("div", { class: `feedback-modal-wrapper ${this.customFont ? 'feedback-modal-wrapper--custom-font' : ''}` }, 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
880
  ? 'feedback-modal-rating-button--selected'
365
881
  : ''}`, onClick: (event) => {
366
882
  event.preventDefault();
@@ -375,7 +891,8 @@ export class FeedbackModal {
375
891
  : ''}`, onClick: (event) => {
376
892
  event.preventDefault();
377
893
  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() }))))));
894
+ } }, 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 16 16" }, h("circle", { cx: "8", cy: "8", r: "6", fill: "none", stroke: "#666", "stroke-width": "2", "stroke-dasharray": "6 6", "transform-origin": "8 8" }, h("animateTransform", { attributeName: "transform", type: "rotate", values: "0 8 8;360 8 8", dur: "1s", repeatCount: "indefinite" }))))), this.takingScreenshot ? this.screenshotTakingText :
895
+ 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
896
  }
380
897
  componentDidRender() {
381
898
  if (this.showModal) {
@@ -1033,6 +1550,60 @@ export class FeedbackModal {
1033
1550
  "attribute": "success-message",
1034
1551
  "reflect": false,
1035
1552
  "defaultValue": "''"
1553
+ },
1554
+ "canvasEditorTitle": {
1555
+ "type": "string",
1556
+ "mutable": false,
1557
+ "complexType": {
1558
+ "original": "string",
1559
+ "resolved": "string",
1560
+ "references": {}
1561
+ },
1562
+ "required": false,
1563
+ "optional": false,
1564
+ "docs": {
1565
+ "tags": [],
1566
+ "text": ""
1567
+ },
1568
+ "attribute": "canvas-editor-title",
1569
+ "reflect": false,
1570
+ "defaultValue": "'Edit screenshot'"
1571
+ },
1572
+ "canvasEditorCancelText": {
1573
+ "type": "string",
1574
+ "mutable": false,
1575
+ "complexType": {
1576
+ "original": "string",
1577
+ "resolved": "string",
1578
+ "references": {}
1579
+ },
1580
+ "required": false,
1581
+ "optional": false,
1582
+ "docs": {
1583
+ "tags": [],
1584
+ "text": ""
1585
+ },
1586
+ "attribute": "canvas-editor-cancel-text",
1587
+ "reflect": false,
1588
+ "defaultValue": "'Cancel'"
1589
+ },
1590
+ "canvasEditorSaveText": {
1591
+ "type": "string",
1592
+ "mutable": false,
1593
+ "complexType": {
1594
+ "original": "string",
1595
+ "resolved": "string",
1596
+ "references": {}
1597
+ },
1598
+ "required": false,
1599
+ "optional": false,
1600
+ "docs": {
1601
+ "tags": [],
1602
+ "text": ""
1603
+ },
1604
+ "attribute": "canvas-editor-save-text",
1605
+ "reflect": false,
1606
+ "defaultValue": "'Save'"
1036
1607
  }
1037
1608
  };
1038
1609
  }
@@ -1052,7 +1623,25 @@ export class FeedbackModal {
1052
1623
  "overlayVisible": {},
1053
1624
  "isAnimating": {},
1054
1625
  "takingScreenshot": {},
1055
- "showPreviewModal": {}
1626
+ "showPreviewModal": {},
1627
+ "showCanvasEditor": {},
1628
+ "canvasDrawingTool": {},
1629
+ "canvasDrawingColor": {},
1630
+ "canvasLineWidth": {},
1631
+ "isDrawing": {},
1632
+ "annotations": {},
1633
+ "currentAnnotation": {},
1634
+ "isDragging": {},
1635
+ "draggedAnnotation": {},
1636
+ "dragStartPos": {},
1637
+ "showColorPicker": {},
1638
+ "editingColorIndex": {},
1639
+ "isResizing": {},
1640
+ "resizingAnnotation": {},
1641
+ "resizeStartSize": {},
1642
+ "hoveredAnnotation": {},
1643
+ "resizeHandle": {},
1644
+ "defaultColors": {}
1056
1645
  };
1057
1646
  }
1058
1647
  static get events() {