diff --git a/package.json b/package.json index 425a2ff..9229835 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "dependencies": { "encodeurl": "^1.0.2", "lodash": "^4.17.21", + "prosemirror-utils": "1.2.1-0", "react-colorful": "^5.6.1", "tss-react": "^4.8.3", "type-fest": "^3.12.0" @@ -88,6 +89,8 @@ "@tiptap/extension-heading": "^2.0.0-beta.210", "@tiptap/extension-image": "^2.0.0-beta.210", "@tiptap/extension-table": "^2.0.0-beta.210", + "@tiptap/extension-table-cell": "^2.0.0-beta.210", + "@tiptap/extension-table-header": "^2.0.4", "@tiptap/pm": "^2.0.0-beta.210", "@tiptap/react": "^2.0.0-beta.210", "react": "^16.8.0 || ^17.0.2 || ^18.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 577e7d1..746688b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ dependencies: lodash: specifier: ^4.17.21 version: 4.17.21 + prosemirror-utils: + specifier: 1.2.1-0 + version: 1.2.1-0(prosemirror-model@1.19.2)(prosemirror-state@1.4.2) react-colorful: specifier: ^5.6.1 version: 5.6.1(react-dom@18.2.0)(react@18.2.0) @@ -4585,7 +4588,6 @@ packages: /orderedmap@2.1.0: resolution: {integrity: sha512-/pIFexOm6S70EPdznemIz3BQZoJ4VTFrhqzu0ACBqBgeLsLxq8e6Jim63ImIfwW/zAD1AlXpRMlOv3aghmo4dA==} - dev: true /p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} @@ -4855,7 +4857,6 @@ packages: resolution: {integrity: sha512-RXl0Waiss4YtJAUY3NzKH0xkJmsZupCIccqcIFoLTIKFlKNbIvFDRl27/kQy1FP8iUAxrjRRfIVvOebnnXJgqQ==} dependencies: orderedmap: 2.1.0 - dev: true /prosemirror-schema-basic@1.2.2: resolution: {integrity: sha512-/dT4JFEGyO7QnNTe9UaKUhjDXbTNkiWTq/N4VpKaF79bBjSExVV2NXmJpcM7z/gD7mbqNjxbmWW5nf1iNSSGnw==} @@ -4877,7 +4878,6 @@ packages: prosemirror-model: 1.19.2 prosemirror-transform: 1.7.4 prosemirror-view: 1.31.4 - dev: true /prosemirror-tables@1.3.4: resolution: {integrity: sha512-z6uLSQ1BLC3rgbGwZmpfb+xkdvD7W/UOsURDfognZFYaTtc0gsk7u/t71Yijp2eLflVpffMk6X0u0+u+MMDvIw==} @@ -4908,7 +4908,16 @@ packages: resolution: {integrity: sha512-GO38mvqJ2yeI0BbL5E1CdHcly032Dlfn9nHqlnCHqlNf9e9jZwJixxp6VRtOeDZ1uTDpDIziezMKbA41LpAx3A==} dependencies: prosemirror-model: 1.19.2 - dev: true + + /prosemirror-utils@1.2.1-0(prosemirror-model@1.19.2)(prosemirror-state@1.4.2): + resolution: {integrity: sha512-YJNjxSAFhV+w7/nKfLU4SJ0sYEHDJuaIvIxp6V1DgrVFSBBksnvHZTGPVmKGYEnbTqr0kG8HXqopNKPoQtT+3A==} + peerDependencies: + prosemirror-model: ^1.19.2 + prosemirror-state: ^1.4.3 + dependencies: + prosemirror-model: 1.19.2 + prosemirror-state: 1.4.2 + dev: false /prosemirror-view@1.31.4: resolution: {integrity: sha512-nJzH2LpYbonSTYFqQ1BUdEhbd1WPN/rp/K9T9qxBEYpgg3jK3BvEUCR45Ymc9IHpO0m3nBJwPm19RBxZdoBVuw==} @@ -4916,7 +4925,6 @@ packages: prosemirror-model: 1.19.2 prosemirror-state: 1.4.2 prosemirror-transform: 1.7.4 - dev: true /punycode@2.3.0: resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} diff --git a/src/ControlledBubbleMenu.tsx b/src/ControlledBubbleMenu.tsx index 46eaeba..7140449 100644 --- a/src/ControlledBubbleMenu.tsx +++ b/src/ControlledBubbleMenu.tsx @@ -87,6 +87,7 @@ export type ControlledBubbleMenuProps = { * content. */ PaperProps?: Partial; + onMouseLeave?: (event: React.MouseEvent) => void; }; const controlledBubbleMenuClasses: ControlledBubbleMenuClasses = @@ -141,6 +142,7 @@ export default function ControlledBubbleMenu({ ], flipPadding = 8, PaperProps, + onMouseLeave, }: ControlledBubbleMenuProps) { const { classes, cx } = useStyles(undefined, { props: { classes: overrideClasses }, @@ -222,6 +224,7 @@ export default function ControlledBubbleMenu({ container={container} disablePortal={disablePortal} transition + onMouseLeave={onMouseLeave} > {({ TransitionProps }) => ( ); + const tableNode = findParentNodeOfType(editor.state.schema.nodes.table)( + editor.state.selection + ); + + const shouldOpen = Boolean(tableNode); + return ( ; + direction?: "horizontal" | "vertical"; }; -const useStyles = makeStyles({ +const useStyles = makeStyles<{ + direction: MenuControlsContainerProps["direction"]; +}>({ name: { MenuControlsContainer: MenuControlsContainer }, -})((theme) => { +})((theme, { direction }) => { return { root: { display: "flex", + flexDirection: direction === "vertical" ? "column" : "row", rowGap: theme.spacing(0.3), columnGap: theme.spacing(0.3), alignItems: "center", @@ -45,8 +49,9 @@ export default function MenuControlsContainer({ className, debounced, DebounceProps, + direction = "horizontal", }: MenuControlsContainerProps) { - const { classes, cx } = useStyles(); + const { classes, cx } = useStyles({ direction }); const content =
{children}
; return debounced ? ( {content} diff --git a/src/controls/MenuSelectTableBorderStyle.tsx b/src/controls/MenuSelectTableBorderStyle.tsx new file mode 100644 index 0000000..628c70a --- /dev/null +++ b/src/controls/MenuSelectTableBorderStyle.tsx @@ -0,0 +1,193 @@ +import BorderStyle from "@mui/icons-material/BorderStyle"; +import { ClickAwayListener, Fade, Paper, Popper } from "@mui/material"; +import * as React from "react"; +import { makeStyles } from "tss-react/mui"; +import { useRichTextEditorContext } from "../context"; +import { + BorderStyleDashed, + BorderStyleDotted, + BorderStyleDouble, + BorderStyleGroove, + BorderStyleInset, + BorderStyleNone, + BorderStyleOutset, + BorderStyleRidge, + BorderStyleSolid, +} from "../icons"; +import { Z_INDEXES } from "../styles"; +import MenuButton from "./MenuButton"; +import type { MenuButtonTooltipProps } from "./MenuButtonTooltip"; +import MenuControlsContainer from "./MenuControlsContainer"; + +export type BorderStyleSelectOption = { + value: string; + IconComponent: React.ElementType<{ + className: string; + }>; + label?: string; + shortcutKeys?: MenuButtonTooltipProps["shortcutKeys"]; +}; + +export type MenuSelectTableBorderStyleProps = { + options?: BorderStyleSelectOption[]; + label?: string; + className?: string; +}; + +const useStyles = makeStyles({ name: { MenuSelectTableBorderStyle } })( + (theme) => ({ + root: { + zIndex: Z_INDEXES.BUBBLE_MENU, + }, + + paper: { + backgroundColor: theme.palette.background.default, + }, + }) +); + +const DEFAULT_BORDER_STYLE_OPTIONS: BorderStyleSelectOption[] = [ + { + value: "solid", + label: "Solid", + IconComponent: BorderStyleSolid, + }, + { + value: "dashed", + label: "Dashed", + IconComponent: BorderStyleDashed, + }, + { + value: "dotted", + label: "Dotted", + IconComponent: BorderStyleDotted, + }, + { + value: "double", + label: "Double", + IconComponent: BorderStyleDouble, + }, + { + value: "groove", + label: "Groove", + IconComponent: BorderStyleGroove, + }, + { + value: "ridge", + label: "Ridge", + IconComponent: BorderStyleRidge, + }, + { + value: "inset", + label: "Inset", + IconComponent: BorderStyleInset, + }, + { + value: "outset", + label: "Outset", + IconComponent: BorderStyleOutset, + }, + { + value: "none", + label: "None", + IconComponent: BorderStyleNone, + }, +]; + +export default function MenuSelectTableBorderStyle({ + options = DEFAULT_BORDER_STYLE_OPTIONS, + label, + className, +}: MenuSelectTableBorderStyleProps) { + const editor = useRichTextEditorContext(); + const [anchorEl, setAnchorEl] = React.useState(null); + const open = Boolean(anchorEl); + const { classes, cx } = useStyles(); + const handleClick = (event: React.MouseEvent) => { + anchorEl ? handleClose() : setAnchorEl(event.currentTarget); + }; + const handleClose = () => { + setAnchorEl(null); + }; + + if (!editor?.isEditable) { + return null; + } + + const IconComponent = + options.find( + (option) => + editor.getAttributes("tableHeader").borderStyle === option.value || + editor.getAttributes("tableCell").borderStyle === option.value + )?.IconComponent ?? BorderStyle; + + return ( + <> + + + {({ TransitionProps }) => ( + +
+ + + + {options.map((option) => ( + + editor + .chain() + .focus() + .setCellAttribute("borderStyle", option.value) + .updateAttributes("tableHeader", { + borderStyle: option.value, + }) + .run() + } + disabled={ + !editor + .can() + .setCellAttribute("borderStyle", option.value) + } + selected={ + editor.getAttributes("tableHeader").borderStyle === + option.value || + editor.getAttributes("tableCell").borderStyle === + option.value + } + /> + ))} + + + +
+
+ )} +
+ + ); +} diff --git a/src/controls/TableMenuControls.tsx b/src/controls/TableMenuControls.tsx index 458b7b3..7fd5209 100644 --- a/src/controls/TableMenuControls.tsx +++ b/src/controls/TableMenuControls.tsx @@ -16,6 +16,7 @@ import { } from "../icons"; import MenuButton from "./MenuButton"; import MenuControlsContainer from "./MenuControlsContainer"; +import MenuSelectTableBorderStyle from "./MenuSelectTableBorderStyle"; export type TableMenuControlsProps = { /** Class applied to the root controls container element. */ @@ -137,6 +138,10 @@ export default function TableMenuControls({ + + + + { + if (!attributes.borderStyle) { + return {}; + } + + return { + style: `border-style: ${attributes.borderStyle as string}`, + }; + }, + parseHTML: (element) => { + return element.style.borderStyle.replace(/['"]+/g, ""); + }, + }, + }; + }, +}); + +export default TableCellImproved; diff --git a/src/extensions/TableHeaderImproved.ts b/src/extensions/TableHeaderImproved.ts new file mode 100644 index 0000000..21528a6 --- /dev/null +++ b/src/extensions/TableHeaderImproved.ts @@ -0,0 +1,26 @@ +import { TableHeader } from "@tiptap/extension-table-header"; + +const TableHeaderImproved = TableHeader.extend({ + addAttributes() { + return { + ...this.parent?.(), + borderStyle: { + default: null, + renderHTML: (attributes) => { + if (!attributes.borderStyle) { + return {}; + } + + return { + style: `border-style: ${attributes.borderStyle as string}`, + }; + }, + parseHTML: (element) => { + return element.style.borderStyle.replace(/['"]+/g, ""); + }, + }, + }; + }, +}); + +export default TableHeaderImproved; diff --git a/src/extensions/index.ts b/src/extensions/index.ts index c7e24e3..de0e51d 100644 --- a/src/extensions/index.ts +++ b/src/extensions/index.ts @@ -13,4 +13,6 @@ export { type LinkBubbleMenuHandlerStorage, } from "./LinkBubbleMenuHandler"; export { default as ResizableImage } from "./ResizableImage"; +export { default as TableCellImproved } from "./TableCellImproved"; +export { default as TableHeaderImproved } from "./TableHeaderImproved"; export { default as TableImproved } from "./TableImproved"; diff --git a/src/icons/BorderStyleDashed.tsx b/src/icons/BorderStyleDashed.tsx new file mode 100644 index 0000000..696a9a5 --- /dev/null +++ b/src/icons/BorderStyleDashed.tsx @@ -0,0 +1,16 @@ +import Box from "@mui/material/Box"; +import { MENU_BUTTON_FONT_SIZE_DEFAULT } from "../controls/MenuButton"; + +function BorderStyleDashed() { + return ( + ({ + border: `3px dashed ${theme.palette.text.primary}`, + width: MENU_BUTTON_FONT_SIZE_DEFAULT, + height: MENU_BUTTON_FONT_SIZE_DEFAULT, + })} + /> + ); +} + +export default BorderStyleDashed; diff --git a/src/icons/BorderStyleDotted.tsx b/src/icons/BorderStyleDotted.tsx new file mode 100644 index 0000000..241d6df --- /dev/null +++ b/src/icons/BorderStyleDotted.tsx @@ -0,0 +1,16 @@ +import Box from "@mui/material/Box"; +import { MENU_BUTTON_FONT_SIZE_DEFAULT } from "../controls/MenuButton"; + +function BorderStyleDotted() { + return ( + ({ + border: `3px dotted ${theme.palette.text.primary}`, + width: MENU_BUTTON_FONT_SIZE_DEFAULT, + height: MENU_BUTTON_FONT_SIZE_DEFAULT, + })} + /> + ); +} + +export default BorderStyleDotted; diff --git a/src/icons/BorderStyleDouble.tsx b/src/icons/BorderStyleDouble.tsx new file mode 100644 index 0000000..12a20f3 --- /dev/null +++ b/src/icons/BorderStyleDouble.tsx @@ -0,0 +1,16 @@ +import Box from "@mui/material/Box"; +import { MENU_BUTTON_FONT_SIZE_DEFAULT } from "../controls/MenuButton"; + +function BorderStyleDouble() { + return ( + ({ + border: `3px double ${theme.palette.text.primary}`, + width: MENU_BUTTON_FONT_SIZE_DEFAULT, + height: MENU_BUTTON_FONT_SIZE_DEFAULT, + })} + /> + ); +} + +export default BorderStyleDouble; diff --git a/src/icons/BorderStyleGroove.tsx b/src/icons/BorderStyleGroove.tsx new file mode 100644 index 0000000..c6653d8 --- /dev/null +++ b/src/icons/BorderStyleGroove.tsx @@ -0,0 +1,16 @@ +import Box from "@mui/material/Box"; +import { MENU_BUTTON_FONT_SIZE_DEFAULT } from "../controls/MenuButton"; + +function BorderStyleGroove() { + return ( + ({ + border: `3px groove ${theme.palette.text.primary}`, + width: MENU_BUTTON_FONT_SIZE_DEFAULT, + height: MENU_BUTTON_FONT_SIZE_DEFAULT, + })} + /> + ); +} + +export default BorderStyleGroove; diff --git a/src/icons/BorderStyleInset.tsx b/src/icons/BorderStyleInset.tsx new file mode 100644 index 0000000..c6db5d3 --- /dev/null +++ b/src/icons/BorderStyleInset.tsx @@ -0,0 +1,16 @@ +import Box from "@mui/material/Box"; +import { MENU_BUTTON_FONT_SIZE_DEFAULT } from "../controls/MenuButton"; + +function BorderStyleInset() { + return ( + ({ + border: `3px inset ${theme.palette.text.primary}`, + width: MENU_BUTTON_FONT_SIZE_DEFAULT, + height: MENU_BUTTON_FONT_SIZE_DEFAULT, + })} + /> + ); +} + +export default BorderStyleInset; diff --git a/src/icons/BorderStyleNone.tsx b/src/icons/BorderStyleNone.tsx new file mode 100644 index 0000000..244440b --- /dev/null +++ b/src/icons/BorderStyleNone.tsx @@ -0,0 +1,16 @@ +import Box from "@mui/material/Box"; +import { MENU_BUTTON_FONT_SIZE_DEFAULT } from "../controls/MenuButton"; + +function BorderStyleNone() { + return ( + ({ + border: `3px none ${theme.palette.text.primary}`, + width: MENU_BUTTON_FONT_SIZE_DEFAULT, + height: MENU_BUTTON_FONT_SIZE_DEFAULT, + })} + /> + ); +} + +export default BorderStyleNone; diff --git a/src/icons/BorderStyleOutset.tsx b/src/icons/BorderStyleOutset.tsx new file mode 100644 index 0000000..6c83eb6 --- /dev/null +++ b/src/icons/BorderStyleOutset.tsx @@ -0,0 +1,16 @@ +import Box from "@mui/material/Box"; +import { MENU_BUTTON_FONT_SIZE_DEFAULT } from "../controls/MenuButton"; + +function BorderStyleOutset() { + return ( + ({ + border: `3px outset ${theme.palette.text.primary}`, + width: MENU_BUTTON_FONT_SIZE_DEFAULT, + height: MENU_BUTTON_FONT_SIZE_DEFAULT, + })} + /> + ); +} + +export default BorderStyleOutset; diff --git a/src/icons/BorderStyleRidge.tsx b/src/icons/BorderStyleRidge.tsx new file mode 100644 index 0000000..4fce98c --- /dev/null +++ b/src/icons/BorderStyleRidge.tsx @@ -0,0 +1,16 @@ +import Box from "@mui/material/Box"; +import { MENU_BUTTON_FONT_SIZE_DEFAULT } from "../controls/MenuButton"; + +function BorderStyleRidge() { + return ( + ({ + border: `3px ridge ${theme.palette.text.primary}`, + width: MENU_BUTTON_FONT_SIZE_DEFAULT, + height: MENU_BUTTON_FONT_SIZE_DEFAULT, + })} + /> + ); +} + +export default BorderStyleRidge; diff --git a/src/icons/BorderStyleSolid.tsx b/src/icons/BorderStyleSolid.tsx new file mode 100644 index 0000000..8ae7d1d --- /dev/null +++ b/src/icons/BorderStyleSolid.tsx @@ -0,0 +1,16 @@ +import Box from "@mui/material/Box"; +import { MENU_BUTTON_FONT_SIZE_DEFAULT } from "../controls/MenuButton"; + +function BorderStyleSolid() { + return ( + ({ + border: `3px solid ${theme.palette.text.primary}`, + width: MENU_BUTTON_FONT_SIZE_DEFAULT, + height: MENU_BUTTON_FONT_SIZE_DEFAULT, + })} + /> + ); +} + +export default BorderStyleSolid; diff --git a/src/icons/index.ts b/src/icons/index.ts index 27574e6..3911c85 100644 --- a/src/icons/index.ts +++ b/src/icons/index.ts @@ -3,6 +3,15 @@ // install size and external dependencies (see // https://github.com/sjdemartini/mui-tiptap/issues/119). export { default as BorderColorNoBar } from "./BorderColorNoBar"; +export { default as BorderStyleDashed } from "./BorderStyleDashed"; +export { default as BorderStyleDotted } from "./BorderStyleDotted"; +export { default as BorderStyleDouble } from "./BorderStyleDouble"; +export { default as BorderStyleGroove } from "./BorderStyleGroove"; +export { default as BorderStyleInset } from "./BorderStyleInset"; +export { default as BorderStyleNone } from "./BorderStyleNone"; +export { default as BorderStyleOutset } from "./BorderStyleOutset"; +export { default as BorderStyleRidge } from "./BorderStyleRidge"; +export { default as BorderStyleSolid } from "./BorderStyleSolid"; export { default as CodeBlock } from "./CodeBlock"; export { default as DeleteColumn } from "./DeleteColumn"; export { default as DeleteRow } from "./DeleteRow";