Skip to content

Commit

Permalink
basic md editor styles
Browse files Browse the repository at this point in the history
  • Loading branch information
3mcd committed Jun 3, 2024
1 parent 1b778ae commit 6306e4f
Show file tree
Hide file tree
Showing 9 changed files with 205 additions and 16 deletions.
File renamed without changes.
4 changes: 2 additions & 2 deletions core/actions/email/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as z from "zod";

import { Mail } from "ui/icon";

import { markdown } from "../_lib/types";
import { markdown } from "../_lib/zodTypes";
import * as corePubFields from "../corePubFields";
import { defineAction } from "../types";

Expand Down Expand Up @@ -34,7 +34,7 @@ export const action = defineAction({
.optional(),
})
.optional(),
pubFields: [corePubFields.title],
pubFields: [],
icon: Mail,
});

Expand Down
1 change: 1 addition & 0 deletions packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@
"@lexical/markdown": "^0.15.0",
"@lexical/react": "^0.15.0",
"@lexical/rich-text": "^0.15.0",
"@lexical/utils": "^0.15.0",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-avatar": "^1.0.3",
Expand Down
79 changes: 65 additions & 14 deletions packages/ui/src/auto-form/fields/markdown/MarkdownEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,20 @@ import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";
import { HeadingNode, QuoteNode } from "@lexical/rich-text";
import { EditorState } from "lexical";

import { cn } from "utils";

import { FormControl, FormItem, FormMessage } from "../../../form";
import AutoFormDescription from "../../common/description";
import AutoFormLabel from "../../common/label";
import AutoFormTooltip from "../../common/tooltip";
import { AutoFormInputComponentProps } from "../../types";
import { TokenProvider } from "./TokenContext";
import { TokenNode } from "./TokenNode";
import { TokenPlugin } from "./TokenPlugin";

const theme = {};
const theme = {
token: "token",
};

