mirror of
https://github.com/wanglin2/mind-map.git
synced 2026-02-21 18:37:43 +08:00
Feat:支持搜索和替换
This commit is contained in:
parent
5a5c7702f5
commit
07be48d342
@ -10,6 +10,7 @@ import AssociativeLine from './src/plugins/AssociativeLine'
|
||||
import RichText from './src/plugins/RichText'
|
||||
import NodeImgAdjust from './src/plugins/NodeImgAdjust.js'
|
||||
import TouchEvent from './src/plugins/TouchEvent.js'
|
||||
import Search from './src/plugins/Search.js'
|
||||
import xmind from './src/parse/xmind.js'
|
||||
import markdown from './src/parse/markdown.js'
|
||||
import icons from './src/svg/icons.js'
|
||||
@ -36,5 +37,6 @@ MindMap
|
||||
.usePlugin(RichText)
|
||||
.usePlugin(TouchEvent)
|
||||
.usePlugin(NodeImgAdjust)
|
||||
.usePlugin(Search)
|
||||
|
||||
export default MindMap
|
||||
@ -975,7 +975,8 @@ class Render {
|
||||
setNodeText(node, text, richText) {
|
||||
this.setNodeDataRender(node, {
|
||||
text,
|
||||
richText
|
||||
richText,
|
||||
resetRichText: richText
|
||||
})
|
||||
}
|
||||
|
||||
@ -1101,7 +1102,7 @@ class Render {
|
||||
}
|
||||
|
||||
// 定位到指定节点
|
||||
goTargetNode(node) {
|
||||
goTargetNode(node, callback = () => {}) {
|
||||
let uid = typeof node === 'string' ? node : node.nodeData.data.uid
|
||||
if (!uid) return
|
||||
this.expandToNodeUid(uid, () => {
|
||||
@ -1109,6 +1110,7 @@ class Render {
|
||||
if (targetNode) {
|
||||
targetNode.active()
|
||||
this.moveNodeToCenter(targetNode)
|
||||
callback()
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -1121,7 +1123,7 @@ class Render {
|
||||
}
|
||||
|
||||
// 设置节点数据,并判断是否渲染
|
||||
setNodeDataRender(node, data) {
|
||||
setNodeDataRender(node, data, notRender = false) {
|
||||
this.setNodeData(node, data)
|
||||
let changed = node.reRender()
|
||||
if (changed) {
|
||||
@ -1129,7 +1131,7 @@ class Render {
|
||||
// 概要节点
|
||||
node.generalizationBelongNode.updateGeneralization()
|
||||
}
|
||||
this.mindMap.render()
|
||||
if (!notRender) this.mindMap.render()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
143
simple-mind-map/src/plugins/Search.js
Normal file
143
simple-mind-map/src/plugins/Search.js
Normal file
@ -0,0 +1,143 @@
|
||||
import { bfsWalk, getTextFromHtml } from '../utils/index'
|
||||
|
||||
// 搜索插件
|
||||
class Search {
|
||||
// 构造函数
|
||||
constructor({ mindMap }) {
|
||||
this.mindMap = mindMap
|
||||
// 是否正在搜索
|
||||
this.isSearching = false
|
||||
// 搜索文本
|
||||
this.searchText = ''
|
||||
// 匹配的节点列表
|
||||
this.matchNodeList = []
|
||||
// 当前所在的节点列表索引
|
||||
this.currentIndex = -1
|
||||
// 是否正在跳转中
|
||||
this.isJumping = false
|
||||
this.onDataChange = this.onDataChange.bind(this)
|
||||
this.mindMap.on('data_change', this.onDataChange)
|
||||
}
|
||||
|
||||
// 节点数据改变了,需要重新搜索
|
||||
onDataChange() {
|
||||
if (this.isJumping) return
|
||||
this.searchText = ''
|
||||
}
|
||||
|
||||
// 搜索
|
||||
search(text, callback) {
|
||||
text = String(text).trim()
|
||||
if (!text) return this.endSearch()
|
||||
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
|
||||
this.searchText = ''
|
||||
this.matchNodeList = []
|
||||
this.currentIndex = -1
|
||||
this.isJumping = false
|
||||
this.isSearching = false
|
||||
this.emitEvent()
|
||||
}
|
||||
|
||||
// 搜索匹配的节点
|
||||
doSearch() {
|
||||
this.matchNodeList = []
|
||||
this.currentIndex = -1
|
||||
bfsWalk(this.mindMap.renderer.root, node => {
|
||||
let { richText, text } = node.nodeData.data
|
||||
if (richText) {
|
||||
text = getTextFromHtml(text)
|
||||
}
|
||||
if (text.includes(this.searchText)) {
|
||||
this.matchNodeList.push(node)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 搜索下一个,定位到下一个匹配节点
|
||||
searchNext(callback) {
|
||||
if (!this.isSearching || this.matchNodeList.length <= 0) return
|
||||
if (this.currentIndex < this.matchNodeList.length - 1) {
|
||||
this.currentIndex++
|
||||
} else {
|
||||
this.currentIndex = 0
|
||||
}
|
||||
let currentNode = this.matchNodeList[this.currentIndex]
|
||||
this.isJumping = true
|
||||
this.mindMap.execCommand('GO_TARGET_NODE', currentNode, () => {
|
||||
this.isJumping = false
|
||||
callback()
|
||||
})
|
||||
}
|
||||
|
||||
// 替换当前节点
|
||||
replace(replaceText) {
|
||||
replaceText = String(replaceText).trim()
|
||||
if (!replaceText || !this.isSearching || this.matchNodeList.length <= 0)
|
||||
return
|
||||
let currentNode = this.matchNodeList[this.currentIndex]
|
||||
if (!currentNode) return
|
||||
let text = this.getReplacedText(currentNode, this.searchText, replaceText)
|
||||
currentNode.setText(text, currentNode.nodeData.data.richText)
|
||||
this.matchNodeList = this.matchNodeList.filter(node => {
|
||||
return currentNode !== node
|
||||
})
|
||||
this.emitEvent()
|
||||
}
|
||||
|
||||
// 替换所有
|
||||
replaceAll(replaceText) {
|
||||
replaceText = String(replaceText).trim()
|
||||
if (!replaceText || !this.isSearching || this.matchNodeList.length <= 0)
|
||||
return
|
||||
this.matchNodeList.forEach(node => {
|
||||
let text = this.getReplacedText(node, this.searchText, replaceText)
|
||||
this.mindMap.renderer.setNodeDataRender(
|
||||
node,
|
||||
{
|
||||
text,
|
||||
resetRichText: !!node.nodeData.data.richText
|
||||
},
|
||||
true
|
||||
)
|
||||
})
|
||||
this.mindMap.render()
|
||||
this.mindMap.command.addHistory()
|
||||
this.endSearch()
|
||||
}
|
||||
|
||||
// 获取某个节点替换后的文本
|
||||
getReplacedText(node, searchText, replaceText) {
|
||||
let { richText, text } = node.nodeData.data
|
||||
if (richText) {
|
||||
text = getTextFromHtml(text)
|
||||
}
|
||||
return text.replaceAll(searchText, replaceText)
|
||||
}
|
||||
|
||||
// 发送事件
|
||||
emitEvent() {
|
||||
this.mindMap.emit('search_info_change', {
|
||||
currentIndex: this.currentIndex,
|
||||
total: this.matchNodeList.length
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Search.instanceName = 'search'
|
||||
|
||||
export default Search
|
||||
@ -18,6 +18,7 @@
|
||||
></NodeNoteContentShow>
|
||||
<NodeImgPreview v-if="mindMap" :mindMap="mindMap"></NodeImgPreview>
|
||||
<SidebarTrigger v-if="!isZenMode"></SidebarTrigger>
|
||||
<Search v-if="mindMap" :mindMap="mindMap"></Search>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -35,6 +36,7 @@ import RichText from 'simple-mind-map/src/plugins/RichText.js'
|
||||
import AssociativeLine from 'simple-mind-map/src/plugins/AssociativeLine.js'
|
||||
import TouchEvent from 'simple-mind-map/src/plugins/TouchEvent.js'
|
||||
import NodeImgAdjust from 'simple-mind-map/src/plugins/NodeImgAdjust.js'
|
||||
import SearchPlugin from 'simple-mind-map/src/plugins/Search.js'
|
||||
import Outline from './Outline'
|
||||
import Style from './Style'
|
||||
import BaseStyle from './BaseStyle'
|
||||
@ -59,6 +61,7 @@ import Vue from 'vue'
|
||||
import router from '../../../router'
|
||||
import store from '../../../store'
|
||||
import i18n from '../../../i18n'
|
||||
import Search from './Search.vue'
|
||||
|
||||
// 注册插件
|
||||
MindMap
|
||||
@ -73,6 +76,7 @@ MindMap
|
||||
.usePlugin(AssociativeLine)
|
||||
.usePlugin(NodeImgAdjust)
|
||||
.usePlugin(TouchEvent)
|
||||
.usePlugin(SearchPlugin)
|
||||
|
||||
// 注册自定义主题
|
||||
// customThemeList.forEach((item) => {
|
||||
@ -100,7 +104,8 @@ export default {
|
||||
NodeNoteContentShow,
|
||||
Navigator,
|
||||
NodeImgPreview,
|
||||
SidebarTrigger
|
||||
SidebarTrigger,
|
||||
Search
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
188
web/src/pages/Edit/components/Search.vue
Normal file
188
web/src/pages/Edit/components/Search.vue
Normal file
@ -0,0 +1,188 @@
|
||||
<template>
|
||||
<div class="searchContainer" :class="{ isDark: isDark, show: show }">
|
||||
<div class="closeBtnBox">
|
||||
<span class="closeBtn el-icon-close" @click="close"></span>
|
||||
</div>
|
||||
<div class="searchInputBox">
|
||||
<el-input
|
||||
ref="input"
|
||||
placeholder="请输入查找内容"
|
||||
size="small"
|
||||
v-model="searchText"
|
||||
@keyup.native.enter.stop="onSearchNext"
|
||||
>
|
||||
<i slot="prefix" class="el-input__icon el-icon-search"></i>
|
||||
<el-button
|
||||
size="small"
|
||||
slot="append"
|
||||
v-if="!!searchText.trim()"
|
||||
@click="showReplaceInput = true"
|
||||
>替换</el-button
|
||||
>
|
||||
</el-input>
|
||||
<div class="searchInfo" v-if="showSearchInfo">
|
||||
{{ currentIndex }} / {{ total }}
|
||||
</div>
|
||||
</div>
|
||||
<el-input
|
||||
v-if="showReplaceInput"
|
||||
placeholder="请输入替换内容"
|
||||
size="small"
|
||||
v-model="replaceText"
|
||||
style="margin: 12px 0;"
|
||||
>
|
||||
<i slot="prefix" class="el-input__icon el-icon-edit"></i>
|
||||
<el-button size="small" slot="append" @click="hideReplaceInput"
|
||||
>取消</el-button
|
||||
>
|
||||
</el-input>
|
||||
<div class="btnList" v-if="showReplaceInput">
|
||||
<el-button size="small" @click="replace">替换</el-button>
|
||||
<el-button size="small" @click="replaceAll">全部替换</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
// 搜索替换
|
||||
export default {
|
||||
name: 'Search',
|
||||
props: {
|
||||
mindMap: {
|
||||
type: Object
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
show: false,
|
||||
searchText: '',
|
||||
replaceText: '',
|
||||
showReplaceInput: false,
|
||||
currentIndex: 0,
|
||||
total: 0,
|
||||
showSearchInfo: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState(['isDark'])
|
||||
},
|
||||
watch: {
|
||||
searchText() {
|
||||
if (!this.searchText.trim()) {
|
||||
this.currentIndex = 0
|
||||
this.total = 0
|
||||
this.showSearchInfo = false
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.mindMap.on('search_info_change', data => {
|
||||
this.currentIndex = data.currentIndex + 1
|
||||
this.total = data.total
|
||||
this.showSearchInfo = true
|
||||
})
|
||||
this.mindMap.keyCommand.addShortcut('Control+f', () => {
|
||||
this.$bus.$emit('closeSideBar')
|
||||
this.show = true
|
||||
this.$refs.input.focus()
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
hideReplaceInput() {
|
||||
this.showReplaceInput = false
|
||||
this.replaceText = ''
|
||||
},
|
||||
|
||||
onSearchNext() {
|
||||
this.mindMap.search.search(this.searchText, () => {
|
||||
this.$refs.input.focus()
|
||||
})
|
||||
},
|
||||
|
||||
replace() {
|
||||
this.mindMap.search.replace(this.replaceText)
|
||||
},
|
||||
|
||||
replaceAll() {
|
||||
this.mindMap.search.replaceAll(this.replaceText)
|
||||
},
|
||||
|
||||
close() {
|
||||
this.show = false
|
||||
this.showSearchInfo = false
|
||||
this.total = 0
|
||||
this.currentIndex = 0
|
||||
this.searchText = ''
|
||||
this.hideReplaceInput()
|
||||
this.mindMap.search.endSearch()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.searchContainer {
|
||||
position: relative;
|
||||
background-color: #fff;
|
||||
padding: 16px;
|
||||
width: 296px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.1);
|
||||
position: fixed;
|
||||
top: 110px;
|
||||
right: -296px;
|
||||
transition: all 0.3s;
|
||||
|
||||
&.isDark {
|
||||
background-color: #363b3f;
|
||||
|
||||
.closeBtnBox {
|
||||
color: #fff;
|
||||
background-color: #363b3f;
|
||||
}
|
||||
}
|
||||
|
||||
&.show {
|
||||
right: 20px;
|
||||
}
|
||||
|
||||
.btnList {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.closeBtnBox {
|
||||
position: absolute;
|
||||
right: -5px;
|
||||
top: -5px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-color: #fff;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.1);
|
||||
|
||||
.closeBtn {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.searchInputBox {
|
||||
position: relative;
|
||||
|
||||
.searchInfo {
|
||||
position: absolute;
|
||||
right: 70px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #909090;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -39,7 +39,7 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState(['isDark']),
|
||||
...mapState(['isDark'])
|
||||
},
|
||||
watch: {
|
||||
show(val, oldVal) {
|
||||
@ -48,6 +48,11 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.$bus.$on('closeSideBar', () => {
|
||||
this.close()
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
...mapMutations(['setActiveSidebar']),
|
||||
|
||||
@ -74,10 +79,10 @@ export default {
|
||||
|
||||
&.isDark {
|
||||
background-color: #262a2e;
|
||||
border-left-color: hsla(0,0%,100%,.1);
|
||||
border-left-color: hsla(0, 0%, 100%, 0.1);
|
||||
|
||||
.sidebarHeader {
|
||||
border-bottom-color: hsla(0,0%,100%,.1);
|
||||
border-bottom-color: hsla(0, 0%, 100%, 0.1);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user