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 RichText from './src/plugins/RichText'
|
||||||
import NodeImgAdjust from './src/plugins/NodeImgAdjust.js'
|
import NodeImgAdjust from './src/plugins/NodeImgAdjust.js'
|
||||||
import TouchEvent from './src/plugins/TouchEvent.js'
|
import TouchEvent from './src/plugins/TouchEvent.js'
|
||||||
|
import Search from './src/plugins/Search.js'
|
||||||
import xmind from './src/parse/xmind.js'
|
import xmind from './src/parse/xmind.js'
|
||||||
import markdown from './src/parse/markdown.js'
|
import markdown from './src/parse/markdown.js'
|
||||||
import icons from './src/svg/icons.js'
|
import icons from './src/svg/icons.js'
|
||||||
@ -36,5 +37,6 @@ MindMap
|
|||||||
.usePlugin(RichText)
|
.usePlugin(RichText)
|
||||||
.usePlugin(TouchEvent)
|
.usePlugin(TouchEvent)
|
||||||
.usePlugin(NodeImgAdjust)
|
.usePlugin(NodeImgAdjust)
|
||||||
|
.usePlugin(Search)
|
||||||
|
|
||||||
export default MindMap
|
export default MindMap
|
||||||
@ -975,7 +975,8 @@ class Render {
|
|||||||
setNodeText(node, text, richText) {
|
setNodeText(node, text, richText) {
|
||||||
this.setNodeDataRender(node, {
|
this.setNodeDataRender(node, {
|
||||||
text,
|
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
|
let uid = typeof node === 'string' ? node : node.nodeData.data.uid
|
||||||
if (!uid) return
|
if (!uid) return
|
||||||
this.expandToNodeUid(uid, () => {
|
this.expandToNodeUid(uid, () => {
|
||||||
@ -1109,6 +1110,7 @@ class Render {
|
|||||||
if (targetNode) {
|
if (targetNode) {
|
||||||
targetNode.active()
|
targetNode.active()
|
||||||
this.moveNodeToCenter(targetNode)
|
this.moveNodeToCenter(targetNode)
|
||||||
|
callback()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -1121,7 +1123,7 @@ class Render {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 设置节点数据,并判断是否渲染
|
// 设置节点数据,并判断是否渲染
|
||||||
setNodeDataRender(node, data) {
|
setNodeDataRender(node, data, notRender = false) {
|
||||||
this.setNodeData(node, data)
|
this.setNodeData(node, data)
|
||||||
let changed = node.reRender()
|
let changed = node.reRender()
|
||||||
if (changed) {
|
if (changed) {
|
||||||
@ -1129,7 +1131,7 @@ class Render {
|
|||||||
// 概要节点
|
// 概要节点
|
||||||
node.generalizationBelongNode.updateGeneralization()
|
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>
|
></NodeNoteContentShow>
|
||||||
<NodeImgPreview v-if="mindMap" :mindMap="mindMap"></NodeImgPreview>
|
<NodeImgPreview v-if="mindMap" :mindMap="mindMap"></NodeImgPreview>
|
||||||
<SidebarTrigger v-if="!isZenMode"></SidebarTrigger>
|
<SidebarTrigger v-if="!isZenMode"></SidebarTrigger>
|
||||||
|
<Search v-if="mindMap" :mindMap="mindMap"></Search>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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 AssociativeLine from 'simple-mind-map/src/plugins/AssociativeLine.js'
|
||||||
import TouchEvent from 'simple-mind-map/src/plugins/TouchEvent.js'
|
import TouchEvent from 'simple-mind-map/src/plugins/TouchEvent.js'
|
||||||
import NodeImgAdjust from 'simple-mind-map/src/plugins/NodeImgAdjust.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 Outline from './Outline'
|
||||||
import Style from './Style'
|
import Style from './Style'
|
||||||
import BaseStyle from './BaseStyle'
|
import BaseStyle from './BaseStyle'
|
||||||
@ -59,6 +61,7 @@ import Vue from 'vue'
|
|||||||
import router from '../../../router'
|
import router from '../../../router'
|
||||||
import store from '../../../store'
|
import store from '../../../store'
|
||||||
import i18n from '../../../i18n'
|
import i18n from '../../../i18n'
|
||||||
|
import Search from './Search.vue'
|
||||||
|
|
||||||
// 注册插件
|
// 注册插件
|
||||||
MindMap
|
MindMap
|
||||||
@ -73,6 +76,7 @@ MindMap
|
|||||||
.usePlugin(AssociativeLine)
|
.usePlugin(AssociativeLine)
|
||||||
.usePlugin(NodeImgAdjust)
|
.usePlugin(NodeImgAdjust)
|
||||||
.usePlugin(TouchEvent)
|
.usePlugin(TouchEvent)
|
||||||
|
.usePlugin(SearchPlugin)
|
||||||
|
|
||||||
// 注册自定义主题
|
// 注册自定义主题
|
||||||
// customThemeList.forEach((item) => {
|
// customThemeList.forEach((item) => {
|
||||||
@ -100,7 +104,8 @@ export default {
|
|||||||
NodeNoteContentShow,
|
NodeNoteContentShow,
|
||||||
Navigator,
|
Navigator,
|
||||||
NodeImgPreview,
|
NodeImgPreview,
|
||||||
SidebarTrigger
|
SidebarTrigger,
|
||||||
|
Search
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
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: {
|
computed: {
|
||||||
...mapState(['isDark']),
|
...mapState(['isDark'])
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
show(val, oldVal) {
|
show(val, oldVal) {
|
||||||
@ -48,6 +48,11 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
created() {
|
||||||
|
this.$bus.$on('closeSideBar', () => {
|
||||||
|
this.close()
|
||||||
|
})
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapMutations(['setActiveSidebar']),
|
...mapMutations(['setActiveSidebar']),
|
||||||
|
|
||||||
@ -74,10 +79,10 @@ export default {
|
|||||||
|
|
||||||
&.isDark {
|
&.isDark {
|
||||||
background-color: #262a2e;
|
background-color: #262a2e;
|
||||||
border-left-color: hsla(0,0%,100%,.1);
|
border-left-color: hsla(0, 0%, 100%, 0.1);
|
||||||
|
|
||||||
.sidebarHeader {
|
.sidebarHeader {
|
||||||
border-bottom-color: hsla(0,0%,100%,.1);
|
border-bottom-color: hsla(0, 0%, 100%, 0.1);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user