mui-tiptap
Version:
A Material-UI (MUI) styled WYSIWYG rich text editor, using Tiptap
139 lines (138 loc) • 8.54 kB
JavaScript
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;
;