Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
getAllEntityWrappers,
isBlockEntityContainer,
isEntityDelimiter,
isEntityElement,
isNodeOfType,
parseEntityFormat,
Expand All @@ -25,6 +26,15 @@ export function restoreSnapshotHTML(core: EditorCore, snapshot: Snapshot) {
const originalEntityElement = tryGetEntityElement(entityMap, currentNode);

if (originalEntityElement) {
// After restoring the snapshot, we need to clear the delimiter indexes since cached model will be cleared
if (isBlockEntityContainer(originalEntityElement)) {
for (let node = originalEntityElement.firstChild; node; node = node.nextSibling) {
if (isNodeOfType(node, 'ELEMENT_NODE') && isEntityDelimiter(node)) {
core.cache.domIndexer?.clearIndex(node);
}
}
}

refNode = reuseCachedElement(physicalRoot, originalEntityElement, refNode);
} else {
physicalRoot.insertBefore(currentNode, refNode);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,10 @@ export class DomIndexerImpl implements DomIndexer {
}
}

clearIndex(container: Node) {
internalClearIndex(container);
}

reconcileSelection(
model: ContentModelDocument,
newSelection: DOMSelection,
Expand Down Expand Up @@ -711,3 +715,11 @@ function getFirstLeaf(node: Node | null): Node | null {

return node;
}

function internalClearIndex(container: Node) {
unindex(container as IndexedSegmentNode);

for (let node = container.firstChild; node; node = node.nextSibling) {
internalClearIndex(node);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -213,20 +213,13 @@ export function handleDelimiterKeyDownEvent(editor: IEditor, event: KeyDownEvent

switch (key) {
case 'Enter':
if (range.collapsed) {
handleInputOnDelimiter(editor, range, getFocusedElement(selection), rawEvent);
} else {
const helper = editor.getDOMHelper();
const entity = findClosestEntityWrapper(range.startContainer, helper);

if (
entity &&
isNodeOfType(entity, 'ELEMENT_NODE') &&
helper.isNodeInEditor(entity)
) {
triggerEntityEventOnEnter(editor, entity, rawEvent);
}
const helper = editor.getDOMHelper();
const entity = findClosestEntityWrapper(range.startContainer, helper);

if (entity && isNodeOfType(entity, 'ELEMENT_NODE') && helper.isNodeInEditor(entity)) {
triggerEntityEventOnEnter(editor, entity, rawEvent);
}

break;

case 'ArrowLeft':
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1157,6 +1157,53 @@ describe('restoreSnapshotHTML', () => {
expect(div.childNodes[1]).toBe(container2);
expect(div.childNodes[1].firstChild).toBe(entityWrapper2);
});

it('HTML with block entity at root level, need to clear index of entity delimiter', () => {
const snapshot: Snapshot = {
html:
'<div class="_E_EBlockEntityContainer"><span class="entityDelimiterBefore">\u200B</span><div class="_Entity _EType_A _EId_B1"><br></div><span class="entityDelimiterAfter">\u200B</span></div>',
} as any;

const entityWrapper1 = document.createElement('DIV');
const delimiterBefore = document.createElement('span');
const delimiterAfter = document.createElement('span');
const wrapper = document.createElement('div');

delimiterBefore.className = 'entityDelimiterBefore';
delimiterBefore.innerHTML = '\u200B';
delimiterAfter.className = 'entityDelimiterAfter';
delimiterAfter.innerHTML = '\u200B';
entityWrapper1.id = 'div1';

wrapper.className = '_E_EBlockEntityContainer';
wrapper.appendChild(delimiterBefore);
wrapper.appendChild(entityWrapper1);
wrapper.appendChild(delimiterAfter);

div.appendChild(wrapper);

core.entity.entityMap.B1 = {
element: entityWrapper1,
canPersist: true,
};

const clearIndexSpy = jasmine.createSpy('clearIndex');
core.cache = {
domIndexer: {
clearIndex: clearIndexSpy,
} as any,
};

restoreSnapshotHTML(core, snapshot);

expect(div.innerHTML).toBe(
'<div class="_E_EBlockEntityContainer"><span class="entityDelimiterBefore">\u200B</span><div id="div1"></div><span class="entityDelimiterAfter">\u200B</span></div>'
);
expect(div.childNodes[0]).toBe(wrapper);
expect(clearIndexSpy).toHaveBeenCalledTimes(2);
expect(clearIndexSpy).toHaveBeenCalledWith(delimiterBefore);
expect(clearIndexSpy).toHaveBeenCalledWith(delimiterAfter);
});
});

function wrapInContainer(entity: HTMLElement) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1503,3 +1503,34 @@ describe('domIndexerImpl.reconcileElementId', () => {
});
});
});

describe('domIndexerImpl.clearIndex', () => {
it('clear index, no child', () => {
const domIndexer = new DomIndexerImpl(true);

const parent = document.createElement('span');

((parent as any) as IndexedSegmentNode).__roosterjsContentModel = {} as any;

domIndexer.clearIndex(parent);

expect(((parent as Node) as IndexedSegmentNode).__roosterjsContentModel).toBeUndefined();
});

it('clear index, with child', () => {
const domIndexer = new DomIndexerImpl(true);

const parent = document.createElement('span');
const node = document.createTextNode('test');

((parent as any) as IndexedSegmentNode).__roosterjsContentModel = {} as any;
((node as any) as IndexedSegmentNode).__roosterjsContentModel = {} as any;

parent.appendChild(node);

domIndexer.clearIndex(parent);

expect(((node as Node) as IndexedSegmentNode).__roosterjsContentModel).toBeUndefined();
expect(((parent as Node) as IndexedSegmentNode).__roosterjsContentModel).toBeUndefined();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -451,116 +451,6 @@ describe('EntityDelimiterUtils |', () => {
);
});

it('Handle, range selection & delimiter before wrapped in block entity | Enter Key', () => {
const div = document.createElement('div');
const parent = document.createElement('span');
const el = document.createElement('span');
const text = document.createTextNode('span');
el.appendChild(text);
parent.appendChild(el);
el.classList.add('entityDelimiterBefore');
div.classList.add(BlockEntityContainer);
div.appendChild(parent);

const setStartBeforeSpy = jasmine.createSpy('setStartBeforeSpy');
const setStartAfterSpy = jasmine.createSpy('setStartAfterSpy');
const collapseSpy = jasmine.createSpy('collapseSpy');
const preventDefaultSpy = jasmine.createSpy('preventDefaultSpy');

mockedSelection = {
type: 'range',
range: <any>{
endContainer: text,
endOffset: 0,
collapsed: true,
setStartAfter: setStartAfterSpy,
setStartBefore: setStartBeforeSpy,
collapse: collapseSpy,
},
isReverted: false,
};
spyOn(entityUtils, 'isEntityDelimiter').and.returnValue(true);
spyOn(entityUtils, 'isEntityElement').and.returnValue(false);

handleDelimiterKeyDownEvent(mockedEditor, {
eventType: 'keyDown',
rawEvent: <any>{
ctrlKey: false,
altKey: false,
metaKey: false,
key: 'Enter',
preventDefault: preventDefaultSpy,
},
});

expect(rafSpy).not.toHaveBeenCalled();
expect(preventDefaultSpy).toHaveBeenCalled();
expect(setStartAfterSpy).not.toHaveBeenCalled();
expect(setStartBeforeSpy).toHaveBeenCalled();
expect(collapseSpy).toHaveBeenCalled();
expect(formatContentModelSpy).toHaveBeenCalledWith(
DelimiterFile.handleKeyDownInBlockDelimiter,
{
selectionOverride: mockedSelection,
}
);
});

it('Handle, range selection & delimiter after wrapped in block entity | Enter key', () => {
const div = document.createElement('div');
const parent = document.createElement('span');
const el = document.createElement('span');
const text = document.createTextNode('span');
el.appendChild(text);
parent.appendChild(el);
el.classList.add('entityDelimiterAfter');
div.classList.add(BlockEntityContainer);
div.appendChild(parent);

const setStartBeforeSpy = jasmine.createSpy('setStartBeforeSpy');
const setStartAfterSpy = jasmine.createSpy('setStartAfterSpy');
const collapseSpy = jasmine.createSpy('collapseSpy');
const preventDefaultSpy = jasmine.createSpy('preventDefaultSpy');

mockedSelection = {
type: 'range',
range: <any>{
endContainer: text,
endOffset: 0,
collapsed: true,
setStartAfter: setStartAfterSpy,
setStartBefore: setStartBeforeSpy,
collapse: collapseSpy,
},
isReverted: false,
};
spyOn(entityUtils, 'isEntityDelimiter').and.returnValue(true);
spyOn(entityUtils, 'isEntityElement').and.returnValue(false);

handleDelimiterKeyDownEvent(mockedEditor, {
eventType: 'keyDown',
rawEvent: <any>{
ctrlKey: false,
altKey: false,
metaKey: false,
key: 'Enter',
preventDefault: preventDefaultSpy,
},
});

expect(rafSpy).not.toHaveBeenCalled();
expect(preventDefaultSpy).toHaveBeenCalled();
expect(setStartAfterSpy).toHaveBeenCalled();
expect(setStartBeforeSpy).not.toHaveBeenCalled();
expect(collapseSpy).toHaveBeenCalled();
expect(formatContentModelSpy).toHaveBeenCalledWith(
DelimiterFile.handleKeyDownInBlockDelimiter,
{
selectionOverride: mockedSelection,
}
);
});

it('Handle, range expanded selection | EnterKey', () => {
const div = document.createElement('div');
const parent = document.createElement('span');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { createParagraph } from '../../modelApi/creators/createParagraph';
import { formatContainerProcessor } from './formatContainerProcessor';
import { getDefaultStyle } from '../utils/getDefaultStyle';
import { isBlockElement } from '../utils/isBlockElement';
import { isBlockEntityContainer } from '../../domUtils/entityUtils';
import { parseFormat } from '../utils/parseFormat';
import { stackFormat } from '../utils/stackFormat';
import type {
Expand Down Expand Up @@ -45,6 +46,8 @@ export const knownElementProcessor: ElementProcessor<HTMLElement> = (group, elem
shouldUseFormatContainer(element, context)
) {
formatContainerProcessor(group, element, context);
} else if (isBlockEntityContainer(element)) {
context.elementProcessors.child(group, element, context);
} else if (isBlock) {
const decorator = context.blockDecorator.tagName ? context.blockDecorator : undefined;
const isSegmentDecorator = SegmentDecoratorTags.indexOf(element.tagName) >= 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ describe('brProcessor', () => {
onBlockEntity: null!,
reconcileElementId: null!,
onMergeText: null!,
clearIndex: null!,
};

context.domIndexer = domIndexer;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ describe('entityProcessor', () => {
onBlockEntity: null!,
reconcileElementId: null!,
onMergeText: null!,
clearIndex: null!,
};

context.domIndexer = domIndexer;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,7 @@ describe('generalProcessor', () => {
onBlockEntity: null!,
reconcileElementId: null!,
onMergeText: null!,
clearIndex: null!,
};

context.domIndexer = domIndexer;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,7 @@ describe('imageProcessor', () => {
onBlockEntity: null!,
reconcileElementId: null!,
onMergeText: null!,
clearIndex: null!,
};

context.domIndexer = domIndexer;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as blockProcessor from '../../../lib/domToModel/processors/blockProcessor';
import * as formatContainerProcessor from '../../../lib/domToModel/processors/formatContainerProcessor';
import * as parseFormat from '../../../lib/domToModel/utils/parseFormat';
import { createContentModelDocument } from '../../../lib/modelApi/creators/createContentModelDocument';
Expand Down Expand Up @@ -635,4 +636,27 @@ describe('knownElementProcessor', () => {
],
});
});

it('Block entity container', () => {
const group = createContentModelDocument();
const div = document.createElement('div');

div.className = '_E_EBlockEntityContainer';

spyOn(parseFormat, 'parseFormat');
const childProcessorSpy = spyOn(context.elementProcessors, 'child');
const blockProcessorSpy = spyOn(blockProcessor, 'blockProcessor').and.callThrough();

knownElementProcessor(group, div, context);

expect(group).toEqual({
blockGroupType: 'Document',

blocks: [],
});

expect(parseFormat.parseFormat).toHaveBeenCalledTimes(0);
expect(childProcessorSpy).toHaveBeenCalledWith(group, div, context);
expect(blockProcessorSpy).not.toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,7 @@ describe('tableProcessor', () => {
onBlockEntity: null!,
reconcileElementId: null!,
onMergeText: null!,
clearIndex: null!,
};

context.domIndexer = domIndexer;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,7 @@ describe('textProcessor', () => {
onBlockEntity: null!,
reconcileElementId: null!,
onMergeText: null!,
clearIndex: null!,
};

context.domIndexer = domIndexer;
Expand Down Expand Up @@ -619,6 +620,7 @@ describe('textProcessor', () => {
onBlockEntity: null!,
reconcileElementId: null!,
onMergeText: null!,
clearIndex: null!,
};

context.domIndexer = domIndexer;
Expand Down Expand Up @@ -668,6 +670,7 @@ describe('textProcessor', () => {
onBlockEntity: null!,
reconcileElementId: null!,
onMergeText: null!,
clearIndex: null!,
};

context.domIndexer = domIndexer;
Expand Down
Loading
Loading