项目需要 思维导图这个功能,暂时还不知道具体需要什么功能,就自己先搞了一下,简单实现节点添加 编辑 删除 拖拽 及下载导图等功能,还有鼠标右键点击节点出现自定义菜单的功能,看了好多的自己以及其他人写的,发现很多都是有缺失的,不能拿来直接使用,下面我总结了一下,可以直接复制使用。
(注:jsmind.menu.js 引用一定要放到 jsmind后面)
按钮事件 或者 鼠标右键按钮事件 可二选一选用,废话不多说 直接上代码
<template>
<div class="brain_content">
<div class="header_btn">
<el-button @click='zoomOut' ref="zoomOut">缩小</el-button>
<el-button @click='zoomIn' ref="zoomIn">放大</el-button>
<el-button @click="addNode">添加节点</el-button>
<el-button @click="onRemoveNode">删除节点</el-button>
</div>
<js-mind class="js_mind" :options="options" :values="mind" v-show="isShow" ref="jsMind" height="100%"></js-mind>
</div>
</template>
<script>
import Vue from 'vue'
import jm from "vue-jsmind"
import '../../../../../public/static/js/jsmind.menu.js'
Vue.use(jm)
if (window.jsMind) {
console.log('wind')
Vue.prototype.jsMind = window.jsMind
}
export default {
data () {
return {
drawer: false,
direction: 'rtl',
theme_value: '',
mind: {
meta: {/* 元数据,定义思维导图的名称、作者、版本等信息 */
name: 'ztengluo',
author: 'hizzgdev@163.com',
version: '0.2'
},
format: 'node_tree',
data: {
id: 'root',
topic: '三国',
children: [
{
id: 'easy', // [必选] ID, 所有节点的ID不应有重复,否则ID重复的结节将被忽略
topic: '魏', // [必选] 节点上显示的内容
direction: 'left', // [可选] 节点的方向,此数据仅在第一层节点上有效,目前仅支持 left 和 right 两种,默认为 right
expanded: true, // [可选] 该节点是否是展开状态,默认为 true
children: [
{ id: 'easy1', topic: '曹操' },
{ id: 'easy2', topic: '张辽' },
{ id: 'easy3', topic: '司马懿' },
{ id: 'easy4', topic: '司马昭' },
{ id: 'easy5', topic: '邓艾' },
{ id: 'easy6', topic: '夏侯淳' },
]
},
{
id: 'open',
topic: '蜀',
direction: 'right',
expanded: true,
children: [
{
id: 'open1',
topic: '刘备',
children: [
{
id: 'open2',
topic: '文',
children: [
{ id: 'open3', topic: '诸葛亮' },
{ id: 'open4', topic: '徐庶' },
]
},
{
id: 'open5',
topic: '武',
children: [
{ id: 'open6', topic: '黄忠' },
{ id: 'open7', topic: '马超' },
{ id: 'open8', topic: '姜维' },
{ id: 'open9', topic: '张飞' },
{ id: 'open10', topic: '赵云' },
]
},
]
},
]
},
{
id: 'powerful',
topic: '吴',
direction: 'right',
children: [
{ id: 'powerful1', topic: '孙策' },
{ id: 'powerful2', topic: '孙权' },
{ id: 'powerful3', topic: '周瑜' },
{ id: 'powerful4', topic: '鲁肃' },
{ id: 'powerful5', topic: '吕蒙' },
{ id: 'powerful6', topic: '黄盖' },
]
}
]
}
},
options: {
container: 'jsmind_container', // [必选] 容器的ID
editable: false, // [可选] 是否启用编辑
theme: 'clouds', // [可选] 主题
support_html : true, // 是否支持节点里的HTML元素
view:{
engine: 'canvas', // 思维导图各节点之间线条的绘制引擎
hmargin:100, // 思维导图距容器外框的最小水平距离
vmargin:50, // 思维导图距容器外框的最小垂直距离
line_width:1, // 思维导图线条的粗细
line_color:'#C18AFB' // 思维导图线条的颜色
},
menuOpts:{
showMenu: true,
injectionList: [
{target:'edit',text: '编辑节点',
callback: function (node) {
console.log(node)
}
},
{target:'addChild',text: '添加子节点',
callback: function (node) {
console.log(node)
}
},
{target:'addBrother',text: '添加兄弟节点',
callback: function (node) {
console.log(node)
}
},
{target:'delete',text: '删除节点',
callback: function (node,next) {
console.log(node)
}
},
{target:'screenshot',text: '下载导图',
callback: function (node,next) {
console.log(node)
}
},
{target:'showAll',text: '展开全部节点',
callback: function (node,next) {
console.log(node)
}
},
{target:'hideAll',text: '收起全部节点',
callback: function (node,next) {
console.log(node)
}
},
],
},
},
isShow: true,
isLoad: false,
}
},
methods: {
// 缩小
zoomOut () {
if (this.jm.view.zoomOut()) {
this.$refs.zoomOut.disabled = false
} else {
this.$refs.zoomOut.disabled = true
}
},
// 放大
zoomIn () {
if (this.jm.view.zoomIn()) {
this.$refs.zoomIn.disabled = false
} else {
this.$refs.zoomIn.disabled = true
}
},
// 新增节点
addNode () {
var selectedNode = this.jm.get_selected_node() // as parent of new node
if (!selectedNode) { alert('请先选择一个节点'); return }
var nodeid = this.jsMind.util.uuid.newid()
var topic = '请输入子节点名称'
this.jm.add_node(selectedNode, nodeid, topic)
},
// 删除节点
onRemoveNode () {
var selectedId = this.get_selected_nodeid()
if (!selectedId) { alert('请先选择一个节点'); return }
this.jm.remove_node(selectedId)
},
// 获取选中标签的 ID
get_selected_nodeid () {
var selectedNode = this.jm.get_selected_node()
if (selectedNode) {
return selectedNode.id
} else {
return null
}
}
},
mounted () {
// 阻止浏览器默认右键事件
document.oncontextmenu = function () {
return false;
};
this.jm = this.$refs.jsMind.jm
this.jm.enable_edit()
},
}
</script>
<style lang="less" scoped>
.brain_content {
/deep/ jmnodes.theme-clouds jmnode{background-color: #f0fadd;color:#333;}
/deep/ jmnodes.theme-clouds jmnode:hover{background-color:#D6E3BD;}
/deep/ jmnodes.theme-clouds jmnode.selected{background-color: #b6d47e;color:#333;}
height: 100%;
padding: 10px;
display: flex;
flex-direction: column;
.header_btn {
height: 40px;
line-height: 40px;
}
.js_mind {
flex: 1;
}
}
</style>
下面上一下jsmind.menu.js 文件
/*
* Released under BSD License
* Copyright (c) 2019-2020 Allen_sun_js@hotmail.com
*
* Project Home:
* https://github.com/allensunjian
*/
(function ($w, temp) {
var Jm = $w[temp]
var name = 'menu'
var $d = $w['document']
var menuEvent = 'oncontextmenu'
var clickEvent = 'onclick'
var overEvent = 'mouseover'
var $c = function (tag) { return $d.createElement(tag) }
var _noop = function () { }
var logger = (typeof console === 'undefined') ? {
log: _noop, debug: _noop, error: _noop, warn: _noop, info: _noop
} : console
var $t = function (n, t) { if (n.hasChildNodes()) { n.firstChild.nodeValue = t } else { n.appendChild($d.createTextNode(t)) } }
var $h = function (n, t) {
if (t instanceof HTMLElement) {
t.innerHTML = ''
n.appendChild(t)
} else {
n.innerHTML = t
}
}
if (!Jm || Jm[name]) return
Jm.menu = function (_jm) {
this._get_menu_options(_jm, function () {
this.init(_jm)
this._mount_events()
})
}
Jm.menu.prototype = {
defaultDataMap: {
funcMap: {
edit: {
isDepNode: true,
// defaultFn不受到中台变量的控制,始终会先于fn去执行
defaultFn: function (node) {
var f = this._menu_default_mind_methods._menu_begin_edit.call(this.jm)
f && this._menu_default_mind_methods._menu_edit_node_begin(this.jm.view, node)
},
fn: _noop,
text: 'edit node'
},
addChild: {
isDepNode: true,
fn: function (nodeid, text) {
var selected_node = this.get_selected_node()
if (selected_node) {
var node = this.add_node(selected_node, nodeid, text)
if (node) {
this.select_node(nodeid)
this.begin_edit(nodeid)
}
}
},
text: 'append child'
},
addBrother: {
isDepNode: true,
fn: function (nodeid, text) {
var selected_node = this.get_selected_node()
if (!!selected_node && !selected_node.isroot) {
var node = this.insert_node_after(selected_node, nodeid, text)
if (node) {
this.select_node(nodeid)
this.begin_edit(nodeid)
}
}
},
text: 'append brother'
},
delete: {
isDepNode: true,
fn: function () {
this.shortcut.handle_delnode.call(this.shortcut, this)
},
text: 'delete node'
},
showAll: {
sDepNode: false,
fn: function () {
this.expand_all(this)
},
text: 'show all'
},
hideAll: {
isDepNode: false,
fn: function () {
this.collapse_all(this)
},
text: 'hide all'
},
screenshot: {
isDepNode: false,
fn: function () {
if (!this.screenshot) {
logger.error('[jsmind] screenshot dependent on jsmind.screenshot.js !')
return
}
this.screenshot.shootDownload()
},
text: 'load mind picture'
},
showNode: {
isDepNode: true,
fn: function (node) {
this.expand_node(node)
},
text: 'show target node'
},
hideNode: {
isDepNode: true,
fn: function (node) {
this.collapse_node(node)
},
text: 'hide target node'
}
},
menuStl: {
'width': '150px',
'padding': '12px 0',
'position': 'fixed',
'z-index': '10',
'background': '#fff',
'box-shadow': '0 2px 12px 0 rgba(0,0,0,0.1)',
'border-radius': '5px',
'font-size': '12px',
'display': 'none'
},
menuItemStl: {
padding: '5px 15px',
cursor: 'pointer',
display: 'block',
'text-align': 'center',
'transition': 'all .2s'
},
injectionList: ['edit', 'addChild', 'delete']
},
init: function (_jm) {
this._create_menu(_jm)
this._get_injectionList(_jm)
this.menuOpts.switchMidStage && Jm.util.dom.add_event(_jm.view.e_editor, 'blur', function (e) {
this._menu_default_mind_methods._menu_edit_node_end.call(_jm.view)
if (typeof this.menuOpts.editCaller === 'function') {
this.menuOpts.editCaller($w.menu._update_node_info, this._menu_default_mind_methods._menu_update_edit_node)
return
}
this._menu_default_mind_methods._menu_update_edit_node()
}.bind(this))
},
_event_contextMenu (e) {
e.preventDefault()
this.menu.style.left = e.clientX + 'px'
this.menu.style.top = e.clientY + 'px'
this.menu.style.display = 'block'
this.selected_node = this.jm.get_selected_node()
},
_event_hideMenu () {
this.menu.style.display = 'none'
},
_mount_events () {
$w[menuEvent] = this._event_contextMenu.bind(this)
$w[clickEvent] = this._event_hideMenu.bind(this)
},
_create_menu (_jm) {
var d = $c('menu')
this._set_menu_wrap_syl(d)
this.menu = d
this.e_panel = _jm.view.e_panel
this.e_panel.appendChild(d)
},
_create_menu_item (j, text, fn, isDepNode, cb, defaultFn) {
var d = $c('menu-item'); var _this = this
this._set_menu_item_syl(d)
d.innerText = text
d.addEventListener('click', function () {
if (this.selected_node || !isDepNode) {
defaultFn.call(_this, this.selected_node)
if (!_this._get_mid_opts()) {
cb(this.selected_node, _noop)
fn.call(j, Jm.util.uuid.newid(), this.menuOpts.newNodeText || '请输入节点名称')
return
}
cb(this.selected_node, _this._mid_stage_next(function () {
var retArgs = [this.selected_node]
var argus = Array.prototype.slice.call(arguments[0], 0)
argus[1] = this.menuOpts.newNodeText || '请输入节点名称'
if (argus[0]) {
retArgs = argus
}
fn.apply(j, retArgs)
}.bind(this)))
return
}
alert(this.menuOpts.tipContent || 'Continue with node selected!')
}.bind(this))
d.addEventListener('mouseover', function () {
d.style.background = 'rgb(179, 216, 255)'
})
d.addEventListener('mouseleave', function () {
d.style.background = '#fff'
})
return d
},
_set_menu_wrap_syl (d) {
var os = this._get_option_sty('menu', this._get_mixin_sty)
d.style.cssText = this._format_cssText(os)
},
_set_menu_item_syl (d) {
var os = this._get_option_sty('menuItem', this._get_mixin_sty)
d.style.cssText = this._format_cssText(os)
},
_format_cssText (o) {
var text = ''
Object.keys(o).forEach(function (k) {
text += k + ':' + o[k] + ';'
})
return text
},
_empty_object (o) {
return Object.keys(o).length == 0
},
_get_option_sty (type, fn) {
var sty = this.menuOpts.style
var menu = this.defaultDataMap.menuStl
var menuItem = this.defaultDataMap.menuItemStl
var o = {menu, menuItem}
if (!sty) return o[type]
if (!sty[type]) return o[type]
if (!sty[type] || this._empty_object(sty[type])) return o[type]
return fn(o[type], sty[type])
},
_get_mixin_sty (dSty, oSty) {
var o = {}
Object.keys(oSty).forEach(function (k) {
o[k] = oSty[k]
})
Object.keys(dSty).forEach(function (k) {
if (!o[k]) o[k] = dSty[k]
})
return o
},
_get_menu_options (j, fn) {
var options = j.options
if (!options.menuOpts) return
if (!options.menuOpts.showMenu) return
this.menuOpts = j.options.menuOpts
fn.call(this)
},
_get_injectionDetail () {
var iLs = this.menuOpts.injectionList
var dLs = this.defaultDataMap.injectionList
if (!iLs) return dLs
if (!Array.isArray(iLs)) {
logger.error('[jsmind] injectionList must be a Array')
return
}
if (iLs.length == 0) return dLs
return iLs
},
_get_injectionList (j) {
var list = this._get_injectionDetail()
var _this = this
list.forEach(function (k) {
var o = null
var text = ''
var callback = _noop
var defaultFn = _noop
if (typeof k === 'object') {
o = _this.defaultDataMap.funcMap[k.target]
text = k.text
k.callback && (callback = k.callback)
} else {
o = _this.defaultDataMap.funcMap[k]
text = o.text
}
if (o.defaultFn) defaultFn = o.defaultFn
_this.menu.appendChild(_this._create_menu_item(j, text, o.fn, o.isDepNode, callback, defaultFn))
})
},
_get_mid_opts () {
var b = this.menuOpts.switchMidStage
if (!b) return false
if (typeof b !== 'boolean') {
logger.error('[jsmind] switchMidStage must be Boolean')
return false
}
return b
},
_switch_view_db_event (b, jm) {
Jm.prototype.dblclick_handle = _noop
Jm.shortcut_provider.prototype.handler = _noop
Jm.view_provider.prototype.edit_node_end = _noop
},
_mid_stage_next (fn) {
return function () {
fn(arguments)
}
},
_reset_mind_event_edit () {},
_menu_default_mind_methods: {
_menu_begin_edit: function () {
var f = this.get_editable()
if (!f) {
logger.error('fail, this mind map is not editable.')
};
return f
},
_menu_edit_node_begin (scope, node) {
if (!node.topic) {
logger.warn("don't edit image nodes")
return
}
if (scope.editing_node != null) {
this._menu_default_mind_methods._menu_edit_node_end.call(scope)
}
scope.editing_node = node
var view_data = node._data.view
var element = view_data.element
var topic = node.topic
var ncs = getComputedStyle(element)
scope.e_editor.value = topic
scope.e_editor.style.width = (element.clientWidth - parseInt(ncs.getPropertyValue('padding-left')) - parseInt(ncs.getPropertyValue('padding-right'))) + 'px'
element.innerHTML = ''
element.appendChild(scope.e_editor)
element.style.zIndex = 5
scope.e_editor.focus()
scope.e_editor.select()
},
_menu_edit_node_end: function () {
if (this.editing_node != null) {
var node = this.editing_node
this.editing_node = null
var view_data = node._data.view
var element = view_data.element
var topic = this.e_editor.value
element.style.zIndex = 'auto'
element.removeChild(this.e_editor)
$w.menu._update_node_info = {id: node.id, topic: topic}
if (Jm.util.text.is_empty(topic) || node.topic === topic) {
if (this.opts.support_html) {
$h(element, node.topic)
} else {
$t(element, node.topic)
}
}
}
},
_menu_update_edit_node: function () {
var info = $w.menu._update_node_info
$w.menu.jm.update_node(info.id, info.topic)
}
}
}
var plugin = new Jm.plugin('menu', function (_jm) {
$w.menu = new Jm.menu(_jm)
menu.jm = _jm
if (menu.menuOpts) _jm.menu = menu
})
Jm.register_plugin(plugin)
function preventMindEventDefault () {
Jm.menu.prototype._switch_view_db_event()
}
Jm.preventMindEventDefault = preventMindEventDefault
})(window, 'jsMind')
备注:(如果要更改 思维导图 如hover节点样式 以及选中样式 或者其他配置中不可配置的样式时, 可以选中引入 jsmind.css 文件,然后在css文件中找到对应主题进行更改, 或者跟我这个一样直接在当前页面中 使用 deep属性直接更改他们的样式,会把你当前设置主题的样式给覆盖掉)