Merge branch 'feature' into main

This commit is contained in:
wanglin2 2023-07-28 09:36:54 +08:00
commit 74d37f2cbc
64 changed files with 903 additions and 61 deletions

View File

@ -140,4 +140,12 @@ const mindMap = new MindMap({
<img src="./web/src/assets/avatar/Chris.jpg" style="width: 50px;height: 50px;" />
<span>Chris</span>
</span>
<span>
<img src="./web/src/assets/avatar/水车.jpg" style="width: 50px;height: 50px;" />
<span>水车</span>
</span>
<span>
<img src="./web/src/assets/avatar/仓鼠.jpg" style="width: 50px;height: 50px;" />
<span>仓鼠</span>
</span>
</p>

File diff suppressed because one or more lines are too long

View File

@ -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

View File

@ -7,9 +7,9 @@ import Style from './src/core/render/node/Style'
import KeyCommand from './src/core/command/KeyCommand'
import Command from './src/core/command/Command'
import BatchExecution from './src/utils/BatchExecution'
import { layoutValueList, CONSTANTS } from './src/constants/constant'
import { layoutValueList, CONSTANTS, commonCaches } from './src/constants/constant'
import { SVG } from '@svgdotjs/svg.js'
import { simpleDeepClone } from './src/utils'
import { simpleDeepClone, getType } from './src/utils'
import defaultTheme, { checkIsNodeSizeIndependenceConfig } from './src/themes/default'
import { defaultOpt } from './src/constants/defaultOptions'
@ -35,6 +35,9 @@ class MindMap {
// 初始化主题
this.initTheme()
// 初始化缓存数据
this.initCache()
// 事件类
this.event = new Event({
mindMap: this
@ -129,6 +132,23 @@ class MindMap {
this.event.off(event, fn)
}
// 初始化缓存数据
initCache() {
Object.keys(commonCaches).forEach((key) => {
let type = getType(commonCaches[key])
let value = ''
switch(type) {
case 'Boolean':
value = false
break
default:
value = null
break
}
commonCaches[key] = value
})
}
// 设置主题
initTheme() {
// 合并主题配置

View File

@ -1,6 +1,6 @@
{
"name": "simple-mind-map",
"version": "0.6.8",
"version": "0.6.9",
"description": "一个简单的web在线思维导图",
"authors": [
{

View File

@ -324,4 +324,9 @@ export const nodeDataNoStylePropList = [
'resetRichText',
'uid',
'activeStyle'
]
]
// 数据缓存
export const commonCaches = {
measureCustomNodeContentSizeEl: null
}

View File

@ -444,6 +444,7 @@ class Render {
if (this.activeNodeList.length <= 0 && appointNodes.length <= 0) {
return
}
this.textEdit.hideEditTextBox()
let {
defaultInsertSecondLevelNodeText,
defaultInsertBelowSecondLevelNodeText
@ -486,6 +487,7 @@ class Render {
if (this.activeNodeList.length <= 0 && appointNodes.length <= 0) {
return
}
this.textEdit.hideEditTextBox()
let {
defaultInsertSecondLevelNodeText,
defaultInsertBelowSecondLevelNodeText
@ -973,7 +975,8 @@ class Render {
setNodeText(node, text, richText) {
this.setNodeDataRender(node, {
text,
richText
richText,
resetRichText: richText
})
}
@ -1099,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, () => {
@ -1107,6 +1110,7 @@ class Render {
if (targetNode) {
targetNode.active()
this.moveNodeToCenter(targetNode)
callback()
}
})
}
@ -1119,7 +1123,7 @@ class Render {
}
// 设置节点数据,并判断是否渲染
setNodeDataRender(node, data) {
setNodeDataRender(node, data, notRender = false) {
this.setNodeData(node, data)
let changed = node.reRender()
if (changed) {
@ -1127,7 +1131,7 @@ class Render {
// 概要节点
node.generalizationBelongNode.updateGeneralization()
}
this.mindMap.render()
if (!notRender) this.mindMap.render()
}
}

View File

@ -106,6 +106,9 @@ export default class TextEdit {
this.mindMap.keyCommand.addShortcut('Enter', () => {
this.hideEditTextBox()
})
this.mindMap.keyCommand.addShortcut('Tab', () => {
this.hideEditTextBox()
})
}
// 显示文本编辑框

View File

@ -1,7 +1,7 @@
import { measureText, resizeImgSize, getTextFromHtml } from '../../../utils'
import { Image, SVG, A, G, Rect, Text, ForeignObject } from '@svgdotjs/svg.js'
import iconsSvg from '../../../svg/icons'
import { CONSTANTS } from '../../../constants/constant'
import { CONSTANTS, commonCaches } from '../../../constants/constant'
// 创建图片节点
function createImgNode() {
@ -293,20 +293,19 @@ function createNoteNode() {
}
// 测量自定义节点内容元素的宽高
let warpEl = null
function measureCustomNodeContentSize (content) {
if (!warpEl) {
warpEl = document.createElement('div')
warpEl.style.cssText = `
if (!commonCaches.measureCustomNodeContentSizeEl) {
commonCaches.measureCustomNodeContentSizeEl = document.createElement('div')
commonCaches.measureCustomNodeContentSizeEl.style.cssText = `
position: fixed;
left: -99999px;
top: -99999px;
`
this.mindMap.el.appendChild(warpEl)
this.mindMap.el.appendChild(commonCaches.measureCustomNodeContentSizeEl)
}
warpEl.innerHTML = ''
warpEl.appendChild(content)
let rect = warpEl.getBoundingClientRect()
commonCaches.measureCustomNodeContentSizeEl.innerHTML = ''
commonCaches.measureCustomNodeContentSizeEl.appendChild(content)
let rect = commonCaches.measureCustomNodeContentSizeEl.getBoundingClientRect()
return {
width: rect.width,
height: rect.height

View File

@ -36,15 +36,14 @@ function createGeneralizationNode () {
// 更新概要节点
function updateGeneralization () {
if (this.isGeneralization) return
this.removeGeneralization()
this.createGeneralizationNode()
}
// 渲染概要节点
function renderGeneralization () {
if (this.isGeneralization) {
return
}
if (this.isGeneralization) return
if (!this.checkHasGeneralization()) {
this.removeGeneralization()
this._generalizationNodeWidth = 0
@ -67,6 +66,7 @@ function renderGeneralization () {
// 删除概要节点
function removeGeneralization () {
if (this.isGeneralization) return
if (this._generalizationLine) {
this._generalizationLine.remove()
this._generalizationLine = null
@ -87,6 +87,7 @@ function removeGeneralization () {
// 隐藏概要节点
function hideGeneralization () {
if (this.isGeneralization) return
if (this._generalizationLine) {
this._generalizationLine.hide()
}
@ -97,6 +98,7 @@ function hideGeneralization () {
// 显示概要节点
function showGeneralization () {
if (this.isGeneralization) return
if (this._generalizationLine) {
this._generalizationLine.show()
}

View File

@ -11,7 +11,7 @@ import {
import associativeLineControlsMethods from './associativeLine/associativeLineControls'
import associativeLineTextMethods from './associativeLine/associativeLineText'
// 关联线
// 关联线插件
class AssociativeLine {
constructor(opt = {}) {
this.mindMap = opt.mindMap

View File

@ -1,8 +1,7 @@
import { bfsWalk, throttle } from '../utils'
import Base from '../layouts/Base'
// 节点拖动类
// 节点拖动插件
class Drag extends Base {
// 构造函数
constructor({ mindMap }) {

View File

@ -1,9 +1,9 @@
import { imgToDataUrl, downloadFile, readBlob } from '../utils'
import { imgToDataUrl, downloadFile, readBlob, removeHTMLEntities } from '../utils'
import { SVG } from '@svgdotjs/svg.js'
import drawBackgroundImageToCanvas from '../utils/simulateCSSBackgroundInCanvas'
import { transformToMarkdown } from '../parse/toMarkdown'
// 导出
// 导出插件
class Export {
// 构造函数
constructor(opt) {
@ -154,6 +154,7 @@ class Export {
*/
async png(name, transparent = false) {
let { node, str } = await this.getSvgData()
str = removeHTMLEntities(str)
// 如果开启了富文本则使用htmltocanvas转换为图片
if (this.mindMap.richText) {
let res = await this.mindMap.richText.handleExportPng(node.node)
@ -207,6 +208,7 @@ class Export {
node.first().before(SVG(`<title>${name}</title>`))
await this.drawBackgroundToSvg(node)
let str = node.svg()
str = removeHTMLEntities(str)
// 转换成blob数据
let blob = new Blob([str], {
type: 'image/svg+xml'

View File

@ -1,6 +1,6 @@
import JsPDF from 'jspdf'
// 导出PDF需要通过Export插件使用
// 导出PDF插件需要通过Export插件使用
class ExportPDF {
// 构造函数
constructor(opt) {

View File

@ -1,6 +1,6 @@
import xmind from '../parse/xmind'
// 导出XMind需要通过Export插件使用
// 导出XMind插件需要通过Export插件使用
class ExportXMind {
// 构造函数
constructor(opt) {

View File

@ -1,7 +1,7 @@
import { bfsWalk } from '../utils'
import { CONSTANTS } from '../constants/constant'
// 键盘导航
// 键盘导航插件
class KeyboardNavigation {
// 构造函数
constructor(opt) {

View File

@ -1,4 +1,4 @@
// 小地图
// 小地图插件
class MiniMap {
// 构造函数
constructor(opt) {

View File

@ -28,7 +28,7 @@ let fontSizeList = new Array(100).fill(0).map((_, index) => {
return index + 'px'
})
// 节点支持富文本编辑功能
// 富文本编辑插件
class RichText {
constructor({ mindMap, pluginOpt }) {
this.mindMap = mindMap
@ -268,6 +268,12 @@ class RichText {
handler: function () {
// 覆盖默认的回车键换行
}
},
tab: {
key: 9,
handler: function () {
// 覆盖默认的tab键
}
}
}
}

View 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

View File

@ -1,7 +1,6 @@
import { bfsWalk, throttle } from '../utils'
// 选择节点类
// 节点选择插件
class Select {
// 构造函数
constructor({ mindMap }) {

View File

@ -1,5 +1,4 @@
// 手势事件支持类
// 手势事件支持插件
class TouchEvent {
// 构造函数
constructor({ mindMap }) {

View File

@ -2,7 +2,7 @@ import { Text, G } from '@svgdotjs/svg.js'
import { degToRad, camelCaseToHyphen } from '../utils'
import merge from 'deepmerge'
// 水印
// 水印插件
class Watermark {
constructor(opt = {}) {
this.mindMap = opt.mindMap

View File

@ -453,3 +453,16 @@ export const loadImage = imgFile => {
}
})
}
// 移除字符串中的html实体
export const removeHTMLEntities = (str) => {
[['&nbsp;', '&#160;']].forEach((item) => {
str = str.replaceAll(item[0], item[1])
})
return str
}
// 获取一个数据的类型
export const getType = (data) => {
return Object.prototype.toString.call(data).slice(7, -1)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -1,8 +1,8 @@
@font-face {
font-family: "iconfont"; /* Project id 2479351 */
src: url('iconfont.woff2?t=1689407546912') format('woff2'),
url('iconfont.woff?t=1689407546912') format('woff'),
url('iconfont.ttf?t=1689407546912') format('truetype');
src: url('iconfont.woff2?t=1690506335310') format('woff2'),
url('iconfont.woff?t=1690506335310') format('woff'),
url('iconfont.ttf?t=1690506335310') format('truetype');
}
.iconfont {
@ -13,6 +13,10 @@
-moz-osx-font-smoothing: grayscale;
}
.iconsousuo:before {
content: "\e693";
}
.iconjiantouyou:before {
content: "\e62d";
}

View File

@ -271,6 +271,11 @@ export const shortcutKeyList = [
icon: 'iconzhengli',
name: 'Arrange layout',
value: 'Ctrl + L'
},
{
icon: 'iconsousuo',
name: 'Search and Replace',
value: 'Ctrl + F'
}
]
},

View File

@ -331,6 +331,11 @@ export const shortcutKeyList = [
icon: 'iconzhengli',
name: '一键整理布局',
value: 'Ctrl + L'
},
{
icon: 'iconsousuo',
name: '搜索和替换',
value: 'Ctrl + F'
}
]
},

View File

@ -210,5 +210,12 @@ export default {
mouseAction: {
tip1: 'Current: Left click to drag the canvas, right click to box select nodes',
tip2: 'Current: Left click to box select nodes, right click to drag the canvas',
},
search: {
searchPlaceholder: 'Please enter the search content',
replacePlaceholder: 'Please enter replacement content',
replace: 'Replace',
replaceAll: 'Replace all',
cancel: 'Cancel'
}
}

View File

@ -210,5 +210,12 @@ export default {
mouseAction: {
tip1: '当前:左键拖动画布,右键框选节点',
tip2: '当前:左键框选节点,右键拖动画布',
},
search: {
searchPlaceholder: '请输入查找内容',
replacePlaceholder: '请输入替换内容',
replace: '替换',
replaceAll: '全部替换',
cancel: '取消'
}
}

View File

@ -32,6 +32,7 @@ let APIList = [
'associativeLine',
'touchEvent',
'nodeImgAdjust',
'search',
'xmind',
'markdown',
'utils'

View File

@ -1,5 +1,13 @@
# Changelog
## 0.6.9
Fix: 1.Fixed an issue where setting styles to summary nodes would cause summary nodes to disappear. 2.Fixed the issue of node content not rendering when creating a root instance again when customizing node content. 3.Fix the issue of losing focus when adding a new node while the node is in editing. 2.Fix the issue of continuously pressing the tab key not being able to continuously create child nodes.
New: 1.Replace existing `&nbsp;` in SVG when exporting Characters to avoid exporting SVG errors. 2.Support for search and replace.
Demo: 1.When switching themes, it is supported to choose whether to overwrite the set basic style.
## 0.6.8
Fix: 1.Change the shortcut key for inserting a summary to Ctrl+G to avoid conflicts with the save shortcut key. 2.Fix the issue of abnormal switching between rich text editing configuration input boxes while nodes are being edited.

View File

@ -1,6 +1,10 @@
<template>
<div>
<h1>Changelog</h1>
<h2>0.6.9</h2>
<p>Fix: 1.Fixed an issue where setting styles to summary nodes would cause summary nodes to disappear. 2.Fixed the issue of node content not rendering when creating a root instance again when customizing node content. 3.Fix the issue of losing focus when adding a new node while the node is in editing. 2.Fix the issue of continuously pressing the tab key not being able to continuously create child nodes.</p>
<p>New: 1.Replace existing <code>&amp;nbsp;</code> in SVG when exporting Characters to avoid exporting SVG errors. 2.Support for search and replace.</p>
<p>Demo: 1.When switching themes, it is supported to choose whether to overwrite the set basic style.</p>
<h2>0.6.8</h2>
<p>Fix: 1.Change the shortcut key for inserting a summary to Ctrl+G to avoid conflicts with the save shortcut key. 2.Fix the issue of abnormal switching between rich text editing configuration input boxes while nodes are being edited.</p>
<p>New: 1.Modify the copy, cut, and paste logic, and support pasting data from the clipboard.</p>

View File

@ -347,7 +347,7 @@ redo. All commands are as follows:
| SET_NODE_CUSTOM_POSITION (v0.2.0+) | Set a custom position for a node | node (the node to set), left (custom x coordinate, default is undefined), top (custom y coordinate, default is undefined) |
| RESET_LAYOUT (v0.2.0+) | Arrange layout with one click | |
| SET_NODE_SHAPE (v0.2.4+) | Set the shape of a node | node (the node to set), shape (the shape, all shapes: [Shape.js](https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/src/core/render/node/Shape.js)) |
| GO_TARGET_NODEv0.6.7+ | Navigate to a node, and if the node is collapsed, it will automatically expand to that node | nodeNode instance or node uid to locate |
| GO_TARGET_NODEv0.6.7+ | Navigate to a node, and if the node is collapsed, it will automatically expand to that node | nodeNode instance or node uid to locate、callbackv0.6.9+, Callback function after positioning completion |
### setData(data)

View File

@ -923,7 +923,7 @@ redo. All commands are as follows:</p>
<tr>
<td>GO_TARGET_NODEv0.6.7+</td>
<td>Navigate to a node, and if the node is collapsed, it will automatically expand to that node</td>
<td>nodeNode instance or node uid to locate</td>
<td>nodeNode instance or node uid to locatecallbackv0.6.9+, Callback function after positioning completion</td>
</tr>
</tbody>
</table>

View File

@ -160,4 +160,12 @@ Open source is not easy. If this project is helpful to you, you can invite the a
<img src="../../../../assets/avatar/Chris.jpg" style="width: 50px;height: 50px;object-fit: cover;border-radius: 50%;" />
<p>Chris</p>
</div>
<div style="display: flex; flex-direction: column; align-items: center; width: fit-content; margin: 5px;">
<img src="../../../../assets/avatar/水车.jpg" style="width: 50px;height: 50px;object-fit: cover;border-radius: 50%;" />
<p>水车</p>
</div>
<div style="display: flex; flex-direction: column; align-items: center; width: fit-content; margin: 5px;">
<img src="../../../../assets/avatar/仓鼠.jpg" style="width: 50px;height: 50px;object-fit: cover;border-radius: 50%;" />
<p>仓鼠</p>
</div>
</div>

View File

@ -119,6 +119,14 @@ full screen, support mini map</li>
<img src="../../../../assets/avatar/Chris.jpg" style="width: 50px;height: 50px;object-fit: cover;border-radius: 50%;" />
<p>Chris</p>
</div>
<div style="display: flex; flex-direction: column; align-items: center; width: fit-content; margin: 5px;">
<img src="../../../../assets/avatar/水车.jpg" style="width: 50px;height: 50px;object-fit: cover;border-radius: 50%;" />
<p>水车</p>
</div>
<div style="display: flex; flex-direction: column; align-items: center; width: fit-content; margin: 5px;">
<img src="../../../../assets/avatar/仓鼠.jpg" style="width: 50px;height: 50px;object-fit: cover;border-radius: 50%;" />
<p>仓鼠</p>
</div>
</div>
</div>
</template>

View File

@ -61,7 +61,9 @@ Delete a specific node
Copy a node, the active node is the node to be operated on, if there are
multiple active nodes, only the first node will be operated on
### setNodeDataRender(node, data)
### setNodeDataRender(node, data, notRender)
- `notRender`: v0.6.9+, `Boolean`, Default is `false`, Do not trigger rendering.
Set node `data`, i.e. the data in the data field, and will determine whether the
node needs to be re-rendered based on whether the node size has changed, `data`

View File

@ -37,7 +37,10 @@ disable the enter key and delete key related shortcuts to prevent conflicts</p>
<h3>copyNode()</h3>
<p>Copy a node, the active node is the node to be operated on, if there are
multiple active nodes, only the first node will be operated on</p>
<h3>setNodeDataRender(node, data)</h3>
<h3>setNodeDataRender(node, data, notRender)</h3>
<ul>
<li><code>notRender</code>: v0.6.9+, <code>Boolean</code>, Default is <code>false</code>, Do not trigger rendering.</li>
</ul>
<p>Set node <code>data</code>, i.e. the data in the data field, and will determine whether the
node needs to be re-rendered based on whether the node size has changed, <code>data</code>
is an object, e.g. <code>{text: 'I am new text'}</code></p>

View File

@ -0,0 +1,68 @@
# Search plugin
> v0.6.9+
This plugin provides the ability to search and replace node content.
## Register
```js
import MindMap from 'simple-mind-map'
import Search from 'simple-mind-map/src/plugins/Search.js'
MindMap.usePlugin(Search)
```
After registration and instantiation of `MindMap`, the instance can be obtained through `mindMap.Search`.
## Event
### search_info_change
You can listen to 'search_info_change' event to get the number of current search results and the index currently located.
```js
mindMap.on('search_info_change', (data) => {
/*
data: {
currentIndex,// Index, from zero
total
}
*/
})
```
## Method
### search(searchText, callback)
- `searchText`: Text to search for
- `callback`: The callback function that completes this search will be triggered after jumping to the node
Search for node content, which can be called repeatedly. Each call will search and locate to the next matching node. If the search text changes, it will be searched again.
### endSearch()
End search.
### replace(replaceText)
- `replaceText`: Text to be replaced
To replace the content of the current node, call the 'search' method after calling it to replace the content of the currently located matching node.
### replaceAll(replaceText)
- `replaceText`: Text to be replaced
Replace all matching node contents, and call it after calling the 'search' method.
### getReplacedText(node, searchText, replaceText)
- `node`: Node instance
- `searchText`: Text to search for
- `replaceText`: Text to be replaced
Return the text content of the node after search and replacement. Note that the node content will not be actually changed, but is only used to calculate the content of a node after replacement.

View File

@ -0,0 +1,74 @@
<template>
<div>
<h1>Search plugin</h1>
<blockquote>
<p>v0.6.9+</p>
</blockquote>
<p>This plugin provides the ability to search and replace node content.</p>
<h2>Register</h2>
<pre class="hljs"><code><span class="hljs-keyword">import</span> MindMap <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;simple-mind-map&#x27;</span>
<span class="hljs-keyword">import</span> Search <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;simple-mind-map/src/plugins/Search.js&#x27;</span>
MindMap.usePlugin(Search)
</code></pre>
<p>After registration and instantiation of <code>MindMap</code>, the instance can be obtained through <code>mindMap.Search</code>.</p>
<h2>Event</h2>
<h3>search_info_change</h3>
<p>You can listen to 'search_info_change' event to get the number of current search results and the index currently located.</p>
<pre class="hljs"><code>mindMap.on(<span class="hljs-string">&#x27;search_info_change&#x27;</span>, <span class="hljs-function">(<span class="hljs-params">data</span>) =&gt;</span> {
<span class="hljs-comment">/*
data: {
currentIndex,// Index, from zero
total
}
*/</span>
})
</code></pre>
<h2>Method</h2>
<h3>search(searchText, callback)</h3>
<ul>
<li>
<p><code>searchText</code>: Text to search for</p>
</li>
<li>
<p><code>callback</code>: The callback function that completes this search will be triggered after jumping to the node</p>
</li>
</ul>
<p>Search for node content, which can be called repeatedly. Each call will search and locate to the next matching node. If the search text changes, it will be searched again.</p>
<h3>endSearch()</h3>
<p>End search.</p>
<h3>replace(replaceText)</h3>
<ul>
<li><code>replaceText</code>: Text to be replaced</li>
</ul>
<p>To replace the content of the current node, call the 'search' method after calling it to replace the content of the currently located matching node.</p>
<h3>replaceAll(replaceText)</h3>
<ul>
<li><code>replaceText</code>: Text to be replaced</li>
</ul>
<p>Replace all matching node contents, and call it after calling the 'search' method.</p>
<h3>getReplacedText(node, searchText, replaceText)</h3>
<ul>
<li>
<p><code>node</code>: Node instance</p>
</li>
<li>
<p><code>searchText</code>: Text to search for</p>
</li>
<li>
<p><code>replaceText</code>: Text to be replaced</p>
</li>
</ul>
<p>Return the text content of the node after search and replacement. Note that the node content will not be actually changed, but is only used to calculate the content of a node after replacement.</p>
</div>
</template>
<script>
export default {
}
</script>
<style>
</style>

View File

@ -196,6 +196,12 @@ Load image, return:
}
```
#### getType(data)
> v0.6.9+
Get the type of a data, such as `Boolean`、`Array`.
## Simulate CSS background in Canvas
Import:

View File

@ -134,6 +134,11 @@ and copying the <code>data</code> of the data object, example:</p>
size<span class="hljs-comment">// { width, height } width and height of image</span>
}
</code></pre>
<h4>getType(data)</h4>
<blockquote>
<p>v0.6.9+</p>
</blockquote>
<p>Get the type of a data, such as <code>Boolean</code><code>Array</code>.</p>
<h2>Simulate CSS background in Canvas</h2>
<p>Import:</p>
<pre class="hljs"><code><span class="hljs-keyword">import</span> drawBackgroundImageToCanvas <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;simple-mind-map/src/utils/simulateCSSBackgroundInCanvas&#x27;</span>

View File

@ -49,6 +49,7 @@ export default [
{ path: 'client', title: '客户端' },
{ path: 'touchEvent', title: 'TouchEvent插件' },
{ path: 'nodeImgAdjust', title: 'NodeImgAdjust插件' },
{ path: 'search', title: 'Search插件' },
{ path: 'help1', title: '概要/关联线' },
{ path: 'help2', title: '客户端' }
]
@ -80,7 +81,8 @@ export default [
{ path: 'xmind', title: 'XMind parse' },
{ path: 'deploy', title: 'Deploy' },
{ path: 'touchEvent', title: 'TouchEvent plugin' },
{ path: 'nodeImgAdjust', title: 'NodeImgAdjust plugin' }
{ path: 'nodeImgAdjust', title: 'NodeImgAdjust plugin' },
{ path: 'search', title: 'Search plugin' }
]
}
]

View File

@ -1,5 +1,13 @@
# Changelog
## 0.6.9
修复1.修复给概要节点设置样式概要节点会消失的问题。2.修复自定义节点内容时二次创建根实例时节点内容不渲染的问题。3.修复节点处于编辑中时添加新节点时新节点的焦点丢失问题。 2.修复连续按tab键无法连续创建子节点的问题。
新增1.导出svg时替换svg中存在的`&nbsp;`字符避免导出的svg报错。 2.支持搜索和替换。
Demo1.切换主题时支持选择是否覆盖设置过的基础样式。
## 0.6.8
修复1.修改插入概要的快捷键为Ctrl+G避免和保存快捷键冲突。 2.修复节点正在编辑时切换富文本编辑配置输入框出现异常的问题。

View File

@ -1,6 +1,10 @@
<template>
<div>
<h1>Changelog</h1>
<h2>0.6.9</h2>
<p>修复1.修复给概要节点设置样式概要节点会消失的问题2.修复自定义节点内容时二次创建根实例时节点内容不渲染的问题3.修复节点处于编辑中时添加新节点时新节点的焦点丢失问题 2.修复连续按tab键无法连续创建子节点的问题</p>
<p>新增1.导出svg时替换svg中存在的<code>&amp;nbsp;</code>字符避免导出的svg报错 2.支持搜索和替换</p>
<p>Demo1.切换主题时支持选择是否覆盖设置过的基础样式</p>
<h2>0.6.8</h2>
<p>修复1.修改插入概要的快捷键为Ctrl+G避免和保存快捷键冲突 2.修复节点正在编辑时切换富文本编辑配置输入框出现异常的问题</p>
<p>新增1.修改复制剪切粘贴逻辑支持粘贴剪切板中的数据</p>

View File

@ -340,7 +340,7 @@ mindMap.updateConfig({
| SET_NODE_CUSTOM_POSITIONv0.2.0+ | 设置节点自定义位置 | node要设置的节点、 left自定义的x坐标默认为undefined、 top自定义的y坐标默认为undefined |
| RESET_LAYOUTv0.2.0+ | 一键整理布局 | |
| SET_NODE_SHAPEv0.2.4+ | 设置节点形状 | node要设置的节点、shape形状全部形状[Shape.js](https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/src/core/render/node/Shape.js) |
| GO_TARGET_NODEv0.6.7+ | 定位到某个节点,如果该节点被收起,那么会自动展开到该节点 | node要定位到的节点实例或节点uid |
| GO_TARGET_NODEv0.6.7+ | 定位到某个节点,如果该节点被收起,那么会自动展开到该节点 | node要定位到的节点实例或节点uid、callbackv0.6.9+,定位完成后的回调函数) |
### setData(data)

View File

@ -918,7 +918,7 @@ mindMap.setTheme(<span class="hljs-string">&#x27;主题名称&#x27;</span>)
<tr>
<td>GO_TARGET_NODEv0.6.7+</td>
<td>定位到某个节点如果该节点被收起那么会自动展开到该节点</td>
<td>node要定位到的节点实例或节点uid</td>
<td>node要定位到的节点实例或节点uidcallbackv0.6.9+定位完成后的回调函数</td>
</tr>
</tbody>
</table>

View File

@ -151,4 +151,12 @@
<img src="../../../../assets/avatar/Chris.jpg" style="width: 50px;height: 50px;object-fit: cover;border-radius: 50%;" />
<p>Chris</p>
</div>
<div style="display: flex; flex-direction: column; align-items: center; width: fit-content; margin: 5px;">
<img src="../../../../assets/avatar/水车.jpg" style="width: 50px;height: 50px;object-fit: cover;border-radius: 50%;" />
<p>水车</p>
</div>
<div style="display: flex; flex-direction: column; align-items: center; width: fit-content; margin: 5px;">
<img src="../../../../assets/avatar/仓鼠.jpg" style="width: 50px;height: 50px;object-fit: cover;border-radius: 50%;" />
<p>仓鼠</p>
</div>
</div>

View File

@ -111,6 +111,14 @@
<img src="../../../../assets/avatar/Chris.jpg" style="width: 50px;height: 50px;object-fit: cover;border-radius: 50%;" />
<p>Chris</p>
</div>
<div style="display: flex; flex-direction: column; align-items: center; width: fit-content; margin: 5px;">
<img src="../../../../assets/avatar/水车.jpg" style="width: 50px;height: 50px;object-fit: cover;border-radius: 50%;" />
<p>水车</p>
</div>
<div style="display: flex; flex-direction: column; align-items: center; width: fit-content; margin: 5px;">
<img src="../../../../assets/avatar/仓鼠.jpg" style="width: 50px;height: 50px;object-fit: cover;border-radius: 50%;" />
<p>仓鼠</p>
</div>
</div>
</div>
</template>

View File

@ -54,7 +54,9 @@
复制节点,操作节点为当前激活节点,有多个激活节点只会操作第一个节点
### setNodeDataRender(node, data)
### setNodeDataRender(node, data, notRender)
- `notRender`v0.6.9+`Boolean`,默认为`false`,是否不要触发渲染。
设置节点数据,即`data`字段的数据,并会根据节点大小是否变化来判断是否需要重新渲染该节点,`data`为对象,如:`{text: '我是新文本'}`

View File

@ -28,7 +28,10 @@
<p>删除某个指定节点</p>
<h3>copyNode()</h3>
<p>复制节点操作节点为当前激活节点有多个激活节点只会操作第一个节点</p>
<h3>setNodeDataRender(node, data)</h3>
<h3>setNodeDataRender(node, data, notRender)</h3>
<ul>
<li><code>notRender</code>v0.6.9+<code>Boolean</code>默认为<code>false</code>是否不要触发渲染</li>
</ul>
<p>设置节点数据<code>data</code>字段的数据并会根据节点大小是否变化来判断是否需要重新渲染该节点<code>data</code>为对象<code>{text: '我是新文本'}</code></p>
<h3>moveNodeTo(node, toNode)</h3>
<blockquote>

View File

@ -0,0 +1,68 @@
# Search 插件
> v0.6.9+
该插件提供搜索和替换节点内容的功能。
## 注册
```js
import MindMap from 'simple-mind-map'
import Search from 'simple-mind-map/src/plugins/Search.js'
MindMap.usePlugin(Search)
```
注册完且实例化`MindMap`后可通过`mindMap.search`获取到该实例。
## 事件
### search_info_change
可以通过监听`search_info_change`事件来获取当前搜索结果的数量和当前定位到的索引。
```js
mindMap.on('search_info_change', (data) => {
/*
data: {
currentIndex,// 索引从0开始
total
}
*/
})
```
## 方法
### search(searchText, callback)
- `searchText`:要进行搜索的文本
- `callback`:本次搜索完成的回调函数,会在跳转到节点后触发
搜索节点内容,可以重复调用,每调一次,会搜索和定位到下一个匹配的节点。如果搜索文本改变了,那么会重新搜索。
### endSearch()
结束搜索。
### replace(replaceText)
- `replaceText`:要进行替换的文本
替换当前节点内容,要在调用了`search`方法之后调用,会替换当前定位到的匹配节点内容。
### replaceAll(replaceText)
- `replaceText`:要进行替换的文本
替换所有匹配的节点内容,要在调用了`search`方法之后调用。
### getReplacedText(node, searchText, replaceText)
- `node`:节点实例
- `searchText`:要进行搜索的文本
- `replaceText`:要进行替换的文本
返回该节点搜索和替换后的文本内容,注意,不会实际改变节点内容,只是用来计算一个节点替换后的内容。

View File

@ -0,0 +1,74 @@
<template>
<div>
<h1>Search 插件</h1>
<blockquote>
<p>v0.6.9+</p>
</blockquote>
<p>该插件提供搜索和替换节点内容的功能</p>
<h2>注册</h2>
<pre class="hljs"><code><span class="hljs-keyword">import</span> MindMap <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;simple-mind-map&#x27;</span>
<span class="hljs-keyword">import</span> Search <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;simple-mind-map/src/plugins/Search.js&#x27;</span>
MindMap.usePlugin(Search)
</code></pre>
<p>注册完且实例化<code>MindMap</code>后可通过<code>mindMap.search</code>获取到该实例</p>
<h2>事件</h2>
<h3>search_info_change</h3>
<p>可以通过监听<code>search_info_change</code>事件来获取当前搜索结果的数量和当前定位到的索引</p>
<pre class="hljs"><code>mindMap.on(<span class="hljs-string">&#x27;search_info_change&#x27;</span>, <span class="hljs-function">(<span class="hljs-params">data</span>) =&gt;</span> {
<span class="hljs-comment">/*
data: {
currentIndex,// 0
total
}
*/</span>
})
</code></pre>
<h2>方法</h2>
<h3>search(searchText, callback)</h3>
<ul>
<li>
<p><code>searchText</code>要进行搜索的文本</p>
</li>
<li>
<p><code>callback</code>本次搜索完成的回调函数会在跳转到节点后触发</p>
</li>
</ul>
<p>搜索节点内容可以重复调用每调一次会搜索和定位到下一个匹配的节点如果搜索文本改变了那么会重新搜索</p>
<h3>endSearch()</h3>
<p>结束搜索</p>
<h3>replace(replaceText)</h3>
<ul>
<li><code>replaceText</code>要进行替换的文本</li>
</ul>
<p>替换当前节点内容要在调用了<code>search</code>方法之后调用会替换当前定位到的匹配节点内容</p>
<h3>replaceAll(replaceText)</h3>
<ul>
<li><code>replaceText</code>要进行替换的文本</li>
</ul>
<p>替换所有匹配的节点内容要在调用了<code>search</code>方法之后调用</p>
<h3>getReplacedText(node, searchText, replaceText)</h3>
<ul>
<li>
<p><code>node</code>节点实例</p>
</li>
<li>
<p><code>searchText</code>要进行搜索的文本</p>
</li>
<li>
<p><code>replaceText</code>要进行替换的文本</p>
</li>
</ul>
<p>返回该节点搜索和替换后的文本内容注意不会实际改变节点内容只是用来计算一个节点替换后的内容</p>
</div>
</template>
<script>
export default {
}
</script>
<style>
</style>

View File

@ -191,6 +191,12 @@ copyNodeTree({}, node)
}
```
#### getType(data)
> v0.6.9+
获取一个数据的类型,比如`Boolean`、`Array`等。
## 在canvas中模拟css的背景属性
引入:

View File

@ -129,6 +129,11 @@
size<span class="hljs-comment">// { width, height } </span>
}
</code></pre>
<h4>getType(data)</h4>
<blockquote>
<p>v0.6.9+</p>
</blockquote>
<p>获取一个数据的类型比如<code>Boolean</code><code>Array</code></p>
<h2>在canvas中模拟css的背景属性</h2>
<p>引入</p>
<pre class="hljs"><code><span class="hljs-keyword">import</span> drawBackgroundImageToCanvas <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;simple-mind-map/src/utils/simulateCSSBackgroundInCanvas&#x27;</span>

View File

@ -79,6 +79,14 @@ export default {
body {
&.isDark {
/* el-button */
.el-button {
background-color: #363b3f;
color: hsla(0,0%,100%,.9);
border-color: hsla(0, 0%, 100%, 0.1);
}
/* el-input */
.el-input__inner {
background-color: #363b3f;
border-color: hsla(0, 0%, 100%, 0.1);
@ -91,6 +99,15 @@ body {
color: hsla(0,0%,100%,.3);
}
.el-input-group__append, .el-input-group__prepend {
background-color: #363b3f;
border-color: hsla(0, 0%, 100%, 0.1);
}
.el-input-group__append button.el-button {
color: hsla(0, 0%, 100%, 0.9);
}
/* el-select */
.el-select-dropdown {
background-color: #36393d;

View File

@ -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 {

View File

@ -0,0 +1,192 @@
<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="$t('search.searchPlaceholder')"
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"
>{{ $t('search.replace') }}</el-button
>
</el-input>
<div class="searchInfo" v-if="showSearchInfo">
{{ currentIndex }} / {{ total }}
</div>
</div>
<el-input
v-if="showReplaceInput"
:placeholder="$t('search.replacePlaceholder')"
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">{{
$t('search.cancel')
}}</el-button>
</el-input>
<div class="btnList" v-if="showReplaceInput">
<el-button size="small" @click="replace">{{
$t('search.replace')
}}</el-button>
<el-button size="small" @click="replaceAll">{{
$t('search.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>

View File

@ -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;
}

View File

@ -42,7 +42,7 @@ export default {
},
data() {
return {
themeList: [...themeList].reverse(),// ...customThemeList
themeList: [...themeList].reverse(), // ...customThemeList
themeMap,
theme: ''
}
@ -61,32 +61,48 @@ export default {
}
}
},
created () {
created() {
this.theme = this.mindMap.getTheme()
this.handleDark()
},
methods: {
...mapMutations(['setIsDark']),
/**
* @Author: 王林
* @Date: 2021-06-24 23:04:38
* @Desc: 使用主题
*/
useTheme(theme) {
this.theme = theme.value
this.handleDark()
const customThemeConfig = this.mindMap.getCustomThemeConfig()
const hasCustomThemeConfig = Object.keys(customThemeConfig).length > 0
if (hasCustomThemeConfig) {
this.$confirm('你当前自定义过基础样式,是否覆盖?', '提示', {
confirmButtonText: '覆盖',
cancelButtonText: '保留',
type: 'warning'
})
.then(() => {
this.mindMap.setThemeConfig({})
this.changeTheme(theme, {})
})
.catch(() => {
this.changeTheme(theme, customThemeConfig)
})
} else {
this.changeTheme(theme, customThemeConfig)
}
},
changeTheme(theme, config) {
this.mindMap.setTheme(theme.value)
storeConfig({
theme: {
template: theme.value,
config: this.mindMap.getCustomThemeConfig()
config
}
})
},
handleDark() {
let target = themeList.find((item) => {
let target = themeList.find(item => {
return item.value === this.theme
})
this.setIsDark(target.dark)