-
Notifications
You must be signed in to change notification settings - Fork 167
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Content Model Selection API step 3: Add new selection API (#1478)
* Selection API step 1 * Selection API 2 * New selection API * add test
- Loading branch information
1 parent
1b03fe8
commit a726705
Showing
13 changed files
with
2,969 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
45 changes: 45 additions & 0 deletions
45
demo/scripts/controls/ribbonButtons/contentModel/insertImageButton.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import isContentModelEditor from '../../editor/isContentModelEditor'; | ||
import { createElement } from 'roosterjs-editor-dom'; | ||
import { CreateElementData } from 'roosterjs-editor-types'; | ||
import { insertImage } from 'roosterjs-content-model'; | ||
import { InsertImageButtonStringKey, RibbonButton } from 'roosterjs-react'; | ||
|
||
const FileInput: CreateElementData = { | ||
tag: 'input', | ||
attributes: { | ||
type: 'file', | ||
accept: 'image/*', | ||
display: 'none', | ||
}, | ||
}; | ||
|
||
/** | ||
* @internal | ||
* "Insert image" button on the format ribbon | ||
*/ | ||
export const insertImageButton: RibbonButton<InsertImageButtonStringKey> = { | ||
key: 'buttonNameInsertImage', | ||
unlocalizedText: 'Insert image', | ||
iconName: 'Photo2', | ||
onClick: editor => { | ||
if (isContentModelEditor(editor)) { | ||
const document = editor.getDocument(); | ||
const fileInput = createElement(FileInput, document) as HTMLInputElement; | ||
document.body.appendChild(fileInput); | ||
|
||
fileInput.addEventListener('change', () => { | ||
if (fileInput.files) { | ||
for (let i = 0; i < fileInput.files.length; i++) { | ||
insertImage(editor, fileInput.files[i]); | ||
} | ||
} | ||
}); | ||
|
||
try { | ||
fileInput.click(); | ||
} finally { | ||
document.body.removeChild(fileInput); | ||
} | ||
} | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
35 changes: 35 additions & 0 deletions
35
packages/roosterjs-content-model/lib/modelApi/common/insertContent.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import domToContentModel from '../../publicApi/domToContentModel'; | ||
import { ContentModelDocument } from '../../publicTypes/group/ContentModelDocument'; | ||
import { mergeModel } from '../../modelApi/common/mergeModel'; | ||
import { safeInstanceOf, wrap } from 'roosterjs-editor-dom'; | ||
import { setSelection } from '../../modelApi/selection/setSelection'; | ||
|
||
/** | ||
* @internal | ||
*/ | ||
export function insertContent( | ||
model: ContentModelDocument, | ||
htmlContent: DocumentFragment | HTMLElement | ContentModelDocument, | ||
isFromDarkMode?: boolean | ||
) { | ||
if (safeInstanceOf(htmlContent, 'DocumentFragment')) { | ||
htmlContent = wrap(htmlContent, 'span'); | ||
} | ||
|
||
if (safeInstanceOf(htmlContent, 'HTMLElement')) { | ||
htmlContent = domToContentModel( | ||
htmlContent, | ||
{ | ||
isDarkMode: !!isFromDarkMode, | ||
zoomScale: 1, | ||
isRightToLeft: false, | ||
}, | ||
{ | ||
includeRoot: true, | ||
} | ||
); | ||
} | ||
|
||
setSelection(htmlContent); | ||
mergeModel(model, htmlContent); | ||
} |
203 changes: 203 additions & 0 deletions
203
packages/roosterjs-content-model/lib/modelApi/common/mergeModel.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,203 @@ | ||
import { addSegment } from './addSegment'; | ||
import { applyTableFormat } from '../table/applyTableFormat'; | ||
import { ContentModelBlock } from '../../publicTypes/block/ContentModelBlock'; | ||
import { ContentModelDocument } from '../../publicTypes/group/ContentModelDocument'; | ||
import { ContentModelListItem } from '../../publicTypes/group/ContentModelListItem'; | ||
import { ContentModelParagraph } from '../../publicTypes/block/ContentModelParagraph'; | ||
import { ContentModelTable } from '../../publicTypes/block/ContentModelTable'; | ||
import { createListItem } from '../creators/createListItem'; | ||
import { createParagraph } from '../creators/createParagraph'; | ||
import { createSelectionMarker } from '../creators/createSelectionMarker'; | ||
import { createTableCell } from '../creators/createTableCell'; | ||
import { deleteSelection, InsertPosition } from '../selection/deleteSelections'; | ||
import { getClosestAncestorBlockGroupIndex } from './getClosestAncestorBlockGroupIndex'; | ||
import { normalizeContentModel } from './normalizeContentModel'; | ||
import { normalizeTable } from '../table/normalizeTable'; | ||
|
||
/** | ||
* @internal | ||
*/ | ||
export function mergeModel(target: ContentModelDocument, source: ContentModelDocument) { | ||
const insertPosition = deleteSelection(target); | ||
|
||
if (insertPosition) { | ||
for (let i = 0; i < source.blocks.length; i++) { | ||
const block = source.blocks[i]; | ||
|
||
switch (block.blockType) { | ||
case 'Paragraph': | ||
mergeParagraph(insertPosition, block, i == 0); | ||
break; | ||
|
||
case 'Divider': | ||
case 'Entity': | ||
insertBlock(insertPosition, block); | ||
break; | ||
|
||
case 'Table': | ||
mergeTable(insertPosition, block, source); | ||
break; | ||
|
||
case 'BlockGroup': | ||
switch (block.blockGroupType) { | ||
case 'General': | ||
case 'Quote': | ||
insertBlock(insertPosition, block); | ||
break; | ||
case 'ListItem': | ||
mergeList(insertPosition, block); | ||
break; | ||
} | ||
break; | ||
} | ||
} | ||
} | ||
|
||
normalizeContentModel(target); | ||
} | ||
|
||
function mergeParagraph( | ||
markerPosition: InsertPosition, | ||
newPara: ContentModelParagraph, | ||
mergeToCurrentParagraph: boolean | ||
) { | ||
const { paragraph, marker } = markerPosition; | ||
const newParagraph = mergeToCurrentParagraph ? paragraph : splitParagraph(markerPosition); | ||
const segmentIndex = newParagraph.segments.indexOf(marker); | ||
|
||
if (segmentIndex >= 0) { | ||
newParagraph.segments.splice(segmentIndex, 0, ...newPara.segments); | ||
} | ||
} | ||
|
||
function mergeTable( | ||
markerPosition: InsertPosition, | ||
newTable: ContentModelTable, | ||
source: ContentModelDocument | ||
) { | ||
const { tableContext } = markerPosition; | ||
|
||
if (tableContext && source.blocks.length == 1 && source.blocks[0] == newTable) { | ||
const { table, colIndex, rowIndex } = tableContext; | ||
for (let i = 0; i < newTable.cells.length; i++) { | ||
for (let j = 0; j < newTable.cells[i].length; j++) { | ||
const newCell = newTable.cells[i][j]; | ||
|
||
if (i == 0 && colIndex + j >= table.cells[0].length) { | ||
for (let k = 0; k < rowIndex; k++) { | ||
const leftCell = table.cells[k]?.[colIndex + j - 1]; | ||
table.cells[k][colIndex + j] = createTableCell( | ||
false /*spanLeft*/, | ||
false /*spanAbove*/, | ||
leftCell?.isHeader, | ||
leftCell?.format | ||
); | ||
} | ||
} | ||
|
||
if (j == 0 && rowIndex + i >= table.cells.length) { | ||
if (!table.cells[rowIndex + i]) { | ||
table.cells[rowIndex + i] = []; | ||
} | ||
|
||
for (let k = 0; k < colIndex; k++) { | ||
const aboveCell = table.cells[rowIndex + i - 1]?.[k]; | ||
table.cells[rowIndex + i][k] = createTableCell( | ||
false /*spanLeft*/, | ||
false /*spanAbove*/, | ||
false /*isHeader*/, | ||
aboveCell?.format | ||
); | ||
} | ||
} | ||
|
||
table.cells[rowIndex + i][colIndex + j] = newCell; | ||
|
||
if (i == 0 && j == 0) { | ||
addSegment(newCell, createSelectionMarker()); | ||
} | ||
} | ||
} | ||
|
||
normalizeTable(table); | ||
applyTableFormat(table, undefined /*newFormat*/, true /*keepCellShade*/); | ||
} else { | ||
insertBlock(markerPosition, newTable); | ||
} | ||
} | ||
|
||
function mergeList(markerPosition: InsertPosition, newList: ContentModelListItem) { | ||
splitParagraph(markerPosition); | ||
|
||
const { path, paragraph } = markerPosition; | ||
|
||
const listItemIndex = getClosestAncestorBlockGroupIndex(path, ['ListItem']); | ||
const listItem = path[listItemIndex] as ContentModelListItem; | ||
const listParent = path[listItemIndex + 1]; // It is ok here when index is -1, that means there is no list and we just insert a new paragraph and use path[0] as its parent | ||
const blockIndex = listParent.blocks.indexOf(listItem || paragraph); | ||
|
||
if (blockIndex >= 0) { | ||
listParent.blocks.splice(blockIndex, 0, newList); | ||
} | ||
|
||
if (listItem) { | ||
listItem?.levels.forEach((level, i) => { | ||
newList.levels[i] = { ...level }; | ||
}); | ||
} | ||
} | ||
|
||
function splitParagraph(markerPosition: InsertPosition) { | ||
const { paragraph, marker, path } = markerPosition; | ||
const segmentIndex = paragraph.segments.indexOf(marker); | ||
const paraIndex = path[0].blocks.indexOf(paragraph); | ||
const newParagraph = createParagraph(false /*isImplicit*/, paragraph.format); | ||
|
||
if (segmentIndex >= 0) { | ||
newParagraph.segments = paragraph.segments.splice(segmentIndex); | ||
} | ||
|
||
if (paraIndex >= 0) { | ||
path[0].blocks.splice(paraIndex + 1, 0, newParagraph); | ||
} | ||
|
||
const listItemIndex = getClosestAncestorBlockGroupIndex( | ||
path, | ||
['ListItem'], | ||
['Quote', 'TableCell'] | ||
); | ||
const listItem = path[listItemIndex] as ContentModelListItem; | ||
|
||
if (listItem) { | ||
const listParent = listItemIndex >= 0 ? path[listItemIndex + 1] : null; | ||
const blockIndex = listParent ? listParent.blocks.indexOf(listItem) : -1; | ||
|
||
if (blockIndex >= 0 && listParent) { | ||
const newListItem = createListItem(listItem.levels, listItem.formatHolder.format); | ||
|
||
if (paraIndex >= 0) { | ||
newListItem.blocks = listItem.blocks.splice(paraIndex + 1); | ||
} | ||
|
||
if (blockIndex >= 0) { | ||
listParent.blocks.splice(blockIndex + 1, 0, newListItem); | ||
} | ||
|
||
path[listItemIndex] = newListItem; | ||
} | ||
} | ||
|
||
markerPosition.paragraph = newParagraph; | ||
|
||
return newParagraph; | ||
} | ||
|
||
function insertBlock(markerPosition: InsertPosition, block: ContentModelBlock) { | ||
const { path } = markerPosition; | ||
const newPara = splitParagraph(markerPosition); | ||
const blockIndex = path[0].blocks.indexOf(newPara); | ||
|
||
if (blockIndex >= 0) { | ||
path[0].blocks.splice(blockIndex, 0, block); | ||
} | ||
} |
Oops, something went wrong.