Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Content Model: Do color transform for entity when copy/paste #2056

Merged
merged 5 commits into from
Sep 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading