html-overlay-node 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/readme.md ADDED
@@ -0,0 +1,437 @@
1
+ # HTML-overlay-Node
2
+
3
+ [![CI](https://github.com/cheonghakim/html-overlay-node/workflows/CI/badge.svg)](https://github.com/cheonghakim/html-overlay-node/actions)
4
+ [![npm version](https://img.shields.io/npm/v/html-overlay-node.svg)](https://www.npmjs.com/package/html-overlay-node)
5
+ [![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](https://opensource.org/licenses/ISC)
6
+
7
+ **HTML-overlay-Node** is a customizable, LiteGraph-style node editor library for building visual programming interfaces. It uses **Canvas rendering** for fast performance and supports node type registration, execution cycle control, custom drawing, HTML overlays, and group management.
8
+
9
+ ---
10
+
11
+ ## ✨ Features
12
+
13
+ - 🎨 **Production-quality design** - Professional dark theme with refined aesthetics
14
+ - 🔧 **Type registration system** - Easily create custom node types
15
+ - 🔌 **Dual port system** - Exec ports (flow control) and data ports (values)
16
+ - ⚡ **Flexible execution** - Automatic or manual execution modes with trigger nodes
17
+ - 🔗 **Multiple edge styles** - Curved, orthogonal, or straight connections
18
+ - 💾 **Serialization** - Save and load graphs with `toJSON`/`fromJSON`
19
+ - 🖱️ **Rich interactions** - Zoom, pan, drag, box select, and snap-to-grid
20
+ - ⌨️ **Keyboard shortcuts** - Undo/redo, align, group, and more
21
+ - 🎯 **Custom drawing** - Per-node custom rendering with `onDraw`
22
+ - 🌐 **HTML overlays** - Embed interactive HTML UIs with proper port layering
23
+ - 📦 **Group nodes** - Organize nodes in hierarchical groups
24
+ - 🪝 **Event hooks** - Subscribe to graph events for extensibility
25
+ - 🎥 **Visual feedback** - Smooth animations and hover states
26
+
27
+ ---
28
+
29
+ ## 🚀 Quick Start
30
+
31
+ ```javascript
32
+ import { createGraphEditor } from "html-overlay-node";
33
+
34
+ // One-liner Initialization!
35
+ // Pass a selector or HTMLElement. Canvas and overlays are created automatically.
36
+ const editor = createGraphEditor("#editor-container", {
37
+ autorun: true,
38
+ enablePropertyPanel: true, // Integrated property panel (default: true)
39
+ });
40
+
41
+ const { graph, registry, addGroup, start } = editor;
42
+ ```
43
+
44
+ ### 💅 Styles (Required)
45
+
46
+ Make sure to import the necessary CSS files for the editor and Property Panel to look correctly:
47
+
48
+ ```javascript
49
+ import "html-overlay-node/index.css";
50
+ import "html-overlay-node/src/ui/PropertyPanel.css";
51
+ ```
52
+
53
+ // Register and add nodes
54
+ registry.register("math/Add", {
55
+ title: "Add",
56
+ size: { w: 180, h: 80 },
57
+ inputs: [
58
+ { name: "a", datatype: "number" },
59
+ { name: "b", datatype: "number" },
60
+ ],
61
+ outputs: [{ name: "result", datatype: "number" }],
62
+ onCreate(node) {
63
+ node.state.a = 0;
64
+ node.state.b = 0;
65
+ },
66
+ onExecute(node, { getInput, setOutput }) {
67
+ const a = getInput("a") ?? node.state.a;
68
+ const b = getInput("b") ?? node.state.b;
69
+ setOutput("result", a + b);
70
+ },
71
+ });
72
+
73
+ // Add nodes
74
+ const node1 = graph.addNode("math/Add", { x: 100, y: 100 });
75
+ const node2 = graph.addNode("math/Add", { x: 100, y: 200 });
76
+
77
+ // Create a group
78
+ addGroup({
79
+ title: "Math Operations",
80
+ x: 50,
81
+ y: 50,
82
+ width: 300,
83
+ height: 300,
84
+ color: "#4a5568",
85
+ members: [node1.id, node2.id],
86
+ });
87
+
88
+ start();
89
+
90
+ ````
91
+
92
+ ---
93
+
94
+ ## 📦 Group Management
95
+
96
+ HTML-overlay-Node supports organizing nodes into hierarchical groups for better organization.
97
+
98
+ ### Creating Groups
99
+
100
+ ```javascript
101
+ const { addGroup } = editor;
102
+
103
+ // Create a group
104
+ const group = addGroup({
105
+ title: "My Group", // Group name
106
+ x: 0, // X position
107
+ y: 0, // Y position
108
+ width: 400, // Width (min: 100)
109
+ height: 300, // Height (min: 60)
110
+ color: "#2d3748", // Background color
111
+ members: [node1.id, node2.id], // Nodes to include
112
+ });
113
+ ````
114
+
115
+ ### Group Features
116
+
117
+ - **Hierarchical Structure**: Groups can contain multiple nodes
118
+ - **Automatic Movement**: Nodes inside the group move with the group
119
+ - **Resizable**: Resize groups using the handle in the bottom-right corner
120
+ - **Custom Colors**: Set group colors for visual organization
121
+ - **Local Coordinates**: World and local coordinates automatically managed
122
+
123
+ ### Advanced Group Operations
124
+
125
+ ```javascript
126
+ // Access GroupManager
127
+ const groupManager = graph.groupManager;
128
+
129
+ // Reparent a node to a group
130
+ graph.reparent(node, group);
131
+
132
+ // Remove node from group (reparent to root)
133
+ graph.reparent(node, null);
134
+
135
+ // Resize a group
136
+ groupManager.resizeGroup(group.id, 50, 50); // add 50px to width and height
137
+
138
+ // Remove a group (children are un-grouped)
139
+ groupManager.removeGroup(group.id);
140
+
141
+ // Listen to group events
142
+ hooks.on("group:change", () => {
143
+ console.log("Group structure changed");
144
+ });
145
+ ```
146
+
147
+ ---
148
+
149
+ ## 🌐 HTML Overlays
150
+
151
+ Create interactive HTML UIs inside nodes.
152
+
153
+ ### Basic Example
154
+
155
+ ```javascript
156
+ registry.register("ui/TextInput", {
157
+ title: "Text Input",
158
+ size: { w: 220, h: 100 },
159
+ outputs: [{ name: "text", datatype: "string" }],
160
+
161
+ html: {
162
+ init(node, el, { header, body }) {
163
+ el.style.backgroundColor = "#1a1a1a";
164
+ el.style.borderRadius = "8px";
165
+
166
+ const input = document.createElement("input");
167
+ input.type = "text";
168
+ input.placeholder = "Enter text...";
169
+ Object.assign(input.style, {
170
+ width: "100%",
171
+ padding: "8px",
172
+ background: "#111",
173
+ border: "1px solid #444",
174
+ color: "#fff",
175
+ pointerEvents: "auto", // IMPORTANT: Enable interaction
176
+ });
177
+
178
+ input.addEventListener("input", (e) => {
179
+ node.state.text = e.target.value;
180
+ hooks.emit("node:updated", node);
181
+ });
182
+
183
+ input.addEventListener("mousedown", (e) => e.stopPropagation()); // Prevent drag
184
+
185
+ body.appendChild(input);
186
+ el._input = input;
187
+ },
188
+
189
+ update(node, el, { selected }) {
190
+ el.style.borderColor = selected ? "#3b82f6" : "#333";
191
+ if (el._input.value !== (node.state.text || "")) {
192
+ el._input.value = node.state.text || "";
193
+ }
194
+ },
195
+ },
196
+
197
+ onCreate(node) {
198
+ node.state.text = "";
199
+ },
200
+
201
+ onExecute(node, { setOutput }) {
202
+ setOutput("text", node.state.text || "");
203
+ },
204
+ });
205
+ ```
206
+
207
+ ### Best Practices
208
+
209
+ 1. **Enable Interaction**: Set `pointerEvents: "auto"` on interactive elements
210
+ 2. **Stop Propagation**: Prevent canvas drag with `e.stopPropagation()` on `mousedown`
211
+ 3. **Update State**: Emit `"node:updated"` when state changes
212
+ 4. **Store References**: Cache DOM elements in `el._refs` for performance
213
+ 5. **Port Visibility**: HTML overlays are rendered below ports for proper visibility
214
+
215
+ ---
216
+
217
+ ## 🔌 Port Types
218
+
219
+ HTML-overlay-Node supports two types of ports for different purposes:
220
+
221
+ ### Exec Ports (Flow Control)
222
+
223
+ Exec ports control the execution flow between nodes.
224
+
225
+ ```javascript
226
+ registry.register("util/Print", {
227
+ title: "Print",
228
+ inputs: [
229
+ { name: "exec", portType: "exec" }, // Execution input
230
+ { name: "value", portType: "data", datatype: "any" },
231
+ ],
232
+ outputs: [
233
+ { name: "exec", portType: "exec" }, // Execution output
234
+ ],
235
+ onExecute(node, { getInput }) {
236
+ console.log("[Print]", getInput("value"));
237
+ },
238
+ });
239
+ ```
240
+
241
+ ### Data Ports (Values)
242
+
243
+ Data ports transfer values between nodes.
244
+
245
+ ```javascript
246
+ registry.register("math/Add", {
247
+ title: "Add",
248
+ inputs: [
249
+ { name: "exec", portType: "exec" },
250
+ { name: "a", portType: "data", datatype: "number" },
251
+ { name: "b", portType: "data", datatype: "number" },
252
+ ],
253
+ outputs: [
254
+ { name: "exec", portType: "exec" },
255
+ { name: "result", portType: "data", datatype: "number" },
256
+ ],
257
+ onExecute(node, { getInput, setOutput }) {
258
+ const result = (getInput("a") ?? 0) + (getInput("b") ?? 0);
259
+ setOutput("result", result);
260
+ },
261
+ });
262
+ ```
263
+
264
+ ### Visual Style
265
+
266
+ - **Exec ports**: Emerald green rounded squares (8×8px)
267
+ - **Data ports**: Indigo blue circles (10px diameter)
268
+ - Both have subtle outlines for depth
269
+
270
+ ---
271
+
272
+ ## ⌨️ Keyboard Shortcuts
273
+
274
+ | Shortcut | Action |
275
+ | ------------------------------- | --------------------------- |
276
+ | **Selection** | |
277
+ | `Click` | Select node |
278
+ | `Shift + Click` | Add to selection |
279
+ | `Ctrl + Drag` | Box select |
280
+ | **Editing** | |
281
+ | `Delete` | Delete selected nodes |
282
+ | `Ctrl + Z` | Undo |
283
+ | `Ctrl + Y` / `Ctrl + Shift + Z` | Redo |
284
+ | **Grouping** | |
285
+ | `Ctrl + G` | Create group from selection |
286
+ | **Alignment** | |
287
+ | `A` | Align nodes horizontally |
288
+ | `Shift + A` | Align nodes vertically |
289
+ | **Tools** | |
290
+ | `G` | Toggle snap-to-grid |
291
+ | `?` | Toggle shortcuts help |
292
+ | **Navigation** | |
293
+ | `Middle Click + Drag` | Pan canvas |
294
+ | `Mouse Wheel` | Zoom in/out |
295
+ | `Right Click` | Context menu |
296
+
297
+ ---
298
+
299
+ ## 📚 Complete API
300
+
301
+ For full API documentation, see the comments in [src/index.js](src/index.js).
302
+
303
+ ### Editor API
304
+
305
+ | Property | Description |
306
+ | ------------------- | --------------------- |
307
+ | `graph` | Graph instance |
308
+ | `registry` | Node type registry |
309
+ | `hooks` | Event system |
310
+ | `render()` | Trigger manual render |
311
+ | `start()` | Start execution loop |
312
+ | `stop()` | Stop execution loop |
313
+ | `addGroup(options)` | Create a group |
314
+ | `destroy()` | Cleanup |
315
+
316
+ ### Key Methods
317
+
318
+ - `registry.register(type, definition)` - Register node type
319
+ - `graph.addNode(type, options)` - Create node
320
+ - `graph.addEdge(from, fromPort, to, toPort)` - Connect nodes
321
+ - `graph.toJSON()` / `graph.fromJSON(json)` - Serialize/deserialize
322
+ - `hooks.on(event, callback)` - Subscribe to events
323
+
324
+ ### Available Events
325
+
326
+ - `node:create` | `node:move` | `node:resize` | `node:updated`
327
+ - `edge:create` | `edge:delete`
328
+ - `group:change`
329
+ - `runner:start` | `runner:stop` | `runner:tick`
330
+ - `error`
331
+
332
+ ---
333
+
334
+ ## 🎨 Customization
335
+
336
+ ### Theme Colors
337
+
338
+ ```javascript
339
+ const editor = createGraphEditor(canvas, {
340
+ theme: {
341
+ bg: "#0d0d0f", // Canvas background
342
+ grid: "#1a1a1d", // Grid lines
343
+ node: "#16161a", // Node background
344
+ nodeBorder: "#2a2a2f", // Node border
345
+ title: "#1f1f24", // Node header
346
+ text: "#e4e4e7", // Primary text
347
+ textMuted: "#a1a1aa", // Secondary text
348
+ port: "#6366f1", // Data port color (indigo)
349
+ portExec: "#10b981", // Exec port color (emerald)
350
+ edge: "#52525b", // Edge color
351
+ edgeActive: "#8b5cf6", // Active edge (purple)
352
+ accent: "#6366f1", // Accent color
353
+ accentBright: "#818cf8", // Bright accent
354
+ },
355
+ });
356
+ ```
357
+
358
+ ### Edge Styles
359
+
360
+ ```javascript
361
+ // Set edge style
362
+ editor.renderer.setEdgeStyle("orthogonal"); // or "curved", "line"
363
+ ```
364
+
365
+ ### Custom Node Drawing
366
+
367
+ ```javascript
368
+ registry.register("visual/Circle", {
369
+ title: "Circle",
370
+ size: { w: 120, h: 120 },
371
+ onDraw(node, { ctx, theme }) {
372
+ const { x, y, width, height } = node.computed;
373
+ const centerX = x + width / 2;
374
+ const centerY = y + height / 2;
375
+ const radius = Math.min(width, height) / 3;
376
+
377
+ ctx.beginPath();
378
+ ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
379
+ ctx.fillStyle = theme.wire;
380
+ ctx.fill();
381
+ },
382
+ });
383
+ ```
384
+
385
+ ---
386
+
387
+ ## 💾 Serialization
388
+
389
+ ```javascript
390
+ // Save
391
+ const json = graph.toJSON();
392
+ localStorage.setItem("myGraph", JSON.stringify(json));
393
+
394
+ // Load
395
+ const saved = JSON.parse(localStorage.getItem("myGraph"));
396
+ graph.fromJSON(saved);
397
+ ```
398
+
399
+ ---
400
+
401
+ ## 🐛 Troubleshooting
402
+
403
+ | Issue | Solution |
404
+ | ---------------------------- | --------------------------------------------------- |
405
+ | Canvas not rendering | Ensure canvas has explicit width/height |
406
+ | Nodes not executing | Call `start()` or set `autorun: true` |
407
+ | Type errors | Register node types before using them |
408
+ | HTML overlay not interactive | Set `pointerEvents: "auto"` on elements |
409
+ | Performance issues | Limit to <1000 nodes, optimize `onExecute`/`onDraw` |
410
+
411
+ ---
412
+
413
+ ## 🤝 Contributing
414
+
415
+ Contributions welcome! See [CONTRIBUTING.md](CONTRIBUTING.md).
416
+
417
+ ```bash
418
+ npm install # Install dependencies
419
+ npm run dev # Start dev server
420
+ npm test # Run tests
421
+ npm run lint # Check code quality
422
+ npm run build # Build library
423
+ ```
424
+
425
+ ---
426
+
427
+ ## 📄 License
428
+
429
+ [MIT](LICENSE) © cheonghakim
430
+
431
+ ---
432
+
433
+ ## 🔗 Links
434
+
435
+ - [GitHub Repository](https://github.com/cheonghakim/html-overlay-node)
436
+ - [Issue Tracker](https://github.com/cheonghakim/html-overlay-node/issues)
437
+ - [Changelog](CHANGELOG.md)
@@ -0,0 +1,26 @@
1
+ // src/core/CommandStack.js
2
+ export class CommandStack {
3
+ constructor() {
4
+ this.undoStack = [];
5
+ this.redoStack = [];
6
+ }
7
+ exec(cmd) {
8
+ cmd.do();
9
+ this.undoStack.push(cmd);
10
+ this.redoStack.length = 0;
11
+ }
12
+ undo() {
13
+ const c = this.undoStack.pop();
14
+ if (c) {
15
+ c.undo();
16
+ this.redoStack.push(c);
17
+ }
18
+ }
19
+ redo() {
20
+ const c = this.redoStack.pop();
21
+ if (c) {
22
+ c.do();
23
+ this.undoStack.push(c);
24
+ }
25
+ }
26
+ }
@@ -0,0 +1,28 @@
1
+ import { randomUUID } from "../utils/utils.js";
2
+
3
+ // src/core/Edge.js
4
+
5
+ /**
6
+ * Edge represents a connection between two node ports
7
+ */
8
+ export class Edge {
9
+ /**
10
+ * Create a new Edge
11
+ * @param {Object} options - Edge configuration
12
+ * @param {string} [options.id] - Unique identifier (auto-generated if not provided)
13
+ * @param {string} options.fromNode - Source node ID
14
+ * @param {string} options.fromPort - Source port ID
15
+ * @param {string} options.toNode - Target node ID
16
+ * @param {string} options.toPort - Target port ID
17
+ */
18
+ constructor({ id, fromNode, fromPort, toNode, toPort }) {
19
+ if (!fromNode || !fromPort || !toNode || !toPort) {
20
+ throw new Error("Edge requires fromNode, fromPort, toNode, and toPort");
21
+ }
22
+ this.id = id ?? randomUUID();
23
+ this.fromNode = fromNode;
24
+ this.fromPort = fromPort;
25
+ this.toNode = toNode;
26
+ this.toPort = toPort;
27
+ }
28
+ }
@@ -0,0 +1,73 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { Edge } from "./Edge.js";
3
+
4
+ describe("Edge", () => {
5
+ describe("constructor", () => {
6
+ it("should create an edge with all required fields", () => {
7
+ const edge = new Edge({
8
+ fromNode: "node-1",
9
+ fromPort: "port-1",
10
+ toNode: "node-2",
11
+ toPort: "port-2",
12
+ });
13
+
14
+ expect(edge.fromNode).toBe("node-1");
15
+ expect(edge.fromPort).toBe("port-1");
16
+ expect(edge.toNode).toBe("node-2");
17
+ expect(edge.toPort).toBe("port-2");
18
+ expect(edge.id).toBeDefined();
19
+ });
20
+
21
+ it("should use custom ID if provided", () => {
22
+ const edge = new Edge({
23
+ id: "custom-edge-id",
24
+ fromNode: "node-1",
25
+ fromPort: "port-1",
26
+ toNode: "node-2",
27
+ toPort: "port-2",
28
+ });
29
+
30
+ expect(edge.id).toBe("custom-edge-id");
31
+ });
32
+
33
+ it("should throw error when fromNode is missing", () => {
34
+ expect(() => {
35
+ new Edge({
36
+ fromPort: "port-1",
37
+ toNode: "node-2",
38
+ toPort: "port-2",
39
+ });
40
+ }).toThrow(/requires fromNode, fromPort, toNode, and toPort/);
41
+ });
42
+
43
+ it("should throw error when fromPort is missing", () => {
44
+ expect(() => {
45
+ new Edge({
46
+ fromNode: "node-1",
47
+ toNode: "node-2",
48
+ toPort: "port-2",
49
+ });
50
+ }).toThrow(/requires fromNode, fromPort, toNode, and toPort/);
51
+ });
52
+
53
+ it("should throw error when toNode is missing", () => {
54
+ expect(() => {
55
+ new Edge({
56
+ fromNode: "node-1",
57
+ fromPort: "port-1",
58
+ toPort: "port-2",
59
+ });
60
+ }).toThrow(/requires fromNode, fromPort, toNode, and toPort/);
61
+ });
62
+
63
+ it("should throw error when toPort is missing", () => {
64
+ expect(() => {
65
+ new Edge({
66
+ fromNode: "node-1",
67
+ fromPort: "port-1",
68
+ toNode: "node-2",
69
+ });
70
+ }).toThrow(/requires fromNode, fromPort, toNode, and toPort/);
71
+ });
72
+ });
73
+ });