From 978a819602f1934f0d8a8c65db53ce19d44da8f2 Mon Sep 17 00:00:00 2001 From: "Julia Roldi (from Dev Box)" Date: Tue, 29 Oct 2024 16:03:47 -0300 Subject: [PATCH 1/9] images in word table --- .../roosterjs-content-model-api/lib/index.ts | 1 + .../lib/modelApi/common/queryContentModel.ts | 117 ++ .../modelApi/common/queryContentModelTest.ts | 1796 +++++++++++++++++ .../lib/imageEdit/utils/findEditingImage.ts | 79 +- 4 files changed, 1932 insertions(+), 61 deletions(-) create mode 100644 packages/roosterjs-content-model-api/lib/modelApi/common/queryContentModel.ts create mode 100644 packages/roosterjs-content-model-api/test/modelApi/common/queryContentModelTest.ts diff --git a/packages/roosterjs-content-model-api/lib/index.ts b/packages/roosterjs-content-model-api/lib/index.ts index 9010a248c90..2f1558ac367 100644 --- a/packages/roosterjs-content-model-api/lib/index.ts +++ b/packages/roosterjs-content-model-api/lib/index.ts @@ -60,3 +60,4 @@ export { setModelIndentation } from './modelApi/block/setModelIndentation'; export { matchLink } from './modelApi/link/matchLink'; export { promoteLink } from './modelApi/link/promoteLink'; export { getListAnnounceData } from './modelApi/list/getListAnnounceData'; +export { queryContentModel, QueryContentModelOptions } from './modelApi/common/queryContentModel'; diff --git a/packages/roosterjs-content-model-api/lib/modelApi/common/queryContentModel.ts b/packages/roosterjs-content-model-api/lib/modelApi/common/queryContentModel.ts new file mode 100644 index 00000000000..0ff93545342 --- /dev/null +++ b/packages/roosterjs-content-model-api/lib/modelApi/common/queryContentModel.ts @@ -0,0 +1,117 @@ +import type { + ContentModelBlockType, + ContentModelSegmentType, + ReadonlyContentModelBlock, + ReadonlyContentModelBlockGroup, + ReadonlyContentModelParagraph, + ReadonlyContentModelSegment, + ReadonlyContentModelTable, +} from 'roosterjs-content-model-types'; + +/** + * Options for queryContentModel + */ +export interface QueryContentModelOptions { + /** + * The type of block to query @default 'Paragraph' + */ + type?: ContentModelBlockType; + + /** + * The type of segment to query + */ + segmentType?: ContentModelSegmentType; + + /** + * Optional selector to filter the blocks/segments + */ + selector?: (element: T) => boolean; + + /** + * True to return the first block only, false to return all blocks + */ + findFirstOnly?: boolean; +} + +/** + * Query content model blocks or segments + * @param group The block group to query + * @param options The query option + */ +export function queryContentModel< + T extends ReadonlyContentModelBlock | ReadonlyContentModelSegment +>(group: ReadonlyContentModelBlockGroup, options: QueryContentModelOptions): T[] { + const elements: T[] = []; + const searchOptions = options.type ? options : { ...options, type: 'Paragraph' }; + const { type, segmentType, selector, findFirstOnly } = searchOptions; + + for (let i = 0; i < group.blocks.length; i++) { + if (findFirstOnly && elements.length > 0) { + break; + } + const block = group.blocks[i]; + switch (block.blockType) { + case 'BlockGroup': + if (type == block.blockType && (!selector || selector(block as T))) { + elements.push(block as T); + } + const blockGroupsResults = queryContentModel(block, options); + elements.push(...(blockGroupsResults as T[])); + break; + case 'Table': + if (type == block.blockType && (!selector || selector(block as T))) { + elements.push(block as T); + } + const tableResults = searchInTables(block, options); + elements.push(...(tableResults as T[])); + break; + case 'Divider': + case 'Entity': + if (type == block.blockType && (!selector || selector(block as T))) { + elements.push(block as T); + } + break; + case 'Paragraph': + if (type == block.blockType) { + if (!segmentType && (!selector || selector(block as T))) { + elements.push(block as T); + } else if (segmentType) { + const segments = searchInParagraphs(block, segmentType, selector); + elements.push(...(segments as T[])); + } + } + break; + } + } + + return elements; +} + +function searchInTables( + table: ReadonlyContentModelTable, + options: QueryContentModelOptions +): T[] { + const blocks: T[] = []; + for (const row of table.rows) { + for (const cell of row.cells) { + const items = queryContentModel(cell, options); + + blocks.push(...items); + } + } + return blocks; +} + +function searchInParagraphs

