universal_table/app/assets/javascripts/mind_map/utils/custom.search.js

254 lines
7.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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))
inputField.removeEventListener('keydown', this.onKeyDown)
inputField.addEventListener('keydown', this.onKeyDown.bind(this, node))
}
/**
* 處理 Enter 鍵完成輸入
* Handle Enter key to finalize input
* @param {Object} node - 當前節點
* @param {KeyboardEvent} e - 鍵盤事件
*/
onKeyDown(node, e) {
if (e.key === 'Enter') {
e.preventDefault()
const input = e.target.value.trim()
if (input) {
// 更新節點文字
node.data.text = input
this.jm.end_edit()
this.jm.update_node(node.id, input)
// 隱藏 suggestion box避免未選建議但仍留下
if (this.suggestionBox) {
this.suggestionBox.style.display = 'none'
}
}
}
}
/**
* 處理使用者輸入
* 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 => {
const fieldHtml = item.fields.map(f => {
const txt = f.url
? `<a href="${f.url}" target="_blank">${f.text}</a>`
: f.text;
return `<div class="field-row"><strong>${f.title}:</strong> ${txt}</div>`;
}).join("");
return `
<div class="suggestion-item" data-link="${item.link}">
${fieldHtml}
</div>
`;
})
.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'
// }
onSuggestionClick(node, e) {
e.preventDefault()
const item = e.currentTarget // 確保抓到整個 .suggestion-item DIV
const html = item.innerHTML // 取得完整 HTML 當作 topic
node.data.text = html
node.data.link = item.getAttribute('data-link')
this.jm.end_edit()
this.jm.update_node(node.id, html)
this.suggestionBox.style.display = 'none'
}
}
// ✅ 新增播放語音事件委派,支援動態插入的 voice-player
let audio;
document.addEventListener('click', function(e) {
const target = e.target.closest('.voice-player');
if (!target) return;
e.preventDefault();
let status = target.getAttribute('status');
if (audio) {
audio.pause();
audio.currentTime = 0;
}
if (status === 'playing') {
target.setAttribute('status', '');
const icon = target.querySelector('i');
icon?.classList.remove('fa-pause');
icon?.classList.add('fa-play');
} else {
let mp3_url = target.getAttribute('data-content');
audio = new Audio(mp3_url);
audio.play();
target.setAttribute('status', 'playing');
const icon = target.querySelector('i');
icon?.classList.remove('fa-play');
icon?.classList.add('fa-pause');
audio.onended = function() {
target.setAttribute('status', '');
icon?.classList.remove('fa-pause');
icon?.classList.add('fa-play');
};
}
});