完成多选操作

This commit is contained in:
wanglin
2021-07-11 13:33:06 +08:00
parent 554dab56d3
commit eeba3153ef
12 changed files with 372 additions and 153 deletions

View File

@@ -8,6 +8,7 @@ import KeyCommand from './src/KeyCommand'
import Command from './src/Command'
import BatchExecution from './src/BatchExecution'
import Export from './src/Export'
import Select from './src/Select'
import {
SVG
} from '@svgdotjs/svg.js'
@@ -47,17 +48,14 @@ class MindMap {
// 容器元素
this.el = this.opt.el
let {
width,
height
} = this.el.getBoundingClientRect()
this.elRect = this.el.getBoundingClientRect()
// 画布宽高
this.width = width
this.height = height
this.width = this.elRect.width
this.height = this.elRect.height
// 画布
this.svg = SVG().addTo(this.el).size(width, height)
this.svg = SVG().addTo(this.el).size(this.width, this.height)
this.draw = this.svg.group()
// 节点id
@@ -97,6 +95,11 @@ class MindMap {
mindMap: this
})
// 选择类
this.select = new Select({
mindMap: this
})
// 批量执行类
this.batchExecution = new BatchExecution()

View File

@@ -46,6 +46,7 @@ class Event extends EventEmitter {
this.onMousemove = this.onMousemove.bind(this)
this.onMouseup = this.onMouseup.bind(this)
this.onMousewheel = this.onMousewheel.bind(this)
this.onContextmenu = this.onContextmenu.bind(this)
}
/**
@@ -65,6 +66,7 @@ class Event extends EventEmitter {
} else {
this.mindMap.el.addEventListener('mousewheel', this.onMousewheel)
}
this.mindMap.svg.on('contextmenu', this.onContextmenu)
}
/**
@@ -74,10 +76,12 @@ class Event extends EventEmitter {
* @Desc: 解绑事件
*/
unbind() {
this.mindMap.svg.off('click', this.onDrawClick)
this.mindMap.el.removeEventListener('mousedown', this.onMousedown)
window.removeEventListener('mousemove', this.onMousemove)
window.removeEventListener('mouseup', this.onMouseup)
this.mindMap.el.removeEventListener('mousewheel', this.onMousewheel)
this.mindMap.svg.off('contextmenu', this.onContextmenu)
}
/**
@@ -152,6 +156,16 @@ class Event extends EventEmitter {
}
this.emit('mousewheel', e, dir, this)
}
/**
* @Author: 王林
* @Date: 2021-07-10 22:34:13
* @Desc: 鼠标右键菜单事件
*/
onContextmenu(e) {
e.preventDefault()
this.emit('contextmenu', e)
}
}
export default Event

View File

