Skip to content

Commit

Permalink
Standalone Editor: Port LifecyclePlugin (#2219)
Browse files Browse the repository at this point in the history
* Standalone Editor: CreateStandaloneEditorCore

* Standalone Editor: Port LifecyclePlugin

* fix build

* fix test

* improve

* fix test

* fix comment
  • Loading branch information
JiuqingSong authored Nov 22, 2023
1 parent faae180 commit 2699cf9
Show file tree
Hide file tree
Showing 41 changed files with 669 additions and 480 deletions.
16 changes: 13 additions & 3 deletions demo/scripts/controls/ContentModelEditorMainPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ import TitleBar from './titleBar/TitleBar';
import { arrayPush } from 'roosterjs-editor-dom';
import { ContentModelEditPlugin } from 'roosterjs-content-model-plugins';
import { ContentModelRibbonPlugin } from './ribbonButtons/contentModel/ContentModelRibbonPlugin';
import { ContentModelSegmentFormat } from 'roosterjs-content-model-types';
import { createEmojiPlugin, createPasteOptionPlugin, RibbonPlugin } from 'roosterjs-react';
import { EditorPlugin } from 'roosterjs-editor-types';
import { getDarkColor } from 'roosterjs-color-utils';
import { PartialTheme } from '@fluentui/react/lib/Theme';
import { trustedHTMLHandler } from '../utils/trustedHTMLHandler';
import {
Expand Down Expand Up @@ -210,6 +210,17 @@ class ContentModelEditorMainPane extends MainPaneBase<ContentModelMainPaneState>
height: `calc(${100 / this.state.scale}%)`,
width: `calc(${100 / this.state.scale}%)`,
};
const format = this.state.initState.defaultFormat;
const defaultFormat: ContentModelSegmentFormat = {
fontWeight: format.bold ? 'bold' : undefined,
italic: format.italic || undefined,
underline: format.underline || undefined,
fontFamily: format.fontFamily || undefined,
fontSize: format.fontSize || undefined,
textColor: format.textColors?.lightModeColor || format.textColor || undefined,
backgroundColor:
format.backgroundColors?.lightModeColor || format.backgroundColor || undefined,
};

this.updateContentPlugin.forceUpdate();

Expand All @@ -220,9 +231,8 @@ class ContentModelEditorMainPane extends MainPaneBase<ContentModelMainPaneState>
<ContentModelRooster
className={styles.editor}
plugins={allPlugins}
defaultFormat={this.state.initState.defaultFormat}
defaultSegmentFormat={defaultFormat}
inDarkMode={this.state.isDarkMode}
getDarkColor={getDarkColor}
experimentalFeatures={this.state.initState.experimentalFeatures}
undoMetadataSnapshotService={this.snapshotPlugin.getSnapshotService()}
trustedHTMLHandler={trustedHTMLHandler}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { isBold } from '../../publicApi/segment/toggleBold';
import {
extractBorderValues,
getClosestAncestorBlockGroupIndex,
isBold,
iterateSelections,
updateTableMetadata,
} from 'roosterjs-content-model-core';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { formatSegmentWithContentModel } from '../utils/formatSegmentWithContentModel';
import { isBold } from 'roosterjs-content-model-core';
import type { IStandaloneEditor } from 'roosterjs-content-model-types';

/**
Expand All @@ -22,12 +23,3 @@ export default function toggleBold(editor: IStandaloneEditor) {
)
);
}

/**
* @internal
*/
export function isBold(boldStyle?: string): boolean {
return (
!!boldStyle && (boldStyle == 'bold' || boldStyle == 'bolder' || parseInt(boldStyle) >= 600)
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,6 @@ describe('insertLink', () => {
const a = div.querySelector('a');

expect(a!.outerHTML).toBe('<a href="http://test.com" title="title">http://test.com</a>');
expect(onPluginEvent).toHaveBeenCalledTimes(4);
expect(onPluginEvent).toHaveBeenCalledWith({
eventType: PluginEventType.ContentChanged,
source: ChangeSource.CreateLink,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { iterateSelections } from '../publicApi/selection/iterateSelections';
import { moveChildNodes } from 'roosterjs-content-model-dom';
import { PluginEventType } from 'roosterjs-editor-types';
import type { SwitchShadowEdit } from 'roosterjs-content-model-types';
import type { SelectionPath } from 'roosterjs-editor-types';

/**
* @internal
Expand All @@ -20,17 +20,16 @@ export const switchShadowEdit: SwitchShadowEdit = (editorCore, isOn): void => {
// Fake object, not used in Content Model Editor, just to satisfy original editor code
// TODO: we can remove them once we have standalone Content Model Editor
const fragment = core.contentDiv.ownerDocument.createDocumentFragment();
const selectionPath: SelectionPath = {
start: [],
end: [],
};
const clonedRoot = core.contentDiv.cloneNode(true /*deep*/);

moveChildNodes(fragment, clonedRoot);

core.api.triggerEvent(
core,
{
eventType: PluginEventType.EnteredShadowEdit,
fragment,
selectionPath,
selectionPath: null,
},
false /*broadcast*/
);
Expand All @@ -41,11 +40,9 @@ export const switchShadowEdit: SwitchShadowEdit = (editorCore, isOn): void => {
core.cache.cachedModel = model;
}

core.lifecycle.shadowEditSelectionPath = selectionPath;
core.lifecycle.shadowEditFragment = fragment;
} else {
core.lifecycle.shadowEditFragment = null;
core.lifecycle.shadowEditSelectionPath = null;

core.api.triggerEvent(
core,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,18 +38,8 @@ class ContentModelFormatPlugin implements PluginWithState<ContentModelFormatPlug
* @param option The editor option
*/
constructor(option: StandaloneEditorOptions) {
const format = option.defaultFormat || {};
this.state = {
defaultFormat: {
fontWeight: format.bold ? 'bold' : undefined,
italic: format.italic || undefined,
underline: format.underline || undefined,
fontFamily: format.fontFamily || undefined,
fontSize: format.fontSize || undefined,
textColor: format.textColors?.lightModeColor || format.textColor || undefined,
backgroundColor:
format.backgroundColors?.lightModeColor || format.backgroundColor || undefined,
},
defaultFormat: { ...option.defaultSegmentFormat },
pendingFormat: null,
};
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
import { ChangeSource } from '../constants/ChangeSource';
import { ColorTransformDirection, PluginEventType } from 'roosterjs-editor-types';
import {
createBr,
createContentModelDocument,
createParagraph,
createSelectionMarker,
setColor,
} from 'roosterjs-content-model-dom';
import type {
ContentModelBlock,
ContentModelBlockGroup,
ContentModelDecorator,
ContentModelDocument,
ContentModelEntity,
ContentModelSegment,
ContentModelSegmentFormat,
ContentModelTableRow,
IStandaloneEditor,
LifecyclePluginState,
OnNodeCreated,
StandaloneEditorOptions,
} from 'roosterjs-content-model-types';
import type { IEditor, PluginWithState, PluginEvent } from 'roosterjs-editor-types';

const ContentEditableAttributeName = 'contenteditable';
const DefaultTextColor = '#000000';
const DefaultBackColor = '#ffffff';

/**
* Lifecycle plugin handles editor initialization and disposing
*/
class LifecyclePlugin implements PluginWithState<LifecyclePluginState> {
private editor: (IStandaloneEditor & IEditor) | null = null;
private state: LifecyclePluginState;
private initialModel: ContentModelDocument;
private initializer: (() => void) | null = null;
private disposer: (() => void) | null = null;
private adjustColor: () => void;

/**
* Construct a new instance of LifecyclePlugin
* @param options The editor options
* @param contentDiv The editor content DIV
*/
constructor(options: StandaloneEditorOptions, contentDiv: HTMLDivElement) {
this.initialModel =
options.initialModel ?? this.createInitModel(options.defaultSegmentFormat);

// Make the container editable and set its selection styles
if (contentDiv.getAttribute(ContentEditableAttributeName) === null) {
this.initializer = () => {
contentDiv.contentEditable = 'true';
contentDiv.style.userSelect = 'text';
};
this.disposer = () => {
contentDiv.style.userSelect = '';
contentDiv.removeAttribute(ContentEditableAttributeName);
};
}
this.adjustColor = options.doNotAdjustEditorColor
? () => {}
: () => {
this.adjustContainerColor(contentDiv);
};

this.state = {
isDarkMode: !!options.inDarkMode,
onExternalContentTransform: null,
shadowEditFragment: null,
};
}

/**
* Get a friendly name of this plugin
*/
getName() {
return 'Lifecycle';
}

/**
* Initialize this plugin. This should only be called from Editor
* @param editor Editor instance
*/
initialize(editor: IEditor) {
this.editor = editor as IEditor & IStandaloneEditor;

this.editor.setContentModel(
this.initialModel,
{ ignoreSelection: true },
this.editor.isDarkMode() ? this.onInitialNodeCreated : undefined
);

// Initial model is only used once. After that we can just clean it up to make sure we don't cache anything useless
// including the cached DOM element inside the model.
this.initialModel = createContentModelDocument();

// Set content DIV to be editable
this.initializer?.();

// Set editor background color for dark mode
this.adjustColor();

// Let other plugins know that we are ready
this.editor.triggerPluginEvent(PluginEventType.EditorReady, {}, true /*broadcast*/);
}

/**
* Dispose this plugin
*/
dispose() {
this.editor?.triggerPluginEvent(PluginEventType.BeforeDispose, {}, true /*broadcast*/);

if (this.disposer) {
this.disposer();
this.disposer = null;
this.initializer = null;
}

this.editor = null;
}

/**
* Get plugin state object
*/
getState() {
return this.state;
}

/**
* Handle events triggered from editor
* @param event PluginEvent object
*/
onPluginEvent(event: PluginEvent) {
if (
event.eventType == PluginEventType.ContentChanged &&
(event.source == ChangeSource.SwitchToDarkMode ||
event.source == ChangeSource.SwitchToLightMode)
) {
this.state.isDarkMode = event.source == ChangeSource.SwitchToDarkMode;
this.adjustColor();
}
}

private adjustContainerColor(contentDiv: HTMLElement) {
if (this.editor) {
const { isDarkMode } = this.state;
const darkColorHandler = this.editor.getDarkColorHandler();

setColor(
contentDiv,
DefaultTextColor,
false /*isBackground*/,
darkColorHandler,
isDarkMode
);
setColor(
contentDiv,
DefaultBackColor,
true /*isBackground*/,
darkColorHandler,
isDarkMode
);
}
}

private createInitModel(format?: ContentModelSegmentFormat) {
const model = createContentModelDocument(format);
const paragraph = createParagraph(false /*isImplicit*/, undefined /*blockFormat*/, format);

paragraph.segments.push(createSelectionMarker(format), createBr(format));
model.blocks.push(paragraph);

return model;
}

private onInitialNodeCreated: OnNodeCreated = (model, node) => {
if (isEntity(model) && this.editor) {
this.editor.transformToDarkColor(node, ColorTransformDirection.LightToDark);
}
};
}

function isEntity(
modelElement:
| ContentModelBlock
| ContentModelBlockGroup
| ContentModelSegment
| ContentModelDecorator
| ContentModelTableRow
): modelElement is ContentModelEntity {
return (
(modelElement as ContentModelSegment).segmentType == 'Entity' ||
(modelElement as ContentModelBlock).blockType == 'Entity'
);
}

/**
* @internal
* Create a new instance of LifecyclePlugin.
* @param option The editor option
* @param contentDiv The editor content DIV element
*/
export function createLifecyclePlugin(
option: StandaloneEditorOptions,
contentDiv: HTMLDivElement
): PluginWithState<LifecyclePluginState> {
return new LifecyclePlugin(option, contentDiv);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { createContentModelCachePlugin } from './ContentModelCachePlugin';
import { createContentModelCopyPastePlugin } from './ContentModelCopyPastePlugin';
import { createContentModelFormatPlugin } from './ContentModelFormatPlugin';
import { createDOMEventPlugin } from './DOMEventPlugin';
import { createLifecyclePlugin } from './LifecyclePlugin';
import type {
StandaloneEditorCorePlugins,
StandaloneEditorOptions,
Expand All @@ -21,5 +22,6 @@ export function createStandaloneEditorCorePlugins(
format: createContentModelFormatPlugin(options),
copyPaste: createContentModelCopyPastePlugin(options),
domEvent: createDOMEventPlugin(options, contentDiv),
lifecycle: createLifecyclePlugin(options, contentDiv),
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { createStandaloneEditorCorePlugins } from '../corePlugin/createStandalon
import { createStandaloneEditorDefaultSettings } from './createStandaloneEditorDefaultSettings';
import { DarkColorHandlerImpl } from './DarkColorHandlerImpl';
import { standaloneCoreApiMap } from './standaloneCoreApiMap';
import type { EditorPlugin } from 'roosterjs-editor-types';
import type {
EditorEnvironment,
StandaloneEditorCore,
Expand All @@ -20,7 +21,8 @@ export function createStandaloneEditorCore(
contentDiv: HTMLDivElement,
options: StandaloneEditorOptions,
unportedCoreApiMap: UnportedCoreApiMap,
unportedCorePluginState: UnportedCorePluginState
unportedCorePluginState: UnportedCorePluginState,
tempPlugins: EditorPlugin[]
): StandaloneEditorCore {
const corePlugins = createStandaloneEditorCorePlugins(options, contentDiv);

Expand All @@ -33,7 +35,8 @@ export function createStandaloneEditorCore(
corePlugins.format,
corePlugins.copyPaste,
corePlugins.domEvent,
// TODO: Add additional plugins here
...tempPlugins,
corePlugins.lifecycle,
],
environment: createEditorEnvironment(),
darkColorHandler: new DarkColorHandlerImpl(contentDiv, options.baseDarkColor),
Expand Down Expand Up @@ -72,5 +75,6 @@ function getPluginState(corePlugins: StandaloneEditorCorePlugins) {
copyPaste: corePlugins.copyPaste.getState(),
cache: corePlugins.cache.getState(),
format: corePlugins.format.getState(),
lifecycle: corePlugins.lifecycle.getState(),
};
}
Loading

0 comments on commit 2699cf9

Please sign in to comment.