UNPKG

mui-tiptap

Version:

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

135 lines (134 loc) 7.83 kB
"use strict"; 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;