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
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import {
createVListFromRegion,
isBlockElement,
cacheGetEventData,
safeInstanceOf,
VList,
createObjectDefinition,
createNumberDefinition,
getMetadata,
Expand All @@ -33,6 +35,7 @@ import {
RegionBase,
ListType,
ExperimentalFeatures,
PositionType,
NumberingListType,
BulletListType,
} from 'roosterjs-editor-types';
Expand Down Expand Up @@ -456,6 +459,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,
'previousBlock',
() => previousBlock
);
const nextElement = cacheGetEventData<HTMLOListElement | HTMLUListElement>(
event,
'nextBlock',
() => nextBlock
);

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

return false;
},
handleEvent: (event, editor) => {
editor.runAsync(editor => {
const previousList = cacheGetEventData<HTMLOListElement | HTMLUListElement | null>(
event,
'previousBlock',
BryanValverdeU marked this conversation as resolved.
Show resolved Hide resolved
() => null
);
const targetBlock = cacheGetEventData<HTMLOListElement | HTMLUListElement | null>(
event,
'nextBlock',
BryanValverdeU marked this conversation as resolved.
Show resolved Hide resolved
() => 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 = document.createElement('span');
BryanValverdeU marked this conversation as resolved.
Show resolved Hide resolved
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
*/
Expand All @@ -473,4 +556,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
Expand Up @@ -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
Expand Up @@ -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;
}

/**
Expand Down