UNPKG

mui-tiptap

Version:

A Material-UI (MUI) styled WYSIWYG rich text editor, using Tiptap

139 lines (138 loc) 8.54 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const jsx_runtime_1 = require("react/jsx-runtime"); const react_1 = require("@tiptap/react"); const throttle_1 = __importDefault(require("lodash/throttle")); const react_2 = require("react"); const mui_1 = require("tss-react/mui"); const ResizableImageResizer_1 = require("./ResizableImageResizer"); const IMAGE_MINIMUM_WIDTH_PIXELS = 15; const useStyles = (0, mui_1.makeStyles)({ name: { ResizableImageComponent } })((theme) => ({ imageContainer: { // Use inline-flex so that the container is only as big as the inner img display: "inline-flex", // Use relative position so that the resizer is positioned relative to // the img dimensions (via their common container) position: "relative", }, image: { // We need display:block in order for the container element to be // sized properly (no extra space below the image) display: "block", }, imageSelected: { // This "selected" state outline style is copied from our standard editor // styles (which are kept there as well so they appear even if not using our // custom resizable image). outline: `3px solid ${theme.palette.primary.main}`, }, resizer: { // As described here https://github.com/ueberdosis/tiptap/issues/3775, // updates to editor isEditable do not trigger re-rendering of node views. // Even editor state changes external to a given ReactNodeView component // will not trigger re-render (which is probably a good thing most of the // time, in terms of performance). As such, we always render the resizer // component with React (and so in the DOM), but hide it with CSS when the // editor is not editable. This also means its mouse event listeners will // also not fire, as intended. '.ProseMirror[contenteditable="false"] &': { display: "none", }, }, })); function ResizableImageComponent(props) { var _a; const { node, selected, updateAttributes, extension } = props; const { classes, cx } = useStyles(); const { attrs } = node; const imageRef = (0, react_2.useRef)(null); // We store the mouse-down state of the ResizableImageResizer here to properly // control the resizer visibility when `inline` option is enabled. Tiptap // seems to change the selected state of the node to `false` as soon as the // user drags on the resize handle if the extension has `inline: true`, so we // need to consider the resizing state here in order to ensure it's still // shown during a resize. See // https://github.com/sjdemartini/mui-tiptap/issues/211 const [resizerMouseDown, setResizerMouseDown] = (0, react_2.useState)(false); const selectedOrResizing = selected || resizerMouseDown; const handleResize = (0, react_2.useMemo)(() => // Throttle our "on resize" handler, since the event fires very rapidly during // dragging, so rendering would end up stuttering a bit without a throttle (0, throttle_1.default)((event) => { if (!imageRef.current) { return; } const originalBoundingRect = imageRef.current.getBoundingClientRect(); // Get the "width" and "height" of the resized image based on the user's // cursor position after movement, if we were to imagine a box drawn from // the top left corner of the image to their cursor. (clientX/Y and // getBoundingClientRect both reference positions relative to the viewport, // allowing us to use them to calculate the new "resized" image dimensions.) const resizedWidth = event.clientX - originalBoundingRect.x; const resizedHeight = event.clientY - originalBoundingRect.y; // We always preserve the original image aspect ratio, setting only the // `width` to a specific number upon resize (and leaving the `height` of the // `img` as "auto"). So to determine the new width, we'll take the larger of // (a) the new resized width after the user's latest drag resize movement, // (b) the width proportional to the new resized height given the image // aspect ratio, or (c) a minimum width to prevent mistakes. This is similar // to what Google Docs image resizing appears to be doing, which feels // intuitive. const resultantWidth = Math.max(resizedWidth, (originalBoundingRect.width / originalBoundingRect.height) * resizedHeight, // Set a minimum width, since any smaller is probably a mistake, and we // don't want images to get mistakenly shrunken below a size which makes // it hard to later select/resize the image IMAGE_MINIMUM_WIDTH_PIXELS); updateAttributes({ width: Math.round(resultantWidth), }); }, 50, { trailing: true }), [updateAttributes]); const ChildComponent = extension.options.ChildComponent; return ((0, jsx_runtime_1.jsx)(react_1.NodeViewWrapper, { style: { // Handle @tiptap/extension-text-align. Ideally we'd be able to inherit // this style from TextAlign's GlobalAttributes directly, but those are // only applied via `renderHTML` and not the `NodeView` renderer // (https://github.com/ueberdosis/tiptap/blob/6c34dec33ac39c9f037a0a72e4525f3fc6d422bf/packages/extension-text-align/src/text-align.ts#L43-L49), // so we have to do this manually/redundantly here. textAlign: attrs.textAlign, width: "100%", }, // Change the outer component's component to a "span" if the `inline` // extension option is enabled, to ensure it can appear alongside other // inline elements like text. as: extension.options.inline ? "span" : "div", children: (0, jsx_runtime_1.jsxs)("div", { className: classes.imageContainer, children: [(0, jsx_runtime_1.jsx)("img", { ref: imageRef, src: attrs.src, height: "auto", width: attrs.width ? attrs.width : undefined, alt: attrs.alt || undefined, title: attrs.title || undefined, className: cx(classes.image, // For consistency with the standard Image extension selection // class/UI: selectedOrResizing && "ProseMirror-selectednode", // We'll only show the outline when the editor content is selected selectedOrResizing && classes.imageSelected), style: { // If no width has been specified, we use auto max-width maxWidth: attrs.width ? undefined : "auto", // Always specify the aspect-ratio if it's been defined, to improve // initial render (so auto-height works before the image loads) aspectRatio: (_a = attrs.aspectRatio) !== null && _a !== void 0 ? _a : undefined, }, "data-drag-handle": true, // When the image loads, we'll update our width and aspect-ratio based // on the image's natural size, if they're not set. That way, all future // renders will know the image width/height prior to load/render, // preventing flashing onLoad: (event) => { const newAttributes = {}; if (!attrs.width) { newAttributes.width = event.currentTarget.naturalWidth; } if (!attrs.aspectRatio) { newAttributes.aspectRatio = String(event.currentTarget.naturalWidth / event.currentTarget.naturalHeight); } if (newAttributes.width || newAttributes.aspectRatio) { updateAttributes(newAttributes); } } }), selectedOrResizing && ((0, jsx_runtime_1.jsx)(ResizableImageResizer_1.ResizableImageResizer, { onResize: handleResize, className: classes.resizer, mouseDown: resizerMouseDown, setMouseDown: setResizerMouseDown })), ChildComponent && (0, jsx_runtime_1.jsx)(ChildComponent, Object.assign({}, props))] }) })); } exports.default = ResizableImageComponent;