Skip to content
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
5 changes: 5 additions & 0 deletions demo/scripts/controlsV2/sidePane/apiPlayground/apiEntries.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as React from 'react';
import CreateModelFromHtmlPane from './createModelFromHtml/CreateModelFromHtmlPane';
import InsertCustomContainerPane from './insertCustomContainer/InsertCustomContainerPane';
import InsertEntityPane from './insertEntity/InsertEntityPane';
import PastePane from './paste/PastePane';
import { ApiPaneProps, ApiPlaygroundComponent } from './ApiPaneProps';
Expand Down Expand Up @@ -29,6 +30,10 @@ const apiEntries: { [key: string]: ApiEntry } = {
name: 'Create Model from HTML',
component: CreateModelFromHtmlPane,
},
customContainer: {
name: 'Insert Custom Container',
component: InsertCustomContainerPane,
},
more: {
name: 'Coming soon...',
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.textarea {
height: 200px;
}

.blockContent {
display: grid;
row-gap: 5px;
padding-bottom: 5px;
}

.results {
overflow-wrap: anywhere;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import * as React from 'react';
import { ApiPaneProps } from '../ApiPaneProps';
import { createFormatContainer, mergeModel } from 'roosterjs-content-model-dom';
import { createModelFromHtml } from 'roosterjs-content-model-core';
import { trustedHTMLHandler } from '../../../../utils/trustedHTMLHandler';
import {
ContentModelDocument,
ContentModelFormatContainer,
IEditor,
} from 'roosterjs-content-model-types';

interface InsertCustomContainerPaneState {
containers: {
[id: string]: ContentModelFormatContainer;
};
}

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

export default class InsertCustomContainerPane extends React.Component<
ApiPaneProps,
InsertCustomContainerPaneState
> {
public html = React.createRef<HTMLTextAreaElement>();
private containerId = React.createRef<HTMLInputElement>();
private searchId = React.createRef<HTMLInputElement>();
private result = React.createRef<HTMLParagraphElement>();

constructor(props: ApiPaneProps) {
super(props);
this.state = {
containers: {},
};
}

private insertCustomContainer = () => {
const model = createModelFromHtml(this.html.current.value, undefined, trustedHTMLHandler);
const container = createFormatContainer('div');
const containerId = this.containerId.current.value;
container.format.id = containerId;
container.blocks = [...model.blocks];
this.state.containers[containerId] = container;
model.blocks = [container];
const editor = this.props.getEditor();
insertContainer(editor, model);
};

private getCustomContainer = () => {
const id = this.searchId.current.value;
const container = this.state.containers[id];
if (container) {
this.result.current.innerText = JSON.stringify(container);
} else {
this.result.current.innerText = 'No container found';
}
};

render() {
return (
<>
<div className={styles.blockContent}>
<label htmlFor="containerId"> Insert container</label>
<input
ref={this.containerId}
title="Container Id"
id="containerId"
type="text"
placeholder="Container Id"
/>
<textarea
ref={this.html}
className={styles.textarea}
title="Custom Content"
name="Content"
id="customContent"
placeholder="Insert HTML Content"></textarea>
<button onClick={this.insertCustomContainer} type="button">
Insert container
</button>
</div>

<div className={styles.blockContent}>
<label htmlFor="containerId">Id:</label>
<input ref={this.searchId} title="Container Id" id="containerId" type="text" />
<button onClick={this.getCustomContainer} type="button">
Get Custom Container
</button>
<label htmlFor="results"> Results:</label>
<p className={styles.results} ref={this.result} id="results"></p>
</div>
</>
);
}
}

function insertContainer(editor: IEditor, newModel: ContentModelDocument) {
editor.formatContentModel((model, context) => {
mergeModel(model, newModel, context, {
mergeFormat: 'mergeAll',
});
return true;
});
}
22 changes: 16 additions & 6 deletions demo/scripts/controlsV2/sidePane/editorOptions/Plugins.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ abstract class PluginsBase<PluginKey extends keyof BuildInPluginList> extends Re
export class Plugins extends PluginsBase<keyof BuildInPluginList> {
private allowExcelNoBorderTable = React.createRef<HTMLInputElement>();
private handleTabKey = React.createRef<HTMLInputElement>();
private handleEnterKey = React.createRef<HTMLInputElement>();
private listMenu = React.createRef<HTMLInputElement>();
private tableMenu = React.createRef<HTMLInputElement>();
private imageMenu = React.createRef<HTMLInputElement>();
Expand Down Expand Up @@ -201,12 +202,21 @@ export class Plugins extends PluginsBase<keyof BuildInPluginList> {
{this.renderPluginItem(
'edit',
'Edit',
this.renderCheckBox(
'Handle Tab Key',
this.handleTabKey,
this.props.state.editPluginOptions.handleTabKey,
(state, value) => (state.editPluginOptions.handleTabKey = value)
)
<>
{this.renderCheckBox(
'Handle Tab Key',
this.handleTabKey,
this.props.state.editPluginOptions.handleTabKey,
(state, value) => (state.editPluginOptions.handleTabKey = value)
)}
{this.renderCheckBox(
'Handle Enter Key',
this.handleEnterKey,
this.props.state.editPluginOptions.shouldHandleEnterKey as boolean,
(state, value) =>
(state.editPluginOptions.shouldHandleEnterKey = value)
)}
</>
)}
{this.renderPluginItem(
'paste',
Expand Down
40 changes: 36 additions & 4 deletions packages/roosterjs-content-model-plugins/lib/edit/EditPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ export type EditOptions = {
* @default true
*/
handleExpandedSelectionOnDelete?: boolean;

/**
* Callback function to determine whether the Rooster should handle the Enter key press.
* If the function returns true, the Rooster will handle the Enter key press instead of the browser.
* @param editor - The editor instance.
* @returns A boolean
*/
shouldHandleEnterKey?: ((editor: IEditor) => boolean) | boolean;
};

const BACKSPACE_KEY = 8;
Expand Down Expand Up @@ -54,14 +62,34 @@ export class EditPlugin implements EditorPlugin {
private disposer: (() => void) | null = null;
private shouldHandleNextInputEvent = false;
private selectionAfterDelete: DOMSelection | null = null;
private handleNormalEnter = false;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer we keep this parameter here and still check it if the one in EditorOptions is not passed. Because it can be used by some team today, so if we remove the check here, it will break their feature

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can save a checker function in the class, initialize it in ctor, so no need to check it everytime when Enter is pressed. for example:

class EditPlugin {
..
  private handleNormalEnter: (event: PluginEvent) => boolean;
  
  constructor (..) {
    ...
    switch (typeof options.shouldHandleEnterKey) {
      case 'function': 
        this.handleNormalEnter = options.shouldHandleEnterKey;
        break;

     case 'undefined':
         this.shouldHandleEnterKey = this.createNormalEnterChecker(this.editor.isExperimentalFeatureEnabled('handleEnterKey'));
         break;

     default:
          this.createNormalEnterChecker(!!options.shouldHandlerEnterKey);
          break;
   }

   private createNormalEnterChecker(result: boolean) {
        return result ? () => true : () => false;
   }

When handle enter key, just call this.handleNormalEnter(event), no additional checking needed.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And add comment in the experimental feature and the new option to explain the priority

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did a bit different than what you proposed, I added back the experimental feature check, so it do not break the code of the user that has the experimental features enabled.

private handleNormalEnter: (editor: IEditor) => boolean = (editor: IEditor) => false;

/**
* @param options An optional parameter that takes in an object of type EditOptions, which includes the following properties:
* handleTabKey: A boolean that enables or disables Tab key handling. Defaults to true.
*/
constructor(private options: EditOptions = DefaultOptions) {}

private createNormalEnterChecker(result: boolean) {
return result ? () => true : () => false;
}

private getHandleNormalEnter(editor: IEditor) {
switch (typeof this.options.shouldHandleEnterKey) {
case 'function':
return this.options.shouldHandleEnterKey;
break;
case 'boolean':
return this.createNormalEnterChecker(this.options.shouldHandleEnterKey);
break;
default:
return this.createNormalEnterChecker(
editor.isExperimentalFeatureEnabled('HandleEnterKey')
);
break;
}
}

/**
* Get name of this plugin
*/
Expand All @@ -77,7 +105,7 @@ export class EditPlugin implements EditorPlugin {
*/
initialize(editor: IEditor) {
this.editor = editor;
this.handleNormalEnter = this.editor.isExperimentalFeatureEnabled('HandleEnterKey');
this.handleNormalEnter = this.getHandleNormalEnter(editor);

if (editor.getEnvironment().isAndroid) {
this.disposer = this.editor.attachDomEvent({
Expand Down Expand Up @@ -179,7 +207,11 @@ export class EditPlugin implements EditorPlugin {
// No need to clear cache here since if we rely on browser's behavior, there will be Input event and its handler will reconcile cache
// And leave it to browser when shift key is pressed so that browser will trigger cut event
if (!event.rawEvent.shiftKey) {
keyboardDelete(editor, rawEvent, this.options.handleExpandedSelectionOnDelete);
keyboardDelete(
editor,
rawEvent,
this.options.handleExpandedSelectionOnDelete
);
}
break;

Expand All @@ -200,7 +232,7 @@ export class EditPlugin implements EditorPlugin {
!event.rawEvent.isComposing &&
event.rawEvent.keyCode !== DEAD_KEY
) {
keyboardEnter(editor, rawEvent, this.handleNormalEnter);
keyboardEnter(editor, rawEvent, this.handleNormalEnter(editor));
}
break;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,45 @@ import {
setParagraphNotImplicit,
} from 'roosterjs-content-model-dom';
import type {
ContentModelBlockFormat,
InsertPoint,
ShallowMutableContentModelParagraph,
} from 'roosterjs-content-model-types';

const DEFAULT_FORMAT_KEYS: Partial<keyof ContentModelBlockFormat>[] = [
'backgroundColor',
'direction',
'textAlign',
'htmlAlign',
'lineHeight',
'textIndent',
'marginTop',
'marginRight',
'marginBottom',
'marginLeft',
'paddingTop',
'paddingRight',
'paddingBottom',
'paddingLeft',
];

/**
* @internal
* Split the given paragraph from insert point into two paragraphs,
* and move the selection marker to the beginning of the second paragraph
* @param insertPoint The input insert point which includes the paragraph and selection marker
* @param formatKeys The format that needs to be copied from the splitted paragraph, if not specified, some default format will be copied
* @returns The new paragraph it created
*/
export function splitParagraph(insertPoint: InsertPoint) {
export function splitParagraph(
insertPoint: InsertPoint,
formatKeys: Partial<keyof ContentModelBlockFormat>[] = DEFAULT_FORMAT_KEYS
) {
const { paragraph, marker } = insertPoint;
const newFormat = createNewFormat(paragraph.format, formatKeys);
const newParagraph: ShallowMutableContentModelParagraph = createParagraph(
false /*isImplicit*/,
paragraph.format,
newFormat,
paragraph.segmentFormat
);

Expand All @@ -44,3 +67,19 @@ export function splitParagraph(insertPoint: InsertPoint) {

return newParagraph;
}

const createNewFormat = (
format: ContentModelBlockFormat,
formatKeys: Partial<keyof ContentModelBlockFormat>[]
) => {
let newFormat: ContentModelBlockFormat = {};
for (const key of formatKeys) {
if (format[key]) {
newFormat = {
...newFormat,
[key]: format[key],
};
}
}
return newFormat;
};
Loading
Loading