]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/ui/decorators/image.ts
b3ceee65fa41e13bbbe7dbd6bb028956e5992895
[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         context.editor.getEditorState().read(() => {
92             startingWidth = node.getWidth() || startingWidth;
93             startingHeight = node.getHeight() || startingHeight;
94             if (node.getHeight()) {
95                 hasHeight = true;
96             }
97             startingRatio = startingWidth / startingHeight;
98         });
99
100         const flipXChange = element.classList.contains('nw') || element.classList.contains('sw');
101         const flipYChange = element.classList.contains('nw') || element.classList.contains('ne');
102
103         const mouseMoveListener = (event: MouseEvent) => {
104             let xChange = event.screenX - startingX;
105             if (flipXChange) {
106                 xChange = 0 - xChange;
107             }
108             let yChange = event.screenY - startingY;
109             if (flipYChange) {
110                 yChange = 0 - yChange;
111             }
112             const balancedChange = Math.sqrt(Math.pow(xChange, 2) + Math.pow(yChange, 2));
113             const increase = xChange + yChange > 0;
114             const directedChange = increase ? balancedChange : 0-balancedChange;
115             const newWidth = Math.max(5, Math.round(startingWidth + directedChange));
116             let newHeight = 0;
117             if (hasHeight) {
118                 newHeight = newWidth * startingRatio;
119             }
120
121             context.editor.update(() => {
122                 const node = this.getNode() as ImageNode;
123                 node.setWidth(newWidth);
124                 node.setHeight(newHeight);
125             });
126         };
127
128         const mouseUpListener = (event: MouseEvent) => {
129             window.removeEventListener('mousemove', mouseMoveListener);
130             window.removeEventListener('mouseup', mouseUpListener);
131             this.dragLastMouseUp = Date.now();
132         };
133
134         window.addEventListener('mousemove', mouseMoveListener);
135         window.addEventListener('mouseup', mouseUpListener);
136     }
137
138 }