canvas_erd 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -0
- data/README.md +3 -1
- data/lib/canvas_erd/cli.rb +4 -1
- data/lib/canvas_erd/highlighter.rb +57 -0
- data/lib/canvas_erd/model_details.rb +105 -0
- data/lib/canvas_erd/server.rb +69 -3
- data/lib/canvas_erd/source_files.rb +70 -0
- data/lib/canvas_erd/version.rb +1 -1
- data/lib/canvas_erd/web/app.js +595 -24
- data/lib/canvas_erd/web/document.js +131 -0
- data/lib/canvas_erd/web/index.html +62 -0
- data/lib/canvas_erd/web/styles.css +89 -2
- data/lib/canvas_erd.rb +3 -0
- metadata +26 -3
data/lib/canvas_erd/web/app.js
CHANGED
|
@@ -56,6 +56,23 @@ const frameColorControl = {
|
|
|
56
56
|
swatches: document.querySelector("#frame-color-swatches"),
|
|
57
57
|
custom: document.querySelector("#frame-custom-color")
|
|
58
58
|
};
|
|
59
|
+
const codeFormattingElement = document.querySelector("#code-formatting");
|
|
60
|
+
const codeLanguageElement = document.querySelector("#code-language");
|
|
61
|
+
const codeSourceElement = document.querySelector("#code-source");
|
|
62
|
+
const codeLineNumbersButton = document.querySelector("#code-line-numbers");
|
|
63
|
+
const codeSizeElement = document.querySelector("#code-size");
|
|
64
|
+
const codeSourceFieldsElement = document.querySelector("#code-source-fields");
|
|
65
|
+
const codeSourcePathElement = document.querySelector("#code-source-path");
|
|
66
|
+
const codeSourceStartElement = document.querySelector("#code-source-start");
|
|
67
|
+
const codeSourceEndElement = document.querySelector("#code-source-end");
|
|
68
|
+
const sourceFilePathsElement = document.querySelector("#source-file-paths");
|
|
69
|
+
const sourceDialogElement = document.querySelector("#source-dialog");
|
|
70
|
+
const sourceFormElement = document.querySelector("#source-form");
|
|
71
|
+
const sourcePathElement = document.querySelector("#source-path");
|
|
72
|
+
const sourceStartElement = document.querySelector("#source-start");
|
|
73
|
+
const sourceEndElement = document.querySelector("#source-end");
|
|
74
|
+
const sourceErrorElement = document.querySelector("#source-error");
|
|
75
|
+
const sourceSubmitButton = document.querySelector("#source-submit");
|
|
59
76
|
const noteColorControls = [
|
|
60
77
|
{
|
|
61
78
|
property: "fill",
|
|
@@ -90,6 +107,8 @@ let lastCanvasPointer = null;
|
|
|
90
107
|
const tableObjects = new Map();
|
|
91
108
|
let relationshipObjects = [];
|
|
92
109
|
const noteObjects = new Map();
|
|
110
|
+
const codeObjects = new Map();
|
|
111
|
+
const codeHighlightTimers = new Map();
|
|
93
112
|
const arrowObjects = new Map();
|
|
94
113
|
const frameObjects = new Map();
|
|
95
114
|
const frameLabelObjects = new Map();
|
|
@@ -107,6 +126,10 @@ let frameMode = false;
|
|
|
107
126
|
let drawingFrame = null;
|
|
108
127
|
let framePreview = null;
|
|
109
128
|
let lastNoteFormatting = CanvasERDDocument.inheritedNoteFormatting();
|
|
129
|
+
let lastCodeLanguage = "ruby";
|
|
130
|
+
let highlighting = { foreground: "#24292f", background: "#f6f8fa", languages: [] };
|
|
131
|
+
let modelDetails = {};
|
|
132
|
+
let sourceDialogPosition = null;
|
|
110
133
|
const arrowEndpointHandles = new Map();
|
|
111
134
|
const changeTracker = CanvasERDDocument.createChangeTracker(updateSaveButton);
|
|
112
135
|
const layoutEngine = new ELK({ workerUrl: "/assets/elk-worker.min.js" });
|
|
@@ -208,7 +231,7 @@ function updateTableCaching() {
|
|
|
208
231
|
|
|
209
232
|
function updateNoteSelectionPadding() {
|
|
210
233
|
const padding = CanvasERDDocument.noteSelectionPadding(canvas.getZoom());
|
|
211
|
-
noteObjects.forEach((note) => {
|
|
234
|
+
[...noteObjects.values(), ...codeObjects.values()].forEach((note) => {
|
|
212
235
|
if (note.padding === padding) return;
|
|
213
236
|
note.padding = padding;
|
|
214
237
|
note.setCoords();
|
|
@@ -569,29 +592,372 @@ function createNoteObject(note) {
|
|
|
569
592
|
transparentCorners: false,
|
|
570
593
|
lockScalingY: true
|
|
571
594
|
}));
|
|
572
|
-
object._renderBackground =
|
|
573
|
-
if (!this.backgroundColor) return;
|
|
574
|
-
|
|
575
|
-
const dimensions = this._getNonTransformedDimensions();
|
|
576
|
-
const bounds = CanvasERDDocument.paddedBackgroundBounds(
|
|
577
|
-
dimensions.x,
|
|
578
|
-
dimensions.y,
|
|
579
|
-
CanvasERDDocument.NOTE_PADDING
|
|
580
|
-
);
|
|
581
|
-
context.fillStyle = this.backgroundColor;
|
|
582
|
-
context.fillRect(bounds.left, bounds.top, bounds.width, bounds.height);
|
|
583
|
-
this._removeShadow(context);
|
|
584
|
-
};
|
|
595
|
+
object._renderBackground = renderPaddedBackground;
|
|
585
596
|
object.canvasErdType = "note";
|
|
586
597
|
object.noteId = note.id;
|
|
587
598
|
return object;
|
|
588
599
|
}
|
|
589
600
|
|
|
601
|
+
function renderPaddedBackground(context) {
|
|
602
|
+
if (!this.backgroundColor) return;
|
|
603
|
+
|
|
604
|
+
const dimensions = this._getNonTransformedDimensions();
|
|
605
|
+
const bounds = CanvasERDDocument.paddedBackgroundBounds(
|
|
606
|
+
dimensions.x,
|
|
607
|
+
dimensions.y,
|
|
608
|
+
CanvasERDDocument.NOTE_PADDING
|
|
609
|
+
);
|
|
610
|
+
context.fillStyle = this.backgroundColor;
|
|
611
|
+
context.fillRect(bounds.left, bounds.top, bounds.width, bounds.height);
|
|
612
|
+
this._removeShadow(context);
|
|
613
|
+
}
|
|
614
|
+
|
|
590
615
|
function activeNoteObject() {
|
|
591
616
|
const active = canvas?.getActiveObject();
|
|
592
617
|
return active?.canvasErdType === "note" ? active : null;
|
|
593
618
|
}
|
|
594
619
|
|
|
620
|
+
class CodeTextbox extends fabric.Textbox {
|
|
621
|
+
_getLeftOffset() {
|
|
622
|
+
return super._getLeftOffset() + (this.gutterWidth || 0);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
_wrapText(lines, desiredWidth) {
|
|
626
|
+
const wrapped = super._wrapText(lines, desiredWidth - (this.gutterWidth || 0));
|
|
627
|
+
this.dynamicMinWidth += this.gutterWidth || 0;
|
|
628
|
+
return wrapped;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
gutterLabelAt(event) {
|
|
632
|
+
const block = findCodeBlock(this.codeBlockId);
|
|
633
|
+
if (!this.gutterWidth || !block || !this.canvas) return undefined;
|
|
634
|
+
|
|
635
|
+
const point = this.canvas.getScenePoint(event).transform(fabric.util.invertTransform(this.calcTransformMatrix()));
|
|
636
|
+
const x = point.x + this.width / 2;
|
|
637
|
+
if (x < -CanvasERDDocument.NOTE_PADDING || x > this.gutterWidth - this.fontSize * 0.3) return undefined;
|
|
638
|
+
|
|
639
|
+
const row = CanvasERDDocument.codeRowAt(
|
|
640
|
+
this._textLines.map((_, index) => this.getHeightOfLine(index)),
|
|
641
|
+
point.y - this._getTopOffset()
|
|
642
|
+
);
|
|
643
|
+
if (row < 0) return null;
|
|
644
|
+
const labels = CanvasERDDocument.codeLineLabels(block, this.text.split("\n").length);
|
|
645
|
+
return labels[this._styleMap[row].line] ?? null;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
mouseUpHandler(options) {
|
|
649
|
+
if (this.gutterLabelAt(options.e) !== undefined) return;
|
|
650
|
+
super.mouseUpHandler(options);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function toggleCodeLineHighlight(object, label, extend) {
|
|
655
|
+
const block = findCodeBlock(object.codeBlockId);
|
|
656
|
+
if (!block) return;
|
|
657
|
+
|
|
658
|
+
const labels = CanvasERDDocument.codeLineLabels(block, object.text.split("\n").length);
|
|
659
|
+
block.highlightedLines = extend && object.highlightAnchor !== undefined
|
|
660
|
+
? CanvasERDDocument.highlightLineRange(block.highlightedLines, labels, object.highlightAnchor, label)
|
|
661
|
+
: CanvasERDDocument.toggleHighlightedLine(block.highlightedLines, label);
|
|
662
|
+
object.highlightAnchor = label;
|
|
663
|
+
canvas.requestRenderAll();
|
|
664
|
+
markDirty();
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function codeGutterWidth(block, text) {
|
|
668
|
+
if (!block.showLineNumbers) return 0;
|
|
669
|
+
const labels = CanvasERDDocument.codeLineLabels(block, text.split("\n").length);
|
|
670
|
+
return CanvasERDDocument.codeGutterWidth(labels, CanvasERDDocument.codeFontSize(block));
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function renderCodeBackground(context) {
|
|
674
|
+
renderPaddedBackground.call(this, context);
|
|
675
|
+
const block = findCodeBlock(this.codeBlockId);
|
|
676
|
+
if (!block) return;
|
|
677
|
+
|
|
678
|
+
const labels = CanvasERDDocument.codeLineLabels(block, this.text.split("\n").length);
|
|
679
|
+
const highlighted = new Set(block.highlightedLines || []);
|
|
680
|
+
const dimensions = this._getNonTransformedDimensions();
|
|
681
|
+
const padding = CanvasERDDocument.NOTE_PADDING;
|
|
682
|
+
context.save();
|
|
683
|
+
if (highlighted.size > 0) {
|
|
684
|
+
let rowTop = this._getTopOffset();
|
|
685
|
+
context.fillStyle = "#fff8c5";
|
|
686
|
+
this._textLines.forEach((_, index) => {
|
|
687
|
+
const height = this.getHeightOfLine(index);
|
|
688
|
+
if (highlighted.has(labels[this._styleMap[index].line])) {
|
|
689
|
+
context.fillRect(-dimensions.x / 2 - padding, rowTop, dimensions.x + padding * 2, height);
|
|
690
|
+
}
|
|
691
|
+
rowTop += height;
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
if (!this.gutterWidth) {
|
|
695
|
+
context.restore();
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const right = -this.width / 2 + this.gutterWidth - this.fontSize * 0.6;
|
|
700
|
+
const separator = -this.width / 2 + this.gutterWidth - this.fontSize * 0.3;
|
|
701
|
+
let top = this._getTopOffset();
|
|
702
|
+
context.strokeStyle = "#d0d7de";
|
|
703
|
+
context.lineWidth = 1;
|
|
704
|
+
context.beginPath();
|
|
705
|
+
context.moveTo(separator, -dimensions.y / 2 - CanvasERDDocument.NOTE_PADDING);
|
|
706
|
+
context.lineTo(separator, dimensions.y / 2 + CanvasERDDocument.NOTE_PADDING);
|
|
707
|
+
context.stroke();
|
|
708
|
+
context.font = `${this.fontSize}px ${this.fontFamily}`;
|
|
709
|
+
context.fillStyle = "#8c959f";
|
|
710
|
+
context.textAlign = "right";
|
|
711
|
+
this._textLines.forEach((_, index) => {
|
|
712
|
+
const line = this._styleMap[index];
|
|
713
|
+
const label = line.offset === 0 ? labels[line.line] : null;
|
|
714
|
+
if (label !== null && label !== undefined) {
|
|
715
|
+
const baseline = top + this.getHeightOfLineImpl(index) - this.fontSize * this._fontSizeFraction;
|
|
716
|
+
context.fillText(String(label), right, baseline);
|
|
717
|
+
}
|
|
718
|
+
top += this.getHeightOfLine(index);
|
|
719
|
+
});
|
|
720
|
+
context.restore();
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function updateCodeGutter(object) {
|
|
724
|
+
const block = findCodeBlock(object.codeBlockId);
|
|
725
|
+
const width = block ? codeGutterWidth(block, object.text) : 0;
|
|
726
|
+
if ((object.gutterWidth || 0) === width) return;
|
|
727
|
+
object.gutterWidth = width;
|
|
728
|
+
object.initDimensions();
|
|
729
|
+
object.setCoords();
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function toggleCodeLineNumbers() {
|
|
733
|
+
const object = activeCodeObject();
|
|
734
|
+
const block = object && findCodeBlock(object.codeBlockId);
|
|
735
|
+
if (!block) return;
|
|
736
|
+
|
|
737
|
+
block.showLineNumbers = !block.showLineNumbers;
|
|
738
|
+
const previousWidth = object.gutterWidth || 0;
|
|
739
|
+
object.gutterWidth = codeGutterWidth(block, object.text);
|
|
740
|
+
object.set({ width: object.width + object.gutterWidth - previousWidth });
|
|
741
|
+
object.initDimensions();
|
|
742
|
+
object.setCoords();
|
|
743
|
+
updateObjectState(object);
|
|
744
|
+
renderArrows();
|
|
745
|
+
updateCodeFormattingControls();
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function applyCodeSize(size) {
|
|
749
|
+
const object = activeCodeObject();
|
|
750
|
+
const block = object && findCodeBlock(object.codeBlockId);
|
|
751
|
+
if (!block) return;
|
|
752
|
+
|
|
753
|
+
const previousFontSize = CanvasERDDocument.codeFontSize(block);
|
|
754
|
+
block.size = size;
|
|
755
|
+
const fontSize = CanvasERDDocument.codeFontSize(block);
|
|
756
|
+
object.set({ fontSize, width: object.width * fontSize / previousFontSize });
|
|
757
|
+
object.gutterWidth = codeGutterWidth(block, object.text);
|
|
758
|
+
object.initDimensions();
|
|
759
|
+
object.setCoords();
|
|
760
|
+
updateObjectState(object);
|
|
761
|
+
renderArrows();
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
function createCodeObject(block) {
|
|
765
|
+
const borderColor = CanvasERDDocument.noteBorderColor(highlighting.background);
|
|
766
|
+
const object = new CodeTextbox(block.code, CanvasERDDocument.uncachedText({
|
|
767
|
+
...CanvasERDDocument.codeCanvasGeometry(block),
|
|
768
|
+
gutterWidth: codeGutterWidth(block, block.code),
|
|
769
|
+
fill: highlighting.foreground,
|
|
770
|
+
backgroundColor: highlighting.background,
|
|
771
|
+
editable: !block.source,
|
|
772
|
+
padding: CanvasERDDocument.noteSelectionPadding(canvas.getZoom()),
|
|
773
|
+
borderColor,
|
|
774
|
+
editingBorderColor: borderColor,
|
|
775
|
+
cornerColor: borderColor,
|
|
776
|
+
transparentCorners: false,
|
|
777
|
+
lockScalingY: true
|
|
778
|
+
}));
|
|
779
|
+
object._renderBackground = renderCodeBackground;
|
|
780
|
+
object.canvasErdType = "code";
|
|
781
|
+
object.codeBlockId = block.id;
|
|
782
|
+
highlightCodeObject(object);
|
|
783
|
+
return object;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function findCodeBlock(id) {
|
|
787
|
+
return state.codeBlocks.find((candidate) => candidate.id === id);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function activeCodeObject() {
|
|
791
|
+
const active = canvas?.getActiveObject();
|
|
792
|
+
return active?.canvasErdType === "code" ? active : null;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function scheduleCodeHighlight(object, delay = 300) {
|
|
796
|
+
clearTimeout(codeHighlightTimers.get(object.codeBlockId));
|
|
797
|
+
codeHighlightTimers.set(object.codeBlockId, setTimeout(() => highlightCodeObject(object), delay));
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
async function highlightCodeObject(object) {
|
|
801
|
+
codeHighlightTimers.delete(object.codeBlockId);
|
|
802
|
+
const block = findCodeBlock(object.codeBlockId);
|
|
803
|
+
if (!block) return;
|
|
804
|
+
|
|
805
|
+
const code = object.text;
|
|
806
|
+
try {
|
|
807
|
+
const response = await fetch("/api/highlight", {
|
|
808
|
+
method: "POST",
|
|
809
|
+
headers: { "content-type": "application/json" },
|
|
810
|
+
body: JSON.stringify({ code, language: block.language })
|
|
811
|
+
});
|
|
812
|
+
if (!response.ok) throw new Error(await response.text());
|
|
813
|
+
|
|
814
|
+
const result = await response.json();
|
|
815
|
+
if (codeObjects.get(block.id) !== object || object.text !== code) return;
|
|
816
|
+
if (result.code !== code) {
|
|
817
|
+
if (object.isEditing) return;
|
|
818
|
+
object.set({ text: result.code });
|
|
819
|
+
block.code = result.code;
|
|
820
|
+
markDirty();
|
|
821
|
+
}
|
|
822
|
+
object.styles = CanvasERDDocument.codeTokenStyles(result.runs);
|
|
823
|
+
object.initDimensions();
|
|
824
|
+
object.setCoords();
|
|
825
|
+
canvas.requestRenderAll();
|
|
826
|
+
} catch (error) {
|
|
827
|
+
showNotice(`Could not highlight code: ${error.message}`, true);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
function updateCodeFormattingControls() {
|
|
832
|
+
const object = activeCodeObject();
|
|
833
|
+
const block = object && findCodeBlock(object.codeBlockId);
|
|
834
|
+
codeFormattingElement.hidden = !block;
|
|
835
|
+
if (!block) return;
|
|
836
|
+
|
|
837
|
+
lastCodeLanguage = block.language;
|
|
838
|
+
codeLanguageElement.value = block.language;
|
|
839
|
+
codeLineNumbersButton.setAttribute("aria-pressed", String(Boolean(block.showLineNumbers)));
|
|
840
|
+
codeSizeElement.value = block.size || "medium";
|
|
841
|
+
const fileSource = block.source?.path ? block.source : null;
|
|
842
|
+
codeSourceElement.hidden = !block.source?.model;
|
|
843
|
+
codeSourceElement.textContent = block.source?.model ? CanvasERDDocument.sourceReferenceLabel(block.source) : "";
|
|
844
|
+
codeSourceFieldsElement.hidden = !fileSource;
|
|
845
|
+
codeSourcePathElement.value = fileSource?.path || "";
|
|
846
|
+
codeSourceStartElement.value = fileSource?.startLine || "";
|
|
847
|
+
codeSourceEndElement.value = fileSource?.endLine || "";
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
async function applyCodeSource() {
|
|
851
|
+
const object = activeCodeObject();
|
|
852
|
+
const block = object && findCodeBlock(object.codeBlockId);
|
|
853
|
+
if (!block?.source?.path) return;
|
|
854
|
+
|
|
855
|
+
const source = CanvasERDDocument.sourceFromFields(
|
|
856
|
+
codeSourcePathElement.value,
|
|
857
|
+
codeSourceStartElement.value,
|
|
858
|
+
codeSourceEndElement.value
|
|
859
|
+
);
|
|
860
|
+
try {
|
|
861
|
+
if (!source) throw new Error("Enter a source file path.");
|
|
862
|
+
const result = await fetchSource(source);
|
|
863
|
+
if (source.path !== block.source.path) block.language = result.language;
|
|
864
|
+
block.source = source;
|
|
865
|
+
replaceCodeText(object, block, result.code);
|
|
866
|
+
markDirty();
|
|
867
|
+
} catch (error) {
|
|
868
|
+
showNotice(`Could not change source file: ${error.message}`, true);
|
|
869
|
+
}
|
|
870
|
+
if (activeCodeObject() === object) updateCodeFormattingControls();
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
function replaceCodeText(object, block, code) {
|
|
874
|
+
block.code = code;
|
|
875
|
+
object.set({ text: code });
|
|
876
|
+
object.styles = {};
|
|
877
|
+
updateCodeGutter(object);
|
|
878
|
+
object.setCoords();
|
|
879
|
+
renderArrows();
|
|
880
|
+
highlightCodeObject(object);
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
async function loadSourceFilePaths() {
|
|
884
|
+
try {
|
|
885
|
+
const response = await fetch("/api/source_files", { cache: "no-store" });
|
|
886
|
+
if (!response.ok) throw new Error(await response.text());
|
|
887
|
+
|
|
888
|
+
const result = await response.json();
|
|
889
|
+
sourceFilePathsElement.replaceChildren(...result.paths.map((path) => {
|
|
890
|
+
const option = document.createElement("option");
|
|
891
|
+
option.value = path;
|
|
892
|
+
return option;
|
|
893
|
+
}));
|
|
894
|
+
} catch (error) {
|
|
895
|
+
showNotice(`Could not list source files: ${error.message}`, true);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function renderCodeLanguages() {
|
|
900
|
+
codeLanguageElement.replaceChildren(...highlighting.languages.map((language) => {
|
|
901
|
+
const option = document.createElement("option");
|
|
902
|
+
option.value = language.tag;
|
|
903
|
+
option.textContent = language.title;
|
|
904
|
+
return option;
|
|
905
|
+
}));
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
function applyCodeLanguage(language) {
|
|
909
|
+
const object = activeCodeObject();
|
|
910
|
+
const block = object && findCodeBlock(object.codeBlockId);
|
|
911
|
+
if (!block) return;
|
|
912
|
+
|
|
913
|
+
block.language = language;
|
|
914
|
+
lastCodeLanguage = language;
|
|
915
|
+
highlightCodeObject(object);
|
|
916
|
+
markDirty();
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
function renderCodeBlocks() {
|
|
920
|
+
state.codeBlocks.forEach((block) => {
|
|
921
|
+
const object = createCodeObject(block);
|
|
922
|
+
codeObjects.set(block.id, object);
|
|
923
|
+
canvas.add(object);
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
function sourceUrl(source) {
|
|
928
|
+
if (source.model) {
|
|
929
|
+
return `/api/model_detail?${new URLSearchParams({ model: source.model, detail: source.detail })}`;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
const query = new URLSearchParams({ path: source.path });
|
|
933
|
+
if (source.startLine) query.set("start", source.startLine);
|
|
934
|
+
if (source.endLine) query.set("end", source.endLine);
|
|
935
|
+
return `/api/source?${query}`;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
async function fetchSource(source) {
|
|
939
|
+
const response = await fetch(sourceUrl(source), { cache: "no-store" });
|
|
940
|
+
if (!response.ok) throw new Error(await response.text());
|
|
941
|
+
return response.json();
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
async function refreshSourceCodeBlocks() {
|
|
945
|
+
const failures = [];
|
|
946
|
+
await Promise.all(state.codeBlocks.filter((block) => block.source).map(async (block) => {
|
|
947
|
+
try {
|
|
948
|
+
const result = await fetchSource(block.source);
|
|
949
|
+
const object = codeObjects.get(block.id);
|
|
950
|
+
if (result.lineNumbers) block.lineNumbers = result.lineNumbers;
|
|
951
|
+
canvas.requestRenderAll();
|
|
952
|
+
if (result.code === block.code || !object) return;
|
|
953
|
+
replaceCodeText(object, block, result.code);
|
|
954
|
+
} catch (error) {
|
|
955
|
+
failures.push(`${CanvasERDDocument.sourceReferenceLabel(block.source)}: ${error.message}`);
|
|
956
|
+
}
|
|
957
|
+
}));
|
|
958
|
+
if (failures.length > 0) showNotice(`Could not refresh source files: ${failures.join("; ")}`, true);
|
|
959
|
+
}
|
|
960
|
+
|
|
595
961
|
function activeTableEntityIds() {
|
|
596
962
|
return CanvasERDDocument.selectedEntityIds(canvas?.getActiveObject());
|
|
597
963
|
}
|
|
@@ -980,6 +1346,7 @@ function setFrameMode(active) {
|
|
|
980
1346
|
...frameLabelObjects.values(),
|
|
981
1347
|
...tableObjects.values(),
|
|
982
1348
|
...noteObjects.values(),
|
|
1349
|
+
...codeObjects.values(),
|
|
983
1350
|
...arrowObjects.values()
|
|
984
1351
|
].forEach((object) => {
|
|
985
1352
|
object.selectable = !active;
|
|
@@ -1030,6 +1397,7 @@ function attachmentTarget(attachment) {
|
|
|
1030
1397
|
if (!attachment) return null;
|
|
1031
1398
|
if (attachment.type === "entity") return tableObjects.get(attachment.id);
|
|
1032
1399
|
if (attachment.type === "note") return noteObjects.get(attachment.id);
|
|
1400
|
+
if (attachment.type === "code") return codeObjects.get(attachment.id);
|
|
1033
1401
|
return null;
|
|
1034
1402
|
}
|
|
1035
1403
|
|
|
@@ -1044,7 +1412,7 @@ function resolveArrowEndpoint(endpoint) {
|
|
|
1044
1412
|
}
|
|
1045
1413
|
|
|
1046
1414
|
function attachableTargetAt(point) {
|
|
1047
|
-
const objects = [...noteObjects.values(), ...tableObjects.values()].reverse();
|
|
1415
|
+
const objects = [...noteObjects.values(), ...codeObjects.values(), ...tableObjects.values()].reverse();
|
|
1048
1416
|
return objects.find((object) => {
|
|
1049
1417
|
const bounds = object.getBoundingRect();
|
|
1050
1418
|
return point.x >= bounds.left && point.x <= bounds.left + bounds.width &&
|
|
@@ -1220,6 +1588,7 @@ function setArrowMode(active) {
|
|
|
1220
1588
|
...frameLabelObjects.values(),
|
|
1221
1589
|
...tableObjects.values(),
|
|
1222
1590
|
...noteObjects.values(),
|
|
1591
|
+
...codeObjects.values(),
|
|
1223
1592
|
...arrowObjects.values()
|
|
1224
1593
|
].forEach((object) => {
|
|
1225
1594
|
object.selectable = !active;
|
|
@@ -1395,6 +1764,117 @@ function addNote(position = canvas.getVpCenter()) {
|
|
|
1395
1764
|
markDirty();
|
|
1396
1765
|
}
|
|
1397
1766
|
|
|
1767
|
+
function insertCodeBlock(origin, block) {
|
|
1768
|
+
if (frameMode) cancelFrameDrawing();
|
|
1769
|
+
if (arrowMode) cancelArrowDrawing();
|
|
1770
|
+
if (editingArrowId) finishArrowEndpointEditing();
|
|
1771
|
+
Object.assign(block, {
|
|
1772
|
+
id: globalThis.crypto && crypto.randomUUID ? crypto.randomUUID() : `code-${Date.now()}`,
|
|
1773
|
+
x: origin.x,
|
|
1774
|
+
y: origin.y,
|
|
1775
|
+
angle: 0
|
|
1776
|
+
});
|
|
1777
|
+
state.codeBlocks.push(block);
|
|
1778
|
+
|
|
1779
|
+
const object = createCodeObject(block);
|
|
1780
|
+
codeObjects.set(block.id, object);
|
|
1781
|
+
updateModelDetailToggles();
|
|
1782
|
+
canvas.add(object);
|
|
1783
|
+
restackCanvasObjects();
|
|
1784
|
+
canvas.setActiveObject(object);
|
|
1785
|
+
updateCodeFormattingControls();
|
|
1786
|
+
canvas.requestRenderAll();
|
|
1787
|
+
markDirty();
|
|
1788
|
+
return object;
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
function addCodeBlock(position = canvas.getVpCenter()) {
|
|
1792
|
+
const object = insertCodeBlock(CanvasERDDocument.notePositionAt(position), {
|
|
1793
|
+
code: "# Double-click to edit",
|
|
1794
|
+
width: 360,
|
|
1795
|
+
language: lastCodeLanguage,
|
|
1796
|
+
source: null
|
|
1797
|
+
});
|
|
1798
|
+
object.enterEditing();
|
|
1799
|
+
object.selectAll();
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
function openSourceDialog(position = canvas.getVpCenter()) {
|
|
1803
|
+
sourceDialogPosition = position;
|
|
1804
|
+
sourceFormElement.reset();
|
|
1805
|
+
sourceErrorElement.hidden = true;
|
|
1806
|
+
sourceSubmitButton.disabled = false;
|
|
1807
|
+
sourceDialogElement.showModal();
|
|
1808
|
+
sourcePathElement.focus();
|
|
1809
|
+
loadSourceFilePaths();
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
async function addSourceCodeBlock(event) {
|
|
1813
|
+
event.preventDefault();
|
|
1814
|
+
const source = CanvasERDDocument.sourceFromFields(
|
|
1815
|
+
sourcePathElement.value,
|
|
1816
|
+
sourceStartElement.value,
|
|
1817
|
+
sourceEndElement.value
|
|
1818
|
+
);
|
|
1819
|
+
if (!source) return;
|
|
1820
|
+
|
|
1821
|
+
sourceSubmitButton.disabled = true;
|
|
1822
|
+
try {
|
|
1823
|
+
const result = await fetchSource(source);
|
|
1824
|
+
sourceDialogElement.close();
|
|
1825
|
+
insertCodeBlock(
|
|
1826
|
+
CanvasERDDocument.notePositionAt(sourceDialogPosition),
|
|
1827
|
+
{ code: result.code, width: 520, language: result.language, source }
|
|
1828
|
+
);
|
|
1829
|
+
} catch (error) {
|
|
1830
|
+
sourceErrorElement.textContent = error.message;
|
|
1831
|
+
sourceErrorElement.hidden = false;
|
|
1832
|
+
} finally {
|
|
1833
|
+
sourceSubmitButton.disabled = false;
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
async function toggleModelDetail(entityId, detail) {
|
|
1838
|
+
const existing = CanvasERDDocument.modelDetailBlock(state.codeBlocks, entityId, detail);
|
|
1839
|
+
if (existing) {
|
|
1840
|
+
state.arrows = state.arrows.filter((arrow) => (
|
|
1841
|
+
![arrow.start, arrow.end].some((endpoint) => endpoint.attachment?.type === "code" && endpoint.attachment.id === existing.id)
|
|
1842
|
+
));
|
|
1843
|
+
removeCodeBlock(existing.id);
|
|
1844
|
+
return;
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
const source = { model: entityId, detail };
|
|
1848
|
+
try {
|
|
1849
|
+
const result = await fetchSource(source);
|
|
1850
|
+
const origin = CanvasERDDocument.modelDetailOrigin(state.positions[entityId]);
|
|
1851
|
+
const block = {
|
|
1852
|
+
code: result.code,
|
|
1853
|
+
width: 520,
|
|
1854
|
+
language: result.language,
|
|
1855
|
+
lineNumbers: result.lineNumbers,
|
|
1856
|
+
source
|
|
1857
|
+
};
|
|
1858
|
+
insertCodeBlock(origin, block);
|
|
1859
|
+
state.arrows.push({
|
|
1860
|
+
id: globalThis.crypto && crypto.randomUUID ? crypto.randomUUID() : `arrow-${Date.now()}`,
|
|
1861
|
+
start: { x: 0, y: 0, attachment: { type: "entity", id: entityId, x: 1, y: 0.5 } },
|
|
1862
|
+
end: { x: 0, y: 0, attachment: { type: "code", id: block.id, x: 0, y: 0.5 } },
|
|
1863
|
+
...CanvasERDDocument.DEFAULT_ARROW_FORMATTING
|
|
1864
|
+
});
|
|
1865
|
+
renderArrows();
|
|
1866
|
+
} catch (error) {
|
|
1867
|
+
showNotice(`Could not show ${detail} for ${entityId}: ${error.message}`, true);
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
function updateModelDetailToggles() {
|
|
1872
|
+
tableListElement.querySelectorAll("[data-detail]").forEach((button) => {
|
|
1873
|
+
const block = CanvasERDDocument.modelDetailBlock(state.codeBlocks, button.dataset.entityId, button.dataset.detail);
|
|
1874
|
+
button.setAttribute("aria-pressed", String(Boolean(block)));
|
|
1875
|
+
});
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1398
1878
|
function updateObjectState(object) {
|
|
1399
1879
|
if (object.canvasErdType === "entity") {
|
|
1400
1880
|
state.positions[object.entityId] = { x: object.left, y: object.top };
|
|
@@ -1404,6 +1884,11 @@ function updateObjectState(object) {
|
|
|
1404
1884
|
if (!note) return;
|
|
1405
1885
|
Object.assign(note, CanvasERDDocument.noteStateGeometry(object));
|
|
1406
1886
|
scheduleRelationshipRender();
|
|
1887
|
+
} else if (object.canvasErdType === "code") {
|
|
1888
|
+
const block = findCodeBlock(object.codeBlockId);
|
|
1889
|
+
if (!block) return;
|
|
1890
|
+
Object.assign(block, CanvasERDDocument.codeStateGeometry(object));
|
|
1891
|
+
scheduleRelationshipRender();
|
|
1407
1892
|
} else if (object.canvasErdType === "frame") {
|
|
1408
1893
|
const frame = state.frames.find((candidate) => candidate.id === object.frameId);
|
|
1409
1894
|
if (!frame) return;
|
|
@@ -1439,19 +1924,45 @@ function removeActiveFrame() {
|
|
|
1439
1924
|
return true;
|
|
1440
1925
|
}
|
|
1441
1926
|
|
|
1442
|
-
function
|
|
1443
|
-
const active = canvas.getActiveObject();
|
|
1444
|
-
if (!active || active.canvasErdType !== "note" || active.isEditing) return false;
|
|
1445
|
-
|
|
1927
|
+
function detachArrows(type, id) {
|
|
1446
1928
|
state.arrows.forEach((arrow) => {
|
|
1447
1929
|
[arrow.start, arrow.end].forEach((endpoint) => {
|
|
1448
|
-
if (endpoint.attachment?.type !==
|
|
1930
|
+
if (endpoint.attachment?.type !== type || endpoint.attachment.id !== id) return;
|
|
1449
1931
|
const point = resolveArrowEndpoint(endpoint);
|
|
1450
1932
|
endpoint.x = point.x;
|
|
1451
1933
|
endpoint.y = point.y;
|
|
1452
1934
|
endpoint.attachment = null;
|
|
1453
1935
|
});
|
|
1454
1936
|
});
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
function removeActiveCodeBlock() {
|
|
1940
|
+
const active = canvas.getActiveObject();
|
|
1941
|
+
if (!active || active.canvasErdType !== "code" || active.isEditing) return false;
|
|
1942
|
+
|
|
1943
|
+
detachArrows("code", active.codeBlockId);
|
|
1944
|
+
removeCodeBlock(active.codeBlockId);
|
|
1945
|
+
return true;
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
function removeCodeBlock(id) {
|
|
1949
|
+
const object = codeObjects.get(id);
|
|
1950
|
+
clearTimeout(codeHighlightTimers.get(id));
|
|
1951
|
+
codeHighlightTimers.delete(id);
|
|
1952
|
+
if (canvas.getActiveObject() === object) canvas.discardActiveObject();
|
|
1953
|
+
canvas.remove(object);
|
|
1954
|
+
codeObjects.delete(id);
|
|
1955
|
+
state.codeBlocks = state.codeBlocks.filter((block) => block.id !== id);
|
|
1956
|
+
renderArrows();
|
|
1957
|
+
updateModelDetailToggles();
|
|
1958
|
+
markDirty();
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
function removeActiveNote() {
|
|
1962
|
+
const active = canvas.getActiveObject();
|
|
1963
|
+
if (!active || active.canvasErdType !== "note" || active.isEditing) return false;
|
|
1964
|
+
|
|
1965
|
+
detachArrows("note", active.noteId);
|
|
1455
1966
|
canvas.discardActiveObject();
|
|
1456
1967
|
canvas.remove(active);
|
|
1457
1968
|
noteObjects.delete(active.noteId);
|
|
@@ -1512,9 +2023,24 @@ function renderTableList() {
|
|
|
1512
2023
|
const text = document.createElement("span");
|
|
1513
2024
|
text.textContent = entity.label;
|
|
1514
2025
|
row.append(checkbox, text);
|
|
2026
|
+
Object.entries(modelDetails[entity.id] || {}).forEach(([detail, count]) => {
|
|
2027
|
+
const button = document.createElement("button");
|
|
2028
|
+
button.type = "button";
|
|
2029
|
+
button.className = "detail-toggle";
|
|
2030
|
+
button.dataset.entityId = entity.id;
|
|
2031
|
+
button.dataset.detail = detail;
|
|
2032
|
+
button.textContent = detail;
|
|
2033
|
+
button.title = `Show ${detail} (${count})`;
|
|
2034
|
+
button.addEventListener("click", (event) => {
|
|
2035
|
+
event.preventDefault();
|
|
2036
|
+
toggleModelDetail(entity.id, detail);
|
|
2037
|
+
});
|
|
2038
|
+
row.append(button);
|
|
2039
|
+
});
|
|
1515
2040
|
fragment.append(row);
|
|
1516
2041
|
});
|
|
1517
2042
|
tableListElement.replaceChildren(fragment);
|
|
2043
|
+
updateModelDetailToggles();
|
|
1518
2044
|
}
|
|
1519
2045
|
|
|
1520
2046
|
function filterTableList() {
|
|
@@ -1551,6 +2077,7 @@ async function refreshSchema() {
|
|
|
1551
2077
|
applicationSchema = schema;
|
|
1552
2078
|
markDirty();
|
|
1553
2079
|
showNotice(CanvasERDDocument.changeSummary(result.changes));
|
|
2080
|
+
await refreshSourceCodeBlocks();
|
|
1554
2081
|
} catch (error) {
|
|
1555
2082
|
showNotice(`Could not refresh schema: ${error.message}`, true);
|
|
1556
2083
|
} finally {
|
|
@@ -1672,6 +2199,7 @@ function displayDocument(documentData) {
|
|
|
1672
2199
|
...documentData.state,
|
|
1673
2200
|
arrows: documentData.state.arrows || [],
|
|
1674
2201
|
frames: documentData.state.frames || [],
|
|
2202
|
+
codeBlocks: documentData.state.codeBlocks || [],
|
|
1675
2203
|
tableHeaderColors: documentData.state.tableHeaderColors || {}
|
|
1676
2204
|
};
|
|
1677
2205
|
if (frameMode) cancelFrameDrawing();
|
|
@@ -1682,6 +2210,7 @@ function displayDocument(documentData) {
|
|
|
1682
2210
|
tableObjects.clear();
|
|
1683
2211
|
relationshipObjects = [];
|
|
1684
2212
|
noteObjects.clear();
|
|
2213
|
+
codeObjects.clear();
|
|
1685
2214
|
arrowObjects.clear();
|
|
1686
2215
|
frameObjects.clear();
|
|
1687
2216
|
frameLabelObjects.clear();
|
|
@@ -1689,6 +2218,7 @@ function displayDocument(documentData) {
|
|
|
1689
2218
|
renderFrames();
|
|
1690
2219
|
renderTables();
|
|
1691
2220
|
renderNotes();
|
|
2221
|
+
renderCodeBlocks();
|
|
1692
2222
|
renderArrows();
|
|
1693
2223
|
|
|
1694
2224
|
if (Array.isArray(state.viewportTransform) && state.viewportTransform.length === 6) {
|
|
@@ -1756,6 +2286,7 @@ function diagramBounds() {
|
|
|
1756
2286
|
...frameLabelObjects.values(),
|
|
1757
2287
|
...tableObjects.values(),
|
|
1758
2288
|
...noteObjects.values(),
|
|
2289
|
+
...codeObjects.values(),
|
|
1759
2290
|
...arrowObjects.values(),
|
|
1760
2291
|
...relationshipObjects
|
|
1761
2292
|
];
|
|
@@ -1803,6 +2334,7 @@ function configureCanvasEvents() {
|
|
|
1803
2334
|
updateNoteFormattingControls();
|
|
1804
2335
|
updateTableFormattingControls();
|
|
1805
2336
|
updateFrameFormattingControls();
|
|
2337
|
+
updateCodeFormattingControls();
|
|
1806
2338
|
};
|
|
1807
2339
|
canvas.on("selection:created", selectionChanged);
|
|
1808
2340
|
canvas.on("selection:updated", selectionChanged);
|
|
@@ -1830,6 +2362,16 @@ function configureCanvasEvents() {
|
|
|
1830
2362
|
canvas.on("mouse:up", (eventData) => {
|
|
1831
2363
|
if (arrowMode && drawingArrow) finishArrowDrawing(eventData);
|
|
1832
2364
|
});
|
|
2365
|
+
canvas.on("mouse:up", ({ target, e, isClick }) => {
|
|
2366
|
+
if (!isClick || target?.canvasErdType !== "code") return;
|
|
2367
|
+
const label = target.gutterLabelAt(e);
|
|
2368
|
+
if (label !== null && label !== undefined) toggleCodeLineHighlight(target, label, e.shiftKey);
|
|
2369
|
+
});
|
|
2370
|
+
canvas.on("mouse:move", ({ target, e }) => {
|
|
2371
|
+
if (target?.canvasErdType !== "code") return;
|
|
2372
|
+
const label = target.gutterLabelAt(e);
|
|
2373
|
+
target.hoverCursor = label !== null && label !== undefined ? "pointer" : null;
|
|
2374
|
+
});
|
|
1833
2375
|
canvas.on("mouse:down", ({ e, scenePoint }) => {
|
|
1834
2376
|
if (!frameMode || spacePressed) return;
|
|
1835
2377
|
const point = scenePoint || canvas.getScenePoint(e);
|
|
@@ -1860,7 +2402,14 @@ function configureCanvasEvents() {
|
|
|
1860
2402
|
else if (eventData.target.canvasErdType === "arrow-endpoint") updateArrowEndpointHandle(eventData);
|
|
1861
2403
|
else updateObjectState(eventData.target);
|
|
1862
2404
|
});
|
|
1863
|
-
canvas.on("text:changed", ({ target }) =>
|
|
2405
|
+
canvas.on("text:changed", ({ target }) => {
|
|
2406
|
+
if (target.canvasErdType === "code") updateCodeGutter(target);
|
|
2407
|
+
updateObjectState(target);
|
|
2408
|
+
if (target.canvasErdType === "code") scheduleCodeHighlight(target);
|
|
2409
|
+
});
|
|
2410
|
+
canvas.on("text:editing:exited", ({ target }) => {
|
|
2411
|
+
if (target.canvasErdType === "code") scheduleCodeHighlight(target, 0);
|
|
2412
|
+
});
|
|
1864
2413
|
|
|
1865
2414
|
canvas.on("mouse:wheel", ({ e }) => {
|
|
1866
2415
|
if (e.ctrlKey) {
|
|
@@ -1915,7 +2464,7 @@ function configureCanvasEvents() {
|
|
|
1915
2464
|
const acceptsText = ["INPUT", "TEXTAREA", "SELECT"].includes(event.target.tagName) || event.target.isContentEditable;
|
|
1916
2465
|
const noteFormatting = CanvasERDDocument.noteFormattingShortcut(event);
|
|
1917
2466
|
const action = CanvasERDDocument.shortcutAction(event);
|
|
1918
|
-
const bareAction = ["addNote", "arrow", "toggleTables", "layoutTables", "zoomIn", "zoomOut", "fit", "help"].includes(action);
|
|
2467
|
+
const bareAction = ["addNote", "addCode", "arrow", "toggleTables", "layoutTables", "zoomIn", "zoomOut", "fit", "help"].includes(action);
|
|
1919
2468
|
|
|
1920
2469
|
if (noteFormatting && activeNoteObject() && (!acceptsText || editing)) {
|
|
1921
2470
|
event.preventDefault();
|
|
@@ -1944,6 +2493,8 @@ function configureCanvasEvents() {
|
|
|
1944
2493
|
newDiagram();
|
|
1945
2494
|
} else if (action === "addNote") {
|
|
1946
2495
|
addNote(lastCanvasPointer || canvas.getVpCenter());
|
|
2496
|
+
} else if (action === "addCode") {
|
|
2497
|
+
addCodeBlock(lastCanvasPointer || canvas.getVpCenter());
|
|
1947
2498
|
} else if (action === "arrow") {
|
|
1948
2499
|
if (arrowMode) cancelArrowDrawing();
|
|
1949
2500
|
else setArrowMode(true);
|
|
@@ -1982,8 +2533,8 @@ function configureCanvasEvents() {
|
|
|
1982
2533
|
spacePressed = true;
|
|
1983
2534
|
canvas.skipTargetFind = true;
|
|
1984
2535
|
canvas.setCursor("grab");
|
|
1985
|
-
} else if (["Delete", "Backspace"].includes(event.key) && (
|
|
1986
|
-
removeActiveTables() || removeActiveArrows() || removeActiveFrame() || removeActiveNote()
|
|
2536
|
+
} else if (["Delete", "Backspace"].includes(event.key) && !acceptsText && (
|
|
2537
|
+
removeActiveTables() || removeActiveArrows() || removeActiveFrame() || removeActiveNote() || removeActiveCodeBlock()
|
|
1987
2538
|
)) {
|
|
1988
2539
|
event.preventDefault();
|
|
1989
2540
|
}
|
|
@@ -2044,6 +2595,17 @@ function configureControls() {
|
|
|
2044
2595
|
await loadDiagram(filename);
|
|
2045
2596
|
});
|
|
2046
2597
|
document.querySelector("#add-note").addEventListener("click", () => addNote());
|
|
2598
|
+
document.querySelector("#add-code").addEventListener("click", () => addCodeBlock());
|
|
2599
|
+
document.querySelector("#add-source").addEventListener("click", () => openSourceDialog());
|
|
2600
|
+
sourceFormElement.addEventListener("submit", addSourceCodeBlock);
|
|
2601
|
+
document.querySelector("#source-cancel").addEventListener("click", () => sourceDialogElement.close());
|
|
2602
|
+
codeSourcePathElement.addEventListener("focus", loadSourceFilePaths);
|
|
2603
|
+
[codeSourcePathElement, codeSourceStartElement, codeSourceEndElement].forEach((element) => {
|
|
2604
|
+
element.addEventListener("change", applyCodeSource);
|
|
2605
|
+
});
|
|
2606
|
+
codeLanguageElement.addEventListener("change", () => applyCodeLanguage(codeLanguageElement.value));
|
|
2607
|
+
codeLineNumbersButton.addEventListener("click", toggleCodeLineNumbers);
|
|
2608
|
+
codeSizeElement.addEventListener("change", () => applyCodeSize(codeSizeElement.value));
|
|
2047
2609
|
layoutTablesButton.addEventListener("click", layoutTables);
|
|
2048
2610
|
document.querySelector("#zoom-in").addEventListener("click", () => zoom(1.2));
|
|
2049
2611
|
document.querySelector("#zoom-out").addEventListener("click", () => zoom(1 / 1.2));
|
|
@@ -2121,6 +2683,7 @@ function initializeEditor(loadedDocument) {
|
|
|
2121
2683
|
...state,
|
|
2122
2684
|
arrows: state.arrows || [],
|
|
2123
2685
|
frames: state.frames || [],
|
|
2686
|
+
codeBlocks: state.codeBlocks || [],
|
|
2124
2687
|
tableHeaderColors: state.tableHeaderColors || {}
|
|
2125
2688
|
};
|
|
2126
2689
|
setDiagramName(loadedDocument.name);
|
|
@@ -2139,6 +2702,7 @@ function initializeEditor(loadedDocument) {
|
|
|
2139
2702
|
renderFrames();
|
|
2140
2703
|
renderTables();
|
|
2141
2704
|
renderNotes();
|
|
2705
|
+
renderCodeBlocks();
|
|
2142
2706
|
renderArrows();
|
|
2143
2707
|
if (Array.isArray(state.viewportTransform) && state.viewportTransform.length === 6) {
|
|
2144
2708
|
canvas.setViewportTransform(state.viewportTransform);
|
|
@@ -2159,6 +2723,13 @@ fetch("/api/document", { cache: "no-store" })
|
|
|
2159
2723
|
})
|
|
2160
2724
|
.then(async (loadedDocument) => {
|
|
2161
2725
|
if (!loadedDocument.state) loadedDocument.state = await createInitialState(loadedDocument.schema);
|
|
2726
|
+
const response = await fetch("/api/highlighting", { cache: "no-store" });
|
|
2727
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
2728
|
+
highlighting = await response.json();
|
|
2729
|
+
renderCodeLanguages();
|
|
2730
|
+
const detailsResponse = await fetch("/api/model_details", { cache: "no-store" });
|
|
2731
|
+
if (!detailsResponse.ok) throw new Error(`HTTP ${detailsResponse.status}`);
|
|
2732
|
+
modelDetails = await detailsResponse.json();
|
|
2162
2733
|
return loadedDocument;
|
|
2163
2734
|
})
|
|
2164
2735
|
.then(initializeEditor)
|