Skip to content

Commit

Permalink
Merge pull request microsoft#2497 from microsoft/u/juliaroldi/entity-…
Browse files Browse the repository at this point in the history
…features

[Entity Features] Handle Enter
  • Loading branch information
juliaroldi authored Mar 20, 2024
2 parents 1c50878 + 1fcb1b4 commit fb8603c
Show file tree
Hide file tree
Showing 4 changed files with 312 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
isEntityDelimiter,
isEntityElement,
isNodeOfType,
parseEntityFormat,
findClosestEntityWrapper,
} from 'roosterjs-content-model-dom';
import type {
CompositionEndEvent,
Expand Down Expand Up @@ -198,6 +200,7 @@ export function handleDelimiterKeyDownEvent(editor: IEditor, event: KeyDownEvent
return;
}
const isEnter = rawEvent.key === 'Enter';
const helper = editor.getDOMHelper();
if (selection.range.collapsed && (isCharacterValue(rawEvent) || isEnter)) {
const helper = editor.getDOMHelper();
const node = getFocusedElement(selection);
Expand Down Expand Up @@ -238,6 +241,11 @@ export function handleDelimiterKeyDownEvent(editor: IEditor, event: KeyDownEvent
}
}
}
} else if (isEnter) {
const entity = findClosestEntityWrapper(selection.range.startContainer, helper);
if (entity && isNodeOfType(entity, 'ELEMENT_NODE') && helper.isNodeInEditor(entity)) {
triggerEntityEventOnEnter(editor, entity, rawEvent);
}
}
}

Expand Down Expand Up @@ -289,6 +297,16 @@ export const handleEnterInlineEntity: ContentModelFormatter = model => {
selectionBlock.segmentFormat,
selectionBlock.decorator
);

if (
selectionBlock.segments.every(
x => x.segmentType == 'SelectionMarker' || x.segmentType == 'Br'
) ||
segmentsAfterMarker.every(x => x.segmentType == 'SelectionMarker')
) {
newPara.segments.push(createBr(selectionBlock.format));
}

newPara.segments.push(...segmentsAfterMarker);

const selectionBlockIndex = selectionBlockParent.blocks.indexOf(selectionBlock);
Expand All @@ -300,3 +318,23 @@ export const handleEnterInlineEntity: ContentModelFormatter = model => {

return true;
};

const triggerEntityEventOnEnter = (
editor: IEditor,
wrapper: HTMLElement,
rawEvent: KeyboardEvent
) => {
const format = parseEntityFormat(wrapper);
if (format.id && format.entityType && !format.isFakeEntity) {
editor.triggerEvent('entityOperation', {
operation: 'click',
entity: {
id: format.id,
type: format.entityType,
isReadonly: !!format.isReadonly,
wrapper,
},
rawEvent: rawEvent,
});
}
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as DelimiterFile from '../../../lib/corePlugin/entity/entityDelimiterUtils';
import * as entityUtils from 'roosterjs-content-model-dom/lib/domUtils/entityUtils';
import * as isNodeOfType from 'roosterjs-content-model-dom/lib/domUtils/isNodeOfType';
import { ContentModelDocument, DOMSelection, IEditor } from 'roosterjs-content-model-types';
import {
handleDelimiterContentChangedEvent,
Expand Down Expand Up @@ -157,12 +158,14 @@ describe('EntityDelimiterUtils |', () => {
let mockedSelection: DOMSelection;
let rafSpy: jasmine.Spy;
let takeSnapshotSpy: jasmine.Spy;
let triggerEventSpy: jasmine.Spy;

beforeEach(() => {
mockedSelection = undefined!;
rafSpy = jasmine.createSpy('requestAnimationFrame');
formatContentModelSpy = jasmine.createSpy('formatContentModel');
takeSnapshotSpy = jasmine.createSpy('takeSnapshot');
triggerEventSpy = jasmine.createSpy('triggerEvent');

mockedEditor = (<any>{
getDOMSelection: () => mockedSelection,
Expand All @@ -177,6 +180,7 @@ describe('EntityDelimiterUtils |', () => {
queryElements: queryElementsSpy,
isNodeInEditor: () => true,
}),
triggerEvent: triggerEventSpy,
takeSnapshot: takeSnapshotSpy,
}) as Partial<IEditor>;
spyOn(DelimiterFile, 'preventTypeInDelimiter').and.callThrough();
Expand Down Expand Up @@ -499,7 +503,7 @@ describe('EntityDelimiterUtils |', () => {
);
});

it('Handle, range selection & delimiter after wrapped in block entity', () => {
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');
Expand Down Expand Up @@ -553,6 +557,80 @@ describe('EntityDelimiterUtils |', () => {
}
);
});

it('Handle, range expanded selection | EnterKey', () => {
const div = document.createElement('div');
const parent = document.createElement('span');
const el = document.createElement('span');
const textSpan = document.createElement('span');
const text = document.createTextNode('span');
textSpan.appendChild(text);
textSpan.classList.add('_Entity');
el.appendChild(textSpan);
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: false,
setStartAfter: setStartAfterSpy,
setStartBefore: setStartBeforeSpy,
collapse: collapseSpy,
startContainer: textSpan,
},
isReverted: false,
};
spyOn(entityUtils, 'isEntityElement').and.returnValue(true);
spyOn(isNodeOfType, 'isNodeOfType').and.returnValue(true);
spyOn(mockedEditor, 'getDOMSelection').and.returnValue({
type: 'range',
range: mockedSelection.range,
});
spyOn(entityUtils, 'parseEntityFormat').and.returnValue({
isReadonly: true,
id: 'test',
entityType: 'test',
});
spyOn(entityUtils, 'findClosestEntityWrapper').and.returnValue(textSpan);
const mockedEvent = <any>{
ctrlKey: false,
altKey: false,
metaKey: false,
key: 'Enter',
preventDefault: preventDefaultSpy,
};

handleDelimiterKeyDownEvent(mockedEditor, {
eventType: 'keyDown',
rawEvent: mockedEvent,
});

expect(rafSpy).not.toHaveBeenCalled();
expect(preventDefaultSpy).not.toHaveBeenCalled();
expect(setStartAfterSpy).not.toHaveBeenCalled();
expect(setStartBeforeSpy).not.toHaveBeenCalled();
expect(collapseSpy).not.toHaveBeenCalled();
expect(triggerEventSpy).toHaveBeenCalledWith('entityOperation', {
operation: 'click',
entity: {
id: 'test',
type: 'test',
isReadonly: true,
wrapper: textSpan,
},
rawEvent: mockedEvent,
});
});
});
});

