import type {useSvelteFlow} from '@xyflow/svelte'; import {componentName} from './consts'; import {store} from './store/stores.svelte'; import type {TinyflowData, TinyflowOptions, TinyflowTheme} from './types'; type FlowInstance = ReturnType; export class Tinyflow { private options!: TinyflowOptions; private rootEl!: Element; private svelteFlowInstance!: FlowInstance; private tinyflowEl!: HTMLElement & { options: TinyflowOptions; onInit: (svelteFlowInstance: FlowInstance) => void; }; constructor(options: TinyflowOptions) { if ( typeof options.element !== 'string' && !(options.element instanceof Element) ) { throw new Error('element must be a string or Element'); } this._setOptions(options); this._init(); } private _init() { if (typeof this.options.element === 'string') { this.rootEl = document.querySelector(this.options.element)!; if (!this.rootEl) { throw new Error( `element not found by document.querySelector('${this.options.element}')`, ); } } else if (this.options.element instanceof Element) { this.rootEl = this.options.element; } else { throw new Error('element must be a string or Element'); } this.tinyflowEl = this._createTinyflowElement(); this.rootEl.appendChild(this.tinyflowEl); } private _setOptions(options: TinyflowOptions) { this.options = { theme: options.theme || 'light', ...options, }; } private _getFlowInstance() { if (!this.svelteFlowInstance) { console.warn('Tinyflow instance is not initialized'); return null; } return this.svelteFlowInstance; } private _applyThemeClass(targetEl: Element, theme?: TinyflowTheme) { targetEl.classList.remove('tf-theme-light', 'tf-theme-dark'); targetEl.classList.add( theme === 'dark' ? 'tf-theme-dark' : 'tf-theme-light', ); } private _createTinyflowElement() { const tinyflowEl = document.createElement(componentName) as HTMLElement & { options: TinyflowOptions; onInit: (svelteFlowInstance: FlowInstance) => void; }; tinyflowEl.style.display = 'block'; tinyflowEl.style.width = '100%'; tinyflowEl.style.height = '100%'; this._applyThemeClass(tinyflowEl, this.options.theme); tinyflowEl.options = this.options; tinyflowEl.onInit = (svelteFlowInstance: FlowInstance) => { this.svelteFlowInstance = svelteFlowInstance; }; return tinyflowEl; } getOptions() { return this.options; } getData() { const flow = this._getFlowInstance(); if (!flow) { return null; } return flow.toObject(); } updateData(data: TinyflowData, options?: { preserveViewport?: boolean }) { const flow = this._getFlowInstance(); if (!flow) { return false; } const currentViewport = flow.getViewport(); const currentNodes = flow.getNodes(); const currentNodePositions = new Map( currentNodes.map((node) => [node.id, node.position]), ); const nextNodes = options?.preserveViewport === true ? (data.nodes || currentNodes).map((node) => { const currentPosition = currentNodePositions.get(node.id); return currentPosition ? { ...node, position: { ...currentPosition } } : node; }) : data.nodes || currentNodes; store.setNodes(nextNodes); store.setEdges(data.edges || flow.getEdges()); if (data.viewport && options?.preserveViewport !== true) { flow.setViewport(data.viewport, { duration: 0 }); } else { flow.setViewport(currentViewport, { duration: 0 }); } return true; } async focusNode( nodeId: string, options?: { duration?: number; zoom?: number }, ) { const flow = this._getFlowInstance(); if (!flow) { return false; } const targetNode = flow.getNode(nodeId); if (!targetNode) { return false; } // Keep only the target node selected so the canvas has a clear visual focus. flow.getNodes().forEach((node) => { flow.updateNode(node.id, { selected: node.id === nodeId }); }); const internalNode = flow.getInternalNode(nodeId) as any; const absolutePosition = internalNode?.internals?.positionAbsolute || (targetNode as any)?.positionAbsolute || targetNode.position || { x: 0, y: 0 }; const width = internalNode?.measured?.width || (targetNode as any)?.measured?.width || (targetNode as any)?.width || 260; const height = internalNode?.measured?.height || (targetNode as any)?.measured?.height || (targetNode as any)?.height || 120; const centerX = absolutePosition.x + width / 2; const centerY = absolutePosition.y + height / 2; const nextZoom = options?.zoom ?? Math.max(flow.getZoom(), 1); await flow.setCenter(centerX, centerY, { zoom: nextZoom, duration: options?.duration ?? 280, }); return true; } async fitView(options?: { duration?: number; padding?: number }) { const flow = this._getFlowInstance(); if (!flow) { return false; } await flow.fitView({ duration: options?.duration ?? 220, padding: options?.padding ?? 0.2, }); return true; } setTheme(theme: TinyflowTheme) { this.options.theme = theme; if (this.tinyflowEl) { this._applyThemeClass(this.tinyflowEl, theme); } } setData(data: TinyflowData) { this.options.data = data; this.tinyflowEl = this._createTinyflowElement(); this.destroy(); this.rootEl.appendChild(this.tinyflowEl); } destroy() { while (this.rootEl.firstChild) { this.rootEl.removeChild(this.rootEl.firstChild); } } }