Compare commits
4 Commits
main
...
dev/Rooste
Author | SHA1 | Date |
---|---|---|
|
a652f45f5d | 4 days ago |
|
5ec77f21f2 | 4 days ago |
|
16378d2d1a | 4 days ago |
|
e82a23d364 | 6 days ago |
@ -0,0 +1,564 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import * as ReactDOM from 'react-dom';
|
||||||
|
import SampleEntityPlugin from './plugins/SampleEntityPlugin';
|
||||||
|
// import { ApiPlaygroundPlugin } from './sidePane/apiPlayground/ApiPlaygroundPlugin';
|
||||||
|
// import { ContentModelPanePlugin } from './sidePane/contentModel/ContentModelPanePlugin';
|
||||||
|
// import { darkModeButton } from './demoButtons/darkModeButton';
|
||||||
|
import { defaultDomToModelOption } from './options/defaultDomToModelOption';
|
||||||
|
import { Editor } from 'roosterjs-content-model-core';
|
||||||
|
import { EditorOptionsPlugin } from './editorOptions/EditorOptionsPlugin';
|
||||||
|
// import { EventViewPlugin } from './sidePane/eventViewer/EventViewPlugin';
|
||||||
|
// import { exportContentButton } from './demoButtons/exportContentButton';
|
||||||
|
import { FormatPainterPlugin } from './plugins/FormatPainterPlugin';
|
||||||
|
// import { FormatStatePlugin } from './sidePane/formatState/FormatStatePlugin';
|
||||||
|
import { getButtons } from './tabs/ribbonButtons';
|
||||||
|
import { getDarkColor } from 'roosterjs-color-utils';
|
||||||
|
// import { getPresetModelById } from './sidePane/presets/allPresets/allPresets';
|
||||||
|
import { getTabs, tabNames } from './tabs/getTabs';
|
||||||
|
import { getTheme } from './theme/themes';
|
||||||
|
// import { MarkdownPanePlugin } from './sidePane/MarkdownPane/MarkdownPanePlugin';
|
||||||
|
import { OptionState, UrlPlaceholder } from './editorOptions/OptionState';
|
||||||
|
// import { popoutButton } from './demoButtons/popoutButton';
|
||||||
|
// import { PresetPlugin } from './sidePane/presets/PresetPlugin';
|
||||||
|
import { registerWindowForCss, unregisterWindowForCss } from './utils/cssMonitor';
|
||||||
|
import { SamplePickerPlugin } from './plugins/SamplePickerPlugin';
|
||||||
|
// // // import { SidePane } from './sidePane/SidePane';
|
||||||
|
// // // import { SidePanePlugin } from './sidePane/SidePanePlugin';
|
||||||
|
import { SnapshotPlugin } from './snapshot/SnapshotPlugin';
|
||||||
|
import { ThemeProvider } from '@fluentui/react/lib/Theme';
|
||||||
|
// import { TitleBar } from './titleBar/TitleBar';
|
||||||
|
import { trustedHTMLHandler } from './utils/trustedHTMLHandler';
|
||||||
|
import { undeletableLinkChecker } from './options/demoUndeletableAnchorParser';
|
||||||
|
import { UpdateContentPlugin } from './plugins/UpdateContentPlugin';
|
||||||
|
import { WindowProvider } from '@fluentui/react/lib/WindowProvider';
|
||||||
|
// import { zoomButton } from './demoButtons/zoomButton';
|
||||||
|
import type { RibbonButton, RibbonPlugin } from 'roosterjs-react';
|
||||||
|
import {
|
||||||
|
createContextMenuPlugin,
|
||||||
|
createEmojiPlugin,
|
||||||
|
createImageEditMenuProvider,
|
||||||
|
createListEditMenuProvider,
|
||||||
|
createPasteOptionPlugin,
|
||||||
|
createRibbonPlugin,
|
||||||
|
createTableEditMenuProvider,
|
||||||
|
redoButton,
|
||||||
|
Rooster,
|
||||||
|
undoButton,
|
||||||
|
Ribbon,
|
||||||
|
} from 'roosterjs-react';
|
||||||
|
import {
|
||||||
|
Border,
|
||||||
|
Colors,
|
||||||
|
ContentModelDocument,
|
||||||
|
EditorOptions,
|
||||||
|
EditorPlugin,
|
||||||
|
IEditor,
|
||||||
|
KnownAnnounceStrings,
|
||||||
|
Snapshots,
|
||||||
|
} from 'roosterjs-content-model-types';
|
||||||
|
import {
|
||||||
|
AutoFormatPlugin,
|
||||||
|
CustomReplacePlugin,
|
||||||
|
EditPlugin,
|
||||||
|
HiddenPropertyPlugin,
|
||||||
|
HyperlinkPlugin,
|
||||||
|
ImageEditPlugin,
|
||||||
|
MarkdownPlugin,
|
||||||
|
PastePlugin,
|
||||||
|
ShortcutPlugin,
|
||||||
|
TableEditPlugin,
|
||||||
|
WatermarkPlugin,
|
||||||
|
} from 'roosterjs-content-model-plugins';
|
||||||
|
// import DOMPurify = require('dompurify');
|
||||||
|
|
||||||
|
// const styles = require('./MainPane.scss');
|
||||||
|
|
||||||
|
export interface MainPaneState {
|
||||||
|
showSidePane: boolean;
|
||||||
|
popoutWindow: Window;
|
||||||
|
initState: OptionState;
|
||||||
|
scale: number;
|
||||||
|
isDarkMode: boolean;
|
||||||
|
isRtl: boolean;
|
||||||
|
activeTab: tabNames;
|
||||||
|
tableBorderFormat?: Border;
|
||||||
|
editorCreator: (div: HTMLDivElement, options: EditorOptions) => IEditor;
|
||||||
|
}
|
||||||
|
|
||||||
|
// const PopoutRoot = 'mainPane';
|
||||||
|
// const POPOUT_HTML = `<!doctype html><html><head><title>RoosterJs Demo Site</title></head><body><div id=${PopoutRoot}></div></body></html>`;
|
||||||
|
// const POPOUT_FEATURES = 'menubar=no,statusbar=no,width=1200,height=800';
|
||||||
|
// const POPOUT_URL = 'about:blank';
|
||||||
|
// const POPOUT_TARGET = '_blank';
|
||||||
|
|
||||||
|
export class MainPane extends React.Component<{}, MainPaneState> {
|
||||||
|
private mouseX: number;
|
||||||
|
private static instance: MainPane;
|
||||||
|
private popoutRoot: HTMLElement;
|
||||||
|
// private formatStatePlugin: FormatStatePlugin;
|
||||||
|
private editorOptionPlugin: EditorOptionsPlugin;
|
||||||
|
// private eventViewPlugin: EventViewPlugin;
|
||||||
|
// private apiPlaygroundPlugin: ApiPlaygroundPlugin;
|
||||||
|
// private contentModelPanePlugin: ContentModelPanePlugin;
|
||||||
|
// private presetPlugin: PresetPlugin;
|
||||||
|
private ribbonPlugin: RibbonPlugin;
|
||||||
|
private snapshotPlugin: SnapshotPlugin;
|
||||||
|
private formatPainterPlugin: FormatPainterPlugin;
|
||||||
|
private samplePickerPlugin: SamplePickerPlugin;
|
||||||
|
private snapshots: Snapshots;
|
||||||
|
private imageEditPlugin: ImageEditPlugin;
|
||||||
|
// private markdownPanePlugin: MarkdownPanePlugin;
|
||||||
|
|
||||||
|
// protected sidePane = React.createRef<SidePane>();
|
||||||
|
protected updateContentPlugin: UpdateContentPlugin;
|
||||||
|
protected model: ContentModelDocument | null = null;
|
||||||
|
private knownColors: Record<string, Colors> = {};
|
||||||
|
protected themeMatch = window.matchMedia?.('(prefers-color-scheme: dark)');
|
||||||
|
|
||||||
|
static getInstance() {
|
||||||
|
return this.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
static readonly editorDivId = 'RoosterJsContentDiv';
|
||||||
|
|
||||||
|
constructor(props: {}) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
MainPane.instance = this;
|
||||||
|
this.updateContentPlugin = new UpdateContentPlugin(this.onUpdate);
|
||||||
|
|
||||||
|
this.snapshots = {
|
||||||
|
snapshots: [],
|
||||||
|
totalSize: 0,
|
||||||
|
currentIndex: -1,
|
||||||
|
autoCompleteIndex: -1,
|
||||||
|
maxSize: 1e7,
|
||||||
|
};
|
||||||
|
|
||||||
|
// this.formatStatePlugin = new FormatStatePlugin();
|
||||||
|
this.editorOptionPlugin = new EditorOptionsPlugin();
|
||||||
|
// this.eventViewPlugin = new EventViewPlugin();
|
||||||
|
// this.apiPlaygroundPlugin = new ApiPlaygroundPlugin();
|
||||||
|
this.snapshotPlugin = new SnapshotPlugin(this.snapshots);
|
||||||
|
// this.contentModelPanePlugin = new ContentModelPanePlugin();
|
||||||
|
// this.presetPlugin = new PresetPlugin();
|
||||||
|
this.ribbonPlugin = createRibbonPlugin();
|
||||||
|
this.formatPainterPlugin = new FormatPainterPlugin();
|
||||||
|
this.samplePickerPlugin = new SamplePickerPlugin();
|
||||||
|
this.imageEditPlugin = new ImageEditPlugin();
|
||||||
|
// this.markdownPanePlugin = new MarkdownPanePlugin();
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
showSidePane: window.location.hash != '',
|
||||||
|
popoutWindow: null,
|
||||||
|
initState: this.editorOptionPlugin.getBuildInPluginState(),
|
||||||
|
scale: 1,
|
||||||
|
isDarkMode: this.themeMatch?.matches || false,
|
||||||
|
editorCreator: null,
|
||||||
|
isRtl: false,
|
||||||
|
tableBorderFormat: {
|
||||||
|
width: '1px',
|
||||||
|
style: 'solid',
|
||||||
|
color: '#ABABAB',
|
||||||
|
},
|
||||||
|
activeTab: 'all',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const theme = getTheme(this.state.isDarkMode);
|
||||||
|
return (
|
||||||
|
<ThemeProvider applyTo="body" theme={theme} className={'styles.mainPane'}>
|
||||||
|
{/* {this.renderTitleBar()} */}
|
||||||
|
{!this.state.popoutWindow && this.renderTabs()}
|
||||||
|
{!this.state.popoutWindow && this.renderRibbon()}
|
||||||
|
<div className={''}>
|
||||||
|
{this.state.popoutWindow ? this.renderPopout() : this.renderEditor()}
|
||||||
|
</div>
|
||||||
|
</ThemeProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.themeMatch?.addEventListener('change', this.onThemeChange);
|
||||||
|
this.resetEditor();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.themeMatch?.removeEventListener('change', this.onThemeChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
// popout() {
|
||||||
|
// this.updateContentPlugin.update();
|
||||||
|
|
||||||
|
// const win = window.open(POPOUT_URL, POPOUT_TARGET, POPOUT_FEATURES);
|
||||||
|
// 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();
|
||||||
|
|
||||||
|
// unregisterWindowForCss(win);
|
||||||
|
// this.setState({ popoutWindow: null });
|
||||||
|
// });
|
||||||
|
|
||||||
|
// registerWindowForCss(win);
|
||||||
|
|
||||||
|
// this.popoutRoot = win.document.getElementById(PopoutRoot);
|
||||||
|
// this.setState({
|
||||||
|
// popoutWindow: win,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
resetEditorPlugin(pluginState: OptionState) {
|
||||||
|
this.updateContentPlugin.update();
|
||||||
|
this.setState({
|
||||||
|
initState: pluginState,
|
||||||
|
});
|
||||||
|
this.resetEditor();
|
||||||
|
}
|
||||||
|
|
||||||
|
setScale(scale: number): void {
|
||||||
|
this.setState({
|
||||||
|
scale: scale,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getTableBorder(): Border {
|
||||||
|
return this.state.tableBorderFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTableBorderColor(color: string): void {
|
||||||
|
this.setState({
|
||||||
|
tableBorderFormat: { ...this.getTableBorder(), color },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setTableBorderWidth(width: string): void {
|
||||||
|
this.setState({
|
||||||
|
tableBorderFormat: { ...this.getTableBorder(), width },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setTableBorderStyle(style: string): void {
|
||||||
|
this.setState({
|
||||||
|
tableBorderFormat: { ...this.getTableBorder(), style },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleDarkMode(): void {
|
||||||
|
this.setState({
|
||||||
|
isDarkMode: !this.state.isDarkMode,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
changeRibbon(id: tabNames): void {
|
||||||
|
this.setState({
|
||||||
|
activeTab: id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setPreset(preset: ContentModelDocument) {
|
||||||
|
this.model = preset;
|
||||||
|
}
|
||||||
|
|
||||||
|
setPageDirection(isRtl: boolean): void {
|
||||||
|
this.setState({ isRtl: isRtl });
|
||||||
|
[window, this.state.popoutWindow].forEach(win => {
|
||||||
|
if (win) {
|
||||||
|
win.document.body.dir = isRtl ? 'rtl' : 'ltr';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// private renderTitleBar() {
|
||||||
|
// return <TitleBar className={'styles.noGrow'} />;
|
||||||
|
// }
|
||||||
|
|
||||||
|
private renderTabs() {
|
||||||
|
const tabs = getTabs();
|
||||||
|
const topRightButtons: RibbonButton<any>[] = [
|
||||||
|
undoButton,
|
||||||
|
redoButton,
|
||||||
|
// zoomButton,
|
||||||
|
// darkModeButton,
|
||||||
|
// exportContentButton,
|
||||||
|
];
|
||||||
|
// this.state.popoutWindow ? null : topRightButtons.push(popoutButton);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{ display: 'inline-flex', justifyContent: 'space-between', height: '30px' }}>
|
||||||
|
<Ribbon
|
||||||
|
buttons={tabs}
|
||||||
|
plugin={this.ribbonPlugin}
|
||||||
|
dir={this.state.isRtl ? 'rtl' : 'ltr'}></Ribbon>
|
||||||
|
<Ribbon buttons={topRightButtons} plugin={this.ribbonPlugin}></Ribbon>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
private renderRibbon() {
|
||||||
|
return (
|
||||||
|
<Ribbon
|
||||||
|
buttons={getButtons(
|
||||||
|
this.state.activeTab,
|
||||||
|
this.formatPainterPlugin,
|
||||||
|
this.imageEditPlugin
|
||||||
|
)}
|
||||||
|
plugin={this.ribbonPlugin}
|
||||||
|
dir={this.state.isRtl ? 'rtl' : 'ltr'}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// private renderSidePane(fullWidth: boolean) {
|
||||||
|
// return (
|
||||||
|
// <SidePane
|
||||||
|
// ref={this.sidePane}
|
||||||
|
// plugins={this.getSidePanePlugins()}
|
||||||
|
// className={`main-pane ${'styles.sidePane'} ${
|
||||||
|
// fullWidth ? styles.sidePaneFullWidth : ''
|
||||||
|
// }`}
|
||||||
|
// />
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
private resetEditor() {
|
||||||
|
this.setState({
|
||||||
|
editorCreator: (div: HTMLDivElement, options: EditorOptions) => {
|
||||||
|
return new Editor(div, options);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderEditor() {
|
||||||
|
// Set preset if found
|
||||||
|
const search = new URLSearchParams(document.location.search);
|
||||||
|
const hasPreset = search.get('preset');
|
||||||
|
if (hasPreset) {
|
||||||
|
// this.setPreset(getPresetModelById(hasPreset)); // todo:
|
||||||
|
}
|
||||||
|
|
||||||
|
const editorStyles = {
|
||||||
|
transform: `scale(${this.state.scale})`,
|
||||||
|
transformOrigin: this.state.isRtl ? 'right top' : 'left top',
|
||||||
|
height: `calc(${100 / this.state.scale}%)`,
|
||||||
|
width: `calc(${100 / this.state.scale}%)`,
|
||||||
|
};
|
||||||
|
const plugins: EditorPlugin[] = [
|
||||||
|
this.ribbonPlugin,
|
||||||
|
this.formatPainterPlugin,
|
||||||
|
this.samplePickerPlugin,
|
||||||
|
...this.getToggleablePlugins(),
|
||||||
|
// this.contentModelPanePlugin.getInnerRibbonPlugin(),
|
||||||
|
this.updateContentPlugin,
|
||||||
|
];
|
||||||
|
|
||||||
|
// if (this.state.showSidePane || this.state.popoutWindow) {
|
||||||
|
// plugins.push(...this.getSidePanePlugins());
|
||||||
|
// }
|
||||||
|
|
||||||
|
this.updateContentPlugin.update();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={'styles.editorContainer'} id="EditorContainer">
|
||||||
|
<div style={editorStyles}>
|
||||||
|
{this.state.editorCreator && (
|
||||||
|
<Rooster
|
||||||
|
id={MainPane.editorDivId}
|
||||||
|
className={'styles.editor'}
|
||||||
|
plugins={plugins}
|
||||||
|
defaultSegmentFormat={this.state.initState.defaultFormat}
|
||||||
|
inDarkMode={this.state.isDarkMode}
|
||||||
|
getDarkColor={getDarkColor}
|
||||||
|
snapshots={this.snapshotPlugin.getSnapshots()}
|
||||||
|
trustedHTMLHandler={trustedHTMLHandler}
|
||||||
|
initialModel={this.model}
|
||||||
|
editorCreator={this.state.editorCreator}
|
||||||
|
dir={this.state.isRtl ? 'rtl' : 'ltr'}
|
||||||
|
knownColors={this.knownColors}
|
||||||
|
disableCache={this.state.initState.disableCache}
|
||||||
|
announcerStringGetter={getAnnouncingString}
|
||||||
|
experimentalFeatures={Array.from(
|
||||||
|
this.state.initState.experimentalFeatures
|
||||||
|
)}
|
||||||
|
defaultDomToModelOptions={defaultDomToModelOption}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderMainPane() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{this.renderEditor()}
|
||||||
|
{this.state.showSidePane ? (
|
||||||
|
<>
|
||||||
|
<div className={'styles.resizer'} onMouseDown={this.onMouseDown} />
|
||||||
|
{/* {this.renderSidePane(false)} */}
|
||||||
|
{/* {this.renderSidePaneButton()} */}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
// this.renderSidePaneButton()
|
||||||
|
null
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// private renderSidePaneButton() {
|
||||||
|
// return (
|
||||||
|
// <button
|
||||||
|
// className={`side-pane-toggle ${this.state.showSidePane ? 'open' : 'close'} ${
|
||||||
|
// styles.showSidePane
|
||||||
|
// }`}
|
||||||
|
// onClick={this.state.showSidePane ? this.onHideSidePane : this.onShowSidePane}>
|
||||||
|
// <div>{this.state.showSidePane ? 'Hide side pane' : 'Show side pane'}</div>
|
||||||
|
// </button>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
private renderPopout() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* {this.renderSidePane(true )} */}
|
||||||
|
{ReactDOM.createPortal(
|
||||||
|
<WindowProvider window={this.state.popoutWindow}>
|
||||||
|
<ThemeProvider applyTo="body" theme={getTheme(this.state.isDarkMode)}>
|
||||||
|
<div className={'styles.mainPane'}>
|
||||||
|
{this.renderTabs()}
|
||||||
|
{this.renderRibbon()}
|
||||||
|
<div className={'styles.body'}>{this.renderEditor()}</div>
|
||||||
|
</div>
|
||||||
|
</ThemeProvider>
|
||||||
|
</WindowProvider>,
|
||||||
|
this.popoutRoot
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onMouseDown = (e: React.MouseEvent<EventTarget>) => {
|
||||||
|
document.addEventListener('mousemove', this.onMouseMove, true);
|
||||||
|
document.addEventListener('mouseup', this.onMouseUp, true);
|
||||||
|
document.body.style.userSelect = 'none';
|
||||||
|
this.mouseX = e.pageX;
|
||||||
|
};
|
||||||
|
|
||||||
|
private onMouseMove = (e: MouseEvent) => {
|
||||||
|
// this.sidePane.current.changeWidth(this.mouseX - e.pageX);
|
||||||
|
this.mouseX = e.pageX;
|
||||||
|
};
|
||||||
|
|
||||||
|
private onMouseUp = (e: MouseEvent) => {
|
||||||
|
document.removeEventListener('mousemove', this.onMouseMove, true);
|
||||||
|
document.removeEventListener('mouseup', this.onMouseUp, true);
|
||||||
|
document.body.style.userSelect = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
private onUpdate = (model: ContentModelDocument) => {
|
||||||
|
this.model = model;
|
||||||
|
};
|
||||||
|
|
||||||
|
private onShowSidePane = () => {
|
||||||
|
this.setState({
|
||||||
|
showSidePane: true,
|
||||||
|
});
|
||||||
|
this.resetEditor();
|
||||||
|
};
|
||||||
|
|
||||||
|
private onHideSidePane = () => {
|
||||||
|
this.setState({
|
||||||
|
showSidePane: false,
|
||||||
|
});
|
||||||
|
this.resetEditor();
|
||||||
|
window.location.hash = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
private onThemeChange = () => {
|
||||||
|
this.setState({
|
||||||
|
isDarkMode: this.themeMatch?.matches || false,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// private getSidePanePlugins(): SidePanePlugin[] {
|
||||||
|
// return [
|
||||||
|
// this.formatStatePlugin,
|
||||||
|
// this.editorOptionPlugin,
|
||||||
|
// this.eventViewPlugin,
|
||||||
|
// this.apiPlaygroundPlugin,
|
||||||
|
// this.snapshotPlugin,
|
||||||
|
// this.contentModelPanePlugin,
|
||||||
|
// this.presetPlugin,
|
||||||
|
// this.markdownPanePlugin,
|
||||||
|
// ];
|
||||||
|
// }
|
||||||
|
|
||||||
|
private getToggleablePlugins(): EditorPlugin[] {
|
||||||
|
const {
|
||||||
|
pluginList,
|
||||||
|
allowExcelNoBorderTable,
|
||||||
|
listMenu,
|
||||||
|
tableMenu,
|
||||||
|
imageMenu,
|
||||||
|
watermarkText,
|
||||||
|
markdownOptions,
|
||||||
|
autoFormatOptions,
|
||||||
|
linkTitle,
|
||||||
|
customReplacements,
|
||||||
|
editPluginOptions,
|
||||||
|
} = this.state.initState;
|
||||||
|
return [
|
||||||
|
pluginList.autoFormat && new AutoFormatPlugin(autoFormatOptions),
|
||||||
|
pluginList.edit && new EditPlugin(editPluginOptions),
|
||||||
|
pluginList.paste && new PastePlugin(allowExcelNoBorderTable),
|
||||||
|
pluginList.shortcut && new ShortcutPlugin(),
|
||||||
|
pluginList.tableEdit && new TableEditPlugin(),
|
||||||
|
pluginList.watermark && new WatermarkPlugin(watermarkText),
|
||||||
|
pluginList.markdown && new MarkdownPlugin(markdownOptions),
|
||||||
|
pluginList.imageEditPlugin && this.imageEditPlugin,
|
||||||
|
pluginList.emoji && createEmojiPlugin(),
|
||||||
|
pluginList.pasteOption && createPasteOptionPlugin(),
|
||||||
|
pluginList.sampleEntity && new SampleEntityPlugin(),
|
||||||
|
pluginList.contextMenu && createContextMenuPlugin(),
|
||||||
|
pluginList.contextMenu && listMenu && createListEditMenuProvider(),
|
||||||
|
pluginList.contextMenu && tableMenu && createTableEditMenuProvider(),
|
||||||
|
pluginList.contextMenu &&
|
||||||
|
imageMenu &&
|
||||||
|
createImageEditMenuProvider(this.imageEditPlugin),
|
||||||
|
pluginList.hyperlink &&
|
||||||
|
new HyperlinkPlugin(
|
||||||
|
linkTitle?.indexOf(UrlPlaceholder) >= 0
|
||||||
|
? url => linkTitle.replace(UrlPlaceholder, url)
|
||||||
|
: linkTitle
|
||||||
|
),
|
||||||
|
pluginList.customReplace && new CustomReplacePlugin(customReplacements),
|
||||||
|
pluginList.hiddenProperty &&
|
||||||
|
new HiddenPropertyPlugin({
|
||||||
|
undeletableLinkChecker: undeletableLinkChecker,
|
||||||
|
}),
|
||||||
|
].filter(x => !!x);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const AnnounceStringMap: Record<KnownAnnounceStrings, string> = {
|
||||||
|
announceListItemBullet: 'Auto corrected Bullet',
|
||||||
|
announceListItemNumbering: 'Auto corrected {0}',
|
||||||
|
announceOnFocusLastCell: 'Warning, pressing tab here adds an extra row.',
|
||||||
|
};
|
||||||
|
|
||||||
|
function getAnnouncingString(key: KnownAnnounceStrings) {
|
||||||
|
return AnnounceStringMap[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mount(parent: HTMLElement) {
|
||||||
|
ReactDOM.render(<MainPane />, parent);
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
|
||||||
|
/* Basic styling for the editor container */
|
||||||
|
.rooster-editor-container {
|
||||||
|
border: 1px solid #d9d9d9; /* Ant Design border color */
|
||||||
|
border-radius: 4px; /* Ant Design border radius */
|
||||||
|
min-height: 200px;
|
||||||
|
padding: 8px; /* Ant Design default padding for inputs */
|
||||||
|
box-sizing: border-box; /* Ensures padding is included in width */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style the actual editor div (RoosterJs injects content here) */
|
||||||
|
.rooster-editor {
|
||||||
|
min-height: 180px; /* Adjust as needed */
|
||||||
|
outline: none; /* Remove default focus outline */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Optional: Basic styling for the content within the editor */
|
||||||
|
.rooster-editor p {
|
||||||
|
margin-bottom: 8px; /* Space out paragraphs */
|
||||||
|
}
|
||||||
|
|
||||||
|
.rooster-editor ul,
|
||||||
|
.rooster-editor ol {
|
||||||
|
padding-left: 20px; /* Indent lists */
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
import type { EditorPlugin } from 'roosterjs-content-model-types';
|
||||||
|
|
||||||
|
export interface SidePanePlugin extends EditorPlugin {
|
||||||
|
getTitle: () => string;
|
||||||
|
renderSidePane: (updateHash: (pluginName?: string, path?: string[]) => void) => JSX.Element;
|
||||||
|
setHashPath?: (path: string[]) => void;
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
export interface CodeProps {
|
||||||
|
code: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Code extends React.Component<CodeProps, {}> {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<pre>{this.props.code}</pre>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,146 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { ContentModelSegmentFormat } from 'roosterjs-content-model-types';
|
||||||
|
import { getObjectKeys } from 'roosterjs-content-model-dom';
|
||||||
|
import { OptionState } from './OptionState';
|
||||||
|
|
||||||
|
type ToggleFormatId = 'fontWeight' | 'italic' | 'underline';
|
||||||
|
type SelectFormatId = 'textColor' | 'backgroundColor' | 'fontFamily' | 'fontSize';
|
||||||
|
|
||||||
|
const styles = require('./OptionsPane.scss');
|
||||||
|
const NOT_SET = 'NotSet';
|
||||||
|
|
||||||
|
export interface DefaultFormatProps {
|
||||||
|
state: ContentModelSegmentFormat;
|
||||||
|
resetState: (callback: (state: OptionState) => void, resetEditor: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DefaultFormatPane extends React.Component<DefaultFormatProps, {}> {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
{this.renderFormatItem('fontWeight', 'Bold')}
|
||||||
|
{this.renderFormatItem('italic', 'Italic')}
|
||||||
|
{this.renderFormatItem('underline', 'Underline')}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
{this.renderSelectItem('fontFamily', 'Font family: ', {
|
||||||
|
[NOT_SET]: 'Not Set',
|
||||||
|
Arial: 'Arial',
|
||||||
|
Calibri: 'Calibri',
|
||||||
|
'Courier New': 'Courier New',
|
||||||
|
Tahoma: 'Tahoma',
|
||||||
|
'Times New Roman': 'Times New Roman',
|
||||||
|
})}
|
||||||
|
{this.renderSelectItem('fontSize', 'Font size: ', {
|
||||||
|
[NOT_SET]: 'Not Set',
|
||||||
|
'8pt': '8',
|
||||||
|
'10pt': '10',
|
||||||
|
'11pt': '11',
|
||||||
|
'12pt': '12',
|
||||||
|
'16pt': '16',
|
||||||
|
'20pt': '20',
|
||||||
|
'36pt': '36',
|
||||||
|
'72pt': '72',
|
||||||
|
})}
|
||||||
|
{this.renderSelectItem('textColor', 'Text color: ', {
|
||||||
|
[NOT_SET]: 'Not Set',
|
||||||
|
'#757b80': 'Gray',
|
||||||
|
'#bd1398': 'Violet',
|
||||||
|
'#7232ad': 'Purple',
|
||||||
|
'#006fc9': 'Blue',
|
||||||
|
'#4ba524': 'Green',
|
||||||
|
'#e2c501': 'Yellow',
|
||||||
|
'#d05c12': 'Orange',
|
||||||
|
'#ff0000': 'Red',
|
||||||
|
'#ffffff': 'White',
|
||||||
|
'#000000': 'Black',
|
||||||
|
})}
|
||||||
|
{this.renderSelectItem('backgroundColor', 'Back color: ', {
|
||||||
|
[NOT_SET]: 'Not Set',
|
||||||
|
'#ffff00': 'Yellow',
|
||||||
|
'#00ff00': 'Green',
|
||||||
|
'#00ffff': 'Cyan',
|
||||||
|
'#ff00ff': 'Purple',
|
||||||
|
'#0000ff': 'Blue',
|
||||||
|
'#ff0000': 'Red',
|
||||||
|
'#bebebe': 'Gray',
|
||||||
|
'#666666': 'Dark Gray',
|
||||||
|
'#ffffff': 'White',
|
||||||
|
'#000000': 'Black',
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderFormatItem(id: ToggleFormatId, text: string): JSX.Element {
|
||||||
|
let checked = !!this.props.state[id];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr>
|
||||||
|
<td className={styles.checkboxColumn}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id={id}
|
||||||
|
checked={checked}
|
||||||
|
onChange={() => this.onFormatClick(id)}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div>
|
||||||
|
<label htmlFor={id}>{text}</label>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderSelectItem(
|
||||||
|
id: SelectFormatId,
|
||||||
|
label: string,
|
||||||
|
items: { [key: string]: string }
|
||||||
|
): JSX.Element {
|
||||||
|
return (
|
||||||
|
<tr>
|
||||||
|
<td className={styles.defaultFormatLabel}>{label}</td>
|
||||||
|
<td>
|
||||||
|
<select
|
||||||
|
id={id}
|
||||||
|
onChange={() => this.onSelectChanged(id)}
|
||||||
|
defaultValue={(this.props.state[id] || NOT_SET) as string}>
|
||||||
|
{getObjectKeys(items).map(key => (
|
||||||
|
<option value={key} key={key}>
|
||||||
|
{items[key]}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onFormatClick = (id: ToggleFormatId) => {
|
||||||
|
this.props.resetState(state => {
|
||||||
|
let checkbox = document.getElementById(id) as HTMLInputElement;
|
||||||
|
|
||||||
|
if (id == 'fontWeight') {
|
||||||
|
state.defaultFormat.fontWeight = checkbox.checked ? 'bold' : undefined;
|
||||||
|
} else {
|
||||||
|
state.defaultFormat[id] = checkbox.checked;
|
||||||
|
}
|
||||||
|
}, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
private onSelectChanged = (id: SelectFormatId) => {
|
||||||
|
this.props.resetState(state => {
|
||||||
|
let value = (document.getElementById(id) as HTMLSelectElement).value;
|
||||||
|
|
||||||
|
state.defaultFormat[id] = value == NOT_SET ? undefined : value;
|
||||||
|
}, true);
|
||||||
|
};
|
||||||
|
}
|
@ -0,0 +1,88 @@
|
|||||||
|
import { emojiReplacements } from './getReplacements';
|
||||||
|
import { ExperimentalFeature } from 'roosterjs-content-model-types';
|
||||||
|
import { OptionPaneProps, OptionState, UrlPlaceholder } from './OptionState';
|
||||||
|
import { OptionsPane } from './OptionsPane';
|
||||||
|
import { SidePaneElementProps } from '../SidePaneElement';
|
||||||
|
import { SidePanePluginImpl } from '../SidePanePluginImpl';
|
||||||
|
|
||||||
|
const initialState: OptionState = {
|
||||||
|
pluginList: {
|
||||||
|
autoFormat: true,
|
||||||
|
edit: true,
|
||||||
|
paste: true,
|
||||||
|
shortcut: true,
|
||||||
|
tableEdit: true,
|
||||||
|
contextMenu: true,
|
||||||
|
watermark: true,
|
||||||
|
emoji: true,
|
||||||
|
pasteOption: true,
|
||||||
|
sampleEntity: true,
|
||||||
|
markdown: true,
|
||||||
|
imageEditPlugin: true,
|
||||||
|
hyperlink: true,
|
||||||
|
customReplace: true,
|
||||||
|
hiddenProperty: true,
|
||||||
|
},
|
||||||
|
defaultFormat: {
|
||||||
|
fontFamily: 'Calibri',
|
||||||
|
fontSize: '11pt',
|
||||||
|
textColor: '#000000',
|
||||||
|
},
|
||||||
|
linkTitle: 'Ctrl+Click to follow the link:' + UrlPlaceholder,
|
||||||
|
watermarkText: 'Type content here ...',
|
||||||
|
forcePreserveRatio: false,
|
||||||
|
isRtl: false,
|
||||||
|
disableCache: false,
|
||||||
|
tableFeaturesContainerSelector: '#' + 'EditorContainer',
|
||||||
|
allowExcelNoBorderTable: false,
|
||||||
|
imageMenu: true,
|
||||||
|
tableMenu: true,
|
||||||
|
listMenu: true,
|
||||||
|
autoFormatOptions: {
|
||||||
|
autoBullet: true,
|
||||||
|
autoLink: true,
|
||||||
|
autoNumbering: true,
|
||||||
|
autoUnlink: false,
|
||||||
|
autoHyphen: true,
|
||||||
|
autoFraction: true,
|
||||||
|
autoOrdinals: true,
|
||||||
|
autoMailto: true,
|
||||||
|
autoTel: true,
|
||||||
|
removeListMargins: false,
|
||||||
|
autoHorizontalLine: true,
|
||||||
|
},
|
||||||
|
markdownOptions: {
|
||||||
|
bold: true,
|
||||||
|
italic: true,
|
||||||
|
strikethrough: true,
|
||||||
|
codeFormat: {},
|
||||||
|
},
|
||||||
|
editPluginOptions: {
|
||||||
|
handleTabKey: true,
|
||||||
|
},
|
||||||
|
customReplacements: emojiReplacements,
|
||||||
|
experimentalFeatures: new Set<ExperimentalFeature>([
|
||||||
|
'PersistCache',
|
||||||
|
'HandleEnterKey',
|
||||||
|
'CustomCopyCut',
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
|
||||||
|
export class EditorOptionsPlugin extends SidePanePluginImpl<OptionsPane, OptionPaneProps> {
|
||||||
|
constructor() {
|
||||||
|
super(OptionsPane, 'options', 'Editor Options');
|
||||||
|
}
|
||||||
|
|
||||||
|
getBuildInPluginState(): OptionState {
|
||||||
|
let result: OptionState;
|
||||||
|
this.getComponent(component => (result = component.getState()));
|
||||||
|
return result || initialState;
|
||||||
|
}
|
||||||
|
|
||||||
|
getComponentProps(base: SidePaneElementProps) {
|
||||||
|
return {
|
||||||
|
...initialState,
|
||||||
|
...base,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,48 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { ExperimentalFeature } from 'roosterjs-content-model-types';
|
||||||
|
import { OptionState } from './OptionState';
|
||||||
|
|
||||||
|
export interface DefaultFormatProps {
|
||||||
|
state: OptionState;
|
||||||
|
resetState: (callback: (state: OptionState) => void, resetEditor: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ExperimentalFeatures extends React.Component<DefaultFormatProps, {}> {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{this.renderFeature('PersistCache')}
|
||||||
|
{this.renderFeature('HandleEnterKey')}
|
||||||
|
{this.renderFeature('CustomCopyCut')}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderFeature(featureName: ExperimentalFeature): JSX.Element {
|
||||||
|
let checked = this.props.state.experimentalFeatures.has(featureName);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id={featureName}
|
||||||
|
checked={checked}
|
||||||
|
onChange={() => this.onFeatureClick(featureName)}
|
||||||
|
/>
|
||||||
|
<label htmlFor={featureName}>{featureName}</label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onFeatureClick = (featureName: ExperimentalFeature) => {
|
||||||
|
this.props.resetState(state => {
|
||||||
|
let checkbox = document.getElementById(featureName) as HTMLInputElement;
|
||||||
|
|
||||||
|
if (checkbox.checked) {
|
||||||
|
state.experimentalFeatures.add(featureName);
|
||||||
|
} else {
|
||||||
|
state.experimentalFeatures.delete(featureName);
|
||||||
|
}
|
||||||
|
}, true);
|
||||||
|
};
|
||||||
|
}
|
@ -0,0 +1,56 @@
|
|||||||
|
import {
|
||||||
|
AutoFormatOptions,
|
||||||
|
CustomReplace,
|
||||||
|
EditOptions,
|
||||||
|
MarkdownOptions,
|
||||||
|
} from 'roosterjs-content-model-plugins';
|
||||||
|
import type { SidePaneElementProps } from '../SidePaneElement';
|
||||||
|
import type { ContentModelSegmentFormat, ExperimentalFeature } from 'roosterjs-content-model-types';
|
||||||
|
|
||||||
|
export interface BuildInPluginList {
|
||||||
|
autoFormat: boolean;
|
||||||
|
edit: boolean;
|
||||||
|
paste: boolean;
|
||||||
|
shortcut: boolean;
|
||||||
|
tableEdit: boolean;
|
||||||
|
contextMenu: boolean;
|
||||||
|
watermark: boolean;
|
||||||
|
emoji: boolean;
|
||||||
|
pasteOption: boolean;
|
||||||
|
sampleEntity: boolean;
|
||||||
|
markdown: boolean;
|
||||||
|
hyperlink: boolean;
|
||||||
|
imageEditPlugin: boolean;
|
||||||
|
customReplace: boolean;
|
||||||
|
hiddenProperty: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OptionState {
|
||||||
|
pluginList: BuildInPluginList;
|
||||||
|
|
||||||
|
// New plugin options
|
||||||
|
allowExcelNoBorderTable: boolean;
|
||||||
|
listMenu: boolean;
|
||||||
|
tableMenu: boolean;
|
||||||
|
imageMenu: boolean;
|
||||||
|
watermarkText: string;
|
||||||
|
autoFormatOptions: AutoFormatOptions;
|
||||||
|
markdownOptions: MarkdownOptions;
|
||||||
|
customReplacements: CustomReplace[];
|
||||||
|
editPluginOptions: EditOptions;
|
||||||
|
|
||||||
|
// Legacy plugin options
|
||||||
|
defaultFormat: ContentModelSegmentFormat;
|
||||||
|
linkTitle: string;
|
||||||
|
forcePreserveRatio: boolean;
|
||||||
|
tableFeaturesContainerSelector: string;
|
||||||
|
|
||||||
|
// Editor options
|
||||||
|
isRtl: boolean;
|
||||||
|
disableCache: boolean;
|
||||||
|
experimentalFeatures: Set<ExperimentalFeature>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OptionPaneProps extends OptionState, SidePaneElementProps {}
|
||||||
|
|
||||||
|
export const UrlPlaceholder = '$url$';
|
@ -0,0 +1,7 @@
|
|||||||
|
.checkboxColumn {
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.defaultFormatLabel {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
@ -0,0 +1,187 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { Code } from './Code';
|
||||||
|
import { DefaultFormatPane } from './DefaultFormatPane';
|
||||||
|
import { EditorCode } from './codes/EditorCode';
|
||||||
|
import { ExperimentalFeatures } from './ExperimentalFeatures';
|
||||||
|
import { MainPane } from '../../mainPane/MainPane';
|
||||||
|
import { OptionPaneProps, OptionState } from './OptionState';
|
||||||
|
import { Plugins } from './Plugins';
|
||||||
|
|
||||||
|
const htmlStart =
|
||||||
|
'<html>\n' +
|
||||||
|
'<body>\n' +
|
||||||
|
'<div id="contentDiv" style="width: 800px; height: 400px; border: solid 1px black; overflow: auto"></div>\n';
|
||||||
|
const htmlButtons =
|
||||||
|
'<button id=buttonB><b>B</b></button>\n' +
|
||||||
|
'<button id=buttonI><i>I</i></button>\n' +
|
||||||
|
'<button id=buttonU><u>U</u></button>\n' +
|
||||||
|
'<button id=buttonBullet>Bullet</button>\n' +
|
||||||
|
'<button id=buttonNumbering>Numbering</button>\n' +
|
||||||
|
'<button id=buttonUndo>Undo</button>\n' +
|
||||||
|
'<button id=buttonRedo>Redo</button>\n' +
|
||||||
|
'<button id=buttonTable>Insert Table</button>\n' +
|
||||||
|
'<button id=buttonDark>Dark mode</button>\n';
|
||||||
|
'<button id=buttonDark>Dark Mode</button>\n';
|
||||||
|
const jsCode = '<script src="https://microsoft.github.io/roosterjs/rooster-min.js"></script>\n';
|
||||||
|
const htmlEnd = '</body>\n' + '</html>';
|
||||||
|
|
||||||
|
export class OptionsPane extends React.Component<OptionPaneProps, OptionState> {
|
||||||
|
private exportForm = React.createRef<HTMLFormElement>();
|
||||||
|
private exportData = React.createRef<HTMLInputElement>();
|
||||||
|
private rtl = React.createRef<HTMLInputElement>();
|
||||||
|
private disableCache = React.createRef<HTMLInputElement>();
|
||||||
|
|
||||||
|
constructor(props: OptionPaneProps) {
|
||||||
|
super(props);
|
||||||
|
this.state = { ...props };
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
const editorCode = new EditorCode(this.state);
|
||||||
|
const html = this.getHtml();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<details>
|
||||||
|
<summary>
|
||||||
|
<b>Default Format</b>
|
||||||
|
</summary>
|
||||||
|
<DefaultFormatPane
|
||||||
|
state={this.state.defaultFormat}
|
||||||
|
resetState={this.resetState}
|
||||||
|
/>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary>
|
||||||
|
<b>Plugins</b>
|
||||||
|
</summary>
|
||||||
|
<Plugins state={this.state} resetState={this.resetState} />
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary>
|
||||||
|
<b>Experimental features</b>
|
||||||
|
</summary>
|
||||||
|
<ExperimentalFeatures state={this.state} resetState={this.resetState} />
|
||||||
|
</details>
|
||||||
|
<div>
|
||||||
|
<br />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
id="pageRtl"
|
||||||
|
type="checkbox"
|
||||||
|
checked={this.state.isRtl}
|
||||||
|
onChange={this.onToggleDirection}
|
||||||
|
ref={this.rtl}
|
||||||
|
/>
|
||||||
|
<label htmlFor="pageRtl">Show controls from right to left</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
id="disableCache"
|
||||||
|
type="checkbox"
|
||||||
|
checked={this.state.disableCache}
|
||||||
|
onChange={this.onToggleCacheModel}
|
||||||
|
ref={this.disableCache}
|
||||||
|
/>
|
||||||
|
<label htmlFor="disableCache">Disable Content Model Cache</label>
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
<div>
|
||||||
|
<button onClick={this.onExportRoosterContentModel}>
|
||||||
|
Try roosterjs in CodePen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<br />
|
||||||
|
</div>
|
||||||
|
<details>
|
||||||
|
<summary>
|
||||||
|
<b>HTML Code:</b>
|
||||||
|
</summary>
|
||||||
|
<Code code={html} />
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary>
|
||||||
|
<b>Typescript Code:</b>
|
||||||
|
</summary>
|
||||||
|
<Code code={editorCode.getCode()} />
|
||||||
|
</details>
|
||||||
|
<form
|
||||||
|
ref={this.exportForm}
|
||||||
|
method="POST"
|
||||||
|
action="https://codepen.io/pen/define"
|
||||||
|
target="_blank">
|
||||||
|
<input name="data" type="hidden" ref={this.exportData} />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getState(): OptionState {
|
||||||
|
return { ...this.state };
|
||||||
|
}
|
||||||
|
|
||||||
|
private resetState = (callback: (state: OptionState) => void, resetEditor: boolean) => {
|
||||||
|
let state: OptionState = {
|
||||||
|
linkTitle: this.state.linkTitle,
|
||||||
|
watermarkText: this.state.watermarkText,
|
||||||
|
pluginList: { ...this.state.pluginList },
|
||||||
|
defaultFormat: { ...this.state.defaultFormat },
|
||||||
|
forcePreserveRatio: this.state.forcePreserveRatio,
|
||||||
|
|
||||||
|
isRtl: this.state.isRtl,
|
||||||
|
disableCache: this.state.disableCache,
|
||||||
|
tableFeaturesContainerSelector: this.state.tableFeaturesContainerSelector,
|
||||||
|
allowExcelNoBorderTable: this.state.allowExcelNoBorderTable,
|
||||||
|
listMenu: this.state.listMenu,
|
||||||
|
tableMenu: this.state.tableMenu,
|
||||||
|
imageMenu: this.state.imageMenu,
|
||||||
|
autoFormatOptions: { ...this.state.autoFormatOptions },
|
||||||
|
markdownOptions: { ...this.state.markdownOptions },
|
||||||
|
customReplacements: this.state.customReplacements,
|
||||||
|
experimentalFeatures: this.state.experimentalFeatures,
|
||||||
|
editPluginOptions: { ...this.state.editPluginOptions },
|
||||||
|
};
|
||||||
|
|
||||||
|
if (callback) {
|
||||||
|
callback(state);
|
||||||
|
this.setState(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resetEditor) {
|
||||||
|
MainPane.getInstance().resetEditorPlugin(state);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private onExportRoosterContentModel = () => {
|
||||||
|
let editor = new EditorCode(this.state);
|
||||||
|
let code = editor.getCode();
|
||||||
|
let json = {
|
||||||
|
title: 'RoosterJs',
|
||||||
|
html: this.getHtml(),
|
||||||
|
head: '',
|
||||||
|
js: code,
|
||||||
|
js_pre_processor: 'typescript',
|
||||||
|
};
|
||||||
|
this.exportData.current.value = JSON.stringify(json);
|
||||||
|
this.exportForm.current.submit();
|
||||||
|
};
|
||||||
|
|
||||||
|
private onToggleDirection = () => {
|
||||||
|
let isRtl = this.rtl.current.checked;
|
||||||
|
this.setState({
|
||||||
|
isRtl: isRtl,
|
||||||
|
});
|
||||||
|
MainPane.getInstance().setPageDirection(isRtl);
|
||||||
|
};
|
||||||
|
|
||||||
|
private onToggleCacheModel = () => {
|
||||||
|
this.resetState(state => {
|
||||||
|
state.disableCache = this.disableCache.current.checked;
|
||||||
|
}, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
private getHtml() {
|
||||||
|
return `${htmlStart}${htmlButtons}${jsCode}${htmlEnd}`;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,323 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { UrlPlaceholder } from './OptionState';
|
||||||
|
import type { BuildInPluginList, OptionState } from './OptionState';
|
||||||
|
|
||||||
|
const styles = require('./OptionsPane.scss');
|
||||||
|
|
||||||
|
export interface PluginsProps {
|
||||||
|
state: OptionState;
|
||||||
|
resetState: (callback: (state: OptionState) => void, resetEditor: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class PluginsBase<PluginKey extends keyof BuildInPluginList> extends React.Component<
|
||||||
|
PluginsProps,
|
||||||
|
{}
|
||||||
|
> {
|
||||||
|
abstract render(): JSX.Element;
|
||||||
|
|
||||||
|
protected renderPluginItem(
|
||||||
|
id: PluginKey,
|
||||||
|
text: string,
|
||||||
|
moreOptions?: JSX.Element
|
||||||
|
): JSX.Element {
|
||||||
|
const checked = this.props.state.pluginList[id];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr>
|
||||||
|
<td className={styles.checkboxColumn}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id={id}
|
||||||
|
checked={checked}
|
||||||
|
onChange={() => this.onPluginClick(id)}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div>
|
||||||
|
<label htmlFor={id}>{text}</label>
|
||||||
|
</div>
|
||||||
|
{checked && moreOptions}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected renderInputBox(
|
||||||
|
label: string,
|
||||||
|
ref: React.RefObject<HTMLInputElement>,
|
||||||
|
value: string,
|
||||||
|
placeholder: string,
|
||||||
|
onChange: (state: OptionState, value: string) => void
|
||||||
|
): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{label}
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
ref={ref}
|
||||||
|
value={value}
|
||||||
|
placeholder={placeholder}
|
||||||
|
onChange={() =>
|
||||||
|
this.props.resetState(state => onChange(state, ref.current.value), false)
|
||||||
|
}
|
||||||
|
onBlur={() => this.props.resetState(null, true)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected renderCheckBox(
|
||||||
|
label: string,
|
||||||
|
ref: React.RefObject<HTMLInputElement>,
|
||||||
|
value: boolean,
|
||||||
|
onChange: (state: OptionState, value: boolean) => void
|
||||||
|
): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
ref={ref}
|
||||||
|
checked={value}
|
||||||
|
onChange={() =>
|
||||||
|
this.props.resetState(state => onChange(state, ref.current.checked), true)
|
||||||
|
}
|
||||||
|
onBlur={() => this.props.resetState(null, true)}
|
||||||
|
/>
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onPluginClick = (id: PluginKey) => {
|
||||||
|
this.props.resetState(state => {
|
||||||
|
let checkbox = document.getElementById(id) as HTMLInputElement;
|
||||||
|
state.pluginList[id] = checkbox.checked;
|
||||||
|
}, true);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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>();
|
||||||
|
private watermarkText = React.createRef<HTMLInputElement>();
|
||||||
|
private autoBullet = React.createRef<HTMLInputElement>();
|
||||||
|
private autoNumbering = React.createRef<HTMLInputElement>();
|
||||||
|
private autoLink = React.createRef<HTMLInputElement>();
|
||||||
|
private autoUnlink = React.createRef<HTMLInputElement>();
|
||||||
|
private autoHyphen = React.createRef<HTMLInputElement>();
|
||||||
|
private autoFraction = React.createRef<HTMLInputElement>();
|
||||||
|
private autoOrdinals = React.createRef<HTMLInputElement>();
|
||||||
|
private autoTel = React.createRef<HTMLInputElement>();
|
||||||
|
private autoMailto = React.createRef<HTMLInputElement>();
|
||||||
|
private removeListMargins = React.createRef<HTMLInputElement>();
|
||||||
|
private horizontalLine = React.createRef<HTMLInputElement>();
|
||||||
|
private markdownBold = React.createRef<HTMLInputElement>();
|
||||||
|
private markdownItalic = React.createRef<HTMLInputElement>();
|
||||||
|
private markdownStrikethrough = React.createRef<HTMLInputElement>();
|
||||||
|
private markdownCode = React.createRef<HTMLInputElement>();
|
||||||
|
private linkTitle = React.createRef<HTMLInputElement>();
|
||||||
|
|
||||||
|
render(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
{this.renderPluginItem(
|
||||||
|
'autoFormat',
|
||||||
|
'AutoFormat',
|
||||||
|
<>
|
||||||
|
{this.renderCheckBox(
|
||||||
|
'Bullet',
|
||||||
|
this.autoBullet,
|
||||||
|
this.props.state.autoFormatOptions.autoBullet,
|
||||||
|
(state, value) => (state.autoFormatOptions.autoBullet = value)
|
||||||
|
)}
|
||||||
|
{this.renderCheckBox(
|
||||||
|
'Numbering',
|
||||||
|
this.autoNumbering,
|
||||||
|
this.props.state.autoFormatOptions.autoNumbering,
|
||||||
|
(state, value) => (state.autoFormatOptions.autoNumbering = value)
|
||||||
|
)}
|
||||||
|
{this.renderCheckBox(
|
||||||
|
'Link',
|
||||||
|
this.autoLink,
|
||||||
|
this.props.state.autoFormatOptions.autoLink,
|
||||||
|
(state, value) => (state.autoFormatOptions.autoLink = value)
|
||||||
|
)}
|
||||||
|
{this.renderCheckBox(
|
||||||
|
'Unlink',
|
||||||
|
this.autoUnlink,
|
||||||
|
this.props.state.autoFormatOptions.autoUnlink,
|
||||||
|
(state, value) => (state.autoFormatOptions.autoUnlink = value)
|
||||||
|
)}
|
||||||
|
{this.renderCheckBox(
|
||||||
|
'Hyphen',
|
||||||
|
this.autoHyphen,
|
||||||
|
this.props.state.autoFormatOptions.autoHyphen,
|
||||||
|
(state, value) => (state.autoFormatOptions.autoHyphen = value)
|
||||||
|
)}
|
||||||
|
{this.renderCheckBox(
|
||||||
|
'Fraction',
|
||||||
|
this.autoFraction,
|
||||||
|
this.props.state.autoFormatOptions.autoFraction,
|
||||||
|
(state, value) => (state.autoFormatOptions.autoFraction = value)
|
||||||
|
)}
|
||||||
|
{this.renderCheckBox(
|
||||||
|
'Ordinals',
|
||||||
|
this.autoOrdinals,
|
||||||
|
this.props.state.autoFormatOptions.autoOrdinals,
|
||||||
|
(state, value) => (state.autoFormatOptions.autoOrdinals = value)
|
||||||
|
)}
|
||||||
|
{this.renderCheckBox(
|
||||||
|
'Telephone',
|
||||||
|
this.autoTel,
|
||||||
|
this.props.state.autoFormatOptions.autoTel,
|
||||||
|
(state, value) => (state.autoFormatOptions.autoTel = value)
|
||||||
|
)}
|
||||||
|
{this.renderCheckBox(
|
||||||
|
'Email',
|
||||||
|
this.autoMailto,
|
||||||
|
this.props.state.autoFormatOptions.autoMailto,
|
||||||
|
(state, value) => (state.autoFormatOptions.autoMailto = value)
|
||||||
|
)}
|
||||||
|
{this.renderCheckBox(
|
||||||
|
'Remove List Margins',
|
||||||
|
this.removeListMargins,
|
||||||
|
this.props.state.autoFormatOptions.removeListMargins,
|
||||||
|
(state, value) =>
|
||||||
|
(state.autoFormatOptions.removeListMargins = value)
|
||||||
|
)}
|
||||||
|
{this.renderCheckBox(
|
||||||
|
'Horizontal Line',
|
||||||
|
this.horizontalLine,
|
||||||
|
this.props.state.autoFormatOptions.autoHorizontalLine,
|
||||||
|
(state, value) =>
|
||||||
|
(state.autoFormatOptions.autoHorizontalLine = value)
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{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 Enter Key',
|
||||||
|
this.handleEnterKey,
|
||||||
|
this.props.state.editPluginOptions.shouldHandleEnterKey as boolean,
|
||||||
|
(state, value) =>
|
||||||
|
(state.editPluginOptions.shouldHandleEnterKey = value)
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{this.renderPluginItem(
|
||||||
|
'paste',
|
||||||
|
'Paste',
|
||||||
|
this.renderCheckBox(
|
||||||
|
'Do not add border for Excel table',
|
||||||
|
this.allowExcelNoBorderTable,
|
||||||
|
this.props.state.allowExcelNoBorderTable,
|
||||||
|
(state, value) => (state.allowExcelNoBorderTable = value)
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
{this.renderPluginItem('shortcut', 'Shortcut')}
|
||||||
|
{this.renderPluginItem('tableEdit', 'TableEdit')}
|
||||||
|
{this.renderPluginItem(
|
||||||
|
'contextMenu',
|
||||||
|
'ContextMenu',
|
||||||
|
<>
|
||||||
|
{this.renderCheckBox(
|
||||||
|
'List menu',
|
||||||
|
this.listMenu,
|
||||||
|
this.props.state.listMenu,
|
||||||
|
(state, value) => (state.listMenu = value)
|
||||||
|
)}
|
||||||
|
{this.renderCheckBox(
|
||||||
|
'Table menu',
|
||||||
|
this.tableMenu,
|
||||||
|
this.props.state.tableMenu,
|
||||||
|
(state, value) => (state.tableMenu = value)
|
||||||
|
)}
|
||||||
|
{this.renderCheckBox(
|
||||||
|
'Image menu',
|
||||||
|
this.imageMenu,
|
||||||
|
this.props.state.imageMenu,
|
||||||
|
(state, value) => (state.imageMenu = value)
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{this.renderPluginItem(
|
||||||
|
'watermark',
|
||||||
|
'Watermark Plugin',
|
||||||
|
this.renderInputBox(
|
||||||
|
'Watermark text: ',
|
||||||
|
this.watermarkText,
|
||||||
|
this.props.state.watermarkText,
|
||||||
|
'',
|
||||||
|
(state, value) => (state.watermarkText = value)
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
{this.renderPluginItem('emoji', 'Emoji')}
|
||||||
|
{this.renderPluginItem('pasteOption', 'PasteOptions')}
|
||||||
|
{this.renderPluginItem('sampleEntity', 'SampleEntity')}
|
||||||
|
{this.renderPluginItem(
|
||||||
|
'markdown',
|
||||||
|
'Markdown',
|
||||||
|
<>
|
||||||
|
{this.renderCheckBox(
|
||||||
|
'Bold',
|
||||||
|
this.markdownBold,
|
||||||
|
this.props.state.markdownOptions.bold,
|
||||||
|
(state, value) => (state.markdownOptions.bold = value)
|
||||||
|
)}
|
||||||
|
{this.renderCheckBox(
|
||||||
|
'Italic',
|
||||||
|
this.markdownItalic,
|
||||||
|
this.props.state.markdownOptions.italic,
|
||||||
|
(state, value) => (state.markdownOptions.italic = value)
|
||||||
|
)}
|
||||||
|
{this.renderCheckBox(
|
||||||
|
'Strikethrough',
|
||||||
|
this.markdownStrikethrough,
|
||||||
|
this.props.state.markdownOptions.strikethrough,
|
||||||
|
(state, value) => (state.markdownOptions.strikethrough = value)
|
||||||
|
)}
|
||||||
|
|
||||||
|
{this.renderCheckBox(
|
||||||
|
'Code',
|
||||||
|
this.markdownCode,
|
||||||
|
this.props.state.markdownOptions.codeFormat !== undefined,
|
||||||
|
(state, value) =>
|
||||||
|
value
|
||||||
|
? (state.markdownOptions.codeFormat = {})
|
||||||
|
: (state.markdownOptions.codeFormat = undefined)
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{this.renderPluginItem(
|
||||||
|
'hyperlink',
|
||||||
|
'Hyperlink Plugin',
|
||||||
|
this.renderInputBox(
|
||||||
|
'Label title: ',
|
||||||
|
this.linkTitle,
|
||||||
|
this.props.state.linkTitle,
|
||||||
|
'Use "' + UrlPlaceholder + '" for the url string',
|
||||||
|
(state, value) => (state.linkTitle = value)
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
{this.renderPluginItem('customReplace', 'Custom Replace')}
|
||||||
|
{this.renderPluginItem('imageEditPlugin', 'ImageEditPlugin')}
|
||||||
|
{this.renderPluginItem('hiddenProperty', 'Hidden Property')}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
import { AutoFormatOptions } from 'roosterjs-content-model-plugins';
|
||||||
|
import { CodeElement } from './CodeElement';
|
||||||
|
|
||||||
|
export class AutoFormatCode extends CodeElement {
|
||||||
|
constructor(private options: AutoFormatOptions) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
getCode() {
|
||||||
|
return `new roosterjs.AutoFormatPlugin({
|
||||||
|
autoBullet: ${this.options.autoBullet},
|
||||||
|
autoLink: ${this.options.autoLink},
|
||||||
|
autoNumbering: ${this.options.autoNumbering},
|
||||||
|
autoUnlink: ${this.options.autoUnlink},
|
||||||
|
autoHyphen: ${this.options.autoHyphen},
|
||||||
|
autoFraction: ${this.options.autoFraction},
|
||||||
|
autoOrdinals: ${this.options.autoOrdinals},
|
||||||
|
})`;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
import { CodeElement } from './CodeElement';
|
||||||
|
import { getObjectKeys } from 'roosterjs-content-model-dom';
|
||||||
|
|
||||||
|
const codeMap: { [id: string]: string } = {
|
||||||
|
buttonB: 'roosterjs.toggleBold(editor)',
|
||||||
|
buttonI: 'roosterjs.toggleItalic(editor)',
|
||||||
|
buttonU: 'roosterjs.toggleUnderline(editor)',
|
||||||
|
buttonBullet: 'roosterjs.toggleBullet(editor)',
|
||||||
|
buttonNumbering: 'roosterjs.toggleNumbering(editor)',
|
||||||
|
buttonUndo: 'roosterjs.undo(editor)',
|
||||||
|
buttonRedo: 'roosterjs.redo(editor)',
|
||||||
|
buttonTable: 'roosterjs.insertTable(editor, 3, 3)',
|
||||||
|
buttonDark: 'editor.setDarkModeState(!editor.isDarkMode())',
|
||||||
|
};
|
||||||
|
|
||||||
|
export class ButtonsCode extends CodeElement {
|
||||||
|
getCode() {
|
||||||
|
const map = { ...codeMap };
|
||||||
|
return getObjectKeys(map)
|
||||||
|
.map(
|
||||||
|
id =>
|
||||||
|
`document.getElementById('${id}').addEventListener('click', () => ${map[id]});\n`
|
||||||
|
)
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
export abstract class CodeElement {
|
||||||
|
abstract getCode(): string;
|
||||||
|
|
||||||
|
protected encode(src: string): string {
|
||||||
|
return src.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected indent(src: string): string {
|
||||||
|
return src
|
||||||
|
.split('\n')
|
||||||
|
.map(line => (line == '' ? '' : ' ' + line + '\n'))
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
import { CodeElement } from './CodeElement';
|
||||||
|
|
||||||
|
export class DarkModeCode extends CodeElement {
|
||||||
|
getCode() {
|
||||||
|
return 'roosterjs.getDarkColor';
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
import { CodeElement } from './CodeElement';
|
||||||
|
import { ContentModelSegmentFormat } from 'roosterjs-content-model-types';
|
||||||
|
|
||||||
|
export class DefaultFormatCode extends CodeElement {
|
||||||
|
constructor(private defaultFormat: ContentModelSegmentFormat) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
getCode() {
|
||||||
|
let {
|
||||||
|
fontWeight,
|
||||||
|
italic,
|
||||||
|
underline,
|
||||||
|
fontFamily,
|
||||||
|
fontSize,
|
||||||
|
textColor,
|
||||||
|
backgroundColor,
|
||||||
|
} = this.defaultFormat;
|
||||||
|
let lines = [
|
||||||
|
fontWeight ? `fontWeight: '${fontWeight}',\n` : null,
|
||||||
|
italic ? 'italic: true,\n' : null,
|
||||||
|
underline ? 'underline: true,\n' : null,
|
||||||
|
fontFamily ? `fontFamily: '${this.encode(fontFamily)}',\n` : null,
|
||||||
|
fontSize ? `fontSize: '${this.encode(fontSize)}',\n` : null,
|
||||||
|
textColor ? `textColor: '${this.encode(textColor)}',\n` : null,
|
||||||
|
backgroundColor ? `backgroundColor: '${this.encode(backgroundColor)}',\n` : null,
|
||||||
|
].filter(line => !!line);
|
||||||
|
|
||||||
|
return lines.length > 0 ? '{\n' + this.indent(lines.join('')) + '}' : '';
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
import { ButtonsCode } from './ButtonsCode';
|
||||||
|
import { CodeElement } from './CodeElement';
|
||||||
|
import { DarkModeCode } from './DarkModeCode';
|
||||||
|
import { DefaultFormatCode } from './DefaultFormatCode';
|
||||||
|
import { PluginsCode } from './PluginsCode';
|
||||||
|
import type { OptionState } from '../OptionState';
|
||||||
|
|
||||||
|
export class EditorCode extends CodeElement {
|
||||||
|
private plugins: PluginsCode;
|
||||||
|
private defaultFormat: DefaultFormatCode;
|
||||||
|
private buttons: ButtonsCode;
|
||||||
|
private darkMode: DarkModeCode;
|
||||||
|
|
||||||
|
constructor(state: OptionState) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.plugins = new PluginsCode(state);
|
||||||
|
this.defaultFormat = new DefaultFormatCode(state.defaultFormat);
|
||||||
|
this.buttons = new ButtonsCode();
|
||||||
|
this.darkMode = new DarkModeCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
getCode() {
|
||||||
|
let defaultFormat = this.defaultFormat.getCode();
|
||||||
|
let code = "let contentDiv = document.getElementById('contentDiv');\n";
|
||||||
|
let darkMode = this.darkMode.getCode();
|
||||||
|
|
||||||
|
code += `let plugins = ${this.plugins.getCode()};\n`;
|
||||||
|
code += defaultFormat ? `let defaultSegmentFormat = ${defaultFormat};\n` : '';
|
||||||
|
code += 'let options = {\n';
|
||||||
|
code += this.indent('plugins: plugins,\n');
|
||||||
|
code += defaultFormat ? this.indent('defaultSegmentFormat: defaultSegmentFormat,\n') : '';
|
||||||
|
code += this.indent(`getDarkColor: ${darkMode},\n`);
|
||||||
|
code += '};\n';
|
||||||
|
code += `let editor = new roosterjs.Editor(contentDiv, options);\n`;
|
||||||
|
code += this.buttons ? this.buttons.getCode() : '';
|
||||||
|
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
import { CodeElement } from './CodeElement';
|
||||||
|
import { UrlPlaceholder } from '../OptionState';
|
||||||
|
|
||||||
|
export class HyperLinkCode extends CodeElement {
|
||||||
|
constructor(private linkTitle: string) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
getCode() {
|
||||||
|
return 'new roosterjs.HyperlinkPlugin(' + this.getLinkCallback() + ')';
|
||||||
|
}
|
||||||
|
|
||||||
|
private getLinkCallback() {
|
||||||
|
if (!this.linkTitle) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
let index = this.linkTitle.indexOf(UrlPlaceholder);
|
||||||
|
if (index >= 0) {
|
||||||
|
let left = this.linkTitle.substr(0, index);
|
||||||
|
let right = this.linkTitle.substr(index + UrlPlaceholder.length);
|
||||||
|
return (
|
||||||
|
'url => ' +
|
||||||
|
(left ? `'${this.encode(left)}' + ` : '') +
|
||||||
|
'url' +
|
||||||
|
(right ? ` + '${this.encode(right)}'` : '')
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return `() => '${this.linkTitle}'`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
import { CodeElement } from './CodeElement';
|
||||||
|
import { MarkdownOptions } from 'roosterjs-content-model-plugins';
|
||||||
|
|
||||||
|
export class MarkdownCode extends CodeElement {
|
||||||
|
constructor(private markdownOptions: MarkdownOptions) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
getCode() {
|
||||||
|
return `new roosterjs.MarkdownPlugin({
|
||||||
|
bold: ${this.markdownOptions.bold},
|
||||||
|
italic: ${this.markdownOptions.italic},
|
||||||
|
strikethrough: ${this.markdownOptions.strikethrough},
|
||||||
|
codeFormat: ${JSON.stringify(this.markdownOptions.codeFormat)},
|
||||||
|
})`;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,50 @@
|
|||||||
|
import { AutoFormatCode } from './AutoFormatCode';
|
||||||
|
import { CodeElement } from './CodeElement';
|
||||||
|
import { MarkdownCode } from './MarkdownCode';
|
||||||
|
import { OptionState } from '../OptionState';
|
||||||
|
import { WatermarkCode } from './WatermarkCode';
|
||||||
|
|
||||||
|
import {
|
||||||
|
EditPluginCode,
|
||||||
|
PastePluginCode,
|
||||||
|
TableEditPluginCode,
|
||||||
|
ShortcutPluginCode,
|
||||||
|
ImageEditPluginCode,
|
||||||
|
} from './SimplePluginCode';
|
||||||
|
|
||||||
|
export class PluginsCodeBase extends CodeElement {
|
||||||
|
private plugins: CodeElement[];
|
||||||
|
constructor(plugins: CodeElement[]) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.plugins = plugins.filter(plugin => !!plugin);
|
||||||
|
}
|
||||||
|
|
||||||
|
getPluginCount() {
|
||||||
|
return this.plugins.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCode() {
|
||||||
|
let code = '[\n';
|
||||||
|
code += this.indent(this.plugins.map(plugin => plugin.getCode() + ',\n').join(''));
|
||||||
|
code += ']';
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PluginsCode extends PluginsCodeBase {
|
||||||
|
constructor(state: OptionState) {
|
||||||
|
const pluginList = state.pluginList;
|
||||||
|
|
||||||
|
super([
|
||||||
|
pluginList.autoFormat && new AutoFormatCode(state.autoFormatOptions),
|
||||||
|
pluginList.edit && new EditPluginCode(),
|
||||||
|
pluginList.paste && new PastePluginCode(),
|
||||||
|
pluginList.tableEdit && new TableEditPluginCode(),
|
||||||
|
pluginList.shortcut && new ShortcutPluginCode(),
|
||||||
|
pluginList.watermark && new WatermarkCode(state.watermarkText),
|
||||||
|
pluginList.markdown && new MarkdownCode(state.markdownOptions),
|
||||||
|
pluginList.imageEditPlugin && new ImageEditPluginCode(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
import { CodeElement } from './CodeElement';
|
||||||
|
|
||||||
|
class SimplePluginCode extends CodeElement {
|
||||||
|
constructor(private name: string, private namespace: string = 'roosterjs') {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
getCode() {
|
||||||
|
return `new ${this.namespace}.${this.name}()`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EditPluginCode extends SimplePluginCode {
|
||||||
|
constructor() {
|
||||||
|
super('EditPlugin');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PastePluginCode extends SimplePluginCode {
|
||||||
|
constructor() {
|
||||||
|
super('PastePlugin');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ShortcutPluginCode extends SimplePluginCode {
|
||||||
|
constructor() {
|
||||||
|
super('ShortcutPlugin');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TableEditPluginCode extends SimplePluginCode {
|
||||||
|
constructor() {
|
||||||
|
super('TableEditPlugin');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ImageEditPluginCode extends SimplePluginCode {
|
||||||
|
constructor() {
|
||||||
|
super('ImageEditPlugin');
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
import { CodeElement } from './CodeElement';
|
||||||
|
|
||||||
|
export class WatermarkCode extends CodeElement {
|
||||||
|
constructor(private watermarkText: string) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
getCode() {
|
||||||
|
return `new roosterjs.WatermarkPlugin('${this.encode(this.watermarkText)}')`;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
import { ContentModelText } from 'roosterjs-content-model-types';
|
||||||
|
import { CustomReplace } from 'roosterjs-content-model-plugins';
|
||||||
|
|
||||||
|
function replaceEmojis(
|
||||||
|
previousSegment: ContentModelText,
|
||||||
|
stringToReplace: string,
|
||||||
|
replacement: string
|
||||||
|
) {
|
||||||
|
const { text } = previousSegment;
|
||||||
|
const queryString = text.split(' ').pop();
|
||||||
|
if (queryString === stringToReplace) {
|
||||||
|
previousSegment.text = text.replace(stringToReplace, replacement);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeEmojiReplacements(stringToReplace: string, replacement: string) {
|
||||||
|
return {
|
||||||
|
stringToReplace,
|
||||||
|
replacementString: replacement,
|
||||||
|
replacementHandler: replaceEmojis,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const emojiReplacements: CustomReplace[] = [
|
||||||
|
makeEmojiReplacements(';)', '😉'),
|
||||||
|
makeEmojiReplacements(';-)', '😉'),
|
||||||
|
makeEmojiReplacements(';P', '😜'),
|
||||||
|
makeEmojiReplacements(';-P', '😜'),
|
||||||
|
makeEmojiReplacements('<3', '❤️'),
|
||||||
|
makeEmojiReplacements('</3', '💔'),
|
||||||
|
makeEmojiReplacements(';*', '😘'),
|
||||||
|
makeEmojiReplacements(';-*', '😘'),
|
||||||
|
makeEmojiReplacements('B)', '😎'),
|
||||||
|
makeEmojiReplacements('B-)', '😎'),
|
||||||
|
];
|
@ -0,0 +1,117 @@
|
|||||||
|
import React, { useRef, useEffect, useState, useCallback } from 'react';
|
||||||
|
import { createEditor } from 'roosterjs';
|
||||||
|
import { Button, Space, Typography, Radio, Input } from 'antd'; // Ant Design components
|
||||||
|
import './RoosterEditor.css'; // We'll create this for basic styling
|
||||||
|
|
||||||
|
const { Paragraph, Title } = Typography;
|
||||||
|
|
||||||
|
const RoosterEditor = ({ initialContent = '', onChange }) => {
|
||||||
|
const editorDivRef = useRef(null);
|
||||||
|
const editorRef = useRef(null);
|
||||||
|
const [editorContent, setEditorContent] = useState(initialContent);
|
||||||
|
const [outputFormat, setOutputFormat] = useState('html');
|
||||||
|
|
||||||
|
// Initialize the editor
|
||||||
|
useEffect(() => {
|
||||||
|
if (editorDivRef.current && !editorRef.current) {
|
||||||
|
const options = {
|
||||||
|
initialContent: editorContent,
|
||||||
|
// THIS IS THE KEY CHANGE: Use onContentChanged event in options
|
||||||
|
// This callback will be triggered by RoosterJs whenever content changes
|
||||||
|
onContentChanged: (event) => {
|
||||||
|
// event is of type ContentChangedEvent (though we don't declare type in JS)
|
||||||
|
const currentHtml = editorRef.current?.getContent() || '';
|
||||||
|
setEditorContent(currentHtml);
|
||||||
|
if (onChange) {
|
||||||
|
onChange(currentHtml); // Call parent's onChange if provided
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
editorRef.current = createEditor(editorDivRef.current, options);
|
||||||
|
|
||||||
|
// Cleanup function to dispose of the editor when component unmounts
|
||||||
|
return () => {
|
||||||
|
if (editorRef.current) {
|
||||||
|
editorRef.current.dispose();
|
||||||
|
editorRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, []); // Empty dependency array means this runs once on mount
|
||||||
|
|
||||||
|
// Function to execute editor commands
|
||||||
|
const executeCommand = useCallback((command, value) => {
|
||||||
|
if (editorRef.current) {
|
||||||
|
editorRef.current.focus();
|
||||||
|
editorRef.current.executeEdit(command, value);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Function to set content programmatically
|
||||||
|
const setContentProgrammatically = useCallback((newContent) => {
|
||||||
|
if (editorRef.current) {
|
||||||
|
editorRef.current.setContent(newContent);
|
||||||
|
setEditorContent(newContent);
|
||||||
|
if (onChange) {
|
||||||
|
onChange(newContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [onChange]);
|
||||||
|
|
||||||
|
const handleFormatChange = (e) => {
|
||||||
|
setOutputFormat(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '20px', maxWidth: '800px', margin: 'auto' }}>
|
||||||
|
{/* <Title level={2}>RoosterJs Custom Editor with Ant Design</Title> */}
|
||||||
|
|
||||||
|
<Space style={{ marginBottom: '16px' }}>
|
||||||
|
<Button onClick={() => executeCommand('bold')}>Bold</Button>
|
||||||
|
<Button onClick={() => executeCommand('italic')}>Italic</Button>
|
||||||
|
<Button onClick={() => executeCommand('underline')}>Underline</Button>
|
||||||
|
<Button onClick={() => executeCommand('insertUnorderedList')}>Unordered List</Button>
|
||||||
|
<Button onClick={() => executeCommand('insertOrderedList')}>Ordered List</Button>
|
||||||
|
</Space>
|
||||||
|
|
||||||
|
<div style={{ marginBottom: '16px' }}>
|
||||||
|
<Radio.Group onChange={handleFormatChange} value={outputFormat}>
|
||||||
|
<Radio value="html">Show HTML Output</Radio>
|
||||||
|
<Radio value="text">Show Plain Text Output</Radio>
|
||||||
|
</Radio.Group>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rooster-editor-container">
|
||||||
|
<div ref={editorDivRef} className="rooster-editor" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* <Title level={3} style={{ marginTop: '24px' }}>
|
||||||
|
Current Editor Content (from React state, output as {outputFormat.toUpperCase()}):
|
||||||
|
</Title>
|
||||||
|
<Input.TextArea
|
||||||
|
value={
|
||||||
|
outputFormat === 'html'
|
||||||
|
? editorContent
|
||||||
|
: editorRef.current?.getPlainText(true) || ''
|
||||||
|
}
|
||||||
|
autoSize={{ minRows: 5, maxRows: 10 }}
|
||||||
|
readOnly
|
||||||
|
style={{
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
backgroundColor: '#f0f0f0',
|
||||||
|
padding: '10px',
|
||||||
|
borderRadius: '4px',
|
||||||
|
}}
|
||||||
|
/>*/}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
style={{ marginTop: '16px' }}
|
||||||
|
onClick={() => setContentProgrammatically('<h3>This is new content set programmatically from Ant Design Button!</h3><p>It can contain <em><strong>rich text</strong></em> too.</p>')}
|
||||||
|
>
|
||||||
|
Set Content Programmatically
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);};
|
||||||
|
|
||||||
|
export default RoosterEditor;
|
@ -0,0 +1,8 @@
|
|||||||
|
import { demoUndeletableAnchorParser } from './demoUndeletableAnchorParser';
|
||||||
|
import { DomToModelOption } from 'roosterjs-content-model-types';
|
||||||
|
|
||||||
|
export const defaultDomToModelOption: DomToModelOption = {
|
||||||
|
additionalFormatParsers: {
|
||||||
|
link: [demoUndeletableAnchorParser],
|
||||||
|
},
|
||||||
|
};
|
@ -0,0 +1,13 @@
|
|||||||
|
import { FormatParser, UndeletableFormat } from 'roosterjs-content-model-types';
|
||||||
|
|
||||||
|
export const DemoUndeletableName = 'DemoUndeletable';
|
||||||
|
|
||||||
|
export function undeletableLinkChecker(a: HTMLAnchorElement): boolean {
|
||||||
|
return a.getAttribute('name') == DemoUndeletableName;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const demoUndeletableAnchorParser: FormatParser<UndeletableFormat> = (format, element) => {
|
||||||
|
if (undeletableLinkChecker(element as HTMLAnchorElement)) {
|
||||||
|
format.undeletable = true;
|
||||||
|
}
|
||||||
|
};
|
@ -0,0 +1,91 @@
|
|||||||
|
import { applySegmentFormat, getFormatState } from 'roosterjs-content-model-api';
|
||||||
|
import {
|
||||||
|
ContentModelSegmentFormat,
|
||||||
|
EditorPlugin,
|
||||||
|
IEditor,
|
||||||
|
PluginEvent,
|
||||||
|
} from 'roosterjs-content-model-types';
|
||||||
|
|
||||||
|
const FORMATPAINTERCURSOR_SVG = require('./formatpaintercursor.svg');
|
||||||
|
const FORMATPAINTERCURSOR_STYLE = `cursor: url("${FORMATPAINTERCURSOR_SVG}") 8.5 16, auto`;
|
||||||
|
const FORMAT_PAINTER_STYLE_KEY = '_FormatPainter';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format painter handler works together with a format painter button tot let implement format painter functioinality
|
||||||
|
*/
|
||||||
|
export interface FormatPainterHandler {
|
||||||
|
/**
|
||||||
|
* Let editor enter format painter state
|
||||||
|
*/
|
||||||
|
startFormatPainter(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format painter plugin helps implement format painter functionality.
|
||||||
|
* To use this plugin, you need a button to let editor enter format painter state by calling formatPainterPlugin.startFormatPainter(),
|
||||||
|
* then this plugin will handle the rest work.
|
||||||
|
*/
|
||||||
|
export class FormatPainterPlugin implements EditorPlugin, FormatPainterHandler {
|
||||||
|
private editor: IEditor | null = null;
|
||||||
|
private painterFormat: ContentModelSegmentFormat | null = null;
|
||||||
|
|
||||||
|
getName() {
|
||||||
|
return 'FormatPainter';
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize(editor: IEditor) {
|
||||||
|
this.editor = editor;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
this.editor = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
onPluginEvent(event: PluginEvent) {
|
||||||
|
if (this.editor && event.eventType == 'mouseUp') {
|
||||||
|
if (this.painterFormat) {
|
||||||
|
applySegmentFormat(this.editor, this.painterFormat);
|
||||||
|
|
||||||
|
this.setFormatPainterCursor(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private setFormatPainterCursor(format: ContentModelSegmentFormat | null) {
|
||||||
|
this.painterFormat = format;
|
||||||
|
|
||||||
|
this.editor?.setEditorStyle(
|
||||||
|
FORMAT_PAINTER_STYLE_KEY,
|
||||||
|
this.painterFormat ? FORMATPAINTERCURSOR_STYLE : null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
startFormatPainter() {
|
||||||
|
if (this.editor) {
|
||||||
|
const format = getSegmentFormat(this.editor);
|
||||||
|
|
||||||
|
this.setFormatPainterCursor(format);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSegmentFormat(editor: IEditor): ContentModelSegmentFormat {
|
||||||
|
const formatState = getFormatState(editor);
|
||||||
|
|
||||||
|
return {
|
||||||
|
backgroundColor: formatState.backgroundColor,
|
||||||
|
fontFamily: formatState.fontName,
|
||||||
|
fontSize: formatState.fontSize,
|
||||||
|
fontWeight: formatState.isBold ? 'bold' : 'normal',
|
||||||
|
italic: formatState.isItalic,
|
||||||
|
letterSpacing: formatState.letterSpacing,
|
||||||
|
strikethrough: formatState.isStrikeThrough,
|
||||||
|
superOrSubScriptSequence: formatState.isSubscript
|
||||||
|
? 'sub'
|
||||||
|
: formatState.isSuperscript
|
||||||
|
? 'super'
|
||||||
|
: '',
|
||||||
|
textColor: formatState.textColor,
|
||||||
|
underline: formatState.isUnderline,
|
||||||
|
};
|
||||||
|
}
|
@ -0,0 +1,156 @@
|
|||||||
|
import { insertEntity } from 'roosterjs-content-model-api';
|
||||||
|
import type {
|
||||||
|
EditorPlugin,
|
||||||
|
Entity,
|
||||||
|
EntityState,
|
||||||
|
IEditor,
|
||||||
|
PluginEvent,
|
||||||
|
} from 'roosterjs-content-model-types';
|
||||||
|
|
||||||
|
const EntityType = 'SampleEntity';
|
||||||
|
|
||||||
|
interface EntityMetadata {
|
||||||
|
count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class SampleEntityPlugin implements EditorPlugin {
|
||||||
|
private editor: IEditor;
|
||||||
|
private hydratedEntities: Record<string, HydratedEntity> = {};
|
||||||
|
|
||||||
|
getName() {
|
||||||
|
return 'SampleEntity';
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize(editor: IEditor) {
|
||||||
|
this.editor = editor;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
this.editor = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
onPluginEvent(event: PluginEvent) {
|
||||||
|
if (event.eventType == 'keyDown' && event.rawEvent.key == 'm' && event.rawEvent.ctrlKey) {
|
||||||
|
insertEntity(this.editor, EntityType, true /*isBlock*/, 'focus', {
|
||||||
|
contentNode: this.createEntityNode(),
|
||||||
|
initialEntityState: '{}',
|
||||||
|
});
|
||||||
|
|
||||||
|
event.rawEvent.preventDefault();
|
||||||
|
} else if (event.eventType == 'entityOperation' && event.entity.type == EntityType) {
|
||||||
|
const entity = event.entity;
|
||||||
|
const hydratedEntity = this.hydratedEntities[entity.id];
|
||||||
|
|
||||||
|
switch (event.operation) {
|
||||||
|
case 'newEntity':
|
||||||
|
hydratedEntity?.dehydrate();
|
||||||
|
this.hydratedEntities[entity.id] = new HydratedEntity(entity, this.onClick);
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'removeFromEnd':
|
||||||
|
case 'removeFromStart':
|
||||||
|
case 'overwrite':
|
||||||
|
case 'replaceTemporaryContent':
|
||||||
|
hydratedEntity?.dehydrate();
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'updateEntityState':
|
||||||
|
if (event.state) {
|
||||||
|
setMetadata(event.entity.wrapper, JSON.parse(event.state));
|
||||||
|
hydratedEntity?.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'beforeFormat':
|
||||||
|
const span = entity.wrapper.querySelector('span');
|
||||||
|
|
||||||
|
if (span && event.formattableRoots) {
|
||||||
|
event.formattableRoots.push({
|
||||||
|
element: span,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private onClick = (state: EntityState) => {
|
||||||
|
this.editor.takeSnapshot(state);
|
||||||
|
};
|
||||||
|
|
||||||
|
private createEntityNode() {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
|
||||||
|
return div;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class HydratedEntity {
|
||||||
|
constructor(private entity: Entity, private onClick: (entityState: EntityState) => void) {
|
||||||
|
const containerDiv = entity.wrapper.querySelector('div');
|
||||||
|
const span = document.createElement('span');
|
||||||
|
const button = document.createElement('button');
|
||||||
|
|
||||||
|
containerDiv.appendChild(span);
|
||||||
|
containerDiv.appendChild(button);
|
||||||
|
|
||||||
|
button.textContent = 'Test entity';
|
||||||
|
button.addEventListener('click', this.onClickEntity);
|
||||||
|
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
update(increase: number = 0) {
|
||||||
|
const metadata = getMetadata<EntityMetadata>(this.entity.wrapper);
|
||||||
|
const count = (metadata?.count || 0) + increase;
|
||||||
|
|
||||||
|
setMetadata(this.entity.wrapper, {
|
||||||
|
count,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.entity.wrapper.querySelector('span').textContent = 'Count: ' + count;
|
||||||
|
}
|
||||||
|
|
||||||
|
dehydrate() {
|
||||||
|
const containerDiv = this.entity.wrapper.querySelector('div');
|
||||||
|
const button = containerDiv.querySelector('button');
|
||||||
|
|
||||||
|
if (button) {
|
||||||
|
button.removeEventListener('click', this.onClickEntity);
|
||||||
|
containerDiv.removeChild(button);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private onClickEntity = (e: MouseEvent) => {
|
||||||
|
this.update(1);
|
||||||
|
this.onClick({
|
||||||
|
id: this.entity.id,
|
||||||
|
type: this.entity.type,
|
||||||
|
state: this.entity.wrapper.dataset.editingInfo,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const MetadataDataSetName = 'editingInfo';
|
||||||
|
|
||||||
|
function getMetadata<T>(element: HTMLElement): T | null {
|
||||||
|
const str = element.dataset[MetadataDataSetName];
|
||||||
|
let obj: any;
|
||||||
|
|
||||||
|
try {
|
||||||
|
obj = str ? JSON.parse(str) : null;
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
if (typeof obj !== 'undefined') {
|
||||||
|
return obj as T;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setMetadata<T>(element: HTMLElement, metadata: T) {
|
||||||
|
element.dataset[MetadataDataSetName] = JSON.stringify(metadata);
|
||||||
|
}
|
@ -0,0 +1,206 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { Callout } from '@fluentui/react/lib/Callout';
|
||||||
|
import { DOMInsertPoint } from 'roosterjs-content-model-types';
|
||||||
|
import { IContextualMenuItem } from '@fluentui/react/lib/ContextualMenu';
|
||||||
|
import { mergeStyles } from '@fluentui/react/lib/Styling';
|
||||||
|
import { ReactEditorPlugin, UIUtilities } from 'roosterjs-react';
|
||||||
|
import {
|
||||||
|
PickerDirection,
|
||||||
|
PickerHandler,
|
||||||
|
PickerHelper,
|
||||||
|
PickerPlugin,
|
||||||
|
PickerSelectionChangMode,
|
||||||
|
} from 'roosterjs-content-model-plugins';
|
||||||
|
import {
|
||||||
|
createContentModelDocument,
|
||||||
|
createEntity,
|
||||||
|
createParagraph,
|
||||||
|
getDOMInsertPointRect,
|
||||||
|
} from 'roosterjs-content-model-dom';
|
||||||
|
|
||||||
|
const itemStyle = mergeStyles({
|
||||||
|
height: '20px',
|
||||||
|
margin: '4px',
|
||||||
|
padding: '4px',
|
||||||
|
minWidth: '200px',
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedItemStyle = mergeStyles({
|
||||||
|
backgroundColor: 'blue',
|
||||||
|
color: 'white',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
});
|
||||||
|
|
||||||
|
export class SamplePickerPlugin extends PickerPlugin implements ReactEditorPlugin {
|
||||||
|
private pickerHandler: SamplePickerHandler;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
const pickerHandler = new SamplePickerHandler();
|
||||||
|
super('@', pickerHandler);
|
||||||
|
|
||||||
|
this.pickerHandler = pickerHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
setUIUtilities(uiUtilities: UIUtilities): void {
|
||||||
|
this.pickerHandler.setUIUtilities(uiUtilities);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SamplePickerHandler implements PickerHandler {
|
||||||
|
private uiUtilities: UIUtilities;
|
||||||
|
private index = 0;
|
||||||
|
private ref: IPickerMenu | null = null;
|
||||||
|
private queryString: string;
|
||||||
|
private items: IContextualMenuItem[] = [];
|
||||||
|
private onClose: (() => void) | null = null;
|
||||||
|
private helper: PickerHelper | null = null;
|
||||||
|
|
||||||
|
onInitialize(helper: PickerHelper) {
|
||||||
|
this.helper = helper;
|
||||||
|
}
|
||||||
|
|
||||||
|
onDispose() {
|
||||||
|
this.helper = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
setUIUtilities(uiUtilities: UIUtilities): void {
|
||||||
|
this.uiUtilities = uiUtilities;
|
||||||
|
}
|
||||||
|
|
||||||
|
onTrigger(queryString: string, pos: DOMInsertPoint): PickerDirection | null {
|
||||||
|
this.index = 0;
|
||||||
|
this.queryString = queryString;
|
||||||
|
this.items = buildItems(queryString, this.index);
|
||||||
|
|
||||||
|
const rect = getDOMInsertPointRect(this.helper.editor.getDocument(), pos);
|
||||||
|
|
||||||
|
if (rect) {
|
||||||
|
this.onClose = this.uiUtilities.renderComponent(
|
||||||
|
<PickerMenu
|
||||||
|
x={rect.left}
|
||||||
|
y={(rect.bottom + rect.top) / 2}
|
||||||
|
ref={ref => (this.ref = ref)}
|
||||||
|
items={this.items}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
return 'vertical';
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onClosePicker() {
|
||||||
|
this.onClose?.();
|
||||||
|
this.onClose = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
onSelectionChanged(mode: PickerSelectionChangMode): void {
|
||||||
|
switch (mode) {
|
||||||
|
case 'first':
|
||||||
|
case 'firstInRow':
|
||||||
|
case 'previousPage':
|
||||||
|
this.index = 0;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'last':
|
||||||
|
case 'lastInRow':
|
||||||
|
case 'nextPage':
|
||||||
|
this.index = 4;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'previous':
|
||||||
|
this.index = this.index - 1;
|
||||||
|
|
||||||
|
if (this.index < 0) {
|
||||||
|
this.index = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'next':
|
||||||
|
this.index = (this.index + 1) % 5;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.items = buildItems(this.queryString, this.index);
|
||||||
|
this.ref?.setMenuItems(this.items);
|
||||||
|
}
|
||||||
|
|
||||||
|
onSelect(): void {
|
||||||
|
const text = this.items[this.index]?.text;
|
||||||
|
|
||||||
|
if (text) {
|
||||||
|
const span = this.helper.editor.getDocument().createElement('span');
|
||||||
|
span.textContent = '@' + text;
|
||||||
|
span.style.textDecoration = 'underline';
|
||||||
|
span.style.color = 'blue';
|
||||||
|
|
||||||
|
const entity = createEntity(span, true /*isReadonly*/, {}, 'TEST_ENTITY');
|
||||||
|
const paragraph = createParagraph();
|
||||||
|
const doc = createContentModelDocument();
|
||||||
|
|
||||||
|
paragraph.segments.push(entity);
|
||||||
|
doc.blocks.push(paragraph);
|
||||||
|
|
||||||
|
this.helper.replaceQueryString(
|
||||||
|
doc,
|
||||||
|
{
|
||||||
|
changeSource: 'SamplePicker',
|
||||||
|
},
|
||||||
|
true /*canUndoByBackspace*/
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.onClose?.();
|
||||||
|
this.onClose = null;
|
||||||
|
this.ref = null;
|
||||||
|
this.helper.closePicker();
|
||||||
|
}
|
||||||
|
|
||||||
|
onQueryStringChanged(queryString: string): void {
|
||||||
|
this.queryString = queryString;
|
||||||
|
|
||||||
|
if (queryString.length > 100 || queryString.split(' ').length > 4) {
|
||||||
|
// Querystring is too long, so close picker
|
||||||
|
this.helper.closePicker();
|
||||||
|
} else {
|
||||||
|
this.items = buildItems(this.queryString, this.index);
|
||||||
|
this.ref?.setMenuItems(this.items);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildItems(queryString: string, index: number): IContextualMenuItem[] {
|
||||||
|
return [1, 2, 3, 4, 5].map((x, i) => ({
|
||||||
|
key: 'item' + i,
|
||||||
|
text: queryString.substring(1) + ' item ' + x,
|
||||||
|
checked: i == index,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IPickerMenu {
|
||||||
|
setMenuItems: (items: IContextualMenuItem[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PickerMenu = React.forwardRef(
|
||||||
|
(
|
||||||
|
props: { x: number; y: number; items: IContextualMenuItem[] },
|
||||||
|
ref: React.Ref<IPickerMenu>
|
||||||
|
) => {
|
||||||
|
const [items, setItems] = React.useState<IContextualMenuItem[]>(props.items);
|
||||||
|
|
||||||
|
React.useImperativeHandle(ref, () => ({
|
||||||
|
setMenuItems: setItems,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Callout target={{ left: props.x, top: props.y }} isBeakVisible={false} gapSpace={10}>
|
||||||
|
{items.map(item => (
|
||||||
|
<div className={itemStyle + (item.checked ? ' ' + selectedItemStyle : '')}>
|
||||||
|
{item.text}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Callout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
@ -0,0 +1,60 @@
|
|||||||
|
import type {
|
||||||
|
ContentModelDocument,
|
||||||
|
EditorPlugin,
|
||||||
|
IEditor,
|
||||||
|
PluginEvent,
|
||||||
|
} from 'roosterjs-content-model-types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A plugin to help get HTML content from editor
|
||||||
|
*/
|
||||||
|
export class UpdateContentPlugin implements EditorPlugin {
|
||||||
|
private editor: IEditor | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new instance of UpdateContentPlugin class
|
||||||
|
* @param onUpdate A callback to be invoked when update happens
|
||||||
|
*/
|
||||||
|
constructor(private onUpdate: (model: ContentModelDocument) => void) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a friendly name of this plugin
|
||||||
|
*/
|
||||||
|
getName() {
|
||||||
|
return 'UpdateContent';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize this plugin
|
||||||
|
* @param editor The editor instance
|
||||||
|
*/
|
||||||
|
initialize(editor: IEditor) {
|
||||||
|
this.editor = editor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispose this plugin
|
||||||
|
*/
|
||||||
|
dispose() {
|
||||||
|
this.editor = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle events triggered from editor
|
||||||
|
* @param event PluginEvent object
|
||||||
|
*/
|
||||||
|
onPluginEvent(event: PluginEvent) {
|
||||||
|
switch (event.eventType) {
|
||||||
|
case 'beforeDispose':
|
||||||
|
this.update();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update() {
|
||||||
|
if (this.editor) {
|
||||||
|
const model = this.editor.getContentModelCopy('disconnected');
|
||||||
|
this.onUpdate(model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
<svg viewBox="-8 -8 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32">
|
||||||
|
<title>Format painter</title>
|
||||||
|
<g id="formatpainter16-filled" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||||
|
<g id="insertion-point" fill="#000000">
|
||||||
|
<path d="M-3,0 L0,0 L0,1 L-3,1 Z"></path>
|
||||||
|
<path d="M1,0 L4,0 L4,1 L1,1 Z"></path>
|
||||||
|
<path d="M0,1 L1,1 L1,15 L0,15 Z"></path>
|
||||||
|
<path d="M-3,15 L0,15 L0,16 L-3,16 Z"></path>
|
||||||
|
<path d="M1,15 L4,15 L4,16 L1,16 Z"></path>
|
||||||
|
</g>
|
||||||
|
<g id="brush">
|
||||||
|
<polygon id="Path-2" fill="#FFFFFF" fill-rule="nonzero" points="1.57904078 8.63705201 8.34046001 16.041788 14.56362 10.6509412 15.1653426 8.63705201 13.104499 6.78010591 17.340463 2.01220762 15.8855229 0.661038667 11.3773741 4.92433904 9.4597519 2.96951789"></polygon>
|
||||||
|
<g id="Group-3" transform="translate(7.000000, 0.000000)">
|
||||||
|
<path d="M9.4622,2.49777273 C9.5084,2.44813636 9.5359,2.39968182 9.548,2.35004545 C9.5601,2.30040909 9.5656,2.26022727 9.5656,2.2295 C9.5656,2.19995455 9.5601,2.15859091 9.548,2.11013636 C9.5304,2.05459091 9.4996,2.00259091 9.4534,1.95295455 L9.0409,1.50031818 C8.9606,1.42704545 8.8748,1.39040909 8.7835,1.39040909 C8.6801,1.39040909 8.5943,1.42704545 8.525,1.50031818 L4.5815,5.74777273 L2.7335,3.76231818 C2.6642,3.68904545 2.5784,3.65240909 2.475,3.65240909 C2.3782,3.65240909 2.2946,3.68904545 2.2264,3.76231818 L1.5389,4.50095455 L6.798,10.3367727 L7.4602,9.62531818 C7.4712,9.6135 7.4822,9.58631818 7.4943,9.54259091 C7.5064,9.49295455 7.5119,9.42559091 7.5119,9.33931818 C7.5119,9.17268182 7.491,9.03440909 7.4514,8.92331818 C7.4052,8.81340909 7.3744,8.74840909 7.3568,8.7295 L5.5088,6.74522727 L9.4622,2.49777273 Z M10.2355,1.12213636 C10.3785,1.27577273 10.4852,1.45186364 10.5534,1.64804545 C10.6227,1.8395 10.6568,2.03331818 10.6568,2.2295 C10.6568,2.43277273 10.6227,2.63013636 10.5534,2.82040909 C10.4852,3.01186364 10.3785,3.18440909 10.2355,3.33804545 L7.0642,6.74522727 L8.1301,7.88922727 C8.2797,8.04995455 8.3963,8.2615 8.4821,8.52740909 C8.569,8.78504545 8.6119,9.05331818 8.6119,9.32986364 C8.6119,9.56386364 8.58,9.78013636 8.5173,9.97631818 C8.4535,10.1677727 8.3622,10.3273182 8.2423,10.4561364 L6.7892,12.0173182 L-3.73034936e-14,4.48322727 L1.4443,2.92204545 C1.5818,2.77431818 1.7391,2.66440909 1.9173,2.58995455 C2.0944,2.50959091 2.2803,2.47059091 2.475,2.47059091 C2.882,2.47059091 3.2263,2.62068182 3.5068,2.92204545 L4.5815,4.07668182 L7.7517,0.6695 C8.0388,0.362227273 8.3831,0.208590909 8.7835,0.208590909 C8.9782,0.208590909 9.1641,0.247590909 9.3423,0.327954545 C9.5194,0.408318182 9.6767,0.521772727 9.8142,0.6695 L10.2355,1.12213636 Z" id="Fill-1" fill="#666666"></path>
|
||||||
|
</g>
|
||||||
|
<g id="Group-6" transform="translate(0.000000, 3.000000)">
|
||||||
|
<path d="M13.7187692,7.38535 C13.6853077,7.43201667 13.6391538,7.47868333 13.593,7.52535 C13.593,7.52535 13.4649231,7.66535 13.2687692,7.88701667 C13.2122308,7.95701667 13.1545385,8.03868333 13.0841538,8.10868333 C12.8072308,8.41201667 12.4853077,8.75035 12.1045385,9.12368333 C11.8045385,9.43868333 11.4572308,9.75251667 11.1687692,10.0570167 C10.9506923,10.2670167 10.7303077,10.47585 10.4995385,10.6870167 C10.0956923,11.0591833 9.66876923,11.41035 9.288,11.7136833 C9.16107692,11.8186833 9.03415385,11.9120167 8.91876923,11.9936833 C8.538,12.2725167 8.28530769,12.46035 8.28530769,12.46035 L5.35338462,9.06418333 L4.34953846,7.91035 L2.89569231,6.23035 L2.25857859,5.5054604 C2.68550166,5.37712707 3.03280935,5.24879373 3.55088628,5.03879373 C4.07127089,4.82879373 4.61242474,4.57212707 5.15473243,4.2804604 C5.69704012,3.97712707 6.22780935,3.62712707 6.75857859,3.24212707 C7.28934782,2.8454604 7.7739632,2.40212707 8.21242474,1.9004604 L7.38165551,1.0604604 C7.07127089,1.45712707 6.73665551,1.80712707 6.35588628,2.12212707 C5.9739632,2.43712707 5.58280935,2.71712707 5.18934782,2.96212707 C4.7739632,3.20712707 4.35973243,3.42879373 3.95473243,3.6154604 C3.53934782,3.80212707 3.13550166,3.97712707 2.75473243,4.11712707 C1.85473243,4.44379373 1.05857859,4.65379373 0.112424739,4.8054604 L8.07646154,14.00035 C8.56107692,13.6970167 9.04569231,13.37035 9.51876923,13.0075167 C9.993,12.6470167 10.4418462,12.2725167 10.8803077,11.90035 C11.3072308,11.5270167 11.7237692,11.1420167 12.1045385,10.7675167 C12.4853077,10.39535 12.8303077,10.0441833 13.1545385,9.70701667 C13.4649231,9.38035 13.743,9.07701667 13.9737692,8.81918333 C14.2033846,8.55201667 14.388,8.37701667 14.5149231,8.22535 L13.7187692,7.38535 Z" id="Fill-4" fill="#666666"></path>
|
||||||
|
</g>
|
||||||
|
<path d="M12.0690848,11.4533915 C11.5246492,12.008121 10.9664883,12.5639943 10.3511389,13.1027111 C9.74951466,13.6585844 9.11929615,14.1664193 8.48907764,14.6285032 L3.43131675,9 L12.0690848,11.4533915 Z" id="Fill-7" fill="#2E77D0"></path>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 4.8 KiB |
@ -0,0 +1,61 @@
|
|||||||
|
@import '../../theme/theme.scss';
|
||||||
|
|
||||||
|
.snapshotPane {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textarea {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
resize: none;
|
||||||
|
min-height: 100px;
|
||||||
|
border-color: $primaryBorder;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
border-color: $primaryBorder;
|
||||||
|
}
|
||||||
|
|
||||||
|
.snapshotList {
|
||||||
|
min-height: 100px;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow: hidden auto;
|
||||||
|
border: solid 1px $primaryBorder;
|
||||||
|
|
||||||
|
pre {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
cursor: pointer;
|
||||||
|
margin: 0;
|
||||||
|
&:hover {
|
||||||
|
background-color: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.current {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.autoComplete {
|
||||||
|
background-color: #ff0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.snapshotList {
|
||||||
|
border: solid 1px $primaryBorderDark;
|
||||||
|
}
|
||||||
|
.textarea {
|
||||||
|
border-color: $primaryBorderDark;
|
||||||
|
}
|
||||||
|
.snapshotList {
|
||||||
|
border: solid 1px $primaryBorderDark;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,188 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { EntityState, Snapshot, SnapshotSelection } from 'roosterjs-content-model-types';
|
||||||
|
|
||||||
|
const styles = require('./SnapshotPane.scss');
|
||||||
|
|
||||||
|
export interface SnapshotPaneProps {
|
||||||
|
onTakeSnapshot: () => Snapshot;
|
||||||
|
onRestoreSnapshot: (snapshot: Snapshot, triggerContentChangedEvent: boolean) => void;
|
||||||
|
onMove: (moveStep: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SnapshotPaneState {
|
||||||
|
snapshots: Snapshot[];
|
||||||
|
currentIndex: number;
|
||||||
|
autoCompleteIndex: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SnapshotPane extends React.Component<SnapshotPaneProps, SnapshotPaneState> {
|
||||||
|
private html = React.createRef<HTMLTextAreaElement>();
|
||||||
|
private entityStates = React.createRef<HTMLTextAreaElement>();
|
||||||
|
private isDarkColor = React.createRef<HTMLInputElement>();
|
||||||
|
private selection = React.createRef<HTMLTextAreaElement>();
|
||||||
|
private logicalRootPath = React.createRef<HTMLInputElement>();
|
||||||
|
|
||||||
|
constructor(props: SnapshotPaneProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
snapshots: [],
|
||||||
|
currentIndex: -1,
|
||||||
|
autoCompleteIndex: -1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className={styles.snapshotPane} onPaste={this.onPaste}>
|
||||||
|
<h3>Undo Snapshots</h3>
|
||||||
|
<div className={styles.snapshotList}>
|
||||||
|
{this.state.snapshots.map(this.renderItem)}
|
||||||
|
</div>
|
||||||
|
<h3>Selected Snapshot</h3>
|
||||||
|
<div className={styles.buttons}>
|
||||||
|
<button onClick={this.takeSnapshot}>{'Take snapshot'}</button>{' '}
|
||||||
|
<button onClick={this.onClickRestoreSnapshot}>{'Restore snapshot'}</button>
|
||||||
|
<button onClick={this.onCopy}>{'Copy snapshot with metadata'}</button>
|
||||||
|
</div>
|
||||||
|
<div>HTML:</div>
|
||||||
|
<textarea ref={this.html} className={styles.textarea} spellCheck={false} />
|
||||||
|
<div>Selection:</div>
|
||||||
|
<textarea ref={this.selection} className={styles.textarea} spellCheck={false} />
|
||||||
|
<div>Entity states:</div>
|
||||||
|
<textarea ref={this.entityStates} className={styles.textarea} spellCheck={false} />
|
||||||
|
<div>Logical root path:</div>
|
||||||
|
<input ref={this.logicalRootPath} className={styles.input} spellCheck={false} />
|
||||||
|
<div>
|
||||||
|
<input type="checkbox" ref={this.isDarkColor} id="isUndoInDarkColor" />
|
||||||
|
<label htmlFor="isUndoInDarkColor">Is in dark mode</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getCurrentSnapshot(): Snapshot {
|
||||||
|
const html = this.html.current.value;
|
||||||
|
const selection = this.selection.current.value
|
||||||
|
? (JSON.parse(this.selection.current.value) as SnapshotSelection)
|
||||||
|
: undefined;
|
||||||
|
const entityStates = this.entityStates.current.value
|
||||||
|
? (JSON.parse(this.entityStates.current.value) as EntityState[])
|
||||||
|
: undefined;
|
||||||
|
const logicalRootPath = this.logicalRootPath.current.value
|
||||||
|
? (JSON.parse(this.logicalRootPath.current.value) as number[])
|
||||||
|
: undefined;
|
||||||
|
const isDarkMode = !!this.isDarkColor.current.checked;
|
||||||
|
|
||||||
|
return {
|
||||||
|
html,
|
||||||
|
entityStates,
|
||||||
|
isDarkMode,
|
||||||
|
selection,
|
||||||
|
logicalRootPath,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private onClickRestoreSnapshot = () => {
|
||||||
|
const snapshot = this.getCurrentSnapshot();
|
||||||
|
|
||||||
|
this.props.onRestoreSnapshot(snapshot, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
private onCopy = () => {
|
||||||
|
const snapshot = this.getCurrentSnapshot();
|
||||||
|
const metadata = {
|
||||||
|
...snapshot.selection,
|
||||||
|
isDarkMode: snapshot.isDarkMode,
|
||||||
|
logicalRootPath: snapshot.logicalRootPath,
|
||||||
|
};
|
||||||
|
const textToCopy = snapshot.html + `<!--${JSON.stringify(metadata)}-->`;
|
||||||
|
|
||||||
|
navigator.clipboard.writeText(textToCopy);
|
||||||
|
};
|
||||||
|
|
||||||
|
private onPaste = (event: React.ClipboardEvent) => {
|
||||||
|
const str = event.clipboardData.getData('text/plain');
|
||||||
|
|
||||||
|
if (str) {
|
||||||
|
const idx = str.lastIndexOf('<!--');
|
||||||
|
|
||||||
|
if (idx >= 0 && str.endsWith('-->')) {
|
||||||
|
const html = str.substring(0, idx);
|
||||||
|
const json = str.substring(idx + 4, str.length - 3);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const metadata = JSON.parse(json);
|
||||||
|
const isDarkMode = !!metadata.isDarkMode;
|
||||||
|
const logicalRootPath = metadata.logicalRootPath;
|
||||||
|
|
||||||
|
delete metadata.isDarkMode;
|
||||||
|
delete metadata.logicalRootPath;
|
||||||
|
|
||||||
|
this.setSnapshot({
|
||||||
|
html: html,
|
||||||
|
entityStates: [],
|
||||||
|
isDarkMode,
|
||||||
|
selection: metadata as SnapshotSelection,
|
||||||
|
logicalRootPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
updateSnapshots(snapshots: Snapshot[], currentIndex: number, autoCompleteIndex: number) {
|
||||||
|
this.setState({
|
||||||
|
snapshots,
|
||||||
|
currentIndex,
|
||||||
|
autoCompleteIndex,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshotToString(snapshot: Snapshot) {
|
||||||
|
return (
|
||||||
|
snapshot.html +
|
||||||
|
(snapshot.selection ? `<!--${JSON.stringify(snapshot.selection)}-->` : '')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private takeSnapshot = () => {
|
||||||
|
const snapshot = this.props.onTakeSnapshot();
|
||||||
|
this.setSnapshot(snapshot);
|
||||||
|
};
|
||||||
|
|
||||||
|
private setSnapshot = (snapshot: Snapshot) => {
|
||||||
|
this.html.current.value = snapshot.html;
|
||||||
|
this.entityStates.current.value = snapshot.entityStates
|
||||||
|
? JSON.stringify(snapshot.entityStates)
|
||||||
|
: '';
|
||||||
|
this.selection.current.value = snapshot.selection ? JSON.stringify(snapshot.selection) : '';
|
||||||
|
this.logicalRootPath.current.value = snapshot.logicalRootPath
|
||||||
|
? JSON.stringify(snapshot.logicalRootPath)
|
||||||
|
: '';
|
||||||
|
this.isDarkColor.current.checked = !!snapshot.isDarkMode;
|
||||||
|
};
|
||||||
|
|
||||||
|
private renderItem = (snapshot: Snapshot, index: number) => {
|
||||||
|
let className = '';
|
||||||
|
if (index == this.state.currentIndex) {
|
||||||
|
className += ' ' + styles.current;
|
||||||
|
}
|
||||||
|
if (index == this.state.autoCompleteIndex) {
|
||||||
|
className += ' ' + styles.autoComplete;
|
||||||
|
}
|
||||||
|
|
||||||
|
const snapshotStr = this.snapshotToString(snapshot);
|
||||||
|
return (
|
||||||
|
<pre
|
||||||
|
className={className}
|
||||||
|
key={index}
|
||||||
|
onClick={() => this.setSnapshot(snapshot)}
|
||||||
|
onDoubleClick={() => this.props.onMove(index - this.state.currentIndex)}>
|
||||||
|
{(snapshotStr || '<EMPTY SNAPSHOT>').substring(0, 1000)}
|
||||||
|
</pre>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
@ -0,0 +1,85 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { IEditor, PluginEvent, Snapshot, Snapshots } from 'roosterjs-content-model-types';
|
||||||
|
import { SidePanePlugin } from '../SidePanePlugin';
|
||||||
|
import { SnapshotPane } from './SnapshotPane';
|
||||||
|
|
||||||
|
export class SnapshotPlugin implements SidePanePlugin {
|
||||||
|
private editor: IEditor;
|
||||||
|
private component: SnapshotPane;
|
||||||
|
|
||||||
|
constructor(private snapshots: Snapshots) {
|
||||||
|
this.snapshots.onChanged = () => this.updateSnapshots();
|
||||||
|
}
|
||||||
|
|
||||||
|
getName() {
|
||||||
|
return 'Snapshot';
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize(editor: IEditor) {
|
||||||
|
this.editor = editor;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
this.editor = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
onPluginEvent(e: PluginEvent) {
|
||||||
|
if (e.eventType == 'editorReady') {
|
||||||
|
this.updateSnapshots();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getTitle() {
|
||||||
|
return 'Undo Snapshots';
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSidePane() {
|
||||||
|
return <SnapshotPane {...this.getComponentProps()} ref={this.refCallback} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSnapshots() {
|
||||||
|
return this.snapshots;
|
||||||
|
}
|
||||||
|
|
||||||
|
private refCallback = (ref: SnapshotPane) => {
|
||||||
|
this.component = ref;
|
||||||
|
if (ref) {
|
||||||
|
this.updateSnapshots();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private getComponentProps() {
|
||||||
|
return {
|
||||||
|
onRestoreSnapshot: this.onRestoreSnapshot,
|
||||||
|
onTakeSnapshot: this.onTakeSnapshot,
|
||||||
|
onMove: this.onMove,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private onTakeSnapshot = (): Snapshot => {
|
||||||
|
return this.editor.takeSnapshot();
|
||||||
|
};
|
||||||
|
|
||||||
|
private onMove = (step: number) => {
|
||||||
|
const snapshotsManager = this.editor.getSnapshotsManager();
|
||||||
|
const snapshot = snapshotsManager.move(step);
|
||||||
|
this.onRestoreSnapshot(snapshot);
|
||||||
|
};
|
||||||
|
|
||||||
|
private onRestoreSnapshot = (snapshot: Snapshot) => {
|
||||||
|
this.editor.focus();
|
||||||
|
this.editor.restoreSnapshot(snapshot);
|
||||||
|
};
|
||||||
|
|
||||||
|
private updateSnapshots = () => {
|
||||||
|
if (!this.component) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.component.updateSnapshots(
|
||||||
|
this.snapshots.snapshots,
|
||||||
|
this.snapshots.currentIndex,
|
||||||
|
this.snapshots.autoCompleteIndex
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
@ -0,0 +1,61 @@
|
|||||||
|
import { MainPane } from '../mainPane/MainPane';
|
||||||
|
import type { RibbonButton } from 'roosterjs-react';
|
||||||
|
|
||||||
|
const styles = require('../mainPane/MainPane.scss');
|
||||||
|
|
||||||
|
export type tabNames = 'all' | 'text' | 'paragraph' | 'insert' | 'table' | 'image';
|
||||||
|
|
||||||
|
export type TabMainStringKey = 'tabNameMain';
|
||||||
|
export type TabTextStringKey = 'tabNameText';
|
||||||
|
export type TabParagraphStringKey = 'tabNameParagraph';
|
||||||
|
export type TabInsertStringKey = 'tabNameInsert';
|
||||||
|
export type TabTableStringKey = 'tabNameTable';
|
||||||
|
export type TabImageStringKey = 'tabNameImage';
|
||||||
|
|
||||||
|
export type AllTabStringKeys =
|
||||||
|
| TabTableStringKey
|
||||||
|
| TabTextStringKey
|
||||||
|
| TabParagraphStringKey
|
||||||
|
| TabInsertStringKey
|
||||||
|
| TabMainStringKey
|
||||||
|
| TabImageStringKey;
|
||||||
|
|
||||||
|
type TabData = {
|
||||||
|
key: AllTabStringKeys;
|
||||||
|
unlocalizedText: string;
|
||||||
|
name: tabNames;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TabNames: TabData[] = [
|
||||||
|
{ key: 'tabNameMain', unlocalizedText: 'Main', name: 'all' },
|
||||||
|
{ key: 'tabNameText', unlocalizedText: 'Text', name: 'text' },
|
||||||
|
{ key: 'tabNameParagraph', unlocalizedText: 'Paragraph', name: 'paragraph' },
|
||||||
|
{ key: 'tabNameInsert', unlocalizedText: 'Insert', name: 'insert' },
|
||||||
|
{ key: 'tabNameTable', unlocalizedText: 'Table', name: 'table' },
|
||||||
|
{ key: 'tabNameImage', unlocalizedText: 'Image', name: 'image' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function getTabs() {
|
||||||
|
const Tabs: RibbonButton<AllTabStringKeys>[] = [];
|
||||||
|
TabNames.forEach(tab => {
|
||||||
|
const tabButton: RibbonButton<AllTabStringKeys> = {
|
||||||
|
key: tab.key,
|
||||||
|
unlocalizedText: tab.unlocalizedText,
|
||||||
|
iconName: '',
|
||||||
|
onClick: () => {
|
||||||
|
MainPane.getInstance().changeRibbon(tab.name);
|
||||||
|
},
|
||||||
|
commandBarProperties: {
|
||||||
|
buttonStyles: {
|
||||||
|
label: { fontWeight: 'bold', paddingTop: '-5px' },
|
||||||
|
textContainer: { paddingBottom: '10px' },
|
||||||
|
},
|
||||||
|
iconOnly: false,
|
||||||
|
className: styles.menuTab,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
Tabs.push(tabButton);
|
||||||
|
});
|
||||||
|
|
||||||
|
return Tabs;
|
||||||
|
}
|
@ -0,0 +1,222 @@
|
|||||||
|
import { changeImageButton } from '../demoButtons/changeImageButton';
|
||||||
|
import { createFormatPainterButton } from '../demoButtons/formatPainterButton';
|
||||||
|
import { createImageEditButtons } from '../demoButtons/createImageEditButtons';
|
||||||
|
import { FormatPainterPlugin } from '../plugins/FormatPainterPlugin';
|
||||||
|
import { formatTableButton } from '../demoButtons/formatTableButton';
|
||||||
|
import { imageBorderColorButton } from '../demoButtons/imageBorderColorButton';
|
||||||
|
import { imageBorderRemoveButton } from '../demoButtons/imageBorderRemoveButton';
|
||||||
|
import { imageBorderStyleButton } from '../demoButtons/imageBorderStyleButton';
|
||||||
|
import { imageBorderWidthButton } from '../demoButtons/imageBorderWidthButton';
|
||||||
|
import { imageBoxShadowButton } from '../demoButtons/imageBoxShadowButton';
|
||||||
|
import { ImageEditor } from 'roosterjs-content-model-types';
|
||||||
|
import { listStartNumberButton } from '../demoButtons/listStartNumberButton';
|
||||||
|
import { pasteButton } from '../demoButtons/pasteButton';
|
||||||
|
import { setBulletedListStyleButton } from '../demoButtons/setBulletedListStyleButton';
|
||||||
|
import { setNumberedListStyleButton } from '../demoButtons/setNumberedListStyleButton';
|
||||||
|
import { setTableCellShadeButton } from '../demoButtons/setTableCellShadeButton';
|
||||||
|
import { spaceAfterButton, spaceBeforeButton } from '../demoButtons/spaceBeforeAfterButtons';
|
||||||
|
import { spacingButton } from '../demoButtons/spacingButton';
|
||||||
|
import { tableBorderApplyButton } from '../demoButtons/tableBorderApplyButton';
|
||||||
|
import { tableBorderColorButton } from '../demoButtons/tableBorderColorButton';
|
||||||
|
import { tableBorderStyleButton } from '../demoButtons/tableBorderStyleButton';
|
||||||
|
import { tableBorderWidthButton } from '../demoButtons/tableBorderWidthButton';
|
||||||
|
import { tableOptionsButton } from '../demoButtons/tableOptionsButton';
|
||||||
|
import { tableTitleButton } from '../demoButtons/tableTitleButton';
|
||||||
|
import { tabNames } from './getTabs';
|
||||||
|
import {
|
||||||
|
tableAlignCellButton,
|
||||||
|
tableAlignTableButton,
|
||||||
|
tableDeleteButton,
|
||||||
|
tableInsertButton,
|
||||||
|
tableMergeButton,
|
||||||
|
tableSplitButton,
|
||||||
|
} from '../demoButtons/tableEditButtons';
|
||||||
|
import type { RibbonButton } from 'roosterjs-react';
|
||||||
|
import {
|
||||||
|
alignCenterButton,
|
||||||
|
alignJustifyButton,
|
||||||
|
alignLeftButton,
|
||||||
|
alignRightButton,
|
||||||
|
backgroundColorButton,
|
||||||
|
blockQuoteButton,
|
||||||
|
boldButton,
|
||||||
|
bulletedListButton,
|
||||||
|
clearFormatButton,
|
||||||
|
codeButton,
|
||||||
|
decreaseFontSizeButton,
|
||||||
|
decreaseIndentButton,
|
||||||
|
fontButton,
|
||||||
|
fontSizeButton,
|
||||||
|
increaseFontSizeButton,
|
||||||
|
increaseIndentButton,
|
||||||
|
insertImageButton,
|
||||||
|
insertLinkButton,
|
||||||
|
insertTableButton,
|
||||||
|
italicButton,
|
||||||
|
ltrButton,
|
||||||
|
numberedListButton,
|
||||||
|
removeLinkButton,
|
||||||
|
rtlButton,
|
||||||
|
setHeadingLevelButton,
|
||||||
|
strikethroughButton,
|
||||||
|
subscriptButton,
|
||||||
|
superscriptButton,
|
||||||
|
textColorButton,
|
||||||
|
underlineButton,
|
||||||
|
} from 'roosterjs-react';
|
||||||
|
|
||||||
|
const textButtons: RibbonButton<any>[] = [
|
||||||
|
boldButton,
|
||||||
|
italicButton,
|
||||||
|
underlineButton,
|
||||||
|
fontButton,
|
||||||
|
fontSizeButton,
|
||||||
|
increaseFontSizeButton,
|
||||||
|
decreaseFontSizeButton,
|
||||||
|
textColorButton,
|
||||||
|
backgroundColorButton,
|
||||||
|
superscriptButton,
|
||||||
|
subscriptButton,
|
||||||
|
strikethroughButton,
|
||||||
|
];
|
||||||
|
|
||||||
|
const tableButtons: RibbonButton<any>[] = [
|
||||||
|
insertTableButton,
|
||||||
|
formatTableButton,
|
||||||
|
setTableCellShadeButton,
|
||||||
|
tableTitleButton,
|
||||||
|
tableOptionsButton,
|
||||||
|
tableInsertButton,
|
||||||
|
tableDeleteButton,
|
||||||
|
tableBorderApplyButton,
|
||||||
|
tableBorderColorButton,
|
||||||
|
tableBorderWidthButton,
|
||||||
|
tableBorderStyleButton,
|
||||||
|
tableMergeButton,
|
||||||
|
tableSplitButton,
|
||||||
|
tableAlignCellButton,
|
||||||
|
tableAlignTableButton,
|
||||||
|
];
|
||||||
|
|
||||||
|
const imageButtons: RibbonButton<any>[] = [
|
||||||
|
insertImageButton,
|
||||||
|
imageBorderColorButton,
|
||||||
|
imageBorderWidthButton,
|
||||||
|
imageBorderStyleButton,
|
||||||
|
imageBorderRemoveButton,
|
||||||
|
changeImageButton,
|
||||||
|
imageBoxShadowButton,
|
||||||
|
];
|
||||||
|
|
||||||
|
const insertButtons: RibbonButton<any>[] = [
|
||||||
|
insertLinkButton,
|
||||||
|
removeLinkButton,
|
||||||
|
insertTableButton,
|
||||||
|
insertImageButton,
|
||||||
|
];
|
||||||
|
|
||||||
|
const paragraphButtons: RibbonButton<any>[] = [
|
||||||
|
bulletedListButton,
|
||||||
|
numberedListButton,
|
||||||
|
decreaseIndentButton,
|
||||||
|
increaseIndentButton,
|
||||||
|
blockQuoteButton,
|
||||||
|
alignLeftButton,
|
||||||
|
alignCenterButton,
|
||||||
|
alignRightButton,
|
||||||
|
alignJustifyButton,
|
||||||
|
setHeadingLevelButton,
|
||||||
|
codeButton,
|
||||||
|
ltrButton,
|
||||||
|
rtlButton,
|
||||||
|
clearFormatButton,
|
||||||
|
setBulletedListStyleButton,
|
||||||
|
setNumberedListStyleButton,
|
||||||
|
listStartNumberButton,
|
||||||
|
spacingButton,
|
||||||
|
spaceBeforeButton,
|
||||||
|
spaceAfterButton,
|
||||||
|
pasteButton,
|
||||||
|
];
|
||||||
|
|
||||||
|
const allButtons: RibbonButton<any>[] = [
|
||||||
|
boldButton,
|
||||||
|
italicButton,
|
||||||
|
underlineButton,
|
||||||
|
fontButton,
|
||||||
|
fontSizeButton,
|
||||||
|
increaseFontSizeButton,
|
||||||
|
decreaseFontSizeButton,
|
||||||
|
textColorButton,
|
||||||
|
backgroundColorButton,
|
||||||
|
bulletedListButton,
|
||||||
|
numberedListButton,
|
||||||
|
decreaseIndentButton,
|
||||||
|
increaseIndentButton,
|
||||||
|
blockQuoteButton,
|
||||||
|
alignLeftButton,
|
||||||
|
alignCenterButton,
|
||||||
|
alignRightButton,
|
||||||
|
alignJustifyButton,
|
||||||
|
insertLinkButton,
|
||||||
|
removeLinkButton,
|
||||||
|
insertTableButton,
|
||||||
|
insertImageButton,
|
||||||
|
superscriptButton,
|
||||||
|
subscriptButton,
|
||||||
|
strikethroughButton,
|
||||||
|
setHeadingLevelButton,
|
||||||
|
codeButton,
|
||||||
|
ltrButton,
|
||||||
|
rtlButton,
|
||||||
|
clearFormatButton,
|
||||||
|
setBulletedListStyleButton,
|
||||||
|
setNumberedListStyleButton,
|
||||||
|
listStartNumberButton,
|
||||||
|
formatTableButton,
|
||||||
|
setTableCellShadeButton,
|
||||||
|
tableOptionsButton,
|
||||||
|
tableInsertButton,
|
||||||
|
tableDeleteButton,
|
||||||
|
tableMergeButton,
|
||||||
|
tableSplitButton,
|
||||||
|
tableTitleButton,
|
||||||
|
tableAlignCellButton,
|
||||||
|
tableAlignTableButton,
|
||||||
|
tableBorderApplyButton,
|
||||||
|
tableBorderColorButton,
|
||||||
|
tableBorderWidthButton,
|
||||||
|
tableBorderStyleButton,
|
||||||
|
imageBorderColorButton,
|
||||||
|
imageBorderWidthButton,
|
||||||
|
imageBorderStyleButton,
|
||||||
|
imageBorderRemoveButton,
|
||||||
|
changeImageButton,
|
||||||
|
imageBoxShadowButton,
|
||||||
|
spacingButton,
|
||||||
|
spaceBeforeButton,
|
||||||
|
spaceAfterButton,
|
||||||
|
pasteButton,
|
||||||
|
];
|
||||||
|
export function getButtons(
|
||||||
|
id: tabNames,
|
||||||
|
formatPlainerPlugin?: FormatPainterPlugin,
|
||||||
|
imageEditor?: ImageEditor
|
||||||
|
) {
|
||||||
|
switch (id) {
|
||||||
|
case 'text':
|
||||||
|
return [createFormatPainterButton(formatPlainerPlugin), ...textButtons];
|
||||||
|
case 'paragraph':
|
||||||
|
return paragraphButtons;
|
||||||
|
case 'insert':
|
||||||
|
return insertButtons;
|
||||||
|
case 'image':
|
||||||
|
return imageEditor
|
||||||
|
? [...imageButtons, ...createImageEditButtons(imageEditor)]
|
||||||
|
: imageButtons;
|
||||||
|
case 'table':
|
||||||
|
return tableButtons;
|
||||||
|
case 'all':
|
||||||
|
return [createFormatPainterButton(formatPlainerPlugin), ...allButtons];
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
$primaryColor: #cc6688;
|
||||||
|
$primaryLighter: lighten($primaryColor, 5%);
|
||||||
|
$primaryLighter2: lighten($primaryColor, 50%);
|
||||||
|
$primaryBorder: #cc6688;
|
||||||
|
$primaryBackgroundColor: white;
|
||||||
|
|
||||||
|
$primaryColorDark: #cb6587;
|
||||||
|
$primaryLighterDark: lighten($primaryColorDark, 5%);
|
||||||
|
$primaryLighter2Dark: lighten($primaryColorDark, 50%);
|
||||||
|
$primaryBorderDark: #cb6587;
|
||||||
|
$primaryBackgroundColorDark: #333333;
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
button {
|
||||||
|
background-color: $primaryColorDark;
|
||||||
|
color: $primaryLighter2;
|
||||||
|
border: solid 1px $primaryBorderDark;
|
||||||
|
}
|
||||||
|
|
||||||
|
select,
|
||||||
|
input,
|
||||||
|
textarea {
|
||||||
|
background-color: $primaryBackgroundColorDark;
|
||||||
|
color: $primaryLighter2;
|
||||||
|
border: solid 1px $primaryBorderDark;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,59 @@
|
|||||||
|
import { PartialTheme } from '@fluentui/react/lib/Theme';
|
||||||
|
|
||||||
|
const LightTheme: PartialTheme = {
|
||||||
|
palette: {
|
||||||
|
themePrimary: '#cc6688',
|
||||||
|
themeLighterAlt: '#080405',
|
||||||
|
themeLighter: '#211016',
|
||||||
|
themeLight: '#3d1f29',
|
||||||
|
themeTertiary: '#7a3d52',
|
||||||
|
themeSecondary: '#b45a78',
|
||||||
|
themeDarkAlt: '#d17392',
|
||||||
|
themeDark: '#d886a1',
|
||||||
|
themeDarker: '#e2a3b8',
|
||||||
|
neutralLighterAlt: '#f8f8f8',
|
||||||
|
neutralLighter: '#f4f4f4',
|
||||||
|
neutralLight: '#eaeaea',
|
||||||
|
neutralQuaternaryAlt: '#dadada',
|
||||||
|
neutralQuaternary: '#d0d0d0',
|
||||||
|
neutralTertiaryAlt: '#c8c8c8',
|
||||||
|
neutralTertiary: '#595959',
|
||||||
|
neutralSecondary: '#373737',
|
||||||
|
neutralPrimaryAlt: '#2f2f2f',
|
||||||
|
neutralPrimary: '#000000',
|
||||||
|
neutralDark: '#151515',
|
||||||
|
black: '#0b0b0b',
|
||||||
|
white: '#ffffff',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const DarkTheme: PartialTheme = {
|
||||||
|
palette: {
|
||||||
|
themePrimary: '#cb6587',
|
||||||
|
themeLighterAlt: '#fdf8fa',
|
||||||
|
themeLighter: '#f7e3ea',
|
||||||
|
themeLight: '#f0ccd8',
|
||||||
|
themeTertiary: '#e09db4',
|
||||||
|
themeSecondary: '#d27694',
|
||||||
|
themeDarkAlt: '#b85c7a',
|
||||||
|
themeDark: '#9b4e67',
|
||||||
|
themeDarker: '#72394c',
|
||||||
|
neutralLighterAlt: '#3c3c3c',
|
||||||
|
neutralLighter: '#444444',
|
||||||
|
neutralLight: '#515151',
|
||||||
|
neutralQuaternaryAlt: '#595959',
|
||||||
|
neutralQuaternary: '#5f5f5f',
|
||||||
|
neutralTertiaryAlt: '#7a7a7a',
|
||||||
|
neutralTertiary: '#c8c8c8',
|
||||||
|
neutralSecondary: '#d0d0d0',
|
||||||
|
neutralPrimaryAlt: '#dadada',
|
||||||
|
neutralPrimary: '#ffffff',
|
||||||
|
neutralDark: '#f4f4f4',
|
||||||
|
black: '#f8f8f8',
|
||||||
|
white: '#333333',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getTheme(isDark: boolean): PartialTheme {
|
||||||
|
return isDark ? DarkTheme : LightTheme;
|
||||||
|
}
|
@ -0,0 +1,58 @@
|
|||||||
|
@import '../theme/theme.scss';
|
||||||
|
|
||||||
|
.titleBar {
|
||||||
|
display: flex;
|
||||||
|
background-color: $primaryColor;
|
||||||
|
padding: 5px 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
font-size: 24pt;
|
||||||
|
font-family: Arial;
|
||||||
|
font-weight: bold;
|
||||||
|
font-style: italic;
|
||||||
|
color: white;
|
||||||
|
text-shadow: 2px 2px 2px black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
color: white;
|
||||||
|
font-family: Calibri;
|
||||||
|
font-size: 14pt;
|
||||||
|
margin: 10px 0 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.links {
|
||||||
|
color: white;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
text-align: right;
|
||||||
|
font-size: 14pt;
|
||||||
|
font-family: Calibri;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link {
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.externalLink {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.titleBar {
|
||||||
|
background-color: $primaryColorDark;
|
||||||
|
}
|
||||||
|
.title,
|
||||||
|
.link {
|
||||||
|
color: #bbd1e1;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,69 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
const styles = require('./TitleBar.scss');
|
||||||
|
const github = require('./iconmonstr-github-1.svg');
|
||||||
|
|
||||||
|
export interface TitleBarProps {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TitleBar extends React.Component<TitleBarProps, {}> {
|
||||||
|
render() {
|
||||||
|
const { className: baseClassName } = this.props;
|
||||||
|
const className = styles.titleBar + ' ' + (baseClassName || '');
|
||||||
|
const titleText = 'RoosterJs Demo Site';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
<div className={styles.title}>
|
||||||
|
<span className={styles.titleText}>{titleText}</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.version}></div>
|
||||||
|
<div className={styles.links}>
|
||||||
|
<a href="./legacyDemo/index.html" className={styles.link}>
|
||||||
|
Legacy demo
|
||||||
|
</a>
|
||||||
|
{' | '}
|
||||||
|
<a
|
||||||
|
href="https://github.com/Microsoft/roosterjs/wiki"
|
||||||
|
target="_blank"
|
||||||
|
className={styles.link}>
|
||||||
|
Wiki
|
||||||
|
</a>
|
||||||
|
{' | '}
|
||||||
|
<a href="docs/index.html" target="_blank" className={styles.link}>
|
||||||
|
References
|
||||||
|
</a>
|
||||||
|
{' | '}
|
||||||
|
<a href="coverage/index.html" target="_blank" className={styles.link}>
|
||||||
|
Test
|
||||||
|
</a>
|
||||||
|
{' | '}
|
||||||
|
<a
|
||||||
|
href="https://github.com/microsoft/roosterjs/actions/workflows/build-and-deploy.yml"
|
||||||
|
target="_blank">
|
||||||
|
<img
|
||||||
|
className={styles.externalLink}
|
||||||
|
src="https://github.com/microsoft/roosterjs/actions/workflows/build-and-deploy.yml/badge.svg"
|
||||||
|
alt="Build Status"
|
||||||
|
/>
|
||||||
|
</a>{' '}
|
||||||
|
<a href="http://badge.fury.io/js/roosterjs" target="_blank">
|
||||||
|
<img
|
||||||
|
src="https://badge.fury.io/js/roosterjs.svg"
|
||||||
|
alt="NPM Version"
|
||||||
|
className={styles.externalLink}
|
||||||
|
/>
|
||||||
|
</a>{' '}
|
||||||
|
<a
|
||||||
|
href="https://github.com/microsoft/roosterjs"
|
||||||
|
target="_blank"
|
||||||
|
className={styles.link}
|
||||||
|
title="RoosterJs on Github">
|
||||||
|
<img className={styles.externalLink} src={github} />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>
|
After Width: | Height: | Size: 814 B |
@ -0,0 +1,54 @@
|
|||||||
|
import { Stylesheet } from '@fluentui/merge-styles';
|
||||||
|
|
||||||
|
let isCssMonitorStarted: boolean = false;
|
||||||
|
const activeWindows: Window[] = [];
|
||||||
|
|
||||||
|
function startCssMonitor() {
|
||||||
|
if (!isCssMonitorStarted) {
|
||||||
|
isCssMonitorStarted = true;
|
||||||
|
Stylesheet.getInstance().setConfig({
|
||||||
|
onInsertRule: (cssText: string) => {
|
||||||
|
activeWindows.forEach(win => {
|
||||||
|
const style = win.document.createElement('style');
|
||||||
|
style.textContent = cssText;
|
||||||
|
win.document.head.appendChild(style);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerWindowForCss(win: Window) {
|
||||||
|
startCssMonitor();
|
||||||
|
|
||||||
|
activeWindows.push(win);
|
||||||
|
|
||||||
|
const styles = document.getElementsByTagName('STYLE');
|
||||||
|
const fragment = win.document.createDocumentFragment();
|
||||||
|
|
||||||
|
for (let i = 0; i < styles.length; i++) {
|
||||||
|
const style = win.document.createElement('style');
|
||||||
|
fragment.appendChild(style);
|
||||||
|
|
||||||
|
const originalStyle = styles[i] as HTMLStyleElement;
|
||||||
|
const rules = originalStyle.sheet.cssRules;
|
||||||
|
let cssText = '';
|
||||||
|
|
||||||
|
for (let j = 0; j < rules.length; j++) {
|
||||||
|
const rule = rules[j] as CSSStyleRule;
|
||||||
|
cssText += rule.cssText;
|
||||||
|
}
|
||||||
|
|
||||||
|
style.textContent = cssText;
|
||||||
|
}
|
||||||
|
|
||||||
|
win.document.head.appendChild(fragment);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unregisterWindowForCss(win: Window) {
|
||||||
|
const index = activeWindows.indexOf(win);
|
||||||
|
|
||||||
|
if (index >= 0) {
|
||||||
|
activeWindows.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
import * as DOMPurify from 'dompurify';
|
||||||
|
import { DOMCreator } from 'roosterjs-content-model-types';
|
||||||
|
|
||||||
|
export const trustedHTMLHandler: DOMCreator = {
|
||||||
|
htmlToDOM: (html: string) => {
|
||||||
|
return DOMPurify.sanitize(html, {
|
||||||
|
ADD_TAGS: ['head', 'meta', '#comment', 'iframe'],
|
||||||
|
ADD_ATTR: ['name', 'content'],
|
||||||
|
WHOLE_DOCUMENT: true,
|
||||||
|
ALLOW_UNKNOWN_PROTOCOLS: true,
|
||||||
|
RETURN_DOM: true,
|
||||||
|
}).ownerDocument;
|
||||||
|
},
|
||||||
|
};
|
Loading…
Reference in New Issue