diff --git a/.changeset/dull-comics-fold.md b/.changeset/dull-comics-fold.md new file mode 100644 index 0000000000..5679b0fc40 --- /dev/null +++ b/.changeset/dull-comics-fold.md @@ -0,0 +1,5 @@ +--- +"@udecode/plate-toggle": minor +--- + +New plugin: toggle diff --git a/apps/www/content/docs/components/changelog.mdx b/apps/www/content/docs/components/changelog.mdx index 83d9391378..29cc8c1b83 100644 --- a/apps/www/content/docs/components/changelog.mdx +++ b/apps/www/content/docs/components/changelog.mdx @@ -10,6 +10,11 @@ Use the [CLI](https://platejs.org/docs/components/cli) to install the latest ver ## January 2024 #7 +### January 31 #7.5 + +- add `toggle-element` +- add `toggle-toolbar-button` + ### January 11 #7.4 - add support for custom ui dir in `components.json` diff --git a/apps/www/content/docs/components/toggle-element.mdx b/apps/www/content/docs/components/toggle-element.mdx new file mode 100644 index 0000000000..0cca783c52 --- /dev/null +++ b/apps/www/content/docs/components/toggle-element.mdx @@ -0,0 +1,63 @@ +--- +title: Toggle Element +description: Add toggles to your document. +component: true +docs: + - route: /docs/toggle + title: Toggle +--- + +## Installation + + + + +CLI +Manual + + + +```bash +npx @udecode/plate-ui@latest add toggle-element +``` + + + + + + + + + +Install the following dependencies: + +- [Toggle](/docs/toggle) +- [Toolbar](/docs/components/toolbar) + + + + + +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/toggle-toolbar-button.mdx b/apps/www/content/docs/components/toggle-toolbar-button.mdx new file mode 100644 index 0000000000..c252fa5cf8 --- /dev/null +++ b/apps/www/content/docs/components/toggle-toolbar-button.mdx @@ -0,0 +1,86 @@ +--- +title: Toggle Toolbar Button +description: Turns blocks into toggles and vice-versa. +component: true +docs: + - route: /docs/toggle + title: Toggle + - route: /docs/components/toolbar + title: Toolbar +--- + +## Installation + + + + +CLI +Manual + + + +```bash +npx @udecode/plate-ui@latest add toggle-toolbar-button +``` + + + + + + + + + +Install the following dependencies: + +- [Toggle](/docs/toggle) +- [Toolbar](/docs/components/toolbar) + + + + + +Copy and paste the following code into your project. + + + + + + + +Update the import paths to match your project setup. + + + + + + + + + + + +Manual Installation + + +1. Install the following dependencies: + +- [Toggle](/docs/toggle) +- [Toolbar](/docs/components/toolbar) + +2. Copy and paste the following code into your project. + +- `components/plate-ui/toggle-toolbar-button.tsx` + + + + + + + + +## Examples + + + + diff --git a/apps/www/content/docs/toggle.mdx b/apps/www/content/docs/toggle.mdx new file mode 100644 index 0000000000..4201b8bdbf --- /dev/null +++ b/apps/www/content/docs/toggle.mdx @@ -0,0 +1,182 @@ +--- +title: Toggle +description: Add toggles to your document. +docs: + - route: /docs/components/toggle-toolbar-button + title: Toggle Button +--- + + + + + +## Features + +- Add toggles to your document. + - Toggles are closed by default, and can be opened to reveal the content inside. +- Refer to the [Indent documentation](/docs/indent) for more information. + +## Plugin dependencies + +- [Indent](/docs/indent) +- [Node id](/docs/node-id) + + + +## Installation + +```bash +npm install @udecode/plate-indent @udecode/plate-node-id @udecode/plate-toggle +``` + +## Usage + +```tsx +// ... +import { createIndentPlugin } from '@udecode/plate-indent'; +import { createNodeIdPlugin } from '@udecode/plate-node-id'; +import { createTogglePlugin, ELEMENT_TOGGLE } from '@udecode/plate-toggle'; + +const plugins = [ + // ...otherPlugins, + createHeadingPlugin(), + createParagraphPlugin(), + createIndentPlugin({ + inject: { + props: { + validTypes: [ELEMENT_TOGGLE, ELEMENT_PARAGRAPH, ELEMENT_H1], + }, + }, + }), + createNodeIdPlugin(), + createTogglePlugin(), +]; +``` + +## API + +### createTogglePlugin + +### openNextToggles + +Marks the block at the current selection as an open toggle. +Use this function right before inserting a block so that the toggle is expanded upon insertion. + + + + The editor instance. + + + +### toggleIds + +Toggles the open state of toggles. + + + + The editor instance. + + + An array of element ids. + + + Set to true if you want to expand all toggles regardless of their current + state. Set to false if you want to collapse all toggles regardless of their + current state. + + + +## API Components + +### useToggleToolbarButtonState + + + + A boolean indicating whether the button is pressed or not. + + + +### useToggleToolbarButton + +A behavior hook for the toggle toolbar button. + + + + A boolean indicating whether the button is pressed or not. + + + + + + + + A boolean indicating whether the button is pressed or not. + + + A callback function to handle the click event of the button. It toggles + the toggle of the specified node type in the editor and focuses the + editor. + + + A callback function to handle the mouse down event of the button. It + just prevents default so that focus is not lost when clicking on the + button. + + + + + +### useToggleButtonState + + + + The id of the toggle element. + + + + + + The id of the toggle element. + + + A boolean indicating whether the toggle is open (expanded) or not + (collapsed). + + + +### useToggleButton + +A behavior hook for the toggle button that expands or collapses a toggle element. + + + + The id of the toggle element. + + + A boolean indicating whether the toggle is open (expanded) or not + (collapsed). + + + + + + The id of the toggle element. + + + A boolean indicating whether the toggle is open (expanded) or not + (collapsed). + + + + + A callback function to handle the click event of the button. It toggles + the open state of the toggle. + + + A callback function to handle the mouse down event of the button. It + just prevents default so that focus is not lost when clicking on the + button. + + + + diff --git a/apps/www/package.json b/apps/www/package.json index dc7798efd5..4326a6f80c 100644 --- a/apps/www/package.json +++ b/apps/www/package.json @@ -97,6 +97,7 @@ "@udecode/plate-tabbable": "workspace:^", "@udecode/plate-table": "workspace:^", "@udecode/plate-test-utils": "workspace:^", + "@udecode/plate-toggle": "workspace:^", "@udecode/plate-trailing-block": "workspace:^", "@udecode/plate-utils": "workspace:^", "@udecode/react-utils": "workspace:^", diff --git a/apps/www/public/registry/index.json b/apps/www/public/registry/index.json index c4f80f6ee2..51a185306b 100644 --- a/apps/www/public/registry/index.json +++ b/apps/www/public/registry/index.json @@ -817,6 +817,30 @@ ], "type": "components:plate-ui" }, + { + "name": "toggle-element", + "dependencies": [ + "@udecode/plate-toggle" + ], + "registryDependencies": [], + "files": [ + "plate-ui/toggle-element.tsx" + ], + "type": "components:plate-ui" + }, + { + "name": "toggle-toolbar-button", + "dependencies": [ + "@udecode/plate-toggle" + ], + "registryDependencies": [ + "toolbar" + ], + "files": [ + "plate-ui/toggle-toolbar-button.tsx" + ], + "type": "components:plate-ui" + }, { "name": "toolbar", "dependencies": [ diff --git a/apps/www/public/registry/styles/default/toggle-element.json b/apps/www/public/registry/styles/default/toggle-element.json new file mode 100644 index 0000000000..8b8bb72435 --- /dev/null +++ b/apps/www/public/registry/styles/default/toggle-element.json @@ -0,0 +1,14 @@ +{ + "name": "toggle-element", + "dependencies": [ + "@udecode/plate-toggle" + ], + "registryDependencies": [], + "files": [ + { + "name": "toggle-element.tsx", + "content": "import { withRef } from '@udecode/cn';\nimport { PlateElement, useElement } from '@udecode/plate-common';\nimport { useToggleButton, useToggleButtonState } from '@udecode/plate-toggle';\n\nimport { Icons } from '@/components/icons';\n\nexport const ToggleElement = withRef(\n ({ children, ...props }, ref) => {\n const element = useElement();\n const state = useToggleButtonState(element.id as string);\n const { open, buttonProps } = useToggleButton(state);\n\n return (\n \n
\n \n {open ? : }\n \n {children}\n
\n
\n );\n }\n);\n" + } + ], + "type": "components:plate-ui" +} \ No newline at end of file diff --git a/apps/www/public/registry/styles/default/toggle-toolbar-button.json b/apps/www/public/registry/styles/default/toggle-toolbar-button.json new file mode 100644 index 0000000000..1aa2802041 --- /dev/null +++ b/apps/www/public/registry/styles/default/toggle-toolbar-button.json @@ -0,0 +1,16 @@ +{ + "name": "toggle-toolbar-button", + "dependencies": [ + "@udecode/plate-toggle" + ], + "registryDependencies": [ + "toolbar" + ], + "files": [ + { + "name": "toggle-toolbar-button.tsx", + "content": "import React from 'react';\nimport { withRef } from '@udecode/cn';\nimport {\n useToggleToolbarButton,\n useToggleToolbarButtonState,\n} from '@udecode/plate-toggle';\n\nimport { Icons } from '@/components/icons';\n\nimport { ToolbarButton } from './toolbar';\n\nexport const ToggleToolbarButton = withRef(\n (rest, ref) => {\n const state = useToggleToolbarButtonState();\n const { props } = useToggleToolbarButton(state);\n\n return (\n \n \n \n );\n }\n);\n" + } + ], + "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 5a5b9d9086..28074e0074 100644 --- a/apps/www/src/__registry__/index.tsx +++ b/apps/www/src/__registry__/index.tsx @@ -641,6 +641,20 @@ export const Index: Record = { files: ['registry/default/plate-ui/todo-list-element.tsx'], component: React.lazy(() => import('@/registry/default/plate-ui/todo-list-element')), }, + 'toggle-element': { + name: 'toggle-element', + type: 'components:plate-ui', + registryDependencies: [], + files: ['registry/default/plate-ui/toggle-element.tsx'], + component: React.lazy(() => import('@/registry/default/plate-ui/toggle-element')), + }, + 'toggle-toolbar-button': { + name: 'toggle-toolbar-button', + type: 'components:plate-ui', + registryDependencies: ["toolbar"], + files: ['registry/default/plate-ui/toggle-toolbar-button.tsx'], + component: React.lazy(() => import('@/registry/default/plate-ui/toggle-toolbar-button')), + }, 'toolbar': { name: 'toolbar', type: 'components:plate-ui', diff --git a/apps/www/src/components/plate-ui/playground-fixed-toolbar-buttons.tsx b/apps/www/src/components/plate-ui/playground-fixed-toolbar-buttons.tsx index dc6e17cfb1..8cfd537dbb 100644 --- a/apps/www/src/components/plate-ui/playground-fixed-toolbar-buttons.tsx +++ b/apps/www/src/components/plate-ui/playground-fixed-toolbar-buttons.tsx @@ -29,6 +29,7 @@ import { MarkToolbarButton } from '@/registry/default/plate-ui/mark-toolbar-butt import { MediaToolbarButton } from '@/registry/default/plate-ui/media-toolbar-button'; import { OutdentToolbarButton } from '@/registry/default/plate-ui/outdent-toolbar-button'; import { TableDropdownMenu } from '@/registry/default/plate-ui/table-dropdown-menu'; +import { ToggleToolbarButton } from '@/registry/default/plate-ui/toggle-toolbar-button'; import { ToolbarGroup } from '@/registry/default/plate-ui/toolbar'; import { PlaygroundInsertDropdownMenu } from './playground-insert-dropdown-menu'; @@ -137,6 +138,8 @@ export function PlaygroundFixedToolbarButtons({ id }: { id?: ValueId }) { {isEnabled('link', id) && } + {isEnabled('toggle', id) && } + {isEnabled('media', id) && ( )} diff --git a/apps/www/src/components/settings-combobox.tsx b/apps/www/src/components/settings-combobox.tsx index 9a3397ef13..6c369dbe9c 100644 --- a/apps/www/src/components/settings-combobox.tsx +++ b/apps/www/src/components/settings-combobox.tsx @@ -65,6 +65,7 @@ const categories = [ customizerPlugins.tabbable, customizerPlugins.table, customizerPlugins.todoli, + customizerPlugins.toggle, customizerPlugins.trailingblock, ], }, diff --git a/apps/www/src/config/customizer-components.ts b/apps/www/src/config/customizer-components.ts index 6c5bdc6c78..650bc961aa 100644 --- a/apps/www/src/config/customizer-components.ts +++ b/apps/www/src/config/customizer-components.ts @@ -240,6 +240,14 @@ export const customizerComponents = { href: '/docs/components/todo-list-element', label: 'Element', }, + toggleElement: { + title: 'Toggle', + href: '/docs/components/toggle', + }, + toggleToolbarButton: { + title: 'Toggle Toolbar Button', + href: '/docs/components/toggle-toolbar-button', + }, toolbar: { title: 'Toolbar', href: '/docs/components/toolbar' }, tooltip: { title: 'Tooltip', href: '/docs/components/tooltip' }, turnIntoDropdownMenu: { diff --git a/apps/www/src/config/customizer-items.ts b/apps/www/src/config/customizer-items.ts index 2794de203d..ce10b3ff19 100644 --- a/apps/www/src/config/customizer-items.ts +++ b/apps/www/src/config/customizer-items.ts @@ -46,6 +46,7 @@ import { KEY_DESERIALIZE_DOCX } from '@udecode/plate-serializer-docx'; import { KEY_DESERIALIZE_MD } from '@udecode/plate-serializer-md'; import { KEY_TABBABLE } from '@udecode/plate-tabbable'; import { ELEMENT_TABLE } from '@udecode/plate-table'; +import { ELEMENT_TOGGLE } from '@udecode/plate-toggle'; import { KEY_TRAILING_BLOCK } from '@udecode/plate-trailing-block'; import { CustomizerBadge, customizerBadges } from '@/config/customizer-badges'; @@ -255,6 +256,23 @@ export const customizerItems: Record = { }, ], }, + [ELEMENT_TOGGLE]: { + id: ELEMENT_TOGGLE, + npmPackage: '@udecode/plate-toggle', + pluginFactory: 'createTogglePlugin', + label: 'Toggle', + badges: [customizerBadges.element], + route: customizerPlugins.toggle.route, + components: [ + { + id: 'toggle-element', + label: 'ToggleElement', + pluginKey: 'ELEMENT_TOGGLE', + usage: 'ToggleElement', + route: customizerComponents.toggleElement.href, + }, + ], + }, heading: { id: 'heading', npmPackage: '@udecode/plate-heading', diff --git a/apps/www/src/config/customizer-list.ts b/apps/www/src/config/customizer-list.ts index d046aa42d4..36202b0d31 100644 --- a/apps/www/src/config/customizer-list.ts +++ b/apps/www/src/config/customizer-list.ts @@ -46,6 +46,7 @@ import { KEY_DESERIALIZE_DOCX } from '@udecode/plate-serializer-docx'; import { KEY_DESERIALIZE_MD } from '@udecode/plate-serializer-md'; import { KEY_TABBABLE } from '@udecode/plate-tabbable'; import { ELEMENT_TABLE } from '@udecode/plate-table'; +import { ELEMENT_TOGGLE } from '@udecode/plate-toggle'; import { KEY_TRAILING_BLOCK } from '@udecode/plate-trailing-block'; import { uniqBy } from 'lodash'; @@ -62,6 +63,7 @@ export const customizerList = [ customizerItems[ELEMENT_HR], customizerItems[ELEMENT_IMAGE], customizerItems[ELEMENT_LINK], + customizerItems[ELEMENT_TOGGLE], customizerItems.heading, customizerItems.list, customizerItems[ELEMENT_MEDIA_EMBED], diff --git a/apps/www/src/config/customizer-plugins.ts b/apps/www/src/config/customizer-plugins.ts index c3743ee09b..51d1ba77ca 100644 --- a/apps/www/src/config/customizer-plugins.ts +++ b/apps/www/src/config/customizer-plugins.ts @@ -31,6 +31,7 @@ import { singleLineValue } from '@/plate/demo/values/singleLineValue'; import { softBreakValue } from '@/plate/demo/values/softBreakValue'; import { tabbableValue } from '@/plate/demo/values/tabbableValue'; import { tableValue } from '@/plate/demo/values/tableValue'; +import { toggleValue } from '@/plate/demo/values/toggleValue'; import { KEY_ALIGN } from '@udecode/plate-alignment'; import { KEY_AUTOFORMAT } from '@udecode/plate-autoformat'; import { @@ -64,6 +65,7 @@ import { KEY_DESERIALIZE_DOCX } from '@udecode/plate-serializer-docx'; import { KEY_DESERIALIZE_MD } from '@udecode/plate-serializer-md'; import { KEY_TABBABLE } from '@udecode/plate-tabbable'; import { ELEMENT_TABLE } from '@udecode/plate-table'; +import { ELEMENT_TOGGLE } from '@udecode/plate-toggle'; import { KEY_TRAILING_BLOCK } from '@udecode/plate-trailing-block'; export type ValueId = keyof typeof customizerPlugins | 'tableMerge'; @@ -331,6 +333,13 @@ export const customizerPlugins = { route: '/docs/todo-list', plugins: [ELEMENT_TODO_LI], }, + toggle: { + id: 'toggle', + label: 'Toggle', + value: toggleValue, + route: '/docs/toggle', + plugins: [ELEMENT_TOGGLE], + }, trailingblock: { id: 'trailingblock', label: 'Trailing Block', diff --git a/apps/www/src/config/descriptions.ts b/apps/www/src/config/descriptions.ts index 44fc4ea073..2307290af0 100644 --- a/apps/www/src/config/descriptions.ts +++ b/apps/www/src/config/descriptions.ts @@ -44,6 +44,7 @@ import { KEY_DESERIALIZE_DOCX } from '@udecode/plate-serializer-docx'; import { KEY_DESERIALIZE_MD } from '@udecode/plate-serializer-md'; import { KEY_TABBABLE } from '@udecode/plate-tabbable'; import { ELEMENT_TABLE } from '@udecode/plate-table'; +import { ELEMENT_TOGGLE } from '@udecode/plate-toggle'; import { KEY_TRAILING_BLOCK } from '@udecode/plate-trailing-block'; export const descriptions: Record = { @@ -54,6 +55,7 @@ export const descriptions: Record = { [ELEMENT_HR]: 'Insert horizontal lines.', [ELEMENT_IMAGE]: 'Embed images into your document.', [ELEMENT_LINK]: 'Insert and manage hyperlinks.', + [ELEMENT_TOGGLE]: 'Add toggles to your document.', heading: 'Organize your document with up to 6 headings.', list: 'Organize nestable items in a bulleted or numbered list.', [ELEMENT_MEDIA_EMBED]: diff --git a/apps/www/src/config/doc-to-package.ts b/apps/www/src/config/doc-to-package.ts index 9cd3e9e2fc..054b65823f 100644 --- a/apps/www/src/config/doc-to-package.ts +++ b/apps/www/src/config/doc-to-package.ts @@ -35,6 +35,7 @@ const plateOverrides = { 'soft-break': 'plate-break', tabbable: 'plate-tabbable', table: 'plate-table', + toggle: 'plate-toggle', }; export const docToPackage = (name?: string) => { diff --git a/apps/www/src/config/docs.ts b/apps/www/src/config/docs.ts index 85763eb9cc..9a54b85851 100644 --- a/apps/www/src/config/docs.ts +++ b/apps/www/src/config/docs.ts @@ -228,6 +228,11 @@ export const docsConfig: DocsConfig = { href: '/docs/table', label: 'Element', }, + { + title: 'Toggle', + href: '/docs/toggle', + label: 'Element', + }, ], }, { @@ -773,6 +778,8 @@ export const docsConfig: DocsConfig = { customizerComponents.tableElement, customizerComponents.tableRowElement, customizerComponents.todoListElement, + customizerComponents.toggleElement, + customizerComponents.toggleToolbarButton, customizerComponents.toolbar, customizerComponents.tooltip, customizerComponents.turnIntoDropdownMenu, diff --git a/apps/www/src/lib/plate/create-plate-ui.ts b/apps/www/src/lib/plate/create-plate-ui.ts index 4c5c72552b..400c1b1a55 100644 --- a/apps/www/src/lib/plate/create-plate-ui.ts +++ b/apps/www/src/lib/plate/create-plate-ui.ts @@ -49,6 +49,7 @@ import { ELEMENT_TH, ELEMENT_TR, } from '@udecode/plate-table'; +import { ELEMENT_TOGGLE } from '@udecode/plate-toggle'; import { BlockquoteElement } from '@/registry/default/plate-ui/blockquote-element'; import { CodeBlockElement } from '@/registry/default/plate-ui/code-block-element'; @@ -77,6 +78,7 @@ import { import { TableElement } from '@/registry/default/plate-ui/table-element'; import { TableRowElement } from '@/registry/default/plate-ui/table-row-element'; 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'; export const createPlateUI = ( @@ -111,6 +113,7 @@ export const createPlateUI = ( [ELEMENT_TD]: TableCellElement, [ELEMENT_TH]: TableCellHeaderElement, [ELEMENT_TODO_LI]: TodoListElement, + [ELEMENT_TOGGLE]: ToggleElement, [ELEMENT_TR]: TableRowElement, [ELEMENT_EXCALIDRAW]: ExcalidrawElement, [MARK_BOLD]: withProps(PlateLeaf, { as: 'strong' }), diff --git a/apps/www/src/lib/plate/demo/plugins/autoformatBlocks.ts b/apps/www/src/lib/plate/demo/plugins/autoformatBlocks.ts index 1b5da1dcf8..da88e0f0e5 100644 --- a/apps/www/src/lib/plate/demo/plugins/autoformatBlocks.ts +++ b/apps/www/src/lib/plate/demo/plugins/autoformatBlocks.ts @@ -14,6 +14,7 @@ import { ELEMENT_H6, } from '@udecode/plate-heading'; import { ELEMENT_HR } from '@udecode/plate-horizontal-rule'; +import { ELEMENT_TOGGLE, openNextToggles } from '@udecode/plate-toggle'; import { preFormat } from './autoformatUtils'; @@ -73,6 +74,12 @@ export const autoformatBlocks: AutoformatRule[] = [ }); }, }, + { + mode: 'block', + type: ELEMENT_TOGGLE, + match: '+ ', + preFormat: openNextToggles, + }, { mode: 'block', type: ELEMENT_HR, diff --git a/apps/www/src/lib/plate/demo/plugins/indentPlugin.ts b/apps/www/src/lib/plate/demo/plugins/indentPlugin.ts index 73aef4467d..3e49aab9bd 100644 --- a/apps/www/src/lib/plate/demo/plugins/indentPlugin.ts +++ b/apps/www/src/lib/plate/demo/plugins/indentPlugin.ts @@ -9,6 +9,7 @@ import { ELEMENT_H6, } from '@udecode/plate-heading'; import { ELEMENT_PARAGRAPH } from '@udecode/plate-paragraph'; +import { ELEMENT_TOGGLE } from '@udecode/plate-toggle'; export const indentPlugin = { inject: { @@ -23,6 +24,7 @@ export const indentPlugin = { ELEMENT_H6, ELEMENT_BLOCKQUOTE, ELEMENT_CODE_BLOCK, + ELEMENT_TOGGLE, ], }, }, diff --git a/apps/www/src/lib/plate/demo/values/toggleValue.tsx b/apps/www/src/lib/plate/demo/values/toggleValue.tsx new file mode 100644 index 0000000000..17e73b5380 --- /dev/null +++ b/apps/www/src/lib/plate/demo/values/toggleValue.tsx @@ -0,0 +1,19 @@ +/** @jsxRuntime classic */ +/** @jsx jsx */ +import { jsx } from '@udecode/plate-test-utils'; + +jsx; + +export const toggleValue: any = ( + + Toggle + Create toggles with multiple levels of indentation + Level 1 toggle + Inside level 1 toggle + + Level 2 toggle + + Inside level 2 toggle + After toggles + +); diff --git a/apps/www/src/lib/plate/demo/values/usePlaygroundValue.ts b/apps/www/src/lib/plate/demo/values/usePlaygroundValue.ts index 5772506b82..486df4d1ec 100644 --- a/apps/www/src/lib/plate/demo/values/usePlaygroundValue.ts +++ b/apps/www/src/lib/plate/demo/values/usePlaygroundValue.ts @@ -32,6 +32,7 @@ import { mentionValue } from './mentionValue'; import { softBreakValue } from './softBreakValue'; import { tabbableValue } from './tabbableValue'; import { tableMergeValue, tableValue } from './tableValue'; +import { toggleValue } from './toggleValue'; export const usePlaygroundValue = (id?: ValueId) => { let valueId = settingsStore.use.valueId(); @@ -75,6 +76,7 @@ export const usePlaygroundValue = (id?: ValueId) => { if (enabled.list) value.push(...listValue); if (enabled.img || enabled.media_embed) value.push(...mediaValue); if (enabled.table) value.push(...tableValue); + if (enabled.toggle) value.push(...toggleValue); // Functionalities if (enabled.autoformat) value.push(...autoformatValue); @@ -125,6 +127,7 @@ export const usePlaygroundValue = (id?: ValueId) => { enabled.softBreak, enabled.tabbable, enabled.table, + enabled.toggle, enabled.trailingBlock, valueId, ]); diff --git a/apps/www/src/registry/default/example/cards/cards-toolbar.tsx b/apps/www/src/registry/default/example/cards/cards-toolbar.tsx index 9452f3102e..3989eb09ca 100644 --- a/apps/www/src/registry/default/example/cards/cards-toolbar.tsx +++ b/apps/www/src/registry/default/example/cards/cards-toolbar.tsx @@ -28,6 +28,7 @@ import { ModeDropdownMenu } from '@/registry/default/plate-ui/mode-dropdown-menu import { MoreDropdownMenu } from '@/registry/default/plate-ui/more-dropdown-menu'; import { OutdentToolbarButton } from '@/registry/default/plate-ui/outdent-toolbar-button'; import { TableDropdownMenu } from '@/registry/default/plate-ui/table-dropdown-menu'; +import { ToggleToolbarButton } from '@/registry/default/plate-ui/toggle-toolbar-button'; import { ToolbarGroup } from '@/registry/default/plate-ui/toolbar'; import { TurnIntoDropdownMenu } from '@/registry/default/plate-ui/turn-into-dropdown-menu'; @@ -98,6 +99,8 @@ export function CardsToolbar() { + + diff --git a/apps/www/src/registry/default/example/playground-demo.tsx b/apps/www/src/registry/default/example/playground-demo.tsx index 2618da37fa..7456fb96bb 100644 --- a/apps/www/src/registry/default/example/playground-demo.tsx +++ b/apps/www/src/registry/default/example/playground-demo.tsx @@ -87,6 +87,7 @@ import { createDeserializeDocxPlugin } from '@udecode/plate-serializer-docx'; import { createDeserializeMdPlugin } from '@udecode/plate-serializer-md'; import { createTabbablePlugin } from '@udecode/plate-tabbable'; import { createTablePlugin } from '@udecode/plate-table'; +import { createTogglePlugin } from '@udecode/plate-toggle'; import { createTrailingBlockPlugin } from '@udecode/plate-trailing-block'; import { DndProvider } from 'react-dnd'; import { HTML5Backend } from 'react-dnd-html5-backend'; @@ -157,6 +158,7 @@ export const usePlaygroundPlugins = ({ }, }), createTodoListPlugin({ enabled: !!enabled.action_item }), + createTogglePlugin({ enabled: !!enabled.toggle }), createExcalidrawPlugin({ enabled: !!enabled.excalidraw }), // Marks diff --git a/apps/www/src/registry/default/plate-ui/toggle-element.tsx b/apps/www/src/registry/default/plate-ui/toggle-element.tsx new file mode 100644 index 0000000000..bca433746f --- /dev/null +++ b/apps/www/src/registry/default/plate-ui/toggle-element.tsx @@ -0,0 +1,28 @@ +import { withRef } from '@udecode/cn'; +import { PlateElement, useElement } from '@udecode/plate-common'; +import { useToggleButton, useToggleButtonState } from '@udecode/plate-toggle'; + +import { Icons } from '@/components/icons'; + +export const ToggleElement = withRef( + ({ children, ...props }, ref) => { + const element = useElement(); + const state = useToggleButtonState(element.id as string); + const { open, buttonProps } = useToggleButton(state); + + return ( + +
+ + {open ? : } + + {children} +
+
+ ); + } +); diff --git a/apps/www/src/registry/default/plate-ui/toggle-toolbar-button.tsx b/apps/www/src/registry/default/plate-ui/toggle-toolbar-button.tsx new file mode 100644 index 0000000000..1403e97a6e --- /dev/null +++ b/apps/www/src/registry/default/plate-ui/toggle-toolbar-button.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { withRef } from '@udecode/cn'; +import { + useToggleToolbarButton, + useToggleToolbarButtonState, +} from '@udecode/plate-toggle'; + +import { Icons } from '@/components/icons'; + +import { ToolbarButton } from './toolbar'; + +export const ToggleToolbarButton = withRef( + (rest, ref) => { + const state = useToggleToolbarButtonState(); + const { props } = useToggleToolbarButton(state); + + return ( + + + + ); + } +); diff --git a/apps/www/src/registry/registry.ts b/apps/www/src/registry/registry.ts index 23afc93bcc..8464b42ba5 100644 --- a/apps/www/src/registry/registry.ts +++ b/apps/www/src/registry/registry.ts @@ -521,6 +521,20 @@ const ui: Registry = [ registryDependencies: ['checkbox'], files: ['plate-ui/todo-list-element.tsx'], }, + { + name: 'toggle-element', + type: 'components:plate-ui', + dependencies: ['@udecode/plate-toggle'], + registryDependencies: [], + files: ['plate-ui/toggle-element.tsx'], + }, + { + name: 'toggle-toolbar-button', + type: 'components:plate-ui', + dependencies: ['@udecode/plate-toggle'], + registryDependencies: ['toolbar'], + files: ['plate-ui/toggle-toolbar-button.tsx'], + }, { name: 'toolbar', type: 'components:plate-ui', diff --git a/apps/www/src/types/plate-types.ts b/apps/www/src/types/plate-types.ts index ae0fca7578..18c1a5c155 100644 --- a/apps/www/src/types/plate-types.ts +++ b/apps/www/src/types/plate-types.ts @@ -87,6 +87,7 @@ import { ELEMENT_TR, TTableElement, } from '@udecode/plate-table'; +import { ELEMENT_TOGGLE, TToggleElement } from '@udecode/plate-toggle'; import { TText } from '@udecode/slate'; /** @@ -262,6 +263,11 @@ export interface MyTodoListElement children: MyInlineChildren; } +export interface MyToggleElement extends TToggleElement, MyBlockElement { + type: typeof ELEMENT_TOGGLE; + children: MyInlineChildren; +} + export interface MyImageElement extends TImageElement, MyBlockElement { type: typeof ELEMENT_IMAGE; children: [EmptyText]; @@ -305,6 +311,7 @@ export type MyRootBlock = | MyBulletedListElement | MyNumberedListElement | MyTodoListElement + | MyToggleElement | MyImageElement | MyMediaEmbedElement | MyHrElement diff --git a/config/aliases.js b/config/aliases.js index b66344b9b0..1e6e5bbfbe 100644 --- a/config/aliases.js +++ b/config/aliases.js @@ -46,6 +46,7 @@ module.exports = { '@udecode/plate-tabbable': 'tabbable', '@udecode/plate-table': 'table', '@udecode/plate-test-utils': 'test-utils', + '@udecode/plate-toggle': 'toggle', '@udecode/plate-trailing-block': 'trailing-block', '@udecode/plate-utils': 'plate-utils', '@udecode/react-utils': 'react-utils', diff --git a/packages/autoformat/src/__tests__/withAutoformat/block/list.spec.tsx b/packages/autoformat/src/__tests__/withAutoformat/block/list.spec.tsx index 3820ba11a0..c30bf8d560 100644 --- a/packages/autoformat/src/__tests__/withAutoformat/block/list.spec.tsx +++ b/packages/autoformat/src/__tests__/withAutoformat/block/list.spec.tsx @@ -5,6 +5,7 @@ import { jsx } from '@udecode/plate-test-utils'; import { withReact } from 'slate-react'; import { autoformatPlugin } from 'www/src/lib/plate/demo/plugins/autoformatPlugin'; +import { AutoformatBlockRule } from '../../../types'; import { withAutoformat } from '../../../withAutoformat'; jsx; @@ -132,3 +133,50 @@ describe('when [x].space', () => { expect(input.children).toEqual(output.children); }); }); + +describe('when +space', () => { + it('should format to a toggle', () => { + const input = ( + + + + + + hello + + + ) as any; + + const output = ( + + hello + + ) as any; + + // See useHooksToggle.ts, we overload the plugin with a `setOpenIds` function until there's a JOTAI layer in plate-core, + // so here we need to remove the `preformat` property of the autoformat rule that uses this overload. + + const autoformatPluginRulesWitoutTogglePreformat = + autoformatPlugin.options!.rules!.map((rule) => { + const { preFormat, ...rest } = rule as AutoformatBlockRule; + if (rule.match === '+ ') return rest; + return rule; + }); + + const autoformatPluginWitoutTogglePreformat: typeof autoformatPlugin = { + ...autoformatPlugin, + options: { + ...autoformatPlugin.options, + rules: autoformatPluginRulesWitoutTogglePreformat as any, + }, + }; + + const editor = withAutoformat( + withReact(input), + mockPlugin(autoformatPluginWitoutTogglePreformat) + ); + + editor.insertText(' '); + + expect(input.children).toEqual(output.children); + }); +}); diff --git a/packages/plate/package.json b/packages/plate/package.json index 0d800cbbbc..b9a459ccf2 100644 --- a/packages/plate/package.json +++ b/packages/plate/package.json @@ -79,6 +79,7 @@ "@udecode/plate-suggestion": "30.1.2", "@udecode/plate-tabbable": "30.1.2", "@udecode/plate-table": "30.1.2", + "@udecode/plate-toggle": "30.1.2", "@udecode/plate-trailing-block": "30.1.2" }, "peerDependencies": { diff --git a/packages/plate/src/index.tsx b/packages/plate/src/index.tsx index d890cf146e..cfb258aa07 100644 --- a/packages/plate/src/index.tsx +++ b/packages/plate/src/index.tsx @@ -38,5 +38,6 @@ export * from '@udecode/plate-serializer-md'; export * from '@udecode/plate-suggestion'; export * from '@udecode/plate-tabbable'; export * from '@udecode/plate-table'; +export * from '@udecode/plate-toggle'; export * from '@udecode/plate-trailing-block'; export * from '@udecode/plate-resizable'; diff --git a/packages/test-utils/src/jsx.ts b/packages/test-utils/src/jsx.ts index df44d3adc6..6bd79ebb2b 100644 --- a/packages/test-utils/src/jsx.ts +++ b/packages/test-utils/src/jsx.ts @@ -59,6 +59,7 @@ const ELEMENT_TABLE = 'table'; const ELEMENT_TD = 'td'; const ELEMENT_TH = 'th'; const ELEMENT_TODO_LI = 'action_item'; +const ELEMENT_TOGGLE = 'toggle'; const ELEMENT_TR = 'tr'; const ELEMENT_UL = 'ul'; @@ -88,6 +89,7 @@ const elements: HyperscriptShorthands = { htd: { type: ELEMENT_TD }, hth: { type: ELEMENT_TH }, htodoli: { type: ELEMENT_TODO_LI }, + htoggle: { type: ELEMENT_TOGGLE }, htr: { type: ELEMENT_TR }, hul: { type: ELEMENT_UL }, }; diff --git a/packages/toggle/.npmignore b/packages/toggle/.npmignore new file mode 100644 index 0000000000..7d3b305b17 --- /dev/null +++ b/packages/toggle/.npmignore @@ -0,0 +1,3 @@ +__tests__ +__test-utils__ +__mocks__ diff --git a/packages/toggle/CHANGELOG.md b/packages/toggle/CHANGELOG.md new file mode 100644 index 0000000000..e4b0e44359 --- /dev/null +++ b/packages/toggle/CHANGELOG.md @@ -0,0 +1,5 @@ +# @udecode/plate-toggle + +## 30.1.2 + +TODO release \ No newline at end of file diff --git a/packages/toggle/README.md b/packages/toggle/README.md new file mode 100644 index 0000000000..b7208ebeaf --- /dev/null +++ b/packages/toggle/README.md @@ -0,0 +1,22 @@ +# Plate toggle plugin + +This package implements the toggle plugin for Plate. +It's similar to the indent list plugin, in that it relies on the indent of siblings. + +## Documentation + +Check out [Toggle](https://platejs.org/docs/toggle). + +## Ideas to improve this plugin + +1. Adding an option `initialValue` of open `toggleIds` and a callback `onChange`, for instance to store the state of open toggles in local storage and remember the state upon browser refresh. +2. Adding an option `defaultOpen`. Currently, toggles are closed on initial rendering. +3. Adding an option to specify how to get the indent value of elements, right now we are relying on this being the default `KEY_ELEMENT` from the `indent` plugin +4. An option to specify how to get the id of elements, right now we are using the default id attribute from the `node-id` plugin. +5. Adding a placeholder below the toggle, like Notion does, when the toggle is expanded without any elements below. +6. Make toggle button more accessible +7. When indenting an element right of a closed toggle, it becomes hidden. This makes sense, but a nicer UI would be to open the toggle in that case, like Notion does. + +## License + +[MIT](../../LICENSE) diff --git a/packages/toggle/package.json b/packages/toggle/package.json new file mode 100644 index 0000000000..210e90ade9 --- /dev/null +++ b/packages/toggle/package.json @@ -0,0 +1,63 @@ +{ + "name": "@udecode/plate-toggle", + "version": "30.1.2", + "description": "Toggle plugin for Plate", + "license": "MIT", + "homepage": "https://platejs.org", + "repository": { + "type": "git", + "url": "https://github.com/udecode/plate.git", + "directory": "packages/toggle" + }, + "bugs": { + "url": "https://github.com/udecode/plate/issues" + }, + "sideEffects": false, + "main": "dist/index.js", + "module": "dist/index.mjs", + "types": "dist/index.d.ts", + "files": [ + "dist/**/*" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "module": "./dist/index.mjs", + "require": "./dist/index.js" + } + }, + "scripts": { + "build": "yarn p:build", + "build:watch": "yarn p:build:watch", + "brl": "yarn p:brl", + "clean": "yarn p:clean", + "lint": "yarn p:lint", + "lint:fix": "yarn p:lint:fix", + "test": "yarn p:test", + "test:watch": "yarn p:test:watch", + "typecheck": "yarn p:typecheck" + }, + "dependencies": { + "@udecode/plate-common": "30.1.2", + "@udecode/plate-indent": "30.1.2", + "@udecode/plate-node-id": "30.1.2", + "lodash": "^4.17.21" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "slate": ">=0.94.0", + "slate-history": ">=0.93.0", + "slate-hyperscript": ">=0.66.0", + "slate-react": ">=0.99.0" + }, + "keywords": [ + "plate", + "plugin", + "slate" + ], + "publishConfig": { + "access": "public" + } +} diff --git a/packages/toggle/src/createTogglePlugin.ts b/packages/toggle/src/createTogglePlugin.ts new file mode 100644 index 0000000000..9fc2a10cb4 --- /dev/null +++ b/packages/toggle/src/createTogglePlugin.ts @@ -0,0 +1,16 @@ +import { createPluginFactory } from '@udecode/plate-common'; + +import { useHooksToggle } from './hooks/useHooksToggle'; +import { injectToggle } from './injectToggle'; +import { ToggleControllerProvider } from './toggle-controller-store'; +import { ELEMENT_TOGGLE, TogglePlugin } from './types'; +import { withToggle } from './withToggle'; + +export const createTogglePlugin = createPluginFactory({ + key: ELEMENT_TOGGLE, + isElement: true, + inject: { aboveComponent: injectToggle }, + useHooks: useHooksToggle, + renderAboveEditable: ToggleControllerProvider, + withOverrides: withToggle, +}); diff --git a/packages/toggle/src/hooks/index.ts b/packages/toggle/src/hooks/index.ts new file mode 100644 index 0000000000..e91ef60524 --- /dev/null +++ b/packages/toggle/src/hooks/index.ts @@ -0,0 +1,7 @@ +/** + * @file Automatically generated by barrelsby. + */ + +export * from './useToggleButton'; +export * from './useToggleToolbarButton'; +export * from './useHooksToggle'; diff --git a/packages/toggle/src/hooks/useHooksToggle.ts b/packages/toggle/src/hooks/useHooksToggle.ts new file mode 100644 index 0000000000..67ac87fa7f --- /dev/null +++ b/packages/toggle/src/hooks/useHooksToggle.ts @@ -0,0 +1,30 @@ +import { useEffect } from 'react'; +import { getPluginOptions, PlateEditor, Value } from '@udecode/plate-common'; + +import { + useToggleControllerStore, + useToggleIndex, +} from '../toggle-controller-store'; +import { ELEMENT_TOGGLE, TogglePlugin } from '../types'; + +export const useHooksToggle = < + V extends Value = Value, + E extends PlateEditor = PlateEditor, +>( + editor: E +) => { + const [openIds, setOpenIds] = useToggleControllerStore().use.openIds(); + const toggleIndex = useToggleIndex(); + + // This is hacky + // TODO a JOTAI layer in plate-core instead of relying on plugin options + useEffect(() => { + const options = getPluginOptions( + editor, + ELEMENT_TOGGLE + ); + options.openIds = openIds; + options.setOpenIds = setOpenIds; + options.toggleIndex = toggleIndex; + }, [editor, openIds, setOpenIds, toggleIndex]); +}; diff --git a/packages/toggle/src/hooks/useToggleButton.ts b/packages/toggle/src/hooks/useToggleButton.ts new file mode 100644 index 0000000000..af7b04ef93 --- /dev/null +++ b/packages/toggle/src/hooks/useToggleButton.ts @@ -0,0 +1,32 @@ +import { useEditorRef } from '@udecode/plate-common'; + +import { + toggleIds, + useToggleControllerStore, +} from '../toggle-controller-store'; + +export const useToggleButtonState = (toggleId: string) => { + const [openIds] = useToggleControllerStore().use.openIds(); + return { + toggleId, + open: openIds.has(toggleId), + }; +}; + +export const useToggleButton = ( + state: ReturnType +) => { + const editor = useEditorRef(); + return { + ...state, + buttonProps: { + onMouseDown: (e: React.MouseEvent) => { + e.preventDefault(); + }, + onClick: (e: React.MouseEvent) => { + e.preventDefault(); + toggleIds(editor, [state.toggleId]); + }, + }, + }; +}; diff --git a/packages/toggle/src/hooks/useToggleToolbarButton.ts b/packages/toggle/src/hooks/useToggleToolbarButton.ts new file mode 100644 index 0000000000..d724b24b46 --- /dev/null +++ b/packages/toggle/src/hooks/useToggleToolbarButton.ts @@ -0,0 +1,42 @@ +import { + collapseSelection, + focusEditor, + PlateEditor, + toggleNodeType, + useEditorRef, + useEditorSelector, + Value, +} from '@udecode/plate-common'; + +import { someToggle } from '../queries/someToggle'; +import { openNextToggles } from '../transforms'; +import { ELEMENT_TOGGLE } from '../types'; + +export const useToggleToolbarButtonState = () => { + const pressed = useEditorSelector((editor) => someToggle(editor), []); + + return { + pressed, + }; +}; + +export const useToggleToolbarButton = ({ + pressed, +}: ReturnType) => { + const editor = useEditorRef>(); + + return { + props: { + pressed, + onMouseDown: (e: React.MouseEvent) => { + e.preventDefault(); + }, + onClick: () => { + openNextToggles(editor); + toggleNodeType(editor, { activeType: ELEMENT_TOGGLE }); + collapseSelection(editor); + focusEditor(editor); + }, + }, + }; +}; diff --git a/packages/toggle/src/index.ts b/packages/toggle/src/index.ts new file mode 100644 index 0000000000..0ee44da450 --- /dev/null +++ b/packages/toggle/src/index.ts @@ -0,0 +1,11 @@ +/** + * @file Automatically generated by barrelsby. + */ + +export * from './createTogglePlugin'; +export * from './types'; +export * from './withToggle'; +export * from './hooks/index'; +export * from './queries/index'; +export * from './transforms/index'; +export * from './toggle-controller-store'; diff --git a/packages/toggle/src/injectToggle.tsx b/packages/toggle/src/injectToggle.tsx new file mode 100644 index 0000000000..5eebf2d73f --- /dev/null +++ b/packages/toggle/src/injectToggle.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { InjectComponentReturnType } from '@udecode/plate-common'; + +import { useIsVisible } from './toggle-controller-store'; + +export const injectToggle = (): InjectComponentReturnType => WithToggle; + +const WithToggle: InjectComponentReturnType = ({ element, children }) => { + const isVisible = useIsVisible(element.id as string); + + if (isVisible) return children; + + return
{children}
; +}; + +const hiddenStyle: React.CSSProperties = { + visibility: 'hidden', + height: 0, + margin: 0, +}; diff --git a/packages/toggle/src/queries/findElementIdsHiddenInToggle.ts b/packages/toggle/src/queries/findElementIdsHiddenInToggle.ts new file mode 100644 index 0000000000..cb855b3a48 --- /dev/null +++ b/packages/toggle/src/queries/findElementIdsHiddenInToggle.ts @@ -0,0 +1,18 @@ +import { TIndentElement } from '@udecode/plate-indent'; + +import { buildToggleIndex } from '../toggle-controller-store'; + +export const findElementIdsHiddenInToggle = ( + openToggleIds: Set, + elements: TIndentElement[] +): string[] => { + const toggleIndex = buildToggleIndex(elements); + return elements + .filter((element) => { + const enclosingToggleIds = toggleIndex.get(element.id as string) || []; + return enclosingToggleIds.some( + (toggleId) => !openToggleIds.has(toggleId) + ); + }) + .map((element) => element.id as string); +}; diff --git a/packages/toggle/src/queries/getEnclosingToggleIds.ts b/packages/toggle/src/queries/getEnclosingToggleIds.ts new file mode 100644 index 0000000000..961dbdc787 --- /dev/null +++ b/packages/toggle/src/queries/getEnclosingToggleIds.ts @@ -0,0 +1,11 @@ +import { getPluginOptions, PlateEditor, Value } from '@udecode/plate-common'; + +import { ELEMENT_TOGGLE, TogglePlugin } from '../types'; + +export function getEnclosingToggleIds< + V extends Value = Value, + E extends PlateEditor = PlateEditor, +>(editor: E, elementId: string): string[] { + const options = getPluginOptions(editor, ELEMENT_TOGGLE); + return options.toggleIndex?.get(elementId) || []; +} diff --git a/packages/toggle/src/queries/getLastEntryEnclosedInToggle.ts b/packages/toggle/src/queries/getLastEntryEnclosedInToggle.ts new file mode 100644 index 0000000000..373262da2b --- /dev/null +++ b/packages/toggle/src/queries/getLastEntryEnclosedInToggle.ts @@ -0,0 +1,20 @@ +import { PlateEditor, TNodeEntry, Value } from '@udecode/plate-common'; +import last from 'lodash/last'; + +import { buildToggleIndex } from '../toggle-controller-store'; + +export const getLastEntryEnclosedInToggle = < + V extends Value = Value, + E extends PlateEditor = PlateEditor, +>( + editor: E, + toggleId: string +): TNodeEntry | undefined => { + const toggleIndex = buildToggleIndex(editor.children); + const entriesInToggle = editor.children + .map((node, index) => [node, [index]] as TNodeEntry) + .filter(([node]) => { + return (toggleIndex.get(node.id) || []).includes(toggleId); + }); + return last(entriesInToggle); +}; diff --git a/packages/toggle/src/queries/index.ts b/packages/toggle/src/queries/index.ts new file mode 100644 index 0000000000..fa245ffc4f --- /dev/null +++ b/packages/toggle/src/queries/index.ts @@ -0,0 +1,9 @@ +/** + * @file Automatically generated by barrelsby. + */ + +export * from './findElementIdsHiddenInToggle'; +export * from './getEnclosingToggleIds'; +export * from './getLastEntryEnclosedInToggle'; +export * from './isInClosedToggle'; +export * from './someToggle'; diff --git a/packages/toggle/src/queries/isInClosedToggle.ts b/packages/toggle/src/queries/isInClosedToggle.ts new file mode 100644 index 0000000000..9248655551 --- /dev/null +++ b/packages/toggle/src/queries/isInClosedToggle.ts @@ -0,0 +1,15 @@ +import { PlateEditor, Value } from '@udecode/plate-common'; + +import { someToggleClosed } from '../toggle-controller-store'; +import { getEnclosingToggleIds } from './getEnclosingToggleIds'; + +export const isInClosedToggle = < + V extends Value = Value, + E extends PlateEditor = PlateEditor, +>( + editor: E, + elementId: string +) => { + const enclosingToggleIds = getEnclosingToggleIds(editor, elementId); + return someToggleClosed(editor, enclosingToggleIds); +}; diff --git a/packages/toggle/src/queries/someToggle.ts b/packages/toggle/src/queries/someToggle.ts new file mode 100644 index 0000000000..2b14d02af9 --- /dev/null +++ b/packages/toggle/src/queries/someToggle.ts @@ -0,0 +1,12 @@ +import { PlateEditor, someNode, Value } from '@udecode/plate-common'; + +import { ELEMENT_TOGGLE } from '../types'; + +export const someToggle = (editor: PlateEditor) => { + return ( + !!editor.selection && + someNode(editor, { + match: (n) => n.type === ELEMENT_TOGGLE, + }) + ); +}; diff --git a/packages/toggle/src/toggle-controller-store.ts b/packages/toggle/src/toggle-controller-store.ts new file mode 100644 index 0000000000..a0fa633829 --- /dev/null +++ b/packages/toggle/src/toggle-controller-store.ts @@ -0,0 +1,135 @@ +import { useMemo } from 'react'; +import { + atom, + createAtomStore, + getPluginOptions, + PlateEditor, + plateStore, + usePlateStore, + Value, +} from '@udecode/plate-common'; +import { KEY_INDENT, TIndentElement } from '@udecode/plate-indent'; + +import { ELEMENT_TOGGLE, TogglePlugin } from './types'; + +// Duplicate constant instead of importing from "plate-indent-list" to avoid a dependency. +const KEY_LIST_STYLE_TYPE = 'listStyleType'; + +export const { + toggleControllerStore, + ToggleControllerProvider, + useToggleControllerStore, +} = createAtomStore( + { + openIds: atom(new Set()), + }, + { name: 'toggleController' as const } +); + +// Due to a limitation of jotai-x, it's not possible to derive a state from both `toggleControllerStore` and plateStore`. +// In order minimize re-renders, we subscribe to both separately, but only re-render unnecessarily when opening or closing a toggle, +// which is less frequent than changing the editor's children. +export const useIsVisible = (elementId: string) => { + const [openIds] = useToggleControllerStore().use.openIds(); + const isVisibleAtom = useMemo( + () => + atom((get) => { + const toggleIndex = get(toggleIndexAtom); + const enclosedInToggleIds = toggleIndex.get(elementId) || []; + return enclosedInToggleIds.every((enclosedId) => + openIds.has(enclosedId) + ); + }), + [elementId, openIds] + ); + + return usePlateStore().get.atom(isVisibleAtom); +}; + +const editorAtom = plateStore.atom.trackedEditor; +export const toggleIndexAtom = atom((get) => + buildToggleIndex(get(editorAtom).editor.children as TIndentElement[]) +); +export const useToggleIndex = () => usePlateStore().get.atom(toggleIndexAtom); + +export const someToggleClosed = < + V extends Value = Value, + E extends PlateEditor = PlateEditor, +>( + editor: E, + toggleIds: string[] +): boolean => { + const options = getPluginOptions(editor, ELEMENT_TOGGLE); + const openIds = options.openIds!; + return toggleIds.some((id) => !openIds.has(id)); +}; + +export const isToggleOpen = < + V extends Value = Value, + E extends PlateEditor = PlateEditor, +>( + editor: E, + toggleId: string +): boolean => { + const options = getPluginOptions(editor, ELEMENT_TOGGLE); + const openIds = options.openIds!; + return openIds.has(toggleId); +}; + +export const toggleIds = < + V extends Value = Value, + E extends PlateEditor = PlateEditor, +>( + editor: E, + ids: string[], + force: boolean | null = null +): void => { + const options = getPluginOptions(editor, ELEMENT_TOGGLE); + options.setOpenIds!((openIds) => _toggleIds(openIds, ids, force)); +}; + +const _toggleIds = ( + openIds: Set, + ids: string[], + force: boolean | null = null +) => { + const newOpenIds = new Set(openIds.values()); + ids.forEach((id) => { + const isCurrentlyOpen = openIds.has(id); + const newIsOpen = force === null ? !isCurrentlyOpen : force; + if (newIsOpen) { + newOpenIds.add(id); + } else { + newOpenIds.delete(id); + } + }); + return newOpenIds; +}; + +// Returns, for each child, the enclosing toggle ids +export const buildToggleIndex = (elements: Value): Map => { + const result: Map = new Map(); + let currentEnclosingToggles: [string, number][] = []; // [toggleId, indent][] + elements.forEach((element) => { + const elementIndent = (element[KEY_INDENT] as number) || 0; + // For some reason, indent lists have a min indent of 1, even though they are not indented + const elementIndentWithIndentListCorrection = + element[KEY_LIST_STYLE_TYPE] && element[KEY_INDENT] + ? elementIndent - 1 + : elementIndent; + + const enclosingToggles = currentEnclosingToggles.filter(([_, indent]) => { + return indent < elementIndentWithIndentListCorrection; + }); + currentEnclosingToggles = enclosingToggles; + result.set( + element.id as string, + enclosingToggles.map(([toggleId]) => toggleId) + ); + if (element.type === ELEMENT_TOGGLE) { + currentEnclosingToggles.push([element.id as string, elementIndent]); + } + }); + + return result; +}; diff --git a/packages/toggle/src/transforms/index.ts b/packages/toggle/src/transforms/index.ts new file mode 100644 index 0000000000..6715cdf8d5 --- /dev/null +++ b/packages/toggle/src/transforms/index.ts @@ -0,0 +1,5 @@ +/** + * @file Automatically generated by barrelsby. + */ + +export * from './openNextToggles'; diff --git a/packages/toggle/src/transforms/openNextToggles.ts b/packages/toggle/src/transforms/openNextToggles.ts new file mode 100644 index 0000000000..41319d7541 --- /dev/null +++ b/packages/toggle/src/transforms/openNextToggles.ts @@ -0,0 +1,20 @@ +import { getNodeEntries, PlateEditor } from '@udecode/plate-common'; + +import { toggleIds } from '../toggle-controller-store'; + +// When creating a toggle, we open it by default. +// So before inserting the toggle, we update the store to mark the id of the blocks about to be turned into toggles as open. +export const openNextToggles = (editor: PlateEditor) => { + const nodeEntries = Array.from( + getNodeEntries(editor, { + block: true, + mode: 'lowest', + }) + ); + + toggleIds( + editor, + nodeEntries.map(([node]) => node.id as string), + true + ); +}; diff --git a/packages/toggle/src/types.ts b/packages/toggle/src/types.ts new file mode 100644 index 0000000000..13f3b8baff --- /dev/null +++ b/packages/toggle/src/types.ts @@ -0,0 +1,17 @@ +import { SetStateAction } from 'react'; + +import { buildToggleIndex } from './toggle-controller-store'; + +export interface TogglePlugin { + // Options would go here + // TODO a JOTAI layer in plate-core instead of relying on plugin options + openIds?: Set; + setOpenIds?: (args_0: SetStateAction>) => void; + toggleIndex?: ReturnType; +} + +export const ELEMENT_TOGGLE = 'toggle'; + +export type TToggleElement = { + type: typeof ELEMENT_TOGGLE; +}; diff --git a/packages/toggle/src/withToggle.ts b/packages/toggle/src/withToggle.ts new file mode 100644 index 0000000000..c171949781 --- /dev/null +++ b/packages/toggle/src/withToggle.ts @@ -0,0 +1,70 @@ +import { + getBlockAbove, + isNode, + moveNodes, + PlateEditor, + toggleNodeType, + Value, +} from '@udecode/plate-common'; +import { indent, TIndentElement } from '@udecode/plate-indent'; + +import { getLastEntryEnclosedInToggle, isInClosedToggle } from './queries'; +import { isToggleOpen } from './toggle-controller-store'; +import { ELEMENT_TOGGLE } from './types'; + +export const withToggle = < + V extends Value = Value, + E extends PlateEditor = PlateEditor, +>( + editor: E +) => { + const { insertBreak, isSelectable } = editor; + + editor.isSelectable = (element) => { + if (isNode(element) && isInClosedToggle(editor, element.id as string)) + return false; + return isSelectable(element); + }; + + editor.insertBreak = () => { + // If we are inserting a break in a toggle: + // If the toggle is open + // - Add a new paragraph right after the toggle + // - Focus on that paragraph + // If the the toggle is closed: + // - Add a new paragraph after the last sibling enclosed in the toggle + // - Focus on that paragraph + // Note: We are relying on the default behaviour of `insertBreak` which inserts a toggle right after the current toggle with the same indent + const currentBlockEntry = getBlockAbove(editor); + if (!currentBlockEntry || currentBlockEntry[0].type !== ELEMENT_TOGGLE) { + return insertBreak(); + } + + const toggleId = currentBlockEntry[0].id as string; + const isOpen = isToggleOpen(editor, toggleId); + + editor.withoutNormalizing(() => { + if (isOpen) { + insertBreak(); + toggleNodeType(editor, { activeType: ELEMENT_TOGGLE }); + indent(editor); + } else { + const lastEntryEnclosedInToggle = getLastEntryEnclosedInToggle( + editor, + toggleId + ); + + insertBreak(); + + if (lastEntryEnclosedInToggle) { + moveNodes(editor, { + at: [currentBlockEntry[1][0] + 1], // Newly inserted toggle + to: [lastEntryEnclosedInToggle[1][0] + 1], // Right after the last enclosed element + }); + } + } + }); + }; + + return editor; +}; diff --git a/packages/toggle/tsconfig.json b/packages/toggle/tsconfig.json new file mode 100644 index 0000000000..0db2a27277 --- /dev/null +++ b/packages/toggle/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../config/tsconfig.build.json", + "compilerOptions": { + "declarationDir": "./dist", + "outDir": "./dist" + }, + "include": [ + "src" + ] +} diff --git a/yarn.lock b/yarn.lock index 8315cb0559..03528055de 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6940,6 +6940,24 @@ __metadata: languageName: unknown linkType: soft +"@udecode/plate-toggle@npm:30.1.2, @udecode/plate-toggle@workspace:^, @udecode/plate-toggle@workspace:packages/toggle": + version: 0.0.0-use.local + resolution: "@udecode/plate-toggle@workspace:packages/toggle" + dependencies: + "@udecode/plate-common": "npm:30.1.2" + "@udecode/plate-indent": "npm:30.1.2" + "@udecode/plate-node-id": "npm:30.1.2" + lodash: "npm:^4.17.21" + peerDependencies: + react: ">=16.8.0" + react-dom: ">=16.8.0" + slate: ">=0.94.0" + slate-history: ">=0.93.0" + slate-hyperscript: ">=0.66.0" + slate-react: ">=0.99.0" + languageName: unknown + linkType: soft + "@udecode/plate-trailing-block@npm:30.1.2, @udecode/plate-trailing-block@workspace:^, @udecode/plate-trailing-block@workspace:packages/trailing-block": version: 0.0.0-use.local resolution: "@udecode/plate-trailing-block@workspace:packages/trailing-block" @@ -7067,6 +7085,7 @@ __metadata: "@udecode/plate-suggestion": "npm:30.1.2" "@udecode/plate-tabbable": "npm:30.1.2" "@udecode/plate-table": "npm:30.1.2" + "@udecode/plate-toggle": "npm:30.1.2" "@udecode/plate-trailing-block": "npm:30.1.2" peerDependencies: react: ">=16.8.0" @@ -21684,6 +21703,7 @@ __metadata: "@udecode/plate-tabbable": "workspace:^" "@udecode/plate-table": "workspace:^" "@udecode/plate-test-utils": "workspace:^" + "@udecode/plate-toggle": "workspace:^" "@udecode/plate-trailing-block": "workspace:^" "@udecode/plate-utils": "workspace:^" "@udecode/react-utils": "workspace:^"