@@ -152,7 +152,7 @@ class Node {
this._expandBtn.off(['mouseover', 'mouseout', 'click'])
}
if (this.group) {
this.group.off(['click', 'dblclick'])
this.group.off(['click', 'dblclick', 'contextmenu'])
}
}
@@ -471,7 +471,7 @@ class Node {
let { paddingY } = this.getPaddingVale()
// 创建组
this.group = new G()
this.updatePos(false)
this.update(false)
// 节点矩形
this.style.rect(this.group.rect(width, height))
// 图片节点
@@ -540,6 +540,11 @@ class Node {
this.group.on('dblclick', () => {
this.mindMap.emit('node_dblclick', this)
})
// 右键菜单事件
this.group.on('contextmenu', (e) => {
e.stopPropagation()
e.preventDefault()
})
}
/**
@@ -555,7 +560,7 @@ class Node {
this.mindMap.emit('before_node_active', this, this.renderer.activeNodeList)
this.renderer.clearActive()
this.mindMap.execCommand('SET_NODE_ACTIVE', this, !this.nodeData.data.isActive)
this.renderer.activeNodeList.push(this)
this.renderer.addActiveNode(this)
this.mindMap.emit('node_active', this, this.renderer.activeNodeList)
}
@@ -576,12 +581,18 @@ class Node {
/**
* @Author: 王林
* @Date: 2021-07-04 22:47:01
* @Desc: 更新节点位置
* @Desc: 更新节点
*/
updatePos(animate = true) {
update(animate = true) {
if (!this.group) {
return;
}
// 需要移除展开收缩按钮
if (this._expandBtn && this.nodeData.children.length <= 0) {
this.removeExpandBtn()
} else if (!this._expandBtn && this.nodeData.children.length > 0) {// 需要添加展开收缩按钮
this.renderExpandBtn()
}
let t = this.group.transform()
if (animate) {
this.group.animate(300).translate(this.left - t.translateX, this.top - t.translateY)
@@ -604,7 +615,7 @@ class Node {
this._initRender = false
this.renderNode()
} else {
this.updatePos()
this.update()
}
// 子节点
if (this.children && this.children.length && this.nodeData.data.expand !== false) {
@@ -730,6 +741,22 @@ class Node {
this.renderer.layout.renderExpandBtn(this, this._expandBtn)
}
/**
* @Author: 王林
* @Date: 2021-07-11 13:26:00
* @Desc: 移除展开收缩按钮
*/
removeExpandBtn() {
if (this._expandBtn) {
this._expandBtn.off(['mouseover', 'mouseout', 'click'])
}
// 展开收缩按钮
if (this._expandBtn) {
this._expandBtn.remove()
this._expandBtn = null
}
}
/**
* @Author: 王林
* @Date: 2021-06-20 22:51:57

View File

@@ -139,16 +139,37 @@ class Render {
this.activeNodeList = []
}
/**
* @Author: 王林
* @Date: 2021-07-11 10:54:00
* @Desc: 添加节点到激活列表里
*/
addActiveNode(node) {
let index = this.findActiveNodeIndex(node)
if (index === -1) {
this.activeNodeList.push(node)
}
}
/**
* @Author: 王林
* @Date: 2021-07-10 10:04:04
* @Desc: 在激活列表里移除某个节点
*/
removeActiveNode(node) {
let index = this.activeNodeList.findIndex((item) => {
let index = this.findActiveNodeIndex(node)
this.activeNodeList.splice(index, 1)
}
/**
* @Author: 王林
* @Date: 2021-07-11 10:55:23
* @Desc: 检索某个节点在激活列表里的索引
*/
findActiveNodeIndex(node) {
return this.activeNodeList.findIndex((item) => {
return item === node;
})
this.activeNodeList.splice(index, 1)
}
/**
@@ -165,7 +186,7 @@ class Render {
/**
* @Author: 王林
* @Date: 2021-05-04 13:19:54
* @Desc: 插入同级节点
* @Desc: 插入同级节点,多个节点只会操作第一个节点
*/
insertNode() {
if (this.activeNodeList.length <= 0) {
@@ -196,24 +217,24 @@ class Render {
if (this.activeNodeList.length <= 0) {
return;
}
let node = this.activeNodeList[0]
if (!node.nodeData.children) {
node.nodeData.children = []
}
let len = node.nodeData.children.length
node.nodeData.children.push({
"data": {
"text": "分支主题",
"expand": true
},
"children": []
this.activeNodeList.forEach((node, index) => {
if (!node.nodeData.children) {
node.nodeData.children = []
}
node.nodeData.children.push({
"data": {
"text": "分支主题",
"expand": true
},
"children": []
})
if (node.isRoot) {
this.mindMap.batchExecution.push('renderNode' + index, () => {
node.renderNode()
})
}
})
this.mindMap.render()
if (node.isRoot || len <= 0) {
this.mindMap.batchExecution.push('renderNode', () => {
node.renderNode()
})
}
}
/**
@@ -225,21 +246,24 @@ class Render {
if (this.activeNodeList.length <= 0) {
return;
}
this.activeNodeList.forEach((item) => {
if (item.isRoot) {
item.children.forEach((child) => {
for (let i = 0; i < this.activeNodeList.length; i++) {
let node = this.activeNodeList[i]
if (node.isRoot) {
node.children.forEach((child) => {
child.remove()
})
item.children = []
item.nodeData.children = []
node.children = []
node.nodeData.children = []
break
} else {
this.removeActiveNode(item)
let index = this.getNodeIndex(item)
item.remove()
item.parent.children.splice(index, 1)
item.parent.nodeData.children.splice(index, 1)
this.removeActiveNode(node)
let index = this.getNodeIndex(node)
node.remove()
node.parent.children.splice(index, 1)
node.parent.nodeData.children.splice(index, 1)
i--
}
})
}
this.mindMap.render()
}

View File

@@ -0,0 +1,123 @@
import { bfsWalk, throttle } from './utils'
/**
* @Author: 王林
* @Date: 2021-07-10 22:34:51
* @Desc: 选择节点类
*/
class Select {
/**
* @Author: 王林
* @Date: 2021-07-10 22:35:16
* @Desc: 构造函数
*/
constructor({ mindMap }) {
this.mindMap = mindMap
this.rect = null
this.isMousedown = false
this.mouseDownX = 0
this.mouseDownY = 0
this.mouseMoveX = 0
this.mouseMoveY = 0
this.bindEvent()
}
/**
* @Author: 王林
* @Date: 2021-07-10 22:36:36
* @Desc: 绑定事件
*/
bindEvent() {
this.checkInNodes = throttle(this.checkInNodes, 500, this)
this.mindMap.on('contextmenu', (e) => {
this.isMousedown = true
let { x, y } = this.toPos(e.clientX, e.clientY)
this.mouseDownX = x
this.mouseDownY = y
this.createRect(x, y)
})
this.mindMap.on('mousemove', (e) => {
if (!this.isMousedown) {
return
}
this.mouseMoveX = e.clientX
this.mouseMoveY = e.clientY
this.rect.plot([
[this.mouseDownX, this.mouseDownY],
[this.mouseMoveX, this.mouseDownY],
[this.mouseMoveX, this.mouseMoveY],
[this.mouseDownX, this.mouseMoveY]
])
this.mindMap.batchExecution.push('checkInNodes', this.checkInNodes)
})
this.mindMap.on('mouseup', (e) => {
if (!this.isMousedown) {
return;
}
this.mindMap.emit('node_active', null, this.mindMap.renderer.activeNodeList)
this.isMousedown = false
if (this.rect) this.rect.remove()
this.rect = null
})
}
/**
* @Author: 王林
* @Date: 2021-07-11 10:19:37
* @Desc: 创建矩形
*/
createRect(x, y) {
this.rect = this.mindMap.svg.polygon().stroke({
color: '#0984e3'
}).fill({
color: 'rgba(9,132,227,0.3)'
}).plot([[x, y]])
}
/**
* @Author: 王林
* @Date: 2021-07-11 09:20:03
* @Desc: 转换位置
*/
toPos(x, y) {
return {
x: x - this.mindMap.elRect.left,
y: y - this.mindMap.elRect.top
}
}
/**
* @Author: 王林
* @Date: 2021-07-11 10:20:43
* @Desc: 检测在选区里的节点
*/
checkInNodes() {
let { scaleX, scaleY, translateX, translateY } = this.mindMap.draw.transform()
let minx = Math.min(this.mouseDownX, this.mouseMoveX)
let miny = Math.min(this.mouseDownY, this.mouseMoveY)
let maxx = Math.max(this.mouseDownX, this.mouseMoveX)
let maxy = Math.max(this.mouseDownY, this.mouseMoveY)
bfsWalk(this.mindMap.renderer.root, (node) => {
let { left, top, width, height } = node
let right = (left + width) * scaleX + translateX
let bottom = (top + height) * scaleY + translateY
left = left * scaleX + translateX
top = top * scaleY + translateY
if (
left >= minx &&
right <= maxx &&
top >= miny &&
bottom <= maxy
) {
this.mindMap.execCommand('SET_NODE_ACTIVE', node, true)
this.mindMap.renderer.addActiveNode(node)
} else if (node.nodeData.data.isActive) {
this.mindMap.execCommand('SET_NODE_ACTIVE', node, false)
this.mindMap.renderer.removeActiveNode(node)
}
})
}
}
export default Select

View File

@@ -8,7 +8,7 @@ export const walk = (root, parent, beforeCallback, afterCallback, isRoot, layerI
let stop = false
if (beforeCallback) {
stop = beforeCallback(root, parent, isRoot, layerIndex, index)
}
}
if (!stop && root.children && root.children.length > 0) {
let _layerIndex = layerIndex + 1
root.children.forEach((node, nodeIndex) => {
@@ -176,4 +176,22 @@ export const downloadFile = (file, fileName) => {
a.href = file
a.download = fileName
a.click()
}
/**
* @Author: 王林
* @Date: 2021-07-11 10:36:47
* @Desc: 节流函数
*/
export const throttle = (fn, time = 300, ctx) => {
let timer = null
return () => {
if (timer) {
return
}
timer = setTimeout(() => {
fn.call(ctx)
timer = null
}, 300)
};
}

View File

@@ -6,13 +6,17 @@
width="500"
>
<div class="item">
<span class="name">链接</span>
<el-input v-model="link" size="mini" placeholder="http://xxxx.com/"></el-input>
</div>
<span class="name">链接</span>
<el-input
v-model="link"
size="mini"
placeholder="http://xxxx.com/"
></el-input>
</div>
<div class="item">
<span class="name">名称</span>
<el-input v-model="linkTitle" size="mini"></el-input>
</div>
<span class="name">名称</span>
<el-input v-model="linkTitle" size="mini"></el-input>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="cancel"> </el-button>
<el-button type="primary" @click="confirm"> </el-button>
@@ -21,10 +25,10 @@
</template>
<script>
/**
* @Author: 王林
* @Date: 2021-06-24 22:53:17
* @Desc: 节点超链接内容设置
/**
* @Author: 王林
* @Date: 2021-06-24 22:53:17
* @Desc: 节点超链接内容设置
*/
export default {
name: "NodeHyperlink",
@@ -33,16 +37,16 @@ export default {
dialogVisible: false,
link: "",
linkTitle: "",
activeNode: null
activeNodes: [],
};
},
created() {
this.$bus.$on("node_active", (...args) => {
let activeNodes = args[1];
if (activeNodes.length > 0) {
this.activeNode = activeNodes[0];
this.link = this.activeNode.getData("hyperlink");
this.linkTitle = this.activeNode.getData("hyperlinkTitle");
this.activeNodes = args[1];
if (this.activeNodes.length > 0) {
let firstNode = this.activeNodes[0];
this.link = firstNode.getData("hyperlink");
this.linkTitle = firstNode.getData("hyperlinkTitle");
} else {
this.link = "";
this.linkTitle = "";
@@ -68,8 +72,10 @@ export default {
* @Desc: 确定
*/
confirm() {
this.activeNode.setHyperlink(this.link, this.linkTitle);
this.cancel();
this.activeNodes.forEach((node) => {
node.setHyperlink(this.link, this.linkTitle);
this.cancel();
});
},
},
};
@@ -78,14 +84,14 @@ export default {
<style lang="less" scoped>
.nodeDialog {
.item {
display: flex;
align-items: center;
margin-bottom: 10px;
.name {
display: block;
width: 50px;
}
}
display: flex;
align-items: center;
margin-bottom: 10px;
.name {
display: block;
width: 50px;
}
}
}
</style>

View File

@@ -13,7 +13,7 @@
v-for="icon in item.list"
:key="icon.name"
v-html="icon.icon"
@click="setIcon(item.type, icon.name)"
@click="setIcon(item.type, icon.name)"
></div>
</div>
</div>
@@ -23,10 +23,10 @@
<script>
import { nodeIconList } from "simple-mind-map/src/svg/icons";
/**
* @Author: 王林
* @Date: 2021-06-24 22:53:33
* @Desc: 节点图标内容设置
/**
* @Author: 王林
* @Date: 2021-06-24 22:53:33
* @Desc: 节点图标内容设置
*/
export default {
name: "NodeIcon",
@@ -35,15 +35,15 @@ export default {
nodeIconList,
dialogVisible: false,
icon: [],
activeNode: null,
activeNodes: [],
};
},
created() {
this.$bus.$on("node_active", (...args) => {
let activeNodes = args[1];
if (activeNodes.length > 0) {
this.activeNode = activeNodes[0];
this.icon = this.activeNode.getData("icon") || [];
this.activeNodes = args[1];
if (this.activeNodes.length > 0) {
let firstNode = this.activeNodes[0];
this.icon = firstNode.getData("icon") || [];
} else {
this.icon = [];
}
@@ -53,52 +53,54 @@ export default {
});
},
methods: {
/**
* @Author: 王林
* @Date: 2021-06-23 23:16:56
* @Desc: 设置icon
*/
setIcon(type, name) {
let index = this.icon.findIndex((item) => {
return item.split('_')[0] === type;
})
if (index !== -1) {
this.icon.splice(index, 1, type + '_' + name)
} else {
this.icon.push(type + '_' + name)
}
this.activeNode.setIcon([...this.icon])
}
/**
* @Author: 王林
* @Date: 2021-06-23 23:16:56
* @Desc: 设置icon
*/
setIcon(type, name) {
let index = this.icon.findIndex((item) => {
return item.split("_")[0] === type;
});
if (index !== -1) {
this.icon.splice(index, 1, type + "_" + name);
} else {
this.icon.push(type + "_" + name);
}
this.activeNodes.forEach((node) => {
node.setIcon([...this.icon]);
});
},
},
};
</script>
<style lang="less" scoped>
.nodeDialog {
/deep/ .el-dialog__body {
padding: 0 20px;
}
/deep/ .el-dialog__body {
padding: 0 20px;
}
.item {
margin-bottom: 20px;
font-weight: bold;
font-weight: bold;
.title {
margin-bottom: 10px;
}
.list {
display: flex;
flex-wrap: wrap;
.list {
display: flex;
flex-wrap: wrap;
.icon {
width: 24px;
height: 24px;
margin-right: 10px;
margin-bottom: 10px;
cursor: pointer;
}
}
.icon {
width: 24px;
height: 24px;
margin-right: 10px;
margin-bottom: 10px;
cursor: pointer;
}
}
}
}
</style>

View File

@@ -35,20 +35,19 @@ export default {
dialogVisible: false,
img: "",
imgTitle: "",
activeNode: null,
activeNodes: null,
};
},
created() {
this.$bus.$on("node_active", (...args) => {
let activeNodes = args[1];
if (activeNodes.length > 0) {
this.activeNode = activeNodes[0];
this.img = this.activeNode.getData("image");
this.imgTitle = this.activeNode.getData("imageTitle");
this.activeNodes = args[1];
if (this.activeNodes.length > 0) {
let firstNode = this.activeNodes[0];
this.img = firstNode.getData("image");
this.imgTitle = firstNode.getData("imageTitle");
} else {
this.img = "";
this.imgTitle = "";
this.activeNode = null;
}
});
this.$bus.$on("showNodeImage", () => {
@@ -73,11 +72,13 @@ export default {
async confirm() {
try {
let { width, height } = await this.$refs.ImgUpload.getSize();
this.activeNode.setImage({
url: this.img || "none",
title: this.imgTitle,
width,
height,
this.activeNodes.forEach((node) => {
node.setImage({
url: this.img || "none",
title: this.imgTitle,
width,
height,
});
});
this.cancel();
} catch (error) {

View File

@@ -21,10 +21,10 @@
</template>
<script>
/**
* @Author: 王林
* @Date: 2021-06-24 22:53:54
* @Desc: 节点备注内容设置
/**
* @Author: 王林
* @Date: 2021-06-24 22:53:54
* @Desc: 节点备注内容设置
*/
export default {
name: "NodeNote",
@@ -32,15 +32,15 @@ export default {
return {
dialogVisible: false,
note: "",
activeNode: null,
activeNodes: [],
};
},
created() {
this.$bus.$on("node_active", (...args) => {
let activeNodes = args[1];
if (activeNodes.length > 0) {
this.activeNode = activeNodes[0];
this.note = this.activeNode.getData("note");
this.activeNodes = args[1];
if (this.activeNodes.length > 0) {
let firstNode = this.activeNodes[0];
this.note = firstNode.getData("note");
} else {
this.note = "";
}
@@ -65,7 +65,9 @@ export default {
* @Desc: 确定
*/
confirm() {
this.activeNode.setNote(this.note);
this.activeNodes.forEach((node) => {
node.setNote(this.note);
});
this.cancel();
},
},
@@ -76,7 +78,7 @@ export default {
.nodeDialog {
.tip {
margin-top: 5px;
color: #DCDFE6;
color: #dcdfe6;
}
}
</style>

View File

@@ -51,16 +51,16 @@ export default {
tagColorList,
tagArr: [],
tag: "",
activeNode: null,
activeNodes: [],
max: 5,
};
},
created() {
this.$bus.$on("node_active", (...args) => {
let activeNodes = args[1];
if (activeNodes.length > 0) {
this.activeNode = activeNodes[0];
this.tagArr = this.activeNode.getData("tag") || [];
this.activeNodes = args[1];
if (this.activeNodes.length > 0) {
let firstNode = this.activeNodes[0];
this.tagArr = firstNode.getData("tag") || [];
} else {
this.tagArr = [];
this.tag = "";
@@ -105,7 +105,9 @@ export default {
* @Desc: 确定
*/
confirm() {
this.activeNode.setTag(this.tagArr);
this.activeNodes.forEach((node) => {
node.setTag(this.tagArr);
});
this.cancel();
},
},

View File

@@ -5,7 +5,7 @@
<el-tab-pane label="常态" name="normal"></el-tab-pane>
<el-tab-pane label="选中状态" name="active"></el-tab-pane>
</el-tabs>
<div class="sidebarContent" v-if="activeNode">
<div class="sidebarContent" v-if="activeNodes.length > 0">
<!-- 文字 -->
<div class="title noTop">文字</div>
<div class="row">
@@ -267,7 +267,7 @@ export default {
borderDasharrayList,
borderRadiusList,
lineHeightList,
activeNode: null,
activeNodes: [],
activeTab: "normal",
style: {
paddingX: 0,
@@ -292,9 +292,8 @@ export default {
this.$refs.sidebar.show = false;
this.$nextTick(() => {
this.activeTab = "normal";
let activeNodes = args[1];
this.activeNode = activeNodes[0];
this.$refs.sidebar.show = activeNodes.length > 0;
this.activeNodes = args[1];
this.$refs.sidebar.show = this.activeNodes.length > 0;
this.initNodeStyle();
});
});
@@ -315,7 +314,7 @@ export default {
* @Desc: 初始节点样式
*/
initNodeStyle() {
if (!this.activeNode) {
if (this.activeNodes.length <= 0) {
this.activeTab = "normal";
return;
}
@@ -335,7 +334,7 @@ export default {
"borderDasharray",
"borderRadius",
].forEach((item) => {
this.style[item] = this.activeNode.getStyle(
this.style[item] = this.activeNodes[0].getStyle(
item,
false,
this.activeTab === "active"
@@ -349,11 +348,9 @@ export default {
* @Desc: 修改样式
*/
update(prop) {
this.activeNode.setStyle(
prop,
this.style[prop],
this.activeTab === "active"
);
this.activeNodes.forEach((node) => {
node.setStyle(prop, this.style[prop], this.activeTab === "active");
});
},
/**