plotboilerplate
Version:
A simple javascript plotting boilerplate for 2d stuff.
418 lines (395 loc) • 14.2 kB
text/typescript
/**
* @author Ikaros Kappler
* @date 2018-03-19
* @modified 2018-04-28 Added the param 'wasDragged'.
* @modified 2018-08-16 Added the param 'dragAmount'.
* @modified 2018-08-27 Added the param 'element'.
* @modified 2018-11-11 Changed the scope from a simple global var to a member of window/_context.
* @modified 2018-11-19 Renamed the 'mousedown' function to 'down' and the 'mouseup' function to 'up'.
* @modified 2018-11-28 Added the 'wheel' listener.
* @modified 2018-12-09 Cleaned up some code.
* @modified 2019-02-10 Cleaned up some more code.
* @modified 2020-03-25 Ported this class from vanilla-JS to Typescript.
* @modified 2020-04-08 Fixed the click event (internally fired a 'mouseup' event) (1.0.10)
* @modified 2020-04-08 Added the optional 'name' property. (1.0.11)
* @modified 2020-04-08 The new version always installs internal listenrs to track drag events even
* if there is no external drag listener installed (1.1.0).
* @modified 2020-10-04 Added extended JSDoc comments.
* @modified 2020-11-25 Added the `isTouchEvent` param.
* @modified 2021-01-10 The mouse handler is now also working with SVGElements.
* @modified 2022-08-16 Fixed a bug in the mouse button detection.
* @version 1.2.1
*
* @file MouseHandler
* @public
**/
import { XYCoords } from "./interfaces";
export interface XMouseParams {
button: number;
element: HTMLElement | SVGElement;
isTouchEvent: boolean;
name: string;
pos: { x: number; y: number };
leftButton: boolean;
middleButton: boolean;
rightButton: boolean;
mouseDownPos: { x: number; y: number };
draggedFrom: { x: number; y: number };
wasDragged: boolean;
dragAmount: { x: number; y: number };
}
export class XMouseEvent extends MouseEvent {
params: XMouseParams;
}
export class XWheelEvent extends WheelEvent {
params: XMouseParams;
}
export type XMouseCallback = (e: XMouseEvent) => void;
export type XWheelCallback = (e: XWheelEvent) => void;
/**
* @classdesc A simple mouse handler for demos.
* Use to avoid load massive libraries like jQuery.
*
* @requires XYCoords
*/
export class MouseHandler {
private name: string | undefined;
private element: HTMLElement | SVGElement;
private mouseDownPos: { x: number; y: number } | undefined = undefined;
private mouseDragPos: { x: number; y: number } | undefined = undefined;
// TODO: cc
// private mousePos : { x:number, y:number }|undefined = undefined;
private mouseButton: number = -1;
private listeners: Record<string, XMouseCallback> = {};
private installed: Record<string, boolean> = {};
private handlers: Record<string, XMouseCallback> = {};
/**
* The constructor.
*
* Pass the DOM element you want to receive mouse events from.
*
* Usage
* =====
* @example
* // Javascript
* new MouseHandler( document.getElementById('mycanvas') )
* .drag( function(e) {
* console.log( 'Mouse dragged: ' + JSON.stringify(e) );
* if( e.params.leftMouse ) ;
* else if( e.params.rightMouse ) ;
* } )
* .move( function(e) {
* console.log( 'Mouse moved: ' + JSON.stringify(e.params) );
* } )
* .up( function(e) {
* console.log( 'Mouse up. Was dragged?', e.params.wasDragged );
* } )
* .down( function(e) {
* console.log( 'Mouse down.' );
* } )
* .click( function(e) {
* console.log( 'Click.' );
* } )
* .wheel( function(e) {
* console.log( 'Wheel. delta='+e.deltaY );
* } )
*
* @example
* // Typescript
* new MouseHandler( document.getElementById('mycanvas') )
* .drag( (e:XMouseEvent) => {
* console.log( 'Mouse dragged: ' + JSON.stringify(e) );
* if( e.params.leftMouse ) ;
* else if( e.params.rightMouse ) ;
* } )
* .move( (e:XMouseEvent) => {
* console.log( 'Mouse moved: ' + JSON.stringify(e.params) );
* } )
* .up( (e:XMouseEvent) => {
* console.log( 'Mouse up. Was dragged?', e.params.wasDragged );
* } )
* .down( (e:XMouseEvent) => {
* console.log( 'Mouse down.' );
* } )
* .click( (e:XMouseEvent) => {
* console.log( 'Click.' );
* } )
* .wheel( (e:XWheelEvent) => {
* console.log( 'Wheel. delta='+e.deltaY );
* } )
*
* @constructor
* @instance
* @memberof MouseHandler
* @param {HTMLElement} element
**/
constructor(element: HTMLElement | SVGElement, name?: string) {
// +----------------------------------------------------------------------
// | Some private vars to store the current mouse/position/button state.
// +-------------------------------------------------
this.name = name;
this.element = element;
this.mouseDownPos = undefined;
this.mouseDragPos = undefined;
// this.mousePos = null;
this.mouseButton = -1;
this.listeners = {};
this.installed = {};
this.handlers = {};
// +----------------------------------------------------------------------
// | Define the internal event handlers.
// |
// | They will dispatch the modified event (relative mouse position,
// | drag offset, ...) to the callbacks.
// +-------------------------------------------------
const _self: MouseHandler = this;
this.handlers["mousemove"] = (e: MouseEvent) => {
if (_self.listeners.mousemove) _self.listeners.mousemove(_self.mkParams(e, "mousemove"));
if (_self.mouseDragPos && _self.listeners.drag) _self.listeners.drag(_self.mkParams(e, "drag"));
if (_self.mouseDownPos) _self.mouseDragPos = _self.relPos(e);
};
this.handlers["mouseup"] = (e: MouseEvent) => {
if (_self.listeners.mouseup) _self.listeners.mouseup(_self.mkParams(e, "mouseup"));
_self.mouseDragPos = undefined;
_self.mouseDownPos = undefined;
_self.mouseButton = -1;
};
this.handlers["mousedown"] = (e: MouseEvent) => {
_self.mouseDragPos = _self.relPos(e);
_self.mouseDownPos = _self.relPos(e);
_self.mouseButton = e.button;
if (_self.listeners.mousedown) _self.listeners.mousedown(_self.mkParams(e, "mousedown"));
};
this.handlers["click"] = (e: MouseEvent) => {
if (_self.listeners.click) _self.listeners.click(_self.mkParams(e, "click"));
};
this.handlers["wheel"] = (e: MouseEvent) => {
if (_self.listeners.wheel) _self.listeners.wheel(_self.mkParams(e, "wheel"));
};
this.element.addEventListener("mousemove", this.handlers["mousemove"] as EventListener);
this.element.addEventListener("mouseup", this.handlers["mouseup"] as EventListener);
this.element.addEventListener("mousedown", this.handlers["mousedown"] as EventListener);
this.element.addEventListener("click", this.handlers["click"] as EventListener);
this.element.addEventListener("wheel", this.handlers["wheel"] as EventListener);
}
/**
* Get relative position from the given MouseEvent.
*
* @name relPos
* @memberof MouseHandler
* @instance
* @private
* @param {MouseEvent} e - The mouse event to get the relative position for.
* @return {XYCoords} The relative mouse coordinates.
*/
private relPos(e: MouseEvent): XYCoords {
return { x: e.offsetX, y: e.offsetY };
}
/**
* Build the extended event params.
*
* @name mkParams
* @memberof MouseHandler
* @instance
* @private
* @param {MouseEvent} event - The mouse event to get the relative position for.
* @param {string} eventName - The name of the firing event.
* @return {XMouseEvent}
*/
private mkParams(event: MouseEvent, eventName: string): XMouseEvent {
const rel: { x: number; y: number } = this.relPos(event);
const xEvent: XMouseEvent = event as unknown as XMouseEvent;
xEvent.params = {
element: this.element,
name: eventName,
isTouchEvent: false,
pos: rel,
button: event.button, // this.mouseButton,
leftButton: event.button === 0, // this.mouseButton === 0,
middleButton: event.button === 1, // this.mouseButton === 1,
rightButton: event.button === 2, // this.mouseButton === 2,
mouseDownPos: this.mouseDownPos ?? { x: NaN, y: NaN },
draggedFrom: this.mouseDragPos ?? { x: NaN, y: NaN },
wasDragged: this.mouseDownPos != null && (this.mouseDownPos.x != rel.x || this.mouseDownPos.y != rel.y),
dragAmount: this.mouseDragPos != null ? { x: rel.x - this.mouseDragPos.x, y: rel.y - this.mouseDragPos.y } : { x: 0, y: 0 }
};
return xEvent;
}
/**
* Install a new listener.
* Please note that this mouse handler can only handle one listener per event type.
*
* @name listenFor
* @memberof MouseHandler
* @instance
* @private
* @param {string} eventName - The name of the firing event to listen for.
* @return {void}
*/
private listenFor(eventName: string) {
if (this.installed[eventName]) return;
// In the new version 1.1.0 has all internal listeners installed by default.
this.installed[eventName] = true;
}
/**
* Un-install a new listener.
*
* @name listenFor
* @memberof MouseHandler
* @instance
* @private
* @param {string} eventName - The name of the firing event to unlisten for.
* @return {void}
*/
private unlistenFor(eventName: string) {
if (!this.installed[eventName]) return;
// In the new version 1.1.0 has all internal listeners installed by default.
delete this.installed[eventName];
}
/**
* Installer function to listen for a specific event: mouse-drag.
* Pass your callbacks here.
*
* Note: this support chaining.
*
* @name drag
* @memberof MouseHandler
* @instance
* @param {XMouseCallback} callback - The drag-callback to listen for.
* @return {MouseHandler} this
*/
drag(callback: XMouseCallback): MouseHandler {
if (this.listeners.drag) this.throwAlreadyInstalled("drag");
this.listeners.drag = callback;
this.listenFor("mousedown");
this.listenFor("mousemove");
this.listenFor("mouseup");
return this;
}
/**
* Installer function to listen for a specific event: mouse-move.
* Pass your callbacks here.
*
* Note: this support chaining.
*
* @name move
* @memberof MouseHandler
* @instance
* @param {XMouseCallback} callback - The move-callback to listen for.
* @return {MouseHandler} this
*/
move(callback: (e: XMouseEvent) => void): MouseHandler {
if (this.listeners.mousemove) this.throwAlreadyInstalled("mousemove");
this.listenFor("mousemove");
this.listeners.mousemove = callback;
return this;
}
/**
* Installer function to listen for a specific event: mouse-up.
* Pass your callbacks here.
*
* Note: this support chaining.
*
* @name up
* @memberof MouseHandler
* @instance
* @param {XMouseCallback} callback - The up-callback to listen for.
* @return {MouseHandler} this
*/
up(callback: (e: XMouseEvent) => void): MouseHandler {
if (this.listeners.mouseup) this.throwAlreadyInstalled("mouseup");
this.listenFor("mouseup");
this.listeners.mouseup = callback;
return this;
}
/**
* Installer function to listen for a specific event: mouse-down.
* Pass your callbacks here.
*
* Note: this support chaining.
*
* @name down
* @memberof MouseHandler
* @instance
* @param {XMouseCallback} callback - The down-callback to listen for.
* @return {MouseHandler} this
*/
down(callback: (e: XMouseEvent) => void): MouseHandler {
if (this.listeners.mousedown) this.throwAlreadyInstalled("mousedown");
this.listenFor("mousedown");
this.listeners.mousedown = callback;
return this;
}
/**
* Installer function to listen for a specific event: mouse-click.
* Pass your callbacks here.
*
* Note: this support chaining.
*
* @name click
* @memberof MouseHandler
* @instance
* @param {XMouseCallback} callback - The click-callback to listen for.
* @return {MouseHandler} this
*/
click(callback: (e: XMouseEvent) => void): MouseHandler {
if (this.listeners.click) this.throwAlreadyInstalled("click");
this.listenFor("click");
this.listeners.click = callback;
return this;
}
/**
* Installer function to listen for a specific event: mouse-wheel.
* Pass your callbacks here.
*
* Note: this support chaining.
*
* @name wheel
* @memberof MouseHandler
* @instance
* @param {XWheelCallback} callback - The wheel-callback to listen for.
* @return {MouseHandler} this
*/
wheel(callback: (e: XWheelEvent) => void): MouseHandler {
if (this.listeners.wheel) this.throwAlreadyInstalled("wheel");
this.listenFor("wheel");
this.listeners.wheel = callback as XMouseCallback;
return this;
}
/**
* An internal function to throw events.
*
* @name throwAlreadyInstalled
* @memberof MouseHandler
* @instance
* @private
* @param {string} name - The name of the event.
* @return {void}
*/
private throwAlreadyInstalled(name: string) {
throw `This MouseHandler already has a '${name}' callback. To keep the code simple there is only room for one.`;
}
/**
* Call this when your work is done.
*
* The function will un-install all event listeners.
*
* @name destroy
* @memberof MouseHandler
* @instance
* @private
* @return {void}
*/
destroy() {
this.unlistenFor("mousedown");
this.unlistenFor("mousemove");
this.unlistenFor("moseup");
this.unlistenFor("click");
this.unlistenFor("wheel");
this.element.removeEventListener("mousemove", this.handlers["mousemove"] as EventListener);
this.element.removeEventListener("mouseup", this.handlers["mousedown"] as EventListener);
this.element.removeEventListener("mousedown", this.handlers["mousedown"] as EventListener);
this.element.removeEventListener("click", this.handlers["click"] as EventListener);
this.element.removeEventListener("wheel", this.handlers["wheel"] as EventListener);
}
}