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