mui-tiptap
Version:
A Material-UI (MUI) styled WYSIWYG rich text editor, using Tiptap
135 lines (134 loc) • 7.83 kB
JavaScript
var __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
Object.defineProperty(exports, "__esModule", { value: true });
const jsx_runtime_1 = require("react/jsx-runtime");
const material_1 = require("@mui/material");
const core_1 = require("@tiptap/core");
const react_1 = require("react");
const mui_1 = require("tss-react/mui");
const styles_1 = require("./styles");
const controlledBubbleMenuClasses = (0, styles_1.getUtilityClasses)("ControlledBubbleMenu", ["root", "paper"]);
const useStyles = (0, mui_1.makeStyles)({ name: { ControlledBubbleMenu } })((theme) => ({
root: {
// Ensure the bubble menu is above modals, in case the editor is rendered in
// a modal, consistent with recommendations here
// https://github.com/mui/material-ui/issues/14216. See
// https://github.com/sjdemartini/mui-tiptap/issues/265.
zIndex: theme.zIndex.tooltip,
},
paper: {
backgroundColor: theme.palette.background.default,
},
}));
// The `BubbleMenu` React component provided by Tiptap in @tiptap/react and the
// underlying BubbleMenuPlugin don't work very well in practice. There are two
// primary problems:
// 1) BubbleMenu places its tippy DOM element *within* the editor DOM structure,
// so it can get clipped by the edges of the editor, especially noticeable
// when there is no content in the editor yet (so it'll get sliced off at the
// top of the editor). It's not possible to use a React Portal there as a
// workaround due to the way in which the element is dynamically
// created/destroyed via tippy inside Tiptap, preventing interactivity (see
// https://github.com/ueberdosis/tiptap/issues/2292).
// 2) The BubbleMenu visibility cannot be controlled programmatically. Its
// `shouldShow` callback only runs when editor internal state changes, so we
// can't control it beyond that without wacky hacks. See the issue here
// https://github.com/ueberdosis/tiptap/issues/2305.
//
// This alternative component has a simpler API, with just an `open` flag, which
// properly responds to all changes in React props, and it uses MUI's Popper
// rather than relying on tippy, so we inherently get "Portal" behavior and
// don't have to worry about visual clipping.
function ControlledBubbleMenu(_a) {
var { editor, open, className, classes: overrideClasses = {}, sx, children, anchorEl, container, disablePortal, placement = "top", fallbackPlacements = [
"top",
"bottom",
"top-start",
"bottom-start",
"top-end",
"bottom-end",
], flipPadding = 8, PaperProps } = _a, popperProps = __rest(_a, ["editor", "open", "className", "classes", "sx", "children", "anchorEl", "container", "disablePortal", "placement", "fallbackPlacements", "flipPadding", "PaperProps"]);
const { classes, cx } = useStyles(undefined, {
props: { classes: overrideClasses },
});
const theme = (0, material_1.useTheme)();
const defaultAnchorEl = (0, react_1.useCallback)(() => {
// The logic here is taken from the positioning implementation in Tiptap's BubbleMenuPlugin
// https://github.com/ueberdosis/tiptap/blob/16bec4e9d0c99feded855b261edb6e0d3f0bad21/packages/extension-bubble-menu/src/bubble-menu-plugin.ts#L183-L193
const { ranges } = editor.state.selection;
const from = Math.min(...ranges.map((range) => range.$from.pos));
const to = Math.max(...ranges.map((range) => range.$to.pos));
return {
getBoundingClientRect: () => {
if ((0, core_1.isNodeSelection)(editor.state.selection)) {
const node = editor.view.nodeDOM(from);
if (node instanceof HTMLElement) {
return node.getBoundingClientRect();
}
}
return (0, core_1.posToDOMRect)(editor.view, from, to);
},
};
}, [editor]);
return ((0, jsx_runtime_1.jsx)(material_1.Popper, Object.assign({ open: open, placement: placement, modifiers: [
{
name: "offset",
options: {
// Add a slight vertical offset for the popper from the current selection
offset: [0, 6],
},
},
{
name: "flip",
enabled: true,
options: {
// We'll reposition (to one of the below fallback placements) whenever our Popper goes
// outside of the editor. (This is necessary since our children aren't actually rendered
// here, but instead with a portal, so the editor DOM node isn't a parent.)
boundary: editor.options.element,
fallbackPlacements: fallbackPlacements,
padding: flipPadding,
},
},
{
// Don't allow the bubble menu to overflow outside of the its clipping parents
// or viewport
name: "preventOverflow",
enabled: true,
options: {
// Check for overflow in the y-axis direction instead of x-axis direction
// (the default for top and bottom placements), since that's likely to be
// the more problematic direction when scrolling. (Theoretically it would be
// nice to have it check all axes which seemingly could be done with
// `mainAxis: false`, but for an element that is wide and tall, this ends up
// not placing the Popper in a visible location, so the behavior of
// `altAxis: true` seems preferable.)
altAxis: true,
boundary: "clippingParents",
padding: 8,
},
},
// If we want to add an arrow to the Popper, we'll seemingly need to implement a lot
// of custom styling and whatnot, like in
// https://github.com/mui-org/material-ui/blob/84671ab1d6db4f6901d60206f2375bd51862c66e/docs/src/pages/components/popper/ScrollPlayground.js#L19-L103,
// which is probably not worth it
], anchorEl: anchorEl !== null && anchorEl !== void 0 ? anchorEl : defaultAnchorEl, className: cx(controlledBubbleMenuClasses.root, classes.root, className), sx: sx, container: container, disablePortal: disablePortal, transition: true }, popperProps, { children: ({ TransitionProps }) => ((0, jsx_runtime_1.jsx)(material_1.Fade, Object.assign({}, TransitionProps, { timeout: {
enter: theme.transitions.duration.enteringScreen,
// Exit immediately rather than using a transition, since the
// content of the bubble menu will usually be updating as the editor
// content and thus `open` state changes, and we don't want it to
// "flash" with incorrect content during the transition
exit: 0,
}, children: (0, jsx_runtime_1.jsx)(material_1.Paper, Object.assign({ elevation: 7 }, PaperProps, { className: cx(controlledBubbleMenuClasses.paper, classes.paper, PaperProps === null || PaperProps === void 0 ? void 0 : PaperProps.className), children: children })) }))) })));
}
exports.default = ControlledBubbleMenu;
;