From 999a54ca3c1f6455c892230bc7eaf498b82207b9 Mon Sep 17 00:00:00 2001 From: "SOUTHAMERICA\\bvalverde" Date: Wed, 26 Oct 2022 19:07:55 -0600 Subject: [PATCH 1/7] init --- .../editorOptions/ContentEditFeatures.tsx | 1 + .../ContentEdit/features/listFeatures.ts | 80 ++++++++++++++++ .../ContentEdit/features/listFeaturesTest.ts | 94 +++++++++++++++++++ .../interface/ContentEditFeatureSettings.ts | 6 ++ 4 files changed, 181 insertions(+) diff --git a/demo/scripts/controls/sidePane/editorOptions/ContentEditFeatures.tsx b/demo/scripts/controls/sidePane/editorOptions/ContentEditFeatures.tsx index 761c6440fdc..5b5fd35382f 100644 --- a/demo/scripts/controls/sidePane/editorOptions/ContentEditFeatures.tsx +++ b/demo/scripts/controls/sidePane/editorOptions/ContentEditFeatures.tsx @@ -47,6 +47,7 @@ const EditFeatureDescriptionMap: Record, -->, >, => in an empty line, toggle bullet', autoNumberingList: 'When press space after an number, a letter or roman number followed by ), ., -, or between parenthesis in an empty line, toggle numbering', + mergeListOnBackspaceAfterList: 'When backspacing between lists, merge the lists', }; export interface ContentEditFeaturessProps { diff --git a/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/listFeatures.ts b/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/listFeatures.ts index 1ecb8718c2c..157d23c6875 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/listFeatures.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/listFeatures.ts @@ -1,5 +1,6 @@ import getAutoBulletListStyle from '../utils/getAutoBulletListStyle'; import getAutoNumberingListStyle from '../utils/getAutoNumberingListStyle'; +import StartEndBlockElement from 'roosterjs-editor-dom/lib/blockElements/StartEndBlockElement'; import { blockFormat, commitListChains, @@ -18,6 +19,8 @@ import { createVListFromRegion, isBlockElement, cacheGetEventData, + safeInstanceOf, + VList, } from 'roosterjs-editor-dom'; import { BuildInEditFeature, @@ -30,6 +33,7 @@ import { RegionBase, ListType, ExperimentalFeatures, + PositionType, } from 'roosterjs-editor-types'; /** @@ -386,6 +390,74 @@ function shouldTriggerList( ); } +/** + * MergeListOnBackspaceAfterList edit feature, provides the ability to merge list on backspace on block after a list. + */ +const MergeListOnBackspaceAfterList: BuildInEditFeature = { + keys: [Keys.BACKSPACE], + shouldHandleEvent: (event, editor) => { + const target = editor.getElementAtCursor(); + const range = editor.getSelectionRange(); + if (range && range.collapsed && target) { + const cursorBlock = StartEndBlockElement.getBlockContext(target); + const previousBlock = cursorBlock?.previousElementSibling ?? null; + + const tempBlock = cursorBlock?.nextElementSibling; + const nextBlock = isList(tempBlock) ? tempBlock : tempBlock?.firstChild; + + if (isList(previousBlock) && isList(nextBlock)) { + const element = cacheGetEventData( + event, + 'previousBlock', + () => previousBlock + ); + const nextElement = cacheGetEventData( + event, + 'nextBlock', + () => nextBlock + ); + + return !!element && !!nextElement; + } + } + return false; + }, + handleEvent: (event, editor) => { + editor.runAsync(editor => { + const previousList = cacheGetEventData( + event, + 'previousBlock', + () => null + ); + const targetBlock = cacheGetEventData( + event, + 'nextBlock', + () => null + ); + + const rangeBeforeWriteBack = editor.getSelectionRange(); + + if (previousList && targetBlock && rangeBeforeWriteBack) { + const fvList = new VList(previousList); + fvList.mergeVList(new VList(targetBlock)); + + let span = document.createElement('span'); + span.id = 'restoreRange'; + rangeBeforeWriteBack.insertNode(span); + + fvList.writeBack(); + + span = editor.queryElements('#restoreRange')[0]; + + if (span.parentElement) { + editor.select(new Position(span, PositionType.After)); + span.parentElement.removeChild(span); + } + } + }); + }, +}; + /** * @internal */ @@ -403,4 +475,12 @@ export const ListFeatures: Record< maintainListChainWhenDelete: MaintainListChainWhenDelete, autoNumberingList: AutoNumberingList, autoBulletList: AutoBulletList, + mergeListOnBackspaceAfterList: MergeListOnBackspaceAfterList, }; + +function isList(element: Node | null | undefined): element is HTMLOListElement | HTMLOListElement { + return ( + !!element && + (safeInstanceOf(element, 'HTMLOListElement') || safeInstanceOf(element, 'HTMLUListElement')) + ); +} diff --git a/packages/roosterjs-editor-plugins/test/ContentEdit/features/listFeaturesTest.ts b/packages/roosterjs-editor-plugins/test/ContentEdit/features/listFeaturesTest.ts index 7fd749a1e97..e8a596c5eb1 100644 --- a/packages/roosterjs-editor-plugins/test/ContentEdit/features/listFeaturesTest.ts +++ b/packages/roosterjs-editor-plugins/test/ContentEdit/features/listFeaturesTest.ts @@ -648,3 +648,97 @@ describe('listFeatures | MaintainListChain', () => { ); }); }); + +describe('listFeatures | mergeListOnBackspaceAfterList', () => { + let editor: IEditor; + const TEST_ID = 'listFeatureTests'; + const ITEM_1 = 'ITEM_1'; + const getKeyboardEvent = (shiftKey: boolean) => + new KeyboardEvent('keydown', { + shiftKey, + altKey: false, + ctrlKey: false, + }); + + beforeEach(() => { + editor = TestHelper.initEditor(TEST_ID); + + editor.runAsync = callback => { + callback(editor); + return () => {}; + }; + }); + + afterEach(() => { + let element = document.getElementById(TEST_ID); + if (element) { + element.parentElement?.removeChild(element); + } + editor.dispose(); + }); + + function runTestShouldHandleEvent(content: string, shouldHandle: boolean) { + editor.setContent(content); + editor.focus(); + const item = editor.getDocument().getElementById(ITEM_1); + if (item) { + const range = document.createRange(); + range.setStart(item, 0); + editor.select(range); + } + + const keyboardEvent: PluginKeyboardEvent = { + eventType: PluginEventType.KeyDown, + rawEvent: getKeyboardEvent(false), + }; + + const triggered = ListFeatures.mergeListOnBackspaceAfterList.shouldHandleEvent( + keyboardEvent, + editor, + false + ) + ? true + : false; + expect(triggered).toBe(shouldHandle); + } + + function runTestHandleEvent(content: string) { + editor.setContent(content); + editor.focus(); + const item = editor.getDocument().getElementById(ITEM_1); + if (item) { + const range = document.createRange(); + range.setStart(item, 0); + range.collapse(); + editor.select(range); + } + const keyboardEvent: PluginKeyboardEvent = { + eventType: PluginEventType.KeyDown, + rawEvent: getKeyboardEvent(false), + }; + + ListFeatures.mergeListOnBackspaceAfterList.shouldHandleEvent(keyboardEvent, editor, false); + item?.parentElement?.removeChild(item); + ListFeatures.mergeListOnBackspaceAfterList.handleEvent(keyboardEvent, editor); + + expect(editor.queryElements('ol,ul').length).toEqual(1); + } + + it('should handle event', () => { + runTestShouldHandleEvent( + `
  1. 123

  1. 213

