canvas_erd 0.1.0 → 0.2.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 +11 -0
- data/README.md +6 -2
- data/lib/canvas_erd/diagram_store.rb +16 -2
- data/lib/canvas_erd/server.rb +10 -1
- data/lib/canvas_erd/version.rb +1 -2
- data/lib/canvas_erd/web/app.js +845 -70
- data/lib/canvas_erd/web/document.js +401 -2
- data/lib/canvas_erd/web/index.html +153 -1
- data/lib/canvas_erd/web/styles.css +208 -0
- metadata +1 -1
data/lib/canvas_erd/web/app.js
CHANGED
|
@@ -19,7 +19,64 @@ 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 noteColorControls = [
|
|
60
|
+
{
|
|
61
|
+
property: "fill",
|
|
62
|
+
defaults: CanvasERDDocument.DEFAULT_FOREGROUND_COLORS,
|
|
63
|
+
button: document.querySelector("#note-text-color-button"),
|
|
64
|
+
preview: document.querySelector("#note-text-color-preview"),
|
|
65
|
+
menu: document.querySelector("#note-text-color-menu"),
|
|
66
|
+
swatches: document.querySelector("#note-text-color-swatches"),
|
|
67
|
+
custom: document.querySelector("#note-text-custom-color")
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
property: "backgroundColor",
|
|
71
|
+
defaults: CanvasERDDocument.DEFAULT_BACKGROUND_COLORS,
|
|
72
|
+
button: document.querySelector("#note-background-color-button"),
|
|
73
|
+
preview: document.querySelector("#note-background-color-preview"),
|
|
74
|
+
menu: document.querySelector("#note-background-color-menu"),
|
|
75
|
+
swatches: document.querySelector("#note-background-color-swatches"),
|
|
76
|
+
custom: document.querySelector("#note-background-custom-color"),
|
|
77
|
+
transparent: document.querySelector("#note-background-transparent")
|
|
78
|
+
}
|
|
79
|
+
];
|
|
23
80
|
|
|
24
81
|
let schema;
|
|
25
82
|
let applicationSchema;
|
|
@@ -34,6 +91,8 @@ const tableObjects = new Map();
|
|
|
34
91
|
let relationshipObjects = [];
|
|
35
92
|
const noteObjects = new Map();
|
|
36
93
|
const arrowObjects = new Map();
|
|
94
|
+
const frameObjects = new Map();
|
|
95
|
+
const frameLabelObjects = new Map();
|
|
37
96
|
let noticeTimer;
|
|
38
97
|
let currentDiagramFilename = null;
|
|
39
98
|
let diagramsDirectory = "docs/erd";
|
|
@@ -44,6 +103,10 @@ let arrowMode = false;
|
|
|
44
103
|
let drawingArrow = null;
|
|
45
104
|
let arrowPreview = null;
|
|
46
105
|
let editingArrowId = null;
|
|
106
|
+
let frameMode = false;
|
|
107
|
+
let drawingFrame = null;
|
|
108
|
+
let framePreview = null;
|
|
109
|
+
let lastNoteFormatting = CanvasERDDocument.inheritedNoteFormatting();
|
|
47
110
|
const arrowEndpointHandles = new Map();
|
|
48
111
|
const changeTracker = CanvasERDDocument.createChangeTracker(updateSaveButton);
|
|
49
112
|
const layoutEngine = new ELK({ workerUrl: "/assets/elk-worker.min.js" });
|
|
@@ -177,6 +240,7 @@ function truncate(value, length) {
|
|
|
177
240
|
function createTableObject(entity) {
|
|
178
241
|
const width = CanvasERDDocument.CARD_WIDTH;
|
|
179
242
|
const height = CanvasERDDocument.tableHeight(entity);
|
|
243
|
+
const headerColor = CanvasERDDocument.tableHeaderColor(state, entity.id);
|
|
180
244
|
const objects = [
|
|
181
245
|
new fabric.Rect(CanvasERDDocument.topLeft({
|
|
182
246
|
left: 0,
|
|
@@ -195,7 +259,7 @@ function createTableObject(entity) {
|
|
|
195
259
|
top: 0,
|
|
196
260
|
width,
|
|
197
261
|
height: CanvasERDDocument.HEADER_HEIGHT,
|
|
198
|
-
fill:
|
|
262
|
+
fill: headerColor,
|
|
199
263
|
rx: 8,
|
|
200
264
|
ry: 8
|
|
201
265
|
})),
|
|
@@ -204,7 +268,7 @@ function createTableObject(entity) {
|
|
|
204
268
|
top: CanvasERDDocument.HEADER_HEIGHT - 8,
|
|
205
269
|
width,
|
|
206
270
|
height: 8,
|
|
207
|
-
fill:
|
|
271
|
+
fill: headerColor
|
|
208
272
|
})),
|
|
209
273
|
new fabric.FabricText(truncate(entity.label, 34), CanvasERDDocument.uncachedText({
|
|
210
274
|
left: 14,
|
|
@@ -318,26 +382,73 @@ function relationStyle(relationship) {
|
|
|
318
382
|
};
|
|
319
383
|
}
|
|
320
384
|
|
|
385
|
+
function createCardinalityObjects(geometry, style) {
|
|
386
|
+
const markerStyle = { ...style, strokeDashArray: null };
|
|
387
|
+
const objects = geometry.segments.map(({ start, end }) => (
|
|
388
|
+
new fabric.Line([start.x, start.y, end.x, end.y], markerStyle)
|
|
389
|
+
));
|
|
390
|
+
geometry.circles.forEach(({ center, radius }) => {
|
|
391
|
+
objects.push(new fabric.Circle({
|
|
392
|
+
left: center.x,
|
|
393
|
+
top: center.y,
|
|
394
|
+
originX: "center",
|
|
395
|
+
originY: "center",
|
|
396
|
+
radius,
|
|
397
|
+
fill: "#f7f8fb",
|
|
398
|
+
stroke: style.stroke,
|
|
399
|
+
strokeWidth: style.strokeWidth,
|
|
400
|
+
selectable: false,
|
|
401
|
+
evented: false,
|
|
402
|
+
objectCaching: false
|
|
403
|
+
}));
|
|
404
|
+
});
|
|
405
|
+
if (geometry.label) {
|
|
406
|
+
objects.push(new fabric.FabricText(geometry.label, {
|
|
407
|
+
left: geometry.labelPosition.x,
|
|
408
|
+
top: geometry.labelPosition.y,
|
|
409
|
+
originX: "center",
|
|
410
|
+
originY: "center",
|
|
411
|
+
fontFamily: CanvasERDDocument.CANVAS_FONT_FAMILY,
|
|
412
|
+
fontSize: 10,
|
|
413
|
+
fill: "#526277",
|
|
414
|
+
backgroundColor: "#f7f8fb",
|
|
415
|
+
selectable: false,
|
|
416
|
+
evented: false
|
|
417
|
+
}));
|
|
418
|
+
}
|
|
419
|
+
return objects;
|
|
420
|
+
}
|
|
421
|
+
|
|
321
422
|
function createRelationshipObjects(relationship, source, destination) {
|
|
322
423
|
const sourceBounds = tableBounds(source);
|
|
323
424
|
const destinationBounds = tableBounds(destination);
|
|
324
425
|
const style = relationStyle(relationship);
|
|
325
426
|
let connector;
|
|
326
|
-
let
|
|
427
|
+
let sourceEndpoint;
|
|
428
|
+
let destinationEndpoint;
|
|
327
429
|
|
|
328
430
|
if (source === destination) {
|
|
329
431
|
const right = sourceBounds.left + sourceBounds.width;
|
|
330
432
|
const top = sourceBounds.top;
|
|
331
433
|
const centerY = top + sourceBounds.height / 2;
|
|
332
|
-
|
|
434
|
+
sourceEndpoint = CanvasERDDocument.cardinalityEndpointGeometry(
|
|
333
435
|
{ x: right, y: centerY },
|
|
334
|
-
{ x:
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
436
|
+
{ x: 1, y: 0 },
|
|
437
|
+
relationship.cardinality.source
|
|
438
|
+
);
|
|
439
|
+
destinationEndpoint = CanvasERDDocument.cardinalityEndpointGeometry(
|
|
440
|
+
{ x: right - 20, y: top },
|
|
441
|
+
{ x: 0, y: -1 },
|
|
442
|
+
relationship.cardinality.destination
|
|
443
|
+
);
|
|
444
|
+
const points = [
|
|
445
|
+
sourceEndpoint.shaftPoint,
|
|
446
|
+
{ x: right + 48, y: centerY },
|
|
447
|
+
{ x: right + 48, y: top - 48 },
|
|
448
|
+
{ x: right - 20, y: top - 48 },
|
|
449
|
+
destinationEndpoint.shaftPoint
|
|
338
450
|
];
|
|
339
451
|
connector = new fabric.Polyline(points, style);
|
|
340
|
-
labelPosition = { x: right + 48, y: top - 28 };
|
|
341
452
|
} else {
|
|
342
453
|
const sourceCenter = {
|
|
343
454
|
x: sourceBounds.left + sourceBounds.width / 2,
|
|
@@ -347,33 +458,31 @@ function createRelationshipObjects(relationship, source, destination) {
|
|
|
347
458
|
x: destinationBounds.left + destinationBounds.width / 2,
|
|
348
459
|
y: destinationBounds.top + destinationBounds.height / 2
|
|
349
460
|
};
|
|
461
|
+
sourceEndpoint = CanvasERDDocument.relationshipEndpointGeometry(
|
|
462
|
+
sourceBounds,
|
|
463
|
+
destinationCenter,
|
|
464
|
+
relationship.cardinality.source
|
|
465
|
+
);
|
|
466
|
+
destinationEndpoint = CanvasERDDocument.relationshipEndpointGeometry(
|
|
467
|
+
destinationBounds,
|
|
468
|
+
sourceCenter,
|
|
469
|
+
relationship.cardinality.destination
|
|
470
|
+
);
|
|
350
471
|
connector = new fabric.Line([
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
472
|
+
sourceEndpoint.shaftPoint.x,
|
|
473
|
+
sourceEndpoint.shaftPoint.y,
|
|
474
|
+
destinationEndpoint.shaftPoint.x,
|
|
475
|
+
destinationEndpoint.shaftPoint.y
|
|
355
476
|
], style);
|
|
356
|
-
labelPosition = {
|
|
357
|
-
x: (sourceCenter.x + destinationCenter.x) / 2,
|
|
358
|
-
y: (sourceCenter.y + destinationCenter.y) / 2
|
|
359
|
-
};
|
|
360
477
|
}
|
|
361
478
|
|
|
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];
|
|
479
|
+
const objects = [
|
|
480
|
+
connector,
|
|
481
|
+
...createCardinalityObjects(sourceEndpoint, style),
|
|
482
|
+
...createCardinalityObjects(destinationEndpoint, style)
|
|
483
|
+
];
|
|
484
|
+
objects.forEach((object) => { object.canvasErdType = "relationship"; });
|
|
485
|
+
return objects;
|
|
377
486
|
}
|
|
378
487
|
|
|
379
488
|
function renderRelationships() {
|
|
@@ -390,7 +499,7 @@ function renderRelationships() {
|
|
|
390
499
|
canvas.add(...objects);
|
|
391
500
|
});
|
|
392
501
|
|
|
393
|
-
|
|
502
|
+
restackCanvasObjects();
|
|
394
503
|
canvas.requestRenderAll();
|
|
395
504
|
updateStatus();
|
|
396
505
|
}
|
|
@@ -448,17 +557,15 @@ async function layoutTables() {
|
|
|
448
557
|
}
|
|
449
558
|
|
|
450
559
|
function createNoteObject(note) {
|
|
560
|
+
const borderColor = CanvasERDDocument.noteBorderColor(
|
|
561
|
+
CanvasERDDocument.noteFormatting(note).backgroundColor
|
|
562
|
+
);
|
|
451
563
|
const object = new fabric.Textbox(note.text, CanvasERDDocument.uncachedText({
|
|
452
564
|
...CanvasERDDocument.noteCanvasGeometry(note),
|
|
453
|
-
fill: "#4a3f12",
|
|
454
|
-
backgroundColor: "#fff2a8",
|
|
455
|
-
fontFamily: CanvasERDDocument.CANVAS_FONT_FAMILY,
|
|
456
|
-
fontSize: 15,
|
|
457
|
-
lineHeight: 1.25,
|
|
458
565
|
padding: CanvasERDDocument.noteSelectionPadding(canvas.getZoom()),
|
|
459
|
-
borderColor
|
|
460
|
-
editingBorderColor:
|
|
461
|
-
cornerColor:
|
|
566
|
+
borderColor,
|
|
567
|
+
editingBorderColor: borderColor,
|
|
568
|
+
cornerColor: borderColor,
|
|
462
569
|
transparentCorners: false,
|
|
463
570
|
lockScalingY: true
|
|
464
571
|
}));
|
|
@@ -480,6 +587,293 @@ function createNoteObject(note) {
|
|
|
480
587
|
return object;
|
|
481
588
|
}
|
|
482
589
|
|
|
590
|
+
function activeNoteObject() {
|
|
591
|
+
const active = canvas?.getActiveObject();
|
|
592
|
+
return active?.canvasErdType === "note" ? active : null;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function activeTableEntityIds() {
|
|
596
|
+
return CanvasERDDocument.selectedEntityIds(canvas?.getActiveObject());
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function updateNoteFormattingControls() {
|
|
600
|
+
const note = activeNoteObject();
|
|
601
|
+
noteFormattingElement.hidden = !note;
|
|
602
|
+
if (!note) return;
|
|
603
|
+
|
|
604
|
+
const formatting = CanvasERDDocument.noteFormatting(note);
|
|
605
|
+
lastNoteFormatting = CanvasERDDocument.inheritedNoteFormatting(formatting);
|
|
606
|
+
noteBoldButton.setAttribute("aria-pressed", String(formatting.fontWeight === "bold"));
|
|
607
|
+
noteItalicButton.setAttribute("aria-pressed", String(formatting.fontStyle === "italic"));
|
|
608
|
+
noteUnderlineButton.setAttribute("aria-pressed", String(formatting.underline));
|
|
609
|
+
noteColorControls.forEach((control) => {
|
|
610
|
+
const color = formatting[control.property];
|
|
611
|
+
control.preview.style.backgroundColor = color;
|
|
612
|
+
control.preview.classList.toggle("transparent-color", color === "transparent");
|
|
613
|
+
if (control.transparent) {
|
|
614
|
+
control.transparent.setAttribute("aria-pressed", String(color === "transparent"));
|
|
615
|
+
}
|
|
616
|
+
if (/^#[0-9a-f]{6}$/i.test(color)) control.custom.value = color;
|
|
617
|
+
});
|
|
618
|
+
noteParagraphStyleElement.value = formatting.paragraphStyle;
|
|
619
|
+
noteAlignmentElement.value = formatting.textAlign;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function closeNoteFormattingMenus() {
|
|
623
|
+
let closed = false;
|
|
624
|
+
noteColorControls.forEach((control) => {
|
|
625
|
+
if (!control.menu.hidden) closed = true;
|
|
626
|
+
control.menu.hidden = true;
|
|
627
|
+
control.button.setAttribute("aria-expanded", "false");
|
|
628
|
+
});
|
|
629
|
+
if (!tableHeaderColorControl.menu.hidden) closed = true;
|
|
630
|
+
tableHeaderColorControl.menu.hidden = true;
|
|
631
|
+
tableHeaderColorControl.button.setAttribute("aria-expanded", "false");
|
|
632
|
+
if (!arrowColorControl.menu.hidden) closed = true;
|
|
633
|
+
arrowColorControl.menu.hidden = true;
|
|
634
|
+
arrowColorControl.button.setAttribute("aria-expanded", "false");
|
|
635
|
+
if (!frameColorControl.menu.hidden) closed = true;
|
|
636
|
+
frameColorControl.menu.hidden = true;
|
|
637
|
+
frameColorControl.button.setAttribute("aria-expanded", "false");
|
|
638
|
+
return closed;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function renderColorSwatches(element, colors, defaults, currentColor, applyColor) {
|
|
642
|
+
const fragment = document.createDocumentFragment();
|
|
643
|
+
const sections = CanvasERDDocument.colorPaletteSections(colors, defaults);
|
|
644
|
+
const appendSwatches = (swatches) => swatches.forEach((color) => {
|
|
645
|
+
const swatch = document.createElement("button");
|
|
646
|
+
swatch.className = "color-swatch";
|
|
647
|
+
swatch.type = "button";
|
|
648
|
+
swatch.title = color;
|
|
649
|
+
swatch.setAttribute("aria-label", `Use ${color}`);
|
|
650
|
+
swatch.setAttribute("aria-pressed", String(color === currentColor));
|
|
651
|
+
swatch.style.backgroundColor = color;
|
|
652
|
+
swatch.classList.toggle("transparent-color", color === "transparent");
|
|
653
|
+
swatch.addEventListener("click", () => {
|
|
654
|
+
applyColor(color);
|
|
655
|
+
closeNoteFormattingMenus();
|
|
656
|
+
});
|
|
657
|
+
fragment.append(swatch);
|
|
658
|
+
});
|
|
659
|
+
appendSwatches(sections.defaults);
|
|
660
|
+
if (sections.custom.length > 0) {
|
|
661
|
+
const separator = document.createElement("div");
|
|
662
|
+
separator.className = "swatch-separator";
|
|
663
|
+
separator.setAttribute("role", "separator");
|
|
664
|
+
fragment.append(separator);
|
|
665
|
+
appendSwatches(sections.custom);
|
|
666
|
+
}
|
|
667
|
+
element.replaceChildren(fragment);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function renderNoteColorSwatches(control) {
|
|
671
|
+
const note = activeNoteObject();
|
|
672
|
+
if (!note) return;
|
|
673
|
+
|
|
674
|
+
const currentColor = CanvasERDDocument.noteFormatting(note)[control.property].toLowerCase();
|
|
675
|
+
renderColorSwatches(
|
|
676
|
+
control.swatches,
|
|
677
|
+
CanvasERDDocument.noteColorSwatches(state.notes, control.property, control.defaults),
|
|
678
|
+
control.defaults,
|
|
679
|
+
currentColor,
|
|
680
|
+
(color) => applyNoteFormatting({ [control.property]: color })
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function toggleNoteColorMenu(control) {
|
|
685
|
+
const opening = control.menu.hidden;
|
|
686
|
+
closeNoteFormattingMenus();
|
|
687
|
+
if (!opening || !activeNoteObject()) return;
|
|
688
|
+
|
|
689
|
+
renderNoteColorSwatches(control);
|
|
690
|
+
control.menu.hidden = false;
|
|
691
|
+
control.button.setAttribute("aria-expanded", "true");
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function updateTableFormattingControls() {
|
|
695
|
+
const entityIds = activeTableEntityIds();
|
|
696
|
+
tableFormattingElement.hidden = entityIds.length === 0;
|
|
697
|
+
if (entityIds.length === 0) return;
|
|
698
|
+
|
|
699
|
+
const color = CanvasERDDocument.selectedTableHeaderColor(state, entityIds);
|
|
700
|
+
tableHeaderColorControl.preview.style.backgroundColor = color || "#ffffff";
|
|
701
|
+
tableHeaderColorControl.preview.classList.toggle("mixed-color", color === null);
|
|
702
|
+
if (color) tableHeaderColorControl.custom.value = color;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function applyTableHeaderColor(color) {
|
|
706
|
+
const entityIds = activeTableEntityIds();
|
|
707
|
+
if (entityIds.length === 0) return;
|
|
708
|
+
|
|
709
|
+
state = CanvasERDDocument.setTableHeaderColor(state, entityIds, color);
|
|
710
|
+
entityIds.forEach((entityId) => {
|
|
711
|
+
const table = tableObjects.get(entityId);
|
|
712
|
+
if (!table) return;
|
|
713
|
+
table.getObjects().slice(1, 3).forEach((header) => header.set({ fill: color }));
|
|
714
|
+
table.dirty = true;
|
|
715
|
+
});
|
|
716
|
+
updateTableFormattingControls();
|
|
717
|
+
canvas.requestRenderAll();
|
|
718
|
+
markDirty();
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function renderTableHeaderColorSwatches() {
|
|
722
|
+
const entityIds = activeTableEntityIds();
|
|
723
|
+
if (entityIds.length === 0) return;
|
|
724
|
+
|
|
725
|
+
renderColorSwatches(
|
|
726
|
+
tableHeaderColorControl.swatches,
|
|
727
|
+
CanvasERDDocument.tableHeaderColorSwatches(state.tableHeaderColors),
|
|
728
|
+
CanvasERDDocument.DEFAULT_TABLE_HEADER_COLORS,
|
|
729
|
+
CanvasERDDocument.selectedTableHeaderColor(state, entityIds),
|
|
730
|
+
applyTableHeaderColor
|
|
731
|
+
);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function toggleTableHeaderColorMenu() {
|
|
735
|
+
const opening = tableHeaderColorControl.menu.hidden;
|
|
736
|
+
closeNoteFormattingMenus();
|
|
737
|
+
if (!opening || activeTableEntityIds().length === 0) return;
|
|
738
|
+
|
|
739
|
+
renderTableHeaderColorSwatches();
|
|
740
|
+
tableHeaderColorControl.menu.hidden = false;
|
|
741
|
+
tableHeaderColorControl.button.setAttribute("aria-expanded", "true");
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function editingArrow() {
|
|
745
|
+
return state.arrows.find((arrow) => arrow.id === editingArrowId) || null;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function updateArrowFormattingControls() {
|
|
749
|
+
const arrow = editingArrow();
|
|
750
|
+
arrowFormattingElement.hidden = !arrow;
|
|
751
|
+
if (!arrow) return;
|
|
752
|
+
|
|
753
|
+
const formatting = CanvasERDDocument.arrowFormatting(arrow);
|
|
754
|
+
arrowColorControl.preview.style.backgroundColor = formatting.color;
|
|
755
|
+
if (/^#[0-9a-f]{6}$/i.test(formatting.color)) arrowColorControl.custom.value = formatting.color;
|
|
756
|
+
arrowStartHeadElement.value = formatting.startHead;
|
|
757
|
+
arrowEndHeadElement.value = formatting.endHead;
|
|
758
|
+
arrowLineStyleElement.value = formatting.lineStyle;
|
|
759
|
+
arrowStrokeWidthElement.value = formatting.strokeWidth;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function applyArrowFormatting(changes) {
|
|
763
|
+
const arrow = editingArrow();
|
|
764
|
+
if (!arrow) return;
|
|
765
|
+
|
|
766
|
+
Object.assign(arrow, CanvasERDDocument.arrowFormatting({ ...arrow, ...changes }));
|
|
767
|
+
renderArrows();
|
|
768
|
+
updateArrowFormattingControls();
|
|
769
|
+
markDirty();
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
function renderArrowColorSwatches() {
|
|
773
|
+
const arrow = editingArrow();
|
|
774
|
+
if (!arrow) return;
|
|
775
|
+
|
|
776
|
+
renderColorSwatches(
|
|
777
|
+
arrowColorControl.swatches,
|
|
778
|
+
CanvasERDDocument.arrowColorSwatches(state.arrows),
|
|
779
|
+
CanvasERDDocument.DEFAULT_FOREGROUND_COLORS,
|
|
780
|
+
CanvasERDDocument.arrowFormatting(arrow).color.toLowerCase(),
|
|
781
|
+
(color) => applyArrowFormatting({ color })
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function toggleArrowColorMenu() {
|
|
786
|
+
const opening = arrowColorControl.menu.hidden;
|
|
787
|
+
closeNoteFormattingMenus();
|
|
788
|
+
if (!opening || !editingArrow()) return;
|
|
789
|
+
|
|
790
|
+
renderArrowColorSwatches();
|
|
791
|
+
arrowColorControl.menu.hidden = false;
|
|
792
|
+
arrowColorControl.button.setAttribute("aria-expanded", "true");
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function activeFrame() {
|
|
796
|
+
const active = canvas?.getActiveObject();
|
|
797
|
+
const frameId = ["frame", "frame-label"].includes(active?.canvasErdType) ? active.frameId : null;
|
|
798
|
+
return state.frames.find((frame) => frame.id === frameId) || null;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function updateFrameFormattingControls() {
|
|
802
|
+
const frame = activeFrame();
|
|
803
|
+
frameFormattingElement.hidden = !frame;
|
|
804
|
+
if (!frame) return;
|
|
805
|
+
|
|
806
|
+
const formatting = CanvasERDDocument.frameFormatting(frame);
|
|
807
|
+
frameColorControl.preview.style.backgroundColor = formatting.color;
|
|
808
|
+
if (/^#[0-9a-f]{6}$/i.test(formatting.color)) frameColorControl.custom.value = formatting.color;
|
|
809
|
+
frameLineStyleElement.value = formatting.lineStyle;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function applyFrameFormatting(changes) {
|
|
813
|
+
const frame = activeFrame();
|
|
814
|
+
if (!frame) return;
|
|
815
|
+
|
|
816
|
+
Object.assign(frame, CanvasERDDocument.frameFormatting({ ...frame, ...changes }));
|
|
817
|
+
frameObjects.get(frame.id)?.set({
|
|
818
|
+
stroke: frame.color,
|
|
819
|
+
strokeDashArray: CanvasERDDocument.arrowStrokeDashArray(frame.lineStyle),
|
|
820
|
+
strokeLineCap: frame.lineStyle === "dotted" ? "round" : "butt"
|
|
821
|
+
});
|
|
822
|
+
frameLabelObjects.get(frame.id)?.set({ fill: frame.color });
|
|
823
|
+
updateFrameFormattingControls();
|
|
824
|
+
canvas.requestRenderAll();
|
|
825
|
+
markDirty();
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function renderFrameColorSwatches() {
|
|
829
|
+
const frame = activeFrame();
|
|
830
|
+
if (!frame) return;
|
|
831
|
+
|
|
832
|
+
renderColorSwatches(
|
|
833
|
+
frameColorControl.swatches,
|
|
834
|
+
CanvasERDDocument.frameColorSwatches(state.frames),
|
|
835
|
+
CanvasERDDocument.DEFAULT_FOREGROUND_COLORS,
|
|
836
|
+
CanvasERDDocument.frameFormatting(frame).color.toLowerCase(),
|
|
837
|
+
(color) => applyFrameFormatting({ color })
|
|
838
|
+
);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function toggleFrameColorMenu() {
|
|
842
|
+
const opening = frameColorControl.menu.hidden;
|
|
843
|
+
closeNoteFormattingMenus();
|
|
844
|
+
if (!opening || !activeFrame()) return;
|
|
845
|
+
|
|
846
|
+
renderFrameColorSwatches();
|
|
847
|
+
frameColorControl.menu.hidden = false;
|
|
848
|
+
frameColorControl.button.setAttribute("aria-expanded", "true");
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
function applyNoteFormatting(formatting) {
|
|
852
|
+
const note = activeNoteObject();
|
|
853
|
+
if (!note) return;
|
|
854
|
+
|
|
855
|
+
note.set(formatting);
|
|
856
|
+
const borderColor = CanvasERDDocument.noteBorderColor(note.backgroundColor);
|
|
857
|
+
note.set({ borderColor, editingBorderColor: borderColor, cornerColor: borderColor });
|
|
858
|
+
note.setCoords();
|
|
859
|
+
updateObjectState(note);
|
|
860
|
+
updateNoteFormattingControls();
|
|
861
|
+
canvas.requestRenderAll();
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
function toggleNoteFormatting(formatting) {
|
|
865
|
+
const note = activeNoteObject();
|
|
866
|
+
if (!note) return;
|
|
867
|
+
|
|
868
|
+
if (formatting === "bold") {
|
|
869
|
+
applyNoteFormatting({ fontWeight: note.fontWeight === "bold" ? "normal" : "bold" });
|
|
870
|
+
} else if (formatting === "italic") {
|
|
871
|
+
applyNoteFormatting({ fontStyle: note.fontStyle === "italic" ? "normal" : "italic" });
|
|
872
|
+
} else if (formatting === "underline") {
|
|
873
|
+
applyNoteFormatting({ underline: !note.underline });
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
483
877
|
function renderNotes() {
|
|
484
878
|
state.notes.forEach((note) => {
|
|
485
879
|
const object = createNoteObject(note);
|
|
@@ -488,6 +882,150 @@ function renderNotes() {
|
|
|
488
882
|
});
|
|
489
883
|
}
|
|
490
884
|
|
|
885
|
+
function createFrameObject(frame) {
|
|
886
|
+
const formatting = CanvasERDDocument.frameFormatting(frame);
|
|
887
|
+
const object = new fabric.Rect(CanvasERDDocument.topLeft({
|
|
888
|
+
left: frame.x,
|
|
889
|
+
top: frame.y,
|
|
890
|
+
width: frame.width,
|
|
891
|
+
height: frame.height,
|
|
892
|
+
fill: "transparent",
|
|
893
|
+
stroke: formatting.color,
|
|
894
|
+
strokeWidth: 2,
|
|
895
|
+
strokeDashArray: CanvasERDDocument.arrowStrokeDashArray(formatting.lineStyle),
|
|
896
|
+
strokeLineCap: formatting.lineStyle === "dotted" ? "round" : "butt",
|
|
897
|
+
strokeUniform: true,
|
|
898
|
+
objectCaching: false,
|
|
899
|
+
perPixelTargetFind: true,
|
|
900
|
+
padding: 4,
|
|
901
|
+
borderColor: "#2563eb",
|
|
902
|
+
cornerColor: "#2563eb",
|
|
903
|
+
transparentCorners: false,
|
|
904
|
+
lockRotation: true,
|
|
905
|
+
lockSkewingX: true,
|
|
906
|
+
lockSkewingY: true
|
|
907
|
+
}));
|
|
908
|
+
object.setControlsVisibility({ mtr: false });
|
|
909
|
+
object.canvasErdType = "frame";
|
|
910
|
+
object.frameId = frame.id;
|
|
911
|
+
return object;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
function createFrameLabelObject(frame) {
|
|
915
|
+
const label = typeof frame.label === "string" ? frame.label : "Frame";
|
|
916
|
+
const object = new fabric.IText(label, CanvasERDDocument.uncachedText({
|
|
917
|
+
...CanvasERDDocument.frameLabelCanvasStyle(frame),
|
|
918
|
+
fontFamily: CanvasERDDocument.CANVAS_FONT_FAMILY,
|
|
919
|
+
fontSize: 15,
|
|
920
|
+
fontWeight: "bold",
|
|
921
|
+
padding: 3,
|
|
922
|
+
hasControls: false,
|
|
923
|
+
lockMovementX: true,
|
|
924
|
+
lockMovementY: true,
|
|
925
|
+
borderColor: "#2563eb",
|
|
926
|
+
editingBorderColor: "#2563eb"
|
|
927
|
+
}));
|
|
928
|
+
object.canvasErdType = "frame-label";
|
|
929
|
+
object.frameId = frame.id;
|
|
930
|
+
return object;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
function renderFrames() {
|
|
934
|
+
frameObjects.forEach((object) => canvas.remove(object));
|
|
935
|
+
frameLabelObjects.forEach((object) => canvas.remove(object));
|
|
936
|
+
frameObjects.clear();
|
|
937
|
+
frameLabelObjects.clear();
|
|
938
|
+
state.frames.forEach((frame) => {
|
|
939
|
+
const object = createFrameObject(frame);
|
|
940
|
+
const label = createFrameLabelObject(frame);
|
|
941
|
+
frameObjects.set(frame.id, object);
|
|
942
|
+
frameLabelObjects.set(frame.id, label);
|
|
943
|
+
canvas.add(object, label);
|
|
944
|
+
});
|
|
945
|
+
restackCanvasObjects();
|
|
946
|
+
canvas.requestRenderAll();
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
function renderFramePreview() {
|
|
950
|
+
if (framePreview) canvas.remove(framePreview);
|
|
951
|
+
const bounds = CanvasERDDocument.frameBounds(drawingFrame.start, drawingFrame.end);
|
|
952
|
+
framePreview = new fabric.Rect(CanvasERDDocument.topLeft({
|
|
953
|
+
left: bounds.x,
|
|
954
|
+
top: bounds.y,
|
|
955
|
+
width: bounds.width,
|
|
956
|
+
height: bounds.height,
|
|
957
|
+
fill: "rgba(37, 99, 235, 0.03)",
|
|
958
|
+
stroke: "#2563eb",
|
|
959
|
+
strokeWidth: 2,
|
|
960
|
+
strokeDashArray: [7, 5],
|
|
961
|
+
selectable: false,
|
|
962
|
+
evented: false,
|
|
963
|
+
objectCaching: false
|
|
964
|
+
}));
|
|
965
|
+
framePreview.canvasErdType = "frame-preview";
|
|
966
|
+
canvas.add(framePreview);
|
|
967
|
+
restackCanvasObjects();
|
|
968
|
+
canvas.requestRenderAll();
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
function setFrameMode(active) {
|
|
972
|
+
if (active && arrowMode) cancelArrowDrawing();
|
|
973
|
+
if (active && editingArrowId) finishArrowEndpointEditing();
|
|
974
|
+
frameMode = active;
|
|
975
|
+
frameButton.setAttribute("aria-pressed", String(active));
|
|
976
|
+
canvas.selection = !active;
|
|
977
|
+
canvas.setCursor(active ? "crosshair" : "default");
|
|
978
|
+
[
|
|
979
|
+
...frameObjects.values(),
|
|
980
|
+
...frameLabelObjects.values(),
|
|
981
|
+
...tableObjects.values(),
|
|
982
|
+
...noteObjects.values(),
|
|
983
|
+
...arrowObjects.values()
|
|
984
|
+
].forEach((object) => {
|
|
985
|
+
object.selectable = !active;
|
|
986
|
+
object.evented = !active;
|
|
987
|
+
});
|
|
988
|
+
if (active) canvas.discardActiveObject();
|
|
989
|
+
canvas.requestRenderAll();
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
function cancelFrameDrawing() {
|
|
993
|
+
if (framePreview) canvas.remove(framePreview);
|
|
994
|
+
framePreview = null;
|
|
995
|
+
drawingFrame = null;
|
|
996
|
+
setFrameMode(false);
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
function finishFrameDrawing(point) {
|
|
1000
|
+
if (!drawingFrame) return;
|
|
1001
|
+
drawingFrame.end = point;
|
|
1002
|
+
const bounds = CanvasERDDocument.frameBounds(drawingFrame.start, drawingFrame.end);
|
|
1003
|
+
if (framePreview) canvas.remove(framePreview);
|
|
1004
|
+
framePreview = null;
|
|
1005
|
+
|
|
1006
|
+
if (bounds.width >= 40 && bounds.height >= 30) {
|
|
1007
|
+
const frame = {
|
|
1008
|
+
id: globalThis.crypto && crypto.randomUUID ? crypto.randomUUID() : `frame-${Date.now()}`,
|
|
1009
|
+
label: "Frame",
|
|
1010
|
+
...bounds,
|
|
1011
|
+
...CanvasERDDocument.DEFAULT_FRAME_FORMATTING
|
|
1012
|
+
};
|
|
1013
|
+
state.frames.push(frame);
|
|
1014
|
+
drawingFrame = null;
|
|
1015
|
+
renderFrames();
|
|
1016
|
+
setFrameMode(false);
|
|
1017
|
+
const label = frameLabelObjects.get(frame.id);
|
|
1018
|
+
canvas.setActiveObject(label);
|
|
1019
|
+
label.enterEditing();
|
|
1020
|
+
label.selectAll();
|
|
1021
|
+
canvas.requestRenderAll();
|
|
1022
|
+
markDirty();
|
|
1023
|
+
} else {
|
|
1024
|
+
drawingFrame = null;
|
|
1025
|
+
setFrameMode(false);
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
|
|
491
1029
|
function attachmentTarget(attachment) {
|
|
492
1030
|
if (!attachment) return null;
|
|
493
1031
|
if (attachment.type === "entity") return tableObjects.get(attachment.id);
|
|
@@ -530,13 +1068,58 @@ function arrowEndpoint(eventData) {
|
|
|
530
1068
|
return endpointAt(point, eventData.target, eventData.e.ctrlKey);
|
|
531
1069
|
}
|
|
532
1070
|
|
|
1071
|
+
function drawArrowHead(context, point, angle, style, color, strokeWidth) {
|
|
1072
|
+
if (style === "none") return;
|
|
1073
|
+
|
|
1074
|
+
const { length, halfWidth, radius } = CanvasERDDocument.arrowHeadGeometry(style, strokeWidth);
|
|
1075
|
+
context.save();
|
|
1076
|
+
context.translate(point.x, point.y);
|
|
1077
|
+
context.rotate(angle);
|
|
1078
|
+
context.fillStyle = color;
|
|
1079
|
+
context.strokeStyle = color;
|
|
1080
|
+
context.lineWidth = strokeWidth;
|
|
1081
|
+
context.lineCap = "round";
|
|
1082
|
+
context.lineJoin = "round";
|
|
1083
|
+
context.setLineDash([]);
|
|
1084
|
+
context.beginPath();
|
|
1085
|
+
if (style === "open") {
|
|
1086
|
+
context.moveTo(-length, -halfWidth);
|
|
1087
|
+
context.lineTo(0, 0);
|
|
1088
|
+
context.lineTo(-length, halfWidth);
|
|
1089
|
+
context.stroke();
|
|
1090
|
+
} else if (["circle", "circle-open"].includes(style)) {
|
|
1091
|
+
context.arc(-radius, 0, radius, 0, Math.PI * 2);
|
|
1092
|
+
if (style === "circle-open") context.stroke();
|
|
1093
|
+
else context.fill();
|
|
1094
|
+
} else if (["diamond", "diamond-open"].includes(style)) {
|
|
1095
|
+
context.moveTo(0, 0);
|
|
1096
|
+
context.lineTo(-length / 2, -halfWidth);
|
|
1097
|
+
context.lineTo(-length, 0);
|
|
1098
|
+
context.lineTo(-length / 2, halfWidth);
|
|
1099
|
+
context.closePath();
|
|
1100
|
+
if (style === "diamond-open") context.stroke();
|
|
1101
|
+
else context.fill();
|
|
1102
|
+
} else {
|
|
1103
|
+
context.moveTo(0, 0);
|
|
1104
|
+
context.lineTo(-length, -halfWidth);
|
|
1105
|
+
context.lineTo(-length, halfWidth);
|
|
1106
|
+
context.closePath();
|
|
1107
|
+
context.fill();
|
|
1108
|
+
}
|
|
1109
|
+
context.restore();
|
|
1110
|
+
}
|
|
1111
|
+
|
|
533
1112
|
function createArrowObject(arrow, options = {}) {
|
|
534
1113
|
const start = resolveArrowEndpoint(arrow.start);
|
|
535
1114
|
const end = resolveArrowEndpoint(arrow.end);
|
|
1115
|
+
const formatting = CanvasERDDocument.arrowFormatting(arrow);
|
|
1116
|
+
const color = options.stroke || formatting.color;
|
|
536
1117
|
const object = new fabric.Line([start.x, start.y, end.x, end.y], {
|
|
537
|
-
stroke:
|
|
538
|
-
strokeWidth:
|
|
539
|
-
|
|
1118
|
+
stroke: color,
|
|
1119
|
+
strokeWidth: formatting.strokeWidth,
|
|
1120
|
+
strokeDashArray: CanvasERDDocument.arrowStrokeDashArray(formatting.lineStyle),
|
|
1121
|
+
strokeLineCap: formatting.lineStyle === "dotted" ? "round" : "butt",
|
|
1122
|
+
fill: color,
|
|
540
1123
|
objectCaching: false,
|
|
541
1124
|
perPixelTargetFind: true,
|
|
542
1125
|
padding: 6,
|
|
@@ -549,20 +1132,40 @@ function createArrowObject(arrow, options = {}) {
|
|
|
549
1132
|
lockSkewingY: true
|
|
550
1133
|
});
|
|
551
1134
|
object._render = function (context) {
|
|
552
|
-
fabric.Line.prototype._render.call(this, context);
|
|
553
1135
|
const points = this.calcLinePoints();
|
|
554
|
-
const
|
|
1136
|
+
const shaft = CanvasERDDocument.shortenedArrowLinePoints(
|
|
1137
|
+
points,
|
|
1138
|
+
formatting.startHead,
|
|
1139
|
+
formatting.endHead,
|
|
1140
|
+
this.strokeWidth
|
|
1141
|
+
);
|
|
555
1142
|
context.save();
|
|
556
|
-
context.translate(points.x2, points.y2);
|
|
557
|
-
context.rotate(angle);
|
|
558
|
-
context.fillStyle = this.stroke;
|
|
559
1143
|
context.beginPath();
|
|
560
|
-
context.moveTo(
|
|
561
|
-
context.lineTo(
|
|
562
|
-
context.
|
|
563
|
-
context.
|
|
564
|
-
context.
|
|
1144
|
+
context.moveTo(shaft.x1, shaft.y1);
|
|
1145
|
+
context.lineTo(shaft.x2, shaft.y2);
|
|
1146
|
+
context.strokeStyle = this.stroke;
|
|
1147
|
+
context.lineWidth = this.strokeWidth;
|
|
1148
|
+
context.lineCap = this.strokeLineCap;
|
|
1149
|
+
context.setLineDash(this.strokeDashArray || []);
|
|
1150
|
+
context.stroke();
|
|
565
1151
|
context.restore();
|
|
1152
|
+
const angle = Math.atan2(points.y2 - points.y1, points.x2 - points.x1);
|
|
1153
|
+
drawArrowHead(
|
|
1154
|
+
context,
|
|
1155
|
+
{ x: points.x1, y: points.y1 },
|
|
1156
|
+
angle + Math.PI,
|
|
1157
|
+
formatting.startHead,
|
|
1158
|
+
this.stroke,
|
|
1159
|
+
this.strokeWidth
|
|
1160
|
+
);
|
|
1161
|
+
drawArrowHead(
|
|
1162
|
+
context,
|
|
1163
|
+
{ x: points.x2, y: points.y2 },
|
|
1164
|
+
angle,
|
|
1165
|
+
formatting.endHead,
|
|
1166
|
+
this.stroke,
|
|
1167
|
+
this.strokeWidth
|
|
1168
|
+
);
|
|
566
1169
|
};
|
|
567
1170
|
object.canvasErdType = options.preview ? "arrow-preview" : "arrow";
|
|
568
1171
|
object.arrowId = arrow.id;
|
|
@@ -606,15 +1209,20 @@ function renderArrowPreview() {
|
|
|
606
1209
|
}
|
|
607
1210
|
|
|
608
1211
|
function setArrowMode(active) {
|
|
1212
|
+
if (active && frameMode) cancelFrameDrawing();
|
|
609
1213
|
if (active && editingArrowId) finishArrowEndpointEditing();
|
|
610
1214
|
arrowMode = active;
|
|
611
1215
|
arrowButton.setAttribute("aria-pressed", String(active));
|
|
612
1216
|
canvas.selection = !active;
|
|
613
1217
|
canvas.setCursor(active ? "crosshair" : "default");
|
|
614
|
-
[
|
|
1218
|
+
[
|
|
1219
|
+
...frameObjects.values(),
|
|
1220
|
+
...frameLabelObjects.values(),
|
|
1221
|
+
...tableObjects.values(),
|
|
1222
|
+
...noteObjects.values(),
|
|
1223
|
+
...arrowObjects.values()
|
|
1224
|
+
].forEach((object) => {
|
|
615
1225
|
object.selectable = !active;
|
|
616
|
-
});
|
|
617
|
-
arrowObjects.forEach((object) => {
|
|
618
1226
|
object.evented = !active;
|
|
619
1227
|
});
|
|
620
1228
|
if (active) canvas.discardActiveObject();
|
|
@@ -700,6 +1308,7 @@ function beginArrowEndpointEditing(arrowId) {
|
|
|
700
1308
|
arrowEndpointHandles.set(endpointName, handle);
|
|
701
1309
|
canvas.add(handle);
|
|
702
1310
|
});
|
|
1311
|
+
updateArrowFormattingControls();
|
|
703
1312
|
canvas.requestRenderAll();
|
|
704
1313
|
}
|
|
705
1314
|
|
|
@@ -713,6 +1322,8 @@ function finishArrowEndpointEditing() {
|
|
|
713
1322
|
object.evented = true;
|
|
714
1323
|
}
|
|
715
1324
|
editingArrowId = null;
|
|
1325
|
+
updateArrowFormattingControls();
|
|
1326
|
+
closeNoteFormattingMenus();
|
|
716
1327
|
canvas.discardActiveObject();
|
|
717
1328
|
canvas.requestRenderAll();
|
|
718
1329
|
}
|
|
@@ -757,6 +1368,7 @@ function removeActiveArrows() {
|
|
|
757
1368
|
}
|
|
758
1369
|
|
|
759
1370
|
function addNote(position = canvas.getVpCenter()) {
|
|
1371
|
+
if (frameMode) cancelFrameDrawing();
|
|
760
1372
|
if (arrowMode) cancelArrowDrawing();
|
|
761
1373
|
if (editingArrowId) finishArrowEndpointEditing();
|
|
762
1374
|
const origin = CanvasERDDocument.notePositionAt(position);
|
|
@@ -766,7 +1378,8 @@ function addNote(position = canvas.getVpCenter()) {
|
|
|
766
1378
|
x: origin.x,
|
|
767
1379
|
y: origin.y,
|
|
768
1380
|
width: 220,
|
|
769
|
-
angle: 0
|
|
1381
|
+
angle: 0,
|
|
1382
|
+
...lastNoteFormatting
|
|
770
1383
|
};
|
|
771
1384
|
state.notes.push(note);
|
|
772
1385
|
|
|
@@ -777,6 +1390,7 @@ function addNote(position = canvas.getVpCenter()) {
|
|
|
777
1390
|
canvas.setActiveObject(object);
|
|
778
1391
|
object.enterEditing();
|
|
779
1392
|
object.selectAll();
|
|
1393
|
+
updateNoteFormattingControls();
|
|
780
1394
|
canvas.requestRenderAll();
|
|
781
1395
|
markDirty();
|
|
782
1396
|
}
|
|
@@ -790,10 +1404,41 @@ function updateObjectState(object) {
|
|
|
790
1404
|
if (!note) return;
|
|
791
1405
|
Object.assign(note, CanvasERDDocument.noteStateGeometry(object));
|
|
792
1406
|
scheduleRelationshipRender();
|
|
1407
|
+
} else if (object.canvasErdType === "frame") {
|
|
1408
|
+
const frame = state.frames.find((candidate) => candidate.id === object.frameId);
|
|
1409
|
+
if (!frame) return;
|
|
1410
|
+
Object.assign(frame, {
|
|
1411
|
+
x: object.left,
|
|
1412
|
+
y: object.top,
|
|
1413
|
+
width: object.width * object.scaleX,
|
|
1414
|
+
height: object.height * object.scaleY
|
|
1415
|
+
});
|
|
1416
|
+
const label = frameLabelObjects.get(frame.id);
|
|
1417
|
+
const position = CanvasERDDocument.frameLabelPosition(frame);
|
|
1418
|
+
label?.set({ left: position.x, top: position.y });
|
|
1419
|
+
label?.setCoords();
|
|
1420
|
+
} else if (object.canvasErdType === "frame-label") {
|
|
1421
|
+
const frame = state.frames.find((candidate) => candidate.id === object.frameId);
|
|
1422
|
+
if (!frame) return;
|
|
1423
|
+
frame.label = object.text;
|
|
793
1424
|
}
|
|
794
1425
|
markDirty();
|
|
795
1426
|
}
|
|
796
1427
|
|
|
1428
|
+
function removeActiveFrame() {
|
|
1429
|
+
const active = canvas.getActiveObject();
|
|
1430
|
+
if (!["frame", "frame-label"].includes(active?.canvasErdType) || active.isEditing) return false;
|
|
1431
|
+
|
|
1432
|
+
const frameId = active.frameId;
|
|
1433
|
+
canvas.discardActiveObject();
|
|
1434
|
+
canvas.remove(frameObjects.get(frameId), frameLabelObjects.get(frameId));
|
|
1435
|
+
frameObjects.delete(frameId);
|
|
1436
|
+
frameLabelObjects.delete(frameId);
|
|
1437
|
+
state.frames = state.frames.filter((frame) => frame.id !== frameId);
|
|
1438
|
+
markDirty();
|
|
1439
|
+
return true;
|
|
1440
|
+
}
|
|
1441
|
+
|
|
797
1442
|
function removeActiveNote() {
|
|
798
1443
|
const active = canvas.getActiveObject();
|
|
799
1444
|
if (!active || active.canvasErdType !== "note" || active.isEditing) return false;
|
|
@@ -807,6 +1452,7 @@ function removeActiveNote() {
|
|
|
807
1452
|
endpoint.attachment = null;
|
|
808
1453
|
});
|
|
809
1454
|
});
|
|
1455
|
+
canvas.discardActiveObject();
|
|
810
1456
|
canvas.remove(active);
|
|
811
1457
|
noteObjects.delete(active.noteId);
|
|
812
1458
|
state.notes = state.notes.filter((note) => note.id !== active.noteId);
|
|
@@ -1022,7 +1668,13 @@ async function saveDiagram() {
|
|
|
1022
1668
|
|
|
1023
1669
|
function displayDocument(documentData) {
|
|
1024
1670
|
schema = documentData.schema;
|
|
1025
|
-
state = {
|
|
1671
|
+
state = {
|
|
1672
|
+
...documentData.state,
|
|
1673
|
+
arrows: documentData.state.arrows || [],
|
|
1674
|
+
frames: documentData.state.frames || [],
|
|
1675
|
+
tableHeaderColors: documentData.state.tableHeaderColors || {}
|
|
1676
|
+
};
|
|
1677
|
+
if (frameMode) cancelFrameDrawing();
|
|
1026
1678
|
if (arrowMode) cancelArrowDrawing();
|
|
1027
1679
|
if (editingArrowId) finishArrowEndpointEditing();
|
|
1028
1680
|
canvas.discardActiveObject();
|
|
@@ -1031,7 +1683,10 @@ function displayDocument(documentData) {
|
|
|
1031
1683
|
relationshipObjects = [];
|
|
1032
1684
|
noteObjects.clear();
|
|
1033
1685
|
arrowObjects.clear();
|
|
1686
|
+
frameObjects.clear();
|
|
1687
|
+
frameLabelObjects.clear();
|
|
1034
1688
|
renderTableList();
|
|
1689
|
+
renderFrames();
|
|
1035
1690
|
renderTables();
|
|
1036
1691
|
renderNotes();
|
|
1037
1692
|
renderArrows();
|
|
@@ -1097,6 +1752,8 @@ async function newDiagram() {
|
|
|
1097
1752
|
|
|
1098
1753
|
function diagramBounds() {
|
|
1099
1754
|
const objects = [
|
|
1755
|
+
...frameObjects.values(),
|
|
1756
|
+
...frameLabelObjects.values(),
|
|
1100
1757
|
...tableObjects.values(),
|
|
1101
1758
|
...noteObjects.values(),
|
|
1102
1759
|
...arrowObjects.values(),
|
|
@@ -1141,18 +1798,27 @@ function resizeCanvas() {
|
|
|
1141
1798
|
}
|
|
1142
1799
|
|
|
1143
1800
|
function configureCanvasEvents() {
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1801
|
+
const selectionChanged = () => {
|
|
1802
|
+
closeNoteFormattingMenus();
|
|
1803
|
+
updateNoteFormattingControls();
|
|
1804
|
+
updateTableFormattingControls();
|
|
1805
|
+
updateFrameFormattingControls();
|
|
1806
|
+
};
|
|
1807
|
+
canvas.on("selection:created", selectionChanged);
|
|
1808
|
+
canvas.on("selection:updated", selectionChanged);
|
|
1809
|
+
canvas.on("selection:cleared", selectionChanged);
|
|
1147
1810
|
canvas.on("mouse:down", ({ target }) => {
|
|
1148
1811
|
if (editingArrowId && target?.canvasErdType !== "arrow-endpoint") finishArrowEndpointEditing();
|
|
1812
|
+
const arrowId = CanvasERDDocument.arrowIdForEndpointEditing(target);
|
|
1813
|
+
if (arrowId) beginArrowEndpointEditing(arrowId);
|
|
1149
1814
|
});
|
|
1150
1815
|
canvas.on("mouse:down", (eventData) => {
|
|
1151
1816
|
if (!arrowMode || spacePressed) return;
|
|
1152
1817
|
drawingArrow = {
|
|
1153
1818
|
id: globalThis.crypto && crypto.randomUUID ? crypto.randomUUID() : `arrow-${Date.now()}`,
|
|
1154
1819
|
start: arrowEndpoint(eventData),
|
|
1155
|
-
end: arrowEndpoint(eventData)
|
|
1820
|
+
end: arrowEndpoint(eventData),
|
|
1821
|
+
...CanvasERDDocument.DEFAULT_ARROW_FORMATTING
|
|
1156
1822
|
};
|
|
1157
1823
|
renderArrowPreview();
|
|
1158
1824
|
});
|
|
@@ -1164,11 +1830,31 @@ function configureCanvasEvents() {
|
|
|
1164
1830
|
canvas.on("mouse:up", (eventData) => {
|
|
1165
1831
|
if (arrowMode && drawingArrow) finishArrowDrawing(eventData);
|
|
1166
1832
|
});
|
|
1833
|
+
canvas.on("mouse:down", ({ e, scenePoint }) => {
|
|
1834
|
+
if (!frameMode || spacePressed) return;
|
|
1835
|
+
const point = scenePoint || canvas.getScenePoint(e);
|
|
1836
|
+
drawingFrame = { start: { x: point.x, y: point.y }, end: { x: point.x, y: point.y } };
|
|
1837
|
+
renderFramePreview();
|
|
1838
|
+
});
|
|
1839
|
+
canvas.on("mouse:move", ({ e, scenePoint }) => {
|
|
1840
|
+
if (!frameMode || !drawingFrame) return;
|
|
1841
|
+
const point = scenePoint || canvas.getScenePoint(e);
|
|
1842
|
+
drawingFrame.end = { x: point.x, y: point.y };
|
|
1843
|
+
renderFramePreview();
|
|
1844
|
+
});
|
|
1845
|
+
canvas.on("mouse:up", ({ e, scenePoint }) => {
|
|
1846
|
+
if (!frameMode || !drawingFrame) return;
|
|
1847
|
+
const point = scenePoint || canvas.getScenePoint(e);
|
|
1848
|
+
finishFrameDrawing({ x: point.x, y: point.y });
|
|
1849
|
+
});
|
|
1167
1850
|
|
|
1168
1851
|
canvas.on("object:moving", (eventData) => {
|
|
1169
1852
|
if (eventData.target.canvasErdType === "arrow-endpoint") updateArrowEndpointHandle(eventData);
|
|
1170
1853
|
else if (eventData.target.canvasErdType !== "arrow") updateObjectState(eventData.target);
|
|
1171
1854
|
});
|
|
1855
|
+
canvas.on("object:scaling", ({ target }) => {
|
|
1856
|
+
if (target.canvasErdType === "frame") updateObjectState(target);
|
|
1857
|
+
});
|
|
1172
1858
|
canvas.on("object:modified", (eventData) => {
|
|
1173
1859
|
if (eventData.target.canvasErdType === "arrow") updateArrowState(eventData.target);
|
|
1174
1860
|
else if (eventData.target.canvasErdType === "arrow-endpoint") updateArrowEndpointHandle(eventData);
|
|
@@ -1227,9 +1913,22 @@ function configureCanvasEvents() {
|
|
|
1227
1913
|
document.addEventListener("keydown", (event) => {
|
|
1228
1914
|
const editing = canvas.getActiveObject() && canvas.getActiveObject().isEditing;
|
|
1229
1915
|
const acceptsText = ["INPUT", "TEXTAREA", "SELECT"].includes(event.target.tagName) || event.target.isContentEditable;
|
|
1916
|
+
const noteFormatting = CanvasERDDocument.noteFormattingShortcut(event);
|
|
1230
1917
|
const action = CanvasERDDocument.shortcutAction(event);
|
|
1231
1918
|
const bareAction = ["addNote", "arrow", "toggleTables", "layoutTables", "zoomIn", "zoomOut", "fit", "help"].includes(action);
|
|
1232
1919
|
|
|
1920
|
+
if (noteFormatting && activeNoteObject() && (!acceptsText || editing)) {
|
|
1921
|
+
event.preventDefault();
|
|
1922
|
+
if (!event.repeat) {
|
|
1923
|
+
if (CanvasERDDocument.NOTE_PARAGRAPH_STYLES[noteFormatting]) {
|
|
1924
|
+
applyNoteFormatting(CanvasERDDocument.noteParagraphStyleFormatting(noteFormatting));
|
|
1925
|
+
} else {
|
|
1926
|
+
toggleNoteFormatting(noteFormatting);
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
return;
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1233
1932
|
if (action && (!bareAction || (!editing && !acceptsText))) {
|
|
1234
1933
|
event.preventDefault();
|
|
1235
1934
|
if (event.repeat) return;
|
|
@@ -1264,12 +1963,17 @@ function configureCanvasEvents() {
|
|
|
1264
1963
|
return;
|
|
1265
1964
|
}
|
|
1266
1965
|
|
|
1267
|
-
if (event.key === "Escape" &&
|
|
1966
|
+
if (event.key === "Escape" && closeNoteFormattingMenus()) {
|
|
1967
|
+
event.preventDefault();
|
|
1968
|
+
} else if (event.key === "Escape" && editingArrowId) {
|
|
1268
1969
|
event.preventDefault();
|
|
1269
1970
|
finishArrowEndpointEditing();
|
|
1270
1971
|
} else if (event.key === "Escape" && arrowMode) {
|
|
1271
1972
|
event.preventDefault();
|
|
1272
1973
|
cancelArrowDrawing();
|
|
1974
|
+
} else if (event.key === "Escape" && frameMode) {
|
|
1975
|
+
event.preventDefault();
|
|
1976
|
+
cancelFrameDrawing();
|
|
1273
1977
|
} else if (event.key === "Escape" && !shortcutPanelElement.hidden) {
|
|
1274
1978
|
event.preventDefault();
|
|
1275
1979
|
showShortcutHelp(false);
|
|
@@ -1279,7 +1983,7 @@ function configureCanvasEvents() {
|
|
|
1279
1983
|
canvas.skipTargetFind = true;
|
|
1280
1984
|
canvas.setCursor("grab");
|
|
1281
1985
|
} else if (["Delete", "Backspace"].includes(event.key) && (
|
|
1282
|
-
removeActiveTables() || removeActiveArrows() || removeActiveNote()
|
|
1986
|
+
removeActiveTables() || removeActiveArrows() || removeActiveFrame() || removeActiveNote()
|
|
1283
1987
|
)) {
|
|
1284
1988
|
event.preventDefault();
|
|
1285
1989
|
}
|
|
@@ -1290,8 +1994,8 @@ function configureCanvasEvents() {
|
|
|
1290
1994
|
spacePressed = false;
|
|
1291
1995
|
isPanning = false;
|
|
1292
1996
|
canvas.skipTargetFind = false;
|
|
1293
|
-
canvas.selection =
|
|
1294
|
-
canvas.setCursor(arrowMode ? "crosshair" : "default");
|
|
1997
|
+
canvas.selection = !arrowMode && !frameMode;
|
|
1998
|
+
canvas.setCursor(arrowMode || frameMode ? "crosshair" : "default");
|
|
1295
1999
|
});
|
|
1296
2000
|
}
|
|
1297
2001
|
|
|
@@ -1304,6 +2008,10 @@ function configureControls() {
|
|
|
1304
2008
|
if (arrowMode) cancelArrowDrawing();
|
|
1305
2009
|
else setArrowMode(true);
|
|
1306
2010
|
});
|
|
2011
|
+
frameButton.addEventListener("click", () => {
|
|
2012
|
+
if (frameMode) cancelFrameDrawing();
|
|
2013
|
+
else setFrameMode(true);
|
|
2014
|
+
});
|
|
1307
2015
|
saveButton.addEventListener("click", saveDiagram);
|
|
1308
2016
|
newButton.addEventListener("click", newDiagram);
|
|
1309
2017
|
openButton.addEventListener("click", () => {
|
|
@@ -1343,13 +2051,78 @@ function configureControls() {
|
|
|
1343
2051
|
document.querySelector("#select-all").addEventListener("click", () => setAllTables(true));
|
|
1344
2052
|
document.querySelector("#select-none").addEventListener("click", () => setAllTables(false));
|
|
1345
2053
|
searchElement.addEventListener("input", filterTableList);
|
|
2054
|
+
noteBoldButton.addEventListener("click", () => toggleNoteFormatting("bold"));
|
|
2055
|
+
noteItalicButton.addEventListener("click", () => toggleNoteFormatting("italic"));
|
|
2056
|
+
noteUnderlineButton.addEventListener("click", () => toggleNoteFormatting("underline"));
|
|
2057
|
+
noteColorControls.forEach((control) => {
|
|
2058
|
+
control.button.addEventListener("click", () => toggleNoteColorMenu(control));
|
|
2059
|
+
control.custom.addEventListener("change", () => {
|
|
2060
|
+
applyNoteFormatting({ [control.property]: control.custom.value });
|
|
2061
|
+
closeNoteFormattingMenus();
|
|
2062
|
+
});
|
|
2063
|
+
control.transparent?.addEventListener("click", () => {
|
|
2064
|
+
applyNoteFormatting({ [control.property]: "transparent" });
|
|
2065
|
+
closeNoteFormattingMenus();
|
|
2066
|
+
});
|
|
2067
|
+
});
|
|
2068
|
+
tableHeaderColorControl.button.addEventListener("click", toggleTableHeaderColorMenu);
|
|
2069
|
+
tableHeaderColorControl.custom.addEventListener("change", () => {
|
|
2070
|
+
applyTableHeaderColor(tableHeaderColorControl.custom.value);
|
|
2071
|
+
closeNoteFormattingMenus();
|
|
2072
|
+
});
|
|
2073
|
+
arrowColorControl.button.addEventListener("click", toggleArrowColorMenu);
|
|
2074
|
+
arrowColorControl.custom.addEventListener("change", () => {
|
|
2075
|
+
applyArrowFormatting({ color: arrowColorControl.custom.value });
|
|
2076
|
+
closeNoteFormattingMenus();
|
|
2077
|
+
});
|
|
2078
|
+
arrowStartHeadElement.addEventListener("change", () => {
|
|
2079
|
+
applyArrowFormatting({ startHead: arrowStartHeadElement.value });
|
|
2080
|
+
});
|
|
2081
|
+
arrowEndHeadElement.addEventListener("change", () => {
|
|
2082
|
+
applyArrowFormatting({ endHead: arrowEndHeadElement.value });
|
|
2083
|
+
});
|
|
2084
|
+
arrowLineStyleElement.addEventListener("change", () => {
|
|
2085
|
+
applyArrowFormatting({ lineStyle: arrowLineStyleElement.value });
|
|
2086
|
+
});
|
|
2087
|
+
arrowStrokeWidthElement.addEventListener("change", () => {
|
|
2088
|
+
applyArrowFormatting({ strokeWidth: arrowStrokeWidthElement.value });
|
|
2089
|
+
});
|
|
2090
|
+
frameColorControl.button.addEventListener("click", toggleFrameColorMenu);
|
|
2091
|
+
frameColorControl.custom.addEventListener("change", () => {
|
|
2092
|
+
applyFrameFormatting({ color: frameColorControl.custom.value });
|
|
2093
|
+
closeNoteFormattingMenus();
|
|
2094
|
+
});
|
|
2095
|
+
frameLineStyleElement.addEventListener("change", () => {
|
|
2096
|
+
applyFrameFormatting({ lineStyle: frameLineStyleElement.value });
|
|
2097
|
+
});
|
|
2098
|
+
document.addEventListener("pointerdown", (event) => {
|
|
2099
|
+
const insideFormatting = noteFormattingElement.contains(event.target) ||
|
|
2100
|
+
tableFormattingElement.contains(event.target) ||
|
|
2101
|
+
arrowFormattingElement.contains(event.target) ||
|
|
2102
|
+
frameFormattingElement.contains(event.target);
|
|
2103
|
+
if (!insideFormatting) closeNoteFormattingMenus();
|
|
2104
|
+
});
|
|
2105
|
+
noteParagraphStyleElement.addEventListener("change", () => {
|
|
2106
|
+
applyNoteFormatting(
|
|
2107
|
+
CanvasERDDocument.noteParagraphStyleFormatting(noteParagraphStyleElement.value)
|
|
2108
|
+
);
|
|
2109
|
+
});
|
|
2110
|
+
noteAlignmentElement.addEventListener("change", () => {
|
|
2111
|
+
applyNoteFormatting({ textAlign: noteAlignmentElement.value });
|
|
2112
|
+
});
|
|
1346
2113
|
}
|
|
1347
2114
|
|
|
1348
2115
|
function initializeEditor(loadedDocument) {
|
|
1349
2116
|
schema = loadedDocument.schema;
|
|
1350
|
-
applicationSchema = schema;
|
|
2117
|
+
applicationSchema = loadedDocument.application_schema || schema;
|
|
2118
|
+
currentDiagramFilename = loadedDocument.filename || null;
|
|
1351
2119
|
state = loadedDocument.state || CanvasERDDocument.createState(schema);
|
|
1352
|
-
state = {
|
|
2120
|
+
state = {
|
|
2121
|
+
...state,
|
|
2122
|
+
arrows: state.arrows || [],
|
|
2123
|
+
frames: state.frames || [],
|
|
2124
|
+
tableHeaderColors: state.tableHeaderColors || {}
|
|
2125
|
+
};
|
|
1353
2126
|
setDiagramName(loadedDocument.name);
|
|
1354
2127
|
canvas = new fabric.Canvas("erd-canvas", {
|
|
1355
2128
|
backgroundColor: "transparent",
|
|
@@ -1363,6 +2136,7 @@ function initializeEditor(loadedDocument) {
|
|
|
1363
2136
|
configureCanvasEvents();
|
|
1364
2137
|
configureControls();
|
|
1365
2138
|
renderTableList();
|
|
2139
|
+
renderFrames();
|
|
1366
2140
|
renderTables();
|
|
1367
2141
|
renderNotes();
|
|
1368
2142
|
renderArrows();
|
|
@@ -1373,7 +2147,8 @@ function initializeEditor(loadedDocument) {
|
|
|
1373
2147
|
fitDiagram(false);
|
|
1374
2148
|
}
|
|
1375
2149
|
new ResizeObserver(resizeCanvas).observe(panelElement);
|
|
1376
|
-
refreshSavedDiagrams()
|
|
2150
|
+
refreshSavedDiagrams(currentDiagramFilename)
|
|
2151
|
+
.catch((error) => showNotice(`Could not list diagrams: ${error.message}`, true));
|
|
1377
2152
|
loadingElement.hidden = true;
|
|
1378
2153
|
}
|
|
1379
2154
|
|