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
1 change: 1 addition & 0 deletions packages/roosterjs-content-model-api/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export { insertTableRow } from './modelApi/table/insertTableRow';
export { insertTableColumn } from './modelApi/table/insertTableColumn';
export { clearSelectedCells } from './modelApi/table/clearSelectedCells';

export { createEditorContextForEntity } from './publicApi/utils/createEditorContextForEntity';
export { formatTableWithContentModel } from './publicApi/utils/formatTableWithContentModel';
export { formatImageWithContentModel } from './publicApi/utils/formatImageWithContentModel';
export { formatParagraphWithContentModel } from './publicApi/utils/formatParagraphWithContentModel';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { createDomToModelContext, domToContentModel } from 'roosterjs-content-model-dom';
import type {
ContentModelBlockType,
ContentModelEntity,
EditorContext,
ReadonlyContentModelBlock,
ReadonlyContentModelBlockBase,
ReadonlyContentModelBlockGroup,
Expand All @@ -11,12 +14,14 @@ import type {
* @param type The type of block to query
* @param filter Optional selector to filter the blocks
* @param findFirstOnly True to return the first block only, false to return all blocks
* @param shouldExpandEntity Optional function to determine if an entity's children should be recursively queried, should return a EditorContext if the entity should be expanded, or null if not
*/
export function queryContentModelBlocks<T extends ReadonlyContentModelBlock>(
group: ReadonlyContentModelBlockGroup,
type: T extends ReadonlyContentModelBlockBase<infer U> ? U : never,
filter?: (element: T) => element is T,
findFirstOnly?: boolean
findFirstOnly?: boolean,
shouldExpandEntity?: (entity: ContentModelEntity) => EditorContext | null
): T[] {
const elements: T[] = [];
for (let i = 0; i < group.blocks.length; i++) {
Expand All @@ -32,12 +37,33 @@ export function queryContentModelBlocks<T extends ReadonlyContentModelBlock>(
if (isExpectedBlockType(block, type, filter)) {
elements.push(block);
}
if (block.blockType == 'Entity' && shouldExpandEntity) {
const editorContext = shouldExpandEntity(block);
if (editorContext) {
const context = createDomToModelContext(editorContext);
const model = domToContentModel(block.wrapper, context);
const results = queryContentModelBlocks<T>(
model,
type,
filter,
findFirstOnly,
shouldExpandEntity
);
elements.push(...results);
}
}
break;
case 'BlockGroup':
if (isExpectedBlockType(block, type, filter)) {
elements.push(block);
}
const results = queryContentModelBlocks<T>(block, type, filter, findFirstOnly);
const results = queryContentModelBlocks<T>(
block,
type,
filter,
findFirstOnly,
shouldExpandEntity
);
elements.push(...results);
break;
case 'Table':
Expand All @@ -50,7 +76,8 @@ export function queryContentModelBlocks<T extends ReadonlyContentModelBlock>(
cell,
type,
filter,
findFirstOnly
findFirstOnly,
shouldExpandEntity
);
elements.push(...results);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { ContentModelEntity, EditorContext, IEditor } from 'roosterjs-content-model-types';

/**
* Create an EditorContext for an entity
* @param editor The editor object
* @param entity The entity to create the context for
* @returns The generated EditorContext for the entity
*/
export function createEditorContextForEntity(
editor: IEditor,
entity: ContentModelEntity
): EditorContext {
const domHelper = editor.getDOMHelper();
const context: EditorContext = {
isDarkMode: editor.isDarkMode(),
defaultFormat: { ...entity.format },
darkColorHandler: editor.getColorManager(),
addDelimiterForEntity: false,
allowCacheElement: false,
domIndexer: undefined,
zoomScale: domHelper.calculateZoomScale(),
experimentalFeatures: [],
};

if (editor.getDocument().defaultView?.getComputedStyle(entity.wrapper).direction == 'rtl') {
context.isRootRtl = true;
}

return context;
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { adjustWordSelection } from '../../modelApi/selection/adjustWordSelection';
import {
contentModelToDom,
createDomToModelContext,
Expand All @@ -11,14 +10,15 @@ import type {
ContentModelDocument,
ContentModelEntity,
ContentModelSegmentFormat,
EditorContext,
FormattableRoot,
IEditor,
PluginEventData,
ReadonlyContentModelDocument,
ShallowMutableContentModelParagraph,
ShallowMutableContentModelSegment,
} from 'roosterjs-content-model-types';
import { adjustWordSelection } from '../../modelApi/selection/adjustWordSelection';
import { createEditorContextForEntity } from './createEditorContextForEntity';

/**
* Invoke a callback to format the selected segment using Content Model
Expand Down Expand Up @@ -135,26 +135,6 @@ export function formatSegmentWithContentModel(
);
}

function createEditorContextForEntity(editor: IEditor, entity: ContentModelEntity): EditorContext {
const domHelper = editor.getDOMHelper();
const context: EditorContext = {
isDarkMode: editor.isDarkMode(),
defaultFormat: { ...entity.format },
darkColorHandler: editor.getColorManager(),
addDelimiterForEntity: false,
allowCacheElement: false,
domIndexer: undefined,
zoomScale: domHelper.calculateZoomScale(),
experimentalFeatures: [],
};

if (editor.getDocument().defaultView?.getComputedStyle(entity.wrapper).direction == 'rtl') {
context.isRootRtl = true;
}

return context;
}

function expandEntitySelections(
editor: IEditor,
entity: ContentModelEntity,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import * as domToContentModel from 'roosterjs-content-model-dom/lib/domToModel/domToContentModel';
import * as createDomToModelContext from 'roosterjs-content-model-dom/lib/domToModel/context/createDomToModelContext';
import { queryContentModelBlocks } from '../../../lib/modelApi/common/queryContentModelBlocks';
import {
ContentModelDocument,
DomToModelContext,
EditorContext,
ReadonlyContentModelBlockGroup,
ReadonlyContentModelListItem,
ReadonlyContentModelParagraph,
Expand Down Expand Up @@ -1464,4 +1469,84 @@ describe('queryContentModelBlocksBlocks', () => {
);
expect(result).toEqual([imageAndParagraph]);
});

it('should return empty array if no blocks match the type', () => {
// Arrange
const fakeWrapper = ('wrapper' as unknown) as HTMLElement;
const fakeEditorContext = ('editorContext' as unknown) as EditorContext;
const fakeDomToModelContext = ('domToModelContext' as unknown) as DomToModelContext;

const insideEntity: ContentModelDocument = {
blockGroupType: 'Document',
blocks: [
{
blockType: 'Paragraph',
segments: [],
format: { backgroundColor: 'green' },
segmentFormat: {},
},
],
format: {},
};
const createDomToModelContextSpy = spyOn(
createDomToModelContext,
'createDomToModelContext'
).and.returnValue(fakeDomToModelContext);
const domToContentModelSpy = spyOn(domToContentModel, 'domToContentModel').and.returnValue(
insideEntity
);

const group: ReadonlyContentModelBlockGroup = {
blockGroupType: 'Document',
blocks: [
{
blockType: 'Paragraph',
segments: [],
format: { backgroundColor: 'red' },
segmentFormat: {},
},
{
blockType: 'Entity',
wrapper: fakeWrapper,
entityFormat: {
id: '',
entityType: '',
isReadonly: true,
isFakeEntity: true,
},
format: {},
segmentType: 'Entity',
},
],
};

// Act
const result = queryContentModelBlocks<ReadonlyContentModelParagraph>(
group,
'Paragraph',
undefined /* filter */,
undefined /* findFirstOnly */,
entity => fakeEditorContext
);

// Assert
expect(createDomToModelContextSpy).toHaveBeenCalledWith(fakeEditorContext);
expect(domToContentModelSpy).toHaveBeenCalledWith(fakeWrapper, fakeDomToModelContext);
expect(result).toEqual([
// group (Document) > blocks[0] (Paragraph)
{
blockType: 'Paragraph',
segments: [],
format: { backgroundColor: 'red' },
segmentFormat: {},
},
// group (Document) > blocks[1] (Entity) > insideEntity (FormatContainer) > blocks[0] (Paragraph)
{
blockType: 'Paragraph',
segments: [],
format: { backgroundColor: 'green' },
segmentFormat: {},
},
]);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { createEditorContextForEntity } from '../../../lib/publicApi/utils/createEditorContextForEntity';
import { ContentModelEntity, IEditor } from 'roosterjs-content-model-types';

describe('createEditorContextForEntity', () => {
let isDarkMode: jasmine.Spy;
let calculateZoomScale: jasmine.Spy;
let getComputedStyle: jasmine.Spy;
let getDocument: jasmine.Spy;
let getDOMHelper: jasmine.Spy;
let getColorManager: jasmine.Spy;
let editor: IEditor;
let entity: ContentModelEntity;

beforeEach(() => {
isDarkMode = jasmine.createSpy('isDarkMode').and.returnValue(false);
calculateZoomScale = jasmine.createSpy('calculateZoomScale').and.returnValue(1);
getComputedStyle = jasmine.createSpy('getComputedStyle').and.returnValue({
direction: 'ltr',
});
getDocument = jasmine.createSpy('getDocument').and.returnValue({
defaultView: {
getComputedStyle,
},
});
getDOMHelper = jasmine.createSpy('getDOMHelper').and.returnValue({ calculateZoomScale });
getColorManager = jasmine.createSpy('getColorManager').and.returnValue('colorManager');
editor = ({
isDarkMode,
getDocument,
getDOMHelper,
getColorManager,
} as unknown) as IEditor;

entity = {
wrapper: 'fakeWrapper' as any,
format: {
backgroundColor: 'red',
},
blockType: 'Entity',
entityFormat: {
entityType: 'TestEntity',
id: 'TestEntityId',
},
segmentType: 'Entity',
};
});

it('should create an EditorContext for an entity', () => {
const context = createEditorContextForEntity(editor, entity);

expect(context).toEqual({
isDarkMode: false,
defaultFormat: {
backgroundColor: 'red',
},
darkColorHandler: 'colorManager' as any,
addDelimiterForEntity: false,
allowCacheElement: false,
domIndexer: undefined,
zoomScale: 1,
experimentalFeatures: [],
});
expect(isDarkMode).toHaveBeenCalled();
expect(calculateZoomScale).toHaveBeenCalled();
expect(getComputedStyle).toHaveBeenCalledWith(entity.wrapper);
expect(getDocument).toHaveBeenCalled();
expect(getDOMHelper).toHaveBeenCalled();
expect(getColorManager).toHaveBeenCalled();
});

it('should detect RTL', () => {
getComputedStyle = jasmine.createSpy('getComputedStyle').and.returnValue({
direction: 'rtl',
});
getDocument = jasmine.createSpy('getDocument').and.returnValue({
defaultView: {
getComputedStyle,
},
});
editor = ({
isDarkMode,
getDocument,
getDOMHelper,
getColorManager,
} as unknown) as IEditor;

const context = createEditorContextForEntity(editor, entity);

expect(context).toEqual({
isDarkMode: false,
defaultFormat: {
backgroundColor: 'red',
},
darkColorHandler: 'colorManager' as any,
addDelimiterForEntity: false,
allowCacheElement: false,
domIndexer: undefined,
zoomScale: 1,
experimentalFeatures: [],
isRootRtl: true,
});
expect(isDarkMode).toHaveBeenCalled();
expect(calculateZoomScale).toHaveBeenCalled();
expect(getComputedStyle).toHaveBeenCalledWith(entity.wrapper);
expect(getDocument).toHaveBeenCalled();
expect(getDOMHelper).toHaveBeenCalled();
expect(getColorManager).toHaveBeenCalled();
});
});
4 changes: 3 additions & 1 deletion versions.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,7 @@
"react": "9.0.2",
"main": "9.27.0",
"legacyAdapter": "8.63.2",
"overrides": {}
"overrides": {
"roosterjs-content-model-api": "9.28.0"
}
}
Loading