( + block: ReadonlyContentModelParagraph, + segmentType: ContentModelSegmentType, + selector?: (element: P) => boolean +): P[] { + const segments: P[] = []; + for (const segment of block.segments) { + if (segment.segmentType == segmentType && (!selector || selector(segment as P))) { + segments.push(segment as P); + } + } + return segments; +} diff --git a/packages/roosterjs-content-model-api/test/modelApi/common/queryContentModelTest.ts b/packages/roosterjs-content-model-api/test/modelApi/common/queryContentModelTest.ts new file mode 100644 index 00000000000..9306f1ead08 --- /dev/null +++ b/packages/roosterjs-content-model-api/test/modelApi/common/queryContentModelTest.ts @@ -0,0 +1,1796 @@ +import { queryContentModel } from '../../../lib/modelApi/common/queryContentModel'; +import { + ReadonlyContentModelBlockGroup, + ReadonlyContentModelImage, + ReadonlyContentModelListItem, + ReadonlyContentModelParagraph, + ReadonlyContentModelTable, +} from 'roosterjs-content-model-types'; + +describe('queryContentModel', () => { + it('should return empty array if no blocks', () => { + // Arrange + const group: ReadonlyContentModelBlockGroup = { + blockGroupType: 'Document', + blocks: [], + }; + + // Act + const result = queryContentModel(group, {}); + + // Assert + expect(result).toEqual([]); + }); + + it('should return empty array if no blocks match the type', () => { + // Arrange + const group: ReadonlyContentModelBlockGroup = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [], + format: {}, + segmentFormat: {}, + }, + ], + }; + + // Act + const result = queryContentModel(group, { type: 'Table' }); + + // Assert + expect(result).toEqual([]); + }); + + it('should return blocks that match the type', () => { + // Arrange + const group: ReadonlyContentModelBlockGroup = { + blockGroupType: 'Document', + blocks: [ + { + widths: [120, 120], + rows: [ + { + height: 22, + cells: [ + { + spanAbove: false, + spanLeft: false, + isHeader: false, + blockGroupType: 'TableCell', + blocks: [ + { + segments: [ + { + segmentType: 'Br', + format: {}, + }, + ], + segmentFormat: {}, + blockType: 'Paragraph', + format: {}, + }, + ], + format: {}, + dataset: {}, + }, + { + spanAbove: false, + spanLeft: false, + isHeader: false, + blockGroupType: 'TableCell', + blocks: [ + { + segments: [ + { + segmentType: 'Br', + format: {}, + }, + ], + segmentFormat: {}, + blockType: 'Paragraph', + format: {}, + }, + ], + format: {}, + dataset: {}, + }, + ], + format: {}, + }, + { + height: 22, + cells: [ + { + spanAbove: false, + spanLeft: false, + isHeader: false, + blockGroupType: 'TableCell', + blocks: [ + { + segments: [ + { + segmentType: 'Br', + format: {}, + }, + ], + segmentFormat: {}, + blockType: 'Paragraph', + format: {}, + }, + ], + format: {}, + dataset: {}, + }, + { + spanAbove: false, + spanLeft: false, + isHeader: false, + blockGroupType: 'TableCell', + blocks: [ + { + segments: [ + { + segmentType: 'Br', + format: {}, + }, + ], + segmentFormat: {}, + blockType: 'Paragraph', + format: {}, + }, + ], + format: {}, + dataset: {}, + }, + ], + format: {}, + }, + ], + blockType: 'Table', + format: { + useBorderBox: true, + borderCollapse: true, + }, + dataset: { + editingInfo: + '{"topBorderColor":"#ABABAB","bottomBorderColor":"#ABABAB","verticalBorderColor":"#ABABAB","hasHeaderRow":false,"hasFirstColumn":false,"hasBandedRows":false,"hasBandedColumns":false,"bgColorEven":null,"bgColorOdd":"#ABABAB20","headerRowColor":"#ABABAB","tableBorderFormat":0,"verticalAlign":"top"}', + }, + }, + { + segments: [ + { + segmentType: 'Br', + format: {}, + }, + ], + segmentFormat: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + }, + blockType: 'Paragraph', + format: {}, + }, + { + segments: [ + { + text: 'Test', + segmentType: 'Text', + format: {}, + }, + { + isSelected: true, + segmentType: 'SelectionMarker', + format: {}, + }, + ], + segmentFormat: {}, + blockType: 'Paragraph', + format: {}, + }, + { + segments: [ + { + segmentType: 'Br', + format: {}, + }, + ], + segmentFormat: {}, + blockType: 'Paragraph', + format: {}, + }, + ], + format: {}, + }; + + const expected: ReadonlyContentModelTable[] = [ + { + widths: [120, 120], + rows: [ + { + height: 22, + cells: [ + { + spanAbove: false, + spanLeft: false, + isHeader: false, + blockGroupType: 'TableCell', + blocks: [ + { + segments: [ + { + segmentType: 'Br', + format: {}, + }, + ], + segmentFormat: {}, + blockType: 'Paragraph', + format: {}, + }, + ], + format: {}, + dataset: {}, + }, + { + spanAbove: false, + spanLeft: false, + isHeader: false, + blockGroupType: 'TableCell', + blocks: [ + { + segments: [ + { + segmentType: 'Br', + format: {}, + }, + ], + segmentFormat: {}, + blockType: 'Paragraph', + format: {}, + }, + ], + format: {}, + dataset: {}, + }, + ], + format: {}, + }, + { + height: 22, + cells: [ + { + spanAbove: false, + spanLeft: false, + isHeader: false, + blockGroupType: 'TableCell', + blocks: [ + { + segments: [ + { + segmentType: 'Br', + format: {}, + }, + ], + segmentFormat: {}, + blockType: 'Paragraph', + format: {}, + }, + ], + format: {}, + dataset: {}, + }, + { + spanAbove: false, + spanLeft: false, + isHeader: false, + blockGroupType: 'TableCell', + blocks: [ + { + segments: [ + { + segmentType: 'Br', + format: {}, + }, + ], + segmentFormat: {}, + blockType: 'Paragraph', + format: {}, + }, + ], + format: {}, + dataset: {}, + }, + ], + format: {}, + }, + ], + blockType: 'Table', + format: { + useBorderBox: true, + borderCollapse: true, + }, + dataset: { + editingInfo: + '{"topBorderColor":"#ABABAB","bottomBorderColor":"#ABABAB","verticalBorderColor":"#ABABAB","hasHeaderRow":false,"hasFirstColumn":false,"hasBandedRows":false,"hasBandedColumns":false,"bgColorEven":null,"bgColorOdd":"#ABABAB20","headerRowColor":"#ABABAB","tableBorderFormat":0,"verticalAlign":"top"}', + }, + }, + ]; + + // Act + const result = queryContentModel(group, { type: 'Table' }); + + // Assert + expect(result).toEqual(expected); + }); + + it('should return blocks that match the type and selector', () => { + const paragraph: ReadonlyContentModelParagraph = { + segments: [ + { + text: 'Test', + segmentType: 'Text', + format: {}, + }, + { + isSelected: true, + segmentType: 'SelectionMarker', + format: {}, + }, + ], + segmentFormat: {}, + blockType: 'Paragraph', + format: {}, + }; + + // Arrange + const group: ReadonlyContentModelBlockGroup = { + blockGroupType: 'Document', + blocks: [ + { + segments: [ + { + segmentType: 'Br', + format: {}, + }, + ], + segmentFormat: {}, + blockType: 'Paragraph', + format: {}, + }, + paragraph, + { + segments: [ + { + segmentType: 'Br', + format: {}, + }, + ], + segmentFormat: {}, + blockType: 'Paragraph', + format: {}, + }, + ], + format: {}, + }; + + // Act + const result = queryContentModel(group, { + type: 'Paragraph', + + selector: block => block.segments.length == 2, + }); + + // Assert + expect(result).toEqual([paragraph]); + }); + + it('should return first segment that match the type and selector', () => { + const image: ReadonlyContentModelImage = { + src: + 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAsJCQcJCQcJCQkJCwkJCQkJCQsJCwsMCwsLDA0QDB...', + isSelectedAsImageSelection: true, + segmentType: 'Image', + isSelected: true, + format: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + id: 'image_0', + maxWidth: '1492px', + }, + dataset: { + isEditing: 'true', + }, + }; + const model: ReadonlyContentModelBlockGroup = { + blockGroupType: 'Document', + blocks: [ + { + widths: [120, 153], + rows: [ + { + height: 157, + cells: [ + { + spanAbove: false, + spanLeft: false, + isHeader: false, + blockGroupType: 'TableCell', + blocks: [ + { + segments: [ + { + segmentType: 'Br', + format: {}, + }, + ], + segmentFormat: {}, + blockType: 'Paragraph', + format: {}, + }, + ], + format: {}, + dataset: {}, + }, + { + spanAbove: false, + spanLeft: false, + isHeader: false, + blockGroupType: 'TableCell', + blocks: [ + { + segments: [image], + segmentFormat: {}, + blockType: 'Paragraph', + format: {}, + }, + ], + format: {}, + dataset: {}, + }, + ], + format: {}, + }, + ], + blockType: 'Table', + format: { + useBorderBox: true, + borderCollapse: true, + }, + dataset: { + editingInfo: + '{"topBorderColor":"#ABABAB","bottomBorderColor":"#ABABAB","verticalBorderColor":"#ABABAB","hasHeaderRow":false,"hasFirstColumn":false,"hasBandedRows":false,"hasBandedColumns":false,"bgColorEven":null,"bgColorOdd":"#ABABAB20","headerRowColor":"#ABABAB","tableBorderFormat":0,"verticalAlign":"top"}', + }, + }, + { + segments: [ + { + segmentType: 'Br', + format: {}, + }, + ], + segmentFormat: {}, + blockType: 'Paragraph', + format: {}, + }, + { + segments: [ + { + segmentType: 'Br', + format: {}, + }, + ], + segmentFormat: {}, + blockType: 'Paragraph', + format: {}, + }, + ], + }; + const result = queryContentModel(model, { + segmentType: 'Image', + selector: (segment: ReadonlyContentModelImage) => !!segment.dataset.isEditing, + findFirstOnly: true, + }); + expect(result).toEqual([image]); + }); + + it('should return all tables that match the type and selector', () => { + const model: ReadonlyContentModelBlockGroup = { + blockGroupType: 'Document', + blocks: [ + { + widths: [120, 120], + rows: [ + { + height: 22, + cells: [ + { + spanAbove: false, + spanLeft: false, + isHeader: false, + blockGroupType: 'TableCell', + blocks: [ + { + segments: [ + { + segmentType: 'Br', + format: {}, + }, + ], + segmentFormat: {}, + blockType: 'Paragraph', + format: {}, + }, + ], + format: {}, + dataset: {}, + }, + { + spanAbove: false, + spanLeft: false, + isHeader: false, + blockGroupType: 'TableCell', + blocks: [ + { + segments: [ + { + text: 'Test', + segmentType: 'Text', + format: {}, + }, + ], + segmentFormat: {}, + blockType: 'Paragraph', + format: {}, + }, + ], + format: {}, + dataset: {}, + }, + ], + format: {}, + }, + ], + blockType: 'Table', + format: { + useBorderBox: true, + borderCollapse: true, + }, + dataset: { + editingInfo: + '{"topBorderColor":"#ABABAB","bottomBorderColor":"#ABABAB","verticalBorderColor":"#ABABAB","hasHeaderRow":false,"hasFirstColumn":false,"hasBandedRows":false,"hasBandedColumns":false,"bgColorEven":null,"bgColorOdd":"#ABABAB20","headerRowColor":"#ABABAB","tableBorderFormat":0,"verticalAlign":"top"}', + }, + }, + { + isImplicit: false, + segments: [ + { + segmentType: 'Br', + format: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: '#000000', + }, + }, + ], + segmentFormat: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: '#000000', + }, + blockType: 'Paragraph', + format: {}, + }, + { + segments: [ + { + text: 'not table', + segmentType: 'Text', + format: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: '#000000', + }, + }, + { + isSelected: true, + segmentType: 'SelectionMarker', + format: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: '#000000', + }, + }, + ], + segmentFormat: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: '#000000', + }, + blockType: 'Paragraph', + format: {}, + }, + { + segments: [ + { + segmentType: 'Br', + format: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: '#000000', + }, + }, + ], + segmentFormat: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: '#000000', + }, + blockType: 'Paragraph', + format: {}, + }, + { + widths: [120], + rows: [ + { + height: 22, + cells: [ + { + spanAbove: false, + spanLeft: false, + isHeader: false, + blockGroupType: 'TableCell', + blocks: [ + { + segments: [ + { + text: 'table 2', + segmentType: 'Text', + format: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: '#000000', + }, + }, + ], + segmentFormat: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: '#000000', + }, + blockType: 'Paragraph', + format: {}, + }, + ], + format: { + useBorderBox: true, + borderTop: '1px solid #ABABAB', + borderRight: '1px solid #ABABAB', + borderBottom: '1px solid #ABABAB', + borderLeft: '1px solid #ABABAB', + verticalAlign: 'top', + }, + dataset: {}, + }, + ], + format: {}, + }, + ], + blockType: 'Table', + format: { + borderCollapse: true, + useBorderBox: true, + }, + dataset: { + editingInfo: + '{"topBorderColor":"#ABABAB","bottomBorderColor":"#ABABAB","verticalBorderColor":"#ABABAB","hasHeaderRow":false,"hasFirstColumn":false,"hasBandedRows":false,"hasBandedColumns":false,"bgColorEven":null,"bgColorOdd":"#ABABAB20","headerRowColor":"#ABABAB","tableBorderFormat":0,"verticalAlign":"top"}', + }, + }, + { + segments: [ + { + segmentType: 'Br', + format: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: '#000000', + }, + }, + ], + segmentFormat: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: '#000000', + }, + blockType: 'Paragraph', + format: {}, + }, + { + segments: [ + { + segmentType: 'Br', + format: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + }, + }, + ], + segmentFormat: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + }, + blockType: 'Paragraph', + format: {}, + }, + { + segments: [ + { + segmentType: 'Br', + format: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + }, + }, + ], + segmentFormat: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + }, + blockType: 'Paragraph', + format: {}, + }, + ], + format: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: '#000000', + }, + }; + + const expected: ReadonlyContentModelTable[] = [ + { + widths: [120, 120], + rows: [ + { + height: 22, + cells: [ + { + spanAbove: false, + spanLeft: false, + isHeader: false, + blockGroupType: 'TableCell', + blocks: [ + { + segments: [ + { + segmentType: 'Br', + format: {}, + }, + ], + segmentFormat: {}, + blockType: 'Paragraph', + format: {}, + }, + ], + format: {}, + dataset: {}, + }, + { + spanAbove: false, + spanLeft: false, + isHeader: false, + blockGroupType: 'TableCell', + blocks: [ + { + segments: [ + { + text: 'Test', + segmentType: 'Text', + format: {}, + }, + ], + segmentFormat: {}, + blockType: 'Paragraph', + format: {}, + }, + ], + format: {}, + dataset: {}, + }, + ], + format: {}, + }, + ], + blockType: 'Table', + format: { + useBorderBox: true, + borderCollapse: true, + }, + dataset: { + editingInfo: + '{"topBorderColor":"#ABABAB","bottomBorderColor":"#ABABAB","verticalBorderColor":"#ABABAB","hasHeaderRow":false,"hasFirstColumn":false,"hasBandedRows":false,"hasBandedColumns":false,"bgColorEven":null,"bgColorOdd":"#ABABAB20","headerRowColor":"#ABABAB","tableBorderFormat":0,"verticalAlign":"top"}', + }, + }, + { + widths: [120], + rows: [ + { + height: 22, + cells: [ + { + spanAbove: false, + spanLeft: false, + isHeader: false, + blockGroupType: 'TableCell', + blocks: [ + { + segments: [ + { + text: 'table 2', + segmentType: 'Text', + format: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: '#000000', + }, + }, + ], + segmentFormat: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: '#000000', + }, + blockType: 'Paragraph', + format: {}, + }, + ], + format: { + useBorderBox: true, + borderTop: '1px solid #ABABAB', + borderRight: '1px solid #ABABAB', + borderBottom: '1px solid #ABABAB', + borderLeft: '1px solid #ABABAB', + verticalAlign: 'top', + }, + dataset: {}, + }, + ], + format: {}, + }, + ], + blockType: 'Table', + format: { + borderCollapse: true, + useBorderBox: true, + }, + dataset: { + editingInfo: + '{"topBorderColor":"#ABABAB","bottomBorderColor":"#ABABAB","verticalBorderColor":"#ABABAB","hasHeaderRow":false,"hasFirstColumn":false,"hasBandedRows":false,"hasBandedColumns":false,"bgColorEven":null,"bgColorOdd":"#ABABAB20","headerRowColor":"#ABABAB","tableBorderFormat":0,"verticalAlign":"top"}', + }, + }, + ]; + const result = queryContentModel(model, { type: 'Table' }); + expect(result).toEqual(expected); + }); + + it('should return all tables in list', () => { + const table: ReadonlyContentModelTable = { + widths: [120], + rows: [ + { + height: 22, + cells: [ + { + spanAbove: false, + spanLeft: false, + isHeader: false, + blockGroupType: 'TableCell', + blocks: [ + { + segments: [ + { + text: 'table 2', + segmentType: 'Text', + format: {}, + }, + ], + segmentFormat: {}, + blockType: 'Paragraph', + format: {}, + }, + ], + format: {}, + dataset: {}, + }, + ], + format: {}, + }, + ], + blockType: 'Table', + format: { + useBorderBox: true, + borderCollapse: true, + }, + dataset: { + editingInfo: + '{"topBorderColor":"#ABABAB","bottomBorderColor":"#ABABAB","verticalBorderColor":"#ABABAB","hasHeaderRow":false,"hasFirstColumn":false,"hasBandedRows":false,"hasBandedColumns":false,"bgColorEven":null,"bgColorOdd":"#ABABAB20","headerRowColor":"#ABABAB","tableBorderFormat":0,"verticalAlign":"top"}', + }, + }; + + const model: ReadonlyContentModelBlockGroup = { + blockGroupType: 'Document', + blocks: [ + { + isImplicit: true, + segments: [ + { + segmentType: 'Br', + format: {}, + }, + ], + segmentFormat: {}, + blockType: 'Paragraph', + format: {}, + }, + { + formatHolder: { + isSelected: false, + segmentType: 'SelectionMarker', + format: {}, + }, + levels: [ + { + listType: 'UL', + format: { + listStyleType: 'disc', + }, + dataset: { + editingInfo: '{"applyListStyleFromLevel":true}', + }, + }, + ], + blockType: 'BlockGroup', + format: {}, + blockGroupType: 'ListItem', + blocks: [ + { + isImplicit: true, + segments: [ + { + isSelected: true, + segmentType: 'SelectionMarker', + format: {}, + }, + ], + segmentFormat: {}, + blockType: 'Paragraph', + format: {}, + }, + table, + ], + }, + { + segments: [ + { + segmentType: 'Br', + format: {}, + }, + ], + segmentFormat: {}, + blockType: 'Paragraph', + format: {}, + }, + { + segments: [ + { + segmentType: 'Br', + format: {}, + }, + ], + segmentFormat: {}, + blockType: 'Paragraph', + format: {}, + }, + { + segments: [ + { + segmentType: 'Br', + format: {}, + }, + ], + segmentFormat: {}, + blockType: 'Paragraph', + format: {}, + }, + ], + format: {}, + }; + + const expected: ReadonlyContentModelTable[] = [table]; + const result = queryContentModel(model, { type: 'Table' }); + expect(result).toEqual(expected); + }); + + it('should return all lists', () => { + const model: ReadonlyContentModelBlockGroup = { + blockGroupType: 'Document', + blocks: [ + { + formatHolder: { + isSelected: false, + segmentType: 'SelectionMarker', + format: {}, + }, + levels: [ + { + listType: 'UL', + format: { + startNumberOverride: 1, + listStyleType: 'disc', + }, + dataset: { + editingInfo: + '{"applyListStyleFromLevel":false,"unorderedStyleType":1}', + }, + }, + ], + blockType: 'BlockGroup', + format: {}, + blockGroupType: 'ListItem', + blocks: [ + { + segments: [ + { + text: 'table', + segmentType: 'Text', + format: {}, + }, + ], + segmentFormat: {}, + blockType: 'Paragraph', + format: {}, + }, + ], + }, + { + segments: [ + { + text: 'test', + segmentType: 'Text', + format: {}, + }, + ], + segmentFormat: {}, + blockType: 'Paragraph', + format: {}, + }, + { + formatHolder: { + isSelected: false, + segmentType: 'SelectionMarker', + format: {}, + }, + levels: [ + { + listType: 'OL', + format: { + startNumberOverride: 1, + listStyleType: 'decimal', + }, + dataset: { + editingInfo: + '{"applyListStyleFromLevel":false,"orderedStyleType":1}', + }, + }, + ], + blockType: 'BlockGroup', + format: {}, + blockGroupType: 'ListItem', + blocks: [ + { + segments: [ + { + text: 'test', + segmentType: 'Text', + format: {}, + }, + ], + segmentFormat: {}, + blockType: 'Paragraph', + format: {}, + }, + ], + }, + { + formatHolder: { + isSelected: false, + segmentType: 'SelectionMarker', + format: {}, + }, + levels: [ + { + listType: 'OL', + format: { + listStyleType: 'decimal', + }, + dataset: { + editingInfo: + '{"applyListStyleFromLevel":false,"orderedStyleType":1}', + }, + }, + ], + blockType: 'BlockGroup', + format: {}, + blockGroupType: 'ListItem', + blocks: [ + { + segments: [ + { + text: 'test', + segmentType: 'Text', + format: {}, + }, + ], + segmentFormat: {}, + blockType: 'Paragraph', + format: {}, + }, + ], + }, + { + segments: [ + { + isSelected: true, + segmentType: 'SelectionMarker', + format: {}, + }, + { + segmentType: 'Br', + format: {}, + }, + ], + segmentFormat: {}, + blockType: 'Paragraph', + format: {}, + }, + ], + format: {}, + }; + + const listExpected: ReadonlyContentModelListItem[] = [ + { + formatHolder: { + isSelected: false, + segmentType: 'SelectionMarker', + format: {}, + }, + levels: [ + { + listType: 'UL', + format: { + startNumberOverride: 1, + listStyleType: 'disc', + }, + dataset: { + editingInfo: '{"applyListStyleFromLevel":false,"unorderedStyleType":1}', + }, + }, + ], + blockType: 'BlockGroup', + format: {}, + blockGroupType: 'ListItem', + blocks: [ + { + segments: [ + { + text: 'table', + segmentType: 'Text', + format: {}, + }, + ], + segmentFormat: {}, + blockType: 'Paragraph', + format: {}, + }, + ], + }, + { + formatHolder: { + isSelected: false, + segmentType: 'SelectionMarker', + format: {}, + }, + levels: [ + { + listType: 'OL', + format: { + startNumberOverride: 1, + listStyleType: 'decimal', + }, + dataset: { + editingInfo: '{"applyListStyleFromLevel":false,"orderedStyleType":1}', + }, + }, + ], + blockType: 'BlockGroup', + format: {}, + blockGroupType: 'ListItem', + blocks: [ + { + segments: [ + { + text: 'test', + segmentType: 'Text', + format: {}, + }, + ], + segmentFormat: {}, + blockType: 'Paragraph', + format: {}, + }, + ], + }, + { + formatHolder: { + isSelected: false, + segmentType: 'SelectionMarker', + format: {}, + }, + levels: [ + { + listType: 'OL', + format: { + listStyleType: 'decimal', + }, + dataset: { + editingInfo: '{"applyListStyleFromLevel":false,"orderedStyleType":1}', + }, + }, + ], + blockType: 'BlockGroup', + format: {}, + blockGroupType: 'ListItem', + blocks: [ + { + segments: [ + { + text: 'test', + segmentType: 'Text', + format: {}, + }, + ], + segmentFormat: {}, + blockType: 'Paragraph', + format: {}, + }, + ], + }, + ]; + + const result = queryContentModel(model, { + type: 'BlockGroup', + selector: block => block.blockGroupType == 'ListItem', + }); + expect(result).toEqual(listExpected); + }); + + it('should return all images', () => { + const model: ReadonlyContentModelBlockGroup = { + blockGroupType: 'Document', + blocks: [ + { + segments: [ + { + src: + 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAsJCQcJCQcJCQkJCwkJCQkJCQsJCwsMCwsLDA0QDB...', + segmentType: 'Image', + format: {}, + dataset: {}, + }, + ], + segmentFormat: {}, + blockType: 'Paragraph', + format: {}, + }, + { + formatHolder: { + isSelected: false, + segmentType: 'SelectionMarker', + format: {}, + }, + levels: [ + { + listType: 'OL', + format: { + startNumberOverride: 1, + listStyleType: 'decimal', + }, + dataset: { + editingInfo: + '{"applyListStyleFromLevel":false,"orderedStyleType":1}', + }, + }, + ], + blockType: 'BlockGroup', + format: {}, + blockGroupType: 'ListItem', + blocks: [ + { + segments: [ + { + text: 'test', + segmentType: 'Text', + format: {}, + }, + ], + segmentFormat: {}, + blockType: 'Paragraph', + format: {}, + }, + ], + }, + { + formatHolder: { + isSelected: false, + segmentType: 'SelectionMarker', + format: {}, + }, + levels: [ + { + listType: 'OL', + format: { + listStyleType: 'decimal', + }, + dataset: { + editingInfo: + '{"applyListStyleFromLevel":false,"orderedStyleType":1}', + }, + }, + ], + blockType: 'BlockGroup', + format: {}, + blockGroupType: 'ListItem', + blocks: [ + { + segments: [ + { + src: + 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAsJCQcJCQcJCQkJCwkJCQkJCQsJCwsMCwsLDA0QDB...', + segmentType: 'Image', + format: {}, + dataset: {}, + }, + ], + segmentFormat: {}, + blockType: 'Paragraph', + format: {}, + }, + ], + }, + { + widths: [153], + rows: [ + { + height: 157, + cells: [ + { + spanAbove: false, + spanLeft: false, + isHeader: false, + blockGroupType: 'TableCell', + blocks: [ + { + segments: [ + { + src: + 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAsJCQcJCQcJCQkJCwkJCQkJCQsJCwsMCwsLDA0QDB...', + segmentType: 'Image', + format: {}, + dataset: {}, + }, + ], + segmentFormat: {}, + blockType: 'Paragraph', + format: {}, + }, + ], + format: {}, + dataset: {}, + }, + ], + format: {}, + }, + ], + blockType: 'Table', + format: {}, + dataset: { + editingInfo: + '{"topBorderColor":"#ABABAB","bottomBorderColor":"#ABABAB","verticalBorderColor":"#ABABAB","hasHeaderRow":false,"hasFirstColumn":false,"hasBandedRows":false,"hasBandedColumns":false,"bgColorEven":null,"bgColorOdd":"#ABABAB20","headerRowColor":"#ABABAB","tableBorderFormat":0,"verticalAlign":"top"}', + }, + }, + { + isImplicit: true, + segments: [ + { + isSelected: true, + segmentType: 'SelectionMarker', + format: {}, + }, + ], + segmentFormat: {}, + blockType: 'Paragraph', + format: {}, + }, + { + segments: [ + { + segmentType: 'Br', + format: {}, + }, + ], + segmentFormat: {}, + blockType: 'Paragraph', + format: {}, + }, + { + segments: [ + { + segmentType: 'Br', + format: {}, + }, + ], + segmentFormat: {}, + blockType: 'Paragraph', + format: {}, + }, + { + segments: [ + { + segmentType: 'Br', + format: {}, + }, + ], + segmentFormat: {}, + blockType: 'Paragraph', + format: {}, + }, + { + formatHolder: { + isSelected: false, + segmentType: 'SelectionMarker', + format: {}, + }, + levels: [ + { + listType: 'OL', + format: { + listStyleType: 'decimal', + displayForDummyItem: 'block', + }, + dataset: { + editingInfo: + '{"applyListStyleFromLevel":false,"orderedStyleType":1}', + }, + }, + ], + blockType: 'BlockGroup', + format: {}, + blockGroupType: 'ListItem', + blocks: [ + { + segments: [ + { + segmentType: 'Br', + format: {}, + }, + ], + segmentFormat: {}, + blockType: 'Paragraph', + format: {}, + }, + ], + }, + { + segments: [ + { + segmentType: 'Br', + format: {}, + }, + ], + segmentFormat: {}, + blockType: 'Paragraph', + format: {}, + }, + ], + format: {}, + }; + + const expected: ReadonlyContentModelImage[] = [ + { + src: + 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAsJCQcJCQcJCQkJCwkJCQkJCQsJCwsMCwsLDA0QDB...', + segmentType: 'Image', + format: {}, + dataset: {}, + }, + { + src: + 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAsJCQcJCQcJCQkJCwkJCQkJCQsJCwsMCwsLDA0QDB...', + segmentType: 'Image', + format: {}, + dataset: {}, + }, + { + src: + 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAsJCQcJCQcJCQkJCwkJCQkJCQsJCwsMCwsLDA0QDB...', + segmentType: 'Image', + format: {}, + dataset: {}, + }, + ]; + + const result = queryContentModel(model, { + segmentType: 'Image', + }); + expect(result).toEqual(expected); + }); + + it('should return image from a word online table', () => { + const model: ReadonlyContentModelBlockGroup = { + blockGroupType: 'Document', + blocks: [ + { + widths: [], + rows: [ + { + height: 0, + cells: [ + { + spanAbove: false, + spanLeft: false, + isHeader: false, + blockGroupType: 'TableCell', + blocks: [ + { + tagName: 'div', + blockType: 'BlockGroup', + format: { + textAlign: 'start', + marginLeft: '0px', + marginRight: '0px', + marginTop: '0px', + marginBottom: '0px', + paddingRight: '7px', + paddingLeft: '7px', + }, + blockGroupType: 'FormatContainer', + blocks: [ + { + segments: [ + { + text: ' ', + segmentType: 'Text', + format: { + fontFamily: + 'Aptos, Aptos_EmbeddedFont, Aptos_MSFontService, sans-serif', + fontSize: '12pt', + textColor: 'rgb(0, 0, 0)', + italic: false, + fontWeight: 'normal', + lineHeight: '18px', + }, + }, + ], + segmentFormat: { + italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', + }, + blockType: 'Paragraph', + format: { + textAlign: 'start', + direction: 'ltr', + marginLeft: '0px', + marginRight: '0px', + textIndent: '0px', + whiteSpace: 'pre-wrap', + marginTop: '0px', + marginBottom: '0px', + }, + decorator: { + tagName: 'p', + format: {}, + }, + }, + ], + }, + ], + format: { + textAlign: 'start', + borderTop: '1px solid', + borderRight: '1px solid', + borderBottom: '1px solid', + borderLeft: '1px solid', + verticalAlign: 'top', + width: '312px', + }, + dataset: { + celllook: '0', + }, + }, + { + spanAbove: false, + spanLeft: false, + isHeader: false, + blockGroupType: 'TableCell', + blocks: [ + { + tagName: 'div', + blockType: 'BlockGroup', + format: { + textAlign: 'start', + marginLeft: '0px', + marginRight: '0px', + marginTop: '0px', + marginBottom: '0px', + paddingRight: '7px', + paddingLeft: '7px', + }, + blockGroupType: 'FormatContainer', + blocks: [ + { + segments: [ + { + text: ' ', + segmentType: 'Text', + format: { + fontFamily: + 'Aptos, Aptos_EmbeddedFont, Aptos_MSFontService, sans-serif', + fontSize: '12pt', + textColor: 'rgb(0, 0, 0)', + italic: false, + fontWeight: 'normal', + lineHeight: '18px', + }, + }, + { + src: + 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAsJCQcJCQcJCQkJCwkJCQkJCQsJCwsMCwsLDA0QDB...', + isSelectedAsImageSelection: true, + segmentType: 'Image', + isSelected: true, + format: { + fontFamily: + 'Aptos, Aptos_EmbeddedFont, Aptos_MSFontService, sans-serif', + fontSize: '12pt', + textColor: 'rgb(0, 0, 0)', + italic: false, + fontWeight: 'normal', + lineHeight: '18px', + backgroundColor: '', + maxWidth: '1492px', + id: 'image_0', + }, + dataset: { + isEditing: 'true', + }, + }, + ], + segmentFormat: { + italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', + }, + blockType: 'Paragraph', + format: { + textAlign: 'start', + direction: 'ltr', + marginLeft: '0px', + marginRight: '0px', + textIndent: '0px', + whiteSpace: 'pre-wrap', + marginTop: '0px', + marginBottom: '0px', + }, + decorator: { + tagName: 'p', + format: {}, + }, + }, + ], + }, + ], + format: { + textAlign: 'start', + borderTop: '1px solid', + borderRight: '1px solid', + borderBottom: '1px solid', + borderLeft: '1px solid', + verticalAlign: 'top', + width: '312px', + }, + dataset: { + celllook: '0', + }, + }, + ], + format: {}, + }, + ], + blockType: 'Table', + format: { + textAlign: 'start', + marginTop: '0px', + marginRight: '0px', + marginBottom: '0px', + marginLeft: '0px', + width: '0px', + tableLayout: 'fixed', + borderCollapse: true, + }, + dataset: { + tablelook: '1696', + tablestyle: 'MsoTableGrid', + }, + }, + { + segments: [ + { + segmentType: 'Br', + format: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + }, + }, + ], + segmentFormat: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + }, + blockType: 'Paragraph', + format: {}, + }, + ], + format: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: '#000000', + }, + }; + const image: ReadonlyContentModelImage = { + src: + 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAsJCQcJCQcJCQkJCwkJCQkJCQsJCwsMCwsLDA0QDB...', + isSelectedAsImageSelection: true, + segmentType: 'Image', + isSelected: true, + format: { + fontFamily: 'Aptos, Aptos_EmbeddedFont, Aptos_MSFontService, sans-serif', + fontSize: '12pt', + textColor: 'rgb(0, 0, 0)', + italic: false, + fontWeight: 'normal', + lineHeight: '18px', + backgroundColor: '', + maxWidth: '1492px', + id: 'image_0', + }, + dataset: { + isEditing: 'true', + }, + }; + const result = queryContentModel(model, { + segmentType: 'Image', + findFirstOnly: true, + selector: (segment: ReadonlyContentModelImage) => !!segment.dataset.isEditing, + }); + expect(result).toEqual([image]); + }); +}); diff --git a/packages/roosterjs-content-model-plugins/lib/imageEdit/utils/findEditingImage.ts b/packages/roosterjs-content-model-plugins/lib/imageEdit/utils/findEditingImage.ts index 4fff6e96286..b49de71efcb 100644 --- a/packages/roosterjs-content-model-plugins/lib/imageEdit/utils/findEditingImage.ts +++ b/packages/roosterjs-content-model-plugins/lib/imageEdit/utils/findEditingImage.ts @@ -1,6 +1,7 @@ +import { queryContentModel } from 'roosterjs-content-model-api'; import type { ReadonlyContentModelBlockGroup, - ReadonlyContentModelTable, + ReadonlyContentModelParagraph, } from 'roosterjs-content-model-types'; import type { ImageAndParagraph } from '../types/ImageAndParagraph'; @@ -11,65 +12,21 @@ export function findEditingImage( group: ReadonlyContentModelBlockGroup, imageId?: string ): ImageAndParagraph | null { - for (let i = 0; i < group.blocks.length; i++) { - const block = group.blocks[i]; - - switch (block.blockType) { - case 'BlockGroup': - const result = findEditingImage(block, imageId); - - if (result) { - return result; - } - break; - - case 'Paragraph': - for (let j = 0; j < block.segments.length; j++) { - const segment = block.segments[j]; - switch (segment.segmentType) { - case 'Image': - if ( - (imageId && segment.format.id == imageId) || - segment.dataset.isEditing - ) { - return { - paragraph: block, - image: segment, - }; - } - break; - - case 'General': - const result = findEditingImage(segment, imageId); - - if (result) { - return result; - } - break; - } + let imageAndParagraph: ImageAndParagraph | null = null; + queryContentModel(group, { + selector: (paragraph: ReadonlyContentModelParagraph) => { + for (const segment of paragraph.segments) { + if ( + segment.segmentType == 'Image' && + ((imageId && segment.format.id == imageId) || segment.dataset.isEditing) + ) { + imageAndParagraph = { image: segment, paragraph }; + break; } - break; - case 'Table': - const imageInTable = findEditingImageOnTable(block, imageId); - - if (imageInTable) { - return imageInTable; - } - break; - } - } - - return null; -} - -const findEditingImageOnTable = (table: ReadonlyContentModelTable, imageId?: string) => { - for (const row of table.rows) { - for (const cell of row.cells) { - const result = findEditingImage(cell, imageId); - if (result) { - return result; } - } - } - return null; -}; + return !!imageAndParagraph; + }, + findFirstOnly: true, + }); + return imageAndParagraph; +} From 2a56e9a27341b73c973db03aa4002899991997f7 Mon Sep 17 00:00:00 2001 From: "Julia Roldi (from Dev Box)" Date: Tue, 29 Oct 2024 16:39:53 -0300 Subject: [PATCH 2/9] nit --- .../lib/imageEdit/utils/findEditingImage.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/roosterjs-content-model-plugins/lib/imageEdit/utils/findEditingImage.ts b/packages/roosterjs-content-model-plugins/lib/imageEdit/utils/findEditingImage.ts index b49de71efcb..35bc9f8140c 100644 --- a/packages/roosterjs-content-model-plugins/lib/imageEdit/utils/findEditingImage.ts +++ b/packages/roosterjs-content-model-plugins/lib/imageEdit/utils/findEditingImage.ts @@ -21,12 +21,13 @@ export function findEditingImage( ((imageId && segment.format.id == imageId) || segment.dataset.isEditing) ) { imageAndParagraph = { image: segment, paragraph }; - break; + return true; } } - return !!imageAndParagraph; + return false; }, findFirstOnly: true, }); + return imageAndParagraph; } From b5d60cd17cdadb909a09ac2249e184aa140864ce Mon Sep 17 00:00:00 2001 From: "Julia Roldi (from Dev Box)" Date: Mon, 4 Nov 2024 14:52:15 -0300 Subject: [PATCH 3/9] WIP --- .../lib/modelApi/common/queryContentModel.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/roosterjs-content-model-api/lib/modelApi/common/queryContentModel.ts b/packages/roosterjs-content-model-api/lib/modelApi/common/queryContentModel.ts index 0ff93545342..048b7447609 100644 --- a/packages/roosterjs-content-model-api/lib/modelApi/common/queryContentModel.ts +++ b/packages/roosterjs-content-model-api/lib/modelApi/common/queryContentModel.ts @@ -47,9 +47,10 @@ export function queryContentModel< for (let i = 0; i < group.blocks.length; i++) { if (findFirstOnly && elements.length > 0) { - break; + return elements; } const block = group.blocks[i]; + console.log(block.blockType); switch (block.blockType) { case 'BlockGroup': if (type == block.blockType && (!selector || selector(block as T))) { @@ -76,7 +77,7 @@ export function queryContentModel< if (!segmentType && (!selector || selector(block as T))) { elements.push(block as T); } else if (segmentType) { - const segments = searchInParagraphs(block, segmentType, selector); + const segments = searchInParagraphs(block, segmentType, options, selector); elements.push(...(segments as T[])); } } @@ -95,7 +96,6 @@ function searchInTables(cell, options); - blocks.push(...items); } } @@ -105,12 +105,18 @@ function searchInTables( block: ReadonlyContentModelParagraph, segmentType: ContentModelSegmentType, + options: QueryContentModelOptions

, selector?: (element: P) => boolean ): P[] { const segments: P[] = []; for (const segment of block.segments) { if (segment.segmentType == segmentType && (!selector || selector(segment as P))) { - segments.push(segment as P); + if (segment.segmentType !== 'General') { + segments.push(segment as P); + } else { + const blocks = queryContentModel

(segment, options); + segments.push(...blocks); + } } } return segments; From 6370fded7131838ea8626438e3dd0433194080fe Mon Sep 17 00:00:00 2001 From: "Julia Roldi (from Dev Box)" Date: Mon, 4 Nov 2024 15:49:03 -0300 Subject: [PATCH 4/9] nits --- .../lib/modelApi/common/queryContentModel.ts | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/packages/roosterjs-content-model-api/lib/modelApi/common/queryContentModel.ts b/packages/roosterjs-content-model-api/lib/modelApi/common/queryContentModel.ts index 048b7447609..e89a351f656 100644 --- a/packages/roosterjs-content-model-api/lib/modelApi/common/queryContentModel.ts +++ b/packages/roosterjs-content-model-api/lib/modelApi/common/queryContentModel.ts @@ -50,7 +50,6 @@ export function queryContentModel< return elements; } const block = group.blocks[i]; - console.log(block.blockType); switch (block.blockType) { case 'BlockGroup': if (type == block.blockType && (!selector || selector(block as T))) { @@ -77,7 +76,7 @@ export function queryContentModel< if (!segmentType && (!selector || selector(block as T))) { elements.push(block as T); } else if (segmentType) { - const segments = searchInParagraphs(block, segmentType, options, selector); + const segments = searchInParagraphs(block, segmentType, selector); elements.push(...(segments as T[])); } } @@ -105,18 +104,12 @@ function searchInTables( block: ReadonlyContentModelParagraph, segmentType: ContentModelSegmentType, - options: QueryContentModelOptions

, selector?: (element: P) => boolean ): P[] { const segments: P[] = []; for (const segment of block.segments) { if (segment.segmentType == segmentType && (!selector || selector(segment as P))) { - if (segment.segmentType !== 'General') { - segments.push(segment as P); - } else { - const blocks = queryContentModel

(segment, options); - segments.push(...blocks); - } + segments.push(segment as P); } } return segments; From c172ca4c63ca9cd5b707838bcbef863ae1b7973c Mon Sep 17 00:00:00 2001 From: "Julia Roldi (from Dev Box)" Date: Tue, 5 Nov 2024 14:10:07 -0300 Subject: [PATCH 5/9] refactor --- .../controlsV2/demoButtons/cutButton.ts | 22 + .../roosterjs-content-model-api/lib/index.ts | 5 +- .../lib/modelApi/common/queryContentModel.ts | 116 ----- .../common/queryContentModelBlocks.ts | 108 ++++ ...Test.ts => queryContentModelBlocksTest.ts} | 490 +++--------------- .../lib/imageEdit/utils/findEditingImage.ts | 8 +- 6 files changed, 223 insertions(+), 526 deletions(-) create mode 100644 demo/scripts/controlsV2/demoButtons/cutButton.ts delete mode 100644 packages/roosterjs-content-model-api/lib/modelApi/common/queryContentModel.ts create mode 100644 packages/roosterjs-content-model-api/lib/modelApi/common/queryContentModelBlocks.ts rename packages/roosterjs-content-model-api/test/modelApi/common/{queryContentModelTest.ts => queryContentModelBlocksTest.ts} (78%) diff --git a/demo/scripts/controlsV2/demoButtons/cutButton.ts b/demo/scripts/controlsV2/demoButtons/cutButton.ts new file mode 100644 index 00000000000..9c118a4a9b1 --- /dev/null +++ b/demo/scripts/controlsV2/demoButtons/cutButton.ts @@ -0,0 +1,22 @@ +import type { RibbonButton } from 'roosterjs-react'; + +/** + * Key of localized strings of Cut button + */ +export type CutButtonStringKey = 'buttonNameCut'; + +/** + * "Cut" button on the format ribbon + */ +export const cutButton: RibbonButton = { + key: 'buttonNameCut', + unlocalizedText: ' Cut', + iconName: 'ClearNight', + onClick: editor => { + const selection = editor.getDOMSelection(); + if (selection) { + document.execCommand('cut'); + } + return true; + }, +}; diff --git a/packages/roosterjs-content-model-api/lib/index.ts b/packages/roosterjs-content-model-api/lib/index.ts index 2f1558ac367..88d60893bc5 100644 --- a/packages/roosterjs-content-model-api/lib/index.ts +++ b/packages/roosterjs-content-model-api/lib/index.ts @@ -60,4 +60,7 @@ export { setModelIndentation } from './modelApi/block/setModelIndentation'; export { matchLink } from './modelApi/link/matchLink'; export { promoteLink } from './modelApi/link/promoteLink'; export { getListAnnounceData } from './modelApi/list/getListAnnounceData'; -export { queryContentModel, QueryContentModelOptions } from './modelApi/common/queryContentModel'; +export { + queryContentModelBlocks, + QueryContentModelOptions, +} from './modelApi/common/queryContentModelBlocks'; diff --git a/packages/roosterjs-content-model-api/lib/modelApi/common/queryContentModel.ts b/packages/roosterjs-content-model-api/lib/modelApi/common/queryContentModel.ts deleted file mode 100644 index e89a351f656..00000000000 --- a/packages/roosterjs-content-model-api/lib/modelApi/common/queryContentModel.ts +++ /dev/null @@ -1,116 +0,0 @@ -import type { - ContentModelBlockType, - ContentModelSegmentType, - ReadonlyContentModelBlock, - ReadonlyContentModelBlockGroup, - ReadonlyContentModelParagraph, - ReadonlyContentModelSegment, - ReadonlyContentModelTable, -} from 'roosterjs-content-model-types'; - -/** - * Options for queryContentModel - */ -export interface QueryContentModelOptions { - /** - * The type of block to query @default 'Paragraph' - */ - type?: ContentModelBlockType; - - /** - * The type of segment to query - */ - segmentType?: ContentModelSegmentType; - - /** - * Optional selector to filter the blocks/segments - */ - selector?: (element: T) => boolean; - - /** - * True to return the first block only, false to return all blocks - */ - findFirstOnly?: boolean; -} - -/** - * Query content model blocks or segments - * @param group The block group to query - * @param options The query option - */ -export function queryContentModel< - T extends ReadonlyContentModelBlock | ReadonlyContentModelSegment ->(group: ReadonlyContentModelBlockGroup, options: QueryContentModelOptions): T[] { - const elements: T[] = []; - const searchOptions = options.type ? options : { ...options, type: 'Paragraph' }; - const { type, segmentType, selector, findFirstOnly } = searchOptions; - - for (let i = 0; i < group.blocks.length; i++) { - if (findFirstOnly && elements.length > 0) { - return elements; - } - const block = group.blocks[i]; - switch (block.blockType) { - case 'BlockGroup': - if (type == block.blockType && (!selector || selector(block as T))) { - elements.push(block as T); - } - const blockGroupsResults = queryContentModel(block, options); - elements.push(...(blockGroupsResults as T[])); - break; - case 'Table': - if (type == block.blockType && (!selector || selector(block as T))) { - elements.push(block as T); - } - const tableResults = searchInTables(block, options); - elements.push(...(tableResults as T[])); - break; - case 'Divider': - case 'Entity': - if (type == block.blockType && (!selector || selector(block as T))) { - elements.push(block as T); - } - break; - case 'Paragraph': - if (type == block.blockType) { - if (!segmentType && (!selector || selector(block as T))) { - elements.push(block as T); - } else if (segmentType) { - const segments = searchInParagraphs(block, segmentType, selector); - elements.push(...(segments as T[])); - } - } - break; - } - } - - return elements; -} - -function searchInTables( - table: ReadonlyContentModelTable, - options: QueryContentModelOptions -): T[] { - const blocks: T[] = []; - for (const row of table.rows) { - for (const cell of row.cells) { - const items = queryContentModel(cell, options); - blocks.push(...items); - } - } - return blocks; -} - -function searchInParagraphs

( - block: ReadonlyContentModelParagraph, - segmentType: ContentModelSegmentType, - selector?: (element: P) => boolean -): P[] { - const segments: P[] = []; - for (const segment of block.segments) { - if (segment.segmentType == segmentType && (!selector || selector(segment as P))) { - segments.push(segment as P); - } - } - return segments; -} diff --git a/packages/roosterjs-content-model-api/lib/modelApi/common/queryContentModelBlocks.ts b/packages/roosterjs-content-model-api/lib/modelApi/common/queryContentModelBlocks.ts new file mode 100644 index 00000000000..afceced01b5 --- /dev/null +++ b/packages/roosterjs-content-model-api/lib/modelApi/common/queryContentModelBlocks.ts @@ -0,0 +1,108 @@ +import type { + ContentModelBlockType, + ReadonlyContentModelBlock, + ReadonlyContentModelBlockGroup, + ReadonlyContentModelTable, +} from 'roosterjs-content-model-types'; + +/** + * Options for queryContentModel + */ +export interface QueryContentModelOptions { + /** + * The type of block to query @default 'Paragraph' + */ + blockType?: ContentModelBlockType; + + /** + * Optional selector to filter the blocks + */ + filter?: (element: T) => element is T; + + /** + * True to return the first block only, false to return all blocks + */ + findFirstOnly?: boolean; +} + +/** + * Query content model blocks + * @param group The block group to query + * @param options The query option + */ +export function queryContentModelBlocks( + group: ReadonlyContentModelBlockGroup, + options: QueryContentModelOptions +): T[] { + const { blockType, filter, findFirstOnly } = options; + const type = blockType || 'Paragraph'; + + return queryContentModelBlocksInternal(group, type, filter, findFirstOnly); +} + +function queryContentModelBlocksInternal( + group: ReadonlyContentModelBlockGroup, + type: ContentModelBlockType, + filter?: (element: T) => element is T, + findFirstOnly?: boolean +): T[] { + const elements: T[] = []; + for (let i = 0; i < group.blocks.length; i++) { + if (findFirstOnly && elements.length > 0) { + return elements; + } + const block = group.blocks[i]; + switch (block.blockType) { + case 'BlockGroup': + if (isBlockType(block, type) && (!filter || filter(block))) { + elements.push(block); + } + const blockGroupsResults = queryContentModelBlocksInternal( + block, + type, + filter, + findFirstOnly + ); + elements.push(...blockGroupsResults); + break; + case 'Table': + if (isBlockType(block, type) && (!filter || filter(block))) { + elements.push(block); + } + const tableResults = searchInTables(block, type, filter, findFirstOnly); + elements.push(...tableResults); + break; + case 'Divider': + case 'Entity': + case 'Paragraph': + if (isBlockType(block, type) && (!filter || filter(block))) { + elements.push(block); + } + break; + } + } + return elements; +} + +function isBlockType( + block: ReadonlyContentModelBlock, + type: string +): block is T { + return block.blockType == type; +} + +function searchInTables( + table: ReadonlyContentModelTable, + type: ContentModelBlockType, + filter?: (element: T) => element is T, + findFirstOnly?: boolean +): T[] { + const blocks: T[] = []; + for (const row of table.rows) { + for (const cell of row.cells) { + const items = queryContentModelBlocksInternal(cell, type, filter, findFirstOnly); + blocks.push(...items); + } + } + return blocks; +} diff --git a/packages/roosterjs-content-model-api/test/modelApi/common/queryContentModelTest.ts b/packages/roosterjs-content-model-api/test/modelApi/common/queryContentModelBlocksTest.ts similarity index 78% rename from packages/roosterjs-content-model-api/test/modelApi/common/queryContentModelTest.ts rename to packages/roosterjs-content-model-api/test/modelApi/common/queryContentModelBlocksTest.ts index 9306f1ead08..34c64d45c49 100644 --- a/packages/roosterjs-content-model-api/test/modelApi/common/queryContentModelTest.ts +++ b/packages/roosterjs-content-model-api/test/modelApi/common/queryContentModelBlocksTest.ts @@ -1,13 +1,12 @@ -import { queryContentModel } from '../../../lib/modelApi/common/queryContentModel'; +import { queryContentModelBlocks } from '../../../lib/modelApi/common/queryContentModelBlocks'; import { ReadonlyContentModelBlockGroup, - ReadonlyContentModelImage, ReadonlyContentModelListItem, ReadonlyContentModelParagraph, ReadonlyContentModelTable, } from 'roosterjs-content-model-types'; -describe('queryContentModel', () => { +describe('queryContentModelBlocksBlocks', () => { it('should return empty array if no blocks', () => { // Arrange const group: ReadonlyContentModelBlockGroup = { @@ -16,7 +15,7 @@ describe('queryContentModel', () => { }; // Act - const result = queryContentModel(group, {}); + const result = queryContentModelBlocks(group, {}); // Assert expect(result).toEqual([]); @@ -37,7 +36,7 @@ describe('queryContentModel', () => { }; // Act - const result = queryContentModel(group, { type: 'Table' }); + const result = queryContentModelBlocks(group, { blockType: 'Table' }); // Assert expect(result).toEqual([]); @@ -319,7 +318,9 @@ describe('queryContentModel', () => { ]; // Act - const result = queryContentModel(group, { type: 'Table' }); + const result = queryContentModelBlocks(group, { + blockType: 'Table', + }); // Assert expect(result).toEqual(expected); @@ -376,126 +377,15 @@ describe('queryContentModel', () => { }; // Act - const result = queryContentModel(group, { - type: 'Paragraph', - - selector: block => block.segments.length == 2, + const result = queryContentModelBlocks(group, { + blockType: 'Paragraph', + filter: (block): block is ReadonlyContentModelParagraph => block.segments.length == 2, }); // Assert expect(result).toEqual([paragraph]); }); - it('should return first segment that match the type and selector', () => { - const image: ReadonlyContentModelImage = { - src: - 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAsJCQcJCQcJCQkJCwkJCQkJCQsJCwsMCwsLDA0QDB...', - isSelectedAsImageSelection: true, - segmentType: 'Image', - isSelected: true, - format: { - fontFamily: 'Calibri', - fontSize: '11pt', - textColor: 'rgb(0, 0, 0)', - id: 'image_0', - maxWidth: '1492px', - }, - dataset: { - isEditing: 'true', - }, - }; - const model: ReadonlyContentModelBlockGroup = { - blockGroupType: 'Document', - blocks: [ - { - widths: [120, 153], - rows: [ - { - height: 157, - cells: [ - { - spanAbove: false, - spanLeft: false, - isHeader: false, - blockGroupType: 'TableCell', - blocks: [ - { - segments: [ - { - segmentType: 'Br', - format: {}, - }, - ], - segmentFormat: {}, - blockType: 'Paragraph', - format: {}, - }, - ], - format: {}, - dataset: {}, - }, - { - spanAbove: false, - spanLeft: false, - isHeader: false, - blockGroupType: 'TableCell', - blocks: [ - { - segments: [image], - segmentFormat: {}, - blockType: 'Paragraph', - format: {}, - }, - ], - format: {}, - dataset: {}, - }, - ], - format: {}, - }, - ], - blockType: 'Table', - format: { - useBorderBox: true, - borderCollapse: true, - }, - dataset: { - editingInfo: - '{"topBorderColor":"#ABABAB","bottomBorderColor":"#ABABAB","verticalBorderColor":"#ABABAB","hasHeaderRow":false,"hasFirstColumn":false,"hasBandedRows":false,"hasBandedColumns":false,"bgColorEven":null,"bgColorOdd":"#ABABAB20","headerRowColor":"#ABABAB","tableBorderFormat":0,"verticalAlign":"top"}', - }, - }, - { - segments: [ - { - segmentType: 'Br', - format: {}, - }, - ], - segmentFormat: {}, - blockType: 'Paragraph', - format: {}, - }, - { - segments: [ - { - segmentType: 'Br', - format: {}, - }, - ], - segmentFormat: {}, - blockType: 'Paragraph', - format: {}, - }, - ], - }; - const result = queryContentModel(model, { - segmentType: 'Image', - selector: (segment: ReadonlyContentModelImage) => !!segment.dataset.isEditing, - findFirstOnly: true, - }); - expect(result).toEqual([image]); - }); - it('should return all tables that match the type and selector', () => { const model: ReadonlyContentModelBlockGroup = { blockGroupType: 'Document', @@ -875,7 +765,9 @@ describe('queryContentModel', () => { }, }, ]; - const result = queryContentModel(model, { type: 'Table' }); + const result = queryContentModelBlocks(model, { + blockType: 'Table', + }); expect(result).toEqual(expected); }); @@ -1013,7 +905,9 @@ describe('queryContentModel', () => { }; const expected: ReadonlyContentModelTable[] = [table]; - const result = queryContentModel(model, { type: 'Table' }); + const result = queryContentModelBlocks(model, { + blockType: 'Table', + }); expect(result).toEqual(expected); }); @@ -1273,274 +1167,14 @@ describe('queryContentModel', () => { }, ]; - const result = queryContentModel(model, { - type: 'BlockGroup', - selector: block => block.blockGroupType == 'ListItem', + const result = queryContentModelBlocks(model, { + blockType: 'BlockGroup', + filter: (block): block is ReadonlyContentModelListItem => + block.blockGroupType == 'ListItem', }); expect(result).toEqual(listExpected); }); - it('should return all images', () => { - const model: ReadonlyContentModelBlockGroup = { - blockGroupType: 'Document', - blocks: [ - { - segments: [ - { - src: - 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAsJCQcJCQcJCQkJCwkJCQkJCQsJCwsMCwsLDA0QDB...', - segmentType: 'Image', - format: {}, - dataset: {}, - }, - ], - segmentFormat: {}, - blockType: 'Paragraph', - format: {}, - }, - { - formatHolder: { - isSelected: false, - segmentType: 'SelectionMarker', - format: {}, - }, - levels: [ - { - listType: 'OL', - format: { - startNumberOverride: 1, - listStyleType: 'decimal', - }, - dataset: { - editingInfo: - '{"applyListStyleFromLevel":false,"orderedStyleType":1}', - }, - }, - ], - blockType: 'BlockGroup', - format: {}, - blockGroupType: 'ListItem', - blocks: [ - { - segments: [ - { - text: 'test', - segmentType: 'Text', - format: {}, - }, - ], - segmentFormat: {}, - blockType: 'Paragraph', - format: {}, - }, - ], - }, - { - formatHolder: { - isSelected: false, - segmentType: 'SelectionMarker', - format: {}, - }, - levels: [ - { - listType: 'OL', - format: { - listStyleType: 'decimal', - }, - dataset: { - editingInfo: - '{"applyListStyleFromLevel":false,"orderedStyleType":1}', - }, - }, - ], - blockType: 'BlockGroup', - format: {}, - blockGroupType: 'ListItem', - blocks: [ - { - segments: [ - { - src: - 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAsJCQcJCQcJCQkJCwkJCQkJCQsJCwsMCwsLDA0QDB...', - segmentType: 'Image', - format: {}, - dataset: {}, - }, - ], - segmentFormat: {}, - blockType: 'Paragraph', - format: {}, - }, - ], - }, - { - widths: [153], - rows: [ - { - height: 157, - cells: [ - { - spanAbove: false, - spanLeft: false, - isHeader: false, - blockGroupType: 'TableCell', - blocks: [ - { - segments: [ - { - src: - 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAsJCQcJCQcJCQkJCwkJCQkJCQsJCwsMCwsLDA0QDB...', - segmentType: 'Image', - format: {}, - dataset: {}, - }, - ], - segmentFormat: {}, - blockType: 'Paragraph', - format: {}, - }, - ], - format: {}, - dataset: {}, - }, - ], - format: {}, - }, - ], - blockType: 'Table', - format: {}, - dataset: { - editingInfo: - '{"topBorderColor":"#ABABAB","bottomBorderColor":"#ABABAB","verticalBorderColor":"#ABABAB","hasHeaderRow":false,"hasFirstColumn":false,"hasBandedRows":false,"hasBandedColumns":false,"bgColorEven":null,"bgColorOdd":"#ABABAB20","headerRowColor":"#ABABAB","tableBorderFormat":0,"verticalAlign":"top"}', - }, - }, - { - isImplicit: true, - segments: [ - { - isSelected: true, - segmentType: 'SelectionMarker', - format: {}, - }, - ], - segmentFormat: {}, - blockType: 'Paragraph', - format: {}, - }, - { - segments: [ - { - segmentType: 'Br', - format: {}, - }, - ], - segmentFormat: {}, - blockType: 'Paragraph', - format: {}, - }, - { - segments: [ - { - segmentType: 'Br', - format: {}, - }, - ], - segmentFormat: {}, - blockType: 'Paragraph', - format: {}, - }, - { - segments: [ - { - segmentType: 'Br', - format: {}, - }, - ], - segmentFormat: {}, - blockType: 'Paragraph', - format: {}, - }, - { - formatHolder: { - isSelected: false, - segmentType: 'SelectionMarker', - format: {}, - }, - levels: [ - { - listType: 'OL', - format: { - listStyleType: 'decimal', - displayForDummyItem: 'block', - }, - dataset: { - editingInfo: - '{"applyListStyleFromLevel":false,"orderedStyleType":1}', - }, - }, - ], - blockType: 'BlockGroup', - format: {}, - blockGroupType: 'ListItem', - blocks: [ - { - segments: [ - { - segmentType: 'Br', - format: {}, - }, - ], - segmentFormat: {}, - blockType: 'Paragraph', - format: {}, - }, - ], - }, - { - segments: [ - { - segmentType: 'Br', - format: {}, - }, - ], - segmentFormat: {}, - blockType: 'Paragraph', - format: {}, - }, - ], - format: {}, - }; - - const expected: ReadonlyContentModelImage[] = [ - { - src: - 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAsJCQcJCQcJCQkJCwkJCQkJCQsJCwsMCwsLDA0QDB...', - segmentType: 'Image', - format: {}, - dataset: {}, - }, - { - src: - 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAsJCQcJCQcJCQkJCwkJCQkJCQsJCwsMCwsLDA0QDB...', - segmentType: 'Image', - format: {}, - dataset: {}, - }, - { - src: - 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAsJCQcJCQcJCQkJCwkJCQkJCQsJCwsMCwsLDA0QDB...', - segmentType: 'Image', - format: {}, - dataset: {}, - }, - ]; - - const result = queryContentModel(model, { - segmentType: 'Image', - }); - expect(result).toEqual(expected); - }); - it('should return image from a word online table', () => { const model: ReadonlyContentModelBlockGroup = { blockGroupType: 'Document', @@ -1765,32 +1399,76 @@ describe('queryContentModel', () => { textColor: '#000000', }, }; - const image: ReadonlyContentModelImage = { - src: - 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAsJCQcJCQcJCQkJCwkJCQkJCQsJCwsMCwsLDA0QDB...', - isSelectedAsImageSelection: true, - segmentType: 'Image', - isSelected: true, - format: { - fontFamily: 'Aptos, Aptos_EmbeddedFont, Aptos_MSFontService, sans-serif', - fontSize: '12pt', - textColor: 'rgb(0, 0, 0)', + const imageAndParagraph: ReadonlyContentModelParagraph = { + segments: [ + { + text: ' ', + segmentType: 'Text', + format: { + fontFamily: 'Aptos, Aptos_EmbeddedFont, Aptos_MSFontService, sans-serif', + fontSize: '12pt', + textColor: 'rgb(0, 0, 0)', + italic: false, + fontWeight: 'normal', + lineHeight: '18px', + }, + }, + { + src: + 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAsJCQcJCQcJCQkJCwkJCQkJCQsJCwsMCwsLDA0QDB...', + isSelectedAsImageSelection: true, + segmentType: 'Image', + isSelected: true, + format: { + fontFamily: 'Aptos, Aptos_EmbeddedFont, Aptos_MSFontService, sans-serif', + fontSize: '12pt', + textColor: 'rgb(0, 0, 0)', + italic: false, + fontWeight: 'normal', + lineHeight: '18px', + backgroundColor: '', + maxWidth: '1492px', + id: 'image_0', + }, + dataset: { + isEditing: 'true', + }, + }, + ], + segmentFormat: { italic: false, fontWeight: 'normal', - lineHeight: '18px', - backgroundColor: '', - maxWidth: '1492px', - id: 'image_0', + textColor: 'rgb(0, 0, 0)', }, - dataset: { - isEditing: 'true', + blockType: 'Paragraph', + format: { + textAlign: 'start', + direction: 'ltr', + marginLeft: '0px', + marginRight: '0px', + textIndent: '0px', + whiteSpace: 'pre-wrap', + marginTop: '0px', + marginBottom: '0px', + }, + decorator: { + tagName: 'p', + format: {}, }, }; - const result = queryContentModel(model, { - segmentType: 'Image', + const result = queryContentModelBlocks(model, { findFirstOnly: true, - selector: (segment: ReadonlyContentModelImage) => !!segment.dataset.isEditing, + filter: ( + block: ReadonlyContentModelParagraph + ): block is ReadonlyContentModelParagraph => { + for (const segment of block.segments) { + if (segment.segmentType == 'Image' && segment.dataset.isEditing) { + return true; + } + } + return false; + }, }); - expect(result).toEqual([image]); + expect(result).toEqual([imageAndParagraph]); }); }); diff --git a/packages/roosterjs-content-model-plugins/lib/imageEdit/utils/findEditingImage.ts b/packages/roosterjs-content-model-plugins/lib/imageEdit/utils/findEditingImage.ts index 35bc9f8140c..8c20467c371 100644 --- a/packages/roosterjs-content-model-plugins/lib/imageEdit/utils/findEditingImage.ts +++ b/packages/roosterjs-content-model-plugins/lib/imageEdit/utils/findEditingImage.ts @@ -1,4 +1,4 @@ -import { queryContentModel } from 'roosterjs-content-model-api'; +import { queryContentModelBlocks } from 'roosterjs-content-model-api'; import type { ReadonlyContentModelBlockGroup, ReadonlyContentModelParagraph, @@ -13,8 +13,10 @@ export function findEditingImage( imageId?: string ): ImageAndParagraph | null { let imageAndParagraph: ImageAndParagraph | null = null; - queryContentModel(group, { - selector: (paragraph: ReadonlyContentModelParagraph) => { + queryContentModelBlocks(group, { + filter: ( + paragraph: ReadonlyContentModelParagraph + ): paragraph is ReadonlyContentModelParagraph => { for (const segment of paragraph.segments) { if ( segment.segmentType == 'Image' && From 1f3422cef84c58d92ba4c311f251f9d5d4aa34ef Mon Sep 17 00:00:00 2001 From: "Julia Roldi (from Dev Box)" Date: Wed, 6 Nov 2024 11:23:34 -0300 Subject: [PATCH 6/9] refactor --- .../common/queryContentModelBlocks.ts | 100 +++++++++--------- 1 file changed, 50 insertions(+), 50 deletions(-) diff --git a/packages/roosterjs-content-model-api/lib/modelApi/common/queryContentModelBlocks.ts b/packages/roosterjs-content-model-api/lib/modelApi/common/queryContentModelBlocks.ts index afceced01b5..aed7d04f0f7 100644 --- a/packages/roosterjs-content-model-api/lib/modelApi/common/queryContentModelBlocks.ts +++ b/packages/roosterjs-content-model-api/lib/modelApi/common/queryContentModelBlocks.ts @@ -2,7 +2,6 @@ import type { ContentModelBlockType, ReadonlyContentModelBlock, ReadonlyContentModelBlockGroup, - ReadonlyContentModelTable, } from 'roosterjs-content-model-types'; /** @@ -36,73 +35,74 @@ export function queryContentModelBlocks( ): T[] { const { blockType, filter, findFirstOnly } = options; const type = blockType || 'Paragraph'; - - return queryContentModelBlocksInternal(group, type, filter, findFirstOnly); + const elements: T[] = []; + for (let i = 0; i < group.blocks.length; i++) { + if (findFirstOnly && elements.length > 0) { + return elements; + } + const block = group.blocks[i]; + const results = queryContentModelBlocksInternal(block, type, filter, findFirstOnly); + elements.push(...results); + } + return elements; } function queryContentModelBlocksInternal( - group: ReadonlyContentModelBlockGroup, + block: ReadonlyContentModelBlock, type: ContentModelBlockType, filter?: (element: T) => element is T, findFirstOnly?: boolean ): T[] { const elements: T[] = []; - for (let i = 0; i < group.blocks.length; i++) { - if (findFirstOnly && elements.length > 0) { - return elements; + if (isExpectedBlockType(block, type, filter)) { + elements.push(block); + } + + if (block.blockType == 'BlockGroup') { + for (const childBlock of block.blocks) { + if (findFirstOnly && elements.length > 0) { + return elements; + } + const results = queryContentModelBlocksInternal( + childBlock, + type, + filter, + findFirstOnly + ); + elements.push(...results); } - const block = group.blocks[i]; - switch (block.blockType) { - case 'BlockGroup': - if (isBlockType(block, type) && (!filter || filter(block))) { - elements.push(block); - } - const blockGroupsResults = queryContentModelBlocksInternal( - block, - type, - filter, - findFirstOnly - ); - elements.push(...blockGroupsResults); - break; - case 'Table': - if (isBlockType(block, type) && (!filter || filter(block))) { - elements.push(block); - } - const tableResults = searchInTables(block, type, filter, findFirstOnly); - elements.push(...tableResults); - break; - case 'Divider': - case 'Entity': - case 'Paragraph': - if (isBlockType(block, type) && (!filter || filter(block))) { - elements.push(block); + } + + if (block.blockType == 'Table') { + const table = block; + for (const row of table.rows) { + for (const cell of row.cells) { + for (const cellBlock of cell.blocks) { + const results = queryContentModelBlocksInternal( + cellBlock, + type, + filter, + findFirstOnly + ); + elements.push(...results); } - break; + } } } return elements; } +function isExpectedBlockType( + block: ReadonlyContentModelBlock, + type: ContentModelBlockType, + filter?: (element: T) => element is T +): block is T { + return isBlockType(block, type) && (!filter || filter(block)); +} + function isBlockType( block: ReadonlyContentModelBlock, type: string ): block is T { return block.blockType == type; } - -function searchInTables( - table: ReadonlyContentModelTable, - type: ContentModelBlockType, - filter?: (element: T) => element is T, - findFirstOnly?: boolean -): T[] { - const blocks: T[] = []; - for (const row of table.rows) { - for (const cell of row.cells) { - const items = queryContentModelBlocksInternal(cell, type, filter, findFirstOnly); - blocks.push(...items); - } - } - return blocks; -} From 3a00c66a0417fee350589830803fc50349e14a22 Mon Sep 17 00:00:00 2001 From: "Julia Roldi (from Dev Box)" Date: Wed, 6 Nov 2024 11:41:54 -0300 Subject: [PATCH 7/9] refactor --- .../roosterjs-content-model-api/lib/index.ts | 5 +- .../common/queryContentModelBlocks.ts | 29 +++--------- .../common/queryContentModelBlocksTest.ts | 47 +++++++++---------- .../lib/imageEdit/utils/findEditingImage.ts | 12 ++--- 4 files changed, 34 insertions(+), 59 deletions(-) diff --git a/packages/roosterjs-content-model-api/lib/index.ts b/packages/roosterjs-content-model-api/lib/index.ts index 88d60893bc5..292f44d08e5 100644 --- a/packages/roosterjs-content-model-api/lib/index.ts +++ b/packages/roosterjs-content-model-api/lib/index.ts @@ -60,7 +60,4 @@ export { setModelIndentation } from './modelApi/block/setModelIndentation'; export { matchLink } from './modelApi/link/matchLink'; export { promoteLink } from './modelApi/link/promoteLink'; export { getListAnnounceData } from './modelApi/list/getListAnnounceData'; -export { - queryContentModelBlocks, - QueryContentModelOptions, -} from './modelApi/common/queryContentModelBlocks'; +export { queryContentModelBlocks } from './modelApi/common/queryContentModelBlocks'; diff --git a/packages/roosterjs-content-model-api/lib/modelApi/common/queryContentModelBlocks.ts b/packages/roosterjs-content-model-api/lib/modelApi/common/queryContentModelBlocks.ts index aed7d04f0f7..ebd290de2a7 100644 --- a/packages/roosterjs-content-model-api/lib/modelApi/common/queryContentModelBlocks.ts +++ b/packages/roosterjs-content-model-api/lib/modelApi/common/queryContentModelBlocks.ts @@ -4,36 +4,19 @@ import type { ReadonlyContentModelBlockGroup, } from 'roosterjs-content-model-types'; -/** - * Options for queryContentModel - */ -export interface QueryContentModelOptions { - /** - * The type of block to query @default 'Paragraph' - */ - blockType?: ContentModelBlockType; - - /** - * Optional selector to filter the blocks - */ - filter?: (element: T) => element is T; - - /** - * True to return the first block only, false to return all blocks - */ - findFirstOnly?: boolean; -} - /** * Query content model blocks * @param group The block group to query - * @param options The query option + * @param blockType The type of block to query @default 'Paragraph' + * @param filter Optional selector to filter the blocks + * @param findFirstOnly True to return the first block only, false to return all blocks */ export function queryContentModelBlocks( group: ReadonlyContentModelBlockGroup, - options: QueryContentModelOptions + blockType?: ContentModelBlockType, + filter?: (element: T) => element is T, + findFirstOnly?: boolean ): T[] { - const { blockType, filter, findFirstOnly } = options; const type = blockType || 'Paragraph'; const elements: T[] = []; for (let i = 0; i < group.blocks.length; i++) { diff --git a/packages/roosterjs-content-model-api/test/modelApi/common/queryContentModelBlocksTest.ts b/packages/roosterjs-content-model-api/test/modelApi/common/queryContentModelBlocksTest.ts index 34c64d45c49..8a07dc41da6 100644 --- a/packages/roosterjs-content-model-api/test/modelApi/common/queryContentModelBlocksTest.ts +++ b/packages/roosterjs-content-model-api/test/modelApi/common/queryContentModelBlocksTest.ts @@ -15,7 +15,7 @@ describe('queryContentModelBlocksBlocks', () => { }; // Act - const result = queryContentModelBlocks(group, {}); + const result = queryContentModelBlocks(group); // Assert expect(result).toEqual([]); @@ -36,7 +36,7 @@ describe('queryContentModelBlocksBlocks', () => { }; // Act - const result = queryContentModelBlocks(group, { blockType: 'Table' }); + const result = queryContentModelBlocks(group, 'Table'); // Assert expect(result).toEqual([]); @@ -318,9 +318,7 @@ describe('queryContentModelBlocksBlocks', () => { ]; // Act - const result = queryContentModelBlocks(group, { - blockType: 'Table', - }); + const result = queryContentModelBlocks(group, 'Table'); // Assert expect(result).toEqual(expected); @@ -377,10 +375,11 @@ describe('queryContentModelBlocksBlocks', () => { }; // Act - const result = queryContentModelBlocks(group, { - blockType: 'Paragraph', - filter: (block): block is ReadonlyContentModelParagraph => block.segments.length == 2, - }); + const result = queryContentModelBlocks( + group, + 'Paragraph', + (block): block is ReadonlyContentModelParagraph => block.segments.length == 2 + ); // Assert expect(result).toEqual([paragraph]); @@ -765,9 +764,7 @@ describe('queryContentModelBlocksBlocks', () => { }, }, ]; - const result = queryContentModelBlocks(model, { - blockType: 'Table', - }); + const result = queryContentModelBlocks(model, 'Table'); expect(result).toEqual(expected); }); @@ -905,9 +902,7 @@ describe('queryContentModelBlocksBlocks', () => { }; const expected: ReadonlyContentModelTable[] = [table]; - const result = queryContentModelBlocks(model, { - blockType: 'Table', - }); + const result = queryContentModelBlocks(model, 'Table'); expect(result).toEqual(expected); }); @@ -1167,11 +1162,11 @@ describe('queryContentModelBlocksBlocks', () => { }, ]; - const result = queryContentModelBlocks(model, { - blockType: 'BlockGroup', - filter: (block): block is ReadonlyContentModelListItem => - block.blockGroupType == 'ListItem', - }); + const result = queryContentModelBlocks( + model, + 'BlockGroup', + (block): block is ReadonlyContentModelListItem => block.blockGroupType == 'ListItem' + ); expect(result).toEqual(listExpected); }); @@ -1456,11 +1451,10 @@ describe('queryContentModelBlocksBlocks', () => { format: {}, }, }; - const result = queryContentModelBlocks(model, { - findFirstOnly: true, - filter: ( - block: ReadonlyContentModelParagraph - ): block is ReadonlyContentModelParagraph => { + const result = queryContentModelBlocks( + model, + 'Paragraph', + (block: ReadonlyContentModelParagraph): block is ReadonlyContentModelParagraph => { for (const segment of block.segments) { if (segment.segmentType == 'Image' && segment.dataset.isEditing) { return true; @@ -1468,7 +1462,8 @@ describe('queryContentModelBlocksBlocks', () => { } return false; }, - }); + true /* findFirstOnly */ + ); expect(result).toEqual([imageAndParagraph]); }); }); diff --git a/packages/roosterjs-content-model-plugins/lib/imageEdit/utils/findEditingImage.ts b/packages/roosterjs-content-model-plugins/lib/imageEdit/utils/findEditingImage.ts index 8c20467c371..a3b257c9d94 100644 --- a/packages/roosterjs-content-model-plugins/lib/imageEdit/utils/findEditingImage.ts +++ b/packages/roosterjs-content-model-plugins/lib/imageEdit/utils/findEditingImage.ts @@ -13,10 +13,10 @@ export function findEditingImage( imageId?: string ): ImageAndParagraph | null { let imageAndParagraph: ImageAndParagraph | null = null; - queryContentModelBlocks(group, { - filter: ( - paragraph: ReadonlyContentModelParagraph - ): paragraph is ReadonlyContentModelParagraph => { + queryContentModelBlocks( + group, + 'Paragraph', + (paragraph: ReadonlyContentModelParagraph): paragraph is ReadonlyContentModelParagraph => { for (const segment of paragraph.segments) { if ( segment.segmentType == 'Image' && @@ -28,8 +28,8 @@ export function findEditingImage( } return false; }, - findFirstOnly: true, - }); + true /*findFirstOnly*/ + ); return imageAndParagraph; } From d27448442941ffa5270c6318389c26ec2e393297 Mon Sep 17 00:00:00 2001 From: "Julia Roldi (from Dev Box)" Date: Wed, 6 Nov 2024 11:55:21 -0300 Subject: [PATCH 8/9] nit --- .../controlsV2/demoButtons/cutButton.ts | 22 ------------------- 1 file changed, 22 deletions(-) delete mode 100644 demo/scripts/controlsV2/demoButtons/cutButton.ts diff --git a/demo/scripts/controlsV2/demoButtons/cutButton.ts b/demo/scripts/controlsV2/demoButtons/cutButton.ts deleted file mode 100644 index 9c118a4a9b1..00000000000 --- a/demo/scripts/controlsV2/demoButtons/cutButton.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { RibbonButton } from 'roosterjs-react'; - -/** - * Key of localized strings of Cut button - */ -export type CutButtonStringKey = 'buttonNameCut'; - -/** - * "Cut" button on the format ribbon - */ -export const cutButton: RibbonButton = { - key: 'buttonNameCut', - unlocalizedText: ' Cut', - iconName: 'ClearNight', - onClick: editor => { - const selection = editor.getDOMSelection(); - if (selection) { - document.execCommand('cut'); - } - return true; - }, -}; From a91d593a72c8884b4137d277f3945cf455edb223 Mon Sep 17 00:00:00 2001 From: "Julia Roldi (from Dev Box)" Date: Thu, 21 Nov 2024 14:40:49 -0300 Subject: [PATCH 9/9] refactor --- .../common/queryContentModelBlocks.ts | 82 ++++++++----------- .../common/queryContentModelBlocksTest.ts | 4 +- 2 files changed, 36 insertions(+), 50 deletions(-) diff --git a/packages/roosterjs-content-model-api/lib/modelApi/common/queryContentModelBlocks.ts b/packages/roosterjs-content-model-api/lib/modelApi/common/queryContentModelBlocks.ts index ebd290de2a7..3b098bfc4ee 100644 --- a/packages/roosterjs-content-model-api/lib/modelApi/common/queryContentModelBlocks.ts +++ b/packages/roosterjs-content-model-api/lib/modelApi/common/queryContentModelBlocks.ts @@ -1,75 +1,61 @@ import type { ContentModelBlockType, ReadonlyContentModelBlock, + ReadonlyContentModelBlockBase, ReadonlyContentModelBlockGroup, } from 'roosterjs-content-model-types'; /** * Query content model blocks * @param group The block group to query - * @param blockType The type of block to query @default 'Paragraph' + * @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 */ export function queryContentModelBlocks( group: ReadonlyContentModelBlockGroup, - blockType?: ContentModelBlockType, + type: T extends ReadonlyContentModelBlockBase ? U : never, filter?: (element: T) => element is T, findFirstOnly?: boolean ): T[] { - const type = blockType || 'Paragraph'; const elements: T[] = []; for (let i = 0; i < group.blocks.length; i++) { if (findFirstOnly && elements.length > 0) { return elements; } const block = group.blocks[i]; - const results = queryContentModelBlocksInternal(block, type, filter, findFirstOnly); - elements.push(...results); - } - return elements; -} -function queryContentModelBlocksInternal( - block: ReadonlyContentModelBlock, - type: ContentModelBlockType, - filter?: (element: T) => element is T, - findFirstOnly?: boolean -): T[] { - const elements: T[] = []; - if (isExpectedBlockType(block, type, filter)) { - elements.push(block); - } - - if (block.blockType == 'BlockGroup') { - for (const childBlock of block.blocks) { - if (findFirstOnly && elements.length > 0) { - return elements; - } - const results = queryContentModelBlocksInternal( - childBlock, - type, - filter, - findFirstOnly - ); - elements.push(...results); - } - } - - if (block.blockType == 'Table') { - const table = block; - for (const row of table.rows) { - for (const cell of row.cells) { - for (const cellBlock of cell.blocks) { - const results = queryContentModelBlocksInternal( - cellBlock, - type, - filter, - findFirstOnly - ); - elements.push(...results); + switch (block.blockType) { + case 'Paragraph': + case 'Divider': + case 'Entity': + if (isExpectedBlockType(block, type, filter)) { + elements.push(block); + } + break; + case 'BlockGroup': + if (isExpectedBlockType(block, type, filter)) { + elements.push(block); + } + const results = queryContentModelBlocks(block, type, filter, findFirstOnly); + elements.push(...results); + break; + case 'Table': + if (isExpectedBlockType(block, type, filter)) { + elements.push(block); + } + for (const row of block.rows) { + for (const cell of row.cells) { + const results = queryContentModelBlocks( + cell, + type, + filter, + findFirstOnly + ); + elements.push(...results); + } } - } + break; } } return elements; @@ -85,7 +71,7 @@ function isExpectedBlockType( function isBlockType( block: ReadonlyContentModelBlock, - type: string + type: ContentModelBlockType ): block is T { return block.blockType == type; } diff --git a/packages/roosterjs-content-model-api/test/modelApi/common/queryContentModelBlocksTest.ts b/packages/roosterjs-content-model-api/test/modelApi/common/queryContentModelBlocksTest.ts index 8a07dc41da6..64a76d37dde 100644 --- a/packages/roosterjs-content-model-api/test/modelApi/common/queryContentModelBlocksTest.ts +++ b/packages/roosterjs-content-model-api/test/modelApi/common/queryContentModelBlocksTest.ts @@ -15,7 +15,7 @@ describe('queryContentModelBlocksBlocks', () => { }; // Act - const result = queryContentModelBlocks(group); + const result = queryContentModelBlocks(group, 'Paragraph'); // Assert expect(result).toEqual([]); @@ -36,7 +36,7 @@ describe('queryContentModelBlocksBlocks', () => { }; // Act - const result = queryContentModelBlocks(group, 'Table'); + const result = queryContentModelBlocks(group, 'Table'); // Assert expect(result).toEqual([]);