Skip to content

Commit

Permalink
feat
Browse files Browse the repository at this point in the history
  • Loading branch information
zbeyens committed Dec 18, 2024
1 parent e1d5f93 commit 6759ee3
Show file tree
Hide file tree
Showing 33 changed files with 778 additions and 310 deletions.
4 changes: 4 additions & 0 deletions apps/www/content/docs/components/changelog.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ Use the [CLI](https://platejs.org/docs/components/cli) to install the latest ver

## December 2024 #17

### December 18 #17.2

New RSC components for element and leaf components, filename ending with `-static.tsx`. Those are now installed along with the default client components.

### December 16 #17.1

- `column-element`:
Expand Down
5 changes: 0 additions & 5 deletions apps/www/content/docs/examples/iframe.mdx

This file was deleted.

287 changes: 182 additions & 105 deletions apps/www/content/docs/html.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,190 @@ title: Serializing HTML

<ComponentPreview name="html-demo" />

<PackageInfo>
<Callout className="my-4">
**Note**: Round-tripping is not yet supported: the HTML serializer will not
preserve all information from the Slate value when converting to HTML and
back.
</Callout>

## Slate -> HTML

[Server-side example](/docs/examples/slate-to-html)

### Usage

```tsx
// ...
import { createSlateEditor, serializeHtml } from '@udecode/plate-common';
import { EditorStatic } from '@/components/plate-ui/editor-static';

// Create an editor and configure all the plugins you need
const editor = createSlateEditor({
// ... your plugins ...
});

// Provide the components that map Slate nodes to HTML elements
const components = {
// [ParagraphPlugin.key]: ParagraphElementStatic,
// [HeadingPlugin.key]: HeadingElementStatic,
// ...
};

// You can also pass a custom editor component and props
// For example, EditorStatic is a styled version of PlateStatic.
const html = await serializeHtml(editor, {
components,
editorComponent: EditorStatic, // defaults to PlateStatic if not provided
props: { variant: 'none', className: 'p-2' },
});
```

If you use a custom component, like [EditorStatic](/docs/components/editor), you must also ensure that all required styles and classes are included in your final HTML file. Since serialize only returns the inner editor HTML, you may need to wrap it in a full HTML document with any external CSS, scripts, or `<style>` tags.

For example:

```tsx
// After serializing the content:
const html = await serializeHtml(editor, {
components,
editorComponent: EditorStatic,
props: { variant: 'none' },
});

// Wrap the HTML in a full HTML document
const fullHtml = `<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="/path/to/tailwind.css" />
<!-- other head elements -->
</head>
<body>
${html}
</body>
</html>`;
```

<Callout className="my-4"> **Note**: The serialization process converts Slate nodes into static HTML markup. Interactivity like React event handlers, client-side hooks, or components that rely on browser APIs won't work in the serialized output. </Callout>

### Using Static Components

When serializing Slate content to HTML, **you must use static versions of your components**. Your interactive, "client" components often rely on browser APIs or client-side React features that are not available during server-side rendering (SSR).

**What does this mean?**
- **Interactive Components:** Components that use `use client`, event handlers (`onClick`), or browser-only APIs (like `window` or `document`) cannot run on the server.
- **Static Components:** Instead, for serialization, you must provide "static" versions of these components. Static components should:
- Not include any event handlers like `onClick`.
- Not rely on browser APIs or client-side hooks.
- Simply render the content as plain HTML.

**Example:**

If you have a client-only component like `ImageElement` that uses `use client` and event handlers, you need to create a static version `ImageElementStatic` that just returns a plain `<img>` tag with all its attributes and no interactivity.

For each plugin key, provide its corresponding static component in the `components` object passed to `serializeHtml`:

```tsx
// Instead of using interactive components that rely on 'use client',
// use statically defined components that simply return plain HTML.

import { createSlateEditor, serializeHtml } from '@udecode/plate-common';

// Import static versions of your components
import { ParagraphElementStatic } from '@/components/plate-ui/paragraph-element-static';
import { HeadingElementStatic } from '@/components/plate-ui/heading-element-static';
import { ImageElementStatic } from '@/components/plate-ui/image-element-static';
// ... and so on for each plugin ...

const editor = createSlateEditor({ /* ...plugins... */ });

const components = {
[BaseParagraphPlugin.key]: ParagraphElementStatic,
[BaseHeadingPlugin.key]: HeadingElementStatic,
[BaseImagePlugin.key]: ImageElementStatic,
// ... other static components ...
};

const html = await serializeHtml(editor, { components });
```

### PlateStatic vs. Plate

When rendering your editor output without the need for an interactive environment, you have two primary options: `PlateStatic` and `Plate`. Both are designed to produce static, non-editable views of your content, but they differ in purpose and performance characteristics.

**PlateStatic**
- **Purpose:** A more lightweight, server-compatible static renderer of the editor content.
- **Performance:** Generally more performant than `Plate` in read-only mode.
- **Use Case:** Ideal when you need a static preview of the content (e.g., in a server-side render or a static build). `PlateStatic` should be the default choice if you want a fast, static representation of your content.

**Plate (Read-Only)**
- **Purpose:** The standard `Plate` editor component used with `readOnly={true}`. While this prevents editing, it still operates as a client component relying on browser APIs.
- **Use Case:** Suitable if you’re rendering in a browser and want an interactive environment (e.g. comments) that just happens to be non-editable. It’s **not** suitable for server-side HTML serialization or scenarios where static output is required, as it still runs client-side code.

**In summary:**

- **If you need high performance, a static view or HTML serialization:** Use `PlateStatic`.
- **If you need an interactive environment and can run in a browser, but just want it read-only:** Use `Plate` with `readOnly={true}`.

### API

#### serializeHtml

Convert Slate Nodes into HTML string.

<APIParameters>
<APIItem name="options" type="object">

Options to control the HTML serialization process.

<APISubList>
<APISubListItem parent="options" name="components" type="NodeComponents">

A mapping of plugin keys to React components. Each component renders the corresponding Slate node as HTML.

</APISubListItem>

<APISubListItem parent="options" name="editorComponent" type="React.ComponentType<T>" optional>

A React component to render the entire editor content. Defaults to `PlateStatic` if not provided. This component receives `components`, `editor`, and `props`.

</APISubListItem>

<APISubListItem parent="options" name="props" type="Partial<T>" optional>

Props to pass to the `editorComponent`. The generic type `T` extends `PlateStaticProps`.

</APISubListItem>

<APISubListItem parent="options" name="preserveClassNames" type="string[]" optional>

A list of class name prefixes to preserve if `stripClassNames` is enabled.

</APISubListItem>

<APISubListItem parent="options" name="stripClassNames" type="boolean" optional>

If `true`, remove class names from the output HTML except those listed in `preserveClassNames`.

- **Default:** `false`

</APISubListItem>

<APISubListItem parent="options" name="stripDataAttributes" type="boolean" optional>

Many Plate plugins include HTML deserialization rules. These rules define how HTML elements and styles are mapped to Plate's node types and attributes.
If `true`, remove `data-*` attributes from the output HTML.

</PackageInfo>
- **Default:** `false`

</APISubListItem>
</APISubList>
</APIItem>
</APIParameters>

<APIReturns>
<APIItem type="string">
A HTML string representing the Slate content.
</APIItem>
</APIReturns>

## HTML -> Slate

Expand Down Expand Up @@ -207,105 +386,3 @@ Flag to enable or disable the removal of whitespace from the serialized HTML.
The deserialized Slate value.
</APIItem>
</APIReturns>

## Slate -> React -> HTML

### Installation

```bash
npm install @udecode/plate-html
```

### Usage

```tsx
// ...
import { HtmlReactPlugin } from '@udecode/plate-html/react';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';

const editor = createPlateEditor({
plugins: [
HtmlReactPlugin
// all plugins that you want to serialize
],
override: {
// do not forget to add your custom components, otherwise it won't work
components: createPlateUI(),
},
});

const html = editor.api.htmlReact.serialize({
nodes: editor.children,
// if you use @udecode/plate-dnd
dndWrapper: (props) => <DndProvider backend={HTML5Backend} {...props} />,
});
```

<Callout className="my-4">
**Note**: Round-tripping is not yet supported: the HTML serializer will not
preserve all information from the Slate value when converting to HTML and
back.
</Callout>

### API

#### editor.api.htmlReact.serialize

Convert Slate Nodes into HTML string.

<APIParameters>
<APIItem name="options" type="object">

Options to control the HTML serialization process.

<APISubList>
<APISubListItem parent="options" name="nodes" type="DescendantOf<E>[]">

The Slate nodes to convert into HTML.

</APISubListItem>
<APISubListItem parent="options" name="stripDataAttributes" type="boolean" optional>

Flag to enable or disable the removal of data attributes from the serialized HTML.

- **Default:** `true` (Data attributes will be removed.)

</APISubListItem>
<APISubListItem parent="options" name="preserveClassNames" type="string[]" optional>

A list of class name prefixes that should not be stripped out from the serialized HTML.

</APISubListItem>
<APISubListItem parent="options" name="slateProps" type="Partial<SlateProps>" optional>

Additional Slate properties to provide, in case the rendering process depends on certain Slate hooks.

</APISubListItem>
<APISubListItem parent="options" name="stripWhitespace" type="boolean" optional>

Flag to enable or disable the removal of whitespace from the serialized HTML.

- **Default:** `true` (Whitespace will be removed.)

</APISubListItem>
<APISubListItem parent="options" name="convertNewLinesToHtmlBr" type="boolean" optional>

Optionally convert newline characters (`\n`) to HTML `<br />` tags.

- **Default:** `false` (Newline characters will not be converted.)

</APISubListItem>
<APISubListItem parent="options" name="dndWrapper" type="string | FunctionComponent | ComponentClass" optional>

Specifies a component to be used for wrapping the rendered elements during a drag-and-drop operation.

</APISubListItem>
</APISubList>
</APIItem>
</APIParameters>
<APIReturns>
<APIItem type="string">
A HTML string representing the Slate nodes.
</APIItem>
</APIReturns>
2 changes: 1 addition & 1 deletion apps/www/public/r/styles/default/column-element.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
},
"files": [
{
"content": "'use client';\n\nimport React from 'react';\n\nimport type { TColumnElement } from '@udecode/plate-layout';\n\nimport { cn, withRef } from '@udecode/cn';\nimport { useElement, withHOC } from '@udecode/plate-common/react';\nimport { ResizableProvider } from '@udecode/plate-resizable';\nimport { useReadOnly } from 'slate-react';\n\nimport { PlateElement } from './plate-element';\n\nexport const ColumnElement = withHOC(\n ResizableProvider,\n withRef<typeof PlateElement>(({ children, className, ...props }, ref) => {\n const readOnly = useReadOnly();\n const { width } = useElement<TColumnElement>();\n\n return (\n <PlateElement\n ref={ref}\n className={cn(\n className,\n 'border border-transparent p-1.5',\n !readOnly && 'rounded-lg border-dashed'\n )}\n style={{ width: width ?? '100%' }}\n {...props}\n >\n {children}\n </PlateElement>\n );\n })\n);\n",
"content": "'use client';\n\nimport React from 'react';\n\nimport type { TColumnElement } from '@udecode/plate-layout';\n\nimport { cn, useComposedRef, withRef } from '@udecode/cn';\nimport { useElement, withHOC } from '@udecode/plate-common/react';\nimport {\n useDraggable,\n useDraggableState,\n useDropLine,\n} from '@udecode/plate-dnd';\nimport { ResizableProvider } from '@udecode/plate-resizable';\nimport { GripHorizontal } from 'lucide-react';\nimport { Path } from 'slate';\nimport { useReadOnly } from 'slate-react';\n\nimport { Button } from './button';\nimport { PlateElement } from './plate-element';\nimport {\n Tooltip,\n TooltipContent,\n TooltipPortal,\n TooltipProvider,\n TooltipTrigger,\n} from './tooltip';\n\nconst DRAG_ITEM_COLUMN = 'column';\n\nexport const ColumnElement = withHOC(\n ResizableProvider,\n withRef<typeof PlateElement>(({ children, className, ...props }, ref) => {\n const readOnly = useReadOnly();\n const { width } = useElement<TColumnElement>();\n\n const state = useDraggableState({\n canDropNode: ({ dragEntry, dropEntry }) =>\n Path.equals(Path.parent(dragEntry[1]), Path.parent(dropEntry[1])),\n element: props.element,\n orientation: 'horizontal',\n type: DRAG_ITEM_COLUMN,\n });\n\n const { previewRef, handleRef } = useDraggable(state);\n\n return (\n <div className=\"group/column relative\" style={{ width: width ?? '100%' }}>\n <div\n ref={handleRef}\n className={cn(\n 'absolute left-1/2 top-2 z-50 -translate-x-1/2 -translate-y-1/2',\n 'pointer-events-auto flex items-center',\n 'opacity-0 transition-opacity group-hover/column:opacity-100'\n )}\n >\n <ColumnDragHandle />\n </div>\n\n <PlateElement\n ref={useComposedRef(ref, previewRef)}\n className={cn(\n className,\n 'h-full px-2 pt-2 group-first/column:pl-0 group-last/column:pr-0'\n )}\n {...props}\n >\n <div\n className={cn(\n 'relative h-full border border-transparent p-1.5',\n !readOnly && 'rounded-lg border-dashed border-border',\n state.isDragging && 'opacity-50'\n )}\n >\n {children}\n <DropLine />\n </div>\n </PlateElement>\n </div>\n );\n })\n);\n\nconst ColumnDragHandle = React.memo(() => {\n return (\n <TooltipProvider>\n <Tooltip>\n <TooltipTrigger asChild>\n <Button size=\"none\" variant=\"ghost\" className=\"h-5 px-1\">\n <GripHorizontal\n className=\"size-4 text-muted-foreground\"\n onClick={(event) => {\n event.stopPropagation();\n event.preventDefault();\n }}\n />\n </Button>\n </TooltipTrigger>\n <TooltipPortal>\n <TooltipContent>Drag to move column</TooltipContent>\n </TooltipPortal>\n </Tooltip>\n </TooltipProvider>\n );\n});\n\nconst DropLine = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => {\n const state = useDropLine({ orientation: 'horizontal' });\n\n if (!state.dropLine) return null;\n\n return (\n <div\n ref={ref}\n {...props}\n // eslint-disable-next-line tailwindcss/no-custom-classname\n className={cn(\n 'slate-dropLine',\n 'absolute bg-brand/50',\n state.dropLine === 'left' &&\n 'inset-y-0 left-[-10.5px] w-1 group-first/column:-left-1',\n state.dropLine === 'right' &&\n 'inset-y-0 right-[-11px] w-1 group-last/column:-right-1',\n className\n )}\n />\n );\n});\n",
"path": "plate-ui/column-element.tsx",
"target": "components/plate-ui/column-element.tsx",
"type": "registry:ui"
Expand Down
Loading

0 comments on commit 6759ee3

Please sign in to comment.