diff --git a/.github/assets/node-shapes-simple.png b/.github/assets/node-shapes-simple.png new file mode 100644 index 0000000..cd00e6c Binary files /dev/null and b/.github/assets/node-shapes-simple.png differ diff --git a/.gitignore b/.gitignore index 1aba88e..5b8e860 100644 --- a/.gitignore +++ b/.gitignore @@ -49,7 +49,7 @@ coverage/ # Generated types (if using vite-plugin-dts or tsc emit to dist) dist/**/*.d.ts -types/ +/types/ typings/ # Lockfiles (if you prefer to ignore some; typically keep one) diff --git a/README.md b/README.md index 2492982..5cee7fb 100644 --- a/README.md +++ b/README.md @@ -1 +1,38 @@ -# Node Shapes \ No newline at end of file +# Node Shapes +Node shapes is an npm package (node-shapes) which allow to create nodes and render them as shapes in a canvas that can be dragged (moved). + +[![0.0.1](https://static-production.npmjs.com/da3ab40fb0861d15c83854c29f5f2962.png) npm package](https://www.npmjs.com/package/node-shapes) + +### Shapes +You can add more custom shapes. This package have default shapes which include: +- rectangle +- circle (being developed) + +## How to Install +To install npm package make sure you have npm installed and access to the internet. + +Open your terminal and under your project run the following command: +``` +npm install node-shapes +``` +**Note:** Make sure to run through the setup to setup shapes. + +## How to Setup +To set up the shapes run the following command: +``` +npx node-shapes init +``` +This will create a new folder as `default-shapes` within your project's `public` folder. This folder is where all the shapes will exist. You can add custom shapes within this folder as well. To keep custom shapes organized from the default shapes, it is recommended that you keep custom shapes in there own dedicated folder created inside `public/default-shapes`. + +--- +## Simple Example of Usage +The JavaScript code bellow creates a simple block that is draggable and the canvas with 20 px on all sides. +``` +const nodeShapes = new NodeShapes("node-shapes-graph"); // declare by passing the id or the element. +nodeShapes.canvasPadding = {inline: 20, block: 20}; // set the padding inside the canvas. + +const node = nodeShapes.node("Sample Text", {size: {width: "400", height: "100"}, draggable: true}); // creates the node. + +node.add(); // Add the node to the canvas (visual). +``` +![Node Shapes Representation](.github/assets/node-shapes-simple.png) \ No newline at end of file diff --git a/bin/init.js b/bin/init.js new file mode 100644 index 0000000..36376c1 --- /dev/null +++ b/bin/init.js @@ -0,0 +1,37 @@ +#!/usr/bin/env node +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +function initPackage() { + // 1. Locate the user's current project working directory + const userProjectRoot = process.cwd(); + const targetPublicFolder = path.join(userProjectRoot, 'public', 'my-svg-package'); + + // 2. Locate your package's asset folder inside node_modules + const packageAssetFolder = path.join(__dirname, '../dist/svgs'); + + try { + // Create the target folder in their public directory if it doesn't exist + if (!fs.existsSync(targetPublicFolder)) { + fs.mkdirSync(targetPublicFolder, { recursive: true }); + } + + // 3. Copy the files over + const files = fs.readdirSync(packageAssetFolder); + for (const file of files) { + fs.copyFileSync( + path.join(packageAssetFolder, file), + path.join(targetPublicFolder, file) + ); + } + + console.log('✅ Successfully copied 100+ SVGs to your public/my-svg-package/ directory!'); + } catch (error) { + console.error('❌ Failed to copy assets automatically:', error.message); + } +} + +initPackage(); \ No newline at end of file diff --git a/index.html b/index.html index 0a97d59..c1fd680 100644 --- a/index.html +++ b/index.html @@ -9,17 +9,35 @@

Node Shapes

