canvas_erd 0.1.0 → 0.3.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +18 -0
- data/README.md +9 -3
- data/lib/canvas_erd/cli.rb +4 -1
- data/lib/canvas_erd/diagram_store.rb +16 -2
- data/lib/canvas_erd/highlighter.rb +57 -0
- data/lib/canvas_erd/model_details.rb +105 -0
- data/lib/canvas_erd/server.rb +79 -4
- data/lib/canvas_erd/source_files.rb +70 -0
- data/lib/canvas_erd/version.rb +1 -2
- data/lib/canvas_erd/web/app.js +1437 -91
- data/lib/canvas_erd/web/document.js +532 -2
- data/lib/canvas_erd/web/index.html +215 -1
- data/lib/canvas_erd/web/styles.css +295 -0
- data/lib/canvas_erd.rb +3 -0
- metadata +26 -3
data/lib/canvas_erd/web/app.js
CHANGED
|
@@ -19,7 +19,81 @@ const noticeElement = document.querySelector("#notice");
|
|
|
19
19
|
const shortcutHelpButton = document.querySelector("#shortcut-help");
|
|
20
20
|
const shortcutPanelElement = document.querySelector("#shortcut-panel");
|
|
21
21
|
const arrowButton = document.querySelector("#add-arrow");
|
|
22
|
+
const frameButton = document.querySelector("#add-frame");
|
|
22
23
|
const layoutTablesButton = document.querySelector("#layout-tables");
|
|
24
|
+
const noteFormattingElement = document.querySelector("#note-formatting");
|
|
25
|
+
const noteBoldButton = document.querySelector("#note-bold");
|
|
26
|
+
const noteItalicButton = document.querySelector("#note-italic");
|
|
27
|
+
const noteUnderlineButton = document.querySelector("#note-underline");
|
|
28
|
+
const noteParagraphStyleElement = document.querySelector("#note-paragraph-style");
|
|
29
|
+
const noteAlignmentElement = document.querySelector("#note-alignment");
|
|
30
|
+
const tableFormattingElement = document.querySelector("#table-formatting");
|
|
31
|
+
const tableHeaderColorControl = {
|
|
32
|
+
button: document.querySelector("#table-header-color-button"),
|
|
33
|
+
preview: document.querySelector("#table-header-color-preview"),
|
|
34
|
+
menu: document.querySelector("#table-header-color-menu"),
|
|
35
|
+
swatches: document.querySelector("#table-header-color-swatches"),
|
|
36
|
+
custom: document.querySelector("#table-header-custom-color")
|
|
37
|
+
};
|
|
38
|
+
const arrowFormattingElement = document.querySelector("#arrow-formatting");
|
|
39
|
+
const arrowStartHeadElement = document.querySelector("#arrow-start-head");
|
|
40
|
+
const arrowEndHeadElement = document.querySelector("#arrow-end-head");
|
|
41
|
+
const arrowLineStyleElement = document.querySelector("#arrow-line-style");
|
|
42
|
+
const arrowStrokeWidthElement = document.querySelector("#arrow-stroke-width");
|
|
43
|
+
const arrowColorControl = {
|
|
44
|
+
button: document.querySelector("#arrow-color-button"),
|
|
45
|
+
preview: document.querySelector("#arrow-color-preview"),
|
|
46
|
+
menu: document.querySelector("#arrow-color-menu"),
|
|
47
|
+
swatches: document.querySelector("#arrow-color-swatches"),
|
|
48
|
+
custom: document.querySelector("#arrow-custom-color")
|
|
49
|
+
};
|
|
50
|
+
const frameFormattingElement = document.querySelector("#frame-formatting");
|
|
51
|
+
const frameLineStyleElement = document.querySelector("#frame-line-style");
|
|
52
|
+
const frameColorControl = {
|
|
53
|
+
button: document.querySelector("#frame-color-button"),
|
|
54
|
+
preview: document.querySelector("#frame-color-preview"),
|
|
55
|
+
menu: document.querySelector("#frame-color-menu"),
|
|
56
|
+
swatches: document.querySelector("#frame-color-swatches"),
|
|
57
|
+
custom: document.querySelector("#frame-custom-color")
|
|
58
|
+
};
|
|
59
|
+
const codeFormattingElement = document.querySelector("#code-formatting");
|
|
60
|
+
const codeLanguageElement = document.querySelector("#code-language");
|
|
61
|
+
const codeSourceElement = document.querySelector("#code-source");
|
|
62
|
+
const codeLineNumbersButton = document.querySelector("#code-line-numbers");
|
|
63
|
+
const codeSizeElement = document.querySelector("#code-size");
|
|
64
|
+
const codeSourceFieldsElement = document.querySelector("#code-source-fields");
|
|
65
|
+
const codeSourcePathElement = document.querySelector("#code-source-path");
|
|
66
|
+
const codeSourceStartElement = document.querySelector("#code-source-start");
|
|
67
|
+
const codeSourceEndElement = document.querySelector("#code-source-end");
|
|
68
|
+
const sourceFilePathsElement = document.querySelector("#source-file-paths");
|
|
69
|
+
const sourceDialogElement = document.querySelector("#source-dialog");
|
|
70
|
+
const sourceFormElement = document.querySelector("#source-form");
|
|
71
|
+
const sourcePathElement = document.querySelector("#source-path");
|
|
72
|
+
const sourceStartElement = document.querySelector("#source-start");
|
|
73
|
+
const sourceEndElement = document.querySelector("#source-end");
|
|
74
|
+
const sourceErrorElement = document.querySelector("#source-error");
|
|
75
|
+
const sourceSubmitButton = document.querySelector("#source-submit");
|
|
76
|
+
const noteColorControls = [
|
|
77
|
+
{
|
|
78
|
+
property: "fill",
|
|
79
|
+
defaults: CanvasERDDocument.DEFAULT_FOREGROUND_COLORS,
|
|
80
|
+
button: document.querySelector("#note-text-color-button"),
|
|
81
|
+
preview: document.querySelector("#note-text-color-preview"),
|
|
82
|
+
menu: document.querySelector("#note-text-color-menu"),
|
|
83
|
+
swatches: document.querySelector("#note-text-color-swatches"),
|
|
84
|
+
custom: document.querySelector("#note-text-custom-color")
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
property: "backgroundColor",
|
|
88
|
+
defaults: CanvasERDDocument.DEFAULT_BACKGROUND_COLORS,
|
|
89
|
+
button: document.querySelector("#note-background-color-button"),
|
|
90
|
+
preview: document.querySelector("#note-background-color-preview"),
|
|
91
|
+
menu: document.querySelector("#note-background-color-menu"),
|
|
92
|
+
swatches: document.querySelector("#note-background-color-swatches"),
|
|
93
|
+
custom: document.querySelector("#note-background-custom-color"),
|
|
94
|
+
transparent: document.querySelector("#note-background-transparent")
|
|
95
|
+
}
|
|
96
|
+
];
|
|
23
97
|
|
|
24
98
|
let schema;
|
|
25
99
|
let applicationSchema;
|
|
@@ -33,7 +107,11 @@ let lastCanvasPointer = null;
|
|
|
33
107
|
const tableObjects = new Map();
|
|
34
108
|
let relationshipObjects = [];
|
|
35
109
|
const noteObjects = new Map();
|
|
110
|
+
const codeObjects = new Map();
|
|
111
|
+
const codeHighlightTimers = new Map();
|
|
36
112
|
const arrowObjects = new Map();
|
|
113
|
+
const frameObjects = new Map();
|
|
114
|
+
const frameLabelObjects = new Map();
|
|
37
115
|
let noticeTimer;
|
|
38
116
|
let currentDiagramFilename = null;
|
|
39
117
|
let diagramsDirectory = "docs/erd";
|
|
@@ -44,6 +122,14 @@ let arrowMode = false;
|
|
|
44
122
|
let drawingArrow = null;
|
|
45
123
|
let arrowPreview = null;
|
|
46
124
|
let editingArrowId = null;
|
|
125
|
+
let frameMode = false;
|
|
126
|
+
let drawingFrame = null;
|
|
127
|
+
let framePreview = null;
|
|
128
|
+
let lastNoteFormatting = CanvasERDDocument.inheritedNoteFormatting();
|
|
129
|
+
let lastCodeLanguage = "ruby";
|
|
130
|
+
let highlighting = { foreground: "#24292f", background: "#f6f8fa", languages: [] };
|
|
131
|
+
let modelDetails = {};
|
|
132
|
+
let sourceDialogPosition = null;
|
|
47
133
|
const arrowEndpointHandles = new Map();
|
|
48
134
|
const changeTracker = CanvasERDDocument.createChangeTracker(updateSaveButton);
|
|
49
135
|
const layoutEngine = new ELK({ workerUrl: "/assets/elk-worker.min.js" });
|
|
@@ -145,7 +231,7 @@ function updateTableCaching() {
|
|
|
145
231
|
|
|
146
232
|
function updateNoteSelectionPadding() {
|
|
147
233
|
const padding = CanvasERDDocument.noteSelectionPadding(canvas.getZoom());
|
|
148
|
-
noteObjects.forEach((note) => {
|
|
234
|
+
[...noteObjects.values(), ...codeObjects.values()].forEach((note) => {
|
|
149
235
|
if (note.padding === padding) return;
|
|
150
236
|
note.padding = padding;
|
|
151
237
|
note.setCoords();
|
|
@@ -177,6 +263,7 @@ function truncate(value, length) {
|
|
|
177
263
|
function createTableObject(entity) {
|
|
178
264
|
const width = CanvasERDDocument.CARD_WIDTH;
|
|
179
265
|
const height = CanvasERDDocument.tableHeight(entity);
|
|
266
|
+
const headerColor = CanvasERDDocument.tableHeaderColor(state, entity.id);
|
|
180
267
|
const objects = [
|
|
181
268
|
new fabric.Rect(CanvasERDDocument.topLeft({
|
|
182
269
|
left: 0,
|
|
@@ -195,7 +282,7 @@ function createTableObject(entity) {
|
|
|
195
282
|
top: 0,
|
|
196
283
|
width,
|
|
197
284
|
height: CanvasERDDocument.HEADER_HEIGHT,
|
|
198
|
-
fill:
|
|
285
|
+
fill: headerColor,
|
|
199
286
|
rx: 8,
|
|
200
287
|
ry: 8
|
|
201
288
|
})),
|
|
@@ -204,7 +291,7 @@ function createTableObject(entity) {
|
|
|
204
291
|
top: CanvasERDDocument.HEADER_HEIGHT - 8,
|
|
205
292
|
width,
|
|
206
293
|
height: 8,
|
|
207
|
-
fill:
|
|
294
|
+
fill: headerColor
|
|
208
295
|
})),
|
|
209
296
|
new fabric.FabricText(truncate(entity.label, 34), CanvasERDDocument.uncachedText({
|
|
210
297
|
left: 14,
|
|
@@ -318,26 +405,73 @@ function relationStyle(relationship) {
|
|
|
318
405
|
};
|
|
319
406
|
}
|
|
320
407
|
|
|
408
|
+
function createCardinalityObjects(geometry, style) {
|
|
409
|
+
const markerStyle = { ...style, strokeDashArray: null };
|
|
410
|
+
const objects = geometry.segments.map(({ start, end }) => (
|
|
411
|
+
new fabric.Line([start.x, start.y, end.x, end.y], markerStyle)
|
|
412
|
+
));
|
|
413
|
+
geometry.circles.forEach(({ center, radius }) => {
|
|
414
|
+
objects.push(new fabric.Circle({
|
|
415
|
+
left: center.x,
|
|
416
|
+
top: center.y,
|
|
417
|
+
originX: "center",
|
|
418
|
+
originY: "center",
|
|
419
|
+
radius,
|
|
420
|
+
fill: "#f7f8fb",
|
|
421
|
+
stroke: style.stroke,
|
|
422
|
+
strokeWidth: style.strokeWidth,
|
|
423
|
+
selectable: false,
|
|
424
|
+
evented: false,
|
|
425
|
+
objectCaching: false
|
|
426
|
+
}));
|
|
427
|
+
});
|
|
428
|
+
if (geometry.label) {
|
|
429
|
+
objects.push(new fabric.FabricText(geometry.label, {
|
|
430
|
+
left: geometry.labelPosition.x,
|
|
431
|
+
top: geometry.labelPosition.y,
|
|
432
|
+
originX: "center",
|
|
433
|
+
originY: "center",
|
|
434
|
+
fontFamily: CanvasERDDocument.CANVAS_FONT_FAMILY,
|
|
435
|
+
fontSize: 10,
|
|
436
|
+
fill: "#526277",
|
|
437
|
+
backgroundColor: "#f7f8fb",
|
|
438
|
+
selectable: false,
|
|
439
|
+
evented: false
|
|
440
|
+
}));
|
|
441
|
+
}
|
|
442
|
+
return objects;
|
|
443
|
+
}
|
|
444
|
+
|
|
321
445
|
function createRelationshipObjects(relationship, source, destination) {
|
|
322
446
|
const sourceBounds = tableBounds(source);
|
|
323
447
|
const destinationBounds = tableBounds(destination);
|
|
324
448
|
const style = relationStyle(relationship);
|
|
325
449
|
let connector;
|
|
326
|
-
let
|
|
450
|
+
let sourceEndpoint;
|
|
451
|
+
let destinationEndpoint;
|
|
327
452
|
|
|
328
453
|
if (source === destination) {
|
|
329
454
|
const right = sourceBounds.left + sourceBounds.width;
|
|
330
455
|
const top = sourceBounds.top;
|
|
331
456
|
const centerY = top + sourceBounds.height / 2;
|
|
332
|
-
|
|
457
|
+
sourceEndpoint = CanvasERDDocument.cardinalityEndpointGeometry(
|
|
333
458
|
{ x: right, y: centerY },
|
|
334
|
-
{ x:
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
459
|
+
{ x: 1, y: 0 },
|
|
460
|
+
relationship.cardinality.source
|
|
461
|
+
);
|
|
462
|
+
destinationEndpoint = CanvasERDDocument.cardinalityEndpointGeometry(
|
|
463
|
+
{ x: right - 20, y: top },
|
|
464
|
+
{ x: 0, y: -1 },
|
|
465
|
+
relationship.cardinality.destination
|
|
466
|
+
);
|
|
467
|
+
const points = [
|
|
468
|
+
sourceEndpoint.shaftPoint,
|
|
469
|
+
{ x: right + 48, y: centerY },
|
|
470
|
+
{ x: right + 48, y: top - 48 },
|
|
471
|
+
{ x: right - 20, y: top - 48 },
|
|
472
|
+
destinationEndpoint.shaftPoint
|
|
338
473
|
];
|
|
339
474
|
connector = new fabric.Polyline(points, style);
|
|
340
|
-
labelPosition = { x: right + 48, y: top - 28 };
|
|
341
475
|
} else {
|
|
342
476
|
const sourceCenter = {
|
|
343
477
|
x: sourceBounds.left + sourceBounds.width / 2,
|
|
@@ -347,33 +481,31 @@ function createRelationshipObjects(relationship, source, destination) {
|
|
|
347
481
|
x: destinationBounds.left + destinationBounds.width / 2,
|
|
348
482
|
y: destinationBounds.top + destinationBounds.height / 2
|
|
349
483
|
};
|
|
484
|
+
sourceEndpoint = CanvasERDDocument.relationshipEndpointGeometry(
|
|
485
|
+
sourceBounds,
|
|
486
|
+
destinationCenter,
|
|
487
|
+
relationship.cardinality.source
|
|
488
|
+
);
|
|
489
|
+
destinationEndpoint = CanvasERDDocument.relationshipEndpointGeometry(
|
|
490
|
+
destinationBounds,
|
|
491
|
+
sourceCenter,
|
|
492
|
+
relationship.cardinality.destination
|
|
493
|
+
);
|
|
350
494
|
connector = new fabric.Line([
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
495
|
+
sourceEndpoint.shaftPoint.x,
|
|
496
|
+
sourceEndpoint.shaftPoint.y,
|
|
497
|
+
destinationEndpoint.shaftPoint.x,
|
|
498
|
+
destinationEndpoint.shaftPoint.y
|
|
355
499
|
], style);
|
|
356
|
-
labelPosition = {
|
|
357
|
-
x: (sourceCenter.x + destinationCenter.x) / 2,
|
|
358
|
-
y: (sourceCenter.y + destinationCenter.y) / 2
|
|
359
|
-
};
|
|
360
500
|
}
|
|
361
501
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
fontSize: 10,
|
|
370
|
-
fill: "#526277",
|
|
371
|
-
backgroundColor: "#f7f8fb",
|
|
372
|
-
selectable: false,
|
|
373
|
-
evented: false
|
|
374
|
-
});
|
|
375
|
-
label.canvasErdType = "relationship";
|
|
376
|
-
return [connector, label];
|
|
502
|
+
const objects = [
|
|
503
|
+
connector,
|
|
504
|
+
...createCardinalityObjects(sourceEndpoint, style),
|
|
505
|
+
...createCardinalityObjects(destinationEndpoint, style)
|
|
506
|
+
];
|
|
507
|
+
objects.forEach((object) => { object.canvasErdType = "relationship"; });
|
|
508
|
+
return objects;
|
|
377
509
|
}
|
|
378
510
|
|
|
379
511
|
function renderRelationships() {
|
|
@@ -390,7 +522,7 @@ function renderRelationships() {
|
|
|
390
522
|
canvas.add(...objects);
|
|
391
523
|
});
|
|
392
524
|
|
|
393
|
-
|
|
525
|
+
restackCanvasObjects();
|
|
394
526
|
canvas.requestRenderAll();
|
|
395
527
|
updateStatus();
|
|
396
528
|
}
|
|
@@ -448,38 +580,666 @@ async function layoutTables() {
|
|
|
448
580
|
}
|
|
449
581
|
|
|
450
582
|
function createNoteObject(note) {
|
|
583
|
+
const borderColor = CanvasERDDocument.noteBorderColor(
|
|
584
|
+
CanvasERDDocument.noteFormatting(note).backgroundColor
|
|
585
|
+
);
|
|
451
586
|
const object = new fabric.Textbox(note.text, CanvasERDDocument.uncachedText({
|
|
452
587
|
...CanvasERDDocument.noteCanvasGeometry(note),
|
|
453
|
-
fill: "#4a3f12",
|
|
454
|
-
backgroundColor: "#fff2a8",
|
|
455
|
-
fontFamily: CanvasERDDocument.CANVAS_FONT_FAMILY,
|
|
456
|
-
fontSize: 15,
|
|
457
|
-
lineHeight: 1.25,
|
|
458
588
|
padding: CanvasERDDocument.noteSelectionPadding(canvas.getZoom()),
|
|
459
|
-
borderColor
|
|
460
|
-
editingBorderColor:
|
|
461
|
-
cornerColor:
|
|
589
|
+
borderColor,
|
|
590
|
+
editingBorderColor: borderColor,
|
|
591
|
+
cornerColor: borderColor,
|
|
462
592
|
transparentCorners: false,
|
|
463
593
|
lockScalingY: true
|
|
464
594
|
}));
|
|
465
|
-
object._renderBackground =
|
|
466
|
-
if (!this.backgroundColor) return;
|
|
467
|
-
|
|
468
|
-
const dimensions = this._getNonTransformedDimensions();
|
|
469
|
-
const bounds = CanvasERDDocument.paddedBackgroundBounds(
|
|
470
|
-
dimensions.x,
|
|
471
|
-
dimensions.y,
|
|
472
|
-
CanvasERDDocument.NOTE_PADDING
|
|
473
|
-
);
|
|
474
|
-
context.fillStyle = this.backgroundColor;
|
|
475
|
-
context.fillRect(bounds.left, bounds.top, bounds.width, bounds.height);
|
|
476
|
-
this._removeShadow(context);
|
|
477
|
-
};
|
|
595
|
+
object._renderBackground = renderPaddedBackground;
|
|
478
596
|
object.canvasErdType = "note";
|
|
479
597
|
object.noteId = note.id;
|
|
480
598
|
return object;
|
|
481
599
|
}
|
|
482
600
|
|
|
601
|
+
function renderPaddedBackground(context) {
|
|
602
|
+
if (!this.backgroundColor) return;
|
|
603
|
+
|
|
604
|
+
const dimensions = this._getNonTransformedDimensions();
|
|
605
|
+
const bounds = CanvasERDDocument.paddedBackgroundBounds(
|
|
606
|
+
dimensions.x,
|
|
607
|
+
dimensions.y,
|
|
608
|
+
CanvasERDDocument.NOTE_PADDING
|
|
609
|
+
);
|
|
610
|
+
context.fillStyle = this.backgroundColor;
|
|
611
|
+
context.fillRect(bounds.left, bounds.top, bounds.width, bounds.height);
|
|
612
|
+
this._removeShadow(context);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function activeNoteObject() {
|
|
616
|
+
const active = canvas?.getActiveObject();
|
|
617
|
+
return active?.canvasErdType === "note" ? active : null;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
class CodeTextbox extends fabric.Textbox {
|
|
621
|
+
_getLeftOffset() {
|
|
622
|
+
return super._getLeftOffset() + (this.gutterWidth || 0);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
_wrapText(lines, desiredWidth) {
|
|
626
|
+
const wrapped = super._wrapText(lines, desiredWidth - (this.gutterWidth || 0));
|
|
627
|
+
this.dynamicMinWidth += this.gutterWidth || 0;
|
|
628
|
+
return wrapped;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
gutterLabelAt(event) {
|
|
632
|
+
const block = findCodeBlock(this.codeBlockId);
|
|
633
|
+
if (!this.gutterWidth || !block || !this.canvas) return undefined;
|
|
634
|
+
|
|
635
|
+
const point = this.canvas.getScenePoint(event).transform(fabric.util.invertTransform(this.calcTransformMatrix()));
|
|
636
|
+
const x = point.x + this.width / 2;
|
|
637
|
+
if (x < -CanvasERDDocument.NOTE_PADDING || x > this.gutterWidth - this.fontSize * 0.3) return undefined;
|
|
638
|
+
|
|
639
|
+
const row = CanvasERDDocument.codeRowAt(
|
|
640
|
+
this._textLines.map((_, index) => this.getHeightOfLine(index)),
|
|
641
|
+
point.y - this._getTopOffset()
|
|
642
|
+
);
|
|
643
|
+
if (row < 0) return null;
|
|
644
|
+
const labels = CanvasERDDocument.codeLineLabels(block, this.text.split("\n").length);
|
|
645
|
+
return labels[this._styleMap[row].line] ?? null;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
mouseUpHandler(options) {
|
|
649
|
+
if (this.gutterLabelAt(options.e) !== undefined) return;
|
|
650
|
+
super.mouseUpHandler(options);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function toggleCodeLineHighlight(object, label, extend) {
|
|
655
|
+
const block = findCodeBlock(object.codeBlockId);
|
|
656
|
+
if (!block) return;
|
|
657
|
+
|
|
658
|
+
const labels = CanvasERDDocument.codeLineLabels(block, object.text.split("\n").length);
|
|
659
|
+
block.highlightedLines = extend && object.highlightAnchor !== undefined
|
|
660
|
+
? CanvasERDDocument.highlightLineRange(block.highlightedLines, labels, object.highlightAnchor, label)
|
|
661
|
+
: CanvasERDDocument.toggleHighlightedLine(block.highlightedLines, label);
|
|
662
|
+
object.highlightAnchor = label;
|
|
663
|
+
canvas.requestRenderAll();
|
|
664
|
+
markDirty();
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function codeGutterWidth(block, text) {
|
|
668
|
+
if (!block.showLineNumbers) return 0;
|
|
669
|
+
const labels = CanvasERDDocument.codeLineLabels(block, text.split("\n").length);
|
|
670
|
+
return CanvasERDDocument.codeGutterWidth(labels, CanvasERDDocument.codeFontSize(block));
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function renderCodeBackground(context) {
|
|
674
|
+
renderPaddedBackground.call(this, context);
|
|
675
|
+
const block = findCodeBlock(this.codeBlockId);
|
|
676
|
+
if (!block) return;
|
|
677
|
+
|
|
678
|
+
const labels = CanvasERDDocument.codeLineLabels(block, this.text.split("\n").length);
|
|
679
|
+
const highlighted = new Set(block.highlightedLines || []);
|
|
680
|
+
const dimensions = this._getNonTransformedDimensions();
|
|
681
|
+
const padding = CanvasERDDocument.NOTE_PADDING;
|
|
682
|
+
context.save();
|
|
683
|
+
if (highlighted.size > 0) {
|
|
684
|
+
let rowTop = this._getTopOffset();
|
|
685
|
+
context.fillStyle = "#fff8c5";
|
|
686
|
+
this._textLines.forEach((_, index) => {
|
|
687
|
+
const height = this.getHeightOfLine(index);
|
|
688
|
+
if (highlighted.has(labels[this._styleMap[index].line])) {
|
|
689
|
+
context.fillRect(-dimensions.x / 2 - padding, rowTop, dimensions.x + padding * 2, height);
|
|
690
|
+
}
|
|
691
|
+
rowTop += height;
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
if (!this.gutterWidth) {
|
|
695
|
+
context.restore();
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const right = -this.width / 2 + this.gutterWidth - this.fontSize * 0.6;
|
|
700
|
+
const separator = -this.width / 2 + this.gutterWidth - this.fontSize * 0.3;
|
|
701
|
+
let top = this._getTopOffset();
|
|
702
|
+
context.strokeStyle = "#d0d7de";
|
|
703
|
+
context.lineWidth = 1;
|
|
704
|
+
context.beginPath();
|
|
705
|
+
context.moveTo(separator, -dimensions.y / 2 - CanvasERDDocument.NOTE_PADDING);
|
|
706
|
+
context.lineTo(separator, dimensions.y / 2 + CanvasERDDocument.NOTE_PADDING);
|
|
707
|
+
context.stroke();
|
|
708
|
+
context.font = `${this.fontSize}px ${this.fontFamily}`;
|
|
709
|
+
context.fillStyle = "#8c959f";
|
|
710
|
+
context.textAlign = "right";
|
|
711
|
+
this._textLines.forEach((_, index) => {
|
|
712
|
+
const line = this._styleMap[index];
|
|
713
|
+
const label = line.offset === 0 ? labels[line.line] : null;
|
|
714
|
+
if (label !== null && label !== undefined) {
|
|
715
|
+
const baseline = top + this.getHeightOfLineImpl(index) - this.fontSize * this._fontSizeFraction;
|
|
716
|
+
context.fillText(String(label), right, baseline);
|
|
717
|
+
}
|
|
718
|
+
top += this.getHeightOfLine(index);
|
|
719
|
+
});
|
|
720
|
+
context.restore();
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function updateCodeGutter(object) {
|
|
724
|
+
const block = findCodeBlock(object.codeBlockId);
|
|
725
|
+
const width = block ? codeGutterWidth(block, object.text) : 0;
|
|
726
|
+
if ((object.gutterWidth || 0) === width) return;
|
|
727
|
+
object.gutterWidth = width;
|
|
728
|
+
object.initDimensions();
|
|
729
|
+
object.setCoords();
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function toggleCodeLineNumbers() {
|
|
733
|
+
const object = activeCodeObject();
|
|
734
|
+
const block = object && findCodeBlock(object.codeBlockId);
|
|
735
|
+
if (!block) return;
|
|
736
|
+
|
|
737
|
+
block.showLineNumbers = !block.showLineNumbers;
|
|
738
|
+
const previousWidth = object.gutterWidth || 0;
|
|
739
|
+
object.gutterWidth = codeGutterWidth(block, object.text);
|
|
740
|
+
object.set({ width: object.width + object.gutterWidth - previousWidth });
|
|
741
|
+
object.initDimensions();
|
|
742
|
+
object.setCoords();
|
|
743
|
+
updateObjectState(object);
|
|
744
|
+
renderArrows();
|
|
745
|
+
updateCodeFormattingControls();
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function applyCodeSize(size) {
|
|
749
|
+
const object = activeCodeObject();
|
|
750
|
+
const block = object && findCodeBlock(object.codeBlockId);
|
|
751
|
+
if (!block) return;
|
|
752
|
+
|
|
753
|
+
const previousFontSize = CanvasERDDocument.codeFontSize(block);
|
|
754
|
+
block.size = size;
|
|
755
|
+
const fontSize = CanvasERDDocument.codeFontSize(block);
|
|
756
|
+
object.set({ fontSize, width: object.width * fontSize / previousFontSize });
|
|
757
|
+
object.gutterWidth = codeGutterWidth(block, object.text);
|
|
758
|
+
object.initDimensions();
|
|
759
|
+
object.setCoords();
|
|
760
|
+
updateObjectState(object);
|
|
761
|
+
renderArrows();
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
function createCodeObject(block) {
|
|
765
|
+
const borderColor = CanvasERDDocument.noteBorderColor(highlighting.background);
|
|
766
|
+
const object = new CodeTextbox(block.code, CanvasERDDocument.uncachedText({
|
|
767
|
+
...CanvasERDDocument.codeCanvasGeometry(block),
|
|
768
|
+
gutterWidth: codeGutterWidth(block, block.code),
|
|
769
|
+
fill: highlighting.foreground,
|
|
770
|
+
backgroundColor: highlighting.background,
|
|
771
|
+
editable: !block.source,
|
|
772
|
+
padding: CanvasERDDocument.noteSelectionPadding(canvas.getZoom()),
|
|
773
|
+
borderColor,
|
|
774
|
+
editingBorderColor: borderColor,
|
|
775
|
+
cornerColor: borderColor,
|
|
776
|
+
transparentCorners: false,
|
|
777
|
+
lockScalingY: true
|
|
778
|
+
}));
|
|
779
|
+
object._renderBackground = renderCodeBackground;
|
|
780
|
+
object.canvasErdType = "code";
|
|
781
|
+
object.codeBlockId = block.id;
|
|
782
|
+
highlightCodeObject(object);
|
|
783
|
+
return object;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function findCodeBlock(id) {
|
|
787
|
+
return state.codeBlocks.find((candidate) => candidate.id === id);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function activeCodeObject() {
|
|
791
|
+
const active = canvas?.getActiveObject();
|
|
792
|
+
return active?.canvasErdType === "code" ? active : null;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function scheduleCodeHighlight(object, delay = 300) {
|
|
796
|
+
clearTimeout(codeHighlightTimers.get(object.codeBlockId));
|
|
797
|
+
codeHighlightTimers.set(object.codeBlockId, setTimeout(() => highlightCodeObject(object), delay));
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
async function highlightCodeObject(object) {
|
|
801
|
+
codeHighlightTimers.delete(object.codeBlockId);
|
|
802
|
+
const block = findCodeBlock(object.codeBlockId);
|
|
803
|
+
if (!block) return;
|
|
804
|
+
|
|
805
|
+
const code = object.text;
|
|
806
|
+
try {
|
|
807
|
+
const response = await fetch("/api/highlight", {
|
|
808
|
+
method: "POST",
|
|
809
|
+
headers: { "content-type": "application/json" },
|
|
810
|
+
body: JSON.stringify({ code, language: block.language })
|
|
811
|
+
});
|
|
812
|
+
if (!response.ok) throw new Error(await response.text());
|
|
813
|
+
|
|
814
|
+
const result = await response.json();
|
|
815
|
+
if (codeObjects.get(block.id) !== object || object.text !== code) return;
|
|
816
|
+
if (result.code !== code) {
|
|
817
|
+
if (object.isEditing) return;
|
|
818
|
+
object.set({ text: result.code });
|
|
819
|
+
block.code = result.code;
|
|
820
|
+
markDirty();
|
|
821
|
+
}
|
|
822
|
+
object.styles = CanvasERDDocument.codeTokenStyles(result.runs);
|
|
823
|
+
object.initDimensions();
|
|
824
|
+
object.setCoords();
|
|
825
|
+
canvas.requestRenderAll();
|
|
826
|
+
} catch (error) {
|
|
827
|
+
showNotice(`Could not highlight code: ${error.message}`, true);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
function updateCodeFormattingControls() {
|
|
832
|
+
const object = activeCodeObject();
|
|
833
|
+
const block = object && findCodeBlock(object.codeBlockId);
|
|
834
|
+
codeFormattingElement.hidden = !block;
|
|
835
|
+
if (!block) return;
|
|
836
|
+
|
|
837
|
+
lastCodeLanguage = block.language;
|
|
838
|
+
codeLanguageElement.value = block.language;
|
|
839
|
+
codeLineNumbersButton.setAttribute("aria-pressed", String(Boolean(block.showLineNumbers)));
|
|
840
|
+
codeSizeElement.value = block.size || "medium";
|
|
841
|
+
const fileSource = block.source?.path ? block.source : null;
|
|
842
|
+
codeSourceElement.hidden = !block.source?.model;
|
|
843
|
+
codeSourceElement.textContent = block.source?.model ? CanvasERDDocument.sourceReferenceLabel(block.source) : "";
|
|
844
|
+
codeSourceFieldsElement.hidden = !fileSource;
|
|
845
|
+
codeSourcePathElement.value = fileSource?.path || "";
|
|
846
|
+
codeSourceStartElement.value = fileSource?.startLine || "";
|
|
847
|
+
codeSourceEndElement.value = fileSource?.endLine || "";
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
async function applyCodeSource() {
|
|
851
|
+
const object = activeCodeObject();
|
|
852
|
+
const block = object && findCodeBlock(object.codeBlockId);
|
|
853
|
+
if (!block?.source?.path) return;
|
|
854
|
+
|
|
855
|
+
const source = CanvasERDDocument.sourceFromFields(
|
|
856
|
+
codeSourcePathElement.value,
|
|
857
|
+
codeSourceStartElement.value,
|
|
858
|
+
codeSourceEndElement.value
|
|
859
|
+
);
|
|
860
|
+
try {
|
|
861
|
+
if (!source) throw new Error("Enter a source file path.");
|
|
862
|
+
const result = await fetchSource(source);
|
|
863
|
+
if (source.path !== block.source.path) block.language = result.language;
|
|
864
|
+
block.source = source;
|
|
865
|
+
replaceCodeText(object, block, result.code);
|
|
866
|
+
markDirty();
|
|
867
|
+
} catch (error) {
|
|
868
|
+
showNotice(`Could not change source file: ${error.message}`, true);
|
|
869
|
+
}
|
|
870
|
+
if (activeCodeObject() === object) updateCodeFormattingControls();
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
function replaceCodeText(object, block, code) {
|
|
874
|
+
block.code = code;
|
|
875
|
+
object.set({ text: code });
|
|
876
|
+
object.styles = {};
|
|
877
|
+
updateCodeGutter(object);
|
|
878
|
+
object.setCoords();
|
|
879
|
+
renderArrows();
|
|
880
|
+
highlightCodeObject(object);
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
async function loadSourceFilePaths() {
|
|
884
|
+
try {
|
|
885
|
+
const response = await fetch("/api/source_files", { cache: "no-store" });
|
|
886
|
+
if (!response.ok) throw new Error(await response.text());
|
|
887
|
+
|
|
888
|
+
const result = await response.json();
|
|
889
|
+
sourceFilePathsElement.replaceChildren(...result.paths.map((path) => {
|
|
890
|
+
const option = document.createElement("option");
|
|
891
|
+
option.value = path;
|
|
892
|
+
return option;
|
|
893
|
+
}));
|
|
894
|
+
} catch (error) {
|
|
895
|
+
showNotice(`Could not list source files: ${error.message}`, true);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function renderCodeLanguages() {
|
|
900
|
+
codeLanguageElement.replaceChildren(...highlighting.languages.map((language) => {
|
|
901
|
+
const option = document.createElement("option");
|
|
902
|
+
option.value = language.tag;
|
|
903
|
+
option.textContent = language.title;
|
|
904
|
+
return option;
|
|
905
|
+
}));
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
function applyCodeLanguage(language) {
|
|
909
|
+
const object = activeCodeObject();
|
|
910
|
+
const block = object && findCodeBlock(object.codeBlockId);
|
|
911
|
+
if (!block) return;
|
|
912
|
+
|
|
913
|
+
block.language = language;
|
|
914
|
+
lastCodeLanguage = language;
|
|
915
|
+
highlightCodeObject(object);
|
|
916
|
+
markDirty();
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
function renderCodeBlocks() {
|
|
920
|
+
state.codeBlocks.forEach((block) => {
|
|
921
|
+
const object = createCodeObject(block);
|
|
922
|
+
codeObjects.set(block.id, object);
|
|
923
|
+
canvas.add(object);
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
function sourceUrl(source) {
|
|
928
|
+
if (source.model) {
|
|
929
|
+
return `/api/model_detail?${new URLSearchParams({ model: source.model, detail: source.detail })}`;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
const query = new URLSearchParams({ path: source.path });
|
|
933
|
+
if (source.startLine) query.set("start", source.startLine);
|
|
934
|
+
if (source.endLine) query.set("end", source.endLine);
|
|
935
|
+
return `/api/source?${query}`;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
async function fetchSource(source) {
|
|
939
|
+
const response = await fetch(sourceUrl(source), { cache: "no-store" });
|
|
940
|
+
if (!response.ok) throw new Error(await response.text());
|
|
941
|
+
return response.json();
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
async function refreshSourceCodeBlocks() {
|
|
945
|
+
const failures = [];
|
|
946
|
+
await Promise.all(state.codeBlocks.filter((block) => block.source).map(async (block) => {
|
|
947
|
+
try {
|
|
948
|
+
const result = await fetchSource(block.source);
|
|
949
|
+
const object = codeObjects.get(block.id);
|
|
950
|
+
if (result.lineNumbers) block.lineNumbers = result.lineNumbers;
|
|
951
|
+
canvas.requestRenderAll();
|
|
952
|
+
if (result.code === block.code || !object) return;
|
|
953
|
+
replaceCodeText(object, block, result.code);
|
|
954
|
+
} catch (error) {
|
|
955
|
+
failures.push(`${CanvasERDDocument.sourceReferenceLabel(block.source)}: ${error.message}`);
|
|
956
|
+
}
|
|
957
|
+
}));
|
|
958
|
+
if (failures.length > 0) showNotice(`Could not refresh source files: ${failures.join("; ")}`, true);
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
function activeTableEntityIds() {
|
|
962
|
+
return CanvasERDDocument.selectedEntityIds(canvas?.getActiveObject());
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
function updateNoteFormattingControls() {
|
|
966
|
+
const note = activeNoteObject();
|
|
967
|
+
noteFormattingElement.hidden = !note;
|
|
968
|
+
if (!note) return;
|
|
969
|
+
|
|
970
|
+
const formatting = CanvasERDDocument.noteFormatting(note);
|
|
971
|
+
lastNoteFormatting = CanvasERDDocument.inheritedNoteFormatting(formatting);
|
|
972
|
+
noteBoldButton.setAttribute("aria-pressed", String(formatting.fontWeight === "bold"));
|
|
973
|
+
noteItalicButton.setAttribute("aria-pressed", String(formatting.fontStyle === "italic"));
|
|
974
|
+
noteUnderlineButton.setAttribute("aria-pressed", String(formatting.underline));
|
|
975
|
+
noteColorControls.forEach((control) => {
|
|
976
|
+
const color = formatting[control.property];
|
|
977
|
+
control.preview.style.backgroundColor = color;
|
|
978
|
+
control.preview.classList.toggle("transparent-color", color === "transparent");
|
|
979
|
+
if (control.transparent) {
|
|
980
|
+
control.transparent.setAttribute("aria-pressed", String(color === "transparent"));
|
|
981
|
+
}
|
|
982
|
+
if (/^#[0-9a-f]{6}$/i.test(color)) control.custom.value = color;
|
|
983
|
+
});
|
|
984
|
+
noteParagraphStyleElement.value = formatting.paragraphStyle;
|
|
985
|
+
noteAlignmentElement.value = formatting.textAlign;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
function closeNoteFormattingMenus() {
|
|
989
|
+
let closed = false;
|
|
990
|
+
noteColorControls.forEach((control) => {
|
|
991
|
+
if (!control.menu.hidden) closed = true;
|
|
992
|
+
control.menu.hidden = true;
|
|
993
|
+
control.button.setAttribute("aria-expanded", "false");
|
|
994
|
+
});
|
|
995
|
+
if (!tableHeaderColorControl.menu.hidden) closed = true;
|
|
996
|
+
tableHeaderColorControl.menu.hidden = true;
|
|
997
|
+
tableHeaderColorControl.button.setAttribute("aria-expanded", "false");
|
|
998
|
+
if (!arrowColorControl.menu.hidden) closed = true;
|
|
999
|
+
arrowColorControl.menu.hidden = true;
|
|
1000
|
+
arrowColorControl.button.setAttribute("aria-expanded", "false");
|
|
1001
|
+
if (!frameColorControl.menu.hidden) closed = true;
|
|
1002
|
+
frameColorControl.menu.hidden = true;
|
|
1003
|
+
frameColorControl.button.setAttribute("aria-expanded", "false");
|
|
1004
|
+
return closed;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
function renderColorSwatches(element, colors, defaults, currentColor, applyColor) {
|
|
1008
|
+
const fragment = document.createDocumentFragment();
|
|
1009
|
+
const sections = CanvasERDDocument.colorPaletteSections(colors, defaults);
|
|
1010
|
+
const appendSwatches = (swatches) => swatches.forEach((color) => {
|
|
1011
|
+
const swatch = document.createElement("button");
|
|
1012
|
+
swatch.className = "color-swatch";
|
|
1013
|
+
swatch.type = "button";
|
|
1014
|
+
swatch.title = color;
|
|
1015
|
+
swatch.setAttribute("aria-label", `Use ${color}`);
|
|
1016
|
+
swatch.setAttribute("aria-pressed", String(color === currentColor));
|
|
1017
|
+
swatch.style.backgroundColor = color;
|
|
1018
|
+
swatch.classList.toggle("transparent-color", color === "transparent");
|
|
1019
|
+
swatch.addEventListener("click", () => {
|
|
1020
|
+
applyColor(color);
|
|
1021
|
+
closeNoteFormattingMenus();
|
|
1022
|
+
});
|
|
1023
|
+
fragment.append(swatch);
|
|
1024
|
+
});
|
|
1025
|
+
appendSwatches(sections.defaults);
|
|
1026
|
+
if (sections.custom.length > 0) {
|
|
1027
|
+
const separator = document.createElement("div");
|
|
1028
|
+
separator.className = "swatch-separator";
|
|
1029
|
+
separator.setAttribute("role", "separator");
|
|
1030
|
+
fragment.append(separator);
|
|
1031
|
+
appendSwatches(sections.custom);
|
|
1032
|
+
}
|
|
1033
|
+
element.replaceChildren(fragment);
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
function renderNoteColorSwatches(control) {
|
|
1037
|
+
const note = activeNoteObject();
|
|
1038
|
+
if (!note) return;
|
|
1039
|
+
|
|
1040
|
+
const currentColor = CanvasERDDocument.noteFormatting(note)[control.property].toLowerCase();
|
|
1041
|
+
renderColorSwatches(
|
|
1042
|
+
control.swatches,
|
|
1043
|
+
CanvasERDDocument.noteColorSwatches(state.notes, control.property, control.defaults),
|
|
1044
|
+
control.defaults,
|
|
1045
|
+
currentColor,
|
|
1046
|
+
(color) => applyNoteFormatting({ [control.property]: color })
|
|
1047
|
+
);
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
function toggleNoteColorMenu(control) {
|
|
1051
|
+
const opening = control.menu.hidden;
|
|
1052
|
+
closeNoteFormattingMenus();
|
|
1053
|
+
if (!opening || !activeNoteObject()) return;
|
|
1054
|
+
|
|
1055
|
+
renderNoteColorSwatches(control);
|
|
1056
|
+
control.menu.hidden = false;
|
|
1057
|
+
control.button.setAttribute("aria-expanded", "true");
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
function updateTableFormattingControls() {
|
|
1061
|
+
const entityIds = activeTableEntityIds();
|
|
1062
|
+
tableFormattingElement.hidden = entityIds.length === 0;
|
|
1063
|
+
if (entityIds.length === 0) return;
|
|
1064
|
+
|
|
1065
|
+
const color = CanvasERDDocument.selectedTableHeaderColor(state, entityIds);
|
|
1066
|
+
tableHeaderColorControl.preview.style.backgroundColor = color || "#ffffff";
|
|
1067
|
+
tableHeaderColorControl.preview.classList.toggle("mixed-color", color === null);
|
|
1068
|
+
if (color) tableHeaderColorControl.custom.value = color;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
function applyTableHeaderColor(color) {
|
|
1072
|
+
const entityIds = activeTableEntityIds();
|
|
1073
|
+
if (entityIds.length === 0) return;
|
|
1074
|
+
|
|
1075
|
+
state = CanvasERDDocument.setTableHeaderColor(state, entityIds, color);
|
|
1076
|
+
entityIds.forEach((entityId) => {
|
|
1077
|
+
const table = tableObjects.get(entityId);
|
|
1078
|
+
if (!table) return;
|
|
1079
|
+
table.getObjects().slice(1, 3).forEach((header) => header.set({ fill: color }));
|
|
1080
|
+
table.dirty = true;
|
|
1081
|
+
});
|
|
1082
|
+
updateTableFormattingControls();
|
|
1083
|
+
canvas.requestRenderAll();
|
|
1084
|
+
markDirty();
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
function renderTableHeaderColorSwatches() {
|
|
1088
|
+
const entityIds = activeTableEntityIds();
|
|
1089
|
+
if (entityIds.length === 0) return;
|
|
1090
|
+
|
|
1091
|
+
renderColorSwatches(
|
|
1092
|
+
tableHeaderColorControl.swatches,
|
|
1093
|
+
CanvasERDDocument.tableHeaderColorSwatches(state.tableHeaderColors),
|
|
1094
|
+
CanvasERDDocument.DEFAULT_TABLE_HEADER_COLORS,
|
|
1095
|
+
CanvasERDDocument.selectedTableHeaderColor(state, entityIds),
|
|
1096
|
+
applyTableHeaderColor
|
|
1097
|
+
);
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
function toggleTableHeaderColorMenu() {
|
|
1101
|
+
const opening = tableHeaderColorControl.menu.hidden;
|
|
1102
|
+
closeNoteFormattingMenus();
|
|
1103
|
+
if (!opening || activeTableEntityIds().length === 0) return;
|
|
1104
|
+
|
|
1105
|
+
renderTableHeaderColorSwatches();
|
|
1106
|
+
tableHeaderColorControl.menu.hidden = false;
|
|
1107
|
+
tableHeaderColorControl.button.setAttribute("aria-expanded", "true");
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
function editingArrow() {
|
|
1111
|
+
return state.arrows.find((arrow) => arrow.id === editingArrowId) || null;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
function updateArrowFormattingControls() {
|
|
1115
|
+
const arrow = editingArrow();
|
|
1116
|
+
arrowFormattingElement.hidden = !arrow;
|
|
1117
|
+
if (!arrow) return;
|
|
1118
|
+
|
|
1119
|
+
const formatting = CanvasERDDocument.arrowFormatting(arrow);
|
|
1120
|
+
arrowColorControl.preview.style.backgroundColor = formatting.color;
|
|
1121
|
+
if (/^#[0-9a-f]{6}$/i.test(formatting.color)) arrowColorControl.custom.value = formatting.color;
|
|
1122
|
+
arrowStartHeadElement.value = formatting.startHead;
|
|
1123
|
+
arrowEndHeadElement.value = formatting.endHead;
|
|
1124
|
+
arrowLineStyleElement.value = formatting.lineStyle;
|
|
1125
|
+
arrowStrokeWidthElement.value = formatting.strokeWidth;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
function applyArrowFormatting(changes) {
|
|
1129
|
+
const arrow = editingArrow();
|
|
1130
|
+
if (!arrow) return;
|
|
1131
|
+
|
|
1132
|
+
Object.assign(arrow, CanvasERDDocument.arrowFormatting({ ...arrow, ...changes }));
|
|
1133
|
+
renderArrows();
|
|
1134
|
+
updateArrowFormattingControls();
|
|
1135
|
+
markDirty();
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
function renderArrowColorSwatches() {
|
|
1139
|
+
const arrow = editingArrow();
|
|
1140
|
+
if (!arrow) return;
|
|
1141
|
+
|
|
1142
|
+
renderColorSwatches(
|
|
1143
|
+
arrowColorControl.swatches,
|
|
1144
|
+
CanvasERDDocument.arrowColorSwatches(state.arrows),
|
|
1145
|
+
CanvasERDDocument.DEFAULT_FOREGROUND_COLORS,
|
|
1146
|
+
CanvasERDDocument.arrowFormatting(arrow).color.toLowerCase(),
|
|
1147
|
+
(color) => applyArrowFormatting({ color })
|
|
1148
|
+
);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
function toggleArrowColorMenu() {
|
|
1152
|
+
const opening = arrowColorControl.menu.hidden;
|
|
1153
|
+
closeNoteFormattingMenus();
|
|
1154
|
+
if (!opening || !editingArrow()) return;
|
|
1155
|
+
|
|
1156
|
+
renderArrowColorSwatches();
|
|
1157
|
+
arrowColorControl.menu.hidden = false;
|
|
1158
|
+
arrowColorControl.button.setAttribute("aria-expanded", "true");
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
function activeFrame() {
|
|
1162
|
+
const active = canvas?.getActiveObject();
|
|
1163
|
+
const frameId = ["frame", "frame-label"].includes(active?.canvasErdType) ? active.frameId : null;
|
|
1164
|
+
return state.frames.find((frame) => frame.id === frameId) || null;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
function updateFrameFormattingControls() {
|
|
1168
|
+
const frame = activeFrame();
|
|
1169
|
+
frameFormattingElement.hidden = !frame;
|
|
1170
|
+
if (!frame) return;
|
|
1171
|
+
|
|
1172
|
+
const formatting = CanvasERDDocument.frameFormatting(frame);
|
|
1173
|
+
frameColorControl.preview.style.backgroundColor = formatting.color;
|
|
1174
|
+
if (/^#[0-9a-f]{6}$/i.test(formatting.color)) frameColorControl.custom.value = formatting.color;
|
|
1175
|
+
frameLineStyleElement.value = formatting.lineStyle;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
function applyFrameFormatting(changes) {
|
|
1179
|
+
const frame = activeFrame();
|
|
1180
|
+
if (!frame) return;
|
|
1181
|
+
|
|
1182
|
+
Object.assign(frame, CanvasERDDocument.frameFormatting({ ...frame, ...changes }));
|
|
1183
|
+
frameObjects.get(frame.id)?.set({
|
|
1184
|
+
stroke: frame.color,
|
|
1185
|
+
strokeDashArray: CanvasERDDocument.arrowStrokeDashArray(frame.lineStyle),
|
|
1186
|
+
strokeLineCap: frame.lineStyle === "dotted" ? "round" : "butt"
|
|
1187
|
+
});
|
|
1188
|
+
frameLabelObjects.get(frame.id)?.set({ fill: frame.color });
|
|
1189
|
+
updateFrameFormattingControls();
|
|
1190
|
+
canvas.requestRenderAll();
|
|
1191
|
+
markDirty();
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
function renderFrameColorSwatches() {
|
|
1195
|
+
const frame = activeFrame();
|
|
1196
|
+
if (!frame) return;
|
|
1197
|
+
|
|
1198
|
+
renderColorSwatches(
|
|
1199
|
+
frameColorControl.swatches,
|
|
1200
|
+
CanvasERDDocument.frameColorSwatches(state.frames),
|
|
1201
|
+
CanvasERDDocument.DEFAULT_FOREGROUND_COLORS,
|
|
1202
|
+
CanvasERDDocument.frameFormatting(frame).color.toLowerCase(),
|
|
1203
|
+
(color) => applyFrameFormatting({ color })
|
|
1204
|
+
);
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
function toggleFrameColorMenu() {
|
|
1208
|
+
const opening = frameColorControl.menu.hidden;
|
|
1209
|
+
closeNoteFormattingMenus();
|
|
1210
|
+
if (!opening || !activeFrame()) return;
|
|
1211
|
+
|
|
1212
|
+
renderFrameColorSwatches();
|
|
1213
|
+
frameColorControl.menu.hidden = false;
|
|
1214
|
+
frameColorControl.button.setAttribute("aria-expanded", "true");
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
function applyNoteFormatting(formatting) {
|
|
1218
|
+
const note = activeNoteObject();
|
|
1219
|
+
if (!note) return;
|
|
1220
|
+
|
|
1221
|
+
note.set(formatting);
|
|
1222
|
+
const borderColor = CanvasERDDocument.noteBorderColor(note.backgroundColor);
|
|
1223
|
+
note.set({ borderColor, editingBorderColor: borderColor, cornerColor: borderColor });
|
|
1224
|
+
note.setCoords();
|
|
1225
|
+
updateObjectState(note);
|
|
1226
|
+
updateNoteFormattingControls();
|
|
1227
|
+
canvas.requestRenderAll();
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
function toggleNoteFormatting(formatting) {
|
|
1231
|
+
const note = activeNoteObject();
|
|
1232
|
+
if (!note) return;
|
|
1233
|
+
|
|
1234
|
+
if (formatting === "bold") {
|
|
1235
|
+
applyNoteFormatting({ fontWeight: note.fontWeight === "bold" ? "normal" : "bold" });
|
|
1236
|
+
} else if (formatting === "italic") {
|
|
1237
|
+
applyNoteFormatting({ fontStyle: note.fontStyle === "italic" ? "normal" : "italic" });
|
|
1238
|
+
} else if (formatting === "underline") {
|
|
1239
|
+
applyNoteFormatting({ underline: !note.underline });
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
|
|
483
1243
|
function renderNotes() {
|
|
484
1244
|
state.notes.forEach((note) => {
|
|
485
1245
|
const object = createNoteObject(note);
|
|
@@ -488,10 +1248,156 @@ function renderNotes() {
|
|
|
488
1248
|
});
|
|
489
1249
|
}
|
|
490
1250
|
|
|
1251
|
+
function createFrameObject(frame) {
|
|
1252
|
+
const formatting = CanvasERDDocument.frameFormatting(frame);
|
|
1253
|
+
const object = new fabric.Rect(CanvasERDDocument.topLeft({
|
|
1254
|
+
left: frame.x,
|
|
1255
|
+
top: frame.y,
|
|
1256
|
+
width: frame.width,
|
|
1257
|
+
height: frame.height,
|
|
1258
|
+
fill: "transparent",
|
|
1259
|
+
stroke: formatting.color,
|
|
1260
|
+
strokeWidth: 2,
|
|
1261
|
+
strokeDashArray: CanvasERDDocument.arrowStrokeDashArray(formatting.lineStyle),
|
|
1262
|
+
strokeLineCap: formatting.lineStyle === "dotted" ? "round" : "butt",
|
|
1263
|
+
strokeUniform: true,
|
|
1264
|
+
objectCaching: false,
|
|
1265
|
+
perPixelTargetFind: true,
|
|
1266
|
+
padding: 4,
|
|
1267
|
+
borderColor: "#2563eb",
|
|
1268
|
+
cornerColor: "#2563eb",
|
|
1269
|
+
transparentCorners: false,
|
|
1270
|
+
lockRotation: true,
|
|
1271
|
+
lockSkewingX: true,
|
|
1272
|
+
lockSkewingY: true
|
|
1273
|
+
}));
|
|
1274
|
+
object.setControlsVisibility({ mtr: false });
|
|
1275
|
+
object.canvasErdType = "frame";
|
|
1276
|
+
object.frameId = frame.id;
|
|
1277
|
+
return object;
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
function createFrameLabelObject(frame) {
|
|
1281
|
+
const label = typeof frame.label === "string" ? frame.label : "Frame";
|
|
1282
|
+
const object = new fabric.IText(label, CanvasERDDocument.uncachedText({
|
|
1283
|
+
...CanvasERDDocument.frameLabelCanvasStyle(frame),
|
|
1284
|
+
fontFamily: CanvasERDDocument.CANVAS_FONT_FAMILY,
|
|
1285
|
+
fontSize: 15,
|
|
1286
|
+
fontWeight: "bold",
|
|
1287
|
+
padding: 3,
|
|
1288
|
+
hasControls: false,
|
|
1289
|
+
lockMovementX: true,
|
|
1290
|
+
lockMovementY: true,
|
|
1291
|
+
borderColor: "#2563eb",
|
|
1292
|
+
editingBorderColor: "#2563eb"
|
|
1293
|
+
}));
|
|
1294
|
+
object.canvasErdType = "frame-label";
|
|
1295
|
+
object.frameId = frame.id;
|
|
1296
|
+
return object;
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
function renderFrames() {
|
|
1300
|
+
frameObjects.forEach((object) => canvas.remove(object));
|
|
1301
|
+
frameLabelObjects.forEach((object) => canvas.remove(object));
|
|
1302
|
+
frameObjects.clear();
|
|
1303
|
+
frameLabelObjects.clear();
|
|
1304
|
+
state.frames.forEach((frame) => {
|
|
1305
|
+
const object = createFrameObject(frame);
|
|
1306
|
+
const label = createFrameLabelObject(frame);
|
|
1307
|
+
frameObjects.set(frame.id, object);
|
|
1308
|
+
frameLabelObjects.set(frame.id, label);
|
|
1309
|
+
canvas.add(object, label);
|
|
1310
|
+
});
|
|
1311
|
+
restackCanvasObjects();
|
|
1312
|
+
canvas.requestRenderAll();
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
function renderFramePreview() {
|
|
1316
|
+
if (framePreview) canvas.remove(framePreview);
|
|
1317
|
+
const bounds = CanvasERDDocument.frameBounds(drawingFrame.start, drawingFrame.end);
|
|
1318
|
+
framePreview = new fabric.Rect(CanvasERDDocument.topLeft({
|
|
1319
|
+
left: bounds.x,
|
|
1320
|
+
top: bounds.y,
|
|
1321
|
+
width: bounds.width,
|
|
1322
|
+
height: bounds.height,
|
|
1323
|
+
fill: "rgba(37, 99, 235, 0.03)",
|
|
1324
|
+
stroke: "#2563eb",
|
|
1325
|
+
strokeWidth: 2,
|
|
1326
|
+
strokeDashArray: [7, 5],
|
|
1327
|
+
selectable: false,
|
|
1328
|
+
evented: false,
|
|
1329
|
+
objectCaching: false
|
|
1330
|
+
}));
|
|
1331
|
+
framePreview.canvasErdType = "frame-preview";
|
|
1332
|
+
canvas.add(framePreview);
|
|
1333
|
+
restackCanvasObjects();
|
|
1334
|
+
canvas.requestRenderAll();
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
function setFrameMode(active) {
|
|
1338
|
+
if (active && arrowMode) cancelArrowDrawing();
|
|
1339
|
+
if (active && editingArrowId) finishArrowEndpointEditing();
|
|
1340
|
+
frameMode = active;
|
|
1341
|
+
frameButton.setAttribute("aria-pressed", String(active));
|
|
1342
|
+
canvas.selection = !active;
|
|
1343
|
+
canvas.setCursor(active ? "crosshair" : "default");
|
|
1344
|
+
[
|
|
1345
|
+
...frameObjects.values(),
|
|
1346
|
+
...frameLabelObjects.values(),
|
|
1347
|
+
...tableObjects.values(),
|
|
1348
|
+
...noteObjects.values(),
|
|
1349
|
+
...codeObjects.values(),
|
|
1350
|
+
...arrowObjects.values()
|
|
1351
|
+
].forEach((object) => {
|
|
1352
|
+
object.selectable = !active;
|
|
1353
|
+
object.evented = !active;
|
|
1354
|
+
});
|
|
1355
|
+
if (active) canvas.discardActiveObject();
|
|
1356
|
+
canvas.requestRenderAll();
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
function cancelFrameDrawing() {
|
|
1360
|
+
if (framePreview) canvas.remove(framePreview);
|
|
1361
|
+
framePreview = null;
|
|
1362
|
+
drawingFrame = null;
|
|
1363
|
+
setFrameMode(false);
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
function finishFrameDrawing(point) {
|
|
1367
|
+
if (!drawingFrame) return;
|
|
1368
|
+
drawingFrame.end = point;
|
|
1369
|
+
const bounds = CanvasERDDocument.frameBounds(drawingFrame.start, drawingFrame.end);
|
|
1370
|
+
if (framePreview) canvas.remove(framePreview);
|
|
1371
|
+
framePreview = null;
|
|
1372
|
+
|
|
1373
|
+
if (bounds.width >= 40 && bounds.height >= 30) {
|
|
1374
|
+
const frame = {
|
|
1375
|
+
id: globalThis.crypto && crypto.randomUUID ? crypto.randomUUID() : `frame-${Date.now()}`,
|
|
1376
|
+
label: "Frame",
|
|
1377
|
+
...bounds,
|
|
1378
|
+
...CanvasERDDocument.DEFAULT_FRAME_FORMATTING
|
|
1379
|
+
};
|
|
1380
|
+
state.frames.push(frame);
|
|
1381
|
+
drawingFrame = null;
|
|
1382
|
+
renderFrames();
|
|
1383
|
+
setFrameMode(false);
|
|
1384
|
+
const label = frameLabelObjects.get(frame.id);
|
|
1385
|
+
canvas.setActiveObject(label);
|
|
1386
|
+
label.enterEditing();
|
|
1387
|
+
label.selectAll();
|
|
1388
|
+
canvas.requestRenderAll();
|
|
1389
|
+
markDirty();
|
|
1390
|
+
} else {
|
|
1391
|
+
drawingFrame = null;
|
|
1392
|
+
setFrameMode(false);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
|
|
491
1396
|
function attachmentTarget(attachment) {
|
|
492
1397
|
if (!attachment) return null;
|
|
493
1398
|
if (attachment.type === "entity") return tableObjects.get(attachment.id);
|
|
494
1399
|
if (attachment.type === "note") return noteObjects.get(attachment.id);
|
|
1400
|
+
if (attachment.type === "code") return codeObjects.get(attachment.id);
|
|
495
1401
|
return null;
|
|
496
1402
|
}
|
|
497
1403
|
|
|
@@ -506,7 +1412,7 @@ function resolveArrowEndpoint(endpoint) {
|
|
|
506
1412
|
}
|
|
507
1413
|
|
|
508
1414
|
function attachableTargetAt(point) {
|
|
509
|
-
const objects = [...noteObjects.values(), ...tableObjects.values()].reverse();
|
|
1415
|
+
const objects = [...noteObjects.values(), ...codeObjects.values(), ...tableObjects.values()].reverse();
|
|
510
1416
|
return objects.find((object) => {
|
|
511
1417
|
const bounds = object.getBoundingRect();
|
|
512
1418
|
return point.x >= bounds.left && point.x <= bounds.left + bounds.width &&
|
|
@@ -530,13 +1436,58 @@ function arrowEndpoint(eventData) {
|
|
|
530
1436
|
return endpointAt(point, eventData.target, eventData.e.ctrlKey);
|
|
531
1437
|
}
|
|
532
1438
|
|
|
1439
|
+
function drawArrowHead(context, point, angle, style, color, strokeWidth) {
|
|
1440
|
+
if (style === "none") return;
|
|
1441
|
+
|
|
1442
|
+
const { length, halfWidth, radius } = CanvasERDDocument.arrowHeadGeometry(style, strokeWidth);
|
|
1443
|
+
context.save();
|
|
1444
|
+
context.translate(point.x, point.y);
|
|
1445
|
+
context.rotate(angle);
|
|
1446
|
+
context.fillStyle = color;
|
|
1447
|
+
context.strokeStyle = color;
|
|
1448
|
+
context.lineWidth = strokeWidth;
|
|
1449
|
+
context.lineCap = "round";
|
|
1450
|
+
context.lineJoin = "round";
|
|
1451
|
+
context.setLineDash([]);
|
|
1452
|
+
context.beginPath();
|
|
1453
|
+
if (style === "open") {
|
|
1454
|
+
context.moveTo(-length, -halfWidth);
|
|
1455
|
+
context.lineTo(0, 0);
|
|
1456
|
+
context.lineTo(-length, halfWidth);
|
|
1457
|
+
context.stroke();
|
|
1458
|
+
} else if (["circle", "circle-open"].includes(style)) {
|
|
1459
|
+
context.arc(-radius, 0, radius, 0, Math.PI * 2);
|
|
1460
|
+
if (style === "circle-open") context.stroke();
|
|
1461
|
+
else context.fill();
|
|
1462
|
+
} else if (["diamond", "diamond-open"].includes(style)) {
|
|
1463
|
+
context.moveTo(0, 0);
|
|
1464
|
+
context.lineTo(-length / 2, -halfWidth);
|
|
1465
|
+
context.lineTo(-length, 0);
|
|
1466
|
+
context.lineTo(-length / 2, halfWidth);
|
|
1467
|
+
context.closePath();
|
|
1468
|
+
if (style === "diamond-open") context.stroke();
|
|
1469
|
+
else context.fill();
|
|
1470
|
+
} else {
|
|
1471
|
+
context.moveTo(0, 0);
|
|
1472
|
+
context.lineTo(-length, -halfWidth);
|
|
1473
|
+
context.lineTo(-length, halfWidth);
|
|
1474
|
+
context.closePath();
|
|
1475
|
+
context.fill();
|
|
1476
|
+
}
|
|
1477
|
+
context.restore();
|
|
1478
|
+
}
|
|
1479
|
+
|
|
533
1480
|
function createArrowObject(arrow, options = {}) {
|
|
534
1481
|
const start = resolveArrowEndpoint(arrow.start);
|
|
535
1482
|
const end = resolveArrowEndpoint(arrow.end);
|
|
1483
|
+
const formatting = CanvasERDDocument.arrowFormatting(arrow);
|
|
1484
|
+
const color = options.stroke || formatting.color;
|
|
536
1485
|
const object = new fabric.Line([start.x, start.y, end.x, end.y], {
|
|
537
|
-
stroke:
|
|
538
|
-
strokeWidth:
|
|
539
|
-
|
|
1486
|
+
stroke: color,
|
|
1487
|
+
strokeWidth: formatting.strokeWidth,
|
|
1488
|
+
strokeDashArray: CanvasERDDocument.arrowStrokeDashArray(formatting.lineStyle),
|
|
1489
|
+
strokeLineCap: formatting.lineStyle === "dotted" ? "round" : "butt",
|
|
1490
|
+
fill: color,
|
|
540
1491
|
objectCaching: false,
|
|
541
1492
|
perPixelTargetFind: true,
|
|
542
1493
|
padding: 6,
|
|
@@ -549,20 +1500,40 @@ function createArrowObject(arrow, options = {}) {
|
|
|
549
1500
|
lockSkewingY: true
|
|
550
1501
|
});
|
|
551
1502
|
object._render = function (context) {
|
|
552
|
-
fabric.Line.prototype._render.call(this, context);
|
|
553
1503
|
const points = this.calcLinePoints();
|
|
554
|
-
const
|
|
1504
|
+
const shaft = CanvasERDDocument.shortenedArrowLinePoints(
|
|
1505
|
+
points,
|
|
1506
|
+
formatting.startHead,
|
|
1507
|
+
formatting.endHead,
|
|
1508
|
+
this.strokeWidth
|
|
1509
|
+
);
|
|
555
1510
|
context.save();
|
|
556
|
-
context.translate(points.x2, points.y2);
|
|
557
|
-
context.rotate(angle);
|
|
558
|
-
context.fillStyle = this.stroke;
|
|
559
1511
|
context.beginPath();
|
|
560
|
-
context.moveTo(
|
|
561
|
-
context.lineTo(
|
|
562
|
-
context.
|
|
563
|
-
context.
|
|
564
|
-
context.
|
|
1512
|
+
context.moveTo(shaft.x1, shaft.y1);
|
|
1513
|
+
context.lineTo(shaft.x2, shaft.y2);
|
|
1514
|
+
context.strokeStyle = this.stroke;
|
|
1515
|
+
context.lineWidth = this.strokeWidth;
|
|
1516
|
+
context.lineCap = this.strokeLineCap;
|
|
1517
|
+
context.setLineDash(this.strokeDashArray || []);
|
|
1518
|
+
context.stroke();
|
|
565
1519
|
context.restore();
|
|
1520
|
+
const angle = Math.atan2(points.y2 - points.y1, points.x2 - points.x1);
|
|
1521
|
+
drawArrowHead(
|
|
1522
|
+
context,
|
|
1523
|
+
{ x: points.x1, y: points.y1 },
|
|
1524
|
+
angle + Math.PI,
|
|
1525
|
+
formatting.startHead,
|
|
1526
|
+
this.stroke,
|
|
1527
|
+
this.strokeWidth
|
|
1528
|
+
);
|
|
1529
|
+
drawArrowHead(
|
|
1530
|
+
context,
|
|
1531
|
+
{ x: points.x2, y: points.y2 },
|
|
1532
|
+
angle,
|
|
1533
|
+
formatting.endHead,
|
|
1534
|
+
this.stroke,
|
|
1535
|
+
this.strokeWidth
|
|
1536
|
+
);
|
|
566
1537
|
};
|
|
567
1538
|
object.canvasErdType = options.preview ? "arrow-preview" : "arrow";
|
|
568
1539
|
object.arrowId = arrow.id;
|
|
@@ -606,15 +1577,21 @@ function renderArrowPreview() {
|
|
|
606
1577
|
}
|
|
607
1578
|
|
|
608
1579
|
function setArrowMode(active) {
|
|
1580
|
+
if (active && frameMode) cancelFrameDrawing();
|
|
609
1581
|
if (active && editingArrowId) finishArrowEndpointEditing();
|
|
610
1582
|
arrowMode = active;
|
|
611
1583
|
arrowButton.setAttribute("aria-pressed", String(active));
|
|
612
1584
|
canvas.selection = !active;
|
|
613
1585
|
canvas.setCursor(active ? "crosshair" : "default");
|
|
614
|
-
[
|
|
1586
|
+
[
|
|
1587
|
+
...frameObjects.values(),
|
|
1588
|
+
...frameLabelObjects.values(),
|
|
1589
|
+
...tableObjects.values(),
|
|
1590
|
+
...noteObjects.values(),
|
|
1591
|
+
...codeObjects.values(),
|
|
1592
|
+
...arrowObjects.values()
|
|
1593
|
+
].forEach((object) => {
|
|
615
1594
|
object.selectable = !active;
|
|
616
|
-
});
|
|
617
|
-
arrowObjects.forEach((object) => {
|
|
618
1595
|
object.evented = !active;
|
|
619
1596
|
});
|
|
620
1597
|
if (active) canvas.discardActiveObject();
|
|
@@ -700,6 +1677,7 @@ function beginArrowEndpointEditing(arrowId) {
|
|
|
700
1677
|
arrowEndpointHandles.set(endpointName, handle);
|
|
701
1678
|
canvas.add(handle);
|
|
702
1679
|
});
|
|
1680
|
+
updateArrowFormattingControls();
|
|
703
1681
|
canvas.requestRenderAll();
|
|
704
1682
|
}
|
|
705
1683
|
|
|
@@ -713,6 +1691,8 @@ function finishArrowEndpointEditing() {
|
|
|
713
1691
|
object.evented = true;
|
|
714
1692
|
}
|
|
715
1693
|
editingArrowId = null;
|
|
1694
|
+
updateArrowFormattingControls();
|
|
1695
|
+
closeNoteFormattingMenus();
|
|
716
1696
|
canvas.discardActiveObject();
|
|
717
1697
|
canvas.requestRenderAll();
|
|
718
1698
|
}
|
|
@@ -757,6 +1737,7 @@ function removeActiveArrows() {
|
|
|
757
1737
|
}
|
|
758
1738
|
|
|
759
1739
|
function addNote(position = canvas.getVpCenter()) {
|
|
1740
|
+
if (frameMode) cancelFrameDrawing();
|
|
760
1741
|
if (arrowMode) cancelArrowDrawing();
|
|
761
1742
|
if (editingArrowId) finishArrowEndpointEditing();
|
|
762
1743
|
const origin = CanvasERDDocument.notePositionAt(position);
|
|
@@ -766,7 +1747,8 @@ function addNote(position = canvas.getVpCenter()) {
|
|
|
766
1747
|
x: origin.x,
|
|
767
1748
|
y: origin.y,
|
|
768
1749
|
width: 220,
|
|
769
|
-
angle: 0
|
|
1750
|
+
angle: 0,
|
|
1751
|
+
...lastNoteFormatting
|
|
770
1752
|
};
|
|
771
1753
|
state.notes.push(note);
|
|
772
1754
|
|
|
@@ -777,10 +1759,122 @@ function addNote(position = canvas.getVpCenter()) {
|
|
|
777
1759
|
canvas.setActiveObject(object);
|
|
778
1760
|
object.enterEditing();
|
|
779
1761
|
object.selectAll();
|
|
1762
|
+
updateNoteFormattingControls();
|
|
780
1763
|
canvas.requestRenderAll();
|
|
781
1764
|
markDirty();
|
|
782
1765
|
}
|
|
783
1766
|
|
|
1767
|
+
function insertCodeBlock(origin, block) {
|
|
1768
|
+
if (frameMode) cancelFrameDrawing();
|
|
1769
|
+
if (arrowMode) cancelArrowDrawing();
|
|
1770
|
+
if (editingArrowId) finishArrowEndpointEditing();
|
|
1771
|
+
Object.assign(block, {
|
|
1772
|
+
id: globalThis.crypto && crypto.randomUUID ? crypto.randomUUID() : `code-${Date.now()}`,
|
|
1773
|
+
x: origin.x,
|
|
1774
|
+
y: origin.y,
|
|
1775
|
+
angle: 0
|
|
1776
|
+
});
|
|
1777
|
+
state.codeBlocks.push(block);
|
|
1778
|
+
|
|
1779
|
+
const object = createCodeObject(block);
|
|
1780
|
+
codeObjects.set(block.id, object);
|
|
1781
|
+
updateModelDetailToggles();
|
|
1782
|
+
canvas.add(object);
|
|
1783
|
+
restackCanvasObjects();
|
|
1784
|
+
canvas.setActiveObject(object);
|
|
1785
|
+
updateCodeFormattingControls();
|
|
1786
|
+
canvas.requestRenderAll();
|
|
1787
|
+
markDirty();
|
|
1788
|
+
return object;
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
function addCodeBlock(position = canvas.getVpCenter()) {
|
|
1792
|
+
const object = insertCodeBlock(CanvasERDDocument.notePositionAt(position), {
|
|
1793
|
+
code: "# Double-click to edit",
|
|
1794
|
+
width: 360,
|
|
1795
|
+
language: lastCodeLanguage,
|
|
1796
|
+
source: null
|
|
1797
|
+
});
|
|
1798
|
+
object.enterEditing();
|
|
1799
|
+
object.selectAll();
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
function openSourceDialog(position = canvas.getVpCenter()) {
|
|
1803
|
+
sourceDialogPosition = position;
|
|
1804
|
+
sourceFormElement.reset();
|
|
1805
|
+
sourceErrorElement.hidden = true;
|
|
1806
|
+
sourceSubmitButton.disabled = false;
|
|
1807
|
+
sourceDialogElement.showModal();
|
|
1808
|
+
sourcePathElement.focus();
|
|
1809
|
+
loadSourceFilePaths();
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
async function addSourceCodeBlock(event) {
|
|
1813
|
+
event.preventDefault();
|
|
1814
|
+
const source = CanvasERDDocument.sourceFromFields(
|
|
1815
|
+
sourcePathElement.value,
|
|
1816
|
+
sourceStartElement.value,
|
|
1817
|
+
sourceEndElement.value
|
|
1818
|
+
);
|
|
1819
|
+
if (!source) return;
|
|
1820
|
+
|
|
1821
|
+
sourceSubmitButton.disabled = true;
|
|
1822
|
+
try {
|
|
1823
|
+
const result = await fetchSource(source);
|
|
1824
|
+
sourceDialogElement.close();
|
|
1825
|
+
insertCodeBlock(
|
|
1826
|
+
CanvasERDDocument.notePositionAt(sourceDialogPosition),
|
|
1827
|
+
{ code: result.code, width: 520, language: result.language, source }
|
|
1828
|
+
);
|
|
1829
|
+
} catch (error) {
|
|
1830
|
+
sourceErrorElement.textContent = error.message;
|
|
1831
|
+
sourceErrorElement.hidden = false;
|
|
1832
|
+
} finally {
|
|
1833
|
+
sourceSubmitButton.disabled = false;
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
async function toggleModelDetail(entityId, detail) {
|
|
1838
|
+
const existing = CanvasERDDocument.modelDetailBlock(state.codeBlocks, entityId, detail);
|
|
1839
|
+
if (existing) {
|
|
1840
|
+
state.arrows = state.arrows.filter((arrow) => (
|
|
1841
|
+
![arrow.start, arrow.end].some((endpoint) => endpoint.attachment?.type === "code" && endpoint.attachment.id === existing.id)
|
|
1842
|
+
));
|
|
1843
|
+
removeCodeBlock(existing.id);
|
|
1844
|
+
return;
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
const source = { model: entityId, detail };
|
|
1848
|
+
try {
|
|
1849
|
+
const result = await fetchSource(source);
|
|
1850
|
+
const origin = CanvasERDDocument.modelDetailOrigin(state.positions[entityId]);
|
|
1851
|
+
const block = {
|
|
1852
|
+
code: result.code,
|
|
1853
|
+
width: 520,
|
|
1854
|
+
language: result.language,
|
|
1855
|
+
lineNumbers: result.lineNumbers,
|
|
1856
|
+
source
|
|
1857
|
+
};
|
|
1858
|
+
insertCodeBlock(origin, block);
|
|
1859
|
+
state.arrows.push({
|
|
1860
|
+
id: globalThis.crypto && crypto.randomUUID ? crypto.randomUUID() : `arrow-${Date.now()}`,
|
|
1861
|
+
start: { x: 0, y: 0, attachment: { type: "entity", id: entityId, x: 1, y: 0.5 } },
|
|
1862
|
+
end: { x: 0, y: 0, attachment: { type: "code", id: block.id, x: 0, y: 0.5 } },
|
|
1863
|
+
...CanvasERDDocument.DEFAULT_ARROW_FORMATTING
|
|
1864
|
+
});
|
|
1865
|
+
renderArrows();
|
|
1866
|
+
} catch (error) {
|
|
1867
|
+
showNotice(`Could not show ${detail} for ${entityId}: ${error.message}`, true);
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
function updateModelDetailToggles() {
|
|
1872
|
+
tableListElement.querySelectorAll("[data-detail]").forEach((button) => {
|
|
1873
|
+
const block = CanvasERDDocument.modelDetailBlock(state.codeBlocks, button.dataset.entityId, button.dataset.detail);
|
|
1874
|
+
button.setAttribute("aria-pressed", String(Boolean(block)));
|
|
1875
|
+
});
|
|
1876
|
+
}
|
|
1877
|
+
|
|
784
1878
|
function updateObjectState(object) {
|
|
785
1879
|
if (object.canvasErdType === "entity") {
|
|
786
1880
|
state.positions[object.entityId] = { x: object.left, y: object.top };
|
|
@@ -790,23 +1884,86 @@ function updateObjectState(object) {
|
|
|
790
1884
|
if (!note) return;
|
|
791
1885
|
Object.assign(note, CanvasERDDocument.noteStateGeometry(object));
|
|
792
1886
|
scheduleRelationshipRender();
|
|
1887
|
+
} else if (object.canvasErdType === "code") {
|
|
1888
|
+
const block = findCodeBlock(object.codeBlockId);
|
|
1889
|
+
if (!block) return;
|
|
1890
|
+
Object.assign(block, CanvasERDDocument.codeStateGeometry(object));
|
|
1891
|
+
scheduleRelationshipRender();
|
|
1892
|
+
} else if (object.canvasErdType === "frame") {
|
|
1893
|
+
const frame = state.frames.find((candidate) => candidate.id === object.frameId);
|
|
1894
|
+
if (!frame) return;
|
|
1895
|
+
Object.assign(frame, {
|
|
1896
|
+
x: object.left,
|
|
1897
|
+
y: object.top,
|
|
1898
|
+
width: object.width * object.scaleX,
|
|
1899
|
+
height: object.height * object.scaleY
|
|
1900
|
+
});
|
|
1901
|
+
const label = frameLabelObjects.get(frame.id);
|
|
1902
|
+
const position = CanvasERDDocument.frameLabelPosition(frame);
|
|
1903
|
+
label?.set({ left: position.x, top: position.y });
|
|
1904
|
+
label?.setCoords();
|
|
1905
|
+
} else if (object.canvasErdType === "frame-label") {
|
|
1906
|
+
const frame = state.frames.find((candidate) => candidate.id === object.frameId);
|
|
1907
|
+
if (!frame) return;
|
|
1908
|
+
frame.label = object.text;
|
|
793
1909
|
}
|
|
794
1910
|
markDirty();
|
|
795
1911
|
}
|
|
796
1912
|
|
|
797
|
-
function
|
|
1913
|
+
function removeActiveFrame() {
|
|
798
1914
|
const active = canvas.getActiveObject();
|
|
799
|
-
if (!
|
|
1915
|
+
if (!["frame", "frame-label"].includes(active?.canvasErdType) || active.isEditing) return false;
|
|
800
1916
|
|
|
1917
|
+
const frameId = active.frameId;
|
|
1918
|
+
canvas.discardActiveObject();
|
|
1919
|
+
canvas.remove(frameObjects.get(frameId), frameLabelObjects.get(frameId));
|
|
1920
|
+
frameObjects.delete(frameId);
|
|
1921
|
+
frameLabelObjects.delete(frameId);
|
|
1922
|
+
state.frames = state.frames.filter((frame) => frame.id !== frameId);
|
|
1923
|
+
markDirty();
|
|
1924
|
+
return true;
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
function detachArrows(type, id) {
|
|
801
1928
|
state.arrows.forEach((arrow) => {
|
|
802
1929
|
[arrow.start, arrow.end].forEach((endpoint) => {
|
|
803
|
-
if (endpoint.attachment?.type !==
|
|
1930
|
+
if (endpoint.attachment?.type !== type || endpoint.attachment.id !== id) return;
|
|
804
1931
|
const point = resolveArrowEndpoint(endpoint);
|
|
805
1932
|
endpoint.x = point.x;
|
|
806
1933
|
endpoint.y = point.y;
|
|
807
1934
|
endpoint.attachment = null;
|
|
808
1935
|
});
|
|
809
1936
|
});
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
function removeActiveCodeBlock() {
|
|
1940
|
+
const active = canvas.getActiveObject();
|
|
1941
|
+
if (!active || active.canvasErdType !== "code" || active.isEditing) return false;
|
|
1942
|
+
|
|
1943
|
+
detachArrows("code", active.codeBlockId);
|
|
1944
|
+
removeCodeBlock(active.codeBlockId);
|
|
1945
|
+
return true;
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
function removeCodeBlock(id) {
|
|
1949
|
+
const object = codeObjects.get(id);
|
|
1950
|
+
clearTimeout(codeHighlightTimers.get(id));
|
|
1951
|
+
codeHighlightTimers.delete(id);
|
|
1952
|
+
if (canvas.getActiveObject() === object) canvas.discardActiveObject();
|
|
1953
|
+
canvas.remove(object);
|
|
1954
|
+
codeObjects.delete(id);
|
|
1955
|
+
state.codeBlocks = state.codeBlocks.filter((block) => block.id !== id);
|
|
1956
|
+
renderArrows();
|
|
1957
|
+
updateModelDetailToggles();
|
|
1958
|
+
markDirty();
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
function removeActiveNote() {
|
|
1962
|
+
const active = canvas.getActiveObject();
|
|
1963
|
+
if (!active || active.canvasErdType !== "note" || active.isEditing) return false;
|
|
1964
|
+
|
|
1965
|
+
detachArrows("note", active.noteId);
|
|
1966
|
+
canvas.discardActiveObject();
|
|
810
1967
|
canvas.remove(active);
|
|
811
1968
|
noteObjects.delete(active.noteId);
|
|
812
1969
|
state.notes = state.notes.filter((note) => note.id !== active.noteId);
|
|
@@ -866,9 +2023,24 @@ function renderTableList() {
|
|
|
866
2023
|
const text = document.createElement("span");
|
|
867
2024
|
text.textContent = entity.label;
|
|
868
2025
|
row.append(checkbox, text);
|
|
2026
|
+
Object.entries(modelDetails[entity.id] || {}).forEach(([detail, count]) => {
|
|
2027
|
+
const button = document.createElement("button");
|
|
2028
|
+
button.type = "button";
|
|
2029
|
+
button.className = "detail-toggle";
|
|
2030
|
+
button.dataset.entityId = entity.id;
|
|
2031
|
+
button.dataset.detail = detail;
|
|
2032
|
+
button.textContent = detail;
|
|
2033
|
+
button.title = `Show ${detail} (${count})`;
|
|
2034
|
+
button.addEventListener("click", (event) => {
|
|
2035
|
+
event.preventDefault();
|
|
2036
|
+
toggleModelDetail(entity.id, detail);
|
|
2037
|
+
});
|
|
2038
|
+
row.append(button);
|
|
2039
|
+
});
|
|
869
2040
|
fragment.append(row);
|
|
870
2041
|
});
|
|
871
2042
|
tableListElement.replaceChildren(fragment);
|
|
2043
|
+
updateModelDetailToggles();
|
|
872
2044
|
}
|
|
873
2045
|
|
|
874
2046
|
function filterTableList() {
|
|
@@ -905,6 +2077,7 @@ async function refreshSchema() {
|
|
|
905
2077
|
applicationSchema = schema;
|
|
906
2078
|
markDirty();
|
|
907
2079
|
showNotice(CanvasERDDocument.changeSummary(result.changes));
|
|
2080
|
+
await refreshSourceCodeBlocks();
|
|
908
2081
|
} catch (error) {
|
|
909
2082
|
showNotice(`Could not refresh schema: ${error.message}`, true);
|
|
910
2083
|
} finally {
|
|
@@ -1022,7 +2195,14 @@ async function saveDiagram() {
|
|
|
1022
2195
|
|
|
1023
2196
|
function displayDocument(documentData) {
|
|
1024
2197
|
schema = documentData.schema;
|
|
1025
|
-
state = {
|
|
2198
|
+
state = {
|
|
2199
|
+
...documentData.state,
|
|
2200
|
+
arrows: documentData.state.arrows || [],
|
|
2201
|
+
frames: documentData.state.frames || [],
|
|
2202
|
+
codeBlocks: documentData.state.codeBlocks || [],
|
|
2203
|
+
tableHeaderColors: documentData.state.tableHeaderColors || {}
|
|
2204
|
+
};
|
|
2205
|
+
if (frameMode) cancelFrameDrawing();
|
|
1026
2206
|
if (arrowMode) cancelArrowDrawing();
|
|
1027
2207
|
if (editingArrowId) finishArrowEndpointEditing();
|
|
1028
2208
|
canvas.discardActiveObject();
|
|
@@ -1030,10 +2210,15 @@ function displayDocument(documentData) {
|
|
|
1030
2210
|
tableObjects.clear();
|
|
1031
2211
|
relationshipObjects = [];
|
|
1032
2212
|
noteObjects.clear();
|
|
2213
|
+
codeObjects.clear();
|
|
1033
2214
|
arrowObjects.clear();
|
|
2215
|
+
frameObjects.clear();
|
|
2216
|
+
frameLabelObjects.clear();
|
|
1034
2217
|
renderTableList();
|
|
2218
|
+
renderFrames();
|
|
1035
2219
|
renderTables();
|
|
1036
2220
|
renderNotes();
|
|
2221
|
+
renderCodeBlocks();
|
|
1037
2222
|
renderArrows();
|
|
1038
2223
|
|
|
1039
2224
|
if (Array.isArray(state.viewportTransform) && state.viewportTransform.length === 6) {
|
|
@@ -1097,8 +2282,11 @@ async function newDiagram() {
|
|
|
1097
2282
|
|
|
1098
2283
|
function diagramBounds() {
|
|
1099
2284
|
const objects = [
|
|
2285
|
+
...frameObjects.values(),
|
|
2286
|
+
...frameLabelObjects.values(),
|
|
1100
2287
|
...tableObjects.values(),
|
|
1101
2288
|
...noteObjects.values(),
|
|
2289
|
+
...codeObjects.values(),
|
|
1102
2290
|
...arrowObjects.values(),
|
|
1103
2291
|
...relationshipObjects
|
|
1104
2292
|
];
|
|
@@ -1141,18 +2329,28 @@ function resizeCanvas() {
|
|
|
1141
2329
|
}
|
|
1142
2330
|
|
|
1143
2331
|
function configureCanvasEvents() {
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
2332
|
+
const selectionChanged = () => {
|
|
2333
|
+
closeNoteFormattingMenus();
|
|
2334
|
+
updateNoteFormattingControls();
|
|
2335
|
+
updateTableFormattingControls();
|
|
2336
|
+
updateFrameFormattingControls();
|
|
2337
|
+
updateCodeFormattingControls();
|
|
2338
|
+
};
|
|
2339
|
+
canvas.on("selection:created", selectionChanged);
|
|
2340
|
+
canvas.on("selection:updated", selectionChanged);
|
|
2341
|
+
canvas.on("selection:cleared", selectionChanged);
|
|
1147
2342
|
canvas.on("mouse:down", ({ target }) => {
|
|
1148
2343
|
if (editingArrowId && target?.canvasErdType !== "arrow-endpoint") finishArrowEndpointEditing();
|
|
2344
|
+
const arrowId = CanvasERDDocument.arrowIdForEndpointEditing(target);
|
|
2345
|
+
if (arrowId) beginArrowEndpointEditing(arrowId);
|
|
1149
2346
|
});
|
|
1150
2347
|
canvas.on("mouse:down", (eventData) => {
|
|
1151
2348
|
if (!arrowMode || spacePressed) return;
|
|
1152
2349
|
drawingArrow = {
|
|
1153
2350
|
id: globalThis.crypto && crypto.randomUUID ? crypto.randomUUID() : `arrow-${Date.now()}`,
|
|
1154
2351
|
start: arrowEndpoint(eventData),
|
|
1155
|
-
end: arrowEndpoint(eventData)
|
|
2352
|
+
end: arrowEndpoint(eventData),
|
|
2353
|
+
...CanvasERDDocument.DEFAULT_ARROW_FORMATTING
|
|
1156
2354
|
};
|
|
1157
2355
|
renderArrowPreview();
|
|
1158
2356
|
});
|
|
@@ -1164,17 +2362,54 @@ function configureCanvasEvents() {
|
|
|
1164
2362
|
canvas.on("mouse:up", (eventData) => {
|
|
1165
2363
|
if (arrowMode && drawingArrow) finishArrowDrawing(eventData);
|
|
1166
2364
|
});
|
|
2365
|
+
canvas.on("mouse:up", ({ target, e, isClick }) => {
|
|
2366
|
+
if (!isClick || target?.canvasErdType !== "code") return;
|
|
2367
|
+
const label = target.gutterLabelAt(e);
|
|
2368
|
+
if (label !== null && label !== undefined) toggleCodeLineHighlight(target, label, e.shiftKey);
|
|
2369
|
+
});
|
|
2370
|
+
canvas.on("mouse:move", ({ target, e }) => {
|
|
2371
|
+
if (target?.canvasErdType !== "code") return;
|
|
2372
|
+
const label = target.gutterLabelAt(e);
|
|
2373
|
+
target.hoverCursor = label !== null && label !== undefined ? "pointer" : null;
|
|
2374
|
+
});
|
|
2375
|
+
canvas.on("mouse:down", ({ e, scenePoint }) => {
|
|
2376
|
+
if (!frameMode || spacePressed) return;
|
|
2377
|
+
const point = scenePoint || canvas.getScenePoint(e);
|
|
2378
|
+
drawingFrame = { start: { x: point.x, y: point.y }, end: { x: point.x, y: point.y } };
|
|
2379
|
+
renderFramePreview();
|
|
2380
|
+
});
|
|
2381
|
+
canvas.on("mouse:move", ({ e, scenePoint }) => {
|
|
2382
|
+
if (!frameMode || !drawingFrame) return;
|
|
2383
|
+
const point = scenePoint || canvas.getScenePoint(e);
|
|
2384
|
+
drawingFrame.end = { x: point.x, y: point.y };
|
|
2385
|
+
renderFramePreview();
|
|
2386
|
+
});
|
|
2387
|
+
canvas.on("mouse:up", ({ e, scenePoint }) => {
|
|
2388
|
+
if (!frameMode || !drawingFrame) return;
|
|
2389
|
+
const point = scenePoint || canvas.getScenePoint(e);
|
|
2390
|
+
finishFrameDrawing({ x: point.x, y: point.y });
|
|
2391
|
+
});
|
|
1167
2392
|
|
|
1168
2393
|
canvas.on("object:moving", (eventData) => {
|
|
1169
2394
|
if (eventData.target.canvasErdType === "arrow-endpoint") updateArrowEndpointHandle(eventData);
|
|
1170
2395
|
else if (eventData.target.canvasErdType !== "arrow") updateObjectState(eventData.target);
|
|
1171
2396
|
});
|
|
2397
|
+
canvas.on("object:scaling", ({ target }) => {
|
|
2398
|
+
if (target.canvasErdType === "frame") updateObjectState(target);
|
|
2399
|
+
});
|
|
1172
2400
|
canvas.on("object:modified", (eventData) => {
|
|
1173
2401
|
if (eventData.target.canvasErdType === "arrow") updateArrowState(eventData.target);
|
|
1174
2402
|
else if (eventData.target.canvasErdType === "arrow-endpoint") updateArrowEndpointHandle(eventData);
|
|
1175
2403
|
else updateObjectState(eventData.target);
|
|
1176
2404
|
});
|
|
1177
|
-
canvas.on("text:changed", ({ target }) =>
|
|
2405
|
+
canvas.on("text:changed", ({ target }) => {
|
|
2406
|
+
if (target.canvasErdType === "code") updateCodeGutter(target);
|
|
2407
|
+
updateObjectState(target);
|
|
2408
|
+
if (target.canvasErdType === "code") scheduleCodeHighlight(target);
|
|
2409
|
+
});
|
|
2410
|
+
canvas.on("text:editing:exited", ({ target }) => {
|
|
2411
|
+
if (target.canvasErdType === "code") scheduleCodeHighlight(target, 0);
|
|
2412
|
+
});
|
|
1178
2413
|
|
|
1179
2414
|
canvas.on("mouse:wheel", ({ e }) => {
|
|
1180
2415
|
if (e.ctrlKey) {
|
|
@@ -1227,8 +2462,21 @@ function configureCanvasEvents() {
|
|
|
1227
2462
|
document.addEventListener("keydown", (event) => {
|
|
1228
2463
|
const editing = canvas.getActiveObject() && canvas.getActiveObject().isEditing;
|
|
1229
2464
|
const acceptsText = ["INPUT", "TEXTAREA", "SELECT"].includes(event.target.tagName) || event.target.isContentEditable;
|
|
2465
|
+
const noteFormatting = CanvasERDDocument.noteFormattingShortcut(event);
|
|
1230
2466
|
const action = CanvasERDDocument.shortcutAction(event);
|
|
1231
|
-
const bareAction = ["addNote", "arrow", "toggleTables", "layoutTables", "zoomIn", "zoomOut", "fit", "help"].includes(action);
|
|
2467
|
+
const bareAction = ["addNote", "addCode", "arrow", "toggleTables", "layoutTables", "zoomIn", "zoomOut", "fit", "help"].includes(action);
|
|
2468
|
+
|
|
2469
|
+
if (noteFormatting && activeNoteObject() && (!acceptsText || editing)) {
|
|
2470
|
+
event.preventDefault();
|
|
2471
|
+
if (!event.repeat) {
|
|
2472
|
+
if (CanvasERDDocument.NOTE_PARAGRAPH_STYLES[noteFormatting]) {
|
|
2473
|
+
applyNoteFormatting(CanvasERDDocument.noteParagraphStyleFormatting(noteFormatting));
|
|
2474
|
+
} else {
|
|
2475
|
+
toggleNoteFormatting(noteFormatting);
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
return;
|
|
2479
|
+
}
|
|
1232
2480
|
|
|
1233
2481
|
if (action && (!bareAction || (!editing && !acceptsText))) {
|
|
1234
2482
|
event.preventDefault();
|
|
@@ -1245,6 +2493,8 @@ function configureCanvasEvents() {
|
|
|
1245
2493
|
newDiagram();
|
|
1246
2494
|
} else if (action === "addNote") {
|
|
1247
2495
|
addNote(lastCanvasPointer || canvas.getVpCenter());
|
|
2496
|
+
} else if (action === "addCode") {
|
|
2497
|
+
addCodeBlock(lastCanvasPointer || canvas.getVpCenter());
|
|
1248
2498
|
} else if (action === "arrow") {
|
|
1249
2499
|
if (arrowMode) cancelArrowDrawing();
|
|
1250
2500
|
else setArrowMode(true);
|
|
@@ -1264,12 +2514,17 @@ function configureCanvasEvents() {
|
|
|
1264
2514
|
return;
|
|
1265
2515
|
}
|
|
1266
2516
|
|
|
1267
|
-
if (event.key === "Escape" &&
|
|
2517
|
+
if (event.key === "Escape" && closeNoteFormattingMenus()) {
|
|
2518
|
+
event.preventDefault();
|
|
2519
|
+
} else if (event.key === "Escape" && editingArrowId) {
|
|
1268
2520
|
event.preventDefault();
|
|
1269
2521
|
finishArrowEndpointEditing();
|
|
1270
2522
|
} else if (event.key === "Escape" && arrowMode) {
|
|
1271
2523
|
event.preventDefault();
|
|
1272
2524
|
cancelArrowDrawing();
|
|
2525
|
+
} else if (event.key === "Escape" && frameMode) {
|
|
2526
|
+
event.preventDefault();
|
|
2527
|
+
cancelFrameDrawing();
|
|
1273
2528
|
} else if (event.key === "Escape" && !shortcutPanelElement.hidden) {
|
|
1274
2529
|
event.preventDefault();
|
|
1275
2530
|
showShortcutHelp(false);
|
|
@@ -1278,8 +2533,8 @@ function configureCanvasEvents() {
|
|
|
1278
2533
|
spacePressed = true;
|
|
1279
2534
|
canvas.skipTargetFind = true;
|
|
1280
2535
|
canvas.setCursor("grab");
|
|
1281
|
-
} else if (["Delete", "Backspace"].includes(event.key) && (
|
|
1282
|
-
removeActiveTables() || removeActiveArrows() || removeActiveNote()
|
|
2536
|
+
} else if (["Delete", "Backspace"].includes(event.key) && !acceptsText && (
|
|
2537
|
+
removeActiveTables() || removeActiveArrows() || removeActiveFrame() || removeActiveNote() || removeActiveCodeBlock()
|
|
1283
2538
|
)) {
|
|
1284
2539
|
event.preventDefault();
|
|
1285
2540
|
}
|
|
@@ -1290,8 +2545,8 @@ function configureCanvasEvents() {
|
|
|
1290
2545
|
spacePressed = false;
|
|
1291
2546
|
isPanning = false;
|
|
1292
2547
|
canvas.skipTargetFind = false;
|
|
1293
|
-
canvas.selection =
|
|
1294
|
-
canvas.setCursor(arrowMode ? "crosshair" : "default");
|
|
2548
|
+
canvas.selection = !arrowMode && !frameMode;
|
|
2549
|
+
canvas.setCursor(arrowMode || frameMode ? "crosshair" : "default");
|
|
1295
2550
|
});
|
|
1296
2551
|
}
|
|
1297
2552
|
|
|
@@ -1304,6 +2559,10 @@ function configureControls() {
|
|
|
1304
2559
|
if (arrowMode) cancelArrowDrawing();
|
|
1305
2560
|
else setArrowMode(true);
|
|
1306
2561
|
});
|
|
2562
|
+
frameButton.addEventListener("click", () => {
|
|
2563
|
+
if (frameMode) cancelFrameDrawing();
|
|
2564
|
+
else setFrameMode(true);
|
|
2565
|
+
});
|
|
1307
2566
|
saveButton.addEventListener("click", saveDiagram);
|
|
1308
2567
|
newButton.addEventListener("click", newDiagram);
|
|
1309
2568
|
openButton.addEventListener("click", () => {
|
|
@@ -1336,6 +2595,17 @@ function configureControls() {
|
|
|
1336
2595
|
await loadDiagram(filename);
|
|
1337
2596
|
});
|
|
1338
2597
|
document.querySelector("#add-note").addEventListener("click", () => addNote());
|
|
2598
|
+
document.querySelector("#add-code").addEventListener("click", () => addCodeBlock());
|
|
2599
|
+
document.querySelector("#add-source").addEventListener("click", () => openSourceDialog());
|
|
2600
|
+
sourceFormElement.addEventListener("submit", addSourceCodeBlock);
|
|
2601
|
+
document.querySelector("#source-cancel").addEventListener("click", () => sourceDialogElement.close());
|
|
2602
|
+
codeSourcePathElement.addEventListener("focus", loadSourceFilePaths);
|
|
2603
|
+
[codeSourcePathElement, codeSourceStartElement, codeSourceEndElement].forEach((element) => {
|
|
2604
|
+
element.addEventListener("change", applyCodeSource);
|
|
2605
|
+
});
|
|
2606
|
+
codeLanguageElement.addEventListener("change", () => applyCodeLanguage(codeLanguageElement.value));
|
|
2607
|
+
codeLineNumbersButton.addEventListener("click", toggleCodeLineNumbers);
|
|
2608
|
+
codeSizeElement.addEventListener("change", () => applyCodeSize(codeSizeElement.value));
|
|
1339
2609
|
layoutTablesButton.addEventListener("click", layoutTables);
|
|
1340
2610
|
document.querySelector("#zoom-in").addEventListener("click", () => zoom(1.2));
|
|
1341
2611
|
document.querySelector("#zoom-out").addEventListener("click", () => zoom(1 / 1.2));
|
|
@@ -1343,13 +2613,79 @@ function configureControls() {
|
|
|
1343
2613
|
document.querySelector("#select-all").addEventListener("click", () => setAllTables(true));
|
|
1344
2614
|
document.querySelector("#select-none").addEventListener("click", () => setAllTables(false));
|
|
1345
2615
|
searchElement.addEventListener("input", filterTableList);
|
|
2616
|
+
noteBoldButton.addEventListener("click", () => toggleNoteFormatting("bold"));
|
|
2617
|
+
noteItalicButton.addEventListener("click", () => toggleNoteFormatting("italic"));
|
|
2618
|
+
noteUnderlineButton.addEventListener("click", () => toggleNoteFormatting("underline"));
|
|
2619
|
+
noteColorControls.forEach((control) => {
|
|
2620
|
+
control.button.addEventListener("click", () => toggleNoteColorMenu(control));
|
|
2621
|
+
control.custom.addEventListener("change", () => {
|
|
2622
|
+
applyNoteFormatting({ [control.property]: control.custom.value });
|
|
2623
|
+
closeNoteFormattingMenus();
|
|
2624
|
+
});
|
|
2625
|
+
control.transparent?.addEventListener("click", () => {
|
|
2626
|
+
applyNoteFormatting({ [control.property]: "transparent" });
|
|
2627
|
+
closeNoteFormattingMenus();
|
|
2628
|
+
});
|
|
2629
|
+
});
|
|
2630
|
+
tableHeaderColorControl.button.addEventListener("click", toggleTableHeaderColorMenu);
|
|
2631
|
+
tableHeaderColorControl.custom.addEventListener("change", () => {
|
|
2632
|
+
applyTableHeaderColor(tableHeaderColorControl.custom.value);
|
|
2633
|
+
closeNoteFormattingMenus();
|
|
2634
|
+
});
|
|
2635
|
+
arrowColorControl.button.addEventListener("click", toggleArrowColorMenu);
|
|
2636
|
+
arrowColorControl.custom.addEventListener("change", () => {
|
|
2637
|
+
applyArrowFormatting({ color: arrowColorControl.custom.value });
|
|
2638
|
+
closeNoteFormattingMenus();
|
|
2639
|
+
});
|
|
2640
|
+
arrowStartHeadElement.addEventListener("change", () => {
|
|
2641
|
+
applyArrowFormatting({ startHead: arrowStartHeadElement.value });
|
|
2642
|
+
});
|
|
2643
|
+
arrowEndHeadElement.addEventListener("change", () => {
|
|
2644
|
+
applyArrowFormatting({ endHead: arrowEndHeadElement.value });
|
|
2645
|
+
});
|
|
2646
|
+
arrowLineStyleElement.addEventListener("change", () => {
|
|
2647
|
+
applyArrowFormatting({ lineStyle: arrowLineStyleElement.value });
|
|
2648
|
+
});
|
|
2649
|
+
arrowStrokeWidthElement.addEventListener("change", () => {
|
|
2650
|
+
applyArrowFormatting({ strokeWidth: arrowStrokeWidthElement.value });
|
|
2651
|
+
});
|
|
2652
|
+
frameColorControl.button.addEventListener("click", toggleFrameColorMenu);
|
|
2653
|
+
frameColorControl.custom.addEventListener("change", () => {
|
|
2654
|
+
applyFrameFormatting({ color: frameColorControl.custom.value });
|
|
2655
|
+
closeNoteFormattingMenus();
|
|
2656
|
+
});
|
|
2657
|
+
frameLineStyleElement.addEventListener("change", () => {
|
|
2658
|
+
applyFrameFormatting({ lineStyle: frameLineStyleElement.value });
|
|
2659
|
+
});
|
|
2660
|
+
document.addEventListener("pointerdown", (event) => {
|
|
2661
|
+
const insideFormatting = noteFormattingElement.contains(event.target) ||
|
|
2662
|
+
tableFormattingElement.contains(event.target) ||
|
|
2663
|
+
arrowFormattingElement.contains(event.target) ||
|
|
2664
|
+
frameFormattingElement.contains(event.target);
|
|
2665
|
+
if (!insideFormatting) closeNoteFormattingMenus();
|
|
2666
|
+
});
|
|
2667
|
+
noteParagraphStyleElement.addEventListener("change", () => {
|
|
2668
|
+
applyNoteFormatting(
|
|
2669
|
+
CanvasERDDocument.noteParagraphStyleFormatting(noteParagraphStyleElement.value)
|
|
2670
|
+
);
|
|
2671
|
+
});
|
|
2672
|
+
noteAlignmentElement.addEventListener("change", () => {
|
|
2673
|
+
applyNoteFormatting({ textAlign: noteAlignmentElement.value });
|
|
2674
|
+
});
|
|
1346
2675
|
}
|
|
1347
2676
|
|
|
1348
2677
|
function initializeEditor(loadedDocument) {
|
|
1349
2678
|
schema = loadedDocument.schema;
|
|
1350
|
-
applicationSchema = schema;
|
|
2679
|
+
applicationSchema = loadedDocument.application_schema || schema;
|
|
2680
|
+
currentDiagramFilename = loadedDocument.filename || null;
|
|
1351
2681
|
state = loadedDocument.state || CanvasERDDocument.createState(schema);
|
|
1352
|
-
state = {
|
|
2682
|
+
state = {
|
|
2683
|
+
...state,
|
|
2684
|
+
arrows: state.arrows || [],
|
|
2685
|
+
frames: state.frames || [],
|
|
2686
|
+
codeBlocks: state.codeBlocks || [],
|
|
2687
|
+
tableHeaderColors: state.tableHeaderColors || {}
|
|
2688
|
+
};
|
|
1353
2689
|
setDiagramName(loadedDocument.name);
|
|
1354
2690
|
canvas = new fabric.Canvas("erd-canvas", {
|
|
1355
2691
|
backgroundColor: "transparent",
|
|
@@ -1363,8 +2699,10 @@ function initializeEditor(loadedDocument) {
|
|
|
1363
2699
|
configureCanvasEvents();
|
|
1364
2700
|
configureControls();
|
|
1365
2701
|
renderTableList();
|
|
2702
|
+
renderFrames();
|
|
1366
2703
|
renderTables();
|
|
1367
2704
|
renderNotes();
|
|
2705
|
+
renderCodeBlocks();
|
|
1368
2706
|
renderArrows();
|
|
1369
2707
|
if (Array.isArray(state.viewportTransform) && state.viewportTransform.length === 6) {
|
|
1370
2708
|
canvas.setViewportTransform(state.viewportTransform);
|
|
@@ -1373,7 +2711,8 @@ function initializeEditor(loadedDocument) {
|
|
|
1373
2711
|
fitDiagram(false);
|
|
1374
2712
|
}
|
|
1375
2713
|
new ResizeObserver(resizeCanvas).observe(panelElement);
|
|
1376
|
-
refreshSavedDiagrams()
|
|
2714
|
+
refreshSavedDiagrams(currentDiagramFilename)
|
|
2715
|
+
.catch((error) => showNotice(`Could not list diagrams: ${error.message}`, true));
|
|
1377
2716
|
loadingElement.hidden = true;
|
|
1378
2717
|
}
|
|
1379
2718
|
|
|
@@ -1384,6 +2723,13 @@ fetch("/api/document", { cache: "no-store" })
|
|
|
1384
2723
|
})
|
|
1385
2724
|
.then(async (loadedDocument) => {
|
|
1386
2725
|
if (!loadedDocument.state) loadedDocument.state = await createInitialState(loadedDocument.schema);
|
|
2726
|
+
const response = await fetch("/api/highlighting", { cache: "no-store" });
|
|
2727
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
2728
|
+
highlighting = await response.json();
|
|
2729
|
+
renderCodeLanguages();
|
|
2730
|
+
const detailsResponse = await fetch("/api/model_details", { cache: "no-store" });
|
|
2731
|
+
if (!detailsResponse.ok) throw new Error(`HTTP ${detailsResponse.status}`);
|
|
2732
|
+
modelDetails = await detailsResponse.json();
|
|
1387
2733
|
return loadedDocument;
|
|
1388
2734
|
})
|
|
1389
2735
|
.then(initializeEditor)
|