267 lines
8.1 KiB
JavaScript
Executable File
267 lines
8.1 KiB
JavaScript
Executable File
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
|
|
? `<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');
|
|
};
|
|
}
|
|
}); |