Added image resizing via drag handles
This commit is contained in:
parent
7622106665
commit
7125530e55
8 changed files with 324 additions and 6 deletions
7
TODO
7
TODO
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
198
resources/js/editor/node-views/ImageView.js
Normal file
198
resources/js/editor/node-views/ImageView.js
Normal 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;
|
7
resources/js/editor/node-views/index.js
Normal file
7
resources/js/editor/node-views/index.js
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import ImageView from "./ImageView";
|
||||||
|
|
||||||
|
const views = {
|
||||||
|
image: (node, view, getPos) => new ImageView(node, view, getPos),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default views;
|
58
resources/js/editor/node-views/node-view-utils.js
Normal file
58
resources/js/editor/node-views/node-view-utils.js
Normal 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';
|
||||||
|
}
|
||||||
|
}
|
|
@ -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}]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
Loading…
Reference in a new issue