]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/ui/decorators/image.ts
1692d078d0c0c39913d6ba1714470735eacabe40
[bookstack] / resources / js / wysiwyg / ui / decorators / image.ts
1 import {EditorDecorator} from "../framework/decorator";
2 import {el} from "../../helpers";
3 import {$createNodeSelection, $setSelection} from "lexical";
4 import {EditorUiContext} from "../framework/core";
5 import {ImageNode} from "../../nodes/image";
6
7
8 export class ImageDecorator extends EditorDecorator {
9     protected dom: HTMLElement|null = null;
10     protected dragLastMouseUp: number = 0;
11
12     buildDOM(context: EditorUiContext) {
13         let handleElems: HTMLElement[] = [];
14         const decorateEl = el('div', {
15             class: 'editor-image-decorator',
16         }, []);
17         let selected = false;
18
19         const windowClick = (event: MouseEvent) => {
20             if (!decorateEl.contains(event.target as Node) && (Date.now() - this.dragLastMouseUp > 100)) {
21                 unselect();
22             }
23         };
24
25         const mouseDown = (event: MouseEvent) => {
26             const handle = (event.target as HTMLElement).closest('.editor-image-decorator-handle') as HTMLElement|null;
27             if (handle) {
28                 // handlingResize = true;
29                 this.startHandlingResize(handle, event, context);
30             }
31         };
32
33         const select = () => {
34             if (selected) {
35                 return;
36             }
37
38             selected = true;
39             decorateEl.classList.add('selected');
40             window.addEventListener('click', windowClick);
41
42             const handleClasses = ['nw', 'ne', 'se', 'sw'];
43             handleElems = handleClasses.map(c => {
44                 return el('div', {class: `editor-image-decorator-handle ${c}`});
45             });
46             decorateEl.append(...handleElems);
47             decorateEl.addEventListener('mousedown', mouseDown);
48
49             context.editor.update(() => {
50                 const nodeSelection = $createNodeSelection();
51                 nodeSelection.add(this.getNode().getKey());
52                 $setSelection(nodeSelection);
53             });
54         };
55
56         const unselect = () => {
57             selected = false;
58             // handlingResize = false;
59             decorateEl.classList.remove('selected');
60             window.removeEventListener('click', windowClick);
61             decorateEl.removeEventListener('mousedown', mouseDown);
62             for (const el of handleElems) {
63                 el.remove();
64             }
65         };
66
67         decorateEl.addEventListener('click', (event) => {
68             select();
69         });
70
71         return decorateEl;
72     }
73
74     render(context: EditorUiContext): HTMLElement {
75         if (this.dom) {
76             return this.dom;
77         }
78
79         this.dom = this.buildDOM(context);
80         return this.dom;
81     }
82
83     startHandlingResize(element: HTMLElement, event: MouseEvent, context: EditorUiContext) {
84         const startingX = event.screenX;
85         const startingY = event.screenY;
86         const node = this.getNode() as ImageNode;
87         let startingWidth = element.clientWidth;
88         let startingHeight = element.clientHeight;
89         let startingRatio = startingWidth / startingHeight;
90         let hasHeight = false;
91         let firstChange = true;
92         context.editor.getEditorState().read(() => {
93             startingWidth = node.getWidth() || startingWidth;
94             startingHeight = node.getHeight() || startingHeight;
95             if (node.getHeight()) {
96                 hasHeight = true;
97             }
98             startingRatio = startingWidth / startingHeight;
99         });
100
101         const flipXChange = element.classList.contains('nw') || element.classList.contains('sw');
102         const flipYChange = element.classList.contains('nw') || element.classList.contains('ne');
103
104         const mouseMoveListener = (event: MouseEvent) => {
105             let xChange = event.screenX - startingX;
106             if (flipXChange) {
107                 xChange = 0 - xChange;
108             }
109             let yChange = event.screenY - startingY;
110             if (flipYChange) {
111                 yChange = 0 - yChange;
112             }
113             const balancedChange = Math.sqrt(Math.pow(Math.abs(xChange), 2) + Math.pow(Math.abs(yChange), 2));
114             const increase = xChange + yChange > 0;
115             const directedChange = increase ? balancedChange : 0-balancedChange;
116             const newWidth = Math.max(5, Math.round(startingWidth + directedChange));
117             let newHeight = 0;
118             if (hasHeight) {
119                 newHeight = newWidth * startingRatio;
120             }
121
122             const updateOptions = firstChange ? {} : {tag: 'history-merge'};
123             context.editor.update(() => {
124                 const node = this.getNode() as ImageNode;
125                 node.setWidth(newWidth);
126                 node.setHeight(newHeight);
127             }, updateOptions);
128             firstChange = false;
129         };
130
131         const mouseUpListener = (event: MouseEvent) => {
132             window.removeEventListener('mousemove', mouseMoveListener);
133             window.removeEventListener('mouseup', mouseUpListener);
134             this.dragLastMouseUp = Date.now();
135         };
136
137         window.addEventListener('mousemove', mouseMoveListener);
138         window.addEventListener('mouseup', mouseUpListener);
139     }
140
141 }