leo-mind-map
Version:
一个简单的web在线思维导图
449 lines (422 loc) • 12.8 kB
JavaScript
import { CONSTANTS } from '../../constants/constant'
// 视图操作类
class View {
// 构造函数
constructor(opt = {}) {
this.opt = opt
this.mindMap = this.opt.mindMap
this.scale = 1
this.sx = 0
this.sy = 0
this.x = 0
this.y = 0
this.firstDrag = true
this.setTransformData(this.mindMap.opt.viewData)
this.bind()
}
// 绑定
bind() {
// 快捷键
this.mindMap.keyCommand.addShortcut('Control+=', () => {
this.enlarge()
})
this.mindMap.keyCommand.addShortcut('Control+-', () => {
this.narrow()
})
this.mindMap.keyCommand.addShortcut('Control+i', () => {
this.fit()
})
// 拖动视图
this.mindMap.event.on('mousedown', e => {
const { isDisableDrag, mousedownEventPreventDefault } = this.mindMap.opt
if (isDisableDrag) return
if (mousedownEventPreventDefault) {
e.preventDefault()
}
this.sx = this.x
this.sy = this.y
})
this.mindMap.event.on('drag', (e, event) => {
// 按住ctrl键拖动为多选
// 禁用拖拽
if (e.ctrlKey || e.metaKey || this.mindMap.opt.isDisableDrag) {
return
}
if (this.firstDrag) {
this.firstDrag = false
// 清除激活节点
if (this.mindMap.renderer.activeNodeList.length > 0) {
this.mindMap.execCommand('CLEAR_ACTIVE_NODE')
}
}
this.x = this.sx + event.mousemoveOffset.x
this.y = this.sy + event.mousemoveOffset.y
this.transform()
})
this.mindMap.event.on('mouseup', () => {
this.firstDrag = true
})
// 放大缩小视图
this.mindMap.event.on('mousewheel', (e, dirs, event, isTouchPad) => {
const {
customHandleMousewheel,
mousewheelAction,
mouseScaleCenterUseMousePosition,
mousewheelMoveStep,
mousewheelZoomActionReverse,
disableMouseWheelZoom,
translateRatio
} = this.mindMap.opt
// 是否自定义鼠标滚轮事件
if (
customHandleMousewheel &&
typeof customHandleMousewheel === 'function'
) {
return customHandleMousewheel(e)
}
// 1.鼠标滚轮事件控制缩放
if (
mousewheelAction === CONSTANTS.MOUSE_WHEEL_ACTION.ZOOM ||
e.ctrlKey ||
e.metaKey
) {
if (disableMouseWheelZoom) return
const { x: clientX, y: clientY } = this.mindMap.toPos(
e.clientX,
e.clientY
)
const cx = mouseScaleCenterUseMousePosition ? clientX : undefined
const cy = mouseScaleCenterUseMousePosition ? clientY : undefined
// 如果来自触控板,那么过滤掉左右的移动
if (
isTouchPad &&
(dirs.includes(CONSTANTS.DIR.LEFT) ||
dirs.includes(CONSTANTS.DIR.RIGHT))
) {
dirs = dirs.filter(dir => {
return ![CONSTANTS.DIR.LEFT, CONSTANTS.DIR.RIGHT].includes(dir)
})
}
switch (true) {
// 鼠标滚轮,向上和向左,都是缩小
case dirs.includes(CONSTANTS.DIR.UP || CONSTANTS.DIR.LEFT):
mousewheelZoomActionReverse
? this.enlarge(cx, cy, isTouchPad)
: this.narrow(cx, cy, isTouchPad)
break
// 鼠标滚轮,向下和向右,都是放大
case dirs.includes(CONSTANTS.DIR.DOWN || CONSTANTS.DIR.RIGHT):
mousewheelZoomActionReverse
? this.narrow(cx, cy, isTouchPad)
: this.enlarge(cx, cy, isTouchPad)
break
}
} else {
// 2.鼠标滚轮事件控制画布移动
let stepX = 0
let stepY = 0
if (isTouchPad) {
// 如果是触控板,那么直接使用触控板滑动距离
stepX = Math.abs(e.wheelDeltaX)
stepY = Math.abs(e.wheelDeltaY)
} else {
stepX = stepY = mousewheelMoveStep
}
let mx = 0
let my = 0
// 上移
if (dirs.includes(CONSTANTS.DIR.DOWN)) {
my = -stepY
}
// 下移
if (dirs.includes(CONSTANTS.DIR.UP)) {
my = stepY
}
// 右移
if (dirs.includes(CONSTANTS.DIR.LEFT)) {
mx = stepX
}
// 左移
if (dirs.includes(CONSTANTS.DIR.RIGHT)) {
mx = -stepX
}
this.translateXY(mx * translateRatio, my * translateRatio)
}
})
this.mindMap.on('resize', () => {
if (!this.checkNeedMindMapInCanvas()) return
this.transform()
})
}
// 获取当前变换状态数据
getTransformData() {
return {
transform: this.mindMap.draw.transform(),
state: {
scale: this.scale,
x: this.x,
y: this.y,
sx: this.sx,
sy: this.sy
}
}
}
// 动态设置变换状态数据
setTransformData(viewData) {
if (viewData) {
Object.keys(viewData.state).forEach(prop => {
this[prop] = viewData.state[prop]
})
this.mindMap.draw.transform({
...viewData.transform
})
this.mindMap.emit('view_data_change', this.getTransformData())
this.emitEvent('scale')
this.emitEvent('translate')
}
}
// 平移x,y方向
translateXY(x, y) {
if (x === 0 && y === 0) return
this.x += x
this.y += y
this.transform()
this.emitEvent('translate')
}
// 平移x方向
translateX(step) {
if (step === 0) return
this.x += step
this.transform()
this.emitEvent('translate')
}
// 平移x方式到
translateXTo(x) {
this.x = x
this.transform()
this.emitEvent('translate')
}
// 平移y方向
translateY(step) {
if (step === 0) return
this.y += step
this.transform()
this.emitEvent('translate')
}
// 平移y方向到
translateYTo(y) {
this.y = y
this.transform()
this.emitEvent('translate')
}
// 应用变换
transform() {
try {
this.limitMindMapInCanvas()
} catch (error) {}
this.mindMap.draw.transform({
origin: [0, 0],
scale: this.scale,
translate: [this.x, this.y]
})
this.mindMap.emit('view_data_change', this.getTransformData())
}
// 恢复
reset() {
const scaleChange = this.scale !== 1
const translateChange = this.x !== 0 || this.y !== 0
this.scale = 1
this.x = 0
this.y = 0
this.transform()
if (scaleChange) {
this.emitEvent('scale')
}
if (translateChange) {
this.emitEvent('translate')
}
}
// 缩小
narrow(cx, cy, isTouchPad) {
let { scaleRatio, minZoomRatio } = this.mindMap.opt
scaleRatio = scaleRatio / (isTouchPad ? 5 : 1)
const scale = Math.max(this.scale - scaleRatio, minZoomRatio / 100)
this.scaleInCenter(scale, cx, cy)
this.transform()
this.emitEvent('scale')
}
// 放大
enlarge(cx, cy, isTouchPad) {
let { scaleRatio, maxZoomRatio } = this.mindMap.opt
scaleRatio = scaleRatio / (isTouchPad ? 5 : 1)
let scale = 0
if (maxZoomRatio === -1) {
scale = this.scale + scaleRatio
} else {
scale = Math.min(this.scale + scaleRatio, maxZoomRatio / 100)
}
this.scaleInCenter(scale, cx, cy)
this.transform()
this.emitEvent('scale')
}
// 基于指定中心进行缩放,cx,cy 可不指定,此时会使用画布中心点
scaleInCenter(scale, cx, cy) {
if (cx === undefined || cy === undefined) {
cx = this.mindMap.width / 2
cy = this.mindMap.height / 2
}
const prevScale = this.scale
const ratio = 1 - scale / prevScale
const dx = (cx - this.x) * ratio
const dy = (cy - this.y) * ratio
this.x += dx
this.y += dy
this.scale = scale
}
// 设置缩放
setScale(scale, cx, cy) {
if (cx !== undefined && cy !== undefined) {
this.scaleInCenter(scale, cx, cy)
} else {
this.scale = scale
}
this.transform()
this.emitEvent('scale')
}
// 适应画布大小
fit(getRbox = () => {}, enlarge = false, fitPadding) {
fitPadding =
fitPadding === undefined ? this.mindMap.opt.fitPadding : fitPadding
const draw = this.mindMap.draw
const origTransform = draw.transform()
const rect = getRbox() || draw.rbox()
const drawWidth = rect.width / origTransform.scaleX
const drawHeight = rect.height / origTransform.scaleY
const drawRatio = drawWidth / drawHeight
let { width: elWidth, height: elHeight } = this.mindMap.elRect
elWidth = elWidth - fitPadding * 2
elHeight = elHeight - fitPadding * 2
const elRatio = elWidth / elHeight
let newScale = 0
let flag = ''
if (drawWidth <= elWidth && drawHeight <= elHeight && !enlarge) {
newScale = 1
flag = 1
} else {
let newWidth = 0
let newHeight = 0
if (drawRatio > elRatio) {
newWidth = elWidth
newHeight = elWidth / drawRatio
flag = 2
} else {
newHeight = elHeight
newWidth = elHeight * drawRatio
flag = 3
}
newScale = newWidth / drawWidth
}
this.setScale(newScale)
const newRect = getRbox() || draw.rbox()
// 需要考虑画布容器距浏览器窗口左上角的距离
newRect.x -= this.mindMap.elRect.left
newRect.y -= this.mindMap.elRect.top
let newX = 0
let newY = 0
if (flag === 1) {
newX = -newRect.x + fitPadding + (elWidth - newRect.width) / 2
newY = -newRect.y + fitPadding + (elHeight - newRect.height) / 2
} else if (flag === 2) {
newX = -newRect.x + fitPadding
newY = -newRect.y + fitPadding + (elHeight - newRect.height) / 2
} else if (flag === 3) {
newX = -newRect.x + fitPadding + (elWidth - newRect.width) / 2
newY = -newRect.y + fitPadding
}
this.translateXY(newX, newY)
}
// 判断是否需要将思维导图限制在画布内
checkNeedMindMapInCanvas() {
// 如果当前在演示模式,那么不需要限制
if (this.mindMap.demonstrate && this.mindMap.demonstrate.isInDemonstrate) {
return false
}
const { isLimitMindMapInCanvasWhenHasScrollbar, isLimitMindMapInCanvas } =
this.mindMap.opt
// 如果注册了滚动条插件,那么使用isLimitMindMapInCanvasWhenHasScrollbar配置
if (this.mindMap.scrollbar) {
return isLimitMindMapInCanvasWhenHasScrollbar
} else {
// 否则使用isLimitMindMapInCanvas配置
return isLimitMindMapInCanvas
}
}
// 将思维导图限制在画布内
limitMindMapInCanvas() {
if (!this.checkNeedMindMapInCanvas()) return
let { scale, left, top, right, bottom } = this.getPositionLimit()
// 画布宽高改变了,但是思维导图元素变换的中心点依旧是原有位置,所以需要加上中心点变化量
const centerXChange =
((this.mindMap.width - this.mindMap.initWidth) / 2) * scale
const centerYChange =
((this.mindMap.height - this.mindMap.initHeight) / 2) * scale
// 如果缩放值改变了
const scaleRatio = this.scale / scale
left *= scaleRatio
right *= scaleRatio
top *= scaleRatio
bottom *= scaleRatio
// 加上画布中心点距离
const centerX = this.mindMap.width / 2
const centerY = this.mindMap.height / 2
const scaleOffset = this.scale - 1
left -= scaleOffset * centerX - centerXChange
right -= scaleOffset * centerX - centerXChange
top -= scaleOffset * centerY - centerYChange
bottom -= scaleOffset * centerY - centerYChange
// 判断是否超出边界
if (this.x > left) {
this.x = left
}
if (this.x < right) {
this.x = right
}
if (this.y > top) {
this.y = top
}
if (this.y < bottom) {
this.y = bottom
}
}
// 计算图形四个方向的位置边界值
getPositionLimit() {
const { scaleX, scaleY } = this.mindMap.draw.transform()
const drawRect = this.mindMap.draw.rbox()
const rootRect = this.mindMap.renderer.root.group.rbox()
const rootCenterOffset = this.mindMap.renderer.layout.getRootCenterOffset(
rootRect.width,
rootRect.height
)
const left = rootRect.x - drawRect.x - rootCenterOffset.x * scaleX
const right = rootRect.x - drawRect.x2 - rootCenterOffset.x * scaleX
const top = rootRect.y - drawRect.y - rootCenterOffset.y * scaleY
const bottom = rootRect.y - drawRect.y2 - rootCenterOffset.y * scaleY
return {
scale: scaleX,
left,
right,
top,
bottom
}
}
// 派发事件
emitEvent(type) {
switch (type) {
case 'scale':
this.mindMap.emit('scale', this.scale)
case 'translate':
this.mindMap.emit('translate', this.x, this.y)
}
}
}
export default View