Skip to content

Commit

Permalink
Content Model: Do color transform for entity when copy/paste (#2056)
Browse files Browse the repository at this point in the history
* Content Model: Do color transform for entity when copy/paste

* Call normalizeContentModel
  • Loading branch information
JiuqingSong authored Sep 8, 2023
1 parent fb0febc commit f3f6831
Show file tree
Hide file tree
Showing 38 changed files with 645 additions and 85 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
ClipboardData,
SelectionRangeTypes,
SelectionRangeEx,
ColorTransformDirection,
} from 'roosterjs-editor-types';

/**
Expand Down Expand Up @@ -94,7 +95,24 @@ export default class ContentModelCopyPastePlugin implements PluginWithState<Copy
if (selection && !selection.areAllCollapsed) {
const model = this.editor.createContentModel();

const pasteModel = cloneModel(model);
const pasteModel = cloneModel(model, {
includeCachedElement: this.editor.isDarkMode()
? (node, type) => {
if (type == 'cache') {
return undefined;
} else {
const result = node.cloneNode(true /*deep*/) as HTMLElement;

this.editor?.transformToDarkColor(
result,
ColorTransformDirection.DarkToLight
);

return result;
}
}
: false,
});
if (selection.type === SelectionRangeTypes.TableSelection) {
iterateSelections([pasteModel], (path, tableContext) => {
if (tableContext?.table) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,27 @@ import type {
ContentModelListLevel,
} from 'roosterjs-content-model-types';

/**
* @internal
*/
export type CachedElementHandler = (
node: HTMLElement,
type: 'general' | 'entity' | 'cache'
) => HTMLElement | undefined;

/**
* @internal
* Options for cloneModel API
*/
export interface CloneModelOptions {
/**
* When pass false or not passed, the cloned model will not have cached element even they exist in original model.
* For entity and general model, a cloned wrapper element will be added into cloned model. So that the cloned model will be fully disconnected from the original one
* When pass true, cloned model will have the same cached element and element wrapper with the original model
* @default true
* Specify how to deal with cached element, including cached block element, element in General Model, and wrapper element in Entity
* - True: Cloned model will have the same reference to the cached element
* - False/Not passed: For cached block element, cached element will be undefined. For General Model and Entity, the element will have deep clone and assign to the cloned model
* - A callback: invoke the callback with the source cached element and a string to specify model type, let the callback return the expected value of cached element.
* For General Model and Entity, the callback must return a valid element, otherwise there will be exception thrown.
*/
includeCachedElement?: boolean;
includeCachedElement?: boolean | CachedElementHandler;
}

/**
Expand Down Expand Up @@ -167,9 +176,7 @@ function cloneEntity(entity: ContentModelEntity, options: CloneModelOptions): Co

return Object.assign(
{
wrapper: options.includeCachedElement
? wrapper
: (wrapper.cloneNode(true /*deep*/) as HTMLElement),
wrapper: handleCachedElement(wrapper, 'entity', options),
isReadonly,
type,
id,
Expand All @@ -187,7 +194,7 @@ function cloneParagraph(

const newParagraph: ContentModelParagraph = Object.assign(
{
cachedElement: options.includeCachedElement ? cachedElement : undefined,
cachedElement: handleCachedElement(cachedElement, 'cache', options),
isImplicit,
segments: segments.map(segment => cloneSegment(segment, options)),
segmentFormat: segmentFormat ? { ...segmentFormat } : undefined,
Expand All @@ -213,7 +220,7 @@ function cloneTable(table: ContentModelTable, options: CloneModelOptions): Conte

return Object.assign(
{
cachedElement: options.includeCachedElement ? cachedElement : undefined,
cachedElement: handleCachedElement(cachedElement, 'cache', options),
widths: Array.from(widths),
rows: rows.map(row => cloneTableRow(row, options)),
},
Expand All @@ -231,7 +238,7 @@ function cloneTableRow(
return Object.assign(
{
height,
cachedElement: options.includeCachedElement ? cachedElement : undefined,
cachedElement: handleCachedElement(cachedElement, 'cache', options),
cells: cells.map(cell => cloneTableCell(cell, options)),
},
cloneModelWithFormat(row)
Expand All @@ -246,7 +253,7 @@ function cloneTableCell(

return Object.assign(
{
cachedElement: options.includeCachedElement ? cachedElement : undefined,
cachedElement: handleCachedElement(cachedElement, 'cache', options),
isSelected,
spanAbove,
spanLeft,
Expand All @@ -264,7 +271,7 @@ function cloneFormatContainer(
): ContentModelFormatContainer {
const { tagName, cachedElement } = container;
const newContainer: ContentModelFormatContainer = Object.assign(
{ tagName, cachedElement: options.includeCachedElement ? cachedElement : undefined },
{ tagName, cachedElement: handleCachedElement(cachedElement, 'cache', options) },
cloneBlockBase(container),
cloneBlockGroupBase(container, options)
);
Expand Down Expand Up @@ -307,7 +314,7 @@ function cloneDivider(
{
isSelected,
tagName,
cachedElement: options.includeCachedElement ? cachedElement : undefined,
cachedElement: handleCachedElement(cachedElement, 'cache', options),
},
cloneBlockBase(divider)
);
Expand All @@ -321,9 +328,7 @@ function cloneGeneralBlock(

return Object.assign(
{
element: options.includeCachedElement
? element
: (element.cloneNode(true /*deep*/) as HTMLElement),
element: handleCachedElement(element, 'general', options),
},
cloneBlockBase(general),
cloneBlockGroupBase(general, options)
Expand Down Expand Up @@ -355,3 +360,39 @@ function cloneText(textSegment: ContentModelText): ContentModelText {
const { text } = textSegment;
return Object.assign({ text }, cloneSegmentBase(textSegment));
}

function handleCachedElement<T extends HTMLElement>(
node: T,
type: 'general' | 'entity',
options: CloneModelOptions
): T;

function handleCachedElement<T extends HTMLElement>(
node: T | undefined,
type: 'cache',
options: CloneModelOptions
): T | undefined;

function handleCachedElement<T extends HTMLElement>(
node: T | undefined,
type: 'general' | 'entity' | 'cache',
options: CloneModelOptions
): T | undefined {
const { includeCachedElement } = options;

if (!node) {
return undefined;
} else if (!includeCachedElement) {
return type == 'cache' ? undefined : (node.cloneNode(true /*deep*/) as T);
} else if (includeCachedElement === true) {
return node;
} else {
const result = includeCachedElement(node, type) as T | undefined;

if ((type == 'general' || type == 'entity') && !result) {
throw new Error('Entity and General Model must has wrapper element');
}

return result;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,16 @@ export function mergeModel(

switch (block.blockType) {
case 'Paragraph':
mergeParagraph(insertPosition, block, i == 0);
mergeParagraph(insertPosition, block, i == 0, context);
break;

case 'Divider':
insertBlock(insertPosition, block);
break;

case 'Entity':
insertBlock(insertPosition, block);
context?.newEntities.push(block);
break;

case 'Table':
Expand Down Expand Up @@ -120,7 +124,8 @@ export function mergeModel(
function mergeParagraph(
markerPosition: InsertPoint,
newPara: ContentModelParagraph,
mergeToCurrentParagraph: boolean
mergeToCurrentParagraph: boolean,
context?: FormatWithContentModelContext
) {
const { paragraph, marker } = markerPosition;
const newParagraph = mergeToCurrentParagraph
Expand All @@ -129,7 +134,15 @@ function mergeParagraph(
const segmentIndex = newParagraph.segments.indexOf(marker);

if (segmentIndex >= 0) {
newParagraph.segments.splice(segmentIndex, 0, ...newPara.segments);
for (let i = 0; i < newPara.segments.length; i++) {
const segment = newPara.segments[i];

newParagraph.segments.splice(segmentIndex + i, 0, segment);

if (context && segment.segmentType == 'Entity') {
context.newEntities.push(segment);
}
}
}

if (newPara.decorator) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ChangeSource, Entity, SelectionRangeEx } from 'roosterjs-editor-types';
import { commitEntity, getEntityFromElement } from 'roosterjs-editor-dom';
import { createEntity } from 'roosterjs-content-model-dom';
import { createEntity, normalizeContentModel } from 'roosterjs-content-model-dom';
import { formatWithContentModel } from '../utils/formatWithContentModel';
import { IContentModelEditor } from '../../publicTypes/IContentModelEditor';
import { insertEntityModel } from '../../modelApi/entity/insertEntityModel';
Expand Down Expand Up @@ -84,7 +84,10 @@ export default function insertEntity(
context
);

normalizeContentModel(model);

context.skipUndoSnapshot = skipUndoSnapshot;
context.newEntities.push(entityModel);

return true;
},
Expand All @@ -93,10 +96,6 @@ export default function insertEntity(
}
);

if (editor.isDarkMode()) {
editor.transformToDarkColor(wrapper);
}

const newEntity = getEntityFromElement(wrapper);

editor.triggerContentChangedEvent(ChangeSource.InsertEntity, newEntity);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,14 @@ export function formatWithContentModel(

const model = editor.createContentModel(undefined /*option*/, selectionOverride);
const context: FormatWithContentModelContext = {
newEntities: [],
deletedEntities: [],
rawEvent,
};

if (formatter(model, context)) {
const callback = () => {
handleNewEntities(editor, context);
handleDeletedEntities(editor, context);

if (model) {
Expand Down Expand Up @@ -81,6 +83,18 @@ export function formatWithContentModel(
}
}

function handleNewEntities(editor: IContentModelEditor, context: FormatWithContentModelContext) {
// TODO: Ideally we can trigger NewEntity event here. But to be compatible with original editor code, we don't do it here for now.
// Once Content Model Editor can be standalone, we can change this behavior to move triggering NewEntity event code
// from EntityPlugin to here

if (editor.isDarkMode()) {
context.newEntities.forEach(entity => {
editor.transformToDarkColor(entity.wrapper);
});
}
}

function handleDeletedEntities(
editor: IContentModelEditor,
context: FormatWithContentModelContext
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ export interface DeletedEntity {
* Context object for API formatWithContentModel
*/
export interface FormatWithContentModelContext {
/**
* New entities added during the format process
*/
readonly newEntities: ContentModelEntity[];

/**
* Entities got deleted during formatting. Need to be set by the formatter function
*/
Expand Down
Loading

0 comments on commit f3f6831

Please sign in to comment.