Adding the rich text widget

This commit is contained in:
2025-04-21 01:30:16 +02:00
parent 3fbb82642b
commit 6c480a4971
8 changed files with 1249 additions and 2 deletions

View File

@@ -0,0 +1,66 @@
import React, { ReactElement, ReactNode } from "react";
import clsx from "clsx";
import { InputLabel, styled } from "@mui/material";
import NotchedOutline from "@mui/material/OutlinedInput/NotchedOutline";
const DivRoot = styled("div")(({ theme }) => ({
position: "relative",
marginTop: "8px",
}));
const DivContentWrapper = styled("div")(({ theme }) => ({
position: "relative",
}));
const DivContent = styled("div")(({ theme }) => ({
paddingTop: "1px",
paddingLeft: theme.spacing(1),
paddingRight: theme.spacing(1),
}));
const StyledInputLabel = styled(InputLabel)(({ theme }) => ({
position: "absolute",
left: 0,
top: 0,
}));
const StyledNotchedOutline = styled(NotchedOutline)(({ theme }) => [
{
borderRadius: theme.shape.borderRadius,
borderColor: theme.palette.grey["400"]
},
theme.applyStyles('dark', {
borderRadius: theme.shape.borderRadius,
borderColor: theme.palette.grey["800"]
})
]);
interface Props {
id: string;
label: string;
children: ReactNode;
className?: string;
}
export default function LabelledOutlined({ id, label, children, className }: Props): ReactElement {
const labelRef = React.useRef(null);
return (
<DivRoot className={clsx(className)}>
<StyledInputLabel
ref={labelRef}
htmlFor={id}
variant="outlined"
shrink
>
{label}
</StyledInputLabel>
<DivContentWrapper>
<DivContent id={id}>
{children}
<StyledNotchedOutline notched label={label + "*"} />
</DivContent>
</DivContentWrapper>
</DivRoot>
);
}

View File

