image resize
This commit is contained in:
+104
-1
@@ -13,6 +13,14 @@ export const CustomImage = Image.extend({
|
|||||||
src: {
|
src: {
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
|
width: {
|
||||||
|
default: null,
|
||||||
|
parseHTML: element => element.getAttribute('width'),
|
||||||
|
renderHTML: attributes => {
|
||||||
|
if (!attributes.width) return {};
|
||||||
|
return { width: attributes.width };
|
||||||
|
},
|
||||||
|
},
|
||||||
filename: {
|
filename: {
|
||||||
default: null,
|
default: null,
|
||||||
parseHTML: element => element.getAttribute('data-filename'),
|
parseHTML: element => element.getAttribute('data-filename'),
|
||||||
@@ -39,10 +47,17 @@ export const CustomImage = Image.extend({
|
|||||||
return ({ node, getPos, editor }) => {
|
return ({ node, getPos, editor }) => {
|
||||||
const wrapper = document.createElement('div');
|
const wrapper = document.createElement('div');
|
||||||
// Give it position: relative and flex/inline-block to wrap the image tightly
|
// Give it position: relative and flex/inline-block to wrap the image tightly
|
||||||
wrapper.classList.add('image-wrapper', 'relative', 'inline-block', 'my-4', 'group', 'w-fit', 'max-w-full');
|
wrapper.classList.add('image-wrapper', 'relative', 'inline-block', 'my-4', 'group', 'max-w-full');
|
||||||
wrapper.style.display = 'block'; // Block level to not mess up flow
|
wrapper.style.display = 'block'; // Block level to not mess up flow
|
||||||
wrapper.style.marginInline = 'auto'; // Center if desired, or let it flow
|
wrapper.style.marginInline = 'auto'; // Center if desired, or let it flow
|
||||||
|
|
||||||
|
if (node.attrs.width) {
|
||||||
|
const w = typeof node.attrs.width === 'number' ? `${node.attrs.width}px` : node.attrs.width;
|
||||||
|
wrapper.style.width = w;
|
||||||
|
} else {
|
||||||
|
wrapper.style.width = 'fit-content';
|
||||||
|
}
|
||||||
|
|
||||||
const img = document.createElement('img');
|
const img = document.createElement('img');
|
||||||
Object.entries(node.attrs).forEach(([key, value]) => {
|
Object.entries(node.attrs).forEach(([key, value]) => {
|
||||||
if (value !== null && value !== undefined) {
|
if (value !== null && value !== undefined) {
|
||||||
@@ -54,6 +69,93 @@ export const CustomImage = Image.extend({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
img.classList.add('rounded-lg', 'border', 'border-border', 'max-w-full', 'h-auto', 'cursor-default');
|
img.classList.add('rounded-lg', 'border', 'border-border', 'max-w-full', 'h-auto', 'cursor-default');
|
||||||
|
img.style.width = '100%';
|
||||||
|
|
||||||
|
// Resize handle
|
||||||
|
const resizer = document.createElement('div');
|
||||||
|
// Adding style.touchAction = 'none' to prevent mobile browser scrolling when dragging
|
||||||
|
resizer.style.touchAction = 'none';
|
||||||
|
resizer.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-white"><path d="m21 21-6-6m6 6v-4.8m0 4.8h-4.8M3 16.2V21m0 0h4.8M3 21l6-6M21 7.8V3m0 0h-4.8M21 3l-6 6M3 7.8V3m0 0h4.8M3 3l6 6"/></svg>';
|
||||||
|
resizer.classList.add('absolute', 'bottom-2', 'right-2', 'w-6', 'h-6', 'bg-black/50', 'hover:bg-black/80', 'backdrop-blur-sm', 'cursor-nwse-resize', 'transition-opacity', 'z-20', 'rounded-md', 'flex', 'items-center', 'justify-center', 'shadow-sm');
|
||||||
|
resizer.contentEditable = 'false';
|
||||||
|
|
||||||
|
let isResizing = false;
|
||||||
|
let startX = 0;
|
||||||
|
let startWidth = 0;
|
||||||
|
|
||||||
|
const initResize = (clientX: number) => {
|
||||||
|
isResizing = true;
|
||||||
|
startX = clientX;
|
||||||
|
startWidth = wrapper.offsetWidth;
|
||||||
|
};
|
||||||
|
|
||||||
|
const doResize = (clientX: number, minW: number) => {
|
||||||
|
if (!isResizing) return;
|
||||||
|
const dx = clientX - startX;
|
||||||
|
const newWidth = Math.max(minW, startWidth + dx);
|
||||||
|
wrapper.style.width = `${newWidth}px`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopResize = () => {
|
||||||
|
if (!isResizing) return;
|
||||||
|
isResizing = false;
|
||||||
|
if (typeof getPos === 'function') {
|
||||||
|
const pos = getPos();
|
||||||
|
if (typeof pos === 'number') {
|
||||||
|
editor.commands.command(({ tr }) => {
|
||||||
|
tr.setNodeMarkup(pos, undefined, {
|
||||||
|
...node.attrs,
|
||||||
|
width: wrapper.offsetWidth,
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
resizer.addEventListener('mousedown', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
initResize(e.clientX);
|
||||||
|
|
||||||
|
const onMouseMove = (ev: MouseEvent) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
doResize(ev.clientX, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMouseUp = (ev: MouseEvent) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
stopResize();
|
||||||
|
document.removeEventListener('mousemove', onMouseMove);
|
||||||
|
document.removeEventListener('mouseup', onMouseUp);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', onMouseMove);
|
||||||
|
document.addEventListener('mouseup', onMouseUp);
|
||||||
|
});
|
||||||
|
|
||||||
|
resizer.addEventListener('touchstart', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
initResize(e.touches[0].clientX);
|
||||||
|
|
||||||
|
const onTouchMove = (ev: TouchEvent) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
doResize(ev.touches[0].clientX, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onTouchEnd = () => {
|
||||||
|
// touchend doesn't have touches[0] usually
|
||||||
|
stopResize();
|
||||||
|
document.removeEventListener('touchmove', onTouchMove);
|
||||||
|
document.removeEventListener('touchend', onTouchEnd);
|
||||||
|
document.removeEventListener('touchcancel', onTouchEnd);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('touchmove', onTouchMove, { passive: false });
|
||||||
|
document.addEventListener('touchend', onTouchEnd);
|
||||||
|
document.addEventListener('touchcancel', onTouchEnd);
|
||||||
|
}, { passive: false });
|
||||||
|
|
||||||
// Delete button overlay
|
// Delete button overlay
|
||||||
const overlay = document.createElement('div');
|
const overlay = document.createElement('div');
|
||||||
@@ -76,6 +178,7 @@ export const CustomImage = Image.extend({
|
|||||||
|
|
||||||
overlay.appendChild(deleteBtn);
|
overlay.appendChild(deleteBtn);
|
||||||
wrapper.appendChild(img);
|
wrapper.appendChild(img);
|
||||||
|
wrapper.appendChild(resizer);
|
||||||
wrapper.appendChild(overlay);
|
wrapper.appendChild(overlay);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
+105
-1
@@ -27,6 +27,14 @@ export const Video = Node.create<VideoOptions>({
|
|||||||
src: {
|
src: {
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
|
width: {
|
||||||
|
default: null,
|
||||||
|
parseHTML: element => element.getAttribute('width'),
|
||||||
|
renderHTML: attributes => {
|
||||||
|
if (!attributes.width) return {};
|
||||||
|
return { width: attributes.width };
|
||||||
|
},
|
||||||
|
},
|
||||||
filename: {
|
filename: {
|
||||||
default: null,
|
default: null,
|
||||||
parseHTML: element => element.getAttribute('data-filename'),
|
parseHTML: element => element.getAttribute('data-filename'),
|
||||||
@@ -89,8 +97,16 @@ export const Video = Node.create<VideoOptions>({
|
|||||||
addNodeView() {
|
addNodeView() {
|
||||||
return ({ node, getPos, editor }) => {
|
return ({ node, getPos, editor }) => {
|
||||||
const wrapper = document.createElement('div');
|
const wrapper = document.createElement('div');
|
||||||
wrapper.classList.add('video-wrapper', 'relative', 'inline-block', 'my-4', 'group', 'w-full', 'max-w-full');
|
wrapper.classList.add('video-wrapper', 'relative', 'inline-block', 'my-4', 'group', 'max-w-full');
|
||||||
wrapper.style.display = 'block';
|
wrapper.style.display = 'block';
|
||||||
|
wrapper.style.marginInline = 'auto'; // Center if desired
|
||||||
|
|
||||||
|
if (node.attrs.width) {
|
||||||
|
const w = typeof node.attrs.width === 'number' ? `${node.attrs.width}px` : node.attrs.width;
|
||||||
|
wrapper.style.width = w;
|
||||||
|
} else {
|
||||||
|
wrapper.style.width = '100%';
|
||||||
|
}
|
||||||
|
|
||||||
const video = document.createElement('video');
|
const video = document.createElement('video');
|
||||||
Object.entries(node.attrs).forEach(([key, value]) => {
|
Object.entries(node.attrs).forEach(([key, value]) => {
|
||||||
@@ -107,6 +123,93 @@ export const Video = Node.create<VideoOptions>({
|
|||||||
video.setAttribute('controls', 'true');
|
video.setAttribute('controls', 'true');
|
||||||
}
|
}
|
||||||
video.classList.add('w-full', 'rounded-lg', 'border', 'border-border/50', 'shadow-sm', 'overflow-hidden');
|
video.classList.add('w-full', 'rounded-lg', 'border', 'border-border/50', 'shadow-sm', 'overflow-hidden');
|
||||||
|
video.style.width = '100%';
|
||||||
|
|
||||||
|
// Resize handle
|
||||||
|
const resizer = document.createElement('div');
|
||||||
|
// Adding style.touchAction = 'none' to prevent mobile browser scrolling when dragging
|
||||||
|
resizer.style.touchAction = 'none';
|
||||||
|
resizer.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-white"><path d="m21 21-6-6m6 6v-4.8m0 4.8h-4.8M3 16.2V21m0 0h4.8M3 21l6-6M21 7.8V3m0 0h-4.8M21 3l-6 6M3 7.8V3m0 0h4.8M3 3l6 6"/></svg>';
|
||||||
|
resizer.classList.add('absolute', 'bottom-2', 'right-2', 'w-6', 'h-6', 'bg-black/50', 'hover:bg-black/80', 'backdrop-blur-sm', 'cursor-nwse-resize', 'transition-opacity', 'z-20', 'rounded-md', 'flex', 'items-center', 'justify-center', 'shadow-sm');
|
||||||
|
resizer.contentEditable = 'false';
|
||||||
|
|
||||||
|
let isResizing = false;
|
||||||
|
let startX = 0;
|
||||||
|
let startWidth = 0;
|
||||||
|
|
||||||
|
const initResize = (clientX: number) => {
|
||||||
|
isResizing = true;
|
||||||
|
startX = clientX;
|
||||||
|
startWidth = wrapper.offsetWidth;
|
||||||
|
};
|
||||||
|
|
||||||
|
const doResize = (clientX: number, minW: number) => {
|
||||||
|
if (!isResizing) return;
|
||||||
|
const dx = clientX - startX;
|
||||||
|
const newWidth = Math.max(minW, startWidth + dx);
|
||||||
|
wrapper.style.width = `${newWidth}px`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopResize = () => {
|
||||||
|
if (!isResizing) return;
|
||||||
|
isResizing = false;
|
||||||
|
if (typeof getPos === 'function') {
|
||||||
|
const pos = getPos();
|
||||||
|
if (typeof pos === 'number') {
|
||||||
|
editor.commands.command(({ tr }) => {
|
||||||
|
tr.setNodeMarkup(pos, undefined, {
|
||||||
|
...node.attrs,
|
||||||
|
width: wrapper.offsetWidth,
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
resizer.addEventListener('mousedown', (e) => {
|
||||||
|
// Prevent event from dragging the whole block
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
initResize(e.clientX);
|
||||||
|
|
||||||
|
const onMouseMove = (ev: MouseEvent) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
doResize(ev.clientX, 200);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMouseUp = (ev: MouseEvent) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
stopResize();
|
||||||
|
document.removeEventListener('mousemove', onMouseMove);
|
||||||
|
document.removeEventListener('mouseup', onMouseUp);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', onMouseMove);
|
||||||
|
document.addEventListener('mouseup', onMouseUp);
|
||||||
|
});
|
||||||
|
|
||||||
|
resizer.addEventListener('touchstart', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
initResize(e.touches[0].clientX);
|
||||||
|
|
||||||
|
const onTouchMove = (ev: TouchEvent) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
doResize(ev.touches[0].clientX, 200);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onTouchEnd = () => {
|
||||||
|
stopResize();
|
||||||
|
document.removeEventListener('touchmove', onTouchMove);
|
||||||
|
document.removeEventListener('touchend', onTouchEnd);
|
||||||
|
document.removeEventListener('touchcancel', onTouchEnd);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('touchmove', onTouchMove, { passive: false });
|
||||||
|
document.addEventListener('touchend', onTouchEnd);
|
||||||
|
document.addEventListener('touchcancel', onTouchEnd);
|
||||||
|
}, { passive: false });
|
||||||
|
|
||||||
// Delete button overlay
|
// Delete button overlay
|
||||||
const overlay = document.createElement('div');
|
const overlay = document.createElement('div');
|
||||||
@@ -129,6 +232,7 @@ export const Video = Node.create<VideoOptions>({
|
|||||||
|
|
||||||
overlay.appendChild(deleteBtn);
|
overlay.appendChild(deleteBtn);
|
||||||
wrapper.appendChild(video);
|
wrapper.appendChild(video);
|
||||||
|
wrapper.appendChild(resizer);
|
||||||
wrapper.appendChild(overlay);
|
wrapper.appendChild(overlay);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user