mui-tiptap
Version:
A Material-UI (MUI) styled WYSIWYG rich text editor, using Tiptap
181 lines (180 loc) • 12.8 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
/// <reference types="@tiptap/extension-paragraph" />
import { MenuItem } from "@mui/material";
import { useCallback, useMemo } from "react";
import { makeStyles } from "tss-react/mui";
import { useRichTextEditorContext } from "../context";
import { getEditorStyles } from "../styles";
import { getAttributesForEachSelected } from "../utils/getAttributesForEachSelected";
import MenuButtonTooltip from "./MenuButtonTooltip";
import MenuSelect from "./MenuSelect";
const useStyles = makeStyles({ name: { MenuSelectHeading } })((theme) => {
const editorStyles = getEditorStyles(theme);
return {
selectInput: {
// We use a fixed width so that the Select element won't change sizes as
// the selected option changes (which would shift other elements in the
// menu bar)
width: 77,
},
menuOption: {
// These styles ensure the item fills its MenuItem container, and the
// tooltip appears in the same place when hovering over the item generally
// (not just the text of the item)
display: "block",
width: "100%",
},
headingOption: {
marginBlockStart: 0,
marginBlockEnd: 0,
fontWeight: "bold",
},
headingOption1: {
fontSize: editorStyles["& h1"].fontSize,
},
headingOption2: {
fontSize: editorStyles["& h2"].fontSize,
},
headingOption3: {
fontSize: editorStyles["& h3"].fontSize,
},
headingOption4: {
fontSize: editorStyles["& h4"].fontSize,
},
headingOption5: {
fontSize: editorStyles["& h5"].fontSize,
},
headingOption6: {
fontSize: editorStyles["& h6"].fontSize,
},
};
});
const HEADING_OPTION_VALUES = {
Paragraph: "Paragraph",
Heading1: "Heading 1",
Heading2: "Heading 2",
Heading3: "Heading 3",
Heading4: "Heading 4",
Heading5: "Heading 5",
Heading6: "Heading 6",
};
const HEADING_OPTION_VALUE_TO_LEVEL = {
[HEADING_OPTION_VALUES.Heading1]: 1,
[HEADING_OPTION_VALUES.Heading2]: 2,
[HEADING_OPTION_VALUES.Heading3]: 3,
[HEADING_OPTION_VALUES.Heading4]: 4,
[HEADING_OPTION_VALUES.Heading5]: 5,
[HEADING_OPTION_VALUES.Heading6]: 6,
};
const LEVEL_TO_HEADING_OPTION_VALUE = {
1: HEADING_OPTION_VALUES.Heading1,
2: HEADING_OPTION_VALUES.Heading2,
3: HEADING_OPTION_VALUES.Heading3,
4: HEADING_OPTION_VALUES.Heading4,
5: HEADING_OPTION_VALUES.Heading5,
6: HEADING_OPTION_VALUES.Heading6,
};
export default function MenuSelectHeading({ labels, hideShortcuts = false, ...menuSelectProps }) {
var _a, _b, _c, _d, _e, _f, _g, _h;
const { classes, cx } = useStyles();
const editor = useRichTextEditorContext();
const handleHeadingType = useCallback((event) => {
const value = event.target.value;
if (value === HEADING_OPTION_VALUES.Paragraph) {
editor === null || editor === void 0 ? void 0 : editor.chain().setParagraph().focus().run();
}
else if (value in HEADING_OPTION_VALUE_TO_LEVEL) {
editor === null || editor === void 0 ? void 0 : editor.chain().setHeading({
level: HEADING_OPTION_VALUE_TO_LEVEL[value],
}).focus().run();
}
}, [editor]);
let selectedValue = "";
let currentLevel;
if (editor === null || editor === void 0 ? void 0 : editor.isActive("paragraph")) {
selectedValue = HEADING_OPTION_VALUES.Paragraph;
}
else if (editor === null || editor === void 0 ? void 0 : editor.isActive("heading")) {
const currentNodeHeadingAttributes = getAttributesForEachSelected(editor.state, "heading");
const currentNodeLevels = currentNodeHeadingAttributes.map((attrs) => attrs.level);
const numCurrentNodeLevels = new Set(currentNodeLevels).size;
// We only want to show a selected level value if all of the selected nodes
// have the same level. (That way a user can properly change the level when
// selecting across two separate headings, and so we don't mistakenly just
// show the first of the selected nodes' levels and not allow changing all
// selected to that heading level. See
// https://github.com/ueberdosis/tiptap/issues/3481.)
currentLevel =
numCurrentNodeLevels === 1 ? currentNodeLevels[0] : undefined;
if (currentLevel && currentLevel in LEVEL_TO_HEADING_OPTION_VALUE) {
selectedValue =
LEVEL_TO_HEADING_OPTION_VALUE[currentLevel];
}
}
const isCurrentlyParagraphOrHeading = selectedValue !== "";
const canSetParagraph = !!(editor === null || editor === void 0 ? void 0 : editor.can().setParagraph());
// Figure out which settings the user has enabled with the heading extension
const enabledHeadingLevels = useMemo(() => {
var _a;
const headingExtension = editor === null || editor === void 0 ? void 0 : editor.extensionManager.extensions.find((extension) => extension.name == "heading");
return new Set((_a = headingExtension === null || headingExtension === void 0 ? void 0 : headingExtension.options.levels) !== null && _a !== void 0 ? _a : []);
}, [editor]);
// In determining whether we can set a heading, at least one heading level
// must be enabled in the extension configuration. We have to pass a level
// when running `can().setHeading()`, so we just use the first one that is
// enabled. And since some Tiptap versions return `false` for
// `can().setHeading()` when passing the current level, we also have to check
// whether that arbitrary first level is the `currentLevel` (see
// https://github.com/sjdemartini/mui-tiptap/issues/197).
const firstEnabledHeadingResult = enabledHeadingLevels.values().next();
const firstEnabledHeading = firstEnabledHeadingResult.done
? undefined
: firstEnabledHeadingResult.value;
const canSetHeading = firstEnabledHeading !== undefined &&
(currentLevel === firstEnabledHeading ||
!!(editor === null || editor === void 0 ? void 0 : editor.can().setHeading({ level: firstEnabledHeading })));
return (
// We currently have to specify that the value is of type
// `HeadingOptionValue | ""` rather than just `HeadingOptionValue` due to
// the bug reported here https://github.com/mui/material-ui/issues/34083. We
// need it to support "" as a possible value in the `renderValue` function
// below since we have `displayEmpty=true`, and the types don't properly
// handle that scenario.
_jsxs(MenuSelect, { onChange: handleHeadingType, disabled: !(editor === null || editor === void 0 ? void 0 : editor.isEditable) ||
(!isCurrentlyParagraphOrHeading && !canSetParagraph && !canSetHeading), displayEmpty: true, renderValue: (selected) => {
var _a, _b;
let result;
if (selected === "") {
// Handle the deprecated `emptyValue` label name, falling back to the
// newer `labels.empty`, and finally our default empty label
// eslint-disable-next-line @typescript-eslint/no-deprecated
result = (_b = (_a = labels === null || labels === void 0 ? void 0 : labels.emptyValue) !== null && _a !== void 0 ? _a : labels === null || labels === void 0 ? void 0 : labels.empty) !== null && _b !== void 0 ? _b : _jsx("em", { children: "Change to\u2026" });
}
else if (selected === HEADING_OPTION_VALUES.Paragraph) {
result = labels === null || labels === void 0 ? void 0 : labels.paragraph;
}
else if (selected === HEADING_OPTION_VALUES.Heading1) {
result = labels === null || labels === void 0 ? void 0 : labels.heading1;
}
else if (selected === HEADING_OPTION_VALUES.Heading2) {
result = labels === null || labels === void 0 ? void 0 : labels.heading2;
}
else if (selected === HEADING_OPTION_VALUES.Heading3) {
result = labels === null || labels === void 0 ? void 0 : labels.heading3;
}
else if (selected === HEADING_OPTION_VALUES.Heading4) {
result = labels === null || labels === void 0 ? void 0 : labels.heading4;
}
else if (selected === HEADING_OPTION_VALUES.Heading5) {
result = labels === null || labels === void 0 ? void 0 : labels.heading5;
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
}
else if (selected === HEADING_OPTION_VALUES.Heading6) {
result = labels === null || labels === void 0 ? void 0 : labels.heading6;
}
return result !== null && result !== void 0 ? result : selected;
}, "aria-label": "Text headings", tooltipTitle: "Styles", ...menuSelectProps, value: selectedValue, inputProps: {
...menuSelectProps.inputProps,
className: cx(classes.selectInput, (_a = menuSelectProps.inputProps) === null || _a === void 0 ? void 0 : _a.className),
}, children: [_jsx(MenuItem, { value: HEADING_OPTION_VALUES.Paragraph, disabled: !isCurrentlyParagraphOrHeading && !canSetParagraph, children: _jsx(MenuButtonTooltip, { label: "", shortcutKeys: hideShortcuts ? undefined : ["mod", "alt", "0"], placement: "right", contentWrapperClassName: classes.menuOption, children: (_b = labels === null || labels === void 0 ? void 0 : labels.paragraph) !== null && _b !== void 0 ? _b : HEADING_OPTION_VALUES.Paragraph }) }), enabledHeadingLevels.has(1) && (_jsx(MenuItem, { value: HEADING_OPTION_VALUES.Heading1, disabled: !canSetHeading, children: _jsx(MenuButtonTooltip, { label: "", shortcutKeys: hideShortcuts ? undefined : ["mod", "alt", "1"], placement: "right", contentWrapperClassName: cx(classes.menuOption, classes.headingOption, classes.headingOption1), children: (_c = labels === null || labels === void 0 ? void 0 : labels.heading1) !== null && _c !== void 0 ? _c : HEADING_OPTION_VALUES.Heading1 }) })), enabledHeadingLevels.has(2) && (_jsx(MenuItem, { value: HEADING_OPTION_VALUES.Heading2, disabled: !canSetHeading, children: _jsx(MenuButtonTooltip, { label: "", shortcutKeys: hideShortcuts ? undefined : ["mod", "alt", "2"], placement: "right", contentWrapperClassName: cx(classes.menuOption, classes.headingOption, classes.headingOption2), children: (_d = labels === null || labels === void 0 ? void 0 : labels.heading2) !== null && _d !== void 0 ? _d : HEADING_OPTION_VALUES.Heading2 }) })), enabledHeadingLevels.has(3) && (_jsx(MenuItem, { value: HEADING_OPTION_VALUES.Heading3, disabled: !canSetHeading, children: _jsx(MenuButtonTooltip, { label: "", shortcutKeys: hideShortcuts ? undefined : ["mod", "alt", "3"], placement: "right", contentWrapperClassName: cx(classes.menuOption, classes.headingOption, classes.headingOption3), children: (_e = labels === null || labels === void 0 ? void 0 : labels.heading3) !== null && _e !== void 0 ? _e : HEADING_OPTION_VALUES.Heading3 }) })), enabledHeadingLevels.has(4) && (_jsx(MenuItem, { value: HEADING_OPTION_VALUES.Heading4, disabled: !canSetHeading, children: _jsx(MenuButtonTooltip, { label: "", shortcutKeys: hideShortcuts ? undefined : ["mod", "alt", "4"], placement: "right", contentWrapperClassName: cx(classes.menuOption, classes.headingOption, classes.headingOption4), children: (_f = labels === null || labels === void 0 ? void 0 : labels.heading4) !== null && _f !== void 0 ? _f : HEADING_OPTION_VALUES.Heading4 }) })), enabledHeadingLevels.has(5) && (_jsx(MenuItem, { value: HEADING_OPTION_VALUES.Heading5, disabled: !canSetHeading, children: _jsx(MenuButtonTooltip, { label: "", shortcutKeys: hideShortcuts ? undefined : ["mod", "alt", "5"], placement: "right", contentWrapperClassName: cx(classes.menuOption, classes.headingOption, classes.headingOption5), children: (_g = labels === null || labels === void 0 ? void 0 : labels.heading5) !== null && _g !== void 0 ? _g : HEADING_OPTION_VALUES.Heading5 }) })), enabledHeadingLevels.has(6) && (_jsx(MenuItem, { value: HEADING_OPTION_VALUES.Heading6, disabled: !canSetHeading, children: _jsx(MenuButtonTooltip, { label: "", shortcutKeys: hideShortcuts ? undefined : ["mod", "alt", "6"], placement: "right", contentWrapperClassName: cx(classes.menuOption, classes.headingOption, classes.headingOption6), children: (_h = labels === null || labels === void 0 ? void 0 : labels.heading6) !== null && _h !== void 0 ? _h : HEADING_OPTION_VALUES.Heading6 }) }))] }));
}