--- /dev/null
+import {positionHandlesAtCorners, removeHandles, renderHandlesAtCorners} from "./node-view-utils";
+import {NodeSelection} from "prosemirror-state";
+
+class ImageView {
+ /**
+ * @param {PmNode} node
+ * @param {PmView} view
+ * @param {(function(): number)} getPos
+ */
+ constructor(node, view, getPos) {
+ this.dom = document.createElement('div');
+ this.dom.classList.add('ProseMirror-imagewrap');
+
+ this.image = document.createElement("img");
+ this.image.src = node.attrs.src;
+ this.image.alt = node.attrs.alt;
+ if (node.attrs.width) {
+ this.image.width = node.attrs.width;
+ }
+ if (node.attrs.height) {
+ this.image.height = node.attrs.height;
+ }
+
+ this.dom.appendChild(this.image);
+
+ this.handles = [];
+ this.handleDragStartInfo = null;
+ this.handleDragMoveDimensions = null;
+ this.removeHandlesListener = this.removeHandlesListener.bind(this);
+ this.handleMouseMove = this.handleMouseMove.bind(this);
+ this.handleMouseUp = this.handleMouseUp.bind(this);
+ this.handleMouseDown = this.handleMouseDown.bind(this);
+
+ this.dom.addEventListener("click", event => {
+ this.showHandles();
+ });
+
+ // Show handles if selected
+ if (view.state.selection.node === node) {
+ window.setTimeout(() => {
+ this.showHandles();
+ }, 10);
+ }
+
+ this.updateImageDimensions = function (width, height) {
+ const attrs = Object.assign({}, node.attrs, {width, height});
+ let tr = view.state.tr;
+ const position = getPos();
+ tr = tr.setNodeMarkup(position, null, attrs)
+ tr = tr.setSelection(NodeSelection.create(tr.doc, position));
+ view.dispatch(tr);
+ };
+
+ }
+
+ showHandles() {
+ if (this.handles.length === 0) {
+ this.image.dataset.showHandles = 'true';
+ window.addEventListener('click', this.removeHandlesListener);
+ this.handles = renderHandlesAtCorners(this.image);
+ for (const handle of this.handles) {
+ handle.addEventListener('mousedown', this.handleMouseDown);
+ }
+ }
+ }
+
+ removeHandlesListener(event) {
+ console.log(this.dom.contains(event.target), event.target);
+ if (!this.dom.contains(event.target)) {
+ this.removeHandles();
+ this.handles = [];
+ }
+ }
+
+ removeHandles() {
+ removeHandles(this.handles);
+ window.removeEventListener('click', this.removeHandlesListener);
+ delete this.image.dataset.showHandles;
+ }
+
+ stopEvent() {
+ return false;
+ }
+
+ /**
+ * @param {MouseEvent} event
+ */
+ handleMouseDown(event) {
+ event.preventDefault();
+
+ const imageBounds = this.image.getBoundingClientRect();
+ const handle = event.target;
+ this.handleDragStartInfo = {
+ x: event.screenX,
+ y: event.screenY,
+ ratio: imageBounds.width / imageBounds.height,
+ bounds: imageBounds,
+ handleX: handle.dataset.x,
+ handleY: handle.dataset.y,
+ };
+
+ this.createDragDummy(imageBounds);
+ this.dom.appendChild(this.dragDummy);
+
+ window.addEventListener('mousemove', this.handleMouseMove);
+ window.addEventListener('mouseup', this.handleMouseUp);
+ }
+
+ /**
+ * @param {DOMRect} bounds
+ */
+ createDragDummy(bounds) {
+ this.dragDummy = this.image.cloneNode();
+ this.dragDummy.style.opacity = '0.5';
+ this.dragDummy.classList.add('ProseMirror-dragdummy');
+ this.dragDummy.style.width = bounds.width + 'px';
+ this.dragDummy.style.height = bounds.height + 'px';
+ }
+
+ /**
+ * @param {MouseEvent} event
+ */
+ handleMouseUp(event) {
+ if (this.handleDragMoveDimensions) {
+ const {width, height} = this.handleDragMoveDimensions;
+ this.updateImageDimensions(String(width), String(height));
+ }
+
+ window.removeEventListener('mousemove', this.handleMouseMove);
+ window.removeEventListener('mouseup', this.handleMouseUp);
+ this.handleDragStartInfo = null;
+ this.handleDragMoveDimensions = null;
+ this.dragDummy.remove();
+ positionHandlesAtCorners(this.image, this.handles);
+ }
+
+ /**
+ * @param {MouseEvent} event
+ */
+ handleMouseMove(event) {
+ const originalBounds = this.handleDragStartInfo.bounds;
+
+ // Calculate change in x & y, flip amounts depending on handle
+ let xChange = event.screenX - this.handleDragStartInfo.x;
+ if (this.handleDragStartInfo.handleX === 'left') {
+ xChange = -xChange;
+ }
+ let yChange = event.screenY - this.handleDragStartInfo.y;
+ if (this.handleDragStartInfo.handleY === 'top') {
+ yChange = -yChange;
+ }
+
+ // Prevent images going too small or into negative bounds
+ if (originalBounds.width + xChange < 10) {
+ xChange = -originalBounds.width + 10;
+ }
+ if (originalBounds.height + yChange < 10) {
+ yChange = -originalBounds.height + 10;
+ }
+
+ // Choose the larger dimension change and align the other to keep
+ // image aspect ratio, aligning growth/reduction direction
+ if (Math.abs(xChange) > Math.abs(yChange)) {
+ yChange = Math.floor(xChange * this.handleDragStartInfo.ratio);
+ if (yChange * xChange < 0) {
+ yChange = -yChange;
+ }
+ } else {
+ xChange = Math.floor(yChange / this.handleDragStartInfo.ratio);
+ if (xChange * yChange < 0) {
+ xChange = -xChange;
+ }
+ }
+
+ // Calculate our new sizes
+ const newWidth = originalBounds.width + xChange;
+ const newHeight = originalBounds.height + yChange;
+
+ // Apply the sizes and positioning to our ghost dummy
+ this.dragDummy.style.width = `${newWidth}px`;
+ if (this.handleDragStartInfo.handleX === 'left') {
+ this.dragDummy.style.left = `${-xChange}px`;
+ }
+ this.dragDummy.style.height = `${newHeight}px`;
+ if (this.handleDragStartInfo.handleY === 'top') {
+ this.dragDummy.style.top = `${-yChange}px`;
+ }
+
+ // Update corners and track dimension changes for later application
+ positionHandlesAtCorners(this.dragDummy, this.handles);
+ this.handleDragMoveDimensions = {
+ width: newWidth,
+ height: newHeight,
+ }
+ }
+}
+
+export default ImageView;
\ No newline at end of file
--- /dev/null
+import crel from "crelt";
+
+/**
+ * Render grab handles at the corners of the given element.
+ * @param {Element} elem
+ * @return {Element[]}
+ */
+export function renderHandlesAtCorners(elem) {
+ const handles = [];
+ const baseClass = 'ProseMirror-grabhandle';
+
+ for (let i = 0; i < 4; i++) {
+ const y = (i < 2) ? 'top' : 'bottom';
+ const x = (i === 0 || i === 3) ? 'left' : 'right';
+ const handle = crel('div', {
+ class: `${baseClass} ${baseClass}-${x}-${y}`,
+ });
+ handle.dataset.y = y;
+ handle.dataset.x = x;
+ handles.push(handle);
+ elem.parentNode.appendChild(handle);
+ }
+
+ positionHandlesAtCorners(elem, handles);
+ return handles;
+}
+
+/**
+ * @param {Element[]} handles
+ */
+export function removeHandles(handles) {
+ for (const handle of handles) {
+ handle.remove();
+ }
+}
+
+/**
+ *
+ * @param {Element} element
+ * @param {[Element, Element, Element, Element]}handles
+ */
+export function positionHandlesAtCorners(element, handles) {
+ const bounds = element.getBoundingClientRect();
+ const parentBounds = element.parentElement.getBoundingClientRect();
+ const positions = [
+ {x: bounds.left - parentBounds.left, y: bounds.top - parentBounds.top},
+ {x: bounds.right - parentBounds.left, y: bounds.top - parentBounds.top},
+ {x: bounds.right - parentBounds.left, y: bounds.bottom - parentBounds.top},
+ {x: bounds.left - parentBounds.left, y: bounds.bottom - parentBounds.top},
+ ];
+
+ for (let i = 0; i < 4; i++) {
+ const {x, y} = positions[i];
+ const handle = handles[i];
+ handle.style.left = (x - 6) + 'px';
+ handle.style.top = (y - 6) + 'px';
+ }
+}
\ No newline at end of file