@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
package/(1.0.7)kabel.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
**chore(release): Kabel 1.0.6 release**
|
|
2
|
+
- Implemented the Apollo renderer, a renderer that tries to match the "sticky note" style of other blueprint interfaces.
|
|
3
|
+
- Added shadow FX to default renderer.
|
|
4
|
+
- Added background pattern system under wsSvg.grid, options in Kabel.inject for grid added as well.
|
|
5
|
+
- Created new /themes folder, created dark and classic (Default) themes, added theme options to inject.
|
|
6
|
+
- Fixed input boxes (comment, raw field) so that when you click at a specific point, the caret starts there.
|
|
7
|
+
- Decided that where you click within a comment input is where it should start editing at.
|
|
8
|
+
- Added wsSvg.dragState to store information about the current block being dragged.
|
|
9
|
+
- Cleaned up the Flyout class to break up large methods into multiple. This makes extensions easier.
|
|
10
|
+
- Implemented headless inject method along with the WorkspaceSVG's headless counterpart class. (Creating simulated workspaces)
|
|
11
|
+
- Cleaned up renderer which was in preparation for the second default renderer.
|
|
12
|
+
- Added full JSDoc comments to renderer for cleanliness and maintainability.
|
|
13
|
+
- Cleaned up comment renderer for easier subclassing.
|
|
14
|
+
- Removed two useless frame-waits in WorkspaceSVG class.
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
**chore(release): Kabel 1.0.7 release**
|
|
18
|
+
- Very minor documentation update.
|
package/README.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
|
|
2
|
+
# Kabel
|
|
3
|
+
[](https://www.npmjs.com/package/@kabel-project/kabel)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
|
|
6
|
+
Node-based visual editor framework with an API inspired by Google’s Blockly project.
|
|
7
|
+
Fully extensible with custom nodes, fields, and renderers.
|
|
8
|
+
Written in TypeScript and ready to use out of the box.
|
|
9
|
+
|
|
10
|
+

|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
You can install Kabel in two main ways:
|
|
17
|
+
|
|
18
|
+
**Using npm (recommended):**
|
|
19
|
+
```bash
|
|
20
|
+
cd path/to/your/project
|
|
21
|
+
npm install @kabel-project/kabel
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**Cloning from GitHub (Experimental builds):**
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
git clone https://github.com/FentFentFent/Kabel.git --depth 1
|
|
28
|
+
cd Kabel
|
|
29
|
+
npm install
|
|
30
|
+
npm run build
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Then import Kabel from the build:
|
|
34
|
+
|
|
35
|
+
```js
|
|
36
|
+
import Kabel from './Kabel/dist/kabel.js';
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
##### Quick Starter HTML
|
|
40
|
+
|
|
41
|
+
```html
|
|
42
|
+
<div id="workspace-container" style="width:800px;height:600px;"></div>
|
|
43
|
+
<script type="module">
|
|
44
|
+
import Kabel from 'kabel';
|
|
45
|
+
Kabel.inject('workspace-container', {
|
|
46
|
+
/* Your options here... */
|
|
47
|
+
});
|
|
48
|
+
</script>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Features
|
|
54
|
+
|
|
55
|
+
* ⚡ Extensible: create custom nodes, fields, and renderers.
|
|
56
|
+
* 🛠️ TypeScript support out of the box.
|
|
57
|
+
* 🎨 Renderer override system for custom visuals.
|
|
58
|
+
* ⌨️ Built-in workspace controls (WASD movement, drag, etc.).
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Example
|
|
63
|
+
|
|
64
|
+
```js
|
|
65
|
+
import Kabel from 'kabel';
|
|
66
|
+
|
|
67
|
+
const ws = Kabel.inject('workspace-container', {
|
|
68
|
+
controls: { wasd: true, wasdSmooth: true }
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Register a simple node
|
|
72
|
+
Kabel.Nodes['example'] = {
|
|
73
|
+
init() {
|
|
74
|
+
this.jsonInit({
|
|
75
|
+
labelText: 'Hello Kabel',
|
|
76
|
+
type: 'example_node',
|
|
77
|
+
primaryColor: '#cc0c00'
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
For another example refer to [Kabel Test](./dist/index.html)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Documentation
|
|
89
|
+
|
|
90
|
+
Please refer to [Kabel Guide](./_READ_ME_MEDIA_/documentation/docs.md)
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## License
|
|
95
|
+
|
|
96
|
+
MIT
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
|
|
2
|
+
# Kabel Documentation
|
|
3
|
+
|
|
4
|
+
**Kabel** is a node-based visual programming editor for building interactive, logic-driven UIs in the browser. It’s built around JavaScript and uses SVG for rendering. Kabel supports node creation, custom fields, widgets, toolboxes, and smooth workspace controls.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Table of Contents
|
|
9
|
+
|
|
10
|
+
1. Getting Started
|
|
11
|
+
2. Setting Up Your First Workspace
|
|
12
|
+
3. Creating Your First Node Prototype
|
|
13
|
+
4. Creating a Toolbox
|
|
14
|
+
5. Manipulating Nodes
|
|
15
|
+
6. Serializing & Deserializing Nodes
|
|
16
|
+
7. Finishing Up
|
|
17
|
+
|
|
18
|
+
## In-depth Docs
|
|
19
|
+
[Kabel API Documentation](../../docs/globals.md)
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 1. Getting Started
|
|
24
|
+
|
|
25
|
+
You can include Kabel either via a script tag or import it as a module:
|
|
26
|
+
|
|
27
|
+
```html
|
|
28
|
+
<script src="./kabel.js"></script>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Once loaded:
|
|
32
|
+
|
|
33
|
+
```js
|
|
34
|
+
if (typeof Kabel !== 'undefined') {
|
|
35
|
+
console.log('Kabel is ready!');
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
That’s all you need to confirm it’s available before initializing a workspace.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 2. Setting Up Your First Workspace
|
|
44
|
+
|
|
45
|
+
The **workspace** is where nodes live and interact. Start by creating an HTML container:
|
|
46
|
+
|
|
47
|
+
```js
|
|
48
|
+
const el = document.createElement('div');
|
|
49
|
+
Object.assign(el.style, {
|
|
50
|
+
width: '100vw',
|
|
51
|
+
height: '100vh',
|
|
52
|
+
background: '#f0f0f0'
|
|
53
|
+
});
|
|
54
|
+
document.body.appendChild(el);
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Then inject Kabel into that container:
|
|
58
|
+
|
|
59
|
+
```js
|
|
60
|
+
const ws = Kabel.inject(el, {
|
|
61
|
+
theme: 'Dark',
|
|
62
|
+
renderer: 'atlas', // Or `Apollo`
|
|
63
|
+
moveSpeed: 6,
|
|
64
|
+
controls: {
|
|
65
|
+
wasd: true,
|
|
66
|
+
wasdSmooth: true,
|
|
67
|
+
wasdAccelerate: 1,
|
|
68
|
+
wasdFriction: 0.9
|
|
69
|
+
},
|
|
70
|
+
toolbox: { /* defined later */ }
|
|
71
|
+
});
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
This gives you a fullscreen workspace with smooth WASD movement controls.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## 3. Creating Your First Node Prototype
|
|
79
|
+
|
|
80
|
+
Nodes are defined once as **prototypes** under `Kabel.Nodes`. Here’s an example:
|
|
81
|
+
|
|
82
|
+
```js
|
|
83
|
+
Kabel.Nodes['my_node'] = {
|
|
84
|
+
init() {
|
|
85
|
+
this.jsonInit({
|
|
86
|
+
previousConnection: true,
|
|
87
|
+
nextConnection: true,
|
|
88
|
+
labelText: 'TopbarText',
|
|
89
|
+
type: 'my_node',
|
|
90
|
+
primaryColor: '#cc0c00',
|
|
91
|
+
tertiaryColor: '#660500',
|
|
92
|
+
arguments: [
|
|
93
|
+
{ name: 'FLD', type: 'field_str', label: 'LabelText: ', value: 'ValueText' },
|
|
94
|
+
{ name: 'Dummy', type: 'field_dummy', label: 'Label with no input!' },
|
|
95
|
+
{ name: 'Conn', type: 'connection', label: 'Label!' },
|
|
96
|
+
{ name: 'Conn2', type: 'field_both', label: 'lowk: ', value: 'lowk' },
|
|
97
|
+
{
|
|
98
|
+
name: 'Dropdown',
|
|
99
|
+
type: 'dropdown',
|
|
100
|
+
label: 'lowk dropdown?: ',
|
|
101
|
+
options: ['Oooo option', { text: 'Opt', value: 'opt' }]
|
|
102
|
+
}
|
|
103
|
+
]
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**Key options:**
|
|
110
|
+
|
|
111
|
+
* `previousConnection` / `nextConnection` → Whether the node can chain with others.
|
|
112
|
+
* `arguments` → Defines fields, inputs, and connection points.
|
|
113
|
+
* `jsonInit()` → Initializes node configuration.
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## 4. Creating a Toolbox
|
|
118
|
+
|
|
119
|
+
Toolboxes organize your available nodes:
|
|
120
|
+
|
|
121
|
+
```js
|
|
122
|
+
toolbox: {
|
|
123
|
+
type: 'category',
|
|
124
|
+
contents: [{
|
|
125
|
+
name: 'Cat',
|
|
126
|
+
color: '#cc0c00',
|
|
127
|
+
contents: [{
|
|
128
|
+
type: 'my_node',
|
|
129
|
+
arguments: {
|
|
130
|
+
//FIELD_NAME: FIELD_VALUE
|
|
131
|
+
}
|
|
132
|
+
}]
|
|
133
|
+
}]
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
**Toolbox types:**
|
|
138
|
+
|
|
139
|
+
* `'category'` → Groups nodes visually into collapsible sections.
|
|
140
|
+
* `'flyout'` → Displays all nodes in one scrollable list.
|
|
141
|
+
|
|
142
|
+
**Category JSON fields:**
|
|
143
|
+
|
|
144
|
+
* `name`: The category name.
|
|
145
|
+
* `color`: The display color.
|
|
146
|
+
* `contents`: Array of node definitions.
|
|
147
|
+
|
|
148
|
+
> Currently, categories and flyouts can only contain nodes. Support for buttons or UI elements may be added later.
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## 5. Manipulating Node Properties
|
|
153
|
+
|
|
154
|
+
Once a node is created, you can move, modify, or connect it programmatically:
|
|
155
|
+
|
|
156
|
+
```js
|
|
157
|
+
const node = new Kabel.NodeSvg(Kabel.Nodes['my_node'], ws);
|
|
158
|
+
node.init();
|
|
159
|
+
|
|
160
|
+
const node2 = new Kabel.NodeSvg(Kabel.Nodes['my_node'], ws);
|
|
161
|
+
node2.init();
|
|
162
|
+
node2.relativeCoords.set(20, 0);
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Connect them:
|
|
166
|
+
|
|
167
|
+
```js
|
|
168
|
+
node.nextConnection.to = node2;
|
|
169
|
+
node2.previousConnection.from = node;
|
|
170
|
+
ws.redraw();
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
You can also manipulate the SVG directly (temporary, not saved across rerenders):
|
|
174
|
+
|
|
175
|
+
```js
|
|
176
|
+
node.svg.highlight('#ff0');
|
|
177
|
+
node.svg.setScale(1);
|
|
178
|
+
node.svg.moveTo(0, 1);
|
|
179
|
+
node.svg.applyTransform('translate(0, 1)');
|
|
180
|
+
node.svg.getRaw(); // Returns the svg.js G element
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
### Optional: Widgets
|
|
186
|
+
|
|
187
|
+
**Widgets** are custom UI overlays within the workspace:
|
|
188
|
+
|
|
189
|
+
```js
|
|
190
|
+
Kabel.Widgets['testWidget'] = {
|
|
191
|
+
name: 'Test Widget',
|
|
192
|
+
width: 150,
|
|
193
|
+
height: 100,
|
|
194
|
+
coords: new Kabel.Coordinates(50, 50),
|
|
195
|
+
html: '<button id="counterBtn">Count: 0</button>',
|
|
196
|
+
init(widget, container) {
|
|
197
|
+
let count = 0;
|
|
198
|
+
const btn = container.querySelector('#counterBtn');
|
|
199
|
+
if (btn) {
|
|
200
|
+
btn.addEventListener('click', () => {
|
|
201
|
+
count++;
|
|
202
|
+
btn.textContent = 'Count: ' + count;
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const myWidget = ws.newWidget('testWidget');
|
|
209
|
+
if (myWidget) myWidget.show();
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Widgets let you add custom interactivity or information panels.
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## 6. Serializing Nodes
|
|
217
|
+
|
|
218
|
+
Kabel supports both **circular** and **non-circular** JSON serialization.
|
|
219
|
+
|
|
220
|
+
### Circular Serialization
|
|
221
|
+
|
|
222
|
+
Contains live object references (not exportable as plain JSON, but usable within JS).
|
|
223
|
+
|
|
224
|
+
```js
|
|
225
|
+
const circular = node.serialize();
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Example (simplified):
|
|
229
|
+
|
|
230
|
+
```json
|
|
231
|
+
{
|
|
232
|
+
"id": "sisDG24Gp6lc7*S#m#I9c",
|
|
233
|
+
"type": "my_node",
|
|
234
|
+
"nextConnection": {
|
|
235
|
+
"node": "[CircularNodeRef]"
|
|
236
|
+
},
|
|
237
|
+
"fields": [...]
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
### Non-Circular Serialization
|
|
244
|
+
|
|
245
|
+
Safe for exporting or saving to files:
|
|
246
|
+
|
|
247
|
+
```js
|
|
248
|
+
const nonCircular = node.toJson();
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Example:
|
|
252
|
+
|
|
253
|
+
```json
|
|
254
|
+
{
|
|
255
|
+
"7f7#2Rrt16uFuH^2f2^ad": {
|
|
256
|
+
"id": "7f7#2Rrt16uFuH^2f2^ad",
|
|
257
|
+
"type": "my_node",
|
|
258
|
+
"colors": { "primary": "#cc0c00", "tertiary": "#660500" },
|
|
259
|
+
"label": "TopbarText",
|
|
260
|
+
"nextConnection": { "node": "jX^$kh)yHW5p^bx98vD#s" },
|
|
261
|
+
"fields": [...]
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
### Workspace Serialization
|
|
269
|
+
|
|
270
|
+
```js
|
|
271
|
+
// Serialize
|
|
272
|
+
const data = workspace.toJson(false); // false = non-circular
|
|
273
|
+
|
|
274
|
+
// Deserialize
|
|
275
|
+
workspace.fromJson(data);
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
The serialization format automatically includes the circular/non-circular flag internally.
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## 7. Finishing Up
|
|
283
|
+
|
|
284
|
+
You can expose internals for quick debugging:
|
|
285
|
+
|
|
286
|
+
```js
|
|
287
|
+
window.ws = ws;
|
|
288
|
+
window.nodes = [node, node2];
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
That’s it — you now have a working Kabel workspace with nodes, a toolbox, and serialization support.
|
|
292
|
+
|
|
293
|
+
---
|
|
Binary file
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import Renderer from '../renderers/renderer';
|
|
2
|
+
import WorkspaceSvg from '../src/workspace-svg';
|
|
3
|
+
import NodeSvg from '../src/nodesvg';
|
|
4
|
+
import CommentModel from '../src/comment';
|
|
5
|
+
import { Element, SVG, Svg } from '@svgdotjs/svg.js';
|
|
6
|
+
import eventer from '../util/eventer';
|
|
7
|
+
|
|
8
|
+
class CommentRenderer {
|
|
9
|
+
static get NAME() { return 'cr_atlas'; }
|
|
10
|
+
static get COMMENT_G_TAG() { return 'KabelCommentGroup'; }
|
|
11
|
+
static get COMMENT_LINE_TAG() { return 'KabelCommentLine'; }
|
|
12
|
+
static get COMMENT_TEXT_EL() { return 'KabelCommentText'; }
|
|
13
|
+
static get COMMENT_DRAG_EL() { return 'KabelCommentDragHandle'; }
|
|
14
|
+
static get RENDERER_TAG_EL() { return 'KabelCommentElTag'; }
|
|
15
|
+
|
|
16
|
+
workspace: WorkspaceSvg;
|
|
17
|
+
|
|
18
|
+
constructor(workspace: WorkspaceSvg) {
|
|
19
|
+
this.workspace = workspace;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
encodeForSvg(s: string) {
|
|
23
|
+
return s.replace(/ /g, "\u00A0");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
ensureTspansPreserve(node: HTMLElement) {
|
|
27
|
+
Array.from(node.childNodes).forEach((child) => {
|
|
28
|
+
if (child.nodeType === 1) {
|
|
29
|
+
(child as HTMLElement).setAttribute("xml:space", "preserve");
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
measureTextBbox(textEl: Element) {
|
|
35
|
+
return textEl.bbox();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
drawComment(comment: CommentModel) {
|
|
39
|
+
const svg = this.getSvg();
|
|
40
|
+
this._removeExistingElements(comment);
|
|
41
|
+
|
|
42
|
+
const g = svg.group().addClass(CommentRenderer.COMMENT_G_TAG);
|
|
43
|
+
g.attr({
|
|
44
|
+
'comment-data': JSON.stringify({
|
|
45
|
+
id: comment.id,
|
|
46
|
+
isws: comment.isWorkspaceComment()
|
|
47
|
+
})
|
|
48
|
+
});
|
|
49
|
+
comment.svgGroup = g;
|
|
50
|
+
|
|
51
|
+
const padding = 4;
|
|
52
|
+
const handleRadius = 6;
|
|
53
|
+
const textOffsetX = padding + handleRadius * 2 + 4;
|
|
54
|
+
|
|
55
|
+
const displayEncoded = this.encodeForSvg(comment.getText());
|
|
56
|
+
const textEl = this._createTextElement(g, displayEncoded, textOffsetX, padding);
|
|
57
|
+
|
|
58
|
+
const bbox = this.measureTextBbox(textEl);
|
|
59
|
+
|
|
60
|
+
const rect = this._createBackgroundRect(g, bbox, padding, handleRadius);
|
|
61
|
+
const dragHandle = this._createDragHandle(g, bbox, padding, handleRadius);
|
|
62
|
+
|
|
63
|
+
this._addEventer(rect, dragHandle, comment, textEl);
|
|
64
|
+
|
|
65
|
+
const { screenPos, zoom } = this._computeScreenPosition(comment);
|
|
66
|
+
g.attr({ transform: `translate(${screenPos.x}, ${screenPos.y}) scale(${zoom})` });
|
|
67
|
+
|
|
68
|
+
if (comment._parent instanceof NodeSvg && comment._parent.svgGroup) {
|
|
69
|
+
this._drawLineToNode(comment, padding);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
refreshCommentTransforms() {
|
|
74
|
+
for (const comment of [
|
|
75
|
+
...this.workspace.getComments(),
|
|
76
|
+
...Array.from(this.workspace._nodeDB.values())
|
|
77
|
+
.map(n => n.getComment())
|
|
78
|
+
.filter(c => !!c) as CommentModel[]
|
|
79
|
+
]) {
|
|
80
|
+
if (!comment.svgGroup) continue;
|
|
81
|
+
|
|
82
|
+
const { screenPos, zoom } = this._computeScreenPosition(comment);
|
|
83
|
+
comment.svgGroup.attr({ transform: `translate(${screenPos.x}, ${screenPos.y}) scale(${zoom})` });
|
|
84
|
+
|
|
85
|
+
if (comment.svgLine) {
|
|
86
|
+
comment.svgLine.remove();
|
|
87
|
+
comment.svgLine = undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (comment._parent instanceof NodeSvg) {
|
|
91
|
+
this._drawLineToNode(comment, 4);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private _removeExistingElements(comment: CommentModel) {
|
|
97
|
+
if (comment.svgGroup) comment.svgGroup.remove();
|
|
98
|
+
if (comment.svgLine) comment.svgLine.remove();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private _createTextElement(parent: Element, text: string, x: number, y: number) {
|
|
102
|
+
//@ts-ignore
|
|
103
|
+
const textEl = parent.text(text)
|
|
104
|
+
.font({ family: 'Arial', size: 12 })
|
|
105
|
+
.fill('#000')
|
|
106
|
+
.move(x, y)
|
|
107
|
+
.addClass(CommentRenderer.COMMENT_TEXT_EL);
|
|
108
|
+
|
|
109
|
+
textEl.node.style.userSelect = 'none';
|
|
110
|
+
// @ts-ignore
|
|
111
|
+
textEl.node.style!.webkitUserSelect = 'none';
|
|
112
|
+
// @ts-ignore
|
|
113
|
+
textEl.node.style!.msUserSelect = 'none';
|
|
114
|
+
// @ts-ignore
|
|
115
|
+
textEl.node.style!.MozUserSelect = 'none';
|
|
116
|
+
return textEl;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private _createBackgroundRect(parent: Element, bbox: any, padding: number, handleRadius: number) {
|
|
120
|
+
//@ts-ignore
|
|
121
|
+
return parent.rect(bbox.width + padding * 2 + handleRadius * 2 + 4, bbox.height + padding * 2)
|
|
122
|
+
.fill('#ffffcc')
|
|
123
|
+
.stroke({ color: '#cccc99', width: 1 })
|
|
124
|
+
.radius(4)
|
|
125
|
+
.back();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private _createDragHandle(parent: Element, bbox: any, padding: number, handleRadius: number) {
|
|
129
|
+
//@ts-ignore
|
|
130
|
+
return parent.circle(handleRadius * 2)
|
|
131
|
+
.fill('#adad7d')
|
|
132
|
+
.move(padding, padding + (bbox.height / 2) - handleRadius)
|
|
133
|
+
.addClass(CommentRenderer.COMMENT_DRAG_EL);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private _addEventer(rect: Element, dragHandle: Element, comment: CommentModel, textEl: Element) {
|
|
137
|
+
eventer.addElement(dragHandle, 'k_draghandle', { comment })
|
|
138
|
+
.tagElement(dragHandle, CommentRenderer.RENDERER_TAG_EL);
|
|
139
|
+
eventer.addElement(rect, "k_commentinp", { comment, text: textEl, renderer: this })
|
|
140
|
+
.tagElement(rect, CommentRenderer.RENDERER_TAG_EL);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private _computeScreenPosition(comment: CommentModel) {
|
|
144
|
+
let workspaceX = comment.relativeCoords.x;
|
|
145
|
+
let workspaceY = comment.relativeCoords.y;
|
|
146
|
+
|
|
147
|
+
if (comment._parent instanceof NodeSvg) {
|
|
148
|
+
const nodePos = comment._parent.relativeCoords;
|
|
149
|
+
workspaceX += nodePos.x;
|
|
150
|
+
workspaceY += nodePos.y;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const screenPos = this.workspace.workspaceToScreen(workspaceX, workspaceY);
|
|
154
|
+
const zoom = this.workspace.getZoom();
|
|
155
|
+
return { screenPos, zoom };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private _drawLineToNode(comment: CommentModel, padding: number) {
|
|
159
|
+
if (!comment || !(comment._parent instanceof NodeSvg) || !comment.svgGroup) return;
|
|
160
|
+
const svg = this.getSvg();
|
|
161
|
+
const ws = this.workspace;
|
|
162
|
+
|
|
163
|
+
if (comment.svgLine) comment.svgLine.remove();
|
|
164
|
+
|
|
165
|
+
const nodeWSPos = comment._parent.relativeCoords;
|
|
166
|
+
const nodeBBox = comment._parent.svgGroup!.bbox();
|
|
167
|
+
const nodeTopCenter = ws.workspaceToScreen(
|
|
168
|
+
nodeWSPos.x + nodeBBox.width / 2,
|
|
169
|
+
nodeWSPos.y
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
const parentWSPos = comment._parent.relativeCoords;
|
|
173
|
+
const commentRelPos = comment.relativeCoords;
|
|
174
|
+
const commentAbsX = parentWSPos.x + commentRelPos.x;
|
|
175
|
+
const commentAbsY = parentWSPos.y + commentRelPos.y;
|
|
176
|
+
const commentBBox = comment.svgGroup!.bbox();
|
|
177
|
+
const commentTopCenter = ws.workspaceToScreen(
|
|
178
|
+
commentAbsX + (commentBBox.width + padding * 2) / 2,
|
|
179
|
+
commentAbsY
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
comment.svgLine = svg.line(
|
|
183
|
+
nodeTopCenter.x, nodeTopCenter.y,
|
|
184
|
+
commentTopCenter.x, commentTopCenter.y
|
|
185
|
+
)
|
|
186
|
+
.stroke({ color: '#888', width: 1, dasharray: '3,2' })
|
|
187
|
+
.addClass(CommentRenderer.COMMENT_LINE_TAG)
|
|
188
|
+
.back();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
clearAllComments() {
|
|
192
|
+
const svg = this.getSvg();
|
|
193
|
+
svg.find(`.${CommentRenderer.COMMENT_G_TAG}`).forEach(el => el.remove());
|
|
194
|
+
svg.find(`.${CommentRenderer.COMMENT_LINE_TAG}`).forEach(el => el.remove());
|
|
195
|
+
eventer.destroyByTag(CommentRenderer.RENDERER_TAG_EL);
|
|
196
|
+
|
|
197
|
+
for (let [_, node] of this.workspace._nodeDB) {
|
|
198
|
+
const comment = node.getComment();
|
|
199
|
+
if (comment) comment.svgGroup = undefined;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
for (const comment of this.workspace.getComments()) {
|
|
203
|
+
comment.svgGroup = undefined;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
clearCommentLines() {
|
|
208
|
+
const svg = this.getSvg();
|
|
209
|
+
svg.find(`.${CommentRenderer.COMMENT_LINE_TAG}`).forEach(el => el.remove());
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
drawAllComments() {
|
|
213
|
+
for (const comment of this.workspace.getComments()) this.drawComment(comment);
|
|
214
|
+
for (let [_, node] of this.workspace._nodeDB) {
|
|
215
|
+
const comment = node.getComment();
|
|
216
|
+
if (comment) this.drawComment(comment);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
getSvg(): Svg {
|
|
221
|
+
return this.workspace.svg;
|
|
222
|
+
}
|
|
223
|
+
getWs(): WorkspaceSvg {
|
|
224
|
+
return this.workspace;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export default CommentRenderer;
|