diff --git a/src/lib/extensions/image.ts b/src/lib/extensions/image.ts
index 1a0f774..95ca800 100644
--- a/src/lib/extensions/image.ts
+++ b/src/lib/extensions/image.ts
@@ -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 = '';
+ 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 {
diff --git a/src/lib/extensions/video.ts b/src/lib/extensions/video.ts
index d50c814..bd3fb4b 100644
--- a/src/lib/extensions/video.ts
+++ b/src/lib/extensions/video.ts
@@ -27,6 +27,14 @@ export const Video = Node.create({
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({
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({
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 = '';
+ 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({
overlay.appendChild(deleteBtn);
wrapper.appendChild(video);
+ wrapper.appendChild(resizer);
wrapper.appendChild(overlay);
return {