- - + + + - +
+ \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 55e7c89..eb83911 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,18 +8,47 @@ "name": "node-shapes", "version": "0.0.1", "license": "MIT", + "dependencies": { + "uuid": "^14.0.2" + }, "devDependencies": { "typescript": "^6.0.3", "vite": "^8.0.16" } }, + "node_modules/@emnapi/core": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.3.tgz", + "integrity": "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.3", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz", + "integrity": "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==", "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "tslib": "^2.4.0" } @@ -276,6 +305,40 @@ "node": "^20.19.0 || >=22.12.0" } }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@rolldown/binding-win32-arm64-msvc": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", @@ -664,7 +727,6 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -784,6 +846,19 @@ "node": ">=14.17" } }, + "node_modules/uuid": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.2.tgz", + "integrity": "sha512-xZe/16rV4aa+HGSOCiY2YeLT1OybRLrrkL/Rqaq7p7GMVXjFh+6wN4oMYgjFmnSnhY8t6Xpdl2l9qmnHYuMHwQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, "node_modules/vite": { "version": "8.0.16", "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", diff --git a/package.json b/package.json index f99e3cb..9a962c2 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "index.js", "license": "MIT", "private": false, - "type": "commonjs", + "type": "module", "scripts": { "dev": "vite", "build": "vite build", @@ -32,5 +32,8 @@ "devDependencies": { "typescript": "^6.0.3", "vite": "^8.0.16" + }, + "dependencies": { + "uuid": "^14.0.2" } } diff --git a/public/default-shapes/rect.svg b/public/default-shapes/rect.svg new file mode 100644 index 0000000..7cd4a74 --- /dev/null +++ b/public/default-shapes/rect.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/public/script.js b/public/script.js new file mode 100644 index 0000000..6a91986 --- /dev/null +++ b/public/script.js @@ -0,0 +1,9 @@ +import NodeShapes from '../src/index'; + +const text = "Sample Text"; + +const nodeShapes = new NodeShapes("node-shapes-graph"); +nodeShapes.canvasPadding = {inline: 20, block: 20}; +console.log(nodeShapes); +const node = nodeShapes.node(text, {size: {width: "400", height: "100"}, draggable: true}); +node.add(); diff --git a/src/default-shapes/rect.svg b/src/default-shapes/rect.svg deleted file mode 100644 index e69de29..0000000 diff --git a/src/index.ts b/src/index.ts index 5de3f5f..2e74ae6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,27 +1,151 @@ -(function(){ - const TEXT_FOREIGN_OBJ = document.getElementById('node-shapes-text-foreign-object') as HTMLElement; - const SHAPE = document.getElementById('node-shapes-text-shape'); - const TEXT = document.getElementById('node-shapes-text') as HTMLElement; - - function applyClamp() { - const style = getComputedStyle(TEXT); - // parse line-height: if numeric (e.g., "16.8px") use that; fallback to font-size * line-height multiplier - let lineHeightPx = parseFloat(style.lineHeight); - if (isNaN(lineHeightPx)) { - const fontSizePx = parseFloat(style.fontSize); - const lh = parseFloat(style.lineHeight) || 1.2; - lineHeightPx = fontSizePx * lh; + +/* TODO: + = Create a function inside canvas to hide overflow and make it default. + - create text overflow handler func for node. + - Create hash map / set for the nodes. + - Create a function to resize and fit. + - Put Node class as default export in seprate file / dir. +*/ + +// ============================ + +// import utils +import Shapes from './utils/shapes.js'; + +// import modules +import Node from './modules/node.js'; + +/** + * NodeShapes provides the ability to create nodes, edges (connection between nodes object) + * and display them in visual representation. + * @example + * const nodeShapes = new NodeShapes("node-shapes-target-canvas-element-id"); + * @constructor Set's the canvas in instance. If id of element was given it finds the element. + * @throws If element id is not found or the given value is not a valid HTML element or id. + */ +class NodeShapes { + // private _canvas: HTMLElement; + private _canvas: SVGSVGElement; + /** Set's default draggable property. @default false */ + public draggable: boolean = false; + /** Enables Context menu for nodes. @default true */ + public contextMenu: boolean = true; + /** Takes a number for padding inline and block in pixels. Number could be in single decimal. + * @example canvasPadding = {inline: 20, block: 20} //or + * canvasPadding = {inline: 19.8, block: 17.67} + */ + private _canvasPadding: {inline: number, block: number} | null = null; + /** Default padding for nodes. @default "16px" */ + public defaultNodePadding: string = "16px"; + /** Default font family for nodes. @default null // uses the default font set's on your html body or parent. */ + public defaultNodeFontFamily: string | null = null; + /** Default font size for nodes. @default "16px" */ + public defaultNodeFontSize: string = "16px"; + /** Canvas Background. Expects a url or path to imagefile */ + public canvasBackground: string = ""; + public panSize: {width: string, height: string} | null = null; + private _nodes: object = {}; + private _edges: object = {}; + private _shapes = new Shapes(); + + /** + * Set's the canvas in instance. If id of element was given it finds the element. + * @param canvas - HTML element or element id to access for representing nodes and edges. + * @throws If element id is not found or the given value is not a valid HTML element or id. + */ + constructor(canvas: string | HTMLElement) { + const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + svg.id = "node-shapes-canvas"; + svg.setAttribute("xmlns", "http://www.w3.org/2000/svg"); + svg.style = "width: 100%; height: 100%; box-sizing: border-box;"; + // svg.setAttribute("width", "100"); + // svg.setAttribute("height", "100"); + + if (typeof(canvas) == "string") { + const element = document.getElementById(canvas); + if (element) { + element.appendChild(svg); + this._canvas = svg; + } + else throw new Error(`Node Shapes: No such element having an id of '${canvas}' was found!`); + } + else { + canvas.appendChild(svg); + this._canvas = svg; } - // use foreignObject height (or rect height) - const foreignObjHeight = parseFloat(TEXT_FOREIGN_OBJ.getAttribute('height') ?? '') || TEXT.clientHeight; - const paddingTopBottom = 16; // matches padding in CSS (8px top + 8px bottom) - const available = Math.max(0, foreignObjHeight - paddingTopBottom); - const maxLines = Math.floor(available / lineHeightPx) || 1; - TEXT.style.webkitLineClamp = String(maxLines); - TEXT.style.maxHeight = (maxLines * lineHeightPx) + 'px'; - } - - // init + observe resize of svg/rect - applyClamp(); - new ResizeObserver(applyClamp).observe(document.getElementById('svg') as HTMLElement); -})(); \ No newline at end of file + this.injectCSS(); + } + + /** + * Create's the Node Object. + * @param text - Text to display inside a node. + * @param id - By default it uses uuid if not provided explicitly. Must be *unique*! + * @param shape - Default options are "rectangle" | "circle" | "triangle". + * @param draggable - Makes nodes object moveable inside canvas. By default it's **False**. + * @param size - Sets the size (px: pixels) of node by the given width and height. By default it's **{width: "200", height: "40"}** + * @returns Returns the **Node** Object. + * @throws If the id isn't a valid id (unique string). + */ + public node(text: string, {id, shape, draggable, size}: {id: string, shape: string, draggable: boolean, size: {width: string, height: string}}): Node { + let edges: Array = []; + const NODE: Node = new Node(text, id, shape, draggable, size, this); + + return NODE; + } + + public edge(id: string, from: void, to: void) { + // + } + + get canvas(): SVGSVGElement { + return this._canvas; + } + + /** + * addShape + */ + public addShape() { + + } + get shapes() { + return this._shapes; + } + + get nodes() { + return this._nodes; + } + set nodes(data: object) { + // + } + private appendNodes(node: Node) { + // appends the node object in nodes. + } + + get edges() { + return this._edges; + } + set edges(data) { + // + } + + get canvasPadding(): {inline: number, block: number} { + if (this._canvasPadding) return this._canvasPadding; + else return {inline: 0, block: 0}; + } + set canvasPadding(padding: {inline: number, block: number}) { + this._canvasPadding = padding; + this._canvas.style.padding = `${padding.block}px ${padding.inline}px`; + } + + private injectCSS(): void { + const style = document.createElement("style"); + style.textContent = ` + .node-shapes-text-shape { + width: 100%; + height: 100%; + } + `; + document.head.appendChild(style); + } +} +export default NodeShapes; diff --git a/src/modules/node.ts b/src/modules/node.ts new file mode 100644 index 0000000..fe4e5f7 --- /dev/null +++ b/src/modules/node.ts @@ -0,0 +1,101 @@ +// import packages +import {v4 as uuid4} from 'uuid'; + +// import utils +import { NodeFunctions } from '../utils/node.js'; + +// import types +import type NodeShapes from '../index.js'; + +class Node { + private text: string; + private id: string; + private shape: string; + private draggable: boolean; + private nodeShapes: NodeShapes; + private size: {width: string, height: string}; + private canvas: SVGSVGElement; + private canvasPadding: {inline: number, block: number}; + + constructor(text: string = "", id: string = uuid4(), shape: string = "rectangle", draggable: boolean = false, size: {width: string, height: string} = {width: "200", height: "40"}, nodeShapes: NodeShapes) { + this.text = text; + this.id = id; + this.shape = shape; + this.draggable = draggable; + this.nodeShapes = nodeShapes; + this.size = size; + this.canvas = nodeShapes.canvas; + this.canvasPadding = nodeShapes.canvasPadding; + } + + /** + * Add's the node to visual representation (canvas an HTML element) + */ + public async add() { + // console.log(this.nodeShapes.shapes.defaultShapes[this.shape]); + + const svg = await this.nodeShapes.shapes.getShapeSVG(this.shape) as string; + + const parser = new DOMParser(); + const xml = parser.parseFromString(svg, "image/svg+xml"); + + const svgElement = xml.querySelector('svg'); + if(!svgElement) throw new Error(`Target tag not found.`); + svgElement.id = `svg_${this.id}`; + svgElement.setAttribute("draggable", "true"); + svgElement.setAttribute("width", this.size.width); + svgElement.setAttribute("height", this.size.height); + svgElement.setAttribute("viewBox", `0 0 ${this.size.width} ${this.size.height}`); + + const gElement = xml.querySelector('g'); + if(!gElement) throw new Error(`Target tag not found.`); + + const foreignObject = xml.createElementNS("http://www.w3.org/2000/svg", "foreignObject"); + foreignObject.id = `node-shapes-text-foreign-object_${this.id}`; + foreignObject.setAttribute("width", this.size.width); + foreignObject.setAttribute("height", this.size.height); + const textElement: HTMLElement = document.createElement("p"); + textElement.id = `node-shapes-text_${this.id}`; + textElement.setAttribute("style", `width:100%; + line-height:1.2em; + font-size:16px; + display:-webkit-box; + -webkit-box-orient:vertical; + overflow:hidden; + text-align:center; + color: rgba(0,0,0,1);`); + textElement.innerText = `${this.text}`; + + + foreignObject.innerHTML = `
+ ${new XMLSerializer().serializeToString(textElement)} +
`; + + gElement.appendChild(foreignObject); + + const serializer = new XMLSerializer(); + const createdSVG = serializer.serializeToString(xml); + + this.canvas.insertAdjacentHTML("beforeend", createdSVG); + + if (await document.getElementById(svgElement.id) != null) { + const canvas = this.canvas; + const svg = document.getElementById(svgElement.id) as HTMLElement; + const foreignObjectElement = document.getElementById(foreignObject.id) as HTMLElement; + const textObjectElement = document.getElementById(textElement.id) as HTMLElement; + const nodeFunctions = new NodeFunctions(canvas, this.id, svg, foreignObjectElement, textObjectElement); + nodeFunctions.paddingInline = this.canvasPadding.inline; + nodeFunctions.paddingBlock = this.canvasPadding.block; + nodeFunctions.elipse(); + if(this.draggable) nodeFunctions.draggable(svgElement.id); + } + } + + public get(id: string) {} + public getAll() {} + public getJson(id: string) {} + public getAllJson() {} + public update() {}; + public remove() {}; +} +export default Node; \ No newline at end of file diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..02c66f1 --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,18 @@ +// Types +type QueryOptions = { + table: string; + [key: string]: any; +}; +// For canvas size +type CanvasSize = { + width: number; + height: number; + widthUnit: string; + heightUnit: string; +} +// For node & edge data +type ShapeData = { + [key: string]: any; +} + +export type {QueryOptions, CanvasSize, ShapeData}; \ No newline at end of file diff --git a/src/types/shape.ts b/src/types/shape.ts new file mode 100644 index 0000000..cdb8dde --- /dev/null +++ b/src/types/shape.ts @@ -0,0 +1,38 @@ +type Size = { + width: number; + height: number; +} + +type Radius = { + size: number; + unit: 'px' | 'em' | 'rem' | '%'; +} + +type Border = { + size: number; + color: string; + style: 'solid' | 'dashed' | 'dotted' | 'none'; +} + +type Text = { + value: string; + visible: boolean; + color: string; + position: { + vertical: 'top' | 'center' | 'bottom'; + horizontal: 'left' | 'center' | 'bottom'; + }; +} + +export type Shape = { + [key: string]: { + id: string; // to be removed + size: Size; + radius: Radius; + border: Border; + text: Text; + title: string; + cursor: 'default' | 'pointer' | 'move' | 'not-allowed' | string; + background: string, + } +} \ No newline at end of file diff --git a/src/utils/node.ts b/src/utils/node.ts new file mode 100644 index 0000000..b7065d4 --- /dev/null +++ b/src/utils/node.ts @@ -0,0 +1,112 @@ +class NodeFunctions { + private canvas: SVGSVGElement; + private elementId: string; + private svg: HTMLElement; + private TEXT_FOREIGN_OBJ: HTMLElement; + private TEXT: HTMLElement; + public paddingInline: number = 0; + public paddingBlock: number = 0; + + constructor(canvas: SVGSVGElement, elementId: string, svg: HTMLElement, TEXT_FOREIGN_OBJ: HTMLElement, TEXT: HTMLElement) { + this.canvas = canvas; + this.elementId = elementId; + this.svg = svg; + this.TEXT_FOREIGN_OBJ = TEXT_FOREIGN_OBJ; + this.TEXT = TEXT; + } + + elipse(): void { + const TEXT_FOREIGN_OBJ = this.TEXT_FOREIGN_OBJ as HTMLElement; + const TEXT = this.TEXT as HTMLElement; + + function applyClamp() { + const style = getComputedStyle(TEXT); // use html element + // parse line-height: if numeric (e.g., "16.8px") use that; fallback to font-size * line-height multiplier + let lineHeightPx = parseFloat(style.lineHeight); + if (isNaN(lineHeightPx)) { + const fontSizePx = parseFloat(style.fontSize); + const lh = parseFloat(style.lineHeight) || 1.2; + lineHeightPx = fontSizePx * lh; + } + // use foreignObject height (or rect height) + const foreignObjHeight = parseFloat(TEXT_FOREIGN_OBJ.getAttribute('height') ?? '') || TEXT.clientHeight; + const paddingTopBottom = 16; // matches padding in CSS (8px top + 8px bottom) + const available = Math.max(0, foreignObjHeight - paddingTopBottom); + const maxLines = Math.floor(available / lineHeightPx) || 1; + console.log("maxLines", maxLines, available, lineHeightPx); + TEXT.style.webkitLineClamp = String(maxLines); + TEXT.style.maxHeight = (maxLines * lineHeightPx) + 'px'; + } + + // init + observe resize of svg/rect + applyClamp(); + + new ResizeObserver(applyClamp).observe(this.svg as HTMLElement); + } + + draggable(id: string): void { + const element = document.getElementById(id) as HTMLElement; + let dragging: boolean = false; + + let offsetX: number = 0; + let offsetY: number = 0; + let elementWidth: number = 0; + let elementHeight: number = 0; + + element.addEventListener("pointerdown", (event) => { + dragging = true; + + element.setPointerCapture(event.pointerId); + + const rect = element.getBoundingClientRect(); + + offsetX = event.clientX - rect.left; + offsetY = event.clientY - rect.top; + + elementWidth = rect.width; + elementHeight = rect.height; + + console.log('start', event.target); + }); + + element.addEventListener("pointermove", (event) => { + if (!dragging) return; + + const parentRect: DOMRect = this.canvas.getBoundingClientRect(); + + let x: number = event.offsetX - this.paddingInline - offsetX; + let y: number = event.offsetY - this.paddingBlock - offsetY; + console.log("x: ", x, "| y: ", y); + + x = Math.max( + 0, + Math.min(x, parentRect.width - elementWidth - (this.paddingInline * 2)) // padding times 2, because padding is applied on both sides (left and right) + ); + + y = Math.max( + 0, + Math.min(y, parentRect.height - elementHeight - (this.paddingBlock * 2)) // padding times 2, because padding is applied on both ends (top and bottom) + ); + + console.log("Limits", x, y); + console.log("Size", elementWidth, elementHeight); + + console.log("Bounding", parentRect.left, parentRect.top); + + element.style.cursor = "grab"; + element.setAttribute("x", (x).toString()); + element.setAttribute("y", (y).toString()); + + console.log('moving', event.clientX, event.clientY); + }); + + element.addEventListener("pointerup", (event) => { + dragging = false; + element.releasePointerCapture(event.pointerId); + element.style.cursor = "default"; + + console.log('end'); + }); + } +} +export { NodeFunctions }; \ No newline at end of file diff --git a/src/utils/shapes.ts b/src/utils/shapes.ts new file mode 100644 index 0000000..7109956 --- /dev/null +++ b/src/utils/shapes.ts @@ -0,0 +1,65 @@ +// import types +import type {Shape} from "../types/shape.js" + +class Shapes { + private static svgCache: Record = {}; + private _defaultShapes: Shape = { + "rectangle": { + id: crypto.randomUUID(), + size: { + width: 100, + height: 40, + }, + radius: { + size: 0, + unit: 'px', + }, + border: { + size: 0, + color: "rgba(0,0,0,1)", + style: "solid", + }, + text: { + value: '', + visible: true, + color: 'rgba(0,0,0,1)', + position: { + vertical: 'center', + horizontal: 'center' + }, + }, + title: '', + cursor: 'default', + background: 'rgba(220,220,220,1)', + }, + }; + + get defaultShapes() { + return this._defaultShapes; + } + set defaultShapes(data) { + // + } + + public async getShapeSVG(name: string): Promise { + const svgPath = `/default-shapes/${name}.svg`; + if (Shapes.svgCache[svgPath]) { + return Shapes.svgCache[svgPath]; + } + + const response = await fetch('default-shapes/rect.svg', {cache: 'force-cache'}); + + if(!response.ok) { + throw new Error( + `Failed to fetch SVG. Ensure you ran 'npx your-svg-init' ` + + `to copy assets to their local public folder. Status: ${response.status}` + ); + } + + const svg: string = await response.text(); + + return svg; + } +} + +export default Shapes;