254 lines
7.8 KiB
JavaScript
254 lines
7.8 KiB
JavaScript
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');
|
||
};
|
||
}
|
||
}); |