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.
@@ -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: "#243b63",
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: "#243b63"
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 labelPosition;
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
- const points = [
434
+ sourceEndpoint = CanvasERDDocument.cardinalityEndpointGeometry(
333
435
  { x: right, y: centerY },
334
- { x: right + 44, y: centerY },
335
- { x: right + 44, y: top - 34 },
336
- { x: right - 20, y: top - 34 },
337
- { x: right - 20, y: top }
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
- sourceCenter.x,
352
- sourceCenter.y,
353
- destinationCenter.x,
354
- destinationCenter.y
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
- connector.canvasErdType = "relationship";
363
- const label = new fabric.FabricText(CanvasERDDocument.cardinalityLabel(relationship), {
364
- left: labelPosition.x,
365
- top: labelPosition.y,
366
- originX: "center",
367
- originY: "center",
368
- fontFamily: CanvasERDDocument.CANVAS_FONT_FAMILY,
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
- [...relationshipObjects].reverse().forEach((object) => canvas.sendObjectToBack(object));
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: "#d09b16",
460
- editingBorderColor: "#d09b16",
461
- cornerColor: "#d09b16",
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: options.stroke || "#334e75",
538
- strokeWidth: 2.25,
539
- fill: options.stroke || "#334e75",
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 angle = Math.atan2(points.y2 - points.y1, points.x2 - points.x1);
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(1, 0);
561
- context.lineTo(-10, -5.5);
562
- context.lineTo(-10, 5.5);
563
- context.closePath();
564
- context.fill();
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
- [...tableObjects.values(), ...noteObjects.values(), ...arrowObjects.values()].forEach((object) => {
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 = { ...documentData.state, arrows: documentData.state.arrows || [] };
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
- canvas.on("mouse:dblclick", ({ target }) => {
1145
- if (target?.canvasErdType === "arrow") beginArrowEndpointEditing(target.arrowId);
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" && editingArrowId) {
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 = true;
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 = { ...state, arrows: state.arrows || [] };
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().catch((error) => showNotice(`Could not list diagrams: ${error.message}`, true));
2150
+ refreshSavedDiagrams(currentDiagramFilename)
2151
+ .catch((error) => showNotice(`Could not list diagrams: ${error.message}`, true));
1377
2152
  loadingElement.hidden = true;
1378
2153
  }
1379
2154