318 lines
8.8 KiB
JavaScript
318 lines
8.8 KiB
JavaScript
import {
|
||
bfsWalk,
|
||
getTextFromHtml,
|
||
isUndef,
|
||
replaceHtmlText,
|
||
formatGetNodeGeneralization
|
||
} from '../utils/index'
|
||
import MindMapNode from '../core/render/node/MindMapNode'
|
||
import { CONSTANTS } from '../constants/constant'
|
||
|
||
// 搜索插件
|
||
class Search {
|
||
// 构造函数
|
||
constructor({ mindMap }) {
|
||
this.mindMap = mindMap
|
||
// 是否正在搜索
|
||
this.isSearching = false
|
||
// 搜索文本
|
||
this.searchText = ''
|
||
// 匹配的节点列表
|
||
this.matchNodeList = []
|
||
// 当前所在的节点列表索引
|
||
this.currentIndex = -1
|
||
// 不要复位搜索文本
|
||
this.notResetSearchText = false
|
||
// 是否自动跳转下一个匹配节点
|
||
this.isJumpNext = false
|
||
|
||
this.bindEvent()
|
||
}
|
||
|
||
bindEvent() {
|
||
this.onDataChange = this.onDataChange.bind(this)
|
||
this.onModeChange = this.onModeChange.bind(this)
|
||
this.mindMap.on('data_change', this.onDataChange)
|
||
this.mindMap.on('mode_change', this.onModeChange)
|
||
}
|
||
|
||
unBindEvent() {
|
||
this.mindMap.off('data_change', this.onDataChange)
|
||
this.mindMap.off('mode_change', this.onModeChange)
|
||
}
|
||
|
||
// 节点数据改变了,需要重新搜索
|
||
onDataChange() {
|
||
if (this.isJumpNext) {
|
||
this.isJumpNext = false
|
||
this.search(this.searchText)
|
||
return
|
||
}
|
||
if (this.notResetSearchText) {
|
||
this.notResetSearchText = false
|
||
return
|
||
}
|
||
this.searchText = ''
|
||
}
|
||
|
||
// 监听只读模式切换
|
||
onModeChange(mode) {
|
||
const isReadonly = mode === CONSTANTS.MODE.READONLY
|
||
// 如果是由只读模式切换为非只读模式,需要清除只读模式下的节点高亮
|
||
if (
|
||
!isReadonly &&
|
||
this.isSearching &&
|
||
this.matchNodeList[this.currentIndex]
|
||
) {
|
||
this.matchNodeList[this.currentIndex].closeHighlight()
|
||
}
|
||
}
|
||
|
||
// 搜索
|
||
search(text, callback = () => {}) {
|
||
if (isUndef(text)) return this.endSearch()
|
||
text = String(text)
|
||
this.isSearching = true
|
||
if (this.searchText === text) {
|
||
// 和上一次搜索文本一样,那么搜索下一个
|
||
this.searchNext(callback)
|
||
} else {
|
||
// 和上次搜索文本不一样,那么重新开始
|
||
this.searchText = text
|
||
this.doSearch()
|
||
this.searchNext(callback)
|
||
}
|
||
this.emitEvent()
|
||
}
|
||
|
||
// 更新匹配节点列表
|
||
updateMatchNodeList(list) {
|
||
this.matchNodeList = list
|
||
this.mindMap.emit('search_match_node_list_change', list)
|
||
}
|
||
|
||
// 结束搜索
|
||
endSearch() {
|
||
if (!this.isSearching) return
|
||
if (this.mindMap.opt.readonly && this.matchNodeList[this.currentIndex]) {
|
||
this.matchNodeList[this.currentIndex].closeHighlight()
|
||
}
|
||
this.searchText = ''
|
||
this.updateMatchNodeList([])
|
||
this.currentIndex = -1
|
||
this.notResetSearchText = false
|
||
this.isSearching = false
|
||
this.emitEvent()
|
||
}
|
||
|
||
// 搜索匹配的节点
|
||
doSearch() {
|
||
this.updateMatchNodeList([])
|
||
this.currentIndex = -1
|
||
const { isOnlySearchCurrentRenderNodes } = this.mindMap.opt
|
||
// 如果要搜索收起来的节点,那么要遍历渲染树而不是节点树
|
||
const tree = isOnlySearchCurrentRenderNodes
|
||
? this.mindMap.renderer.root
|
||
: this.mindMap.renderer.renderTree
|
||
if (!tree) return
|
||
const matchList = []
|
||
bfsWalk(tree, node => {
|
||
let { richText, text, generalization } = isOnlySearchCurrentRenderNodes
|
||
? node.getData()
|
||
: node.data
|
||
if (richText) {
|
||
text = getTextFromHtml(text)
|
||
}
|
||
if (text.includes(this.searchText)) {
|
||
matchList.push(node)
|
||
}
|
||
// 概要节点
|
||
const generalizationList = formatGetNodeGeneralization({
|
||
generalization
|
||
})
|
||
generalizationList.forEach(gNode => {
|
||
let { richText, text, uid } = gNode
|
||
if (
|
||
isOnlySearchCurrentRenderNodes &&
|
||
!this.mindMap.renderer.findNodeByUid(uid)
|
||
) {
|
||
return
|
||
}
|
||
if (richText) {
|
||
text = getTextFromHtml(text)
|
||
}
|
||
if (text.includes(this.searchText)) {
|
||
matchList.push({
|
||
data: gNode
|
||
})
|
||
}
|
||
})
|
||
})
|
||
this.updateMatchNodeList(matchList)
|
||
}
|
||
|
||
// 判断对象是否是节点实例
|
||
isNodeInstance(node) {
|
||
return node instanceof MindMapNode
|
||
}
|
||
|
||
// 搜索下一个或指定索引,定位到下一个匹配节点
|
||
searchNext(callback, index) {
|
||
if (!this.isSearching || this.matchNodeList.length <= 0) return
|
||
if (
|
||
index !== undefined &&
|
||
Number.isInteger(index) &&
|
||
index >= 0 &&
|
||
index < this.matchNodeList.length
|
||
) {
|
||
this.currentIndex = index
|
||
} else {
|
||
if (this.currentIndex < this.matchNodeList.length - 1) {
|
||
this.currentIndex++
|
||
} else {
|
||
this.currentIndex = 0
|
||
}
|
||
}
|
||
const { readonly } = this.mindMap.opt
|
||
// 只读模式下需要激活之前节点的高亮
|
||
if (readonly) {
|
||
this.matchNodeList.forEach(node => {
|
||
if (this.isNodeInstance(node)) {
|
||
node.closeHighlight()
|
||
}
|
||
})
|
||
}
|
||
const currentNode = this.matchNodeList[this.currentIndex]
|
||
this.notResetSearchText = true
|
||
const uid = this.isNodeInstance(currentNode)
|
||
? currentNode.getData('uid')
|
||
: currentNode.data.uid
|
||
const targetNode = this.mindMap.renderer.findNodeByUid(uid)
|
||
this.mindMap.execCommand('GO_TARGET_NODE', uid, node => {
|
||
if (!this.isNodeInstance(currentNode)) {
|
||
this.matchNodeList[this.currentIndex] = node
|
||
this.updateMatchNodeList(this.matchNodeList)
|
||
}
|
||
callback()
|
||
// 只读模式下节点无法激活,所以通过高亮的方式
|
||
if (readonly) {
|
||
node.highlight()
|
||
}
|
||
// 如果当前节点实例已经存在,则不会触发data_change事件,那么需要手动把标志复位
|
||
if (targetNode) {
|
||
this.notResetSearchText = false
|
||
}
|
||
})
|
||
}
|
||
|
||
// 定位到指定搜索结果索引的节点
|
||
jump(index, callback = () => {}) {
|
||
this.searchNext(callback, index)
|
||
}
|
||
|
||
// 替换当前节点
|
||
replace(replaceText, jumpNext = false) {
|
||
if (
|
||
replaceText === null ||
|
||
replaceText === undefined ||
|
||
!this.isSearching ||
|
||
this.matchNodeList.length <= 0
|
||
)
|
||
return
|
||
// 自动跳转下一个匹配节点
|
||
this.isJumpNext = jumpNext
|
||
replaceText = String(replaceText)
|
||
let currentNode = this.matchNodeList[this.currentIndex]
|
||
if (!currentNode) return
|
||
// 如果当前搜索文本是替换文本的子串,那么该节点还是符合搜索结果的
|
||
const keep = replaceText.includes(this.searchText)
|
||
const text = this.getReplacedText(currentNode, this.searchText, replaceText)
|
||
this.notResetSearchText = true
|
||
currentNode.setText(text, currentNode.getData('richText'), true)
|
||
if (keep) {
|
||
this.updateMatchNodeList(this.matchNodeList)
|
||
return
|
||
}
|
||
const newList = this.matchNodeList.filter(node => {
|
||
return currentNode !== node
|
||
})
|
||
this.updateMatchNodeList(newList)
|
||
if (this.currentIndex > this.matchNodeList.length - 1) {
|
||
this.currentIndex = -1
|
||
} else {
|
||
this.currentIndex--
|
||
}
|
||
this.emitEvent()
|
||
}
|
||
|
||
// 替换所有
|
||
replaceAll(replaceText) {
|
||
if (
|
||
replaceText === null ||
|
||
replaceText === undefined ||
|
||
!this.isSearching ||
|
||
this.matchNodeList.length <= 0
|
||
)
|
||
return
|
||
replaceText = String(replaceText)
|
||
// 如果当前搜索文本是替换文本的子串,那么该节点还是符合搜索结果的
|
||
const keep = replaceText.includes(this.searchText)
|
||
this.notResetSearchText = true
|
||
const hasRichTextPlugin = this.mindMap.renderer.hasRichTextPlugin()
|
||
this.matchNodeList.forEach(node => {
|
||
const text = this.getReplacedText(node, this.searchText, replaceText)
|
||
if (this.isNodeInstance(node)) {
|
||
const data = {
|
||
text
|
||
}
|
||
if (hasRichTextPlugin) data.resetRichText = !!node.getData('richText')
|
||
this.mindMap.renderer.setNodeDataRender(node, data, true)
|
||
} else {
|
||
node.data.text = text
|
||
if (hasRichTextPlugin) node.data.resetRichText = !!node.data.richText
|
||
}
|
||
})
|
||
this.mindMap.render()
|
||
this.mindMap.command.addHistory()
|
||
if (keep) {
|
||
this.updateMatchNodeList(this.matchNodeList)
|
||
} else {
|
||
this.endSearch()
|
||
}
|
||
}
|
||
|
||
// 获取某个节点替换后的文本
|
||
getReplacedText(node, searchText, replaceText) {
|
||
let { richText, text } = this.isNodeInstance(node)
|
||
? node.getData()
|
||
: node.data
|
||
if (richText) {
|
||
return replaceHtmlText(text, searchText, replaceText)
|
||
} else {
|
||
return text.replaceAll(searchText, replaceText)
|
||
}
|
||
}
|
||
|
||
// 发送事件
|
||
emitEvent() {
|
||
this.mindMap.emit('search_info_change', {
|
||
currentIndex: this.currentIndex,
|
||
total: this.matchNodeList.length
|
||
})
|
||
}
|
||
|
||
// 插件被移除前做的事情
|
||
beforePluginRemove() {
|
||
this.unBindEvent()
|
||
}
|
||
|
||
// 插件被卸载前做的事情
|
||
beforePluginDestroy() {
|
||
this.unBindEvent()
|
||
}
|
||
}
|
||
|
||
Search.instanceName = 'search'
|
||
|
||
export default Search
|