Added image resizing via drag handles

This commit is contained in:
Dan Brown 2022-01-17 17:43:16 +00:00
parent 7622106665
commit 7125530e55
No known key found for this signature in database
GPG key ID: 46D9F943C24A2EF9
8 changed files with 324 additions and 6 deletions

7
TODO
View file

@ -1,10 +1,15 @@
### Next
//
### In-Progress ### In-Progress
//
### Features ### Features
- Tables - Tables
- Images - Images
- Image Resizing in editor
- Drawings - Drawings
- LTR/RTL control - LTR/RTL control
- Fullscreen - Fullscreen

View file

@ -6,6 +6,7 @@ import {DOMParser, DOMSerializer} from "prosemirror-model";
import schema from "./schema"; import schema from "./schema";
import menu from "./menu"; import menu from "./menu";
import nodeViews from "./node-views";
class ProseMirrorView { class ProseMirrorView {
constructor(target, content) { constructor(target, content) {
@ -21,7 +22,8 @@ class ProseMirrorView {
...exampleSetup({schema, menuBar: false}), ...exampleSetup({schema, menuBar: false}),
menu, menu,
] ]
}) }),
nodeViews,
}); });
} }

View file

@ -92,7 +92,7 @@ function writeNodeAsHtml(state, node) {
// formatting or content. // formatting or content.
for (const [nodeType, serializerFunction] of Object.entries(nodes)) { for (const [nodeType, serializerFunction] of Object.entries(nodes)) {
nodes[nodeType] = function (state, node, parent, index) { nodes[nodeType] = function (state, node, parent, index) {
if (node.attrs.align) { if (node.attrs.align || node.attrs.height || node.attrs.width) {
writeNodeAsHtml(state, node); writeNodeAsHtml(state, node);
} else { } else {
serializerFunction(state, node, parent, index); serializerFunction(state, node, parent, index);

View file

@ -0,0 +1,198 @@
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;

View file

@ -0,0 +1,7 @@
import ImageView from "./ImageView";
const views = {
image: (node, view, getPos) => new ImageView(node, view, getPos),
};
export default views;

View file

@ -0,0 +1,58 @@
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';
}
}

View file

@ -139,7 +139,9 @@ const image = {
attrs: { attrs: {
src: {}, src: {},
alt: {default: null}, alt: {default: null},
title: {default: null} title: {default: null},
height: {default: null},
width: {default: null},
}, },
group: "inline", group: "inline",
draggable: true, draggable: true,
@ -148,7 +150,9 @@ const image = {
return { return {
src: dom.getAttribute("src"), src: dom.getAttribute("src"),
title: dom.getAttribute("title"), title: dom.getAttribute("title"),
alt: dom.getAttribute("alt") alt: dom.getAttribute("alt"),
height: dom.getAttribute("height"),
width: dom.getAttribute("width"),
} }
} }
}], }],
@ -157,7 +161,9 @@ const image = {
const src = ref.src; const src = ref.src;
const alt = ref.alt; const alt = ref.alt;
const title = ref.title; const title = ref.title;
return ["img", {src: src, alt: alt, title: title}] const width = ref.width;
const height = ref.height;
return ["img", {src, alt, title, width, height}]
} }
}; };

View file

@ -453,3 +453,45 @@ img.ProseMirror-separator {
margin: 0 $-s; margin: 0 $-s;
} }
} }
.ProseMirror-imagewrap {
display: inline-block;
line-height: 0;
font-size: 0;
position: relative;
}
.ProseMirror-imagewrap.ProseMirror-selectednode {
outline: 0;
}
.ProseMirror img[data-show-handles] {
outline: 4px solid #000;
}
.ProseMirror-dragdummy {
position: absolute;
z-index: 2;
left: 0;
top: 0;
max-width: none !important;
max-height: none !important;
}
.ProseMirror-grabhandle {
width: 12px;
height: 12px;
border: 2px solid #000;
z-index: 4;
position: absolute;
background-color: #FFF;
}
.ProseMirror-grabhandle-left-top {
cursor: nw-resize;
}
.ProseMirror-grabhandle-right-top {
cursor: ne-resize;
}
.ProseMirror-grabhandle-right-bottom {
cursor: se-resize;
}
.ProseMirror-grabhandle-left-bottom {
cursor: sw-resize;
}