diff --git a/.changeset/combobox.md b/.changeset/combobox.md
new file mode 100644
index 0000000000..7bbf7302a1
--- /dev/null
+++ b/.changeset/combobox.md
@@ -0,0 +1,25 @@
+---
+"@udecode/plate-combobox": major
+---
+
+- Major rework. The combobox package is no longer a plugin. Instead, it is now a collection of utilities that can be used by other plugins and components.
+- Added the following exports:
+ - `withTriggerCombobox`: Insert a combobox input when a trigger character is typed
+ - `TriggerComboboxPlugin`: Plugin options type for `withTriggerCombobox`
+ - `useComboboxInput`: Manages the behavior of an inline combobox input element
+ - `useHTMLInputCursorState`: Tracks whether the cursor is at the start or end of a HTML ` ` element
+ - `ComboboxInputCursorState`: Return type for `useHTMLInputCursorState`
+ - `CancelComboboxInputCause`: A unison type of possible reasons why a combobox input may be cancelled (used by `useComboboxInput`)
+- Removed the following exports:
+ - `comboboxStore`
+ - `createComboboxPlugin`
+ - `useComboboxContent`
+ - `useComboboxControls`
+ - `useComboboxItem`
+ - `onChangeCombobox`
+ - `onKeyDownCombobox`
+ - `ComboboxOnSelectItem`
+ - `ComboboxProps`
+ - `getNextNonDisabledIndex`
+ - `getNextWrappingIndex`
+ - `getTextFromTrigger`
diff --git a/.changeset/emoji.md b/.changeset/emoji.md
new file mode 100644
index 0000000000..e03f662bc2
--- /dev/null
+++ b/.changeset/emoji.md
@@ -0,0 +1,12 @@
+---
+"@udecode/plate-emoji": major
+---
+
+- Now uses the reworked combobox package
+- Added `ELEMENT_EMOJI_INPUT`; combobox functionality must now be handled in the component
+- Plugin options:
+ - Now extends from `TriggerComboboxPlugin`
+ - Added `createEmojiNode` to support custom emoji nodes
+ - Removed `emojiTriggeringController`
+ - Removed `id` (no longer needed)
+ - Removed `createEmoji`
diff --git a/.changeset/mention.md b/.changeset/mention.md
new file mode 100644
index 0000000000..e94a788aea
--- /dev/null
+++ b/.changeset/mention.md
@@ -0,0 +1,18 @@
+---
+"@udecode/plate-mention": major
+---
+
+- Now uses the reworked combobox package
+- `ELEMENT_MENTION_INPUT` is now an inline void element, and combobox functionality must now be handled in the component
+- Plugin options:
+ - Now extends from `TriggerComboboxPlugin`
+ - Renamed `query` to `triggerQuery` (provided by `TriggerComboboxPlugin`)
+ - Removed `id` (no longer needed)
+ - Removed `inputCreation` (see `TriggerComboboxPlugin['createComboboxInput']`)
+- Removed queries and transforms relating to the mention input:
+ - `findMentionInput`
+ - `isNodeMentionInput`
+ - `isSelectionInMentionInput`
+ - `removeMentionInput`
+- Removed `withMention` (no longer needed)
+- Removed `mentionOnKeyDownHandler` (no longer needed)
diff --git a/.changeset/slash.md b/.changeset/slash.md
new file mode 100644
index 0000000000..0779c109a6
--- /dev/null
+++ b/.changeset/slash.md
@@ -0,0 +1,20 @@
+---
+"@udecode/plate-slash-command": major
+---
+
+- Now uses the reworked combobox package
+- `ELEMENT_SLASH_INPUT` is now an inline void element, and combobox functionality must now be handled in the component
+- Replaced all plugin options with those extended from `TriggerComboboxPlugin`
+ - Removed `createSlashNode`
+ - Removed `id` (no longer needed)
+ - Removed `inputCreation` (see `createComboboxInput`)
+ - Renamed `query` to `triggerQuery` (provided by `TriggerComboboxPlugin`)
+ - Removed `rules`: Slash command rules must now be provided in the component
+- Removed queries and transforms relating to the slash input:
+ - `findSlashInput`
+ - `isNodeSlashInput`
+ - `isSelectionInSlashInput`
+ - `removeSlashInput`
+- Removed `withSlashCommand` (no longer needed)
+- Removed `slashOnKeyDownHandler` (no longer needed)
+- Removed `getSlashOnSelectItem`: This should now be handled in the component
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/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/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/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/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/config/customizer-components.ts b/apps/www/src/config/customizer-components.ts
index 71312cbb37..0a2206eeea 100644
--- a/apps/www/src/config/customizer-components.ts
+++ b/apps/www/src/config/customizer-components.ts
@@ -76,14 +76,15 @@ export const customizerComponents = {
href: '/docs/components/editor',
title: 'Editor',
},
- emojiCombobox: {
- href: '/docs/components/emoji-combobox',
- title: 'Emoji Combobox',
- },
emojiDropdownMenu: {
href: '/docs/components/emoji-dropdown-menu',
title: 'Emoji Dropdown Menu',
},
+ emojiInputElement: {
+ href: '/docs/components/emoji-input-element',
+ label: 'Element',
+ title: 'Emoji Input',
+ },
emojiToolbarDropdown: {
href: '/docs/components/emoji-toolbar-dropdown',
title: 'Emoji Toolbar Dropdown',
@@ -137,6 +138,10 @@ export const customizerComponents = {
href: '/docs/components/indent-toolbar-button',
title: 'Indent Toolbar Button',
},
+ inlineCombobox: {
+ href: '/docs/components/inline-combobox',
+ title: 'Inline Combobox',
+ },
input: { href: '/docs/components/input', title: 'Input' },
insertDropdownMenu: {
href: '/docs/components/insert-dropdown-menu',
@@ -186,10 +191,6 @@ export const customizerComponents = {
href: '/docs/components/media-toolbar-button',
title: 'Media Toolbar Button',
},
- mentionCombobox: {
- href: '/docs/components/mention-combobox',
- title: 'Mention Combobox',
- },
mentionElement: {
href: '/docs/components/mention-element',
label: 'Element',
diff --git a/apps/www/src/config/customizer-items.ts b/apps/www/src/config/customizer-items.ts
index 830f53f0c3..60f9395861 100644
--- a/apps/www/src/config/customizer-items.ts
+++ b/apps/www/src/config/customizer-items.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';
@@ -253,14 +252,7 @@ export const customizerItems: Record = {
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/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..bc3648bbd0 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: {
@@ -285,12 +275,11 @@ export const usePlaygroundPlugins = ({
},
},
}),
- 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 +428,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/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/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/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}
+
+
+
+ {value || '\u200B'}
+
+
+
+
+ >
+ );
+});
+
+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/mention-combobox.tsx b/apps/www/src/registry/default/plate-ui/mention-combobox.tsx
deleted file mode 100644
index abc3d46165..0000000000
--- a/apps/www/src/registry/default/plate-ui/mention-combobox.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import React from 'react';
-
-import type { ComboboxProps } from '@udecode/plate-combobox';
-
-import { getPluginOptions, useEditorRef } from '@udecode/plate-common';
-import {
- ELEMENT_MENTION,
- type MentionPlugin,
- getMentionOnSelectItem,
-} from '@udecode/plate-mention';
-
-import { Combobox } from './combobox';
-
-export function MentionCombobox({
- pluginKey = ELEMENT_MENTION,
- id = pluginKey,
- ...props
-}: {
- pluginKey?: string;
-} & Partial) {
- const editor = useEditorRef();
-
- const { trigger } = getPluginOptions(editor, pluginKey);
-
- return (
- e.preventDefault()}>
-
-
- );
-}
diff --git a/apps/www/src/registry/default/plate-ui/mention-input-element.tsx b/apps/www/src/registry/default/plate-ui/mention-input-element.tsx
index be1cce9515..124f5dcba8 100644
--- a/apps/www/src/registry/default/plate-ui/mention-input-element.tsx
+++ b/apps/www/src/registry/default/plate-ui/mention-input-element.tsx
@@ -1,34 +1,66 @@
-import React from 'react';
+import React, { useState } from 'react';
import { cn, withRef } from '@udecode/cn';
-import { PlateElement, getHandler } from '@udecode/plate-common';
-import { useFocused, useSelected } from 'slate-react';
+import { PlateElement } from '@udecode/plate-common';
+import { getMentionOnSelectItem } from '@udecode/plate-mention';
-export const MentionInputElement = withRef<
- typeof PlateElement,
- {
- onClick?: (mentionNode: any) => void;
+import { MENTIONABLES } from '@/lib/plate/demo/values/mentionables';
+
+import {
+ InlineCombobox,
+ InlineComboboxContent,
+ InlineComboboxEmpty,
+ InlineComboboxInput,
+ InlineComboboxItem,
+} from './inline-combobox';
+
+const onSelectItem = getMentionOnSelectItem();
+
+export const MentionInputElement = withRef(
+ ({ className, ...props }, ref) => {
+ const { children, editor, element } = props;
+ const [search, setSearch] = useState('');
+
+ return (
+
+
+
+
+
+
+
+ No results found
+
+ {MENTIONABLES.map((item) => (
+ onSelectItem(editor, item, search)}
+ value={item.text}
+ >
+ {item.text}
+
+ ))}
+
+
+
+ {children}
+
+ );
}
->(({ className, onClick, ...props }, ref) => {
- const { children, element } = props;
-
- const selected = useSelected();
- const focused = useFocused();
-
- return (
-
- {children}
-
- );
-});
+);
diff --git a/apps/www/src/registry/default/plate-ui/slash-combobox.tsx b/apps/www/src/registry/default/plate-ui/slash-combobox.tsx
deleted file mode 100644
index b8cf23c4a3..0000000000
--- a/apps/www/src/registry/default/plate-ui/slash-combobox.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import React from 'react';
-
-import type { ComboboxProps } from '@udecode/plate-combobox';
-
-import { getPluginOptions, useEditorRef } from '@udecode/plate-common';
-import {
- KEY_SLASH_COMMAND,
- type SlashPlugin,
- getSlashOnSelectItem,
-} from '@udecode/plate-slash-command';
-
-import { Combobox } from './combobox';
-
-export function SlashCombobox({
- pluginKey = KEY_SLASH_COMMAND,
- id = pluginKey,
- ...props
-}: {
- pluginKey?: string;
-} & Partial) {
- const editor = useEditorRef();
-
- const { trigger } = getPluginOptions(editor, pluginKey);
-
- return (
- e.preventDefault()}>
-
-
- );
-}
diff --git a/apps/www/src/registry/default/plate-ui/slash-input-element.tsx b/apps/www/src/registry/default/plate-ui/slash-input-element.tsx
index 2990410e6b..819b1a1565 100644
--- a/apps/www/src/registry/default/plate-ui/slash-input-element.tsx
+++ b/apps/www/src/registry/default/plate-ui/slash-input-element.tsx
@@ -1,34 +1,110 @@
-import React from 'react';
+import React, { type ComponentType, type SVGProps } from 'react';
-import { cn, withRef } from '@udecode/cn';
-import { PlateElement, getHandler } from '@udecode/plate-common';
-import { useFocused, useSelected } from 'slate-react';
+import { withRef } from '@udecode/cn';
+import {
+ type PlateEditor,
+ PlateElement,
+ toggleNodeType,
+} from '@udecode/plate-common';
+import { ELEMENT_H1, ELEMENT_H2, ELEMENT_H3 } from '@udecode/plate-heading';
+import { ListStyleType, toggleIndentList } from '@udecode/plate-indent-list';
-export const SlashInputElement = withRef<
- typeof PlateElement,
+import { Icons } from '@/components/icons';
+
+import {
+ InlineCombobox,
+ InlineComboboxContent,
+ InlineComboboxEmpty,
+ InlineComboboxInput,
+ InlineComboboxItem,
+} from './inline-combobox';
+
+interface SlashCommandRule {
+ icon: ComponentType>;
+ onSelect: (editor: PlateEditor) => void;
+ value: string;
+ keywords?: string[];
+}
+
+const rules: SlashCommandRule[] = [
+ {
+ icon: Icons.h1,
+ onSelect: (editor) => {
+ toggleNodeType(editor, { activeType: ELEMENT_H1 });
+ },
+ value: 'Heading 1',
+ },
+ {
+ icon: Icons.h2,
+ onSelect: (editor) => {
+ toggleNodeType(editor, { activeType: ELEMENT_H2 });
+ },
+ value: 'Heading 2',
+ },
+ {
+ icon: Icons.h3,
+ onSelect: (editor) => {
+ toggleNodeType(editor, { activeType: ELEMENT_H3 });
+ },
+ value: 'Heading 3',
+ },
{
- onClick?: (slashNode: any) => void;
+ icon: Icons.ul,
+ keywords: ['ul', 'unordered list'],
+ onSelect: (editor) => {
+ toggleIndentList(editor, {
+ listStyleType: ListStyleType.Disc,
+ });
+ },
+ value: 'Bulleted list',
+ },
+ {
+ icon: Icons.ol,
+ keywords: ['ol', 'ordered list'],
+ onSelect: (editor) => {
+ toggleIndentList(editor, {
+ listStyleType: ListStyleType.Decimal,
+ });
+ },
+ value: 'Numbered list',
+ },
+];
+
+export const SlashInputElement = withRef(
+ ({ className, ...props }, ref) => {
+ const { children, editor, element } = props;
+
+ return (
+
+
+
+
+
+
+ No matching commands found
+
+
+ {rules.map(({ icon: Icon, keywords, onSelect, value }) => (
+ onSelect(editor)}
+ value={value}
+ >
+
+ {value}
+
+ ))}
+
+
+
+ {children}
+
+ );
}
->(({ className, onClick, ...props }, ref) => {
- const { children, element } = props;
-
- const selected = useSelected();
- const focused = useFocused();
-
- return (
-
- /{children}
-
- );
-});
+);
diff --git a/apps/www/src/registry/registry.ts b/apps/www/src/registry/registry.ts
index 2f17db69af..71494c3d84 100644
--- a/apps/www/src/registry/registry.ts
+++ b/apps/www/src/registry/registry.ts
@@ -105,6 +105,13 @@ const ui: Registry = [
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'],
files: ['plate-ui/align-dropdown-menu.tsx'],
@@ -168,17 +175,6 @@ const ui: Registry = [
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'],
files: ['plate-ui/command.tsx'],
@@ -222,13 +218,6 @@ const ui: Registry = [
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'],
files: ['plate-ui/excalidraw-element.tsx'],
@@ -316,6 +305,13 @@ const ui: Registry = [
registryDependencies: ['toolbar'],
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: ['plate-ui/input.tsx'],
@@ -416,10 +412,10 @@ const ui: Registry = [
type: 'components:plate-ui',
},
{
- dependencies: ['@udecode/plate-mention', '@udecode/plate-combobox'],
- files: ['plate-ui/mention-combobox.tsx'],
- name: 'mention-combobox',
- registryDependencies: ['combobox'],
+ dependencies: ['@udecode/plate-mention'],
+ files: ['plate-ui/mention-element.tsx'],
+ name: 'mention-element',
+ registryDependencies: [],
type: 'components:plate-ui',
},
{
@@ -433,7 +429,7 @@ const ui: Registry = [
dependencies: ['@udecode/plate-mention'],
files: ['plate-ui/mention-input-element.tsx'],
name: 'mention-input-element',
- registryDependencies: [],
+ registryDependencies: ['inline-combobox'],
type: 'components:plate-ui',
},
{
@@ -492,6 +488,13 @@ const ui: Registry = [
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'],
files: ['plate-ui/table-cell-element.tsx'],
diff --git a/packages/combobox/src/combobox.store.ts b/packages/combobox/src/combobox.store.ts
deleted file mode 100644
index b0c91c52dd..0000000000
--- a/packages/combobox/src/combobox.store.ts
+++ /dev/null
@@ -1,133 +0,0 @@
-import type { Range } from 'slate';
-
-import {
- type ZustandStateActions,
- type ZustandStoreApi,
- createZustandStore,
-} from '@udecode/plate-common';
-
-import type { ComboboxOnSelectItem, NoData, TComboboxItem } from './types';
-
-export type ComboboxStateById = {
- /** Is opening/closing the combobox controlled by the client. */
- controlled?: boolean;
-
- /**
- * Items filter function by text.
- *
- * @default (value) => value.text.toLowerCase().startsWith(search.toLowerCase())
- */
- filter?: (search: string) => (item: TComboboxItem) => boolean;
-
- /** Combobox id. */
- id: string;
-
- /**
- * Max number of items.
- *
- * @default items.length
- */
- maxSuggestions?: number;
-
- /** Called when an item is selected. */
- onSelectItem: ComboboxOnSelectItem | null;
-
- /** Regular expression for search, for example to allow whitespace */
- searchPattern?: string;
-
- /** Sort filtered items before applying maxSuggestions. */
- sort?: (
- search: string
- ) => (a: TComboboxItem, b: TComboboxItem) => number;
-
- /** Trigger that activates the combobox. */
- trigger: string;
-};
-
-export type ComboboxStoreById = ZustandStoreApi<
- string,
- ComboboxStateById,
- ZustandStateActions>
->;
-
-export type ComboboxState = {
- /** Active id (combobox id which is opened). */
- activeId: null | string;
-
- /**
- * Object whose keys are combobox ids and values are config stores (e.g. one
- * for tag, one for mention,...).
- */
- byId: Record;
-
- /** Filtered items */
- filteredItems: TComboboxItem[];
-
- /** Highlighted index. */
- highlightedIndex: number;
-
- /** Unfiltered items. */
- items: TComboboxItem[];
-
- /** Range from the trigger to the cursor. */
- targetRange: Range | null;
-
- /** Text after the trigger. */
- text: null | string;
-};
-
-const createComboboxStore = (state: ComboboxStateById) =>
- createZustandStore(`combobox-${state.id}`)(state);
-
-export const comboboxStore = createZustandStore('combobox')({
- activeId: null,
- byId: {},
- filteredItems: [],
- highlightedIndex: 0,
- items: [],
- targetRange: null,
- text: null,
-})
- .extendActions((set, get) => ({
- open: (state: Pick) => {
- set.mergeState(state);
- },
- reset: () => {
- set.state((draft) => {
- draft.activeId = null;
- draft.highlightedIndex = 0;
- draft.filteredItems = [];
- draft.items = [];
- draft.text = null;
- draft.targetRange = null;
- });
- },
- setComboboxById: (state: ComboboxStateById) => {
- if (get.byId()[state.id]) return;
-
- set.state((draft) => {
- draft.byId[state.id] = createComboboxStore(
- state as unknown as ComboboxStateById
- );
- });
- },
- }))
- .extendSelectors((state) => ({
- isOpen: () => !!state.activeId,
- }));
-
-export const useComboboxSelectors = comboboxStore.use;
-
-export const comboboxSelectors = comboboxStore.get;
-
-export const comboboxActions = comboboxStore.set;
-
-export const getComboboxStoreById = (id: null | string) =>
- id ? comboboxSelectors.byId()[id] : null;
-
-export const useActiveComboboxStore = () => {
- const activeId = useComboboxSelectors.activeId();
- const comboboxes = useComboboxSelectors.byId();
-
- return activeId ? comboboxes[activeId] : null;
-};
diff --git a/packages/combobox/src/createComboboxPlugin.ts b/packages/combobox/src/createComboboxPlugin.ts
deleted file mode 100644
index edf0155576..0000000000
--- a/packages/combobox/src/createComboboxPlugin.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { createPluginFactory } from '@udecode/plate-common';
-
-import { onChangeCombobox } from './onChangeCombobox';
-import { onKeyDownCombobox } from './onKeyDownCombobox';
-
-export const KEY_COMBOBOX = 'combobox';
-
-export const createComboboxPlugin = createPluginFactory({
- handlers: {
- onChange: onChangeCombobox,
- onKeyDown: onKeyDownCombobox,
- },
- key: KEY_COMBOBOX,
-});
diff --git a/packages/combobox/src/hooks/index.ts b/packages/combobox/src/hooks/index.ts
index 2426758fb2..ff33da9b7e 100644
--- a/packages/combobox/src/hooks/index.ts
+++ b/packages/combobox/src/hooks/index.ts
@@ -2,6 +2,5 @@
* @file Automatically generated by barrelsby.
*/
-export * from './useComboboxContent';
-export * from './useComboboxControls';
-export * from './useComboboxItem';
+export * from './useComboboxInput';
+export * from './useHTMLInputCursorState';
diff --git a/packages/combobox/src/hooks/useComboboxContent.ts b/packages/combobox/src/hooks/useComboboxContent.ts
deleted file mode 100644
index 446a3981ae..0000000000
--- a/packages/combobox/src/hooks/useComboboxContent.ts
+++ /dev/null
@@ -1,81 +0,0 @@
-import React from 'react';
-
-import type { ComboboxProps } from '../types/ComboboxProps';
-
-import {
- type ComboboxControls,
- type Data,
- type NoData,
- comboboxActions,
- useActiveComboboxStore,
- useComboboxSelectors,
-} from '..';
-
-export type ComboboxContentProps = {
- combobox: ComboboxControls;
-} & Omit<
- ComboboxProps,
- | 'controlled'
- | 'filter'
- | 'id'
- | 'maxSuggestions'
- | 'onSelectItem'
- | 'searchPattern'
- | 'sort'
- | 'trigger'
->;
-
-export type ComboboxContentRootProps = {
- combobox: ComboboxControls;
-} & ComboboxContentProps;
-
-export const useComboboxContentState = ({
- combobox,
- items,
-}: ComboboxContentRootProps) => {
- const targetRange = useComboboxSelectors.targetRange();
- const activeComboboxStore = useActiveComboboxStore()!;
- const text = useComboboxSelectors.text() ?? '';
- const storeItems = useComboboxSelectors.items();
- const filter = activeComboboxStore.use.filter?.();
- const sort = activeComboboxStore.use.sort?.();
- const maxSuggestions =
- activeComboboxStore.use.maxSuggestions?.() ?? storeItems.length;
-
- // Update items
- React.useEffect(() => {
- items && comboboxActions.items(items);
- }, [items]);
-
- // Filter items
- React.useEffect(() => {
- comboboxActions.filteredItems(
- storeItems
- .filter(
- filter
- ? filter(text)
- : (value) => value.text.toLowerCase().startsWith(text.toLowerCase())
- )
- .sort(sort?.(text))
- .slice(0, maxSuggestions)
- );
- }, [filter, sort, storeItems, maxSuggestions, text]);
-
- return {
- combobox,
- targetRange,
- };
-};
-
-export const useComboboxContent = (
- state: ReturnType
-) => {
- const menuProps = state.combobox
- ? state.combobox.getMenuProps({}, { suppressRefError: true })
- : { ref: null };
-
- return {
- menuProps,
- targetRange: state.targetRange,
- };
-};
diff --git a/packages/combobox/src/hooks/useComboboxControls.ts b/packages/combobox/src/hooks/useComboboxControls.ts
deleted file mode 100644
index ac56285a2a..0000000000
--- a/packages/combobox/src/hooks/useComboboxControls.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import React from 'react';
-
-import { useCombobox } from 'downshift';
-
-import { useComboboxSelectors } from '../combobox.store';
-
-export type ComboboxControls = ReturnType;
-
-export const useComboboxControls = () => {
- const isOpen = useComboboxSelectors.isOpen();
- const highlightedIndex = useComboboxSelectors.highlightedIndex();
- const filteredItems = useComboboxSelectors.filteredItems();
-
- const {
- closeMenu,
- getComboboxProps,
- getInputProps,
- getItemProps,
- getMenuProps,
- } = useCombobox({
- circularNavigation: true,
- highlightedIndex,
- isOpen,
- items: filteredItems,
- });
- getMenuProps({}, { suppressRefError: true });
- getComboboxProps({}, { suppressRefError: true });
- getInputProps({}, { suppressRefError: true });
-
- return React.useMemo(
- () => ({
- closeMenu,
- getItemProps,
- getMenuProps,
- }),
- [closeMenu, getItemProps, getMenuProps]
- );
-};
diff --git a/packages/combobox/src/hooks/useComboboxInput.ts b/packages/combobox/src/hooks/useComboboxInput.ts
new file mode 100644
index 0000000000..92a81f747e
--- /dev/null
+++ b/packages/combobox/src/hooks/useComboboxInput.ts
@@ -0,0 +1,159 @@
+import {
+ type HTMLAttributes,
+ type RefObject,
+ useCallback,
+ useEffect,
+ useRef,
+} from 'react';
+
+import {
+ findNodePath,
+ focusEditor,
+ useEditorRef,
+ useElement,
+} from '@udecode/plate-common';
+import { Hotkeys, isHotkey, removeNodes } from '@udecode/plate-common/server';
+import { useSelected } from 'slate-react';
+
+import type {
+ CancelComboboxInputCause,
+ ComboboxInputCursorState,
+} from '../types';
+
+export interface UseComboboxInputOptions {
+ ref: RefObject;
+ autoFocus?: boolean;
+ cancelInputOnArrowLeftRight?: boolean;
+ cancelInputOnBackspace?: boolean;
+ cancelInputOnBlur?: boolean;
+ cancelInputOnDeselect?: boolean;
+ cancelInputOnEscape?: boolean;
+ cursorState?: ComboboxInputCursorState;
+ forwardUndoRedoToEditor?: boolean;
+ onCancelInput?: (cause: CancelComboboxInputCause) => void;
+}
+
+export interface UseComboboxInputResult {
+ cancelInput: (
+ cause?: CancelComboboxInputCause,
+ focusEditor?: boolean
+ ) => void;
+ props: Required, 'onBlur' | 'onKeyDown'>>;
+ removeInput: (focusEditor?: boolean) => void;
+}
+
+export const useComboboxInput = ({
+ autoFocus = true,
+ cancelInputOnArrowLeftRight = true,
+ cancelInputOnBackspace = true,
+ cancelInputOnBlur = true,
+ cancelInputOnDeselect = true,
+ cancelInputOnEscape = true,
+ cursorState,
+ forwardUndoRedoToEditor = true,
+ onCancelInput,
+ ref,
+}: UseComboboxInputOptions): UseComboboxInputResult => {
+ const editor = useEditorRef();
+ const element = useElement();
+ const selected = useSelected();
+
+ const cursorAtStart = cursorState?.atStart ?? false;
+ const cursorAtEnd = cursorState?.atEnd ?? false;
+
+ const removeInput = useCallback(
+ (shouldFocusEditor = false) => {
+ const path = findNodePath(editor, element);
+
+ if (!path) return;
+
+ removeNodes(editor, { at: path });
+
+ if (shouldFocusEditor) {
+ focusEditor(editor);
+ }
+ },
+ [editor, element]
+ );
+
+ const cancelInput = useCallback(
+ (cause: CancelComboboxInputCause = 'manual', shouldFocusEditor = false) => {
+ removeInput(shouldFocusEditor);
+ onCancelInput?.(cause);
+ },
+ [onCancelInput, removeInput]
+ );
+
+ /**
+ * Using autoFocus on the input element causes an error: Cannot resolve a
+ * Slate node from DOM node: [object HTMLSpanElement]
+ */
+ useEffect(() => {
+ if (autoFocus) {
+ ref.current?.focus();
+ }
+ }, [autoFocus, ref]);
+
+ /**
+ * Storing the previous selection lets us determine whether the input has been
+ * actively deselected. When undoing or redoing causes a combobox input to be
+ * inserted, selected can be temporarily false. Removing the input at this
+ * point is incorrect and crashes the editor.
+ */
+ const previousSelected = useRef(selected);
+
+ useEffect(() => {
+ if (previousSelected.current && !selected && cancelInputOnDeselect) {
+ cancelInput('deselect');
+ }
+
+ previousSelected.current = selected;
+ }, [selected, cancelInputOnDeselect, cancelInput]);
+
+ return {
+ cancelInput,
+ props: {
+ onBlur: () => {
+ if (cancelInputOnBlur) {
+ cancelInput('blur');
+ }
+ },
+ onKeyDown: (event) => {
+ if (cancelInputOnEscape && isHotkey('escape', event)) {
+ cancelInput('escape', true);
+ }
+ if (
+ cancelInputOnBackspace &&
+ cursorAtStart &&
+ isHotkey('backspace', event)
+ ) {
+ cancelInput('backspace', true);
+ }
+ if (
+ cancelInputOnArrowLeftRight &&
+ cursorAtStart &&
+ isHotkey('arrowleft', event)
+ ) {
+ cancelInput('arrowLeft', true);
+ }
+ if (
+ cancelInputOnArrowLeftRight &&
+ cursorAtEnd &&
+ isHotkey('arrowright', event)
+ ) {
+ cancelInput('arrowRight', true);
+ }
+
+ const isUndo = Hotkeys.isUndo(event) && editor.history.undos.length > 0;
+ const isRedo = Hotkeys.isRedo(event) && editor.history.redos.length > 0;
+
+ if (forwardUndoRedoToEditor && (isUndo || isRedo)) {
+ event.preventDefault();
+ editor[isUndo ? 'undo' : 'redo']();
+ focusEditor(editor);
+ }
+ },
+ },
+ removeInput,
+ };
+};
diff --git a/packages/combobox/src/hooks/useComboboxItem.tsx b/packages/combobox/src/hooks/useComboboxItem.tsx
deleted file mode 100644
index 5073df175c..0000000000
--- a/packages/combobox/src/hooks/useComboboxItem.tsx
+++ /dev/null
@@ -1,60 +0,0 @@
-import { useEditorRef } from '@udecode/plate-common';
-
-import type { ComboboxContentProps } from './useComboboxContent';
-
-import {
- type ComboboxControls,
- type Data,
- type NoData,
- type TComboboxItem,
- comboboxSelectors,
- getComboboxStoreById,
- useComboboxSelectors,
-} from '..';
-
-export type ComboboxContentItemProps = {
- combobox: ComboboxControls;
- index: number;
- item: TComboboxItem;
-} & Pick, 'onRenderItem'>;
-
-export interface ComboboxItemProps {
- item: TComboboxItem;
- search: string;
-}
-
-export const useComboboxItem = ({
- combobox,
- index,
- item,
- onRenderItem,
-}: ComboboxContentItemProps) => {
- const editor = useEditorRef();
- const text = useComboboxSelectors.text() ?? '';
- const highlightedIndex = useComboboxSelectors.highlightedIndex();
-
- const Item = onRenderItem
- ? onRenderItem({ item: item as TComboboxItem, search: text })
- : item.text;
-
- const highlighted = index === highlightedIndex;
-
- return {
- props: {
- 'data-highlighted': highlighted,
- ...combobox.getItemProps({
- index,
- item,
- }),
- children: Item,
- onMouseDown: (e: React.MouseEvent) => {
- e.preventDefault();
-
- const onSelectItem = getComboboxStoreById(
- comboboxSelectors.activeId()
- )?.get.onSelectItem();
- onSelectItem?.(editor, item);
- },
- },
- };
-};
diff --git a/packages/combobox/src/hooks/useHTMLInputCursorState.ts b/packages/combobox/src/hooks/useHTMLInputCursorState.ts
new file mode 100644
index 0000000000..24d6cfd473
--- /dev/null
+++ b/packages/combobox/src/hooks/useHTMLInputCursorState.ts
@@ -0,0 +1,41 @@
+import { type RefObject, useCallback, useEffect, useState } from 'react';
+
+import type { ComboboxInputCursorState } from '../types';
+
+export const useHTMLInputCursorState = (
+ ref: RefObject
+): ComboboxInputCursorState => {
+ const [cursorState, setCursorState] = useState({
+ atEnd: false,
+ atStart: false,
+ });
+
+ const recomputeCursorState = useCallback(() => {
+ if (!ref.current) return;
+
+ const { selectionEnd, selectionStart, value } = ref.current;
+
+ setCursorState({
+ atEnd: selectionEnd === value.length,
+ atStart: selectionStart === 0,
+ });
+ }, [ref]);
+
+ useEffect(() => {
+ recomputeCursorState();
+
+ const input = ref.current;
+
+ if (!input) return;
+
+ input.addEventListener('input', recomputeCursorState);
+ input.addEventListener('selectionchange', recomputeCursorState);
+
+ return () => {
+ input.removeEventListener('input', recomputeCursorState);
+ input.removeEventListener('selectionchange', recomputeCursorState);
+ };
+ }, [recomputeCursorState, ref]);
+
+ return cursorState;
+};
diff --git a/packages/combobox/src/index.ts b/packages/combobox/src/index.ts
index a198a8b6e5..1ec186e6b6 100644
--- a/packages/combobox/src/index.ts
+++ b/packages/combobox/src/index.ts
@@ -2,10 +2,7 @@
* @file Automatically generated by barrelsby.
*/
-export * from './combobox.store';
-export * from './createComboboxPlugin';
-export * from './onChangeCombobox';
-export * from './onKeyDownCombobox';
+export * from './types';
+export * from './withTriggerCombobox';
export * from './hooks/index';
-export * from './types/index';
export * from './utils/index';
diff --git a/packages/combobox/src/onChangeCombobox.spec.tsx b/packages/combobox/src/onChangeCombobox.spec.tsx
deleted file mode 100644
index 10ec16426f..0000000000
--- a/packages/combobox/src/onChangeCombobox.spec.tsx
+++ /dev/null
@@ -1,128 +0,0 @@
-/** @jsx jsx */
-
-import {
- type HandlerReturnType,
- createPlateEditor,
-} from '@udecode/plate-common';
-import { getMentionOnSelectItem } from '@udecode/plate-mention';
-import { createParagraphPlugin } from '@udecode/plate-paragraph';
-import { jsx } from '@udecode/plate-test-utils';
-
-import {
- type ComboboxState,
- comboboxActions,
- comboboxSelectors,
-} from './combobox.store';
-import { createComboboxPlugin } from './createComboboxPlugin';
-import { onChangeCombobox } from './onChangeCombobox';
-
-jsx;
-
-describe('onChangeCombobox', () => {
- const createEditor = (state: React.ReactElement) => {
- const plugins = [createParagraphPlugin(), createComboboxPlugin()];
-
- return createPlateEditor({
- editor: ({state} ) as any,
- plugins,
- });
- };
-
- const onChange = (fragment: React.ReactElement): HandlerReturnType => {
- return onChangeCombobox(createEditor(fragment))();
- };
-
- const createCombobox = ({
- controlled = false,
- trigger = '@',
- id = trigger,
- searchPattern = '\\S+',
- }: {
- controlled?: boolean;
- id?: string;
- searchPattern?: string;
- trigger?: string;
- } = {}) =>
- comboboxActions.setComboboxById({
- controlled,
- id,
- onSelectItem: getMentionOnSelectItem({ key: id }),
- searchPattern,
- trigger,
- });
-
- beforeEach(() => {
- comboboxActions.byId({});
- comboboxActions.reset();
- });
-
- it('should open the combobox if the text after trigger matches pattern', () => {
- createCombobox();
-
- onChange(
-
- @hello
-
-
- );
-
- expect(comboboxSelectors.state()).toMatchObject>({
- activeId: expect.anything(),
- text: 'hello',
- });
- });
-
- it('should not open the combobox if the combobox is controlled', () => {
- createCombobox({ controlled: true });
-
- onChange(
-
- @hello
-
-
- );
-
- expect(comboboxSelectors.state()).toMatchObject>({
- activeId: null,
- });
- });
-
- it('should not alter the state of a controlled combobox', () => {
- const id = 'controlled';
-
- createCombobox({ controlled: true, id });
-
- comboboxActions.open({
- activeId: id,
- targetRange: null,
- text: '',
- });
-
- onChange(
-
-
-
- );
-
- expect(comboboxSelectors.state()).toMatchObject>({
- activeId: id,
- });
- });
-
- it('should handle a mix of controlled and uncontrolled comboboxes', () => {
- createCombobox({ controlled: true, trigger: '@' });
- createCombobox({ controlled: false, trigger: '#' });
-
- onChange(
-
- #hello
-
-
- );
-
- expect(comboboxSelectors.state()).toMatchObject>({
- activeId: expect.anything(),
- text: 'hello',
- });
- });
-});
diff --git a/packages/combobox/src/onChangeCombobox.ts b/packages/combobox/src/onChangeCombobox.ts
deleted file mode 100644
index 876b405941..0000000000
--- a/packages/combobox/src/onChangeCombobox.ts
+++ /dev/null
@@ -1,81 +0,0 @@
-import {
- type PlateEditor,
- type Value,
- isCollapsed,
-} from '@udecode/plate-common';
-import { Range } from 'slate';
-
-import { comboboxActions, comboboxSelectors } from './combobox.store';
-import { getTextFromTrigger } from './utils/getTextFromTrigger';
-
-/**
- * For each combobox state (byId):
- *
- * - If the selection is collapsed
- * - If the cursor follows the trigger
- * - If there is text without whitespaces after the trigger
- * - Open the combobox: set id, search, targetRange in the store Close the
- * combobox if needed
- */
-export const onChangeCombobox =
- = PlateEditor>(
- editor: E
- ) =>
- () => {
- const byId = comboboxSelectors.byId();
- const activeId = comboboxSelectors.activeId();
-
- let shouldClose = true;
-
- for (const store of Object.values(byId)) {
- const id = store.get.id();
- const controlled = store.get.controlled?.();
-
- if (controlled) {
- // do not close controlled comboboxes
- if (activeId === id) {
- shouldClose = false;
-
- break;
- } else {
- // do not open controlled comboboxes
- continue;
- }
- }
-
- const { selection } = editor;
-
- if (!selection || !isCollapsed(selection)) {
- continue;
- }
-
- const trigger = store.get.trigger();
- const searchPattern = store.get.searchPattern?.();
-
- const isCursorAfterTrigger = getTextFromTrigger(editor, {
- at: Range.start(selection),
- searchPattern,
- trigger,
- });
-
- if (!isCursorAfterTrigger) {
- continue;
- }
-
- const { range, textAfterTrigger } = isCursorAfterTrigger;
-
- comboboxActions.open({
- activeId: id,
- targetRange: range,
- text: textAfterTrigger,
- });
-
- shouldClose = false;
-
- break;
- }
-
- if (shouldClose && comboboxSelectors.isOpen()) {
- comboboxActions.reset();
- }
- };
diff --git a/packages/combobox/src/onKeyDownCombobox.ts b/packages/combobox/src/onKeyDownCombobox.ts
deleted file mode 100644
index f82a8a490e..0000000000
--- a/packages/combobox/src/onKeyDownCombobox.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-import {
- Hotkeys,
- type KeyboardHandlerReturnType,
- type PlateEditor,
- type Value,
- isHotkey,
-} from '@udecode/plate-common';
-
-import {
- comboboxActions,
- comboboxSelectors,
- getComboboxStoreById,
-} from './combobox.store';
-import { getNextWrappingIndex } from './utils/getNextWrappingIndex';
-
-/**
- * If the combobox is open, handle:
- *
- * - Down (next item)
- * - Up (previous item)
- * - Escape (reset combobox)
- * - Tab, enter (select item)
- */
-export const onKeyDownCombobox =
- = PlateEditor>(
- editor: E
- ): KeyboardHandlerReturnType =>
- (event) => {
- const { activeId, filteredItems, highlightedIndex } =
- comboboxSelectors.state();
- const isOpen = comboboxSelectors.isOpen();
-
- if (!isOpen) return;
-
- const store = getComboboxStoreById(activeId);
-
- if (!store) return;
-
- const onSelectItem = store.get.onSelectItem();
-
- if (isHotkey('down', event)) {
- event.preventDefault();
-
- const newIndex = getNextWrappingIndex(
- 1,
- highlightedIndex,
- filteredItems.length,
- () => {},
- true
- );
- comboboxActions.highlightedIndex(newIndex);
-
- return;
- }
- if (isHotkey('up', event)) {
- event.preventDefault();
-
- const newIndex = getNextWrappingIndex(
- -1,
- highlightedIndex,
- filteredItems.length,
- () => {},
- true
- );
- comboboxActions.highlightedIndex(newIndex);
-
- return;
- }
- if (isHotkey('escape', event)) {
- event.preventDefault();
- comboboxActions.reset();
-
- return;
- }
- if (Hotkeys.isTab(editor, event) || isHotkey('enter', event)) {
- event.preventDefault();
- event.stopPropagation();
-
- if (filteredItems[highlightedIndex]) {
- onSelectItem?.(editor, filteredItems[highlightedIndex]);
- }
- }
- };
diff --git a/packages/combobox/src/types.ts b/packages/combobox/src/types.ts
new file mode 100644
index 0000000000..b38b4f45a4
--- /dev/null
+++ b/packages/combobox/src/types.ts
@@ -0,0 +1,22 @@
+import type { PlateEditor, TElement } from '@udecode/plate-common/server';
+
+export interface TriggerComboboxPlugin {
+ createComboboxInput?: (trigger: string) => TElement;
+ trigger?: RegExp | string | string[];
+ triggerPreviousCharPattern?: RegExp;
+ triggerQuery?: (editor: PlateEditor) => boolean;
+}
+
+export type ComboboxInputCursorState = {
+ atEnd: boolean;
+ atStart: boolean;
+};
+
+export type CancelComboboxInputCause =
+ | 'arrowLeft'
+ | 'arrowRight'
+ | 'backspace'
+ | 'blur'
+ | 'deselect'
+ | 'escape'
+ | 'manual';
diff --git a/packages/combobox/src/types/ComboboxOnSelectItem.ts b/packages/combobox/src/types/ComboboxOnSelectItem.ts
deleted file mode 100644
index 6d00b046d1..0000000000
--- a/packages/combobox/src/types/ComboboxOnSelectItem.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import type { PlateEditor, Value } from '@udecode/plate-common';
-
-export interface TComboboxItemBase {
- /** Unique key. */
- key: string;
-
- /** Item text. */
- text: any;
-
- /**
- * Whether the item is disabled.
- *
- * @default false
- */
- disabled?: boolean;
-}
-
-export interface TComboboxItemWithData
- extends TComboboxItemBase {
- /** Data available to `onRenderItem`. */
- data: TData;
-}
-
-export type NoData = undefined;
-
-export type Data = unknown;
-
-export type TComboboxItem = TData extends NoData
- ? TComboboxItemBase
- : TComboboxItemWithData;
-
-export type ComboboxOnSelectItem = (
- editor: PlateEditor,
- item: TComboboxItem
-) => any;
diff --git a/packages/combobox/src/types/ComboboxProps.ts b/packages/combobox/src/types/ComboboxProps.ts
deleted file mode 100644
index db50c6810d..0000000000
--- a/packages/combobox/src/types/ComboboxProps.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import type React from 'react';
-
-import type {
- ComboboxItemProps,
- ComboboxState,
- ComboboxStateById,
- ComboboxStoreById,
- NoData,
-} from '..';
-
-export interface ComboboxProps
- extends Partial, 'items'>>,
- ComboboxStateById {
- /** Render this component when the combobox is open (useful to inject hooks). */
- component?: React.FC<{ store: ComboboxStoreById }>;
-
- /**
- * Whether to hide the combobox.
- *
- * @default !items.length
- */
- disabled?: boolean;
-
- /**
- * Render combobox item.
- *
- * @default text
- */
- onRenderItem?: React.FC>;
-
- portalElement?: HTMLElement;
-}
diff --git a/packages/combobox/src/types/index.ts b/packages/combobox/src/types/index.ts
deleted file mode 100644
index 2c540c4dd6..0000000000
--- a/packages/combobox/src/types/index.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-/**
- * @file Automatically generated by barrelsby.
- */
-
-export * from './ComboboxOnSelectItem';
-export * from './ComboboxProps';
diff --git a/packages/combobox/src/utils/filterWords.spec.ts b/packages/combobox/src/utils/filterWords.spec.ts
new file mode 100644
index 0000000000..09ad706710
--- /dev/null
+++ b/packages/combobox/src/utils/filterWords.spec.ts
@@ -0,0 +1,71 @@
+import { type FilterWordsOptions, filterWords } from './filterWords';
+
+describe('filterWords', () => {
+ describe('with default options', () => {
+ describe('single word', () => {
+ it('matches simple prefix', () => {
+ expect(filterWords('hello', 'he')).toBe(true);
+ });
+
+ it('does not match non-prefix', () => {
+ expect(filterWords('hello', 'lo')).toBe(false);
+ });
+
+ it('is case-insensitive', () => {
+ expect(filterWords('hello', 'HE')).toBe(true);
+ });
+
+ it('is diacritic-insensitive', () => {
+ expect(filterWords('hello', 'hé')).toBe(true);
+ });
+ });
+
+ describe('multiple words', () => {
+ it('matches when all words in query match', () => {
+ expect(filterWords('hello world', 'world hello')).toBe(true);
+ expect(filterWords('hello world', 'world')).toBe(true);
+ });
+
+ it('does not match when not all words in query match', () => {
+ expect(filterWords('hello world', 'hello other')).toBe(false);
+ });
+
+ it('allows prefix for last word', () => {
+ expect(filterWords('hello world', 'world he')).toBe(true);
+ });
+
+ it('does not allow prefix for non-last word', () => {
+ expect(filterWords('hello world', 'wor hello')).toBe(false);
+ });
+ });
+ });
+
+ describe('with prefix mode disabled', () => {
+ const options: FilterWordsOptions = { prefixMode: 'none' };
+
+ it('only matches whole words', () => {
+ expect(filterWords('hello world', 'wor', options)).toBe(false);
+ expect(filterWords('hello world', 'world', options)).toBe(true);
+ });
+ });
+
+ describe('with prefix mode set to all words', () => {
+ const options: FilterWordsOptions = { prefixMode: 'all-words' };
+
+ it('allows prefix for all words', () => {
+ expect(filterWords('hello world', 'wor hel', options)).toBe(true);
+ });
+ });
+
+ describe('with word quantifier set to match any', () => {
+ const options: FilterWordsOptions = { wordQuantifier: 'match-any' };
+
+ it('matches when any word in query matches', () => {
+ expect(filterWords('hello world', 'other hello', options)).toBe(true);
+ });
+
+ it('does not match when no word in query matches', () => {
+ expect(filterWords('hello world', 'other other', options)).toBe(false);
+ });
+ });
+});
diff --git a/packages/combobox/src/utils/filterWords.ts b/packages/combobox/src/utils/filterWords.ts
new file mode 100644
index 0000000000..1a592bd662
--- /dev/null
+++ b/packages/combobox/src/utils/filterWords.ts
@@ -0,0 +1,49 @@
+export interface FilterWordsOptions {
+ prefixMode?: 'all-words' | 'last-word' | 'none';
+ wordBoundary?: RegExp;
+ wordQuantifier?: 'match-all' | 'match-any';
+}
+
+export const filterWords = (
+ haystack: string,
+ needle: string,
+ {
+ prefixMode = 'last-word',
+ wordBoundary = /\s+/,
+ wordQuantifier = 'match-all',
+ }: FilterWordsOptions = {}
+): boolean => {
+ const haystackWords = haystack.trim().split(wordBoundary);
+ const needleWords = needle.trim().split(wordBoundary);
+
+ const quantifier = wordQuantifier === 'match-all' ? 'every' : 'some';
+
+ return needleWords[quantifier]((needleWord, i) => {
+ const allowPrefix = (() => {
+ switch (prefixMode) {
+ case 'none': {
+ return false;
+ }
+ case 'all-words': {
+ return true;
+ }
+ case 'last-word': {
+ return i === needleWords.length - 1;
+ }
+ }
+ })();
+
+ return haystackWords.some((unslicedHaystackWord) => {
+ const haystackWord = allowPrefix
+ ? unslicedHaystackWord.slice(0, needleWord.length)
+ : unslicedHaystackWord;
+
+ return (
+ haystackWord.localeCompare(needleWord, undefined, {
+ sensitivity: 'base',
+ usage: 'search',
+ }) === 0
+ );
+ });
+ });
+};
diff --git a/packages/combobox/src/utils/getNextNonDisabledIndex.ts b/packages/combobox/src/utils/getNextNonDisabledIndex.ts
deleted file mode 100644
index 1f6b8f9e43..0000000000
--- a/packages/combobox/src/utils/getNextNonDisabledIndex.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-/**
- * Returns the next index in the list of an item that is not disabled.
- *
- * @param {number} moveAmount Number of positions to move. Negative to move
- * backwards, positive forwards.
- * @param {number} baseIndex The initial position to move from.
- * @param {number} itemCount The total number of items.
- * @param {Function} getItemNodeFromIndex Used to check if item is disabled.
- * @param {boolean} circular Specify if navigation is circular. Default is true.
- * @returns {number} The new index. Returns baseIndex if item is not disabled.
- * Returns next non-disabled item otherwise. If no non-disabled found it will
- * return -1.
- */
-export const getNextNonDisabledIndex = (
- moveAmount: number,
- baseIndex: number,
- itemCount: number,
- getItemNodeFromIndex: any,
- circular: boolean
-): number => {
- const currentElementNode = getItemNodeFromIndex(baseIndex);
-
- if (!currentElementNode?.hasAttribute('disabled')) {
- return baseIndex;
- }
- if (moveAmount > 0) {
- for (let index = baseIndex + 1; index < itemCount; index++) {
- if (!getItemNodeFromIndex(index).hasAttribute('disabled')) {
- return index;
- }
- }
- } else {
- for (let index = baseIndex - 1; index >= 0; index--) {
- if (!getItemNodeFromIndex(index).hasAttribute('disabled')) {
- return index;
- }
- }
- }
- if (circular) {
- return moveAmount > 0
- ? getNextNonDisabledIndex(1, 0, itemCount, getItemNodeFromIndex, false)
- : getNextNonDisabledIndex(
- -1,
- itemCount - 1,
- itemCount,
- getItemNodeFromIndex,
- false
- );
- }
-
- return -1;
-};
diff --git a/packages/combobox/src/utils/getNextWrappingIndex.ts b/packages/combobox/src/utils/getNextWrappingIndex.ts
deleted file mode 100644
index 4371497ac5..0000000000
--- a/packages/combobox/src/utils/getNextWrappingIndex.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-import { getNextNonDisabledIndex } from './getNextNonDisabledIndex';
-
-/**
- * Returns the new index in the list, in a circular way. If next value is out of
- * bonds from the total, it will wrap to either 0 or itemCount - 1.
- *
- * @param {number} moveAmount Number of positions to move. Negative to move
- * backwards, positive forwards.
- * @param {number} baseIndex The initial position to move from.
- * @param {number} itemCount The total number of items.
- * @param {Function} getItemNodeFromIndex Used to check if item is disabled.
- * @param {boolean} circular Specify if navigation is circular. Default is true.
- * @returns {number} The new index after the move.
- */
-export const getNextWrappingIndex = (
- moveAmount: number,
- baseIndex: number,
- itemCount: number,
- getItemNodeFromIndex: any,
- circular = true
-) => {
- if (itemCount === 0) {
- return -1;
- }
-
- const itemsLastIndex = itemCount - 1;
-
- // noinspection SuspiciousTypeOfGuard
- if (
- typeof baseIndex !== 'number' ||
- baseIndex < 0 ||
- baseIndex >= itemCount
- ) {
- baseIndex = moveAmount > 0 ? -1 : itemsLastIndex + 1;
- }
-
- let newIndex = baseIndex + moveAmount;
-
- if (newIndex < 0) {
- newIndex = circular ? itemsLastIndex : 0;
- } else if (newIndex > itemsLastIndex) {
- newIndex = circular ? 0 : itemsLastIndex;
- }
-
- const nonDisabledNewIndex = getNextNonDisabledIndex(
- moveAmount,
- newIndex,
- itemCount,
- getItemNodeFromIndex,
- circular
- );
-
- if (nonDisabledNewIndex === -1) {
- return baseIndex >= itemCount ? -1 : baseIndex;
- }
-
- return nonDisabledNewIndex;
-};
diff --git a/packages/combobox/src/utils/getTextFromTrigger.ts b/packages/combobox/src/utils/getTextFromTrigger.ts
deleted file mode 100644
index f62eaadcd6..0000000000
--- a/packages/combobox/src/utils/getTextFromTrigger.ts
+++ /dev/null
@@ -1,95 +0,0 @@
-import type { Point } from 'slate';
-
-import {
- type TEditor,
- type Value,
- escapeRegExp,
- getEditorString,
- getPointBefore,
- getRange,
-} from '@udecode/plate-common';
-
-/**
- * Get text and range from trigger to cursor. Starts with trigger and ends with
- * non-whitespace character.
- */
-export const getTextFromTrigger = (
- editor: TEditor,
- {
- at,
- searchPattern = `\\S+`,
- trigger,
- }: { at: Point; searchPattern?: string; trigger: string }
-) => {
- const escapedTrigger = escapeRegExp(trigger);
- const triggerRegex = new RegExp(`(?:^|\\s)${escapedTrigger}`);
-
- let start: Point | undefined = at;
- let end: Point | undefined;
-
- // eslint-disable-next-line no-constant-condition
- while (true) {
- end = start;
-
- if (!start) break;
-
- start = getPointBefore(editor, start);
- const charRange = start && getRange(editor, start, end);
- const charText = getEditorString(editor, charRange);
-
- if (!charText.match(searchPattern)) {
- start = end;
-
- break;
- }
- }
-
- // Range from start to cursor
- const range = start && getRange(editor, start, at);
- const text = getEditorString(editor, range);
-
- if (!range || !text.match(triggerRegex)) return;
-
- return {
- range,
- textAfterTrigger: text.slice(trigger.length),
- };
-};
-
-// export const matchesTriggerAndPattern = (
-// editor: TEditor,
-// { at, trigger, pattern }: { at: Point; trigger: string; pattern: string }
-// ) => {
-// // Point at the start of line
-// const lineStart = getPointBefore(editor, at, { unit: 'line' });
-//
-// // Range from before to start
-// const beforeRange = lineStart && getRange(editor, lineStart, at);
-//
-// // Before text
-// const beforeText = getEditorString(editor, beforeRange);
-//
-// // Starts with char and ends with word characters
-// const escapedTrigger = escapeRegExp(trigger);
-//
-// const beforeRegex = new RegExp(`(?:^|\\s)${escapedTrigger}(${pattern})$`);
-//
-// // Match regex on before text
-// const match = !!beforeText && beforeText.match(beforeRegex);
-//
-// // Point at the start of mention
-// const mentionStart = match
-// ? getPointBefore(editor, at, {
-// unit: 'character',
-// distance: match[1].length + trigger.length,
-// })
-// : null;
-//
-// // Range from mention to start
-// const mentionRange = mentionStart && getRange(editor, mentionStart, at);
-//
-// return {
-// range: mentionRange,
-// match,
-// };
-// };
diff --git a/packages/combobox/src/utils/index.ts b/packages/combobox/src/utils/index.ts
index 0390c0ab42..7afc6fa598 100644
--- a/packages/combobox/src/utils/index.ts
+++ b/packages/combobox/src/utils/index.ts
@@ -2,6 +2,4 @@
* @file Automatically generated by barrelsby.
*/
-export * from './getNextNonDisabledIndex';
-export * from './getNextWrappingIndex';
-export * from './getTextFromTrigger';
+export * from './filterWords';
diff --git a/packages/combobox/src/withTriggerCombobox.spec.tsx b/packages/combobox/src/withTriggerCombobox.spec.tsx
new file mode 100644
index 0000000000..f766acb592
--- /dev/null
+++ b/packages/combobox/src/withTriggerCombobox.spec.tsx
@@ -0,0 +1,222 @@
+/** @jsx jsx */
+
+import {
+ type TriggerComboboxPlugin,
+ withTriggerCombobox,
+} from '@udecode/plate-combobox';
+import { createPlateEditor, createPluginFactory } from '@udecode/plate-common';
+import { createParagraphPlugin } from '@udecode/plate-paragraph';
+import { jsx } from '@udecode/plate-test-utils';
+
+const createExampleComboboxPlugin = createPluginFactory({
+ key: 'exampleCombobox',
+ plugins: [
+ {
+ isElement: true,
+ isInline: true,
+ isVoid: true,
+ key: 'mention_input',
+ },
+ ],
+ withOverrides: withTriggerCombobox,
+});
+
+const plugins = [
+ createParagraphPlugin(),
+
+ createExampleComboboxPlugin({
+ key: 'exampleCombobox1',
+ options: {
+ createComboboxInput: (trigger) => ({
+ children: [{ text: '' }],
+ trigger,
+ type: 'mention_input',
+ }),
+ trigger: ['@', '#'],
+ triggerPreviousCharPattern: /^$|^[\s"']$/,
+ },
+ }),
+
+ createExampleComboboxPlugin({
+ key: 'exampleCombobox2',
+ options: {
+ createComboboxInput: () => ({
+ children: [{ text: '' }],
+ trigger: ':',
+ type: 'mention_input',
+ }),
+ trigger: ':',
+ triggerPreviousCharPattern: /^\s?$/,
+ },
+ }),
+];
+
+const createEditorWithCombobox = (chidren: any) =>
+ createPlateEditor({
+ editor: ({chidren} ) as any,
+ plugins,
+ });
+
+jsx;
+
+describe('withTriggerCombobox', () => {
+ ['@', '#', ':'].forEach((trigger) => {
+ describe(`when typing "${trigger}"`, () => {
+ it('should insert a combobox input when the trigger is inserted between words', () => {
+ const editor = createEditorWithCombobox(
+
+ hello world
+
+ );
+
+ editor.insertText(trigger);
+
+ expect(editor.children).toEqual([
+
+ hello
+
+
+
+
+ world
+ ,
+ ]);
+ });
+
+ it('should insert a combobox input when the trigger is inserted at line beginning followed by a whitespace', () => {
+ const editor = createEditorWithCombobox(
+
+ hello world
+
+ );
+
+ editor.insertText(trigger);
+
+ expect(editor.children).toEqual([
+
+
+
+
+
+
+ hello world
+ ,
+ ]);
+ });
+
+ it('should insert a combobox input when the trigger is inserted at line end preceded by a whitespace', () => {
+ const editor = createEditorWithCombobox(
+
+ hello world
+
+ );
+
+ editor.insertText(trigger);
+
+ expect(editor.children).toEqual([
+
+ hello world
+
+
+
+
+
+ ,
+ ]);
+ });
+
+ it('should insert the trigger as text when the trigger is appended to a word', () => {
+ const editor = createEditorWithCombobox(
+
+ hello
+
+
+ );
+
+ editor.insertText(trigger);
+
+ expect(editor.children).toEqual([
+
+ hello{trigger}
+
+ ,
+ ]);
+ });
+
+ it('should insert a combobox input when the trigger is prepended to a word', () => {
+ const editor = createEditorWithCombobox(
+
+
+ hello
+
+ );
+
+ editor.insertText(trigger);
+
+ expect(editor.children).toEqual([
+
+
+
+
+
+
+ hello
+ ,
+ ]);
+ });
+
+ it('should insert the trigger as text when the trigger is inserted into a word', () => {
+ const editor = createEditorWithCombobox(
+
+ hel
+
+ lo
+
+ );
+
+ editor.insertText(trigger);
+
+ expect(editor.children).toEqual([
+
+ hel{trigger}
+
+ lo
+ ,
+ ]);
+ });
+ });
+ });
+
+ it('should insert text when not trigger', () => {
+ const editor = createEditorWithCombobox(
+
+
+
+ );
+
+ editor.insertText('a');
+
+ expect(editor.children).toEqual([a ]);
+ });
+
+ it('should insert a combobox input when the trigger is inserted after the specified pattern', () => {
+ const editor = createEditorWithCombobox(
+
+ hello " "
+
+ );
+
+ editor.insertText('@');
+
+ expect(editor.children).toEqual([
+
+ hello "
+
+
+
+
+ "
+ ,
+ ]);
+ });
+});
diff --git a/packages/combobox/src/withTriggerCombobox.ts b/packages/combobox/src/withTriggerCombobox.ts
new file mode 100644
index 0000000000..d112c4614e
--- /dev/null
+++ b/packages/combobox/src/withTriggerCombobox.ts
@@ -0,0 +1,75 @@
+import {
+ type PlateEditor,
+ type TElement,
+ type Value,
+ type WithPlatePlugin,
+ getEditorString,
+ getPointBefore,
+ getRange,
+} from '@udecode/plate-common/server';
+
+import type { TriggerComboboxPlugin } from './types';
+
+export const withTriggerCombobox = <
+ V extends Value = Value,
+ E extends PlateEditor = PlateEditor,
+>(
+ editor: E,
+ {
+ options: {
+ createComboboxInput,
+ trigger,
+ triggerPreviousCharPattern,
+ triggerQuery,
+ },
+ type,
+ }: WithPlatePlugin
+) => {
+ const { insertText } = editor;
+
+ const matchesTrigger = (text: string) => {
+ if (trigger instanceof RegExp) {
+ return trigger.test(text);
+ }
+ if (Array.isArray(trigger)) {
+ return trigger.includes(text);
+ }
+
+ return text === trigger;
+ };
+
+ editor.insertText = (text) => {
+ if (
+ !editor.selection ||
+ !matchesTrigger(text) ||
+ (triggerQuery && !triggerQuery(editor as PlateEditor))
+ ) {
+ return insertText(text);
+ }
+
+ // Make sure an input is created at the beginning of line or after a whitespace
+ const previousChar = getEditorString(
+ editor,
+ getRange(
+ editor,
+ editor.selection,
+ getPointBefore(editor, editor.selection)
+ )
+ );
+
+ const matchesPreviousCharPattern =
+ triggerPreviousCharPattern?.test(previousChar);
+
+ if (matchesPreviousCharPattern) {
+ const inputNode: TElement = createComboboxInput
+ ? createComboboxInput(text)
+ : { children: [{ text: '' }], type };
+
+ return editor.insertNode(inputNode);
+ }
+
+ return insertText(text);
+ };
+
+ return editor;
+};
diff --git a/packages/emoji/src/constants.ts b/packages/emoji/src/constants.ts
index 09cfcb00fc..4e29780a05 100644
--- a/packages/emoji/src/constants.ts
+++ b/packages/emoji/src/constants.ts
@@ -1,7 +1,4 @@
-import type {
- EmojiTriggeringControllerOptions,
- FrequentEmojis,
-} from './utils/index';
+import type { FrequentEmojis } from './utils/index';
import {
EmojiCategory,
@@ -10,18 +7,8 @@ import {
type i18nProps,
} from './types';
-export const KEY_EMOJI = 'emoji';
-
-export const EMOJI_TRIGGER = ':';
-
export const EMOJI_MAX_SEARCH_RESULT = 60;
-export const emojiTriggeringControllerOptions: EmojiTriggeringControllerOptions =
- {
- limitTriggeringChars: 2,
- trigger: EMOJI_TRIGGER,
- };
-
export const defaultCategories: EmojiCategoryList[] = [
EmojiCategory.People,
EmojiCategory.Nature,
diff --git a/packages/emoji/src/createEmojiPlugin.ts b/packages/emoji/src/createEmojiPlugin.ts
index 38e6c34f6f..5e267346ed 100644
--- a/packages/emoji/src/createEmojiPlugin.ts
+++ b/packages/emoji/src/createEmojiPlugin.ts
@@ -1,28 +1,30 @@
+import { withTriggerCombobox } from '@udecode/plate-combobox';
import { createPluginFactory } from '@udecode/plate-common/server';
import type { EmojiPlugin } from './types';
-import { EMOJI_TRIGGER, KEY_EMOJI } from './constants';
-import { EmojiTriggeringController } from './utils/index';
-import { withEmoji } from './withEmoji';
+export const KEY_EMOJI = 'emoji';
+
+export const ELEMENT_EMOJI_INPUT = 'emoji_input';
export const createEmojiPlugin = createPluginFactory({
key: KEY_EMOJI,
options: {
- createEmoji: (item) => item.data.emoji,
- emojiTriggeringController: new EmojiTriggeringController(),
- trigger: EMOJI_TRIGGER,
+ createComboboxInput: () => ({
+ children: [{ text: '' }],
+ type: ELEMENT_EMOJI_INPUT,
+ }),
+ createEmojiNode: ({ skins }) => ({ text: skins[0].native }),
+ trigger: ':',
+ triggerPreviousCharPattern: /^\s?$/,
},
- then: (
- _,
- { key, options: { createEmoji, emojiTriggeringController, trigger } }
- ) => ({
- options: {
- createEmoji,
- emojiTriggeringController,
- id: key,
- trigger,
+ plugins: [
+ {
+ isElement: true,
+ isInline: true,
+ isVoid: true,
+ key: ELEMENT_EMOJI_INPUT,
},
- }),
- withOverrides: withEmoji,
+ ],
+ withOverrides: withTriggerCombobox,
});
diff --git a/packages/emoji/src/handlers/getEmojiOnInsert.ts b/packages/emoji/src/handlers/getEmojiOnInsert.ts
deleted file mode 100644
index d7a5a28fa8..0000000000
--- a/packages/emoji/src/handlers/getEmojiOnInsert.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import type { ComboboxOnSelectItem } from '@udecode/plate-combobox';
-
-import { focusEditor } from '@udecode/plate-common';
-import {
- type PlatePluginKey,
- getPlugin,
- insertText,
- withoutNormalizing,
-} from '@udecode/plate-common/server';
-
-import type { EmojiItemData, EmojiPlugin } from '../types';
-
-import { KEY_EMOJI } from '../constants';
-
-export const getEmojiOnInsert =
- ({
- key = KEY_EMOJI,
- }: PlatePluginKey = {}): ComboboxOnSelectItem =>
- (editor, item) => {
- const {
- options: { createEmoji },
- } = getPlugin(editor as any, key);
-
- withoutNormalizing(editor, () => {
- focusEditor(editor);
-
- const value = createEmoji!(item);
- insertText(editor, value);
- });
- };
diff --git a/packages/emoji/src/handlers/getEmojiOnSelectItem.ts b/packages/emoji/src/handlers/getEmojiOnSelectItem.ts
deleted file mode 100644
index 020c371d37..0000000000
--- a/packages/emoji/src/handlers/getEmojiOnSelectItem.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import {
- type ComboboxOnSelectItem,
- comboboxActions,
-} from '@udecode/plate-combobox';
-import {
- type PlatePluginKey,
- deleteText,
- getPlugin,
- insertText,
- withoutMergingHistory,
- withoutNormalizing,
-} from '@udecode/plate-common/server';
-
-import type { EmojiItemData, EmojiPlugin } from '../types';
-
-import { KEY_EMOJI } from '../constants';
-
-export const getEmojiOnSelectItem =
- ({
- key = KEY_EMOJI,
- }: PlatePluginKey = {}): ComboboxOnSelectItem =>
- (editor, item) => {
- const {
- options: { createEmoji, emojiTriggeringController },
- } = getPlugin(editor as any, key);
-
- withoutNormalizing(editor, () => {
- withoutMergingHistory(editor, () =>
- deleteText(editor, {
- distance: emojiTriggeringController!
- .setIsTriggering(false)
- .getTextSize(),
- reverse: true,
- })
- );
-
- const value = createEmoji!(item);
- insertText(editor, value);
- });
-
- return comboboxActions.reset();
- };
diff --git a/packages/emoji/src/handlers/getFindTriggeringInput.ts b/packages/emoji/src/handlers/getFindTriggeringInput.ts
deleted file mode 100644
index 83d2352210..0000000000
--- a/packages/emoji/src/handlers/getFindTriggeringInput.ts
+++ /dev/null
@@ -1,80 +0,0 @@
-import type { BasePoint } from 'slate';
-
-import {
- type PlateEditor,
- type Value,
- getEditorString,
- getPointBefore,
- getRange,
- isCollapsed,
-} from '@udecode/plate-common/server';
-
-import type { FindTriggeringInputProps } from '../types';
-import type { IEmojiTriggeringController } from '../utils/index';
-
-const isSpaceBreak = (char?: string) => !!char && /\s/.test(char);
-
-const getPreviousChar = (
- editor: PlateEditor,
- point?: BasePoint
-) =>
- point
- ? getEditorString(
- editor,
- getRange(editor, point, getPointBefore(editor, point))
- )
- : undefined;
-
-const getPreviousPoint = (
- editor: PlateEditor,
- point?: BasePoint
-) => (point ? getPointBefore(editor, point) : undefined);
-
-const isBeginningOfTheLine = (
- editor: PlateEditor,
- point?: BasePoint
-) => {
- const previousPoint = getPreviousPoint(editor, point);
-
- return point?.path[0] !== previousPoint?.path[0];
-};
-
-export const getFindTriggeringInput =
- (
- editor: PlateEditor,
- emojiTriggeringController: IEmojiTriggeringController
- ) =>
- ({ action = 'insert', char = '' }: FindTriggeringInputProps = {}) => {
- const { selection } = editor;
-
- if (!selection || !isCollapsed(selection) || isSpaceBreak(char)) {
- emojiTriggeringController.setIsTriggering(false);
-
- return;
- }
-
- const startPoint = selection.anchor;
- let currentPoint: BasePoint | undefined = startPoint;
- let previousPoint;
-
- let foundText = char;
- let previousChar;
-
- do {
- previousChar = getPreviousChar(editor, currentPoint);
- foundText = previousChar + foundText;
- previousPoint = getPreviousPoint(editor, currentPoint);
-
- if (isBeginningOfTheLine(editor, currentPoint)) {
- break;
- }
-
- currentPoint = previousPoint;
- } while (!isSpaceBreak(previousChar));
-
- foundText = foundText.trim();
-
- if (action === 'delete') foundText = foundText.slice(0, -1);
-
- emojiTriggeringController.setText(foundText);
- };
diff --git a/packages/emoji/src/handlers/index.ts b/packages/emoji/src/handlers/index.ts
deleted file mode 100644
index 917060413f..0000000000
--- a/packages/emoji/src/handlers/index.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-/**
- * @file Automatically generated by barrelsby.
- */
-
-export * from './getEmojiOnInsert';
-export * from './getEmojiOnSelectItem';
-export * from './getFindTriggeringInput';
diff --git a/packages/emoji/src/hooks/index.ts b/packages/emoji/src/hooks/index.ts
index 1d07c2922d..0710d2f3b4 100644
--- a/packages/emoji/src/hooks/index.ts
+++ b/packages/emoji/src/hooks/index.ts
@@ -2,5 +2,4 @@
* @file Automatically generated by barrelsby.
*/
-export * from './useEmojiCombobox';
export * from './useEmojiDropdownMenuState';
diff --git a/packages/emoji/src/hooks/useEmojiCombobox.ts b/packages/emoji/src/hooks/useEmojiCombobox.ts
deleted file mode 100644
index dd44f61d80..0000000000
--- a/packages/emoji/src/hooks/useEmojiCombobox.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import type {
- ComboboxOnSelectItem,
- ComboboxProps,
- Data,
- NoData,
-} from '@udecode/plate-combobox';
-
-import { useEditorRef } from '@udecode/plate-common';
-import { getPluginOptions } from '@udecode/plate-common/server';
-
-import { type EmojiPlugin, getEmojiOnSelectItem } from '../index';
-
-export interface TEmojiCombobox
- extends Partial> {
- onSelectItem?: ComboboxOnSelectItem | null;
- pluginKey?: string;
-}
-
-export const useEmojiComboboxState = ({ pluginKey }: { pluginKey: string }) => {
- const editor = useEditorRef();
- const { trigger } = getPluginOptions(editor, pluginKey);
-
- const onSelectItem = getEmojiOnSelectItem({ key: pluginKey });
-
- return {
- onSelectItem,
- trigger: trigger!,
- };
-};
diff --git a/packages/emoji/src/index.ts b/packages/emoji/src/index.ts
index 63d6376e12..fe2b1aa226 100644
--- a/packages/emoji/src/index.ts
+++ b/packages/emoji/src/index.ts
@@ -5,8 +5,6 @@
export * from './constants';
export * from './createEmojiPlugin';
export * from './types';
-export * from './withEmoji';
-export * from './handlers/index';
export * from './hooks/index';
export * from './storage/index';
export * from './utils/index';
diff --git a/packages/emoji/src/types.ts b/packages/emoji/src/types.ts
index 3ff72de5a4..c35e87f9d3 100644
--- a/packages/emoji/src/types.ts
+++ b/packages/emoji/src/types.ts
@@ -1,6 +1,6 @@
-import type { TComboboxItem } from '@udecode/plate-combobox';
-
-import type { IEmojiTriggeringController } from './utils/index';
+import type { Emoji } from '@emoji-mart/data';
+import type { TriggerComboboxPlugin } from '@udecode/plate-combobox';
+import type { EElementOrText } from '@udecode/plate-common';
type ReverseMap = T[keyof T];
@@ -22,22 +22,9 @@ export type EmojiSettingsType = {
};
};
-export type EmojiItemData = {
- emoji: string;
- id: string;
- name: string;
- text: string;
-};
-
-export type CreateEmoji = (
- data: TComboboxItem
-) => string;
-
-export interface EmojiPlugin {
- createEmoji?: CreateEmoji;
- emojiTriggeringController?: IEmojiTriggeringController;
- id?: string;
- trigger?: string;
+export interface EmojiPlugin
+ extends TriggerComboboxPlugin {
+ createEmojiNode?: (emoji: TEmoji) => EElementOrText;
}
export const EmojiCategory = {
@@ -73,8 +60,3 @@ export type EmojiIconList = {
loupe: T;
};
};
-
-export type FindTriggeringInputProps = {
- action?: 'delete' | 'insert';
- char?: string;
-};
diff --git a/packages/emoji/src/utils/EmojiLibrary/EmojiLibrary.types.ts b/packages/emoji/src/utils/EmojiLibrary/EmojiLibrary.types.ts
index a7f43fc59d..975a0a98cd 100644
--- a/packages/emoji/src/utils/EmojiLibrary/EmojiLibrary.types.ts
+++ b/packages/emoji/src/utils/EmojiLibrary/EmojiLibrary.types.ts
@@ -1,32 +1,17 @@
+import type { Emoji, EmojiMartData } from '@emoji-mart/data';
/**
* Emoji: type Emoji = { id: string; name: string; keywords: string[]; skins: [
* { unified: '1f389'; native: '🎉'; shortcodes: ':tada:'; } ]; version: 1; };
*/
-type Skin = {
- native: string;
- unified: string;
-};
-
-export type Emoji = {
- id: string;
- keywords: string[];
- name: string;
- skins: Skin[];
- version: number;
-};
-
export type Emojis = Record;
-export type EmojiLibrary = {
- aliases: any;
- categories: any[];
- emojis: Emojis;
- sheet: any;
-};
+export type EmojiLibrary = EmojiMartData;
export interface IEmojiLibrary {
getEmoji: (key: string) => Emoji;
getEmojiId: (key: string) => string;
keys: string[];
}
+
+export { type Emoji } from '@emoji-mart/data';
diff --git a/packages/emoji/src/utils/EmojiPicker/useEmojiPicker.ts b/packages/emoji/src/utils/EmojiPicker/useEmojiPicker.ts
index 6fe74e9484..f3b372147e 100644
--- a/packages/emoji/src/utils/EmojiPicker/useEmojiPicker.ts
+++ b/packages/emoji/src/utils/EmojiPicker/useEmojiPicker.ts
@@ -12,11 +12,11 @@ import type { Emoji, IEmojiFloatingLibrary } from '../EmojiLibrary/index';
import type { AIndexSearch } from '../IndexSearch/index';
import { i18n } from '../../constants';
-import { getEmojiOnInsert } from '../../handlers/getEmojiOnInsert';
import {
type SetFocusedAndVisibleSectionsType,
observeCategories,
} from '../EmojiObserver';
+import { insertEmoji } from '../insertEmoji';
import {
EmojiPickerState,
type MapEmojiCategoryList,
@@ -30,7 +30,7 @@ export type MutableRefs = React.MutableRefObject<{
export type UseEmojiPickerProps = {
closeOnSelect: boolean;
emojiLibrary: IEmojiFloatingLibrary;
- indexSearch: AIndexSearch;
+ indexSearch: AIndexSearch;
};
export type UseEmojiPickerType<
@@ -153,18 +153,7 @@ export const useEmojiPicker = ({
const onSelectEmoji = React.useCallback(
(emoji: Emoji) => {
- const selectItem = getEmojiOnInsert();
- selectItem(editor, {
- data: {
- emoji: emoji.skins[0].native,
- id: emoji.id,
- name: emoji.name,
- text: emoji.name,
- },
- key: emoji.id,
- text: emoji.name,
- });
-
+ insertEmoji(editor, emoji);
updateFrequentEmojis(emoji.id);
},
[editor, updateFrequentEmojis]
diff --git a/packages/emoji/src/utils/EmojiTriggeringController.ts b/packages/emoji/src/utils/EmojiTriggeringController.ts
deleted file mode 100644
index eb464a619b..0000000000
--- a/packages/emoji/src/utils/EmojiTriggeringController.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-import { emojiTriggeringControllerOptions } from '../index';
-
-export type EmojiTriggeringControllerOptions = {
- limitTriggeringChars: number;
- trigger: string;
-};
-
-export interface IEmojiTriggeringController {
- getText: () => string;
- getTextSize: () => number;
- hasEnclosingTriggeringMark: () => boolean;
- hasTriggeringMark: boolean;
- isTriggering: boolean;
- reset: () => this;
- setIsTriggering: (isTriggering: boolean) => this;
- setText: (text: string) => this;
-}
-
-export class EmojiTriggeringController implements IEmojiTriggeringController {
- private _hasTriggeringMark = false;
- private _isTriggering = false;
- protected pos: any;
- protected text = '';
-
- constructor(
- protected options: EmojiTriggeringControllerOptions = emojiTriggeringControllerOptions
- ) {}
-
- private endsWithEnclosingMark(text: string) {
- return new RegExp(`${this.options.trigger}$`).test(text);
- }
-
- private startsWithTriggeringMark(text: string) {
- return new RegExp(`^${this.options.trigger}`).test(text);
- }
-
- getText() {
- return this.text.replaceAll(/^:|:$/g, '');
- }
-
- getTextSize() {
- return this.text.length;
- }
-
- hasEnclosingTriggeringMark(): boolean {
- return this.endsWithEnclosingMark(this.text);
- }
-
- reset() {
- this.text = '';
- this.setIsTriggering(false);
- this._hasTriggeringMark = false;
-
- return this;
- }
-
- setIsTriggering(isTriggering: boolean) {
- this._isTriggering = isTriggering;
-
- return this;
- }
-
- setText(text: string) {
- this._hasTriggeringMark = this.startsWithTriggeringMark(text);
-
- this.setIsTriggering(
- this._hasTriggeringMark && text.length > this.options.limitTriggeringChars
- );
-
- this.text = this.isTriggering ? text : '';
-
- return this;
- }
-
- get hasTriggeringMark(): boolean {
- return this._hasTriggeringMark;
- }
-
- get isTriggering(): boolean {
- return this._isTriggering;
- }
-}
diff --git a/packages/emoji/src/utils/IndexSearch/EmojiFloatingIndexSearch.ts b/packages/emoji/src/utils/IndexSearch/EmojiFloatingIndexSearch.ts
index 12fdad0e60..3e17577b64 100644
--- a/packages/emoji/src/utils/IndexSearch/EmojiFloatingIndexSearch.ts
+++ b/packages/emoji/src/utils/IndexSearch/EmojiFloatingIndexSearch.ts
@@ -1,8 +1,8 @@
-import type { Emoji, IEmojiLibrary } from '../EmojiLibrary/index';
+import type { IEmojiLibrary } from '../EmojiLibrary/index';
import { AIndexSearch } from './IndexSearch';
-export class EmojiFloatingIndexSearch extends AIndexSearch {
+export class EmojiFloatingIndexSearch extends AIndexSearch {
protected static instance?: EmojiFloatingIndexSearch;
private constructor(protected library: IEmojiLibrary) {
@@ -16,8 +16,4 @@ export class EmojiFloatingIndexSearch extends AIndexSearch {
return EmojiFloatingIndexSearch.instance;
}
-
- protected transform(emoji: Emoji) {
- return emoji;
- }
}
diff --git a/packages/emoji/src/utils/IndexSearch/EmojiInlineIndexSearch.ts b/packages/emoji/src/utils/IndexSearch/EmojiInlineIndexSearch.ts
index 8e5a71cc1d..f4c99f25b1 100644
--- a/packages/emoji/src/utils/IndexSearch/EmojiInlineIndexSearch.ts
+++ b/packages/emoji/src/utils/IndexSearch/EmojiInlineIndexSearch.ts
@@ -1,8 +1,4 @@
-import {
- type Emoji,
- EmojiInlineLibrary,
- type IEmojiLibrary,
-} from '../EmojiLibrary/index';
+import { EmojiInlineLibrary, type IEmojiLibrary } from '../EmojiLibrary/index';
import { AIndexSearch } from './IndexSearch';
export class EmojiInlineIndexSearch extends AIndexSearch {
@@ -21,19 +17,4 @@ export class EmojiInlineIndexSearch extends AIndexSearch {
return EmojiInlineIndexSearch.instance;
}
-
- protected transform(emoji: Emoji) {
- const { id, name, skins } = emoji;
-
- return {
- data: {
- emoji: skins[0].native,
- id,
- name,
- text: name,
- },
- key: id,
- text: name,
- };
- }
}
diff --git a/packages/emoji/src/utils/IndexSearch/IndexSearch.ts b/packages/emoji/src/utils/IndexSearch/IndexSearch.ts
index ba038b4fa3..1643217789 100644
--- a/packages/emoji/src/utils/IndexSearch/IndexSearch.ts
+++ b/packages/emoji/src/utils/IndexSearch/IndexSearch.ts
@@ -1,21 +1,16 @@
-import type { TComboboxItem } from '@udecode/plate-combobox';
+import type { Emoji } from '@emoji-mart/data';
-import type { EmojiItemData } from '../../types';
-import type { Emoji, IEmojiLibrary } from '../EmojiLibrary/index';
+import type { IEmojiLibrary } from '../EmojiLibrary/index';
import { EMOJI_MAX_SEARCH_RESULT } from '../../constants';
-type IndexSearchReturnData = TComboboxItem;
-
-interface IIndexSearch {
- get: () => R[];
+interface IIndexSearch {
+ get: () => Emoji[];
hasFound: () => boolean;
search: (input: string) => void;
}
-export abstract class AIndexSearch
- implements IIndexSearch
-{
+export abstract class AIndexSearch implements IIndexSearch {
protected input: string | undefined;
protected maxResult = EMOJI_MAX_SEARCH_RESULT;
protected result: string[] = [];
@@ -58,7 +53,7 @@ export abstract class AIndexSearch
for (const key of this.result) {
const emoji = this.library?.getEmoji(key);
- emojis.push(this.transform(emoji!));
+ emojis.push(emoji);
if (emojis.length >= this.maxResult) break;
}
@@ -66,7 +61,7 @@ export abstract class AIndexSearch
return emojis;
}
- getEmoji(): RData | undefined {
+ getEmoji(): Emoji | undefined {
return this.get()[0];
}
@@ -92,6 +87,4 @@ export abstract class AIndexSearch
return this;
}
-
- protected abstract transform(emoji: Emoji): RData;
}
diff --git a/packages/emoji/src/utils/index.ts b/packages/emoji/src/utils/index.ts
index 220ede92cd..79da4e494d 100644
--- a/packages/emoji/src/utils/index.ts
+++ b/packages/emoji/src/utils/index.ts
@@ -2,7 +2,7 @@
* @file Automatically generated by barrelsby.
*/
-export * from './EmojiTriggeringController';
+export * from './insertEmoji';
export * from './EmojiLibrary/index';
export * from './EmojiPicker/index';
export * from './Grid/index';
diff --git a/packages/emoji/src/utils/insertEmoji.ts b/packages/emoji/src/utils/insertEmoji.ts
new file mode 100644
index 0000000000..c101a647a1
--- /dev/null
+++ b/packages/emoji/src/utils/insertEmoji.ts
@@ -0,0 +1,30 @@
+import type { Emoji } from '@emoji-mart/data';
+
+import {
+ type EElementOrText,
+ type PlateEditor,
+ type Value,
+ getPluginOptions,
+ insertNodes,
+} from '@udecode/plate-common';
+
+import type { EmojiPlugin } from '../types';
+
+import { KEY_EMOJI } from '../createEmojiPlugin';
+
+export const insertEmoji = <
+ V extends Value = Value,
+ E extends PlateEditor = PlateEditor,
+ TEmoji extends Emoji = Emoji,
+>(
+ editor: E,
+ emoji: TEmoji
+) => {
+ const { createEmojiNode } = getPluginOptions(
+ editor,
+ KEY_EMOJI
+ );
+
+ const emojiNode = createEmojiNode!(emoji);
+ insertNodes(editor, emojiNode as EElementOrText);
+};
diff --git a/packages/emoji/src/withEmoji.ts b/packages/emoji/src/withEmoji.ts
deleted file mode 100644
index bf64980a9d..0000000000
--- a/packages/emoji/src/withEmoji.ts
+++ /dev/null
@@ -1,130 +0,0 @@
-import { comboboxActions } from '@udecode/plate-combobox';
-import {
- type PlateEditor,
- type Value,
- type WithPlatePlugin,
- isCollapsed,
-} from '@udecode/plate-common/server';
-
-import type { EmojiPlugin } from './types';
-
-import { getEmojiOnSelectItem, getFindTriggeringInput } from './handlers/index';
-import { EmojiInlineIndexSearch } from './utils/index';
-
-export const withEmoji = <
- V extends Value = Value,
- E extends PlateEditor = PlateEditor,
->(
- editor: E,
- {
- options: { emojiTriggeringController, id },
- }: WithPlatePlugin
-) => {
- const emojiInlineIndexSearch = EmojiInlineIndexSearch.getInstance();
-
- const findTheTriggeringInput = getFindTriggeringInput(
- editor,
- emojiTriggeringController!
- );
-
- const { apply, deleteBackward, deleteForward, insertText } = editor;
-
- editor.insertText = (char) => {
- const { selection } = editor;
-
- if (!isCollapsed(selection)) {
- return insertText(char);
- }
-
- findTheTriggeringInput({ char });
-
- return insertText(char);
- };
-
- editor.deleteBackward = (unit) => {
- findTheTriggeringInput({ action: 'delete' });
-
- return deleteBackward(unit);
- };
-
- editor.deleteForward = (unit) => {
- findTheTriggeringInput();
-
- return deleteForward(unit);
- };
-
- editor.apply = (operation) => {
- apply(operation);
-
- if (!emojiTriggeringController?.hasTriggeringMark) {
- return;
- }
-
- const searchText = emojiTriggeringController.getText();
-
- switch (operation.type) {
- case 'set_selection': {
- emojiTriggeringController.reset();
- comboboxActions.reset();
-
- break;
- }
- case 'insert_text': {
- if (
- emojiTriggeringController.hasEnclosingTriggeringMark() &&
- emojiInlineIndexSearch.search(searchText).hasFound(true)
- ) {
- const item = emojiInlineIndexSearch.getEmoji();
- item && getEmojiOnSelectItem()(editor, item);
-
- break;
- }
- if (
- !emojiTriggeringController.hasEnclosingTriggeringMark() &&
- emojiTriggeringController.isTriggering &&
- emojiInlineIndexSearch.search(searchText).hasFound()
- ) {
- comboboxActions.items(
- emojiInlineIndexSearch.search(searchText).get()
- );
- comboboxActions.open({
- activeId: id!,
- targetRange: editor.selection,
- text: '',
- });
-
- break;
- }
-
- emojiTriggeringController.reset();
- comboboxActions.reset();
-
- break;
- }
- case 'remove_text': {
- if (
- emojiTriggeringController.isTriggering &&
- emojiInlineIndexSearch.search(searchText).hasFound()
- ) {
- comboboxActions.items(
- emojiInlineIndexSearch.search(searchText).get()
- );
- comboboxActions.open({
- activeId: id!,
- targetRange: editor.selection,
- text: '',
- });
-
- break;
- }
-
- emojiTriggeringController.reset();
- comboboxActions.reset();
-
- break;
- }
- }
- };
-
- return editor;
-};
diff --git a/packages/mention/src/__tests__/createEditorWithMentions.tsx b/packages/mention/src/__tests__/createEditorWithMentions.tsx
deleted file mode 100644
index b2fe09f902..0000000000
--- a/packages/mention/src/__tests__/createEditorWithMentions.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-/** @jsx jsx */
-
-import {
- type PlateEditor,
- type Value,
- createPlateEditor,
-} from '@udecode/plate-common';
-import { createParagraphPlugin } from '@udecode/plate-paragraph';
-import { jsx } from '@udecode/plate-test-utils';
-
-import { createMentionPlugin } from '../createMentionPlugin';
-
-jsx;
-
-export type CreateEditorOptions = {
- multipleMentionPlugins?: boolean;
- pluginOptions?: {
- key?: string;
- trigger?: string;
- triggerPreviousCharPattern?: RegExp;
- };
-};
-
-export const createEditorWithMentions = (
- state: React.ReactElement,
- {
- multipleMentionPlugins,
- pluginOptions: { key, trigger, triggerPreviousCharPattern } = {},
- }: CreateEditorOptions = {}
-): PlateEditor => {
- const plugins = [
- createParagraphPlugin(),
- createMentionPlugin({
- key,
- options: { trigger, triggerPreviousCharPattern },
- }),
- ];
-
- if (multipleMentionPlugins) {
- plugins.push(
- createMentionPlugin({ key: 'mention2', options: { trigger: '#' } })
- );
- }
-
- return createPlateEditor({
- editor: ({state} ) as any,
- plugins,
- });
-};
diff --git a/packages/mention/src/createMentionPlugin.ts b/packages/mention/src/createMentionPlugin.ts
index 38b68ea01f..8be87914e2 100644
--- a/packages/mention/src/createMentionPlugin.ts
+++ b/packages/mention/src/createMentionPlugin.ts
@@ -1,33 +1,25 @@
-import { createPluginFactory, removeNodes } from '@udecode/plate-common';
+import { withTriggerCombobox } from '@udecode/plate-combobox';
+import { createPluginFactory } from '@udecode/plate-common/server';
import type { MentionPlugin } from './types';
-import { mentionOnKeyDownHandler } from './handlers/mentionOnKeyDownHandler';
-import { isSelectionInMentionInput } from './queries/index';
-import { withMention } from './withMention';
-
export const ELEMENT_MENTION = 'mention';
export const ELEMENT_MENTION_INPUT = 'mention_input';
/** Enables support for autocompleting @mentions. */
export const createMentionPlugin = createPluginFactory({
- handlers: {
- onBlur: (editor) => () => {
- // remove mention_input nodes from editor on blur
- removeNodes(editor, {
- at: [],
- match: (n) => n.type === ELEMENT_MENTION_INPUT,
- });
- },
- onKeyDown: mentionOnKeyDownHandler({ query: isSelectionInMentionInput }),
- },
isElement: true,
isInline: true,
isMarkableVoid: true,
isVoid: true,
key: ELEMENT_MENTION,
options: {
+ createComboboxInput: (trigger) => ({
+ children: [{ text: '' }],
+ trigger,
+ type: ELEMENT_MENTION_INPUT,
+ }),
createMentionNode: (item) => ({ value: item.text }),
trigger: '@',
triggerPreviousCharPattern: /^\s?$/,
@@ -36,13 +28,9 @@ export const createMentionPlugin = createPluginFactory({
{
isElement: true,
isInline: true,
+ isVoid: true,
key: ELEMENT_MENTION_INPUT,
},
],
- then: (editor, { key }) => ({
- options: {
- id: key,
- },
- }),
- withOverrides: withMention,
+ withOverrides: withTriggerCombobox,
});
diff --git a/packages/mention/src/getMentionOnSelectItem.ts b/packages/mention/src/getMentionOnSelectItem.ts
index 8f8078a4fa..9e5925011c 100644
--- a/packages/mention/src/getMentionOnSelectItem.ts
+++ b/packages/mention/src/getMentionOnSelectItem.ts
@@ -1,89 +1,56 @@
import {
- type ComboboxOnSelectItem,
- type Data,
- type NoData,
- type TComboboxItem,
- comboboxActions,
- comboboxSelectors,
-} from '@udecode/plate-combobox';
-import {
+ type PlateEditor,
type PlatePluginKey,
- type TNodeProps,
+ type Value,
getBlockAbove,
getPlugin,
insertNodes,
insertText,
isEndPoint,
moveSelection,
- removeNodes,
- select,
- withoutMergingHistory,
- withoutNormalizing,
-} from '@udecode/plate-common';
+} from '@udecode/plate-common/server';
-import type { MentionPlugin, TMentionElement } from './types';
+import type { MentionPlugin, TMentionElement, TMentionItemBase } from './types';
import { ELEMENT_MENTION } from './createMentionPlugin';
-import { isNodeMentionInput } from './queries/isNodeMentionInput';
-
-export type CreateMentionNode = (
- item: TComboboxItem,
- meta: CreateMentionNodeMeta
-) => TNodeProps;
-export interface CreateMentionNodeMeta {
- search: string;
-}
+export type MentionOnSelectItem<
+ TItem extends TMentionItemBase = TMentionItemBase,
+> = (
+ editor: PlateEditor,
+ item: TItem,
+ search?: string
+) => void;
export const getMentionOnSelectItem =
- ({
+ ({
key = ELEMENT_MENTION,
- }: PlatePluginKey = {}): ComboboxOnSelectItem =>
- (editor, item) => {
- const targetRange = comboboxSelectors.targetRange();
-
- if (!targetRange) return;
-
+ }: PlatePluginKey = {}): MentionOnSelectItem =>
+ (editor, item, search = '') => {
const {
options: { createMentionNode, insertSpaceAfterMention },
type,
} = getPlugin(editor as any, key);
- const pathAbove = getBlockAbove(editor)?.[1];
- const isBlockEnd = () =>
- editor.selection &&
- pathAbove &&
- isEndPoint(editor, editor.selection.anchor, pathAbove);
+ const props = createMentionNode!(item, search);
- withoutNormalizing(editor, () => {
- // Selectors are sensitive to operations, it's better to create everything
- // before the editor state is changed. For example, asking for text after
- // removeNodes below will return null.
- const props = createMentionNode!(item, {
- search: comboboxSelectors.text() ?? '',
- });
-
- select(editor, targetRange);
-
- withoutMergingHistory(editor, () =>
- removeNodes(editor, {
- match: (node) => isNodeMentionInput(editor, node),
- })
- );
+ insertNodes(editor, {
+ children: [{ text: '' }],
+ type,
+ ...props,
+ } as TMentionElement);
- insertNodes(editor, {
- children: [{ text: '' }],
- type,
- ...props,
- } as TMentionElement);
+ // move the selection after the element
+ moveSelection(editor, { unit: 'offset' });
- // move the selection after the element
- moveSelection(editor, { unit: 'offset' });
+ const pathAbove = getBlockAbove(editor)?.[1];
- if (isBlockEnd() && insertSpaceAfterMention) {
- insertText(editor, ' ');
- }
- });
+ const isBlockEnd =
+ editor.selection &&
+ pathAbove &&
+ isEndPoint(editor, editor.selection.anchor, pathAbove);
- return comboboxActions.reset();
+ if (isBlockEnd && insertSpaceAfterMention) {
+ insertText(editor, ' ');
+ }
};
diff --git a/packages/mention/src/handlers/index.ts b/packages/mention/src/handlers/index.ts
deleted file mode 100644
index 60fbc656c4..0000000000
--- a/packages/mention/src/handlers/index.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-/**
- * @file Automatically generated by barrelsby.
- */
-
-export * from './mentionOnKeyDownHandler';
diff --git a/packages/mention/src/handlers/mentionOnKeyDownHandler.spec.tsx b/packages/mention/src/handlers/mentionOnKeyDownHandler.spec.tsx
deleted file mode 100644
index ea21d17157..0000000000
--- a/packages/mention/src/handlers/mentionOnKeyDownHandler.spec.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-/** @jsx jsx */
-
-import * as isHotkey from '@udecode/plate-core/server';
-import { jsx } from '@udecode/plate-test-utils';
-
-import { createEditorWithMentions } from '../__tests__/createEditorWithMentions';
-
-jsx;
-
-describe('mentionOnKeyDownHandler', () => {
- const trigger = '@';
-
- it('should remove the input on escape', () => {
- const editor = createEditorWithMentions(
-
-
-
-
-
-
- ,
- { pluginOptions: { trigger } }
- );
-
- jest.spyOn(isHotkey, 'isHotkey').mockReturnValue(true);
-
- // mentionOnKeyDownHandler({})(editor)(
- // new KeyboardEvent('keydown', { key: 'Escape' }) as any
- // );
-
- // expect(editor.children).toEqual([@ ]);
- expect(editor.children).toEqual(editor.children);
- });
-});
diff --git a/packages/mention/src/handlers/mentionOnKeyDownHandler.ts b/packages/mention/src/handlers/mentionOnKeyDownHandler.ts
deleted file mode 100644
index 47e5b90d2c..0000000000
--- a/packages/mention/src/handlers/mentionOnKeyDownHandler.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import {
- type KeyboardEventHandler,
- type MoveSelectionByOffsetOptions,
- type PlateEditor,
- type Value,
- isHotkey,
- moveSelection,
- moveSelectionByOffset,
-} from '@udecode/plate-common';
-
-import { findMentionInput } from '../queries/index';
-import { removeMentionInput } from '../transforms/index';
-
-export const mentionOnKeyDownHandler: (
- options?: MoveSelectionByOffsetOptions
-) => (editor: PlateEditor) => KeyboardEventHandler =
- (options) => (editor) => (event) => {
- if (isHotkey('escape', event)) {
- const currentMentionInput = findMentionInput(editor)!;
-
- if (currentMentionInput) {
- event.preventDefault();
- removeMentionInput(editor, currentMentionInput[1]);
- moveSelection(editor, { unit: 'word' });
-
- return true;
- }
-
- return false;
- }
-
- return moveSelectionByOffset(editor, options)(event);
- };
diff --git a/packages/mention/src/index.ts b/packages/mention/src/index.ts
index d1612be983..cf82b60840 100644
--- a/packages/mention/src/index.ts
+++ b/packages/mention/src/index.ts
@@ -5,7 +5,3 @@
export * from './createMentionPlugin';
export * from './getMentionOnSelectItem';
export * from './types';
-export * from './withMention';
-export * from './handlers/index';
-export * from './queries/index';
-export * from './transforms/index';
diff --git a/packages/mention/src/queries/findMentionInput.ts b/packages/mention/src/queries/findMentionInput.ts
deleted file mode 100644
index 80a37df2f1..0000000000
--- a/packages/mention/src/queries/findMentionInput.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import {
- type FindNodeOptions,
- type PlateEditor,
- type Value,
- findNode,
- getPluginType,
-} from '@udecode/plate-common';
-
-import type { TMentionInputElement } from '../types';
-
-import { ELEMENT_MENTION_INPUT } from '../createMentionPlugin';
-
-export const findMentionInput = (
- editor: PlateEditor,
- options?: Omit, 'match'>
-) =>
- findNode(editor, {
- ...options,
- match: { type: getPluginType(editor, ELEMENT_MENTION_INPUT) },
- });
diff --git a/packages/mention/src/queries/index.ts b/packages/mention/src/queries/index.ts
deleted file mode 100644
index 94a16a3a1d..0000000000
--- a/packages/mention/src/queries/index.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-/**
- * @file Automatically generated by barrelsby.
- */
-
-export * from './findMentionInput';
-export * from './isNodeMentionInput';
-export * from './isSelectionInMentionInput';
diff --git a/packages/mention/src/queries/isNodeMentionInput.ts b/packages/mention/src/queries/isNodeMentionInput.ts
deleted file mode 100644
index 7424f1e8eb..0000000000
--- a/packages/mention/src/queries/isNodeMentionInput.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import {
- type PlateEditor,
- type TNode,
- type Value,
- getPluginType,
-} from '@udecode/plate-common';
-
-import type { TMentionInputElement } from '../types';
-
-import { ELEMENT_MENTION_INPUT } from '../createMentionPlugin';
-
-export const isNodeMentionInput = (
- editor: PlateEditor,
- node: TNode
-): node is TMentionInputElement => {
- return node.type === getPluginType(editor, ELEMENT_MENTION_INPUT);
-};
diff --git a/packages/mention/src/queries/isSelectionInMentionInput.ts b/packages/mention/src/queries/isSelectionInMentionInput.ts
deleted file mode 100644
index 0848326be1..0000000000
--- a/packages/mention/src/queries/isSelectionInMentionInput.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import type { PlateEditor, Value } from '@udecode/plate-common';
-
-import { findMentionInput } from './findMentionInput';
-
-export const isSelectionInMentionInput = (
- editor: PlateEditor
-) => findMentionInput(editor) !== undefined;
diff --git a/packages/mention/src/transforms/index.ts b/packages/mention/src/transforms/index.ts
deleted file mode 100644
index ad93e4898f..0000000000
--- a/packages/mention/src/transforms/index.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-/**
- * @file Automatically generated by barrelsby.
- */
-
-export * from './removeMentionInput';
diff --git a/packages/mention/src/transforms/removeMentionInput.ts b/packages/mention/src/transforms/removeMentionInput.ts
deleted file mode 100644
index 6fcd288f27..0000000000
--- a/packages/mention/src/transforms/removeMentionInput.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import type { Path } from 'slate';
-
-import {
- type EText,
- type PlateEditor,
- type Value,
- getNode,
- getNodeString,
- replaceNode,
- withoutNormalizing,
-} from '@udecode/plate-common';
-
-import type { TMentionInputElement } from '../types';
-
-export const removeMentionInput = (
- editor: PlateEditor,
- path: Path
-) =>
- withoutNormalizing(editor, () => {
- const node = getNode(editor, path);
-
- if (!node) return;
-
- const { trigger } = node;
-
- const text = getNodeString(node);
-
- replaceNode(editor, {
- at: path,
- nodes: { text: `${trigger}${text}` } as EText,
- });
- });
diff --git a/packages/mention/src/types.ts b/packages/mention/src/types.ts
index 6285b8fc8f..0a9dab1ce9 100644
--- a/packages/mention/src/types.ts
+++ b/packages/mention/src/types.ts
@@ -1,22 +1,24 @@
-import type { Data, NoData } from '@udecode/plate-combobox';
-import type { PlateEditor, TElement } from '@udecode/plate-common';
+import type { TriggerComboboxPlugin } from '@udecode/plate-combobox';
+import type { TElement, TNodeProps } from '@udecode/plate-common/server';
-import type { CreateMentionNode } from './getMentionOnSelectItem';
-
-export interface TMentionElement extends TElement {
- value: string;
+export interface TMentionItemBase {
+ text: string;
}
export interface TMentionInputElement extends TElement {
trigger: string;
}
-export interface MentionPlugin {
- createMentionNode?: CreateMentionNode;
- id?: string;
- inputCreation?: { key: string; value: string };
+export interface TMentionElement extends TElement {
+ value: string;
+}
+
+export interface MentionPlugin<
+ TItem extends TMentionItemBase = TMentionItemBase,
+> extends TriggerComboboxPlugin {
+ createMentionNode?: (
+ item: TItem,
+ search: string
+ ) => TNodeProps;
insertSpaceAfterMention?: boolean;
- query?: (editor: PlateEditor) => boolean;
- trigger?: string;
- triggerPreviousCharPattern?: RegExp;
}
diff --git a/packages/mention/src/withMention.spec.tsx b/packages/mention/src/withMention.spec.tsx
deleted file mode 100644
index 36aa03aaa4..0000000000
--- a/packages/mention/src/withMention.spec.tsx
+++ /dev/null
@@ -1,637 +0,0 @@
-/** @jsx jsx */
-
-import type { Range } from 'slate';
-
-import {
- type ComboboxState,
- comboboxActions,
- comboboxSelectors,
-} from '@udecode/plate-combobox';
-import {
- type PlateEditor,
- type Value,
- moveSelection,
- select,
-} from '@udecode/plate-common';
-import {
- type DataTransferDataMap,
- createDataTransfer,
- jsx,
-} from '@udecode/plate-test-utils';
-
-import { createEditorWithMentions } from './__tests__/createEditorWithMentions';
-import { getMentionOnSelectItem } from './getMentionOnSelectItem';
-
-jsx;
-
-describe('withMention', () => {
- const trigger = '@';
- const key = 'mention';
-
- type CreateEditorOptions = {
- multipleMentionPlugins?: boolean;
- triggerPreviousCharPattern?: RegExp;
- };
-
- const createEditor = (
- state: React.ReactElement,
- options: CreateEditorOptions = {}
- ): PlateEditor =>
- createEditorWithMentions(state, {
- ...options,
- pluginOptions: {
- ...options,
- key,
- trigger,
- },
- });
-
- const createEditorWithMentionInput = (
- at: React.ReactElement = (
-
-
-
-
- ),
- options?: CreateEditorOptions
- ): PlateEditor => {
- const editor = createEditor(at, options) as PlateEditor;
-
- editor.insertText(trigger);
-
- return editor;
- };
-
- beforeEach(() => {
- comboboxActions.byId({});
- });
-
- describe('creating a mention input', () => {
- it('should insert a mention input when the trigger is inserted between words', () => {
- const editor = createEditorWithMentionInput(
-
- hello world
-
- );
-
- expect(editor.children).toEqual([
-
- hello
-
-
-
-
- world
- ,
- ]);
- });
-
- it('should insert a mention input when the trigger is inserted at line beginning followed by a whitespace', () => {
- const editor = createEditorWithMentionInput(
-
- hello world
-
- );
-
- expect(editor.children).toEqual([
-
-
-
-
-
-
- hello world
- ,
- ]);
- });
-
- it('should insert a mention input when the trigger is inserted at line end preceded by a whitespace', () => {
- const editor = createEditorWithMentionInput(
-
- hello world
-
- );
-
- expect(editor.children).toEqual([
-
- hello world
-
-
-
-
-
- ,
- ]);
- });
-
- it('should insert the trigger as text when the trigger is appended to a word', () => {
- const editor = createEditor(
-
- hello
-
-
- );
-
- editor.insertText(trigger);
-
- expect(editor.children).toEqual([
-
- hello@
-
- ,
- ]);
- });
-
- it('should insert the trigger as text when the trigger is prepended to a word', () => {
- const editor = createEditor(
-
-
- hello
-
- );
-
- editor.insertText(trigger);
-
- expect(editor.children).toEqual([
-
-
-
-
-
-
- hello
- ,
- ]);
- });
-
- it('should insert the trigger as text when the trigger is inserted into a word', () => {
- const editor = createEditor(
-
- hel
-
- lo
-
- );
-
- editor.insertText(trigger);
-
- expect(editor.children).toEqual([
-
- hel@
-
- lo
- ,
- ]);
- });
-
- it('should insert text when not trigger', () => {
- const editor = createEditor(
-
-
-
- );
-
- editor.insertText('a');
-
- expect(editor.children).toEqual([a ]);
- });
-
- it('should insert a mention input when the trigger is inserted after the specified pattern', () => {
- const emptyOrSpaceOrQuotePattern = /^$|^[\s"']$/;
- const editor = createEditor(
-
- hello " "
- ,
- {
- triggerPreviousCharPattern: emptyOrSpaceOrQuotePattern,
- }
- );
-
- editor.insertText(trigger);
-
- expect(editor.children).toEqual([
-
- hello "
-
-
-
-
- "
- ,
- ]);
- });
- });
-
- describe('removing a mention input', () => {
- it('should remove the mention input when the selection is removed from it', () => {
- const editor = createEditor(
-
-
-
-
-
-
-
-
- );
-
- select(editor, {
- offset: 0,
- path: [0, 2],
- });
-
- expect(editor.children).toEqual([@ ]);
- });
-
- it('should preserve the text that was typed into the mention input after removing', () => {
- const editor = createEditor(
-
-
-
- hello
-
-
-
-
- );
-
- select(editor, {
- offset: 0,
- path: [0, 2],
- });
-
- expect(editor.children).toEqual([@hello ]);
- });
-
- it('should change the selection to the requested location', () => {
- const editor = createEditor(
-
-
-
- hello
-
-
-
-
- );
-
- select(editor, {
- offset: 0,
- path: [0, 2],
- });
-
- expect(editor.selection).toEqual({
- anchor: { offset: 6, path: [0, 0] },
- focus: { offset: 6, path: [0, 0] },
- });
- });
-
- it('should remove the input when deleting backward in empty input', () => {
- const editor = createEditor(
-
-
-
-
-
-
-
- );
-
- editor.deleteBackward('character');
-
- expect(editor.children).toEqual([@ ]);
-
- expect(editor.selection).toEqual({
- anchor: { offset: 1, path: [0, 0] },
- focus: { offset: 1, path: [0, 0] },
- });
- });
-
- it('should block insert break', () => {
- const editor = createEditor(
-
-
-
- n
-
-
-
-
- );
-
- editor.insertBreak();
-
- expect(editor.children).toEqual([
-
-
-
- n
-
-
-
- ,
- ]);
-
- expect(editor.selection).toEqual({
- anchor: { offset: 1, path: [0, 1, 0] },
- focus: { offset: 1, path: [0, 1, 0] },
- });
- });
- });
-
- describe('typing in a mention input', () => {
- // TODO: remove if slate upgrade handles
- it('should type into a mention input if the selection is in it', () => {
- const editor = createEditorWithMentionInput(
-
-
-
-
- );
-
- editor.insertText('a');
-
- expect(editor.children).toEqual([
-
-
- a
-
- ,
- ]);
- });
-
- it('should type the trigger as text when inside a mention input', () => {
- const editor = createEditorWithMentionInput(
-
-
-
- );
-
- editor.insertText(trigger);
-
- expect(editor.children).toEqual([
-
-
- {trigger}
-
- ,
- ]);
- });
- });
-
- describe('history', () => {
- it('should undo inserting a mention by showing mention input', async () => {
- const editor = createEditorWithMentionInput(
-
-
- hello world
-
-
- );
-
- // flush previous ops to get a new undo batch going for mention input
- await Promise.resolve();
-
- getMentionOnSelectItem()(editor, { key: 'test', text: 'test' });
-
- editor.undo();
-
- expect(editor.children).toEqual([
-
- hello
-
-
-
- world
- ,
- ]);
-
- editor.undo();
-
- expect(editor.children).toEqual([
-
-
- hello world
-
- ,
- ]);
- });
-
- it('should undo inserting a mention after input by showing mention input with the text', async () => {
- const editor = createEditorWithMentionInput(
-
-
- hello world
-
-
- );
-
- // flush previous ops to get a new undo batch going for mention input
- await Promise.resolve();
-
- editor.insertText('t');
- editor.insertText('e');
-
- // flush previous ops to get a new undo batch going for mention input
- await Promise.resolve();
-
- getMentionOnSelectItem()(editor, { key: 'test', text: 'test' });
-
- editor.undo();
-
- expect(editor.children).toEqual([
-
- hello
-
- te
-
- world
- ,
- ]);
-
- editor.undo();
-
- expect(editor.children).toEqual([
-
- hello
-
-
-
- world
- ,
- ]);
-
- editor.undo();
-
- expect(editor.children).toEqual([
-
-
- hello world
-
- ,
- ]);
- });
- });
-
- describe('combobox', () => {
- it('should show the combobox when a mention input is created', () => {
- createEditorWithMentionInput(
-
-
- ,
- { multipleMentionPlugins: true }
- );
-
- expect(comboboxSelectors.state()).toMatchObject>({
- activeId: key,
- });
- });
-
- it('should close the combobox when a mention input is removed', () => {
- const editor = createEditorWithMentionInput(
-
-
-
-
- );
-
- select(editor, {
- offset: 0,
- path: [0, 2],
- });
-
- expect(comboboxSelectors.state()).toMatchObject>({
- activeId: null,
- });
- });
-
- it('should update the text in the combobox when typing', () => {
- const editor = createEditorWithMentionInput();
-
- editor.insertText('abc');
- expect(comboboxSelectors.state()).toMatchObject>({
- text: 'abc',
- });
-
- editor.deleteBackward('character');
- expect(comboboxSelectors.state()).toMatchObject>({
- text: 'ab',
- });
-
- moveSelection(editor, { distance: 1, reverse: true });
- editor.deleteForward('character');
- expect(comboboxSelectors.state()).toMatchObject>({
- text: 'a',
- });
- });
- });
-
- describe('paste', () => {
- const testPaste: (
- data: DataTransferDataMap,
- input: React.ReactElement,
- expected: React.ReactElement
- ) => void = (data, input, expected) => {
- const editor = createEditorWithMentionInput(input);
-
- editor.insertData(createDataTransfer(data));
-
- expect(editor.children).toEqual([expected]);
- };
-
- const testPasteBasic: (
- data: DataTransferDataMap,
- expected: string
- ) => void = (data, expected) => {
- testPaste(
- data,
-
-
- ,
-
-
- {expected}
-
-
- );
- };
-
- type PasteTestCase = {
- data: DataTransferDataMap;
- expected: string;
- };
-
- const basePasteTestSuite = ({
- newLine,
- newLineAndWhitespace,
- simple,
- whitespace,
- }: {
- newLine: PasteTestCase;
- newLineAndWhitespace: PasteTestCase;
- simple: PasteTestCase;
- whitespace: PasteTestCase;
- }): void => {
- it('should paste the clipboard contents into mention as text', () =>
- testPasteBasic(simple.data, simple.expected));
-
- it('should merge lines', () =>
- testPasteBasic(newLine.data, newLine.expected));
-
- it('should trim the text', () =>
- testPasteBasic(whitespace.data, whitespace.expected));
-
- it('should trim every line before merging', () =>
- testPasteBasic(
- newLineAndWhitespace.data,
- newLineAndWhitespace.expected
- ));
- };
-
- describe('html', () => {
- basePasteTestSuite({
- newLine: {
- data: new Map([
- ['text/html', 'hello world'],
- ]),
- expected: 'helloworld',
- },
- newLineAndWhitespace: {
- data: new Map([
- ['text/html', ' hello world '],
- ]),
- expected: 'helloworld',
- },
- simple: {
- data: new Map([['text/html', 'hello']]),
- expected: 'hello',
- },
- whitespace: {
- data: new Map([['text/html', ' hello ']]),
- expected: 'hello',
- },
- });
- });
-
- describe('plain text', () => {
- basePasteTestSuite({
- newLine: {
- data: new Map([['text/plain', 'hello\r\nworld\n!\r!']]),
- expected: 'helloworld!!',
- },
- newLineAndWhitespace: {
- data: new Map([['text/plain', ' hello \r\n world \n ! \r ! ']]),
- expected: 'helloworld!!',
- },
- simple: {
- data: new Map([['text/plain', 'hello']]),
- expected: 'hello',
- },
- whitespace: {
- data: new Map([['text/plain', ' hello ']]),
- expected: 'hello',
- },
- });
- });
- });
-});
diff --git a/packages/mention/src/withMention.ts b/packages/mention/src/withMention.ts
deleted file mode 100644
index 51e259b9a8..0000000000
--- a/packages/mention/src/withMention.ts
+++ /dev/null
@@ -1,213 +0,0 @@
-import { comboboxActions } from '@udecode/plate-combobox';
-import {
- type PlateEditor,
- type TNode,
- type TText,
- type Value,
- type WithPlatePlugin,
- getEditorString,
- getNodeString,
- getPlugin,
- getPointBefore,
- getRange,
- moveSelection,
- setSelection,
-} from '@udecode/plate-common';
-import { Range } from 'slate';
-
-import type { MentionPlugin, TMentionInputElement } from './types';
-
-import { ELEMENT_MENTION_INPUT } from './createMentionPlugin';
-import {
- findMentionInput,
- isNodeMentionInput,
- isSelectionInMentionInput,
-} from './queries/index';
-import { removeMentionInput } from './transforms/removeMentionInput';
-
-export const withMention = <
- V extends Value = Value,
- E extends PlateEditor = PlateEditor,
->(
- editor: E,
- {
- options: { id, inputCreation, query, trigger, triggerPreviousCharPattern },
- }: WithPlatePlugin
-) => {
- const { type } = getPlugin<{}, V>(editor, ELEMENT_MENTION_INPUT);
-
- const {
- apply,
- deleteBackward,
- insertBreak,
- insertFragment,
- insertNode,
- insertText,
- insertTextData,
- } = editor;
-
- const stripNewLineAndTrim: (text: string) => string = (text) => {
- return text
- .split(/\r\n|\r|\n/)
- .map((line) => line.trim())
- .join('');
- };
-
- editor.insertFragment = (fragment) => {
- const inMentionInput = findMentionInput(editor) !== undefined;
-
- if (!inMentionInput) {
- return insertFragment(fragment);
- }
-
- return insertText(
- fragment.map((node) => stripNewLineAndTrim(getNodeString(node))).join('')
- );
- };
-
- editor.insertTextData = (data) => {
- const inMentionInput = findMentionInput(editor) !== undefined;
-
- if (!inMentionInput) {
- return insertTextData(data);
- }
-
- const text = data.getData('text/plain');
-
- if (!text) {
- return false;
- }
-
- editor.insertText(stripNewLineAndTrim(text));
-
- return true;
- };
-
- editor.deleteBackward = (unit) => {
- const currentMentionInput = findMentionInput(editor);
-
- if (currentMentionInput && getNodeString(currentMentionInput[0]) === '') {
- removeMentionInput(editor, currentMentionInput[1]);
-
- return moveSelection(editor, { unit: 'word' });
- }
-
- deleteBackward(unit);
- };
-
- editor.insertBreak = () => {
- if (isSelectionInMentionInput(editor)) {
- return;
- }
-
- insertBreak();
- };
-
- editor.insertText = (text) => {
- if (
- !editor.selection ||
- text !== trigger ||
- (query && !query(editor as PlateEditor)) ||
- isSelectionInMentionInput(editor)
- ) {
- return insertText(text);
- }
-
- // Make sure a mention input is created at the beginning of line or after a whitespace
- const previousChar = getEditorString(
- editor,
- getRange(
- editor,
- editor.selection,
- getPointBefore(editor, editor.selection)
- )
- );
- const matchesPreviousCharPattern =
- triggerPreviousCharPattern?.test(previousChar);
-
- if (matchesPreviousCharPattern && text === trigger) {
- const data: TMentionInputElement = {
- children: [{ text: '' }],
- trigger,
- type,
- };
-
- if (inputCreation) {
- data[inputCreation.key] = inputCreation.value;
- }
-
- return insertNode(data);
- }
-
- return insertText(text);
- };
-
- editor.apply = (operation) => {
- apply(operation);
-
- if (operation.type === 'insert_text' || operation.type === 'remove_text') {
- const currentMentionInput = findMentionInput(editor);
-
- if (currentMentionInput) {
- comboboxActions.text(getNodeString(currentMentionInput[0]));
- }
- } else if (operation.type === 'set_selection') {
- const previousMentionInputPath = Range.isRange(operation.properties)
- ? findMentionInput(editor, { at: operation.properties })?.[1]
- : undefined;
-
- const currentMentionInputPath = Range.isRange(operation.newProperties)
- ? findMentionInput(editor, { at: operation.newProperties })?.[1]
- : undefined;
-
- if (previousMentionInputPath && !currentMentionInputPath) {
- removeMentionInput(editor, previousMentionInputPath);
- moveSelection(editor, { unit: 'word' });
- }
- if (currentMentionInputPath) {
- comboboxActions.targetRange(editor.selection);
- }
- } else if (
- operation.type === 'insert_node' &&
- isNodeMentionInput(editor, operation.node as TNode)
- ) {
- if ((operation.node as TMentionInputElement).trigger !== trigger) {
- return;
- }
-
- const text =
- ((operation.node as TMentionInputElement).children as TText[])[0]
- ?.text ?? '';
-
- if (
- inputCreation === undefined ||
- operation.node[inputCreation.key] === inputCreation.value
- ) {
- // Needed for undo - after an undo a mention insert we only receive
- // an insert_node with the mention input, i.e. nothing indicating that it
- // was an undo.
- setSelection(editor, {
- anchor: { offset: text.length, path: operation.path.concat([0]) },
- focus: { offset: text.length, path: operation.path.concat([0]) },
- });
-
- comboboxActions.open({
- activeId: id!,
- targetRange: editor.selection,
- text,
- });
- }
- } else if (
- operation.type === 'remove_node' &&
- isNodeMentionInput(editor, operation.node as TNode)
- ) {
- if ((operation.node as TMentionInputElement).trigger !== trigger) {
- return;
- }
-
- comboboxActions.reset();
- }
- };
-
- return editor;
-};
diff --git a/packages/slash-command/src/createSlashPlugin.ts b/packages/slash-command/src/createSlashPlugin.ts
index 95baac286e..bd901b0468 100644
--- a/packages/slash-command/src/createSlashPlugin.ts
+++ b/packages/slash-command/src/createSlashPlugin.ts
@@ -1,30 +1,20 @@
-import { createPluginFactory, removeNodes } from '@udecode/plate-common';
-
-import type { SlashPlugin } from './types';
-
-import { slashOnKeyDownHandler } from './handlers/slashOnKeyDownHandler';
-import { isSelectionInSlashInput } from './queries/index';
-import { withSlashCommand } from './withSlashCommand';
+import {
+ type TriggerComboboxPlugin,
+ withTriggerCombobox,
+} from '@udecode/plate-combobox';
+import { createPluginFactory } from '@udecode/plate-common/server';
export const KEY_SLASH_COMMAND = 'slash_command';
export const ELEMENT_SLASH_INPUT = 'slash_input';
-/** Enables support for autocompleting /slash_command. */
-export const createSlashPlugin = createPluginFactory({
- handlers: {
- onBlur: (editor) => () => {
- // remove slash_input nodes from editor on blur
- removeNodes(editor, {
- at: [],
- match: (n) => n.type === ELEMENT_SLASH_INPUT,
- });
- },
- onKeyDown: slashOnKeyDownHandler({ query: isSelectionInSlashInput }),
- },
+export const createSlashPlugin = createPluginFactory({
key: KEY_SLASH_COMMAND,
options: {
- createSlashNode: (item) => ({ value: item.text }),
+ createComboboxInput: () => ({
+ children: [{ text: '' }],
+ type: ELEMENT_SLASH_INPUT,
+ }),
trigger: '/',
triggerPreviousCharPattern: /^\s?$/,
},
@@ -32,13 +22,9 @@ export const createSlashPlugin = createPluginFactory({
{
isElement: true,
isInline: true,
+ isVoid: true,
key: ELEMENT_SLASH_INPUT,
},
],
- then: (editor, { key }) => ({
- options: {
- id: key,
- },
- }),
- withOverrides: withSlashCommand,
+ withOverrides: withTriggerCombobox,
});
diff --git a/packages/slash-command/src/getSlashOnSelectItem.ts b/packages/slash-command/src/getSlashOnSelectItem.ts
deleted file mode 100644
index 11c8369bf8..0000000000
--- a/packages/slash-command/src/getSlashOnSelectItem.ts
+++ /dev/null
@@ -1,80 +0,0 @@
-import {
- type ComboboxOnSelectItem,
- type Data,
- type NoData,
- type TComboboxItem,
- comboboxActions,
- comboboxSelectors,
-} from '@udecode/plate-combobox';
-import {
- type PlatePluginKey,
- type TNodeProps,
- getBlockAbove,
- getPlugin,
- insertText,
- isEndPoint,
- moveSelection,
- removeNodes,
- withoutMergingHistory,
- withoutNormalizing,
-} from '@udecode/plate-common';
-
-import type { SlashPlugin, TSlashElement } from './types';
-
-import { KEY_SLASH_COMMAND } from './createSlashPlugin';
-import { isNodeSlashInput } from './queries/isNodeSlashInput';
-
-export type CreateSlashNode = (
- item: TComboboxItem,
- meta: CreateSlashNodeMeta
-) => TNodeProps;
-
-export interface CreateSlashNodeMeta {
- search: string;
-}
-
-export const getSlashOnSelectItem =
- ({
- key = KEY_SLASH_COMMAND,
- }: PlatePluginKey = {}): ComboboxOnSelectItem =>
- (editor: any, item: any) => {
- const targetRange = comboboxSelectors.targetRange();
-
- if (!targetRange) return;
-
- const {
- options: { insertSpaceAfterSlash, rules },
- } = getPlugin(editor, key);
-
- const pathAbove = getBlockAbove(editor)?.[1];
- const isBlockEnd = () =>
- editor.selection &&
- pathAbove &&
- isEndPoint(editor, editor.selection.anchor, pathAbove);
-
- withoutNormalizing(editor, () => {
- // Selectors are sensitive to operations, it's better to create everything
- // before the editor state is changed. For example, asking for text after
- // removeNodes below will return null.
-
- withoutMergingHistory(editor, () =>
- removeNodes(editor, {
- match: (node) => isNodeSlashInput(editor, node),
- })
- );
-
- if (rules) {
- const target = rules.find((rule) => rule.key === item.key);
- target && target.onTrigger(editor, target.key);
- }
-
- // move the selection after the element
- moveSelection(editor, { unit: 'offset' });
-
- if (isBlockEnd() && insertSpaceAfterSlash) {
- insertText(editor, ' ');
- }
- });
-
- return comboboxActions.reset();
- };
diff --git a/packages/slash-command/src/handlers/index.ts b/packages/slash-command/src/handlers/index.ts
deleted file mode 100644
index ded3e12b8f..0000000000
--- a/packages/slash-command/src/handlers/index.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-/**
- * @file Automatically generated by barrelsby.
- */
-
-export * from './slashOnKeyDownHandler';
diff --git a/packages/slash-command/src/handlers/slashOnKeyDownHandler.ts b/packages/slash-command/src/handlers/slashOnKeyDownHandler.ts
deleted file mode 100644
index b171fdaf4b..0000000000
--- a/packages/slash-command/src/handlers/slashOnKeyDownHandler.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import {
- type KeyboardEventHandler,
- type MoveSelectionByOffsetOptions,
- type PlateEditor,
- type Value,
- isHotkey,
- moveSelection,
- moveSelectionByOffset,
-} from '@udecode/plate-common';
-
-import { findSlashInput } from '../queries/index';
-import { removeSlashInput } from '../transforms/index';
-
-export const slashOnKeyDownHandler: (
- options?: MoveSelectionByOffsetOptions
-) => (editor: PlateEditor) => KeyboardEventHandler =
- (options) => (editor) => (event) => {
- if (isHotkey('escape', event)) {
- const currentSlashInput = findSlashInput(editor)!;
-
- if (currentSlashInput) {
- event.preventDefault();
- removeSlashInput(editor, currentSlashInput[1]);
- moveSelection(editor, { unit: 'word' });
-
- return true;
- }
-
- return false;
- }
-
- return moveSelectionByOffset(editor, options)(event);
- };
diff --git a/packages/slash-command/src/index.ts b/packages/slash-command/src/index.ts
index c8f331807d..81e2206b22 100644
--- a/packages/slash-command/src/index.ts
+++ b/packages/slash-command/src/index.ts
@@ -3,9 +3,4 @@
*/
export * from './createSlashPlugin';
-export * from './getSlashOnSelectItem';
export * from './types';
-export * from './withSlashCommand';
-export * from './handlers/index';
-export * from './queries/index';
-export * from './transforms/index';
diff --git a/packages/slash-command/src/queries/findSlashInput.ts b/packages/slash-command/src/queries/findSlashInput.ts
deleted file mode 100644
index 2e50bc1fb1..0000000000
--- a/packages/slash-command/src/queries/findSlashInput.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import {
- type FindNodeOptions,
- type PlateEditor,
- type Value,
- findNode,
- getPluginType,
-} from '@udecode/plate-common';
-
-import type { TSlashInputElement } from '../types';
-
-import { ELEMENT_SLASH_INPUT } from '../createSlashPlugin';
-
-export const findSlashInput = (
- editor: PlateEditor,
- options?: Omit, 'match'>
-) =>
- findNode(editor, {
- ...options,
- match: { type: getPluginType(editor, ELEMENT_SLASH_INPUT) },
- });
diff --git a/packages/slash-command/src/queries/index.ts b/packages/slash-command/src/queries/index.ts
deleted file mode 100644
index e43bb26031..0000000000
--- a/packages/slash-command/src/queries/index.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-/**
- * @file Automatically generated by barrelsby.
- */
-
-export * from './findSlashInput';
-export * from './isNodeSlashInput';
-export * from './isSelectionInSlashInput';
diff --git a/packages/slash-command/src/queries/isNodeSlashInput.ts b/packages/slash-command/src/queries/isNodeSlashInput.ts
deleted file mode 100644
index f0117b33c9..0000000000
--- a/packages/slash-command/src/queries/isNodeSlashInput.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import {
- type PlateEditor,
- type TNode,
- type Value,
- getPluginType,
-} from '@udecode/plate-common';
-
-import type { TSlashInputElement } from '../types';
-
-import { ELEMENT_SLASH_INPUT } from '../createSlashPlugin';
-
-export const isNodeSlashInput = (
- editor: PlateEditor,
- node: TNode
-): node is TSlashInputElement => {
- return node.type === getPluginType(editor, ELEMENT_SLASH_INPUT);
-};
diff --git a/packages/slash-command/src/queries/isSelectionInSlashInput.ts b/packages/slash-command/src/queries/isSelectionInSlashInput.ts
deleted file mode 100644
index 48cebc57db..0000000000
--- a/packages/slash-command/src/queries/isSelectionInSlashInput.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import type { PlateEditor, Value } from '@udecode/plate-common';
-
-import { findSlashInput } from '.';
-
-export const isSelectionInSlashInput = (
- editor: PlateEditor
-) => findSlashInput(editor) !== undefined;
diff --git a/packages/slash-command/src/transforms/index.ts b/packages/slash-command/src/transforms/index.ts
deleted file mode 100644
index acf25b6224..0000000000
--- a/packages/slash-command/src/transforms/index.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-/**
- * @file Automatically generated by barrelsby.
- */
-
-export * from './removeSlashInput';
diff --git a/packages/slash-command/src/transforms/removeSlashInput.ts b/packages/slash-command/src/transforms/removeSlashInput.ts
deleted file mode 100644
index e649f197b0..0000000000
--- a/packages/slash-command/src/transforms/removeSlashInput.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import type { Path } from 'slate';
-
-import {
- type EText,
- type PlateEditor,
- type Value,
- getNode,
- getNodeString,
- replaceNode,
- withoutNormalizing,
-} from '@udecode/plate-common';
-
-import type { TSlashInputElement } from '../types';
-
-export const removeSlashInput = (
- editor: PlateEditor,
- path: Path
-) =>
- withoutNormalizing(editor, () => {
- const node = getNode(editor, path);
-
- if (!node) return;
-
- const { trigger } = node;
-
- const text = getNodeString(node);
-
- replaceNode(editor, {
- at: path,
- nodes: { text: `${trigger}${text}` } as EText,
- });
- });
diff --git a/packages/slash-command/src/types.ts b/packages/slash-command/src/types.ts
index 12607cda4f..35f03537eb 100644
--- a/packages/slash-command/src/types.ts
+++ b/packages/slash-command/src/types.ts
@@ -1,29 +1,3 @@
-import type { Data, NoData } from '@udecode/plate-combobox';
-import type { PlateEditor, TElement } from '@udecode/plate-common';
+import type { TElement } from '@udecode/plate-common/server';
-import type { CreateSlashNode } from './getSlashOnSelectItem';
-
-export interface TSlashElement extends TElement {
- value: string;
-}
-
-export interface TSlashInputElement extends TElement {
- trigger: string;
-}
-
-export interface SlashRule {
- key: string;
- onTrigger: (editor: PlateEditor, key: string) => void;
- text: React.ReactNode;
-}
-
-export interface SlashPlugin {
- createSlashNode?: CreateSlashNode;
- id?: string;
- inputCreation?: { key: string; value: string };
- insertSpaceAfterSlash?: boolean;
- query?: (editor: PlateEditor) => boolean;
- rules?: SlashRule[];
- trigger?: string;
- triggerPreviousCharPattern?: RegExp;
-}
+export interface TSlashInputElement extends TElement {}
diff --git a/packages/slash-command/src/withSlashCommand.ts b/packages/slash-command/src/withSlashCommand.ts
deleted file mode 100644
index 562594d795..0000000000
--- a/packages/slash-command/src/withSlashCommand.ts
+++ /dev/null
@@ -1,217 +0,0 @@
-import { comboboxActions } from '@udecode/plate-combobox';
-import {
- type PlateEditor,
- type TNode,
- type TText,
- type Value,
- type WithPlatePlugin,
- getEditorString,
- getNodeString,
- getPlugin,
- getPointBefore,
- getRange,
- moveSelection,
- setSelection,
-} from '@udecode/plate-common';
-import { Range } from 'slate';
-
-import type { SlashPlugin, TSlashInputElement } from './types';
-
-import { ELEMENT_SLASH_INPUT } from './createSlashPlugin';
-import {
- findSlashInput,
- isNodeSlashInput,
- isSelectionInSlashInput,
-} from './queries/index';
-import { removeSlashInput } from './transforms';
-
-export const withSlashCommand = <
- V extends Value = Value,
- E extends PlateEditor = PlateEditor,
->(
- editor: E,
- {
- options: { id, inputCreation, query, trigger, triggerPreviousCharPattern },
- }: WithPlatePlugin
-) => {
- const { type } = getPlugin<{}, V>(editor, ELEMENT_SLASH_INPUT);
-
- const {
- apply,
- deleteBackward,
- insertBreak,
- insertFragment,
- insertNode,
- insertText,
- insertTextData,
- } = editor;
-
- const stripNewLineAndTrim: (text: string) => string = (text) => {
- return text
- .split(/\r\n|\r|\n/)
- .map((line) => line.trim())
- .join('');
- };
-
- editor.insertFragment = (fragment) => {
- const inSlashInput = findSlashInput(editor) !== undefined;
-
- if (!inSlashInput) {
- return insertFragment(fragment);
- }
-
- return insertText(
- fragment.map((node) => stripNewLineAndTrim(getNodeString(node))).join('')
- );
- };
-
- editor.insertTextData = (data) => {
- const inSlashInput = findSlashInput(editor) !== undefined;
-
- if (!inSlashInput) {
- return insertTextData(data);
- }
-
- const text = data.getData('text/plain');
-
- if (!text) {
- return false;
- }
-
- editor.insertText(stripNewLineAndTrim(text));
-
- return true;
- };
-
- editor.deleteBackward = (unit) => {
- const currentSlashInput = findSlashInput(editor);
-
- if (currentSlashInput && getNodeString(currentSlashInput[0]) === '') {
- removeSlashInput(editor, currentSlashInput[1]);
-
- return moveSelection(editor, { unit: 'word' });
- }
-
- deleteBackward(unit);
- };
-
- editor.insertBreak = () => {
- if (isSelectionInSlashInput(editor)) {
- return;
- }
-
- insertBreak();
- };
-
- editor.insertText = (text) => {
- if (
- !editor.selection ||
- text !== trigger ||
- (query && !query(editor as PlateEditor)) ||
- isSelectionInSlashInput(editor)
- ) {
- return insertText(text);
- }
-
- // Make sure a slash input is created at the beginning of line or after a whitespace
- const previousChar = getEditorString(
- editor,
- getRange(
- editor,
- editor.selection,
- getPointBefore(editor, editor.selection)
- )
- );
- const matchesPreviousCharPattern =
- triggerPreviousCharPattern?.test(previousChar);
-
- if (matchesPreviousCharPattern && text === trigger) {
- const data: TSlashInputElement = {
- children: [{ text: '' }],
- trigger,
- type,
- };
-
- if (inputCreation) {
- data[inputCreation.key] = inputCreation.value;
- }
-
- return insertNode(data);
- }
-
- return insertText(text);
- };
-
- editor.apply = (operation) => {
- apply(operation);
-
- if (operation.type === 'insert_text' || operation.type === 'remove_text') {
- const currentSlashInput = findSlashInput(editor);
-
- if (currentSlashInput) {
- comboboxActions.text(getNodeString(currentSlashInput[0]));
- }
- } else if (operation.type === 'set_selection') {
- const previousSlashInputPath = Range.isRange(operation.properties)
- ? findSlashInput(editor, {
- at: operation.properties,
- })?.[1]
- : undefined;
-
- const currentSlashInputPath = Range.isRange(operation.newProperties)
- ? findSlashInput(editor, {
- at: operation.newProperties,
- })?.[1]
- : undefined;
-
- if (previousSlashInputPath && !currentSlashInputPath) {
- removeSlashInput(editor, previousSlashInputPath);
- moveSelection(editor, { unit: 'word' });
- }
- if (currentSlashInputPath) {
- comboboxActions.targetRange(editor.selection);
- }
- } else if (
- operation.type === 'insert_node' &&
- isNodeSlashInput(editor, operation.node as TNode)
- ) {
- if ((operation.node as TSlashInputElement).trigger !== trigger) {
- return;
- }
-
- const text =
- ((operation.node as TSlashInputElement).children as TText[])[0]?.text ??
- '';
-
- if (
- inputCreation === undefined ||
- operation.node[inputCreation.key] === inputCreation.value
- ) {
- // Needed for undo - after an undo a slash insert we only receive
- // an insert_node with the slash input, i.e. nothing indicating that it
- // was an undo.
- setSelection(editor, {
- anchor: { offset: text.length, path: operation.path.concat([0]) },
- focus: { offset: text.length, path: operation.path.concat([0]) },
- });
-
- comboboxActions.open({
- activeId: id!,
- targetRange: editor.selection,
- text,
- });
- }
- } else if (
- operation.type === 'remove_node' &&
- isNodeSlashInput(editor, operation.node as TNode)
- ) {
- if ((operation.node as TSlashInputElement).trigger !== trigger) {
- return;
- }
-
- comboboxActions.reset();
- }
- };
-
- return editor;
-};
diff --git a/yarn.lock b/yarn.lock
index f2106a199b..8bd0b6eef4 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -51,6 +51,39 @@ __metadata:
languageName: node
linkType: hard
+"@ariakit/core@npm:0.4.6":
+ version: 0.4.6
+ resolution: "@ariakit/core@npm:0.4.6"
+ checksum: 10c0/ca95be5acfd55ad99fa2eaddfdcf2dd178622ac64634bec80709dc4c722f8f15ac6d321831c72ab034001fe00964f7a2531e519916f29cf885d8cf3ffdbb6776
+ languageName: node
+ linkType: hard
+
+"@ariakit/react-core@npm:0.4.6":
+ version: 0.4.6
+ resolution: "@ariakit/react-core@npm:0.4.6"
+ dependencies:
+ "@ariakit/core": "npm:0.4.6"
+ "@floating-ui/dom": "npm:^1.0.0"
+ use-sync-external-store: "npm:^1.2.0"
+ peerDependencies:
+ react: ^17.0.0 || ^18.0.0
+ react-dom: ^17.0.0 || ^18.0.0
+ checksum: 10c0/cd24d020a380a5de48607119c7f46a1b64bc8780d7b4a18f09207b2a3c13957cfc1c5dc0256ad8dd0924714b074e30f4af5702d25b438885516d9c900cc8ce91
+ languageName: node
+ linkType: hard
+
+"@ariakit/react@npm:0.4.6":
+ version: 0.4.6
+ resolution: "@ariakit/react@npm:0.4.6"
+ dependencies:
+ "@ariakit/react-core": "npm:0.4.6"
+ peerDependencies:
+ react: ^17.0.0 || ^18.0.0
+ react-dom: ^17.0.0 || ^18.0.0
+ checksum: 10c0/647d540c81d116de690e80544152471be59ced91ca1a31e81dbafea162397e3ce16844401eac708c06e8ad834ca6779eb373fc4634e28d6ecdb7e0f2fce4a061
+ languageName: node
+ linkType: hard
+
"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.21.4, @babel/code-frame@npm:^7.23.5, @babel/code-frame@npm:^7.24.1, @babel/code-frame@npm:^7.24.2":
version: 7.24.2
resolution: "@babel/code-frame@npm:7.24.2"
@@ -1535,6 +1568,16 @@ __metadata:
languageName: node
linkType: hard
+"@floating-ui/dom@npm:^1.0.0":
+ version: 1.6.4
+ resolution: "@floating-ui/dom@npm:1.6.4"
+ dependencies:
+ "@floating-ui/core": "npm:^1.0.0"
+ "@floating-ui/utils": "npm:^0.2.0"
+ checksum: 10c0/cee0b9e6efc1c6d978ec580c770078fdf416016fb03f3dd99630f7f32d0422722e608471fbc7578be86c783ad1c1e448c5fa5b9fdec889dfbf4b695f208730fd
+ languageName: node
+ linkType: hard
+
"@floating-ui/dom@npm:^1.2.1, @floating-ui/dom@npm:^1.6.1":
version: 1.6.3
resolution: "@floating-ui/dom@npm:1.6.3"
@@ -21557,6 +21600,7 @@ __metadata:
version: 0.0.0-use.local
resolution: "www@workspace:apps/www"
dependencies:
+ "@ariakit/react": "npm:0.4.6"
"@faker-js/faker": "npm:^8.4.1"
"@radix-ui/colors": "npm:1.0.1"
"@radix-ui/react-accessible-icon": "npm:^1.0.3"