Skip to content

Commit

Permalink
1. fix: equation input undo not working
Browse files Browse the repository at this point in the history
2. feat: add equation and inline-equation elements in editor and slashmenu
  • Loading branch information
wststone committed Dec 26, 2024
1 parent 63c4652 commit cb44395
Show file tree
Hide file tree
Showing 6 changed files with 273 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ import { HorizontalRulePlugin } from '@udecode/plate-horizontal-rule/react';
import { KbdPlugin } from '@udecode/plate-kbd/react';
import { ColumnItemPlugin, ColumnPlugin } from '@udecode/plate-layout/react';
import { LinkPlugin } from '@udecode/plate-link/react';
import {
EquationPlugin,
InlineEquationPlugin,
} from '@udecode/plate-math/react';
import {
AudioPlugin,
FilePlugin,
Expand Down Expand Up @@ -92,6 +96,8 @@ import { TableRowElement } from '@/registry/default/plate-ui/table-row-element';
import { TocElement } from '@/registry/default/plate-ui/toc-element';
import { ToggleElement } from '@/registry/default/plate-ui/toggle-element';

import { EquationElement } from '../../plate-ui/equation-element';
import { InlineEquationElement } from '../../plate-ui/inline-equation-element';
import { editorPlugins, viewPlugins } from './plugins/editor-plugins';

export const viewComponents = {
Expand All @@ -106,6 +112,7 @@ export const viewComponents = {
[ColumnPlugin.key]: ColumnGroupElement,
[CommentsPlugin.key]: CommentLeaf,
[DatePlugin.key]: DateElement,
[EquationPlugin.key]: EquationElement,
[FilePlugin.key]: MediaFileElement,
[HEADING_KEYS.h1]: withProps(HeadingElement, { variant: 'h1' }),
[HEADING_KEYS.h2]: withProps(HeadingElement, { variant: 'h2' }),
Expand All @@ -116,6 +123,7 @@ export const viewComponents = {
[HighlightPlugin.key]: HighlightLeaf,
[HorizontalRulePlugin.key]: HrElement,
[ImagePlugin.key]: ImageElement,
[InlineEquationPlugin.key]: InlineEquationElement,
[ItalicPlugin.key]: withProps(PlateLeaf, { as: 'em' }),
[KbdPlugin.key]: KbdLeaf,
[LinkPlugin.key]: LinkElement,
Expand Down
79 changes: 79 additions & 0 deletions apps/www/src/registry/default/plate-ui/equation-element.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
'use client';

import React, { useRef, useState } from 'react';

import type { TEquationElement } from '@udecode/plate-math';

import { cn, withRef } from '@udecode/cn';
import { useElement } from '@udecode/plate-common/react';
import { useEquationElement } from '@udecode/plate-math/react';
import { RadicalIcon } from 'lucide-react';

import { EquationPopoverContent } from './equation-popover';
import { PlateElement } from './plate-element';
import { Popover, PopoverTrigger } from './popover';

export const EquationElement = withRef<typeof PlateElement>(
({ children, className, ...props }, ref) => {
const element = useElement<TEquationElement>();

const [open, setOpen] = useState(false);
const katexRef = useRef<HTMLDivElement | null>(null);

useEquationElement({
element,
katexRef: katexRef,
options: {
displayMode: true,
errorColor: '#cc0000',
fleqn: false,
leqno: false,
macros: { '\\f': '#1f(#2)' },
output: 'htmlAndMathml',
strict: 'warn',
throwOnError: false,
trust: false,
},
});

return (
<PlateElement
ref={ref}
className={cn('relative my-1', className)}
{...props}
>
<Popover open={open} onOpenChange={setOpen} modal={false}>
<PopoverTrigger asChild>
<div
className={cn(
'group flex cursor-pointer select-none items-center justify-center rounded-sm hover:bg-primary/10',
element.texExpression.length === 0
? 'bg-muted p-3 pr-9'
: 'px-2 py-1'
)}
contentEditable={false}
role="button"
>
{element.texExpression.length > 0 ? (
<span ref={katexRef} />
) : (
<div className="flex h-7 w-full items-center gap-2 whitespace-nowrap text-sm text-muted-foreground">
<RadicalIcon className="size-6 text-muted-foreground/80" />
<div>Add a Tex equation</div>
</div>
)}
</div>
</PopoverTrigger>

<EquationPopoverContent
placeholder={`f(x) = \\begin{cases}\n x^2, &\\quad x > 0 \\\\\n 0, &\\quad x = 0 \\\\\n -x^2, &\\quad x < 0\n\\end{cases}`}
isInline={false}
setOpen={setOpen}
/>
</Popover>

{children}
</PlateElement>
);
}
);
83 changes: 83 additions & 0 deletions apps/www/src/registry/default/plate-ui/equation-popover.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
'use client';

import React, { useEffect } from 'react';
import TextareaAutosize, {
type TextareaAutosizeProps,
} from 'react-textarea-autosize';

import type { TEquationElement } from '@udecode/plate-math';

import { cn } from '@udecode/cn';
import {
createPrimitiveComponent,
selectSiblingNodePoint,
useEditorRef,
useElement,
} from '@udecode/plate-common/react';
import { useEquationInput } from '@udecode/plate-math/react';
import { BlockSelectionPlugin } from '@udecode/plate-selection/react';
import { CornerDownLeftIcon } from 'lucide-react';
import { useReadOnly, useSelected } from 'slate-react';

import { Button } from './button';
import { PopoverContent } from './popover';

const EquationInput = createPrimitiveComponent(TextareaAutosize)({
propsHook: useEquationInput,
});

const EquationPopoverContent = ({
className,
isInline,
setOpen,
...props
}: {
isInline: boolean;
setOpen: (open: boolean) => void;
} & TextareaAutosizeProps) => {
const editor = useEditorRef();
const readOnly = useReadOnly();
const element = useElement<TEquationElement>();
const selected = useSelected();

useEffect(() => {
setOpen(selected);
}, [selected, setOpen]);

if (readOnly) return null;

const onClose = () => {
setOpen(false);

if (isInline) {
selectSiblingNodePoint(editor, { node: element });
} else {
editor
.getApi(BlockSelectionPlugin)
.blockSelection.addSelectedRow(element.id as string);
}
};

return (
<PopoverContent
className="flex gap-2"
onEscapeKeyDown={(e) => {
e.preventDefault();
}}
contentEditable={false}
>
<EquationInput
className={cn('max-h-[50vh] grow resize-none p-2 text-sm', className)}
state={{ isInline, open: true, onClose }}
autoFocus
{...props}
/>

<Button variant="secondary" className="px-3" onClick={onClose}>
Done <CornerDownLeftIcon className="size-3.5" />
</Button>
</PopoverContent>
);
};

export { EquationPopoverContent };
88 changes: 88 additions & 0 deletions apps/www/src/registry/default/plate-ui/inline-equation-element.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
'use client';

import { useRef, useState } from 'react';

import type { TEquationElement } from '@udecode/plate-math';

import { cn, withRef } from '@udecode/cn';
import { useElement } from '@udecode/plate-common/react';
import { useEquationElement } from '@udecode/plate-math/react';
import { RadicalIcon } from 'lucide-react';

import { EquationPopoverContent } from './equation-popover';
import { PlateElement } from './plate-element';
import { Popover, PopoverTrigger } from './popover';

export const InlineEquationElement = withRef<typeof PlateElement>(
({ children, className, ...props }, ref) => {
const element = useElement<TEquationElement>();
const katexRef = useRef<HTMLDivElement | null>(null);
const [open, setOpen] = useState(false);

useEquationElement({
element,
katexRef: katexRef,
options: {
displayMode: true,
errorColor: '#cc0000',
fleqn: false,
leqno: false,
macros: { '\\f': '#1f(#2)' },
output: 'htmlAndMathml',
strict: 'warn',
throwOnError: false,
trust: false,
},
});

return (
<PlateElement
ref={ref}
className={cn(
'inline-block select-none rounded-sm [&_.katex-display]:my-0',
className
)}
{...props}
>
<Popover open={open} onOpenChange={setOpen} modal={false}>
<PopoverTrigger asChild>
<div
className={cn(
'after:absolute after:inset-0 after:-left-1 after:-top-0.5 after:z-[1] after:h-[calc(100%)+4px] after:w-[calc(100%+8px)] after:rounded-sm after:content-[""]',
'h-6',
element.texExpression.length > 0 && open && 'after:bg-brand/15',
element.texExpression.length === 0 &&
'text-muted-foreground after:bg-neutral-500/10',
className
)}
contentEditable={false}
>
<span
ref={katexRef}
className={cn(
element.texExpression.length === 0 && 'hidden',
'font-mono leading-none'
)}
/>
{element.texExpression.length === 0 && (
<span>
<RadicalIcon className="mr-1 inline-block h-[19px] w-4 py-[1.5px] align-text-bottom" />
New equation
</span>
)}
</div>
</PopoverTrigger>

<EquationPopoverContent
className="my-auto"
placeholder="E = mc^2"
setOpen={setOpen}
isInline
/>
</Popover>

{children}
</PlateElement>
);
}
);
15 changes: 15 additions & 0 deletions apps/www/src/registry/default/plate-ui/slash-input-element.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import { DatePlugin } from '@udecode/plate-date/react';
import { HEADING_KEYS } from '@udecode/plate-heading';
import { TocPlugin } from '@udecode/plate-heading/react';
import { INDENT_LIST_KEYS, ListStyleType } from '@udecode/plate-indent-list';
import {
EquationPlugin,
InlineEquationPlugin,
} from '@udecode/plate-math/react';
import { TablePlugin } from '@udecode/plate-table/react';
import { TogglePlugin } from '@udecode/plate-toggle/react';
import {
Expand All @@ -25,6 +29,7 @@ import {
ListOrdered,
PilcrowIcon,
Quote,
RadicalIcon,
SparklesIcon,
Square,
Table,
Expand Down Expand Up @@ -167,6 +172,11 @@ const groups: Group[] = [
label: '3 columns',
value: 'action_three_columns',
},
{
icon: <RadicalIcon />,
label: 'Equation',
value: EquationPlugin.key,
},
].map((item) => ({
...item,
onSelect: (editor, value) => {
Expand All @@ -184,6 +194,11 @@ const groups: Group[] = [
label: 'Date',
value: DatePlugin.key,
},
{
icon: <RadicalIcon />,
label: 'Equation',
value: InlineEquationPlugin.key,
},
].map((item) => ({
...item,
onSelect: (editor, value) => {
Expand Down
4 changes: 0 additions & 4 deletions packages/math/src/react/hooks/useEquationInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ export const useEquationInput = ({
const editor = useEditorRef();
const element = useElement<TEquationElement>();
const inputRef = useRef<HTMLTextAreaElement>(null);

const [expressionInput, setExpressionInput] = React.useState<string>(
element.texExpression
);
Expand Down Expand Up @@ -79,9 +78,6 @@ export const useEquationInput = ({
} else if (isHotkey('escape')(e)) {
e.preventDefault();
onDismiss();
} else if (isHotkey('meta+z')(e)) {
e.preventDefault();
editor.undo();
} else if (isHotkey('meta+y')(e) || isHotkey('meta+shift+z')(e)) {
e.preventDefault();
editor.redo();
Expand Down

0 comments on commit cb44395

Please sign in to comment.