UNPKG

@amcharts/amcharts4

Version:
1,271 lines (1,270 loc) 107 kB
/** * Interaction manages all aspects of user interaction - mouse move, * click, hover, drag events, touch gestures. * * [[InteractionObject]] elements that want to use certain events, must attach event * listeners to Interaction instance. * * Interaction itself will not modify [[InteractionObject]] elements, it will be up to * those elements to handle interaction information received via event triggers. */ import { __extends } from "tslib"; /** * ============================================================================ * IMPORTS * ============================================================================ * @hidden */ import { BaseObjectEvents } from "../Base"; import { List } from "../utils/List"; import { Animation } from "../utils/Animation"; import { MultiDisposer } from "../utils/Disposer"; import { InteractionObject } from "./InteractionObject"; import { InteractionKeyboardObject } from "./InteractionKeyboardObject"; import { Dictionary } from "../utils/Dictionary"; import { Inertia } from "./Inertia"; import { addEventListener } from "../utils/DOM"; import { keyboard } from "../utils/Keyboard"; import { system } from "./../System"; import { options } from "./../Options"; import * as $ease from "../utils/Ease"; import * as $math from "../utils/Math"; import * as $array from "../utils/Array"; import * as $dom from "../utils/DOM"; import * as $iter from "../utils/Iterator"; import * as $type from "../utils/Type"; import * as $time from "../utils/Time"; /** * ============================================================================ * MAIN CLASS * ============================================================================ * @hidden */ /** * Interaction manages all aspects of user interaction - mouse move, * click, hover, drag events, touch gestures. * * [[InteractionObject]] elements that want to use certain events, must attach event * listeners to Interaction instance. * * Interaction itself will not modify [[InteractionObject]] elements, it will be up to * those elements to handle interaction information received via event triggers. * * @see {@link IInteractionEvents} for a list of available events */ var Interaction = /** @class */ (function (_super) { __extends(Interaction, _super); /** * Constructor. Sets up universal document-wide move events to handle stuff * outside particular chart container. */ function Interaction() { var _this = // Call super _super.call(this) || this; /** * An indicator of global events were already initialized. */ _this._globalEventsAdded = false; /** * Holds which mouse event listeners to use. */ _this._pointerEvents = { "pointerdown": "mousedown", "pointerup": "mouseup", "pointermove": "mousemove", "pointercancel": "mouseup", "pointerover": "mouseover", "pointerout": "mouseout", "wheel": "wheel" }; /** * Indicates if Interaction should use only "pointer" type events, like * "pointermove", available in all modern browsers, ignoring "legacy" * events, like "touchmove". */ _this._usePointerEventsOnly = false; /** * Use only touch events (for touch only devices such as tablets and phones) */ _this._useTouchEventsOnly = false; /** * Add special hover events. Normally, touch device tap will also simulate * hover event. On some devices (ahem iOS) we want to prevent that so that * over/out events are not duplicated. */ _this._addHoverEvents = true; /** * Indicates if passive mode options is supported by this browser. */ _this._passiveSupported = false; /** * Holds list of delayed events */ _this._delayedEvents = { out: [] }; /** * List of objects that current have a pointer hovered over them. */ _this.overObjects = new List(); /** * List of objects that currently has a pressed pointer. */ _this.downObjects = new List(); /** * List of objects that need mouse position to be reported to them. */ _this.trackedObjects = new List(); /** * List of objects that are currently being dragged. */ _this.transformedObjects = new List(); /** * Holds all known pointers. */ _this.pointers = new Dictionary(); /** * Inertia options that need to be applied to after element drag, if it's * `inert = true`. * * This is just a default, which can and probably will be overridden by * actual elements. */ _this.inertiaOptions = new Dictionary(); /** * Default options for click events. These can be overridden in * [[InteractionObject]]. */ _this.hitOptions = { "doubleHitTime": 300, //"delayFirstHit": false, "hitTolerance": 10, "noFocus": true }; /** * Default options for hover events. These can be overridden in * [[InteractionObject]]. */ _this.hoverOptions = { "touchOutBehavior": "leave", "touchOutDelay": 1000 }; /** * Default options for detecting a swipe gesture. These can be overridden in * [[InteractionObject]]. */ _this.swipeOptions = { "time": 500, "verticalThreshold": 75, "horizontalThreshold": 30 }; /** * Default options for keyboard operations. These can be overridden in * [[InteractionObject]]. */ _this.keyboardOptions = { "speed": 0.1, "accelleration": 1.2, "accellerationDelay": 2000 }; /** * Default options for keyboard operations. These can be overridden in * [[InteractionObject]]. * * @since 4.5.14 */ _this.mouseOptions = { "sensitivity": 1 }; // Set class name _this.className = "Interaction"; // Create InteractionObject for <body> _this.body = _this.getInteraction(document.body); _this._disposers.push(_this.body); // Detect browser capabilities and determine what event listeners to use if (window.hasOwnProperty("PointerEvent")) { // IE10+/Edge without touch controls enabled _this._pointerEvents.pointerdown = "pointerdown"; _this._pointerEvents.pointerup = "pointerup"; _this._pointerEvents.pointermove = "pointermove"; _this._pointerEvents.pointercancel = "pointercancel"; _this._pointerEvents.pointerover = "pointerover"; _this._pointerEvents.pointerout = "pointerout"; //this._usePointerEventsOnly = true; } else if (window.hasOwnProperty("MSPointerEvent")) { // IE9 _this._pointerEvents.pointerdown = "MSPointerDown"; _this._pointerEvents.pointerup = "MSPointerUp"; _this._pointerEvents.pointermove = "MSPointerMove"; _this._pointerEvents.pointercancel = "MSPointerUp"; _this._pointerEvents.pointerover = "MSPointerOver"; _this._pointerEvents.pointerout = "MSPointerOut"; //this._usePointerEventsOnly = true; } else if ((typeof matchMedia !== "undefined") && matchMedia('(pointer:fine)').matches) { // This is only for Safari as it does not support PointerEvent // Do nothing and let it use regular `mouse*` events // Hi Apple ;) // Additionally disable hover events for iOS devices if ('ontouchstart' in window) { _this._addHoverEvents = false; _this._useTouchEventsOnly = true; } } else if (window.navigator.userAgent.match(/MSIE /)) { // Oh looky, an MSIE that does not support PointerEvent. Hi granpa IE9! _this._usePointerEventsOnly = true; } else if (_this.fullFF()) { // Old FF, let's use regular events. // (Newer FFs would be detected by the PointerEvent availability check) _this._usePointerEventsOnly = true; } else { // Uses defaults for normal browsers // We also assume that this must be a touch device that does not have // any pointer events _this._useTouchEventsOnly = true; } // Detect if device has a mouse // This is turning out to be not reliable // @todo remove /*if (!window.navigator.msPointerEnabled && (typeof matchMedia !== "undefined") && !matchMedia('(pointer:fine)').matches && !this.fullFF()) { this._useTouchEventsOnly = true; }*/ // Detect proper mouse wheel events if ("onwheel" in document.createElement("div")) { // Modern browsers _this._pointerEvents.wheel = "wheel"; } else if ($type.hasValue(document.onmousewheel)) { // Webkit and IE support at least "mousewheel" _this._pointerEvents.wheel = "mousewheel"; } // Set up default inertia options _this.inertiaOptions.setKey("move", { "time": 100, "duration": 500, "factor": 1, "easing": $ease.polyOut3 }); _this.inertiaOptions.setKey("resize", { "time": 100, "duration": 500, "factor": 1, "easing": $ease.polyOut3 }); // Set the passive mode support _this._passiveSupported = Interaction.passiveSupported; // Apply theme _this.applyTheme(); return _this; } /** * This is a nasty detection for Firefox. The reason why we have is that * Firefox ESR version does not support matchMedia correctly. * * On iOS, Firefox uses different userAgent, so we don't have to detect iOS. * * @return Full Firefox? */ Interaction.prototype.fullFF = function () { return (window.navigator.userAgent.match(/Firefox/)) && !(window.navigator.userAgent.match(/Android/)); }; Interaction.prototype.debug = function () { }; /** * ========================================================================== * Processing * ========================================================================== * @hidden */ /** * Sets up global events. * * We need this so that we can track drag movement beyond chart's container. * * @ignore Exclude from docs */ Interaction.prototype.addGlobalEvents = function () { var _this = this; if (!this._globalEventsAdded) { if (!this._useTouchEventsOnly) { this._disposers.push(addEventListener(document, this._pointerEvents.pointerdown, function (ev) { _this.handleGlobalPointerDown(ev); })); this._disposers.push(addEventListener(document, this._pointerEvents.pointermove, function (ev) { _this.handleGlobalPointerMove(ev); })); this._disposers.push(addEventListener(document, this._pointerEvents.pointerup, function (ev) { _this.handleGlobalPointerUp(ev); })); this._disposers.push(addEventListener(document, this._pointerEvents.pointercancel, function (ev) { _this.handleGlobalPointerUp(ev, true); })); this._disposers.push(addEventListener(document, "mouseenter", function (ev) { if (!$type.hasValue(ev.relatedTarget) && (ev.buttons == 0 || ev.which == 0)) { _this.handleDocumentLeave(ev); } })); } // No need to duplicate events for hubrid systems that support both // pointer events and touch events. Touch events are need only for // some touch-only systems, like Mobile Safari. if (!this._usePointerEventsOnly) { this._disposers.push(addEventListener(document, "touchstart", function (ev) { _this.handleGlobalTouchStart(ev); })); this._disposers.push(addEventListener(document, "touchmove", function (ev) { _this.handleGlobalTouchMove(ev); })); this._disposers.push(addEventListener(document, "touchend", function (ev) { _this.handleGlobalTouchEnd(ev); })); } this._disposers.push(addEventListener(document, "keydown", function (ev) { _this.handleGlobalKeyDown(ev); })); this._disposers.push(addEventListener(document, "keyup", function (ev) { _this.handleGlobalKeyUp(ev); })); this._globalEventsAdded = true; } }; /** * Sets if [[InteractionObject]] is clickable. * * @ignore Exclude from docs * @param io [[InteractionObject]] instance */ Interaction.prototype.processClickable = function (io) { // Add or remove touch events this.processTouchable(io); }; /** * Sets if [[InteractionObject]] will display context menu when right-clicked. * * @ignore Exclude from docs * @param io [[InteractionObject]] instance */ Interaction.prototype.processContextMenu = function (io) { if (io.contextMenuDisabled) { if (!io.eventDisposers.hasKey("contextMenuDisabled")) { io.eventDisposers.setKey("contextMenuDisabled", addEventListener(io.element, "contextmenu", function (e) { e.preventDefault(); })); } } else { if (io.eventDisposers.hasKey("contextMenuDisabled")) { io.eventDisposers.getKey("contextMenuDisabled").dispose(); } } }; /** * Sets if [[InteractionObject]] is hoverable. * * @ignore Exclude from docs * @param io [[InteractionObject]] instance */ Interaction.prototype.processHoverable = function (io) { var _this = this; if (io.hoverable || io.trackable) { // Add global events this.addGlobalEvents(); // Add hover styles this.applyCursorOverStyle(io); // Add local events if (!io.eventDisposers.hasKey("hoverable") && this._addHoverEvents) { io.eventDisposers.setKey("hoverable", new MultiDisposer([ addEventListener(io.element, this._pointerEvents.pointerout, function (e) { return _this.handlePointerOut(io, e); }), addEventListener(io.element, this._pointerEvents.pointerover, function (e) { return _this.handlePointerOver(io, e); }) ])); } if (io.trackable) { //sprite.addEventListener("touchmove", this.handleTouchMove, false, this); } } else { var disposer = io.eventDisposers.getKey("hoverable"); if (disposer != null) { disposer.dispose(); io.eventDisposers.removeKey("hoverable"); } } // Add or remove touch events this.processTouchable(io); }; /** * Sets up [[InteractionObject]] as movable. Movable can be any * transformation, e.g. drag, swipe, resize, track. * * @ignore Exclude from docs * @param io Element */ Interaction.prototype.processMovable = function (io) { // Add unified events if (io.draggable || io.swipeable || io.trackable || io.resizable) { // Prep the element if (!this.isGlobalElement(io) && !io.isTouchProtected) { this.prepElement(io); } // Add hover styles this.applyCursorOverStyle(io); } // Add or remove touch events this.processTouchable(io); }; /** * Checks if [[InteractionObject]] is trackable and sets relative events. * * @ignore Exclude from docs * @param io Element */ Interaction.prototype.processTrackable = function (io) { this.processHoverable(io); this.processMovable(io); if (io.trackable) { this.trackedObjects.moveValue(io); } else { this.trackedObjects.removeValue(io); } }; /** * Checks if [[InteractionObject]] is draggable. * * @ignore Exclude from docs * @param io Element */ Interaction.prototype.processDraggable = function (io) { this.processMovable(io); }; /** * Checks if [[InteractionObject]] is swipeable and sets relative events. * * A swipe event is triggered when a horizontal drag of 75px or more (and * less than 30px vertically) occurs within 700 milliseconds. This can be * overridden in sprites [[swipeOptions]]. * * @ignore Exclude from docs * @param io Element */ Interaction.prototype.processSwipeable = function (io) { this.processMovable(io); }; /** * Checks if [[InteractionObject]] is resizable and attaches required events * to it. * * @ignore Exclude from docs * @param io Element */ Interaction.prototype.processResizable = function (io) { this.processMovable(io); }; /** * Checks if [[InteractionObject]] is supposed to capture mouse wheel events * and prepares it to catch those events. * * @ignore Exclude from docs * @param io Element */ Interaction.prototype.processWheelable = function (io) { var _this = this; if (io.wheelable) { //io.hoverable = true; if (!io.eventDisposers.hasKey("wheelable")) { io.eventDisposers.setKey("wheelable", new MultiDisposer([ addEventListener(io.element, this._pointerEvents.wheel, function (e) { return _this.handleMouseWheel(io, e); }, this._passiveSupported ? { passive: false } : false), io.events.on("out", function (e) { if (io.wheelable) { _this.unlockWheel(); } }), io.events.on("over", function (e) { //console.log("whelab over") if (io.wheelable) { _this.lockWheel(); } }) ])); } } else { var disposer = io.eventDisposers.getKey("wheelable"); if (disposer != null) { disposer.dispose(); io.eventDisposers.removeKey("wheelable"); } } }; /** * Checks if [[InteractionObject]] is focusable. A focusable element is an * element that will be highlighted when users presses TAB key. If the * element is focusable, this function will attach relative focus/blur * events to it. * * @ignore Exclude from docs * @param io Element */ Interaction.prototype.processFocusable = function (io) { var _this = this; if (io.focusable === true && (io.tabindex > -1) && !this._useTouchEventsOnly) { if (!io.eventDisposers.hasKey("focusable")) { io.eventDisposers.setKey("focusable", new MultiDisposer([ addEventListener(io.element, "focus", function (e) { return _this.handleFocus(io, e); }), addEventListener(io.element, "blur", function (e) { return _this.handleBlur(io, e); }), addEventListener(io.element, this._pointerEvents.pointerdown, function (e) { return _this.handleFocusBlur(io, e); }), addEventListener(io.element, "touchstart", function (e) { return _this.handleFocusBlur(io, e); }, this._passiveSupported ? { passive: false } : false) ])); } } else { var disposer = io.eventDisposers.getKey("focusable"); if (disposer != null) { disposer.dispose(); io.eventDisposers.removeKey("focusable"); } } }; /** * Checks if [[InteractionObject]] is "touchable". It means any interaction * whatsoever: mouse click, touch screen tap, swipe, drag, resize, etc. * * @ignore Exclude from docs * @param io Element */ Interaction.prototype.processTouchable = function (io) { var _this = this; // Add unified events if (io.clickable || io.hoverable || io.trackable || io.draggable || io.swipeable || io.resizable) { // Add global events this.addGlobalEvents(); // Add local events if (!io.eventDisposers.hasKey("touchable")) { if (!this._useTouchEventsOnly && !this._usePointerEventsOnly) { io.eventDisposers.setKey("touchable", new MultiDisposer([ addEventListener(io.element, this._pointerEvents.pointerdown, function (e) { return _this.handlePointerDown(io, e); }), addEventListener(io.element, "touchstart", function (e) { return _this.handleTouchDown(io, e); }, this._passiveSupported ? { passive: false } : false) ])); } else if (!this._useTouchEventsOnly) { io.eventDisposers.setKey("touchable", addEventListener(io.element, this._pointerEvents.pointerdown, function (e) { return _this.handlePointerDown(io, e); })); } else if (!this._usePointerEventsOnly) { io.eventDisposers.setKey("touchable", addEventListener(io.element, "touchstart", function (e) { return _this.handleTouchDown(io, e); }, this._passiveSupported ? { passive: false } : false)); } } } else { var disposer = io.eventDisposers.getKey("touchable"); if (disposer != null) { disposer.dispose(); io.eventDisposers.removeKey("touchable"); } } }; /** * ========================================================================== * Non-pointer events * ========================================================================== * @hidden */ /** * Dispatches "focus" event when element gains focus. * * @ignore Exclude from docs * @param io Element * @param ev Original event */ Interaction.prototype.handleFocus = function (io, ev) { if (!io.focusable) { ev.preventDefault(); return; } io.isFocused = true; if (io.events.isEnabled("focus") && !system.isPaused) { var imev = { type: "focus", target: io, event: ev }; io.events.dispatchImmediately("focus", imev); } }; /** * Used by regular click events to prevent focus if "noFocus" is set. * * This should not be called by "focus" handlers. * * @param io Element * @param ev Original event */ Interaction.prototype.handleFocusBlur = function (io, ev) { if (io.focusable !== false && this.getHitOption(io, "noFocus")) { io.events.once("focus", function () { io.events.disableType("blur"); $dom.blur(); if (io.sprite) { io.sprite.handleBlur(); } io.events.enableType("blur"); }); } }; /** * Dispatches "blur" event when element loses focus. * * @ignore Exclude from docs * @param io Element * @param ev Original event */ Interaction.prototype.handleBlur = function (io, ev) { if (!io.focusable) { ev.preventDefault(); return; } io.isFocused = false; if (io.events.isEnabled("blur") && !system.isPaused) { var imev = { type: "blur", target: io, event: ev }; io.events.dispatchImmediately("blur", imev); } }; /** * ========================================================================== * Global keyboard-related even handlers * ========================================================================== * @hidden */ /** * Checks if there is an item that has currently focus and that they key is * one of the directional keys. If both of the conditions are true, it * creates an object to simulate movement of dragable element with keyboard. * * @ignore Exclude from docs * @param ev An original keyboard event */ Interaction.prototype.handleGlobalKeyDown = function (ev) { if (this.focusedObject) { if (keyboard.isKey(ev, "esc")) { // ESC removes focus $dom.blur(); } else if (this.focusedObject.draggable && keyboard.isKey(ev, ["up", "down", "left", "right"])) { // Prevent scrolling of the document ev.preventDefault(); // Get focused object var io = this.focusedObject; // Get particular key var disposerKey = "interactionKeyboardObject"; // If such disposer already exists we know the event is going on so we // just move on if (io.eventDisposers.hasKey(disposerKey)) { return; } // Create a keyboard mover var ko = new InteractionKeyboardObject(io, ev); io.eventDisposers.setKey(disposerKey, ko); switch (keyboard.getEventKey(ev)) { case "up": ko.directionY = -1; break; case "down": ko.directionY = 1; break; case "left": ko.directionX = -1; break; case "right": ko.directionX = 1; break; } } } }; /** * Dispatches related events when the keyboard key is realeasd. * * @ignore Exclude from docs * @param ev An original keyboard event */ Interaction.prototype.handleGlobalKeyUp = function (ev) { var disposerKey = "interactionKeyboardObject"; if (this.focusedObject) { var disposer = this.focusedObject.eventDisposers.getKey(disposerKey); if (disposer != null) { // Prevent scrolling of the document ev.preventDefault(); // Dispose stuff disposer.dispose(); this.focusedObject.eventDisposers.removeKey(disposerKey); } // Does focused object have "hit" event? var sprite = this.focusedObject.sprite; if (keyboard.isKey(ev, "enter") && sprite) { if (sprite.events.isEnabled("hit") || sprite.events.isEnabled("toggled")) { this.focusedObject.dispatchImmediately("hit"); } else if (sprite.showTooltipOn == "hit") { this.focusedObject.dispatchImmediately("up"); } } } }; /** * ========================================================================== * Global pointer-related even handlers * ========================================================================== * @hidden */ /** * Handler for a global "pointermove" event. * * @ignore Exclude from docs * @param ev Event object */ Interaction.prototype.handleGlobalPointerMove = function (ev) { // Get pointer var pointer = this.getPointer(ev); // Update current point position pointer.point = this.getPointerPoint(ev); // Prepare and fire global event if (this.events.isEnabled("track") && !system.isPaused) { var imev = { type: "track", target: this, event: ev, pointer: pointer, touch: pointer.touch }; this.events.dispatchImmediately("track", imev); } // Track this.addBreadCrumb(pointer, pointer.point); // Process further this.handleGlobalMove(pointer, ev); }; /** * Handler for a global "pointerdown" event. * * @ignore Exclude from docs * @param ev Event object */ Interaction.prototype.handleGlobalPointerDown = function (ev) { // Remove delayed hovers this.processDelayed(); // Get pointer var pointer = this.getPointer(ev); // Prepare and fire global event if (this.events.isEnabled("down") && !system.isPaused) { var imev = { type: "down", target: this, event: ev, pointer: pointer, touch: pointer.touch }; this.events.dispatchImmediately("down", imev); } }; /** * Prevents touch action from firing. * * @ignore Exclude from docs * @param ev Event */ Interaction.prototype.preventTouchAction = function (ev) { if (!ev.defaultPrevented) { ev.preventDefault(); } }; /** * Handler for a global "pointerup" event. * * @ignore Exclude from docs * @param ev Event object */ Interaction.prototype.handleGlobalPointerUp = function (ev, cancelled) { if (cancelled === void 0) { cancelled = false; } // Get pointer var pointer = this.getPointer(ev); // Prepare and fire global event if (this.events.isEnabled("up") && !system.isPaused) { var imev = { type: "up", target: this, event: ev, pointer: pointer, touch: pointer.touch }; this.events.dispatchImmediately("up", imev); } // Process further this.handleGlobalUp(pointer, ev, cancelled); }; /** * ========================================================================== * Global touch-related even handlers * ========================================================================== */ /** * Handler for a global "touchmove" event. * * @ignore Exclude from docs * @param ev Event object */ Interaction.prototype.handleGlobalTouchMove = function (ev) { // Process each changed touch point for (var i = 0; i < ev.changedTouches.length; i++) { // Get pointer var pointer = this.getPointer(ev.changedTouches[i]); // Update current point position pointer.point = this.getPointerPoint(ev.changedTouches[i]); // Prepare and fire global event if (this.events.isEnabled("track") && !system.isPaused) { var imev = { type: "track", target: this, event: ev, pointer: pointer, touch: pointer.touch }; this.events.dispatchImmediately("track", imev); } // Track this.addBreadCrumb(pointer, pointer.point); // Process further this.handleGlobalMove(pointer, ev); } }; /** * Handler for a global "touchstart" event. * * @ignore Exclude from docs * @param ev Event object */ Interaction.prototype.handleGlobalTouchStart = function (ev) { // Remove delayed hovers this.processDelayed(); // Process each changed touch point for (var i = 0; i < ev.changedTouches.length; i++) { // Get pointer var pointer = this.getPointer(ev.changedTouches[i]); // Prepare and fire global event if (!this._usePointerEventsOnly && this.events.isEnabled("down") && !system.isPaused) { var imev = { type: "down", target: this, event: ev, pointer: pointer, touch: pointer.touch }; this.events.dispatchImmediately("down", imev); } } }; /** * Handler for a global "touchend" event. * * @ignore Exclude from docs * @param ev Event object */ Interaction.prototype.handleGlobalTouchEnd = function (ev) { // Process each changed touch point for (var i = 0; i < ev.changedTouches.length; i++) { // Get pointer var pointer = this.getPointer(ev.changedTouches[i]); // Prepare and fire global event if (this.events.isEnabled("up") && !system.isPaused) { var imev = { type: "up", target: this, event: ev, pointer: pointer, touch: pointer.touch }; this.events.dispatchImmediately("up", imev); } // Handle element-related events this.handleGlobalUp(pointer, ev); } }; /** * ========================================================================== * Element-specific pointer-related even handlers * ========================================================================== * @hidden */ /** * Handles event when pointer is over [[InteractionObject]] and button is * pressed. * * @ignore Exclude from docs * @param io Element * @param ev Original event */ Interaction.prototype.handlePointerDown = function (io, ev) { // Stop further propagation so we don't get multiple triggers on hybrid // devices (both mouse and touch capabilities) //ev.preventDefault(); //ev.stopPropagation(); //if (ev.defaultPrevented) { //} // Get pointer var pointer = this.getPointer(ev); // Ignore if it's anything but mouse's primary button if (!pointer.touch && ev.which != 1 && ev.which != 3) { return; } // Set mouse button pointer.button = ev.which; // Reset pointer this.resetPointer(pointer, ev); // Process down this.handleDown(io, pointer, ev); }; /** * Handles event when [[InteractionObject]] is hovered by a mouse pointer. * * @ignore Exclude from docs * @param io Element * @param ev Original event */ Interaction.prototype.handlePointerOver = function (io, ev) { // Get pointer var pointer = this.getPointer(ev); // Process down this.handleOver(io, pointer, ev); }; /** * Handles event when [[InteractionObject]] loses hover from a mouse pointer. * * @ignore Exclude from docs * @param io Element * @param ev Original event */ Interaction.prototype.handlePointerOut = function (io, ev) { // Get pointer var pointer = this.getPointer(ev); // Process down this.handleOut(io, pointer, ev); }; /** * Handles event when mouse wheel is crolled over the [[InteractionObject]]. * * @ignore Exclude from docs * @param io Element * @param ev Original event * @todo Investigate more-cross browser stuff https://developer.mozilla.org/en-US/docs/Web/Events/wheel */ Interaction.prototype.handleMouseWheel = function (io, ev) { // Get pointer var pointer = this.getPointer(ev); // Update current point position pointer.point = this.getPointerPoint(ev); // Init delta values var deltaX = 0, deltaY = 0; // Set up modifier // This is needed because FireFox reports wheel deltas in "lines" instead // of pixels so we have to approximate pixel value var mod = 1; if (ev.deltaMode == 1) { mod = 50; } // Adjust configurable sensitivity mod *= this.getMouseOption(io, "sensitivity"); // Calculate deltas if (ev instanceof WheelEvent) { deltaX = Math.round((-1 * ev.wheelDeltaX) || (ev.deltaX * mod)); deltaY = Math.round((-1 * ev.wheelDeltaY) || (ev.deltaY * mod)); } else { throw new Error("Invalid event type"); } // Handle the event this.handleWheel(io, pointer, deltaX, deltaY, ev); }; /** * ========================================================================== * Element-specific touch-related even handlers * ========================================================================== * @hidden */ /** * Handles an event when an [[InteractionObject]] is touched on a touch * device. * * @ignore Exclude from docs * @param io Element * @param ev Original event */ Interaction.prototype.handleTouchDown = function (io, ev) { // Stop further propagation so we don't get multiple triggers on hybrid // devices (both mouse and touch capabilities) //this.maybePreventDefault(io, ev); //return; // Process each changed touch point for (var i = 0; i < ev.changedTouches.length; i++) { // Get pointer var pointer = this.getPointer(ev.changedTouches[i]); this.maybePreventDefault(io, ev, pointer); // Reset pointer this.resetPointer(pointer, ev.changedTouches[i]); // Process down this.handleDown(io, pointer, ev); } }; /** * ========================================================================== * Universal handlers * ========================================================================== * @hidden */ /** * Handles click/tap. Checks for doublehit. * * @ignore Exclude from docs * @param io Interaction object * @param pointer Pointer * @param ev Original event */ Interaction.prototype.handleHit = function (io, pointer, ev) { // Check if this is a double-hit var now = $time.getTime(); if (io.lastHit && (io.lastHit >= (now - this.getHitOption(io, "doubleHitTime")))) { // Yup - it's a double-hit // Cancel the hit //clearTimeout(io.lastHitPointer.hitTimeout); // If it happened too fast it probably means that hybrid device just // generated two events for the same tap if ((now - io.lastHit) < 100) { // Ignore return; } // Clear last hit io.lastHit = undefined; io.lastHitPointer = undefined; // Dispatch event if (io.events.isEnabled("doublehit") && !system.isPaused) { var imev = { type: "doublehit", target: io, point: pointer.point, event: ev, touch: pointer.touch }; io.events.dispatchImmediately("doublehit", imev); } } else { // Log last hit io.lastHit = now; io.lastHitPointer = pointer; if (pointer.button === 3) { // Execute HIT now if (io.events.isEnabled("rightclick") && !system.isPaused) { var imev = { type: "rightclick", target: io, event: ev }; io.events.dispatchImmediately("rightclick", imev); } } else { if (io.events.isEnabled("hit") && !system.isPaused) { var imev = { type: "hit", target: io, event: ev, point: pointer.point, touch: pointer.touch }; io.events.dispatchImmediately("hit", imev); } } } }; /** * Handles pointer hovering over [[InteractionObject]]. * * @ignore Exclude from docs * @param io Interaction object * @param pointer Pointer * @param ev Original event * @param soft Invoked by helper function */ Interaction.prototype.handleOver = function (io, pointer, ev, soft) { if (soft === void 0) { soft = false; } if (!io.hoverable) { return; } var hoversPaused = false; if (this.shouldCancelHovers(pointer) && this.areTransformed() && this.moved(pointer, this.getHitOption(io, "hitTolerance"))) { hoversPaused = true; this.cancelAllHovers(ev); } // Remove any delayed outs this.processDelayed(); // Add pointer io.overPointers.moveValue(pointer); // Check if object is not yet hovered if (!io.isRealHover) { // Set element as hovered if (!hoversPaused) { io.isHover = true; io.isRealHover = true; this.overObjects.moveValue(io); } // Generate body track event. This is needed so that if element loads // under unmoved mouse cursor, we still need all the actions that are // required to happen to kick in. this.handleTrack(this.body, pointer, ev, true); // Event if (io.events.isEnabled("over") && !system.isPaused && !hoversPaused) { var imev = { type: "over", target: io, event: ev, pointer: pointer, touch: pointer.touch }; io.events.dispatchImmediately("over", imev); } } }; /** * Handles when [[InteractionObject]] is no longer hovered. * * If `soft = true`, this means that method is being invoked by some other * code, not hard "out" function, like `handleUp` which implies we need to * run additional checks before unhovering the object. * * @ignore Exclude from docs * @param io Interaction object * @param pointer Pointer * @param ev Original event * @param soft Invoked by helper function * @param force Force imediate out */ Interaction.prototype.handleOut = function (io, pointer, ev, soft, force) { var _this = this; if (soft === void 0) { soft = false; } if (force === void 0) { force = false; } if (!io.hoverable) { return; } // Remove pointer io.overPointers.removeValue(pointer); // Check if element is still hovered if (io.isHover && (!io.hasDelayedOut || force)) { // Should we run additional checks? if (soft && io.overPointers.length) { // There are still pointers hovering - don't do anything else and // wait until either no over pointers are there or we get a hard out // event. return; } // Should we delay "out" if this is happening on a touch device? if (pointer && pointer.touch && !force && !this.old(pointer)) { // This is a touch pointer, and it hasn't moved, let's pretend // the object is still hovered, and act as per "behavior" setting var behavior = this.getHoverOption(io, "touchOutBehavior"); if (behavior == "leave") { // Set to "leave", so we do not execute any "out" event. // It will be handled by any other interaction that happens // afterwards. this._delayedEvents.out.push({ type: "out", io: io, pointer: pointer, event: ev, keepUntil: $time.getTime() + 500 }); io.hasDelayedOut = true; return; } else if (behavior == "delay" && this.getHoverOption(io, "touchOutDelay")) { this._delayedEvents.out.push({ type: "out", io: io, pointer: pointer, event: ev, keepUntil: $time.getTime() + 500, timeout: this.setTimeout(function () { _this.handleOut(io, pointer, ev, true); }, this.getHoverOption(io, "touchOutDelay")) }); return; } else { // Nothing for "remove" - that's how it works "out-of-the-box" } } // Set element as not hovered io.isHover = false; this.overObjects.removeValue(io); // Invoke event if (!io.isDisposed() && io.events.isEnabled("out") && !system.isPaused) { var imev = { type: "out", target: io, event: ev, pointer: pointer, touch: pointer.touch }; io.events.dispatchImmediately("out", imev); } // Reset object from lefover delayed outs, pointers io.overPointers.clear(); io.hasDelayedOut = false; // @todo (clean delayed) } }; /** * Processes dalyed events, such as "out" event that was initiated for * elements by touch. */ Interaction.prototype.processDelayed = function () { var delayedEvent; while (true) { delayedEvent = this._delayedEvents.out.pop(); if (!delayedEvent) { break; } if (delayedEvent.timeout) { delayedEvent.timeout.dispose(); } this.handleOut(delayedEvent.io, delayedEvent.pointer, delayedEvent.event, false, true); } }; /** * Performs tasks on pointer down. * * @ignore Exclude from docs * @param io Element * @param pointer Pointer * @param ev Original event */ Interaction.prototype.handleDown = function (io, pointer, ev) { // Need to prevent default event from happening on transformable objects this.maybePreventDefault(io, ev, pointer); // Stop inertia animations if they're currently being played out if (io.inert) { this.stopInertia(io); } // Trigger hover because some touch devices won't trigger over events // on their own this.handleOver(io, pointer, ev, true); // Add pointer to list io.downPointers.moveValue(pointer); // Apply styles if necessary this.applyCursorDownStyle(io, pointer); // Check if object is already down if (!io.isDown) { // Lose focus if needed if (io.focusable !== false && this.getHitOption(io, "noFocus") && this.focusedObject) { $dom.blur(); } // Set object as hovered io.isDown = true; this.downObjects.moveValue(io); // Prep object for dragging and/or resizing if (io.draggable) { this.processDragStart(io, pointer, ev); } if (io.resizable) { this.processResizeStart(io, pointer, ev); } } // Dispatch "down" event if (io.events.isEnabled("down") && !system.isPaused) { var imev = { type: "down", target: io,