-
Notifications
You must be signed in to change notification settings - Fork 2
[STC-806] Documents: impossible to delete characters with backspace after adding a wp link #165
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
ihordubas99
wants to merge
1
commit into
dev
Choose a base branch
from
bug/stc-806-documents-impossible-to-delete-characters-with-backspace-after-adding-a-wp-link
base: dev
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| import { createExtension } from '@blocknote/core'; | ||
| import { Plugin, PluginKey, TextSelection } from 'prosemirror-state'; | ||
|
|
||
| const pluginKey = new PluginKey('opKeyboardDelete'); | ||
|
|
||
| // Intl.Segmenter is ES2022; cast to avoid bumping tsconfig lib target. | ||
| interface SegmenterEntry {segment:string} | ||
| type SegmenterCtor = new (locale?:string, opts?:{granularity?:string}) => {segment(text:string):Iterable<SegmenterEntry>}; | ||
| const SegmenterCls = (Intl as {Segmenter?:SegmenterCtor}).Segmenter; | ||
| const segmenter = SegmenterCls ? new SegmenterCls(undefined, { granularity: 'grapheme' }) : null; | ||
|
|
||
| function graphemes(text:string):string[] { | ||
| if (segmenter) { | ||
| return [...segmenter.segment(text)].map(e => e.segment); | ||
| } | ||
| return [...text]; | ||
| } | ||
|
|
||
| function backwardCharSize(node:{isText:boolean; text?:string|null; nodeSize:number}):number { | ||
| if (!node.isText) return node.nodeSize; | ||
| const g = graphemes(node.text!); | ||
| return g[g.length - 1]?.length ?? 1; | ||
| } | ||
|
|
||
| function forwardCharSize(node:{isText:boolean; text?:string|null; nodeSize:number}):number { | ||
| if (!node.isText) return node.nodeSize; | ||
| return graphemes(node.text!)[0]?.length ?? 1; | ||
| } | ||
|
|
||
| const keyboardDeletePlugin = new Plugin({ | ||
| key: pluginKey, | ||
| props: { | ||
| handleKeyDown(view, event) { | ||
| const isBackspace = event.key === 'Backspace'; | ||
| const isDelete = event.key === 'Delete'; | ||
| if ((!isBackspace && !isDelete) || event.isComposing) return false; | ||
|
ihordubas99 marked this conversation as resolved.
|
||
|
|
||
| const { selection } = view.state; | ||
| if (!(selection instanceof TextSelection) || !selection.empty) return false; | ||
|
|
||
| const $cursor = selection.$cursor; | ||
| if (!$cursor) return false; | ||
|
|
||
| const isLineDelete = event.metaKey && !event.ctrlKey && !event.altKey; | ||
| const isWordDelete = event.ctrlKey || event.altKey; | ||
|
|
||
| if (isBackspace) { | ||
| if ($cursor.parentOffset === 0) return false; | ||
| const nodeBefore = $cursor.nodeBefore; | ||
| if (!nodeBefore) return false; | ||
|
|
||
| let from:number; | ||
| if (isLineDelete) { | ||
| from = $cursor.pos - $cursor.parentOffset; | ||
| } else if (isWordDelete && nodeBefore.isText) { | ||
| const text = nodeBefore.text!; | ||
| let i = text.length; | ||
| while (i > 0 && /\s/.test(text[i - 1])) i -= 1; | ||
| while (i > 0 && !/\s/.test(text[i - 1])) i -= 1; | ||
| from = $cursor.pos - (text.length - i); | ||
| } else { | ||
| from = $cursor.pos - backwardCharSize(nodeBefore); | ||
| } | ||
|
|
||
| view.dispatch(view.state.tr.delete(from, $cursor.pos)); | ||
| return true; | ||
| } | ||
|
|
||
| const nodeAfter = $cursor.nodeAfter; | ||
| if (!nodeAfter) return false; | ||
|
|
||
| let to:number; | ||
| if (isLineDelete) { | ||
| to = $cursor.end(); | ||
| } else if (isWordDelete && nodeAfter.isText) { | ||
| const text = nodeAfter.text!; | ||
| let i = 0; | ||
| while (i < text.length && !/\s/.test(text[i])) i += 1; | ||
| while (i < text.length && /\s/.test(text[i])) i += 1; | ||
| to = $cursor.pos + i; | ||
| } else { | ||
| to = $cursor.pos + forwardCharSize(nodeAfter); | ||
| } | ||
|
|
||
| view.dispatch(view.state.tr.delete($cursor.pos, to)); | ||
| return true; | ||
| }, | ||
| }, | ||
| }); | ||
|
|
||
| export const KeyboardDeleteExtension = createExtension({ | ||
| key: 'opKeyboardDelete', | ||
| prosemirrorPlugins: [keyboardDeletePlugin], | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| import { createExtension } from '@blocknote/core'; | ||
| import { KeyboardDeleteExtension } from './keyboardDeleteExtension'; | ||
| import { PasteDeduplicateExtension } from './pasteDeduplicateExtension'; | ||
|
|
||
| /** | ||
| * Required extensions for op-blocknote-extensions. | ||
| * | ||
| * Must be added to `editorOptions.extensions: [...]` at editor construction | ||
| * time, not registered post-mount via `editor.registerPlugin(...)`. | ||
| */ | ||
| export const OpBlockNoteExtensions = createExtension({ | ||
| key: 'opBlockNoteExtensions', | ||
| blockNoteExtensions: [PasteDeduplicateExtension, KeyboardDeleteExtension], | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
159 changes: 159 additions & 0 deletions
159
test/lib/components/integration/editor.backspaceDelete.browser.test.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,159 @@ | ||
| import { describe, it, expect } from 'vitest'; | ||
| import { page, userEvent } from 'vitest/browser'; | ||
| import { renderEditor } from '../../../helpers/renderEditor'; | ||
| import { | ||
| insertInlineWorkPackageViaHash, | ||
| insertInlineWorkPackageViaSlashMenu, | ||
| convertToCompactCard, | ||
| } from '../../../helpers/editorHelpers'; | ||
|
|
||
| describe('Backspace - inline WP', () => { | ||
| it('deletes text typed after inline chip, character by character', async () => { | ||
| renderEditor(); | ||
| await insertInlineWorkPackageViaHash('#'); | ||
|
|
||
| const editor = page.getByRole('textbox'); | ||
| await userEvent.type(editor, 'abc'); | ||
| await expect.element(editor).toHaveTextContent('abc'); | ||
|
|
||
| await userEvent.keyboard('{Backspace}'); | ||
| await expect.element(editor).not.toHaveTextContent('abc'); | ||
| await expect.element(editor).toHaveTextContent('ab'); | ||
|
|
||
| await userEvent.keyboard('{Backspace}'); | ||
| await expect.element(editor).not.toHaveTextContent('ab'); | ||
| await expect.element(editor).toHaveTextContent('a'); | ||
|
|
||
|
ihordubas99 marked this conversation as resolved.
|
||
| await userEvent.keyboard('{Backspace}'); | ||
| await expect.element(editor).not.toHaveTextContent('a'); | ||
|
|
||
| await expect.element(page.getByText('#123')).toBeVisible(); | ||
| }); | ||
|
|
||
| it('deletes the inline WP itself with Backspace', async () => { | ||
| renderEditor(); | ||
| await insertInlineWorkPackageViaHash('#'); | ||
|
|
||
| // Cursor is right after the WP | ||
| await userEvent.keyboard('{Backspace}'); | ||
|
|
||
| await expect.element(page.getByText('#123')).not.toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('deletes typed text and then the WP with successive Backspace presses', async () => { | ||
| renderEditor(); | ||
| await insertInlineWorkPackageViaHash('#'); | ||
|
|
||
| const editor = page.getByRole('textbox'); | ||
| await userEvent.type(editor, 'hi'); | ||
| await expect.element(editor).toHaveTextContent('hi'); | ||
|
|
||
| await userEvent.keyboard('{Backspace}'); | ||
| await expect.element(editor).toHaveTextContent('h'); | ||
| await expect.element(editor).not.toHaveTextContent('hi'); | ||
|
|
||
| await userEvent.keyboard('{Backspace}'); | ||
| await expect.element(editor).not.toHaveTextContent('h'); | ||
|
|
||
| // Cursor is now right after the WP - Backspace removes the WP | ||
| await userEvent.keyboard('{Backspace}'); | ||
| await expect.element(page.getByText('#123')).not.toBeInTheDocument(); | ||
| }); | ||
| }); | ||
|
|
||
| describe('Backspace - block WP', () => { | ||
| it('deletes text typed after a block WP, character by character', async () => { | ||
| renderEditor(); | ||
| await insertInlineWorkPackageViaSlashMenu(); | ||
| await convertToCompactCard(); | ||
| await expect.element(page.getByTestId('block-card')).toBeVisible(); | ||
|
|
||
| // After conversion the cursor lands in the empty paragraph following the block card | ||
| const editor = page.getByRole('textbox'); | ||
| await userEvent.type(editor, 'abc'); | ||
| await expect.element(editor).toHaveTextContent('abc'); | ||
|
|
||
| await userEvent.keyboard('{Backspace}'); | ||
| await expect.element(editor).not.toHaveTextContent('abc'); | ||
| await expect.element(editor).toHaveTextContent('ab'); | ||
|
|
||
| await userEvent.keyboard('{Backspace}'); | ||
| await expect.element(editor).not.toHaveTextContent('ab'); | ||
| await expect.element(editor).toHaveTextContent('a'); | ||
|
|
||
|
ihordubas99 marked this conversation as resolved.
|
||
| await userEvent.keyboard('{Backspace}'); | ||
| await expect.element(editor).not.toHaveTextContent('a'); | ||
|
|
||
| await expect.element(page.getByTestId('block-card')).toBeVisible(); | ||
| }); | ||
| }); | ||
|
|
||
| describe('Delete - inline WP', () => { | ||
| it('deletes text in front of the cursor, character by character', async () => { | ||
| renderEditor(); | ||
| await insertInlineWorkPackageViaHash('#'); | ||
|
|
||
| const editor = page.getByRole('textbox'); | ||
| await userEvent.type(editor, 'abc'); | ||
| await expect.element(editor).toHaveTextContent('abc'); | ||
|
|
||
| // Move cursor before 'a' (right after the chip) | ||
| await userEvent.keyboard('{ArrowLeft}{ArrowLeft}{ArrowLeft}'); | ||
|
|
||
| await userEvent.keyboard('{Delete}'); | ||
| await expect.element(editor).not.toHaveTextContent('abc'); | ||
| await expect.element(editor).toHaveTextContent('bc'); | ||
|
|
||
| await userEvent.keyboard('{Delete}'); | ||
| await expect.element(editor).not.toHaveTextContent('bc'); | ||
| await expect.element(editor).toHaveTextContent('c'); | ||
|
|
||
| await userEvent.keyboard('{Delete}'); | ||
| await expect.element(editor).not.toHaveTextContent('c'); | ||
|
|
||
| await expect.element(page.getByText('#123')).toBeVisible(); | ||
| }); | ||
|
|
||
| it('deletes the inline WP itself with Delete', async () => { | ||
| renderEditor(); | ||
| await insertInlineWorkPackageViaHash('#'); | ||
|
|
||
| // Move cursor before the chip | ||
| await userEvent.keyboard('{ArrowLeft}'); | ||
|
|
||
| await userEvent.keyboard('{Delete}'); | ||
|
|
||
| await expect.element(page.getByText('#123')).not.toBeInTheDocument(); | ||
| }); | ||
|
|
||
| }); | ||
|
|
||
| describe('Delete - block WP', () => { | ||
| it('deletes text in front of the cursor, character by character', async () => { | ||
| renderEditor(); | ||
| await insertInlineWorkPackageViaSlashMenu(); | ||
| await convertToCompactCard(); | ||
| await expect.element(page.getByTestId('block-card')).toBeVisible(); | ||
|
|
||
| // After conversion the cursor lands in the empty paragraph following the block card | ||
| const editor = page.getByRole('textbox'); | ||
| await userEvent.type(editor, 'abc'); | ||
| await expect.element(editor).toHaveTextContent('abc'); | ||
|
|
||
| // Move cursor before 'a' | ||
| await userEvent.keyboard('{ArrowLeft}{ArrowLeft}{ArrowLeft}'); | ||
|
|
||
| await userEvent.keyboard('{Delete}'); | ||
| await expect.element(editor).not.toHaveTextContent('abc'); | ||
| await expect.element(editor).toHaveTextContent('bc'); | ||
|
|
||
| await userEvent.keyboard('{Delete}'); | ||
| await expect.element(editor).not.toHaveTextContent('bc'); | ||
| await expect.element(editor).toHaveTextContent('c'); | ||
|
|
||
| await userEvent.keyboard('{Delete}'); | ||
| await expect.element(editor).not.toHaveTextContent('c'); | ||
|
|
||
| await expect.element(page.getByTestId('block-card')).toBeVisible(); | ||
| }); | ||
| }); | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.