UNPKG

@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
// 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 };