@harshtalks/image-tiptap
Version:
A Tiptap extension for React.js to add image resize and alignment options to the image extension.
252 lines (244 loc) • 6.87 kB
JavaScript
// src/components/image-aligner.tsx
import {
BubbleMenu,
useCurrentEditor as useCurrentEditor2
} from "@tiptap/react";
import * as React2 from "react";
// src/constants.ts
var DATA_ALIGNMENT_KEY = "data-alignment";
var IMAGE_NODE = "image";
var PROSE_ACTIVE_NODE = ".ProseMirror-selectednode";
// src/components/image-aligner.tsx
import { Slot } from "@radix-ui/react-slot";
// src/components/image-resize.tsx
import "@tiptap/react";
import Moveable from "react-moveable";
import * as React from "react";
var ImageResizer = ({
editor,
onResize,
onResizeEnd,
onScale,
...moveableProps
}) => {
if (!editor) {
return null;
}
if (!editor.isActive(IMAGE_NODE)) {
return null;
}
const updateImageSize = () => {
const imageNodeInfo = document.querySelector(PROSE_ACTIVE_NODE);
if (imageNodeInfo) {
const selection = editor.state.selection;
const setImage = editor.commands.setImage;
const width = Number(imageNodeInfo.style.width.replace("px", ""));
const height = Number(imageNodeInfo.style.height.replace("px", ""));
setImage({
src: imageNodeInfo.src,
[DATA_ALIGNMENT_KEY]: imageNodeInfo.getAttribute(
DATA_ALIGNMENT_KEY
),
...imageNodeInfo.alt && { alt: imageNodeInfo.alt },
...imageNodeInfo.title && { title: imageNodeInfo.title },
width,
height
});
editor.commands.setNodeSelection(selection.from);
}
};
return /* @__PURE__ */ React.createElement(
Moveable,
{
target: document.querySelector(PROSE_ACTIVE_NODE),
container: null,
origin: false,
edge: false,
throttleDrag: 0,
keepRatio: true,
resizable: true,
throttleResize: 0,
onResize: (e) => {
const {
target,
width,
height,
// dist,
delta
} = e;
if (delta[0]) target.style.width = `${width}px`;
if (delta[1]) target.style.height = `${height}px`;
onResize?.(e);
},
onResizeEnd: (e) => {
updateImageSize();
onResizeEnd?.(e);
},
scalable: true,
throttleScale: 0,
renderDirections: ["w", "e", "s", "n"],
onScale: (e) => {
const {
target,
// scale,
// dist,
// delta,
transform
} = e;
target.style.transform = transform;
onScale?.(e);
},
...moveableProps
}
);
};
var image_resize_default = ImageResizer;
// src/components/image-aligner.tsx
var alignerContext = React2.createContext(null);
var Root = ({
children,
editor: propEditor,
...resizerProps
}) => {
const { editor: editorContext } = useCurrentEditor2() || null;
const editor = propEditor || editorContext;
return /* @__PURE__ */ React2.createElement(alignerContext.Provider, { value: editor }, children, /* @__PURE__ */ React2.createElement(image_resize_default, { editor, ...resizerProps }));
};
var AlignMenu = ({
shouldShow,
children,
...props
}) => {
const editor = React2.useContext(alignerContext);
if (!editor) {
return null;
}
return /* @__PURE__ */ React2.createElement(
BubbleMenu,
{
editor,
...props,
shouldShow: (args) => {
const { editor: editor2 } = args;
return editor2.isActive("image") && (shouldShow?.(args) || true);
}
},
children
);
};
var Items = React2.forwardRef(
(props, ref) => {
return /* @__PURE__ */ React2.createElement("div", { ref, ...props });
}
);
Items.displayName = "ImageAligner.Items";
var Item = React2.forwardRef(
({ onClick, alignment, asChild, ...props }, ref) => {
const editor = React2.useContext(alignerContext);
const Component = asChild ? Slot : "button";
if (!editor) {
return null;
}
const getImageInfo = () => {
if (editor.isActive(IMAGE_NODE)) {
const imageNodeInfo = document.querySelector(PROSE_ACTIVE_NODE);
return imageNodeInfo;
}
return null;
};
const isItemActive = () => {
const imageNodeInfo = getImageInfo();
if (imageNodeInfo) {
const imageAlignment = imageNodeInfo.getAttribute(DATA_ALIGNMENT_KEY);
if (imageAlignment === alignment) {
return true;
} else {
return false;
}
}
return false;
};
const updateImageAlignment = () => {
const imageNodeInfo = getImageInfo();
if (imageNodeInfo) {
const selection = editor.state.selection;
const setImage = editor.commands.setImage;
setImage({
src: imageNodeInfo.src,
...imageNodeInfo.alt && { alt: imageNodeInfo.alt },
...imageNodeInfo.title && { title: imageNodeInfo.title },
...imageNodeInfo.width && { width: imageNodeInfo.width },
...imageNodeInfo.height && { height: imageNodeInfo.height },
[DATA_ALIGNMENT_KEY]: alignment
});
editor.commands.setNodeSelection(selection.from);
}
};
return /* @__PURE__ */ React2.createElement(
Component,
{
ref,
...props,
onClick: (e) => {
updateImageAlignment();
if (onClick) {
onClick(e);
}
},
"data-active": isItemActive() ? "true" : null,
"data-alignment": alignment
}
);
}
);
Item.displayName = "ImageAligner.Item";
var ImageAligner = {
Root,
AlignMenu,
Items,
Item
};
var image_aligner_default = ImageAligner;
// src/extensions/image-extension.ts
import ImageExtensionBase from "@tiptap/extension-image";
// src/utils.ts
var alignmentVariants = {
center: `margin: 0 auto;`,
left: `margin-right: auto; margin-top: 0;`,
right: `margin-left: auto; margin-top: 0;`
};
// src/extensions/image-extension.ts
var ImageExtension = ImageExtensionBase.extend({
// Adding name to the extension
name: IMAGE_NODE,
// we will extend the base image extension to include few custom attirbutes
// this custom attributes will appear in the DOM for the given image.
addAttributes() {
return {
...this.parent?.(),
// Adding custom attributes
// Custom Attribute for height
height: {
default: null
},
// Custom Attribute for width
width: {
default: null
},
// this will be used to determine current alignment of the image
[DATA_ALIGNMENT_KEY]: {
default: "center",
// use current alignment to set the style attribute
renderHTML: (attributes) => ({
[DATA_ALIGNMENT_KEY]: attributes[DATA_ALIGNMENT_KEY],
style: alignmentVariants[attributes[DATA_ALIGNMENT_KEY]]
})
}
};
}
});
var image_extension_default = ImageExtension;
export {
image_aligner_default as ImageAligner,
image_extension_default as ImageExtension
};