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 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 // 確保不會重複綁定 input 事件 // Ensure input event is not bound multiple times inputField.removeEventListener('input', this.onInput) inputField.addEventListener('input', this.onInput.bind(this, node)) } /** * 處理使用者輸入 * Handle user input * @param {Object} node - 當前選中節點 (Selected node) * @param {Event} e - 輸入事件 (Input event) */ async onInput(node, 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(node, e.target, results) } catch (error) { // Search API error handling 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) => `
${item.text}
` ) .join('') this.suggestionBox.style.left = `${left}px` this.suggestionBox.style.top = `${top + height}px` this.suggestionBox.style.display = 'block' // 綁定建議點擊事件 // Bind suggestion click events document.querySelectorAll(`.${SUGGESTION_ITEM_CLASS}`).forEach((item) => { item.removeEventListener('mousedown', this.onSuggestionClick) item.addEventListener('mousedown', this.onSuggestionClick.bind(this, node)) }) } /** * 建立搜尋建議框 * Create search suggestion box * @returns {HTMLElement} - 建議框 DOM (Suggestion box DOM) */ createSuggestionBox() { let suggestionBox = document.getElementById(SUGGESTION_BOX_CLASS) if (!suggestionBox) { suggestionBox = document.createElement('div') suggestionBox.classList.add(SUGGESTION_BOX_CLASS) this.container.appendChild(suggestionBox) } return suggestionBox } /** * 處理點擊建議 * Handle suggestion click * @param {Object} node - 當前選中節點 (Selected node) * @param {Event} e - 點擊事件 (Click event) */ onSuggestionClick(node, e) { e.preventDefault() const text = e.target.getAttribute('data-text') const link = e.target.getAttribute('data-link') node.data.text = text node.data.link = link this.jm.end_edit() this.jm.update_node(node.id, text) // 選擇後隱藏建議框 // Hide suggestions after selection this.suggestionBox.style.display = 'none' } }