From 06005490641b9025790d823539a59b16383e685f Mon Sep 17 00:00:00 2001 From: Flarnie Marchan Date: Thu, 9 Aug 2018 12:12:54 -0700 Subject: [PATCH] 5/n Disable forward delete across blocks when nested blocks are enabled Summary: We are disabling forward delete across blocks for now because it is rarely used compared to backspace. We expect that people may not even notice, but if/when they do then part of the roll-out for nested blocks will be to implement this feature. For more details about our ideal implementation of the forward-delete behavior across nested blocks, see https://fb.quip.com/tybJAJoCmiRL Reviewed By: mitermayer Differential Revision: D9186965 fbshipit-source-id: a2abb17dd8a77949616d7e057da2476c534f2a77 --- .../removeTextWithStrategy.test.js.snap | 1019 +++++++++++++++++ .../__tests__/removeTextWithStrategy.test.js | 232 ++++ .../edit/commands/removeTextWithStrategy.js | 31 + .../NestedRichTextEditorUtil-test.js | 33 +- .../NestedRichTextEditorUtil-test.js.snap | 4 + 5 files changed, 1311 insertions(+), 8 deletions(-) create mode 100644 src/component/handlers/edit/commands/__tests__/__snapshots__/removeTextWithStrategy.test.js.snap create mode 100644 src/component/handlers/edit/commands/__tests__/removeTextWithStrategy.test.js diff --git a/src/component/handlers/edit/commands/__tests__/__snapshots__/removeTextWithStrategy.test.js.snap b/src/component/handlers/edit/commands/__tests__/__snapshots__/removeTextWithStrategy.test.js.snap new file mode 100644 index 0000000000..5e2c860ea6 --- /dev/null +++ b/src/component/handlers/edit/commands/__tests__/__snapshots__/removeTextWithStrategy.test.js.snap @@ -0,0 +1,1019 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`across blocks with forward delete is a no-op 1`] = ` +Object { + "A": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "A", + "nextSibling": "B", + "parent": null, + "prevSibling": null, + "text": "Alpha", + "type": "blockquote", + }, + "B": Object { + "characterList": Array [], + "children": Array [ + "C", + "F", + ], + "data": Object {}, + "depth": 0, + "key": "B", + "nextSibling": "G", + "parent": null, + "prevSibling": "A", + "text": "", + "type": "ordered-list-item", + }, + "C": Object { + "characterList": Array [], + "children": Array [ + "D", + "E", + ], + "data": Object {}, + "depth": 0, + "key": "C", + "nextSibling": "F", + "parent": "B", + "prevSibling": null, + "text": "", + "type": "blockquote", + }, + "D": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "D", + "nextSibling": "E", + "parent": "C", + "prevSibling": null, + "text": "Delta", + "type": "header-two", + }, + "E": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "E", + "nextSibling": null, + "parent": "C", + "prevSibling": "D", + "text": "Elephant", + "type": "unstyled", + }, + "F": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "F", + "nextSibling": null, + "parent": "B", + "prevSibling": "C", + "text": "Fire", + "type": "code-block", + }, + "G": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "G", + "nextSibling": "H", + "parent": null, + "prevSibling": "B", + "text": "Gorila", + "type": "ordered-list-item", + }, + "H": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "H", + "nextSibling": "I", + "parent": null, + "prevSibling": "G", + "text": " ", + "type": "atomic", + }, + "I": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "I", + "nextSibling": null, + "parent": null, + "prevSibling": "H", + "text": "last", + "type": "unstyled", + }, +} +`; + +exports[`at end of a leaf block and sibling is another leaf block forward delete concatenates 1`] = ` +Object { + "A": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "A", + "nextSibling": "B", + "parent": null, + "prevSibling": null, + "text": "Alpha", + "type": "blockquote", + }, + "B": Object { + "characterList": Array [], + "children": Array [ + "C", + "F", + ], + "data": Object {}, + "depth": 0, + "key": "B", + "nextSibling": "G", + "parent": null, + "prevSibling": "A", + "text": "", + "type": "ordered-list-item", + }, + "C": Object { + "characterList": Array [], + "children": Array [ + "D", + ], + "data": Object {}, + "depth": 0, + "key": "C", + "nextSibling": "F", + "parent": "B", + "prevSibling": null, + "text": "", + "type": "blockquote", + }, + "D": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "D", + "nextSibling": null, + "parent": "C", + "prevSibling": null, + "text": "DeltaElephant", + "type": "header-two", + }, + "F": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "F", + "nextSibling": null, + "parent": "B", + "prevSibling": "C", + "text": "Fire", + "type": "code-block", + }, + "G": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "G", + "nextSibling": "H", + "parent": null, + "prevSibling": "B", + "text": "Gorila", + "type": "ordered-list-item", + }, + "H": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "H", + "nextSibling": "I", + "parent": null, + "prevSibling": "G", + "text": " ", + "type": "atomic", + }, + "I": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "I", + "nextSibling": null, + "parent": null, + "prevSibling": "H", + "text": "last", + "type": "unstyled", + }, +} +`; + +exports[`at end of a leaf block and sibling is not another leaf block forward delete is no-op 1`] = ` +Object { + "A": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "A", + "nextSibling": "B", + "parent": null, + "prevSibling": null, + "text": "Alpha", + "type": "blockquote", + }, + "B": Object { + "characterList": Array [], + "children": Array [ + "C", + "F", + ], + "data": Object {}, + "depth": 0, + "key": "B", + "nextSibling": "G", + "parent": null, + "prevSibling": "A", + "text": "", + "type": "ordered-list-item", + }, + "C": Object { + "characterList": Array [], + "children": Array [ + "D", + "E", + ], + "data": Object {}, + "depth": 0, + "key": "C", + "nextSibling": "F", + "parent": "B", + "prevSibling": null, + "text": "", + "type": "blockquote", + }, + "D": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "D", + "nextSibling": "E", + "parent": "C", + "prevSibling": null, + "text": "Delta", + "type": "header-two", + }, + "E": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "E", + "nextSibling": null, + "parent": "C", + "prevSibling": "D", + "text": "Elephant", + "type": "unstyled", + }, + "F": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "F", + "nextSibling": null, + "parent": "B", + "prevSibling": "C", + "text": "Fire", + "type": "code-block", + }, + "G": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "G", + "nextSibling": "H", + "parent": null, + "prevSibling": "B", + "text": "Gorila", + "type": "ordered-list-item", + }, + "H": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "H", + "nextSibling": "I", + "parent": null, + "prevSibling": "G", + "text": " ", + "type": "atomic", + }, + "I": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "I", + "nextSibling": null, + "parent": null, + "prevSibling": "H", + "text": "last", + "type": "unstyled", + }, +} +`; + +exports[`at end of a leaf block and sibling is not another leaf block forward delete is no-op 2`] = ` +Object { + "A": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "A", + "nextSibling": "B", + "parent": null, + "prevSibling": null, + "text": "Alpha", + "type": "blockquote", + }, + "B": Object { + "characterList": Array [], + "children": Array [ + "C", + "F", + ], + "data": Object {}, + "depth": 0, + "key": "B", + "nextSibling": "G", + "parent": null, + "prevSibling": "A", + "text": "", + "type": "ordered-list-item", + }, + "C": Object { + "characterList": Array [], + "children": Array [ + "D", + "E", + ], + "data": Object {}, + "depth": 0, + "key": "C", + "nextSibling": "F", + "parent": "B", + "prevSibling": null, + "text": "", + "type": "blockquote", + }, + "D": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "D", + "nextSibling": "E", + "parent": "C", + "prevSibling": null, + "text": "Delta", + "type": "header-two", + }, + "E": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "E", + "nextSibling": null, + "parent": "C", + "prevSibling": "D", + "text": "Elephant", + "type": "unstyled", + }, + "F": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "F", + "nextSibling": null, + "parent": "B", + "prevSibling": "C", + "text": "Fire", + "type": "code-block", + }, + "G": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "G", + "nextSibling": "H", + "parent": null, + "prevSibling": "B", + "text": "Gorila", + "type": "ordered-list-item", + }, + "H": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "H", + "nextSibling": "I", + "parent": null, + "prevSibling": "G", + "text": " ", + "type": "atomic", + }, + "I": Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "I", + "nextSibling": null, + "parent": null, + "prevSibling": "H", + "text": "last", + "type": "unstyled", + }, +} +`; diff --git a/src/component/handlers/edit/commands/__tests__/removeTextWithStrategy.test.js b/src/component/handlers/edit/commands/__tests__/removeTextWithStrategy.test.js new file mode 100644 index 0000000000..19f5886a20 --- /dev/null +++ b/src/component/handlers/edit/commands/__tests__/removeTextWithStrategy.test.js @@ -0,0 +1,232 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @emails oncall+ui_infra + * @format + * @flow strict-local + */ + +'use strict'; + +jest.disableAutomock(); + +jest.mock('generateRandomKey'); + +const toggleExperimentalTreeDataSupport = enabled => { + jest.doMock('gkx', () => name => { + return name === 'draft_tree_data_support' ? enabled : false; + }); +}; + +// Seems to be important to put this at the top +toggleExperimentalTreeDataSupport(true); + +const BlockMapBuilder = require('BlockMapBuilder'); +const ContentBlockNode = require('ContentBlockNode'); +const EditorState = require('EditorState'); +const SelectionState = require('SelectionState'); +const UnicodeUtils = require('UnicodeUtils'); + +const getSampleStateForTesting = require('getSampleStateForTesting'); +const Immutable = require('immutable'); +const moveSelectionForward = require('moveSelectionForward'); +const removeTextWithStrategy = require('removeTextWithStrategy'); + +const {List} = Immutable; + +const {contentState} = getSampleStateForTesting(); + +const contentBlockNodes = [ + new ContentBlockNode({ + key: 'A', + nextSibling: 'B', + text: 'Alpha', + type: 'blockquote', + }), + new ContentBlockNode({ + key: 'B', + prevSibling: 'A', + nextSibling: 'G', + type: 'ordered-list-item', + children: List(['C', 'F']), + }), + new ContentBlockNode({ + parent: 'B', + key: 'C', + nextSibling: 'F', + type: 'blockquote', + children: List(['D', 'E']), + }), + new ContentBlockNode({ + parent: 'C', + key: 'D', + nextSibling: 'E', + type: 'header-two', + text: 'Delta', + }), + new ContentBlockNode({ + parent: 'C', + key: 'E', + prevSibling: 'D', + type: 'unstyled', + text: 'Elephant', + }), + new ContentBlockNode({ + parent: 'B', + key: 'F', + prevSibling: 'C', + type: 'code-block', + text: 'Fire', + }), + new ContentBlockNode({ + key: 'G', + prevSibling: 'B', + nextSibling: 'H', + type: 'ordered-list-item', + text: 'Gorila', + }), + new ContentBlockNode({ + key: 'H', + prevSibling: 'G', + nextSibling: 'I', + text: ' ', + type: 'atomic', + }), + new ContentBlockNode({ + key: 'I', + prevSibling: 'H', + text: 'last', + type: 'unstyled', + }), +]; + +const assertRemoveTextOperation = ( + operation, + selection = {}, + content = contentBlockNodes, +) => { + const result = operation( + EditorState.forceSelection( + EditorState.createWithContent( + contentState.set('blockMap', BlockMapBuilder.createFromArray(content)), + ), + SelectionState.createEmpty(content[0].key).merge(selection), + ), + ); + const expected = result.getBlockMap().toJS(); + + expect(expected).toMatchSnapshot(); +}; + +test(`at end of a leaf block and sibling is another leaf block forward delete concatenates`, () => { + assertRemoveTextOperation( + editorState => + removeTextWithStrategy( + editorState, + strategyState => { + const selection = strategyState.getSelection(); + const content = strategyState.getCurrentContent(); + const key = selection.getAnchorKey(); + const offset = selection.getAnchorOffset(); + const charAhead = content.getBlockForKey(key).getText()[offset]; + return moveSelectionForward( + strategyState, + charAhead ? UnicodeUtils.getUTF16Length(charAhead, 0) : 1, + ); + }, + 'forward', + ), + { + anchorKey: 'D', + anchorOffset: contentBlockNodes[3].getLength(), + focusKey: 'D', + focusOffset: contentBlockNodes[3].getLength(), + }, + ); +}); + +test(`at end of a leaf block and sibling is not another leaf block forward delete is no-op`, () => { + // no next sibling + assertRemoveTextOperation( + editorState => + removeTextWithStrategy( + editorState, + strategyState => { + const selection = strategyState.getSelection(); + const content = strategyState.getCurrentContent(); + const key = selection.getAnchorKey(); + const offset = selection.getAnchorOffset(); + const charAhead = content.getBlockForKey(key).getText()[offset]; + return moveSelectionForward( + strategyState, + charAhead ? UnicodeUtils.getUTF16Length(charAhead, 0) : 1, + ); + }, + 'forward', + ), + { + anchorKey: 'E', + anchorOffset: contentBlockNodes[4].getLength(), + focusKey: 'E', + focusOffset: contentBlockNodes[4].getLength(), + }, + ); + // next sibling is not a leaf + assertRemoveTextOperation( + editorState => + removeTextWithStrategy( + editorState, + strategyState => { + const selection = strategyState.getSelection(); + const content = strategyState.getCurrentContent(); + const key = selection.getAnchorKey(); + const offset = selection.getAnchorOffset(); + const charAhead = content.getBlockForKey(key).getText()[offset]; + return moveSelectionForward( + strategyState, + charAhead ? UnicodeUtils.getUTF16Length(charAhead, 0) : 1, + ); + }, + 'forward', + ), + { + anchorKey: 'E', + anchorOffset: contentBlockNodes[4].getLength(), + focusKey: 'E', + focusOffset: contentBlockNodes[4].getLength(), + }, + ); +}); + +test(`across blocks with forward delete is a no-op`, () => { + assertRemoveTextOperation( + editorState => + removeTextWithStrategy( + editorState, + strategyState => { + const selection = strategyState.getSelection(); + const content = strategyState.getCurrentContent(); + const key = selection.getAnchorKey(); + const offset = selection.getAnchorOffset(); + const charAhead = content.getBlockForKey(key).getText()[offset]; + return moveSelectionForward( + strategyState, + charAhead ? UnicodeUtils.getUTF16Length(charAhead, 0) : 1, + ); + }, + 'forward', + ), + { + anchorKey: 'D', + anchorOffset: contentBlockNodes[3].getLength(), + focusKey: 'E', + focusOffset: contentBlockNodes[4].getLength(), + }, + ); +}); diff --git a/src/component/handlers/edit/commands/removeTextWithStrategy.js b/src/component/handlers/edit/commands/removeTextWithStrategy.js index 219dacaf05..b21e432ad9 100644 --- a/src/component/handlers/edit/commands/removeTextWithStrategy.js +++ b/src/component/handlers/edit/commands/removeTextWithStrategy.js @@ -19,6 +19,10 @@ import type SelectionState from 'SelectionState'; const DraftModifier = require('DraftModifier'); +const gkx = require('gkx'); + +const experimentalTreeDataSupport = gkx('draft_tree_data_support'); + /** * For a collapsed selection state, remove text based on the specified strategy. * If the selection state is not collapsed, remove the entire selected range. @@ -31,11 +35,38 @@ function removeTextWithStrategy( const selection = editorState.getSelection(); const content = editorState.getCurrentContent(); let target = selection; + const anchorKey = selection.getAnchorKey(); + const focusKey = selection.getFocusKey(); + const anchorBlock = content.getBlockForKey(anchorKey); + if (experimentalTreeDataSupport) { + if (direction === 'forward') { + if (anchorKey !== focusKey) { + // For now we ignore forward delete across blocks, + // if there is demand for this we will implement it. + return content; + } + } + } if (selection.isCollapsed()) { if (direction === 'forward') { if (editorState.isSelectionAtEndOfContent()) { return content; } + if (experimentalTreeDataSupport) { + const isAtEndOfBlock = + selection.getAnchorOffset() === + content.getBlockForKey(anchorKey).getLength(); + if (isAtEndOfBlock) { + const anchorBlockSibling = content.getBlockForKey( + anchorBlock.nextSibling, + ); + if (!anchorBlockSibling || anchorBlockSibling.getLength() === 0) { + // For now we ignore forward delete at the end of a block, + // if there is demand for this we will implement it. + return content; + } + } + } } else if (editorState.isSelectionAtStartOfContent()) { return content; } diff --git a/src/model/modifier/exploration/__tests__/NestedRichTextEditorUtil-test.js b/src/model/modifier/exploration/__tests__/NestedRichTextEditorUtil-test.js index 3793de35f2..b447bbb999 100644 --- a/src/model/modifier/exploration/__tests__/NestedRichTextEditorUtil-test.js +++ b/src/model/modifier/exploration/__tests__/NestedRichTextEditorUtil-test.js @@ -250,6 +250,7 @@ test('toggleBlockType does not handle nesting enabled blocks with same blockType * should become * unordered-list > h1 */ +// TODO (T32099101) test('toggleBlockType should change parent block type when changing type for same tag element', () => { expect(true).toBe(true); }); @@ -264,6 +265,7 @@ test('toggleBlockType should change parent block type when changing type for sam * should become * blockquote > ordered-list-item > unstyled */ +// TODO (T32099101) test('toggleBlockType with ranged selection should retain parent type and create a new nested block with text from parent', () => { expect(true).toBe(true); }); @@ -299,22 +301,37 @@ test('onBackspace removes a preceding atomic block', () => { }); }); +// TODO (T32099101) test('onBackspace on the start of a leaf unstyled block should remove block and merge text to previous leaf', () => { expect(true).toBe(true); }); -test('onDelete does not handle if it is the last block on the blockMap', () => { - expect(true).toBe(true); -}); - -test('onDelete does not handle if the next block has no children', () => { - expect(true).toBe(true); +test('onDelete is a no-op if its at the end of the blockMap', () => { + const lastContentBlock = contentBlockNodes[contentBlockNodes.length - 1]; + const lastContentBlockKey = lastContentBlock.getKey(); + const endOfLastContentBlock = lastContentBlock.getLength(); + assertNestedUtilOperation(editorState => onDelete(editorState), { + anchorKey: lastContentBlockKey, + anchorOffset: endOfLastContentBlock, + focusKey: lastContentBlockKey, + focusOffset: lastContentBlockKey, + }); }); -test('onDelete on the end of a leaf block should remove block and merge text to previous leaf', () => { - expect(true).toBe(true); +// NOTE: We may implement this in the future +test('onDelete is a no-op the end of a leaf', () => { + const someLeafBlock = contentBlockNodes[3]; + const someLeafBlockKey = someLeafBlock.getKey(); + const endOfSomeLeafBlock = someLeafBlock.getLength(); + assertNestedUtilOperation(editorState => onDelete(editorState), { + anchorKey: someLeafBlockKey, + anchorOffset: endOfSomeLeafBlock, + focusKey: someLeafBlockKey, + focusOffset: endOfSomeLeafBlock, + }); }); +// TODO (T32099101) test('onSplitParent must split a nested block retaining parent', () => { expect(true).toBe(true); }); diff --git a/src/model/modifier/exploration/__tests__/__snapshots__/NestedRichTextEditorUtil-test.js.snap b/src/model/modifier/exploration/__tests__/__snapshots__/NestedRichTextEditorUtil-test.js.snap index 4df14ec433..61ef98bc39 100644 --- a/src/model/modifier/exploration/__tests__/__snapshots__/NestedRichTextEditorUtil-test.js.snap +++ b/src/model/modifier/exploration/__tests__/__snapshots__/NestedRichTextEditorUtil-test.js.snap @@ -507,6 +507,10 @@ exports[`onDelete does not handle non-block-end or non-collapsed selections 1`] exports[`onDelete does not handle non-block-end or non-collapsed selections 2`] = `true`; +exports[`onDelete is a no-op if its at the end of the blockMap 1`] = `null`; + +exports[`onDelete is a no-op the end of a leaf 1`] = `null`; + exports[`onDelete removes a following atomic block 1`] = `false`; exports[`onDelete removes a following atomic block 2`] = `true`;