Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
a6071da
Use FormatContainer to represent DIV with id (#3003)
JiuqingSong Apr 21, 2025
f2d2e3b
Fix #3005 (#3007)
JiuqingSong Apr 24, 2025
83deda2
Fix a cache issue (#3006)
JiuqingSong Apr 25, 2025
5bbab35
Refactor getStyleMetadata to not rely on DomCreator and only use Stri…
BryanValverdeU Apr 28, 2025
ab2bb52
Clean image edit when undo (#3015)
juliaroldi Apr 30, 2025
e103fd2
Add 'CustomCopyCut' experimental feature to fix some copy cut bugs (#…
BryanValverdeU Apr 30, 2025
e82f3a2
Demo site: Add preset content for undeleteable anchor (#3014)
JiuqingSong Apr 30, 2025
32f47bf
Revert "Refactor getStyleMetadata to not rely on DomCreator and only …
BryanValverdeU May 5, 2025
58c6514
Add API playground for createModelFromHTML (#3019)
JiuqingSong May 5, 2025
4e94808
Do not copy div ID on Enter (#3011)
juliaroldi May 5, 2025
8ba1938
Add image hidden marker (#3021)
juliaroldi May 7, 2025
9a14db0
Include ImageMetadata in FormatState (#3023)
JiuqingSong May 7, 2025
ed226f0
Support List Pasting from PowerPoint Desktop (#3012)
BryanValverdeU May 8, 2025
d7760c1
Remove comments `<!--` and `-->` from styles and re apply fix for Wor…
BryanValverdeU May 8, 2025
f49c13e
Merge branch 'master' of https://github.com/microsoft/roosterjs into …
BryanValverdeU May 8, 2025
08d3399
Bump main version to 9.27.0 in versions.json
BryanValverdeU May 8, 2025
2b7075d
insert link in the image (#3027)
juliaroldi May 8, 2025
9eb2d29
square (#3029)
juliaroldi May 8, 2025
489b802
Normalize default format (#3028)
JiuqingSong May 8, 2025
612ffbd
auto link (#3026)
juliaroldi May 8, 2025
1668f0b
Merge branch 'master' of https://github.com/microsoft/roosterjs into …
BryanValverdeU May 8, 2025
4a632ce
Add margin-inline-start to watermark styles for improved positioning …
BryanValverdeU May 8, 2025
2b4bbdb
Merge branch 'master' of https://github.com/microsoft/roosterjs into …
BryanValverdeU May 8, 2025
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
11 changes: 10 additions & 1 deletion demo/scripts/controlsV2/mainPane/MainPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ import {
TableEditPlugin,
WatermarkPlugin,
} from 'roosterjs-content-model-plugins';
import DOMPurify = require('dompurify');

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

Expand Down Expand Up @@ -191,7 +192,15 @@ export class MainPane extends React.Component<{}, MainPaneState> {
this.updateContentPlugin.update();

const win = window.open(POPOUT_URL, POPOUT_TARGET, POPOUT_FEATURES);
win.document.write(trustedHTMLHandler(POPOUT_HTML));
win.document.write(
(DOMPurify.sanitize(POPOUT_HTML, {
ADD_TAGS: ['head', 'meta', 'iframe'],
ADD_ATTR: ['name', 'content'],
WHOLE_DOCUMENT: true,
ALLOW_UNKNOWN_PROTOCOLS: true,
RETURN_TRUSTED_TYPE: true,
}) as any) as string
);
win.addEventListener('beforeunload', () => {
this.updateContentPlugin.update();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { FormatParser, UndeletableFormat } from 'roosterjs-content-model-types';

const DemoUndeletableName = 'DemoUndeletable';
export const DemoUndeletableName = 'DemoUndeletable';

export function undeletableLinkChecker(a: HTMLAnchorElement): boolean {
return a.getAttribute('name') == DemoUndeletableName;
Expand Down
10 changes: 10 additions & 0 deletions demo/scripts/controlsV2/sidePane/apiPlayground/apiEntries.ts
Original file line number Diff line number Diff line change
@@ -1,4 +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 All @@ -24,6 +26,14 @@ const apiEntries: { [key: string]: ApiEntry } = {
name: 'Paste',
component: PastePane,
},
createModelFromHtml: {
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,5 @@
.textarea {
outline: none;
min-height: 200px;
width: 90%;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import * as React from 'react';
import { ApiPaneProps } from '../ApiPaneProps';
import { cloneModel } from 'roosterjs-content-model-dom';
import { ContentModelDocument } from 'roosterjs-content-model-types';
import { ContentModelDocumentView } from '../../contentModel/components/model/ContentModelDocumentView';
import { createModelFromHtml } from 'roosterjs-content-model-core';

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

interface CreateModelFromHtmlPaneState {
model: ContentModelDocument | null;
}

export default class CreateModelFromHtmlPane extends React.Component<
ApiPaneProps,
CreateModelFromHtmlPaneState
> {
private html = React.createRef<HTMLTextAreaElement>();

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

render() {
return (
<>
<div>HTML:</div>
<div>
<textarea className={styles.textarea} ref={this.html}></textarea>
</div>
<div>
<button onClick={this.createModel}>Create Model</button>
</div>
<div>
{this.state.model ? <ContentModelDocumentView doc={this.state.model} /> : null}
</div>
<div>
<button onClick={this.setModel} disabled={!this.state.model}>
Set Model into Editor
</button>
</div>
</>
);
}

private createModel = () => {
const html = this.html.current?.value || '';

if (html) {
const model = createModelFromHtml(
html,
undefined,
this.props.getEditor().getDOMCreator()
);
this.setState({ model });
} else {
this.setState({ model: null });
}
};

private setModel = () => {
if (this.state.model) {
this.props.getEditor().formatContentModel(model => {
model.blocks = cloneModel(this.state.model).blocks;

return true;
});
}
};
}
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;
});
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as React from 'react';
import { ApiPaneProps } from '../ApiPaneProps';
import { insertEntity } from 'roosterjs-content-model-api';
import { moveChildNodes } from 'roosterjs-content-model-dom';
import { trustedHTMLHandler } from '../../../../utils/trustedHTMLHandler';
import {
ContentModelEntity,
Expand Down Expand Up @@ -98,7 +99,8 @@ export default class InsertEntityPane extends React.Component<ApiPaneProps, Inse
private insertEntity = () => {
const entityType = this.entityType.current.value;
const node = document.createElement('span');
node.innerHTML = trustedHTMLHandler(this.html.current.value);

moveChildNodes(node, trustedHTMLHandler.htmlToDOM(this.html.current.value).body);
const isBlock = this.styleBlock.current.checked;
const focusAfterEntity = this.focusAfterEntity.current.checked;
const insertAtTop = this.posTop.current.checked;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,11 @@ const initialState: OptionState = {
handleTabKey: true,
},
customReplacements: emojiReplacements,
experimentalFeatures: new Set<ExperimentalFeature>(['PersistCache', 'HandleEnterKey']),
experimentalFeatures: new Set<ExperimentalFeature>([
'PersistCache',
'HandleEnterKey',
'CustomCopyCut',
]),
};

export class EditorOptionsPlugin extends SidePanePluginImpl<OptionsPane, OptionPaneProps> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export class ExperimentalFeatures extends React.Component<DefaultFormatProps, {}
<>
{this.renderFeature('PersistCache')}
{this.renderFeature('HandleEnterKey')}
{this.renderFeature('CustomCopyCut')}
</>
);
}
Expand Down
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
3 changes: 2 additions & 1 deletion demo/scripts/controlsV2/sidePane/presets/PresetPane.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as React from 'react';
import { allPresets, Preset } from './allPresets/allPresets';
import { allPresets } from './allPresets/allPresets';
import { IEditor } from 'roosterjs-content-model-types';
import { Preset } from './allPresets/Preset';
import { SidePaneElementProps } from '../SidePaneElement';

const styles = require('./PresetPane.scss');
Expand Down
7 changes: 7 additions & 0 deletions demo/scripts/controlsV2/sidePane/presets/allPresets/Preset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { ContentModelDocument } from 'roosterjs-content-model-types';

export type Preset = {
buttonName: string;
id: string;
content: ContentModelDocument;
};
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
import { allTextFormats } from './textPresets';
import { ContentModelDocument } from 'roosterjs-content-model-types';
import { image64x64Black, image64x64Gradient, image64x64White } from './imagePresets';
import { mergedTableNoText, simpleTable, simpleTableWithHeader } from './tablePresets';
import { mixedParagraphs } from './paragraphPresets';
import { numberedList, simpleList } from './listPresets';

export type Preset = {
buttonName: string;
id: string;
content: ContentModelDocument;
};
import { Preset } from './Preset';
import { undeleteableText } from './undeleteablePresets';

const wipeEditor: Preset = {
buttonName: 'Wipe Editor',
Expand All @@ -33,6 +28,7 @@ export const allPresets: Preset[] = [
image64x64Gradient,
image64x64Black,
image64x64White,
undeleteableText,
];

export function getPresetModelById(id: string) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Preset } from './allPresets';
import { Preset } from './Preset';

export const image64x64Gradient: Preset = {
buttonName: 'Image 64x64 All Colours - png',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Preset } from './allPresets';
import { Preset } from './Preset';

export const simpleList: Preset = {
buttonName: 'Simple List',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Preset } from './allPresets';
import { Preset } from './Preset';

export const mixedParagraphs: Preset = {
buttonName: 'Mixed Paragraphs',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Preset } from './allPresets';
import { Preset } from './Preset';

export const simpleTable: Preset = {
buttonName: 'Simple Table',
Expand Down
Loading
Loading