@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,11 @@
|
|
|
1
|
+
[**Kabel Project Docs v1.0.6**](../README.md)
|
|
2
|
+
|
|
3
|
+
***
|
|
4
|
+
|
|
5
|
+
[Kabel Project Docs](../globals.md) / default
|
|
6
|
+
|
|
7
|
+
# Variable: default
|
|
8
|
+
|
|
9
|
+
> `const` **default**: [`KabelInterface`](../interfaces/KabelInterface.md) = `K`
|
|
10
|
+
|
|
11
|
+
Defined in: [src/index.ts:43](https://github.com/FentFentFent/Kabel/blob/6a658c7afa967c18ecfb5cdff24af90b7d7319c3/src/index.ts#L43)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Element } from '@svgdotjs/svg.js';
|
|
2
|
+
import NodeSvg from '../src/nodesvg';
|
|
3
|
+
import eventer from '../util/eventer';
|
|
4
|
+
import type { EventSetupFn } from '../util/eventer';
|
|
5
|
+
import WorkspaceSvg from '../src/workspace-svg';
|
|
6
|
+
import userState from '../util/user-state';
|
|
7
|
+
import CommentModel from '../src/comment';
|
|
8
|
+
|
|
9
|
+
/** The drag handle for comments */
|
|
10
|
+
function initDraggable(element: Element, args: Record<string, any>): () => void {
|
|
11
|
+
const comment = args.comment as CommentModel;
|
|
12
|
+
if (!comment) return () => { };
|
|
13
|
+
|
|
14
|
+
let startX = 0;
|
|
15
|
+
let startY = 0;
|
|
16
|
+
let startRelX = 0;
|
|
17
|
+
let startRelY = 0;
|
|
18
|
+
|
|
19
|
+
function onPointerDown(ev: PointerEvent) {
|
|
20
|
+
ev.preventDefault();
|
|
21
|
+
|
|
22
|
+
const ws = comment.getWorkspace();
|
|
23
|
+
const rel = comment.relativeCoords;
|
|
24
|
+
|
|
25
|
+
// Capture offset from pointer to comment in workspace coords
|
|
26
|
+
const pointerWS = ws.screenToWorkspace(ev.clientX, ev.clientY);
|
|
27
|
+
startRelX = rel.x - pointerWS.x;
|
|
28
|
+
startRelY = rel.y - pointerWS.y;
|
|
29
|
+
|
|
30
|
+
document.addEventListener('pointermove', onPointerMove);
|
|
31
|
+
document.addEventListener('pointerup', onPointerUp);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function onPointerMove(ev: PointerEvent) {
|
|
35
|
+
const ws = comment.getWorkspace();
|
|
36
|
+
|
|
37
|
+
// Map pointer to workspace
|
|
38
|
+
const pointerWS = ws.screenToWorkspace(ev.clientX, ev.clientY);
|
|
39
|
+
|
|
40
|
+
// Add initial offset
|
|
41
|
+
comment.relativeCoords.set(pointerWS.x + startRelX, pointerWS.y + startRelY);
|
|
42
|
+
|
|
43
|
+
ws.refreshComments();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
function onPointerUp() {
|
|
49
|
+
document.removeEventListener('pointermove', onPointerMove);
|
|
50
|
+
document.removeEventListener('pointerup', onPointerUp);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
element.node.addEventListener('pointerdown', onPointerDown);
|
|
54
|
+
|
|
55
|
+
// Cleanup function
|
|
56
|
+
return () => {
|
|
57
|
+
element.node.removeEventListener('pointerdown', onPointerDown);
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
eventer.registerEvent('k_draghandle', initDraggable as EventSetupFn);
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { Element as SvgElement, Rect as SvgRect, Text as SvgText } from "@svgdotjs/svg.js";
|
|
2
|
+
import CommentModel from "../src/comment";
|
|
3
|
+
import eventer, { EventSetupFn } from "../util/eventer";
|
|
4
|
+
import userState from '../util/user-state';
|
|
5
|
+
import Renderer from "../comment-renderer/renderer"
|
|
6
|
+
|
|
7
|
+
type InitArgs = {
|
|
8
|
+
comment: CommentModel;
|
|
9
|
+
text: SvgText;
|
|
10
|
+
renderer: Renderer;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function initCommentInput(element: SvgElement, rawArgs: any) {
|
|
14
|
+
const args = rawArgs as InitArgs;
|
|
15
|
+
const comment = args.comment;
|
|
16
|
+
const txt = args.text as SvgText;
|
|
17
|
+
const rect = element as unknown as SvgRect;
|
|
18
|
+
const renderer = args.renderer;
|
|
19
|
+
|
|
20
|
+
let editing = false;
|
|
21
|
+
let skipNextClick = false;
|
|
22
|
+
let buffer = comment.getText() ?? "";
|
|
23
|
+
let cursorPos = buffer.length;
|
|
24
|
+
let anchorPos = buffer.length;
|
|
25
|
+
|
|
26
|
+
const PADDING_X = 4;
|
|
27
|
+
const PADDING_Y = 4;
|
|
28
|
+
|
|
29
|
+
function getSelectionRange() {
|
|
30
|
+
const start = Math.min(cursorPos, anchorPos);
|
|
31
|
+
const end = Math.max(cursorPos, anchorPos);
|
|
32
|
+
return { start, end };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function hasSelection() {
|
|
36
|
+
return cursorPos !== anchorPos;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function deleteSelection() {
|
|
40
|
+
if (!hasSelection()) return false;
|
|
41
|
+
const { start, end } = getSelectionRange();
|
|
42
|
+
buffer = buffer.slice(0, start) + buffer.slice(end);
|
|
43
|
+
cursorPos = start;
|
|
44
|
+
anchorPos = start;
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function encodeForSvg(s: string) {
|
|
49
|
+
return s.replace(/ /g, "\u00A0").replace(/\n/g, " ");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function ensureTspansPreserve(node: Element) {
|
|
53
|
+
Array.from(node.childNodes).forEach((child) => {
|
|
54
|
+
if (child.nodeType === 1) (child as Element).setAttribute("xml:space", "preserve");
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let caretLine: SvgRect | null = null;
|
|
59
|
+
let selectionRect: SvgRect | null = null;
|
|
60
|
+
|
|
61
|
+
// --- MEASURE TEXT WIDTH USING COMMENT RENDERER ---
|
|
62
|
+
function measureTextWidth(text: string, fontSize?: number, fontFamily?: string) {
|
|
63
|
+
// Delegate to renderer’s measureTextBbox logic
|
|
64
|
+
const tempText = renderer.getSvg().text(text).font({ size: fontSize || 12, family: fontFamily || "Arial" });
|
|
65
|
+
const bbox = renderer.measureTextBbox(tempText);
|
|
66
|
+
tempText.remove();
|
|
67
|
+
return bbox.width;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function updateText() {
|
|
71
|
+
const { start, end } = getSelectionRange();
|
|
72
|
+
const hasSel = hasSelection();
|
|
73
|
+
|
|
74
|
+
// redraw main text
|
|
75
|
+
txt.clear().tspan(encodeForSvg(buffer));
|
|
76
|
+
txt.attr({ "xml:space": "preserve" });
|
|
77
|
+
try { ensureTspansPreserve(txt.node as Element); } catch { }
|
|
78
|
+
txt.leading(1.2);
|
|
79
|
+
|
|
80
|
+
// --- selection highlight ---
|
|
81
|
+
if (hasSel) {
|
|
82
|
+
const fontSize = parseFloat(txt.attr('font-size') as string) || 14;
|
|
83
|
+
const fontFamily = txt.attr('font-family') as string || 'Arial';
|
|
84
|
+
|
|
85
|
+
const textBeforeStart = buffer.slice(0, start);
|
|
86
|
+
const textBeforeEnd = buffer.slice(0, end);
|
|
87
|
+
|
|
88
|
+
const bbox = txt.bbox();
|
|
89
|
+
const highlightX = bbox.x + measureTextWidth(textBeforeStart, fontSize, fontFamily);
|
|
90
|
+
const highlightWidth = Math.max(
|
|
91
|
+
measureTextWidth(textBeforeEnd, fontSize, fontFamily) - measureTextWidth(textBeforeStart, fontSize, fontFamily),
|
|
92
|
+
1
|
|
93
|
+
);
|
|
94
|
+
const highlightY = bbox.y;
|
|
95
|
+
const highlightH = bbox.height;
|
|
96
|
+
|
|
97
|
+
if (!selectionRect) {
|
|
98
|
+
//@ts-ignore
|
|
99
|
+
selectionRect = rect!.parent()!.rect?.(highlightWidth, highlightH)
|
|
100
|
+
.fill('#3390ff')
|
|
101
|
+
.attr({ 'fill-opacity': 0.35 });
|
|
102
|
+
selectionRect!.node.parentNode!.insertBefore(selectionRect!.node, txt.node);
|
|
103
|
+
} else {
|
|
104
|
+
selectionRect.size(highlightWidth, highlightH);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
selectionRect!.move(highlightX, highlightY);
|
|
108
|
+
} else {
|
|
109
|
+
if (selectionRect) { selectionRect.remove(); selectionRect = null; }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// --- caret ---
|
|
113
|
+
if (editing) {
|
|
114
|
+
const fontSize = parseFloat(txt.attr('font-size') as string) || 14;
|
|
115
|
+
const fontFamily = txt.attr('font-family') as string || 'Arial';
|
|
116
|
+
const textBeforeCaret = buffer.slice(0, cursorPos);
|
|
117
|
+
const caretXOffset = measureTextWidth(textBeforeCaret, fontSize, fontFamily);
|
|
118
|
+
|
|
119
|
+
const bbox = txt.bbox();
|
|
120
|
+
const caretY = bbox.y;
|
|
121
|
+
const caretH = bbox.height;
|
|
122
|
+
|
|
123
|
+
//@ts-ignore
|
|
124
|
+
if (!caretLine) caretLine = rect.parent().rect(1, caretH).fill('#000');
|
|
125
|
+
|
|
126
|
+
caretLine!.size(1, caretH).move(bbox.x + caretXOffset, caretY);
|
|
127
|
+
} else {
|
|
128
|
+
if (caretLine) { caretLine.remove(); caretLine = null; }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// --- resize background rect ---
|
|
132
|
+
const bbox = txt.bbox();
|
|
133
|
+
const handleRadius = 6;
|
|
134
|
+
const textOffsetX = PADDING_X + handleRadius * 2 + 4;
|
|
135
|
+
const rectW = Math.max(16, Math.ceil(bbox.width) + textOffsetX + PADDING_X);
|
|
136
|
+
const rectH = Math.max(16, Math.ceil(bbox.height) + PADDING_Y * 2);
|
|
137
|
+
rect.size(rectW, rectH);
|
|
138
|
+
|
|
139
|
+
comment._tempInputBBox = {
|
|
140
|
+
width: rectW,
|
|
141
|
+
height: rectH,
|
|
142
|
+
textX: textOffsetX,
|
|
143
|
+
textY: PADDING_Y
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// --- editing lifecycle ---
|
|
148
|
+
function startEditing(ev?: MouseEvent) {
|
|
149
|
+
if (editing) return;
|
|
150
|
+
editing = true;
|
|
151
|
+
buffer = comment.getText() ?? "";
|
|
152
|
+
cursorPos = buffer.length;
|
|
153
|
+
anchorPos = buffer.length;
|
|
154
|
+
|
|
155
|
+
userState.setState('typing');
|
|
156
|
+
|
|
157
|
+
if (ev) {
|
|
158
|
+
const rectBox = txt.node.getBoundingClientRect();
|
|
159
|
+
//@ts-ignore
|
|
160
|
+
const zoom = renderer.getWs().getZoom();
|
|
161
|
+
const clickX = (ev.clientX - rectBox.left - PADDING_X) / zoom;
|
|
162
|
+
|
|
163
|
+
let cumulativeWidth = 0;
|
|
164
|
+
cursorPos = 0;
|
|
165
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
166
|
+
const charWidth = measureTextWidth(buffer[i] as string);
|
|
167
|
+
if (cumulativeWidth + charWidth / 2 >= clickX) break;
|
|
168
|
+
cumulativeWidth += charWidth;
|
|
169
|
+
cursorPos = i + 1;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
anchorPos = cursorPos;
|
|
174
|
+
updateText();
|
|
175
|
+
skipNextClick = true;
|
|
176
|
+
|
|
177
|
+
document.addEventListener("keydown", onKeyDown);
|
|
178
|
+
document.addEventListener("mousedown", onClickOutside);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function stopEditing(commit = true) {
|
|
182
|
+
if (!editing) return;
|
|
183
|
+
editing = false;
|
|
184
|
+
userState.removeState("typing");
|
|
185
|
+
document.removeEventListener("keydown", onKeyDown, { capture: true });
|
|
186
|
+
document.removeEventListener("mousedown", onClickOutside);
|
|
187
|
+
|
|
188
|
+
if (commit) comment.setTextNoRedraw(buffer);
|
|
189
|
+
updateText();
|
|
190
|
+
comment.getWorkspace()?.redraw?.();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
function onKeyDown(e: KeyboardEvent) {
|
|
195
|
+
if (!editing) return;
|
|
196
|
+
|
|
197
|
+
if (e.key === "Escape") { e.preventDefault(); stopEditing(false); return; }
|
|
198
|
+
|
|
199
|
+
if (e.key === "Enter") {
|
|
200
|
+
e.preventDefault();
|
|
201
|
+
stopEditing(true);
|
|
202
|
+
} else if (e.key === "Backspace") {
|
|
203
|
+
e.preventDefault();
|
|
204
|
+
if (hasSelection()) {
|
|
205
|
+
deleteSelection();
|
|
206
|
+
} else if (cursorPos > 0) {
|
|
207
|
+
buffer = buffer.slice(0, cursorPos - 1) + buffer.slice(cursorPos);
|
|
208
|
+
cursorPos--;
|
|
209
|
+
anchorPos = cursorPos;
|
|
210
|
+
}
|
|
211
|
+
} else if (e.key === "Delete") {
|
|
212
|
+
e.preventDefault();
|
|
213
|
+
if (hasSelection()) {
|
|
214
|
+
deleteSelection();
|
|
215
|
+
} else if (cursorPos < buffer.length) {
|
|
216
|
+
buffer = buffer.slice(0, cursorPos) + buffer.slice(cursorPos + 1);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
else if (e.key === "ArrowLeft") {
|
|
220
|
+
e.preventDefault();
|
|
221
|
+
if (e.shiftKey) {
|
|
222
|
+
// extend selection left
|
|
223
|
+
cursorPos = Math.max(0, cursorPos - 1);
|
|
224
|
+
} else {
|
|
225
|
+
// collapse selection and move
|
|
226
|
+
cursorPos = Math.max(0, cursorPos - 1);
|
|
227
|
+
anchorPos = cursorPos;
|
|
228
|
+
}
|
|
229
|
+
} else if (e.key === "ArrowRight") {
|
|
230
|
+
e.preventDefault();
|
|
231
|
+
if (e.shiftKey) {
|
|
232
|
+
// extend selection right
|
|
233
|
+
cursorPos = Math.min(buffer.length, cursorPos + 1);
|
|
234
|
+
} else {
|
|
235
|
+
// collapse selection and move
|
|
236
|
+
cursorPos = Math.min(buffer.length, cursorPos + 1);
|
|
237
|
+
anchorPos = cursorPos;
|
|
238
|
+
}
|
|
239
|
+
} else if (e.key === "Home") {
|
|
240
|
+
e.preventDefault();
|
|
241
|
+
if (e.shiftKey) {
|
|
242
|
+
cursorPos = 0; // anchor unchanged
|
|
243
|
+
} else {
|
|
244
|
+
cursorPos = 0;
|
|
245
|
+
anchorPos = cursorPos;
|
|
246
|
+
}
|
|
247
|
+
} else if (e.key === "End") {
|
|
248
|
+
e.preventDefault();
|
|
249
|
+
if (e.shiftKey) {
|
|
250
|
+
cursorPos = buffer.length;
|
|
251
|
+
} else {
|
|
252
|
+
cursorPos = buffer.length;
|
|
253
|
+
anchorPos = cursorPos;
|
|
254
|
+
}
|
|
255
|
+
} else if (e.key.length === 1 && !e.ctrlKey && !e.metaKey) {
|
|
256
|
+
e.preventDefault();
|
|
257
|
+
if (hasSelection()) deleteSelection();
|
|
258
|
+
buffer = buffer.slice(0, cursorPos) + e.key + buffer.slice(cursorPos);
|
|
259
|
+
cursorPos++;
|
|
260
|
+
anchorPos = cursorPos;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
updateText();
|
|
264
|
+
comment.setTextNoRedraw(buffer);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function onClickOutside(ev: MouseEvent) {
|
|
268
|
+
if (!editing) return;
|
|
269
|
+
if (skipNextClick) { skipNextClick = false; return; }
|
|
270
|
+
const targ = ev.target as Node;
|
|
271
|
+
if (targ !== rect.node && targ !== txt.node) stopEditing(true);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const onRectDown = (ev: Event) => startEditing(ev as MouseEvent);
|
|
275
|
+
const onTextDown = (ev: Event) => startEditing(ev as MouseEvent);
|
|
276
|
+
|
|
277
|
+
rect.on("mousedown", onRectDown as any);
|
|
278
|
+
txt.on("mousedown", onTextDown as any);
|
|
279
|
+
|
|
280
|
+
updateText();
|
|
281
|
+
|
|
282
|
+
return () => {
|
|
283
|
+
rect.off("mousedown", onRectDown as any);
|
|
284
|
+
txt.off("mousedown", onTextDown as any);
|
|
285
|
+
document.removeEventListener("keydown", onKeyDown, { capture: true });
|
|
286
|
+
document.removeEventListener("mousedown", onClickOutside);
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
eventer.registerEvent("k_commentinp", initCommentInput as EventSetupFn);
|
|
291
|
+
export default initCommentInput;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Element, Svg } from '@svgdotjs/svg.js';
|
|
2
|
+
import NodeSvg from '../src/nodesvg';
|
|
3
|
+
import eventer from '../util/eventer';
|
|
4
|
+
import type { EventSetupFn } from '../util/eventer';
|
|
5
|
+
import WorkspaceSvg from '../src/workspace-svg';
|
|
6
|
+
import userState from '../util/user-state';
|
|
7
|
+
import Renderer, { ConnectorToFrom, DrawState } from '../renderers/renderer';
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
function initConnectionLine(element: Element, args: Record<string, any>): () => void {
|
|
12
|
+
// click destroys the line
|
|
13
|
+
const handleClick = () => {
|
|
14
|
+
args.fromConn.disconnectTo();
|
|
15
|
+
args.toConn.disconnectFrom();
|
|
16
|
+
element.remove();
|
|
17
|
+
xMark.remove();
|
|
18
|
+
const remove: ConnectorToFrom[] = [];
|
|
19
|
+
for (let state of args.renderer._drawStates as DrawState[]) {
|
|
20
|
+
for (let pair of state.pendingConnections) {
|
|
21
|
+
if (pair.from == args.fromConn && pair.to == args.toConn) {
|
|
22
|
+
remove.push(pair);
|
|
23
|
+
}
|
|
24
|
+
if (pair.from = args.toConn && pair.to == args.fromConn) {
|
|
25
|
+
remove.push(pair);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
state.pendingConnections = state.pendingConnections.filter(e => !remove.includes(e));
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// create X element
|
|
33
|
+
const bbox = element.bbox();
|
|
34
|
+
const midX = bbox.x + bbox.width / 2;
|
|
35
|
+
const midY = bbox.y + bbox.height / 2;
|
|
36
|
+
const xMark = (element.parent()! as Svg).text('X').font({
|
|
37
|
+
family: 'Fredoka, sans-serif',
|
|
38
|
+
size: 46,
|
|
39
|
+
weight: '700',
|
|
40
|
+
anchor: 'middle',
|
|
41
|
+
leading: '1em'
|
|
42
|
+
})
|
|
43
|
+
.addClass((args.renderer.constructor as typeof Renderer).LINE_X_MARK_TAG)
|
|
44
|
+
.center(midX, midY)
|
|
45
|
+
.fill('#fff') // white fill
|
|
46
|
+
.stroke({ color: '#000', width: 2 }) // black outline
|
|
47
|
+
.hide();
|
|
48
|
+
|
|
49
|
+
xMark.on('click', handleClick)
|
|
50
|
+
xMark.node.style.userSelect = 'none';
|
|
51
|
+
// show X on hover
|
|
52
|
+
element.on('mouseover', () => xMark.show());
|
|
53
|
+
element.on('mouseout', () => xMark.hide());
|
|
54
|
+
|
|
55
|
+
// attach click event
|
|
56
|
+
element.on('click', handleClick);
|
|
57
|
+
|
|
58
|
+
// cleanup function
|
|
59
|
+
return () => {
|
|
60
|
+
element.off('click', handleClick);
|
|
61
|
+
element.off('mouseover');
|
|
62
|
+
element.off('mouseout');
|
|
63
|
+
xMark.off('click', handleClick);
|
|
64
|
+
xMark.remove();
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
// ok
|
|
68
|
+
eventer.registerEvent('k_connline', initConnectionLine as EventSetupFn);
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { Element } from '@svgdotjs/svg.js';
|
|
2
|
+
import Connection, { Connectable } from '../src/connection';
|
|
3
|
+
import eventer, { EventArgs } from '../util/eventer';
|
|
4
|
+
import userState from '../util/user-state';
|
|
5
|
+
import Field, { AnyField } from '../src/field';
|
|
6
|
+
import NodeSvg from '../src/nodesvg';
|
|
7
|
+
import waitFrames from '../util/wait-anim-frames';
|
|
8
|
+
import WorkspaceSvg from '../src/workspace-svg';
|
|
9
|
+
interface ConnectionV {
|
|
10
|
+
conn: Connection,
|
|
11
|
+
el: Element,
|
|
12
|
+
args?: EventArgs
|
|
13
|
+
}
|
|
14
|
+
interface ConnectionState {
|
|
15
|
+
one: null | ConnectionV,
|
|
16
|
+
two: null | ConnectionV
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
const cState: ConnectionState = {
|
|
21
|
+
one: null,
|
|
22
|
+
two: null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
function initConnector(el: Element, args?: EventArgs) {
|
|
27
|
+
args = args as {
|
|
28
|
+
connection: Connection,
|
|
29
|
+
node?: NodeSvg,
|
|
30
|
+
field?: AnyField
|
|
31
|
+
};
|
|
32
|
+
if (args.field && !args.field?.canEditConnector) { // If editing isnt allowed, close the event early.
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
el.on('click', () => {
|
|
36
|
+
const isPrev = args.connection.isPrevious;
|
|
37
|
+
|
|
38
|
+
// First click → must NOT be previous
|
|
39
|
+
if (!cState.one) {
|
|
40
|
+
if (isPrev) {
|
|
41
|
+
el.addClass('KabelConnectionBubbleHighlightWrong');
|
|
42
|
+
waitFrames(10, () => {
|
|
43
|
+
el.removeClass('KabelConnectionBubbleHighlightWrong');
|
|
44
|
+
})
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
cState.one = { conn: args.connection, el, args };
|
|
48
|
+
el.addClass('KabelConnectionBubbleHighlight');
|
|
49
|
+
userState.setState('connecting')
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Second click → must be previous
|
|
54
|
+
if (!cState.two && (args?.node !== cState.one?.args?.node)) {
|
|
55
|
+
if (!isPrev) {
|
|
56
|
+
el.addClass('KabelConnectionBubbleHighlightWrong');
|
|
57
|
+
waitFrames(10, () => {
|
|
58
|
+
el.removeClass('KabelConnectionBubbleHighlightWrong');
|
|
59
|
+
})
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (cState.one.conn === args.connection) return; // ignore clicking the same again
|
|
63
|
+
cState.two = { conn: args.connection, el, args };
|
|
64
|
+
el.addClass('KabelConnectionBubbleHighlight');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Both are filled → attempt to connect
|
|
68
|
+
if (cState.one && cState.two) {
|
|
69
|
+
if (cState.two.args?.node == cState.one.args?.node || cState.two.args?.node === cState.one?.args?.field?.node) {
|
|
70
|
+
cState.one.el.addClass('KabelConnectionBubbleHighlightWrong');
|
|
71
|
+
cState.two.el.addClass('KabelConnectionBubbleHighlightWrong');
|
|
72
|
+
waitFrames(10, () => {
|
|
73
|
+
if (!cState) return;
|
|
74
|
+
if (!cState.one || !cState.two) return;
|
|
75
|
+
cState.one.el.removeClass('KabelConnectionBubbleHighlightWrong');
|
|
76
|
+
cState.two.el.removeClass('KabelConnectionBubbleHighlightWrong');
|
|
77
|
+
cState.one = null;
|
|
78
|
+
cState.two = null;
|
|
79
|
+
})
|
|
80
|
+
userState.removeState('connecting');
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const { conn: connA } = cState.one;
|
|
84
|
+
const { conn: connB } = cState.two;
|
|
85
|
+
|
|
86
|
+
connA.disconnectTo();
|
|
87
|
+
connB.disconnectFrom();
|
|
88
|
+
|
|
89
|
+
connA.to = cState.two.args?.node;
|
|
90
|
+
connB.from = cState.one.args?.node || cState.one.args?.field;
|
|
91
|
+
|
|
92
|
+
waitFrames(2, () => {
|
|
93
|
+
if (cState.one?.args?.node) {
|
|
94
|
+
cState.one.args.node.workspace?.redraw();
|
|
95
|
+
(cState.one.args.node.workspace as WorkspaceSvg).emitChange();
|
|
96
|
+
} else if (cState.two?.args?.node) {
|
|
97
|
+
cState.two.args.node.workspace?.redraw();
|
|
98
|
+
(cState!.two!.args.node!.workspace as WorkspaceSvg).emitChange();
|
|
99
|
+
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
cState.one = null;
|
|
104
|
+
cState.two = null;
|
|
105
|
+
userState.removeState('connecting');
|
|
106
|
+
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
return () => {
|
|
111
|
+
el.off('click'); // removes the click listener
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
eventer.registerEvent('k_connectbubble', initConnector);
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { Element, G } from '@svgdotjs/svg.js';
|
|
2
|
+
import NodeSvg from '../src/nodesvg';
|
|
3
|
+
import eventer from '../util/eventer';
|
|
4
|
+
import type { EventSetupFn } from '../util/eventer';
|
|
5
|
+
import WorkspaceSvg from '../src/workspace-svg';
|
|
6
|
+
import userState from '../util/user-state';
|
|
7
|
+
|
|
8
|
+
const draggableStore: Record<string, { x: number, y: number }> = {};
|
|
9
|
+
|
|
10
|
+
function initDraggable(element: Element, args: Record<string, any>): () => void {
|
|
11
|
+
let isDragging = false;
|
|
12
|
+
let offsetX = 0;
|
|
13
|
+
let offsetY = 0;
|
|
14
|
+
|
|
15
|
+
// fallback to element itself if no drag handle is given
|
|
16
|
+
const dragTarget: Element = args.dragel ?? element;
|
|
17
|
+
|
|
18
|
+
// Restore position if previously stored (type 3)
|
|
19
|
+
if (args.type === 3 && args.id && draggableStore[args.id]) {
|
|
20
|
+
const pos = draggableStore[args.id];
|
|
21
|
+
if (pos) element.move(pos.x, pos.y);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function onMouseDown(e: MouseEvent) {
|
|
25
|
+
if (args.type === 2 && args.node) {
|
|
26
|
+
const ws : WorkspaceSvg = args.node.workspace;
|
|
27
|
+
if (!ws) return;
|
|
28
|
+
|
|
29
|
+
const start = ws.screenToWorkspace(e.clientX, e.clientY);
|
|
30
|
+
const nodePos = args.node.relativeCoords;
|
|
31
|
+
|
|
32
|
+
offsetX = start.x - nodePos.x;
|
|
33
|
+
offsetY = start.y - nodePos.y;
|
|
34
|
+
ws.beginDrag(args.node, start.x, start.y);
|
|
35
|
+
(args.group as G).front();
|
|
36
|
+
|
|
37
|
+
} else {
|
|
38
|
+
const bbox = element.bbox();
|
|
39
|
+
offsetX = e.clientX - bbox.x;
|
|
40
|
+
offsetY = e.clientY - bbox.y;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
isDragging = false;
|
|
44
|
+
|
|
45
|
+
// Mark user as dragging
|
|
46
|
+
userState.setState('dragging');
|
|
47
|
+
|
|
48
|
+
document.addEventListener('mousemove', onMouseMove);
|
|
49
|
+
document.addEventListener('mouseup', onMouseUp);
|
|
50
|
+
|
|
51
|
+
if (args.type === 1 && args.ondrag) args.ondrag(e);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
function onMouseMove(e: MouseEvent) {
|
|
56
|
+
if (!isDragging) {
|
|
57
|
+
const dx = e.movementX || 0;
|
|
58
|
+
const dy = e.movementY || 0;
|
|
59
|
+
if (Math.abs(dx) > 2 || Math.abs(dy) > 2) isDragging = true;
|
|
60
|
+
}
|
|
61
|
+
if (!isDragging) return;
|
|
62
|
+
|
|
63
|
+
if (args.type === 2 && args.node && args.node instanceof NodeSvg) {
|
|
64
|
+
const ws: WorkspaceSvg = args.node.workspace as WorkspaceSvg;
|
|
65
|
+
if (!ws) return;
|
|
66
|
+
|
|
67
|
+
// Compute new workspace coordinates
|
|
68
|
+
const mouseWS = ws.screenToWorkspace(e.clientX, e.clientY);
|
|
69
|
+
const newX = mouseWS.x - offsetX;
|
|
70
|
+
const newY = mouseWS.y - offsetY;
|
|
71
|
+
args.node.relativeCoords.set(newX, newY);
|
|
72
|
+
ws.updateDrag(newX, newY);
|
|
73
|
+
ws.refresh();
|
|
74
|
+
// Move node visually
|
|
75
|
+
const screenPos = ws.workspaceToScreen(newX, newY);
|
|
76
|
+
element.attr({ transform: `translate(${screenPos.x}, ${screenPos.y}) scale(${ws.getZoom()})` });
|
|
77
|
+
|
|
78
|
+
args.node.emit('NODE_DRAG', null);
|
|
79
|
+
} else if (args.type === 1 && args.onmove) {
|
|
80
|
+
const newX = e.clientX - offsetX;
|
|
81
|
+
const newY = e.clientY - offsetY;
|
|
82
|
+
element.move(newX, newY);
|
|
83
|
+
args.onmove({ x: newX, y: newY });
|
|
84
|
+
} else if (args.type === 3 && args.id) {
|
|
85
|
+
const newX = e.clientX - offsetX;
|
|
86
|
+
const newY = e.clientY - offsetY;
|
|
87
|
+
element.move(newX, newY);
|
|
88
|
+
draggableStore[args.id] = { x: newX, y: newY };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function onMouseUp(e: MouseEvent) {
|
|
93
|
+
document.removeEventListener('mousemove', onMouseMove);
|
|
94
|
+
document.removeEventListener('mouseup', onMouseUp);
|
|
95
|
+
|
|
96
|
+
// Remove dragging state
|
|
97
|
+
userState.removeState('dragging');
|
|
98
|
+
|
|
99
|
+
if (args.type === 1 && args.enddrag) args.enddrag(e);
|
|
100
|
+
if (args.type === 2 && args.node && args.node instanceof NodeSvg) {
|
|
101
|
+
const ws: WorkspaceSvg = args.node.workspace as WorkspaceSvg;
|
|
102
|
+
if (!ws) return;
|
|
103
|
+
ws.emitChange();
|
|
104
|
+
ws.endDrag();
|
|
105
|
+
}
|
|
106
|
+
isDragging = false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
dragTarget.node.addEventListener('mousedown', onMouseDown);
|
|
110
|
+
|
|
111
|
+
// cleanup
|
|
112
|
+
return () => {
|
|
113
|
+
dragTarget.node.removeEventListener('mousedown', onMouseDown);
|
|
114
|
+
document.removeEventListener('mousemove', onMouseMove);
|
|
115
|
+
document.removeEventListener('mouseup', onMouseUp);
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
eventer.registerEvent('k_draggable', initDraggable as EventSetupFn);
|