/* * 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'; // 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 = 'zoom' | 'scrollMove' | 'pan' type RoamBehavior = 'zoomOnMouseWheel' | 'moveOnMouseMove' | 'moveOnMouseWheel' export type RoamEventParams = { 'zoom': { scale: number originX: number originY: number isAvailableBehavior: Bind3 } 'scrollMove': { scrollDelta: number originX: number originY: number isAvailableBehavior: Bind3 } 'pan': { dx: number dy: number oldX: number oldY: number newX: number newY: number isAvailableBehavior: Bind3 } } class RoamController extends Eventful { pointerChecker: (e: ZRElementEvent, x: number, y: number) => boolean private _zr: ZRenderType private _opt: Required 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) || (e.target && e.target.draggable) ) { return; } var x = e.offsetX; var 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; } var x = e.offsetX; var y = e.offsetY; var oldX = this._x; var oldY = this._y; var dx = x - oldX; var 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) { var shouldZoom = isAvailableBehavior('zoomOnMouseWheel', e, this._opt); var shouldMove = isAvailableBehavior('moveOnMouseWheel', e, this._opt); var wheelDelta = e.wheelDelta; var absWheelDeltaDelta = Math.abs(wheelDelta); var originX = e.offsetX; var 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. var factor = absWheelDeltaDelta > 3 ? 1.4 : absWheelDeltaDelta > 1 ? 1.2 : 1.1; var 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. var absDelta = Math.abs(wheelDelta); // wheelDelta of mouse wheel is bigger than touch pad. var 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; } var 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( 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( 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); controller.trigger(eventName, contollerEvent); } // settings: { // zoomOnMouseWheel // moveOnMouseMove // moveOnMouseWheel // } // The value can be: true / false / 'shift' / 'ctrl' / 'alt'. function isAvailableBehavior( behaviorToCheck: RoamBehavior, e: ZRElementEvent, settings: Pick ) { var setting = settings[behaviorToCheck]; return !behaviorToCheck || ( setting && (!isString(setting) || e.event[setting + 'Key' as 'shiftKey' | 'ctrlKey' | 'altKey']) ); } export default RoamController;