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