@@ -3,7 +3,7 @@ import { FormContextType, getTemplate, RJSFSchema, WidgetProps } from "@rjsf/uti
import Typography from "@mui/material/Typography";
import ForeignKeyWidget from "./foreign-key";
import Typography from "@mui/material/Typography";
import RichtextWidget from "./richtext";
export type CrudTextRJSFSchema = RJSFSchema & { props? : any };
@@ -15,6 +15,8 @@ export default function CrudTextWidget<T = any, S extends CrudTextRJSFSchema = C
return <ForeignKeyWidget {...props} />;
} else if (schema.hasOwnProperty("const")) {
return <Typography >{schema.const as string}</Typography>;
} else if (schema.props?.hasOwnProperty("richtext")) {
return <RichtextWidget {...props} />;
} else {
const { options, registry } = props;
const BaseInputTemplate = getTemplate<'BaseInputTemplate', T, S, F>('BaseInputTemplate', registry, options);

View File

@@ -0,0 +1,119 @@
import { Extension } from "@tiptap/core";
type IndentOptions = {
/**
* @default ["paragraph", "heading"]
*/
types: string[];
/**
* Amount of margin to increase and decrease the indent
*
* @default 40
*/
margin: number;
};
declare module "@tiptap/core" {
interface Commands<ReturnType> {
indent: {
indent: () => ReturnType;
outdent: () => ReturnType;
};
}
}
export default Extension.create<IndentOptions>({
name: "indent",
defaultOptions: {
types: ["paragraph", "heading"],
margin: 40
},
addGlobalAttributes() {
return [
{
types: this.options.types,
attributes: {
indent: {
default: 0,
renderHTML: (attrs) => ({
style: `margin-left: ${(attrs.indent || 0) * this.options.margin}px`
}),
parseHTML: (attrs) => parseInt(attrs.style.marginLeft) / this.options.margin || 0
}
}
}
];
},
addCommands() {
return {
indent:
() =>
({ editor, chain, commands }) => {
// Check for a list
if (
editor.isActive("listItem") ||
editor.isActive("bulletList") ||
editor.isActive("orderedList")
) {
return chain().sinkListItem("listItem").run();
}
return this.options.types
.map((type) => {
const attrs = editor.getAttributes(type).indent;
const indent = (attrs || 0) + 1;
return commands.updateAttributes(type, { indent });
})
.every(Boolean);
},
outdent:
() =>
({ editor, chain, commands }) => {
// Check for a list
if (
editor.isActive("listItem") ||
editor.isActive("bulletList") ||
editor.isActive("orderedList")
) {
return chain().liftListItem("listItem").run();
}
const result = this.options.types
.filter((type) => {
const attrs = editor.getAttributes(type).indent;
return attrs > 0;
})
.map((type) => {
const attrs = editor.getAttributes(type).indent;
const indent = (attrs || 0) - 1;
return commands.updateAttributes(type, { indent });
});
return result.every(Boolean) && result.length > 0;
}
};
},
// addKeyboardShortcuts() {
// return {
// Tab: ({ editor }) => {
// return editor.commands.indent();
// },
// "Shift-Tab": ({ editor }) => {
// return editor.commands.outdent();
// },
// Backspace: ({ editor }) => {
// const { selection } = editor.state;
//
// // Make sure we are at the start of the node
// if (selection.$anchor.parentOffset > 0 || selection.from !== selection.to) {
// return false;
// }
//
// return editor.commands.outdent();
// }
// };
// }
});

View File

@@ -0,0 +1,19 @@
import FormatIndentIncrease from "@mui/icons-material/FormatIndentIncrease";
import { MenuButton, MenuButtonProps, useRichTextEditorContext } from "mui-tiptap";
export type MenuButtonIndentProps = Partial<MenuButtonProps>;
export default function MenuButtonIndent(props: MenuButtonIndentProps) {
const editor = useRichTextEditorContext();
return (
<MenuButton
tooltipLabel="Indent"
tooltipShortcutKeys={["Tab"]}
IconComponent={FormatIndentIncrease}
disabled={!editor?.isEditable || (!editor.can().indent())}
onClick={() => editor?.chain().focus().indent().run()}
{...props}
/>
);
}

View File

@@ -0,0 +1,18 @@
import FormatIndentDecrease from "@mui/icons-material/FormatIndentDecrease";
import { MenuButton, MenuButtonProps, useRichTextEditorContext } from "mui-tiptap";
export type MenuButtonUnindentProps = Partial<MenuButtonProps>;
export default function MenuButtonUnindent(props: MenuButtonUnindentProps) {
const editor = useRichTextEditorContext();
return (
<MenuButton
tooltipLabel="Unindent"
tooltipShortcutKeys={["Shift", "Tab"]}
IconComponent={FormatIndentDecrease}
disabled={!editor?.isEditable || !editor.can().outdent()}
onClick={() => editor?.chain().focus().outdent().run()}
{...props}
/>
);
}

View File

@@ -0,0 +1,145 @@
import { FormContextType, WidgetProps } from "@rjsf/utils";
import React from "react";
import { CrudTextRJSFSchema } from "../crud-text-widget";
import { useEditor, BubbleMenu } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import Underline from "@tiptap/extension-underline"
import TextAlign from "@tiptap/extension-text-align"
import TableCell from '@tiptap/extension-table-cell'
import TableHeader from '@tiptap/extension-table-header'
import TableRow from '@tiptap/extension-table-row'
import {
MenuButtonAddTable, MenuButtonAlignCenter, MenuButtonAlignJustify, MenuButtonAlignLeft, MenuButtonAlignRight,
MenuButtonBold, MenuButtonBulletedList, MenuButtonItalic, MenuButtonOrderedList, MenuButtonRedo, MenuButtonUnderline,
MenuButtonUndo, MenuControlsContainer, MenuDivider, RichTextEditorProvider, RichTextField, TableBubbleMenu,
TableImproved,
} from "mui-tiptap";
import { UseEditorOptions } from "@tiptap/react/src/useEditor";
import LabelledOutlined from "../../../../LabelledOutlined";
import MenuButtonUnindent from "./MenuButtonUnindent";
import MenuButtonIndent from "./MenuButtonIndent";
import IndentExtension from "./IndentExtension"
import { Container, Paper, styled } from "@mui/material";
import Stack from "@mui/material/Stack";
const LeftContainer = styled(Container)(({ theme }) => [{
width: "2cm",
borderLeft: "8px ridge black",
borderRight: "1px dashed grey",
padding: "0px !important",
margin: "0px !important"
},
]);
const TextContainer = styled(Container)(({ theme }) => [{
maxWidth: "580px",
padding: "0px !important",
margin: "0px !important"
},
]);
const RightContainer = styled(Container)(({ theme }) => [{
width: "2cm",
borderRight: "8px groove black",
borderLeft: "1px dashed grey",
padding: "0px !important",
margin: "0px !important"
},
]);
const StyledLabelledOutlined = styled(LabelledOutlined)(({ theme }) => [{
padding: "1px !important",
},
]);
const RichtextWidget = <T = any, S extends CrudTextRJSFSchema = CrudTextRJSFSchema, F extends FormContextType = any>(
props: WidgetProps<T, S, F>
) => {
const { schema, value, onChange, label, id } = props;
const isMultiline = schema.props.multiline === true;
let editorOptions: UseEditorOptions;
if (isMultiline) {
editorOptions = {
extensions: [StarterKit, Underline, TextAlign.configure({types: ['paragraph', "table"]}), TableImproved.configure({resizable: true}), TableRow, TableHeader, TableCell, IndentExtension],
onUpdate: ({ editor }) => {
onChange(editor.getHTML())
}
}
} else {
editorOptions = {
extensions: [StarterKit, Underline,],
onUpdate: ({ editor }) => {
let text = editor.getText();
if (text.includes("\n")) {
text = text.replace("\n", " ");
editor.commands.setContent(text);
}
onChange(text);
}
}
}
editorOptions.content = value
const editor = useEditor(editorOptions)
return (
<StyledLabelledOutlined label={label} id={id}>
<Stack direction="row" spacing={0} sx={{justifyContent: "center", alignItems: "stretch"}}>
<LeftContainer>&nbsp;</LeftContainer>
<TextContainer>
<RichTextEditorProvider editor={editor}>
<TableBubbleMenu />
<RichTextField
controls={
<MenuControlsContainer>
{isMultiline ? multilineButtons : singlelineButtons}
</MenuControlsContainer>
}
variant="standard"
/>
</RichTextEditorProvider>
</TextContainer>
<RightContainer>&nbsp;</RightContainer>
</Stack>
</StyledLabelledOutlined>
)
}
export default RichtextWidget
const singlelineButtons = (
<>
<MenuButtonUndo tabIndex={-1} />
<MenuButtonRedo tabIndex={-1} />
<MenuDivider />
<MenuButtonBold tabIndex={-1} />
<MenuButtonItalic tabIndex={-1} />
<MenuButtonUnderline tabIndex={-1} />
</>
);
const multilineButtons = (
<>
<MenuButtonUndo tabIndex={-1} />
<MenuButtonRedo tabIndex={-1} />
<MenuDivider />
<MenuButtonBold tabIndex={-1} />
<MenuButtonItalic tabIndex={-1} />
<MenuButtonUnderline tabIndex={-1} />
<MenuDivider />
<MenuButtonAlignLeft tabIndex={-1} />
<MenuButtonAlignCenter tabIndex={-1} />
<MenuButtonAlignRight tabIndex={-1} />
<MenuButtonAlignJustify tabIndex={-1} />
<MenuDivider />
<MenuButtonUnindent tabIndex={-1} />
<MenuButtonIndent tabIndex={-1} />
<MenuButtonBulletedList tabIndex={-1} />
<MenuButtonOrderedList tabIndex={-1} />
<MenuDivider />
<MenuButtonAddTable tabIndex={-1} />
</>
);