mui-tiptap
Version:
A Material-UI (MUI) styled WYSIWYG rich text editor, using Tiptap
141 lines (140 loc) • 8.78 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;
};
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;
;