UNPKG

mui-tiptap

Version:

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

141 lines (140 loc) 8.78 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; }; 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 core_1 = require("@tiptap/core"); const react_1 = require("react"); const mui_1 = require("tss-react/mui"); const ControlledBubbleMenu_1 = __importDefault(require("./ControlledBubbleMenu")); const context_1 = require("./context"); const TableMenuControls_1 = __importDefault(require("./controls/TableMenuControls")); const hooks_1 = require("./hooks"); const DebounceRender_1 = __importDefault(require("./utils/DebounceRender")); const useStyles = (0, mui_1.makeStyles)({ name: { TableBubbleMenu }, })((theme) => ({ controls: { maxWidth: "90vw", padding: theme.spacing(0.5, 1), }, })); /** * Renders a bubble menu to manipulate the contents of a Table (add or delete * columns or rows, merge cells, etc.), when the user's caret/selection is * inside a Table. * * For use with mui-tiptap's `TableImproved` extension or Tiptap's * `@tiptap/extension-table` extension. * * If you're using `RichTextEditor`, include this component via * `RichTextEditor`’s `children` render-prop. Otherwise, include the * `TableBubbleMenu` as a child of the component where you call `useEditor` and * render your `RichTextField` or `RichTextContent`. (The bubble menu itself * will be positioned appropriately no matter where you put it in your React * tree, as long as it is re-rendered whenever the Tiptap `editor` forces an * update, which will happen if it's a child of the component using * `useEditor`). */ function TableBubbleMenu(_a) { var { disableDebounce = false, DebounceProps, labels } = _a, controlledBubbleMenuProps = __rest(_a, ["disableDebounce", "DebounceProps", "labels"]); const editor = (0, context_1.useRichTextEditorContext)(); const { classes } = useStyles(); // Because the user interactions with the table menu bar buttons unfocus the // editor (since it's not part of the editor content), we'll debounce our // visual focused state so that we keep the bubble menu open during those // interactions. That way we don't close it upon menu bar button click // immediately, which can prevent menu button callbacks from working and // also undesirably will close the bubble menu rather than keeping it open for // future menu interaction. const isEditorFocusedDebounced = (0, hooks_1.useDebouncedFocus)({ editor }); // We want to position the table menu outside the entire table, rather than at the // current cursor position, so that it's essentially static even as the table changes // in size and doesn't "block" things within the table while you're trying to edit. // NOTE: Popper accepts an `anchorEl` prop as an HTML element, virtualElement // (https://popper.js.org/docs/v2/virtual-elements/), or a function that returns // either. However, if you use a function that return an element, Popper will *not* // re-evaluate which element that is except when the function itself changes, or when // the Popper `open` state changes // (https://github.com/mui/material-ui/blob/5b2583a1c8b227661c4bf4113a79346634ea53af/packages/mui-base/src/PopperUnstyled/PopperUnstyled.tsx#L126-L130). // As such, we need to return a virtualElement (object with `getBoundingClientRect`) // and *not* return an HTML element, since we don't want it to get cached. Otherwise // clicking from one table to another will incorrectly get the bubble menu "stuck" on // the table that was first used to position the Popper. const bubbleMenuAnchorEl = (0, react_1.useMemo)(() => editor ? { getBoundingClientRect: () => { const nearestTableParent = editor.isActive("table") ? (0, core_1.findParentNodeClosestToPos)(editor.state.selection.$anchor, (node) => node.type.name === "table") : null; if (nearestTableParent) { const wrapperDomNode = editor.view.nodeDOM(nearestTableParent.pos); // The DOM node of a Tiptap table node is a div wrapper, which contains a `table` child. // The div wrapper is a block element that fills the entire row, but the table may not be // full width, so we want to get our bounding rectangle based on the `table` (to align it // with the table itself), not the div. See // https://github.com/ueberdosis/tiptap/blob/40a9404c94c7fef7900610c195536384781ae101/packages/extension-table/src/TableView.ts#L69-L71 const tableDomNode = wrapperDomNode === null || wrapperDomNode === void 0 ? void 0 : wrapperDomNode.querySelector("table"); if (tableDomNode) { return tableDomNode.getBoundingClientRect(); } } // Since we weren't able to find a table from the current user position, that means the user // hasn't put their cursor in a table. We'll be hiding the table in this case, but we need // to return a bounding rect regardless (can't return `null`), so we use the standard logic // based on the current cursor position/selection instead. 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 (0, core_1.posToDOMRect)(editor.view, from, to); }, } : null, [editor]); if (!(editor === null || editor === void 0 ? void 0 : editor.isEditable)) { return null; } const controls = ((0, jsx_runtime_1.jsx)(TableMenuControls_1.default, { className: classes.controls, labels: labels })); return ((0, jsx_runtime_1.jsx)(ControlledBubbleMenu_1.default, Object.assign({ editor: editor, open: isEditorFocusedDebounced && editor.isActive("table"), anchorEl: bubbleMenuAnchorEl, // So the menu doesn't move as columns are added, removed, or resized, we // prefer "foo-start" rather than the centered "foo" placement. Similarly, // we prefer "top" to "bottom" so that the menu doesn't move as the number // and size of rows changes. placement: "top-start", fallbackPlacements: [ "bottom-start", "top", "bottom", "top-end", "bottom-end", ], // Though we prefer for the menu to stay on top if there's room, we // definitely do not want the table bubble menu to cover up the main // editor menu bar, which is typically going to be above the editor, since // users are likely to want to change styles of elements within a table // while using/editing a table. This overlap could happen if the Table is // the first element within the editor content, or if the content is long // and the menu bar is sticky, with the user having scrolled such that a // table is at the top of the page. What would be nice is if PopperJS let // you specify a placement to use if the `placement` *and none of the // fallbacks* are satisfied, so that we could default to "bottom-start" in // that scenario rather than the main `placement` value of "top-start". // Since that is not an option, we add an artificial infinite negative // bottom padding (so that it's like we actually have infinite extra room // below our table bubble menu within the editor) as a way to ensure we // only fall back to bottom placements if the top has no room. Similarly // we add a top padding equal to what should give us enough room to avoid // overlapping the main menu bar. flipPadding: { top: 35, left: 8, right: 8, bottom: -Infinity } }, controlledBubbleMenuProps, { children: disableDebounce ? (controls) : ((0, jsx_runtime_1.jsx)(DebounceRender_1.default, Object.assign({}, DebounceProps, { children: controls }))) }))); } exports.default = TableBubbleMenu;