@webwriter/geometry-cloze
Version:
Create and view geometry exercises with coloring, styling and labeling options.
354 lines (322 loc) • 10.2 kB
text/typescript
import Calc, { MathPoint } from '../helper/Calc';
import Draggable from '../elements/base/Draggable';
import ChildrenManager from './ChildrenManager';
import { WwGeomContextMenu } from '../../components/context-menu/ww-geom-context-menu';
export default abstract class EventManager extends ChildrenManager {
private wrapper: HTMLElement;
private clickTargetEle: HTMLElement;
private rootEle: HTMLElement;
private contextMenu: WwGeomContextMenu;
/**
* Currently selected element
*/
private _selected: Draggable[] = [];
constructor(
canvas: HTMLCanvasElement,
rootEle: HTMLElement,
contextMenu: WwGeomContextMenu
) {
super(canvas);
this.wrapper = canvas;
this.contextMenu = contextMenu;
this.clickTargetEle = canvas;
this.rootEle = rootEle;
this.clickTargetEle.addEventListener(
'mousedown',
this.onMouseDown.bind(this)
);
this.clickTargetEle.addEventListener(
'touchstart',
this.onMouseDown.bind(this)
);
this.clickTargetEle.addEventListener('mouseup', this.onMouseUp.bind(this));
this.clickTargetEle.addEventListener('touchend', this.onMouseUp.bind(this));
this.clickTargetEle.addEventListener(
'mousemove',
this.onMouseMove.bind(this),
{
passive: true,
capture: false
}
);
this.clickTargetEle.addEventListener(
'touchmove',
this.onMouseMove.bind(this),
{
passive: true,
capture: false
}
);
this.clickTargetEle.addEventListener('touchmove', this.preventTouchScroll, {
passive: false,
capture: false
});
this.clickTargetEle.addEventListener(
'contextmenu',
this.handleContextMenu.bind(this)
);
this.rootEle.addEventListener(
'keydown',
this._handleKeyboardEvent.bind(this)
);
this.rootEle.addEventListener('keyup', this.handleKeyUp.bind(this));
}
unmount() {
this.clickTargetEle.removeEventListener(
'mousedown',
this.onMouseDown.bind(this)
);
this.clickTargetEle.removeEventListener(
'touchstart',
this.onMouseDown.bind(this)
);
this.clickTargetEle.removeEventListener(
'mouseup',
this.onMouseUp.bind(this)
);
this.clickTargetEle.removeEventListener(
'touchend',
this.onMouseUp.bind(this)
);
this.clickTargetEle.removeEventListener(
'mousemove',
this.onMouseMove.bind(this)
);
this.clickTargetEle.removeEventListener(
'touchmove',
this.onMouseMove.bind(this)
);
this.clickTargetEle.removeEventListener(
'touchmove',
this.preventTouchScroll
);
this.clickTargetEle.removeEventListener(
'contextmenu',
this.handleContextMenu.bind(this)
);
this.rootEle.removeEventListener(
'keydown',
this._handleKeyboardEvent.bind(this)
);
this.rootEle.removeEventListener('keyup', this.handleKeyUp.bind(this));
}
private preventTouchScroll(event: TouchEvent) {
event.preventDefault();
}
private getRelativeCoordinates(event: MouseEvent | TouchEvent) {
const rect = this.wrapper.getBoundingClientRect();
const absX =
('clientX' in event ? event.clientX : event.touches[0].clientX) -
rect.left;
const absY =
('clientX' in event ? event.clientY : event.touches[0].clientY) -
rect.top;
const { width: canvasWidth, height: canvasHeight } =
this.getCanvasDimensions();
const x = (absX / rect.width) * canvasWidth;
const y = (absY / rect.height) * canvasHeight;
return { x, y };
}
/**
* Wether for the current mouse down event the mouse has been moved beyond the threshold
*/
private moved: boolean = false;
private mouseDownTarget: { element: Draggable; wasSelected: boolean } | null =
null;
private onMouseDown(event: MouseEvent | TouchEvent) {
event.stopPropagation();
this.moved = false;
const coords = this.getRelativeCoordinates(event);
const hit = this.getElementAt(coords);
if (hit) {
const wasSelected = this._selected.includes(hit);
if (!wasSelected && this.onSelect(hit))
this.select(hit, { keepSelection: event.ctrlKey });
this.mouseDownTarget = {
element: hit,
wasSelected
};
} else {
this.mouseDownTarget = null;
}
this.dragStart = {
...coords,
startPositions: this._selected.map((shape) => ({
x: shape.x,
y: shape.y
}))
};
}
private onMouseUp(event: MouseEvent | TouchEvent) {
event.stopPropagation();
const isRightClick = 'button' in event && event.button === 2;
if (this.moved) {
this.handleDragEnd({
from: this.dragStart!,
to: this.getRelativeCoordinates(event),
element: this.mouseDownTarget?.element ?? null
});
return;
} else {
const hit = this.getElementAt(this.dragStart!);
// needs to be more complicated since selected state is set in mouseDown listener
const alreadySelected = (() => {
if (!hit) return false;
if (this.mouseDownTarget) return this.mouseDownTarget.wasSelected;
return this._selected.includes(hit);
})();
this.handleClick({
coords: this.getRelativeCoordinates(event),
hit,
alreadySelected,
ctrlPressed: event.ctrlKey,
isRightClick
});
}
}
/**
* Information about the first click/mouse down when dragging an element
*/
private dragStart: // position of the first click/mouse down
| (MathPoint & {
// positions of selected elements at the time of the first click/mouse down
startPositions: MathPoint[];
})
| null = null;
private onMouseMove(event: MouseEvent | TouchEvent) {
event.stopPropagation();
const coords = this.getRelativeCoordinates(event);
// when somehow the mouseup event is not fired, we still want to stop dragging -> can occur when user pressed alt+tab while dragging
if ('buttons' in event && event.buttons !== 1) {
this.clickTargetEle.style.cursor = this.upadateCursor(coords);
if (this.moved) {
this.handleDragEnd({
from: this.dragStart!,
to: coords,
element: this.mouseDownTarget?.element ?? null
});
this.moved = false;
}
this.handleMouseMove({ current: coords });
return;
}
// set moved to true if we moved more than threshold
if (
!this.moved &&
this.dragStart &&
Calc.distance(coords, this.dragStart) > 5
) {
this.moved = true;
this.handleDragStart({
start: this.dragStart,
hit: this.getElementAt(this.dragStart!)
});
}
this.handleDragging({
start: this.dragStart!,
current: coords,
dragStart: this.dragStart!,
element: this.mouseDownTarget?.element ?? null
});
}
private handleContextMenu(event: MouseEvent) {
event.stopPropagation();
const coords = this.getRelativeCoordinates(event);
const hit = this.getElementAt(coords);
if (hit) {
event.preventDefault();
const menuitems = hit.getContextMenuItems();
if (menuitems.length) {
const localX =
event.clientX - this.wrapper.getBoundingClientRect().left;
const localY = event.clientY - this.wrapper.getBoundingClientRect().top;
this.contextMenu.items = menuitems;
this.contextMenu.open(localX, localY);
}
}
}
protected keys = {
alt: false,
shift: false,
ctrl: false
};
private _handleKeyboardEvent(event: KeyboardEvent) {
// ctrl+z is bubbled up to be handle outside this widget
if (event.key.toLowerCase() === 'z' && event.ctrlKey) return;
event.stopPropagation();
event.preventDefault();
this.keys = {
ctrl: event.ctrlKey,
shift: event.shiftKey,
alt: event.altKey
};
this.handleKeyboardEvent(event.key);
}
private handleKeyUp(event: KeyboardEvent) {
this.keys = {
ctrl: event.ctrlKey,
shift: event.shiftKey,
alt: event.altKey
};
}
protected abstract handleClick(_event: {
coords: MathPoint;
hit: Draggable | null;
alreadySelected: boolean;
ctrlPressed: boolean;
isRightClick: boolean;
}): void;
protected abstract handleMouseMove(_event: { current: MathPoint }): void;
protected abstract handleDragStart(_event: {
start: MathPoint;
hit: Draggable | null;
}): void;
protected abstract handleDragging(_event: {
start: MathPoint;
current: MathPoint;
element: Draggable | null;
dragStart: MathPoint & { startPositions: MathPoint[] };
}): void;
protected abstract handleDragEnd(_event: {
from: MathPoint;
to: MathPoint;
element: Draggable | null;
}): void;
protected abstract handleKeyboardEvent(key: string): void;
protected upadateCursor(_coords: MathPoint): CSSStyleDeclaration['cursor'] {
return 'default';
}
protected onSelect(element: Draggable): boolean {
return true;
}
protected get selected() {
return this._selected;
}
select(
shape: Draggable | Draggable[],
options: { keepSelection?: boolean } = {}
) {
const { keepSelection = false } = options;
if (!keepSelection) this.blur();
const shapes = Array.isArray(shape) ? shape : [shape];
for (const shape of shapes) {
if (!this._selected.includes(shape)) {
this._selected.push(shape);
shape.select();
}
}
this.requestRedraw();
}
blur(element?: Draggable | null) {
if (element) {
const index = this._selected.indexOf(element);
if (index < 0) return;
this._selected.splice(index, 1);
element.blur();
} else {
this._selected.forEach((shape) => shape.blur());
this._selected = [];
}
this.requestRedraw();
}
}