diff --git a/.eslintrc.js b/.eslintrc.js index a393a5662f6..62b68460026 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -27,6 +27,7 @@ module.exports = { '@typescript-eslint', '@typescript-eslint/tslint', 'eslint-plugin-import', + 'etc', ], root: true, rules: { @@ -148,5 +149,6 @@ module.exports = { 'import/no-duplicates': 'error', 'prefer-const': 'error', 'no-var': 'error', + 'etc/no-const-enum': ['error', { allowLocal: true }], }, }; diff --git a/demo/scripts/controls/BuildInPluginState.ts b/demo/scripts/controls/BuildInPluginState.ts index 709a0818ab0..fb61f825b38 100644 --- a/demo/scripts/controls/BuildInPluginState.ts +++ b/demo/scripts/controls/BuildInPluginState.ts @@ -22,7 +22,6 @@ export interface BuildInPluginList { tableEditMenu: boolean; contextMenu: boolean; autoFormat: boolean; - contentModelPaste: boolean; announce: boolean; } diff --git a/demo/scripts/controls/ContentModelEditorMainPane.tsx b/demo/scripts/controls/ContentModelEditorMainPane.tsx index b6e9a6e5720..7ddadb797fc 100644 --- a/demo/scripts/controls/ContentModelEditorMainPane.tsx +++ b/demo/scripts/controls/ContentModelEditorMainPane.tsx @@ -11,18 +11,23 @@ import ContentModelRooster from './contentModel/editor/ContentModelRooster'; import ContentModelSnapshotPlugin from './sidePane/snapshot/ContentModelSnapshotPlugin'; import getToggleablePlugins from './getToggleablePlugins'; import MainPaneBase, { MainPaneBaseState } from './MainPaneBase'; +import RibbonPlugin from './ribbonButtons/contentModel/RibbonPlugin'; import SampleEntityPlugin from './sampleEntity/SampleEntityPlugin'; import SidePane from './sidePane/SidePane'; import TitleBar from './titleBar/TitleBar'; import { arrayPush } from 'roosterjs-editor-dom'; -import { ContentModelEditPlugin, EntityDelimiterPlugin } from 'roosterjs-content-model-plugins'; import { ContentModelRibbonPlugin } from './ribbonButtons/contentModel/ContentModelRibbonPlugin'; -import { ContentModelSegmentFormat, Snapshot } from 'roosterjs-content-model-types'; -import { createEmojiPlugin, createPasteOptionPlugin, RibbonPlugin } from 'roosterjs-react'; -import { EditorPlugin, Snapshots } from 'roosterjs-editor-types'; +import { ContentModelSegmentFormat, Snapshots } from 'roosterjs-content-model-types'; +import { createEmojiPlugin, createPasteOptionPlugin } from 'roosterjs-react'; +import { EditorPlugin } from 'roosterjs-editor-types'; import { getDarkColor } from 'roosterjs-color-utils'; import { PartialTheme } from '@fluentui/react/lib/Theme'; import { trustedHTMLHandler } from '../utils/trustedHTMLHandler'; +import { + ContentModelEditPlugin, + ContentModelPastePlugin, + EntityDelimiterPlugin, +} from 'roosterjs-content-model-plugins'; import { ContentModelEditor, ContentModelEditorOptions, @@ -103,8 +108,9 @@ class ContentModelEditorMainPane extends MainPaneBase private entityDelimiterPlugin: EntityDelimiterPlugin; private toggleablePlugins: EditorPlugin[] | null = null; private formatPainterPlugin: ContentModelFormatPainterPlugin; + private pastePlugin: ContentModelPastePlugin; private sampleEntityPlugin: SampleEntityPlugin; - private snapshots: Snapshots; + private snapshots: Snapshots; constructor(props: {}) { super(props); @@ -129,6 +135,7 @@ class ContentModelEditorMainPane extends MainPaneBase this.emojiPlugin = createEmojiPlugin(); this.entityDelimiterPlugin = new EntityDelimiterPlugin(); this.formatPainterPlugin = new ContentModelFormatPainterPlugin(); + this.pastePlugin = new ContentModelPastePlugin(); this.sampleEntityPlugin = new SampleEntityPlugin(); this.state = { showSidePane: window.location.hash != '', @@ -171,7 +178,7 @@ class ContentModelEditorMainPane extends MainPaneBase const plugins = [ ...this.toggleablePlugins, - this.contentModelRibbonPlugin, this.contentModelPanePlugin.getInnerRibbonPlugin(), - this.contentModelEditPlugin, this.pasteOptionPlugin, this.emojiPlugin, this.entityDelimiterPlugin, - this.formatPainterPlugin, this.sampleEntityPlugin, ]; @@ -243,13 +247,20 @@ class ContentModelEditorMainPane extends MainPaneBase
{this.state.editorCreator && ( { extends return this.instance; } + static readonly editorDivId = 'RoosterJsContentDiv'; + constructor(props: {}) { super(props); @@ -58,8 +59,6 @@ export default abstract class MainPaneBase extends abstract renderSidePane(fullWidth: boolean): JSX.Element; - abstract getPlugins(): EditorPlugin[]; - abstract resetEditor(): void; abstract getTheme(isDark: boolean): PartialTheme; diff --git a/demo/scripts/controls/StandaloneEditorMainPane.scss b/demo/scripts/controls/StandaloneEditorMainPane.scss new file mode 100644 index 00000000000..9d6e9f7a7a7 --- /dev/null +++ b/demo/scripts/controls/StandaloneEditorMainPane.scss @@ -0,0 +1,88 @@ +@import './theme/standaloneEditorTheme.scss'; + +.mainPane { + display: flex; + flex-direction: column; + width: 100%; + height: 100%; +} + +.noGrow { + flex: 0 0 auto; + overflow-x: hidden; +} + +.body { + flex: 1 1 auto; + position: relative; + display: flex; +} + +.editorContainer { + width: '100%'; + min-width: 200px; + flex-grow: 1; + flex-shrink: 1; + position: relative; +} + +@media (prefers-color-scheme: dark) { + .editorContainer { + a:link, + a:visited { + color: #ba7cff; + } + } +} + +.editor { + border: solid 1px $primaryBorder; + overflow: auto; + padding: 10px; + outline: none; + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 0; +} + +.resizer { + flex-grow: 0; + flex-shrink: 0; + width: 6px; + cursor: col-resize; + &:hover { + background-color: #ccc; + } +} + +.showSidePane { + flex-grow: 0; + flex-shrink: 0; + width: 30px; + cursor: hand; + white-space: nowrap; + div { + transform: rotate(-90deg); + } + &:hover { + background-color: #ccc; + } +} + +.sidePane { + min-width: 340px; + flex-shrink: 0; + flex-grow: 0; + width: 300px; + &.sidePaneFullWidth { + width: 100%; + } +} + +@media (prefers-color-scheme: dark) { + .editor { + border: solid 1px $primaryBorderDark; + } +} diff --git a/demo/scripts/controls/StandaloneEditorMainPane.tsx b/demo/scripts/controls/StandaloneEditorMainPane.tsx new file mode 100644 index 00000000000..687308db6cb --- /dev/null +++ b/demo/scripts/controls/StandaloneEditorMainPane.tsx @@ -0,0 +1,250 @@ +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; +import ApiPlaygroundPlugin from './sidePane/contentModelApiPlayground/ApiPlaygroundPlugin'; +import ContentModelEditorOptionsPlugin from './sidePane/editorOptions/ContentModelEditorOptionsPlugin'; +import ContentModelEventViewPlugin from './sidePane/eventViewer/ContentModelEventViewPlugin'; +import ContentModelFormatPainterPlugin from './contentModel/plugins/ContentModelFormatPainterPlugin'; +import ContentModelFormatStatePlugin from './sidePane/formatState/ContentModelFormatStatePlugin'; +import ContentModelPanePlugin from './sidePane/contentModel/ContentModelPanePlugin'; +import ContentModelRibbon from './ribbonButtons/contentModel/ContentModelRibbon'; +import ContentModelRooster from './contentModel/editor/ContentModelRooster'; +import ContentModelSnapshotPlugin from './sidePane/snapshot/ContentModelSnapshotPlugin'; +import MainPaneBase, { MainPaneBaseState } from './MainPaneBase'; +import RibbonPlugin from './ribbonButtons/contentModel/RibbonPlugin'; +import SidePane from './sidePane/SidePane'; +import TitleBar from './titleBar/TitleBar'; +import { ContentModelEditPlugin } from 'roosterjs-content-model-plugins'; +import { ContentModelRibbonPlugin } from './ribbonButtons/contentModel/ContentModelRibbonPlugin'; +import { getDarkColor } from 'roosterjs-color-utils'; +import { PartialTheme } from '@fluentui/react/lib/Theme'; +import { Snapshots } from 'roosterjs-editor-types'; +import { StandaloneEditor } from 'roosterjs-content-model-core'; +import { trustedHTMLHandler } from '../utils/trustedHTMLHandler'; +import { + ContentModelSegmentFormat, + IStandaloneEditor, + Snapshot, + StandaloneEditorOptions, +} from 'roosterjs-content-model-types'; + +const styles = require('./StandaloneEditorMainPane.scss'); + +const LightTheme: PartialTheme = { + palette: { + themePrimary: '#4466aa', + themeLighterAlt: '#f6f8fc', + themeLighter: '#dae2f2', + themeLight: '#bccae6', + themeTertiary: '#839bcd', + themeSecondary: '#5575b5', + themeDarkAlt: '#3e5c9a', + themeDark: '#344e82', + themeDarker: '#263960', + neutralLighterAlt: '#faf9f8', + neutralLighter: '#f3f2f1', + neutralLight: '#edebe9', + neutralQuaternaryAlt: '#e1dfdd', + neutralQuaternary: '#d0d0d0', + neutralTertiaryAlt: '#c8c6c4', + neutralTertiary: '#c2c2c2', + neutralSecondary: '#858585', + neutralPrimaryAlt: '#4b4b4b', + neutralPrimary: '#333333', + neutralDark: '#272727', + black: '#1d1d1d', + white: '#ffffff', + }, +}; + +const DarkTheme: PartialTheme = { + palette: { + themePrimary: '#335599', + themeLighterAlt: '#f4f6fb', + themeLighter: '#d5deef', + themeLight: '#b3c2e0', + themeTertiary: '#748ec2', + themeSecondary: '#4464a5', + themeDarkAlt: '#2d4c8a', + themeDark: '#264074', + themeDarker: '#1c2f56', + neutralLighterAlt: '#faf9f8', + neutralLighter: '#f3f2f1', + neutralLight: '#edebe9', + neutralQuaternaryAlt: '#e1dfdd', + neutralQuaternary: '#d0d0d0', + neutralTertiaryAlt: '#c8c6c4', + neutralTertiary: '#c2c2c2', + neutralSecondary: '#858585', + neutralPrimaryAlt: '#4b4b4b', + neutralPrimary: '#333333', + neutralDark: '#272727', + black: '#1d1d1d', + white: '#ffffff', + }, +}; + +interface ContentModelMainPaneState extends MainPaneBaseState { + editorCreator: (div: HTMLDivElement, options: StandaloneEditorOptions) => IStandaloneEditor; +} + +class ContentModelEditorMainPane extends MainPaneBase { + private formatStatePlugin: ContentModelFormatStatePlugin; + private editorOptionPlugin: ContentModelEditorOptionsPlugin; + private eventViewPlugin: ContentModelEventViewPlugin; + private apiPlaygroundPlugin: ApiPlaygroundPlugin; + private contentModelPanePlugin: ContentModelPanePlugin; + private contentModelEditPlugin: ContentModelEditPlugin; + private contentModelRibbonPlugin: RibbonPlugin; + private snapshotPlugin: ContentModelSnapshotPlugin; + private formatPainterPlugin: ContentModelFormatPainterPlugin; + private snapshots: Snapshots; + + constructor(props: {}) { + super(props); + + this.snapshots = { + snapshots: [], + totalSize: 0, + currentIndex: -1, + autoCompleteIndex: -1, + maxSize: 1e7, + }; + + this.formatStatePlugin = new ContentModelFormatStatePlugin(); + this.editorOptionPlugin = new ContentModelEditorOptionsPlugin(); + this.eventViewPlugin = new ContentModelEventViewPlugin(); + this.apiPlaygroundPlugin = new ApiPlaygroundPlugin(); + this.snapshotPlugin = new ContentModelSnapshotPlugin(this.snapshots); + this.contentModelPanePlugin = new ContentModelPanePlugin(); + this.contentModelEditPlugin = new ContentModelEditPlugin(); + this.contentModelRibbonPlugin = new ContentModelRibbonPlugin(); + this.formatPainterPlugin = new ContentModelFormatPainterPlugin(); + 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', + }, + }; + } + + getStyles(): Record { + return styles; + } + + renderTitleBar() { + return ; + } + + renderRibbon(isPopout: boolean) { + return ( + + ); + } + + renderSidePane(fullWidth: boolean) { + const styles = this.getStyles(); + + return ( + + ); + } + + resetEditor() { + this.setState({ + editorCreator: (div: HTMLDivElement, options: StandaloneEditorOptions) => + new StandaloneEditor(div, { + ...options, + cacheModel: this.state.initState.cacheModel, + }), + }); + } + + renderEditor() { + const styles = this.getStyles(); + 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 format = this.state.initState.defaultFormat; + const defaultFormat: ContentModelSegmentFormat = { + fontWeight: format.bold ? 'bold' : undefined, + italic: format.italic || undefined, + underline: format.underline || undefined, + fontFamily: format.fontFamily || undefined, + fontSize: format.fontSize || undefined, + textColor: format.textColors?.lightModeColor || format.textColor || undefined, + backgroundColor: + format.backgroundColors?.lightModeColor || format.backgroundColor || undefined, + }; + + this.updateContentPlugin.forceUpdate(); + + return ( +
+
+ {this.state.editorCreator && ( + + )} +
+
+ ); + } + + getTheme(isDark: boolean): PartialTheme { + return isDark ? DarkTheme : LightTheme; + } + + private getSidePanePlugins() { + return [ + this.formatStatePlugin, + this.editorOptionPlugin, + this.eventViewPlugin, + this.apiPlaygroundPlugin, + this.snapshotPlugin, + this.contentModelPanePlugin, + ]; + } +} + +export function mount(parent: HTMLElement) { + ReactDOM.render(, parent); +} diff --git a/demo/scripts/controls/contentModel/components/format/BlockFormatView.tsx b/demo/scripts/controls/contentModel/components/format/BlockFormatView.tsx index 34598e2ef72..a4e0621e669 100644 --- a/demo/scripts/controls/contentModel/components/format/BlockFormatView.tsx +++ b/demo/scripts/controls/contentModel/components/format/BlockFormatView.tsx @@ -10,6 +10,7 @@ import { LineHeightFormatRenderer } from './formatPart/LineHeightFormatRenderer' import { MarginFormatRenderer } from './formatPart/MarginFormatRenderer'; import { PaddingFormatRenderer } from './formatPart/PaddingFormatRenderer'; import { TextAlignFormatRenderer } from './formatPart/TextAlignFormatRenderer'; +import { TextIndentFormatRenderer } from './formatPart/TextIndentFormatRenderer'; import { WhiteSpaceFormatRenderer } from './formatPart/WhiteSpaceFormatRenderer'; const BlockFormatRenders: FormatRenderer[] = [ @@ -21,6 +22,7 @@ const BlockFormatRenders: FormatRenderer[] = [ PaddingFormatRenderer, LineHeightFormatRenderer, WhiteSpaceFormatRenderer, + TextIndentFormatRenderer, ...BorderFormatRenderers, ]; diff --git a/demo/scripts/controls/contentModel/components/format/formatPart/TextIndentFormatRenderer.ts b/demo/scripts/controls/contentModel/components/format/formatPart/TextIndentFormatRenderer.ts new file mode 100644 index 00000000000..622deb0a95c --- /dev/null +++ b/demo/scripts/controls/contentModel/components/format/formatPart/TextIndentFormatRenderer.ts @@ -0,0 +1,11 @@ +import { createTextFormatRenderer } from '../utils/createTextFormatRenderer'; +import { FormatRenderer } from '../utils/FormatRenderer'; +import { TextIndentFormat } from 'roosterjs-content-model-types'; + +export const TextIndentFormatRenderer: FormatRenderer = createTextFormatRenderer< + TextIndentFormat +>( + 'Text indent', + format => format.textIndent, + (format, value) => (format.textIndent = value) +); diff --git a/demo/scripts/controls/contentModel/editor/ContentModelRooster.tsx b/demo/scripts/controls/contentModel/editor/ContentModelRooster.tsx index eeaa8475c7f..d31481a5692 100644 --- a/demo/scripts/controls/contentModel/editor/ContentModelRooster.tsx +++ b/demo/scripts/controls/contentModel/editor/ContentModelRooster.tsx @@ -1,13 +1,14 @@ import * as React from 'react'; -import { createUIUtilities, ReactEditorPlugin } from 'roosterjs-react'; +import { ContentModelEditor, ContentModelEditorOptions } from 'roosterjs-content-model-editor'; +import { createUIUtilities, ReactEditorPlugin, UIUtilities } from 'roosterjs-react'; import { divProperties, getNativeProps } from '@fluentui/react/lib/Utilities'; import { useTheme } from '@fluentui/react/lib/Theme'; import { - ContentModelEditor, - ContentModelEditorOptions, - IContentModelEditor, -} from 'roosterjs-content-model-editor'; -import type { EditorPlugin } from 'roosterjs-editor-types'; + EditorPlugin, + IStandaloneEditor, + StandaloneEditorOptions, +} from 'roosterjs-content-model-types'; +import type { EditorPlugin as LegacyEditorPlugin } from 'roosterjs-editor-types'; /** * Properties for Rooster react component @@ -19,10 +20,7 @@ export interface ContentModelRoosterProps * Creator function used for creating the instance of roosterjs editor. * Use this callback when you have your own sub class of roosterjs Editor or force trigging a reset of editor */ - editorCreator?: ( - div: HTMLDivElement, - options: ContentModelEditorOptions - ) => IContentModelEditor; + editorCreator?: (div: HTMLDivElement, options: StandaloneEditorOptions) => IStandaloneEditor; /** * Whether editor should get focus once it is created @@ -38,20 +36,17 @@ export interface ContentModelRoosterProps */ export default function ContentModelRooster(props: ContentModelRoosterProps) { const editorDiv = React.useRef(null); - const editor = React.useRef(null); + const editor = React.useRef(null); const theme = useTheme(); - const { focusOnInit, editorCreator, zoomScale, inDarkMode, plugins } = props; + const { focusOnInit, editorCreator, zoomScale, inDarkMode, plugins, legacyPlugins } = props; React.useEffect(() => { - if (plugins && editorDiv.current) { + if (editorDiv.current) { const uiUtilities = createUIUtilities(editorDiv.current, theme); - plugins.forEach(plugin => { - if (isReactEditorPlugin(plugin)) { - plugin.setUIUtilities(uiUtilities); - } - }); + setUIUtilities(uiUtilities, plugins); + setUIUtilities(uiUtilities, legacyPlugins); } }, [theme, editorCreator]); @@ -86,10 +81,23 @@ export default function ContentModelRooster(props: ContentModelRoosterProps) { return
; } +function setUIUtilities( + uiUtilities: UIUtilities, + plugins: (LegacyEditorPlugin | EditorPlugin)[] | undefined +) { + plugins?.forEach(plugin => { + if (isReactEditorPlugin(plugin)) { + plugin.setUIUtilities(uiUtilities); + } + }); +} + function defaultEditorCreator(div: HTMLDivElement, options: ContentModelEditorOptions) { return new ContentModelEditor(div, options); } -function isReactEditorPlugin(plugin: EditorPlugin): plugin is ReactEditorPlugin { +function isReactEditorPlugin( + plugin: LegacyEditorPlugin | EditorPlugin +): plugin is ReactEditorPlugin { return !!(plugin as ReactEditorPlugin)?.setUIUtilities; } diff --git a/demo/scripts/controls/contentModel/plugins/ContentModelFormatPainterPlugin.ts b/demo/scripts/controls/contentModel/plugins/ContentModelFormatPainterPlugin.ts index 616096e1d59..a5ad17cfb36 100644 --- a/demo/scripts/controls/contentModel/plugins/ContentModelFormatPainterPlugin.ts +++ b/demo/scripts/controls/contentModel/plugins/ContentModelFormatPainterPlugin.ts @@ -1,73 +1,83 @@ +import MainPaneBase from '../../MainPaneBase'; import { applySegmentFormat, getFormatState } from 'roosterjs-content-model-api'; -import { ContentModelSegmentFormat } from 'roosterjs-content-model-types'; -import { EditorPlugin, IEditor, PluginEvent, PluginEventType } from 'roosterjs-editor-types'; -import { IContentModelEditor } from 'roosterjs-content-model-editor'; +import { + ContentModelSegmentFormat, + EditorPlugin, + IStandaloneEditor, + PluginEvent, +} from 'roosterjs-content-model-types'; const FORMATPAINTERCURSOR_SVG = require('./formatpaintercursor.svg'); -const FORMATPAINTERCURSOR_STYLE = `;cursor: url("${FORMATPAINTERCURSOR_SVG}") 8.5 16, auto`; -const CURSOR_REGEX = /;?\s*cursor:\s*url\(\".*?\"\)[^;]*/gi; - -interface FormatPainterFormatHolder { - format: ContentModelSegmentFormat | null; -} +const FORMATPAINTERCURSOR_STYLE = `cursor: url("${FORMATPAINTERCURSOR_SVG}") 8.5 16, auto`; export default class ContentModelFormatPainterPlugin implements EditorPlugin { - private editor: IContentModelEditor | null = null; + private editor: IStandaloneEditor | null = null; + private styleNode: HTMLStyleElement | null = null; + private painterFormat: ContentModelSegmentFormat | null = null; + private static instance: ContentModelFormatPainterPlugin | undefined; + + constructor() { + ContentModelFormatPainterPlugin.instance = this; + } getName() { return 'FormatPainter'; } - initialize(editor: IEditor) { - this.editor = editor as IContentModelEditor; + initialize(editor: IStandaloneEditor) { + this.editor = editor; + + const doc = this.editor.getDocument(); + this.styleNode = doc.createElement('style'); + + doc.head.appendChild(this.styleNode); } dispose() { this.editor = null; + + if (this.styleNode) { + this.styleNode.parentNode?.removeChild(this.styleNode); + this.styleNode = null; + } } onPluginEvent(event: PluginEvent) { - if (this.editor && event.eventType == PluginEventType.MouseUp) { - const formatHolder = getFormatHolder(this.editor); + if (this.editor && event.eventType == 'mouseUp') { + if (this.painterFormat) { + applySegmentFormat(this.editor, this.painterFormat); - if (formatHolder.format) { - applySegmentFormat(this.editor, formatHolder.format); - formatHolder.format = null; - - setFormatPainterCursor(this.editor, false /*isOn*/); + this.setFormatPainterCursor(null); } } } - static startFormatPainter(editor: IContentModelEditor) { - const formatHolder = getFormatHolder(editor); - const format = getSegmentFormat(editor); + private setFormatPainterCursor(format: ContentModelSegmentFormat | null) { + const sheet = this.styleNode.sheet; - if (format) { - formatHolder.format = { ...format }; - setFormatPainterCursor(editor, true /*isOn*/); + if (this.painterFormat) { + for (let i = sheet.cssRules.length - 1; i >= 0; i--) { + sheet.deleteRule(i); + } } - } -} - -function getFormatHolder(editor: IContentModelEditor): FormatPainterFormatHolder { - return editor.getCustomData('__FormatPainterFormat', () => { - return {} as FormatPainterFormatHolder; - }); -} -function setFormatPainterCursor(editor: IContentModelEditor, isOn: boolean) { - let styles = editor.getEditorDomAttribute('style') || ''; - styles = styles.replace(CURSOR_REGEX, ''); + this.painterFormat = format; - if (isOn) { - styles += FORMATPAINTERCURSOR_STYLE; + if (this.painterFormat) { + sheet.insertRule(`#${MainPaneBase.editorDivId} {${FORMATPAINTERCURSOR_STYLE}}`); + } } - editor.setEditorDomAttribute('style', styles); + static startFormatPainter() { + const format = getSegmentFormat(this.instance.editor); + + if (format) { + this.instance.setFormatPainterCursor(format); + } + } } -function getSegmentFormat(editor: IContentModelEditor): ContentModelSegmentFormat { +function getSegmentFormat(editor: IStandaloneEditor): ContentModelSegmentFormat { const formatState = getFormatState(editor); return { diff --git a/demo/scripts/controls/getToggleablePlugins.ts b/demo/scripts/controls/getToggleablePlugins.ts index 501dc6d1c28..9a3512a312c 100644 --- a/demo/scripts/controls/getToggleablePlugins.ts +++ b/demo/scripts/controls/getToggleablePlugins.ts @@ -2,7 +2,6 @@ import BuildInPluginState, { BuildInPluginList, UrlPlaceholder } from './BuildIn import { Announce } from 'roosterjs-editor-plugins/lib/Announce'; import { AutoFormat } from 'roosterjs-editor-plugins/lib/AutoFormat'; import { ContentEdit } from 'roosterjs-editor-plugins/lib/ContentEdit'; -import { ContentModelPastePlugin } from 'roosterjs-content-model-plugins'; import { CustomReplace as CustomReplacePlugin } from 'roosterjs-editor-plugins/lib/CustomReplace'; import { CutPasteListChain } from 'roosterjs-editor-plugins/lib/CutPasteListChain'; import { EditorPlugin, KnownAnnounceStrings } from 'roosterjs-editor-types'; @@ -60,7 +59,6 @@ export default function getToggleablePlugins(initState: BuildInPluginState) { ? createTableEditMenuProvider() : null, contextMenu: pluginList.contextMenu ? createContextMenuPlugin() : null, - contentModelPaste: pluginList.contentModelPaste ? new ContentModelPastePlugin() : null, announce: pluginList.announce ? new Announce(getDefaultStringsMap()) : null, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/ContentModelRibbon.tsx b/demo/scripts/controls/ribbonButtons/contentModel/ContentModelRibbon.tsx index fdd99c1f47c..f083a761cc5 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/ContentModelRibbon.tsx +++ b/demo/scripts/controls/ribbonButtons/contentModel/ContentModelRibbon.tsx @@ -1,4 +1,6 @@ import * as React from 'react'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; +import RibbonPlugin from './RibbonPlugin'; import { alignCenterButton } from './alignCenterButton'; import { alignJustifyButton } from './alignJustifyButton'; import { alignLeftButton } from './alignLeftButton'; @@ -10,14 +12,20 @@ import { bulletedListButton } from './bulletedListButton'; import { changeImageButton } from './changeImageButton'; import { clearFormatButton } from './clearFormatButton'; import { codeButton } from './codeButton'; -import { darkMode } from '../darkMode'; +import { CommandBar, ICommandBarItemProps, ICommandBarProps } from '@fluentui/react/lib/CommandBar'; +import { darkMode } from './darkMode'; import { decreaseFontSizeButton } from './decreaseFontSizeButton'; import { decreaseIndentButton } from './decreaseIndentButton'; -import { exportContent } from '../export'; +import { exportContent } from './export'; +import { FocusZoneDirection } from '@fluentui/react/lib/FocusZone'; import { fontButton } from './fontButton'; import { fontSizeButton } from './fontSizeButton'; import { formatPainterButton } from './formatPainterButton'; +import { FormatState } from 'roosterjs-editor-types'; import { formatTableButton } from './formatTableButton'; +import { getLocalizedString, LocalizedStrings } from 'roosterjs-react'; +import { getObjectKeys } from 'roosterjs-content-model-dom'; +import { IContextualMenuItem, IContextualMenuItemProps } from '@fluentui/react/lib/ContextualMenu'; import { imageBorderColorButton } from './imageBorderColorButton'; import { imageBorderRemoveButton } from './imageBorderRemoveButton'; import { imageBorderStyleButton } from './imageBorderStyleButton'; @@ -28,15 +36,17 @@ import { increaseIndentButton } from './increaseIndentButton'; import { insertImageButton } from './insertImageButton'; import { insertLinkButton } from './insertLinkButton'; import { insertTableButton } from './insertTableButton'; +import { IRenderFunction } from '@fluentui/react/lib/Utilities'; import { italicButton } from './italicButton'; import { listStartNumberButton } from './listStartNumberButton'; import { ltrButton } from './ltrButton'; +import { mergeStyles } from '@fluentui/react/lib/Styling'; +import { moreCommands } from './moreCommands'; import { numberedListButton } from './numberedListButton'; import { pasteButton } from './pasteButton'; -import { popout } from '../popout'; +import { popout } from './popout'; import { redoButton } from './redoButton'; import { removeLinkButton } from './removeLinkButton'; -import { Ribbon, RibbonButton, RibbonPlugin } from 'roosterjs-react'; import { rtlButton } from './rtlButton'; import { setBulletedListStyleButton } from './setBulletedListStyleButton'; import { setHeadingLevelButton } from './setHeadingLevelButton'; @@ -55,7 +65,7 @@ import { tableBorderWidthButton } from './tableBorderWidthButton'; import { textColorButton } from './textColorButton'; import { underlineButton } from './underlineButton'; import { undoButton } from './undoButton'; -import { zoom } from '../zoom'; +import { zoom } from './zoom'; import { tableAlignCellButton, tableAlignTableButton, @@ -65,7 +75,7 @@ import { tableSplitButton, } from './tableEditButtons'; -const buttons = [ +const buttons: ContentModelRibbonButton[] = [ formatPainterButton, boldButton, italicButton, @@ -127,6 +137,172 @@ const buttons = [ pasteButton, ]; +const ribbonClassName = mergeStyles({ + '& .ms-CommandBar': { + padding: '0px', + }, +}); + +const rtlIcon = mergeStyles({ + transform: 'scaleX(-1)', +}); + +interface RibbonProps extends Partial { + /** + * The ribbon plugin used for connect editor and the ribbon + */ + plugin: RibbonPlugin; + + /** + * Buttons in this ribbon + */ + buttons: ContentModelRibbonButton[]; + + /** + * A dictionary of localized strings for all buttons. + * Key of the dictionary is the key of each button, value will be the string or a function to return the string + */ + strings?: LocalizedStrings; +} + +/** + * The format ribbon component of roosterjs-react + * @param props Properties of format ribbon component + * @returns The format ribbon component + */ +function Ribbon(props: RibbonProps) { + const { plugin, buttons, strings, dir } = props; + const [formatState, setFormatState] = React.useState(null); + const isRtl = dir == 'rtl'; + + const onClick = React.useCallback( + (_, item?: IContextualMenuItem) => { + if (item) { + plugin?.onButtonClick(item.data, item.key, strings); + } + }, + [plugin, strings] + ); + + const onHover = React.useCallback( + (button: ContentModelRibbonButton, key: string) => { + plugin.startLivePreview(button, key as T, strings); + }, + [plugin, strings] + ); + + const onDismiss = React.useCallback(() => { + plugin.stopLivePreview(); + }, [plugin]); + + const flipIcon = React.useCallback( + ( + props?: IContextualMenuItemProps, + defaultRender?: (props?: IContextualMenuItemProps) => JSX.Element | null + ): JSX.Element | null => { + if (!defaultRender) { + return null; + } + return {defaultRender(props)}; + }, + [] + ); + + const commandBarItems = React.useMemo((): ICommandBarItemProps[] => { + return buttons.map( + (button): ICommandBarItemProps => { + const selectedItem = + formatState && button.dropDownMenu?.getSelectedItemKey?.(formatState); + const dropDownMenu = button.dropDownMenu; + + const result: ICommandBarItemProps = { + key: button.key, + data: button, + iconProps: { + iconName: button.iconName, + }, + onRenderIcon: isRtl && button.flipWhenRtl ? flipIcon : undefined, + iconOnly: true, + text: getLocalizedString(strings, button.key, button.unlocalizedText), + ariaLabel: getLocalizedString(strings, button.key, button.unlocalizedText), + canCheck: true, + checked: (formatState && button.isChecked?.(formatState)) || false, + disabled: (formatState && button.isDisabled?.(formatState)) || false, + ...(button.commandBarProperties || {}), + }; + + const contextMenuItemRenderer: IRenderFunction = ( + props, + defaultRenderer + ) => + props && defaultRenderer ? ( +
onHover(button, props.key)}> + {defaultRenderer(props)} +
+ ) : null; + + if (dropDownMenu) { + result.subMenuProps = { + shouldFocusOnMount: true, + focusZoneProps: { direction: FocusZoneDirection.bidirectional }, + onMenuDismissed: onDismiss, + onItemClick: onClick, + onRenderContextualMenuItem: dropDownMenu.allowLivePreview + ? contextMenuItemRenderer + : undefined, + items: getObjectKeys(dropDownMenu.items).map(key => ({ + key: key, + text: getLocalizedString( + strings, + key, + dropDownMenu.items[key] + ), + data: button, + canCheck: !!dropDownMenu.getSelectedItemKey, + checked: selectedItem == key || false, + className: dropDownMenu.itemClassName, + onRender: dropDownMenu.itemRender + ? item => dropDownMenu.itemRender!(item, onClick) + : undefined, + })), + ...(dropDownMenu.commandBarSubMenuProperties || {}), + }; + } else { + result.onClick = onClick; + } + + return result; + } + ); + }, [buttons, formatState, isRtl, strings, onClick, onDismiss, onHover]); + + React.useEffect(() => { + const disposer = plugin?.registerFormatChangedCallback(setFormatState); + + return () => { + disposer?.(); + }; + }, [plugin]); + + const moreCommandsBtn = moreCommands as ContentModelRibbonButton; + + return ( + ( + strings, + moreCommandsBtn.key, + moreCommandsBtn.unlocalizedText + ), + ...props?.overflowButtonProps, + }} + /> + ); +} + export default function ContentModelRibbon(props: { ribbonPlugin: RibbonPlugin; isRtl: boolean; @@ -134,7 +310,7 @@ export default function ContentModelRibbon(props: { }) { const { ribbonPlugin, isRtl, isInPopout } = props; const ribbonButtons = React.useMemo(() => { - const result: RibbonButton[] = [...buttons, darkMode, zoom, exportContent]; + const result: ContentModelRibbonButton[] = [...buttons, darkMode, zoom, exportContent]; if (!isInPopout) { result.push(popout); diff --git a/demo/scripts/controls/ribbonButtons/contentModel/ContentModelRibbonButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/ContentModelRibbonButton.ts new file mode 100644 index 00000000000..a31a7845057 --- /dev/null +++ b/demo/scripts/controls/ribbonButtons/contentModel/ContentModelRibbonButton.ts @@ -0,0 +1,74 @@ +import { LocalizedStrings, RibbonButtonDropDown, UIUtilities } from 'roosterjs-react'; +import type { IStandaloneEditor } from 'roosterjs-content-model-types'; +import type { FormatState } from 'roosterjs-editor-types'; +import type { ICommandBarItemProps } from '@fluentui/react/lib/CommandBar'; + +/** + * Represents a button on format ribbon + */ +export default interface ContentModelRibbonButton { + /** + * key of this button, needs to be unique + */ + key: T; + + /** + * Name of button icon. See https://developer.microsoft.com/en-us/fluentui#/styles/web/icons for all icons + */ + iconName: string; + + /** + * True if we need to flip the icon when render in Right-to-left page + */ + flipWhenRtl?: boolean; + + /** + * Text of the button. This text is not localized. To show a localized text, pass a dictionary to Ribbon component via RibbonProps.strings. + */ + unlocalizedText: string; + + /** + * Click handler of this button. + * @param editor the editor instance + * @param key key of the button that is clicked + * @param strings localized strings used by any UI element of this click handler + * @param uiUtilities a utilities object to help render addition UI elements + */ + onClick: ( + editor: IStandaloneEditor, + key: T, + strings: LocalizedStrings | undefined, + uiUtilities: UIUtilities + ) => void; + + /** + * Get if the current button should be checked + * @param formatState The current formatState of editor + * @returns True to show the button in a checked state, otherwise false + * @default False When not specified, it is treated as always returning false + */ + isChecked?: (formatState: FormatState) => boolean; + + /** + * Get if the current button should be disabled + * @param formatState The current formatState of editor + * @returns True to show the button in a disabled state, otherwise false + * @default False When not specified, it is treated as always returning false + */ + isDisabled?: (formatState: FormatState) => boolean; + + /** + * A drop down menu of this button. When set this value, the button will has a "v" icon to let user + * know it will open a drop down menu. And the onClick handler will only be triggered when user click + * a menu item of the drop down. + */ + dropDownMenu?: RibbonButtonDropDown; + + /** + * Use this property to pass in Fluent UI CommandBar property directly. It will overwrite the values of other conflict properties + * + * Do not use ICommandBarItemProps.subMenuProps here since it will be overwritten. + * If need, specify its value using RibbonButton.dropDownMenu.commandBarSubMenuProperties. + */ + commandBarProperties?: Partial; +} diff --git a/demo/scripts/controls/ribbonButtons/contentModel/ContentModelRibbonPlugin.ts b/demo/scripts/controls/ribbonButtons/contentModel/ContentModelRibbonPlugin.ts index 7dd24709acd..6e27346c751 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/ContentModelRibbonPlugin.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/ContentModelRibbonPlugin.ts @@ -1,12 +1,17 @@ -import { ContentModelFormatState } from 'roosterjs-content-model-types'; -import { FormatState, PluginEvent, PluginEventType } from 'roosterjs-editor-types'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; +import RibbonPlugin from './RibbonPlugin'; +import { FormatState } from 'roosterjs-editor-types'; import { getFormatState } from 'roosterjs-content-model-api'; import { getObjectKeys } from 'roosterjs-editor-dom'; -import { IContentModelEditor } from 'roosterjs-content-model-editor'; -import { LocalizedStrings, RibbonButton, RibbonPlugin, UIUtilities } from 'roosterjs-react'; +import { LocalizedStrings, UIUtilities } from 'roosterjs-react'; +import { + ContentModelFormatState, + IStandaloneEditor, + PluginEvent, +} from 'roosterjs-content-model-types'; export class ContentModelRibbonPlugin implements RibbonPlugin { - private editor: IContentModelEditor | null = null; + private editor: IStandaloneEditor | null = null; private onFormatChanged: ((formatState: FormatState) => void) | null = null; private timer = 0; private formatState: ContentModelFormatState | null = null; @@ -29,7 +34,7 @@ export class ContentModelRibbonPlugin implements RibbonPlugin { * Initialize this plugin * @param editor The editor instance */ - initialize(editor: IContentModelEditor) { + initialize(editor: IStandaloneEditor) { this.editor = editor; } @@ -46,14 +51,14 @@ export class ContentModelRibbonPlugin implements RibbonPlugin { */ onPluginEvent(event: PluginEvent) { switch (event.eventType) { - case PluginEventType.EditorReady: - case PluginEventType.ContentChanged: - case PluginEventType.ZoomChanged: + case 'editorReady': + case 'contentChanged': + case 'zoomChanged': this.updateFormat(); break; - case PluginEventType.KeyDown: - case PluginEventType.MouseUp: + case 'keyDown': + case 'mouseUp': this.delayUpdate(); break; } @@ -85,7 +90,7 @@ export class ContentModelRibbonPlugin implements RibbonPlugin { * @param strings The localized string map for this button */ onButtonClick( - button: RibbonButton, + button: ContentModelRibbonButton, key: T, strings?: LocalizedStrings ) { @@ -107,7 +112,7 @@ export class ContentModelRibbonPlugin implements RibbonPlugin { * @param strings The localized string map for this button */ startLivePreview( - button: RibbonButton, + button: ContentModelRibbonButton, key: T, strings?: LocalizedStrings ) { @@ -116,9 +121,9 @@ export class ContentModelRibbonPlugin implements RibbonPlugin { // If editor is already in shadow edit, no need to check again. // And the check result may be incorrect because the content is changed from last shadow edit and the cached selection path won't apply - const range = !isInShadowEdit && this.editor.getSelectionRangeEx(); + const range = !isInShadowEdit && this.editor.getDOMSelection(); - if (isInShadowEdit || (range && !range.areAllCollapsed)) { + if (isInShadowEdit || (range && (range.type != 'range' || !range.range.collapsed))) { this.editor.startShadowEdit(); button.onClick(this.editor, key, strings, this.uiUtilities); } @@ -160,7 +165,7 @@ export class ContentModelRibbonPlugin implements RibbonPlugin { ) ) { this.formatState = newFormatState; - this.onFormatChanged((newFormatState as any) as FormatState); + this.onFormatChanged(newFormatState); } } } diff --git a/demo/scripts/controls/ribbonButtons/contentModel/RibbonPlugin.ts b/demo/scripts/controls/ribbonButtons/contentModel/RibbonPlugin.ts new file mode 100644 index 00000000000..54b732ce848 --- /dev/null +++ b/demo/scripts/controls/ribbonButtons/contentModel/RibbonPlugin.ts @@ -0,0 +1,49 @@ +import ContentModelRibbonButton from './ContentModelRibbonButton'; +import { EditorPlugin } from 'roosterjs-content-model-types'; +import { LocalizedStrings, UIUtilities } from 'roosterjs-react'; +import type { FormatState } from 'roosterjs-editor-types'; + +/** + * Represents a plugin to connect format ribbon component and the editor + */ +export default interface RibbonPlugin extends EditorPlugin { + /** + * Set the UI utilities objects to this plugin to help render additional UI elements + * @param uiUtilities The UI utilities object to set + */ + setUIUtilities(uiUtilities: UIUtilities): void; + + /** + * Register a callback to be invoked when format state of editor is changed, returns a disposer function. + */ + registerFormatChangedCallback: (callback: (formatState: FormatState) => void) => () => void; + + /** + * When user clicks on a button, call this method to let the plugin to handle this click event + * @param button The button that is clicked + * @param key Key of child menu item that is clicked if any + * @param strings The localized string map for this button + */ + onButtonClick: ( + button: ContentModelRibbonButton, + key: T, + strings?: LocalizedStrings + ) => void; + + /** + * Enter live preview state (shadow edit) of editor if there is a non-collapsed selection + * @param button The button that triggered this action + * @param key Key of the hovered button sub item + * @param strings The localized string map for this button + */ + startLivePreview: ( + button: ContentModelRibbonButton, + key: T, + strings?: LocalizedStrings + ) => void; + + /** + * Leave live preview state (shadow edit) of editor + */ + stopLivePreview: () => void; +} diff --git a/demo/scripts/controls/ribbonButtons/contentModel/alignCenterButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/alignCenterButton.ts index 968d17e3d01..a2306f0b8bd 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/alignCenterButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/alignCenterButton.ts @@ -1,19 +1,17 @@ -import { AlignCenterButtonStringKey, RibbonButton } from 'roosterjs-react'; -import { isContentModelEditor } from 'roosterjs-content-model-editor'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { setAlignment } from 'roosterjs-content-model-api'; +import { AlignCenterButtonStringKey } from 'roosterjs-react'; /** * @internal * "Align center" button on the format ribbon */ -export const alignCenterButton: RibbonButton = { +export const alignCenterButton: ContentModelRibbonButton = { key: 'buttonNameAlignCenter', unlocalizedText: 'Align center', iconName: 'AlignCenter', onClick: editor => { - if (isContentModelEditor(editor)) { - setAlignment(editor, 'center'); - } + setAlignment(editor, 'center'); return true; }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/alignJustifyButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/alignJustifyButton.ts index 9b7803f26ff..a93db909a0d 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/alignJustifyButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/alignJustifyButton.ts @@ -1,19 +1,16 @@ -import { isContentModelEditor } from 'roosterjs-content-model-editor'; -import { RibbonButton } from 'roosterjs-react'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { setAlignment } from 'roosterjs-content-model-api'; /** * @internal * "Align justify" button on the format ribbon */ -export const alignJustifyButton: RibbonButton<'buttonNameAlignJustify'> = { +export const alignJustifyButton: ContentModelRibbonButton<'buttonNameAlignJustify'> = { key: 'buttonNameAlignJustify', unlocalizedText: 'Align justify', iconName: 'AlignJustify', onClick: editor => { - if (isContentModelEditor(editor)) { - setAlignment(editor, 'justify'); - } + setAlignment(editor, 'justify'); return true; }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/alignLeftButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/alignLeftButton.ts index 55088314ec5..c1b0e014af8 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/alignLeftButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/alignLeftButton.ts @@ -1,19 +1,17 @@ -import { AlignLeftButtonStringKey, RibbonButton } from 'roosterjs-react'; -import { isContentModelEditor } from 'roosterjs-content-model-editor'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { setAlignment } from 'roosterjs-content-model-api'; +import { AlignLeftButtonStringKey } from 'roosterjs-react'; /** * @internal * "Align left" button on the format ribbon */ -export const alignLeftButton: RibbonButton = { +export const alignLeftButton: ContentModelRibbonButton = { key: 'buttonNameAlignLeft', unlocalizedText: 'Align left', iconName: 'AlignLeft', onClick: editor => { - if (isContentModelEditor(editor)) { - setAlignment(editor, 'left'); - } + setAlignment(editor, 'left'); return true; }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/alignRightButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/alignRightButton.ts index bc73fd6dce1..fa3ab23d40d 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/alignRightButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/alignRightButton.ts @@ -1,19 +1,17 @@ -import { AlignRightButtonStringKey, RibbonButton } from 'roosterjs-react'; -import { isContentModelEditor } from 'roosterjs-content-model-editor'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { setAlignment } from 'roosterjs-content-model-api'; +import { AlignRightButtonStringKey } from 'roosterjs-react'; /** * @internal * "Align right" button on the format ribbon */ -export const alignRightButton: RibbonButton = { +export const alignRightButton: ContentModelRibbonButton = { key: 'buttonNameAlignRight', unlocalizedText: 'Align right', iconName: 'AlignRight', onClick: editor => { - if (isContentModelEditor(editor)) { - setAlignment(editor, 'right'); - } + setAlignment(editor, 'right'); return true; }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/backgroundColorButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/backgroundColorButton.ts index b3b9e2641ad..36b16cf0e06 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/backgroundColorButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/backgroundColorButton.ts @@ -1,4 +1,4 @@ -import { isContentModelEditor } from 'roosterjs-content-model-editor'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { setBackgroundColor } from 'roosterjs-content-model-api'; import { BackgroundColorButtonStringKey, @@ -16,11 +16,11 @@ const originalButton = getButtons([KnownRibbonButtonKey.BackgroundColor])[0] as * @internal * "Background color" button on the format ribbon */ -export const backgroundColorButton: RibbonButton = { +export const backgroundColorButton: ContentModelRibbonButton = { ...originalButton, onClick: (editor, key) => { // This check will always be true, add it here just to satisfy compiler - if (key != 'buttonNameBackgroundColor' && isContentModelEditor(editor)) { + if (key != 'buttonNameBackgroundColor') { setBackgroundColor(editor, getBackgroundColorValue(key).lightModeColor); } }, diff --git a/demo/scripts/controls/ribbonButtons/contentModel/blockQuoteButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/blockQuoteButton.ts index 583287296be..dfe8d88ca2e 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/blockQuoteButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/blockQuoteButton.ts @@ -1,20 +1,18 @@ -import { isContentModelEditor } from 'roosterjs-content-model-editor'; -import { QuoteButtonStringKey, RibbonButton } from 'roosterjs-react'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { toggleBlockQuote } from 'roosterjs-content-model-api'; +import { QuoteButtonStringKey } from 'roosterjs-react'; /** * @internal * "Block quote" button on the format ribbon */ -export const blockQuoteButton: RibbonButton = { +export const blockQuoteButton: ContentModelRibbonButton = { key: 'buttonNameQuote', unlocalizedText: 'Quote', iconName: 'RightDoubleQuote', isChecked: formatState => !!formatState.isBlockQuote, onClick: editor => { - if (isContentModelEditor(editor)) { - toggleBlockQuote(editor); - } + toggleBlockQuote(editor); return true; }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/boldButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/boldButton.ts index 9bcd3597502..9877b554f32 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/boldButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/boldButton.ts @@ -1,20 +1,18 @@ -import { BoldButtonStringKey, RibbonButton } from 'roosterjs-react'; -import { isContentModelEditor } from 'roosterjs-content-model-editor'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; +import { BoldButtonStringKey } from 'roosterjs-react'; import { toggleBold } from 'roosterjs-content-model-api'; /** * @internal * "Bold" button on the format ribbon */ -export const boldButton: RibbonButton = { +export const boldButton: ContentModelRibbonButton = { key: 'buttonNameBold', unlocalizedText: 'Bold', iconName: 'Bold', isChecked: formatState => formatState.isBold, onClick: editor => { - if (isContentModelEditor(editor)) { - toggleBold(editor); - } + toggleBold(editor); return true; }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/bulletedListButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/bulletedListButton.ts index 12c2d492889..543a187bff1 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/bulletedListButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/bulletedListButton.ts @@ -1,20 +1,18 @@ -import { BulletedListButtonStringKey, RibbonButton } from 'roosterjs-react'; -import { isContentModelEditor } from 'roosterjs-content-model-editor'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { toggleBullet } from 'roosterjs-content-model-api'; +import { BulletedListButtonStringKey } from 'roosterjs-react'; /** * @internal * "Bulleted list" button on the format ribbon */ -export const bulletedListButton: RibbonButton = { +export const bulletedListButton: ContentModelRibbonButton = { key: 'buttonNameBulletedList', unlocalizedText: 'Bulleted list', iconName: 'BulletedList', isChecked: formatState => formatState.isBullet, onClick: editor => { - if (isContentModelEditor(editor)) { - toggleBullet(editor); - } + toggleBullet(editor); return true; }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/changeImageButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/changeImageButton.ts index d6ab58b8c71..af76da20f48 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/changeImageButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/changeImageButton.ts @@ -1,8 +1,7 @@ +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { changeImage } from 'roosterjs-content-model-api'; import { createElement } from 'roosterjs-editor-dom'; import { CreateElementData } from 'roosterjs-editor-types'; -import { isContentModelEditor } from 'roosterjs-content-model-editor'; -import { RibbonButton } from 'roosterjs-react'; const FileInput: CreateElementData = { tag: 'input', @@ -17,30 +16,28 @@ const FileInput: CreateElementData = { * @internal * "Change Image" button on the format ribbon */ -export const changeImageButton: RibbonButton<'buttonNameChangeImage'> = { +export const changeImageButton: ContentModelRibbonButton<'buttonNameChangeImage'> = { key: 'buttonNameChangeImage', unlocalizedText: 'Change Image', iconName: 'ImageSearch', isDisabled: formatState => !formatState.canAddImageAltText, onClick: editor => { - if (isContentModelEditor(editor)) { - const document = editor.getDocument(); - const fileInput = createElement(FileInput, document) as HTMLInputElement; - document.body.appendChild(fileInput); + const document = editor.getDocument(); + const fileInput = createElement(FileInput, document) as HTMLInputElement; + document.body.appendChild(fileInput); - fileInput.addEventListener('change', () => { - if (fileInput.files) { - for (let i = 0; i < fileInput.files.length; i++) { - changeImage(editor, fileInput.files[i]); - } + fileInput.addEventListener('change', () => { + if (fileInput.files) { + for (let i = 0; i < fileInput.files.length; i++) { + changeImage(editor, fileInput.files[i]); } - }); - - try { - fileInput.click(); - } finally { - document.body.removeChild(fileInput); } + }); + + try { + fileInput.click(); + } finally { + document.body.removeChild(fileInput); } }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/clearFormatButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/clearFormatButton.ts index cdfc02e6f51..0ca5a9ccec9 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/clearFormatButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/clearFormatButton.ts @@ -1,17 +1,15 @@ +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { clearFormat } from 'roosterjs-content-model-api'; -import { ClearFormatButtonStringKey, RibbonButton } from 'roosterjs-react'; -import { isContentModelEditor } from 'roosterjs-content-model-editor'; +import { ClearFormatButtonStringKey } from 'roosterjs-react'; /** * "Clear format" button on the format ribbon */ -export const clearFormatButton: RibbonButton = { +export const clearFormatButton: ContentModelRibbonButton = { key: 'buttonNameClearFormat', unlocalizedText: 'Clear format', iconName: 'ClearFormatting', onClick: editor => { - if (isContentModelEditor(editor)) { - clearFormat(editor); - } + clearFormat(editor); }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/codeButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/codeButton.ts index 5c26c62e66c..77c33176be6 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/codeButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/codeButton.ts @@ -1,19 +1,17 @@ -import { CodeButtonStringKey, RibbonButton } from 'roosterjs-react'; -import { isContentModelEditor } from 'roosterjs-content-model-editor'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { toggleCode } from 'roosterjs-content-model-api'; +import { CodeButtonStringKey } from 'roosterjs-react'; /** * @internal * "Code" button on the format ribbon */ -export const codeButton: RibbonButton = { +export const codeButton: ContentModelRibbonButton = { key: 'buttonNameCode', unlocalizedText: 'Code', iconName: 'Code', isChecked: formatState => !!formatState.isCodeInline, onClick: editor => { - if (isContentModelEditor(editor)) { - toggleCode(editor); - } + toggleCode(editor); }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/darkMode.ts b/demo/scripts/controls/ribbonButtons/contentModel/darkMode.ts new file mode 100644 index 00000000000..f0a5b6a4b73 --- /dev/null +++ b/demo/scripts/controls/ribbonButtons/contentModel/darkMode.ts @@ -0,0 +1,24 @@ +import ContentModelRibbonButton from './ContentModelRibbonButton'; +import MainPaneBase from '../../MainPaneBase'; + +/** + * Key of localized strings of Dark mode button + */ +export type DarkModeButtonStringKey = 'buttonNameDarkMode'; + +/** + * "Dark mode" button on the format ribbon + */ +export const darkMode: ContentModelRibbonButton = { + key: 'buttonNameDarkMode', + unlocalizedText: 'Dark Mode', + iconName: 'ClearNight', + isChecked: formatState => formatState.isDarkMode, + onClick: editor => { + editor.focus(); + + // Let main pane know this state change so that it can be persisted when pop out/pop in + MainPaneBase.getInstance().toggleDarkMode(); + return true; + }, +}; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/decreaseFontSizeButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/decreaseFontSizeButton.ts index f0d0ac14a1d..b4fa8213d40 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/decreaseFontSizeButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/decreaseFontSizeButton.ts @@ -1,18 +1,16 @@ +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { changeFontSize } from 'roosterjs-content-model-api'; -import { DecreaseFontSizeButtonStringKey, RibbonButton } from 'roosterjs-react'; -import { isContentModelEditor } from 'roosterjs-content-model-editor'; +import { DecreaseFontSizeButtonStringKey } from 'roosterjs-react'; /** * @internal * "Decrease font size" button on the format ribbon */ -export const decreaseFontSizeButton: RibbonButton = { +export const decreaseFontSizeButton: ContentModelRibbonButton = { key: 'buttonNameDecreaseFontSize', unlocalizedText: 'Decrease font size', iconName: 'FontDecrease', onClick: editor => { - if (isContentModelEditor(editor)) { - changeFontSize(editor, 'decrease'); - } + changeFontSize(editor, 'decrease'); }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/decreaseIndentButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/decreaseIndentButton.ts index c1d99a80c7a..0825b629428 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/decreaseIndentButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/decreaseIndentButton.ts @@ -1,19 +1,17 @@ -import { DecreaseIndentButtonStringKey, RibbonButton } from 'roosterjs-react'; -import { isContentModelEditor } from 'roosterjs-content-model-editor'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { setIndentation } from 'roosterjs-content-model-api'; +import { DecreaseIndentButtonStringKey } from 'roosterjs-react'; /** * @internal * "Decrease indent" button on the format ribbon */ -export const decreaseIndentButton: RibbonButton = { +export const decreaseIndentButton: ContentModelRibbonButton = { key: 'buttonNameDecreaseIndent', unlocalizedText: 'Decrease indent', iconName: 'DecreaseIndentLegacy', flipWhenRtl: true, onClick: editor => { - if (isContentModelEditor(editor)) { - setIndentation(editor, 'outdent'); - } + setIndentation(editor, 'outdent'); }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/export.ts b/demo/scripts/controls/ribbonButtons/contentModel/export.ts new file mode 100644 index 00000000000..b9db78b0149 --- /dev/null +++ b/demo/scripts/controls/ribbonButtons/contentModel/export.ts @@ -0,0 +1,73 @@ +import ContentModelRibbonButton from './ContentModelRibbonButton'; +import { cloneModel } from 'roosterjs-content-model-core'; +import { ContentModelEntityFormat } from 'roosterjs-content-model-types'; +import { + contentModelToDom, + createModelToDomContext, + parseEntityClassName, +} from 'roosterjs-content-model-dom'; + +/** + * Key of localized strings of Zoom button + */ +export type ExportButtonStringKey = 'buttonNameExport'; + +/** + * "Export content" button on the format ribbon + */ +export const exportContent: ContentModelRibbonButton = { + key: 'buttonNameExport', + unlocalizedText: 'Export', + iconName: 'Export', + flipWhenRtl: true, + onClick: editor => { + // TODO: We need a export function in dev code to handle this feature + const win = editor.getDocument().defaultView.open(); + + editor.formatContentModel(model => { + const clonedModel = cloneModel(model, { + includeCachedElement: (node, type) => { + switch (type) { + case 'cache': + return undefined; + + case 'general': + return node.cloneNode() as HTMLElement; + + case 'entity': + const clonedRoot = node.cloneNode(true) as HTMLElement; + const format: ContentModelEntityFormat = {}; + let isEntity = false; + + clonedRoot.classList.forEach(name => { + isEntity = parseEntityClassName(name, format) || isEntity; + }); + + if (isEntity && format.id && format.entityType) { + editor.triggerEvent('entityOperation', { + operation: 'replaceTemporaryContent', + entity: { + wrapper: clonedRoot, + id: format.id, + type: format.entityType, + isReadonly: !!format.isReadonly, + }, + }); + } + + return clonedRoot; + } + }, + }); + + contentModelToDom( + win.document, + win.document.body, + clonedModel, + createModelToDomContext() + ); + + return false; + }); + }, +}; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/fontButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/fontButton.ts index 139fec0771d..e2e714b881f 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/fontButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/fontButton.ts @@ -1,6 +1,6 @@ -import { FontButtonStringKey, RibbonButton } from 'roosterjs-react'; -import { isContentModelEditor } from 'roosterjs-content-model-editor'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { setFontName } from 'roosterjs-content-model-api'; +import { FontButtonStringKey } from 'roosterjs-react'; interface FontName { name: string; @@ -150,7 +150,7 @@ const FirstFontRegex = /^['"]?([^'",]+)/i; * @internal * "Font" button on the format ribbon */ -export const fontButton: RibbonButton = { +export const fontButton: ContentModelRibbonButton = { key: 'buttonNameFont', unlocalizedText: 'Font', iconName: 'Font', @@ -166,8 +166,6 @@ export const fontButton: RibbonButton = { allowLivePreview: true, }, onClick: (editor, font) => { - if (isContentModelEditor(editor)) { - setFontName(editor, font); - } + setFontName(editor, font); }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/fontSizeButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/fontSizeButton.ts index 5fc487586ee..6e33042b8a1 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/fontSizeButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/fontSizeButton.ts @@ -1,6 +1,6 @@ -import { FontSizeButtonStringKey, RibbonButton } from 'roosterjs-react'; -import { isContentModelEditor } from 'roosterjs-content-model-editor'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { setFontSize } from 'roosterjs-content-model-api'; +import { FontSizeButtonStringKey } from 'roosterjs-react'; const FONT_SIZES = [8, 9, 10, 11, 12, 14, 16, 18, 20, 22, 24, 26, 28, 36, 48, 72]; @@ -8,7 +8,7 @@ const FONT_SIZES = [8, 9, 10, 11, 12, 14, 16, 18, 20, 22, 24, 26, 28, 36, 48, 72 * @internal * "Font Size" button on the format ribbon */ -export const fontSizeButton: RibbonButton = { +export const fontSizeButton: ContentModelRibbonButton = { key: 'buttonNameFontSize', unlocalizedText: 'Font size', iconName: 'FontSize', @@ -21,8 +21,6 @@ export const fontSizeButton: RibbonButton = { allowLivePreview: true, }, onClick: (editor, size) => { - if (isContentModelEditor(editor)) { - setFontSize(editor, size); - } + setFontSize(editor, size); }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/formatPainterButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/formatPainterButton.ts index 8b98a9ef676..df5585fd990 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/formatPainterButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/formatPainterButton.ts @@ -1,19 +1,16 @@ import ContentModelFormatPainterPlugin from '../../contentModel/plugins/ContentModelFormatPainterPlugin'; -import { isContentModelEditor } from 'roosterjs-content-model-editor'; -import { RibbonButton } from 'roosterjs-react'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; /** * @internal * "Format Painter" button on the format ribbon */ -export const formatPainterButton: RibbonButton<'formatPainter'> = { +export const formatPainterButton: ContentModelRibbonButton<'formatPainter'> = { key: 'formatPainter', unlocalizedText: 'Format painter', iconName: 'Brush', onClick: editor => { - if (isContentModelEditor(editor)) { - ContentModelFormatPainterPlugin.startFormatPainter(editor); - } + ContentModelFormatPainterPlugin.startFormatPainter(); return true; }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/formatTableButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/formatTableButton.ts index 9c81a9b9662..ea34aefa2d2 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/formatTableButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/formatTableButton.ts @@ -1,6 +1,5 @@ +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { formatTable } from 'roosterjs-content-model-api'; -import { isContentModelEditor } from 'roosterjs-content-model-editor'; -import { RibbonButton } from 'roosterjs-react'; import { TableBorderFormat } from 'roosterjs-content-model-core'; import { TableMetadataFormat } from 'roosterjs-content-model-types'; @@ -192,7 +191,7 @@ export function createTableFormat( }; } -export const formatTableButton: RibbonButton<'ribbonButtonTableFormat'> = { +export const formatTableButton: ContentModelRibbonButton<'ribbonButtonTableFormat'> = { key: 'ribbonButtonTableFormat', iconName: 'TableComputed', unlocalizedText: 'Format Table', @@ -215,7 +214,7 @@ export const formatTableButton: RibbonButton<'ribbonButtonTableFormat'> = { onClick: (editor, key) => { const format = PREDEFINED_STYLES[key]?.('#ABABAB', '#ABABAB20'); - if (format && isContentModelEditor(editor)) { + if (format) { formatTable(editor, format); } }, diff --git a/demo/scripts/controls/ribbonButtons/contentModel/imageBorderColorButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/imageBorderColorButton.ts index a45ea0174dc..39d742f251d 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/imageBorderColorButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/imageBorderColorButton.ts @@ -1,6 +1,5 @@ -import { getButtons, getTextColorValue, KnownRibbonButtonKey } from 'roosterjs-react'; -import { isContentModelEditor } from 'roosterjs-content-model-editor'; -import { RibbonButton } from 'roosterjs-react'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; +import { getButtons, getTextColorValue, KnownRibbonButtonKey, RibbonButton } from 'roosterjs-react'; import { setImageBorder } from 'roosterjs-content-model-api'; const originalButton = getButtons([KnownRibbonButtonKey.TextColor])[0] as RibbonButton< @@ -11,14 +10,14 @@ const originalButton = getButtons([KnownRibbonButtonKey.TextColor])[0] as Ribbon * @internal * "Image Border Color" button on the format ribbon */ -export const imageBorderColorButton: RibbonButton<'buttonNameImageBorderColor'> = { +export const imageBorderColorButton: ContentModelRibbonButton<'buttonNameImageBorderColor'> = { ...originalButton, unlocalizedText: 'Image Border Color', iconName: 'Photo2', isDisabled: formatState => !formatState.canAddImageAltText, onClick: (editor, key) => { // This check will always be true, add it here just to satisfy compiler - if (key != 'buttonNameImageBorderColor' && isContentModelEditor(editor)) { + if (key != 'buttonNameImageBorderColor') { setImageBorder(editor, { color: getTextColorValue(key).lightModeColor }, '5px'); } }, diff --git a/demo/scripts/controls/ribbonButtons/contentModel/imageBorderRemoveButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/imageBorderRemoveButton.ts index 3fe95a2dfcc..da91812e2dd 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/imageBorderRemoveButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/imageBorderRemoveButton.ts @@ -1,19 +1,16 @@ -import { isContentModelEditor } from 'roosterjs-content-model-editor'; -import { RibbonButton } from 'roosterjs-react'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { setImageBorder } from 'roosterjs-content-model-api'; /** * @internal * "Remove Image Border" button on the format ribbon */ -export const imageBorderRemoveButton: RibbonButton<'buttonNameImageBorderRemove'> = { +export const imageBorderRemoveButton: ContentModelRibbonButton<'buttonNameImageBorderRemove'> = { key: 'buttonNameImageBorderRemove', unlocalizedText: 'Remove Image Border', iconName: 'Cancel', isDisabled: formatState => !formatState.canAddImageAltText, onClick: editor => { - if (isContentModelEditor(editor)) { - setImageBorder(editor, null); - } + setImageBorder(editor, null); }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/imageBorderStyleButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/imageBorderStyleButton.ts index c39e9ee88d3..19e76fb71be 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/imageBorderStyleButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/imageBorderStyleButton.ts @@ -1,5 +1,4 @@ -import { isContentModelEditor } from 'roosterjs-content-model-editor'; -import { RibbonButton } from 'roosterjs-react'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { setImageBorder } from 'roosterjs-content-model-api'; const STYLES: Record = { @@ -17,7 +16,7 @@ const STYLES: Record = { * @internal * "Image Border Style" button on the format ribbon */ -export const imageBorderStyleButton: RibbonButton<'buttonNameImageBorderStyle'> = { +export const imageBorderStyleButton: ContentModelRibbonButton<'buttonNameImageBorderStyle'> = { key: 'buttonNameImageBorderStyle', unlocalizedText: 'Image Border Style', iconName: 'BorderDash', @@ -27,9 +26,7 @@ export const imageBorderStyleButton: RibbonButton<'buttonNameImageBorderStyle'> allowLivePreview: true, }, onClick: (editor, style) => { - if (isContentModelEditor(editor)) { - setImageBorder(editor, { style: style }, '5px'); - } + setImageBorder(editor, { style: style }, '5px'); return true; }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/imageBorderWidthButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/imageBorderWidthButton.ts index be6086f3fc8..29f7a6522bc 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/imageBorderWidthButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/imageBorderWidthButton.ts @@ -1,5 +1,4 @@ -import { isContentModelEditor } from 'roosterjs-content-model-editor'; -import { RibbonButton } from 'roosterjs-react'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { setImageBorder } from 'roosterjs-content-model-api'; const WIDTH = [8, 9, 10, 11, 12, 14, 16, 18, 20, 22, 24, 26, 28, 36, 48, 72]; @@ -8,7 +7,7 @@ const WIDTH = [8, 9, 10, 11, 12, 14, 16, 18, 20, 22, 24, 26, 28, 36, 48, 72]; * @internal * "Image Border Width" button on the format ribbon */ -export const imageBorderWidthButton: RibbonButton<'buttonNameImageBorderWidth'> = { +export const imageBorderWidthButton: ContentModelRibbonButton<'buttonNameImageBorderWidth'> = { key: 'buttonNameImageBorderWidth', unlocalizedText: 'Image Border Width', iconName: 'Photo2', @@ -21,15 +20,14 @@ export const imageBorderWidthButton: RibbonButton<'buttonNameImageBorderWidth'> allowLivePreview: true, }, onClick: (editor, size) => { - if (isContentModelEditor(editor)) { - setImageBorder( - editor, - { - width: size, - }, - '5px' - ); - } + setImageBorder( + editor, + { + width: size, + }, + '5px' + ); + return true; }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/imageBoxShadowButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/imageBoxShadowButton.ts index a1eed8d8f92..d1f936ab1d9 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/imageBoxShadowButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/imageBoxShadowButton.ts @@ -1,5 +1,4 @@ -import { isContentModelEditor } from 'roosterjs-content-model-editor'; -import { RibbonButton } from 'roosterjs-react'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { setImageBoxShadow } from 'roosterjs-content-model-api'; const STYLES_NAMES: Record = { @@ -32,7 +31,7 @@ const STYLES: Record = { * @internal * "Image Shadow" button on the format ribbon */ -export const imageBoxShadowButton: RibbonButton<'buttonNameImageBoxSHadow'> = { +export const imageBoxShadowButton: ContentModelRibbonButton<'buttonNameImageBoxSHadow'> = { key: 'buttonNameImageBoxSHadow', unlocalizedText: 'Image Shadow', iconName: 'Photo2', @@ -42,9 +41,7 @@ export const imageBoxShadowButton: RibbonButton<'buttonNameImageBoxSHadow'> = { allowLivePreview: true, }, onClick: (editor, size) => { - if (isContentModelEditor(editor)) { - setImageBoxShadow(editor, STYLES[size], STYLES[size].length ? '4px' : null); - } + setImageBoxShadow(editor, STYLES[size], STYLES[size].length ? '4px' : null); return true; }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/increaseFontSizeButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/increaseFontSizeButton.ts index f9308f87024..d0a6e57af79 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/increaseFontSizeButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/increaseFontSizeButton.ts @@ -1,18 +1,16 @@ +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { changeFontSize } from 'roosterjs-content-model-api'; -import { IncreaseFontSizeButtonStringKey, RibbonButton } from 'roosterjs-react'; -import { isContentModelEditor } from 'roosterjs-content-model-editor'; +import { IncreaseFontSizeButtonStringKey } from 'roosterjs-react'; /** * @internal * "Increase font size" button on the format ribbon */ -export const increaseFontSizeButton: RibbonButton = { +export const increaseFontSizeButton: ContentModelRibbonButton = { key: 'buttonNameIncreaseFontSize', unlocalizedText: 'Increase font size', iconName: 'FontIncrease', onClick: editor => { - if (isContentModelEditor(editor)) { - changeFontSize(editor, 'increase'); - } + changeFontSize(editor, 'increase'); }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/increaseIndentButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/increaseIndentButton.ts index bdfefb9cbc6..397b02d56ef 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/increaseIndentButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/increaseIndentButton.ts @@ -1,19 +1,17 @@ -import { IncreaseIndentButtonStringKey, RibbonButton } from 'roosterjs-react'; -import { isContentModelEditor } from 'roosterjs-content-model-editor'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { setIndentation } from 'roosterjs-content-model-api'; +import { IncreaseIndentButtonStringKey } from 'roosterjs-react'; /** * @internal * "Increase indent" button on the format ribbon */ -export const increaseIndentButton: RibbonButton = { +export const increaseIndentButton: ContentModelRibbonButton = { key: 'buttonNameIncreaseIndent', unlocalizedText: 'Increase indent', iconName: 'IncreaseIndentLegacy', flipWhenRtl: true, onClick: editor => { - if (isContentModelEditor(editor)) { - setIndentation(editor, 'indent'); - } + setIndentation(editor, 'indent'); }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/insertImageButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/insertImageButton.ts index 62a14215b5c..6d88485b47d 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/insertImageButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/insertImageButton.ts @@ -1,8 +1,8 @@ +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { createElement } from 'roosterjs-editor-dom'; import { CreateElementData } from 'roosterjs-editor-types'; import { insertImage } from 'roosterjs-content-model-api'; -import { InsertImageButtonStringKey, RibbonButton } from 'roosterjs-react'; -import { isContentModelEditor } from 'roosterjs-content-model-editor'; +import { InsertImageButtonStringKey } from 'roosterjs-react'; const FileInput: CreateElementData = { tag: 'input', @@ -17,29 +17,27 @@ const FileInput: CreateElementData = { * @internal * "Insert image" button on the format ribbon */ -export const insertImageButton: RibbonButton = { +export const insertImageButton: ContentModelRibbonButton = { key: 'buttonNameInsertImage', unlocalizedText: 'Insert image', iconName: 'Photo2', onClick: editor => { - if (isContentModelEditor(editor)) { - const document = editor.getDocument(); - const fileInput = createElement(FileInput, document) as HTMLInputElement; - document.body.appendChild(fileInput); + const document = editor.getDocument(); + const fileInput = createElement(FileInput, document) as HTMLInputElement; + document.body.appendChild(fileInput); - fileInput.addEventListener('change', () => { - if (fileInput.files) { - for (let i = 0; i < fileInput.files.length; i++) { - insertImage(editor, fileInput.files[i]); - } + fileInput.addEventListener('change', () => { + if (fileInput.files) { + for (let i = 0; i < fileInput.files.length; i++) { + insertImage(editor, fileInput.files[i]); } - }); - - try { - fileInput.click(); - } finally { - document.body.removeChild(fileInput); } + }); + + try { + fileInput.click(); + } finally { + document.body.removeChild(fileInput); } }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/insertLinkButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/insertLinkButton.ts index b0e6c717754..e380519986b 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/insertLinkButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/insertLinkButton.ts @@ -1,60 +1,54 @@ +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { adjustLinkSelection, insertLink } from 'roosterjs-content-model-api'; -import { InsertLinkButtonStringKey, RibbonButton } from 'roosterjs-react'; -import { isContentModelEditor } from 'roosterjs-content-model-editor'; import { showInputDialog } from 'roosterjs-react/lib/inputDialog'; +import { InsertLinkButtonStringKey } from 'roosterjs-react'; /** * @internal * "Insert link" button on the format ribbon */ -export const insertLinkButton: RibbonButton = { +export const insertLinkButton: ContentModelRibbonButton = { key: 'buttonNameInsertLink', unlocalizedText: 'Insert link', iconName: 'Link', // isDisabled: formatState => !!formatState.isMultilineSelection, onClick: (editor, _, strings, uiUtilities) => { - if (isContentModelEditor(editor)) { - const [displayText, url] = adjustLinkSelection(editor); - const items = { - url: { - autoFocus: true, - labelKey: 'insertLinkDialogUrl' as const, - unlocalizedLabel: 'Web address (URL)', - initValue: url, - }, - displayText: { - labelKey: 'insertLinkDialogDisplayAs' as const, - unlocalizedLabel: 'Display as', - initValue: displayText, - }, - }; + const [displayText, url] = adjustLinkSelection(editor); + const items = { + url: { + autoFocus: true, + labelKey: 'insertLinkDialogUrl' as const, + unlocalizedLabel: 'Web address (URL)', + initValue: url, + }, + displayText: { + labelKey: 'insertLinkDialogDisplayAs' as const, + unlocalizedLabel: 'Display as', + initValue: displayText, + }, + }; - showInputDialog( - uiUtilities, - 'insertLinkTitle', - 'Insert link', - items, - strings, - (itemName, newValue, values) => { - if (itemName == 'url' && values.displayText == values.url) { - values.displayText = newValue; - values.url = newValue; - return values; - } else { - return null; - } + showInputDialog( + uiUtilities, + 'insertLinkTitle', + 'Insert link', + items, + strings, + (itemName, newValue, values) => { + if (itemName == 'url' && values.displayText == values.url) { + values.displayText = newValue; + values.url = newValue; + return values; + } else { + return null; } - ).then(result => { - editor.focus(); + } + ).then(result => { + editor.focus(); - if ( - result && - result.url && - (result.displayText != displayText || result.url != url) - ) { - insertLink(editor, result.url, result.url, result.displayText); - } - }); - } + if (result && result.url && (result.displayText != displayText || result.url != url)) { + insertLink(editor, result.url, result.url, result.displayText); + } + }); }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/insertTableButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/insertTableButton.ts index 91fe7a4cfb9..d337376696c 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/insertTableButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/insertTableButton.ts @@ -1,19 +1,17 @@ +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { getButtons, KnownRibbonButtonKey } from 'roosterjs-react'; import { insertTable } from 'roosterjs-content-model-api'; import { InsertTableButtonStringKey, RibbonButton } from 'roosterjs-react'; -import { isContentModelEditor } from 'roosterjs-content-model-editor'; const originalPasteButton: RibbonButton = getButtons([ KnownRibbonButtonKey.InsertTable, ])[0] as RibbonButton; -export const insertTableButton: RibbonButton = { +export const insertTableButton: ContentModelRibbonButton = { ...originalPasteButton, onClick: (editor, key) => { - if (isContentModelEditor(editor)) { - const { row, col } = parseKey(key); - insertTable(editor, col, row); - } + const { row, col } = parseKey(key); + insertTable(editor, col, row); }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/italicButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/italicButton.ts index 18b3f5a70cd..dbd15c77388 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/italicButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/italicButton.ts @@ -1,20 +1,18 @@ -import { isContentModelEditor } from 'roosterjs-content-model-editor'; -import { ItalicButtonStringKey, RibbonButton } from 'roosterjs-react'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { toggleItalic } from 'roosterjs-content-model-api'; +import { ItalicButtonStringKey } from 'roosterjs-react'; /** * @internal * "Italic" button on the format ribbon */ -export const italicButton: RibbonButton = { +export const italicButton: ContentModelRibbonButton = { key: 'buttonNameItalic', unlocalizedText: 'Italic', iconName: 'Italic', isChecked: formatState => formatState.isItalic, onClick: editor => { - if (isContentModelEditor(editor)) { - toggleItalic(editor); - } + toggleItalic(editor); return true; }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/listStartNumberButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/listStartNumberButton.ts index cacf17df6c6..c53f3766556 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/listStartNumberButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/listStartNumberButton.ts @@ -1,13 +1,13 @@ -import showInputDialog from 'roosterjs-react/lib/inputDialog/utils/showInputDialog'; -import { CancelButtonStringKey, OkButtonStringKey, RibbonButton } from 'roosterjs-react'; -import { isContentModelEditor } from 'roosterjs-content-model-editor'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; +import { CancelButtonStringKey, OkButtonStringKey } from 'roosterjs-react'; import { setListStartNumber } from 'roosterjs-content-model-api'; +import { showInputDialog } from 'roosterjs-react/lib/inputDialog'; /** * @internal * "Bulleted list" button on the format ribbon */ -export const listStartNumberButton: RibbonButton< +export const listStartNumberButton: ContentModelRibbonButton< | 'ribbonButtonSetStartNumber' | 'ribbonButtonSetStartNumberTo1' | 'ribbonButtonSetStartNumberCustomize' @@ -25,30 +25,28 @@ export const listStartNumberButton: RibbonButton< iconName: 'NumberSymbol', isDisabled: formatState => !formatState.isNumbering, onClick: (editor, key, strings, uiUtility) => { - if (isContentModelEditor(editor)) { - if (key == 'ribbonButtonSetStartNumberCustomize') { - showInputDialog( - uiUtility, - 'ribbonButtonSetStartNumberCustomize', - 'Start numbering value', - { - startNumber: { - labelKey: null, - unlocalizedLabel: null, - initValue: '1', - }, + if (key == 'ribbonButtonSetStartNumberCustomize') { + showInputDialog( + uiUtility, + 'ribbonButtonSetStartNumberCustomize', + 'Start numbering value', + { + startNumber: { + labelKey: null, + unlocalizedLabel: null, + initValue: '1', }, - strings - ).then(values => { - const newValue = parseInt(values.startNumber); + }, + strings + ).then(values => { + const newValue = parseInt(values.startNumber); - if (newValue > 0) { - setListStartNumber(editor, newValue); - } - }); - } else { - setListStartNumber(editor, 1); - } + if (newValue > 0) { + setListStartNumber(editor, newValue); + } + }); + } else { + setListStartNumber(editor, 1); } return true; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/ltrButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/ltrButton.ts index ac32f4fdc18..16dae98fb3c 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/ltrButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/ltrButton.ts @@ -1,19 +1,17 @@ -import { isContentModelEditor } from 'roosterjs-content-model-editor'; -import { LtrButtonStringKey, RibbonButton } from 'roosterjs-react'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { setDirection } from 'roosterjs-content-model-api'; +import { LtrButtonStringKey } from 'roosterjs-react'; /** * @internal * "Left to right" button on the format ribbon */ -export const ltrButton: RibbonButton = { +export const ltrButton: ContentModelRibbonButton = { key: 'buttonNameLtr', unlocalizedText: 'Left to right', iconName: 'BidiLtr', onClick: editor => { - if (isContentModelEditor(editor)) { - setDirection(editor, 'ltr'); - } + setDirection(editor, 'ltr'); return true; }, diff --git a/demo/scripts/controls/ribbonButtons/contentModel/moreCommands.ts b/demo/scripts/controls/ribbonButtons/contentModel/moreCommands.ts new file mode 100644 index 00000000000..201fca74c11 --- /dev/null +++ b/demo/scripts/controls/ribbonButtons/contentModel/moreCommands.ts @@ -0,0 +1,15 @@ +import ContentModelRibbonButton from './ContentModelRibbonButton'; +import { MoreCommandsButtonStringKey } from 'roosterjs-react'; + +/** + * @internal + * "More commands" (overflow) button on the format ribbon + */ +export const moreCommands: ContentModelRibbonButton = { + key: 'buttonNameMoreCommands', + unlocalizedText: 'More commands', + iconName: 'MoreCommands', + onClick: () => { + return true; + }, +}; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/numberedListButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/numberedListButton.ts index 45919be26f9..1cff4c628a9 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/numberedListButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/numberedListButton.ts @@ -1,20 +1,19 @@ -import { isContentModelEditor } from 'roosterjs-content-model-editor'; -import { NumberedListButtonStringKey, RibbonButton } from 'roosterjs-react'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { toggleNumbering } from 'roosterjs-content-model-api'; +import { NumberedListButtonStringKey } from 'roosterjs-react'; /** * @internal * "Numbering list" button on the format ribbon */ -export const numberedListButton: RibbonButton = { +export const numberedListButton: ContentModelRibbonButton = { key: 'buttonNameNumberedList', unlocalizedText: 'Numbered List', iconName: 'NumberedList', isChecked: formatState => formatState.isNumbering, onClick: editor => { - if (isContentModelEditor(editor)) { - toggleNumbering(editor); - } + toggleNumbering(editor); + return true; }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/pasteButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/pasteButton.ts index be62467b902..8225b1a9e3f 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/pasteButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/pasteButton.ts @@ -1,30 +1,28 @@ +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { extractClipboardItems } from 'roosterjs-editor-dom'; -import { isContentModelEditor } from 'roosterjs-content-model-editor'; -import { RibbonButton } from 'roosterjs-react'; /** * @internal * "Paste" button on the format ribbon */ -export const pasteButton: RibbonButton<'buttonNamePaste'> = { +export const pasteButton: ContentModelRibbonButton<'buttonNamePaste'> = { key: 'buttonNamePaste', unlocalizedText: 'Paste', iconName: 'Paste', onClick: async editor => { - if (isContentModelEditor(editor)) { - const doc = editor.getDocument(); - const clipboard = doc.defaultView.navigator.clipboard; - if (clipboard && clipboard.read) { - try { - const clipboardItems = await clipboard.read(); - const dataTransferItems = await Promise.all( - createDataTransferItems(clipboardItems) - ); - const clipboardData = await extractClipboardItems(dataTransferItems); - editor.paste(clipboardData); - } catch {} - } + const doc = editor.getDocument(); + const clipboard = doc.defaultView.navigator.clipboard; + if (clipboard && clipboard.read) { + try { + const clipboardItems = await clipboard.read(); + const dataTransferItems = await Promise.all( + createDataTransferItems(clipboardItems) + ); + const clipboardData = await extractClipboardItems(dataTransferItems); + editor.pasteFromClipboard(clipboardData); + } catch {} } + return true; }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/popout.ts b/demo/scripts/controls/ribbonButtons/contentModel/popout.ts new file mode 100644 index 00000000000..a10f4dbdf48 --- /dev/null +++ b/demo/scripts/controls/ribbonButtons/contentModel/popout.ts @@ -0,0 +1,20 @@ +import ContentModelRibbonButton from './ContentModelRibbonButton'; +import MainPaneBase from '../../MainPaneBase'; + +/** + * Key of localized strings of Popout button + */ +export type PopoutButtonStringKey = 'buttonNamePopout'; + +/** + * "Popout" button on the format ribbon + */ +export const popout: ContentModelRibbonButton = { + key: 'buttonNamePopout', + unlocalizedText: 'Open in a separate window', + iconName: 'OpenInNewWindow', + flipWhenRtl: true, + onClick: _ => { + MainPaneBase.getInstance().popout(); + }, +}; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/redoButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/redoButton.ts index 856eb6277cf..935f1574bec 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/redoButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/redoButton.ts @@ -1,20 +1,19 @@ -import { isContentModelEditor } from 'roosterjs-content-model-editor'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { redo } from 'roosterjs-content-model-core'; -import { RedoButtonStringKey, RibbonButton } from 'roosterjs-react'; +import { RedoButtonStringKey } from 'roosterjs-react'; /** * @internal * "Undo" button on the format ribbon */ -export const redoButton: RibbonButton = { +export const redoButton: ContentModelRibbonButton = { key: 'buttonNameRedo', unlocalizedText: 'Redo', iconName: 'Redo', isDisabled: formatState => !formatState.canRedo, onClick: editor => { - if (isContentModelEditor(editor)) { - redo(editor); - } + redo(editor); + return true; }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/removeLinkButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/removeLinkButton.ts index 3be5b6a0e7f..e9b22cc4493 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/removeLinkButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/removeLinkButton.ts @@ -1,19 +1,17 @@ -import { isContentModelEditor } from 'roosterjs-content-model-editor'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { removeLink } from 'roosterjs-content-model-api'; -import { RemoveLinkButtonStringKey, RibbonButton } from 'roosterjs-react'; +import { RemoveLinkButtonStringKey } from 'roosterjs-react'; /** * @internal * "Remove link" button on the format ribbon */ -export const removeLinkButton: RibbonButton = { +export const removeLinkButton: ContentModelRibbonButton = { key: 'buttonNameRemoveLink', unlocalizedText: 'Remove link', iconName: 'RemoveLink', isDisabled: formatState => !formatState.canUnlink, onClick: editor => { - if (isContentModelEditor(editor)) { - removeLink(editor); - } + removeLink(editor); }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/rtlButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/rtlButton.ts index ad786f118fb..880d3e9d582 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/rtlButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/rtlButton.ts @@ -1,19 +1,18 @@ -import { isContentModelEditor } from 'roosterjs-content-model-editor'; -import { RibbonButton, RtlButtonStringKey } from 'roosterjs-react'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; +import { RtlButtonStringKey } from 'roosterjs-react'; import { setDirection } from 'roosterjs-content-model-api'; /** * @internal * "Right to left" button on the format ribbon */ -export const rtlButton: RibbonButton = { +export const rtlButton: ContentModelRibbonButton = { key: 'buttonNameRtl', unlocalizedText: 'Right to left', iconName: 'BidiRtl', onClick: editor => { - if (isContentModelEditor(editor)) { - setDirection(editor, 'rtl'); - } + setDirection(editor, 'rtl'); + return true; }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/setBulletedListStyleButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/setBulletedListStyleButton.ts index d95efa096c7..bbcfaf64369 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/setBulletedListStyleButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/setBulletedListStyleButton.ts @@ -1,6 +1,5 @@ +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { BulletListType } from 'roosterjs-content-model-core'; -import { isContentModelEditor } from 'roosterjs-content-model-editor'; -import { RibbonButton } from 'roosterjs-react'; import { setListStyle } from 'roosterjs-content-model-api'; const dropDownMenuItems = { [BulletListType.Disc]: 'Disc', @@ -14,7 +13,7 @@ const dropDownMenuItems = { [BulletListType.Circle]: 'Circle', }; -export const setBulletedListStyleButton: RibbonButton<'ribbonButtonBulletedListStyle'> = { +export const setBulletedListStyleButton: ContentModelRibbonButton<'ribbonButtonBulletedListStyle'> = { key: 'ribbonButtonBulletedListStyle', dropDownMenu: { items: dropDownMenuItems }, unlocalizedText: 'Set unordered list style', @@ -23,10 +22,8 @@ export const setBulletedListStyleButton: RibbonButton<'ribbonButtonBulletedListS onClick: (editor, key) => { const value = parseInt(key); - if (isContentModelEditor(editor)) { - setListStyle(editor, { - unorderedStyleType: value, - }); - } + setListStyle(editor, { + unorderedStyleType: value, + }); }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/setHeadingLevelButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/setHeadingLevelButton.ts index 2b9dd18e30d..d1934298d3a 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/setHeadingLevelButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/setHeadingLevelButton.ts @@ -1,4 +1,4 @@ -import { isContentModelEditor } from 'roosterjs-content-model-editor'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { setHeadingLevel } from 'roosterjs-content-model-api'; import { getButtons, @@ -20,7 +20,7 @@ const keys: HeadingButtonStringKey[] = [ 'buttonNameHeading6', ]; -export const setHeadingLevelButton: RibbonButton = { +export const setHeadingLevelButton: ContentModelRibbonButton = { dropDownMenu: { ...originalHeadingButton.dropDownMenu, }, @@ -30,8 +30,6 @@ export const setHeadingLevelButton: RibbonButton = { onClick: (editor, key) => { const headingLevel = keys.indexOf(key); - if (isContentModelEditor(editor) && headingLevel >= 0) { - setHeadingLevel(editor, headingLevel as 0 | 1 | 2 | 3 | 4 | 5 | 6); - } + setHeadingLevel(editor, headingLevel as 0 | 1 | 2 | 3 | 4 | 5 | 6); }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/setNumberedListStyleButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/setNumberedListStyleButton.ts index ae195e3fb3a..f1215267f30 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/setNumberedListStyleButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/setNumberedListStyleButton.ts @@ -1,6 +1,5 @@ -import { isContentModelEditor } from 'roosterjs-content-model-editor'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { NumberingListType } from 'roosterjs-content-model-core'; -import { RibbonButton } from 'roosterjs-react'; import { setListStyle } from 'roosterjs-content-model-api'; const dropDownMenuItems = { @@ -26,7 +25,7 @@ const dropDownMenuItems = { [NumberingListType.UpperRomanDash]: 'UpperRomanDash', }; -export const setNumberedListStyleButton: RibbonButton<'ribbonButtonNumberedListStyle'> = { +export const setNumberedListStyleButton: ContentModelRibbonButton<'ribbonButtonNumberedListStyle'> = { key: 'ribbonButtonNumberedListStyle', dropDownMenu: { items: dropDownMenuItems }, unlocalizedText: 'Set ordered list style', @@ -35,10 +34,8 @@ export const setNumberedListStyleButton: RibbonButton<'ribbonButtonNumberedListS onClick: (editor, key) => { const value = parseInt(key); - if (isContentModelEditor(editor)) { - setListStyle(editor, { - orderedStyleType: value, - }); - } + setListStyle(editor, { + orderedStyleType: value, + }); }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/setTableCellShadeButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/setTableCellShadeButton.ts index f463e37a3f2..a8ebb0fb2d6 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/setTableCellShadeButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/setTableCellShadeButton.ts @@ -1,4 +1,4 @@ -import { isContentModelEditor } from 'roosterjs-content-model-editor'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { setTableCellShade } from 'roosterjs-content-model-api'; import { BackgroundColorKeys, @@ -12,7 +12,7 @@ const originalBackgroundColorButton: RibbonButton = getButt KnownRibbonButtonKey.BackgroundColor, ])[0] as RibbonButton; -export const setTableCellShadeButton: RibbonButton< +export const setTableCellShadeButton: ContentModelRibbonButton< 'ribbonButtonSetTableCellShade' | BackgroundColorKeys > = { dropDownMenu: { @@ -24,7 +24,7 @@ export const setTableCellShadeButton: RibbonButton< iconName: 'BackgroundColor', isDisabled: formatState => !formatState.isInTable, onClick: (editor, key) => { - if (key != 'ribbonButtonSetTableCellShade' && isContentModelEditor(editor)) { + if (key != 'ribbonButtonSetTableCellShade') { const color = getBackgroundColorValue(key); // Content Model doesn't need dark mode color at this point, so always pass in light mode color diff --git a/demo/scripts/controls/ribbonButtons/contentModel/setTableHeaderButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/setTableHeaderButton.ts index 83795a3336b..ff3b71555cc 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/setTableHeaderButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/setTableHeaderButton.ts @@ -1,17 +1,13 @@ -import { formatTable } from 'roosterjs-content-model-api'; -import { getFormatState } from 'roosterjs-editor-api'; -import { isContentModelEditor } from 'roosterjs-content-model-editor'; -import { RibbonButton } from 'roosterjs-react'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; +import { formatTable, getFormatState } from 'roosterjs-content-model-api'; -export const setTableHeaderButton: RibbonButton<'ribbonButtonSetTableHeader'> = { +export const setTableHeaderButton: ContentModelRibbonButton<'ribbonButtonSetTableHeader'> = { key: 'ribbonButtonSetTableHeader', unlocalizedText: 'Toggle table header', iconName: 'Header', isDisabled: formatState => !formatState.isInTable, onClick: editor => { - if (isContentModelEditor(editor)) { - const format = getFormatState(editor); - formatTable(editor, { hasHeaderRow: !format.tableHasHeader }, true /*keepCellShade*/); - } + const format = getFormatState(editor); + formatTable(editor, { hasHeaderRow: !format.tableHasHeader }, true /*keepCellShade*/); }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/spaceBeforeAfterButtons.ts b/demo/scripts/controls/ribbonButtons/contentModel/spaceBeforeAfterButtons.ts index 9dd219ddfa5..2cf3d481e2e 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/spaceBeforeAfterButtons.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/spaceBeforeAfterButtons.ts @@ -1,6 +1,5 @@ +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { getFormatState, setParagraphMargin } from 'roosterjs-content-model-api'; -import { isContentModelEditor } from 'roosterjs-content-model-editor'; -import { RibbonButton } from 'roosterjs-react'; const spaceAfterButtonKey = 'buttonNameSpaceAfter'; const spaceBeforeButtonKey = 'buttonNameSpaceBefore'; @@ -9,20 +8,19 @@ const spaceBeforeButtonKey = 'buttonNameSpaceBefore'; * @internal * "Add space after" button on the format ribbon */ -export const spaceAfterButton: RibbonButton = { +export const spaceAfterButton: ContentModelRibbonButton = { key: spaceAfterButtonKey, unlocalizedText: 'Remove space after', iconName: 'CaretDown8', isChecked: formatState => !formatState.marginBottom || parseInt(formatState.marginBottom) <= 0, onClick: editor => { - if (isContentModelEditor(editor)) { - const marginBottom = getFormatState(editor).marginBottom; - setParagraphMargin( - editor, - undefined /* marginTop */, - parseInt(marginBottom) ? null : '8pt' - ); - } + const marginBottom = getFormatState(editor).marginBottom; + setParagraphMargin( + editor, + undefined /* marginTop */, + parseInt(marginBottom) ? null : '8pt' + ); + return true; }, }; @@ -31,20 +29,19 @@ export const spaceAfterButton: RibbonButton = { * @internal * "Add space before" button on the format ribbon */ -export const spaceBeforeButton: RibbonButton = { +export const spaceBeforeButton: ContentModelRibbonButton = { key: spaceBeforeButtonKey, unlocalizedText: 'Add space before', iconName: 'CaretUp8', isChecked: formatState => parseInt(formatState.marginTop) > 0, onClick: editor => { - if (isContentModelEditor(editor)) { - const marginTop = getFormatState(editor).marginTop; - setParagraphMargin( - editor, - parseInt(marginTop) ? null : '12pt', - undefined /* marginBottom */ - ); - } + const marginTop = getFormatState(editor).marginTop; + setParagraphMargin( + editor, + parseInt(marginTop) ? null : '12pt', + undefined /* marginBottom */ + ); + return true; }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/spacingButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/spacingButton.ts index 32f50c7eb6c..cefc667dc5f 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/spacingButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/spacingButton.ts @@ -1,6 +1,5 @@ -import { isContentModelEditor } from 'roosterjs-content-model-editor'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { setSpacing } from 'roosterjs-content-model-api'; -import type { RibbonButton } from 'roosterjs-react'; const SPACING_OPTIONS = ['1.0', '1.15', '1.5', '2.0']; const NORMAL_SPACING = 1.2; @@ -18,7 +17,7 @@ function findClosest(lineHeight?: string) { * @internal * "Spacing" button on the format ribbon */ -export const spacingButton: RibbonButton = { +export const spacingButton: ContentModelRibbonButton = { key: spacingButtonKey, unlocalizedText: 'Spacing', iconName: 'LineSpacing', @@ -31,8 +30,6 @@ export const spacingButton: RibbonButton = { allowLivePreview: true, }, onClick: (editor, size) => { - if (isContentModelEditor(editor)) { - setSpacing(editor, +size * NORMAL_SPACING); - } + setSpacing(editor, +size * NORMAL_SPACING); }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/strikethroughButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/strikethroughButton.ts index 040752c5b5d..b767de2c01b 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/strikethroughButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/strikethroughButton.ts @@ -1,20 +1,19 @@ -import { isContentModelEditor } from 'roosterjs-content-model-editor'; -import { RibbonButton, StrikethroughButtonStringKey } from 'roosterjs-react'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; +import { StrikethroughButtonStringKey } from 'roosterjs-react'; import { toggleStrikethrough } from 'roosterjs-content-model-api'; /** * @internal * "Strikethrough" button on the format ribbon */ -export const strikethroughButton: RibbonButton = { +export const strikethroughButton: ContentModelRibbonButton = { key: 'buttonNameStrikethrough', unlocalizedText: 'Strikethrough', iconName: 'Strikethrough', isChecked: formatState => formatState.isStrikeThrough, onClick: editor => { - if (isContentModelEditor(editor)) { - toggleStrikethrough(editor); - } + toggleStrikethrough(editor); + return true; }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/subscriptButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/subscriptButton.ts index 4490322311a..12625017db3 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/subscriptButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/subscriptButton.ts @@ -1,20 +1,19 @@ -import { isContentModelEditor } from 'roosterjs-content-model-editor'; -import { RibbonButton, SubscriptButtonStringKey } from 'roosterjs-react'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; +import { SubscriptButtonStringKey } from 'roosterjs-react'; import { toggleSubscript } from 'roosterjs-content-model-api'; /** * @internal * "Subscript" button on the format ribbon */ -export const subscriptButton: RibbonButton = { +export const subscriptButton: ContentModelRibbonButton = { key: 'buttonNameSubscript', unlocalizedText: 'Subscript', iconName: 'Subscript', isChecked: formatState => formatState.isSubscript, onClick: editor => { - if (isContentModelEditor(editor)) { - toggleSubscript(editor); - } + toggleSubscript(editor); + return true; }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/superscriptButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/superscriptButton.ts index ad2c161e357..e32c0e85cac 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/superscriptButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/superscriptButton.ts @@ -1,20 +1,19 @@ -import { isContentModelEditor } from 'roosterjs-content-model-editor'; -import { RibbonButton, SuperscriptButtonStringKey } from 'roosterjs-react'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; +import { SuperscriptButtonStringKey } from 'roosterjs-react'; import { toggleSuperscript } from 'roosterjs-content-model-api'; /** * @internal * "Superscript" button on the format ribbon */ -export const superscriptButton: RibbonButton = { +export const superscriptButton: ContentModelRibbonButton = { key: 'buttonNameSuperscript', unlocalizedText: 'Superscript', iconName: 'Superscript', isChecked: formatState => formatState.isSuperscript, onClick: editor => { - if (isContentModelEditor(editor)) { - toggleSuperscript(editor); - } + toggleSuperscript(editor); + return true; }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/tableBorderApplyButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/tableBorderApplyButton.ts index 12552ed4c3d..d28572ba532 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/tableBorderApplyButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/tableBorderApplyButton.ts @@ -1,8 +1,7 @@ +import ContentModelRibbonButton from './ContentModelRibbonButton'; import MainPaneBase from '../../MainPaneBase'; import { applyTableBorderFormat } from 'roosterjs-content-model-api'; import { BorderOperations } from 'roosterjs-content-model-types'; -import { isContentModelEditor } from 'roosterjs-content-model-editor'; -import { RibbonButton } from 'roosterjs-react'; const TABLE_OPERATIONS: Record = { menuNameTableAllBorder: 'allBorders', @@ -15,7 +14,7 @@ const TABLE_OPERATIONS: Record = { menuNameTableOutsideBorder: 'outsideBorders', }; -export const tableBorderApplyButton: RibbonButton<'ribbonButtonTableBorder'> = { +export const tableBorderApplyButton: ContentModelRibbonButton<'ribbonButtonTableBorder'> = { key: 'ribbonButtonTableBorder', iconName: 'TableComputed', unlocalizedText: 'Table Border', @@ -33,9 +32,7 @@ export const tableBorderApplyButton: RibbonButton<'ribbonButtonTableBorder'> = { }, }, onClick: (editor, key) => { - if (isContentModelEditor(editor) && key != 'ribbonButtonTableBorder') { - const border = MainPaneBase.getInstance().getTableBorder(); - applyTableBorderFormat(editor, border, TABLE_OPERATIONS[key]); - } + const border = MainPaneBase.getInstance().getTableBorder(); + applyTableBorderFormat(editor, border, TABLE_OPERATIONS[key]); }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/tableBorderColorButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/tableBorderColorButton.ts index 3548f8974e7..44e7f96a934 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/tableBorderColorButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/tableBorderColorButton.ts @@ -1,7 +1,6 @@ +import ContentModelRibbonButton from './ContentModelRibbonButton'; import MainPaneBase from '../../MainPaneBase'; -import { getButtons, getTextColorValue, KnownRibbonButtonKey } from 'roosterjs-react'; -import { isContentModelEditor } from 'roosterjs-content-model-editor'; -import { RibbonButton } from 'roosterjs-react'; +import { getButtons, getTextColorValue, KnownRibbonButtonKey, RibbonButton } from 'roosterjs-react'; const originalButton = getButtons([KnownRibbonButtonKey.TextColor])[0] as RibbonButton< 'buttonNameTableBorderColor' @@ -11,14 +10,14 @@ const originalButton = getButtons([KnownRibbonButtonKey.TextColor])[0] as Ribbon * @internal * "Table Border Color" button on the format ribbon */ -export const tableBorderColorButton: RibbonButton<'buttonNameTableBorderColor'> = { +export const tableBorderColorButton: ContentModelRibbonButton<'buttonNameTableBorderColor'> = { ...originalButton, unlocalizedText: 'Table Border Color', iconName: 'ColorSolid', isDisabled: formatState => !formatState.isInTable, onClick: (editor, key) => { // This check will always be true, add it here just to satisfy compiler - if (key != 'buttonNameTableBorderColor' && isContentModelEditor(editor)) { + if (key != 'buttonNameTableBorderColor') { MainPaneBase.getInstance().setTableBorderColor(getTextColorValue(key).lightModeColor); editor.focus(); } diff --git a/demo/scripts/controls/ribbonButtons/contentModel/tableBorderStyleButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/tableBorderStyleButton.ts index 525c3ba7612..f9e7c410ab4 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/tableBorderStyleButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/tableBorderStyleButton.ts @@ -1,6 +1,5 @@ +import ContentModelRibbonButton from './ContentModelRibbonButton'; import MainPaneBase from '../../MainPaneBase'; -import { isContentModelEditor } from 'roosterjs-content-model-editor'; -import { RibbonButton } from 'roosterjs-react'; const STYLES: Record = { dashed: 'dashed', @@ -17,7 +16,7 @@ const STYLES: Record = { * @internal * "Table Border Style" button on the format ribbon */ -export const tableBorderStyleButton: RibbonButton<'buttonNameTableBorderStyle'> = { +export const tableBorderStyleButton: ContentModelRibbonButton<'buttonNameTableBorderStyle'> = { key: 'buttonNameTableBorderStyle', unlocalizedText: 'Table Border Style', iconName: 'LineStyle', @@ -27,10 +26,9 @@ export const tableBorderStyleButton: RibbonButton<'buttonNameTableBorderStyle'> allowLivePreview: true, }, onClick: (editor, style) => { - if (isContentModelEditor(editor)) { - MainPaneBase.getInstance().setTableBorderStyle(style); - editor.focus(); - } + MainPaneBase.getInstance().setTableBorderStyle(style); + editor.focus(); + return true; }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/tableBorderWidthButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/tableBorderWidthButton.ts index 89eeb6dc008..d1fefea87be 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/tableBorderWidthButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/tableBorderWidthButton.ts @@ -1,6 +1,5 @@ +import ContentModelRibbonButton from './ContentModelRibbonButton'; import MainPaneBase from '../../MainPaneBase'; -import { isContentModelEditor } from 'roosterjs-content-model-editor'; -import { RibbonButton } from 'roosterjs-react'; const WIDTH = [0.25, 0.5, 0.75, 1, 1.5, 2.25, 3, 4.5, 6]; @@ -8,7 +7,7 @@ const WIDTH = [0.25, 0.5, 0.75, 1, 1.5, 2.25, 3, 4.5, 6]; * @internal * "Table Border Width" button on the format ribbon */ -export const tableBorderWidthButton: RibbonButton<'buttonNameTableBorderWidth'> = { +export const tableBorderWidthButton: ContentModelRibbonButton<'buttonNameTableBorderWidth'> = { key: 'buttonNameTableBorderWidth', unlocalizedText: 'Table Border Width', iconName: 'LineThickness', @@ -21,10 +20,9 @@ export const tableBorderWidthButton: RibbonButton<'buttonNameTableBorderWidth'> allowLivePreview: true, }, onClick: (editor, width) => { - if (isContentModelEditor(editor)) { - MainPaneBase.getInstance().setTableBorderWidth(width); - editor.focus(); - } + MainPaneBase.getInstance().setTableBorderWidth(width); + editor.focus(); + return true; }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/tableEditButtons.ts b/demo/scripts/controls/ribbonButtons/contentModel/tableEditButtons.ts index fdf7e39e4f1..f3d2ed1095b 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/tableEditButtons.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/tableEditButtons.ts @@ -1,8 +1,7 @@ +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { editTable } from 'roosterjs-content-model-api'; -import { isContentModelEditor } from 'roosterjs-content-model-editor'; import { TableOperation } from 'roosterjs-content-model-types'; import { - RibbonButton, TableEditAlignMenuItemStringKey, TableEditAlignTableMenuItemStringKey, TableEditDeleteMenuItemStringKey, @@ -38,7 +37,7 @@ const TableEditOperationMap: Partial = { key: 'ribbonButtonTableInsert', @@ -54,13 +53,13 @@ export const tableInsertButton: RibbonButton< }, }, onClick: (editor, key) => { - if (isContentModelEditor(editor) && key != 'ribbonButtonTableInsert') { + if (key != 'ribbonButtonTableInsert') { editTable(editor, TableEditOperationMap[key]); } }, }; -export const tableDeleteButton: RibbonButton< +export const tableDeleteButton: ContentModelRibbonButton< 'ribbonButtonTableDelete' | TableEditDeleteMenuItemStringKey > = { key: 'ribbonButtonTableDelete', @@ -75,13 +74,13 @@ export const tableDeleteButton: RibbonButton< }, }, onClick: (editor, key) => { - if (isContentModelEditor(editor) && key != 'ribbonButtonTableDelete') { + if (key != 'ribbonButtonTableDelete') { editTable(editor, TableEditOperationMap[key]); } }, }; -export const tableMergeButton: RibbonButton< +export const tableMergeButton: ContentModelRibbonButton< 'ribbonButtonTableMerge' | TableEditMergeMenuItemStringKey > = { key: 'ribbonButtonTableMerge', @@ -99,13 +98,13 @@ export const tableMergeButton: RibbonButton< }, }, onClick: (editor, key) => { - if (isContentModelEditor(editor) && key != 'ribbonButtonTableMerge') { + if (key != 'ribbonButtonTableMerge') { editTable(editor, TableEditOperationMap[key]); } }, }; -export const tableSplitButton: RibbonButton< +export const tableSplitButton: ContentModelRibbonButton< 'ribbonButtonTableSplit' | TableEditSplitMenuItemStringKey > = { key: 'ribbonButtonTableSplit', @@ -119,13 +118,13 @@ export const tableSplitButton: RibbonButton< }, }, onClick: (editor, key) => { - if (isContentModelEditor(editor) && key != 'ribbonButtonTableSplit') { + if (key != 'ribbonButtonTableSplit') { editTable(editor, TableEditOperationMap[key]); } }, }; -export const tableAlignCellButton: RibbonButton< +export const tableAlignCellButton: ContentModelRibbonButton< 'ribbonButtonTableAlignCell' | TableEditAlignMenuItemStringKey > = { key: 'ribbonButtonTableAlignCell', @@ -144,13 +143,13 @@ export const tableAlignCellButton: RibbonButton< }, }, onClick: (editor, key) => { - if (isContentModelEditor(editor) && key != 'ribbonButtonTableAlignCell') { + if (key != 'ribbonButtonTableAlignCell') { editTable(editor, TableEditOperationMap[key]); } }, }; -export const tableAlignTableButton: RibbonButton< +export const tableAlignTableButton: ContentModelRibbonButton< 'ribbonButtonTableAlignTable' | TableEditAlignTableMenuItemStringKey > = { key: 'ribbonButtonTableAlignTable', @@ -165,7 +164,7 @@ export const tableAlignTableButton: RibbonButton< }, }, onClick: (editor, key) => { - if (isContentModelEditor(editor) && key != 'ribbonButtonTableAlignTable') { + if (key != 'ribbonButtonTableAlignTable') { editTable(editor, TableEditOperationMap[key]); } }, diff --git a/demo/scripts/controls/ribbonButtons/contentModel/textColorButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/textColorButton.ts index 168ad36c22c..5b9ed653586 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/textColorButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/textColorButton.ts @@ -1,4 +1,4 @@ -import { isContentModelEditor } from 'roosterjs-content-model-editor'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { setTextColor } from 'roosterjs-content-model-api'; import { getButtons, @@ -16,11 +16,11 @@ const originalButton = getButtons([KnownRibbonButtonKey.TextColor])[0] as Ribbon * @internal * "Text color" button on the format ribbon */ -export const textColorButton: RibbonButton = { +export const textColorButton: ContentModelRibbonButton = { ...originalButton, onClick: (editor, key) => { // This check will always be true, add it here just to satisfy compiler - if (key != 'buttonNameTextColor' && isContentModelEditor(editor)) { + if (key != 'buttonNameTextColor') { setTextColor(editor, getTextColorValue(key).lightModeColor); } }, diff --git a/demo/scripts/controls/ribbonButtons/contentModel/underlineButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/underlineButton.ts index 0abfc7ada87..0d8a5ede152 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/underlineButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/underlineButton.ts @@ -1,20 +1,19 @@ -import { isContentModelEditor } from 'roosterjs-content-model-editor'; -import { RibbonButton, UnderlineButtonStringKey } from 'roosterjs-react'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { toggleUnderline } from 'roosterjs-content-model-api'; +import { UnderlineButtonStringKey } from 'roosterjs-react'; /** * @internal * "Underline" button on the format ribbon */ -export const underlineButton: RibbonButton = { +export const underlineButton: ContentModelRibbonButton = { key: 'buttonNameUnderline', unlocalizedText: 'Underline', iconName: 'Underline', isChecked: formatState => formatState.isUnderline, onClick: editor => { - if (isContentModelEditor(editor)) { - toggleUnderline(editor); - } + toggleUnderline(editor); + return true; }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/undoButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/undoButton.ts index 0ae337f7ce0..2f07a1a560c 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/undoButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/undoButton.ts @@ -1,20 +1,19 @@ -import { isContentModelEditor } from 'roosterjs-content-model-editor'; -import { RibbonButton, UndoButtonStringKey } from 'roosterjs-react'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { undo } from 'roosterjs-content-model-core'; +import { UndoButtonStringKey } from 'roosterjs-react'; /** * @internal * "Undo" button on the format ribbon */ -export const undoButton: RibbonButton = { +export const undoButton: ContentModelRibbonButton = { key: 'buttonNameUndo', unlocalizedText: 'Undo', iconName: 'undo', isDisabled: formatState => !formatState.canUndo, onClick: editor => { - if (isContentModelEditor(editor)) { - undo(editor); - } + undo(editor); + return true; }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/zoom.ts b/demo/scripts/controls/ribbonButtons/contentModel/zoom.ts new file mode 100644 index 00000000000..a4ee19075a7 --- /dev/null +++ b/demo/scripts/controls/ribbonButtons/contentModel/zoom.ts @@ -0,0 +1,49 @@ +import ContentModelRibbonButton from './ContentModelRibbonButton'; +import MainPaneBase from '../../MainPaneBase'; +import { getObjectKeys } from 'roosterjs-editor-dom'; + +const DropDownItems = { + 'zoom50%': '50%', + 'zoom75%': '75%', + 'zoom100%': '100%', + 'zoom150%': '150%', + 'zoom200%': '200%', +}; + +const DropDownValues: { [key in keyof typeof DropDownItems]: number } = { + 'zoom50%': 0.5, + 'zoom75%': 0.75, + 'zoom100%': 1, + 'zoom150%': 1.5, + 'zoom200%': 2, +}; + +/** + * Key of localized strings of Zoom button + */ +export type ZoomButtonStringKey = 'buttonNameZoom'; + +/** + * "Zoom" button on the format ribbon + */ +export const zoom: ContentModelRibbonButton = { + key: 'buttonNameZoom', + unlocalizedText: 'Zoom', + iconName: 'ZoomIn', + dropDownMenu: { + items: DropDownItems, + getSelectedItemKey: formatState => + getObjectKeys(DropDownItems).filter( + key => DropDownValues[key] == formatState.zoomScale + )[0], + }, + onClick: (editor, key) => { + const zoomScale = DropDownValues[key as keyof typeof DropDownItems]; + editor.setZoomScale(zoomScale); + editor.focus(); + + // Let main pane know this state change so that it can be persisted when pop out/pop in + MainPaneBase.getInstance().setScale(zoomScale); + return true; + }, +}; diff --git a/demo/scripts/controls/sidePane/SidePane.tsx b/demo/scripts/controls/sidePane/SidePane.tsx index 3459b1ab1bd..8449b230a27 100644 --- a/demo/scripts/controls/sidePane/SidePane.tsx +++ b/demo/scripts/controls/sidePane/SidePane.tsx @@ -3,10 +3,11 @@ import SidePanePlugin from '../SidePanePlugin'; const classicStyles = require('./SidePane.scss'); const contentModelStyles = require('./ContentModelSidePane.scss'); +const standaloneModelStyles = require('./StandaloneSidePane.scss'); export interface SidePaneProps { plugins: SidePanePlugin[]; - isContentModelDemo: boolean; + mode: 'classical' | 'contentModel' | 'standalone'; className?: string; } @@ -96,6 +97,10 @@ export default class SidePane extends React.Component ); diff --git a/demo/scripts/controls/sidePane/editorOptions/EditorOptionsPlugin.ts b/demo/scripts/controls/sidePane/editorOptions/EditorOptionsPlugin.ts index 8c111a1cbca..01c06bc44d0 100644 --- a/demo/scripts/controls/sidePane/editorOptions/EditorOptionsPlugin.ts +++ b/demo/scripts/controls/sidePane/editorOptions/EditorOptionsPlugin.ts @@ -20,7 +20,6 @@ const initialState: BuildInPluginState = { tableEditMenu: true, contextMenu: true, autoFormat: true, - contentModelPaste: false, announce: true, }, contentEditFeatures: getDefaultContentEditFeatureSettings(), diff --git a/demo/scripts/controls/sidePane/editorOptions/codes/PluginsCode.ts b/demo/scripts/controls/sidePane/editorOptions/codes/PluginsCode.ts index 088f787b171..0148327ffff 100644 --- a/demo/scripts/controls/sidePane/editorOptions/codes/PluginsCode.ts +++ b/demo/scripts/controls/sidePane/editorOptions/codes/PluginsCode.ts @@ -22,7 +22,7 @@ export default class PluginsCode extends CodeElement { this.plugins = [ pluginList.contentEdit && new ContentEditCode(state.contentEditFeatures), pluginList.hyperlink && new HyperLinkCode(state.linkTitle), - pluginList.contentModelPaste && new ContentModelPasteCode(), + new ContentModelPasteCode(), pluginList.watermark && new WatermarkCode(this.state.watermarkText), pluginList.imageEdit && new ImageEditCode(), pluginList.cutPasteListChain && new CutPasteListChainCode(), diff --git a/demo/scripts/controls/sidePane/eventViewer/ContentModelEventViewPane.tsx b/demo/scripts/controls/sidePane/eventViewer/ContentModelEventViewPane.tsx index b57357141c0..4fbe6876ec9 100644 --- a/demo/scripts/controls/sidePane/eventViewer/ContentModelEventViewPane.tsx +++ b/demo/scripts/controls/sidePane/eventViewer/ContentModelEventViewPane.tsx @@ -1,5 +1,4 @@ import * as React from 'react'; -import { ContentModelContentChangedEvent } from 'roosterjs-content-model-types'; import { EntityOperation, PluginEvent, PluginEventType } from 'roosterjs-editor-types'; import { SidePaneElementProps } from '../SidePaneElement'; import { @@ -177,25 +176,6 @@ export default class ContentModelEventViewPane extends React.Component< Source= {event.source}, Data= {event.data && event.data.toString && event.data.toString()} - {!!(event as ContentModelContentChangedEvent).contentModel && ( -
- Content Model -
-                                    {JSON.stringify(
-                                        (event as ContentModelContentChangedEvent).contentModel,
-                                        (key, value) =>
-                                            safeInstanceOf(value, 'Node')
-                                                ? Object.prototype.toString.apply(value)
-                                                : key == 'src'
-                                                ? value.length > 100
-                                                    ? value.substring(0, 97) + '...'
-                                                    : value
-                                                : value,
-                                        2
-                                    )}
-                                
-
- )} ); diff --git a/demo/scripts/controls/sidePane/snapshot/ContentModelSnapshotPane.tsx b/demo/scripts/controls/sidePane/snapshot/ContentModelSnapshotPane.tsx index 810eb298cec..d9423c049df 100644 --- a/demo/scripts/controls/sidePane/snapshot/ContentModelSnapshotPane.tsx +++ b/demo/scripts/controls/sidePane/snapshot/ContentModelSnapshotPane.tsx @@ -1,6 +1,5 @@ import * as React from 'react'; import { EntityState, Snapshot, SnapshotSelection } from 'roosterjs-content-model-types'; -import { ModeIndependentColor } from 'roosterjs-editor-types'; const styles = require('./SnapshotPane.scss'); @@ -21,7 +20,6 @@ export default class ContentModelSnapshotPane extends React.Component< ContentModelSnapshotPaneState > { private html = React.createRef(); - private knownColors = React.createRef(); private entityStates = React.createRef(); private isDarkColor = React.createRef(); private selection = React.createRef(); @@ -38,7 +36,7 @@ export default class ContentModelSnapshotPane extends React.Component< render() { return ( -
+

Undo Snapshots

{this.state.snapshots.map(this.renderItem)} @@ -47,6 +45,7 @@ export default class ContentModelSnapshotPane extends React.Component<
{' '} +
HTML: