image resize

This commit is contained in:
2026-03-17 18:36:10 -05:00
parent 5f7f3995f8
commit 94fce5652a
2 changed files with 209 additions and 2 deletions
+104 -1
View File
@@ -13,6 +13,14 @@ export const CustomImage = Image.extend({
src: {
default: null,
},
width: {
default: null,
parseHTML: element => element.getAttribute('width'),
renderHTML: attributes => {
if (!attributes.width) return {};
return { width: attributes.width };
},
},
filename: {
default: null,
parseHTML: element => element.getAttribute('data-filename'),
@@ -39,9 +47,16 @@ export const CustomImage = Image.extend({
return ({ node, getPos, editor }) => {
const wrapper = document.createElement('div');
// 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.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');
Object.entries(node.attrs).forEach(([key, value]) => {
@@ -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.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
const overlay = document.createElement('div');
@@ -76,6 +178,7 @@ export const CustomImage = Image.extend({
overlay.appendChild(deleteBtn);
wrapper.appendChild(img);
wrapper.appendChild(resizer);
wrapper.appendChild(overlay);
return {
+105 -1
View File
@@ -27,6 +27,14 @@ export const Video = Node.create<VideoOptions>({
src: {
default: null,
},
width: {
default: null,
parseHTML: element => element.getAttribute('width'),
renderHTML: attributes => {
if (!attributes.width) return {};
return { width: attributes.width };
},
},
filename: {
default: null,
parseHTML: element => element.getAttribute('data-filename'),
@@ -89,8 +97,16 @@ export const Video = Node.create<VideoOptions>({
addNodeView() {
return ({ node, getPos, editor }) => {
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.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');
Object.entries(node.attrs).forEach(([key, value]) => {
@@ -107,6 +123,93 @@ export const Video = Node.create<VideoOptions>({
video.setAttribute('controls', 'true');
}
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
const overlay = document.createElement('div');
@@ -129,6 +232,7 @@ export const Video = Node.create<VideoOptions>({
overlay.appendChild(deleteBtn);
wrapper.appendChild(video);
wrapper.appendChild(resizer);
wrapper.appendChild(overlay);
return {