function onError(error: unknown) {
console.error(error);
Expand All @@ -35,6 +46,7 @@ const NODES = [
ListNode,
ListItemNode,
QuoteNode,
TokenNode,
];

const makeSyntheticChangeEvent = (value: string) => {
Expand All @@ -46,6 +58,8 @@ const makeSyntheticChangeEvent = (value: string) => {
};

export const MarkdownEditor = (props: AutoFormInputComponentProps) => {
const { showLabel: _showLabel, ...fieldPropsWithoutShowLabel } = props.fieldProps;
const showLabel = _showLabel === undefined ? true : _showLabel;
const initialValue = React.useMemo(() => props.field.value ?? "", []);
const initialConfig = React.useMemo(() => {
return {
Expand All @@ -61,23 +75,60 @@ export const MarkdownEditor = (props: AutoFormInputComponentProps) => {
(editorState: EditorState) => {
editorState.read(() => {
const markdown = $convertToMarkdownString(TRANSFORMERS);
props.fieldProps.onChange(makeSyntheticChangeEvent(markdown));
fieldPropsWithoutShowLabel.onChange(makeSyntheticChangeEvent(markdown));
});
},
[props.fieldProps.onChange]
[fieldPropsWithoutShowLabel.onChange]
);

return (
<LexicalComposer initialConfig={initialConfig}>
<RichTextPlugin
contentEditable={<ContentEditable />}
placeholder={<div>Enter some text...</div>}
ErrorBoundary={LexicalErrorBoundary}
/>
<OnChangePlugin onChange={onChange} />
<HistoryPlugin />
<AutoFocusPlugin />
<MarkdownShortcutPlugin transformers={TRANSFORMERS} />
</LexicalComposer>
<div className="flex flex-row items-center space-x-2">
<FormItem className="flex w-full flex-col justify-start">
{showLabel && (
<>
<AutoFormLabel label={props.label} isRequired={props.isRequired} />
{props.description && (
<AutoFormDescription description={props.description} />
)}
</>
)}
<TokenProvider
staticTokens={[
"user.token",
"user.id",
"user.firstName",
"user.lastName",
"instance.id",
]}
dynamicTokens={null}
>
<FormControl>
<LexicalComposer initialConfig={initialConfig}>
<RichTextPlugin
contentEditable={
<ContentEditable
className={cn(
"editor",
"markdown",
// Copied from ui/src/input.tsx
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
)}
/>
}
placeholder={null}
ErrorBoundary={LexicalErrorBoundary}
/>
<OnChangePlugin onChange={onChange} />
<HistoryPlugin />
<AutoFocusPlugin />
<MarkdownShortcutPlugin transformers={TRANSFORMERS} />
<TokenPlugin />
</LexicalComposer>
</FormControl>
<AutoFormTooltip fieldConfigItem={props.fieldConfigItem} />
</TokenProvider>
<FormMessage />
</FormItem>
</div>
);
};
19 changes: 19 additions & 0 deletions packages/ui/src/auto-form/fields/markdown/TokenContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React, { createContext, PropsWithChildren, useContext } from "react";

export type TokenContext = {
staticTokens: string[];
dynamicTokens: RegExp | null;
};

export const TokenContext = createContext<TokenContext>({
staticTokens: [],
dynamicTokens: /^.$/,
});

export const useTokenContext = () => {
return useContext(TokenContext);
};

export function TokenProvider(props: PropsWithChildren<TokenContext>) {
return <TokenContext.Provider value={props}>{props.children}</TokenContext.Provider>;
}
57 changes: 57 additions & 0 deletions packages/ui/src/auto-form/fields/markdown/TokenNode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type { EditorConfig, LexicalNode, NodeKey, SerializedTextNode } from "lexical";

import { addClassNamesToElement } from "@lexical/utils";
import { $applyNodeReplacement, TextNode } from "lexical";

export class TokenNode extends TextNode {
static getType(): string {
return "token";
}

static clone(node: TokenNode): TokenNode {
return new TokenNode(node.__text, node.__key);
}

constructor(text: string, key?: NodeKey) {
super(text, key);
console.log(text, key);
}

createDOM(config: EditorConfig): HTMLElement {
const element = super.createDOM(config);
addClassNamesToElement(element, config.theme.token);
return element;
}

static importJSON(serializedNode: SerializedTextNode): TokenNode {
const node = $createTokenNode(serializedNode.text);
node.setFormat(serializedNode.format);
node.setDetail(serializedNode.detail);
node.setMode(serializedNode.mode);
node.setStyle(serializedNode.style);
return node;
}

exportJSON(): SerializedTextNode {
return {
...super.exportJSON(),
type: "token",
};
}

canInsertTextBefore(): boolean {
return false;
}

isTextEntity(): true {
return true;
}
}

export function $createTokenNode(text = ""): TokenNode {
return $applyNodeReplacement(new TokenNode(text));
}

export function $isTokenNode(node: LexicalNode | null | undefined): node is TokenNode {
return node instanceof TokenNode;
}
50 changes: 50 additions & 0 deletions packages/ui/src/auto-form/fields/markdown/TokenPlugin.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { useCallback, useEffect, useMemo } from "react";
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
import { useLexicalTextEntity } from "@lexical/react/useLexicalTextEntity";
import { TextNode } from "lexical";

import { useTokenContext } from "./TokenContext";
import { $createTokenNode, TokenNode } from "./TokenNode";

const boundary = "^|$|[^&/" + "*" + "]";

const $createTokenNode_ = (textNode: TextNode): TokenNode => {
return $createTokenNode(textNode.getTextContent());
};

export function TokenPlugin() {
const [editor] = useLexicalComposerContext();
const { staticTokens, dynamicTokens } = useTokenContext();

const REGEX = useMemo(
() => new RegExp(`(${boundary})\{(${staticTokens.join("|")})\}`, "i"),
[staticTokens]
);

useEffect(() => {
if (!editor.hasNodes([TokenNode])) {
throw new Error("TokenPlugin: TokenNode not registered on editor");
}
}, [editor]);

const getTokenMatch = useCallback((text: string) => {
const matchArr = REGEX.exec(text);

if (matchArr === null) {
return null;
}

const tokenLength = matchArr[2].length + 2; // add two for the curly braces
const startOffset = matchArr.index + matchArr[1].length;
const endOffset = startOffset + tokenLength;

return {
end: endOffset,
start: startOffset,
};
}, []);

useLexicalTextEntity<TokenNode>(getTokenMatch, TokenNode, $createTokenNode_);

return null;
}
8 changes: 8 additions & 0 deletions packages/ui/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,11 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

.editor .token {
color: blue;
}

.editor.markdown {
min-height: 200px;
}
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 6306e4f

Please sign in to comment.