mind-map/simple-mind-map/src/core/view/View.js

446 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 => {
if (this.mindMap.opt.isDisableDrag) return
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
} = 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, my)
this.translateXYwithRatio(mx, my)
}
})
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,y方向
translateXYwithRatio(x, y) {
if (x === 0 && y === 0) return
this.x += x * this.mindMap.opt.translateRatio
this.y += y * this.mindMap.opt.translateRatio
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) {
const scaleRatio = this.mindMap.opt.scaleRatio / (isTouchPad ? 5 : 1)
// const scale = Math.max(this.scale - scaleRatio, 0.1)
const scale = Math.max(this.scale - scaleRatio, this.mindMap.opt.minZoomRatio / 100)
this.scaleInCenter(scale, cx, cy)
this.transform()
this.emitEvent('scale')
}
// 放大
enlarge(cx, cy, isTouchPad) {
const scaleRatio = this.mindMap.opt.scaleRatio / (isTouchPad ? 5 : 1)
// const scale = this.scale + scaleRatio
const scale = Math.min(this.scale + scaleRatio, this.mindMap.opt.maxZoomRatio / 100)
this.scaleInCenter(scale, cx, cy)
this.transform()
this.emitEvent('scale')
}
// 基于指定中心进行缩放cxcy 可不指定,此时会使用画布中心点
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() {
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