Adding the rich text widget
This commit is contained in:
66
gui/rpk-gui/src/lib/LabelledOutlined.tsx
Normal file
66
gui/rpk-gui/src/lib/LabelledOutlined.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
// }
|
||||
// };
|
||||
// }
|
||||
});
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
145
gui/rpk-gui/src/lib/crud/components/widgets/richtext/index.tsx
Normal file
145
gui/rpk-gui/src/lib/crud/components/widgets/richtext/index.tsx
Normal 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> </LeftContainer>
|
||||
<TextContainer>
|
||||
<RichTextEditorProvider editor={editor}>
|
||||
<TableBubbleMenu />
|
||||
<RichTextField
|
||||
controls={
|
||||
<MenuControlsContainer>
|
||||
{isMultiline ? multilineButtons : singlelineButtons}
|
||||
</MenuControlsContainer>
|
||||
}
|
||||
variant="standard"
|
||||
/>
|
||||
</RichTextEditorProvider>
|
||||
</TextContainer>
|
||||
<RightContainer> </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} />
|
||||
</>
|
||||
);
|
||||
Reference in New Issue
Block a user