import { getRelativePosition } from './custom.util.js' const EDITOR_CLASS = 'jsmind-editor' // jsmind class name const SUGGESTION_BOX_CLASS = 'jsmind-suggestions' const SUGGESTION_ITEM_CLASS = 'suggestion-item' /** * jsMind 搜尋管理 * jsMind Search Manager */ export class JsmindSearch { /** * 建構搜尋 * Constructor for search * @param {Object} jm - jsMind 實例 (jsMind instance) * @param {Function} searchAPI - 遠程搜尋 API 函式 (Remote search API function) * @param {string} tableUID */ constructor(jm, searchAPI, tableUID) { this.jm = jm this.searchAPI = searchAPI this.container = document.getElementById(jm.options.container) this.suggestionBox = null this.tableUID = tableUID // 新增記錄節點與事件 handler this.currentNode = null this._keydownHandler = null this._inputHandler = null this.init() } /** * 初始化搜尋事件 * Initialize search events */ init() { // 確保不會重複綁定 dblclick 事件 // Ensure double-click event is not bound multiple times this.container.removeEventListener('dblclick', this.onDoubleClick) this.container.addEventListener('dblclick', this.onDoubleClick.bind(this)) } /** * 處理雙擊事件以觸發搜尋 * Handle double-click event to trigger search * @param {Event} e - 事件對象 (Event object) */ onDoubleClick(e) { // 非可編輯狀態不執行 // Ignore if not editable if (!this.jm.options.editable) return const node = this.jm.get_selected_node() if (!node) return // 避免影響原生編輯功能,稍後執行 // Prevent interfering with native edit mode setTimeout(() => this.handleSearch(node), 100) } /** * 開始處理搜尋 * Start handling search * @param {Object} node - 當前選中節點 (Selected node) */ handleSearch(node) { const inputField = document.querySelector(`.${EDITOR_CLASS}`) if (!inputField) return // 記住目前的 node this.currentNode = node // 清除之前的 handler if (this._keydownHandler) inputField.removeEventListener('keydown', this._keydownHandler) if (this._inputHandler) inputField.removeEventListener('input', this._inputHandler) // 新綁定 handler this._keydownHandler = this.onKeyDown.bind(this) this._inputHandler = this.onInput.bind(this) inputField.addEventListener('keydown', this._keydownHandler) inputField.addEventListener('input', this._inputHandler) } /** * 處理 Enter 鍵完成輸入 * Handle Enter key to finalize input * @param {Object} node - 當前節點 * @param {KeyboardEvent} e - 鍵盤事件 */ onKeyDown(e) { if (e.key === 'Enter') { e.preventDefault() const input = e.target.value.trim() const node = this.currentNode if (input && node) { node.data.text = input this.jm.end_edit() this.jm.update_node(node.id, input) if (this.suggestionBox) { this.suggestionBox.style.display = 'none' } this.currentNode = null // 清除參考 } } } /** * 處理使用者輸入 * Handle user input * @param {Object} node - 當前選中節點 (Selected node) * @param {Event} e - 輸入事件 (Input event) */ async onInput(e) { const query = e.target.value.trim() if (!query) return await new Promise((resolve) => setTimeout(resolve, 500)) try { const results = await this.searchAPI(query, this.tableUID) this.showSuggestion(this.currentNode, e.target, results) } catch (error) { console.error('搜尋 API 錯誤:', error) } } /** * 顯示搜尋建議框 * Show search suggestion box * @param {Object} node - 當前選中節點 (Selected node) * @param {HTMLElement} inputElement - 輸入框 (Input field) * @param {Array} results - 搜尋結果 (Search results) */ showSuggestion(node, inputElement, results) { const container = this.container const nodeElement = inputElement.parentNode if (!nodeElement) return const { left, top, height } = getRelativePosition(nodeElement, container) this.suggestionBox = this.suggestionBox || this.createSuggestionBox() // 更新建議框內容 // Update suggestion box content this.suggestionBox.innerHTML = results .map(item => { const fieldHtml = item.fields.map(f => { const txt = f.url ? `${f.text}` : f.text; return `