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: Add model into ContentChangedEvent #2076

Merged
merged 14 commits into from
Sep 22, 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
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