Skip to content

Commit

Permalink
Content Model: insertEntity API (#1800)
Browse files Browse the repository at this point in the history
* Content Model insertEntity

* improve

* improve

* Content Model: Improve cache behavior

* fix build

* Content Model: improve formatWithContentModel

* Content Model: improve formatWithContentModel 2

* Improve

* fix build

* fix build

* improve

* add test

* add test

* add test

* add test

* fix dark color

* fix test

* fix build and test
  • Loading branch information
JiuqingSong authored Aug 11, 2023
1 parent 69005d6 commit a8a9592
Show file tree
Hide file tree
Showing 25 changed files with 3,031 additions and 27 deletions.
2 changes: 1 addition & 1 deletion demo/scripts/controls/ContentModelEditorMainPane.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import ApiPlaygroundPlugin from './sidePane/apiPlayground/ApiPlaygroundPlugin';
import ApiPlaygroundPlugin from './sidePane/contentModelApiPlayground/ApiPlaygroundPlugin';
import ContentModelEditorOptionsPlugin from './sidePane/editorOptions/ContentModelEditorOptionsPlugin';
import ContentModelFormatPainterPlugin from './contentModel/plugins/ContentModelFormatPainterPlugin';
import ContentModelFormatStatePlugin from './sidePane/formatState/ContentModelFormatStatePlugin';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { IEditor, PluginEvent } from 'roosterjs-editor-types';
import { SidePaneElementProps } from '../SidePaneElement';

export default interface ApiPaneProps extends SidePaneElementProps {
getEditor: () => IEditor;
}

export interface ApiPlaygroundComponent {
onPluginEvent?: (e: PluginEvent) => void;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.header {
flex: 0 0 auto;
padding-bottom: 5px;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import * as React from 'react';
import apiEntries, { ApiPlaygroundReactComponent } from './apiEntries';
import ApiPaneProps from './ApiPaneProps';
import { getObjectKeys } from 'roosterjs-editor-dom';
import { PluginEvent } from 'roosterjs-editor-types';
import { SidePaneElement } from '../SidePaneElement';

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

export interface ApiPlaygroundPaneState {
current: string;
}

export default class ApiPlaygroundPane extends React.Component<ApiPaneProps, ApiPlaygroundPaneState>
implements SidePaneElement {
private select = React.createRef<HTMLSelectElement>();
private pane = React.createRef<ApiPlaygroundReactComponent>();
constructor(props: ApiPaneProps) {
super(props);
this.state = { current: 'empty' };
}

render() {
let componentClass = apiEntries[this.state.current].component;
let pane: JSX.Element = null;
if (componentClass) {
pane = React.createElement(componentClass, { ...this.props, ref: this.pane });
}

return (
<>
<div className={styles.header}>
<h3>Select an API to try</h3>

<select ref={this.select} value={this.state.current} onChange={this.onChange}>
{getObjectKeys(apiEntries).map(key => (
<option value={key} key={key}>
{apiEntries[key].name}
</option>
))}
</select>
</div>
{pane}
</>
);
}

onPluginEvent(e: PluginEvent) {
if (this.pane.current && this.pane.current.onPluginEvent) {
this.pane.current.onPluginEvent(e);
}
}

setHashPath(path: string[]) {
let paneName = path && getObjectKeys(apiEntries).indexOf(path[0]) >= 0 ? path[0] : null;

if (paneName && paneName != this.state.current) {
this.setState({
current: paneName,
});
} else {
this.props.updateHash(null, [this.state.current]);
}
}

private onChange = () => {
this.props.updateHash(null, [this.select.current.value]);
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import ApiPaneProps from './ApiPaneProps';
import ApiPlaygroundPane from './ApiPlaygroundPane';
import SidePanePluginImpl from '../SidePanePluginImpl';
import { PluginEvent } from 'roosterjs-editor-types';
import { SidePaneElementProps } from '../SidePaneElement';

export default class ApiPlaygroundPlugin extends SidePanePluginImpl<
ApiPlaygroundPane,
ApiPaneProps
> {
constructor() {
super(ApiPlaygroundPane, 'api', 'API Playground');
}

getComponentProps(base: SidePaneElementProps) {
return {
...base,
getEditor: () => {
return this.editor;
},
};
}

onPluginEvent(e: PluginEvent) {
this.getComponent(component => component.onPluginEvent(e));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import * as React from 'react';
import ApiPaneProps, { ApiPlaygroundComponent } from './ApiPaneProps';
import InsertEntityPane from './insertEntity/InsertEntityPane';

export interface ApiPlaygroundReactComponent
extends React.Component<ApiPaneProps, any>,
ApiPlaygroundComponent {}

interface ApiEntry {
name: string;
component?: { new (prpos: ApiPaneProps): ApiPlaygroundReactComponent };
}

const apiEntries: { [key: string]: ApiEntry } = {
empty: {
name: 'Please select',
},
entity: {
name: 'Insert Entity',
component: InsertEntityPane,
},
more: {
name: 'Coming soon...',
},
};

export default apiEntries;
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.textarea {
outline: none;
resize: none;
min-height: 40px;
width: 90%;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import * as React from 'react';
import ApiPaneProps from '../ApiPaneProps';
import { Entity } from 'roosterjs-editor-types';
import { getEntityFromElement, getEntitySelector } from 'roosterjs-editor-dom';
import { trustedHTMLHandler } from '../../../../utils/trustedHTMLHandler';
import {
IContentModelEditor,
insertEntity,
InsertEntityOptions,
} from 'roosterjs-content-model-editor';

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

interface InsertEntityPaneState {
entities: Entity[];
}

export default class InsertEntityPane extends React.Component<ApiPaneProps, InsertEntityPaneState> {
private entityType = React.createRef<HTMLInputElement>();
private html = React.createRef<HTMLTextAreaElement>();
private styleInline = React.createRef<HTMLInputElement>();
private styleBlock = React.createRef<HTMLInputElement>();
private focusAfterEntity = React.createRef<HTMLInputElement>();

private posFocus = React.createRef<HTMLInputElement>();
private posTop = React.createRef<HTMLInputElement>();
private posBottom = React.createRef<HTMLInputElement>();
private posRegionRoot = React.createRef<HTMLInputElement>();

constructor(props: ApiPaneProps) {
super(props);
this.state = {
entities: [],
};
}

render() {
return (
<>
<div>
Type: <input type="input" ref={this.entityType} />
</div>
<div>
HTML: <textarea className={styles.textarea} ref={this.html}></textarea>
</div>
<div>
Style:
<input
type="radio"
name="entityStyle"
ref={this.styleInline}
id="styleInline"
/>
<label htmlFor="styleInline">Inline</label>
<input type="radio" name="entityStyle" ref={this.styleBlock} id="styleBlock" />
<label htmlFor="styleBlock">Block</label>
</div>
<div>
Position:
<br />
<input type="radio" name="position" ref={this.posFocus} id="posFocus" />
<label htmlFor="posFocus">Current focus</label>
<br />
<input type="radio" name="position" ref={this.posTop} id="posTop" />
<label htmlFor="posTop">Top</label>
<br />
<input type="radio" name="position" ref={this.posBottom} id="posBottom" />
<label htmlFor="posBottom">Bottom</label>
<br />
<input
type="radio"
name="position"
ref={this.posRegionRoot}
id="posRegionRoot"
/>
<label htmlFor="posRegionRoot">Region root</label>
<br />
</div>
<div>
<input id="focusAfterEntity" type="checkbox" ref={this.focusAfterEntity} />
<label htmlFor="focusAfterEntity">Focus after entity</label>
</div>
<div>
<button onClick={this.insertEntity}>Insert Entity</button>
</div>
<hr />
<div>
<button onClick={this.onGetEntities}>Get all entities</button>
</div>
<div>
{this.state.entities.map(entity => (
<EntityButton key={entity.id} entity={entity} />
))}
</div>
</>
);
}

private insertEntity = () => {
const entityType = this.entityType.current.value;
const node = document.createElement('span');
node.innerHTML = trustedHTMLHandler(this.html.current.value);
const isBlock = this.styleBlock.current.checked;
const focusAfterEntity = this.focusAfterEntity.current.checked;
const insertAtTop = this.posTop.current.checked;
const insertAtBottom = this.posBottom.current.checked;
const insertAtRoot = this.posRegionRoot.current.checked;

if (node) {
const editor = this.props.getEditor();

editor.addUndoSnapshot(() => {
const options: InsertEntityOptions = {
contentNode: node,
focusAfterEntity: focusAfterEntity,
};

if (isBlock) {
insertEntity(
editor as IContentModelEditor,
entityType,
true,
insertAtRoot
? 'root'
: insertAtTop
? 'begin'
: insertAtBottom
? 'end'
: 'focus',
options
);
} else {
insertEntity(
editor as IContentModelEditor,
entityType,
isBlock,
insertAtTop ? 'begin' : insertAtBottom ? 'end' : 'focus',
options
);
}
});
}
};

private onGetEntities = () => {
const selector = getEntitySelector();
const nodes = this.props.getEditor().queryElements(selector);
const allEntities = nodes.map(node => getEntityFromElement(node));

this.setState({
entities: allEntities.filter(e => !!e),
});
};
}

function EntityButton({ entity }: { entity: Entity }) {
let background = '';
const onMouseOver = React.useCallback(() => {
background = entity.wrapper.style.backgroundColor;
entity.wrapper.style.backgroundColor = 'blue';
}, [entity]);

const onMouseOut = React.useCallback(() => {
entity.wrapper.style.backgroundColor = background;
}, [entity]);

return (
<div onMouseOver={onMouseOver} onMouseOut={onMouseOut}>
Type: {entity.type}
<br />
Id: {entity.id}
<br />
Readonly: {entity.isReadonly ? 'True' : 'False'}
<br />
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export const entityProcessor: ElementProcessor<HTMLElement> = (group, element, c
context,
{ segment: isBlockEntity ? 'empty' : undefined, paragraph: 'empty' },
() => {
const entityModel = createEntity(element, isReadonly, context.segmentFormat, id, type);
const entityModel = createEntity(element, isReadonly, type, context.segmentFormat, id);

// TODO: Need to handle selection for editable entity
if (context.isInSelection) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,21 @@ import { ContentModelEntity, ContentModelSegmentFormat } from 'roosterjs-content
* Create a ContentModelEntity model
* @param wrapper Wrapper element of this entity
* @param isReadonly Whether this is a readonly entity
* @param segmentFormat Segment format of this entity
* @param id @optional Id of this entity
* @param type @optional Type of this entity
* @param segmentFormat @optional Segment format of this entity
* @param id @optional Id of this entity
*/
export function createEntity(
wrapper: HTMLElement,
isReadonly: boolean,
type?: string,
segmentFormat?: ContentModelSegmentFormat,
id?: string,
type?: string
id?: string
): ContentModelEntity {
return {
segmentType: 'Entity',
blockType: 'Entity',
format: {
...(segmentFormat || {}),
},
format: { ...segmentFormat },
id,
type,
isReadonly,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -432,7 +432,7 @@ describe('Creators', () => {
const type = 'entity';
const isReadonly = true;
const wrapper = document.createElement('div');
const entityModel = createEntity(wrapper, isReadonly, undefined, id, type);
const entityModel = createEntity(wrapper, isReadonly, type, undefined, id);

expect(entityModel).toEqual({
blockType: 'Entity',
Expand All @@ -453,11 +453,11 @@ describe('Creators', () => {
const entityModel = createEntity(
wrapper,
isReadonly,
type,
{
fontSize: '10pt',
},
id,
type
id
);

expect(entityModel).toEqual({
Expand Down
Loading

0 comments on commit a8a9592

Please sign in to comment.