1 |
|
2 |
|
3 |
|
4 |
|
5 | export const enum ResizerType {
|
6 | WIDTH = 'width',
|
7 | HEIGHT = 'height',
|
8 | BIDIRECTION = 'bidirection',
|
9 | }
|
10 |
|
11 | export interface Draggable {
|
12 | type: ResizerType;
|
13 | initialWidth?: number;
|
14 | initialHeight?: number;
|
15 | update({width, height}: {width?: number, height?: number}): void;
|
16 | }
|
17 |
|
18 | export interface Delegate {
|
19 | getDraggable(x: number, y: number): Draggable|undefined;
|
20 | }
|
21 |
|
22 | const cursorByResizerType = new Map([
|
23 | [ResizerType.WIDTH, 'ew-resize'],
|
24 | [ResizerType.HEIGHT, 'ns-resize'],
|
25 | [ResizerType.BIDIRECTION, 'nwse-resize'],
|
26 | ]);
|
27 |
|
28 | type OriginInfo = {
|
29 | coord: number,
|
30 | value: number,
|
31 | };
|
32 |
|
33 | export class DragResizeHandler {
|
34 | private document: Document;
|
35 | private delegate: Delegate;
|
36 | private originX?: OriginInfo;
|
37 | private originY?: OriginInfo;
|
38 | private boundMousemove: (event: MouseEvent) => void;
|
39 | private boundMousedown: (event: MouseEvent) => void;
|
40 |
|
41 | constructor(document: Document, delegate: Delegate) {
|
42 | this.document = document;
|
43 | this.delegate = delegate;
|
44 | this.boundMousemove = this.onMousemove.bind(this);
|
45 | this.boundMousedown = this.onMousedown.bind(this);
|
46 | }
|
47 |
|
48 | install() {
|
49 | this.document.body.addEventListener('mousemove', this.boundMousemove);
|
50 | this.document.body.addEventListener('mousedown', this.boundMousedown);
|
51 | }
|
52 |
|
53 | uninstall() {
|
54 | this.document.body.removeEventListener('mousemove', this.boundMousemove);
|
55 | this.document.body.removeEventListener('mousedown', this.boundMousedown);
|
56 | }
|
57 |
|
58 | |
59 |
|
60 |
|
61 | private onMousemove(event: MouseEvent) {
|
62 | const match = this.delegate.getDraggable(event.clientX, event.clientY);
|
63 | if (!match) {
|
64 | this.document.body.style.cursor = 'default';
|
65 | return;
|
66 | }
|
67 | this.document.body.style.cursor = cursorByResizerType.get(match.type) || 'default';
|
68 | }
|
69 |
|
70 | |
71 |
|
72 |
|
73 | private onMousedown(event: MouseEvent) {
|
74 | const match = this.delegate.getDraggable(event.clientX, event.clientY);
|
75 | if (!match) {
|
76 | return;
|
77 | }
|
78 |
|
79 | const boundOnDrag = this.onDrag.bind(this, match);
|
80 |
|
81 | event.stopPropagation();
|
82 | event.preventDefault();
|
83 |
|
84 | if (match.initialWidth !== undefined &&
|
85 | (match.type === ResizerType.WIDTH || match.type === ResizerType.BIDIRECTION)) {
|
86 | this.originX = {
|
87 | coord: Math.round(event.clientX),
|
88 | value: match.initialWidth,
|
89 | };
|
90 | }
|
91 |
|
92 | if (match.initialHeight !== undefined &&
|
93 | (match.type === ResizerType.HEIGHT || match.type === ResizerType.BIDIRECTION)) {
|
94 | this.originY = {
|
95 | coord: Math.round(event.clientY),
|
96 | value: match.initialHeight,
|
97 | };
|
98 | }
|
99 |
|
100 | this.document.body.removeEventListener('mousemove', this.boundMousemove);
|
101 | this.document.body.style.cursor = cursorByResizerType.get(match.type) || 'default';
|
102 |
|
103 | const endDrag = (event: Event) => {
|
104 | event.stopPropagation();
|
105 | event.preventDefault();
|
106 | this.originX = undefined;
|
107 | this.originY = undefined;
|
108 |
|
109 | this.document.body.style.cursor = 'default';
|
110 | this.document.body.removeEventListener('mousemove', boundOnDrag);
|
111 | this.document.body.addEventListener('mousemove', this.boundMousemove);
|
112 | };
|
113 |
|
114 | this.document.body.addEventListener('mouseup', endDrag, {once: true});
|
115 | window.addEventListener('mouseout', endDrag, {once: true});
|
116 |
|
117 | this.document.body.addEventListener('mousemove', boundOnDrag);
|
118 | }
|
119 |
|
120 | |
121 |
|
122 |
|
123 | private onDrag(match: Draggable, e: MouseEvent) {
|
124 | if (!this.originX && !this.originY) {
|
125 | return;
|
126 | }
|
127 |
|
128 | let width: number|undefined;
|
129 | let height: number|undefined;
|
130 | if (this.originX) {
|
131 | const delta = this.originX.coord - e.clientX;
|
132 | width = Math.round(this.originX.value - delta);
|
133 | }
|
134 |
|
135 | if (this.originY) {
|
136 | const delta = this.originY.coord - e.clientY;
|
137 | height = Math.round(this.originY.value - delta);
|
138 | }
|
139 |
|
140 | match.update({width, height});
|
141 | }
|
142 | }
|