@kabel-project/kabel 1.0.7
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.
- package/(1.0.7)kabel.md +18 -0
- package/README.md +96 -0
- package/_READ_ME_MEDIA_/documentation/docs.md +293 -0
- package/_READ_ME_MEDIA_/workspace.png +0 -0
- package/comment-renderer/renderer.ts +228 -0
- package/controllers/base.ts +186 -0
- package/controllers/wasd.ts +132 -0
- package/docs/README.md +98 -0
- package/docs/_media/docs.md +289 -0
- package/docs/_media/index.html +168 -0
- package/docs/_media/workspace.png +0 -0
- package/docs/classes/CommentModel.md +271 -0
- package/docs/classes/CommentRenderer.md +457 -0
- package/docs/classes/ConnectableField.md +597 -0
- package/docs/classes/Connection.md +191 -0
- package/docs/classes/ContextMenuHTML.md +163 -0
- package/docs/classes/Coordinates.md +187 -0
- package/docs/classes/DropdownContainer.md +300 -0
- package/docs/classes/DummyField.md +393 -0
- package/docs/classes/Eventer.md +185 -0
- package/docs/classes/Field.md +461 -0
- package/docs/classes/InjectMsg.md +85 -0
- package/docs/classes/NodeSvg.md +1011 -0
- package/docs/classes/NumberField.md +559 -0
- package/docs/classes/OptConnectField.md +624 -0
- package/docs/classes/Renderer.md +1636 -0
- package/docs/classes/RendererConstants.md +343 -0
- package/docs/classes/Representer.md +95 -0
- package/docs/classes/RepresenterNode.md +175 -0
- package/docs/classes/TextField.md +559 -0
- package/docs/classes/Toolbox.md +172 -0
- package/docs/classes/WASDController.md +616 -0
- package/docs/classes/Widget.md +195 -0
- package/docs/classes/WorkspaceController.md +385 -0
- package/docs/classes/WorkspaceCoords.md +218 -0
- package/docs/classes/WorkspaceSvg.md +1380 -0
- package/docs/functions/clearMainWorkspace.md +20 -0
- package/docs/functions/getMainWorkspace.md +19 -0
- package/docs/functions/inject.md +35 -0
- package/docs/functions/setMainWorkspace.md +28 -0
- package/docs/globals.md +95 -0
- package/docs/interfaces/ColorStyle.md +43 -0
- package/docs/interfaces/ConnectorToFrom.md +57 -0
- package/docs/interfaces/DrawState.md +81 -0
- package/docs/interfaces/FieldConnectionData.md +25 -0
- package/docs/interfaces/FieldOptions.md +63 -0
- package/docs/interfaces/FieldRawBoxData.md +25 -0
- package/docs/interfaces/FieldVisualInfo.md +65 -0
- package/docs/interfaces/GridOptions.md +61 -0
- package/docs/interfaces/InjectOptions.md +133 -0
- package/docs/interfaces/InputFieldJson.md +50 -0
- package/docs/interfaces/KabelCommentRendering.md +31 -0
- package/docs/interfaces/KabelInterface.md +469 -0
- package/docs/interfaces/KabelNodeRendering.md +77 -0
- package/docs/interfaces/KabelUIX.md +105 -0
- package/docs/interfaces/KabelUtils.md +215 -0
- package/docs/interfaces/NodeEvents.md +42 -0
- package/docs/interfaces/NodeJson.md +104 -0
- package/docs/interfaces/NodePrototype.md +82 -0
- package/docs/interfaces/RegisteredEl.md +53 -0
- package/docs/interfaces/SerializedNode.md +128 -0
- package/docs/interfaces/TblxCategoryStruct.md +41 -0
- package/docs/interfaces/TblxFieldStruct.md +28 -0
- package/docs/interfaces/TblxNodeStruct.md +35 -0
- package/docs/interfaces/WidgetOptions.md +115 -0
- package/docs/interfaces/WidgetPrototypeList.md +15 -0
- package/docs/type-aliases/AnyField.md +13 -0
- package/docs/type-aliases/AnyFieldCls.md +13 -0
- package/docs/type-aliases/Color.md +13 -0
- package/docs/type-aliases/Connectable.md +13 -0
- package/docs/type-aliases/EventArgs.md +11 -0
- package/docs/type-aliases/EventSetupFn.md +25 -0
- package/docs/type-aliases/Hex.md +13 -0
- package/docs/type-aliases/RGBObject.md +37 -0
- package/docs/type-aliases/RGBString.md +13 -0
- package/docs/type-aliases/RGBTuple.md +13 -0
- package/docs/type-aliases/TblxObjStruct.md +52 -0
- package/docs/variables/CategoryColors.md +29 -0
- package/docs/variables/FieldMap.md +41 -0
- package/docs/variables/NodePrototypes.md +18 -0
- package/docs/variables/default.md +11 -0
- package/events/comment-drag-handle.ts +61 -0
- package/events/comment-input.ts +291 -0
- package/events/connection-line.ts +68 -0
- package/events/connector.ts +116 -0
- package/events/draggable.ts +119 -0
- package/events/events.ts +7 -0
- package/events/input-box.ts +213 -0
- package/events/node-x-btn.ts +25 -0
- package/index.d.ts +4 -0
- package/package.json +49 -0
- package/renderers/apollo/apollo.ts +21 -0
- package/renderers/apollo/constants.ts +40 -0
- package/renderers/apollo/renderer.ts +331 -0
- package/renderers/atlas/atlas.ts +15 -0
- package/renderers/constants.ts +87 -0
- package/renderers/renderer.ts +1288 -0
- package/renderers/representer-node.ts +52 -0
- package/renderers/representer.ts +25 -0
- package/src/category.ts +107 -0
- package/src/colors.ts +20 -0
- package/src/comment.ts +142 -0
- package/src/connection.ts +114 -0
- package/src/context-menu.ts +194 -0
- package/src/coordinates.ts +74 -0
- package/src/core.ts +202 -0
- package/src/ctx-menu-registry.ts +143 -0
- package/src/dropdown-menu.ts +215 -0
- package/src/field.ts +595 -0
- package/src/flyout.ts +165 -0
- package/src/fonts-manager.ts +38 -0
- package/src/grid.ts +162 -0
- package/src/headless-node.ts +27 -0
- package/src/index.ts +115 -0
- package/src/inject-headless.ts +18 -0
- package/src/inject.ts +213 -0
- package/src/main-workspace.ts +51 -0
- package/src/mutator.ts +40 -0
- package/src/node-types.ts +27 -0
- package/src/nodesvg.ts +756 -0
- package/src/prototypes.ts +9 -0
- package/src/renderer-map.ts +86 -0
- package/src/styles.css +224 -0
- package/src/toolbox.ts +125 -0
- package/src/types.ts +205 -0
- package/src/undo-redo.ts +87 -0
- package/src/visual-types.ts +29 -0
- package/src/widget-prototypes.ts +11 -0
- package/src/widget.ts +139 -0
- package/src/workspace-coords.ts +14 -0
- package/src/workspace-svg.ts +736 -0
- package/src/workspace.ts +155 -0
- package/test-server.js +61 -0
- package/themes/dark.ts +32 -0
- package/themes/default.ts +28 -0
- package/themes/themes.ts +9 -0
- package/tsconfig.json +25 -0
- package/typedoc.json +10 -0
- package/util/emitter.ts +33 -0
- package/util/env.ts +11 -0
- package/util/escape-html.ts +22 -0
- package/util/eventer.ts +108 -0
- package/util/has-prop.ts +4 -0
- package/util/parse-color.ts +42 -0
- package/util/path.ts +99 -0
- package/util/styler.ts +41 -0
- package/util/uid.ts +184 -0
- package/util/unescape-html.ts +22 -0
- package/util/user-state.ts +68 -0
- package/util/wait-anim-frames.ts +24 -0
- package/util/window-listeners.ts +62 -0
- package/webpack.config.js +80 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import NodeSvg from '../src/nodesvg';
|
|
2
|
+
import WorkspaceSvg from '../src/workspace-svg';
|
|
3
|
+
import { Element } from '@svgdotjs/svg.js';
|
|
4
|
+
import Renderer, { DrawState } from './renderer';
|
|
5
|
+
import RendererConstants from './constants';
|
|
6
|
+
import { parseColor } from '../util/parse-color';
|
|
7
|
+
import {Color} from '../src/visual-types';
|
|
8
|
+
|
|
9
|
+
export class RepresenterNode {
|
|
10
|
+
node: NodeSvg;
|
|
11
|
+
state: DrawState;
|
|
12
|
+
renderer: Renderer;
|
|
13
|
+
constructor(node: NodeSvg, svgState: DrawState, renderer: Renderer) {
|
|
14
|
+
this.renderer = renderer;
|
|
15
|
+
this.node = node;
|
|
16
|
+
this.state = svgState;
|
|
17
|
+
|
|
18
|
+
// Attach this wrapper to the node for external access
|
|
19
|
+
node.svg = this;
|
|
20
|
+
}
|
|
21
|
+
getConstant(name: keyof RendererConstants) {
|
|
22
|
+
return this.renderer._constants[name];
|
|
23
|
+
}
|
|
24
|
+
/** Move node visually without changing its relativeCoords */
|
|
25
|
+
moveTo(x: number, y: number) {
|
|
26
|
+
this.state.group!.move(x, y);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Scale node visually */
|
|
30
|
+
setScale(scale: number) {
|
|
31
|
+
this.state.group!.scale(scale);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Apply raw transform string */
|
|
35
|
+
applyTransform(transform: string) {
|
|
36
|
+
this.state.group!.attr({ transform });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Access the raw SVG group */
|
|
40
|
+
getRaw() {
|
|
41
|
+
return this.state.group;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Optional: highlight node */
|
|
45
|
+
highlight(color = '#ff0') {
|
|
46
|
+
if (!color || color.length == 0 || typeof color !== 'string') {
|
|
47
|
+
this.highlight(parseColor(this.getConstant('NODE_OUTLINE_COLOR') as Color) as string);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
this.state.bg!.stroke({ color, width: 2 });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import NodeSvg from '../src/nodesvg';
|
|
2
|
+
import Renderer, { DrawState } from './renderer';
|
|
3
|
+
import { RepresenterNode } from './representer-node';
|
|
4
|
+
|
|
5
|
+
export default class Representer {
|
|
6
|
+
nodes: Map<string, RepresenterNode> = new Map();
|
|
7
|
+
|
|
8
|
+
/** Build a representer node for a drawn node */
|
|
9
|
+
build(node: NodeSvg, renderer: Renderer, state: DrawState) {
|
|
10
|
+
if (!node) return;
|
|
11
|
+
const repNode = new RepresenterNode(node, state, renderer);
|
|
12
|
+
this.nodes.set(node.id, repNode);
|
|
13
|
+
return repNode;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Get a representer node by node id */
|
|
17
|
+
get(nodeId: string): RepresenterNode | undefined {
|
|
18
|
+
return this.nodes.get(nodeId);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Remove a node from the representer */
|
|
22
|
+
remove(nodeId: string) {
|
|
23
|
+
this.nodes.delete(nodeId);
|
|
24
|
+
}
|
|
25
|
+
}
|
package/src/category.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { parseColor } from "../util/parse-color";
|
|
2
|
+
import { TblxCategoryStruct, TblxNodeStruct } from "./inject";
|
|
3
|
+
import Toolbox from "./toolbox";
|
|
4
|
+
import { Color } from "./visual-types";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Represents a category in the toolbox, containing nodes and a label.
|
|
8
|
+
*/
|
|
9
|
+
class Category {
|
|
10
|
+
/** Display label for the category */
|
|
11
|
+
label: string;
|
|
12
|
+
/** Category color */
|
|
13
|
+
color: Color;
|
|
14
|
+
/** Nodes contained in this category */
|
|
15
|
+
contents: TblxNodeStruct[];
|
|
16
|
+
/** HTML button element representing this category row */
|
|
17
|
+
_rowDiv!: HTMLButtonElement;
|
|
18
|
+
/** Reference to parent toolbox */
|
|
19
|
+
_toolbox: Toolbox;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param toolbox The parent Toolbox instance
|
|
23
|
+
* @param cData Category data (label, color, nodes)
|
|
24
|
+
*/
|
|
25
|
+
constructor(toolbox: Toolbox, cData: TblxCategoryStruct) {
|
|
26
|
+
this.label = cData.name;
|
|
27
|
+
this.color = cData.color;
|
|
28
|
+
this.contents = cData.contents;
|
|
29
|
+
this._toolbox = toolbox;
|
|
30
|
+
this._makeDiv();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Creates the category button in the UI and attaches click events */
|
|
34
|
+
_makeDiv() {
|
|
35
|
+
const btn = document.createElement("button");
|
|
36
|
+
btn.textContent = this.label;
|
|
37
|
+
btn.className = "KabelCategoryRow";
|
|
38
|
+
btn.style.backgroundColor = parseColor(this.color);
|
|
39
|
+
btn.style.color = parseColor('#ffffff');
|
|
40
|
+
this._rowDiv = btn;
|
|
41
|
+
|
|
42
|
+
btn.addEventListener("click", e => {
|
|
43
|
+
e.stopPropagation();
|
|
44
|
+
this._toolbox._flyout.clear();
|
|
45
|
+
this._toolbox._flyout.fill(this.contents);
|
|
46
|
+
this._toolbox._flyout.show();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
this._toolbox.container.appendChild(btn);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Refreshes the category UI with new data
|
|
54
|
+
* @param cData Partial data to update (name, color, contents)
|
|
55
|
+
*/
|
|
56
|
+
refresh(cData: Partial<TblxCategoryStruct>) {
|
|
57
|
+
if (cData.name !== undefined) {
|
|
58
|
+
this.label = cData.name;
|
|
59
|
+
this._rowDiv.textContent = this.label;
|
|
60
|
+
}
|
|
61
|
+
if (cData.color !== undefined) {
|
|
62
|
+
this.color = cData.color;
|
|
63
|
+
this._rowDiv.style.backgroundColor = parseColor(this.color);
|
|
64
|
+
}
|
|
65
|
+
if (cData.contents !== undefined) {
|
|
66
|
+
this.contents = cData.contents;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Remove category row from the toolbox */
|
|
71
|
+
destroy() {
|
|
72
|
+
this._rowDiv.remove();
|
|
73
|
+
this._toolbox = null as unknown as Toolbox;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Hide this category from the UI */
|
|
77
|
+
hide() {
|
|
78
|
+
this._rowDiv.style.display = "none";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Show this category in the UI */
|
|
82
|
+
show() {
|
|
83
|
+
this._rowDiv.style.display = "";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Enable or disable interaction with this category
|
|
88
|
+
* @param disabled True to disable, false to enable
|
|
89
|
+
*/
|
|
90
|
+
setDisabled(disabled: boolean) {
|
|
91
|
+
this._rowDiv.disabled = disabled;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Returns a plain object representation of this category
|
|
96
|
+
* @returns JSON-compatible category object
|
|
97
|
+
*/
|
|
98
|
+
toJSON(): TblxCategoryStruct {
|
|
99
|
+
return {
|
|
100
|
+
name: this.label,
|
|
101
|
+
color: this.color,
|
|
102
|
+
contents: this.contents,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export default Category;
|
package/src/colors.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Color, ColorStyle } from "./visual-types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Stores color styles for node categories.
|
|
5
|
+
*
|
|
6
|
+
* Each key is a category name, and the value is a ColorStyle object
|
|
7
|
+
* containing colors like primary, secondary, and tertiary.
|
|
8
|
+
*
|
|
9
|
+
* Example:
|
|
10
|
+
* ```ts
|
|
11
|
+
* CategoryColors["logic"] = {
|
|
12
|
+
* primary: "#FF0000",
|
|
13
|
+
* secondary: "#00FF00",
|
|
14
|
+
* tertiary: "#0000FF"
|
|
15
|
+
* };
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
const CategoryColors: { [key: string]: ColorStyle } = {};
|
|
19
|
+
|
|
20
|
+
export default CategoryColors;
|
package/src/comment.ts
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { G, Line } from "@svgdotjs/svg.js";
|
|
2
|
+
import NodeSvg from "./nodesvg";
|
|
3
|
+
import WorkspaceSvg from "./workspace-svg";
|
|
4
|
+
import Coordinates from "./coordinates";
|
|
5
|
+
import { generateUID } from "../util/uid";
|
|
6
|
+
import Workspace from "./workspace";
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
export interface CommentSerialized {
|
|
10
|
+
id: string;
|
|
11
|
+
text: string;
|
|
12
|
+
coords: { x: number; y: number };
|
|
13
|
+
parent: string | null; // node ID if node comment, null if workspace comment
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Represents a comment attached to either a NodeSvg or a WorkspaceSvg.
|
|
17
|
+
*/
|
|
18
|
+
class CommentModel {
|
|
19
|
+
/** The comment text */
|
|
20
|
+
_text: string;
|
|
21
|
+
|
|
22
|
+
/** True if this comment belongs to the workspace instead of a node */
|
|
23
|
+
_isWorkspaceComment: boolean;
|
|
24
|
+
|
|
25
|
+
/** Parent NodeSvg or WorkspaceSvg to which this comment belongs */
|
|
26
|
+
_parent: NodeSvg | WorkspaceSvg | Workspace;
|
|
27
|
+
|
|
28
|
+
/** SVG group representing this comment in the DOM */
|
|
29
|
+
svgGroup?: G | undefined;
|
|
30
|
+
|
|
31
|
+
/** Coordinates relative to parent */
|
|
32
|
+
relativeCoords: Coordinates;
|
|
33
|
+
|
|
34
|
+
/** Optional SVG line connecting the comment to its node */
|
|
35
|
+
svgLine?: Line | undefined;
|
|
36
|
+
|
|
37
|
+
/** Temporary bounding box info for input handling */
|
|
38
|
+
_tempInputBBox?: { width: number; height: number; textX: number; textY: number };
|
|
39
|
+
|
|
40
|
+
/** Unique identifier for this comment */
|
|
41
|
+
id: string;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Creates a new comment attached to a node or workspace.
|
|
45
|
+
* @param parent - NodeSvg or WorkspaceSvg this comment belongs to
|
|
46
|
+
*/
|
|
47
|
+
constructor(parent: NodeSvg | WorkspaceSvg | Workspace) {
|
|
48
|
+
this._parent = parent;
|
|
49
|
+
this._isWorkspaceComment = parent instanceof WorkspaceSvg;
|
|
50
|
+
this._text = "";
|
|
51
|
+
this.relativeCoords = new Coordinates(0, 0); // Coordinates relative to this._parent
|
|
52
|
+
this.id = generateUID("nanoid", {
|
|
53
|
+
alphabet: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0129384756!)@(#*$&%^",
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Sets the text of the comment without triggering a comment redraw.
|
|
59
|
+
* @param value - New text content
|
|
60
|
+
* @returns The updated text
|
|
61
|
+
*/
|
|
62
|
+
setTextNoRedraw(value: string): string {
|
|
63
|
+
return (this._text = value);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Retrieves the current text of the comment.
|
|
68
|
+
* @returns The comment text
|
|
69
|
+
*/
|
|
70
|
+
getText(): string {
|
|
71
|
+
return this._text;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Sets the text of the comment and triggers a redraw of all comments in the workspace.
|
|
76
|
+
* @param value - New text content
|
|
77
|
+
* @returns The updated text
|
|
78
|
+
*/
|
|
79
|
+
setText(value: string): string {
|
|
80
|
+
const res = (this._text = value);
|
|
81
|
+
if (this.getWorkspace().isHeadless) return res;
|
|
82
|
+
this.getWorkspace().renderer.clearComments();
|
|
83
|
+
this.getWorkspace().renderer.drawComments();
|
|
84
|
+
return res;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Returns true if this comment is attached to a node.
|
|
89
|
+
*/
|
|
90
|
+
isNodeComment(): boolean {
|
|
91
|
+
return !this._isWorkspaceComment;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Returns true if this comment is attached to the workspace.
|
|
96
|
+
*/
|
|
97
|
+
isWorkspaceComment(): boolean {
|
|
98
|
+
return this._isWorkspaceComment;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Gets the workspace that owns this comment.
|
|
103
|
+
*/
|
|
104
|
+
getWorkspace(): WorkspaceSvg {
|
|
105
|
+
if (this.isWorkspaceComment()) {
|
|
106
|
+
return this._parent as WorkspaceSvg;
|
|
107
|
+
} else {
|
|
108
|
+
return (this._parent as NodeSvg)?.workspace as WorkspaceSvg;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Convert to JSON structure holding all important data.
|
|
113
|
+
*/
|
|
114
|
+
toJson(): CommentSerialized {
|
|
115
|
+
return {
|
|
116
|
+
text: this.getText(),
|
|
117
|
+
coords: { x: this.relativeCoords.x, y: this.relativeCoords.y },
|
|
118
|
+
parent: this.isWorkspaceComment() ? null : (this._parent as NodeSvg).id,
|
|
119
|
+
id: this.id
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Creates a CommentModel from serialized data.
|
|
125
|
+
* @param data - The serialized comment
|
|
126
|
+
* @param workspace - The workspace to attach the comment to
|
|
127
|
+
* @param nodeMap - Optional map of node IDs to NodeSvg instances
|
|
128
|
+
*/
|
|
129
|
+
static fromJson(
|
|
130
|
+
data: CommentSerialized,
|
|
131
|
+
): CommentModel {
|
|
132
|
+
const comment = new CommentModel(null as any as WorkspaceSvg | NodeSvg);
|
|
133
|
+
comment.id = data.id;
|
|
134
|
+
comment.relativeCoords = new Coordinates(data.coords.x, data.coords.y);
|
|
135
|
+
comment.setTextNoRedraw(data.text);
|
|
136
|
+
|
|
137
|
+
return comment;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export default CommentModel;
|
|
142
|
+
export { CommentModel };
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import Field, { ConnectableField } from "./field";
|
|
2
|
+
import NodeSvg from "./nodesvg";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A node or field that can participate in a connection.
|
|
6
|
+
*/
|
|
7
|
+
export type Connectable = NodeSvg | null | Field;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Represents a connection between two connectable objects.
|
|
11
|
+
*/
|
|
12
|
+
class Connection {
|
|
13
|
+
/** The source of the connection */
|
|
14
|
+
from: Connectable;
|
|
15
|
+
/** The target of the connection */
|
|
16
|
+
to: Connectable;
|
|
17
|
+
/** Whether this connection represents a previous connection (affects rendering/logic) */
|
|
18
|
+
isPrevious: boolean;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Creates a new Connection.
|
|
22
|
+
* @param from - The source NodeSvg or Field
|
|
23
|
+
* @param to - The target NodeSvg or Field
|
|
24
|
+
* @param isPrevious - True if this connection is a previous connection
|
|
25
|
+
*/
|
|
26
|
+
constructor(from: Connectable, to: Connectable, isPrevious: boolean = false) {
|
|
27
|
+
this.from = from;
|
|
28
|
+
this.to = to;
|
|
29
|
+
this.isPrevious = isPrevious;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Returns the target of the connection */
|
|
33
|
+
getTo(): Connectable {
|
|
34
|
+
return this.to;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Returns the source of the connection */
|
|
38
|
+
getFrom(): Connectable {
|
|
39
|
+
return this.from;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Disconnects this connection from its target.
|
|
44
|
+
* Safely handles NodeSvg chains and ConnectableFields.
|
|
45
|
+
*/
|
|
46
|
+
disconnectTo() {
|
|
47
|
+
if (!this.to) return;
|
|
48
|
+
|
|
49
|
+
if (this.to instanceof NodeSvg && this.to.previousConnection) {
|
|
50
|
+
const prevConn = this.to.previousConnection;
|
|
51
|
+
const prevFrom = prevConn.from;
|
|
52
|
+
|
|
53
|
+
// Disconnect NodeSvg chain
|
|
54
|
+
if (prevFrom instanceof NodeSvg && prevFrom.nextConnection?.to) {
|
|
55
|
+
prevFrom.nextConnection.to = null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Disconnect Field chain
|
|
59
|
+
if (prevFrom instanceof Field && prevFrom.hasConnectable?.()) {
|
|
60
|
+
(prevFrom as ConnectableField).disconnect();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
this.to = null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Disconnects this connection from its source.
|
|
69
|
+
* Safely handles NodeSvg chains and ConnectableFields.
|
|
70
|
+
*/
|
|
71
|
+
disconnectFrom() {
|
|
72
|
+
if (!this.from) return;
|
|
73
|
+
|
|
74
|
+
if (this.from instanceof NodeSvg && this.from.nextConnection) {
|
|
75
|
+
const next = this.from.nextConnection;
|
|
76
|
+
|
|
77
|
+
if (next.to instanceof NodeSvg && next.to.previousConnection?.from) {
|
|
78
|
+
next.to.previousConnection.from = null;
|
|
79
|
+
} else if (next.to instanceof Field && next.to.hasConnectable()) {
|
|
80
|
+
(next.to as ConnectableField).disconnect();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
next.to = null;
|
|
84
|
+
} else if (this.from instanceof Field && this.from.hasConnectable()) {
|
|
85
|
+
(this.from as ConnectableField).disconnect();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
this.from = null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Sets the target of this connection (used during deserialization)
|
|
93
|
+
* @param target - New connection target
|
|
94
|
+
*/
|
|
95
|
+
setTo(target: Connectable) {
|
|
96
|
+
this.to = target;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Sets the source of this connection (used during deserialization)
|
|
101
|
+
* @param source - New connection source
|
|
102
|
+
*/
|
|
103
|
+
setFrom(source: Connectable) {
|
|
104
|
+
this.from = source;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Completely isolates this connection, clearing both ends */
|
|
108
|
+
isolate() {
|
|
109
|
+
this.from = null as any;
|
|
110
|
+
this.to = null as any;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export default Connection;
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import WorkspaceController from "../controllers/base";
|
|
2
|
+
import unescapeAttr from "../util/unescape-html";
|
|
3
|
+
import CommentModel from "./comment";
|
|
4
|
+
import Coordinates from "./coordinates";
|
|
5
|
+
import ContextOptsRegistry from "./ctx-menu-registry";
|
|
6
|
+
import NodeSvg from "./nodesvg";
|
|
7
|
+
import Widget from "./widget";
|
|
8
|
+
import WorkspaceSvg from "./workspace-svg";
|
|
9
|
+
/**
|
|
10
|
+
* A list of types that a context menu item can show for.
|
|
11
|
+
*/
|
|
12
|
+
export type Showable = 'node' | 'ws' | 'html' | 'comment' | 'any';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Describes a context menu option.
|
|
16
|
+
*/
|
|
17
|
+
export interface ContextMenuOpts {
|
|
18
|
+
/** Function executed when the option is clicked */
|
|
19
|
+
click: (target: NodeSvg | WorkspaceSvg | CommentModel | HTMLElement) => void;
|
|
20
|
+
/** Optional hover start callback */
|
|
21
|
+
onHoverStart?: () => void;
|
|
22
|
+
/** Optional hover end callback */
|
|
23
|
+
onHoverEnd?: () => void;
|
|
24
|
+
/** Specifies which target types this option should appear for */
|
|
25
|
+
showFor?: Showable | Showable[];
|
|
26
|
+
/** Specifies extra logic for drawing */
|
|
27
|
+
onDraw?: (optEl: Node, ws: WorkspaceSvg, opt: ContextMenuOpts) => void;
|
|
28
|
+
/** Text label for the menu option */
|
|
29
|
+
label: string;
|
|
30
|
+
/** Unique ID for the option */
|
|
31
|
+
id: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* HTML context menu rendered on workspace right-click.
|
|
36
|
+
*/
|
|
37
|
+
class ContextMenuHTML {
|
|
38
|
+
/**
|
|
39
|
+
* The workspace.
|
|
40
|
+
*/
|
|
41
|
+
workspace: WorkspaceSvg;
|
|
42
|
+
/**
|
|
43
|
+
* The workspace's controller
|
|
44
|
+
*/
|
|
45
|
+
controller: WorkspaceController;
|
|
46
|
+
/**
|
|
47
|
+
* The widget in the workspace to display the menu on.
|
|
48
|
+
*/
|
|
49
|
+
widget: Widget;
|
|
50
|
+
/**
|
|
51
|
+
* Options for the context menu.
|
|
52
|
+
*/
|
|
53
|
+
options: ContextMenuOpts[];
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Create a new context menu for a workspace.
|
|
57
|
+
* @param workspace - Workspace to attach the context menu to
|
|
58
|
+
*/
|
|
59
|
+
constructor(workspace: WorkspaceSvg) {
|
|
60
|
+
this.workspace = workspace;
|
|
61
|
+
this.controller = this.workspace.controller;
|
|
62
|
+
this.widget = new Widget(this.workspace, {
|
|
63
|
+
coords: new Coordinates(0, 0),
|
|
64
|
+
name: 'k_contextmenu',
|
|
65
|
+
className: 'KabelContextMenu'
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Override show/hide methods to manipulate CSS
|
|
69
|
+
this.widget.show = () => {
|
|
70
|
+
this.widget.container.classList.add('show');
|
|
71
|
+
this.widget.container.style.display = 'flex';
|
|
72
|
+
this.widget.visible = true;
|
|
73
|
+
};
|
|
74
|
+
this.widget.hide = () => {
|
|
75
|
+
this.widget.container.classList.remove('show');
|
|
76
|
+
this.widget.container.style.display = 'none';
|
|
77
|
+
this.widget.visible = false;
|
|
78
|
+
};
|
|
79
|
+
this.widget.container.style.removeProperty('height');
|
|
80
|
+
this.widget.container.style.removeProperty('width');
|
|
81
|
+
|
|
82
|
+
this.widget.hide();
|
|
83
|
+
this.options = ContextOptsRegistry;
|
|
84
|
+
this.initListeners();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Renders context menu options for a given target.
|
|
89
|
+
* @param target - The object the context menu is for
|
|
90
|
+
*/
|
|
91
|
+
renderOptions(target: NodeSvg | WorkspaceSvg | HTMLElement | CommentModel | null) {
|
|
92
|
+
this.widget.container.innerHTML = '';
|
|
93
|
+
|
|
94
|
+
const filteredOptions = this.options.filter(opt => {
|
|
95
|
+
if (!target) return false;
|
|
96
|
+
const showFor = Array.isArray(opt.showFor) ? opt.showFor : [opt.showFor];
|
|
97
|
+
if (showFor.includes('any')) return true;
|
|
98
|
+
if (target instanceof NodeSvg && showFor.includes('node')) return true;
|
|
99
|
+
if (target instanceof WorkspaceSvg && showFor.includes('ws')) return true;
|
|
100
|
+
if (target instanceof HTMLElement && !(target instanceof SVGSVGElement) && showFor.includes('html')) return true;
|
|
101
|
+
if (target instanceof CommentModel && showFor.includes('comment')) return true;
|
|
102
|
+
return false;
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
filteredOptions.forEach((opt, i) => {
|
|
106
|
+
const el = document.createElement('div');
|
|
107
|
+
el.className = 'KabelContextOption';
|
|
108
|
+
el.textContent = opt.label || 'Option ' + i;
|
|
109
|
+
|
|
110
|
+
el.addEventListener('click', () => {
|
|
111
|
+
if (target) opt.click(target);
|
|
112
|
+
this.hide();
|
|
113
|
+
});
|
|
114
|
+
if (opt.onDraw) opt.onDraw(el, this.workspace, opt);
|
|
115
|
+
if (opt.onHoverStart) el.addEventListener('mouseenter', () => opt.onHoverStart?.());
|
|
116
|
+
if (opt.onHoverEnd) el.addEventListener('mouseleave', () => opt.onHoverEnd?.());
|
|
117
|
+
|
|
118
|
+
this.widget.container.appendChild(el);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Initializes event listeners for showing/hiding the menu.
|
|
124
|
+
*/
|
|
125
|
+
initListeners() {
|
|
126
|
+
if (this.controller) {
|
|
127
|
+
this.controller.addMoveListener(() => {
|
|
128
|
+
if (this.widget.visible) this.hide();
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
this.workspace.svg.node.addEventListener('contextmenu', e => {
|
|
132
|
+
e.preventDefault();
|
|
133
|
+
const mouseX = e.clientX;
|
|
134
|
+
const mouseY = e.clientY;
|
|
135
|
+
|
|
136
|
+
this.widget.setCoords(new Coordinates(mouseX, mouseY));
|
|
137
|
+
this.renderOptions(this.target);
|
|
138
|
+
this.widget.show();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
document.addEventListener('mousedown', e => {
|
|
142
|
+
if (!this.widget.container.contains(e.target as Node)) {
|
|
143
|
+
this.hide();
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Hides the context menu */
|
|
149
|
+
hide() {
|
|
150
|
+
this.widget.hide();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Returns the current mouse position in workspace coordinates */
|
|
154
|
+
get mousePos(): { x: number, y: number } {
|
|
155
|
+
return this.controller.mousePos;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Returns the target element under the mouse for context menu.
|
|
160
|
+
*/
|
|
161
|
+
get target(): NodeSvg | WorkspaceSvg | HTMLElement | CommentModel | null {
|
|
162
|
+
let el = document.elementFromPoint(this.mousePos.x, this.mousePos.y) as HTMLElement | null;
|
|
163
|
+
if (!el) return null;
|
|
164
|
+
if ((el as unknown as SVGSVGElement) === this.workspace.svg.node) return this.workspace;
|
|
165
|
+
|
|
166
|
+
if (el && el.classList?.contains?.('WorkspaceBgPattern')) return this.workspace;
|
|
167
|
+
|
|
168
|
+
while (el && el !== document.body) {
|
|
169
|
+
if (el.tagName.toLowerCase() === 'g' && el.hasAttribute('data-node-id')) {
|
|
170
|
+
const nodeId = unescapeAttr(el.getAttribute('data-node-id') as string);
|
|
171
|
+
const node = this.workspace.getNode(nodeId);
|
|
172
|
+
if (node) return node;
|
|
173
|
+
}
|
|
174
|
+
if (el.tagName.toLowerCase() === 'g' && el.hasAttribute('comment-data')) {
|
|
175
|
+
const dta = JSON.parse(el.getAttribute('comment-data') as string);
|
|
176
|
+
if (!dta.id && !dta.isws) continue;
|
|
177
|
+
if (dta.isws) {
|
|
178
|
+
for (let comment of this.workspace._commentDB) {
|
|
179
|
+
if (comment.id === dta.id) return comment;
|
|
180
|
+
}
|
|
181
|
+
} else {
|
|
182
|
+
for (let [_, node] of this.workspace._nodeDB) {
|
|
183
|
+
if (node.comment && node.comment.id == dta.id) return node.comment;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
el = el.parentElement;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return document.elementFromPoint(this.mousePos.x, this.mousePos.y) as HTMLElement | null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export default ContextMenuHTML;
|