`, + true + ); + }); + + it('should not handle event', () => { + runTestShouldHandleEvent(`1`, false); + }); + + it('should handle editor async', () => { + debugger; + runTestHandleEvent( + `
  1. 123

  1. 213

` + ); + }); +}); diff --git a/packages/roosterjs-editor-types/lib/interface/ContentEditFeatureSettings.ts b/packages/roosterjs-editor-types/lib/interface/ContentEditFeatureSettings.ts index 8782e8b9f67..4aa43f01db5 100644 --- a/packages/roosterjs-editor-types/lib/interface/ContentEditFeatureSettings.ts +++ b/packages/roosterjs-editor-types/lib/interface/ContentEditFeatureSettings.ts @@ -125,6 +125,12 @@ export interface ListFeatureSettings { * @default true */ autoNumberingList: boolean; + + /** + * MergeListOnBackspaceAfterList edit feature, provides the ability to merge list on backspace on block after a list. + * @default true + */ + mergeListOnBackspaceAfterList: boolean; } /** From 6ee97560b92a9ddb368925ce0ad0dc9a8c2a2dc4 Mon Sep 17 00:00:00 2001 From: "SOUTHAMERICA\\bvalverde" Date: Wed, 26 Oct 2022 19:33:17 -0600 Subject: [PATCH 2/7] Check if prevElement is List, to avoid perf issue --- .../ContentEdit/features/listFeatures.ts | 45 +++++++++++-------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/listFeatures.ts b/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/listFeatures.ts index 157d23c6875..084b6edb82e 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/listFeatures.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/listFeatures.ts @@ -397,29 +397,38 @@ const MergeListOnBackspaceAfterList: BuildInEditFeature = { keys: [Keys.BACKSPACE], shouldHandleEvent: (event, editor) => { const target = editor.getElementAtCursor(); - const range = editor.getSelectionRange(); - if (range && range.collapsed && target) { + if (target) { const cursorBlock = StartEndBlockElement.getBlockContext(target); const previousBlock = cursorBlock?.previousElementSibling ?? null; - const tempBlock = cursorBlock?.nextElementSibling; - const nextBlock = isList(tempBlock) ? tempBlock : tempBlock?.firstChild; - - if (isList(previousBlock) && isList(nextBlock)) { - const element = cacheGetEventData( - event, - 'previousBlock', - () => previousBlock - ); - const nextElement = cacheGetEventData( - event, - 'nextBlock', - () => nextBlock - ); - - return !!element && !!nextElement; + if (isList(previousBlock)) { + const range = editor.getSelectionRange(); + const searcher = editor.getContentSearcherOfCursor(event); + const textBeforeCursor = searcher?.getSubStringBefore(4); + const nearestInline = searcher?.getNearestNonTextInlineElement(); + + if (range && range.collapsed && textBeforeCursor === '' && !nearestInline) { + const tempBlock = cursorBlock?.nextElementSibling; + const nextBlock = isList(tempBlock) ? tempBlock : tempBlock?.firstChild; + + if (isList(nextBlock)) { + const element = cacheGetEventData( + event, + 'previousBlock', + () => previousBlock + ); + const nextElement = cacheGetEventData( + event, + 'nextBlock', + () => nextBlock + ); + + return !!element && !!nextElement; + } + } } } + return false; }, handleEvent: (event, editor) => { From a6d173af7d83d712f469c07a30b2b213e1d1a1c0 Mon Sep 17 00:00:00 2001 From: "SOUTHAMERICA\\bvalverde" Date: Wed, 26 Oct 2022 19:36:24 -0600 Subject: [PATCH 3/7] remove debugger --- .../test/ContentEdit/features/listFeaturesTest.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/roosterjs-editor-plugins/test/ContentEdit/features/listFeaturesTest.ts b/packages/roosterjs-editor-plugins/test/ContentEdit/features/listFeaturesTest.ts index e8a596c5eb1..32273f2b788 100644 --- a/packages/roosterjs-editor-plugins/test/ContentEdit/features/listFeaturesTest.ts +++ b/packages/roosterjs-editor-plugins/test/ContentEdit/features/listFeaturesTest.ts @@ -736,7 +736,6 @@ describe('listFeatures | mergeListOnBackspaceAfterList', () => { }); it('should handle editor async', () => { - debugger; runTestHandleEvent( `
  1. 123

  1. 213

