UNPKG

@umbraci/jsmind

Version:

jsMind is a pure javascript library for mindmap, it base on html5 canvas. jsMind was released under BSD license, you can embed it in any project, if only you observe the license.

633 lines (632 loc) 26.4 kB
/** * Event callback payload * @typedef {{ evt?: string, data?: unknown[], node?: string }} EventData */ /** * jsMind runtime: orchestrates data/layout/view/shortcut and exposes public API. */ declare class jsMind { static mind: typeof Mind; static node: typeof Node; static direction: import("./jsmind.common.js").DirectionType; static event_type: { show: number; resize: number; edit: number; select: number; reset: number; history_change: number; }; static $: { w: Window; d: Document; g: (id: string) => HTMLElement | null; c: (tag: string) => HTMLElement; t: (n: HTMLElement, t: string) => void; h: (n: HTMLElement, t: string | HTMLElement) => void; i: (el: unknown) => el is HTMLElement; on: (t: HTMLElement, e: string, h: (ev: Event) => void) => void; }; static plugin: typeof Plugin; static register_plugin: typeof _register_plugin; static util: { file: { read: (file: File, cb: (result: string, name: string) => void) => void; save: (data: string, type: string, name: string) => void; }; json: { json2string: (v: unknown) => string; string2json: (s: string) => unknown; merge: (b: object, a: object) => object; }; uuid: { newid: () => string; }; text: { is_empty: (s?: string) => boolean; }; }; static enhanced_plugin: typeof EnhancedPlugin; /** @type {Array<import('./jsmind.enhanced-plugin.js').PluginDescriptor>} */ static enhancedPluginList: Array<import("./jsmind.enhanced-plugin.js").PluginDescriptor>; /** * Register an enhanced plugin * @param {typeof EnhancedPlugin} PluginClass - Plugin class * @param {object} [options={}] - Plugin options * @returns {typeof jsMind} */ static usePlugin(PluginClass: typeof EnhancedPlugin, options?: object): typeof jsMind; /** * Check if an enhanced plugin is registered * @param {typeof EnhancedPlugin} PluginClass - Plugin class * @returns {boolean} */ static hasEnhancedPlugin(PluginClass: typeof EnhancedPlugin): boolean; /** * Deprecated: static show constructor helper. * @param {import('./jsmind.option.js').JsMindRuntimeOptions} options * @param {object | null} mind * @returns {jsMind} */ static show(options: import("./jsmind.option.js").JsMindRuntimeOptions, mind: object | null): jsMind; /** * Create a jsMind instance. * @param {import('./jsmind.option.js').JsMindRuntimeOptions} options */ constructor(options: import("./jsmind.option.js").JsMindRuntimeOptions); options: import("./jsmind.option.js").JsMindRuntimeOptions; version: string; initialized: boolean; mind: Mind; /** @type {'single'|'multi'|null} */ _selection_mode: "single" | "multi" | null; /** @type {import('./jsmind.node.js').Node|null} */ _last_selected_node: import("./jsmind.node.js").Node | null; /** @type {Array<(type: number, data: EventData) => void>} */ event_handles: Array<(type: number, data: EventData) => void>; /** Initialize sub-systems and plugins. */ init(): void; enhancedPluginManager: EnhancedPluginManager; data: DataProvider; layout: LayoutProvider; view: ViewProvider; shortcut: ShortcutProvider; /** @returns {boolean} whether current mind map is editable */ get_editable(): boolean; /** enable editing */ enable_edit(): void; /** disable editing */ disable_edit(): void; /** @returns {boolean} whether view is draggable */ get_view_draggable(): boolean; /** enable view dragging */ enable_view_draggable(): void; /** disable view dragging */ disable_view_draggable(): void; /** * Enable default event handle. * @param {'mousedown'|'click'|'dblclick'|'mousewheel'} event_handle */ enable_event_handle(event_handle: "mousedown" | "click" | "dblclick" | "mousewheel"): void; /** * Disable default event handle. * @param {'mousedown'|'click'|'dblclick'|'mousewheel'} event_handle */ disable_event_handle(event_handle: "mousedown" | "click" | "dblclick" | "mousewheel"): void; /** * Set theme name. * @param {string|null=} theme */ set_theme(theme?: (string | null) | undefined): void; /** bind internal DOM events */ _event_bind(): void; /** @param {MouseEvent} e */ mousedown_handle(e: MouseEvent): void; /** @param {MouseEvent} e */ click_handle(e: MouseEvent): void; /** @param {MouseEvent} e */ dblclick_handle(e: MouseEvent): void; /** @param {WheelEvent} e */ mousewheel_handle(e: WheelEvent): void; /** * Begin editing a node. * @param {string | import('./jsmind.node.js').Node} node * @returns {boolean|void} */ begin_edit(node: string | import("./jsmind.node.js").Node): boolean | void; /** End editing */ end_edit(): void; /** * Toggle a node's expanded state. * @param {string | import('./jsmind.node.js').Node} node * @returns {void} */ toggle_node(node: string | import("./jsmind.node.js").Node): void; /** * Expand a node. * @param {string | import('./jsmind.node.js').Node} node * @returns {void} */ expand_node(node: string | import("./jsmind.node.js").Node): void; /** * Collapse a node. * @param {string | import('./jsmind.node.js').Node} node * @returns {void} */ collapse_node(node: string | import("./jsmind.node.js").Node): void; /** Expand all nodes */ expand_all(): void; /** Collapse all nodes */ collapse_all(): void; /** * Expand nodes up to a specified depth level. * @param {number} depth */ expand_to_depth(depth: number): void; /** reset view/layout/data */ _reset(): void; /** * Internal show flow. * @param {object | null} mind * @param {boolean=} skip_centering */ _show(mind: object | null, skip_centering?: boolean | undefined): void; /** * Show a mind (or example) on the canvas. * @param {object | null} mind * @param {boolean=} skip_centering */ show(mind: object | null, skip_centering?: boolean | undefined): void; /** @returns {{name:string,author:string,version:string}} */ get_meta(): { name: string; author: string; version: string; }; /** * Serialize current mind to given format. * @param {'node_tree'|'node_array'|'freemind'|'text'} [data_format] * @returns {object} */ get_data(data_format?: "node_tree" | "node_array" | "freemind" | "text"): object; /** @returns {import('./jsmind.node.js').Node} */ get_root(): import("./jsmind.node.js").Node; /** * @param {string | import('./jsmind.node.js').Node} node * @returns {import('./jsmind.node.js').Node} */ get_node(node: string | import("./jsmind.node.js").Node): import("./jsmind.node.js").Node; /** * Get the level/depth of a node in the mind map. * @param {string | import('./jsmind.node.js').Node} node - Node id or Node instance * @returns {number} Node level (root node is 0, its children are 1, etc.) */ get_node_level(node: string | import("./jsmind.node.js").Node): number; /** * Add node data to the mind map without triggering UI refresh. * @private * @param {import('./jsmind.node.js').Node} parent_node * @param {string} node_id * @param {string} topic * @param {Record<string, any>=} data * @param {('left'|'center'|'right'|'-1'|'0'|'1'|number)=} direction * @returns {import('./jsmind.node.js').Node|null} */ private _add_node_data; /** * Refresh UI after node changes. * @private * @param {import('./jsmind.node.js').Node} parent_node */ private _refresh_node_ui; /** * Add a new node to the mind map. * @param {string | import('./jsmind.node.js').Node} parent_node * @param {string} node_id * @param {string} topic * @param {Record<string, any>=} data * @param {('left'|'center'|'right'|'-1'|'0'|'1'|number)=} direction - Direction for node placement. Supports string values ('left', 'center', 'right'), numeric strings ('-1', '0', '1'), and numbers (-1, 0, 1) * @returns {import('./jsmind.node.js').Node|null} */ add_node(parent_node: string | import("./jsmind.node.js").Node, node_id: string, topic: string, data?: Record<string, any> | undefined, direction?: ("left" | "center" | "right" | "-1" | "0" | "1" | number) | undefined): import("./jsmind.node.js").Node | null; /** * Add multiple nodes in batch. * * This method provides atomic batch node creation with automatic rollback on failure. * All nodes are created in a single operation, and if any node fails to create, * all previously created nodes in this batch will be automatically removed. * * **Note**: This is a batch operation API that uses standard field names only. * For data import with custom fieldNames, use `show()` instead. * * @example * // Using standard field names * jm.add_nodes('parent_id', [ * { id: 'node1', topic: 'Node 1', data: { color: 'red' }, children: [ * { id: 'node1-1', topic: 'Child 1' } * ]} * ]); * * @param {string | import('./jsmind.node.js').Node} parent_node - Parent node for all new nodes * @param {Array<{id: string, topic: string, data?: Record<string, any>, direction?: ('left'|'center'|'right'|'-1'|'0'|'1'|number), children?: Array}>} nodes_data - Array of node data objects with standard field names (id, topic, children, data, direction) * @returns {Array<import('./jsmind.node.js').Node|null>} Array of created nodes (flattened from all levels) */ add_nodes(parent_node: string | import("./jsmind.node.js").Node, nodes_data: Array<{ id: string; topic: string; data?: Record<string, any>; direction?: ("left" | "center" | "right" | "-1" | "0" | "1" | number); children?: any[]; }>): Array<import("./jsmind.node.js").Node | null>; /** * Recursively add nodes using standard field names. * This is a batch operation API that uses standard field names only. * For data import with custom fieldNames, use `show()` instead. * @private * @param {import('./jsmind.node.js').Node} parent_node * @param {object} node_data - Node data object with standard field names (id, topic, children, data, direction) * @returns {Array<import('./jsmind.node.js').Node|null>} */ private _add_nodes_recursive; /** * Count expected nodes recursively. * Supports custom field names via options.fieldNames configuration. * @private * @param {Array} nodes_data * @returns {number} */ private _count_expected_nodes; /** * Clean up partially created nodes without triggering UI refresh for each node. * @private * @param {Array<import('./jsmind.node.js').Node>} created_nodes */ private _cleanup_partial_nodes; /** * Insert a node before target node. * @param {string | import('./jsmind.node.js').Node} node_before * @param {string} node_id * @param {string} topic * @param {Record<string, any>=} data * @param {('left'|'center'|'right'|'-1'|'0'|'1'|number)=} direction - Direction for node placement. Supports string values ('left', 'center', 'right'), numeric strings ('-1', '0', '1'), and numbers (-1, 0, 1) * @returns {import('./jsmind.node.js').Node|null} */ insert_node_before(node_before: string | import("./jsmind.node.js").Node, node_id: string, topic: string, data?: Record<string, any> | undefined, direction?: ("left" | "center" | "right" | "-1" | "0" | "1" | number) | undefined): import("./jsmind.node.js").Node | null; /** * Insert a node after target node. * @param {string | import('./jsmind.node.js').Node} node_after * @param {string} node_id * @param {string} topic * @param {Record<string, any>=} data * @param {('left'|'center'|'right'|'-1'|'0'|'1'|number)=} direction - Direction for node placement. Supports string values ('left', 'center', 'right'), numeric strings ('-1', '0', '1'), and numbers (-1, 0, 1) * @returns {import('./jsmind.node.js').Node|null} */ insert_node_after(node_after: string | import("./jsmind.node.js").Node, node_id: string, topic: string, data?: Record<string, any> | undefined, direction?: ("left" | "center" | "right" | "-1" | "0" | "1" | number) | undefined): import("./jsmind.node.js").Node | null; /** * Remove a node. * @param {string | import('./jsmind.node.js').Node} node * @returns {boolean} */ remove_node(node: string | import("./jsmind.node.js").Node): boolean; /** * Update node topic text or multiple node properties. * @param {string} node_id - The ID of the node to update * @param {string|Partial<Pick<import('./jsmind.node.js').Node, 'topic' | 'data' | 'id' | 'index' | 'expanded' | 'direction'>>} topic_or_updates - Topic string for backward compatibility, or partial Node object for comprehensive updates */ update_node(node_id: string, topic_or_updates: string | Partial<Pick<import("./jsmind.node.js").Node, "topic" | "data" | "id" | "index" | "expanded" | "direction">>): void; /** * Move a node and optionally change direction. * @param {string} node_id * @param {string=} before_id - The ID of the node before which to place the moved node. Special values: "_first_", "_last_" * @param {string=} parent_id * @param {('left'|'center'|'right'|'-1'|'0'|'1'|number)=} direction - Direction for node placement. Supports string values ('left', 'center', 'right'), numeric strings ('-1', '0', '1'), and numbers (-1, 0, 1). Only effective for second-level nodes (children of root). If not provided, direction will be determined automatically. */ move_node(node_id: string, before_id?: string | undefined, parent_id?: string | undefined, direction?: ("left" | "center" | "right" | "-1" | "0" | "1" | number) | undefined): void; /** * @param {string | import('./jsmind.node.js').Node} node * @returns {void} */ select_node(node: string | import("./jsmind.node.js").Node): void; /** * Get the currently selected node. * * This is a query API that returns the internal Node instance. * For data export with custom fieldNames, use `get_data()` instead. * * @returns {import('./jsmind.node.js').Node|null} Node instance or null */ get_selected_node(): import("./jsmind.node.js").Node | null; /** * Get all currently selected nodes. * @returns {import('./jsmind.node.js').Node[]} */ get_selected_nodes(): import("./jsmind.node.js").Node[]; /** clear selection */ select_clear(): void; /** * Toggle multi-selection for a node (and optionally descendants). * @param {string | import('./jsmind.node.js').Node} node */ toggle_subtree_selection(node: string | import("./jsmind.node.js").Node): void; /** * Determine whether a node is currently selected. * @param {string | import('./jsmind.node.js').Node} node * @returns {boolean} */ is_node_selected(node: string | import("./jsmind.node.js").Node): boolean; /** @param {string | import('./jsmind.node.js').Node} node */ is_node_visible(node: string | import("./jsmind.node.js").Node): boolean; /** * Scroll the mind map to center the specified node. * @param {string | import('./jsmind.node.js').Node} node */ scroll_node_to_center(node: string | import("./jsmind.node.js").Node): void; /** * Add nodes into the current selection set without clearing existing ones. * @param {import('./jsmind.node.js').Node[]} nodes * @returns {import('./jsmind.node.js').Node[]} * @private */ /** * @param {import('./jsmind.node.js').Node[]} nodes * @param {{focusNode?: import('./jsmind.node.js').Node}=} options * @private */ private _append_selection; /** * Remove nodes from the current selection set. * @param {import('./jsmind.node.js').Node[]} nodes * @returns {import('./jsmind.node.js').Node[]} * @private */ private _remove_selection; /** * Deselect a node and all its descendants from the current selection. * @param {string | import('./jsmind.node.js').Node} node * @private */ private _deselect_subtree; /** * Clear all current selections and return the nodes that were cleared. * @returns {import('./jsmind.node.js').Node[]} * @private */ private _clear_selection_state; /** * Collect a node and optionally its descendants respecting filters. * @param {import('./jsmind.node.js').Node} node * @param {{includeChildren?:boolean, respectFilter?:boolean, skipRootFilter?:boolean}=} config * @returns {import('./jsmind.node.js').Node[]} * @private */ private _collect_subtree_nodes; /** * Ensure ancestors of provided nodes are also selected (up to first selected ancestor). * @param {import('./jsmind.node.js').Node[]} nodes * @param {import('./jsmind.node.js').Node=} focusNode * @returns {import('./jsmind.node.js').Node[]} * @private */ /** * @param {import('./jsmind.node.js').Node[]} nodes * @param {import('./jsmind.node.js').Node=} focusNode * @param {{requireAncestorChainSelected?: boolean}=} options * @returns {import('./jsmind.node.js').Node[]} * @private */ private _ensure_ancestor_selection; /** * Get the configured selection filter callback, if any. * @returns {((node: import('./jsmind.node.js').Node)=>boolean)|null} * @private */ private _get_selection_filter; /** * Determine the multi-select mode based on event modifiers. * Returns: null (single select), 'ctrl' (add/remove), 'shift' (range select) * @param {MouseEvent} e * @returns {null|'ctrl'|'shift'} * @private */ private _get_multi_select_mode; /** * Toggle selection of a single node (add if not selected, remove if selected). * Used for Ctrl+Click behavior. * @param {string | import('./jsmind.node.js').Node} node_id * @private */ private _toggle_node_selection; /** * Range select nodes for Shift+Click behavior. * Logic: * - If no nodes are currently selected: select all nodes under the clicked node * - If nodes are already selected: select nodes in the range from first selected to clicked node * @param {string | import('./jsmind.node.js').Node} node_id * @private */ private _range_select_nodes; /** * Find all nodes between two nodes (for range selection). * This includes both nodes and all nodes in between them in tree order. * @param {import('./jsmind.node.js').Node} node1 * @param {import('./jsmind.node.js').Node} node2 * @returns {import('./jsmind.node.js').Node[]} * @private */ private _find_nodes_between; /** * Check whether ancestor is an ancestor of node. * @param {import('./jsmind.node.js').Node} ancestor * @param {import('./jsmind.node.js').Node} node * @returns {boolean} * @private */ private _is_ancestor_of; /** * Return nodes along the ancestor->descendant chain, inclusive. If 'ancestor' is not actually * an ancestor of 'descendant', returns empty array. * @param {import('./jsmind.node.js').Node} ancestor * @param {import('./jsmind.node.js').Node} descendant * @returns {import('./jsmind.node.js').Node[]} * @private */ private _get_path_nodes; /** * Find nearest selected ancestor of the given node from current selection set. * @param {import('./jsmind.node.js').Node} node * @returns {import('./jsmind.node.js').Node|null} * @private */ private _find_nearest_selected_ancestor; /** * Find lowest common ancestor of two nodes. * @param {import('./jsmind.node.js').Node} a * @param {import('./jsmind.node.js').Node} b * @returns {import('./jsmind.node.js').Node|null} * @private */ private _find_lca; /** * Given a lowest common ancestor 'lca' and a descendant 'node', * return the direct child of lca that lies on the path to node. * Returns null if node is not a descendant of lca or node === lca. * @param {import('./jsmind.node.js').Node} lca * @param {import('./jsmind.node.js').Node} node * @returns {import('./jsmind.node.js').Node|null} * @private */ private _child_on_path; /** * From a list of nodes, remove those that are ancestors of any other node in the same list. * Keeps only the deepest nodes so that we don't auto-select parents implicitly. * @param {import('./jsmind.node.js').Node[]} nodes * @returns {import('./jsmind.node.js').Node[]} * @private */ private _remove_ancestor_nodes; /** * From a list of nodes, remove those that are descendants of any other node in the same list. * Keeps only top-most nodes so that expanding subtrees covers full branches across siblings. * @param {import('./jsmind.node.js').Node[]} nodes * @returns {import('./jsmind.node.js').Node[]} * @private */ private _remove_descendant_nodes; /** * Expand a set of base nodes with all their descendants (and themselves). * @param {import('./jsmind.node.js').Node[]} nodes * @param {{respectFilter?: boolean}=} opts * @returns {Set<import('./jsmind.node.js').Node>} * @private */ private _expand_with_descendants; /** * Promote parents into selection ONLY when all their direct children are selected * AND at least one ancestor of that parent is already selected (in previous selection set). * This avoids auto-selecting parents when only siblings are selected. * @param {Set<import('./jsmind.node.js').Node>} set * @returns {Set<import('./jsmind.node.js').Node>} * @private */ private _promote_parents_when_children_selected; /** * Determine selection mode based on current selection size. * @returns {'single'|'multi'|null} * @private */ private _derive_selection_mode; /** * Find the previous sibling node of the given node. * * @param {string | import('./jsmind.node.js').Node} node - Node id or Node instance * @returns {import('./jsmind.node.js').Node | null} */ find_node_before(node: string | import("./jsmind.node.js").Node): import("./jsmind.node.js").Node | null; /** * Find the next sibling node of the given node. * @param {string | import('./jsmind.node.js').Node} node * @returns {import('./jsmind.node.js').Node | null} */ find_node_after(node: string | import("./jsmind.node.js").Node): import("./jsmind.node.js").Node | null; /** * Set background and foreground colors for a node. * @param {string} node_id * @param {string=} bg_color * @param {string=} fg_color * @returns {void} */ set_node_color(node_id: string, bg_color?: string | undefined, fg_color?: string | undefined): void; /** * Set font style for a node. * @param {string} node_id * @param {number=} size * @param {string=} weight * @param {string=} style * @returns {void} */ set_node_font_style(node_id: string, size?: number | undefined, weight?: string | undefined, style?: string | undefined): void; /** * Set background image for a node. * @param {string} node_id * @param {string=} image * @param {number=} width * @param {number=} height * @param {number=} rotation * @returns {void} */ set_node_background_image(node_id: string, image?: string | undefined, width?: number | undefined, height?: number | undefined, rotation?: number | undefined): void; /** * @param {string} node_id * @param {number} rotation * @returns {void} */ set_node_background_rotation(node_id: string, rotation: number): void; /** trigger view resize */ resize(): void; /** @param {(type:number, data: EventData)=>void} callback */ add_event_listener(callback: (type: number, data: EventData) => void): void; /** clear event listeners */ clear_event_listener(): void; /** @param {number} type @param {EventData} data */ invoke_event_handle(type: number, data: EventData): void; /** @param {number} type @param {EventData} data */ _invoke_event_handle(type: number, data: EventData): void; /** * Remove an enhanced plugin * @param {typeof EnhancedPlugin} PluginClass - Plugin class */ removePlugin(PluginClass: typeof EnhancedPlugin): void; removePlugin(PluginClass: typeof EnhancedPlugin): void; /** * Get an enhanced plugin instance * @param {string} instanceName - Plugin instance name * @returns {EnhancedPlugin | undefined} */ getPlugin(instanceName: string): EnhancedPlugin | undefined; getPlugin(instanceName: string): EnhancedPlugin | undefined; /** * Destroy the jsMind instance and clean up resources */ destroy(): void; destroy(): void; } export default jsMind; /** * Event callback payload */ export type EventData = { evt?: string; data?: unknown[]; node?: string; }; export namespace jsMind { } import { Mind } from './jsmind.mind.js'; import { EnhancedPluginManager } from './jsmind.enhanced-plugin.js'; import { DataProvider } from './jsmind.data_provider.js'; import { LayoutProvider } from './jsmind.layout_provider.js'; import { ViewProvider } from './jsmind.view_provider.js'; import { ShortcutProvider } from './jsmind.shortcut_provider.js'; import { EnhancedPlugin } from './jsmind.enhanced-plugin.js'; import { Node } from './jsmind.node.js'; import { Plugin } from './jsmind.plugin.js'; import { register as _register_plugin } from './jsmind.plugin.js';