| /* | |
| * Licensed to the Apache Software Foundation (ASF) under one | |
| * or more contributor license agreements. See the NOTICE file | |
| * distributed with this work for additional information | |
| * regarding copyright ownership. The ASF licenses this file | |
| * to you under the Apache License, Version 2.0 (the | |
| * "License"); you may not use this file except in compliance | |
| * with the License. You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, | |
| * software distributed under the License is distributed on an | |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| * KIND, either express or implied. See the License for the | |
| * specific language governing permissions and limitations | |
| * under the License. | |
| */ | |
| import Eventful from 'zrender/src/core/Eventful'; | |
| import * as eventTool from 'zrender/src/core/event'; | |
| import * as interactionMutex from './interactionMutex'; | |
| import { ZRenderType } from 'zrender/src/zrender'; | |
| import { ZRElementEvent, RoamOptionMixin } from '../../util/types'; | |
| import { Bind3, isString, bind, defaults, clone } from 'zrender/src/core/util'; | |
| import Group from 'zrender/src/graphic/Group'; | |
| // Can be null/undefined or true/false | |
| // or 'pan/move' or 'zoom'/'scale' | |
| export type RoamType = RoamOptionMixin['roam']; | |
| interface RoamOption { | |
| zoomOnMouseWheel?: boolean | 'ctrl' | 'shift' | 'alt' | |
| moveOnMouseMove?: boolean | 'ctrl' | 'shift' | 'alt' | |
| moveOnMouseWheel?: boolean | 'ctrl' | 'shift' | 'alt' | |
| /** | |
| * If fixed the page when pan | |
| */ | |
| preventDefaultMouseMove?: boolean | |
| } | |
| type RoamEventType = keyof RoamEventParams; | |
| type RoamBehavior = 'zoomOnMouseWheel' | 'moveOnMouseMove' | 'moveOnMouseWheel'; | |
| export interface RoamEventParams { | |
| 'zoom': { | |
| scale: number | |
| originX: number | |
| originY: number | |
| isAvailableBehavior: Bind3<typeof isAvailableBehavior, null, RoamBehavior, ZRElementEvent> | |
| } | |
| 'scrollMove': { | |
| scrollDelta: number | |
| originX: number | |
| originY: number | |
| isAvailableBehavior: Bind3<typeof isAvailableBehavior, null, RoamBehavior, ZRElementEvent> | |
| } | |
| 'pan': { | |
| dx: number | |
| dy: number | |
| oldX: number | |
| oldY: number | |
| newX: number | |
| newY: number | |
| isAvailableBehavior: Bind3<typeof isAvailableBehavior, null, RoamBehavior, ZRElementEvent> | |
| } | |
| }; | |
| export interface RoamControllerHost { | |
| target: Group | |
| zoom: number | |
| zoomLimit: { | |
| min?: number | |
| max?: number | |
| } | |
| } | |
| class RoamController extends Eventful<{ | |
| [key in keyof RoamEventParams]: (params: RoamEventParams[key]) => void | undefined | |
| }> { | |
| pointerChecker: (e: ZRElementEvent, x: number, y: number) => boolean; | |
| private _zr: ZRenderType; | |
| private _opt: Required<RoamOption>; | |
| private _dragging: boolean; | |
| private _pinching: boolean; | |
| private _x: number; | |
| private _y: number; | |
| readonly enable: (this: this, controlType?: RoamType, opt?: RoamOption) => void; | |
| readonly disable: () => void; | |
| constructor(zr: ZRenderType) { | |
| super(); | |
| this._zr = zr; | |
| // Avoid two roamController bind the same handler | |
| const mousedownHandler = bind(this._mousedownHandler, this); | |
| const mousemoveHandler = bind(this._mousemoveHandler, this); | |
| const mouseupHandler = bind(this._mouseupHandler, this); | |
| const mousewheelHandler = bind(this._mousewheelHandler, this); | |
| const pinchHandler = bind(this._pinchHandler, this); | |
| /** | |
| * Notice: only enable needed types. For example, if 'zoom' | |
| * is not needed, 'zoom' should not be enabled, otherwise | |
| * default mousewheel behaviour (scroll page) will be disabled. | |
| */ | |
| this.enable = function (controlType, opt) { | |
| // Disable previous first | |
| this.disable(); | |
| this._opt = defaults(clone(opt) || {}, { | |
| zoomOnMouseWheel: true, | |
| moveOnMouseMove: true, | |
| // By default, wheel do not trigger move. | |
| moveOnMouseWheel: false, | |
| preventDefaultMouseMove: true | |
| }); | |
| if (controlType == null) { | |
| controlType = true; | |
| } | |
| if (controlType === true || (controlType === 'move' || controlType === 'pan')) { | |
| zr.on('mousedown', mousedownHandler); | |
| zr.on('mousemove', mousemoveHandler); | |
| zr.on('mouseup', mouseupHandler); | |
| } | |
| if (controlType === true || (controlType === 'scale' || controlType === 'zoom')) { | |
| zr.on('mousewheel', mousewheelHandler); | |
| zr.on('pinch', pinchHandler); | |
| } | |
| }; | |
| this.disable = function () { | |
| zr.off('mousedown', mousedownHandler); | |
| zr.off('mousemove', mousemoveHandler); | |
| zr.off('mouseup', mouseupHandler); | |
| zr.off('mousewheel', mousewheelHandler); | |
| zr.off('pinch', pinchHandler); | |
| }; | |
| } | |
| isDragging() { | |
| return this._dragging; | |
| } | |
| isPinching() { | |
| return this._pinching; | |
| } | |
| setPointerChecker(pointerChecker: RoamController['pointerChecker']) { | |
| this.pointerChecker = pointerChecker; | |
| } | |
| dispose() { | |
| this.disable(); | |
| } | |
| private _mousedownHandler(e: ZRElementEvent) { | |
| if (eventTool.isMiddleOrRightButtonOnMouseUpDown(e)) { | |
| return; | |
| } | |
| let el = e.target; | |
| while (el) { | |
| if (el.draggable) { | |
| return; | |
| } | |
| // check if host is draggable | |
| el = el.__hostTarget || el.parent; | |
| } | |
| const x = e.offsetX; | |
| const y = e.offsetY; | |
| // Only check on mosedown, but not mousemove. | |
| // Mouse can be out of target when mouse moving. | |
| if (this.pointerChecker && this.pointerChecker(e, x, y)) { | |
| this._x = x; | |
| this._y = y; | |
| this._dragging = true; | |
| } | |
| } | |
| private _mousemoveHandler(e: ZRElementEvent) { | |
| if (!this._dragging | |
| || !isAvailableBehavior('moveOnMouseMove', e, this._opt) | |
| || e.gestureEvent === 'pinch' | |
| || interactionMutex.isTaken(this._zr, 'globalPan') | |
| ) { | |
| return; | |
| } | |
| const x = e.offsetX; | |
| const y = e.offsetY; | |
| const oldX = this._x; | |
| const oldY = this._y; | |
| const dx = x - oldX; | |
| const dy = y - oldY; | |
| this._x = x; | |
| this._y = y; | |
| this._opt.preventDefaultMouseMove && eventTool.stop(e.event); | |
| trigger(this, 'pan', 'moveOnMouseMove', e, { | |
| dx: dx, dy: dy, oldX: oldX, oldY: oldY, newX: x, newY: y, isAvailableBehavior: null | |
| }); | |
| } | |
| private _mouseupHandler(e: ZRElementEvent) { | |
| if (!eventTool.isMiddleOrRightButtonOnMouseUpDown(e)) { | |
| this._dragging = false; | |
| } | |
| } | |
| private _mousewheelHandler(e: ZRElementEvent) { | |
| const shouldZoom = isAvailableBehavior('zoomOnMouseWheel', e, this._opt); | |
| const shouldMove = isAvailableBehavior('moveOnMouseWheel', e, this._opt); | |
| const wheelDelta = e.wheelDelta; | |
| const absWheelDeltaDelta = Math.abs(wheelDelta); | |
| const originX = e.offsetX; | |
| const originY = e.offsetY; | |
| // wheelDelta maybe -0 in chrome mac. | |
| if (wheelDelta === 0 || (!shouldZoom && !shouldMove)) { | |
| return; | |
| } | |
| // If both `shouldZoom` and `shouldMove` is true, trigger | |
| // their event both, and the final behavior is determined | |
| // by event listener themselves. | |
| if (shouldZoom) { | |
| // Convenience: | |
| // Mac and VM Windows on Mac: scroll up: zoom out. | |
| // Windows: scroll up: zoom in. | |
| // FIXME: Should do more test in different environment. | |
| // wheelDelta is too complicated in difference nvironment | |
| // (https://developer.mozilla.org/en-US/docs/Web/Events/mousewheel), | |
| // although it has been normallized by zrender. | |
| // wheelDelta of mouse wheel is bigger than touch pad. | |
| const factor = absWheelDeltaDelta > 3 ? 1.4 : absWheelDeltaDelta > 1 ? 1.2 : 1.1; | |
| const scale = wheelDelta > 0 ? factor : 1 / factor; | |
| checkPointerAndTrigger(this, 'zoom', 'zoomOnMouseWheel', e, { | |
| scale: scale, originX: originX, originY: originY, isAvailableBehavior: null | |
| }); | |
| } | |
| if (shouldMove) { | |
| // FIXME: Should do more test in different environment. | |
| const absDelta = Math.abs(wheelDelta); | |
| // wheelDelta of mouse wheel is bigger than touch pad. | |
| const scrollDelta = (wheelDelta > 0 ? 1 : -1) * (absDelta > 3 ? 0.4 : absDelta > 1 ? 0.15 : 0.05); | |
| checkPointerAndTrigger(this, 'scrollMove', 'moveOnMouseWheel', e, { | |
| scrollDelta: scrollDelta, originX: originX, originY: originY, isAvailableBehavior: null | |
| }); | |
| } | |
| } | |
| private _pinchHandler(e: ZRElementEvent) { | |
| if (interactionMutex.isTaken(this._zr, 'globalPan')) { | |
| return; | |
| } | |
| const scale = e.pinchScale > 1 ? 1.1 : 1 / 1.1; | |
| checkPointerAndTrigger(this, 'zoom', null, e, { | |
| scale: scale, originX: e.pinchX, originY: e.pinchY, isAvailableBehavior: null | |
| }); | |
| } | |
| } | |
| function checkPointerAndTrigger<T extends 'scrollMove' | 'zoom'>( | |
| controller: RoamController, | |
| eventName: T, | |
| behaviorToCheck: RoamBehavior, | |
| e: ZRElementEvent, | |
| contollerEvent: RoamEventParams[T] | |
| ) { | |
| if (controller.pointerChecker | |
| && controller.pointerChecker(e, contollerEvent.originX, contollerEvent.originY) | |
| ) { | |
| // When mouse is out of roamController rect, | |
| // default befavoius should not be be disabled, otherwise | |
| // page sliding is disabled, contrary to expectation. | |
| eventTool.stop(e.event); | |
| trigger(controller, eventName, behaviorToCheck, e, contollerEvent); | |
| } | |
| } | |
| function trigger<T extends RoamEventType>( | |
| controller: RoamController, | |
| eventName: T, | |
| behaviorToCheck: RoamBehavior, | |
| e: ZRElementEvent, | |
| contollerEvent: RoamEventParams[T] | |
| ) { | |
| // Also provide behavior checker for event listener, for some case that | |
| // multiple components share one listener. | |
| contollerEvent.isAvailableBehavior = bind(isAvailableBehavior, null, behaviorToCheck, e); | |
| // TODO should not have type issue. | |
| (controller as any).trigger(eventName, contollerEvent); | |
| } | |
| // settings: { | |
| // zoomOnMouseWheel | |
| // moveOnMouseMove | |
| // moveOnMouseWheel | |
| // } | |
| // The value can be: true / false / 'shift' / 'ctrl' / 'alt'. | |
| function isAvailableBehavior( | |
| behaviorToCheck: RoamBehavior, | |
| e: ZRElementEvent, | |
| settings: Pick<RoamOption, RoamBehavior> | |
| ) { | |
| const setting = settings[behaviorToCheck]; | |
| return !behaviorToCheck || ( | |
| setting && (!isString(setting) || e.event[setting + 'Key' as 'shiftKey' | 'ctrlKey' | 'altKey']) | |
| ); | |
| } | |
| export default RoamController; |