mind-map/simple-mind-map/src/plugins/Search.js

261 lines
7.0 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 {
bfsWalk,
getTextFromHtml,
isUndef,
replaceHtmlText
} from '../utils/index'
import Node from '../core/render/node/Node'
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()
}
// 结束搜索
endSearch() {
if (!this.isSearching) return
if (this.mindMap.opt.readonly && this.matchNodeList[this.currentIndex]) {
this.matchNodeList[this.currentIndex].closeHighlight()
}
this.searchText = ''
this.matchNodeList = []
this.currentIndex = -1
this.notResetSearchText = false
this.isSearching = false
this.emitEvent()
}
// 搜索匹配的节点
doSearch() {
this.matchNodeList = []
this.currentIndex = -1
const { isOnlySearchCurrentRenderNodes } = this.mindMap.opt
// 如果要搜索收起来的节点,那么要遍历渲染树而不是节点树
const tree = isOnlySearchCurrentRenderNodes
? this.mindMap.renderer.root
: this.mindMap.renderer.renderTree
if (!tree) return
bfsWalk(tree, node => {
let { richText, text } = isOnlySearchCurrentRenderNodes
? node.getData()
: node.data
if (richText) {
text = getTextFromHtml(text)
}
if (text.includes(this.searchText)) {
this.matchNodeList.push(node)
}
})
}
// 判断对象是否是节点实例
isNodeInstance(node) {
return node instanceof Node
}
// 搜索下一个,定位到下一个匹配节点
searchNext(callback) {
if (!this.isSearching || this.matchNodeList.length <= 0) return
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
}
callback()
// 只读模式下节点无法激活,所以通过高亮的方式
if (readonly) {
node.highlight()
}
// 如果当前节点实例已经存在则不会触发data_change事件那么需要手动把标志复位
if (targetNode) {
this.notResetSearchText = false
}
})
}
// 替换当前节点
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
let text = this.getReplacedText(currentNode, this.searchText, replaceText)
this.notResetSearchText = true
currentNode.setText(text, currentNode.getData('richText'), true)
this.matchNodeList = this.matchNodeList.filter(node => {
return currentNode !== node
})
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)
this.matchNodeList.forEach(node => {
const text = this.getReplacedText(node, this.searchText, replaceText)
if (this.isNodeInstance(node)) {
this.mindMap.renderer.setNodeDataRender(
node,
{
text,
resetRichText: !!node.getData('richText')
},
true
)
} else {
node.data.text = text
node.data.resetRichText = !!node.data.richText
}
})
this.mindMap.render()
this.mindMap.command.addHistory()
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