@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.
Files changed (152) hide show
  1. package/(1.0.7)kabel.md +18 -0
  2. package/README.md +96 -0
  3. package/_READ_ME_MEDIA_/documentation/docs.md +293 -0
  4. package/_READ_ME_MEDIA_/workspace.png +0 -0
  5. package/comment-renderer/renderer.ts +228 -0
  6. package/controllers/base.ts +186 -0
  7. package/controllers/wasd.ts +132 -0
  8. package/docs/README.md +98 -0
  9. package/docs/_media/docs.md +289 -0
  10. package/docs/_media/index.html +168 -0
  11. package/docs/_media/workspace.png +0 -0
  12. package/docs/classes/CommentModel.md +271 -0
  13. package/docs/classes/CommentRenderer.md +457 -0
  14. package/docs/classes/ConnectableField.md +597 -0
  15. package/docs/classes/Connection.md +191 -0
  16. package/docs/classes/ContextMenuHTML.md +163 -0
  17. package/docs/classes/Coordinates.md +187 -0
  18. package/docs/classes/DropdownContainer.md +300 -0
  19. package/docs/classes/DummyField.md +393 -0
  20. package/docs/classes/Eventer.md +185 -0
  21. package/docs/classes/Field.md +461 -0
  22. package/docs/classes/InjectMsg.md +85 -0
  23. package/docs/classes/NodeSvg.md +1011 -0
  24. package/docs/classes/NumberField.md +559 -0
  25. package/docs/classes/OptConnectField.md +624 -0
  26. package/docs/classes/Renderer.md +1636 -0
  27. package/docs/classes/RendererConstants.md +343 -0
  28. package/docs/classes/Representer.md +95 -0
  29. package/docs/classes/RepresenterNode.md +175 -0
  30. package/docs/classes/TextField.md +559 -0
  31. package/docs/classes/Toolbox.md +172 -0
  32. package/docs/classes/WASDController.md +616 -0
  33. package/docs/classes/Widget.md +195 -0
  34. package/docs/classes/WorkspaceController.md +385 -0
  35. package/docs/classes/WorkspaceCoords.md +218 -0
  36. package/docs/classes/WorkspaceSvg.md +1380 -0
  37. package/docs/functions/clearMainWorkspace.md +20 -0
  38. package/docs/functions/getMainWorkspace.md +19 -0
  39. package/docs/functions/inject.md +35 -0
  40. package/docs/functions/setMainWorkspace.md +28 -0
  41. package/docs/globals.md +95 -0
  42. package/docs/interfaces/ColorStyle.md +43 -0
  43. package/docs/interfaces/ConnectorToFrom.md +57 -0
  44. package/docs/interfaces/DrawState.md +81 -0
  45. package/docs/interfaces/FieldConnectionData.md +25 -0
  46. package/docs/interfaces/FieldOptions.md +63 -0
  47. package/docs/interfaces/FieldRawBoxData.md +25 -0
  48. package/docs/interfaces/FieldVisualInfo.md +65 -0
  49. package/docs/interfaces/GridOptions.md +61 -0
  50. package/docs/interfaces/InjectOptions.md +133 -0
  51. package/docs/interfaces/InputFieldJson.md +50 -0
  52. package/docs/interfaces/KabelCommentRendering.md +31 -0
  53. package/docs/interfaces/KabelInterface.md +469 -0
  54. package/docs/interfaces/KabelNodeRendering.md +77 -0
  55. package/docs/interfaces/KabelUIX.md +105 -0
  56. package/docs/interfaces/KabelUtils.md +215 -0
  57. package/docs/interfaces/NodeEvents.md +42 -0
  58. package/docs/interfaces/NodeJson.md +104 -0
  59. package/docs/interfaces/NodePrototype.md +82 -0
  60. package/docs/interfaces/RegisteredEl.md +53 -0
  61. package/docs/interfaces/SerializedNode.md +128 -0
  62. package/docs/interfaces/TblxCategoryStruct.md +41 -0
  63. package/docs/interfaces/TblxFieldStruct.md +28 -0
  64. package/docs/interfaces/TblxNodeStruct.md +35 -0
  65. package/docs/interfaces/WidgetOptions.md +115 -0
  66. package/docs/interfaces/WidgetPrototypeList.md +15 -0
  67. package/docs/type-aliases/AnyField.md +13 -0
  68. package/docs/type-aliases/AnyFieldCls.md +13 -0
  69. package/docs/type-aliases/Color.md +13 -0
  70. package/docs/type-aliases/Connectable.md +13 -0
  71. package/docs/type-aliases/EventArgs.md +11 -0
  72. package/docs/type-aliases/EventSetupFn.md +25 -0
  73. package/docs/type-aliases/Hex.md +13 -0
  74. package/docs/type-aliases/RGBObject.md +37 -0
  75. package/docs/type-aliases/RGBString.md +13 -0
  76. package/docs/type-aliases/RGBTuple.md +13 -0
  77. package/docs/type-aliases/TblxObjStruct.md +52 -0
  78. package/docs/variables/CategoryColors.md +29 -0
  79. package/docs/variables/FieldMap.md +41 -0
  80. package/docs/variables/NodePrototypes.md +18 -0
  81. package/docs/variables/default.md +11 -0
  82. package/events/comment-drag-handle.ts +61 -0
  83. package/events/comment-input.ts +291 -0
  84. package/events/connection-line.ts +68 -0
  85. package/events/connector.ts +116 -0
  86. package/events/draggable.ts +119 -0
  87. package/events/events.ts +7 -0
  88. package/events/input-box.ts +213 -0
  89. package/events/node-x-btn.ts +25 -0
  90. package/index.d.ts +4 -0
  91. package/package.json +49 -0
  92. package/renderers/apollo/apollo.ts +21 -0
  93. package/renderers/apollo/constants.ts +40 -0
  94. package/renderers/apollo/renderer.ts +331 -0
  95. package/renderers/atlas/atlas.ts +15 -0
  96. package/renderers/constants.ts +87 -0
  97. package/renderers/renderer.ts +1288 -0
  98. package/renderers/representer-node.ts +52 -0
  99. package/renderers/representer.ts +25 -0
  100. package/src/category.ts +107 -0
  101. package/src/colors.ts +20 -0
  102. package/src/comment.ts +142 -0
  103. package/src/connection.ts +114 -0
  104. package/src/context-menu.ts +194 -0
  105. package/src/coordinates.ts +74 -0
  106. package/src/core.ts +202 -0
  107. package/src/ctx-menu-registry.ts +143 -0
  108. package/src/dropdown-menu.ts +215 -0
  109. package/src/field.ts +595 -0
  110. package/src/flyout.ts +165 -0
  111. package/src/fonts-manager.ts +38 -0
  112. package/src/grid.ts +162 -0
  113. package/src/headless-node.ts +27 -0
  114. package/src/index.ts +115 -0
  115. package/src/inject-headless.ts +18 -0
  116. package/src/inject.ts +213 -0
  117. package/src/main-workspace.ts +51 -0
  118. package/src/mutator.ts +40 -0
  119. package/src/node-types.ts +27 -0
  120. package/src/nodesvg.ts +756 -0
  121. package/src/prototypes.ts +9 -0
  122. package/src/renderer-map.ts +86 -0
  123. package/src/styles.css +224 -0
  124. package/src/toolbox.ts +125 -0
  125. package/src/types.ts +205 -0
  126. package/src/undo-redo.ts +87 -0
  127. package/src/visual-types.ts +29 -0
  128. package/src/widget-prototypes.ts +11 -0
  129. package/src/widget.ts +139 -0
  130. package/src/workspace-coords.ts +14 -0
  131. package/src/workspace-svg.ts +736 -0
  132. package/src/workspace.ts +155 -0
  133. package/test-server.js +61 -0
  134. package/themes/dark.ts +32 -0
  135. package/themes/default.ts +28 -0
  136. package/themes/themes.ts +9 -0
  137. package/tsconfig.json +25 -0
  138. package/typedoc.json +10 -0
  139. package/util/emitter.ts +33 -0
  140. package/util/env.ts +11 -0
  141. package/util/escape-html.ts +22 -0
  142. package/util/eventer.ts +108 -0
  143. package/util/has-prop.ts +4 -0
  144. package/util/parse-color.ts +42 -0
  145. package/util/path.ts +99 -0
  146. package/util/styler.ts +41 -0
  147. package/util/uid.ts +184 -0
  148. package/util/unescape-html.ts +22 -0
  149. package/util/user-state.ts +68 -0
  150. package/util/wait-anim-frames.ts +24 -0
  151. package/util/window-listeners.ts +62 -0
  152. 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, "&#10;");
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);
@@ -0,0 +1,7 @@
1
+ import './draggable';
2
+ import './input-box';
3
+ import './node-x-btn';
4
+ import './connector';
5
+ import './comment-drag-handle';
6
+ import './comment-input';
7
+ import './connection-line';