diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md deleted file mode 100644 index fc7a852f23..0000000000 --- a/.github/ISSUE_TEMPLATE/bug.md +++ /dev/null @@ -1,52 +0,0 @@ ---- - -name: "🚨 Bug" -about: Report a bug that occurs in plate -title: '' -labels: bug -assignees: '' - ---- - -## Description - - - - - -## Steps to Reproduce - - - - - - -## Sandbox - - - - - -## Expected Behavior - - - - - -## Environment - -- slate: -- slate-react: -- browser: chrome - -## Bounty - -Click [here](https://console.algora.io/org/Udecode/bounties/new) to add a bounty via Algora. diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 0000000000..205ea7f732 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,75 @@ +name: "Bug Report" +description: Create a new ticket for a bug. +labels: ["bug"] +body: + - type: textarea + id: description + attributes: + label: "Description" + description: Please enter an explicit description of your issue + placeholder: Short and explicit description of your incident... + validations: + required: true + - type: input + id: reprod-url + attributes: + label: "Reproduction URL" + description: Please enter your GitHub or Sandbox URL to provide a reproduction of the issue + placeholder: ex. https://github.com/USERNAME/REPO-NAME + - type: textarea + id: reprod + attributes: + label: "Reproduction steps" + description: Please enter an explicit description of your issue + value: | + 1. Go to '...' + 2. Click on '....' + 3. Scroll down to '....' + 4. See error + render: bash + validations: + required: true + - type: input + id: plate + attributes: + label: "Plate version" + description: Please give the version of plate + placeholder: ex. 33.0.0 + validations: + required: true + - type: input + id: slate-react + attributes: + label: "Slate React version" + description: Please give the version of slate-react + placeholder: ex. 0.103.0 + validations: + required: true + - type: textarea + id: screenshot + attributes: + label: "Screenshots" + description: If applicable, add screenshots to help explain your problem. + render: bash + validations: + required: false + - type: textarea + id: logs + attributes: + label: "Logs" + description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. + render: bash + validations: + required: false + - type: dropdown + id: browsers + attributes: + label: "Browsers" + description: What browsers are you seeing the problem on ? + multiple: true + options: + - Chrome + - Firefox + - Safari + validations: + required: false diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 8b8de42f33..4403362f49 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,16 +1,2 @@ -**Description** - -See changesets. - - - - - - - - - - - +- [ ] added [changesets](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md) +- [ ] updated [components changelog](https://github.com/udecode/plate/blob/main/apps/www/content/docs/components/changelog.mdx) diff --git a/apps/www/content/docs/combobox.mdx b/apps/www/content/docs/combobox.mdx index 032f83049c..160542e670 100644 --- a/apps/www/content/docs/combobox.mdx +++ b/apps/www/content/docs/combobox.mdx @@ -1,249 +1,94 @@ --- title: Combobox -description: Select options from a list of predefined values. -docs: - - route: /docs/components/combobox - title: Combobox - - route: /docs/components/emoji-combobox - title: Emoji Combobox - - route: /docs/components/mention-combobox - title: Mention Combobox +description: Utilities for adding comboboxes to your editor. --- - +## TriggerComboboxPlugin -## Features +The TriggerComboboxPlugin mixin configures your plugin to insert a combobox input element when the user types a specified trigger character is typed. -- Displays a combobox for selecting options from a set list. -- Suitable for creating mentions, tags, or slash commands. -- Works in conjunction with the [Mention plugin](/docs/mention). +For example, the [Mention](/docs/mention) plugin uses TriggerComboboxPlugin to insert an `ELEMENT_MENTION_INPUT` whenever the user types `@`. -**Activation Conditions:** +### Usage -- Collapsed text selection. -- Cursor placement immediately after the trigger character. -- Text without spaces follows the trigger character. + -**On Activation:** - -- Sets `id`, `text`, and `targetRange` in the combobox store. - - - -## Installation - -```bash -npm install @udecode/plate-combobox -``` - -## Usage - -```tsx -import { createComboboxPlugin } from '@udecode/plate-combobox'; -import { createMentionPlugin } from '@udecode/plate-mention'; - -const plugins = [ - // ...otherPlugins, - createComboboxPlugin(), - createMentionPlugin(), -]; -``` - -Then render the combobox component inside `Plate`. You can use the [Combobox component](/docs/components/combobox) or create your own. - -## Keyboard Interactions - - - - Highlights the next item in the list. - - - Highlights the previous item in the list. - - Closes the combobox. - - Selects the currently highlighted item. - - - Selects the currently highlighted item. - - - -## API - -### createComboboxPlugin - -### ComboboxProps - -Here are some key aspects of the **`Combobox`**: - -- Multiple Instances: You can render the **`Combobox`** multiple times, each with its unique configuration provided by a different **`ComboboxStateById`**. -- Singleton Behavior: Only one **`Combobox`** can be opened at a time. The state of the active **`Combobox`** is stored in the **`comboboxStore`**. -- Extends **`ComboboxState`**, **`ComboboxStateById`**: - - - -The items for the combobox. An alternative to setting the items is to use -`comboboxActions.items(items)`. - - -A component that is rendered when the combobox is open. Useful for injecting -hooks. - - -A function to render the combobox item. - -- **Default:** item text - - - - -The combobox item. - - - - -The search text. - - - - - - - -The element to which the combobox is rendered. - -- **Default:** `document.body` - - - - - -### ComboboxState - -Represents a combobox's state. The state resides in `comboboxStore`, which uses the [zustood store](https://github.com/udecode/zustood). - - - -Opened combobox ID. - - -A collection of combobox configuration stores, each identified by a unique combobox ID (e.g., one for tags, one for mentions). - -- `ComboboxStateById`: - - - - -Combobox ID. - - -An optional function to filter items by text. - -- **Default:** A function that checks if the item's text begins with the search text. It compares lowercase strings. + +Extend your plugin options type with TriggerComboboxPlugin. ```ts -(search: string) => (item: TComboboxItem) => boolean; +interface MyPlugin extends TriggerComboboxPlugin {} +const createMyPlugin = createPluginFactory({ + // ... +}); + +// Or simply: +const createMyPlugin = createPluginFactory({ + // ... +}); ``` + - - - -An optional function that sorts filtered items before applying `maxSuggestions`. + +Add the withTriggerCombobox override and specify default values for the required options. (See below for the full list of options). ```ts -(search: string) => (a: TComboboxItem, b: TComboboxItem) => - number; +const createMyPlugin = createPluginFactory({ + // ... + withOverrides: withTriggerCombobox, + options: { + createComboboxInput: (trigger) => ({ + children: [{ text: '' }], + trigger, + type: ELEMENT_MY_INPUT, + }), + trigger: '@', + triggerPreviousCharPattern: /^\s?$/, + }, +}); ``` + - - -The maximum number of suggestions to be shown. - -- **Default:** The length of the **`items`** array. - - - - -The trigger character to activate the combobox. - - -An optional regular expression for searching, for example, to allow whitespaces. - - -An optional callback function invoked when an item is selected. + +Define your input element as an inline void element. It's often useful to do this inside a nested plugin. ```ts -(editor: PlateEditor, item: TComboboxItem) => any; +const createMyPlugin = createPluginFactory({ + // ... + plugins: [ + { + isElement: true, + isInline: true, + isVoid: true, + key: ELEMENT_MY_INPUT, + }, + ], +}); ``` - - -Indicates if the opening/closing of the combobox is controlled by the client. - - - - - -The list of unfiltered items. - - -The list of filtered items. - - -The index of the currently highlighted item. - - -The range from the trigger to the cursor. - - -The text that appears after the trigger. - +The input element component can be built using [Inline Combobox](/docs/components/inline-combobox). + - + -### TComboboxItem +### Options -The data structure representing a single item in a combobox. + - - -A unique key for the item. - - -The text of the item. + + A function to create the input node. - -Indicates whether the item is disabled. -- **Default:** `false` + + The character that triggers the combobox. + + + Only trigger the combobox if the char before the trigger character matches a regular expression. For example, `/^\s?$/` matches beginning of the line or a space. - -Data available to `onRenderItem`. + + + A query function to enable the behavior. - - -## API Components - -### useComboboxContent - -A behavior hook for the `ComboboxContent` component. - - - - The items for the combobox. - - - The combobox store. - - - - - - The menu props for the combobox content. - - - The target range of the combobox. - - + + diff --git a/apps/www/content/docs/components/changelog.mdx b/apps/www/content/docs/components/changelog.mdx index 07c1d6f2b7..7b3149a6c6 100644 --- a/apps/www/content/docs/components/changelog.mdx +++ b/apps/www/content/docs/components/changelog.mdx @@ -8,6 +8,17 @@ Since Plate UI is not a component library, a changelog is maintained here. Use the [CLI](https://platejs.org/docs/components/cli) to install the latest version of the components. +## June 2024 #11 + +### June 6 #11.1 + +- replace `combobox` with `inline-combobox` +- remove `mention-combobox`, `emoji-combobox` and `slash-combobox` +- add `emoji-input-element` +- update `mention-input-element` and `slash-input-element` to use the new combobox +- feat `draggable`: add the data-key attribute to facilitate adding selection after the drag-and-drop operation is completed +- breaking change: add `CaptionProvider` on `image-element` and `media-embed-element`. Open the caption by using `CaptionButton` or `useCaptionStore().set.showCaption()` + ## May 2024 #10 ### May 30 #10.2 diff --git a/apps/www/content/docs/components/emoji-combobox.mdx b/apps/www/content/docs/components/emoji-input-element.mdx similarity index 75% rename from apps/www/content/docs/components/emoji-combobox.mdx rename to apps/www/content/docs/components/emoji-input-element.mdx index a46401ee53..49a74b1d8c 100644 --- a/apps/www/content/docs/components/emoji-combobox.mdx +++ b/apps/www/content/docs/components/emoji-input-element.mdx @@ -1,10 +1,8 @@ --- -title: Emoji Combobox -description: Enter and select emojis using a combination of text input and a dropdown menu. +title: Emoji Input Element +description: Search for an emoji using an inline combobox. component: true docs: - - route: /docs/combobox - title: Combobox - route: /docs/emoji title: Emoji --- @@ -20,7 +18,7 @@ docs: ```bash -npx @udecode/plate-ui@latest add emoji-combobox +npx @udecode/plate-ui@latest add emoji-input-element ``` @@ -33,6 +31,7 @@ npx @udecode/plate-ui@latest add emoji-combobox Install the following dependencies: +- [Emoji](/docs/emoji) - [Combobox](/docs/combobox) @@ -43,7 +42,7 @@ Copy and paste the following code into your project. - + diff --git a/apps/www/content/docs/components/combobox.mdx b/apps/www/content/docs/components/inline-combobox.mdx similarity index 58% rename from apps/www/content/docs/components/combobox.mdx rename to apps/www/content/docs/components/inline-combobox.mdx index b9033d28f6..10ecc0a78e 100644 --- a/apps/www/content/docs/components/combobox.mdx +++ b/apps/www/content/docs/components/inline-combobox.mdx @@ -1,6 +1,6 @@ --- -title: Combobox -description: Combine a text input field with a dropdown menu for enhanced user interaction. +title: Inline Combobox +description: Enhance inline nodes with accessible comboboxes. component: true docs: - route: /docs/combobox @@ -18,7 +18,7 @@ docs: ```bash -npx @udecode/plate-ui@latest add combobox +npx @udecode/plate-ui@latest add inline-combobox ``` @@ -31,12 +31,18 @@ npx @udecode/plate-ui@latest add combobox Install the following dependencies: +- [Combobox](/docs/combobox) + + + + + +Install the Combobox component from Ariakit: + ```bash -npm install @radix-ui/react-popover @udecode/plate-floating +npm install @ariakit/react ``` -- [Combobox](/docs/combobox) - @@ -45,7 +51,7 @@ Copy and paste the following code into your project. - + @@ -59,9 +65,8 @@ Update the import paths to match your project setup. -## Examples +## Usage - +See [Mention Input](/docs/components/mention-input-element) for an example of how to use Inline Combobox. - - + diff --git a/apps/www/content/docs/components/mention-combobox.mdx b/apps/www/content/docs/components/mention-combobox.mdx deleted file mode 100644 index 1687d201d4..0000000000 --- a/apps/www/content/docs/components/mention-combobox.mdx +++ /dev/null @@ -1,65 +0,0 @@ ---- -title: Mention Combobox -description: Enter and select mentions or references using a combination of text input and a dropdown menu. -component: true -docs: - - route: /docs/combobox - title: Combobox - - route: /docs/mention - title: Mention ---- - -## Installation - - - - -CLI -Manual - - - -```bash -npx @udecode/plate-ui@latest add mention-combobox -``` - - - - - - - - - -Install the following dependencies: - -- [Combobox](/docs/combobox) -- [Mention](/docs/mention) - - - - - -Copy and paste the following code into your project. - - - - - - - -Update the import paths to match your project setup. - - - - - - - - - -## Examples - - - - diff --git a/apps/www/content/docs/components/mention-input-element.mdx b/apps/www/content/docs/components/mention-input-element.mdx index f97f05e807..f2ec269df9 100644 --- a/apps/www/content/docs/components/mention-input-element.mdx +++ b/apps/www/content/docs/components/mention-input-element.mdx @@ -31,7 +31,8 @@ npx @udecode/plate-ui@latest add mention-input-element Install the following dependencies: -- [Mention](/docs/combobox) +- [Mention](/docs/mention) +- [Combobox](/docs/combobox) diff --git a/apps/www/content/docs/emoji.mdx b/apps/www/content/docs/emoji.mdx index f6a1f9d82f..b550f857dc 100644 --- a/apps/www/content/docs/emoji.mdx +++ b/apps/www/content/docs/emoji.mdx @@ -2,8 +2,8 @@ title: Emoji description: Insert emoji inline. docs: - - route: /docs/components/emoji-combobox - title: Emoji Combobox + - route: /docs/components/emoji-input-element + title: Emoji Input Element - route: /docs/components/emoji-dropdown-menu title: Emoji Dropdown Menu - route: /docs/components/emoji-toolbar-dropdown @@ -24,7 +24,7 @@ docs: ## Installation ```bash -npm install @udecode/plate-emoji @udecode/plate-combobox +npm install @udecode/plate-emoji ``` ## Usage @@ -35,7 +35,6 @@ import { createEmojiPlugin } from '@udecode/plate-emoji'; const plugins = [ // ...otherPlugins, - createComboboxPlugin(), createEmojiPlugin(), ]; ``` @@ -44,57 +43,10 @@ const plugins = [ ### createEmojiPlugin - - - -The trigger character(s) for the emoji. - - - -A function to create an Emoji. TData is the type of the data used to create the emoji. - - - -A controller object that controls the emoji triggering behavior. - - - -Indicates whether the emoji trigger is active. - - - -Indicates whether the triggering mark is present. - - - -A function that sets the triggering status. - - - -A function that sets the text. - - - -A function that gets the current text. - - - -A function that checks whether the current triggering mark is enclosing. - - - -A function that gets the size of the current text. - - - -A function that resets the triggering controller. - - - - - - -The ID of the plugin. +Extends [TriggerComboboxPlugin](/docs/combobox#options) + + + A function to specify the node inserted when an emoji is selected. The default behavior is to insert a text node containing the emoji as a Unicode character. diff --git a/apps/www/content/docs/mention.mdx b/apps/www/content/docs/mention.mdx index d7b37c37c3..69ee000dde 100644 --- a/apps/www/content/docs/mention.mdx +++ b/apps/www/content/docs/mention.mdx @@ -4,8 +4,6 @@ description: Enable autocompletion for user mentions. docs: - route: /docs/components/combobox title: Combobox - - route: /docs/components/mention-combobox - title: Mention Combobox - route: /docs/components/mention-element title: Mention Element - route: /docs/components/mention-input-element @@ -26,18 +24,16 @@ docs: ## Installation ```bash -npm install @udecode/plate-mention @udecode/plate-combobox +npm install @udecode/plate-mention ``` ## Usage ```tsx -import { createComboboxPlugin } from '@udecode/plate-combobox'; import { createMentionPlugin } from '@udecode/plate-mention'; const plugins = [ // ...otherPlugins, - createComboboxPlugin(), createMentionPlugin(), ]; ``` @@ -46,51 +42,21 @@ const plugins = [ ### createMentionPlugin - +Extends [TriggerComboboxPlugin](/docs/combobox#options) + - -A function to create the mention node. - - - - A unique ID for the mention plugin. + A function to create the mention node. + Whether to insert a space after the mention. - - The character that triggers the mention (for example, '@' in the case of a - user mention). - - - The pattern that matches the char before the trigger character - (default to `/^\s?$/` that matches beginning of line or space) - - - - -An object containing the key-value pair for creating an input. - - - - The key for the input. - - - The value for the input. - - - - - - A query function to enable the behavior. - - ### getMentionOnSelectItem -Gets the `ComboboxOnSelectItem` handler for selecting an item in the mention combobox. +Gets the handler for selecting an item in the mention combobox. @@ -106,96 +72,7 @@ The plugin key of the mention plugin. - - The `ComboboxOnSelectItem` handler for selecting an item in the mention - combobox. - - - -### findMentionInput - -Finds the mention input node in the editor. - - - -The editor instance. - - - -Additional options for finding the mention input node. - - - - - - - The mention input node entry if found, otherwise `undefined`. - - - -### isNodeMentionInput - -Checks if a node is a mention input node in the editor. - - - - The editor instance. - - - The node to check. - - - - - - `true` if the node is a mention input node, otherwise `false`. - - - -### isSelectionInMentionInput - -Checks if the current selection in the editor is within a mention input. - - - - The editor instance. - - - - - - `true` if the selection is within a mention input, otherwise `false`. + + The handler for selecting an item in the mention combobox. - -### mentionOnKeyDownHandler - -Handles keydown events for mention-related functionality, such as removing mention inputs and moving the selection by offset. - - - - Options for moving the selection by offset. - - - A query function to enable the behavior. - - - - - -### removeMentionInput - -Removes the mention input node at the specified path. - - - - The editor instance. - - - The path of the mention input node to remove. - - diff --git a/apps/www/package.json b/apps/www/package.json index 1e9c9727f3..184a9565bf 100644 --- a/apps/www/package.json +++ b/apps/www/package.json @@ -34,6 +34,7 @@ "react-dom": "^18.2.0" }, "dependencies": { + "@ariakit/react": "0.4.6", "@faker-js/faker": "^8.4.1", "@radix-ui/colors": "1.0.1", "@radix-ui/react-accessible-icon": "^1.0.3", diff --git a/apps/www/public/registry/index.json b/apps/www/public/registry/index.json index bcf898253b..a9eab43935 100644 --- a/apps/www/public/registry/index.json +++ b/apps/www/public/registry/index.json @@ -132,6 +132,19 @@ ], "type": "components:plate-ui" }, + { + "dependencies": [ + "@udecode/plate-emoji" + ], + "files": [ + "plate-ui/emoji-input-element.tsx" + ], + "name": "emoji-input-element", + "registryDependencies": [ + "inline-combobox" + ], + "type": "components:plate-ui" + }, { "dependencies": [ "@udecode/plate-alignment" @@ -234,19 +247,6 @@ "registryDependencies": [], "type": "components:plate-ui" }, - { - "dependencies": [ - "@radix-ui/react-popover", - "@udecode/plate-combobox", - "@udecode/plate-floating" - ], - "files": [ - "plate-ui/combobox.tsx" - ], - "name": "combobox", - "registryDependencies": [], - "type": "components:plate-ui" - }, { "dependencies": [ "cmdk" @@ -311,17 +311,6 @@ "registryDependencies": [], "type": "components:plate-ui" }, - { - "dependencies": [ - "@udecode/plate-combobox" - ], - "files": [ - "plate-ui/emoji-combobox.tsx" - ], - "name": "emoji-combobox", - "registryDependencies": [], - "type": "components:plate-ui" - }, { "dependencies": [ "@udecode/plate-excalidraw" @@ -463,6 +452,18 @@ ], "type": "components:plate-ui" }, + { + "dependencies": [ + "@ariakit/react", + "@udecode/plate-combobox" + ], + "files": [ + "plate-ui/inline-combobox.tsx" + ], + "name": "inline-combobox", + "registryDependencies": [], + "type": "components:plate-ui" + }, { "dependencies": [], "files": [ @@ -638,16 +639,13 @@ }, { "dependencies": [ - "@udecode/plate-mention", - "@udecode/plate-combobox" + "@udecode/plate-mention" ], "files": [ - "plate-ui/mention-combobox.tsx" - ], - "name": "mention-combobox", - "registryDependencies": [ - "combobox" + "plate-ui/mention-element.tsx" ], + "name": "mention-element", + "registryDependencies": [], "type": "components:plate-ui" }, { @@ -669,7 +667,9 @@ "plate-ui/mention-input-element.tsx" ], "name": "mention-input-element", - "registryDependencies": [], + "registryDependencies": [ + "inline-combobox" + ], "type": "components:plate-ui" }, { @@ -768,6 +768,20 @@ "registryDependencies": [], "type": "components:plate-ui" }, + { + "dependencies": [ + "@udecode/plate-heading", + "@udecode/plate-indent-list" + ], + "files": [ + "plate-ui/slash-input-element.tsx" + ], + "name": "slash-input-element", + "registryDependencies": [ + "inline-combobox" + ], + "type": "components:plate-ui" + }, { "dependencies": [ "@udecode/plate-table" diff --git a/apps/www/public/registry/styles/default/caption.json b/apps/www/public/registry/styles/default/caption.json index 2590adcbcf..338dedb258 100644 --- a/apps/www/public/registry/styles/default/caption.json +++ b/apps/www/public/registry/styles/default/caption.json @@ -4,7 +4,7 @@ ], "files": [ { - "content": "import { cn, withCn, withVariants } from '@udecode/cn';\nimport {\n Caption as CaptionPrimitive,\n CaptionTextarea as CaptionTextareaPrimitive,\n} from '@udecode/plate-caption';\nimport { cva } from 'class-variance-authority';\n\nconst captionVariants = cva('max-w-full', {\n defaultVariants: {\n align: 'center',\n },\n variants: {\n align: {\n center: 'mx-auto',\n left: 'mr-auto',\n right: 'ml-auto',\n },\n },\n});\n\nexport const Caption = withVariants(CaptionPrimitive, captionVariants, [\n 'align',\n]);\n\nexport const CaptionTextarea = withCn(\n CaptionTextareaPrimitive,\n cn(\n 'mt-2 w-full resize-none border-none bg-inherit p-0 font-[inherit] text-inherit',\n 'focus:outline-none focus:[&::placeholder]:opacity-0',\n 'text-center print:placeholder:text-transparent'\n )\n);\n", + "content": "import {\n cn,\n createPrimitiveComponent,\n withCn,\n withVariants,\n} from '@udecode/cn';\nimport {\n Caption as CaptionPrimitive,\n CaptionTextarea as CaptionTextareaPrimitive,\n useCaptionButton,\n useCaptionButtonState,\n} from '@udecode/plate-caption';\nimport { cva } from 'class-variance-authority';\n\nimport { Button } from './button';\n\nconst captionVariants = cva('max-w-full', {\n defaultVariants: {\n align: 'center',\n },\n variants: {\n align: {\n center: 'mx-auto',\n left: 'mr-auto',\n right: 'ml-auto',\n },\n },\n});\n\nexport const Caption = withVariants(CaptionPrimitive, captionVariants, [\n 'align',\n]);\n\nexport const CaptionTextarea = withCn(\n CaptionTextareaPrimitive,\n cn(\n 'mt-2 w-full resize-none border-none bg-inherit p-0 font-[inherit] text-inherit',\n 'focus:outline-none focus:[&::placeholder]:opacity-0',\n 'text-center print:placeholder:text-transparent'\n )\n);\n\nexport const CaptionButton = createPrimitiveComponent(Button)({\n propsHook: useCaptionButton,\n stateHook: useCaptionButtonState,\n});\n", "name": "caption.tsx" } ], diff --git a/apps/www/public/registry/styles/default/draggable.json b/apps/www/public/registry/styles/default/draggable.json index f1328f51ff..46dd1d0684 100644 --- a/apps/www/public/registry/styles/default/draggable.json +++ b/apps/www/public/registry/styles/default/draggable.json @@ -6,7 +6,7 @@ ], "files": [ { - "content": "'use client';\n\nimport React from 'react';\n\nimport type {\n ClassNames,\n PlateElementProps,\n TEditor,\n} from '@udecode/plate-common';\nimport type { DropTargetMonitor } from 'react-dnd';\n\nimport { cn, withRef } from '@udecode/cn';\nimport {\n type DragItemNode,\n useDraggable,\n useDraggableState,\n} from '@udecode/plate-dnd';\n\nimport { Icons } from '@/components/icons';\n\nimport { Tooltip, TooltipContent, TooltipPortal, TooltipTrigger } from './tooltip';\n\nexport interface DraggableProps\n extends PlateElementProps,\n ClassNames<{\n /** Block. */\n block: string;\n\n /** Block and gutter. */\n blockAndGutter: string;\n\n /** Block toolbar in the gutter. */\n blockToolbar: string;\n\n /**\n * Block toolbar wrapper in the gutter left. It has the height of a line\n * of the block.\n */\n blockToolbarWrapper: string;\n\n blockWrapper: string;\n\n /** Button to dnd the block, in the block toolbar. */\n dragHandle: string;\n\n /** Icon of the drag button, in the drag icon. */\n dragIcon: string;\n\n /** Show a dropline above or below the block when dragging a block. */\n dropLine: string;\n\n /** Gutter at the left side of the editor. It has the height of the block */\n gutterLeft: string;\n }> {\n /**\n * Intercepts the drop handling. If `false` is returned, the default drop\n * behavior is called after. If `true` is returned, the default behavior is\n * not called.\n */\n onDropHandler?: (\n editor: TEditor,\n props: {\n dragItem: DragItemNode;\n id: string;\n monitor: DropTargetMonitor;\n nodeRef: any;\n }\n ) => boolean;\n}\n\nconst dragHandle = (\n \n \n \n \n \n Drag to move\n \n \n);\n\nexport const Draggable = withRef<'div', DraggableProps>(\n ({ className, classNames = {}, onDropHandler, ...props }, ref) => {\n const { children, element } = props;\n\n const state = useDraggableState({ element, onDropHandler });\n const { dropLine, isDragging, isHovered } = state;\n const {\n droplineProps,\n groupProps,\n gutterLeftProps,\n handleRef,\n previewRef,\n } = useDraggable(state);\n\n return (\n \n \n
\n \n
\n {isHovered && dragHandle}\n
\n
\n \n \n\n
\n {children}\n\n {!!dropLine && (\n \n )}\n
\n \n );\n }\n);\n", + "content": "'use client';\n\nimport React from 'react';\n\nimport type { DropTargetMonitor } from 'react-dnd';\n\nimport { cn, withRef } from '@udecode/cn';\nimport {\n type ClassNames,\n type PlateElementProps,\n type TEditor,\n type TElement,\n useEditorRef,\n useElement,\n} from '@udecode/plate-common';\nimport {\n type DragItemNode,\n useDraggable,\n useDraggableState,\n} from '@udecode/plate-dnd';\nimport { blockSelectionActions } from '@udecode/plate-selection';\n\nimport { Icons } from '@/components/icons';\n\nimport {\n Tooltip,\n TooltipContent,\n TooltipPortal,\n TooltipTrigger,\n} from './tooltip';\n\nexport interface DraggableProps\n extends PlateElementProps,\n ClassNames<{\n /** Block. */\n block: string;\n\n /** Block and gutter. */\n blockAndGutter: string;\n\n /** Block toolbar in the gutter. */\n blockToolbar: string;\n\n /**\n * Block toolbar wrapper in the gutter left. It has the height of a line\n * of the block.\n */\n blockToolbarWrapper: string;\n\n blockWrapper: string;\n\n /** Button to dnd the block, in the block toolbar. */\n dragHandle: string;\n\n /** Icon of the drag button, in the drag icon. */\n dragIcon: string;\n\n /** Show a dropline above or below the block when dragging a block. */\n dropLine: string;\n\n /** Gutter at the left side of the editor. It has the height of the block */\n gutterLeft: string;\n }> {\n /**\n * Intercepts the drop handling. If `false` is returned, the default drop\n * behavior is called after. If `true` is returned, the default behavior is\n * not called.\n */\n onDropHandler?: (\n editor: TEditor,\n props: {\n dragItem: DragItemNode;\n id: string;\n monitor: DropTargetMonitor;\n nodeRef: any;\n }\n ) => boolean;\n}\n\nconst DragHandle = () => {\n const editor = useEditorRef();\n const element = useElement();\n\n return (\n \n \n {\n event.stopPropagation();\n event.preventDefault();\n\n // if (element.id) {\n // blockSelectionActions.addSelectedRow(element.id as string);\n // blockContextMenuActions.show(editor.id, event as any);\n // }\n }}\n onMouseDown={() => {\n blockSelectionActions.resetSelectedIds();\n }}\n />\n \n \n Drag to move\n \n \n );\n};\n\nexport const Draggable = withRef<'div', DraggableProps>(\n ({ className, classNames = {}, onDropHandler, ...props }, ref) => {\n const { children, element } = props;\n\n const state = useDraggableState({ element, onDropHandler });\n const { dropLine, isDragging, isHovered } = state;\n const {\n droplineProps,\n groupProps,\n gutterLeftProps,\n handleRef,\n previewRef,\n } = useDraggable(state);\n\n return (\n \n \n
\n \n \n {isHovered && }\n
\n \n \n \n\n
\n {children}\n\n {!!dropLine && (\n \n )}\n
\n \n );\n }\n);\n", "name": "draggable.tsx" }, { diff --git a/apps/www/public/registry/styles/default/emoji-input-element.json b/apps/www/public/registry/styles/default/emoji-input-element.json new file mode 100644 index 0000000000..094da67fcf --- /dev/null +++ b/apps/www/public/registry/styles/default/emoji-input-element.json @@ -0,0 +1,16 @@ +{ + "dependencies": [ + "@udecode/plate-emoji" + ], + "files": [ + { + "content": "import React, { useMemo, useState } from 'react';\n\nimport { withRef } from '@udecode/cn';\nimport { PlateElement } from '@udecode/plate-common';\nimport { EmojiInlineIndexSearch, insertEmoji } from '@udecode/plate-emoji';\n\nimport { useDebounce } from '@/hooks/use-debounce';\n\nimport {\n InlineCombobox,\n InlineComboboxContent,\n InlineComboboxEmpty,\n InlineComboboxInput,\n InlineComboboxItem,\n} from './inline-combobox';\n\nexport const EmojiInputElement = withRef(\n ({ className, ...props }, ref) => {\n const { children, editor, element } = props;\n const [value, setValue] = useState('');\n const debouncedValue = useDebounce(value, 100);\n const isPending = value !== debouncedValue;\n\n const filteredEmojis = useMemo(() => {\n if (debouncedValue.trim().length === 0) return [];\n\n return EmojiInlineIndexSearch.getInstance()\n .search(debouncedValue.replace(/:$/, ''))\n .get();\n }, [debouncedValue]);\n\n return (\n \n \n \n\n \n {!isPending && (\n No matching emoji found\n )}\n\n {filteredEmojis.map((emoji) => (\n insertEmoji(editor, emoji)}\n value={emoji.name}\n >\n {emoji.skins[0].native} {emoji.name}\n \n ))}\n \n \n\n {children}\n \n );\n }\n);\n", + "name": "emoji-input-element.tsx" + } + ], + "name": "emoji-input-element", + "registryDependencies": [ + "inline-combobox" + ], + "type": "components:plate-ui" +} \ No newline at end of file diff --git a/apps/www/public/registry/styles/default/floating-toolbar.json b/apps/www/public/registry/styles/default/floating-toolbar.json index 30a62b4e21..2ed61e7121 100644 --- a/apps/www/public/registry/styles/default/floating-toolbar.json +++ b/apps/www/public/registry/styles/default/floating-toolbar.json @@ -4,7 +4,7 @@ ], "files": [ { - "content": "'use client';\n\nimport React from 'react';\n\nimport { cn, withRef } from '@udecode/cn';\nimport { PortalBody, useComposedRef } from '@udecode/plate-common';\nimport {\n type FloatingToolbarState,\n flip,\n offset,\n useFloatingToolbar,\n useFloatingToolbarState,\n} from '@udecode/plate-floating';\n\nimport { Toolbar } from './toolbar';\n\nexport const FloatingToolbar = withRef<\n typeof Toolbar,\n {\n state?: FloatingToolbarState;\n }\n>(({ children, state, ...props }, componentRef) => {\n const floatingToolbarState = useFloatingToolbarState({\n ...state,\n floatingOptions: {\n middleware: [\n offset(12),\n flip({\n fallbackPlacements: [\n 'top-start',\n 'top-end',\n 'bottom-start',\n 'bottom-end',\n ],\n padding: 12,\n }),\n ],\n placement: 'top',\n ...state?.floatingOptions,\n },\n });\n\n const {\n hidden,\n props: rootProps,\n ref: floatingRef,\n } = useFloatingToolbar(floatingToolbarState);\n\n const ref = useComposedRef(componentRef, floatingRef);\n\n if (hidden) return null;\n\n return (\n \n \n {children}\n \n \n );\n});\n", + "content": "'use client';\n\nimport React from 'react';\n\nimport { cn, withRef } from '@udecode/cn';\nimport {\n PortalBody,\n useComposedRef,\n useEventEditorSelectors,\n usePlateSelectors,\n} from '@udecode/plate-common';\nimport {\n type FloatingToolbarState,\n flip,\n offset,\n useFloatingToolbar,\n useFloatingToolbarState,\n} from '@udecode/plate-floating';\n\nimport { Toolbar } from './toolbar';\n\nexport const FloatingToolbar = withRef<\n typeof Toolbar,\n {\n state?: FloatingToolbarState;\n }\n>(({ children, state, ...props }, componentRef) => {\n const editorId = usePlateSelectors().id();\n const focusedEditorId = useEventEditorSelectors.focus();\n\n const floatingToolbarState = useFloatingToolbarState({\n editorId,\n focusedEditorId,\n ...state,\n floatingOptions: {\n middleware: [\n offset(12),\n flip({\n fallbackPlacements: [\n 'top-start',\n 'top-end',\n 'bottom-start',\n 'bottom-end',\n ],\n padding: 12,\n }),\n ],\n placement: 'top',\n ...state?.floatingOptions,\n },\n });\n\n const {\n hidden,\n props: rootProps,\n ref: floatingRef,\n } = useFloatingToolbar(floatingToolbarState);\n\n const ref = useComposedRef(componentRef, floatingRef);\n\n if (hidden) return null;\n\n return (\n \n \n {children}\n \n \n );\n});\n", "name": "floating-toolbar.tsx" } ], diff --git a/apps/www/public/registry/styles/default/image-element.json b/apps/www/public/registry/styles/default/image-element.json index c00c6676b2..15a6c01e8d 100644 --- a/apps/www/public/registry/styles/default/image-element.json +++ b/apps/www/public/registry/styles/default/image-element.json @@ -4,7 +4,7 @@ ], "files": [ { - "content": "import React from 'react';\n\nimport { cn, withRef } from '@udecode/cn';\nimport { PlateElement, withHOC } from '@udecode/plate-common';\nimport { ELEMENT_IMAGE, Image, useMediaState } from '@udecode/plate-media';\nimport { ResizableProvider, useResizableStore } from '@udecode/plate-resizable';\n\nimport { Caption, CaptionTextarea } from './caption';\nimport { MediaPopover } from './media-popover';\nimport {\n Resizable,\n ResizeHandle,\n mediaResizeHandleVariants,\n} from './resizable';\n\nexport const ImageElement = withHOC(\n ResizableProvider,\n withRef(\n ({ children, className, nodeProps, ...props }, ref) => {\n const { align = 'center', focused, readOnly, selected } = useMediaState();\n const width = useResizableStore().get.width();\n\n return (\n \n \n
\n \n \n \n \n \n\n \n \n \n
\n\n {children}\n \n
\n );\n }\n )\n);\n", + "content": "import React from 'react';\n\nimport { cn, withRef } from '@udecode/cn';\nimport { CaptionProvider } from '@udecode/plate-caption';\nimport { PlateElement, withHOC } from '@udecode/plate-common';\nimport { ELEMENT_IMAGE, Image, useMediaState } from '@udecode/plate-media';\nimport { ResizableProvider, useResizableStore } from '@udecode/plate-resizable';\n\nimport { Caption, CaptionTextarea } from './caption';\nimport { MediaPopover } from './media-popover';\nimport {\n Resizable,\n ResizeHandle,\n mediaResizeHandleVariants,\n} from './resizable';\n\nexport const ImageElement = withHOC(\n CaptionProvider,\n withHOC(\n ResizableProvider,\n withRef(\n ({ children, className, nodeProps, ...props }, ref) => {\n const {\n align = 'center',\n focused,\n readOnly,\n selected,\n } = useMediaState();\n\n const width = useResizableStore().get.width();\n\n return (\n \n \n
\n \n \n \n \n \n\n \n \n \n
\n\n {children}\n \n
\n );\n }\n )\n )\n);\n", "name": "image-element.tsx" } ], diff --git a/apps/www/public/registry/styles/default/inline-combobox.json b/apps/www/public/registry/styles/default/inline-combobox.json new file mode 100644 index 0000000000..540153d93b --- /dev/null +++ b/apps/www/public/registry/styles/default/inline-combobox.json @@ -0,0 +1,15 @@ +{ + "dependencies": [ + "@ariakit/react", + "@udecode/plate-combobox" + ], + "files": [ + { + "content": "import React, {\n type HTMLAttributes,\n type ReactNode,\n type RefObject,\n createContext,\n forwardRef,\n startTransition,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useState,\n} from 'react';\n\nimport type { PointRef } from 'slate';\n\nimport {\n Combobox,\n ComboboxItem,\n type ComboboxItemProps,\n ComboboxPopover,\n ComboboxProvider,\n Portal,\n useComboboxContext,\n useComboboxStore,\n} from '@ariakit/react';\nimport { cn } from '@udecode/cn';\nimport {\n type UseComboboxInputResult,\n filterWords,\n useComboboxInput,\n useHTMLInputCursorState,\n} from '@udecode/plate-combobox';\nimport {\n type TElement,\n createPointRef,\n findNodePath,\n getPointBefore,\n insertText,\n moveSelection,\n useComposedRef,\n useEditorRef,\n} from '@udecode/plate-common';\nimport { cva } from 'class-variance-authority';\n\ntype FilterFn = (\n item: { keywords?: string[]; value: string },\n search: string\n) => boolean;\n\ninterface InlineComboboxContextValue {\n filter: FilterFn | false;\n inputProps: UseComboboxInputResult['props'];\n inputRef: RefObject;\n removeInput: UseComboboxInputResult['removeInput'];\n setHasEmpty: (hasEmpty: boolean) => void;\n showTrigger: boolean;\n trigger: string;\n}\n\nconst InlineComboboxContext = createContext(\n null as any\n);\n\nexport const defaultFilter: FilterFn = ({ keywords = [], value }, search) =>\n [value, ...keywords].some((keyword) => filterWords(keyword, search));\n\ninterface InlineComboboxProps {\n children: ReactNode;\n element: TElement;\n trigger: string;\n filter?: FilterFn | false;\n hideWhenNoValue?: boolean;\n setValue?: (value: string) => void;\n showTrigger?: boolean;\n value?: string;\n}\n\nconst InlineCombobox = ({\n children,\n element,\n filter = defaultFilter,\n hideWhenNoValue = false,\n setValue: setValueProp,\n showTrigger = true,\n trigger,\n value: valueProp,\n}: InlineComboboxProps) => {\n const editor = useEditorRef();\n const inputRef = React.useRef(null);\n const cursorState = useHTMLInputCursorState(inputRef);\n\n const [valueState, setValueState] = useState('');\n const hasValueProp = valueProp !== undefined;\n const value = hasValueProp ? valueProp : valueState;\n\n const setValue = useCallback(\n (newValue: string) => {\n setValueProp?.(newValue);\n\n if (!hasValueProp) {\n setValueState(newValue);\n }\n },\n [setValueProp, hasValueProp]\n );\n\n /**\n * Track the point just before the input element so we know where to\n * insertText if the combobox closes due to a selection change.\n */\n const [insertPoint, setInsertPoint] = useState(null);\n\n useEffect(() => {\n const path = findNodePath(editor, element);\n\n if (!path) return;\n\n const point = getPointBefore(editor, path);\n\n if (!point) return;\n\n const pointRef = createPointRef(editor, point);\n setInsertPoint(pointRef);\n\n return () => {\n pointRef.unref();\n };\n }, [editor, element]);\n\n const { props: inputProps, removeInput } = useComboboxInput({\n cancelInputOnBlur: false,\n cursorState,\n onCancelInput: (cause) => {\n if (cause !== 'backspace') {\n insertText(editor, trigger + value, {\n at: insertPoint?.current ?? undefined,\n });\n }\n if (cause === 'arrowLeft' || cause === 'arrowRight') {\n moveSelection(editor, {\n distance: 1,\n reverse: cause === 'arrowLeft',\n });\n }\n },\n ref: inputRef,\n });\n\n const [hasEmpty, setHasEmpty] = useState(false);\n\n const contextValue: InlineComboboxContextValue = useMemo(\n () => ({\n filter,\n inputProps,\n inputRef,\n removeInput,\n setHasEmpty,\n showTrigger,\n trigger,\n }),\n [\n trigger,\n showTrigger,\n filter,\n inputRef,\n inputProps,\n removeInput,\n setHasEmpty,\n ]\n );\n\n const store = useComboboxStore({\n // open: ,\n setValue: (newValue) => startTransition(() => setValue(newValue)),\n });\n\n const items = store.useState('items');\n\n useEffect;\n\n /**\n * If there is no active ID and the list of items changes, select the first\n * item.\n */\n useEffect(() => {\n if (!store.getState().activeId) {\n store.setActiveId(store.first());\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [items, store]);\n\n return (\n \n 0 || hasEmpty) &&\n (!hideWhenNoValue || value.length > 0)\n }\n store={store}\n >\n \n {children}\n \n \n \n );\n};\n\nconst InlineComboboxInput = forwardRef<\n HTMLInputElement,\n HTMLAttributes\n>(({ className, ...props }, propRef) => {\n const {\n inputProps,\n inputRef: contextRef,\n showTrigger,\n trigger,\n } = useContext(InlineComboboxContext);\n\n const store = useComboboxContext()!;\n const value = store.useState('value');\n\n const ref = useComposedRef(propRef, contextRef);\n\n /**\n * To create an auto-resizing input, we render a visually hidden span\n * containing the input value and position the input element on top of it.\n * This works well for all cases except when input exceeds the width of the\n * container.\n */\n\n return (\n <>\n {showTrigger && trigger}\n\n \n \n {value || '\\u200B'}\n \n\n \n \n \n );\n});\n\nInlineComboboxInput.displayName = 'InlineComboboxInput';\n\nconst InlineComboboxContent: typeof ComboboxPopover = ({\n className,\n ...props\n}) => {\n // Portal prevents CSS from leaking into popover\n return (\n \n \n \n );\n};\n\nconst comboboxItemVariants = cva(\n 'relative flex h-9 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none',\n {\n defaultVariants: {\n interactive: true,\n },\n variants: {\n interactive: {\n false: '',\n true: 'cursor-pointer transition-colors hover:bg-accent hover:text-accent-foreground data-[active-item=true]:bg-accent data-[active-item=true]:text-accent-foreground',\n },\n },\n }\n);\n\nexport type InlineComboboxItemProps = {\n keywords?: string[];\n} & ComboboxItemProps &\n Required>;\n\nconst InlineComboboxItem = ({\n className,\n keywords,\n onClick,\n ...props\n}: InlineComboboxItemProps) => {\n const { value } = props;\n\n const { filter, removeInput } = useContext(InlineComboboxContext);\n\n const store = useComboboxContext()!;\n\n // Optimization: Do not subscribe to value if filter is false\n const search = filter && store.useState('value');\n\n const visible = useMemo(\n () => !filter || filter({ keywords, value }, search as string),\n [filter, value, keywords, search]\n );\n\n if (!visible) return null;\n\n return (\n {\n removeInput(true);\n onClick?.(event);\n }}\n {...props}\n />\n );\n};\n\nconst InlineComboboxEmpty = ({\n children,\n className,\n}: HTMLAttributes) => {\n const { setHasEmpty } = useContext(InlineComboboxContext);\n const store = useComboboxContext()!;\n const items = store.useState('items');\n\n useEffect(() => {\n setHasEmpty(true);\n\n return () => {\n setHasEmpty(false);\n };\n }, [setHasEmpty]);\n\n if (items.length > 0) return null;\n\n return (\n \n {children}\n \n );\n};\n\nexport {\n InlineCombobox,\n InlineComboboxContent,\n InlineComboboxEmpty,\n InlineComboboxInput,\n InlineComboboxItem,\n};\n", + "name": "inline-combobox.tsx" + } + ], + "name": "inline-combobox", + "registryDependencies": [], + "type": "components:plate-ui" +} \ No newline at end of file diff --git a/apps/www/public/registry/styles/default/media-embed-element.json b/apps/www/public/registry/styles/default/media-embed-element.json index 8f31b6fe9a..a4b3ec5194 100644 --- a/apps/www/public/registry/styles/default/media-embed-element.json +++ b/apps/www/public/registry/styles/default/media-embed-element.json @@ -6,7 +6,7 @@ ], "files": [ { - "content": "import React from 'react';\nimport LiteYouTubeEmbed from 'react-lite-youtube-embed';\nimport { Tweet } from 'react-tweet';\n\nimport { cn, withRef } from '@udecode/cn';\nimport { PlateElement, withHOC } from '@udecode/plate-common';\nimport {\n ELEMENT_MEDIA_EMBED,\n parseTwitterUrl,\n parseVideoUrl,\n useMediaState,\n} from '@udecode/plate-media';\nimport { ResizableProvider, useResizableStore } from '@udecode/plate-resizable';\n\nimport { Caption, CaptionTextarea } from './caption';\nimport { MediaPopover } from './media-popover';\nimport {\n Resizable,\n ResizeHandle,\n mediaResizeHandleVariants,\n} from './resizable';\n\nexport const MediaEmbedElement = withHOC(\n ResizableProvider,\n withRef(({ children, className, ...props }, ref) => {\n const {\n align = 'center',\n embed,\n focused,\n isTweet,\n isVideo,\n isYoutube,\n readOnly,\n selected,\n } = useMediaState({\n urlParsers: [parseTwitterUrl, parseVideoUrl],\n });\n const width = useResizableStore().get.width();\n const provider = embed?.provider;\n\n return (\n \n \n
\n \n \n\n {isVideo ? (\n isYoutube ? (\n _iframe]:absolute [&_>_iframe]:left-0 [&_>_iframe]:top-0 [&_>_iframe]:size-full',\n '[&_>_.lty-playbtn]:z-[1] [&_>_.lty-playbtn]:h-[46px] [&_>_.lty-playbtn]:w-[70px] [&_>_.lty-playbtn]:rounded-[14%] [&_>_.lty-playbtn]:bg-[#212121] [&_>_.lty-playbtn]:opacity-80 [&_>_.lty-playbtn]:[transition:all_0.2s_cubic-bezier(0,_0,_0.2,_1)]',\n '[&:hover_>_.lty-playbtn]:bg-[red] [&:hover_>_.lty-playbtn]:opacity-100',\n '[&_>_.lty-playbtn]:before:border-y-[11px] [&_>_.lty-playbtn]:before:border-l-[19px] [&_>_.lty-playbtn]:before:border-r-0 [&_>_.lty-playbtn]:before:border-[transparent_transparent_transparent_#fff] [&_>_.lty-playbtn]:before:content-[\"\"]',\n '[&_>_.lty-playbtn]:absolute [&_>_.lty-playbtn]:left-1/2 [&_>_.lty-playbtn]:top-1/2 [&_>_.lty-playbtn]:[transform:translate3d(-50%,-50%,0)]',\n '[&_>_.lty-playbtn]:before:absolute [&_>_.lty-playbtn]:before:left-1/2 [&_>_.lty-playbtn]:before:top-1/2 [&_>_.lty-playbtn]:before:[transform:translate3d(-50%,-50%,0)]',\n '[&.lyt-activated]:cursor-[unset]',\n '[&.lyt-activated]:before:pointer-events-none [&.lyt-activated]:before:opacity-0',\n '[&.lyt-activated_>_.lty-playbtn]:pointer-events-none [&.lyt-activated_>_.lty-playbtn]:!opacity-0'\n )}\n />\n ) : (\n \n \n \n )\n ) : null}\n\n {isTweet && (\n \n \n \n )}\n\n \n \n\n \n \n \n
\n\n {children}\n \n
\n );\n })\n);\n", + "content": "import React from 'react';\nimport LiteYouTubeEmbed from 'react-lite-youtube-embed';\nimport { Tweet } from 'react-tweet';\n\nimport { cn, withRef } from '@udecode/cn';\nimport { CaptionProvider } from '@udecode/plate-caption';\nimport { PlateElement, withHOC } from '@udecode/plate-common';\nimport {\n ELEMENT_MEDIA_EMBED,\n parseTwitterUrl,\n parseVideoUrl,\n useMediaState,\n} from '@udecode/plate-media';\nimport { ResizableProvider, useResizableStore } from '@udecode/plate-resizable';\n\nimport { Caption, CaptionTextarea } from './caption';\nimport { MediaPopover } from './media-popover';\nimport {\n Resizable,\n ResizeHandle,\n mediaResizeHandleVariants,\n} from './resizable';\n\nexport const MediaEmbedElement = withHOC(\n CaptionProvider,\n withHOC(\n ResizableProvider,\n withRef(({ children, className, ...props }, ref) => {\n const {\n align = 'center',\n embed,\n focused,\n isTweet,\n isVideo,\n isYoutube,\n readOnly,\n selected,\n } = useMediaState({\n urlParsers: [parseTwitterUrl, parseVideoUrl],\n });\n const width = useResizableStore().get.width();\n const provider = embed?.provider;\n\n return (\n \n \n \n \n \n\n {isVideo ? (\n isYoutube ? (\n _iframe]:absolute [&_>_iframe]:left-0 [&_>_iframe]:top-0 [&_>_iframe]:size-full',\n '[&_>_.lty-playbtn]:z-[1] [&_>_.lty-playbtn]:h-[46px] [&_>_.lty-playbtn]:w-[70px] [&_>_.lty-playbtn]:rounded-[14%] [&_>_.lty-playbtn]:bg-[#212121] [&_>_.lty-playbtn]:opacity-80 [&_>_.lty-playbtn]:[transition:all_0.2s_cubic-bezier(0,_0,_0.2,_1)]',\n '[&:hover_>_.lty-playbtn]:bg-[red] [&:hover_>_.lty-playbtn]:opacity-100',\n '[&_>_.lty-playbtn]:before:border-y-[11px] [&_>_.lty-playbtn]:before:border-l-[19px] [&_>_.lty-playbtn]:before:border-r-0 [&_>_.lty-playbtn]:before:border-[transparent_transparent_transparent_#fff] [&_>_.lty-playbtn]:before:content-[\"\"]',\n '[&_>_.lty-playbtn]:absolute [&_>_.lty-playbtn]:left-1/2 [&_>_.lty-playbtn]:top-1/2 [&_>_.lty-playbtn]:[transform:translate3d(-50%,-50%,0)]',\n '[&_>_.lty-playbtn]:before:absolute [&_>_.lty-playbtn]:before:left-1/2 [&_>_.lty-playbtn]:before:top-1/2 [&_>_.lty-playbtn]:before:[transform:translate3d(-50%,-50%,0)]',\n '[&.lyt-activated]:cursor-[unset]',\n '[&.lyt-activated]:before:pointer-events-none [&.lyt-activated]:before:opacity-0',\n '[&.lyt-activated_>_.lty-playbtn]:pointer-events-none [&.lyt-activated_>_.lty-playbtn]:!opacity-0'\n )}\n />\n ) : (\n \n \n \n )\n ) : null}\n\n {isTweet && (\n \n \n \n )}\n\n \n \n\n \n \n \n \n\n {children}\n \n \n );\n })\n )\n);\n", "name": "media-embed-element.tsx" } ], diff --git a/apps/www/public/registry/styles/default/media-popover.json b/apps/www/public/registry/styles/default/media-popover.json index 6c1703f395..7cc4e56a45 100644 --- a/apps/www/public/registry/styles/default/media-popover.json +++ b/apps/www/public/registry/styles/default/media-popover.json @@ -4,7 +4,7 @@ ], "files": [ { - "content": "import React, { useEffect } from 'react';\n\nimport {\n isSelectionExpanded,\n useEditorSelector,\n useElement,\n useRemoveNodeButton,\n} from '@udecode/plate-common';\nimport {\n FloatingMedia as FloatingMediaPrimitive,\n floatingMediaActions,\n useFloatingMediaSelectors,\n} from '@udecode/plate-media';\nimport { useReadOnly, useSelected } from 'slate-react';\n\nimport { Icons } from '@/components/icons';\n\nimport { Button, buttonVariants } from './button';\nimport { inputVariants } from './input';\nimport { Popover, PopoverAnchor, PopoverContent } from './popover';\nimport { Separator } from './separator';\n\nexport interface MediaPopoverProps {\n children: React.ReactNode;\n pluginKey?: string;\n}\n\nexport function MediaPopover({ children, pluginKey }: MediaPopoverProps) {\n const readOnly = useReadOnly();\n const selected = useSelected();\n\n const selectionCollapsed = useEditorSelector(\n (editor) => !isSelectionExpanded(editor),\n []\n );\n const isOpen = !readOnly && selected && selectionCollapsed;\n const isEditing = useFloatingMediaSelectors().isEditing();\n\n useEffect(() => {\n if (!isOpen && isEditing) {\n floatingMediaActions.isEditing(false);\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [isOpen]);\n\n const element = useElement();\n const { props: buttonProps } = useRemoveNodeButton({ element });\n\n if (readOnly) return <>{children};\n\n return (\n \n {children}\n\n e.preventDefault()}\n >\n {isEditing ? (\n
\n
\n
\n \n
\n\n \n
\n
\n ) : (\n
\n \n Edit link\n \n\n \n\n \n
\n )}\n \n
\n );\n}\n", + "content": "import React, { useEffect } from 'react';\n\nimport {\n isSelectionExpanded,\n useEditorSelector,\n useElement,\n useRemoveNodeButton,\n} from '@udecode/plate-common';\nimport {\n FloatingMedia as FloatingMediaPrimitive,\n floatingMediaActions,\n useFloatingMediaSelectors,\n} from '@udecode/plate-media';\nimport { useReadOnly, useSelected } from 'slate-react';\n\nimport { Icons } from '@/components/icons';\n\nimport { Button, buttonVariants } from './button';\nimport { CaptionButton } from './caption';\nimport { inputVariants } from './input';\nimport { Popover, PopoverAnchor, PopoverContent } from './popover';\nimport { Separator } from './separator';\n\nexport interface MediaPopoverProps {\n children: React.ReactNode;\n pluginKey?: string;\n}\n\nexport function MediaPopover({ children, pluginKey }: MediaPopoverProps) {\n const readOnly = useReadOnly();\n const selected = useSelected();\n\n const selectionCollapsed = useEditorSelector(\n (editor) => !isSelectionExpanded(editor),\n []\n );\n const isOpen = !readOnly && selected && selectionCollapsed;\n const isEditing = useFloatingMediaSelectors().isEditing();\n\n useEffect(() => {\n if (!isOpen && isEditing) {\n floatingMediaActions.isEditing(false);\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [isOpen]);\n\n const element = useElement();\n const { props: buttonProps } = useRemoveNodeButton({ element });\n\n if (readOnly) return <>{children};\n\n return (\n \n {children}\n\n e.preventDefault()}\n >\n {isEditing ? (\n
\n
\n
\n \n
\n\n \n
\n
\n ) : (\n
\n \n Edit link\n \n\n Caption\n\n \n\n \n
\n )}\n \n
\n );\n}\n", "name": "media-popover.tsx" } ], diff --git a/apps/www/public/registry/styles/default/mention-input-element.json b/apps/www/public/registry/styles/default/mention-input-element.json index fc0435b2b6..bf61f03b2e 100644 --- a/apps/www/public/registry/styles/default/mention-input-element.json +++ b/apps/www/public/registry/styles/default/mention-input-element.json @@ -4,11 +4,13 @@ ], "files": [ { - "content": "import React from 'react';\n\nimport { cn, withRef } from '@udecode/cn';\nimport { PlateElement, getHandler } from '@udecode/plate-common';\nimport { useFocused, useSelected } from 'slate-react';\n\nexport const MentionInputElement = withRef<\n typeof PlateElement,\n {\n onClick?: (mentionNode: any) => void;\n }\n>(({ className, onClick, ...props }, ref) => {\n const { children, element } = props;\n\n const selected = useSelected();\n const focused = useFocused();\n\n return (\n \n {children}\n \n );\n});\n", + "content": "import React, { useState } from 'react';\n\nimport { cn, withRef } from '@udecode/cn';\nimport { PlateElement } from '@udecode/plate-common';\nimport { getMentionOnSelectItem } from '@udecode/plate-mention';\n\nimport { MENTIONABLES } from '@/lib/plate/demo/values/mentionables';\n\nimport {\n InlineCombobox,\n InlineComboboxContent,\n InlineComboboxEmpty,\n InlineComboboxInput,\n InlineComboboxItem,\n} from './inline-combobox';\n\nconst onSelectItem = getMentionOnSelectItem();\n\nexport const MentionInputElement = withRef(\n ({ className, ...props }, ref) => {\n const { children, editor, element } = props;\n const [search, setSearch] = useState('');\n\n return (\n \n \n \n \n \n\n \n No results found\n\n {MENTIONABLES.map((item) => (\n onSelectItem(editor, item, search)}\n value={item.text}\n >\n {item.text}\n \n ))}\n \n \n\n {children}\n \n );\n }\n);\n", "name": "mention-input-element.tsx" } ], "name": "mention-input-element", - "registryDependencies": [], + "registryDependencies": [ + "inline-combobox" + ], "type": "components:plate-ui" } \ No newline at end of file diff --git a/apps/www/public/registry/styles/default/slash-input-element.json b/apps/www/public/registry/styles/default/slash-input-element.json new file mode 100644 index 0000000000..46dfefb9b1 --- /dev/null +++ b/apps/www/public/registry/styles/default/slash-input-element.json @@ -0,0 +1,17 @@ +{ + "dependencies": [ + "@udecode/plate-heading", + "@udecode/plate-indent-list" + ], + "files": [ + { + "content": "import React, { type ComponentType, type SVGProps } from 'react';\n\nimport { withRef } from '@udecode/cn';\nimport {\n type PlateEditor,\n PlateElement,\n toggleNodeType,\n} from '@udecode/plate-common';\nimport { ELEMENT_H1, ELEMENT_H2, ELEMENT_H3 } from '@udecode/plate-heading';\nimport { ListStyleType, toggleIndentList } from '@udecode/plate-indent-list';\n\nimport { Icons } from '@/components/icons';\n\nimport {\n InlineCombobox,\n InlineComboboxContent,\n InlineComboboxEmpty,\n InlineComboboxInput,\n InlineComboboxItem,\n} from './inline-combobox';\n\ninterface SlashCommandRule {\n icon: ComponentType>;\n onSelect: (editor: PlateEditor) => void;\n value: string;\n keywords?: string[];\n}\n\nconst rules: SlashCommandRule[] = [\n {\n icon: Icons.h1,\n onSelect: (editor) => {\n toggleNodeType(editor, { activeType: ELEMENT_H1 });\n },\n value: 'Heading 1',\n },\n {\n icon: Icons.h2,\n onSelect: (editor) => {\n toggleNodeType(editor, { activeType: ELEMENT_H2 });\n },\n value: 'Heading 2',\n },\n {\n icon: Icons.h3,\n onSelect: (editor) => {\n toggleNodeType(editor, { activeType: ELEMENT_H3 });\n },\n value: 'Heading 3',\n },\n {\n icon: Icons.ul,\n keywords: ['ul', 'unordered list'],\n onSelect: (editor) => {\n toggleIndentList(editor, {\n listStyleType: ListStyleType.Disc,\n });\n },\n value: 'Bulleted list',\n },\n {\n icon: Icons.ol,\n keywords: ['ol', 'ordered list'],\n onSelect: (editor) => {\n toggleIndentList(editor, {\n listStyleType: ListStyleType.Decimal,\n });\n },\n value: 'Numbered list',\n },\n];\n\nexport const SlashInputElement = withRef(\n ({ className, ...props }, ref) => {\n const { children, editor, element } = props;\n\n return (\n \n \n \n\n \n \n No matching commands found\n \n\n {rules.map(({ icon: Icon, keywords, onSelect, value }) => (\n onSelect(editor)}\n value={value}\n >\n \n {value}\n \n ))}\n \n \n\n {children}\n \n );\n }\n);\n", + "name": "slash-input-element.tsx" + } + ], + "name": "slash-input-element", + "registryDependencies": [ + "inline-combobox" + ], + "type": "components:plate-ui" +} \ No newline at end of file diff --git a/apps/www/src/__registry__/index.tsx b/apps/www/src/__registry__/index.tsx index 906a875247..477cd9c008 100644 --- a/apps/www/src/__registry__/index.tsx +++ b/apps/www/src/__registry__/index.tsx @@ -256,6 +256,13 @@ export const Index: Record = { files: ['registry/default/plate-ui/emoji-picker-search-bar.tsx'], component: React.lazy(() => import('@/registry/default/plate-ui/emoji-picker-search-bar')), }, + 'emoji-input-element': { + name: 'emoji-input-element', + type: 'components:plate-ui', + registryDependencies: ["inline-combobox"], + files: ['registry/default/plate-ui/emoji-input-element.tsx'], + component: React.lazy(() => import('@/registry/default/plate-ui/emoji-input-element')), + }, 'align-dropdown-menu': { name: 'align-dropdown-menu', type: 'components:plate-ui', @@ -319,13 +326,6 @@ export const Index: Record = { files: ['registry/default/plate-ui/code-syntax-leaf.tsx'], component: React.lazy(() => import('@/registry/default/plate-ui/code-syntax-leaf')), }, - 'combobox': { - name: 'combobox', - type: 'components:plate-ui', - registryDependencies: [], - files: ['registry/default/plate-ui/combobox.tsx'], - component: React.lazy(() => import('@/registry/default/plate-ui/combobox')), - }, 'command': { name: 'command', type: 'components:plate-ui', @@ -368,13 +368,6 @@ export const Index: Record = { files: ['registry/default/plate-ui/dropdown-menu.tsx'], component: React.lazy(() => import('@/registry/default/plate-ui/dropdown-menu')), }, - 'emoji-combobox': { - name: 'emoji-combobox', - type: 'components:plate-ui', - registryDependencies: [], - files: ['registry/default/plate-ui/emoji-combobox.tsx'], - component: React.lazy(() => import('@/registry/default/plate-ui/emoji-combobox')), - }, 'excalidraw-element': { name: 'excalidraw-element', type: 'components:plate-ui', @@ -452,6 +445,13 @@ export const Index: Record = { files: ['registry/default/plate-ui/indent-toolbar-button.tsx'], component: React.lazy(() => import('@/registry/default/plate-ui/indent-toolbar-button')), }, + 'inline-combobox': { + name: 'inline-combobox', + type: 'components:plate-ui', + registryDependencies: [], + files: ['registry/default/plate-ui/inline-combobox.tsx'], + component: React.lazy(() => import('@/registry/default/plate-ui/inline-combobox')), + }, 'input': { name: 'input', type: 'components:plate-ui', @@ -543,12 +543,12 @@ export const Index: Record = { files: ['registry/default/plate-ui/media-toolbar-button.tsx'], component: React.lazy(() => import('@/registry/default/plate-ui/media-toolbar-button')), }, - 'mention-combobox': { - name: 'mention-combobox', + 'mention-element': { + name: 'mention-element', type: 'components:plate-ui', - registryDependencies: ["combobox"], - files: ['registry/default/plate-ui/mention-combobox.tsx'], - component: React.lazy(() => import('@/registry/default/plate-ui/mention-combobox')), + registryDependencies: [], + files: ['registry/default/plate-ui/mention-element.tsx'], + component: React.lazy(() => import('@/registry/default/plate-ui/mention-element')), }, 'mention-element': { name: 'mention-element', @@ -560,7 +560,7 @@ export const Index: Record = { 'mention-input-element': { name: 'mention-input-element', type: 'components:plate-ui', - registryDependencies: [], + registryDependencies: ["inline-combobox"], files: ['registry/default/plate-ui/mention-input-element.tsx'], component: React.lazy(() => import('@/registry/default/plate-ui/mention-input-element')), }, @@ -620,6 +620,13 @@ export const Index: Record = { files: ['registry/default/plate-ui/separator.tsx'], component: React.lazy(() => import('@/registry/default/plate-ui/separator')), }, + 'slash-input-element': { + name: 'slash-input-element', + type: 'components:plate-ui', + registryDependencies: ["inline-combobox"], + files: ['registry/default/plate-ui/slash-input-element.tsx'], + component: React.lazy(() => import('@/registry/default/plate-ui/slash-input-element')), + }, 'table-cell-element': { name: 'table-cell-element', type: 'components:plate-ui', diff --git a/apps/www/src/components/plate-ui/playground-turn-into-dropdown-menu.tsx b/apps/www/src/components/plate-ui/playground-turn-into-dropdown-menu.tsx index 214aef2ebc..f3a72c669b 100644 --- a/apps/www/src/components/plate-ui/playground-turn-into-dropdown-menu.tsx +++ b/apps/www/src/components/plate-ui/playground-turn-into-dropdown-menu.tsx @@ -134,6 +134,13 @@ export function PlaygroundTurnIntoDropdownMenu(props: DropdownMenuProps) { items.find((item) => item.value === value) ?? defaultItem; const { icon: SelectedItemIcon, label: selectedItemLabel } = selectedItem; + const onCloseAutoFocus = React.useCallback((e: Event) => { + focusEditor(editor); + + return e.preventDefault(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + return ( @@ -148,7 +155,11 @@ export function PlaygroundTurnIntoDropdownMenu(props: DropdownMenuProps) { - + Turn into = { route: customizerComponents.mentionInputElement.href, usage: 'MentionInputElement', }, - { - id: 'mention-combobox', - label: 'MentionCombobox', - route: customizerComponents.mentionCombobox.href, - usage: 'MentionCombobox', - }, ], - dependencies: [KEY_COMBOBOX], id: ELEMENT_MENTION, label: 'Mention', npmPackage: '@udecode/plate-mention', @@ -425,14 +417,6 @@ export const customizerItems: Record = { ], route: customizerPlugins.media.route, }, - [KEY_COMBOBOX]: { - badges: [customizerBadges.handler, customizerBadges.ui], - id: KEY_COMBOBOX, - label: 'Combobox', - npmPackage: '@udecode/plate-combobox', - pluginFactory: 'createComboboxPlugin', - route: customizerPlugins.combobox.route, - }, [KEY_DELETE]: { badges: [customizerBadges.handler], id: KEY_DELETE, @@ -501,14 +485,12 @@ export const customizerItems: Record = { badges: [customizerBadges.handler], components: [ { - id: 'emoji-combobox', - label: 'EmojiCombobox', - pluginOptions: [`renderAfterEditable: EmojiCombobox,`], - route: customizerComponents.emojiCombobox.href, - usage: 'EmojiCombobox', + id: 'emoji-input-element', + label: 'EmojiInputElement', + route: customizerComponents.emojiInputElement.href, + usage: 'EmojiInputElement', }, ], - dependencies: [KEY_COMBOBOX], id: KEY_EMOJI, label: 'Emoji', npmPackage: '@udecode/plate-emoji', diff --git a/apps/www/src/config/customizer-list.ts b/apps/www/src/config/customizer-list.ts index 712ad9a9f4..42bfab67ca 100644 --- a/apps/www/src/config/customizer-list.ts +++ b/apps/www/src/config/customizer-list.ts @@ -17,7 +17,6 @@ import { } from '@udecode/plate-break'; import { KEY_CAPTION } from '@udecode/plate-caption'; import { ELEMENT_CODE_BLOCK } from '@udecode/plate-code-block'; -import { KEY_COMBOBOX } from '@udecode/plate-combobox'; import { MARK_COMMENT } from '@udecode/plate-comments'; import { KEY_DND } from '@udecode/plate-dnd'; import { KEY_EMOJI } from '@udecode/plate-emoji'; @@ -109,7 +108,6 @@ export const customizerList = [ customizerItems[KEY_AUTOFORMAT], customizerItems[KEY_BLOCK_SELECTION], customizerItems[KEY_CAPTION], - customizerItems[KEY_COMBOBOX], customizerItems[KEY_DND], customizerItems[KEY_DRAG_OVER_CURSOR], customizerItems[KEY_EMOJI], @@ -178,7 +176,6 @@ export const orderedPluginKeys = [ // Functionality KEY_AUTOFORMAT, KEY_BLOCK_SELECTION, - KEY_COMBOBOX, KEY_DND, KEY_EMOJI, KEY_EXIT_BREAK, diff --git a/apps/www/src/config/customizer-plugins.ts b/apps/www/src/config/customizer-plugins.ts index 1ccc57024e..ff1c2d207c 100644 --- a/apps/www/src/config/customizer-plugins.ts +++ b/apps/www/src/config/customizer-plugins.ts @@ -6,7 +6,6 @@ import { KEY_SOFT_BREAK, } from '@udecode/plate-break'; import { KEY_CAPTION } from '@udecode/plate-caption'; -import { KEY_COMBOBOX } from '@udecode/plate-combobox'; import { MARK_COMMENT } from '@udecode/plate-comments'; import { KEY_DND } from '@udecode/plate-dnd'; import { KEY_EMOJI } from '@udecode/plate-emoji'; @@ -129,12 +128,6 @@ export const customizerPlugins = { route: '/docs/column', value: columnValue, }, - combobox: { - id: 'combobox', - label: 'Combobox', - plugins: [KEY_COMBOBOX], - route: '/docs/combobox', - }, comment: { id: 'comment', label: 'Comment', diff --git a/apps/www/src/config/descriptions.ts b/apps/www/src/config/descriptions.ts index 03d34bad91..54bc12f15f 100644 --- a/apps/www/src/config/descriptions.ts +++ b/apps/www/src/config/descriptions.ts @@ -15,7 +15,6 @@ import { KEY_SINGLE_LINE, KEY_SOFT_BREAK, } from '@udecode/plate-break'; -import { KEY_COMBOBOX } from '@udecode/plate-combobox'; import { MARK_COMMENT } from '@udecode/plate-comments'; import { KEY_DND } from '@udecode/plate-dnd'; import { KEY_EMOJI } from '@udecode/plate-emoji'; @@ -66,7 +65,6 @@ export const descriptions: Record = { [KEY_ALIGN]: 'Align your content to different positions.', [KEY_AUTOFORMAT]: 'Apply formatting automatically using shortcodes.', [KEY_BLOCK_SELECTION]: 'Select and manipulate entire text blocks.', - [KEY_COMBOBOX]: 'Select options from a predefined list.', [KEY_DELETE]: 'Remove the current block if empty when pressing delete forward', [KEY_DESERIALIZE_CSV]: 'Copy paste from CSV to Slate.', diff --git a/apps/www/src/config/docs.ts b/apps/www/src/config/docs.ts index dff5e2ed5f..684115a779 100644 --- a/apps/www/src/config/docs.ts +++ b/apps/www/src/config/docs.ts @@ -69,8 +69,8 @@ export const docsConfig: DocsConfig = { customizerComponents.dialog, customizerComponents.draggable, customizerComponents.dropdownMenu, - customizerComponents.emojiCombobox, customizerComponents.emojiDropdownMenu, + customizerComponents.emojiInputElement, customizerComponents.emojiToolbarDropdown, customizerComponents.excalidrawElement, customizerComponents.fixedToolbar, @@ -83,6 +83,7 @@ export const docsConfig: DocsConfig = { customizerComponents.imageElement, customizerComponents.indentListToolbarButton, customizerComponents.indentToolbarButton, + customizerComponents.inlineCombobox, customizerComponents.input, customizerComponents.insertDropdownMenu, customizerComponents.kbdLeaf, @@ -96,7 +97,6 @@ export const docsConfig: DocsConfig = { customizerComponents.mediaEmbedElement, customizerComponents.mediaPopover, customizerComponents.mediaToolbarButton, - customizerComponents.mentionCombobox, customizerComponents.mentionElement, customizerComponents.mentionInputElement, customizerComponents.modeDropdownMenu, diff --git a/apps/www/src/lib/plate/create-plate-ui.ts b/apps/www/src/lib/plate/create-plate-ui.ts index ccf3195330..a4f4de4210 100644 --- a/apps/www/src/lib/plate/create-plate-ui.ts +++ b/apps/www/src/lib/plate/create-plate-ui.ts @@ -61,6 +61,7 @@ import { CodeSyntaxLeaf } from '@/registry/default/plate-ui/code-syntax-leaf'; import { ColumnElement } from '@/registry/default/plate-ui/column-element'; import { ColumnGroupElement } from '@/registry/default/plate-ui/column-group-element'; import { CommentLeaf } from '@/registry/default/plate-ui/comment-leaf'; +import { EmojiInputElement } from '@/registry/default/plate-ui/emoji-input-element'; import { ExcalidrawElement } from '@/registry/default/plate-ui/excalidraw-element'; import { HeadingElement } from '@/registry/default/plate-ui/heading-element'; import { HighlightLeaf } from '@/registry/default/plate-ui/highlight-leaf'; @@ -86,6 +87,8 @@ import { TodoListElement } from '@/registry/default/plate-ui/todo-list-element'; import { ToggleElement } from '@/registry/default/plate-ui/toggle-element'; import { withDraggables } from '@/registry/default/plate-ui/with-draggables'; +import { ELEMENT_EMOJI_INPUT } from '../../../../../packages/emoji/dist'; + export const createPlateUI = ( overrideByKey?: Partial>, { @@ -100,6 +103,7 @@ export const createPlateUI = ( [ELEMENT_CODE_SYNTAX]: CodeSyntaxLeaf, [ELEMENT_COLUMN]: ColumnElement, [ELEMENT_COLUMN_GROUP]: ColumnGroupElement, + [ELEMENT_EMOJI_INPUT]: EmojiInputElement, [ELEMENT_EXCALIDRAW]: ExcalidrawElement, [ELEMENT_H1]: withProps(HeadingElement, { variant: 'h1' }), [ELEMENT_H2]: withProps(HeadingElement, { variant: 'h2' }), diff --git a/apps/www/src/lib/plate/demo/plugins/alignPlugin.ts b/apps/www/src/lib/plate/demo/plugins/alignPlugin.ts index 242531b114..3e68249b5a 100644 --- a/apps/www/src/lib/plate/demo/plugins/alignPlugin.ts +++ b/apps/www/src/lib/plate/demo/plugins/alignPlugin.ts @@ -8,6 +8,7 @@ import { ELEMENT_H5, ELEMENT_H6, } from '@udecode/plate-heading'; +import { ELEMENT_IMAGE } from '@udecode/plate-media'; import { ELEMENT_PARAGRAPH } from '@udecode/plate-paragraph'; export const alignPlugin: Partial = { @@ -20,6 +21,7 @@ export const alignPlugin: Partial = { ELEMENT_H3, ELEMENT_H4, ELEMENT_H5, + ELEMENT_IMAGE, ELEMENT_H6, ], }, diff --git a/apps/www/src/lib/plate/demo/plugins/emojiPlugin.ts b/apps/www/src/lib/plate/demo/plugins/emojiPlugin.ts deleted file mode 100644 index e4b2a00142..0000000000 --- a/apps/www/src/lib/plate/demo/plugins/emojiPlugin.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { PlatePlugin } from '@udecode/plate-common'; -import type { EmojiPlugin } from '@udecode/plate-emoji'; - -import { EmojiCombobox } from '@/registry/default/plate-ui/emoji-combobox'; - -export const emojiPlugin: Partial> = { - renderAfterEditable: EmojiCombobox, -}; diff --git a/apps/www/src/lib/plate/demo/plugins/imagePlugins.ts b/apps/www/src/lib/plate/demo/plugins/imagePlugins.ts index 346f21915f..b93dc070b1 100644 --- a/apps/www/src/lib/plate/demo/plugins/imagePlugins.ts +++ b/apps/www/src/lib/plate/demo/plugins/imagePlugins.ts @@ -1,5 +1,4 @@ import { createBasicElementsPlugin } from '@udecode/plate-basic-elements'; -import { createComboboxPlugin } from '@udecode/plate-combobox'; import { createPlugins } from '@udecode/plate-core'; import { createImagePlugin } from '@udecode/plate-media'; import { createSelectOnBackspacePlugin } from '@udecode/plate-select'; @@ -15,7 +14,6 @@ export const imagePlugins = createPlugins( ...basicMarksPlugins, createImagePlugin(), createSelectOnBackspacePlugin(selectOnBackspacePlugin), - createComboboxPlugin(), ], { components: plateUI, diff --git a/apps/www/src/lib/plate/demo/values/emojiValue.tsx b/apps/www/src/lib/plate/demo/values/emojiValue.tsx index 3de8eb391a..2d4680b9a1 100644 --- a/apps/www/src/lib/plate/demo/values/emojiValue.tsx +++ b/apps/www/src/lib/plate/demo/values/emojiValue.tsx @@ -8,8 +8,6 @@ export const emojiValue: any = ( 🙂 Emoji's Express yourself with a touch of fun 🎉 and emotion 😃. - - Pick from the toolbar or write after the colon to open the combobox : - + Pick from the toolbar or type a colon to open the combobox. ); diff --git a/apps/www/src/lib/plate/demo/values/mentionables.ts b/apps/www/src/lib/plate/demo/values/mentionables.ts index ce62bca330..4669febd35 100644 --- a/apps/www/src/lib/plate/demo/values/mentionables.ts +++ b/apps/www/src/lib/plate/demo/values/mentionables.ts @@ -1,6 +1,10 @@ -import type { TComboboxItem } from '@udecode/plate-combobox'; +import type { TMentionItemBase } from '@udecode/plate-mention'; -export const MENTIONABLES: TComboboxItem[] = [ +export interface MyMentionItem extends TMentionItemBase { + key: string; +} + +export const MENTIONABLES: MyMentionItem[] = [ { key: '0', text: 'Aayla Secura' }, { key: '1', text: 'Adi Gallia' }, { diff --git a/apps/www/src/lib/plate/demo/values/slashRules.ts b/apps/www/src/lib/plate/demo/values/slashRules.ts deleted file mode 100644 index 48f6301fd1..0000000000 --- a/apps/www/src/lib/plate/demo/values/slashRules.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { SlashRule } from '@udecode/plate-slash-command'; - -import { type PlateEditor, toggleNodeType } from '@udecode/plate-core'; -import { ELEMENT_H1, ELEMENT_H2, ELEMENT_H3 } from '@udecode/plate-heading'; -import { ListStyleType, toggleIndentList } from '@udecode/plate-indent-list'; -import { focusEditor } from '@udecode/slate-react'; - -export const SLASH_RULES: SlashRule[] = [ - { - key: ELEMENT_H1, - onTrigger(editor: PlateEditor) { - toggleNodeType(editor, { activeType: ELEMENT_H1 }); - focusEditor(editor); - }, - text: 'Heading 1', - }, - { - key: ELEMENT_H2, - onTrigger(editor: PlateEditor) { - toggleNodeType(editor, { activeType: ELEMENT_H2 }); - focusEditor(editor); - }, - text: 'Heading 2', - }, - { - key: ELEMENT_H3, - onTrigger(editor: PlateEditor) { - toggleNodeType(editor, { activeType: ELEMENT_H3 }); - focusEditor(editor); - }, - text: 'Heading 3', - }, - { - key: ListStyleType.Disc, - onTrigger(editor: PlateEditor) { - toggleIndentList(editor, { - listStyleType: ListStyleType.Disc, - }); - focusEditor(editor); - }, - text: 'Bulleted list', - }, - { - key: ListStyleType.Decimal, - onTrigger(editor: PlateEditor) { - toggleIndentList(editor, { - listStyleType: ListStyleType.Decimal, - }); - focusEditor(editor); - }, - text: 'Numbered list', - }, -]; diff --git a/apps/www/src/registry/default/example/playground-demo.tsx b/apps/www/src/registry/default/example/playground-demo.tsx index 8620320767..56b1e5f81b 100644 --- a/apps/www/src/registry/default/example/playground-demo.tsx +++ b/apps/www/src/registry/default/example/playground-demo.tsx @@ -32,7 +32,6 @@ import { ELEMENT_CODE_BLOCK, createCodeBlockPlugin, } from '@udecode/plate-code-block'; -import { createComboboxPlugin } from '@udecode/plate-combobox'; import { createCommentsPlugin } from '@udecode/plate-comments'; import { Plate, @@ -96,7 +95,6 @@ import { settingsStore } from '@/components/context/settings-store'; import { PlaygroundFixedToolbarButtons } from '@/components/plate-ui/playground-fixed-toolbar-buttons'; import { PlaygroundFloatingToolbarButtons } from '@/components/plate-ui/playground-floating-toolbar-buttons'; import { captionPlugin } from '@/lib/plate/demo/plugins/captionPlugin'; -import { SLASH_RULES } from '@/lib/plate/demo/values/slashRules'; import { createPlateUI } from '@/plate/create-plate-ui'; import { CommentsProvider } from '@/plate/demo/comments/CommentsProvider'; import { editableProps } from '@/plate/demo/editableProps'; @@ -106,7 +104,6 @@ import { autoformatIndentLists } from '@/plate/demo/plugins/autoformatIndentList import { autoformatLists } from '@/plate/demo/plugins/autoformatLists'; import { autoformatRules } from '@/plate/demo/plugins/autoformatRules'; import { dragOverCursorPlugin } from '@/plate/demo/plugins/dragOverCursorPlugin'; -import { emojiPlugin } from '@/plate/demo/plugins/emojiPlugin'; import { exitBreakPlugin } from '@/plate/demo/plugins/exitBreakPlugin'; import { forcedLayoutPlugin } from '@/plate/demo/plugins/forcedLayoutPlugin'; import { lineHeightPlugin } from '@/plate/demo/plugins/lineHeightPlugin'; @@ -116,7 +113,6 @@ import { selectOnBackspacePlugin } from '@/plate/demo/plugins/selectOnBackspaceP import { softBreakPlugin } from '@/plate/demo/plugins/softBreakPlugin'; import { tabbablePlugin } from '@/plate/demo/plugins/tabbablePlugin'; import { trailingBlockPlugin } from '@/plate/demo/plugins/trailingBlockPlugin'; -import { MENTIONABLES } from '@/plate/demo/values/mentionables'; import { usePlaygroundValue } from '@/plate/demo/values/usePlaygroundValue'; import { CommentsPopover } from '@/registry/default/plate-ui/comments-popover'; import { CursorOverlay } from '@/registry/default/plate-ui/cursor-overlay'; @@ -131,8 +127,6 @@ import { TodoLi, TodoMarker, } from '@/registry/default/plate-ui/indent-todo-marker-component'; -import { MentionCombobox } from '@/registry/default/plate-ui/mention-combobox'; -import { SlashCombobox } from '@/registry/default/plate-ui/slash-combobox'; export const usePlaygroundPlugins = ({ components = createPlateUI(), @@ -181,11 +175,7 @@ export const usePlaygroundPlugins = ({ triggerPreviousCharPattern: /^$|^[\s"']$/, }, }), - createSlashPlugin({ - options: { - rules: SLASH_RULES, - }, - }), + createSlashPlugin(), createTablePlugin({ enabled: !!enabled.table, options: { @@ -213,7 +203,10 @@ export const usePlaygroundPlugins = ({ createKbdPlugin({ enabled: !!enabled.kbd }), // Block Style - createAlignPlugin({ ...alignPlugin, enabled: !!enabled.align }), + createAlignPlugin({ + ...alignPlugin, + enabled: !!enabled.align, + }), createIndentPlugin({ enabled: !!enabled.indent, inject: { @@ -279,18 +272,18 @@ export const usePlaygroundPlugins = ({ createBlockSelectionPlugin({ enabled: id === 'blockselection' || !!enabled.blockSelection, options: { + disableContextMenu: true, sizes: { bottom: 0, top: 0, }, }, }), - createComboboxPlugin({ enabled: !!enabled.combobox }), createDndPlugin({ enabled: !!enabled.dnd, options: { enableScroller: true }, }), - createEmojiPlugin({ ...emojiPlugin, enabled: !!enabled.emoji }), + createEmojiPlugin({ enabled: !!enabled.emoji }), createExitBreakPlugin({ ...exitBreakPlugin, enabled: !!enabled.exitBreak, @@ -439,12 +432,6 @@ export default function PlaygroundDemo({ id }: { id?: ValueId }) { )} - {isEnabled('mention', id, enabled['mention-combobox']) && ( - - )} - - - {isEnabled('cursoroverlay', id) && ( )} diff --git a/apps/www/src/registry/default/plate-ui/caption.tsx b/apps/www/src/registry/default/plate-ui/caption.tsx index 6ca40e099b..bbd5ec41e1 100644 --- a/apps/www/src/registry/default/plate-ui/caption.tsx +++ b/apps/www/src/registry/default/plate-ui/caption.tsx @@ -1,10 +1,19 @@ -import { cn, withCn, withVariants } from '@udecode/cn'; +import { + cn, + createPrimitiveComponent, + withCn, + withVariants, +} from '@udecode/cn'; import { Caption as CaptionPrimitive, CaptionTextarea as CaptionTextareaPrimitive, + useCaptionButton, + useCaptionButtonState, } from '@udecode/plate-caption'; import { cva } from 'class-variance-authority'; +import { Button } from './button'; + const captionVariants = cva('max-w-full', { defaultVariants: { align: 'center', @@ -30,3 +39,8 @@ export const CaptionTextarea = withCn( 'text-center print:placeholder:text-transparent' ) ); + +export const CaptionButton = createPrimitiveComponent(Button)({ + propsHook: useCaptionButton, + stateHook: useCaptionButtonState, +}); diff --git a/apps/www/src/registry/default/plate-ui/combobox.tsx b/apps/www/src/registry/default/plate-ui/combobox.tsx deleted file mode 100644 index ad12816a9e..0000000000 --- a/apps/www/src/registry/default/plate-ui/combobox.tsx +++ /dev/null @@ -1,160 +0,0 @@ -'use client'; - -import React, { useEffect } from 'react'; - -import * as Popover from '@radix-ui/react-popover'; -import { cn, withRef } from '@udecode/cn'; -import { - type ComboboxContentItemProps, - type ComboboxContentProps, - type ComboboxProps, - comboboxActions, - useActiveComboboxStore, - useComboboxContent, - useComboboxContentState, - useComboboxControls, - useComboboxItem, - useComboboxSelectors, -} from '@udecode/plate-combobox'; -import { - useEditorRef, - useEditorSelector, - useEventEditorSelectors, - usePlateSelectors, -} from '@udecode/plate-common'; -import { - createVirtualRef, - getBoundingClientRect, -} from '@udecode/plate-floating'; - -export const ComboboxItem = withRef<'div', ComboboxContentItemProps>( - ({ className, combobox, index, item, onRenderItem, ...rest }, ref) => { - const { props } = useComboboxItem({ combobox, index, item, onRenderItem }); - - return ( -
- ); - } -); - -export function ComboboxContent(props: ComboboxContentProps) { - const { - combobox, - component: Component, - items, - onRenderItem, - portalElement, - } = props; - - const editor = useEditorRef(); - - const filteredItems = useComboboxSelectors.filteredItems(); - const activeComboboxStore = useActiveComboboxStore()!; - - const state = useComboboxContentState({ combobox, items }); - const { menuProps, targetRange } = useComboboxContent(state); - - const virtualRef = createVirtualRef(editor, targetRange ?? undefined, { - fallbackRect: getBoundingClientRect(editor, editor.selection!), - }); - - return ( - - - - - event.preventDefault()} - side="bottom" - sideOffset={5} - > - {Component ? Component({ store: activeComboboxStore }) : null} - - {filteredItems.map((item, index) => ( - - ))} - - - - ); -} - -export function Combobox({ - controlled, - disabled: _disabled, - filter, - id, - maxSuggestions, - onSelectItem, - searchPattern, - sort, - trigger, - ...props -}: ComboboxProps) { - const storeItems = useComboboxSelectors.items(); - const disabled = - _disabled ?? (storeItems.length === 0 && !props.items?.length); - - const focusedEditorId = useEventEditorSelectors.focus?.(); - const combobox = useComboboxControls(); - const activeId = useComboboxSelectors.activeId(); - const selectionDefined = useEditorSelector( - (editor) => !!editor.selection, - [] - ); - const editorId = usePlateSelectors().id(); - - useEffect(() => { - comboboxActions.setComboboxById({ - controlled, - filter, - id, - maxSuggestions, - onSelectItem, - searchPattern, - sort, - trigger, - }); - }, [ - id, - trigger, - searchPattern, - controlled, - onSelectItem, - maxSuggestions, - filter, - sort, - ]); - - if ( - !combobox || - !selectionDefined || - focusedEditorId !== editorId || - activeId !== id || - disabled - ) { - return null; - } - - return ; -} diff --git a/apps/www/src/registry/default/plate-ui/draggable.tsx b/apps/www/src/registry/default/plate-ui/draggable.tsx index 983f3beb0f..88619cf789 100644 --- a/apps/www/src/registry/default/plate-ui/draggable.tsx +++ b/apps/www/src/registry/default/plate-ui/draggable.tsx @@ -2,23 +2,32 @@ import React from 'react'; -import type { - ClassNames, - PlateElementProps, - TEditor, -} from '@udecode/plate-common'; import type { DropTargetMonitor } from 'react-dnd'; import { cn, withRef } from '@udecode/cn'; +import { + type ClassNames, + type PlateElementProps, + type TEditor, + type TElement, + useEditorRef, + useElement, +} from '@udecode/plate-common'; import { type DragItemNode, useDraggable, useDraggableState, } from '@udecode/plate-dnd'; +import { blockSelectionActions } from '@udecode/plate-selection'; import { Icons } from '@/components/icons'; -import { Tooltip, TooltipContent, TooltipPortal, TooltipTrigger } from './tooltip'; +import { + Tooltip, + TooltipContent, + TooltipPortal, + TooltipTrigger, +} from './tooltip'; export interface DraggableProps extends PlateElementProps, @@ -68,16 +77,35 @@ export interface DraggableProps ) => boolean; } -const dragHandle = ( - - - - - - Drag to move - - -); +const DragHandle = () => { + const editor = useEditorRef(); + const element = useElement(); + + return ( + + + { + event.stopPropagation(); + event.preventDefault(); + + // if (element.id) { + // blockSelectionActions.addSelectedRow(element.id as string); + // blockContextMenuActions.show(editor.id, event as any); + // } + }} + onMouseDown={() => { + blockSelectionActions.resetSelectedIds(); + }} + /> + + + Drag to move + + + ); +}; export const Draggable = withRef<'div', DraggableProps>( ({ className, classNames = {}, onDropHandler, ...props }, ref) => { @@ -106,7 +134,7 @@ export const Draggable = withRef<'div', DraggableProps>( >
( classNames.blockToolbar )} > -
- {isHovered && dragHandle} +
+ {isHovered && }
diff --git a/apps/www/src/registry/default/plate-ui/emoji-combobox.tsx b/apps/www/src/registry/default/plate-ui/emoji-combobox.tsx deleted file mode 100644 index b8c0ed0b88..0000000000 --- a/apps/www/src/registry/default/plate-ui/emoji-combobox.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import React from 'react'; - -import type { ComboboxItemProps } from '@udecode/plate-combobox'; - -import { - type EmojiItemData, - KEY_EMOJI, - type TEmojiCombobox, - useEmojiComboboxState, -} from '@udecode/plate-emoji'; - -import { Combobox } from './combobox'; - -export function EmojiComboboxItem({ item }: ComboboxItemProps) { - const { - data: { emoji, id }, - } = item; - - return ( -
- {emoji} :{id}: -
- ); -} - -export function EmojiCombobox({ - pluginKey = KEY_EMOJI, - id = pluginKey, - ...props -}: TEmojiCombobox) { - const { onSelectItem, trigger } = useEmojiComboboxState({ pluginKey }); - - return ( - - ); -} diff --git a/apps/www/src/registry/default/plate-ui/emoji-input-element.tsx b/apps/www/src/registry/default/plate-ui/emoji-input-element.tsx new file mode 100644 index 0000000000..a25ca51cdc --- /dev/null +++ b/apps/www/src/registry/default/plate-ui/emoji-input-element.tsx @@ -0,0 +1,70 @@ +import React, { useMemo, useState } from 'react'; + +import { withRef } from '@udecode/cn'; +import { PlateElement } from '@udecode/plate-common'; +import { EmojiInlineIndexSearch, insertEmoji } from '@udecode/plate-emoji'; + +import { useDebounce } from '@/hooks/use-debounce'; + +import { + InlineCombobox, + InlineComboboxContent, + InlineComboboxEmpty, + InlineComboboxInput, + InlineComboboxItem, +} from './inline-combobox'; + +export const EmojiInputElement = withRef( + ({ className, ...props }, ref) => { + const { children, editor, element } = props; + const [value, setValue] = useState(''); + const debouncedValue = useDebounce(value, 100); + const isPending = value !== debouncedValue; + + const filteredEmojis = useMemo(() => { + if (debouncedValue.trim().length === 0) return []; + + return EmojiInlineIndexSearch.getInstance() + .search(debouncedValue.replace(/:$/, '')) + .get(); + }, [debouncedValue]); + + return ( + + + + + + {!isPending && ( + No matching emoji found + )} + + {filteredEmojis.map((emoji) => ( + insertEmoji(editor, emoji)} + value={emoji.name} + > + {emoji.skins[0].native} {emoji.name} + + ))} + + + + {children} + + ); + } +); diff --git a/apps/www/src/registry/default/plate-ui/floating-toolbar.tsx b/apps/www/src/registry/default/plate-ui/floating-toolbar.tsx index e61a0a1aac..09c412e6fe 100644 --- a/apps/www/src/registry/default/plate-ui/floating-toolbar.tsx +++ b/apps/www/src/registry/default/plate-ui/floating-toolbar.tsx @@ -3,7 +3,12 @@ import React from 'react'; import { cn, withRef } from '@udecode/cn'; -import { PortalBody, useComposedRef } from '@udecode/plate-common'; +import { + PortalBody, + useComposedRef, + useEventEditorSelectors, + usePlateSelectors, +} from '@udecode/plate-common'; import { type FloatingToolbarState, flip, @@ -20,7 +25,12 @@ export const FloatingToolbar = withRef< state?: FloatingToolbarState; } >(({ children, state, ...props }, componentRef) => { + const editorId = usePlateSelectors().id(); + const focusedEditorId = useEventEditorSelectors.focus(); + const floatingToolbarState = useFloatingToolbarState({ + editorId, + focusedEditorId, ...state, floatingOptions: { middleware: [ diff --git a/apps/www/src/registry/default/plate-ui/image-element.tsx b/apps/www/src/registry/default/plate-ui/image-element.tsx index 94e8ecfcad..037a2f0be6 100644 --- a/apps/www/src/registry/default/plate-ui/image-element.tsx +++ b/apps/www/src/registry/default/plate-ui/image-element.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { cn, withRef } from '@udecode/cn'; +import { CaptionProvider } from '@udecode/plate-caption'; import { PlateElement, withHOC } from '@udecode/plate-common'; import { ELEMENT_IMAGE, Image, useMediaState } from '@udecode/plate-media'; import { ResizableProvider, useResizableStore } from '@udecode/plate-resizable'; @@ -14,58 +15,69 @@ import { } from './resizable'; export const ImageElement = withHOC( - ResizableProvider, - withRef( - ({ children, className, nodeProps, ...props }, ref) => { - const { align = 'center', focused, readOnly, selected } = useMediaState(); - const width = useResizableStore().get.width(); + CaptionProvider, + withHOC( + ResizableProvider, + withRef( + ({ children, className, nodeProps, ...props }, ref) => { + const { + align = 'center', + focused, + readOnly, + selected, + } = useMediaState(); - return ( - - -
- - - - - + const width = useResizableStore().get.width(); - - - -
+ return ( + + +
+ + + + + - {children} - - - ); - } + + + +
+ + {children} +
+
+ ); + } + ) ) ); diff --git a/apps/www/src/registry/default/plate-ui/inline-combobox.tsx b/apps/www/src/registry/default/plate-ui/inline-combobox.tsx new file mode 100644 index 0000000000..29b4641e9a --- /dev/null +++ b/apps/www/src/registry/default/plate-ui/inline-combobox.tsx @@ -0,0 +1,368 @@ +import React, { + type HTMLAttributes, + type ReactNode, + type RefObject, + createContext, + forwardRef, + startTransition, + useCallback, + useContext, + useEffect, + useMemo, + useState, +} from 'react'; + +import type { PointRef } from 'slate'; + +import { + Combobox, + ComboboxItem, + type ComboboxItemProps, + ComboboxPopover, + ComboboxProvider, + Portal, + useComboboxContext, + useComboboxStore, +} from '@ariakit/react'; +import { cn } from '@udecode/cn'; +import { + type UseComboboxInputResult, + filterWords, + useComboboxInput, + useHTMLInputCursorState, +} from '@udecode/plate-combobox'; +import { + type TElement, + createPointRef, + findNodePath, + getPointBefore, + insertText, + moveSelection, + useComposedRef, + useEditorRef, +} from '@udecode/plate-common'; +import { cva } from 'class-variance-authority'; + +type FilterFn = ( + item: { keywords?: string[]; value: string }, + search: string +) => boolean; + +interface InlineComboboxContextValue { + filter: FilterFn | false; + inputProps: UseComboboxInputResult['props']; + inputRef: RefObject; + removeInput: UseComboboxInputResult['removeInput']; + setHasEmpty: (hasEmpty: boolean) => void; + showTrigger: boolean; + trigger: string; +} + +const InlineComboboxContext = createContext( + null as any +); + +export const defaultFilter: FilterFn = ({ keywords = [], value }, search) => + [value, ...keywords].some((keyword) => filterWords(keyword, search)); + +interface InlineComboboxProps { + children: ReactNode; + element: TElement; + trigger: string; + filter?: FilterFn | false; + hideWhenNoValue?: boolean; + setValue?: (value: string) => void; + showTrigger?: boolean; + value?: string; +} + +const InlineCombobox = ({ + children, + element, + filter = defaultFilter, + hideWhenNoValue = false, + setValue: setValueProp, + showTrigger = true, + trigger, + value: valueProp, +}: InlineComboboxProps) => { + const editor = useEditorRef(); + const inputRef = React.useRef(null); + const cursorState = useHTMLInputCursorState(inputRef); + + const [valueState, setValueState] = useState(''); + const hasValueProp = valueProp !== undefined; + const value = hasValueProp ? valueProp : valueState; + + const setValue = useCallback( + (newValue: string) => { + setValueProp?.(newValue); + + if (!hasValueProp) { + setValueState(newValue); + } + }, + [setValueProp, hasValueProp] + ); + + /** + * Track the point just before the input element so we know where to + * insertText if the combobox closes due to a selection change. + */ + const [insertPoint, setInsertPoint] = useState(null); + + useEffect(() => { + const path = findNodePath(editor, element); + + if (!path) return; + + const point = getPointBefore(editor, path); + + if (!point) return; + + const pointRef = createPointRef(editor, point); + setInsertPoint(pointRef); + + return () => { + pointRef.unref(); + }; + }, [editor, element]); + + const { props: inputProps, removeInput } = useComboboxInput({ + cancelInputOnBlur: false, + cursorState, + onCancelInput: (cause) => { + if (cause !== 'backspace') { + insertText(editor, trigger + value, { + at: insertPoint?.current ?? undefined, + }); + } + if (cause === 'arrowLeft' || cause === 'arrowRight') { + moveSelection(editor, { + distance: 1, + reverse: cause === 'arrowLeft', + }); + } + }, + ref: inputRef, + }); + + const [hasEmpty, setHasEmpty] = useState(false); + + const contextValue: InlineComboboxContextValue = useMemo( + () => ({ + filter, + inputProps, + inputRef, + removeInput, + setHasEmpty, + showTrigger, + trigger, + }), + [ + trigger, + showTrigger, + filter, + inputRef, + inputProps, + removeInput, + setHasEmpty, + ] + ); + + const store = useComboboxStore({ + // open: , + setValue: (newValue) => startTransition(() => setValue(newValue)), + }); + + const items = store.useState('items'); + + useEffect; + + /** + * If there is no active ID and the list of items changes, select the first + * item. + */ + useEffect(() => { + if (!store.getState().activeId) { + store.setActiveId(store.first()); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [items, store]); + + return ( + + 0 || hasEmpty) && + (!hideWhenNoValue || value.length > 0) + } + store={store} + > + + {children} + + + + ); +}; + +const InlineComboboxInput = forwardRef< + HTMLInputElement, + HTMLAttributes +>(({ className, ...props }, propRef) => { + const { + inputProps, + inputRef: contextRef, + showTrigger, + trigger, + } = useContext(InlineComboboxContext); + + const store = useComboboxContext()!; + const value = store.useState('value'); + + const ref = useComposedRef(propRef, contextRef); + + /** + * To create an auto-resizing input, we render a visually hidden span + * containing the input value and position the input element on top of it. + * This works well for all cases except when input exceeds the width of the + * container. + */ + + return ( + <> + {showTrigger && trigger} + + + + + + + + ); +}); + +InlineComboboxInput.displayName = 'InlineComboboxInput'; + +const InlineComboboxContent: typeof ComboboxPopover = ({ + className, + ...props +}) => { + // Portal prevents CSS from leaking into popover + return ( + + + + ); +}; + +const comboboxItemVariants = cva( + 'relative flex h-9 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none', + { + defaultVariants: { + interactive: true, + }, + variants: { + interactive: { + false: '', + true: 'cursor-pointer transition-colors hover:bg-accent hover:text-accent-foreground data-[active-item=true]:bg-accent data-[active-item=true]:text-accent-foreground', + }, + }, + } +); + +export type InlineComboboxItemProps = { + keywords?: string[]; +} & ComboboxItemProps & + Required>; + +const InlineComboboxItem = ({ + className, + keywords, + onClick, + ...props +}: InlineComboboxItemProps) => { + const { value } = props; + + const { filter, removeInput } = useContext(InlineComboboxContext); + + const store = useComboboxContext()!; + + // Optimization: Do not subscribe to value if filter is false + const search = filter && store.useState('value'); + + const visible = useMemo( + () => !filter || filter({ keywords, value }, search as string), + [filter, value, keywords, search] + ); + + if (!visible) return null; + + return ( + { + removeInput(true); + onClick?.(event); + }} + {...props} + /> + ); +}; + +const InlineComboboxEmpty = ({ + children, + className, +}: HTMLAttributes) => { + const { setHasEmpty } = useContext(InlineComboboxContext); + const store = useComboboxContext()!; + const items = store.useState('items'); + + useEffect(() => { + setHasEmpty(true); + + return () => { + setHasEmpty(false); + }; + }, [setHasEmpty]); + + if (items.length > 0) return null; + + return ( +
+ {children} +
+ ); +}; + +export { + InlineCombobox, + InlineComboboxContent, + InlineComboboxEmpty, + InlineComboboxInput, + InlineComboboxItem, +}; diff --git a/apps/www/src/registry/default/plate-ui/media-embed-element.tsx b/apps/www/src/registry/default/plate-ui/media-embed-element.tsx index 1f3f0f1950..86aebf62f7 100644 --- a/apps/www/src/registry/default/plate-ui/media-embed-element.tsx +++ b/apps/www/src/registry/default/plate-ui/media-embed-element.tsx @@ -3,6 +3,7 @@ import LiteYouTubeEmbed from 'react-lite-youtube-embed'; import { Tweet } from 'react-tweet'; import { cn, withRef } from '@udecode/cn'; +import { CaptionProvider } from '@udecode/plate-caption'; import { PlateElement, withHOC } from '@udecode/plate-common'; import { ELEMENT_MEDIA_EMBED, @@ -21,117 +22,125 @@ import { } from './resizable'; export const MediaEmbedElement = withHOC( - ResizableProvider, - withRef(({ children, className, ...props }, ref) => { - const { - align = 'center', - embed, - focused, - isTweet, - isVideo, - isYoutube, - readOnly, - selected, - } = useMediaState({ - urlParsers: [parseTwitterUrl, parseVideoUrl], - }); - const width = useResizableStore().get.width(); - const provider = embed?.provider; + CaptionProvider, + withHOC( + ResizableProvider, + withRef(({ children, className, ...props }, ref) => { + const { + align = 'center', + embed, + focused, + isTweet, + isVideo, + isYoutube, + readOnly, + selected, + } = useMediaState({ + urlParsers: [parseTwitterUrl, parseVideoUrl], + }); + const width = useResizableStore().get.width(); + const provider = embed?.provider; - return ( - - -
- + +
- + + - {isVideo ? ( - isYoutube ? ( - _iframe]:absolute [&_>_iframe]:left-0 [&_>_iframe]:top-0 [&_>_iframe]:size-full', - '[&_>_.lty-playbtn]:z-[1] [&_>_.lty-playbtn]:h-[46px] [&_>_.lty-playbtn]:w-[70px] [&_>_.lty-playbtn]:rounded-[14%] [&_>_.lty-playbtn]:bg-[#212121] [&_>_.lty-playbtn]:opacity-80 [&_>_.lty-playbtn]:[transition:all_0.2s_cubic-bezier(0,_0,_0.2,_1)]', - '[&:hover_>_.lty-playbtn]:bg-[red] [&:hover_>_.lty-playbtn]:opacity-100', - '[&_>_.lty-playbtn]:before:border-y-[11px] [&_>_.lty-playbtn]:before:border-l-[19px] [&_>_.lty-playbtn]:before:border-r-0 [&_>_.lty-playbtn]:before:border-[transparent_transparent_transparent_#fff] [&_>_.lty-playbtn]:before:content-[""]', - '[&_>_.lty-playbtn]:absolute [&_>_.lty-playbtn]:left-1/2 [&_>_.lty-playbtn]:top-1/2 [&_>_.lty-playbtn]:[transform:translate3d(-50%,-50%,0)]', - '[&_>_.lty-playbtn]:before:absolute [&_>_.lty-playbtn]:before:left-1/2 [&_>_.lty-playbtn]:before:top-1/2 [&_>_.lty-playbtn]:before:[transform:translate3d(-50%,-50%,0)]', - '[&.lyt-activated]:cursor-[unset]', - '[&.lyt-activated]:before:pointer-events-none [&.lyt-activated]:before:opacity-0', - '[&.lyt-activated_>_.lty-playbtn]:pointer-events-none [&.lyt-activated_>_.lty-playbtn]:!opacity-0' - )} - /> - ) : ( + {isVideo ? ( + isYoutube ? ( + _iframe]:absolute [&_>_iframe]:left-0 [&_>_iframe]:top-0 [&_>_iframe]:size-full', + '[&_>_.lty-playbtn]:z-[1] [&_>_.lty-playbtn]:h-[46px] [&_>_.lty-playbtn]:w-[70px] [&_>_.lty-playbtn]:rounded-[14%] [&_>_.lty-playbtn]:bg-[#212121] [&_>_.lty-playbtn]:opacity-80 [&_>_.lty-playbtn]:[transition:all_0.2s_cubic-bezier(0,_0,_0.2,_1)]', + '[&:hover_>_.lty-playbtn]:bg-[red] [&:hover_>_.lty-playbtn]:opacity-100', + '[&_>_.lty-playbtn]:before:border-y-[11px] [&_>_.lty-playbtn]:before:border-l-[19px] [&_>_.lty-playbtn]:before:border-r-0 [&_>_.lty-playbtn]:before:border-[transparent_transparent_transparent_#fff] [&_>_.lty-playbtn]:before:content-[""]', + '[&_>_.lty-playbtn]:absolute [&_>_.lty-playbtn]:left-1/2 [&_>_.lty-playbtn]:top-1/2 [&_>_.lty-playbtn]:[transform:translate3d(-50%,-50%,0)]', + '[&_>_.lty-playbtn]:before:absolute [&_>_.lty-playbtn]:before:left-1/2 [&_>_.lty-playbtn]:before:top-1/2 [&_>_.lty-playbtn]:before:[transform:translate3d(-50%,-50%,0)]', + '[&.lyt-activated]:cursor-[unset]', + '[&.lyt-activated]:before:pointer-events-none [&.lyt-activated]:before:opacity-0', + '[&.lyt-activated_>_.lty-playbtn]:pointer-events-none [&.lyt-activated_>_.lty-playbtn]:!opacity-0' + )} + /> + ) : ( +
+