` ); From 01ef87bead31d2fc48314aa76a99f8e5873cbe5c Mon Sep 17 00:00:00 2001 From: "SOUTHAMERICA\\bvalverde" Date: Wed, 26 Oct 2022 21:02:20 -0600 Subject: [PATCH 4/7] Fix dependency iss --- .../lib/plugins/ContentEdit/features/listFeatures.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/listFeatures.ts b/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/listFeatures.ts index 084b6edb82e..b095d6c8864 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/listFeatures.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/listFeatures.ts @@ -1,6 +1,5 @@ import getAutoBulletListStyle from '../utils/getAutoBulletListStyle'; import getAutoNumberingListStyle from '../utils/getAutoNumberingListStyle'; -import StartEndBlockElement from 'roosterjs-editor-dom/lib/blockElements/StartEndBlockElement'; import { blockFormat, commitListChains, @@ -398,7 +397,7 @@ const MergeListOnBackspaceAfterList: BuildInEditFeature = { shouldHandleEvent: (event, editor) => { const target = editor.getElementAtCursor(); if (target) { - const cursorBlock = StartEndBlockElement.getBlockContext(target); + const cursorBlock = editor.getBlockElementAtNode(target)?.getStartNode() as HTMLElement; const previousBlock = cursorBlock?.previousElementSibling ?? null; if (isList(previousBlock)) { From 59a070332c20f5e4e3b40107728fd5c400c189bf Mon Sep 17 00:00:00 2001 From: "SOUTHAMERICA\\bvalverde" Date: Thu, 27 Oct 2022 08:16:20 -0600 Subject: [PATCH 5/7] Do not merge if lists have different type --- .../lib/plugins/ContentEdit/features/listFeatures.ts | 5 ++++- .../test/ContentEdit/features/listFeaturesTest.ts | 7 +++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/listFeatures.ts b/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/listFeatures.ts index 1b818ecd579..3adfa50901f 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/listFeatures.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/listFeatures.ts @@ -480,7 +480,10 @@ const MergeListOnBackspaceAfterList: BuildInEditFeature = { const tempBlock = cursorBlock?.nextElementSibling; const nextBlock = isList(tempBlock) ? tempBlock : tempBlock?.firstChild; - if (isList(nextBlock)) { + if ( + isList(nextBlock) && + getTagOfNode(previousBlock) == getTagOfNode(nextBlock) + ) { const element = cacheGetEventData( event, 'previousBlock', diff --git a/packages/roosterjs-editor-plugins/test/ContentEdit/features/listFeaturesTest.ts b/packages/roosterjs-editor-plugins/test/ContentEdit/features/listFeaturesTest.ts index 32273f2b788..ef26d050cce 100644 --- a/packages/roosterjs-editor-plugins/test/ContentEdit/features/listFeaturesTest.ts +++ b/packages/roosterjs-editor-plugins/test/ContentEdit/features/listFeaturesTest.ts @@ -731,6 +731,13 @@ describe('listFeatures | mergeListOnBackspaceAfterList', () => { ); }); + it('Should not handle event, lists have different TagName', () => { + runTestShouldHandleEvent( + `
  • 123

  1. 213

