-
Notifications
You must be signed in to change notification settings - Fork 161
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Content Model: Add model into ContentChangedEvent (#2076)
* 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
1 parent
43da112
commit 31fa011
Showing
31 changed files
with
535 additions
and
96 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
324 changes: 324 additions & 0 deletions
324
demo/scripts/controls/sidePane/eventViewer/ContentModelEventViewPane.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(''); | ||
} | ||
} |
21 changes: 21 additions & 0 deletions
21
demo/scripts/controls/sidePane/eventViewer/ContentModelEventViewPlugin.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.