Skip to content

Commit

Permalink
docs
Browse files Browse the repository at this point in the history
  • Loading branch information
felixfeng33 committed Nov 18, 2024
1 parent ca70f4a commit 8e50672
Show file tree
Hide file tree
Showing 18 changed files with 326 additions and 136 deletions.
114 changes: 109 additions & 5 deletions apps/www/content/docs/media-placeholder.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
title: Media Placeholder
description: Media placeholders to be used as clickable placeholders for various media types (image, video, audio, file).
docs:
- route: https://pro.platejs.org/docs/components/media-placeholder-element
- route: components/media-placeholder-element
title: Media Placeholder Element
- route: components/media-upload-toast
title: Media Upload Toast
---

<PackageInfo>
Expand All @@ -22,6 +24,7 @@ npm install @udecode/plate-media
```

## Usage
How to configuration the backend see [Upload](/docs/upload).

```tsx
import {
Expand All @@ -37,7 +40,14 @@ import {
```tsx
const plugins = [
// ...otherPlugins,
PlaceholderPlugin,
PlaceholderPlugin.configure({
options: {
disableEmptyPlaceholder: true,
},
render: {
afterEditable: () => <MediaUploadToast />,
},
}),
];
```

Expand All @@ -48,17 +58,111 @@ const components = {
};
```

- [MediaPlaceholderElement](https://pro.platejs.org/docs/components/media-placeholder-element) (Plus)

## UploadOptions

### uploadConfig

Configuration for different file types:

- You can use this option to configure upload limits for each file type, including:

- Maximum file count (e.g. `maxFileCount: 1`)
- Maximum file size (e.g. `maxFileSize: '8MB'`)
- Minimum file count (e.g. `minFileCount: 1`)
- mediaType: Used for passing to the media-placeholder-elements file to distinguish between different file types and their progress bar styles.

default configuration:

```tsx
uploadConfig: {
audio: {
maxFileCount: 1,
maxFileSize: '8MB',
mediaType: AudioPlugin.key,
minFileCount: 1,
},
blob: {
maxFileCount: 1,
maxFileSize: '8MB',
mediaType: FilePlugin.key,
minFileCount: 1,
},
image: {
maxFileCount: 3,
maxFileSize: '4MB',
mediaType: ImagePlugin.key,
minFileCount: 1,
},
pdf: {
maxFileCount: 1,
maxFileSize: '4MB',
mediaType: FilePlugin.key,
minFileCount: 1,
},
text: {
maxFileCount: 1,
maxFileSize: '64KB',
mediaType: FilePlugin.key,
minFileCount: 1,
},
video: {
maxFileCount: 1,
maxFileSize: '16MB',
mediaType: VideoPlugin.key,
minFileCount: 1,
},
},
```

here is all allowed file types (keys for `uploadConfig`):

```tsx
export const ALLOWED_FILE_TYPES = [
'image',
'video',
'audio',
'pdf',
'text',
'blob',
] as const;
```

### disableEmptyPlaceholder

`boolean` (default: `false`)

Disable empty placeholder when no file is selected.

### disableFileDrop

`boolean` (default: `false`)

Whether we can undo to the placeholder after the file upload is complete.

### maxFileCount

`number` (default: `5`)

Maximum number of files that can be uploaded at once.

### multiple

`boolean` (default: `true`)

Whether multiple files can be uploaded in one time.

## Examples

<ComponentPreview name="playground-demo" id="mediaPlaceholder" />

### Plate UI

Work in progress.
Refer to the preview above.

### Plate Plus

<ComponentPreviewPro name="media-placeholder-pro" />
<ComponentPreviewPro name="upload-pro" />

## Plugins

Expand Down
23 changes: 9 additions & 14 deletions apps/www/content/docs/upload.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,27 @@ docs:
title: Upload
---

### UploadThing Integration

<ComponentPreview name="playground-demo" id="upload" />
Make sure you have install the [media-placeholder-element](/docs/components/media-placeholder-element) component and all the dependencies.

{/* ### UploadThing Integration
Set `UPLOADTHING_TOKEN` in your .env file [get one here](https://uploadthing.com/dashboard).

This component uses UploadThing for file uploads. UploadThing provides a simple and efficient way to handle file uploads in your application.

To use UploadThing:
### Using your own backend

1. Set up an UploadThing account and configure your upload endpoints.
2. Install the UploadThing client in your project:
Remove this two folder `lib/uploadthing` and `/api/uploadthing`.

```bash
npm install uploadthing
```
3. Configure the UploadThing client in your application.
For more details on setting up UploadThing, refer to their [documentation](https://docs.uploadthing.com/). */}
Then impelement a similar hooks like `useUploadFile` using your own backend.

## Examples

<ComponentPreview name="playground-demo" id="mediaPlaceholder" />

### Plate UI

Work in progress.

### Plate Plus

<ComponentPreviewPro name="upload-pro" />
<ComponentPreviewPro name="upload-pro" />
2 changes: 1 addition & 1 deletion apps/www/public/r/styles/default/media-toolbar-button.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
},
"files": [
{
"content": "'use client';\n\nimport React, { useCallback, useEffect, useState } from 'react';\n\nimport type { DropdownMenuProps } from '@radix-ui/react-dropdown-menu';\n\nimport { cn } from '@udecode/cn';\nimport { useEditorRef } from '@udecode/plate-core/react';\nimport {\n AudioPlugin,\n FilePlugin,\n ImagePlugin,\n VideoPlugin,\n} from '@udecode/plate-media/react';\nimport { insertNodes } from '@udecode/slate';\nimport { focusEditor } from '@udecode/slate-react';\nimport {\n AudioLinesIcon,\n FileUpIcon,\n FilmIcon,\n ImageIcon,\n LinkIcon,\n} from 'lucide-react';\nimport { useFilePicker } from 'use-file-picker';\n\nimport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n} from './alert-dialog';\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuRadioGroup,\n DropdownMenuRadioItem,\n DropdownMenuTrigger,\n useOpenState,\n} from './dropdown-menu';\nimport { Input } from './input';\nimport {\n ToolbarSplitButton,\n ToolbarSplitButtonPrimary,\n ToolbarSplitButtonSecondary,\n} from './toolbar';\nconst MEDIA_CONFIG: Record<\n string,\n {\n accept: string[];\n icon: React.ReactNode;\n title: string;\n tooltip: string;\n }\n> = {\n [AudioPlugin.key]: {\n accept: ['audio/*'],\n icon: <AudioLinesIcon className=\"size-4\" />,\n title: 'Insert Audio',\n tooltip: 'Audio',\n },\n [FilePlugin.key]: {\n accept: ['*'],\n icon: <FileUpIcon className=\"size-4\" />,\n title: 'Insert File',\n tooltip: 'File',\n },\n [ImagePlugin.key]: {\n accept: ['image/*'],\n icon: <ImageIcon className=\"size-4\" />,\n title: 'Insert Image',\n tooltip: 'Image',\n },\n [VideoPlugin.key]: {\n accept: ['video/*'],\n icon: <FilmIcon className=\"size-4\" />,\n title: 'Insert Video',\n tooltip: 'Video',\n },\n};\n\nexport function MediaToolbarButton({\n children,\n nodeType,\n ...props\n}: DropdownMenuProps & { nodeType: string }) {\n const currentConfig = MEDIA_CONFIG[nodeType];\n\n const editor = useEditorRef();\n const openState = useOpenState();\n\n const { openFilePicker } = useFilePicker({\n accept: currentConfig.accept,\n multiple: true,\n onFilesSelected: ({ plainFiles: updatedFiles }) => {\n (editor as any).tf.insert.media(updatedFiles);\n },\n });\n\n const [dialogOpen, setDialogOpen] = useState(false);\n\n const [url, setUrl] = useState('');\n\n const embedMedia = useCallback(() => {\n setDialogOpen(false);\n insertNodes(editor, {\n children: [{ text: '' }],\n name: nodeType === FilePlugin.key ? url.split('/').pop() : undefined,\n type: nodeType,\n url,\n });\n }, [editor, nodeType, url]);\n\n useEffect(() => {\n if (!dialogOpen) {\n focusEditor(editor);\n setUrl('');\n }\n }, [dialogOpen, editor]);\n\n return (\n <>\n <DropdownMenu {...openState} modal={false} {...props}>\n <ToolbarSplitButton pressed={openState.open}>\n <ToolbarSplitButtonPrimary\n onClick={() => openFilePicker()}\n onMouseDown={(e) => e.preventDefault()}\n tooltip={currentConfig.tooltip}\n >\n {currentConfig.icon}\n </ToolbarSplitButtonPrimary>\n\n <DropdownMenuTrigger asChild>\n <ToolbarSplitButtonSecondary />\n </DropdownMenuTrigger>\n </ToolbarSplitButton>\n\n <DropdownMenuContent\n className={cn('min-w-0 data-[state=closed]:hidden')}\n align=\"start\"\n >\n <DropdownMenuRadioGroup>\n <DropdownMenuRadioItem\n value=\"upload\"\n onSelect={() => openFilePicker()}\n hideIcon\n >\n <div className=\"flex items-center gap-2\">\n {currentConfig.icon}\n <span className=\"text-sm\">Upload from computer</span>\n </div>\n </DropdownMenuRadioItem>\n <DropdownMenuRadioItem\n value=\"url\"\n onSelect={() => setDialogOpen(true)}\n hideIcon\n >\n <div className=\"flex items-center gap-2\">\n <LinkIcon />\n <span className=\"text-sm\">Insert via URL</span>\n </div>\n </DropdownMenuRadioItem>\n </DropdownMenuRadioGroup>\n </DropdownMenuContent>\n </DropdownMenu>\n\n <AlertDialog open={dialogOpen} onOpenChange={setDialogOpen}>\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>{currentConfig.title}</AlertDialogTitle>\n <AlertDialogDescription className=\"flex items-center gap-2\">\n <div className=\"group relative w-full\">\n <label\n className=\"absolute top-1/2 block -translate-y-1/2 cursor-text px-1 text-sm text-muted-foreground/70 transition-all group-focus-within:pointer-events-none group-focus-within:top-0 group-focus-within:cursor-default group-focus-within:text-xs group-focus-within:font-medium group-focus-within:text-foreground has-[+input:not(:placeholder-shown)]:pointer-events-none has-[+input:not(:placeholder-shown)]:top-0 has-[+input:not(:placeholder-shown)]:cursor-default has-[+input:not(:placeholder-shown)]:text-xs has-[+input:not(:placeholder-shown)]:font-medium has-[+input:not(:placeholder-shown)]:text-foreground\"\n htmlFor=\"input-32\"\n >\n <span className=\"inline-flex bg-background px-2\">URL</span>\n </label>\n <Input\n id=\"input-32\"\n value={url}\n onChange={(e) => setUrl(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === 'Enter') embedMedia();\n }}\n placeholder=\"\"\n type=\"email\"\n autoFocus\n />\n </div>\n </AlertDialogDescription>\n </AlertDialogHeader>\n <AlertDialogFooter>\n <AlertDialogCancel>Cancel</AlertDialogCancel>\n <AlertDialogAction onClick={embedMedia}>Accept</AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n </>\n );\n}\n",
"content": "'use client';\n\nimport React, { useCallback, useState } from 'react';\n\nimport type { DropdownMenuProps } from '@radix-ui/react-dropdown-menu';\n\nimport { cn } from '@udecode/cn';\nimport { insertNodes, isUrl } from '@udecode/plate-common';\nimport { useEditorRef } from '@udecode/plate-core/react';\nimport {\n AudioPlugin,\n FilePlugin,\n ImagePlugin,\n VideoPlugin,\n} from '@udecode/plate-media/react';\nimport {\n AudioLinesIcon,\n FileUpIcon,\n FilmIcon,\n ImageIcon,\n LinkIcon,\n} from 'lucide-react';\nimport { toast } from 'sonner';\nimport { useFilePicker } from 'use-file-picker';\n\nimport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n} from './alert-dialog';\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuRadioGroup,\n DropdownMenuRadioItem,\n DropdownMenuTrigger,\n useOpenState,\n} from './dropdown-menu';\nimport { Input } from './input';\nimport {\n ToolbarSplitButton,\n ToolbarSplitButtonPrimary,\n ToolbarSplitButtonSecondary,\n} from './toolbar';\nconst MEDIA_CONFIG: Record<\n string,\n {\n accept: string[];\n icon: React.ReactNode;\n title: string;\n tooltip: string;\n }\n> = {\n [AudioPlugin.key]: {\n accept: ['audio/*'],\n icon: <AudioLinesIcon className=\"size-4\" />,\n title: 'Insert Audio',\n tooltip: 'Audio',\n },\n [FilePlugin.key]: {\n accept: ['*'],\n icon: <FileUpIcon className=\"size-4\" />,\n title: 'Insert File',\n tooltip: 'File',\n },\n [ImagePlugin.key]: {\n accept: ['image/*'],\n icon: <ImageIcon className=\"size-4\" />,\n title: 'Insert Image',\n tooltip: 'Image',\n },\n [VideoPlugin.key]: {\n accept: ['video/*'],\n icon: <FilmIcon className=\"size-4\" />,\n title: 'Insert Video',\n tooltip: 'Video',\n },\n};\n\nexport function MediaToolbarButton({\n children,\n nodeType,\n ...props\n}: DropdownMenuProps & { nodeType: string }) {\n const currentConfig = MEDIA_CONFIG[nodeType];\n\n const editor = useEditorRef();\n const openState = useOpenState();\n\n const { openFilePicker } = useFilePicker({\n accept: currentConfig.accept,\n multiple: true,\n onFilesSelected: ({ plainFiles: updatedFiles }) => {\n (editor as any).tf.insert.media(updatedFiles);\n },\n });\n\n const [dialogOpen, setDialogOpen] = useState(false);\n\n const [url, setUrl] = useState('');\n\n const embedMedia = useCallback(() => {\n if (!isUrl(url)) return toast.error('Invalid URL');\n\n setDialogOpen(false);\n insertNodes(editor, {\n children: [{ text: '' }],\n name: nodeType === FilePlugin.key ? url.split('/').pop() : undefined,\n type: nodeType,\n url,\n });\n }, [url, editor, nodeType]);\n\n return (\n <>\n <DropdownMenu {...openState} modal={false} {...props}>\n <ToolbarSplitButton pressed={openState.open}>\n <ToolbarSplitButtonPrimary\n onClick={() => openFilePicker()}\n onMouseDown={(e) => e.preventDefault()}\n tooltip={currentConfig.tooltip}\n >\n {currentConfig.icon}\n </ToolbarSplitButtonPrimary>\n\n <DropdownMenuTrigger asChild>\n <ToolbarSplitButtonSecondary />\n </DropdownMenuTrigger>\n </ToolbarSplitButton>\n\n <DropdownMenuContent\n className={cn('min-w-0 data-[state=closed]:hidden')}\n align=\"start\"\n >\n <DropdownMenuRadioGroup>\n <DropdownMenuRadioItem\n value=\"upload\"\n onSelect={() => openFilePicker()}\n hideIcon\n >\n <div className=\"flex items-center gap-2\">\n {currentConfig.icon}\n <span className=\"text-sm\">Upload from computer</span>\n </div>\n </DropdownMenuRadioItem>\n <DropdownMenuRadioItem\n value=\"url\"\n onSelect={() => setDialogOpen(true)}\n hideIcon\n >\n <div className=\"flex items-center gap-2\">\n <LinkIcon />\n <span className=\"text-sm\">Insert via URL</span>\n </div>\n </DropdownMenuRadioItem>\n </DropdownMenuRadioGroup>\n </DropdownMenuContent>\n </DropdownMenu>\n\n <AlertDialog\n open={dialogOpen}\n onOpenChange={(value) => {\n setDialogOpen(value);\n setUrl('');\n }}\n >\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>{currentConfig.title}</AlertDialogTitle>\n <AlertDialogDescription className=\"group relative flex w-full items-center gap-2\">\n <label\n className=\"absolute top-1/2 block -translate-y-1/2 cursor-text px-1 text-sm text-muted-foreground/70 transition-all group-focus-within:pointer-events-none group-focus-within:top-0 group-focus-within:cursor-default group-focus-within:text-xs group-focus-within:font-medium group-focus-within:text-foreground has-[+input:not(:placeholder-shown)]:pointer-events-none has-[+input:not(:placeholder-shown)]:top-0 has-[+input:not(:placeholder-shown)]:cursor-default has-[+input:not(:placeholder-shown)]:text-xs has-[+input:not(:placeholder-shown)]:font-medium has-[+input:not(:placeholder-shown)]:text-foreground\"\n htmlFor=\"input-32\"\n >\n <span className=\"inline-flex bg-background px-2\">URL</span>\n </label>\n <Input\n id=\"input-32\"\n value={url}\n onChange={(e) => setUrl(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === 'Enter') embedMedia();\n }}\n placeholder=\"\"\n type=\"email\"\n autoFocus\n />\n </AlertDialogDescription>\n </AlertDialogHeader>\n <AlertDialogFooter>\n <AlertDialogCancel>Cancel</AlertDialogCancel>\n <AlertDialogAction\n onClick={(e) => {\n e.preventDefault();\n embedMedia();\n }}\n >\n Accept\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n </>\n );\n}\n",
"path": "plate-ui/media-toolbar-button.tsx",
"target": "components/plate-ui/media-toolbar-button.tsx",
"type": "registry:ui"
Expand Down
9 changes: 9 additions & 0 deletions apps/www/src/config/customizer-components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,11 @@ export const customizerComponents = {
label: 'Element',
title: 'Media Embed',
},
mediaPlaceholderElement: {
href: '/docs/components/media-placeholder-element',
label: 'Element',
title: 'Media Placeholder',
},
mediaPopover: {
href: '/docs/components/media-popover',
title: 'Media Popover',
Expand All @@ -228,6 +233,10 @@ export const customizerComponents = {
href: '/docs/components/media-toolbar-button',
title: 'Media Toolbar Button',
},
mediaUploadToast: {
href: '/docs/components/media-upload-toast',
title: 'Media Upload Toast',
},
mentionElement: {
href: '/docs/components/mention-element',
label: 'Element',
Expand Down
14 changes: 13 additions & 1 deletion apps/www/src/config/customizer-items.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,11 @@ import { LineHeightPlugin } from '@udecode/plate-line-height/react';
import { LinkPlugin } from '@udecode/plate-link/react';
import { TodoListPlugin } from '@udecode/plate-list/react';
import { MarkdownPlugin } from '@udecode/plate-markdown';
import { ImagePlugin, MediaEmbedPlugin } from '@udecode/plate-media/react';
import {
ImagePlugin,
MediaEmbedPlugin,
PlaceholderPlugin,
} from '@udecode/plate-media/react';
import {
MentionInputPlugin,
MentionPlugin,
Expand Down Expand Up @@ -110,6 +114,14 @@ export type SettingPlugin = {
};

export const customizerItems: Record<string, SettingPlugin> = {
[`media_${PlaceholderPlugin.key}`]: {
id: `media_${PlaceholderPlugin.key}`,
label: 'MediaPlaceholder',
npmPackage: '@udecode/plate-placeholder',
pluginFactory: 'PlaceholderPlugin',
reactImport: true,
route: customizerPlugins.mediaPlaceholder.route,
},
[AIChatPlugin.key]: {
id: AIChatPlugin.key,
badges: [customizerBadges.handler],
Expand Down
Loading

0 comments on commit 8e50672

Please sign in to comment.