import type {useSvelteFlow} from '@xyflow/svelte'; import {componentName} from './consts'; import type {TinyflowStore} from './store/stores.svelte'; import type {TinyflowData, TinyflowOptions, TinyflowTheme} from './types'; import {installTinyflowBrowserCompat} from './utils/compat'; import {createTinyflowNodeNormalizer} from './utils/nodeInteraction'; installTinyflowBrowserCompat(); type FlowInstance = ReturnType; export class Tinyflow { private options!: TinyflowOptions; private rootEl!: Element; private svelteFlowInstance!: FlowInstance; private store!: TinyflowStore; private tinyflowEl!: HTMLElement & { options: TinyflowOptions; onInit: ( svelteFlowInstance: FlowInstance, store: TinyflowStore, ) => 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, store: TinyflowStore, ) => 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, store: TinyflowStore, ) => { this.svelteFlowInstance = svelteFlowInstance; this.store = store; }; return tinyflowEl; } getOptions() { return this.options; } getData() { const flow = this._getFlowInstance(); if (!flow) { return null; } this.store.flushPendingEdits(); return { ...flow.toObject(), nodes: this.store.getNodes(), edges: this.store.getEdges(), }; } updateData(data: TinyflowData, options?: { preserveViewport?: boolean }) { const flow = this._getFlowInstance(); if (!flow) { return false; } const normalizeNode = createTinyflowNodeNormalizer(this.options); if (data.nodes) { const currentNodePositions = options?.preserveViewport === true ? new Map( flow.getNodes().map((node) => [node.id, node.position] as const), ) : null; const nextNodes = data.nodes.map((node) => { const currentPosition = currentNodePositions?.get(node.id); const nextNode = currentPosition ? { ...node, position: { ...currentPosition } } : node; return normalizeNode(nextNode); }); this.store.setNodes(nextNodes); } if (data.edges) { this.store.setEdges(data.edges); } if (data.viewport && options?.preserveViewport !== true) { flow.setViewport(data.viewport, { 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); } } }