Skip to content
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

Remove empty div Between list when backspacing. #1365

Merged
merged 10 commits into from
Oct 28, 2022
Original file line number Diff line number Diff line change
@@ -47,6 +47,7 @@ const EditFeatureDescriptionMap: Record<keyof ContentEditFeatureSettings, string
'When press space after *, -, --, ->, -->, >, => 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 {
Original file line number Diff line number Diff line change
@@ -18,6 +18,8 @@ import {
createVListFromRegion,
isBlockElement,
cacheGetEventData,
safeInstanceOf,
VList,
createObjectDefinition,
createNumberDefinition,
getMetadata,
@@ -33,10 +35,14 @@ import {
RegionBase,
ListType,
ExperimentalFeatures,
PositionType,
NumberingListType,
BulletListType,
} from 'roosterjs-editor-types';

const PREVIOUS_BLOCK_CACHE_KEY = 'previousBlock';
const NEXT_BLOCK_CACHE_KEY = 'nextBlock';

interface ListStyleMetadata {
orderedStyleType?: NumberingListType;
unorderedStyleType?: BulletListType;
@@ -456,6 +462,86 @@ function shouldTriggerList(
);
}

/**
* MergeListOnBackspaceAfterList edit feature, provides the ability to merge list on backspace on block after a list.
*/
const MergeListOnBackspaceAfterList: BuildInEditFeature<PluginKeyboardEvent> = {
keys: [Keys.BACKSPACE],
shouldHandleEvent: (event, editor) => {
const target = editor.getElementAtCursor();
if (target) {
const cursorBlock = editor.getBlockElementAtNode(target)?.getStartNode() as HTMLElement;
const previousBlock = cursorBlock?.previousElementSibling ?? null;

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) &&
getTagOfNode(previousBlock) == getTagOfNode(nextBlock)
) {
const element = cacheGetEventData<HTMLOListElement | HTMLUListElement>(
event,
PREVIOUS_BLOCK_CACHE_KEY,
() => previousBlock
);
const nextElement = cacheGetEventData<HTMLOListElement | HTMLUListElement>(
event,
NEXT_BLOCK_CACHE_KEY,
() => nextBlock
);

return !!element && !!nextElement;
}
}
}
}

return false;
},
handleEvent: (event, editor) => {
editor.runAsync(editor => {
const previousList = cacheGetEventData<HTMLOListElement | HTMLUListElement | null>(
event,
PREVIOUS_BLOCK_CACHE_KEY,
() => null
);
const targetBlock = cacheGetEventData<HTMLOListElement | HTMLUListElement | null>(
event,
NEXT_BLOCK_CACHE_KEY,
() => null
);

const rangeBeforeWriteBack = editor.getSelectionRange();

if (previousList && targetBlock && rangeBeforeWriteBack) {
const fvList = new VList(previousList);
fvList.mergeVList(new VList(targetBlock));
BryanValverdeU marked this conversation as resolved.
Show resolved Hide resolved

let span = editor.getDocument().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
*/
@@ -473,4 +559,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'))
);
}
Original file line number Diff line number Diff line change
@@ -648,3 +648,103 @@ 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(
`<div><ol><li><span>123</span></li></ol><div id=${ITEM_1}><br></div><ol start="2"><li><span>213</span></li></ol><div><br></div></div>`,
true
);
});

it('Should not handle event, lists have different TagName', () => {
runTestShouldHandleEvent(
`<div><ul><li><span>123</span></li></ul><div id=${ITEM_1}><br></div><ol start="2"><li><span>213</span></li></ol><div><br></div></div>`,
false
);
});

it('should not handle event', () => {
runTestShouldHandleEvent(`<span id="${ITEM_1}">1</span>`, false);
});

it('should handle editor async', () => {
runTestHandleEvent(
`<div><ol><li><span>123</span></li></ol><div id=${ITEM_1}><br></div><ol start="2"><li><span>213</span></li></ol><div><br></div></div>`
);
});
});
Original file line number Diff line number Diff line change
@@ -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;
}

/**