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,12 +1,13 @@
import {
addDelimiters,
createElement,
arrayPush,
createRange,
getDelimiterFromElement,
getEntityFromElement,
getEntitySelector,
isBlockElement,
isCharacterValue,
matchesSelector,
Position,
safeInstanceOf,
splitTextNode,
Expand All @@ -29,7 +30,6 @@ const DELIMITER_SELECTOR =
'.' + DelimiterClasses.DELIMITER_AFTER + ',.' + DelimiterClasses.DELIMITER_BEFORE;
const ZERO_WIDTH_SPACE = '\u200B';
const INLINE_ENTITY_SELECTOR = 'span' + getEntitySelector();
const NBSP = '\u00A0';

export function inlineEntityOnPluginEvent(event: PluginEvent, editor: IEditor) {
switch (event.eventType) {
Expand All @@ -43,7 +43,15 @@ export function inlineEntityOnPluginEvent(event: PluginEvent, editor: IEditor) {
break;

case PluginEventType.BeforePaste:
addDelimitersIfNeeded(event.fragment.querySelectorAll(INLINE_ENTITY_SELECTOR));
const { fragment, sanitizingOption } = event;
addDelimitersIfNeeded(fragment.querySelectorAll(INLINE_ENTITY_SELECTOR));

if (sanitizingOption.additionalAllowedCssClasses) {
arrayPush(sanitizingOption.additionalAllowedCssClasses, [
DelimiterClasses.DELIMITER_AFTER,
DelimiterClasses.DELIMITER_BEFORE,
]);
}
break;

case PluginEventType.ExtractContentWithDom:
Expand Down Expand Up @@ -98,13 +106,13 @@ export function normalizeDelimitersInEditor(editor: IEditor) {

function addDelimitersIfNeeded(nodes: Element[] | NodeListOf<Element>) {
nodes.forEach(node => {
if (tryGetEntityFromNode(node)) {
if (isEntityElement(node)) {
addDelimiters(node);
}
});
}

function tryGetEntityFromNode(node: Element | null): node is HTMLElement {
function isEntityElement(node: Node | null): node is HTMLElement {
return !!(
node &&
safeInstanceOf(node, 'HTMLElement') &&
Expand All @@ -124,7 +132,7 @@ function isReadOnly(entity: Entity | null) {
);
}

function removeInvalidDelimiters(nodes: Element[]) {
function removeInvalidDelimiters(nodes: Element[] | NodeListOf<Element>) {
nodes.forEach(node => {
if (getDelimiterFromElement(node)) {
const sibling = node.classList.contains(DelimiterClasses.DELIMITER_BEFORE)
Expand All @@ -139,14 +147,14 @@ function removeInvalidDelimiters(nodes: Element[]) {
});
}

function removeDelimiterAttr(node: Element | undefined | null) {
function removeDelimiterAttr(node: Element | undefined | null, checkEntity: boolean = true) {
if (!node) {
return;
}

const isAfter = node.classList.contains(DelimiterClasses.DELIMITER_AFTER);
const entitySibling = isAfter ? node.previousElementSibling : node.nextElementSibling;
if (entitySibling && tryGetEntityFromNode(entitySibling)) {
if (checkEntity && entitySibling && isEntityElement(entitySibling)) {
return;
}

Expand All @@ -163,39 +171,34 @@ function removeDelimiterAttr(node: Element | undefined | null) {

function handleCollapsedEnter(editor: IEditor, delimiter: HTMLElement) {
const isAfter = delimiter.classList.contains(DelimiterClasses.DELIMITER_AFTER);
const sibling = isAfter ? delimiter.nextSibling : delimiter.previousSibling;
let positionToUse: Position | undefined;
let element: Element | null;

if (sibling) {
positionToUse = new Position(sibling, isAfter ? PositionType.Begin : PositionType.End);
} else {
element = delimiter.insertAdjacentElement(
isAfter ? 'afterend' : 'beforebegin',
createElement(
{
tag: 'span',
children: [NBSP],
},
editor.getDocument()
)!
);
const entity = !isAfter ? delimiter.nextSibling : delimiter.previousSibling;
const block = editor.getBlockElementAtNode(delimiter)?.getStartNode();

if (!element) {
editor.runAsync(() => {
if (!block) {
return;
}
const blockToCheck = isAfter ? block.nextSibling : block.previousSibling;
if (blockToCheck && safeInstanceOf(blockToCheck, 'HTMLElement')) {
const delimiters = blockToCheck.querySelectorAll(DELIMITER_SELECTOR);
// Check if the last or first delimiter still contain the delimiter class and remove it.
const delimiterToCheck = delimiters.item(isAfter ? 0 : delimiters.length - 1);
removeDelimiterAttr(delimiterToCheck);
}

positionToUse = new Position(element, PositionType.Begin);
}

if (positionToUse) {
editor.select(positionToUse);
editor.runAsync(aEditor => {
const elAfter = aEditor.getElementAtCursor();
removeDelimiterAttr(elAfter);
removeNode(element);
});
}
if (isEntityElement(entity)) {
const { nextElementSibling, previousElementSibling } = entity;
[nextElementSibling, previousElementSibling].forEach(el => {
// Check if after Enter the ZWS got removed but we still have a element with the class
// Remove the attributes of the element if it is invalid now.
if (el && matchesSelector(el, DELIMITER_SELECTOR) && !getDelimiterFromElement(el)) {
removeDelimiterAttr(el, false /* checkEntity */);
}
});
// Add delimiters to the entity if needed because on Enter we can sometimes lose the ZWS of the element.
addDelimiters(entity);
}
});
}

const getPosition = (container: HTMLElement | null) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,25 @@ import * as splitTextNode from 'roosterjs-editor-dom/lib/utils/splitTextNode';
import { inlineEntityOnPluginEvent } from '../../lib/corePlugins/utils/inlineEntityOnPluginEvent';
import {
BeforeCutCopyEvent,
EditorReadyEvent,
BeforePasteEvent,
ChangeSource,
ContentChangedEvent,
DelimiterClasses,
EditorReadyEvent,
Entity,
ExtractContentWithDomEvent,
IEditor,
NormalSelectionRange,
PluginEventType,
ContentChangedEvent,
PluginEvent,
SelectionRangeTypes,
BeforePasteEvent,
PluginEventType,
PluginKeyDownEvent,
ChangeSource,
SelectionRangeTypes,
} from 'roosterjs-editor-types';
import {
addDelimiters,
commitEntity,
findClosestElementAncestor,
getBlockElementAtNode,
Position,
} from 'roosterjs-editor-dom';

Expand Down Expand Up @@ -61,6 +62,7 @@ describe('Inline Entity On Plugin Event |', () => {
};
},
select: selectSpy = jasmine.createSpy('select'),
getBlockElementAtNode: (node: Node) => getBlockElementAtNode(document.body, node),
});
});

Expand Down Expand Up @@ -169,21 +171,16 @@ describe('Inline Entity On Plugin Event |', () => {
expect(splitTextNode.default).not.toHaveBeenCalled();
});

it('Enter on delimiter before, no previous sibling', () => {
arrangeAndAct(13 /* ENTER */, false /* addElementOnRunAsync */);

expect(selectSpy).toHaveBeenCalled();
expect(delimiterBefore?.previousElementSibling).toBeNull();
});
it('Enter on delimiter before, clear previous block delimiter', () => {
const div = document.createElement('div');
testContainer.insertAdjacentElement('beforebegin', div);
div.appendChild(delimiterBefore!.cloneNode(true /* deep */));

it('Enter on delimiter before, no previous sibling', () => {
const testElement = document.createElement('span');
testElement.appendChild(document.createTextNode('Test'));
delimiterBefore?.parentElement?.insertBefore(testElement, delimiterBefore);
arrangeAndAct(13 /* ENTER */, false /* addElementOnRunAsync */);

expect(selectSpy).toHaveBeenCalled();
expect(delimiterBefore?.previousElementSibling).not.toBeNull();
expect(div.firstElementChild?.className).toEqual('');
expect(div.firstElementChild?.textContent).not.toEqual(ZERO_WIDTH_SPACE);
expect(delimiterBefore!.className).toEqual(DelimiterClasses.DELIMITER_BEFORE);
});

it('Key press when selection is not collapsed, delimiter before is the endContainer', () => {
Expand Down Expand Up @@ -343,21 +340,15 @@ describe('Inline Entity On Plugin Event |', () => {
expect(splitTextNode.default).not.toHaveBeenCalled();
});

it('Enter on delimiter after, no previous sibling', () => {
arrangeAndAct(13 /* ENTER */, false /* addElementOnRunAsync */);

expect(selectSpy).toHaveBeenCalled();
expect(delimiterAfter?.nextElementSibling).toBeNull();
});

it('Enter on delimiter after, no previous sibling', () => {
const testElement = document.createElement('span');
testElement.appendChild(document.createTextNode('Test'));
delimiterAfter?.parentElement?.insertBefore(testElement, null);
it('Enter on delimiter after, clear the previous sibling class', () => {
const div = document.createElement('div');
testContainer.insertAdjacentElement('afterend', div);
div.appendChild(delimiterAfter!.cloneNode(true /* deep */));
arrangeAndAct(13 /* ENTER */, false /* addElementOnRunAsync */);

expect(selectSpy).toHaveBeenCalled();
expect(delimiterAfter?.nextElementSibling).not.toBeNull();
expect(div.firstElementChild?.className).toEqual('');
expect(div.firstElementChild?.textContent).not.toEqual(ZERO_WIDTH_SPACE);
expect(delimiterAfter!.className).toEqual(DelimiterClasses.DELIMITER_AFTER);
});

it('Key press when selection is not collapsed, delimiter after is the endContainer', () => {
Expand Down Expand Up @@ -617,17 +608,21 @@ describe('Inline Entity On Plugin Event |', () => {
if (elementToUse) {
rootDiv.appendChild(elementToUse);
}

const additionalAllowedCssClasses: string[] = [];
inlineEntityOnPluginEvent(
<BeforePasteEvent>(<any>{
eventType: PluginEventType.BeforePaste,
clipboardData: {},
fragment: rootDiv,
sanitizingOption: {
additionalAllowedCssClasses,
},
}),
editor
);

expect(rootDiv.querySelectorAll(DELIMITER_SELECTOR).length).toBe(expectedDelimiters);
expect(additionalAllowedCssClasses).toContain(DelimiterClasses.DELIMITER_AFTER);
expect(additionalAllowedCssClasses).toContain(DelimiterClasses.DELIMITER_BEFORE);
}

it('Before Paste with Read only Inline Entity in content', () => {
Expand Down Expand Up @@ -669,6 +664,7 @@ describe('Inline Entity On Plugin Event |', () => {
});
});
});

function addEntityBeforeEach(entity: Entity, wrapper: HTMLElement) {
entity = <Entity>{
id: 'test',
Expand Down