diff --git a/.changeset/shy-billo-bagga.md b/.changeset/shy-billo-bagga.md new file mode 100644 index 0000000000..85b621a445 --- /dev/null +++ b/.changeset/shy-billo-bagga.md @@ -0,0 +1,5 @@ +--- +'@udecode/plate-select': minor +--- + +Added `createDeletePlugin`. If enabled, performing a delete forward inside an empty block will remove that block without affecting the subsequent block. \ No newline at end of file diff --git a/apps/www/src/config/customizer-items.ts b/apps/www/src/config/customizer-items.ts index f572ec38d2..aef2bd67c6 100644 --- a/apps/www/src/config/customizer-items.ts +++ b/apps/www/src/config/customizer-items.ts @@ -39,7 +39,7 @@ import { KEY_NODE_ID } from '@udecode/plate-node-id'; import { KEY_NORMALIZE_TYPES } from '@udecode/plate-normalizers'; import { ELEMENT_PARAGRAPH } from '@udecode/plate-paragraph'; import { KEY_RESET_NODE } from '@udecode/plate-reset-node'; -import { KEY_SELECT_ON_BACKSPACE } from '@udecode/plate-select'; +import { KEY_DELETE, KEY_SELECT_ON_BACKSPACE } from '@udecode/plate-select'; import { KEY_BLOCK_SELECTION } from '@udecode/plate-selection'; import { KEY_DESERIALIZE_CSV } from '@udecode/plate-serializer-csv'; import { KEY_DESERIALIZE_DOCX } from '@udecode/plate-serializer-docx'; @@ -957,6 +957,13 @@ export const customizerItems: Record = { badges: [customizerBadges.handler], route: customizerPlugins.media.route, }, + [KEY_DELETE]: { + id: KEY_DELETE, + npmPackage: '@udecode/plate-select', + pluginFactory: 'createDeletePlugin', + label: 'Delete', + badges: [customizerBadges.handler], + }, [KEY_SINGLE_LINE]: { id: KEY_SINGLE_LINE, npmPackage: '@udecode/plate-break', diff --git a/apps/www/src/config/customizer-list.ts b/apps/www/src/config/customizer-list.ts index c7dc75b50b..d046aa42d4 100644 --- a/apps/www/src/config/customizer-list.ts +++ b/apps/www/src/config/customizer-list.ts @@ -39,7 +39,7 @@ import { KEY_NODE_ID } from '@udecode/plate-node-id'; import { KEY_NORMALIZE_TYPES } from '@udecode/plate-normalizers'; import { ELEMENT_PARAGRAPH } from '@udecode/plate-paragraph'; import { KEY_RESET_NODE } from '@udecode/plate-reset-node'; -import { KEY_SELECT_ON_BACKSPACE } from '@udecode/plate-select'; +import { KEY_DELETE, KEY_SELECT_ON_BACKSPACE } from '@udecode/plate-select'; import { KEY_BLOCK_SELECTION } from '@udecode/plate-selection'; import { KEY_DESERIALIZE_CSV } from '@udecode/plate-serializer-csv'; import { KEY_DESERIALIZE_DOCX } from '@udecode/plate-serializer-docx'; @@ -117,6 +117,7 @@ export const customizerList = [ customizerItems[KEY_NORMALIZE_TYPES], customizerItems[KEY_RESET_NODE], customizerItems[KEY_SELECT_ON_BACKSPACE], + customizerItems[KEY_DELETE], customizerItems[KEY_SINGLE_LINE], customizerItems[KEY_SOFT_BREAK], customizerItems[KEY_TABBABLE], @@ -182,6 +183,7 @@ export const orderedPluginKeys = [ KEY_NORMALIZE_TYPES, KEY_RESET_NODE, KEY_SELECT_ON_BACKSPACE, + KEY_DELETE, KEY_SINGLE_LINE, KEY_SOFT_BREAK, KEY_TABBABLE, diff --git a/apps/www/src/config/descriptions.ts b/apps/www/src/config/descriptions.ts index 15ae6c19af..44fc4ea073 100644 --- a/apps/www/src/config/descriptions.ts +++ b/apps/www/src/config/descriptions.ts @@ -37,7 +37,7 @@ import { KEY_NODE_ID } from '@udecode/plate-node-id'; import { KEY_NORMALIZE_TYPES } from '@udecode/plate-normalizers'; import { ELEMENT_PARAGRAPH } from '@udecode/plate-paragraph'; import { KEY_RESET_NODE } from '@udecode/plate-reset-node'; -import { KEY_SELECT_ON_BACKSPACE } from '@udecode/plate-select'; +import { KEY_DELETE, KEY_SELECT_ON_BACKSPACE } from '@udecode/plate-select'; import { KEY_BLOCK_SELECTION } from '@udecode/plate-selection'; import { KEY_DESERIALIZE_CSV } from '@udecode/plate-serializer-csv'; import { KEY_DESERIALIZE_DOCX } from '@udecode/plate-serializer-docx'; @@ -94,6 +94,8 @@ export const descriptions: Record = { [KEY_RESET_NODE]: 'Reset the block type using rules.', [KEY_SELECT_ON_BACKSPACE]: 'Select the preceding block instead of deleting when pressing backspace.', + [KEY_DELETE]: + 'Remove the current block if empty when pressing delete forward', [KEY_SINGLE_LINE]: 'Restrict the editor to a single block.', [KEY_SOFT_BREAK]: 'Insert line breaks within a block of text without starting a new block.', diff --git a/apps/www/src/registry/default/example/playground-demo.tsx b/apps/www/src/registry/default/example/playground-demo.tsx index 6f565f6046..e5a1074dd0 100644 --- a/apps/www/src/registry/default/example/playground-demo.tsx +++ b/apps/www/src/registry/default/example/playground-demo.tsx @@ -77,7 +77,10 @@ import { createNodeIdPlugin } from '@udecode/plate-node-id'; import { createNormalizeTypesPlugin } from '@udecode/plate-normalizers'; import { createParagraphPlugin } from '@udecode/plate-paragraph'; import { createResetNodePlugin } from '@udecode/plate-reset-node'; -import { createSelectOnBackspacePlugin } from '@udecode/plate-select'; +import { + createDeletePlugin, + createSelectOnBackspacePlugin, +} from '@udecode/plate-select'; import { createBlockSelectionPlugin } from '@udecode/plate-selection'; import { createDeserializeDocxPlugin } from '@udecode/plate-serializer-docx'; import { createDeserializeMdPlugin } from '@udecode/plate-serializer-md'; @@ -216,6 +219,9 @@ export const usePlaygroundPlugins = ({ ...selectOnBackspacePlugin, enabled: !!enabled.selectOnBackspace, }), + createDeletePlugin({ + enabled: !!enabled.delete, + }), createSingleLinePlugin({ enabled: id === 'singleline' || !!enabled.singleLine, }), diff --git a/packages/select/src/createDeletePlugin.spec.tsx b/packages/select/src/createDeletePlugin.spec.tsx new file mode 100644 index 0000000000..d01e1664e6 --- /dev/null +++ b/packages/select/src/createDeletePlugin.spec.tsx @@ -0,0 +1,77 @@ +/** @jsx jsx */ + +import { createPlateEditor, PlateEditor } from '@udecode/plate-common'; +import { jsx } from '@udecode/plate-test-utils'; + +import { createDeletePlugin } from './createDeletePlugin'; + +jsx; + +describe('p (empty) + codeblock when selection not in code block', () => { + it('should remove the p', () => { + const input = ( + + + + + + test + test2 + + + ) as any as PlateEditor; + + const expected = ( + + + test + test2 + + + ) as any as PlateEditor; + + const editor = createPlateEditor({ + editor: input, + plugins: [createDeletePlugin()], + }); + + editor.deleteForward('character'); + + expect(editor.children).toEqual(expected.children); + }); +}); + +describe('p (not empty) + code block when selection not in code block', () => { + it('should remove the p', () => { + const input = ( + + + para + + + + test + test2 + + + ) as any as PlateEditor; + + const expected = ( + + paratest + + test2 + + + ) as any as PlateEditor; + + const editor = createPlateEditor({ + editor: input, + plugins: [createDeletePlugin()], + }); + + editor.deleteForward('character'); + + expect(editor.children).toEqual(expected.children); + }); +}); diff --git a/packages/select/src/createDeletePlugin.ts b/packages/select/src/createDeletePlugin.ts new file mode 100644 index 0000000000..7168d104a6 --- /dev/null +++ b/packages/select/src/createDeletePlugin.ts @@ -0,0 +1,26 @@ +import { + createPluginFactory, + ELEMENT_DEFAULT, + QueryNodeOptions, +} from '@udecode/plate-common'; + +import { withDelete } from './withDelete'; + +export type DeletePlugin = { + query?: QueryNodeOptions; +}; + +export const KEY_DELETE = 'delete'; + +/** + * @see {@link withDelete} + */ +export const createDeletePlugin = createPluginFactory({ + key: KEY_DELETE, + withOverrides: withDelete, + options: { + query: { + allow: [ELEMENT_DEFAULT], + }, + }, +}); diff --git a/packages/select/src/index.ts b/packages/select/src/index.ts index 3a349378d0..34528b4c9a 100644 --- a/packages/select/src/index.ts +++ b/packages/select/src/index.ts @@ -4,3 +4,5 @@ export * from './createSelectOnBackspacePlugin'; export * from './withSelectOnBackspace'; +export * from './createDeletePlugin'; +export * from './withDelete'; diff --git a/packages/select/src/withDelete.ts b/packages/select/src/withDelete.ts new file mode 100644 index 0000000000..00fe1e48ee --- /dev/null +++ b/packages/select/src/withDelete.ts @@ -0,0 +1,41 @@ +import { + getAboveNode, + isBlockAboveEmpty, + isSelectionExpanded, + PlateEditor, + queryNode, + removeNodes, + Value, + WithPlatePlugin, +} from '@udecode/plate-common'; + +import { DeletePlugin } from './createDeletePlugin'; + +/** + * Set a list of element types to select on backspace + */ +export const withDelete = < + V extends Value = Value, + E extends PlateEditor = PlateEditor, +>( + editor: E, + { options: { query } }: WithPlatePlugin +) => { + const { deleteForward } = editor; + editor.deleteForward = (unit) => { + if (!editor.selection) return; + const isValidNode = !query || queryNode(getAboveNode(editor), query); + if ( + !isSelectionExpanded(editor) && + isBlockAboveEmpty(editor) && + isValidNode + ) { + // Cursor is in query blocks and line is empty + removeNodes(editor as any); + } else { + // When the line is not empty or other conditions are not met, fall back to default behavior + deleteForward(unit); + } + }; + return editor; +}; diff --git a/packages/select/src/withSelectOnBackspace.ts b/packages/select/src/withSelectOnBackspace.ts index 8df2a894a0..6a4628fc10 100644 --- a/packages/select/src/withSelectOnBackspace.ts +++ b/packages/select/src/withSelectOnBackspace.ts @@ -32,7 +32,6 @@ export const withSelectOnBackspace = < editor.deleteBackward = (unit: 'character' | 'word' | 'line' | 'block') => { const { selection } = editor; - if (unit === 'character' && isCollapsed(selection)) { const pointBefore = getPointBefore(editor, selection as Slate.Location, { unit, diff --git a/packages/serializer-docx/src/docx-cleaner/utils/cleanDocxImageElements.ts b/packages/serializer-docx/src/docx-cleaner/utils/cleanDocxImageElements.ts index 904f679d5c..5a23399bbb 100644 --- a/packages/serializer-docx/src/docx-cleaner/utils/cleanDocxImageElements.ts +++ b/packages/serializer-docx/src/docx-cleaner/utils/cleanDocxImageElements.ts @@ -30,7 +30,10 @@ export const cleanDocxImageElements = ( const alt = element.getAttribute('alt'); - if (typeof alt === 'string' && validator.isURL(alt, { require_protocol: true })) { + if ( + typeof alt === 'string' && + validator.isURL(alt, { require_protocol: true }) + ) { element.setAttribute('src', alt); return true; } diff --git a/templates/plate-playground-template/src/lib/plate/plate-plugins.ts b/templates/plate-playground-template/src/lib/plate/plate-plugins.ts index b291ef8420..f386d5ac0d 100644 --- a/templates/plate-playground-template/src/lib/plate/plate-plugins.ts +++ b/templates/plate-playground-template/src/lib/plate/plate-plugins.ts @@ -321,6 +321,7 @@ export const plugins = createPlugins( }, }, }), + createSoftBreakPlugin({ options: { rules: [