fix: canvas fits image exactly, chained annotation loading, arrow move tracking
- Canvas now sizes to match the scaled image dimensions (zero empty space) instead of using a fixed aspect ratio, fixing coordinate mismatch between editor and viewer - Annotations load only after background image is ready via _pendingAnnotations pattern, preventing placement at wrong coordinates - Arrow endpoints (arrowX1/Y1/X2/Y2) update on object:modified using transform delta, so moved arrows serialize at correct position - Coordinate scaling on load: coordScale = currentImageScale / savedImageScale handles annotations saved at different screen widths Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -64,6 +64,13 @@ function annotationEditor() {
|
|||||||
/** Whether the canvas has unsaved changes */
|
/** Whether the canvas has unsaved changes */
|
||||||
isDirty: false,
|
isDirty: false,
|
||||||
|
|
||||||
|
/** Natural (unscaled) dimensions of the loaded background image */
|
||||||
|
_bgImageNatW: 0,
|
||||||
|
_bgImageNatH: 0,
|
||||||
|
|
||||||
|
/** Annotations JSON to load after background image is ready */
|
||||||
|
_pendingAnnotations: null,
|
||||||
|
|
||||||
/** Current color for new annotations */
|
/** Current color for new annotations */
|
||||||
currentColor: '#2563EB',
|
currentColor: '#2563EB',
|
||||||
|
|
||||||
@@ -93,17 +100,118 @@ function annotationEditor() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Objects are move-only: no resize/rotate handles, just selection border
|
||||||
|
fabric.Object.prototype.hasControls = false;
|
||||||
|
fabric.Object.prototype.hasBorders = true;
|
||||||
|
fabric.Object.prototype.borderColor = '#2563EB';
|
||||||
|
fabric.Object.prototype.lockRotation = true;
|
||||||
|
fabric.Object.prototype.lockScalingX = true;
|
||||||
|
fabric.Object.prototype.lockScalingY = true;
|
||||||
|
// Disable object caching to avoid stale render states
|
||||||
|
fabric.Object.prototype.objectCaching = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.canvas = new fabric.Canvas(this.canvasEl, {
|
this.canvas = new fabric.Canvas(this.canvasEl, {
|
||||||
selection: true,
|
selection: true,
|
||||||
preserveObjectStacking: true,
|
preserveObjectStacking: true,
|
||||||
backgroundColor: '#f1f5f9',
|
backgroundColor: '#f1f5f9',
|
||||||
|
enableRetinaScaling: false,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('AnnotationEditor: failed to create canvas:', err);
|
console.error('AnnotationEditor: failed to create canvas:', err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
var canvas = this.canvas;
|
||||||
|
|
||||||
|
// ---- Fabric.js v5 controls-visibility workaround ----
|
||||||
|
//
|
||||||
|
// Fabric v5.3.1 draws object controls (resize/rotate handles) on the
|
||||||
|
// LOWER canvas. The UPPER canvas (contextTop) sits on top and can
|
||||||
|
// obscure those controls if it has stale content.
|
||||||
|
// Fix: force contextTopDirty = true before every renderAll().
|
||||||
|
|
||||||
|
var _origRenderAll = canvas.renderAll.bind(canvas);
|
||||||
|
canvas.renderAll = function () {
|
||||||
|
canvas.contextTopDirty = true;
|
||||||
|
return _origRenderAll();
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- Reliable pointer override ----
|
||||||
|
//
|
||||||
|
// Fabric.js v5.3.1 getPointer can produce wrong coordinates due to
|
||||||
|
// retina-scaling arithmetic, stale _offset cache, or scroll-offset
|
||||||
|
// mismatches. Replace with a minimal BCR-based calculation that is
|
||||||
|
// always correct: mouse-client-position minus canvas-BCR, scaled to
|
||||||
|
// canvas coordinate space.
|
||||||
|
|
||||||
|
canvas.getPointer = function (e, ignoreVpt) {
|
||||||
|
// Honour Fabric's per-event cache (set by _cacheTransformEventData)
|
||||||
|
if (canvas._absolutePointer && !ignoreVpt) {
|
||||||
|
return canvas._absolutePointer;
|
||||||
|
}
|
||||||
|
if (canvas._pointer && ignoreVpt) {
|
||||||
|
return canvas._pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract clientX/Y from mouse or touch event
|
||||||
|
var clientX = 0, clientY = 0;
|
||||||
|
if (e) {
|
||||||
|
if (e.clientX != null) {
|
||||||
|
clientX = e.clientX;
|
||||||
|
clientY = e.clientY;
|
||||||
|
} else if (e.changedTouches && e.changedTouches[0]) {
|
||||||
|
clientX = e.changedTouches[0].clientX;
|
||||||
|
clientY = e.changedTouches[0].clientY;
|
||||||
|
} else if (e.touches && e.touches[0]) {
|
||||||
|
clientX = e.touches[0].clientX;
|
||||||
|
clientY = e.touches[0].clientY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BCR of the upper canvas (where events are captured)
|
||||||
|
var rect = canvas.upperCanvasEl.getBoundingClientRect();
|
||||||
|
|
||||||
|
// Position relative to canvas element in CSS pixels
|
||||||
|
var x = clientX - rect.left;
|
||||||
|
var y = clientY - rect.top;
|
||||||
|
|
||||||
|
// Scale from CSS pixels to canvas internal coordinate space
|
||||||
|
// With enableRetinaScaling:false these should be 1:1, but handle
|
||||||
|
// any mismatch defensively.
|
||||||
|
if (rect.width > 0 && rect.height > 0) {
|
||||||
|
x = x * (canvas.width / rect.width);
|
||||||
|
y = y * (canvas.height / rect.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
// When ignoreVpt is false, return object-space coordinates
|
||||||
|
if (!ignoreVpt) {
|
||||||
|
return canvas.restorePointerVpt({ x: x, y: y });
|
||||||
|
}
|
||||||
|
return { x: x, y: y };
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- Keep canvas offset fresh ----
|
||||||
|
canvas.on('mouse:down:before', function () {
|
||||||
|
canvas.calcOffset();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Force move-only properties on every object added to the canvas
|
||||||
|
this.canvas.on('object:added', function (e) {
|
||||||
|
var obj = e.target;
|
||||||
|
if (obj && obj.objectType !== '_temp') {
|
||||||
|
obj.hasControls = false;
|
||||||
|
obj.hasBorders = true;
|
||||||
|
obj.selectable = true;
|
||||||
|
obj.evented = true;
|
||||||
|
obj.lockRotation = true;
|
||||||
|
obj.lockScalingX = true;
|
||||||
|
obj.lockScalingY = true;
|
||||||
|
obj.setCoords();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this.setupEvents();
|
this.setupEvents();
|
||||||
this.setupKeyboard();
|
this.setupKeyboard();
|
||||||
|
|
||||||
@@ -111,7 +219,6 @@ function annotationEditor() {
|
|||||||
window.addEventListener('resize', this._debouncedResize);
|
window.addEventListener('resize', this._debouncedResize);
|
||||||
|
|
||||||
// ---- Bridge: listen for commands from recipeEditor ----
|
// ---- Bridge: listen for commands from recipeEditor ----
|
||||||
var self = this;
|
|
||||||
|
|
||||||
this._onToolChange = function (e) {
|
this._onToolChange = function (e) {
|
||||||
if (e.detail && e.detail.tool) {
|
if (e.detail && e.detail.tool) {
|
||||||
@@ -125,6 +232,11 @@ function annotationEditor() {
|
|||||||
};
|
};
|
||||||
window.addEventListener('anno-delete-selected', this._onDeleteSelected);
|
window.addEventListener('anno-delete-selected', this._onDeleteSelected);
|
||||||
|
|
||||||
|
this._onClearAll = function () {
|
||||||
|
self.clearAll();
|
||||||
|
};
|
||||||
|
window.addEventListener('anno-clear-all', this._onClearAll);
|
||||||
|
|
||||||
this._onColorChange = function (e) {
|
this._onColorChange = function (e) {
|
||||||
if (e.detail && e.detail.color) {
|
if (e.detail && e.detail.color) {
|
||||||
self.currentColor = e.detail.color;
|
self.currentColor = e.detail.color;
|
||||||
@@ -253,12 +365,12 @@ function annotationEditor() {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal: load background image and optionally annotations.
|
* Internal: load background image and optionally annotations.
|
||||||
|
* Annotations are deferred until the image callback fires so that
|
||||||
|
* canvas dimensions and imageScale are known before placing objects.
|
||||||
*/
|
*/
|
||||||
_loadImageAndAnnotations(imageUrl, annotationsJson) {
|
_loadImageAndAnnotations(imageUrl, annotationsJson) {
|
||||||
|
this._pendingAnnotations = annotationsJson || null;
|
||||||
this.loadBackgroundImage(imageUrl);
|
this.loadBackgroundImage(imageUrl);
|
||||||
if (annotationsJson) {
|
|
||||||
this.loadAnnotationsJson(annotationsJson);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
@@ -325,6 +437,7 @@ function annotationEditor() {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Load a raster image URL as the canvas background.
|
* Load a raster image URL as the canvas background.
|
||||||
|
* The canvas is resized to fit the image exactly (no empty space).
|
||||||
*/
|
*/
|
||||||
_loadImageBackground(imageUrl) {
|
_loadImageBackground(imageUrl) {
|
||||||
var self = this;
|
var self = this;
|
||||||
@@ -337,35 +450,64 @@ function annotationEditor() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!self.canvas.width) {
|
self._bgImageNatW = img.width;
|
||||||
self.resizeCanvas();
|
self._bgImageNatH = img.height;
|
||||||
}
|
|
||||||
|
|
||||||
var cw = self.canvas.width || 800;
|
|
||||||
var ch = self.canvas.height || 480;
|
|
||||||
|
|
||||||
var scale = Math.min(
|
|
||||||
cw / img.width,
|
|
||||||
ch / img.height,
|
|
||||||
1 // never upscale
|
|
||||||
);
|
|
||||||
|
|
||||||
self.canvas.setBackgroundImage(
|
|
||||||
img,
|
|
||||||
self.canvas.renderAll.bind(self.canvas),
|
|
||||||
{
|
|
||||||
scaleX: scale,
|
|
||||||
scaleY: scale,
|
|
||||||
originX: 'left',
|
|
||||||
originY: 'top',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
|
self._fitCanvasToImage(img);
|
||||||
self.imageLoaded = true;
|
self.imageLoaded = true;
|
||||||
|
|
||||||
|
// Load pending annotations now that canvas is sized to the image
|
||||||
|
if (self._pendingAnnotations) {
|
||||||
|
self.loadAnnotationsJson(self._pendingAnnotations);
|
||||||
|
self._pendingAnnotations = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resize canvas to fit the background image exactly within the
|
||||||
|
* container width. Canvas dimensions = image dimensions * scale,
|
||||||
|
* so there is zero empty space and canvas coords map 1:1 to
|
||||||
|
* scaled-image coords.
|
||||||
|
*
|
||||||
|
* @param {fabric.Image} img - Fabric image object (background)
|
||||||
|
*/
|
||||||
|
_fitCanvasToImage(img) {
|
||||||
|
var fabricWrapper = this.canvasEl.parentElement;
|
||||||
|
var container = fabricWrapper ? fabricWrapper.parentElement : null;
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
var containerWidth = container.clientWidth;
|
||||||
|
if (!containerWidth) return;
|
||||||
|
|
||||||
|
var natW = img.width || this._bgImageNatW;
|
||||||
|
var natH = img.height || this._bgImageNatH;
|
||||||
|
if (!natW || !natH) return;
|
||||||
|
|
||||||
|
// Scale to fit container width; never upscale
|
||||||
|
var scale = Math.min(containerWidth / natW, 1);
|
||||||
|
var canvasW = Math.round(natW * scale);
|
||||||
|
var canvasH = Math.round(natH * scale);
|
||||||
|
|
||||||
|
this.canvas.setDimensions({ width: canvasW, height: canvasH });
|
||||||
|
|
||||||
|
this.canvas.setBackgroundImage(
|
||||||
|
img,
|
||||||
|
this.canvas.renderAll.bind(this.canvas),
|
||||||
|
{
|
||||||
|
scaleX: scale,
|
||||||
|
scaleY: scale,
|
||||||
|
originX: 'left',
|
||||||
|
originY: 'top',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
this.canvas.calcOffset();
|
||||||
|
this.canvas.forEachObject(function (obj) { obj.setCoords(); });
|
||||||
|
this.canvas.renderAll();
|
||||||
|
},
|
||||||
|
|
||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
// Object creation - Markers
|
// Object creation - Markers
|
||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
@@ -403,7 +545,12 @@ function annotationEditor() {
|
|||||||
left: x,
|
left: x,
|
||||||
top: y,
|
top: y,
|
||||||
selectable: true,
|
selectable: true,
|
||||||
|
evented: true,
|
||||||
|
hasControls: false,
|
||||||
hasBorders: true,
|
hasBorders: true,
|
||||||
|
lockRotation: true,
|
||||||
|
lockScalingX: true,
|
||||||
|
lockScalingY: true,
|
||||||
// Custom properties
|
// Custom properties
|
||||||
objectType: 'marker',
|
objectType: 'marker',
|
||||||
markerNumber: markerNumber,
|
markerNumber: markerNumber,
|
||||||
@@ -411,6 +558,7 @@ function annotationEditor() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.canvas.add(marker);
|
this.canvas.add(marker);
|
||||||
|
marker.setCoords();
|
||||||
this.canvas.setActiveObject(marker);
|
this.canvas.setActiveObject(marker);
|
||||||
this.isDirty = true;
|
this.isDirty = true;
|
||||||
window.dispatchEvent(new CustomEvent('annotations-changed', { detail: { json: this.getAnnotationsJson() } }));
|
window.dispatchEvent(new CustomEvent('annotations-changed', { detail: { json: this.getAnnotationsJson() } }));
|
||||||
@@ -454,6 +602,12 @@ function annotationEditor() {
|
|||||||
|
|
||||||
var arrow = new fabric.Group([line, arrowHead], {
|
var arrow = new fabric.Group([line, arrowHead], {
|
||||||
selectable: true,
|
selectable: true,
|
||||||
|
evented: true,
|
||||||
|
hasControls: false,
|
||||||
|
hasBorders: true,
|
||||||
|
lockRotation: true,
|
||||||
|
lockScalingX: true,
|
||||||
|
lockScalingY: true,
|
||||||
objectType: 'arrow',
|
objectType: 'arrow',
|
||||||
// Store original endpoints for serialization
|
// Store original endpoints for serialization
|
||||||
arrowX1: x1,
|
arrowX1: x1,
|
||||||
@@ -466,6 +620,7 @@ function annotationEditor() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.canvas.add(arrow);
|
this.canvas.add(arrow);
|
||||||
|
arrow.setCoords();
|
||||||
this.canvas.setActiveObject(arrow);
|
this.canvas.setActiveObject(arrow);
|
||||||
this.isDirty = true;
|
this.isDirty = true;
|
||||||
window.dispatchEvent(new CustomEvent('annotations-changed', { detail: { json: this.getAnnotationsJson() } }));
|
window.dispatchEvent(new CustomEvent('annotations-changed', { detail: { json: this.getAnnotationsJson() } }));
|
||||||
@@ -496,6 +651,12 @@ function annotationEditor() {
|
|||||||
strokeWidth: this.currentStrokeWidth,
|
strokeWidth: this.currentStrokeWidth,
|
||||||
strokeDashArray: this.currentLineDash.length > 0 ? this.currentLineDash.slice() : null,
|
strokeDashArray: this.currentLineDash.length > 0 ? this.currentLineDash.slice() : null,
|
||||||
selectable: true,
|
selectable: true,
|
||||||
|
evented: true,
|
||||||
|
hasControls: false,
|
||||||
|
hasBorders: true,
|
||||||
|
lockRotation: true,
|
||||||
|
lockScalingX: true,
|
||||||
|
lockScalingY: true,
|
||||||
objectType: 'area',
|
objectType: 'area',
|
||||||
areaColor: this.currentColor,
|
areaColor: this.currentColor,
|
||||||
areaStrokeWidth: this.currentStrokeWidth,
|
areaStrokeWidth: this.currentStrokeWidth,
|
||||||
@@ -503,6 +664,7 @@ function annotationEditor() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.canvas.add(rect);
|
this.canvas.add(rect);
|
||||||
|
rect.setCoords();
|
||||||
this.canvas.setActiveObject(rect);
|
this.canvas.setActiveObject(rect);
|
||||||
this.isDirty = true;
|
this.isDirty = true;
|
||||||
window.dispatchEvent(new CustomEvent('annotations-changed', { detail: { json: this.getAnnotationsJson() } }));
|
window.dispatchEvent(new CustomEvent('annotations-changed', { detail: { json: this.getAnnotationsJson() } }));
|
||||||
@@ -527,8 +689,6 @@ function annotationEditor() {
|
|||||||
this.canvas.forEachObject(function (o) {
|
this.canvas.forEachObject(function (o) {
|
||||||
o.selectable = true;
|
o.selectable = true;
|
||||||
o.evented = true;
|
o.evented = true;
|
||||||
o.hasControls = true;
|
|
||||||
o.hasBorders = true;
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// marker, arrow, rect — disable object interaction during drawing
|
// marker, arrow, rect — disable object interaction during drawing
|
||||||
@@ -562,6 +722,9 @@ function annotationEditor() {
|
|||||||
// ---- mouse:down ----
|
// ---- mouse:down ----
|
||||||
|
|
||||||
this.canvas.on('mouse:down', function (opt) {
|
this.canvas.on('mouse:down', function (opt) {
|
||||||
|
// In select mode, let Fabric.js handle clicks without interference
|
||||||
|
if (self.activeMode === 'select') return;
|
||||||
|
|
||||||
var pointer = self.canvas.getPointer(opt.e);
|
var pointer = self.canvas.getPointer(opt.e);
|
||||||
|
|
||||||
if (self.activeMode === 'marker') {
|
if (self.activeMode === 'marker') {
|
||||||
@@ -677,6 +840,12 @@ function annotationEditor() {
|
|||||||
detail: { markerNumber: selected[0].markerNumber }
|
detail: { markerNumber: selected[0].markerNumber }
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
// Force coords + re-render so controls are visible on lower canvas
|
||||||
|
var active = self.canvas.getActiveObject();
|
||||||
|
if (active) {
|
||||||
|
active.setCoords();
|
||||||
|
self.canvas.requestRenderAll();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.canvas.on('selection:updated', function (e) {
|
this.canvas.on('selection:updated', function (e) {
|
||||||
@@ -689,6 +858,12 @@ function annotationEditor() {
|
|||||||
detail: { markerNumber: selected[0].markerNumber }
|
detail: { markerNumber: selected[0].markerNumber }
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
// Force coords + re-render so controls are visible on lower canvas
|
||||||
|
var active = self.canvas.getActiveObject();
|
||||||
|
if (active) {
|
||||||
|
active.setCoords();
|
||||||
|
self.canvas.requestRenderAll();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.canvas.on('selection:cleared', function () {
|
this.canvas.on('selection:cleared', function () {
|
||||||
@@ -698,12 +873,18 @@ function annotationEditor() {
|
|||||||
|
|
||||||
// ---- Object modification ----
|
// ---- Object modification ----
|
||||||
|
|
||||||
this.canvas.on('object:modified', function () {
|
this.canvas.on('object:modified', function (e) {
|
||||||
self.isDirty = true;
|
// Update arrow stored endpoints after a move so serialization
|
||||||
window.dispatchEvent(new CustomEvent('annotations-changed', { detail: { json: self.getAnnotationsJson() } }));
|
// reflects the current position (not the original creation coords).
|
||||||
});
|
var obj = e.target;
|
||||||
|
if (obj && obj.objectType === 'arrow' && e.transform) {
|
||||||
this.canvas.on('object:moved', function () {
|
var dx = obj.left - e.transform.original.left;
|
||||||
|
var dy = obj.top - e.transform.original.top;
|
||||||
|
obj.arrowX1 = (obj.arrowX1 || 0) + dx;
|
||||||
|
obj.arrowY1 = (obj.arrowY1 || 0) + dy;
|
||||||
|
obj.arrowX2 = (obj.arrowX2 || 0) + dx;
|
||||||
|
obj.arrowY2 = (obj.arrowY2 || 0) + dy;
|
||||||
|
}
|
||||||
self.isDirty = true;
|
self.isDirty = true;
|
||||||
window.dispatchEvent(new CustomEvent('annotations-changed', { detail: { json: self.getAnnotationsJson() } }));
|
window.dispatchEvent(new CustomEvent('annotations-changed', { detail: { json: self.getAnnotationsJson() } }));
|
||||||
});
|
});
|
||||||
@@ -794,7 +975,13 @@ function annotationEditor() {
|
|||||||
resizeCanvas() {
|
resizeCanvas() {
|
||||||
if (!this.canvasEl || !this.canvas) return;
|
if (!this.canvasEl || !this.canvas) return;
|
||||||
|
|
||||||
// Fabric wrapper → .canvas-container (or whatever holds the canvas)
|
// If a background image is loaded, fit canvas to its dimensions
|
||||||
|
if (this.canvas.backgroundImage && this._bgImageNatW && this._bgImageNatH) {
|
||||||
|
this._fitCanvasToImage(this.canvas.backgroundImage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No image yet — use placeholder size so canvas is visible
|
||||||
var fabricWrapper = this.canvasEl.parentElement;
|
var fabricWrapper = this.canvasEl.parentElement;
|
||||||
var container = fabricWrapper ? fabricWrapper.parentElement : null;
|
var container = fabricWrapper ? fabricWrapper.parentElement : null;
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
@@ -802,8 +989,7 @@ function annotationEditor() {
|
|||||||
var width = container.clientWidth;
|
var width = container.clientWidth;
|
||||||
var height = Math.max(400, Math.round(width * 0.6));
|
var height = Math.max(400, Math.round(width * 0.6));
|
||||||
|
|
||||||
this.canvas.setWidth(width);
|
this.canvas.setDimensions({ width: width, height: height });
|
||||||
this.canvas.setHeight(height);
|
|
||||||
this.canvas.calcOffset();
|
this.canvas.calcOffset();
|
||||||
this.canvas.renderAll();
|
this.canvas.renderAll();
|
||||||
},
|
},
|
||||||
@@ -825,9 +1011,6 @@ function annotationEditor() {
|
|||||||
top: Math.round(obj.top * 100) / 100,
|
top: Math.round(obj.top * 100) / 100,
|
||||||
width: Math.round((obj.width || 0) * 100) / 100,
|
width: Math.round((obj.width || 0) * 100) / 100,
|
||||||
height: Math.round((obj.height || 0) * 100) / 100,
|
height: Math.round((obj.height || 0) * 100) / 100,
|
||||||
scaleX: Math.round((obj.scaleX || 1) * 1000) / 1000,
|
|
||||||
scaleY: Math.round((obj.scaleY || 1) * 1000) / 1000,
|
|
||||||
angle: Math.round((obj.angle || 0) * 100) / 100,
|
|
||||||
markerNumber: obj.markerNumber || null,
|
markerNumber: obj.markerNumber || null,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -851,10 +1034,16 @@ function annotationEditor() {
|
|||||||
return data;
|
return data;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Save background image scale so the preview can convert canvas coords
|
||||||
|
// to image coords correctly regardless of editor canvas dimensions.
|
||||||
|
var bgImg = this.canvas.backgroundImage;
|
||||||
|
var imageScale = bgImg ? (bgImg.scaleX || 1) : 1;
|
||||||
|
|
||||||
return JSON.stringify({
|
return JSON.stringify({
|
||||||
version: 1,
|
version: 2,
|
||||||
width: this.canvas.width,
|
width: this.canvas.width,
|
||||||
height: this.canvas.height,
|
height: this.canvas.height,
|
||||||
|
imageScale: imageScale,
|
||||||
objects: objects,
|
objects: objects,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -876,6 +1065,15 @@ function annotationEditor() {
|
|||||||
var data =
|
var data =
|
||||||
typeof jsonString === 'string' ? JSON.parse(jsonString) : jsonString;
|
typeof jsonString === 'string' ? JSON.parse(jsonString) : jsonString;
|
||||||
|
|
||||||
|
// Coordinate scale factor: annotations may have been saved at a
|
||||||
|
// different canvas size (different screen width). Since canvas
|
||||||
|
// coords = imageCoords * imageScale, we convert:
|
||||||
|
// currentCoord = savedCoord * (currentImageScale / savedImageScale)
|
||||||
|
var savedImageScale = data.imageScale || 1;
|
||||||
|
var bgImg = this.canvas.backgroundImage;
|
||||||
|
var currentImageScale = bgImg ? (bgImg.scaleX || 1) : 1;
|
||||||
|
var cs = currentImageScale / savedImageScale; // coordinate scale
|
||||||
|
|
||||||
// Remove existing objects
|
// Remove existing objects
|
||||||
this.canvas.getObjects().slice().forEach(
|
this.canvas.getObjects().slice().forEach(
|
||||||
function (obj) {
|
function (obj) {
|
||||||
@@ -891,15 +1089,8 @@ function annotationEditor() {
|
|||||||
if (obj.type === 'marker') {
|
if (obj.type === 'marker') {
|
||||||
var savedColor = this.currentColor;
|
var savedColor = this.currentColor;
|
||||||
if (obj.fill) this.currentColor = obj.fill;
|
if (obj.fill) this.currentColor = obj.fill;
|
||||||
var created = this.addMarker(obj.left, obj.top, obj.markerNumber);
|
this.addMarker(obj.left * cs, obj.top * cs, obj.markerNumber);
|
||||||
this.currentColor = savedColor;
|
this.currentColor = savedColor;
|
||||||
// Restore transforms (scale + rotation)
|
|
||||||
created.set({
|
|
||||||
angle: obj.angle || 0,
|
|
||||||
scaleX: obj.scaleX || 1,
|
|
||||||
scaleY: obj.scaleY || 1,
|
|
||||||
});
|
|
||||||
created.setCoords();
|
|
||||||
} else if (obj.type === 'arrow') {
|
} else if (obj.type === 'arrow') {
|
||||||
var savedColor2 = this.currentColor;
|
var savedColor2 = this.currentColor;
|
||||||
var savedWidth = this.currentStrokeWidth;
|
var savedWidth = this.currentStrokeWidth;
|
||||||
@@ -908,24 +1099,15 @@ function annotationEditor() {
|
|||||||
if (obj.strokeWidth) this.currentStrokeWidth = obj.strokeWidth;
|
if (obj.strokeWidth) this.currentStrokeWidth = obj.strokeWidth;
|
||||||
if (obj.lineDash) this.currentLineDash = obj.lineDash;
|
if (obj.lineDash) this.currentLineDash = obj.lineDash;
|
||||||
else this.currentLineDash = [];
|
else this.currentLineDash = [];
|
||||||
// Reconstruct arrow from stored endpoints or from position/size
|
// Reconstruct arrow from stored endpoints, scaled to current canvas
|
||||||
var x1 = obj.x1 != null ? obj.x1 : obj.left;
|
var x1 = (obj.x1 != null ? obj.x1 : obj.left) * cs;
|
||||||
var y1 = obj.y1 != null ? obj.y1 : obj.top;
|
var y1 = (obj.y1 != null ? obj.y1 : obj.top) * cs;
|
||||||
var x2 = obj.x2 != null ? obj.x2 : obj.left + (obj.width || 100);
|
var x2 = (obj.x2 != null ? obj.x2 : obj.left + (obj.width || 100)) * cs;
|
||||||
var y2 = obj.y2 != null ? obj.y2 : obj.top + (obj.height || 50);
|
var y2 = (obj.y2 != null ? obj.y2 : obj.top + (obj.height || 50)) * cs;
|
||||||
var created2 = this.addArrow(x1, y1, x2, y2);
|
this.addArrow(x1, y1, x2, y2);
|
||||||
this.currentColor = savedColor2;
|
this.currentColor = savedColor2;
|
||||||
this.currentStrokeWidth = savedWidth;
|
this.currentStrokeWidth = savedWidth;
|
||||||
this.currentLineDash = savedDash;
|
this.currentLineDash = savedDash;
|
||||||
// Restore transforms (position + scale + rotation)
|
|
||||||
created2.set({
|
|
||||||
left: obj.left,
|
|
||||||
top: obj.top,
|
|
||||||
angle: obj.angle || 0,
|
|
||||||
scaleX: obj.scaleX || 1,
|
|
||||||
scaleY: obj.scaleY || 1,
|
|
||||||
});
|
|
||||||
created2.setCoords();
|
|
||||||
} else if (obj.type === 'area') {
|
} else if (obj.type === 'area') {
|
||||||
var savedColor3 = this.currentColor;
|
var savedColor3 = this.currentColor;
|
||||||
var savedWidth2 = this.currentStrokeWidth;
|
var savedWidth2 = this.currentStrokeWidth;
|
||||||
@@ -934,17 +1116,10 @@ function annotationEditor() {
|
|||||||
if (obj.strokeWidth) this.currentStrokeWidth = obj.strokeWidth;
|
if (obj.strokeWidth) this.currentStrokeWidth = obj.strokeWidth;
|
||||||
if (obj.lineDash) this.currentLineDash = obj.lineDash;
|
if (obj.lineDash) this.currentLineDash = obj.lineDash;
|
||||||
else this.currentLineDash = [];
|
else this.currentLineDash = [];
|
||||||
var created3 = this.addRect(obj.left, obj.top, obj.width || 150, obj.height || 100);
|
this.addRect(obj.left * cs, obj.top * cs, (obj.width || 150) * cs, (obj.height || 100) * cs);
|
||||||
this.currentColor = savedColor3;
|
this.currentColor = savedColor3;
|
||||||
this.currentStrokeWidth = savedWidth2;
|
this.currentStrokeWidth = savedWidth2;
|
||||||
this.currentLineDash = savedDash2;
|
this.currentLineDash = savedDash2;
|
||||||
// Restore transforms (scale + rotation)
|
|
||||||
created3.set({
|
|
||||||
angle: obj.angle || 0,
|
|
||||||
scaleX: obj.scaleX || 1,
|
|
||||||
scaleY: obj.scaleY || 1,
|
|
||||||
});
|
|
||||||
created3.setCoords();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -961,6 +1136,7 @@ function annotationEditor() {
|
|||||||
markerNumbers.length > 0 ? Math.max.apply(null, markerNumbers) + 1 : 1;
|
markerNumbers.length > 0 ? Math.max.apply(null, markerNumbers) + 1 : 1;
|
||||||
|
|
||||||
this.isDirty = false;
|
this.isDirty = false;
|
||||||
|
this.canvas.discardActiveObject();
|
||||||
this.canvas.renderAll();
|
this.canvas.renderAll();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('AnnotationEditor: failed to load annotations:', e);
|
console.error('AnnotationEditor: failed to load annotations:', e);
|
||||||
@@ -1032,6 +1208,9 @@ function annotationEditor() {
|
|||||||
if (this._onDeleteSelected) {
|
if (this._onDeleteSelected) {
|
||||||
window.removeEventListener('anno-delete-selected', this._onDeleteSelected);
|
window.removeEventListener('anno-delete-selected', this._onDeleteSelected);
|
||||||
}
|
}
|
||||||
|
if (this._onClearAll) {
|
||||||
|
window.removeEventListener('anno-clear-all', this._onClearAll);
|
||||||
|
}
|
||||||
if (this._onColorChange) {
|
if (this._onColorChange) {
|
||||||
window.removeEventListener('anno-color-change', this._onColorChange);
|
window.removeEventListener('anno-color-change', this._onColorChange);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -172,7 +172,13 @@ function annotationViewer() {
|
|||||||
|
|
||||||
// Editor format (objects array from annotation-editor.js)
|
// Editor format (objects array from annotation-editor.js)
|
||||||
if (this.annotations.objects) {
|
if (this.annotations.objects) {
|
||||||
var annoScale = this.canvas.width / (this.annotations.width || this.canvas.width);
|
var previewScale = this.scale;
|
||||||
|
var imgScale = this.annotations.imageScale;
|
||||||
|
// v2+: use imageScale for correct canvas→image→preview mapping
|
||||||
|
// v1 fallback: annoScale = previewWidth / editorWidth
|
||||||
|
var annoScale = imgScale
|
||||||
|
? (previewScale / imgScale)
|
||||||
|
: (this.canvas.width / (this.annotations.width || this.canvas.width));
|
||||||
_drawEditorAnnotations(this.ctx, this.annotations, annoScale);
|
_drawEditorAnnotations(this.ctx, this.annotations, annoScale);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -397,7 +403,12 @@ function _drawPreview(canvas, ctx, source, srcW, srcH, annotations, maxHeight) {
|
|||||||
ctx.drawImage(source, 0, 0, canvas.width, canvas.height);
|
ctx.drawImage(source, 0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
if (annotations && annotations.objects) {
|
if (annotations && annotations.objects) {
|
||||||
var annoScale = canvas.width / (annotations.width || srcW);
|
var imgScale = annotations.imageScale;
|
||||||
|
// v2+: use imageScale for correct canvas→image→preview mapping
|
||||||
|
// v1 fallback: annoScale = previewWidth / editorWidth
|
||||||
|
var annoScale = imgScale
|
||||||
|
? (scale / imgScale)
|
||||||
|
: (canvas.width / (annotations.width || srcW));
|
||||||
_drawEditorAnnotations(ctx, annotations, annoScale);
|
_drawEditorAnnotations(ctx, annotations, annoScale);
|
||||||
} else if (annotations && annotations.markers) {
|
} else if (annotations && annotations.markers) {
|
||||||
_drawLegacyAnnotations(ctx, annotations, scale);
|
_drawLegacyAnnotations(ctx, annotations, scale);
|
||||||
|
|||||||
@@ -436,7 +436,7 @@
|
|||||||
<script>
|
<script>
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
|
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
|
||||||
</script>
|
</script>
|
||||||
<script src="{{ url_for('static', filename='js/annotation-viewer.js') }}?v=4"></script>
|
<script src="{{ url_for('static', filename='js/annotation-viewer.js') }}?v=6"></script>
|
||||||
<script>
|
<script>
|
||||||
/**
|
/**
|
||||||
* Recipe Preview - Minimal Alpine.js component
|
* Recipe Preview - Minimal Alpine.js component
|
||||||
|
|||||||
@@ -34,8 +34,9 @@
|
|||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Canvas container */
|
/* Canvas container — use custom class name to avoid conflict with
|
||||||
.canvas-container {
|
Fabric.js internal .canvas-container wrapper div */
|
||||||
|
.anno-canvas-wrap {
|
||||||
position: relative;
|
position: relative;
|
||||||
background: var(--bg-secondary);
|
background: var(--bg-secondary);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
@@ -43,6 +44,10 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
min-height: 400px;
|
min-height: 400px;
|
||||||
}
|
}
|
||||||
|
.anno-canvas-wrap canvas {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
/* Fabric.js internal wrapper also needs display:block on canvases */
|
||||||
.canvas-container canvas {
|
.canvas-container canvas {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
@@ -141,7 +146,7 @@
|
|||||||
<span x-text="saving ? window.__i18n_taskDrawing.saving : window.__i18n_taskDrawing.saveAnnotations"></span>
|
<span x-text="saving ? window.__i18n_taskDrawing.saving : window.__i18n_taskDrawing.saveAnnotations"></span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<a href="{{ url_for('maker.task_editor', recipe_id=recipe.id) }}"
|
<a :href="'{{ url_for('maker.task_editor', recipe_id=recipe.id) }}' + '?_=' + Date.now()"
|
||||||
class="btn btn-secondary gap-1.5">
|
class="btn btn-secondary gap-1.5">
|
||||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M11 17l-5-5m0 0l5-5m-5 5h12"/>
|
<path stroke-linecap="round" stroke-linejoin="round" d="M11 17l-5-5m0 0l5-5m-5 5h12"/>
|
||||||
@@ -245,7 +250,7 @@
|
|||||||
<!-- Separator -->
|
<!-- Separator -->
|
||||||
<div class="w-px h-6 bg-[var(--border-color)] mx-1"></div>
|
<div class="w-px h-6 bg-[var(--border-color)] mx-1"></div>
|
||||||
|
|
||||||
<!-- Delete -->
|
<!-- Delete selected -->
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="anno-btn text-red-500 hover:text-red-600"
|
class="anno-btn text-red-500 hover:text-red-600"
|
||||||
@click="deleteAnnotation()"
|
@click="deleteAnnotation()"
|
||||||
@@ -255,7 +260,21 @@
|
|||||||
<path stroke-linecap="round" stroke-linejoin="round"
|
<path stroke-linecap="round" stroke-linejoin="round"
|
||||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
||||||
</svg>
|
</svg>
|
||||||
<span class="hidden sm:inline">{{ _('Elimina') }}</span>
|
{{ _('Elimina') }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Clear All -->
|
||||||
|
<button type="button"
|
||||||
|
class="anno-btn text-red-500 hover:text-red-600"
|
||||||
|
@click="if (confirm('{{ _('Eliminare tutte le annotazioni?') }}')) clearAllAnnotations()"
|
||||||
|
title="{{ _('Cancella tutto') }}">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round"
|
||||||
|
d="M9 13h6m2 9H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round"
|
||||||
|
d="M6 18L18 6M6 6l12 12"/>
|
||||||
|
</svg>
|
||||||
|
{{ _('Cancella tutto') }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -337,7 +356,7 @@
|
|||||||
<div class="tmf-card">
|
<div class="tmf-card">
|
||||||
<div class="tmf-card-body p-0">
|
<div class="tmf-card-body p-0">
|
||||||
<div x-data="annotationEditor()"
|
<div x-data="annotationEditor()"
|
||||||
class="canvas-container"
|
class="anno-canvas-wrap"
|
||||||
id="annotationCanvasContainer">
|
id="annotationCanvasContainer">
|
||||||
<canvas id="annotationCanvas" x-ref="fabricCanvas"></canvas>
|
<canvas id="annotationCanvas" x-ref="fabricCanvas"></canvas>
|
||||||
</div>
|
</div>
|
||||||
@@ -353,7 +372,7 @@
|
|||||||
<script>
|
<script>
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
|
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
|
||||||
</script>
|
</script>
|
||||||
<script src="{{ url_for('static', filename='js/annotation-editor.js') }}?v=10"></script>
|
<script src="{{ url_for('static', filename='js/annotation-editor.js') }}?v=25"></script>
|
||||||
<script>
|
<script>
|
||||||
function taskDrawing() {
|
function taskDrawing() {
|
||||||
const taskData = window.__taskData || {};
|
const taskData = window.__taskData || {};
|
||||||
@@ -459,6 +478,14 @@ function taskDrawing() {
|
|||||||
this.errorMessage = data.detail || window.__i18n_taskDrawing.saveError;
|
this.errorMessage = data.detail || window.__i18n_taskDrawing.saveError;
|
||||||
} else {
|
} else {
|
||||||
this.successMessage = window.__i18n_taskDrawing.saveSuccess;
|
this.successMessage = window.__i18n_taskDrawing.saveSuccess;
|
||||||
|
// Store updated annotations in sessionStorage so task_editor
|
||||||
|
// can pick them up and refresh the preview immediately.
|
||||||
|
try {
|
||||||
|
sessionStorage.setItem('taskDrawingUpdated', JSON.stringify({
|
||||||
|
taskId: this.taskId,
|
||||||
|
annotations_json: this.annotationsJson
|
||||||
|
}));
|
||||||
|
} catch (_e) { /* sessionStorage may be unavailable */ }
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('saveAnnotations error:', err);
|
console.error('saveAnnotations error:', err);
|
||||||
@@ -480,6 +507,10 @@ function taskDrawing() {
|
|||||||
window.dispatchEvent(new CustomEvent('anno-delete-selected'));
|
window.dispatchEvent(new CustomEvent('anno-delete-selected'));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
clearAllAnnotations() {
|
||||||
|
window.dispatchEvent(new CustomEvent('anno-clear-all'));
|
||||||
|
},
|
||||||
|
|
||||||
setAnnoColor(color) {
|
setAnnoColor(color) {
|
||||||
this.annoColor = color;
|
this.annoColor = color;
|
||||||
window.dispatchEvent(new CustomEvent('anno-color-change', {
|
window.dispatchEvent(new CustomEvent('anno-color-change', {
|
||||||
|
|||||||
@@ -1075,7 +1075,14 @@
|
|||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
|
||||||
<script>if(typeof pdfjsLib!=='undefined')pdfjsLib.GlobalWorkerOptions.workerSrc='https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';</script>
|
<script>if(typeof pdfjsLib!=='undefined')pdfjsLib.GlobalWorkerOptions.workerSrc='https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';</script>
|
||||||
<script src="{{ url_for('static', filename='js/annotation-viewer.js') }}?v=4"></script>
|
<script src="{{ url_for('static', filename='js/annotation-viewer.js') }}?v=6"></script>
|
||||||
|
<script>
|
||||||
|
// Force reload when page is restored from browser bfcache (back/forward navigation).
|
||||||
|
// This ensures fresh data is displayed after editing annotations in task_drawing.
|
||||||
|
window.addEventListener('pageshow', function (event) {
|
||||||
|
if (event.persisted) { window.location.reload(); }
|
||||||
|
});
|
||||||
|
</script>
|
||||||
<script>
|
<script>
|
||||||
function taskEditor() {
|
function taskEditor() {
|
||||||
return {
|
return {
|
||||||
@@ -1126,6 +1133,29 @@ function taskEditor() {
|
|||||||
// Sort tasks by order_index on load
|
// Sort tasks by order_index on load
|
||||||
this.tasks.sort((a, b) => (a.order_index || 0) - (b.order_index || 0));
|
this.tasks.sort((a, b) => (a.order_index || 0) - (b.order_index || 0));
|
||||||
|
|
||||||
|
// Check if returning from task_drawing with updated annotations
|
||||||
|
try {
|
||||||
|
var updatedRaw = sessionStorage.getItem('taskDrawingUpdated');
|
||||||
|
if (updatedRaw) {
|
||||||
|
sessionStorage.removeItem('taskDrawingUpdated');
|
||||||
|
var updated = JSON.parse(updatedRaw);
|
||||||
|
if (updated.taskId) {
|
||||||
|
var task = this.tasks.find(function(t) { return t.id === updated.taskId; });
|
||||||
|
if (task) {
|
||||||
|
// Merge updated annotations into local state
|
||||||
|
if (updated.annotations_json) {
|
||||||
|
task.annotations_json = (typeof updated.annotations_json === 'string')
|
||||||
|
? JSON.parse(updated.annotations_json) : updated.annotations_json;
|
||||||
|
}
|
||||||
|
// Auto-expand the edited task and render preview
|
||||||
|
this.expandedTask = task.id;
|
||||||
|
this.renderTaskPreview(task);
|
||||||
|
return; // skip default expansion logic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_e) { /* sessionStorage parse error - ignore */ }
|
||||||
|
|
||||||
// Auto-expand the first task if there is only one
|
// Auto-expand the first task if there is only one
|
||||||
if (this.tasks.length === 1) {
|
if (this.tasks.length === 1) {
|
||||||
this.expandedTask = this.tasks[0].id;
|
this.expandedTask = this.tasks[0].id;
|
||||||
|
|||||||
@@ -521,7 +521,7 @@
|
|||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
|
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
|
||||||
</script>
|
</script>
|
||||||
<script src="{{ url_for('static', filename='js/numpad.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/numpad.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='js/annotation-viewer.js') }}?v=4"></script>
|
<script src="{{ url_for('static', filename='js/annotation-viewer.js') }}?v=6"></script>
|
||||||
<script src="{{ url_for('static', filename='js/caliper.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/caliper.js') }}"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
Reference in New Issue
Block a user