import { html, css, LitElement } from "lit"; import { customElement, query } from "lit/decorators.js"; import { CanvasNode, Canvas } from "./canvas"; import { BaseTreeNode, styles as treeStyles, template as treeTemplate } from "./ui/tree-view"; const PROPERTY_WIDTH = 200; @customElement("node-editor") export class NodeEditor extends LitElement { editor = new Canvas(this, { size: { width: window.innerWidth - PROPERTY_WIDTH * 2, height: window.innerHeight, }, }); @query("#output") outputContainer!: HTMLDivElement; private _resizeObserver = new ResizeObserver((entries) => { for (const entry of entries) { const { width, height } = entry.contentRect; this.editor.resize({ width, height }); } }); static styles = [ treeStyles, css` :host { display: block; width: 100%; height: 100%; } main { height: 100%; width: 100%; display: flex; flex-direction: row; } #output { flex: 1; overflow: hidden; } .sidebar { width: ${PROPERTY_WIDTH}px; overflow-y: auto; background-color: #f5f5f5; border-right: 1px solid #ddd; } #properties { width: ${PROPERTY_WIDTH}px; display: flex; flex-direction: column; overflow-y: auto; background-color: #eee; border-left: 1px solid #ddd; } .property { display: flex; flex-direction: column; padding: 10px; } .title { font-size: 1.5em; font-weight: bold; padding: 10px; } .destructive { background-color: red; color: white; } #links > span { padding-left: 10px; font-size: 0.9em; font-weight: bold; } `, ]; render() { return html`
${this.editor.canvas}
${this.renderProperties()}
`; } renderProperties() { const nodeId = this.editor.selection[this.editor.selection.length - 1]; const node = this.editor.store.retrieveNode(nodeId) ?? this.editor.store.retrieveEdge(nodeId); if (node?.type === "node") { return html` Node
{ node.name = e.target.value; this.editor.store.updateNode(node); }} />
{ node.backgroundColor = e.target.value; this.editor.store.updateNode(node); }} />
{ node.width = Number(e.target.value); this.editor.store.updateNode(node); }} />
{ node.height = Number(e.target.value); this.editor.store.updateNode(node); }} />
`; } if (node?.type === "edge") { return html` Edge
{ node.name = e.target.value; this.editor.store.updateEdge(node); }} />
`; } return html` Editor
{ const files = e.target.files; if (files.length) { this.editor.clear(); const reader = new FileReader(); reader.onload = (e: any) => { const data = e.target.result; this.editor.import(data); }; reader.readAsText(files[0]); } }} />
{ this.editor.zoom(Number(e.target.value)); }} />
{ this.editor.rotate(Number(e.target.value)); }} />
{ this.editor.pan({ x: Number(e.target.value), y: this.editor.offset.y, }); }} />
{ this.editor.pan({ x: this.editor.offset.x, y: Number(e.target.value), }); }} />
`; } firstUpdated() { this._resizeObserver.observe(this.outputContainer); const amount = 10; // Create random canvas nodes for (let i = 0; i < amount; i++) { this.addRandomNode(i); } // Link random canvas nodes for (let i = 0; i < amount / 2; i++) { const source: CanvasNode = this.editor.store.nodes[i]; const target: CanvasNode = this.editor.store.nodes[i + amount / 2]; this.editor.store.linkNodes(source, target, "simple"); } } disconnectedCallback() { super.disconnectedCallback(); this._resizeObserver.disconnect(); } addRandomNode(i: number = this.editor.store.nodes.length) { const node: CanvasNode = { id: `node${i}`, name: "Node " + i, x: Math.random() * this.editor.canvas.width, y: Math.random() * this.editor.canvas.height, width: 100, height: 100, type: "node", }; this.editor.store.createNode(node); return node; } } declare global { interface HTMLElementTagNameMap { "node-editor": NodeEditor; } }