import { util } from '../jsmind/jsmind.util.js' import { getRelativePosition } from './custom.util.js' import { PALETTE_COLORS } from './custom.config.js' const TOOLBAR_ID = 'jsmind-toolbar' /** * jsMind 工具列管理 * jsMind Toolbar Manager */ export class JsmindToolbar { /** * 建構工具列 * Constructor for toolbar * @param {Object} jm - jsMind 實例 (jsMind instance) * @param {Object} options - jsMind 實例 (options) */ constructor(jm, options) { this.jm = jm this.container = document.getElementById(jm.options.container) this.toolbarNodeId = null this.toolbar = null this.bgColorPalette = null this.strokeColorPalette = null this.textColorPalette = null this.options = options this.init() } /** * 初始化工具列事件 * Initialize toolbar events */ init() { // 監聽節點選取事件 // Listen for node selection events this.jm.add_event_listener((e, f, g) => { // 忽略非選擇節點事件 // Ignore non-selection events if (e !== 4) return const node = this.jm.get_selected_node() if (!node || node.id === this.toolbarNodeId) return this.toolbarNodeId = node.id if (!this.toolbar) { this.createToolbar() } this.moveToolbar(node) }) // 確保不會重複綁定點擊事件 // Ensure click event is not bound multiple times this.container.removeEventListener('click', this.onClickOutside) this.container.addEventListener('click', this.onClickOutside.bind(this)) } /** * 處理點擊事件來隱藏工具列 * Handle click event to hide toolbar * @param {Event} e - 事件對象 (Event object) */ onClickOutside(e) { const clickedNode = e.target.tagName === 'JMNODE' const clickedToolbar = e.target.closest(`#${TOOLBAR_ID}`) if (!clickedNode && !clickedToolbar && this.toolbar) { this.hideToolbar() } } /** * 建立工具列 UI * Create toolbar UI */ createToolbar() { this.toolbar = document.createElement('div') this.toolbar.id = TOOLBAR_ID // 建立工具列按鈕 // Create toolbar buttons const buttons = [ { id: 'toolbar-add-child-btn', text: this.options.text.addNode, onClick: this.handleAddChild.bind(this) }, { id: 'toolbar-delete-btn', text: this.options.text.deleteNode, onClick: this.handleDelete.bind(this) }, { id: 'toolbar-stroke-color-btn', text: this.options.text.strokeColor, onClick: this.handleStrokeColor.bind(this), }, { id: 'toolbar-bg-color-btn', text: this.options.text.bgColor, onClick: this.handleBgColor.bind(this), }, { id: 'toolbar-text-color-btn', text: this.options.text.textColor, onClick: this.handleTextColor.bind(this), }, ] buttons.forEach((button) => { const btn = document.createElement('button') btn.id = button.id btn.innerText = button.text btn.onclick = button.onClick this.toolbar.appendChild(btn) // 附加顏色選單 // Append color palettes to corresponding buttons if (button.id === 'toolbar-bg-color-btn') { this.bgColorPalette = this.createColorPalette( (color) => this.setNodeStyle('background-color', color), btn ) } if (button.id === 'toolbar-stroke-color-btn') { this.strokeColorPalette = this.createColorPalette( (color) => this.setNodeStyle('leading-line-color', color), btn ) } if (button.id === 'toolbar-text-color-btn') { this.textColorPalette = this.createColorPalette( (color) => this.setNodeStyle('foreground-color', color), btn ) } }) this.container.appendChild(this.toolbar) } /** * 移動工具列至選中節點 * Move the toolbar to the selected node */ moveToolbar(node) { const nodeElement = node._data.view.element if (!nodeElement) return const { left, top } = getRelativePosition(nodeElement, this.container) this.toolbar.style.left = `${left}px` this.toolbar.style.top = `${top - 40}px` this.toolbar.style.display = 'block' // 根節點則隱藏刪除與線條顏色按鈕 // Hide delete & stroke color buttons if the node is root const deleteBtn = this.toolbar.querySelector('#toolbar-delete-btn') const strokeColorBtn = this.toolbar.querySelector('#toolbar-stroke-color-btn') if (node.id === 'root') { deleteBtn.style.display = 'none' strokeColorBtn.style.display = 'none' } else { deleteBtn.style.display = 'inline-block' strokeColorBtn.style.display = 'inline-block' } } /** * 隱藏工具列 * Hide the toolbar */ hideToolbar() { this.toolbar.style.display = 'none' this.bgColorPalette.style.display = 'none' this.strokeColorPalette.style.display = 'none' this.textColorPalette.style.display = 'none' this.toolbarNodeId = null } /** * 建立顏色選單 * Create color palette */ createColorPalette(onSelect, button) { const colorPalette = document.createElement('div') colorPalette.classList.add('toolbar-color-palette') colorPalette.style.display = 'none' PALETTE_COLORS.forEach((color) => { const colorBox = document.createElement('div') colorBox.classList.add('toolbar-color-palette-box') colorBox.style.backgroundColor = color colorBox.onclick = () => { onSelect(color) colorPalette.style.display = 'none' } colorPalette.appendChild(colorBox) }) button.appendChild(colorPalette) return colorPalette } /** * 設定節點樣式 * Set node style */ setNodeStyle(style, color) { if (!this.toolbarNodeId) return const node = this.jm.get_node(this.toolbarNodeId) if (!node) return node.data[style] = color if (style === 'leading-line-color') this.jm.view.show_lines() else this.jm.view.restore_selected_node_custom_style(node) } /** * 處理新增節點事件 * Handle add child node event */ handleAddChild(e) { e.preventDefault(); e.stopPropagation() if (!this.toolbarNodeId) return const node = this.jm.get_node(this.toolbarNodeId) if (!node) return const newNode = this.jm.add_node(node, util.uuid.newid(), 'NewNode') this.jm.select_node(newNode) } /** * 處理刪除節點事件 * Handle delete node event */ handleDelete(e) { e.preventDefault(); e.stopPropagation() if (!this.toolbarNodeId) return const node = this.jm.get_node(this.toolbarNodeId) if (!node) return this.jm.remove_node(node) this.hideToolbar() } /** * 處理其他樣式設定事件 * Handle style setting event */ handleStrokeColor(e) { e.preventDefault(); e.stopPropagation() this.toggleColorPalette(this.strokeColorPalette) } handleBgColor(e) { e.preventDefault(); e.stopPropagation() this.toggleColorPalette(this.bgColorPalette) } handleTextColor(e) { e.preventDefault(); e.stopPropagation() this.toggleColorPalette(this.textColorPalette) } /** * 顯示或隱藏顏色選單 * Toggle color palette display */ toggleColorPalette(palette) { ;[this.bgColorPalette, this.strokeColorPalette, this.textColorPalette].forEach((p) => { if (p !== palette) p.style.display = 'none' }) palette.style.display = palette.style.display === 'block' ? 'none' : 'block' } }