feat(ui): rinomina Misure→Attività, textarea descrizione task, tool Testo annotazioni
- Navbar e breadcrumb: label "Misure" rinominata "Attività" (IT) / "Activities" (EN) - Descrizione task: input sostituito con textarea multilinea + display whitespace-pre-wrap - Annotation editor: nuovo tool "Testo" con fabric.IText (inline editing, serializzazione, viewer) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -671,6 +671,53 @@ function annotationEditor() {
|
|||||||
return rect;
|
return rect;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// Object creation - Text
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
|
||||||
|
addText(x, y, textContent, fontSize) {
|
||||||
|
var size = fontSize || 18;
|
||||||
|
var itext = new fabric.IText(textContent || 'Testo', {
|
||||||
|
left: x,
|
||||||
|
top: y,
|
||||||
|
fontSize: size,
|
||||||
|
fill: this.currentColor,
|
||||||
|
fontFamily: 'Inter, sans-serif',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
selectable: true,
|
||||||
|
evented: true,
|
||||||
|
hasControls: false,
|
||||||
|
hasBorders: true,
|
||||||
|
lockRotation: true,
|
||||||
|
lockScalingX: true,
|
||||||
|
lockScalingY: true,
|
||||||
|
objectType: 'text',
|
||||||
|
textColor: this.currentColor,
|
||||||
|
textFontSize: size,
|
||||||
|
});
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
itext.on('editing:exited', function () {
|
||||||
|
if (!itext.text || itext.text.trim() === '') {
|
||||||
|
self.canvas.remove(itext);
|
||||||
|
}
|
||||||
|
itext.textColor = itext.fill;
|
||||||
|
self.isDirty = true;
|
||||||
|
window.dispatchEvent(new CustomEvent('annotations-changed', { detail: { json: self.getAnnotationsJson() } }));
|
||||||
|
});
|
||||||
|
|
||||||
|
itext.on('changed', function () {
|
||||||
|
self.isDirty = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.canvas.add(itext);
|
||||||
|
itext.setCoords();
|
||||||
|
this.canvas.setActiveObject(itext);
|
||||||
|
this.isDirty = true;
|
||||||
|
window.dispatchEvent(new CustomEvent('annotations-changed', { detail: { json: this.getAnnotationsJson() } }));
|
||||||
|
return itext;
|
||||||
|
},
|
||||||
|
|
||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
// Toolbar / mode switching
|
// Toolbar / mode switching
|
||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
@@ -678,7 +725,7 @@ function annotationEditor() {
|
|||||||
/**
|
/**
|
||||||
* Set the current interaction mode.
|
* Set the current interaction mode.
|
||||||
*
|
*
|
||||||
* @param {'select'|'marker'|'arrow'|'rect'} mode
|
* @param {'select'|'marker'|'arrow'|'rect'|'text'} mode
|
||||||
*/
|
*/
|
||||||
setMode(mode) {
|
setMode(mode) {
|
||||||
this.activeMode = mode;
|
this.activeMode = mode;
|
||||||
@@ -731,6 +778,13 @@ function annotationEditor() {
|
|||||||
self.addMarker(pointer.x, pointer.y);
|
self.addMarker(pointer.x, pointer.y);
|
||||||
self.setMode('select');
|
self.setMode('select');
|
||||||
window.dispatchEvent(new CustomEvent('anno-mode-changed', { detail: { mode: 'select' } }));
|
window.dispatchEvent(new CustomEvent('anno-mode-changed', { detail: { mode: 'select' } }));
|
||||||
|
} else if (self.activeMode === 'text') {
|
||||||
|
var textObj = self.addText(pointer.x, pointer.y);
|
||||||
|
self.setMode('select');
|
||||||
|
window.dispatchEvent(new CustomEvent('anno-mode-changed', { detail: { mode: 'select' } }));
|
||||||
|
self.canvas.setActiveObject(textObj);
|
||||||
|
textObj.enterEditing();
|
||||||
|
textObj.selectAll();
|
||||||
} else if (self.activeMode === 'arrow') {
|
} else if (self.activeMode === 'arrow') {
|
||||||
isDrawing = true;
|
isDrawing = true;
|
||||||
drawStartX = pointer.x;
|
drawStartX = pointer.x;
|
||||||
@@ -1023,6 +1077,13 @@ function annotationEditor() {
|
|||||||
if (obj.arrowLineDash) data.lineDash = obj.arrowLineDash;
|
if (obj.arrowLineDash) data.lineDash = obj.arrowLineDash;
|
||||||
if (obj.areaLineDash) data.lineDash = obj.areaLineDash;
|
if (obj.areaLineDash) data.lineDash = obj.areaLineDash;
|
||||||
|
|
||||||
|
// Text-specific
|
||||||
|
if (obj.objectType === 'text') {
|
||||||
|
data.textContent = obj.text;
|
||||||
|
data.textColor = obj.textColor || obj.fill;
|
||||||
|
data.fontSize = obj.textFontSize || obj.fontSize;
|
||||||
|
}
|
||||||
|
|
||||||
// Arrow-specific: store original endpoints
|
// Arrow-specific: store original endpoints
|
||||||
if (obj.objectType === 'arrow') {
|
if (obj.objectType === 'arrow') {
|
||||||
data.x1 = obj.arrowX1 != null ? Math.round(obj.arrowX1 * 100) / 100 : null;
|
data.x1 = obj.arrowX1 != null ? Math.round(obj.arrowX1 * 100) / 100 : null;
|
||||||
@@ -1120,6 +1181,11 @@ function annotationEditor() {
|
|||||||
this.currentColor = savedColor3;
|
this.currentColor = savedColor3;
|
||||||
this.currentStrokeWidth = savedWidth2;
|
this.currentStrokeWidth = savedWidth2;
|
||||||
this.currentLineDash = savedDash2;
|
this.currentLineDash = savedDash2;
|
||||||
|
} else if (obj.type === 'text') {
|
||||||
|
var savedColor4 = this.currentColor;
|
||||||
|
if (obj.textColor) this.currentColor = obj.textColor;
|
||||||
|
this.addText(obj.left * cs, obj.top * cs, obj.textContent, (obj.fontSize || 18) * cs);
|
||||||
|
this.currentColor = savedColor4;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -436,6 +436,8 @@ function _drawEditorAnnotations(ctx, data, scale) {
|
|||||||
_drawPreviewArrow(ctx, obj, annoScale);
|
_drawPreviewArrow(ctx, obj, annoScale);
|
||||||
} else if (obj.type === 'area') {
|
} else if (obj.type === 'area') {
|
||||||
_drawPreviewArea(ctx, obj, annoScale);
|
_drawPreviewArea(ctx, obj, annoScale);
|
||||||
|
} else if (obj.type === 'text') {
|
||||||
|
_drawPreviewText(ctx, obj, annoScale);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -514,6 +516,24 @@ function _drawPreviewArea(ctx, obj, scale) {
|
|||||||
ctx.setLineDash([]);
|
ctx.setLineDash([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _drawPreviewText(ctx, obj, scale) {
|
||||||
|
var x = obj.left * scale;
|
||||||
|
var y = obj.top * scale;
|
||||||
|
var color = obj.textColor || '#000000';
|
||||||
|
var fontSize = Math.max(10, Math.round((obj.fontSize || 18) * scale));
|
||||||
|
|
||||||
|
ctx.fillStyle = color;
|
||||||
|
ctx.font = 'bold ' + fontSize + 'px Inter, sans-serif';
|
||||||
|
ctx.textAlign = 'left';
|
||||||
|
ctx.textBaseline = 'top';
|
||||||
|
|
||||||
|
var lines = (obj.textContent || '').split('\n');
|
||||||
|
var lineHeight = fontSize * 1.2;
|
||||||
|
for (var j = 0; j < lines.length; j++) {
|
||||||
|
ctx.fillText(lines[j], x, y + j * lineHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Draw annotations in the legacy format (markers: point/rectangle).
|
* Draw annotations in the legacy format (markers: point/rectangle).
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -247,6 +247,18 @@
|
|||||||
<span class="hidden sm:inline">{{ _('Rettangolo') }}</span>
|
<span class="hidden sm:inline">{{ _('Rettangolo') }}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<!-- Text Tool -->
|
||||||
|
<button type="button"
|
||||||
|
class="anno-btn"
|
||||||
|
:class="{ 'active': annoTool === 'text' }"
|
||||||
|
@click="setAnnoTool('text')"
|
||||||
|
title="{{ _('Testo') }}">
|
||||||
|
<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="M4 6h16M4 6v2m16-2v2M7 6v12m0 0h2m-2 0H5m12-12v12m0 0h2m-2 0h-2"/>
|
||||||
|
</svg>
|
||||||
|
<span class="hidden sm:inline">{{ _('Testo') }}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
<!-- 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>
|
||||||
|
|
||||||
|
|||||||
@@ -269,12 +269,12 @@
|
|||||||
placeholder="{{ _('Es. Seguire procedura ISO 2768') }}">
|
placeholder="{{ _('Es. Seguire procedura ISO 2768') }}">
|
||||||
</div>
|
</div>
|
||||||
<!-- Descrizione -->
|
<!-- Descrizione -->
|
||||||
<div>
|
<div class="sm:col-span-2">
|
||||||
<label class="tmf-label">{{ _('Descrizione') }}</label>
|
<label class="tmf-label">{{ _('Descrizione') }}</label>
|
||||||
<input type="text"
|
<textarea x-model="newTask.description"
|
||||||
x-model="newTask.description"
|
class="tmf-input text-sm"
|
||||||
class="tmf-input"
|
rows="3"
|
||||||
placeholder="{{ _('Descrizione opzionale...') }}">
|
placeholder="{{ _('Descrizione opzionale...') }}"></textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -497,7 +497,7 @@
|
|||||||
<template x-if="task.description">
|
<template x-if="task.description">
|
||||||
<div class="flex items-start gap-2">
|
<div class="flex items-start gap-2">
|
||||||
<span class="text-xs font-semibold text-[var(--text-muted)] uppercase tracking-wide shrink-0 mt-0.5">{{ _('Descrizione') }}:</span>
|
<span class="text-xs font-semibold text-[var(--text-muted)] uppercase tracking-wide shrink-0 mt-0.5">{{ _('Descrizione') }}:</span>
|
||||||
<span class="text-sm text-[var(--text-secondary)]" x-text="task.description"></span>
|
<span class="text-sm text-[var(--text-secondary)] whitespace-pre-wrap" x-text="task.description"></span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@@ -513,12 +513,12 @@
|
|||||||
class="tmf-input text-sm"
|
class="tmf-input text-sm"
|
||||||
placeholder="{{ _('Direttiva opzionale...') }}">
|
placeholder="{{ _('Direttiva opzionale...') }}">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="sm:col-span-2">
|
||||||
<label class="tmf-label">{{ _('Descrizione') }}</label>
|
<label class="tmf-label">{{ _('Descrizione') }}</label>
|
||||||
<input type="text"
|
<textarea x-model="editTaskData.description"
|
||||||
x-model="editTaskData.description"
|
class="tmf-input text-sm"
|
||||||
class="tmf-input text-sm"
|
rows="3"
|
||||||
placeholder="{{ _('Descrizione opzionale...') }}">
|
placeholder="{{ _('Descrizione opzionale...') }}"></textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -134,7 +134,7 @@
|
|||||||
|
|
||||||
<!-- Directive -->
|
<!-- Directive -->
|
||||||
{% if task.directive or task.description %}
|
{% if task.directive or task.description %}
|
||||||
<p class="text-sm text-[var(--text-secondary)] leading-relaxed mb-3 line-clamp-2">
|
<p class="text-sm text-[var(--text-secondary)] leading-relaxed mb-3 line-clamp-2 whitespace-pre-wrap">
|
||||||
{{ task.directive or task.description }}
|
{{ task.directive or task.description }}
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -650,7 +650,7 @@ msgstr "NON-CONFORMING"
|
|||||||
#: templates/measure/task_complete.html:16
|
#: templates/measure/task_complete.html:16
|
||||||
#: templates/measure/task_execute.html:2 templates/measure/task_list.html:16
|
#: templates/measure/task_execute.html:2 templates/measure/task_list.html:16
|
||||||
msgid "Misure"
|
msgid "Misure"
|
||||||
msgstr "Measurements"
|
msgstr "Activities"
|
||||||
|
|
||||||
#: templates/components/navbar.html:55 templates/components/navbar.html:265
|
#: templates/components/navbar.html:55 templates/components/navbar.html:265
|
||||||
#: templates/maker/recipe_editor.html:51 templates/maker/recipe_list.html:83
|
#: templates/maker/recipe_editor.html:51 templates/maker/recipe_list.html:83
|
||||||
@@ -1069,7 +1069,11 @@ msgstr "Arrow"
|
|||||||
msgid "Rettangolo"
|
msgid "Rettangolo"
|
||||||
msgstr "Rectangle"
|
msgstr "Rectangle"
|
||||||
|
|
||||||
#: templates/maker/task_drawing.html:258
|
#: templates/maker/task_drawing.html:261
|
||||||
|
msgid "Testo"
|
||||||
|
msgstr "Text"
|
||||||
|
|
||||||
|
#: templates/maker/task_drawing.html:272
|
||||||
msgid "Elimina selezionato"
|
msgid "Elimina selezionato"
|
||||||
msgstr "Delete selected"
|
msgstr "Delete selected"
|
||||||
|
|
||||||
|
|||||||
@@ -680,7 +680,7 @@ msgstr "NON CONFORME"
|
|||||||
#: templates/measure/task_complete.html:16
|
#: templates/measure/task_complete.html:16
|
||||||
#: templates/measure/task_execute.html:2 templates/measure/task_list.html:16
|
#: templates/measure/task_execute.html:2 templates/measure/task_list.html:16
|
||||||
msgid "Misure"
|
msgid "Misure"
|
||||||
msgstr "Misure"
|
msgstr "Attività"
|
||||||
|
|
||||||
#: templates/components/navbar.html:55 templates/components/navbar.html:265
|
#: templates/components/navbar.html:55 templates/components/navbar.html:265
|
||||||
#: templates/maker/recipe_editor.html:51 templates/maker/recipe_list.html:83
|
#: templates/maker/recipe_editor.html:51 templates/maker/recipe_list.html:83
|
||||||
@@ -1112,7 +1112,11 @@ msgstr "Freccia"
|
|||||||
msgid "Rettangolo"
|
msgid "Rettangolo"
|
||||||
msgstr "Rettangolo"
|
msgstr "Rettangolo"
|
||||||
|
|
||||||
#: templates/maker/task_drawing.html:258
|
#: templates/maker/task_drawing.html:261
|
||||||
|
msgid "Testo"
|
||||||
|
msgstr "Testo"
|
||||||
|
|
||||||
|
#: templates/maker/task_drawing.html:272
|
||||||
msgid "Elimina selezionato"
|
msgid "Elimina selezionato"
|
||||||
msgstr "Elimina selezionato"
|
msgstr "Elimina selezionato"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user