`, + false + ); + }); + it('should not handle event', () => { runTestShouldHandleEvent(`1`, false); }); From ce7e193f9e6e485b9b48f9f98d9e86ab6b93f865 Mon Sep 17 00:00:00 2001 From: "SOUTHAMERICA\\bvalverde" Date: Thu, 27 Oct 2022 11:53:21 -0600 Subject: [PATCH 6/7] use Editor Document --- .../lib/plugins/ContentEdit/features/listFeatures.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/listFeatures.ts b/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/listFeatures.ts index 3adfa50901f..a4fcdf63fa8 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/listFeatures.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/listFeatures.ts @@ -522,7 +522,7 @@ const MergeListOnBackspaceAfterList: BuildInEditFeature = { const fvList = new VList(previousList); fvList.mergeVList(new VList(targetBlock)); - let span = document.createElement('span'); + let span = editor.getDocument().createElement('span'); span.id = 'restoreRange'; rangeBeforeWriteBack.insertNode(span); From 549b68ac3ba07333ee32b8746c99e93827e65389 Mon Sep 17 00:00:00 2001 From: "SOUTHAMERICA\\bvalverde" Date: Thu, 27 Oct 2022 11:55:56 -0600 Subject: [PATCH 7/7] Make strings constant --- .../lib/plugins/ContentEdit/features/listFeatures.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/listFeatures.ts b/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/listFeatures.ts index a4fcdf63fa8..64863163a3d 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/listFeatures.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/listFeatures.ts @@ -40,6 +40,9 @@ import { BulletListType, } from 'roosterjs-editor-types'; +const PREVIOUS_BLOCK_CACHE_KEY = 'previousBlock'; +const NEXT_BLOCK_CACHE_KEY = 'nextBlock'; + interface ListStyleMetadata { orderedStyleType?: NumberingListType; unorderedStyleType?: BulletListType; @@ -486,12 +489,12 @@ const MergeListOnBackspaceAfterList: BuildInEditFeature = { ) { const element = cacheGetEventData( event, - 'previousBlock', + PREVIOUS_BLOCK_CACHE_KEY, () => previousBlock ); const nextElement = cacheGetEventData( event, - 'nextBlock', + NEXT_BLOCK_CACHE_KEY, () => nextBlock ); @@ -507,12 +510,12 @@ const MergeListOnBackspaceAfterList: BuildInEditFeature = { editor.runAsync(editor => { const previousList = cacheGetEventData( event, - 'previousBlock', + PREVIOUS_BLOCK_CACHE_KEY, () => null ); const targetBlock = cacheGetEventData( event, - 'nextBlock', + NEXT_BLOCK_CACHE_KEY, () => null );