Expand Down Expand Up @@ -1125,4 +1203,165 @@ describe('handleEnterInlineEntity', () => {
format: {},
});
});

it('handle before entity as first segment', () => {
const model: ContentModelDocument = {
blockGroupType: 'Document',
blocks: [
{
blockType: 'Paragraph',
segments: [
{
segmentType: 'SelectionMarker',
isSelected: true,
format: {},
},
{
segmentType: 'Entity',
blockType: 'Entity',
format: {},
entityFormat: {
entityType: '',
isReadonly: true,
},
wrapper: <any>{},
},
{
segmentType: 'Text',
text: '_',
format: {},
},
],
format: {},
},
],
format: {},
};

DelimiterFile.handleEnterInlineEntity(model, <any>{});

expect(model).toEqual({
blockGroupType: 'Document',
blocks: [
{
blockType: 'Paragraph',
segments: [],
format: {},
},
{
blockType: 'Paragraph',
segments: [
{
segmentType: 'Br',
format: {},
},
{
segmentType: 'SelectionMarker',
isSelected: true,
format: {},
},
{
segmentType: 'Entity',
blockType: 'Entity',
format: {},
entityFormat: {
entityType: '',
isReadonly: true,
},
wrapper: jasmine.anything(),
},
{
segmentType: 'Text',
text: '_',
format: {},
},
],
format: {},
},
],
format: {},
});
});

it('handle after entity as last segment', () => {
const model: ContentModelDocument = {
blockGroupType: 'Document',
blocks: [
{
blockType: 'Paragraph',
segments: [
{
segmentType: 'Text',
text: '_',
format: {},
},
{
segmentType: 'Entity',
blockType: 'Entity',
format: {},
entityFormat: {
entityType: '',
isReadonly: true,
},
wrapper: <any>{},
},
{
segmentType: 'SelectionMarker',
isSelected: true,
format: {},
},
],
format: {},
},
],
format: {},
};

DelimiterFile.handleEnterInlineEntity(model, <any>{});

expect(model).toEqual({
blockGroupType: 'Document',
blocks: [
{
blockType: 'Paragraph',
segments: [
{
segmentType: 'Text',
text: '_',
format: {},
},

{
segmentType: 'Entity',
blockType: 'Entity',
format: {},
entityFormat: {
entityType: '',
isReadonly: true,
},
wrapper: jasmine.anything(),
},
],

format: {},
},
{
blockType: 'Paragraph',
segments: [
{
segmentType: 'Br',
format: {},
},
{
segmentType: 'SelectionMarker',
isSelected: true,
format: {},
},
],
format: {},
},
],
format: {},
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ export function isEntityElement(node: Node): boolean {
}

/**
* Get the entity wrapper element for a given DOM node
* Find the closest entity wrapper element from a given DOM node
* @param node The node to start looking for entity wrapper
* @param domHelper The DOM helper to use
*/
export function findClosestEntityWrapper(
startNode: Node,
Expand Down
Loading

0 comments on commit fb8603c

Please sign in to comment.