Skip to content

Commit

Permalink
Add support to cursor around Block entities. (#2350)
Browse files Browse the repository at this point in the history
  • Loading branch information
BryanValverdeU authored Jan 29, 2024
1 parent 01e79bd commit 7998d03
Show file tree
Hide file tree
Showing 8 changed files with 153 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import type {
IStandaloneEditor,
} from 'roosterjs-content-model-types';

const BlockEntityTag = 'div';
const InlineEntityTag = 'span';

/**
Expand Down Expand Up @@ -58,10 +57,11 @@ export default function insertEntity(
options?: InsertEntityOptions
): ContentModelEntity | null {
const { contentNode, focusAfterEntity, wrapperDisplay, skipUndoSnapshot } = options || {};
const wrapper = editor.getDocument().createElement(isBlock ? BlockEntityTag : InlineEntityTag);
const display = wrapperDisplay ?? (isBlock ? undefined : 'inline-block');

wrapper.style.setProperty('display', display || null);
const wrapper = editor.getDocument().createElement(InlineEntityTag);
if (isBlock) {
wrapper.style.width = '100%';
}
wrapper.style.setProperty('display', wrapperDisplay ?? ('inline-block' || null));

if (contentNode) {
wrapper.appendChild(contentNode);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,12 +110,11 @@ describe('insertEntity', () => {
wrapper: wrapper,
});
});

it('block inline entity to root', () => {
const entity = insertEntity(editor, type, true, 'root');

expect(createElementSpy).toHaveBeenCalledWith('div');
expect(setPropertySpy).toHaveBeenCalledWith('display', null);
expect(createElementSpy).toHaveBeenCalledWith('span');
expect(setPropertySpy).toHaveBeenCalledWith('display', 'inline-block');
expect(appendChildSpy).not.toHaveBeenCalled();
expect(formatWithContentModelSpy.calls.argsFor(0)[1].apiName).toBe(apiName);
expect(formatWithContentModelSpy.calls.argsFor(0)[1].changeSource).toEqual(
Expand Down Expand Up @@ -165,7 +164,7 @@ describe('insertEntity', () => {
wrapperDisplay: 'none',
});

expect(createElementSpy).toHaveBeenCalledWith('div');
expect(createElementSpy).toHaveBeenCalledWith('span');
expect(setPropertySpy).toHaveBeenCalledWith('display', 'none');
expect(appendChildSpy).toHaveBeenCalledWith(contentNode);
expect(formatWithContentModelSpy.calls.argsFor(0)[1].apiName).toBe(apiName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { deleteSelection } from '../publicApi/selection/deleteSelection';
import { extractClipboardItems } from '../utils/extractClipboardItems';
import { getSelectedCells } from '../publicApi/table/getSelectedCells';
import { iterateSelections } from '../publicApi/selection/iterateSelections';
import { onCreateCopyEntityNode } from '../override/pasteCopyBlockEntityParser';
import { transformColor } from '../publicApi/color/transformColor';
import {
contentModelToDom,
Expand Down Expand Up @@ -321,13 +322,14 @@ function domSelectionToRange(doc: Document, selection: DOMSelection): Range | nu
* @internal
* Exported only for unit testing
*/
export const onNodeCreated: OnNodeCreated = (_, node): void => {
export const onNodeCreated: OnNodeCreated = (model, node): void => {
if (isNodeOfType(node, 'ELEMENT_NODE') && isElementOfType(node, 'table')) {
wrap(node.ownerDocument, node, 'div');
}
if (isNodeOfType(node, 'ELEMENT_NODE') && !node.isContentEditable) {
node.removeAttribute('contenteditable');
}
onCreateCopyEntityNode(model, node);
};

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { isElementOfType, isNodeOfType } from 'roosterjs-content-model-dom';
import type {
ContentModelEntity,
EntityInfoFormat,
FormatParser,
OnNodeCreated,
} from 'roosterjs-content-model-types';

const BLOCK_ENTITY_CLASS = '_EBlock';
const ONE_HUNDRED_PERCENT = '100%';

/**
* @internal
*/
export const onCreateCopyEntityNode: OnNodeCreated = (model, node) => {
const entityModel = model as ContentModelEntity;
if (
entityModel &&
entityModel.wrapper &&
entityModel.segmentType == 'Entity' &&
isNodeOfType(node, 'ELEMENT_NODE') &&
isElementOfType(node, 'span') &&
node.style.width == ONE_HUNDRED_PERCENT &&
node.style.display == 'inline-block'
) {
node.classList.add(BLOCK_ENTITY_CLASS);
node.style.display = 'block';
}
};

/**
* @internal
*/
export const pasteBlockEntityParser: FormatParser<EntityInfoFormat> = (_, element) => {
if (element.classList.contains(BLOCK_ENTITY_CLASS)) {
element.style.display = 'inline-block';
element.style.width = ONE_HUNDRED_PERCENT;
element.classList.remove(BLOCK_ENTITY_CLASS);
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { createPasteGeneralProcessor } from '../../override/pasteGeneralProcesso
import { getSegmentTextFormat } from '../../publicApi/domUtils/getSegmentTextFormat';
import { getSelectedSegments } from '../../publicApi/selection/collectSelections';
import { mergeModel } from '../../publicApi/model/mergeModel';
import { pasteBlockEntityParser } from '../../override/pasteCopyBlockEntityParser';
import { pasteDisplayFormatParser } from '../../override/pasteDisplayFormatParser';
import { pasteTextProcessor } from '../../override/pasteTextProcessor';
import type { MergeModelOption } from '../../publicApi/model/mergeModel';
Expand Down Expand Up @@ -59,6 +60,7 @@ export function mergePasteContent(
},
additionalFormatParsers: {
container: [containerSizeFormatParser],
entity: [pasteBlockEntityParser],
},
},
domToModelOption
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { ContentModelEntity } from 'roosterjs-content-model-types';
import {
onCreateCopyEntityNode,
pasteBlockEntityParser,
} from '../../lib/override/pasteCopyBlockEntityParser';

describe('onCreateCopyEntityNode', () => {
it('handle', () => {
const span = document.createElement('span');
span.style.width = '100%';
span.style.display = 'inline-block';
const modelEntity: ContentModelEntity = {
entityFormat: {},
format: {},
wrapper: span,
segmentType: 'Entity',
blockType: 'Entity',
};

onCreateCopyEntityNode(modelEntity, span);

expect(span.style.display).toEqual('block');
expect(span.classList.contains('_EBlock')).toBeTrue();
});

it('Dont handle, no 100% width', () => {
const span = document.createElement('span');
span.style.display = 'inline-block';
const modelEntity: ContentModelEntity = {
entityFormat: {},
format: {},
wrapper: span,
segmentType: 'Entity',
blockType: 'Entity',
};

onCreateCopyEntityNode(modelEntity, span);

expect(span.style.display).not.toEqual('block');
expect(span.classList.contains('_EBlock')).not.toBeTrue();
});

it('Dont handle, not inline block', () => {
const span = document.createElement('span');
span.style.width = '100%';
const modelEntity: ContentModelEntity = {
entityFormat: {},
format: {},
wrapper: span,
segmentType: 'Entity',
blockType: 'Entity',
};

onCreateCopyEntityNode(modelEntity, span);

expect(span.style.display).not.toEqual('block');
expect(span.classList.contains('_EBlock')).not.toBeTrue();
});
});

describe('pasteBlockEntityParser', () => {
it('handle', () => {
const span = document.createElement('span');
span.classList.add('_EBlock');

pasteBlockEntityParser({}, span, <any>{}, {});

expect(span.style.width).toEqual('100%');
expect(span.style.display).toEqual('inline-block');
expect(span.classList.contains('_EBlock')).toBeFalse();
});

it('Dont handle', () => {
const span = document.createElement('span');

pasteBlockEntityParser({}, span, <any>{}, {});

expect(span.style.width).not.toEqual('100%');
expect(span.style.display).not.toEqual('inline-block');
expect(span.classList.contains('_EBlock')).toBeFalse();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import * as mergeModelFile from '../../../lib/publicApi/model/mergeModel';
import { containerSizeFormatParser } from '../../../lib/override/containerSizeFormatParser';
import { createContentModelDocument } from 'roosterjs-content-model-dom';
import { mergePasteContent } from '../../../lib/utils/paste/mergePasteContent';
import { pasteBlockEntityParser } from '../../../lib/override/pasteCopyBlockEntityParser';
import { pasteDisplayFormatParser } from '../../../lib/override/pasteDisplayFormatParser';
import { pasteTextProcessor } from '../../../lib/override/pasteTextProcessor';
import {
Expand Down Expand Up @@ -429,6 +430,7 @@ describe('mergePasteContent', () => {
},
additionalFormatParsers: {
container: [containerSizeFormatParser],
entity: [pasteBlockEntityParser],
},
},
mockedDefaultDomToModelOptions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
ContentModelBlockHandler,
ContentModelEntity,
ContentModelSegmentHandler,
ModelToDomContext,
} from 'roosterjs-content-model-types';

/**
Expand Down Expand Up @@ -53,7 +54,7 @@ export const handleEntitySegment: ContentModelSegmentHandler<ContentModelEntity>
applyFormat(wrapper, context.formatAppliers.entity, entityFormat, context);

if (context.addDelimiterForEntity && entityFormat.isReadonly) {
const [after, before] = addDelimiters(doc, wrapper);
const [after, before] = addDelimiterForEntity(doc, wrapper, context);

newSegments?.push(after, before);
context.regularSelection.current.segment = after;
Expand All @@ -63,3 +64,17 @@ export const handleEntitySegment: ContentModelSegmentHandler<ContentModelEntity>

context.onNodeCreated?.(entityModel, wrapper);
};

function addDelimiterForEntity(doc: Document, wrapper: HTMLElement, context: ModelToDomContext) {
const [after, before] = addDelimiters(doc, wrapper);

const format = {
...context.pendingFormat?.format,
...context.defaultFormat,
};

applyFormat(after, context.formatAppliers.segment, format, context);
applyFormat(before, context.formatAppliers.segment, format, context);

return [after, before];
}

0 comments on commit 7998d03

Please sign in to comment.