blob: 53bf499218d5e88fadf72d39e5d3621a527b9c2a [file] [log] [blame]
// Copyright 2021 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
export const enum ResizerType {
WIDTH = 'width',
HEIGHT = 'height',
BIDIRECTION = 'bidirection',
}
export interface Draggable {
type: ResizerType;
initialWidth?: number;
initialHeight?: number;
update({width, height}: {width?: number, height?: number}): void;
}
export interface Delegate {
getDraggable(x: number, y: number): Draggable|undefined;
}
const cursorByResizerType = new Map([
[ResizerType.WIDTH, 'ew-resize'],
[ResizerType.HEIGHT, 'ns-resize'],
[ResizerType.BIDIRECTION, 'nwse-resize'],
]);
type OriginInfo = {
coord: number,
value: number,
};
export class DragResizeHandler {
private document: Document;
private delegate: Delegate;
private originX?: OriginInfo;
private originY?: OriginInfo;
private boundMousemove: (event: MouseEvent) => void;
private boundMousedown: (event: MouseEvent) => void;
constructor(document: Document, delegate: Delegate) {
this.document = document;
this.delegate = delegate;
this.boundMousemove = this.onMousemove.bind(this);
this.boundMousedown = this.onMousedown.bind(this);
}
install() {
this.document.body.addEventListener('mousemove', this.boundMousemove);
this.document.body.addEventListener('mousedown', this.boundMousedown);
}
uninstall() {
this.document.body.removeEventListener('mousemove', this.boundMousemove);
this.document.body.removeEventListener('mousedown', this.boundMousedown);
}
/**
* Updates the cursor style of the mouse is hovered over a resizeable area.
*/
private onMousemove(event: MouseEvent) {
const match = this.delegate.getDraggable(event.clientX, event.clientY);
if (!match) {
this.document.body.style.cursor = 'default';
return;
}
this.document.body.style.cursor = cursorByResizerType.get(match.type) || 'default';
}
/**
* Starts dragging
*/
private onMousedown(event: MouseEvent) {
const match = this.delegate.getDraggable(event.clientX, event.clientY);
if (!match) {
return;
}
const boundOnDrag = this.onDrag.bind(this, match);
event.stopPropagation();
event.preventDefault();
if (match.initialWidth !== undefined &&
(match.type === ResizerType.WIDTH || match.type === ResizerType.BIDIRECTION)) {
this.originX = {
coord: Math.round(event.clientX),
value: match.initialWidth,
};
}
if (match.initialHeight !== undefined &&
(match.type === ResizerType.HEIGHT || match.type === ResizerType.BIDIRECTION)) {
this.originY = {
coord: Math.round(event.clientY),
value: match.initialHeight,
};
}
this.document.body.removeEventListener('mousemove', this.boundMousemove);
this.document.body.style.cursor = cursorByResizerType.get(match.type) || 'default';
const endDrag = (event: Event) => {
event.stopPropagation();
event.preventDefault();
this.originX = undefined;
this.originY = undefined;
this.document.body.style.cursor = 'default';
this.document.body.removeEventListener('mousemove', boundOnDrag);
this.document.body.addEventListener('mousemove', this.boundMousemove);
};
this.document.body.addEventListener('mouseup', endDrag, {once: true});
window.addEventListener('mouseout', endDrag, {once: true});
this.document.body.addEventListener('mousemove', boundOnDrag);
}
/**
* Computes the new value while the cursor is being dragged and calls InspectorOverlayHost with the new value.
*/
private onDrag(match: Draggable, e: MouseEvent) {
if (!this.originX && !this.originY) {
return;
}
let width: number|undefined;
let height: number|undefined;
if (this.originX) {
const delta = this.originX.coord - e.clientX;
width = Math.round(this.originX.value - delta);
}
if (this.originY) {
const delta = this.originY.coord - e.clientY;
height = Math.round(this.originY.value - delta);
}
match.update({width, height});
}
}