]> BookStack Code Mirror - bookstack/blob - resources/js/editor/node-views/ImageView.js
Added image resizing via drag handles
[bookstack] / resources / js / editor / node-views / ImageView.js
1 import {positionHandlesAtCorners, removeHandles, renderHandlesAtCorners} from "./node-view-utils";
2 import {NodeSelection} from "prosemirror-state";
3
4 class ImageView {
5     /**
6      * @param {PmNode} node
7      * @param {PmView} view
8      * @param {(function(): number)} getPos
9      */
10     constructor(node, view, getPos) {
11         this.dom = document.createElement('div');
12         this.dom.classList.add('ProseMirror-imagewrap');
13
14         this.image = document.createElement("img");
15         this.image.src = node.attrs.src;
16         this.image.alt = node.attrs.alt;
17         if (node.attrs.width) {
18             this.image.width = node.attrs.width;
19         }
20         if (node.attrs.height) {
21             this.image.height = node.attrs.height;
22         }
23
24         this.dom.appendChild(this.image);
25
26         this.handles = [];
27         this.handleDragStartInfo = null;
28         this.handleDragMoveDimensions = null;
29         this.removeHandlesListener = this.removeHandlesListener.bind(this);
30         this.handleMouseMove = this.handleMouseMove.bind(this);
31         this.handleMouseUp = this.handleMouseUp.bind(this);
32         this.handleMouseDown = this.handleMouseDown.bind(this);
33
34         this.dom.addEventListener("click", event => {
35             this.showHandles();
36         });
37
38         // Show handles if selected
39         if (view.state.selection.node === node) {
40             window.setTimeout(() => {
41                 this.showHandles();
42             }, 10);
43         }
44
45         this.updateImageDimensions = function (width, height) {
46             const attrs = Object.assign({}, node.attrs, {width, height});
47             let tr = view.state.tr;
48             const position = getPos();
49             tr = tr.setNodeMarkup(position, null, attrs)
50             tr = tr.setSelection(NodeSelection.create(tr.doc, position));
51             view.dispatch(tr);
52         };
53
54     }
55
56     showHandles() {
57         if (this.handles.length === 0) {
58             this.image.dataset.showHandles = 'true';
59             window.addEventListener('click', this.removeHandlesListener);
60             this.handles = renderHandlesAtCorners(this.image);
61             for (const handle of this.handles) {
62                 handle.addEventListener('mousedown', this.handleMouseDown);
63             }
64         }
65     }
66
67     removeHandlesListener(event) {
68         console.log(this.dom.contains(event.target), event.target);
69         if (!this.dom.contains(event.target)) {
70             this.removeHandles();
71             this.handles = [];
72         }
73     }
74
75     removeHandles() {
76         removeHandles(this.handles);
77         window.removeEventListener('click', this.removeHandlesListener);
78         delete this.image.dataset.showHandles;
79     }
80
81     stopEvent() {
82         return false;
83     }
84
85     /**
86      * @param {MouseEvent} event
87      */
88     handleMouseDown(event) {
89         event.preventDefault();
90
91         const imageBounds = this.image.getBoundingClientRect();
92         const handle = event.target;
93         this.handleDragStartInfo = {
94             x: event.screenX,
95             y: event.screenY,
96             ratio: imageBounds.width / imageBounds.height,
97             bounds: imageBounds,
98             handleX: handle.dataset.x,
99             handleY: handle.dataset.y,
100         };
101
102         this.createDragDummy(imageBounds);
103         this.dom.appendChild(this.dragDummy);
104
105         window.addEventListener('mousemove', this.handleMouseMove);
106         window.addEventListener('mouseup', this.handleMouseUp);
107     }
108
109     /**
110      * @param {DOMRect} bounds
111      */
112     createDragDummy(bounds) {
113         this.dragDummy = this.image.cloneNode();
114         this.dragDummy.style.opacity = '0.5';
115         this.dragDummy.classList.add('ProseMirror-dragdummy');
116         this.dragDummy.style.width = bounds.width + 'px';
117         this.dragDummy.style.height = bounds.height + 'px';
118     }
119
120     /**
121      * @param {MouseEvent} event
122      */
123     handleMouseUp(event) {
124         if (this.handleDragMoveDimensions) {
125             const {width, height} = this.handleDragMoveDimensions;
126             this.updateImageDimensions(String(width), String(height));
127         }
128
129         window.removeEventListener('mousemove', this.handleMouseMove);
130         window.removeEventListener('mouseup', this.handleMouseUp);
131         this.handleDragStartInfo = null;
132         this.handleDragMoveDimensions = null;
133         this.dragDummy.remove();
134         positionHandlesAtCorners(this.image, this.handles);
135     }
136
137     /**
138      * @param {MouseEvent} event
139      */
140     handleMouseMove(event) {
141         const originalBounds = this.handleDragStartInfo.bounds;
142
143         // Calculate change in x & y, flip amounts depending on handle
144         let xChange = event.screenX - this.handleDragStartInfo.x;
145         if (this.handleDragStartInfo.handleX === 'left') {
146             xChange = -xChange;
147         }
148         let yChange = event.screenY - this.handleDragStartInfo.y;
149         if (this.handleDragStartInfo.handleY === 'top') {
150             yChange = -yChange;
151         }
152
153         // Prevent images going too small or into negative bounds
154         if (originalBounds.width + xChange < 10) {
155             xChange = -originalBounds.width + 10;
156         }
157         if (originalBounds.height + yChange < 10) {
158             yChange = -originalBounds.height + 10;
159         }
160
161         // Choose the larger dimension change and align the other to keep
162         // image aspect ratio, aligning growth/reduction direction
163         if (Math.abs(xChange) > Math.abs(yChange)) {
164             yChange = Math.floor(xChange * this.handleDragStartInfo.ratio);
165             if (yChange * xChange < 0) {
166                 yChange = -yChange;
167             }
168         } else {
169             xChange = Math.floor(yChange / this.handleDragStartInfo.ratio);
170             if (xChange * yChange < 0) {
171                 xChange = -xChange;
172             }
173         }
174
175         // Calculate our new sizes
176         const newWidth = originalBounds.width + xChange;
177         const newHeight = originalBounds.height + yChange;
178
179         // Apply the sizes and positioning to our ghost dummy
180         this.dragDummy.style.width = `${newWidth}px`;
181         if (this.handleDragStartInfo.handleX === 'left') {
182             this.dragDummy.style.left = `${-xChange}px`;
183         }
184         this.dragDummy.style.height = `${newHeight}px`;
185         if (this.handleDragStartInfo.handleY === 'top') {
186             this.dragDummy.style.top = `${-yChange}px`;
187         }
188
189         // Update corners and track dimension changes for later application
190         positionHandlesAtCorners(this.dragDummy, this.handles);
191         this.handleDragMoveDimensions = {
192             width: newWidth,
193             height: newHeight,
194         }
195     }
196 }
197
198 export default ImageView;