@blueprintjs/table
Version:
Scalable interactive table component
202 lines (171 loc) • 7.27 kB
text/typescript
/*
* Copyright 2016 Palantir Technologies, Inc. All rights reserved.
*
* Licensed 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 type { ClientCoordinates, CoordinateData, DragHandler } from "./dragTypes";
export class DragEvents {
public static DOUBLE_CLICK_TIMEOUT_MSEC = 500;
/**
* Returns true if the event includes a modifier key that often adds the result of the drag
* event to any existing state. For example, holding CTRL before dragging may select another
* region in addition to an existing one, while the absence of a modifier key may clear the
* existing selection first.
*
* @param event the mouse event for the drag interaction
*/
public static isAdditive(event: MouseEvent) {
return event.ctrlKey || event.metaKey;
}
private handler?: DragHandler;
private element?: HTMLElement;
private activationCoordinates?: ClientCoordinates;
private doubleClickTimeoutToken?: number;
private isActivated: boolean = false;
private isDragging: boolean = false;
private lastCoordinates?: ClientCoordinates;
public attach(element: HTMLElement, handler: DragHandler) {
this.detach();
this.handler = handler;
this.element = element;
if (this.isValidDragHandler(handler)) {
this.element.addEventListener("mousedown", this.handleMouseDown);
}
return this;
}
public detach() {
if (this.element != null) {
this.element.removeEventListener("mousedown", this.handleMouseDown);
this.detachDocumentEventListeners();
}
}
private isValidDragHandler(handler: DragHandler) {
return (
handler != null &&
(handler.onActivate != null ||
handler.onDragMove != null ||
handler.onDragEnd != null ||
handler.onClick != null ||
handler.onDoubleClick != null)
);
}
private attachDocumentEventListeners() {
document.addEventListener("mousemove", this.handleMouseMove);
document.addEventListener("mouseup", this.handleMouseUp);
}
private detachDocumentEventListeners() {
document.removeEventListener("mousemove", this.handleMouseMove);
document.removeEventListener("mouseup", this.handleMouseUp);
}
private initCoordinateData(event: MouseEvent) {
this.activationCoordinates = [event.clientX, event.clientY];
this.lastCoordinates = this.activationCoordinates;
}
private updateCoordinateData(event: MouseEvent) {
if (this.activationCoordinates === undefined) {
// invalid state; we should have activation by this point
return undefined;
}
const currentCoordinates: [number, number] = [event.clientX, event.clientY];
const lastCoordinates = this.lastCoordinates ?? [0, 0];
const deltaCoordinates: [number, number] = [
currentCoordinates[0] - lastCoordinates[0],
currentCoordinates[1] - lastCoordinates[1],
];
const offsetCoordinates: [number, number] = [
currentCoordinates[0] - this.activationCoordinates[0],
currentCoordinates[1] - this.activationCoordinates[1],
];
const data: CoordinateData = {
activation: this.activationCoordinates,
current: currentCoordinates,
delta: deltaCoordinates,
last: lastCoordinates,
offset: offsetCoordinates,
};
this.lastCoordinates = [event.clientX, event.clientY];
return data;
}
private maybeAlterEventChain(event: MouseEvent) {
if (this.handler?.preventDefault) {
event.preventDefault();
}
if (this.handler?.stopPropagation) {
event.stopPropagation();
}
}
private handleMouseDown = (event: MouseEvent) => {
this.initCoordinateData(event);
if (this.handler != null && this.handler.onActivate != null) {
const exitCode = this.handler.onActivate(event);
if (exitCode === false) {
return;
}
}
this.isActivated = true;
this.maybeAlterEventChain(event);
// It is possible that the mouseup would not be called after the initial
// mousedown (for example if the mouse is moved out of the window). So,
// we preemptively detach to avoid duplicate listeners.
this.detachDocumentEventListeners();
this.attachDocumentEventListeners();
};
private handleMouseMove = (event: MouseEvent) => {
this.maybeAlterEventChain(event);
if (this.isActivated) {
this.isDragging = true;
}
if (this.isDragging) {
const coords = this.updateCoordinateData(event);
if (coords !== undefined) {
this.handler?.onDragMove?.(event, coords);
}
}
};
private handleMouseUp = (event: MouseEvent) => {
this.maybeAlterEventChain(event);
if (this.handler != null) {
if (this.isDragging) {
const coords = this.updateCoordinateData(event);
if (coords !== undefined) {
this.handler?.onDragMove?.(event, coords);
this.handler?.onDragEnd?.(event, coords);
}
} else if (this.isActivated) {
if (this.handler.onDoubleClick != null) {
if (this.doubleClickTimeoutToken == null) {
// if this the first click of a possible double-click,
// we delay the firing of the click event by the
// timeout.
this.doubleClickTimeoutToken = window.setTimeout(() => {
delete this.doubleClickTimeoutToken;
this.handler?.onClick?.(event);
}, DragEvents.DOUBLE_CLICK_TIMEOUT_MSEC);
} else {
// otherwise, this is the second click in the double-
// click so we cancel the single-click timeout and
// fire the double-click event.
window.clearTimeout(this.doubleClickTimeoutToken);
delete this.doubleClickTimeoutToken;
this.handler.onDoubleClick(event);
}
} else if (this.handler.onClick != null) {
this.handler.onClick(event);
}
}
}
this.isActivated = false;
this.isDragging = false;
this.detachDocumentEventListeners();
};
}