Skip to content

Commit

Permalink
Content Model: Add model into ContentChangedEvent (#2076)
Browse files Browse the repository at this point in the history
* Content Model: Add model into ContentChangedEvent

* fix build

* add demo site page

* fix build

* fix test

* Fix entity issue

---------

Co-authored-by: Bryan Valverde U <[email protected]>
  • Loading branch information
JiuqingSong and BryanValverdeU authored Sep 22, 2023
1 parent 43da112 commit 31fa011
Show file tree
Hide file tree
Showing 31 changed files with 535 additions and 96 deletions.
6 changes: 3 additions & 3 deletions demo/scripts/controls/ContentModelEditorMainPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import * as React from 'react';
import * as ReactDOM from 'react-dom';
import ApiPlaygroundPlugin from './sidePane/contentModelApiPlayground/ApiPlaygroundPlugin';
import ContentModelEditorOptionsPlugin from './sidePane/editorOptions/ContentModelEditorOptionsPlugin';
import ContentModelEventViewPlugin from './sidePane/eventViewer/ContentModelEventViewPlugin';
import ContentModelFormatPainterPlugin from './contentModel/plugins/ContentModelFormatPainterPlugin';
import ContentModelFormatStatePlugin from './sidePane/formatState/ContentModelFormatStatePlugin';
import ContentModelPanePlugin from './sidePane/contentModel/ContentModelPanePlugin';
import ContentModelRibbon from './ribbonButtons/contentModel/ContentModelRibbon';
import EventViewPlugin from './sidePane/eventViewer/EventViewPlugin';
import getToggleablePlugins from './getToggleablePlugins';
import MainPaneBase from './MainPaneBase';
import SampleEntityPlugin from './sampleEntity/SampleEntityPlugin';
Expand Down Expand Up @@ -84,7 +84,7 @@ const DarkTheme: PartialTheme = {
class ContentModelEditorMainPane extends MainPaneBase {
private formatStatePlugin: ContentModelFormatStatePlugin;
private editorOptionPlugin: ContentModelEditorOptionsPlugin;
private eventViewPlugin: EventViewPlugin;
private eventViewPlugin: ContentModelEventViewPlugin;
private apiPlaygroundPlugin: ApiPlaygroundPlugin;
private ContentModelPanePlugin: ContentModelPanePlugin;
private ribbonPlugin: RibbonPlugin;
Expand All @@ -100,7 +100,7 @@ class ContentModelEditorMainPane extends MainPaneBase {

this.formatStatePlugin = new ContentModelFormatStatePlugin();
this.editorOptionPlugin = new ContentModelEditorOptionsPlugin();
this.eventViewPlugin = new EventViewPlugin();
this.eventViewPlugin = new ContentModelEventViewPlugin();
this.apiPlaygroundPlugin = new ApiPlaygroundPlugin();
this.snapshotPlugin = new SnapshotPlugin();
this.ContentModelPanePlugin = new ContentModelPanePlugin();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,324 @@
import * as React from 'react';
import { ContentModelContentChangedEvent } from 'roosterjs-content-model-editor';
import { EntityOperation, PluginEvent, PluginEventType } from 'roosterjs-editor-types';
import { SidePaneElementProps } from '../SidePaneElement';
import {
getObjectKeys,
getTagOfNode,
HtmlSanitizer,
readFile,
safeInstanceOf,
} from 'roosterjs-editor-dom';

const styles = require('./EventViewPane.scss');

export interface EventEntry {
index: number;
time: Date;
event: PluginEvent;
}

export interface EventViewPaneState {
displayCount: number;
currentIndex: number;
}

const EventTypeMap: { [key in PluginEventType]: string } = {
[PluginEventType.BeforeDispose]: 'BeforeDispose',
[PluginEventType.BeforePaste]: 'BeforePaste',
[PluginEventType.CompositionEnd]: 'CompositionEnd',
[PluginEventType.ContentChanged]: 'ContentChanged',
[PluginEventType.EditorReady]: 'EditorReady',
[PluginEventType.EntityOperation]: 'EntityOperation',
[PluginEventType.ExtractContentWithDom]: 'ExtractContentWithDom',
[PluginEventType.KeyDown]: 'KeyDown',
[PluginEventType.KeyPress]: 'KeyPress',
[PluginEventType.KeyUp]: 'KeyUp',
[PluginEventType.MouseDown]: 'MouseDown',
[PluginEventType.MouseUp]: 'MouseUp',
[PluginEventType.Input]: 'Input',
[PluginEventType.PendingFormatStateChanged]: 'PendingFormatStateChanged',
[PluginEventType.Scroll]: 'Scroll',
[PluginEventType.BeforeCutCopy]: 'BeforeCutCopy',
[PluginEventType.ContextMenu]: 'ContextMenu',
[PluginEventType.EnteredShadowEdit]: 'EnteredShadowEdit',
[PluginEventType.LeavingShadowEdit]: 'LeavingShadowEdit',
[PluginEventType.EditImage]: 'EditImage',
[PluginEventType.BeforeSetContent]: 'BeforeSetContent',
[PluginEventType.ZoomChanged]: 'ZoomChanged',
[PluginEventType.SelectionChanged]: 'SelectionChanged',
[PluginEventType.BeforeKeyboardEditing]: 'BeforeKeyboardEditing',
};

const EntityOperationMap: { [key in EntityOperation]: string } = {
[EntityOperation.AddShadowRoot]: 'AddShadowRoot',
[EntityOperation.RemoveShadowRoot]: 'RemoveShadowRoot',
[EntityOperation.Click]: 'Click',
[EntityOperation.ContextMenu]: 'ContextMenu',
[EntityOperation.Escape]: 'Escape',
[EntityOperation.NewEntity]: 'NewEntity',
[EntityOperation.Overwrite]: 'Overwrite',
[EntityOperation.PartialOverwrite]: 'PartialOverwrite',
[EntityOperation.RemoveFromEnd]: 'RemoveFromEnd',
[EntityOperation.RemoveFromStart]: 'RemoveFromStart',
[EntityOperation.ReplaceTemporaryContent]: 'ReplaceTemporaryContent',
[EntityOperation.UpdateEntityState]: 'UpdateEntityState',
};

export default class ContentModelEventViewPane extends React.Component<
SidePaneElementProps,
EventViewPaneState
> {
private events: EventEntry[] = [];
private displayCount = React.createRef<HTMLSelectElement>();
private lastIndex = 0;

constructor(props: SidePaneElementProps) {
super(props);
this.state = {
displayCount: 20,
currentIndex: -1,
};
}

render() {
let displayCount = Math.min(this.events.length, this.state.displayCount);
let displayedEvents =
displayCount > 0 ? this.events.slice(this.events.length - displayCount) : [];
displayedEvents = displayedEvents.reverse();

return (
<>
<div>
Show item count:
<select
defaultValue={this.state.displayCount.toString()}
ref={this.displayCount}
onChange={this.onDisplayCountChanged}>
<option value={'0'}>Disabled</option>
<option value={'20'}>20</option>
<option value={'50'}>50</option>
<option value={'100'}>100</option>
</select>{' '}
<button onClick={this.clear}>Clear all</button>
</div>
<div>
{displayedEvents.map(event => (
<details key={event.index.toString()}>
<summary>
{`${event.time.getHours()}:${event.time.getMinutes()}:${event.time.getSeconds()}.${event.time.getMilliseconds()} `}
{EventTypeMap[event.event.eventType]}
</summary>
<div className={styles.eventContent}>
{this.renderEvent(event.event)}
</div>
</details>
))}
</div>
</>
);
}

addEvent(event: PluginEvent) {
if (this.state.displayCount > 0) {
if (event.eventType == PluginEventType.BeforePaste) {
const sanitizer = new HtmlSanitizer(event.sanitizingOption);
const fragment = event.fragment.cloneNode(true /*deep*/) as DocumentFragment;

sanitizer.convertGlobalCssToInlineCss(fragment);
sanitizer.sanitize(fragment);
(event.clipboardData as any).html = this.getHtml(fragment);
}

this.events.push({
time: new Date(),
event: event,
index: this.lastIndex++,
});

while (this.events.length > 100) {
this.events.shift();
}
this.setState({
currentIndex: this.lastIndex,
});
}
}

private renderEvent(event: PluginEvent): JSX.Element {
switch (event.eventType) {
case PluginEventType.KeyDown:
case PluginEventType.KeyPress:
case PluginEventType.KeyUp:
return (
<span>
Key=
{event.rawEvent.which}
</span>
);

case PluginEventType.MouseDown:
case PluginEventType.MouseUp:
case PluginEventType.ContextMenu:
return (
<span>
Button=
{event.rawEvent.button}, SrcElement=
{event.rawEvent.target && getTagOfNode(event.rawEvent.target as Node)},
PageX=
{event.rawEvent.pageX}, PageY=
{event.rawEvent.pageY}
</span>
);

case PluginEventType.ContentChanged:
return (
<span>
Source=
{event.source}, Data=
{event.data && event.data.toString && event.data.toString()}
{!!(event as ContentModelContentChangedEvent).contentModel && (
<details>
<summary>Content Model</summary>
<pre className={styles.eventContent}>
{JSON.stringify(
(event as ContentModelContentChangedEvent).contentModel,
(key, value) =>
safeInstanceOf(value, 'Node')
? Object.prototype.toString.apply(value)
: key == 'src'
? value.length > 100
? value.substring(0, 97) + '...'
: value
: value,
2
)}
</pre>
</details>
)}
</span>
);

case PluginEventType.BeforePaste:
return (
<span>
Types=
{event.clipboardData.types.join()}
{this.renderPasteContent('Plain text', event.clipboardData.text)}
{this.renderPasteContent(
'Sanitized HTML',
(event.clipboardData as any).html
)}
{this.renderPasteContent('Original HTML', event.clipboardData.rawHtml)}
{this.renderPasteContent('Image', event.clipboardData.image, img => (
<img
ref={ref => ref && this.renderImage(ref, img)}
className={styles.img}
/>
))}
{this.renderPasteContent(
'LinkPreview',
event.clipboardData.linkPreview
? JSON.stringify(event.clipboardData.linkPreview)
: ''
)}
Paste from keyboard or native context menu:
{event.clipboardData.pasteNativeEvent ? ' true' : ' false'}
{getObjectKeys(event.clipboardData.customValues).map(contentType =>
this.renderPasteContent(
contentType,
event.clipboardData.customValues[contentType]
)
)}
</span>
);
case PluginEventType.PendingFormatStateChanged:
const formatState = event.formatState;
const keys = getObjectKeys(formatState);
return <span>{keys.map(key => `${key}=${event.formatState[key]}; `)}</span>;

case PluginEventType.EntityOperation:
const {
operation,
entity: { id, type },
} = event;
return (
<span>
Operation={EntityOperationMap[operation]} Type={type}; Id={id}
</span>
);

case PluginEventType.BeforeCutCopy:
const { isCut } = event;
return <span>isCut={isCut ? 'true' : 'false'}</span>;

case PluginEventType.EditImage:
return (
<>
<span>new src={event.newSrc.substr(0, 100)}</span>
</>
);

case PluginEventType.ZoomChanged:
return (
<span>
Old value={event.oldZoomScale} New value={event.newZoomScale}
</span>
);

case PluginEventType.BeforeKeyboardEditing:
return <span>Key code={event.rawEvent.which}</span>;

default:
return null;
}
}

private clear = () => {
this.events = [];
this.setState({
currentIndex: -1,
});
};

private renderImage = (img: HTMLImageElement, imageFile: File) => {
readFile(imageFile, dataUrl => (img.src = dataUrl));
};

private onDisplayCountChanged = () => {
let value = parseInt(this.displayCount.current.value);
this.setState({
displayCount: value,
});
};

private renderPasteContent(
title: string,
content: any,
renderer: (content: any) => JSX.Element = content => <span>{content}</span>
): JSX.Element {
return (
content && (
<details>
<summary>{title}</summary>
<div className={styles.pasteContent}>{renderer(content)}</div>
</details>
)
);
}

private getHtml(fragment: DocumentFragment) {
const stringArray: string[] = [];
for (let child = fragment.firstChild; child; child = child.nextSibling) {
stringArray.push(
safeInstanceOf(child, 'HTMLElement')
? child.outerHTML
: safeInstanceOf(child, 'Text')
? child.nodeValue
: ''
);
}

return stringArray.join('');
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import ContentModelEventViewPane from './ContentModelEventViewPane';
import SidePanePluginImpl from '../SidePanePluginImpl';
import { PluginEvent } from 'roosterjs-editor-types';
import { SidePaneElementProps } from '../SidePaneElement';

export default class ContentModelEventViewPlugin extends SidePanePluginImpl<
ContentModelEventViewPane,
SidePaneElementProps
> {
constructor() {
super(ContentModelEventViewPane, 'event', 'Event Viewer');
}

onPluginEvent(e: PluginEvent) {
this.getComponent(component => component.addEvent(e));
}

getComponentProps(base: SidePaneElementProps) {
return base;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,10 @@ export default class ContentModelEditor
model: ContentModelDocument,
option?: ModelToDomOption,
onNodeCreated?: OnNodeCreated
) {
): SelectionRangeEx | null {
const core = this.getCore();

core.api.setContentModel(core, model, option, onNodeCreated);
return core.api.setContentModel(core, model, option, onNodeCreated);
}

/**
Expand Down
Loading

0 comments on commit 31fa011

Please sign in to comment.