diff --git a/.vscode/launch.json b/.vscode/launch.json index 297b6d99801..a73736d16d3 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -17,6 +17,14 @@ "args": ["start", "--chrome"], "console": "integratedTerminal" }, + { + "type": "node", + "request": "launch", + "name": "Test all files (Firefox)", + "program": "${workspaceFolder}/node_modules/karma/bin/karma", + "args": ["start", "--firefox"], + "console": "integratedTerminal" + }, { "type": "node", "request": "launch", @@ -33,6 +41,14 @@ "args": ["start", "--chrome", "--components", "${fileBasenameNoExtension}"], "console": "integratedTerminal" }, + { + "type": "node", + "request": "launch", + "name": "Test current file (Firefox)", + "program": "${workspaceFolder}/node_modules/karma/bin/karma", + "args": ["start", "--firefox", "--components", "${fileBasenameNoExtension}"], + "console": "integratedTerminal" + }, { "type": "node", "request": "launch", @@ -49,6 +65,14 @@ "args": ["start", "--chrome", "--no-single-run"], "console": "integratedTerminal" }, + { + "type": "node", + "request": "launch", + "name": "Debug All Unit Tests (Firefox)", + "program": "${workspaceFolder}/node_modules/karma/bin/karma", + "args": ["start", "--firefox", "--no-single-run"], + "console": "integratedTerminal" + }, { "type": "node", "request": "launch", @@ -71,6 +95,20 @@ ], "console": "integratedTerminal" }, + { + "type": "node", + "request": "launch", + "name": "Debug current unit test file (Firefox)", + "program": "${workspaceFolder}/node_modules/karma/bin/karma", + "args": [ + "start", + "--firefox", + "--components", + "${fileBasenameNoExtension}", + "--no-single-run" + ], + "console": "integratedTerminal" + }, { "type": "chrome", "request": "launch", diff --git a/demo/scripts/controlsV2/demoButtons/exportContentButton.ts b/demo/scripts/controlsV2/demoButtons/exportContentButton.ts index 84057a5e470..729d336fcee 100644 --- a/demo/scripts/controlsV2/demoButtons/exportContentButton.ts +++ b/demo/scripts/controlsV2/demoButtons/exportContentButton.ts @@ -1,4 +1,5 @@ import { exportContent } from 'roosterjs-content-model-core'; +import { ModelToTextCallbacks } from 'roosterjs-content-model-types'; import type { RibbonButton } from '../roosterjsReact/ribbon'; /** @@ -9,6 +10,10 @@ export type ExportButtonStringKey = | 'menuNameExportHTML' | 'menuNameExportText'; +const callbacks: ModelToTextCallbacks = { + onImage: () => '[Image]', +}; + /** * "Export content" button on the format ribbon */ @@ -30,7 +35,7 @@ export const exportContentButton: RibbonButton = { if (key == 'menuNameExportHTML') { html = exportContent(editor); } else if (key == 'menuNameExportText') { - html = `
${exportContent(editor, 'PlainText')}
`; + html = `
${exportContent(editor, 'PlainText', callbacks)}
`; } win.document.write(editor.getTrustedHTMLHandler()(html)); diff --git a/demo/scripts/controlsV2/sidePane/apiPlayground/insertEntity/InsertEntityPane.tsx b/demo/scripts/controlsV2/sidePane/apiPlayground/insertEntity/InsertEntityPane.tsx index 15d6f7f6f1c..f03ddcd9e7f 100644 --- a/demo/scripts/controlsV2/sidePane/apiPlayground/insertEntity/InsertEntityPane.tsx +++ b/demo/scripts/controlsV2/sidePane/apiPlayground/insertEntity/InsertEntityPane.tsx @@ -141,10 +141,13 @@ export default class InsertEntityPane extends React.Component { - const model = this.props.getEditor().getContentModelCopy('connected'); const allEntities: ContentModelEntity[] = []; - findAllEntities(model, allEntities); + this.props.getEditor().formatContentModel(model => { + findAllEntities(model, allEntities); + + return false; + }); this.setState({ entities: allEntities.filter(e => !!e), diff --git a/demo/scripts/controlsV2/sidePane/contentModel/ContentModelPane.tsx b/demo/scripts/controlsV2/sidePane/contentModel/ContentModelPane.tsx index a492c87dd4b..0084a93b471 100644 --- a/demo/scripts/controlsV2/sidePane/contentModel/ContentModelPane.tsx +++ b/demo/scripts/controlsV2/sidePane/contentModel/ContentModelPane.tsx @@ -3,7 +3,6 @@ import { ContentModelDocument } from 'roosterjs-content-model-types'; import { ContentModelDocumentView } from './components/model/ContentModelDocumentView'; import { exportButton } from './buttons/exportButton'; import { importModelButton } from './buttons/importModelButton'; -import { refreshButton } from './buttons/refreshButton'; import { Ribbon, RibbonButton, RibbonPlugin } from '../../roosterjsReact/ribbon'; import { SidePaneElementProps } from '../SidePaneElement'; @@ -15,6 +14,7 @@ export interface ContentModelPaneState { export interface ContentModelPaneProps extends ContentModelPaneState, SidePaneElementProps { ribbonPlugin: RibbonPlugin; + refreshButton: RibbonButton; } export class ContentModelPane extends React.Component< @@ -26,7 +26,7 @@ export class ContentModelPane extends React.Component< constructor(props: ContentModelPaneProps) { super(props); - this.contentModelButtons = [refreshButton, exportButton, importModelButton]; + this.contentModelButtons = [this.props.refreshButton, exportButton, importModelButton]; this.state = { model: null, diff --git a/demo/scripts/controlsV2/sidePane/contentModel/ContentModelPanePlugin.ts b/demo/scripts/controlsV2/sidePane/contentModel/ContentModelPanePlugin.ts index c56b67835bd..4bb8825a43a 100644 --- a/demo/scripts/controlsV2/sidePane/contentModel/ContentModelPanePlugin.ts +++ b/demo/scripts/controlsV2/sidePane/contentModel/ContentModelPanePlugin.ts @@ -1,5 +1,6 @@ import { ContentModelPane, ContentModelPaneProps } from './ContentModelPane'; -import { createRibbonPlugin, RibbonPlugin } from '../../roosterjsReact/ribbon'; +import { createRibbonPlugin, RibbonButton, RibbonPlugin } from '../../roosterjsReact/ribbon'; +import { getRefreshButton } from './buttons/refreshButton'; import { IEditor, PluginEvent } from 'roosterjs-content-model-types'; import { setCurrentContentModel } from './currentModel'; import { SidePaneElementProps } from '../SidePaneElement'; @@ -10,10 +11,12 @@ export class ContentModelPanePlugin extends SidePanePluginImpl< ContentModelPaneProps > { private contentModelRibbon: RibbonPlugin; + private refreshButton: RibbonButton; constructor() { super(ContentModelPane, 'contentModel', 'Content Model'); this.contentModelRibbon = createRibbonPlugin(); + this.refreshButton = getRefreshButton(this); } initialize(editor: IEditor): void { @@ -33,13 +36,7 @@ export class ContentModelPanePlugin extends SidePanePluginImpl< } onPluginEvent(e: PluginEvent) { - if (e.eventType == 'contentChanged' && e.source == 'RefreshModel') { - this.getComponent(component => { - const model = this.editor.getContentModelCopy('connected'); - component.setContentModel(model); - setCurrentContentModel(model); - }); - } else if ( + if ( e.eventType == 'input' || e.eventType == 'selectionChanged' || e.eventType == 'editorReady' @@ -59,6 +56,7 @@ export class ContentModelPanePlugin extends SidePanePluginImpl< ...baseProps, model: null, ribbonPlugin: this.contentModelRibbon, + refreshButton: this.refreshButton, }; } @@ -68,11 +66,20 @@ export class ContentModelPanePlugin extends SidePanePluginImpl< } }; - private onModelChange = () => { + onModelChange = (force?: boolean) => { this.getComponent(component => { - const model = this.editor.getContentModelCopy('connected'); - component.setContentModel(model); - setCurrentContentModel(model); + this.editor.formatContentModel( + model => { + component.setContentModel(model); + setCurrentContentModel(model); + + return false; + }, + undefined, + { + tryGetFromCache: !force, + } + ); }); }; } diff --git a/demo/scripts/controlsV2/sidePane/contentModel/buttons/refreshButton.ts b/demo/scripts/controlsV2/sidePane/contentModel/buttons/refreshButton.ts index 5c4be9d6902..01c6faa486b 100644 --- a/demo/scripts/controlsV2/sidePane/contentModel/buttons/refreshButton.ts +++ b/demo/scripts/controlsV2/sidePane/contentModel/buttons/refreshButton.ts @@ -1,12 +1,15 @@ +import { ContentModelPanePlugin } from '../ContentModelPanePlugin'; import { RibbonButton } from '../../../roosterjsReact/ribbon'; -export const refreshButton: RibbonButton<'buttonNameRefresh'> = { - key: 'buttonNameRefresh', - unlocalizedText: 'Refresh', - iconName: 'Refresh', - onClick: editor => { - editor.triggerEvent('contentChanged', { - source: 'RefreshModel', - }); - }, -}; +export function getRefreshButton( + plugin: ContentModelPanePlugin +): RibbonButton<'buttonNameRefresh'> { + return { + key: 'buttonNameRefresh', + unlocalizedText: 'Refresh', + iconName: 'Refresh', + onClick: () => { + plugin.onModelChange(true /*force*/); + }, + }; +} diff --git a/package.json b/package.json index 4d0fa5e77e2..55b27622daa 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "test:chrome": "node tools/build.js normalize & karma start --chrome", "test:firefox": "node tools/build.js normalize & karma start --firefox", "test:debug": "node tools/build.js normalize & karma start --no-single-run --chrome", + "test:debug-firefox": "node tools/build.js normalize & karma start --no-single-run --firefox", "test:coverage": "node tools/build.js normalize & karma start --coverage --firefox --chrome", "publish": "node tools/build.js clean normalize buildcommonjs buildamd buildmjs dts pack packprod builddemo builddoc publish" }, diff --git a/packages/roosterjs-content-model-api/lib/index.ts b/packages/roosterjs-content-model-api/lib/index.ts index 7fc2bdf07d2..4209e3a1a5c 100644 --- a/packages/roosterjs-content-model-api/lib/index.ts +++ b/packages/roosterjs-content-model-api/lib/index.ts @@ -49,6 +49,7 @@ export { formatImageWithContentModel } from './publicApi/utils/formatImageWithCo export { formatParagraphWithContentModel } from './publicApi/utils/formatParagraphWithContentModel'; export { formatSegmentWithContentModel } from './publicApi/utils/formatSegmentWithContentModel'; export { formatTextSegmentBeforeSelectionMarker } from './publicApi/utils/formatTextSegmentBeforeSelectionMarker'; +export { formatInsertPointWithContentModel } from './publicApi/utils/formatInsertPointWithContentModel'; export { setListType } from './modelApi/list/setListType'; export { setModelListStyle } from './modelApi/list/setModelListStyle'; diff --git a/packages/roosterjs-content-model-api/lib/publicApi/utils/formatInsertPointWithContentModel.ts b/packages/roosterjs-content-model-api/lib/publicApi/utils/formatInsertPointWithContentModel.ts index ef3b5eb3ba8..462a9e00ac0 100644 --- a/packages/roosterjs-content-model-api/lib/publicApi/utils/formatInsertPointWithContentModel.ts +++ b/packages/roosterjs-content-model-api/lib/publicApi/utils/formatInsertPointWithContentModel.ts @@ -18,7 +18,11 @@ import type { } from 'roosterjs-content-model-types'; /** - * @internal + * Format content model at a given insert point with a callback function + * @param editor The editor object + * @param insertPoint The insert point to format + * @param callback The callback function to format the content model + * @param options Options to control the behavior of the formatting */ export function formatInsertPointWithContentModel( editor: IEditor, diff --git a/packages/roosterjs-content-model-core/lib/command/exportContent/exportContent.ts b/packages/roosterjs-content-model-core/lib/command/exportContent/exportContent.ts index 3ddf0e27006..428079c129b 100644 --- a/packages/roosterjs-content-model-core/lib/command/exportContent/exportContent.ts +++ b/packages/roosterjs-content-model-core/lib/command/exportContent/exportContent.ts @@ -3,21 +3,45 @@ import { contentModelToText, createModelToDomContext, } from 'roosterjs-content-model-dom'; -import type { ExportContentMode, IEditor, ModelToDomOption } from 'roosterjs-content-model-types'; +import type { + ExportContentMode, + IEditor, + ModelToDomOption, + ModelToTextCallbacks, +} from 'roosterjs-content-model-types'; /** - * Export string content of editor + * Export HTML content. If there are entities, this will cause EntityOperation event with option = 'replaceTemporaryContent' to get a dehydrated entity * @param editor The editor to get content from - * @param mode Mode of content to export. It supports: - * - HTML: Export HTML content. If there are entities, this will cause EntityOperation event with option = 'replaceTemporaryContent' to get a dehydrated entity - * - PlainText: Export plain text content - * - PlainTextFast: Export plain text using editor's textContent property directly + * @param mode Specify HTML to get plain text result. This is the default option * @param options @optional Options for Model to DOM conversion */ +export function exportContent(editor: IEditor, mode?: 'HTML', options?: ModelToDomOption): string; + +/** + * Export plain text content + * @param editor The editor to get content from + * @param mode Specify PlainText to get plain text result + * @param callbacks @optional Callbacks to customize conversion behavior + */ +export function exportContent( + editor: IEditor, + mode: 'PlainText', + callbacks?: ModelToTextCallbacks +): string; + +/** + * Export plain text using editor's textContent property directly + * @param editor The editor to get content from + * @param mode Specify PlainTextFast to get plain text result using textContent property + * @param options @optional Options for Model to DOM conversion + */ +export function exportContent(editor: IEditor, mode: 'PlainTextFast'): string; + export function exportContent( editor: IEditor, mode: ExportContentMode = 'HTML', - options?: ModelToDomOption + optionsOrCallbacks?: ModelToDomOption | ModelToTextCallbacks ): string { if (mode == 'PlainTextFast') { return editor.getDOMHelper().getTextContent(); @@ -25,7 +49,11 @@ export function exportContent( const model = editor.getContentModelCopy('clean'); if (mode == 'PlainText') { - return contentModelToText(model); + return contentModelToText( + model, + undefined /*separator*/, + optionsOrCallbacks as ModelToTextCallbacks + ); } else { const doc = editor.getDocument(); const div = doc.createElement('div'); @@ -34,7 +62,10 @@ export function exportContent( doc, div, model, - createModelToDomContext(undefined /*editorContext*/, options) + createModelToDomContext( + undefined /*editorContext*/, + optionsOrCallbacks as ModelToDomOption + ) ); editor.triggerEvent('extractContentWithDom', { clonedRoot: div }, true /*broadcast*/); diff --git a/packages/roosterjs-content-model-core/lib/command/paste/mergePasteContent.ts b/packages/roosterjs-content-model-core/lib/command/paste/mergePasteContent.ts index 9f191f235c9..9cd3302ad4b 100644 --- a/packages/roosterjs-content-model-core/lib/command/paste/mergePasteContent.ts +++ b/packages/roosterjs-content-model-core/lib/command/paste/mergePasteContent.ts @@ -94,6 +94,7 @@ export function mergePasteContent( { changeSource: ChangeSource.Paste, getChangeData: () => clipboardData, + scrollCaretIntoView: true, apiName: 'paste', } ); diff --git a/packages/roosterjs-content-model-core/lib/command/paste/paste.ts b/packages/roosterjs-content-model-core/lib/command/paste/paste.ts index 190118eb48a..c53748b3659 100644 --- a/packages/roosterjs-content-model-core/lib/command/paste/paste.ts +++ b/packages/roosterjs-content-model-core/lib/command/paste/paste.ts @@ -26,9 +26,11 @@ export function paste( const trustedHTMLHandler = editor.getTrustedHTMLHandler(); if (!clipboardData.modelBeforePaste) { - clipboardData.modelBeforePaste = cloneModelForPaste( - editor.getContentModelCopy('connected') - ); + editor.formatContentModel(model => { + clipboardData.modelBeforePaste = cloneModelForPaste(model); + + return false; + }); } // 1. Prepare variables diff --git a/packages/roosterjs-content-model-core/lib/coreApi/createContentModel/createContentModel.ts b/packages/roosterjs-content-model-core/lib/coreApi/createContentModel/createContentModel.ts index 102c66e3b91..31146042181 100644 --- a/packages/roosterjs-content-model-core/lib/coreApi/createContentModel/createContentModel.ts +++ b/packages/roosterjs-content-model-core/lib/coreApi/createContentModel/createContentModel.ts @@ -18,39 +18,38 @@ export const createContentModel: CreateContentModel = (core, option, selectionOv // Flush all mutations if any, so that we can get an up-to-date Content Model core.cache.textMutationObserver?.flushMutations(); - let cachedModel = - selectionOverride || (option && !option.tryGetFromCache) ? null : core.cache.cachedModel; - - if (cachedModel && core.lifecycle.shadowEditFragment) { - // When in shadow edit, use a cloned model so we won't pollute the cached one - cachedModel = cloneModel(cachedModel, { includeCachedElement: true }); - } - - if (cachedModel) { - return cachedModel; - } else { - const selection = - selectionOverride == 'none' - ? undefined - : selectionOverride || core.api.getDOMSelection(core) || undefined; - const saveIndex = !option && !selectionOverride; - const editorContext = core.api.createEditorContext(core, saveIndex); - const settings = core.environment.domToModelSettings; - const domToModelContext = option - ? createDomToModelContext(editorContext, settings.builtIn, settings.customized, option) - : createDomToModelContextWithConfig(settings.calculated, editorContext); - - if (selection) { - domToModelContext.selection = selection; + if (!selectionOverride && (!option || option.tryGetFromCache)) { + const cachedModel = core.cache.cachedModel; + + if (cachedModel) { + // When in shadow edit, use a cloned model so we won't pollute the cached one + return core.lifecycle.shadowEditFragment + ? cloneModel(cachedModel, { includeCachedElement: true }) + : cachedModel; } + } - const model = domToContentModel(core.logicalRoot, domToModelContext); + const selection = + selectionOverride == 'none' + ? undefined + : selectionOverride || core.api.getDOMSelection(core) || undefined; + const saveIndex = !option && !selectionOverride; + const editorContext = core.api.createEditorContext(core, saveIndex); + const settings = core.environment.domToModelSettings; + const domToModelContext = option + ? createDomToModelContext(editorContext, settings.builtIn, settings.customized, option) + : createDomToModelContextWithConfig(settings.calculated, editorContext); + + if (selection) { + domToModelContext.selection = selection; + } - if (saveIndex) { - core.cache.cachedModel = model; - updateCachedSelection(core.cache, selection); - } + const model = domToContentModel(core.logicalRoot, domToModelContext); - return model; + if (saveIndex) { + core.cache.cachedModel = model; + updateCachedSelection(core.cache, selection); } + + return model; }; diff --git a/packages/roosterjs-content-model-core/lib/coreApi/formatContentModel/formatContentModel.ts b/packages/roosterjs-content-model-core/lib/coreApi/formatContentModel/formatContentModel.ts index 6515ff40b50..bcebd23f23b 100644 --- a/packages/roosterjs-content-model-core/lib/coreApi/formatContentModel/formatContentModel.ts +++ b/packages/roosterjs-content-model-core/lib/coreApi/formatContentModel/formatContentModel.ts @@ -1,4 +1,4 @@ -import { ChangeSource } from 'roosterjs-content-model-dom'; +import { ChangeSource, getSelectionRootNode } from 'roosterjs-content-model-dom'; import type { ChangedEntity, ContentChangedEvent, @@ -24,8 +24,15 @@ export const formatContentModel: FormatContentModel = ( options, domToModelOptions ) => { - const { apiName, onNodeCreated, getChangeData, changeSource, rawEvent, selectionOverride } = - options || {}; + const { + apiName, + onNodeCreated, + getChangeData, + changeSource, + rawEvent, + selectionOverride, + scrollCaretIntoView, + } = options || {}; const model = core.api.createContentModel(core, domToModelOptions, selectionOverride); const context: FormatContentModelContext = { newEntities: [], @@ -63,6 +70,14 @@ export const formatContentModel: FormatContentModel = ( handlePendingFormat(core, context, selection); + if (selection && scrollCaretIntoView) { + const selectionRoot = getSelectionRootNode(selection); + const rootElement = + selectionRoot && core.domHelper.findClosestElementAncestor(selectionRoot); + + rootElement?.scrollIntoView(); + } + const eventData: ContentChangedEvent = { eventType: 'contentChanged', contentModel: clearModelCache ? undefined : model, diff --git a/packages/roosterjs-content-model-core/lib/coreApi/setDOMSelection/setDOMSelection.ts b/packages/roosterjs-content-model-core/lib/coreApi/setDOMSelection/setDOMSelection.ts index 2d199b1889d..a89fee21134 100644 --- a/packages/roosterjs-content-model-core/lib/coreApi/setDOMSelection/setDOMSelection.ts +++ b/packages/roosterjs-content-model-core/lib/coreApi/setDOMSelection/setDOMSelection.ts @@ -2,7 +2,13 @@ import { addRangeToSelection } from './addRangeToSelection'; import { ensureUniqueId } from '../setEditorStyle/ensureUniqueId'; import { findLastedCoInMergedCell } from './findLastedCoInMergedCell'; import { findTableCellElement } from './findTableCellElement'; -import { isNodeOfType, parseTableCells, toArray } from 'roosterjs-content-model-dom'; +import { + isElementOfType, + isNodeOfType, + parseTableCells, + toArray, + wrap, +} from 'roosterjs-content-model-dom'; import type { ParsedTable, SelectionChangedEvent, @@ -18,7 +24,7 @@ const TABLE_ID = 'table'; const DEFAULT_SELECTION_BORDER_COLOR = '#DB626C'; const TABLE_CSS_RULE = 'background-color:#C6C6C6!important;'; const CARET_CSS_RULE = 'caret-color: transparent'; -const TRANSPARENT_SELECTION_CSS_RULE = 'background-color: transparent !important'; +const TRANSPARENT_SELECTION_CSS_RULE = 'background-color: transparent !important;'; const SELECTION_SELECTOR = '*::selection'; /** @@ -39,16 +45,19 @@ export const setDOMSelection: SetDOMSelection = (core, selection, skipSelectionC try { switch (selection?.type) { case 'image': - const image = selection.image; + const image = ensureImageHasSpanParent(selection.image); - core.selection.selection = selection; + core.selection.selection = { + type: 'image', + image, + }; core.api.setEditorStyle( core, DOM_SELECTION_CSS_KEY, `outline-style:auto!important; outline-color:${ core.selection.imageSelectionBorderColor || DEFAULT_SELECTION_BORDER_COLOR }!important;`, - [`#${ensureUniqueId(image, IMAGE_ID)}`] + [`span:has(>img#${ensureUniqueId(image, IMAGE_ID)})`] ); core.api.setEditorStyle( core, @@ -231,3 +240,20 @@ function setRangeSelection(doc: Document, element: HTMLElement | undefined, coll addRangeToSelection(doc, range, isReverted); } } + +function ensureImageHasSpanParent(image: HTMLImageElement): HTMLImageElement { + const parent = image.parentElement; + + if ( + parent && + isNodeOfType(parent, 'ELEMENT_NODE') && + isElementOfType(parent, 'span') && + parent.firstChild == image && + parent.lastChild == image + ) { + return image; + } + + wrap(image.ownerDocument, image, 'span'); + return image; +} diff --git a/packages/roosterjs-content-model-core/lib/corePlugin/selection/SelectionPlugin.ts b/packages/roosterjs-content-model-core/lib/corePlugin/selection/SelectionPlugin.ts index 7c97bf89568..9dfef270c5c 100644 --- a/packages/roosterjs-content-model-core/lib/corePlugin/selection/SelectionPlugin.ts +++ b/packages/roosterjs-content-model-core/lib/corePlugin/selection/SelectionPlugin.ts @@ -1,685 +1,685 @@ -import { findCoordinate } from './findCoordinate'; -import { findTableCellElement } from '../../coreApi/setDOMSelection/findTableCellElement'; -import { isSingleImageInSelection } from './isSingleImageInSelection'; -import { normalizePos } from './normalizePos'; -import { - isCharacterValue, - isElementOfType, - isModifierKey, - isNodeOfType, - parseTableCells, - toArray, -} from 'roosterjs-content-model-dom'; -import type { - DOMSelection, - IEditor, - PluginEvent, - PluginWithState, - SelectionPluginState, - EditorOptions, - DOMHelper, - MouseUpEvent, - ParsedTable, - TableSelectionInfo, - TableCellCoordinate, - RangeSelection, -} from 'roosterjs-content-model-types'; - -const MouseLeftButton = 0; -const MouseMiddleButton = 1; -const MouseRightButton = 2; -const Up = 'ArrowUp'; -const Down = 'ArrowDown'; -const Left = 'ArrowLeft'; -const Right = 'ArrowRight'; -const Tab = 'Tab'; - -class SelectionPlugin implements PluginWithState { - private editor: IEditor | null = null; - private state: SelectionPluginState; - private disposer: (() => void) | null = null; - private isSafari = false; - private isMac = false; - private scrollTopCache: number = 0; - - constructor(options: EditorOptions) { - this.state = { - selection: null, - tableSelection: null, - imageSelectionBorderColor: options.imageSelectionBorderColor, - }; - } - - getName() { - return 'Selection'; - } - - initialize(editor: IEditor) { - this.editor = editor; - - const env = this.editor.getEnvironment(); - const document = this.editor.getDocument(); - - this.isSafari = !!env.isSafari; - this.isMac = !!env.isMac; - document.addEventListener('selectionchange', this.onSelectionChange); - if (this.isSafari) { - this.disposer = this.editor.attachDomEvent({ - focus: { beforeDispatch: this.onFocus }, - drop: { beforeDispatch: this.onDrop }, - }); - } else { - this.disposer = this.editor.attachDomEvent({ - focus: { beforeDispatch: this.onFocus }, - blur: { beforeDispatch: this.onBlur }, - drop: { beforeDispatch: this.onDrop }, - }); - } - } - - dispose() { - this.editor?.getDocument().removeEventListener('selectionchange', this.onSelectionChange); - - if (this.disposer) { - this.disposer(); - this.disposer = null; - } - - this.detachMouseEvent(); - this.editor = null; - } - - getState(): SelectionPluginState { - return this.state; - } - - onPluginEvent(event: PluginEvent) { - if (!this.editor) { - return; - } - - switch (event.eventType) { - case 'mouseDown': - this.onMouseDown(this.editor, event.rawEvent); - break; - - case 'mouseUp': - this.onMouseUp(event); - break; - - case 'keyDown': - this.onKeyDown(this.editor, event.rawEvent); - break; - - case 'contentChanged': - this.state.tableSelection = null; - break; - - case 'scroll': - if (!this.editor.hasFocus()) { - this.scrollTopCache = event.scrollContainer.scrollTop; - } - break; - } - } - - private onMouseDown(editor: IEditor, rawEvent: MouseEvent) { - const selection = editor.getDOMSelection(); - let image: HTMLImageElement | null; - - // Image selection - if ( - rawEvent.button === MouseRightButton && - (image = - this.getClickingImage(rawEvent) ?? - this.getContainedTargetImage(rawEvent, selection)) && - image.isContentEditable - ) { - this.selectImageWithRange(image, rawEvent); - return; - } else if (selection?.type == 'image' && selection.image !== rawEvent.target) { - this.selectBeforeOrAfterElement(editor, selection.image); - return; - } - - // Table selection - if (selection?.type == 'table' && rawEvent.button == MouseLeftButton) { - this.setDOMSelection(null /*domSelection*/, null /*tableSelection*/); - } - - let tableSelection: TableSelectionInfo | null; - const target = rawEvent.target as Node; - - if ( - target && - rawEvent.button == MouseLeftButton && - (tableSelection = this.parseTableSelection(target, target, editor.getDOMHelper())) - ) { - this.state.tableSelection = tableSelection; - - if (rawEvent.detail >= 3) { - const lastCo = findCoordinate( - tableSelection.parsedTable, - rawEvent.target as Node, - editor.getDOMHelper() - ); - - if (lastCo) { - // Triple click, select the current cell - tableSelection.lastCo = lastCo; - this.updateTableSelection(lastCo); - rawEvent.preventDefault(); - } - } - - this.state.mouseDisposer = editor.attachDomEvent({ - mousemove: { - beforeDispatch: this.onMouseMove, - }, - }); - } - } - - private onMouseMove = (event: Event) => { - if (this.editor && this.state.tableSelection) { - const hasTableSelection = !!this.state.tableSelection.lastCo; - const currentNode = event.target as Node; - const domHelper = this.editor.getDOMHelper(); - - const range = this.editor.getDocument().createRange(); - const startNode = this.state.tableSelection.startNode; - const isReverted = - currentNode.compareDocumentPosition(startNode) == Node.DOCUMENT_POSITION_FOLLOWING; - - if (isReverted) { - range.setStart(currentNode, 0); - range.setEnd( - startNode, - isNodeOfType(startNode, 'TEXT_NODE') - ? startNode.nodeValue?.length ?? 0 - : startNode.childNodes.length - ); - } else { - range.setStart(startNode, 0); - range.setEnd(currentNode, 0); - } - - // Use common container of the range to search a common table that covers both start and end node - const tableStart = range.commonAncestorContainer; - const newTableSelection = this.parseTableSelection(tableStart, startNode, domHelper); - - if (newTableSelection) { - const lastCo = findCoordinate( - newTableSelection.parsedTable, - currentNode, - domHelper - ); - - if (newTableSelection.table != this.state.tableSelection.table) { - // Move mouse into another table (nest table scenario) - this.state.tableSelection = newTableSelection; - this.state.tableSelection.lastCo = lastCo ?? undefined; - } - - const updated = lastCo && this.updateTableSelection(lastCo); - - if (hasTableSelection || updated) { - event.preventDefault(); - } - } else if (this.editor.getDOMSelection()?.type == 'table') { - // Move mouse out of table - this.setDOMSelection( - { - type: 'range', - range, - isReverted, - }, - this.state.tableSelection - ); - } - } - }; - - private selectImageWithRange(image: HTMLImageElement, event: Event) { - const range = image.ownerDocument.createRange(); - range.selectNode(image); - - const domSelection = this.editor?.getDOMSelection(); - if (domSelection?.type == 'image' && image == domSelection.image) { - event.preventDefault(); - } else { - this.setDOMSelection( - { - type: 'range', - isReverted: false, - range, - }, - null - ); - } - } - - private onMouseUp(event: MouseUpEvent) { - let image: HTMLImageElement | null; - - if ( - (image = this.getClickingImage(event.rawEvent)) && - image.isContentEditable && - event.rawEvent.button != MouseMiddleButton && - (event.rawEvent.button == - MouseRightButton /* it's not possible to drag using right click */ || - event.isClicking) - ) { - this.selectImageWithRange(image, event.rawEvent); - } - - this.detachMouseEvent(); - } - - private onDrop = () => { - this.detachMouseEvent(); - }; - - private onKeyDown(editor: IEditor, rawEvent: KeyboardEvent) { - const key = rawEvent.key; - const selection = editor.getDOMSelection(); - const win = editor.getDocument().defaultView; - - switch (selection?.type) { - case 'image': - if (!isModifierKey(rawEvent) && !rawEvent.shiftKey && selection.image.parentNode) { - if (key === 'Escape') { - this.selectBeforeOrAfterElement(editor, selection.image); - rawEvent.stopPropagation(); - } else if (key !== 'Delete' && key !== 'Backspace') { - this.selectBeforeOrAfterElement(editor, selection.image); - } - } - break; - - case 'range': - if (key == Up || key == Down || key == Left || key == Right || key == Tab) { - const start = selection.range.startContainer; - this.state.tableSelection = this.parseTableSelection( - start, - start, - editor.getDOMHelper() - ); - - const rangeKey = key == Tab ? this.getTabKey(rawEvent) : key; - - if (this.state.tableSelection) { - win?.requestAnimationFrame(() => this.handleSelectionInTable(rangeKey)); - } - } - break; - - case 'table': - if (this.state.tableSelection?.lastCo) { - const { shiftKey, key } = rawEvent; - - if (shiftKey && (key == Left || key == Right)) { - const isRtl = - win?.getComputedStyle(this.state.tableSelection.table).direction == - 'rtl'; - - this.updateTableSelectionFromKeyboard( - 0, - (key == Left ? -1 : 1) * (isRtl ? -1 : 1) - ); - rawEvent.preventDefault(); - } else if (shiftKey && (key == Up || key == Down)) { - this.updateTableSelectionFromKeyboard(key == Up ? -1 : 1, 0); - rawEvent.preventDefault(); - } else if (key != 'Shift' && !isCharacterValue(rawEvent)) { - if (key == Up || key == Down || key == Left || key == Right) { - this.setDOMSelection(null /*domSelection*/, this.state.tableSelection); - win?.requestAnimationFrame(() => this.handleSelectionInTable(key)); - } - } - } - break; - } - } - - private getTabKey(rawEvent: KeyboardEvent) { - return rawEvent.shiftKey ? 'TabLeft' : 'TabRight'; - } - - private handleSelectionInTable( - key: 'ArrowUp' | 'ArrowDown' | 'ArrowLeft' | 'ArrowRight' | 'TabLeft' | 'TabRight' - ) { - if (!this.editor || !this.state.tableSelection) { - return; - } - - const selection = this.editor.getDOMSelection(); - const domHelper = this.editor.getDOMHelper(); - - if (selection?.type == 'range') { - const { - range: { collapsed, startContainer, endContainer, commonAncestorContainer }, - isReverted, - } = selection; - const start = isReverted ? endContainer : startContainer; - const end: Node | null = isReverted ? startContainer : endContainer; - const tableSel = this.parseTableSelection(commonAncestorContainer, start, domHelper); - - if (!tableSel) { - return; - } - - let lastCo = findCoordinate(tableSel?.parsedTable, end, domHelper); - const { parsedTable, firstCo: oldCo, table } = this.state.tableSelection; - - if (lastCo && tableSel.table == table) { - if (lastCo.col != oldCo.col && (key == Up || key == Down)) { - const change = key == Up ? -1 : 1; - const originalTd = findTableCellElement(parsedTable, oldCo)?.cell; - let td: HTMLTableCellElement | null = null; - - lastCo = { row: oldCo.row + change, col: oldCo.col }; - - while (lastCo.row >= 0 && lastCo.row < parsedTable.length) { - td = findTableCellElement(parsedTable, lastCo)?.cell || null; - - if (td == originalTd) { - lastCo.row += change; - } else { - break; - } - } - - if (collapsed && td) { - this.setRangeSelectionInTable( - td, - key == Up ? td.childNodes.length : 0, - this.editor - ); - } - } else if (key == 'TabLeft' || key == 'TabRight') { - const reverse = key == 'TabLeft'; - for ( - let step = reverse ? -1 : 1, - row = lastCo.row ?? 0, - col = (lastCo.col ?? 0) + step; - ; - col += step - ) { - if (col < 0 || col >= parsedTable[row].length) { - row += step; - if (row < 0) { - this.selectBeforeOrAfterElement(this.editor, tableSel.table); - break; - } else if (row >= parsedTable.length) { - this.selectBeforeOrAfterElement( - this.editor, - tableSel.table, - true /*after*/ - ); - break; - } - col = reverse ? parsedTable[row].length - 1 : 0; - } - const cell = parsedTable[row][col]; - - if (typeof cell != 'string') { - this.setRangeSelectionInTable(cell, 0, this.editor); - lastCo.row = row; - lastCo.col = col; - break; - } - } - } else { - this.state.tableSelection = null; - } - - if ( - collapsed && - (lastCo.col != oldCo.col || lastCo.row != oldCo.row) && - lastCo.row >= 0 && - lastCo.row == parsedTable.length - 1 && - lastCo.col == parsedTable[lastCo.row]?.length - 1 - ) { - this.editor?.announce({ defaultStrings: 'announceOnFocusLastCell' }); - } - } - - if (!collapsed && lastCo) { - this.state.tableSelection = tableSel; - this.updateTableSelection(lastCo); - } - } - } - - private setRangeSelectionInTable(cell: Node, nodeOffset: number, editor: IEditor) { - // Get deepest editable position in the cell - const { node, offset } = normalizePos(cell, nodeOffset); - - const range = editor.getDocument().createRange(); - range.setStart(node, offset); - range.collapse(true /*toStart*/); - - this.setDOMSelection( - { - type: 'range', - range, - isReverted: false, - }, - null /*tableSelection*/ - ); - } - - private updateTableSelectionFromKeyboard(rowChange: number, colChange: number) { - if (this.state.tableSelection?.lastCo && this.editor) { - const { lastCo, parsedTable } = this.state.tableSelection; - const row = lastCo.row + rowChange; - const col = lastCo.col + colChange; - - if (row >= 0 && row < parsedTable.length && col >= 0 && col < parsedTable[row].length) { - this.updateTableSelection({ row, col }); - } - } - } - - private selectBeforeOrAfterElement(editor: IEditor, element: HTMLElement, after?: boolean) { - const doc = editor.getDocument(); - const parent = element.parentNode; - const index = parent && toArray(parent.childNodes).indexOf(element); - - if (parent && index !== null && index >= 0) { - const range = doc.createRange(); - range.setStart(parent, index + (after ? 1 : 0)); - range.collapse(); - - this.setDOMSelection( - { - type: 'range', - range: range, - isReverted: false, - }, - null /*tableSelection*/ - ); - } - } - - private getClickingImage(event: UIEvent): HTMLImageElement | null { - const target = event.target as Node; - - return isNodeOfType(target, 'ELEMENT_NODE') && isElementOfType(target, 'img') - ? target - : null; - } - - // MacOS will not create a mouseUp event if contextMenu event is not prevent defaulted. - // Make sure we capture image target even if image is wrapped - private getContainedTargetImage = ( - event: MouseEvent, - previousSelection: DOMSelection | null - ): HTMLImageElement | null => { - if (!this.isMac || !previousSelection || previousSelection.type !== 'image') { - return null; - } - - const target = event.target as Node; - if ( - isNodeOfType(target, 'ELEMENT_NODE') && - isElementOfType(target, 'span') && - target.firstChild === previousSelection.image - ) { - return previousSelection.image; - } - return null; - }; - - private onFocus = () => { - if (!this.state.skipReselectOnFocus && this.state.selection) { - this.setDOMSelection(this.state.selection, this.state.tableSelection); - } - - if (this.state.selection?.type == 'range' && !this.isSafari) { - // Editor is focused, now we can get live selection. So no need to keep a selection if the selection type is range. - this.state.selection = null; - } - - if (this.scrollTopCache && this.editor) { - const sc = this.editor.getScrollContainer(); - sc.scrollTop = this.scrollTopCache; - this.scrollTopCache = 0; - } - }; - - private onBlur = () => { - if (this.editor) { - if (!this.state.selection) { - this.state.selection = this.editor.getDOMSelection(); - } - const sc = this.editor.getScrollContainer(); - this.scrollTopCache = sc.scrollTop; - } - }; - - private onSelectionChange = () => { - if (this.editor?.hasFocus() && !this.editor.isInShadowEdit()) { - const newSelection = this.editor.getDOMSelection(); - - //If am image selection changed to a wider range due a keyboard event, we should update the selection - const selection = this.editor.getDocument().getSelection(); - - if (newSelection?.type == 'image' && selection) { - if (selection && !isSingleImageInSelection(selection)) { - const range = selection.getRangeAt(0); - this.editor.setDOMSelection({ - type: 'range', - range, - isReverted: - selection.focusNode != range.endContainer || - selection.focusOffset != range.endOffset, - }); - } - } - - // Safari has problem to handle onBlur event. When blur, we cannot get the original selection from editor. - // So we always save a selection whenever editor has focus. Then after blur, we can still use this cached selection. - if (newSelection?.type == 'range') { - if (this.isSafari) { - this.state.selection = newSelection; - } - this.trySelectSingleImage(newSelection); - } - } - }; - - private parseTableSelection( - tableStart: Node, - tdStart: Node, - domHelper: DOMHelper - ): TableSelectionInfo | null { - let table: HTMLTableElement | null; - let parsedTable: ParsedTable | null; - let firstCo: TableCellCoordinate | null; - - if ( - (table = domHelper.findClosestElementAncestor(tableStart, 'table')) && - (parsedTable = parseTableCells(table)) && - (firstCo = findCoordinate(parsedTable, tdStart, domHelper)) - ) { - return { table, parsedTable, firstCo, startNode: tdStart }; - } else { - return null; - } - } - - private updateTableSelection(lastCo: TableCellCoordinate) { - if (this.state.tableSelection && this.editor) { - const { - table, - firstCo, - parsedTable, - startNode, - lastCo: oldCo, - } = this.state.tableSelection; - - if (oldCo || firstCo.row != lastCo.row || firstCo.col != lastCo.col) { - this.state.tableSelection.lastCo = lastCo; - - this.setDOMSelection( - { - type: 'table', - table, - firstRow: firstCo.row, - firstColumn: firstCo.col, - lastRow: lastCo.row, - lastColumn: lastCo.col, - }, - { table, firstCo, lastCo, parsedTable, startNode } - ); - - return true; - } - } - - return false; - } - - private setDOMSelection( - selection: DOMSelection | null, - tableSelection: TableSelectionInfo | null - ) { - this.editor?.setDOMSelection(selection); - this.state.tableSelection = tableSelection; - } - - private detachMouseEvent() { - if (this.state.mouseDisposer) { - this.state.mouseDisposer(); - this.state.mouseDisposer = undefined; - } - } - - private trySelectSingleImage(selection: RangeSelection) { - if (!selection.range.collapsed) { - const image = isSingleImageInSelection(selection.range); - if (image) { - this.setDOMSelection( - { - type: 'image', - image: image, - }, - null /*tableSelection*/ - ); - } - } - } -} - -/** - * @internal - * Create a new instance of SelectionPlugin. - * @param option The editor option - */ -export function createSelectionPlugin( - options: EditorOptions -): PluginWithState { - return new SelectionPlugin(options); -} +import { findCoordinate } from './findCoordinate'; +import { findTableCellElement } from '../../coreApi/setDOMSelection/findTableCellElement'; +import { isSingleImageInSelection } from './isSingleImageInSelection'; +import { normalizePos } from './normalizePos'; +import { + isCharacterValue, + isElementOfType, + isModifierKey, + isNodeOfType, + parseTableCells, + toArray, +} from 'roosterjs-content-model-dom'; +import type { + DOMSelection, + IEditor, + PluginEvent, + PluginWithState, + SelectionPluginState, + EditorOptions, + DOMHelper, + MouseUpEvent, + ParsedTable, + TableSelectionInfo, + TableCellCoordinate, + RangeSelection, +} from 'roosterjs-content-model-types'; + +const MouseLeftButton = 0; +const MouseMiddleButton = 1; +const MouseRightButton = 2; +const Up = 'ArrowUp'; +const Down = 'ArrowDown'; +const Left = 'ArrowLeft'; +const Right = 'ArrowRight'; +const Tab = 'Tab'; + +class SelectionPlugin implements PluginWithState { + private editor: IEditor | null = null; + private state: SelectionPluginState; + private disposer: (() => void) | null = null; + private isSafari = false; + private isMac = false; + private scrollTopCache: number = 0; + + constructor(options: EditorOptions) { + this.state = { + selection: null, + tableSelection: null, + imageSelectionBorderColor: options.imageSelectionBorderColor, + }; + } + + getName() { + return 'Selection'; + } + + initialize(editor: IEditor) { + this.editor = editor; + + const env = this.editor.getEnvironment(); + const document = this.editor.getDocument(); + + this.isSafari = !!env.isSafari; + this.isMac = !!env.isMac; + document.addEventListener('selectionchange', this.onSelectionChange); + if (this.isSafari) { + this.disposer = this.editor.attachDomEvent({ + focus: { beforeDispatch: this.onFocus }, + drop: { beforeDispatch: this.onDrop }, + }); + } else { + this.disposer = this.editor.attachDomEvent({ + focus: { beforeDispatch: this.onFocus }, + blur: { beforeDispatch: this.onBlur }, + drop: { beforeDispatch: this.onDrop }, + }); + } + } + + dispose() { + this.editor?.getDocument().removeEventListener('selectionchange', this.onSelectionChange); + + if (this.disposer) { + this.disposer(); + this.disposer = null; + } + + this.detachMouseEvent(); + this.editor = null; + } + + getState(): SelectionPluginState { + return this.state; + } + + onPluginEvent(event: PluginEvent) { + if (!this.editor) { + return; + } + + switch (event.eventType) { + case 'mouseDown': + this.onMouseDown(this.editor, event.rawEvent); + break; + + case 'mouseUp': + this.onMouseUp(event); + break; + + case 'keyDown': + this.onKeyDown(this.editor, event.rawEvent); + break; + + case 'contentChanged': + this.state.tableSelection = null; + break; + + case 'scroll': + if (!this.editor.hasFocus()) { + this.scrollTopCache = event.scrollContainer.scrollTop; + } + break; + } + } + + private onMouseDown(editor: IEditor, rawEvent: MouseEvent) { + const selection = editor.getDOMSelection(); + let image: HTMLImageElement | null; + + // Image selection + if ( + rawEvent.button === MouseRightButton && + (image = + this.getClickingImage(rawEvent) ?? + this.getContainedTargetImage(rawEvent, selection)) && + image.isContentEditable + ) { + this.selectImageWithRange(image, rawEvent); + return; + } else if (selection?.type == 'image' && selection.image !== rawEvent.target) { + this.selectBeforeOrAfterElement(editor, selection.image); + return; + } + + // Table selection + if (selection?.type == 'table' && rawEvent.button == MouseLeftButton) { + this.setDOMSelection(null /*domSelection*/, null /*tableSelection*/); + } + + let tableSelection: TableSelectionInfo | null; + const target = rawEvent.target as Node; + + if ( + target && + rawEvent.button == MouseLeftButton && + (tableSelection = this.parseTableSelection(target, target, editor.getDOMHelper())) + ) { + this.state.tableSelection = tableSelection; + + if (rawEvent.detail >= 3) { + const lastCo = findCoordinate( + tableSelection.parsedTable, + rawEvent.target as Node, + editor.getDOMHelper() + ); + + if (lastCo) { + // Triple click, select the current cell + tableSelection.lastCo = lastCo; + this.updateTableSelection(lastCo); + rawEvent.preventDefault(); + } + } + + this.state.mouseDisposer = editor.attachDomEvent({ + mousemove: { + beforeDispatch: this.onMouseMove, + }, + }); + } + } + + private onMouseMove = (event: Event) => { + if (this.editor && this.state.tableSelection) { + const hasTableSelection = !!this.state.tableSelection.lastCo; + const currentNode = event.target as Node; + const domHelper = this.editor.getDOMHelper(); + + const range = this.editor.getDocument().createRange(); + const startNode = this.state.tableSelection.startNode; + const isReverted = + currentNode.compareDocumentPosition(startNode) == Node.DOCUMENT_POSITION_FOLLOWING; + + if (isReverted) { + range.setStart(currentNode, 0); + range.setEnd( + startNode, + isNodeOfType(startNode, 'TEXT_NODE') + ? startNode.nodeValue?.length ?? 0 + : startNode.childNodes.length + ); + } else { + range.setStart(startNode, 0); + range.setEnd(currentNode, 0); + } + + // Use common container of the range to search a common table that covers both start and end node + const tableStart = range.commonAncestorContainer; + const newTableSelection = this.parseTableSelection(tableStart, startNode, domHelper); + + if (newTableSelection) { + const lastCo = findCoordinate( + newTableSelection.parsedTable, + currentNode, + domHelper + ); + + if (newTableSelection.table != this.state.tableSelection.table) { + // Move mouse into another table (nest table scenario) + this.state.tableSelection = newTableSelection; + this.state.tableSelection.lastCo = lastCo ?? undefined; + } + + const updated = lastCo && this.updateTableSelection(lastCo); + + if (hasTableSelection || updated) { + event.preventDefault(); + } + } else if (this.editor.getDOMSelection()?.type == 'table') { + // Move mouse out of table + this.setDOMSelection( + { + type: 'range', + range, + isReverted, + }, + this.state.tableSelection + ); + } + } + }; + + private selectImageWithRange(image: HTMLImageElement, event: Event) { + const range = image.ownerDocument.createRange(); + range.selectNode(image); + + const domSelection = this.editor?.getDOMSelection(); + if (domSelection?.type == 'image' && image == domSelection.image) { + event.preventDefault(); + } else { + this.setDOMSelection( + { + type: 'range', + isReverted: false, + range, + }, + null + ); + } + } + + private onMouseUp(event: MouseUpEvent) { + let image: HTMLImageElement | null; + + if ( + (image = this.getClickingImage(event.rawEvent)) && + image.isContentEditable && + event.rawEvent.button != MouseMiddleButton && + (event.rawEvent.button == + MouseRightButton /* it's not possible to drag using right click */ || + event.isClicking) + ) { + this.selectImageWithRange(image, event.rawEvent); + } + + this.detachMouseEvent(); + } + + private onDrop = () => { + this.detachMouseEvent(); + }; + + private onKeyDown(editor: IEditor, rawEvent: KeyboardEvent) { + const key = rawEvent.key; + const selection = editor.getDOMSelection(); + const win = editor.getDocument().defaultView; + + switch (selection?.type) { + case 'image': + if (!isModifierKey(rawEvent) && !rawEvent.shiftKey && selection.image.parentNode) { + if (key === 'Escape') { + this.selectBeforeOrAfterElement(editor, selection.image); + rawEvent.stopPropagation(); + } else if (key !== 'Delete' && key !== 'Backspace') { + this.selectBeforeOrAfterElement(editor, selection.image); + } + } + break; + + case 'range': + if (key == Up || key == Down || key == Left || key == Right || key == Tab) { + const start = selection.range.startContainer; + this.state.tableSelection = this.parseTableSelection( + start, + start, + editor.getDOMHelper() + ); + + const rangeKey = key == Tab ? this.getTabKey(rawEvent) : key; + + if (this.state.tableSelection) { + win?.requestAnimationFrame(() => this.handleSelectionInTable(rangeKey)); + } + } + break; + + case 'table': + if (this.state.tableSelection?.lastCo) { + const { shiftKey, key } = rawEvent; + + if (shiftKey && (key == Left || key == Right)) { + const isRtl = + win?.getComputedStyle(this.state.tableSelection.table).direction == + 'rtl'; + + this.updateTableSelectionFromKeyboard( + 0, + (key == Left ? -1 : 1) * (isRtl ? -1 : 1) + ); + rawEvent.preventDefault(); + } else if (shiftKey && (key == Up || key == Down)) { + this.updateTableSelectionFromKeyboard(key == Up ? -1 : 1, 0); + rawEvent.preventDefault(); + } else if (key != 'Shift' && !isCharacterValue(rawEvent)) { + if (key == Up || key == Down || key == Left || key == Right) { + this.setDOMSelection(null /*domSelection*/, this.state.tableSelection); + win?.requestAnimationFrame(() => this.handleSelectionInTable(key)); + } + } + } + break; + } + } + + private getTabKey(rawEvent: KeyboardEvent) { + return rawEvent.shiftKey ? 'TabLeft' : 'TabRight'; + } + + private handleSelectionInTable( + key: 'ArrowUp' | 'ArrowDown' | 'ArrowLeft' | 'ArrowRight' | 'TabLeft' | 'TabRight' + ) { + if (!this.editor || !this.state.tableSelection) { + return; + } + + const selection = this.editor.getDOMSelection(); + const domHelper = this.editor.getDOMHelper(); + + if (selection?.type == 'range') { + const { + range: { collapsed, startContainer, endContainer, commonAncestorContainer }, + isReverted, + } = selection; + const start = isReverted ? endContainer : startContainer; + const end: Node | null = isReverted ? startContainer : endContainer; + const tableSel = this.parseTableSelection(commonAncestorContainer, start, domHelper); + + if (!tableSel) { + return; + } + + let lastCo = findCoordinate(tableSel?.parsedTable, end, domHelper); + const { parsedTable, firstCo: oldCo, table } = this.state.tableSelection; + + if (lastCo && tableSel.table == table) { + if (lastCo.col != oldCo.col && (key == Up || key == Down)) { + const change = key == Up ? -1 : 1; + const originalTd = findTableCellElement(parsedTable, oldCo)?.cell; + let td: HTMLTableCellElement | null = null; + + lastCo = { row: oldCo.row + change, col: oldCo.col }; + + while (lastCo.row >= 0 && lastCo.row < parsedTable.length) { + td = findTableCellElement(parsedTable, lastCo)?.cell || null; + + if (td == originalTd) { + lastCo.row += change; + } else { + break; + } + } + + if (collapsed && td) { + this.setRangeSelectionInTable( + td, + key == Up ? td.childNodes.length : 0, + this.editor + ); + } + } else if (key == 'TabLeft' || key == 'TabRight') { + const reverse = key == 'TabLeft'; + for ( + let step = reverse ? -1 : 1, + row = lastCo.row ?? 0, + col = (lastCo.col ?? 0) + step; + ; + col += step + ) { + if (col < 0 || col >= parsedTable[row].length) { + row += step; + if (row < 0) { + this.selectBeforeOrAfterElement(this.editor, tableSel.table); + break; + } else if (row >= parsedTable.length) { + this.selectBeforeOrAfterElement( + this.editor, + tableSel.table, + true /*after*/ + ); + break; + } + col = reverse ? parsedTable[row].length - 1 : 0; + } + const cell = parsedTable[row][col]; + + if (typeof cell != 'string') { + this.setRangeSelectionInTable(cell, 0, this.editor); + lastCo.row = row; + lastCo.col = col; + break; + } + } + } else { + this.state.tableSelection = null; + } + + if ( + collapsed && + (lastCo.col != oldCo.col || lastCo.row != oldCo.row) && + lastCo.row >= 0 && + lastCo.row == parsedTable.length - 1 && + lastCo.col == parsedTable[lastCo.row]?.length - 1 + ) { + this.editor?.announce({ defaultStrings: 'announceOnFocusLastCell' }); + } + } + + if (!collapsed && lastCo) { + this.state.tableSelection = tableSel; + this.updateTableSelection(lastCo); + } + } + } + + private setRangeSelectionInTable(cell: Node, nodeOffset: number, editor: IEditor) { + // Get deepest editable position in the cell + const { node, offset } = normalizePos(cell, nodeOffset); + + const range = editor.getDocument().createRange(); + range.setStart(node, offset); + range.collapse(true /*toStart*/); + + this.setDOMSelection( + { + type: 'range', + range, + isReverted: false, + }, + null /*tableSelection*/ + ); + } + + private updateTableSelectionFromKeyboard(rowChange: number, colChange: number) { + if (this.state.tableSelection?.lastCo && this.editor) { + const { lastCo, parsedTable } = this.state.tableSelection; + const row = lastCo.row + rowChange; + const col = lastCo.col + colChange; + + if (row >= 0 && row < parsedTable.length && col >= 0 && col < parsedTable[row].length) { + this.updateTableSelection({ row, col }); + } + } + } + + private selectBeforeOrAfterElement(editor: IEditor, element: HTMLElement, after?: boolean) { + const doc = editor.getDocument(); + const parent = element.parentNode; + const index = parent && toArray(parent.childNodes).indexOf(element); + + if (parent && index !== null && index >= 0) { + const range = doc.createRange(); + range.setStart(parent, index + (after ? 1 : 0)); + range.collapse(); + + this.setDOMSelection( + { + type: 'range', + range: range, + isReverted: false, + }, + null /*tableSelection*/ + ); + } + } + + private getClickingImage(event: UIEvent): HTMLImageElement | null { + const target = event.target as Node; + + return isNodeOfType(target, 'ELEMENT_NODE') && isElementOfType(target, 'img') + ? target + : null; + } + + // MacOS will not create a mouseUp event if contextMenu event is not prevent defaulted. + // Make sure we capture image target even if image is wrapped + private getContainedTargetImage = ( + event: MouseEvent, + previousSelection: DOMSelection | null + ): HTMLImageElement | null => { + if (!this.isMac || !previousSelection || previousSelection.type !== 'image') { + return null; + } + + const target = event.target as Node; + if ( + isNodeOfType(target, 'ELEMENT_NODE') && + isElementOfType(target, 'span') && + target.firstChild === previousSelection.image + ) { + return previousSelection.image; + } + return null; + }; + + private onFocus = () => { + if (!this.state.skipReselectOnFocus && this.state.selection) { + this.setDOMSelection(this.state.selection, this.state.tableSelection); + } + + if (this.state.selection?.type == 'range' && !this.isSafari) { + // Editor is focused, now we can get live selection. So no need to keep a selection if the selection type is range. + this.state.selection = null; + } + + if (this.scrollTopCache && this.editor) { + const sc = this.editor.getScrollContainer(); + sc.scrollTop = this.scrollTopCache; + this.scrollTopCache = 0; + } + }; + + private onBlur = () => { + if (this.editor) { + if (!this.state.selection) { + this.state.selection = this.editor.getDOMSelection(); + } + const sc = this.editor.getScrollContainer(); + this.scrollTopCache = sc.scrollTop; + } + }; + + private onSelectionChange = () => { + if (this.editor?.hasFocus() && !this.editor.isInShadowEdit()) { + const newSelection = this.editor.getDOMSelection(); + + //If am image selection changed to a wider range due a keyboard event, we should update the selection + const selection = this.editor.getDocument().getSelection(); + + if (newSelection?.type == 'image' && selection) { + if (selection && !isSingleImageInSelection(selection)) { + const range = selection.getRangeAt(0); + this.editor.setDOMSelection({ + type: 'range', + range, + isReverted: + selection.focusNode != range.endContainer || + selection.focusOffset != range.endOffset, + }); + } + } + + // Safari has problem to handle onBlur event. When blur, we cannot get the original selection from editor. + // So we always save a selection whenever editor has focus. Then after blur, we can still use this cached selection. + if (newSelection?.type == 'range') { + if (this.isSafari) { + this.state.selection = newSelection; + } + this.trySelectSingleImage(newSelection); + } + } + }; + + private parseTableSelection( + tableStart: Node, + tdStart: Node, + domHelper: DOMHelper + ): TableSelectionInfo | null { + let table: HTMLTableElement | null; + let parsedTable: ParsedTable | null; + let firstCo: TableCellCoordinate | null; + + if ( + (table = domHelper.findClosestElementAncestor(tableStart, 'table')) && + (parsedTable = parseTableCells(table)) && + (firstCo = findCoordinate(parsedTable, tdStart, domHelper)) + ) { + return { table, parsedTable, firstCo, startNode: tdStart }; + } else { + return null; + } + } + + private updateTableSelection(lastCo: TableCellCoordinate) { + if (this.state.tableSelection && this.editor) { + const { + table, + firstCo, + parsedTable, + startNode, + lastCo: oldCo, + } = this.state.tableSelection; + + if (oldCo || firstCo.row != lastCo.row || firstCo.col != lastCo.col) { + this.state.tableSelection.lastCo = lastCo; + + this.setDOMSelection( + { + type: 'table', + table, + firstRow: firstCo.row, + firstColumn: firstCo.col, + lastRow: lastCo.row, + lastColumn: lastCo.col, + }, + { table, firstCo, lastCo, parsedTable, startNode } + ); + + return true; + } + } + + return false; + } + + private setDOMSelection( + selection: DOMSelection | null, + tableSelection: TableSelectionInfo | null + ) { + this.editor?.setDOMSelection(selection); + this.state.tableSelection = tableSelection; + } + + private detachMouseEvent() { + if (this.state.mouseDisposer) { + this.state.mouseDisposer(); + this.state.mouseDisposer = undefined; + } + } + + private trySelectSingleImage(selection: RangeSelection) { + if (!selection.range.collapsed) { + const image = isSingleImageInSelection(selection.range); + if (image) { + this.setDOMSelection( + { + type: 'image', + image: image, + }, + null /*tableSelection*/ + ); + } + } + } +} + +/** + * @internal + * Create a new instance of SelectionPlugin. + * @param option The editor option + */ +export function createSelectionPlugin( + options: EditorOptions +): PluginWithState { + return new SelectionPlugin(options); +} diff --git a/packages/roosterjs-content-model-core/lib/corePlugin/undo/UndoPlugin.ts b/packages/roosterjs-content-model-core/lib/corePlugin/undo/UndoPlugin.ts index df553aa6f57..93c7bd4d4ae 100644 --- a/packages/roosterjs-content-model-core/lib/corePlugin/undo/UndoPlugin.ts +++ b/packages/roosterjs-content-model-core/lib/corePlugin/undo/UndoPlugin.ts @@ -115,6 +115,12 @@ class UndoPlugin implements PluginWithState { case 'beforeKeyboardEditing': this.onBeforeKeyboardEditing(event.rawEvent); break; + + case 'mouseDown': + if (this.state.snapshotsManager.hasNewContent) { + this.addUndoSnapshot(); + } + break; } } diff --git a/packages/roosterjs-content-model-core/lib/editor/Editor.ts b/packages/roosterjs-content-model-core/lib/editor/Editor.ts index 220e76736dd..3301ff252ee 100644 --- a/packages/roosterjs-content-model-core/lib/editor/Editor.ts +++ b/packages/roosterjs-content-model-core/lib/editor/Editor.ts @@ -87,9 +87,6 @@ export class Editor implements IEditor { /** * Create Content Model from DOM tree in this editor * @param mode What kind of Content Model we want. Currently we support the following values: - * - connected: Returns a connect Content Model object. "Connected" means if there is any entity inside editor, the returned Content Model will - * contain the same wrapper element for entity. This option should only be used in some special cases. In most cases we should use "disconnected" - * to get a fully disconnected Content Model so that any change to the model will not impact editor content. * - disconnected: Returns a disconnected clone of Content Model from editor which you can do any change on it and it won't impact the editor content. * If there is any entity in editor, the returned object will contain cloned copy of entity wrapper element. * If editor is in dark mode, the cloned entity will be converted back to light mode. @@ -100,11 +97,7 @@ export class Editor implements IEditor { const core = this.getCore(); switch (mode) { - case 'connected': - return core.api.createContentModel(core, { - tryGetFromCache: true, // Pass an option here to force disable save index - }); - + case 'connected': // Get a connected model is deprecated. Now we will always return disconnected model case 'disconnected': return cloneModel( core.api.createContentModel(core, { diff --git a/packages/roosterjs-content-model-core/test/command/exportContent/exportContentTest.ts b/packages/roosterjs-content-model-core/test/command/exportContent/exportContentTest.ts index 8eec6d30c54..a39b4431cda 100644 --- a/packages/roosterjs-content-model-core/test/command/exportContent/exportContentTest.ts +++ b/packages/roosterjs-content-model-core/test/command/exportContent/exportContentTest.ts @@ -40,7 +40,29 @@ describe('exportContent', () => { expect(text).toBe(mockedText); expect(getContentModelCopySpy).toHaveBeenCalledWith('clean'); - expect(contentModelToTextSpy).toHaveBeenCalledWith(mockedModel); + expect(contentModelToTextSpy).toHaveBeenCalledWith(mockedModel, undefined, undefined); + }); + + it('PlainText with callback', () => { + const mockedModel = 'MODEL' as any; + const getContentModelCopySpy = jasmine + .createSpy('getContentModelCopy') + .and.returnValue(mockedModel); + const editor: IEditor = { + getContentModelCopy: getContentModelCopySpy, + } as any; + const mockedText = 'TEXT'; + const contentModelToTextSpy = spyOn( + contentModelToText, + 'contentModelToText' + ).and.returnValue(mockedText); + const mockedCallbacks = 'CALLBACKS' as any; + + const text = exportContent(editor, 'PlainText', mockedCallbacks); + + expect(text).toBe(mockedText); + expect(getContentModelCopySpy).toHaveBeenCalledWith('clean'); + expect(contentModelToTextSpy).toHaveBeenCalledWith(mockedModel, undefined, mockedCallbacks); }); it('HTML', () => { diff --git a/packages/roosterjs-content-model-core/test/coreApi/createContentModel/createContentModelTest.ts b/packages/roosterjs-content-model-core/test/coreApi/createContentModel/createContentModelTest.ts index 8978da7ad4b..0b0ec01a507 100644 --- a/packages/roosterjs-content-model-core/test/coreApi/createContentModel/createContentModelTest.ts +++ b/packages/roosterjs-content-model-core/test/coreApi/createContentModel/createContentModelTest.ts @@ -311,7 +311,7 @@ describe('createContentModel with selection', () => { /* | Scenarios | can use cache | can write cache | comment | |-----------------------------------|---------------|-----------------|----------------------------------------------------------------------------------------------------------------| -| getContentModelCopy: connected | true | false | Mostly used by demo site, we can use existing model but this should not impact cache | +| getContentModelCopy: connected | false | false | This is now deprecated | | getContentModelCopy: disconnected | false | false | Used by plugins and test code to read current model. We will return a cloned model, and do not impact cache | | getContentModelCopy: clean | false | false | Used by export HTML, do not use cache to make sure the model is up to date | | formatInsertPointWithContentModel | false | false | Used by insertEntity (recent change), do not use cache since we need to add shadow insert point | diff --git a/packages/roosterjs-content-model-core/test/coreApi/formatContentModel/formatContentModelTest.ts b/packages/roosterjs-content-model-core/test/coreApi/formatContentModel/formatContentModelTest.ts index dc1a0afae0c..09996b39e09 100644 --- a/packages/roosterjs-content-model-core/test/coreApi/formatContentModel/formatContentModelTest.ts +++ b/packages/roosterjs-content-model-core/test/coreApi/formatContentModel/formatContentModelTest.ts @@ -21,6 +21,7 @@ describe('formatContentModel', () => { let hasFocus: jasmine.Spy; let getClientWidth: jasmine.Spy; let announce: jasmine.Spy; + let findClosestElementAncestor: jasmine.Spy; const apiName = 'mockedApi'; const mockedContainer = 'C' as any; @@ -42,6 +43,7 @@ describe('formatContentModel', () => { hasFocus = jasmine.createSpy('hasFocus'); getClientWidth = jasmine.createSpy('getClientWidth'); announce = jasmine.createSpy('announce'); + findClosestElementAncestor = jasmine.createSpy('findClosestElementAncestor '); core = ({ api: { @@ -62,6 +64,7 @@ describe('formatContentModel', () => { domHelper: { hasFocus, getClientWidth, + findClosestElementAncestor, }, } as any) as EditorCore; }); @@ -549,6 +552,31 @@ describe('formatContentModel', () => { }); expect(announce).not.toHaveBeenCalled(); }); + + it('Has scrollCaretIntoView, and callback return true', () => { + const scrollIntoViewSpy = jasmine.createSpy('scrollIntoView'); + const mockedImage = { scrollIntoView: scrollIntoViewSpy } as any; + + findClosestElementAncestor.and.returnValue(mockedImage); + setContentModel.and.returnValue({ + type: 'image', + image: mockedImage, + }); + formatContentModel( + core, + (model, context) => { + context.clearModelCache = true; + return true; + }, + { + scrollCaretIntoView: true, + apiName, + } + ); + + expect(findClosestElementAncestor).toHaveBeenCalledWith(mockedImage); + expect(scrollIntoViewSpy).toHaveBeenCalledTimes(1); + }); }); describe('Editor does not have focus', () => { diff --git a/packages/roosterjs-content-model-core/test/coreApi/setDOMSelection/setDOMSelectionTest.ts b/packages/roosterjs-content-model-core/test/coreApi/setDOMSelection/setDOMSelectionTest.ts index a2dd2548a3c..f1db9866d87 100644 --- a/packages/roosterjs-content-model-core/test/coreApi/setDOMSelection/setDOMSelectionTest.ts +++ b/packages/roosterjs-content-model-core/test/coreApi/setDOMSelection/setDOMSelectionTest.ts @@ -14,6 +14,8 @@ describe('setDOMSelection', () => { let doc: Document; let contentDiv: HTMLDivElement; let mockedRange = 'RANGE' as any; + let createElementSpy: jasmine.Spy; + let appendChildSpy: jasmine.Spy; beforeEach(() => { querySelectorAllSpy = jasmine.createSpy('querySelectorAll'); @@ -27,11 +29,16 @@ describe('setDOMSelection', () => { createRangeSpy = jasmine.createSpy('createRange'); setEditorStyleSpy = jasmine.createSpy('setEditorStyle'); containsSpy = jasmine.createSpy('contains').and.returnValue(true); + appendChildSpy = jasmine.createSpy('appendChild'); + createElementSpy = jasmine.createSpy('createElement').and.returnValue({ + appendChild: appendChildSpy, + }); doc = { querySelectorAll: querySelectorAllSpy, createRange: createRangeSpy, contains: containsSpy, + createElement: createElementSpy, } as any; contentDiv = { ownerDocument: doc, @@ -221,9 +228,14 @@ describe('setDOMSelection', () => { describe('Image selection', () => { let mockedImage: HTMLImageElement; - beforeEach(() => { mockedImage = { + parentElement: { + ownerDocument: doc, + firstElementChild: mockedImage, + lastElementChild: mockedImage, + appendChild: appendChildSpy, + }, ownerDocument: doc, } as any; }); @@ -265,6 +277,7 @@ describe('setDOMSelection', () => { expect(mockedImage.id).toBe('image_0'); expect(setEditorStyleSpy).toHaveBeenCalledTimes(5); expect(setEditorStyleSpy).toHaveBeenCalledWith(core, '_DOMSelection', null); + expect(setEditorStyleSpy).toHaveBeenCalledWith(core, '_DOMSelectionHideCursor', null); expect(setEditorStyleSpy).toHaveBeenCalledWith( core, '_DOMSelectionHideSelection', @@ -274,11 +287,17 @@ describe('setDOMSelection', () => { core, '_DOMSelection', 'outline-style:auto!important; outline-color:#DB626C!important;', - ['#image_0'] + ['span:has(>img#image_0)'] + ); + expect(setEditorStyleSpy).toHaveBeenCalledWith( + core, + '_DOMSelectionHideSelection', + 'background-color: transparent !important;', + ['*::selection'] ); }); - it('image selection with duplicated id', () => { + it('image selection with customized selection border color', () => { const mockedSelection = { type: 'image', image: mockedImage, @@ -290,12 +309,11 @@ describe('setDOMSelection', () => { collapse: collapseSpy, }; - mockedImage.id = 'image_0'; + core.selection.imageSelectionBorderColor = 'red'; + createRangeSpy.and.returnValue(mockedRange); - querySelectorAllSpy.and.callFake(selector => { - return selector == '#image_0' ? ['', ''] : ['']; - }); + querySelectorAllSpy.and.returnValue([]); hasFocusSpy.and.returnValue(false); setDOMSelection(core, mockedSelection); @@ -303,6 +321,7 @@ describe('setDOMSelection', () => { expect(core.selection).toEqual({ skipReselectOnFocus: undefined, selection: mockedSelection, + imageSelectionBorderColor: 'red', } as any); expect(triggerEventSpy).toHaveBeenCalledWith( core, @@ -317,6 +336,7 @@ describe('setDOMSelection', () => { expect(addRangeToSelectionSpy).toHaveBeenCalledWith(doc, mockedRange, undefined); expect(setEditorStyleSpy).toHaveBeenCalledTimes(5); expect(setEditorStyleSpy).toHaveBeenCalledWith(core, '_DOMSelection', null); + expect(setEditorStyleSpy).toHaveBeenCalledWith(core, '_DOMSelectionHideCursor', null); expect(setEditorStyleSpy).toHaveBeenCalledWith( core, '_DOMSelectionHideSelection', @@ -325,18 +345,18 @@ describe('setDOMSelection', () => { expect(setEditorStyleSpy).toHaveBeenCalledWith( core, '_DOMSelection', - 'outline-style:auto!important; outline-color:#DB626C!important;', - ['#image_0_0'] + 'outline-style:auto!important; outline-color:red!important;', + ['span:has(>img#image_0)'] ); expect(setEditorStyleSpy).toHaveBeenCalledWith( core, '_DOMSelectionHideSelection', - 'background-color: transparent !important', + 'background-color: transparent !important;', ['*::selection'] ); }); - it('image selection with customized selection border color', () => { + it('do not select if node is out of document', () => { const mockedSelection = { type: 'image', image: mockedImage, @@ -348,7 +368,7 @@ describe('setDOMSelection', () => { collapse: collapseSpy, }; - core.selection.imageSelectionBorderColor = 'red'; + doc.contains = () => false; createRangeSpy.and.returnValue(mockedRange); @@ -360,7 +380,6 @@ describe('setDOMSelection', () => { expect(core.selection).toEqual({ skipReselectOnFocus: undefined, selection: mockedSelection, - imageSelectionBorderColor: 'red', } as any); expect(triggerEventSpy).toHaveBeenCalledWith( core, @@ -370,9 +389,10 @@ describe('setDOMSelection', () => { }, true ); - expect(selectNodeSpy).toHaveBeenCalledWith(mockedImage); - expect(collapseSpy).not.toHaveBeenCalledWith(); - expect(addRangeToSelectionSpy).toHaveBeenCalledWith(doc, mockedRange, undefined); + expect(selectNodeSpy).not.toHaveBeenCalled(); + expect(collapseSpy).not.toHaveBeenCalled(); + expect(addRangeToSelectionSpy).not.toHaveBeenCalled(); + expect(mockedImage.id).toBe('image_0'); expect(setEditorStyleSpy).toHaveBeenCalledTimes(5); expect(setEditorStyleSpy).toHaveBeenCalledWith(core, '_DOMSelection', null); expect(setEditorStyleSpy).toHaveBeenCalledWith(core, '_DOMSelectionHideCursor', null); @@ -384,18 +404,18 @@ describe('setDOMSelection', () => { expect(setEditorStyleSpy).toHaveBeenCalledWith( core, '_DOMSelection', - 'outline-style:auto!important; outline-color:red!important;', - ['#image_0'] + 'outline-style:auto!important; outline-color:#DB626C!important;', + ['span:has(>img#image_0)'] ); expect(setEditorStyleSpy).toHaveBeenCalledWith( core, '_DOMSelectionHideSelection', - 'background-color: transparent !important', + 'background-color: transparent !important;', ['*::selection'] ); }); - it('do not select if node is out of document', () => { + it('image selection with duplicated id', () => { const mockedSelection = { type: 'image', image: mockedImage, @@ -407,11 +427,12 @@ describe('setDOMSelection', () => { collapse: collapseSpy, }; - doc.contains = () => false; - + mockedImage.id = 'image_0'; createRangeSpy.and.returnValue(mockedRange); - querySelectorAllSpy.and.returnValue([]); + querySelectorAllSpy.and.callFake(selector => { + return selector == '#image_0' ? ['', ''] : ['']; + }); hasFocusSpy.and.returnValue(false); setDOMSelection(core, mockedSelection); @@ -428,23 +449,27 @@ describe('setDOMSelection', () => { }, true ); - expect(selectNodeSpy).not.toHaveBeenCalled(); - expect(collapseSpy).not.toHaveBeenCalled(); - expect(addRangeToSelectionSpy).not.toHaveBeenCalled(); - expect(mockedImage.id).toBe('image_0'); + expect(selectNodeSpy).toHaveBeenCalledWith(mockedImage); + expect(collapseSpy).not.toHaveBeenCalledWith(); + expect(addRangeToSelectionSpy).toHaveBeenCalledWith(doc, mockedRange, undefined); expect(setEditorStyleSpy).toHaveBeenCalledTimes(5); expect(setEditorStyleSpy).toHaveBeenCalledWith(core, '_DOMSelection', null); expect(setEditorStyleSpy).toHaveBeenCalledWith(core, '_DOMSelectionHideCursor', null); + expect(setEditorStyleSpy).toHaveBeenCalledWith( + core, + '_DOMSelectionHideSelection', + null + ); expect(setEditorStyleSpy).toHaveBeenCalledWith( core, '_DOMSelection', 'outline-style:auto!important; outline-color:#DB626C!important;', - ['#image_0'] + ['span:has(>img#image_0_0)'] ); expect(setEditorStyleSpy).toHaveBeenCalledWith( core, '_DOMSelectionHideSelection', - 'background-color: transparent !important', + 'background-color: transparent !important;', ['*::selection'] ); }); diff --git a/packages/roosterjs-content-model-core/test/corePlugin/undo/UndoPluginTest.ts b/packages/roosterjs-content-model-core/test/corePlugin/undo/UndoPluginTest.ts index eb0cf93c732..665df07084e 100644 --- a/packages/roosterjs-content-model-core/test/corePlugin/undo/UndoPluginTest.ts +++ b/packages/roosterjs-content-model-core/test/corePlugin/undo/UndoPluginTest.ts @@ -941,5 +941,23 @@ describe('UndoPlugin', () => { expect(mockedSnapshotsManager.hasNewContent).toBeTrue(); expect(clearRedoSpy).toHaveBeenCalledTimes(0); }); + + it('MouseDown addUndoSnapshot if there is new content', () => { + const state = plugin.getState(); + + plugin.onPluginEvent({ + eventType: 'mouseDown', + } as any); + + expect(takeSnapshotSpy).toHaveBeenCalledTimes(0); + + state.snapshotsManager.hasNewContent = true; + + plugin.onPluginEvent({ + eventType: 'mouseDown', + } as any); + + expect(takeSnapshotSpy).toHaveBeenCalledTimes(1); + }); }); }); diff --git a/packages/roosterjs-content-model-core/test/editor/EditorTest.ts b/packages/roosterjs-content-model-core/test/editor/EditorTest.ts index 928afd110d7..042f2d0c9db 100644 --- a/packages/roosterjs-content-model-core/test/editor/EditorTest.ts +++ b/packages/roosterjs-content-model-core/test/editor/EditorTest.ts @@ -135,13 +135,8 @@ describe('Editor', () => { const editor = new Editor(div); - const model1 = editor.getContentModelCopy('connected'); - - expect(model1).toBe(mockedModel); - expect(createContentModelSpy).toHaveBeenCalledWith(mockedCore, { tryGetFromCache: true }); - editor.dispose(); - expect(() => editor.getContentModelCopy('connected')).toThrow(); + expect(() => editor.getContentModelCopy('disconnected')).toThrow(); expect(resetSpy).toHaveBeenCalledWith(); }); diff --git a/packages/roosterjs-content-model-dom/lib/modelToDom/optimizers/removeUnnecessarySpan.ts b/packages/roosterjs-content-model-dom/lib/modelToDom/optimizers/removeUnnecessarySpan.ts index ee048a395bb..3b0ccb11b88 100644 --- a/packages/roosterjs-content-model-dom/lib/modelToDom/optimizers/removeUnnecessarySpan.ts +++ b/packages/roosterjs-content-model-dom/lib/modelToDom/optimizers/removeUnnecessarySpan.ts @@ -8,7 +8,8 @@ export function removeUnnecessarySpan(root: Node) { if ( isNodeOfType(child, 'ELEMENT_NODE') && child.tagName == 'SPAN' && - child.attributes.length == 0 + child.attributes.length == 0 && + !isImageSpan(child) ) { const node = child; let refNode = child.nextSibling; @@ -26,3 +27,11 @@ export function removeUnnecessarySpan(root: Node) { } } } + +const isImageSpan = (child: HTMLElement) => { + return ( + isNodeOfType(child.firstChild, 'ELEMENT_NODE') && + child.firstChild.tagName == 'IMG' && + child.firstChild == child.lastChild + ); +}; diff --git a/packages/roosterjs-content-model-dom/lib/modelToText/contentModelToText.ts b/packages/roosterjs-content-model-dom/lib/modelToText/contentModelToText.ts index 75b8a10a7e6..7332c47b3ca 100644 --- a/packages/roosterjs-content-model-dom/lib/modelToText/contentModelToText.ts +++ b/packages/roosterjs-content-model-dom/lib/modelToText/contentModelToText.ts @@ -1,78 +1,106 @@ -import type { ContentModelBlockGroup, ContentModelDocument } from 'roosterjs-content-model-types'; +import type { + ContentModelBlockGroup, + ContentModelDocument, + ModelToTextCallbacks, +} from 'roosterjs-content-model-types'; const TextForHR = '________________________________________'; +const defaultCallbacks: Required = { + onDivider: divider => (divider.tagName == 'hr' ? TextForHR : ''), + onEntityBlock: () => '', + onEntitySegment: entity => entity.wrapper.textContent ?? '', + onGeneralSegment: segment => segment.element.textContent ?? '', + onImage: () => ' ', + onText: text => text.text, + onParagraph: () => true, + onTable: () => true, + onBlockGroup: () => true, +}; /** * Convert Content Model to plain text * @param model The source Content Model * @param [separator='\r\n'] The separator string used for connect lines + * @param callbacks Callbacks to customize the behavior of contentModelToText function */ export function contentModelToText( model: ContentModelDocument, - separator: string = '\r\n' + separator: string = '\r\n', + callbacks?: ModelToTextCallbacks ): string { const textArray: string[] = []; + const fullCallbacks = Object.assign({}, defaultCallbacks, callbacks); - contentModelToTextArray(model, textArray); + contentModelToTextArray(model, textArray, fullCallbacks); return textArray.join(separator); } -function contentModelToTextArray(group: ContentModelBlockGroup, textArray: string[]) { - group.blocks.forEach(block => { - switch (block.blockType) { - case 'Paragraph': - let text = ''; +function contentModelToTextArray( + group: ContentModelBlockGroup, + textArray: string[], + callbacks: Required +) { + if (callbacks.onBlockGroup(group)) { + group.blocks.forEach(block => { + switch (block.blockType) { + case 'Paragraph': + if (callbacks.onParagraph(block)) { + let text = ''; - block.segments.forEach(segment => { - switch (segment.segmentType) { - case 'Br': - textArray.push(text); - text = ''; - break; + block.segments.forEach(segment => { + switch (segment.segmentType) { + case 'Br': + textArray.push(text); + text = ''; + break; - case 'Entity': - text += segment.wrapper.textContent || ''; - break; + case 'Entity': + text += callbacks.onEntitySegment(segment); + break; - case 'General': - text += segment.element.textContent || ''; - break; + case 'General': + text += callbacks.onGeneralSegment(segment); + break; - case 'Text': - text += segment.text; - break; + case 'Text': + text += callbacks.onText(segment); + break; - case 'Image': - text += ' '; - break; - } - }); + case 'Image': + text += callbacks.onImage(segment); + break; + } + }); - if (text) { - textArray.push(text); - } + if (text) { + textArray.push(text); + } + } - break; + break; - case 'Divider': - textArray.push(block.tagName == 'hr' ? TextForHR : ''); - break; - case 'Entity': - textArray.push(''); - break; + case 'Divider': + textArray.push(callbacks.onDivider(block)); + break; + case 'Entity': + textArray.push(callbacks.onEntityBlock(block)); + break; - case 'Table': - block.rows.forEach(row => - row.cells.forEach(cell => { - contentModelToTextArray(cell, textArray); - }) - ); - break; + case 'Table': + if (callbacks.onTable(block)) { + block.rows.forEach(row => + row.cells.forEach(cell => { + contentModelToTextArray(cell, textArray, callbacks); + }) + ); + } + break; - case 'BlockGroup': - contentModelToTextArray(block, textArray); - break; - } - }); + case 'BlockGroup': + contentModelToTextArray(block, textArray, callbacks); + break; + } + }); + } } diff --git a/packages/roosterjs-content-model-dom/test/modelToDom/optimizers/removeUnnecessarySpanTest.ts b/packages/roosterjs-content-model-dom/test/modelToDom/optimizers/removeUnnecessarySpanTest.ts index fd79446c9af..f2aab1c27e2 100644 --- a/packages/roosterjs-content-model-dom/test/modelToDom/optimizers/removeUnnecessarySpanTest.ts +++ b/packages/roosterjs-content-model-dom/test/modelToDom/optimizers/removeUnnecessarySpanTest.ts @@ -53,4 +53,13 @@ describe('removeUnnecessarySpan', () => { expect(div.innerHTML).toBe('test1test2test3'); }); + + it('Do not remove image span', () => { + const div = document.createElement('div'); + div.innerHTML = ''; + + removeUnnecessarySpan(div); + + expect(div.innerHTML).toBe(''); + }); }); diff --git a/packages/roosterjs-content-model-dom/test/modelToText/contentModelToTextTest.ts b/packages/roosterjs-content-model-dom/test/modelToText/contentModelToTextTest.ts index 8400b40cc49..24b71a53c85 100644 --- a/packages/roosterjs-content-model-dom/test/modelToText/contentModelToTextTest.ts +++ b/packages/roosterjs-content-model-dom/test/modelToText/contentModelToTextTest.ts @@ -3,6 +3,7 @@ import { createBr } from '../../lib/modelApi/creators/createBr'; import { createContentModelDocument } from '../../lib/modelApi/creators/createContentModelDocument'; import { createDivider } from '../../lib/modelApi/creators/createDivider'; import { createEntity } from '../../lib/modelApi/creators/createEntity'; +import { createGeneralSegment } from '../../lib/modelApi/creators/createGeneralSegment'; import { createImage } from '../../lib/modelApi/creators/createImage'; import { createListItem } from '../../lib/modelApi/creators/createListItem'; import { createListLevel } from '../../lib/modelApi/creators/createListLevel'; @@ -189,4 +190,44 @@ describe('modelToText', () => { expect(text).toBe('text1test entitytext2'); }); + + it('With callbacks', () => { + const onDivider = jasmine.createSpy('onDivider').and.returnValue('divider'); + const onEntitySegment = jasmine + .createSpy('onEntitySegment') + .and.returnValue('entity segment'); + const onEntityBlock = jasmine.createSpy('onEntityBlock').and.returnValue('entity block'); + const onGeneralSegment = jasmine.createSpy('onGeneralSegment').and.returnValue('general'); + const onImage = jasmine.createSpy('onImage').and.returnValue('image'); + const onText = jasmine.createSpy('onText').and.returnValue('text'); + + const doc = createContentModelDocument(); + const para = createParagraph(); + const entitySegment = createEntity(null!); + const entityBlock = createEntity(null!); + const generalSegment = createGeneralSegment(null!); + const divider = createDivider('div'); + const image = createImage('src'); + const text = createText('test'); + + para.segments.push(entitySegment, generalSegment, image, text); + doc.blocks.push(para, entityBlock, divider); + + const result = contentModelToText(doc, '/', { + onDivider, + onEntityBlock, + onEntitySegment, + onGeneralSegment, + onImage, + onText, + }); + + expect(result).toBe('entity segmentgeneralimagetext/entity block/divider'); + expect(onDivider).toHaveBeenCalledWith(divider); + expect(onEntityBlock).toHaveBeenCalledWith(entityBlock); + expect(onEntitySegment).toHaveBeenCalledWith(entitySegment); + expect(onGeneralSegment).toHaveBeenCalledWith(generalSegment); + expect(onImage).toHaveBeenCalledWith(image); + expect(onText).toHaveBeenCalledWith(text); + }); }); diff --git a/packages/roosterjs-content-model-plugins/lib/edit/keyboardDelete.ts b/packages/roosterjs-content-model-plugins/lib/edit/keyboardDelete.ts index e4fadcc1a1b..44c73db93aa 100644 --- a/packages/roosterjs-content-model-plugins/lib/edit/keyboardDelete.ts +++ b/packages/roosterjs-content-model-plugins/lib/edit/keyboardDelete.ts @@ -49,6 +49,7 @@ export function keyboardDelete(editor: IEditor, rawEvent: KeyboardEvent) { rawEvent, changeSource: ChangeSource.Keyboard, getChangeData: () => rawEvent.which, + scrollCaretIntoView: true, apiName: rawEvent.key == 'Delete' ? 'handleDeleteKey' : 'handleBackspaceKey', } ); diff --git a/packages/roosterjs-content-model-plugins/lib/edit/keyboardInput.ts b/packages/roosterjs-content-model-plugins/lib/edit/keyboardInput.ts index a25f2d6beaa..91ecf99647e 100644 --- a/packages/roosterjs-content-model-plugins/lib/edit/keyboardInput.ts +++ b/packages/roosterjs-content-model-plugins/lib/edit/keyboardInput.ts @@ -36,6 +36,7 @@ export function keyboardInput(editor: IEditor, rawEvent: KeyboardEvent) { } }, { + scrollCaretIntoView: true, rawEvent, } ); diff --git a/packages/roosterjs-content-model-plugins/lib/hyperlink/HyperlinkPlugin.ts b/packages/roosterjs-content-model-plugins/lib/hyperlink/HyperlinkPlugin.ts index 3e137362211..2b9447ad87b 100644 --- a/packages/roosterjs-content-model-plugins/lib/hyperlink/HyperlinkPlugin.ts +++ b/packages/roosterjs-content-model-plugins/lib/hyperlink/HyperlinkPlugin.ts @@ -131,6 +131,8 @@ export class HyperlinkPlugin implements EditorPlugin { } catch {} } }); + } else if (event.eventType == 'contentChanged') { + this.domHelper?.setDomAttribute('title', null /*value*/); } } diff --git a/packages/roosterjs-content-model-plugins/lib/index.ts b/packages/roosterjs-content-model-plugins/lib/index.ts index f1dc01e3060..fd29faee83f 100644 --- a/packages/roosterjs-content-model-plugins/lib/index.ts +++ b/packages/roosterjs-content-model-plugins/lib/index.ts @@ -1,5 +1,6 @@ export { TableEditPlugin } from './tableEdit/TableEditPlugin'; export { OnTableEditorCreatedCallback } from './tableEdit/OnTableEditorCreatedCallback'; +export { TableEditFeatureName } from './tableEdit/editors/features/TableEditFeatureName'; export { PastePlugin } from './paste/PastePlugin'; export { EditPlugin } from './edit/EditPlugin'; export { AutoFormatPlugin, AutoFormatOptions } from './autoFormat/AutoFormatPlugin'; diff --git a/packages/roosterjs-content-model-plugins/lib/tableEdit/OnTableEditorCreatedCallback.ts b/packages/roosterjs-content-model-plugins/lib/tableEdit/OnTableEditorCreatedCallback.ts index 6019bea2658..4d9946f21c0 100644 --- a/packages/roosterjs-content-model-plugins/lib/tableEdit/OnTableEditorCreatedCallback.ts +++ b/packages/roosterjs-content-model-plugins/lib/tableEdit/OnTableEditorCreatedCallback.ts @@ -1,7 +1,9 @@ +import type { TableEditFeatureName } from './editors/features/TableEditFeatureName'; + /** * Optional callback when creating a TableEditPlugin, allows to customize the Selectors element as required. */ export type OnTableEditorCreatedCallback = ( - editorType: 'HorizontalTableInserter' | 'VerticalTableInserter' | 'TableMover' | 'TableResizer', + featureType: TableEditFeatureName, element: HTMLElement ) => () => void; diff --git a/packages/roosterjs-content-model-plugins/lib/tableEdit/TableEditPlugin.ts b/packages/roosterjs-content-model-plugins/lib/tableEdit/TableEditPlugin.ts index 386f3091453..7005e6ad62d 100644 --- a/packages/roosterjs-content-model-plugins/lib/tableEdit/TableEditPlugin.ts +++ b/packages/roosterjs-content-model-plugins/lib/tableEdit/TableEditPlugin.ts @@ -1,5 +1,6 @@ import { isNodeOfType, normalizeRect } from 'roosterjs-content-model-dom'; import { TableEditor } from './editors/TableEditor'; +import type { TableEditFeatureName } from './editors/features/TableEditFeatureName'; import type { OnTableEditorCreatedCallback } from './OnTableEditorCreatedCallback'; import type { EditorPlugin, IEditor, PluginEvent, Rect } from 'roosterjs-content-model-types'; @@ -20,10 +21,12 @@ export class TableEditPlugin implements EditorPlugin { * The container must not be affected by transform: scale(), otherwise the position calculation will be wrong. * If not specified, the plugin will be inserted in document.body * @param onTableEditorCreated An optional callback to customize the Table Editors elements when created. + * @param disableFeatures An optional array of TableEditFeatures to disable */ constructor( private anchorContainerSelector?: string, - private onTableEditorCreated?: OnTableEditorCreatedCallback + private onTableEditorCreated?: OnTableEditorCreatedCallback, + private disableFeatures?: TableEditFeatureName[] ) {} /** @@ -147,7 +150,8 @@ export class TableEditPlugin implements EditorPlugin { this.invalidateTableRects, isNodeOfType(container, 'ELEMENT_NODE') ? container : undefined, event?.currentTarget, - this.onTableEditorCreated + this.onTableEditorCreated, + this.disableFeatures ); } } diff --git a/packages/roosterjs-content-model-plugins/lib/tableEdit/editors/TableEditor.ts b/packages/roosterjs-content-model-plugins/lib/tableEdit/editors/TableEditor.ts index ef6007dc572..cb29f02c87e 100644 --- a/packages/roosterjs-content-model-plugins/lib/tableEdit/editors/TableEditor.ts +++ b/packages/roosterjs-content-model-plugins/lib/tableEdit/editors/TableEditor.ts @@ -7,6 +7,7 @@ import { isNodeOfType, normalizeRect, parseTableCells } from 'roosterjs-content- import type { OnTableEditorCreatedCallback } from '../OnTableEditorCreatedCallback'; import type { TableEditFeature } from './features/TableEditFeature'; import type { IEditor, TableSelection } from 'roosterjs-content-model-types'; +import type { TableEditFeatureName } from './features/TableEditFeatureName'; const INSERTER_HOVER_OFFSET = 6; const enum TOP_OR_SIDE { @@ -67,7 +68,8 @@ export class TableEditor { private onChanged: () => void, private anchorContainer?: HTMLElement, private contentDiv?: EventTarget | null, - private onTableEditorCreated?: OnTableEditorCreatedCallback + private onTableEditorCreated?: OnTableEditorCreatedCallback, + private disableFeatures?: TableEditFeatureName[] ) { this.isRTL = editor.getDocument().defaultView?.getComputedStyle(table).direction == 'rtl'; this.setEditorFeatures(); @@ -145,10 +147,11 @@ export class TableEditor { if (i === 0 && topOrSide == TOP_OR_SIDE.top) { const center = (tdRect.left + tdRect.right) / 2; const isOnRightHalf = this.isRTL ? x < center : x > center; - this.setInserterTd( - isOnRightHalf ? td : tr.cells[j - 1], - false /*isHorizontal*/ - ); + !this.isFeatureDisabled('VerticalTableInserter') && + this.setInserterTd( + isOnRightHalf ? td : tr.cells[j - 1], + false /*isHorizontal*/ + ); } else if (j === 0 && topOrSide == TOP_OR_SIDE.side) { const tdAbove = this.table.rows[i - 1]?.cells[0]; const tdAboveRect = tdAbove @@ -161,17 +164,18 @@ export class TableEditor { ? tdAboveRect.right === tdRect.right : tdAboveRect.left === tdRect.left; - this.setInserterTd( - y < (tdRect.top + tdRect.bottom) / 2 && isTdNotAboveMerged - ? tdAbove - : td, - true /*isHorizontal*/ - ); + !this.isFeatureDisabled('HorizontalTableInserter') && + this.setInserterTd( + y < (tdRect.top + tdRect.bottom) / 2 && isTdNotAboveMerged + ? tdAbove + : td, + true /*isHorizontal*/ + ); } else { this.setInserterTd(null); } - this.setResizingTd(td); + !this.isFeatureDisabled('CellResizer') && this.setResizingTd(td); //Cell found break; @@ -188,19 +192,24 @@ export class TableEditor { } private setEditorFeatures() { - if (!this.tableMover) { + const disableSelector = this.isFeatureDisabled('TableSelector'); + const disableMovement = this.isFeatureDisabled('TableMover'); + if (!this.tableMover && !(disableSelector && disableMovement)) { this.tableMover = createTableMover( this.table, this.editor, this.isRTL, - this.onSelect, + disableSelector ? () => {} : this.onSelect, + this.onStartTableMove, + this.onEndTableMove, this.contentDiv, this.anchorContainer, - this.onEditorCreated + this.onEditorCreated, + disableMovement ); } - if (!this.tableResizer) { + if (!this.tableResizer && !this.isFeatureDisabled('TableResizer')) { this.tableResizer = createTableResizer( this.table, this.editor, @@ -214,15 +223,8 @@ export class TableEditor { } } - private onEditorCreated = ( - editorType: - | 'HorizontalTableInserter' - | 'VerticalTableInserter' - | 'TableMover' - | 'TableResizer', - element: HTMLElement - ) => { - const disposer = this.onTableEditorCreated?.(editorType, element); + private onEditorCreated = (featureType: TableEditFeatureName, element: HTMLElement) => { + const disposer = this.onTableEditorCreated?.(featureType, element); const onMouseOut = element && this.getOnMouseOut(element); if (onMouseOut) { element.addEventListener('mouseout', onMouseOut); @@ -355,6 +357,13 @@ export class TableEditor { this.onStartResize(); }; + private onStartTableMove = () => { + this.isCurrentlyEditing = true; + this.disposeTableResizer(); + this.disposeTableInserter(); + this.disposeCellResizers(); + }; + private onStartResize() { this.isCurrentlyEditing = true; const range = this.editor.getDOMSelection(); @@ -366,6 +375,11 @@ export class TableEditor { this.editor.takeSnapshot(); } + private onEndTableMove = () => { + this.disposeTableMover(); + return this.onFinishEditing(); + }; + private onInserted = () => { this.disposeTableResizer(); this.onFinishEditing(); @@ -400,10 +414,15 @@ export class TableEditor { ev.relatedTarget != feature && isNodeOfType(this.contentDiv as Node, 'ELEMENT_NODE') && isNodeOfType(ev.relatedTarget as Node, 'ELEMENT_NODE') && - !(this.contentDiv == ev.relatedTarget) + !(this.contentDiv == ev.relatedTarget) && + !this.isEditing() ) { this.dispose(); } }; }; + + private isFeatureDisabled(feature: TableEditFeatureName) { + return this.disableFeatures?.includes(feature); + } } diff --git a/packages/roosterjs-content-model-plugins/lib/tableEdit/editors/features/CellResizer.ts b/packages/roosterjs-content-model-plugins/lib/tableEdit/editors/features/CellResizer.ts index 84a71953456..982bdac877e 100644 --- a/packages/roosterjs-content-model-plugins/lib/tableEdit/editors/features/CellResizer.ts +++ b/packages/roosterjs-content-model-plugins/lib/tableEdit/editors/features/CellResizer.ts @@ -1,5 +1,6 @@ import { createElement } from '../../../pluginUtils/CreateElement/createElement'; import { DragAndDropHelper } from '../../../pluginUtils/DragAndDrop/DragAndDropHelper'; +import type { TableEditFeature } from './TableEditFeature'; import { isElementOfType, normalizeRect, @@ -9,9 +10,16 @@ import { } from 'roosterjs-content-model-dom'; import type { DragAndDropHandler } from '../../../pluginUtils/DragAndDrop/DragAndDropHandler'; import type { ContentModelTable, IEditor } from 'roosterjs-content-model-types'; -import type { TableEditFeature } from './TableEditFeature'; const CELL_RESIZER_WIDTH = 4; +/** + * @internal + */ +export const HORIZONTAL_RESIZER_ID = 'horizontalResizer'; +/** + * @internal + */ +export const VERTICAL_RESIZER_ID = 'verticalResizer'; /** * @internal @@ -223,7 +231,7 @@ function setHorizontalPosition(context: DragAndDropContext, trigger: HTMLElement const { td } = context; const rect = normalizeRect(td.getBoundingClientRect()); if (rect) { - trigger.id = 'horizontalResizer'; + trigger.id = HORIZONTAL_RESIZER_ID; trigger.style.top = rect.bottom - CELL_RESIZER_WIDTH + 'px'; trigger.style.left = rect.left + 'px'; trigger.style.width = rect.right - rect.left + 'px'; @@ -235,7 +243,7 @@ function setVerticalPosition(context: DragAndDropContext, trigger: HTMLElement) const { td, isRTL } = context; const rect = normalizeRect(td.getBoundingClientRect()); if (rect) { - trigger.id = 'verticalResizer'; + trigger.id = VERTICAL_RESIZER_ID; trigger.style.top = rect.top + 'px'; trigger.style.left = (isRTL ? rect.left : rect.right) - CELL_RESIZER_WIDTH + 1 + 'px'; trigger.style.width = CELL_RESIZER_WIDTH + 'px'; diff --git a/packages/roosterjs-content-model-plugins/lib/tableEdit/editors/features/TableEditFeature.ts b/packages/roosterjs-content-model-plugins/lib/tableEdit/editors/features/TableEditFeature.ts index b1bea610c57..58b2fadeaa8 100644 --- a/packages/roosterjs-content-model-plugins/lib/tableEdit/editors/features/TableEditFeature.ts +++ b/packages/roosterjs-content-model-plugins/lib/tableEdit/editors/features/TableEditFeature.ts @@ -12,11 +12,11 @@ export interface TableEditFeature { /** * @internal */ -export function disposeTableEditFeature(resizer: TableEditFeature | null) { - if (resizer) { - resizer.featureHandler?.dispose(); - resizer.featureHandler = null; - resizer.div?.parentNode?.removeChild(resizer.div); - resizer.div = null; +export function disposeTableEditFeature(feature: TableEditFeature | null) { + if (feature) { + feature.featureHandler?.dispose(); + feature.featureHandler = null; + feature.div?.parentNode?.removeChild(feature.div); + feature.div = null; } } diff --git a/packages/roosterjs-content-model-plugins/lib/tableEdit/editors/features/TableEditFeatureName.ts b/packages/roosterjs-content-model-plugins/lib/tableEdit/editors/features/TableEditFeatureName.ts new file mode 100644 index 00000000000..acf3d59a489 --- /dev/null +++ b/packages/roosterjs-content-model-plugins/lib/tableEdit/editors/features/TableEditFeatureName.ts @@ -0,0 +1,10 @@ +/** + * Names of table edit features + */ +export type TableEditFeatureName = + | 'HorizontalTableInserter' + | 'VerticalTableInserter' + | 'TableMover' + | 'TableResizer' + | 'TableSelector' + | 'CellResizer'; diff --git a/packages/roosterjs-content-model-plugins/lib/tableEdit/editors/features/TableInserter.ts b/packages/roosterjs-content-model-plugins/lib/tableEdit/editors/features/TableInserter.ts index bf2798aa398..6e0dfeeb63d 100644 --- a/packages/roosterjs-content-model-plugins/lib/tableEdit/editors/features/TableInserter.ts +++ b/packages/roosterjs-content-model-plugins/lib/tableEdit/editors/features/TableInserter.ts @@ -1,6 +1,7 @@ import { createElement } from '../../../pluginUtils/CreateElement/createElement'; import { getIntersectedRect } from '../../../pluginUtils/Rect/getIntersectedRect'; import { isElementOfType, normalizeRect } from 'roosterjs-content-model-dom'; +import type { TableEditFeature } from './TableEditFeature'; import type { OnTableEditorCreatedCallback } from '../../OnTableEditorCreatedCallback'; import { formatTableWithContentModel, @@ -9,13 +10,20 @@ import { } from 'roosterjs-content-model-api'; import type { CreateElementData } from '../../../pluginUtils/CreateElement/CreateElementData'; import type { Disposable } from '../../../pluginUtils/Disposable'; -import type { TableEditFeature } from './TableEditFeature'; import type { IEditor } from 'roosterjs-content-model-types'; const INSERTER_COLOR = '#4A4A4A'; const INSERTER_COLOR_DARK_MODE = 'white'; const INSERTER_SIDE_LENGTH = 12; const INSERTER_BORDER_SIZE = 1; +/** + * @internal + */ +export const HORIZONTAL_INSERTER_ID = 'horizontalInserter'; +/** + * @internal + */ +export const VERTICAL_INSERTER_ID = 'verticalInserter'; /** * @internal @@ -48,7 +56,7 @@ export function createTableInserter( if (isHorizontal) { // tableRect.left/right is used because the Inserter is always intended to be on the side - div.id = 'horizontalInserter'; + div.id = HORIZONTAL_INSERTER_ID; div.style.left = `${ isRTL ? tableRect.right @@ -57,7 +65,7 @@ export function createTableInserter( div.style.top = `${tdRect.bottom - 8}px`; (div.firstChild as HTMLElement).style.width = `${tableRect.right - tableRect.left}px`; } else { - div.id = 'verticalInserter'; + div.id = VERTICAL_INSERTER_ID; div.style.left = `${isRTL ? tdRect.left - 8 : tdRect.right - 8}px`; // tableRect.top is used because the Inserter is always intended to be on top div.style.top = `${ diff --git a/packages/roosterjs-content-model-plugins/lib/tableEdit/editors/features/TableMover.ts b/packages/roosterjs-content-model-plugins/lib/tableEdit/editors/features/TableMover.ts index 44832b80f9c..6af1d7482a0 100644 --- a/packages/roosterjs-content-model-plugins/lib/tableEdit/editors/features/TableMover.ts +++ b/packages/roosterjs-content-model-plugins/lib/tableEdit/editors/features/TableMover.ts @@ -1,27 +1,50 @@ import { createElement } from '../../../pluginUtils/CreateElement/createElement'; import { DragAndDropHelper } from '../../../pluginUtils/DragAndDrop/DragAndDropHelper'; -import { isNodeOfType, normalizeRect } from 'roosterjs-content-model-dom'; +import { formatInsertPointWithContentModel } from 'roosterjs-content-model-api'; +import type { TableEditFeature } from './TableEditFeature'; import type { OnTableEditorCreatedCallback } from '../../OnTableEditorCreatedCallback'; import type { DragAndDropHandler } from '../../../pluginUtils/DragAndDrop/DragAndDropHandler'; -import type { IEditor, Rect } from 'roosterjs-content-model-types'; -import type { TableEditFeature } from './TableEditFeature'; +import { + createContentModelDocument, + createSelectionMarker, + getFirstSelectedTable, + isNodeOfType, + mergeModel, + normalizeRect, + setParagraphNotImplicit, + setSelection, +} from 'roosterjs-content-model-dom'; +import type { + ContentModelTable, + DOMInsertPoint, + DOMSelection, + IEditor, + Rect, +} from 'roosterjs-content-model-types'; const TABLE_MOVER_LENGTH = 12; -const TABLE_MOVER_ID = '_Table_Mover'; +/** + * @internal + */ +export const TABLE_MOVER_ID = '_Table_Mover'; +const TABLE_MOVER_STYLE_KEY = '_TableMoverCursorStyle'; /** * @internal + * Allows user to move table to another position * Contains the function to select whole table - * Moving behavior not implemented yet */ export function createTableMover( table: HTMLTableElement, editor: IEditor, isRTL: boolean, onFinishDragging: (table: HTMLTableElement) => void, + onStart: () => void, + onEnd: () => void, contentDiv?: EventTarget | null, anchorContainer?: HTMLElement, - onTableEditorCreated?: OnTableEditorCreatedCallback + onTableEditorCreated?: OnTableEditorCreatedCallback, + disableMovement?: boolean ): TableEditFeature | null { const rect = normalizeRect(table.getBoundingClientRect()); @@ -33,7 +56,7 @@ export function createTableMover( const document = table.ownerDocument; const createElementData = { tag: 'div', - style: 'position: fixed; cursor: all-scroll; user-select: none; border: 1px solid #808080', + style: 'position: fixed; cursor: move; user-select: none; border: 1px solid #808080', }; const div = createElement(createElementData, document) as HTMLDivElement; @@ -49,40 +72,60 @@ export function createTableMover( zoomScale, rect, isRTL, + editor, + div, + onFinishDragging, + onStart, + onEnd, + disableMovement, }; setDivPosition(context, div); - const onDragEnd = (context: TableMoverContext, event: MouseEvent): false => { - if (event.target == div) { - onFinishDragging(context.table); - } - return false; - }; - const featureHandler = new TableMoverFeature( div, context, - setDivPosition, - { - onDragEnd, - }, + () => {}, + disableMovement + ? { onDragEnd } + : { + onDragStart, + onDragging, + onDragEnd, + }, context.zoomScale, - onTableEditorCreated + onTableEditorCreated, + editor.getEnvironment().isMobileOrTablet ); - return { div, featureHandler, node: table }; + return { node: table, div, featureHandler }; } -interface TableMoverContext { +/** + * @internal + * Exported for testing + */ +export interface TableMoverContext { table: HTMLTableElement; zoomScale: number; rect: Rect | null; isRTL: boolean; + editor: IEditor; + div: HTMLElement; + onFinishDragging: (table: HTMLTableElement) => void; + onStart: () => void; + onEnd: () => void; + disableMovement?: boolean; } -interface TableMoverInitValue { - event: MouseEvent; +/** + * @internal + * Exported for testing + */ +export interface TableMoverInitValue { + cmTable: ContentModelTable | undefined; + initialSelection: DOMSelection | null; + tableRect: HTMLDivElement; } class TableMoverFeature extends DragAndDropHelper { @@ -98,9 +141,10 @@ class TableMoverFeature extends DragAndDropHelper void, handler: DragAndDropHandler, zoomScale: number, - onTableEditorCreated?: OnTableEditorCreatedCallback + onTableEditorCreated?: OnTableEditorCreatedCallback, + forceMobile?: boolean | undefined ) { - super(div, context, onSubmit, handler, zoomScale); + super(div, context, onSubmit, handler, zoomScale, forceMobile); this.disposer = onTableEditorCreated?.('TableMover', div); } @@ -129,3 +173,219 @@ function isTableTopVisible(editor: IEditor, rect: Rect | null, contentDiv?: Node return true; } + +function setTableMoverCursor(editor: IEditor, state: boolean, type?: 'move' | 'copy') { + editor?.setEditorStyle(TABLE_MOVER_STYLE_KEY, state ? 'cursor: ' + type ?? 'move' : null); +} + +// Get insertion point from coordinate. +function getNodePositionFromEvent(editor: IEditor, x: number, y: number): DOMInsertPoint | null { + const doc = editor.getDocument(); + const domHelper = editor.getDOMHelper(); + + if (doc.caretRangeFromPoint) { + // Chrome, Edge, Safari, Opera + const range = doc.caretRangeFromPoint(x, y); + if (range && domHelper.isNodeInEditor(range.startContainer)) { + return { node: range.startContainer, offset: range.startOffset }; + } + } + + if ('caretPositionFromPoint' in doc) { + // Firefox + const pos = (doc as any).caretPositionFromPoint(x, y); + if (pos && domHelper.isNodeInEditor(pos.offsetNode)) { + return { node: pos.offsetNode, offset: pos.offset }; + } + } + + if (doc.elementFromPoint) { + // Fallback + const element = doc.elementFromPoint(x, y); + if (element && domHelper.isNodeInEditor(element)) { + return { node: element, offset: 0 }; + } + } + + return null; +} + +/** + * @internal + * Exported for testing + */ +export function onDragStart(context: TableMoverContext): TableMoverInitValue { + context.onStart(); + + const { editor, table, div } = context; + + setTableMoverCursor(editor, true, 'move'); + + // Create table outline rectangle + const trect = table.getBoundingClientRect(); + const createElementData = { + tag: 'div', + style: 'position: fixed; user-select: none; border: 1px solid #808080', + }; + const tableRect = createElement(createElementData, document) as HTMLDivElement; + tableRect.style.width = `${trect.width}px`; + tableRect.style.height = `${trect.height}px`; + tableRect.style.top = `${trect.top}px`; + tableRect.style.left = `${trect.left}px`; + div.parentNode?.appendChild(tableRect); + + // Get current selection + const initialSelection = editor.getDOMSelection(); + + // Select first cell of the table + editor.setDOMSelection({ + type: 'table', + firstColumn: 0, + firstRow: 0, + lastColumn: 0, + lastRow: 0, + table: table, + }); + + // Get the table content model + const [cmTable] = getFirstSelectedTable(editor.getContentModelCopy('disconnected')); + + // Restore selection + editor.setDOMSelection(initialSelection); + + return { + cmTable, + initialSelection, + tableRect, + }; +} + +/** + * @internal + * Exported for testing + */ +export function onDragging( + context: TableMoverContext, + event: MouseEvent, + initValue: TableMoverInitValue +) { + const { tableRect } = initValue; + const { editor } = context; + + // Move table outline rectangle + tableRect.style.top = `${event.clientY + TABLE_MOVER_LENGTH}px`; + tableRect.style.left = `${event.clientX + TABLE_MOVER_LENGTH}px`; + + const pos = getNodePositionFromEvent(editor, event.clientX, event.clientY); + if (pos) { + const range = editor.getDocument().createRange(); + range.setStart(pos.node, pos.offset); + range.collapse(true); + + editor.setDOMSelection({ type: 'range', range, isReverted: false }); + return true; + } + return false; +} + +/** + * @internal + * Exported for testing + */ +export function onDragEnd( + context: TableMoverContext, + event: MouseEvent, + initValue: TableMoverInitValue | undefined +) { + const { editor, table, onFinishDragging: selectWholeTable, disableMovement } = context; + const element = event.target; + + // Remove table outline rectangle + initValue?.tableRect.remove(); + + // Reset cursor + setTableMoverCursor(editor, false); + + if (element == context.div) { + // Table mover was only clicked, select whole table + selectWholeTable(table); + context.onEnd(); + return true; + } else { + // Check if table was dragged on itself, element is not in editor, or movement is disabled + if ( + table.contains(element as Node) || + !editor.getDOMHelper().isNodeInEditor(element as Node) || + disableMovement + ) { + editor.setDOMSelection(initValue?.initialSelection ?? null); + context.onEnd(); + return false; + } + + let insertionSuccess: boolean = false; + + // Get position to insert table + const insertPosition = getNodePositionFromEvent(editor, event.clientX, event.clientY); + if (insertPosition) { + // Move table to new position + formatInsertPointWithContentModel( + editor, + insertPosition, + (model, context, ip) => { + // Remove old table + const [oldTable, path] = getFirstSelectedTable(model); + if (oldTable) { + const index = path[0].blocks.indexOf(oldTable); + path[0].blocks.splice(index, 1); + } + + if (ip && initValue?.cmTable) { + // Insert new table + const doc = createContentModelDocument(); + doc.blocks.push(initValue.cmTable); + insertionSuccess = !!mergeModel(model, doc, context, { + mergeFormat: 'none', + insertPosition: ip, + }); + + if (insertionSuccess) { + // After mergeModel, the new table should be selected + const finalTable = getFirstSelectedTable(model)[0] ?? initValue.cmTable; + if (finalTable) { + // Add selection marker to the first cell of the table + const FirstCell = finalTable.rows[0].cells[0]; + const markerParagraph = FirstCell?.blocks[0]; + if (markerParagraph?.blockType == 'Paragraph') { + const marker = createSelectionMarker(model.format); + + markerParagraph.segments.unshift(marker); + setParagraphNotImplicit(markerParagraph); + setSelection(FirstCell, marker); + } + } + } + return insertionSuccess; + } + }, + { + // Select first cell of the old table + selectionOverride: { + type: 'table', + firstColumn: 0, + firstRow: 0, + lastColumn: 0, + lastRow: 0, + table: table, + }, + apiName: 'TableMover', + } + ); + } else { + // No movement, restore initial selection + editor.setDOMSelection(initValue?.initialSelection ?? null); + } + context.onEnd(); + return insertionSuccess; + } +} diff --git a/packages/roosterjs-content-model-plugins/lib/tableEdit/editors/features/TableResizer.ts b/packages/roosterjs-content-model-plugins/lib/tableEdit/editors/features/TableResizer.ts index a7dbc0b8b4b..4ee986a1bdc 100644 --- a/packages/roosterjs-content-model-plugins/lib/tableEdit/editors/features/TableResizer.ts +++ b/packages/roosterjs-content-model-plugins/lib/tableEdit/editors/features/TableResizer.ts @@ -1,6 +1,6 @@ import { createElement } from '../../../pluginUtils/CreateElement/createElement'; import { DragAndDropHelper } from '../../../pluginUtils/DragAndDrop/DragAndDropHelper'; - +import type { TableEditFeature } from './TableEditFeature'; import type { OnTableEditorCreatedCallback } from '../../OnTableEditorCreatedCallback'; import { getFirstSelectedTable, @@ -9,11 +9,13 @@ import { normalizeTable, } from 'roosterjs-content-model-dom'; import type { ContentModelTable, IEditor, Rect } from 'roosterjs-content-model-types'; -import type { TableEditFeature } from './TableEditFeature'; import type { DragAndDropHandler } from '../../../pluginUtils/DragAndDrop/DragAndDropHandler'; const TABLE_RESIZER_LENGTH = 12; -const TABLE_RESIZER_ID = '_Table_Resizer'; +/** + * @internal + */ +export const TABLE_RESIZER_ID = '_Table_Resizer'; /** * @internal diff --git a/packages/roosterjs-content-model-plugins/lib/watermark/WatermarkPlugin.ts b/packages/roosterjs-content-model-plugins/lib/watermark/WatermarkPlugin.ts index 73ff77861ce..b09eef7ee89 100644 --- a/packages/roosterjs-content-model-plugins/lib/watermark/WatermarkPlugin.ts +++ b/packages/roosterjs-content-model-plugins/lib/watermark/WatermarkPlugin.ts @@ -22,7 +22,7 @@ export class WatermarkPlugin implements EditorPlugin { * Create an instance of Watermark plugin * @param watermark The watermark string */ - constructor(private watermark: string, format?: WatermarkFormat) { + constructor(protected watermark: string, format?: WatermarkFormat) { this.format = format || { fontSize: '14px', textColor: '#AAAAAA', @@ -62,7 +62,10 @@ export class WatermarkPlugin implements EditorPlugin { return; } - if (event.eventType == 'input' && event.rawEvent.inputType == 'insertText') { + if ( + (event.eventType == 'input' && event.rawEvent.inputType == 'insertText') || + event.eventType == 'compositionEnd' + ) { // When input text, editor must not be empty, so we can do hide watermark now without checking content model this.showHide(editor, false /*isEmpty*/); } else if ( diff --git a/packages/roosterjs-content-model-plugins/lib/watermark/isModelEmptyFast.ts b/packages/roosterjs-content-model-plugins/lib/watermark/isModelEmptyFast.ts index bf78a5b03eb..4265e799e34 100644 --- a/packages/roosterjs-content-model-plugins/lib/watermark/isModelEmptyFast.ts +++ b/packages/roosterjs-content-model-plugins/lib/watermark/isModelEmptyFast.ts @@ -25,6 +25,11 @@ export function isModelEmptyFast(model: ContentModelDocument): boolean { ) ) { return false; // Has meaningful segments, it is not empty + } else if ( + (firstBlock.format.marginRight && parseFloat(firstBlock.format.marginRight) > 0) || + (firstBlock.format.marginLeft && parseFloat(firstBlock.format.marginLeft) > 0) + ) { + return false; // Has margin (indentation is changed), it is not empty } else { return firstBlock.segments.filter(x => x.segmentType == 'Br').length <= 1; // If there are more than one BR, it is not empty, otherwise it is empty } diff --git a/packages/roosterjs-content-model-plugins/test/hyperlink/HyperlinkPluginTest.ts b/packages/roosterjs-content-model-plugins/test/hyperlink/HyperlinkPluginTest.ts index 47d20637d85..d8a194d27d7 100644 --- a/packages/roosterjs-content-model-plugins/test/hyperlink/HyperlinkPluginTest.ts +++ b/packages/roosterjs-content-model-plugins/test/hyperlink/HyperlinkPluginTest.ts @@ -455,4 +455,15 @@ describe('HyperlinkPlugin', () => { plugin.dispose(); }); + + it('ContentChanged', () => { + const plugin = new HyperlinkPlugin(); + plugin.initialize(editor); + + plugin.onPluginEvent({ + eventType: 'contentChanged', + } as any); + + expect(setDomAttributeSpy).toHaveBeenCalledWith('title', null); + }); }); diff --git a/packages/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromExcelTest.ts b/packages/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromExcelTest.ts index 5d194543fd3..28a3b6c712a 100644 --- a/packages/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromExcelTest.ts +++ b/packages/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromExcelTest.ts @@ -44,7 +44,7 @@ describe(ID, () => { paste(editor, clipboardData, 'asImage'); - const model = editor.getContentModelCopy('connected'); + const model = editor.getContentModelCopy('disconnected'); const width = editor.getDOMHelper().getClientWidth(); expect(model).toEqual({ @@ -60,6 +60,10 @@ describe(ID, () => { maxWidth: `${width}px`, }, dataset: {}, + alt: undefined, + title: undefined, + isSelectedAsImageSelection: undefined, + isSelected: undefined, }, { segmentType: 'SelectionMarker', @@ -80,6 +84,9 @@ describe(ID, () => { }, ], format: {}, + cachedElement: undefined, + isImplicit: undefined, + segmentFormat: undefined, }, ], format: {}, diff --git a/packages/roosterjs-content-model-plugins/test/tableEdit/TableEditTestHelper.ts b/packages/roosterjs-content-model-plugins/test/tableEdit/TableEditTestHelper.ts index cc4b3788e13..caa8e27a6a9 100644 --- a/packages/roosterjs-content-model-plugins/test/tableEdit/TableEditTestHelper.ts +++ b/packages/roosterjs-content-model-plugins/test/tableEdit/TableEditTestHelper.ts @@ -1,6 +1,7 @@ import * as TestHelper from '../TestHelper'; import { DOMEventHandlerFunction } from 'roosterjs-editor-types'; import { getObjectKeys, normalizeTable } from 'roosterjs-content-model-dom'; +import { TableEditFeatureName } from '../../lib/tableEdit/editors/features/TableEditFeatureName'; import { TableEditPlugin } from '../../lib/tableEdit/TableEditPlugin'; import { ContentModelTable, @@ -15,8 +16,12 @@ import { * @param anchorContainerSelector The selector for the anchor container * @returns The editor, plugin, and handler to be used in the test */ -export function beforeTableTest(TEST_ID: string, anchorContainerSelector?: string) { - const plugin = new TableEditPlugin('.' + anchorContainerSelector); +export function beforeTableTest( + TEST_ID: string, + anchorContainerSelector?: string, + disabledFeatures?: TableEditFeatureName[] +) { + const plugin = new TableEditPlugin('.' + anchorContainerSelector, undefined, disabledFeatures); let handler: Record = {}; const attachDomEvent = jasmine diff --git a/packages/roosterjs-content-model-plugins/test/tableEdit/tableEditPluginTest.ts b/packages/roosterjs-content-model-plugins/test/tableEdit/tableEditPluginTest.ts index 5b0b2187b58..f9981981a22 100644 --- a/packages/roosterjs-content-model-plugins/test/tableEdit/tableEditPluginTest.ts +++ b/packages/roosterjs-content-model-plugins/test/tableEdit/tableEditPluginTest.ts @@ -1,15 +1,8 @@ import * as TestHelper from '../TestHelper'; import { createElement } from '../../lib/pluginUtils/CreateElement/createElement'; -import { DOMEventHandlerFunction, IEditor } from 'roosterjs-content-model-types'; import { getModelTable } from './tableData'; +import { IEditor } from 'roosterjs-content-model-types'; import { TableEditPlugin } from '../../lib/tableEdit/TableEditPlugin'; -import { - afterTableTest, - beforeTableTest, - getCellRect, - initialize, - mouseToPoint, -} from './TableEditTestHelper'; describe('TableEditPlugin', () => { let editor: IEditor; @@ -234,57 +227,3 @@ describe('TableEditPlugin', () => { expect(pluginName).toBe(expectedName); }); }); - -describe('anchorContainer', () => { - let editor: IEditor; - let plugin: TableEditPlugin; - const TEST_ID = 'cellResizerTest'; - const ANCHOR_CLASS = 'anchor_' + TEST_ID; - let handler: Record; - - beforeEach(() => {}); - - afterEach(() => { - afterTableTest(editor, plugin, TEST_ID); - }); - - it('Table editor features, resizer and mover, inserted on anchor', () => { - // Create editor, plugin, and table - const setup = beforeTableTest(TEST_ID, ANCHOR_CLASS); - editor = setup.editor; - plugin = setup.plugin; - handler = setup.handler; - initialize(editor, getModelTable()); - - // Move mouse to the first cell - const cellRect = getCellRect(editor, 0, 0); - mouseToPoint({ x: cellRect.left, y: cellRect.bottom }, handler); - - // Look for table mover and resizer on the anchor - const anchor = editor.getDocument().getElementsByClassName(ANCHOR_CLASS)[0]; - const mover = anchor?.querySelector('#_Table_Mover'); - const resizer = anchor?.querySelector('#_Table_Resizer'); - expect(!!mover).toBe(true); - expect(!!resizer).toBe(true); - }); - - it('Table editor features, resizer and mover, not inserted on anchor', () => { - // Create editor, plugin, and table - const setup = beforeTableTest(TEST_ID); - editor = setup.editor; - plugin = setup.plugin; - handler = setup.handler; - initialize(editor, getModelTable()); - - // Move mouse to the first cell - const cellRect = getCellRect(editor, 0, 0); - mouseToPoint({ x: cellRect.left, y: cellRect.bottom }, handler); - - // Look for table mover and resizer on the anchor - const anchor = editor.getDocument().getElementsByClassName(ANCHOR_CLASS)[0]; - const mover = anchor?.querySelector('#_Table_Mover'); - const resizer = anchor?.querySelector('#_Table_Resizer'); - expect(!!mover).toBe(false); - expect(!!resizer).toBe(false); - }); -}); diff --git a/packages/roosterjs-content-model-plugins/test/tableEdit/tableEditorTest.ts b/packages/roosterjs-content-model-plugins/test/tableEdit/tableEditorTest.ts new file mode 100644 index 00000000000..6ad1efb7242 --- /dev/null +++ b/packages/roosterjs-content-model-plugins/test/tableEdit/tableEditorTest.ts @@ -0,0 +1,156 @@ +import { DOMEventHandlerFunction, IEditor } from 'roosterjs-content-model-types'; +import { getModelTable } from './tableData'; +import { TABLE_MOVER_ID } from '../../lib/tableEdit/editors/features/TableMover'; +import { TABLE_RESIZER_ID } from '../../lib/tableEdit/editors/features/TableResizer'; +import { TableEditFeatureName } from '../../lib/tableEdit/editors/features/TableEditFeatureName'; +import { TableEditPlugin } from '../../lib/tableEdit/TableEditPlugin'; +import { + HORIZONTAL_INSERTER_ID, + VERTICAL_INSERTER_ID, +} from '../../lib/tableEdit/editors/features/TableInserter'; +import { + afterTableTest, + beforeTableTest, + getCellRect, + initialize, + mouseToPoint, +} from './TableEditTestHelper'; +import { + HORIZONTAL_RESIZER_ID, + VERTICAL_RESIZER_ID, +} from '../../lib/tableEdit/editors/features/CellResizer'; + +describe('TableEdit', () => { + describe('disableFeatures', () => { + const insideTheOffset = 5; + let editor: IEditor; + let plugin: TableEditPlugin; + let handler: Record; + const TEST_ID = 'test'; + + function runDisableFeatureSetup(featuresToDisable: TableEditFeatureName[]) { + // Create editor, plugin, and table + const setup = beforeTableTest(TEST_ID, undefined, featuresToDisable); + editor = setup.editor; + plugin = setup.plugin; + handler = setup.handler; + return initialize(editor, getModelTable()); + } + + it('Disable Horizontal Inserter', () => { + const tableRect = runDisableFeatureSetup(['HorizontalTableInserter']); + // Move mouse to bottom left of table + mouseToPoint({ x: tableRect.left - insideTheOffset, y: tableRect.bottom }, handler); + const feature = editor.getDocument().getElementById(HORIZONTAL_INSERTER_ID); + expect(!!feature).toBe(false); + }); + + it('Disable Vertical Inserter', () => { + const tableRect = runDisableFeatureSetup(['VerticalTableInserter']); + // Move mouse to top right of table + mouseToPoint({ x: tableRect.right, y: tableRect.top - insideTheOffset }, handler); + const feature = editor.getDocument().getElementById(VERTICAL_INSERTER_ID); + expect(!!feature).toBe(false); + }); + + it('Disable Horizontal Resizer', () => { + const tableRect = runDisableFeatureSetup(['CellResizer']); + // Move mouse to bottom right of table + mouseToPoint({ x: tableRect.right - insideTheOffset, y: tableRect.bottom }, handler); + const feature = editor.getDocument().getElementById(HORIZONTAL_RESIZER_ID); + expect(!!feature).toBe(false); + }); + + it('Disable Vertical Resizer', () => { + const tableRect = runDisableFeatureSetup(['CellResizer']); + // Move mouse to bottom right of table + mouseToPoint({ x: tableRect.right, y: tableRect.bottom - insideTheOffset }, handler); + const feature = editor.getDocument().getElementById(VERTICAL_RESIZER_ID); + expect(!!feature).toBe(false); + }); + + it('Disable Table Resizer', () => { + const tableRect = runDisableFeatureSetup(['TableResizer']); + // Move mouse to center of table + mouseToPoint( + { + x: tableRect.left + tableRect.width / 2, + y: tableRect.top + tableRect.height / 2, + }, + handler + ); + const feature = editor.getDocument().getElementById(TABLE_RESIZER_ID); + expect(!!feature).toBe(false); + }); + + it('Disable Table Mover', () => { + const tableRect = runDisableFeatureSetup(['TableMover', 'TableSelector']); + // Move mouse to center of table + mouseToPoint( + { + x: tableRect.left + tableRect.width / 2, + y: tableRect.top + tableRect.height / 2, + }, + handler + ); + const feature = editor.getDocument().getElementById(TABLE_MOVER_ID); + expect(!!feature).toBe(false); + }); + + afterEach(() => { + afterTableTest(editor, plugin, TEST_ID); + }); + }); + + describe('anchorContainer', () => { + let editor: IEditor; + let plugin: TableEditPlugin; + const TEST_ID = 'cellResizerTest'; + const ANCHOR_CLASS = 'anchor_' + TEST_ID; + let handler: Record; + + afterEach(() => { + afterTableTest(editor, plugin, TEST_ID); + }); + + it('Table editor features, resizer and mover, inserted on anchor', () => { + // Create editor, plugin, and table + const setup = beforeTableTest(TEST_ID, ANCHOR_CLASS); + editor = setup.editor; + plugin = setup.plugin; + handler = setup.handler; + initialize(editor, getModelTable()); + + // Move mouse to the first cell + const cellRect = getCellRect(editor, 0, 0); + mouseToPoint({ x: cellRect.left, y: cellRect.bottom }, handler); + + // Look for table mover and resizer on the anchor + const anchor = editor.getDocument().getElementsByClassName(ANCHOR_CLASS)[0]; + const mover = anchor?.querySelector('#' + TABLE_MOVER_ID); + const resizer = anchor?.querySelector('#' + TABLE_RESIZER_ID); + expect(!!mover).toBe(true); + expect(!!resizer).toBe(true); + }); + + it('Table editor features, resizer and mover, not inserted on anchor', () => { + // Create editor, plugin, and table + const setup = beforeTableTest(TEST_ID); + editor = setup.editor; + plugin = setup.plugin; + handler = setup.handler; + initialize(editor, getModelTable()); + + // Move mouse to the first cell + const cellRect = getCellRect(editor, 0, 0); + mouseToPoint({ x: cellRect.left, y: cellRect.bottom }, handler); + + // Look for table mover and resizer on the anchor + const anchor = editor.getDocument().getElementsByClassName(ANCHOR_CLASS)[0]; + const mover = anchor?.querySelector('#' + TABLE_MOVER_ID); + const resizer = anchor?.querySelector('#' + TABLE_RESIZER_ID); + expect(!!mover).toBe(false); + expect(!!resizer).toBe(false); + }); + }); +}); diff --git a/packages/roosterjs-content-model-plugins/test/tableEdit/tableInserterTest.ts b/packages/roosterjs-content-model-plugins/test/tableEdit/tableInserterTest.ts index fb6a2421c0e..48e6c778b47 100644 --- a/packages/roosterjs-content-model-plugins/test/tableEdit/tableInserterTest.ts +++ b/packages/roosterjs-content-model-plugins/test/tableEdit/tableInserterTest.ts @@ -1,8 +1,12 @@ import * as getIntersectedRect from '../../lib/pluginUtils/Rect/getIntersectedRect'; -import { createTableInserter } from '../../lib/tableEdit/editors/features/TableInserter'; import { DOMEventHandlerFunction, IEditor } from 'roosterjs-content-model-types'; import { getMergedFirstColumnTable, getMergedTopRowTable, getModelTable } from './tableData'; import { TableEditPlugin } from '../../lib/tableEdit/TableEditPlugin'; +import { + HORIZONTAL_INSERTER_ID, + VERTICAL_INSERTER_ID, + createTableInserter, +} from '../../lib/tableEdit/editors/features/TableInserter'; import { Position, afterTableTest, @@ -13,9 +17,6 @@ import { initialize, } from './TableEditTestHelper'; -const VERTICAL_INSERTER_ID = 'verticalInserter'; -const HORIZONTAL_INSERTER_ID = 'horizontalInserter'; - describe('Table Inserter tests', () => { let editor: IEditor; let plugin: TableEditPlugin; diff --git a/packages/roosterjs-content-model-plugins/test/tableEdit/tableMoverTest.ts b/packages/roosterjs-content-model-plugins/test/tableEdit/tableMoverTest.ts index f79c7fec44e..8650338381d 100644 --- a/packages/roosterjs-content-model-plugins/test/tableEdit/tableMoverTest.ts +++ b/packages/roosterjs-content-model-plugins/test/tableEdit/tableMoverTest.ts @@ -1,9 +1,16 @@ -import { createTableMover } from '../../lib/tableEdit/editors/features/TableMover'; +import { ContentModelTable, EditorOptions, IEditor } from 'roosterjs-content-model-types'; import { Editor } from 'roosterjs-content-model-core'; -import { EditorOptions, IEditor } from 'roosterjs-content-model-types'; import { OnTableEditorCreatedCallback } from '../../lib/tableEdit/OnTableEditorCreatedCallback'; import { TableEditor } from '../../lib/tableEdit/editors/TableEditor'; import { TableEditPlugin } from '../../lib/tableEdit/TableEditPlugin'; +import { + TableMoverInitValue, + TableMoverContext, + createTableMover, + onDragEnd, + onDragStart, + onDragging, +} from '../../lib/tableEdit/editors/features/TableMover'; describe('Table Mover Tests', () => { let editor: IEditor; @@ -11,133 +18,132 @@ describe('Table Mover Tests', () => { let targetId = 'tableSelectionTestId'; let tableEdit: TableEditPlugin; let node: HTMLDivElement; - - beforeEach(() => { - document.body.innerHTML = ''; - node = document.createElement('div'); - node.id = id; - document.body.insertBefore(node, document.body.childNodes[0]); - tableEdit = new TableEditPlugin(); - - let options: EditorOptions = { - plugins: [tableEdit], - initialModel: { - blockGroupType: 'Document', - blocks: [ + const cmTable: ContentModelTable = { + blockType: 'Table', + rows: [ + { + height: 20, + format: {}, + cells: [ { - blockType: 'Table', - rows: [ + blockGroupType: 'TableCell', + blocks: [ { - height: 20, - format: {}, - cells: [ + blockType: 'Paragraph', + segments: [ { - blockGroupType: 'TableCell', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'a1', - format: {}, - }, - ], - format: {}, - isImplicit: true, - }, - ], + segmentType: 'Text', + text: 'a1', format: {}, - spanLeft: false, - spanAbove: false, - isHeader: false, - dataset: {}, }, + ], + format: {}, + isImplicit: true, + }, + ], + format: {}, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }, + { + blockGroupType: 'TableCell', + blocks: [ + { + blockType: 'Paragraph', + segments: [ { - blockGroupType: 'TableCell', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'z1', - format: {}, - }, - ], - format: {}, - }, - ], + segmentType: 'Text', + text: 'z1', format: {}, - spanLeft: false, - spanAbove: false, - isHeader: false, - dataset: {}, }, ], + format: {}, }, + ], + format: {}, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }, + ], + }, + { + height: 20, + format: {}, + cells: [ + { + blockGroupType: 'TableCell', + blocks: [ { - height: 20, + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'a2', + format: {}, + }, + ], format: {}, - cells: [ + isImplicit: true, + }, + ], + format: {}, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }, + { + blockGroupType: 'TableCell', + blocks: [ + { + blockType: 'Paragraph', + segments: [ { - blockGroupType: 'TableCell', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'a2', - format: {}, - }, - ], - format: {}, - isImplicit: true, - }, - ], + segmentType: 'Text', + text: 'z2', format: {}, - spanLeft: false, - spanAbove: false, - isHeader: false, - dataset: {}, }, { - blockGroupType: 'TableCell', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'z2', - format: {}, - }, - { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, - ], - format: {}, - }, - ], + segmentType: 'SelectionMarker', + isSelected: true, format: {}, - spanLeft: false, - spanAbove: false, - isHeader: false, - dataset: {}, }, ], + format: {}, }, ], - format: { - id: `${targetId}`, - }, - widths: [10, 10], + format: {}, + spanLeft: false, + spanAbove: false, + isHeader: false, dataset: {}, }, ], + }, + ], + format: { + id: `${targetId}`, + }, + widths: [10, 10], + dataset: {}, + }; + + beforeEach(() => { + document.body.innerHTML = ''; + node = document.createElement('div'); + node.id = id; + document.body.insertBefore(node, document.body.childNodes[0]); + tableEdit = new TableEditPlugin(); + + let options: EditorOptions = { + plugins: [tableEdit], + initialModel: { + blockGroupType: 'Document', + blocks: [{ ...cmTable }], format: {}, }, }; @@ -249,6 +255,517 @@ describe('Table Mover Tests', () => { } }); + it('Move - onDragStart', () => { + //Arrange + node.style.height = '10px'; + node.style.overflowX = 'auto'; + node.scrollTop = 0; + const target = document.getElementById(targetId); + editor.focus(); + + const divParent = document.createElement('div'); + const divChild = document.createElement('div'); + divParent.appendChild(divChild); + const parentSpy = spyOn(divParent, 'appendChild'); + + const onStartSpy = jasmine.createSpy('onStart'); + const context: TableMoverContext = { + table: target as HTMLTableElement, + zoomScale: 0, + rect: null, + isRTL: false, + editor: editor, + div: divChild, + onFinishDragging: () => {}, + onStart: onStartSpy, + onEnd: () => {}, + }; + const initialSelection = editor.getDOMSelection(); + + //Act + const initvalue = onDragStart(context); + + //Assert + expect(parentSpy).toHaveBeenCalled(); + expect(onStartSpy).toHaveBeenCalled(); + + expect(initvalue.cmTable).toBeDefined(); + expect(initvalue.initialSelection).toEqual(initialSelection); + expect(initvalue.tableRect).toBeDefined(); + }); + + it('Move - onDragging', () => { + //Arrange + const nodeHeight = 100; + const nodeWidth = 100; + node.style.height = `${nodeHeight}px`; + node.style.width = `${nodeWidth}px`; + node.style.overflowX = 'auto'; + node.scrollTop = 0; + const target = document.getElementById(targetId); + editor.focus(); + + const div = document.createElement('div'); + + const context: TableMoverContext = { + table: target as HTMLTableElement, + zoomScale: 0, + rect: null, + isRTL: false, + editor: editor, + div: div, + onFinishDragging: () => {}, + onStart: () => {}, + onEnd: () => {}, + }; + const outsiderDiv = document.createElement('div'); + outsiderDiv.style.height = `${nodeHeight}px`; + outsiderDiv.style.width = `${nodeWidth}px`; + node.parentElement?.appendChild(outsiderDiv); + + const divRect = document.createElement('div'); + divRect.style.position = 'fixed'; + divRect.style.top = '0px'; + divRect.style.left = '0px'; + + const initValue: TableMoverInitValue = { + cmTable: undefined, + initialSelection: null, + tableRect: divRect, + }; + + //Act + const outsideDivRect = outsiderDiv.getBoundingClientRect(); + const draggingOutsideEditor = onDragging( + context, + { + clientX: outsideDivRect.right - 10, + clientY: outsideDivRect.bottom - 10, + } as MouseEvent, + initValue + ); + const targetRect = target?.getBoundingClientRect(); + const draggingInsideEditor = onDragging( + context, + { + clientX: targetRect?.height ?? 0 / 2, + clientY: targetRect?.width ?? 0 / 2, + } as MouseEvent, + initValue + ); + node.parentElement?.removeChild(outsiderDiv); + + //Assert + expect(draggingOutsideEditor).toBe(false); + expect(draggingInsideEditor).toBe(true); + expect(parseFloat(divRect.style.top)).toBeGreaterThan(0); + expect(parseFloat(divRect.style.left)).toBeGreaterThan(0); + }); + + describe('Move - onDragEnd', () => { + let target: HTMLTableElement; + const nodeHeight = 300; + + beforeEach(() => { + //Arrange + node.style.height = `${nodeHeight}px`; + node.style.overflowX = 'auto'; + node.scrollTop = 0; + target = document.getElementById(targetId) as HTMLTableElement; + editor.focus(); + }); + + it('remove tableRect', () => { + const div = document.createElement('div'); + const context: TableMoverContext = { + table: target, + zoomScale: 0, + rect: null, + isRTL: false, + editor: editor, + div: div, + onFinishDragging: () => {}, + onStart: () => {}, + onEnd: () => {}, + }; + const divRect = document.createElement('div'); + divRect.id = 'testRect'; + document.body.appendChild(divRect); + + const initValue: TableMoverInitValue = { + cmTable: undefined, + initialSelection: null, + tableRect: divRect, + }; + + //Act + onDragEnd(context, { clientX: 10, clientY: 10 } as MouseEvent, initValue); + + //Assert + expect(document.getElementById('testRect')).toBeNull(); + }); + + it('onFinishDragging', () => { + const div = document.createElement('div'); + const onEndSpy = jasmine.createSpy('onEnd'); + const onFinishDraggingSpy = jasmine.createSpy('onFinishDragging'); + const context: TableMoverContext = { + table: target, + zoomScale: 0, + rect: null, + isRTL: false, + editor: editor, + div: div, + onFinishDragging: onFinishDraggingSpy, + onStart: () => {}, + onEnd: onEndSpy, + }; + const divRect = document.createElement('div'); + const initValue: TableMoverInitValue = { + cmTable: undefined, + initialSelection: null, + tableRect: divRect, + }; + + const event = new MouseEvent('mouseup', { clientX: 10, clientY: 10, bubbles: false }); + spyOnProperty(event, 'target').and.returnValue(div); + + //Act + onDragEnd(context, event, initValue); + + //Assert + expect(onFinishDraggingSpy).toHaveBeenCalled(); + expect(onEndSpy).toHaveBeenCalled(); + }); + + it('Drop table on itself', () => { + const div = document.createElement('div'); + const onEndSpy = jasmine.createSpy('onEnd'); + const onFinishDraggingSpy = jasmine.createSpy('onFinishDragging'); + const context: TableMoverContext = { + table: target, + zoomScale: 0, + rect: null, + isRTL: false, + editor: editor, + div: div, + onFinishDragging: onFinishDraggingSpy, + onStart: () => {}, + onEnd: onEndSpy, + }; + const divRect = document.createElement('div'); + + const initValue: TableMoverInitValue = { + cmTable: undefined, + initialSelection: null, + tableRect: divRect, + }; + + const firstTableChild = target.rows[0].cells[0].firstChild; + const event = new MouseEvent('mouseup', { clientX: 10, clientY: 10, bubbles: false }); + spyOnProperty(event, 'target').and.returnValue(firstTableChild); + + //Act + const dropResult = onDragEnd(context, event, initValue); + + //Assert + expect(dropResult).toBe(false); + expect(onFinishDraggingSpy).not.toHaveBeenCalled(); + expect(onEndSpy).toHaveBeenCalled(); + }); + + it('Drop table outside editor', () => { + const div = document.createElement('div'); + const onEndSpy = jasmine.createSpy('onEnd'); + const onFinishDraggingSpy = jasmine.createSpy('onFinishDragging'); + const context: TableMoverContext = { + table: target, + zoomScale: 0, + rect: null, + isRTL: false, + editor: editor, + div: div, + onFinishDragging: onFinishDraggingSpy, + onStart: () => {}, + onEnd: onEndSpy, + }; + const divRect = document.createElement('div'); + + const initValue: TableMoverInitValue = { + cmTable: undefined, + initialSelection: null, + tableRect: divRect, + }; + + const outsideDiv = document.createElement('div'); + document.body.appendChild(outsideDiv); + const event = new MouseEvent('mouseup', { clientX: 10, clientY: 10, bubbles: false }); + spyOnProperty(event, 'target').and.returnValue(outsideDiv); + + //Act + const dropResult = onDragEnd(context, event, initValue); + + //Assert + expect(dropResult).toBe(false); + expect(onFinishDraggingSpy).not.toHaveBeenCalled(); + expect(onEndSpy).toHaveBeenCalled(); + }); + + it('Drop table inside editor between two texts', () => { + const div = document.createElement('div'); + const onEndSpy = jasmine.createSpy('onEnd'); + const onFinishDraggingSpy = jasmine.createSpy('onFinishDragging'); + const context: TableMoverContext = { + table: target, + zoomScale: 0, + rect: null, + isRTL: false, + editor: editor, + div: div, + onFinishDragging: onFinishDraggingSpy, + onStart: () => {}, + onEnd: onEndSpy, + }; + const divRect = document.createElement('div'); + + const initValue: TableMoverInitValue = { + cmTable: cmTable, + initialSelection: null, + tableRect: divRect, + }; + + editor.formatContentModel(model => { + model.blocks.push( + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'TEXT-A', + format: {}, + }, + ], + format: {}, + segmentFormat: {}, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'TEXT-B', + format: {}, + }, + ], + format: {}, + segmentFormat: {}, + } + ); + + return true; + }); + + const divs = Array.from(node.getElementsByTagName('div')); + let textB: HTMLElement | null = null; + for (const div of divs) { + if (div.textContent == 'TEXT-B') { + textB = div; + break; + } + } + + if (textB == null) { + fail('no text found'); + return false; + } + + const textBRect = textB.getBoundingClientRect(); + const event = new MouseEvent('mouseup', { + clientX: textBRect.left, + clientY: textBRect.top + 2, + bubbles: false, + }); + spyOnProperty(event, 'target').and.returnValue(textB); + + //Act + const dropResult = onDragEnd(context, event, initValue); + + //Assert + const finalModel = editor.getContentModelCopy('disconnected'); + + expect(finalModel.blocks[0]?.blockType).toEqual('Paragraph'); + expect(finalModel.blocks[1]?.blockType).toEqual('Table'); + expect(finalModel.blocks[2]?.blockType).toEqual('Paragraph'); + expect(dropResult).toBe(true); + expect(onFinishDraggingSpy).not.toHaveBeenCalled(); + expect(onEndSpy).toHaveBeenCalled(); + }); + + it('Drop table inside editor last br', () => { + const div = document.createElement('div'); + const onEndSpy = jasmine.createSpy('onEnd'); + const onFinishDraggingSpy = jasmine.createSpy('onFinishDragging'); + const context: TableMoverContext = { + table: target, + zoomScale: 0, + rect: null, + isRTL: false, + editor: editor, + div: div, + onFinishDragging: onFinishDraggingSpy, + onStart: () => {}, + onEnd: onEndSpy, + }; + const divRect = document.createElement('div'); + + const initValue: TableMoverInitValue = { + cmTable: cmTable, + initialSelection: null, + tableRect: divRect, + }; + + editor.formatContentModel(model => { + model.blocks.push( + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'TEXT', + format: {}, + }, + ], + format: {}, + segmentFormat: {}, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Br', + format: {}, + }, + ], + format: {}, + segmentFormat: {}, + } + ); + + return true; + }); + + const brElement = node.getElementsByTagName('br')[0]; + if (brElement == null) { + fail('no br element'); + return false; + } + const brElementRect = brElement.getBoundingClientRect(); + + const event = new MouseEvent('mouseup', { + clientX: brElementRect.left + 1, + clientY: brElementRect.top + 1, + bubbles: false, + }); + spyOnProperty(event, 'target').and.returnValue(brElement); + + //Act + const dropResult = onDragEnd(context, event, initValue); + + //Assert + const finalModel = editor.getContentModelCopy('disconnected'); + expect(finalModel.blocks[0]?.blockType).toEqual('Paragraph'); + expect(finalModel.blocks[1]?.blockType).toEqual('Table'); + finalModel.blocks[2].blockType == 'Paragraph' + ? expect(finalModel.blocks[2]?.segments[0]?.segmentType).toEqual('Br') + : fail('Last block is not paragraph'); + expect(dropResult).toBe(true); + expect(onFinishDraggingSpy).not.toHaveBeenCalled(); + expect(onEndSpy).toHaveBeenCalled(); + }); + + it('Drop table inside editor below last br', () => { + const div = document.createElement('div'); + const onEndSpy = jasmine.createSpy('onEnd'); + const onFinishDraggingSpy = jasmine.createSpy('onFinishDragging'); + const context: TableMoverContext = { + table: target, + zoomScale: 0, + rect: null, + isRTL: false, + editor: editor, + div: div, + onFinishDragging: onFinishDraggingSpy, + onStart: () => {}, + onEnd: onEndSpy, + }; + const divRect = document.createElement('div'); + + const initValue: TableMoverInitValue = { + cmTable: cmTable, + initialSelection: null, + tableRect: divRect, + }; + + editor.formatContentModel(model => { + model.blocks.push( + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'TEXT', + format: {}, + }, + ], + format: {}, + segmentFormat: {}, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Br', + format: {}, + }, + ], + format: {}, + segmentFormat: {}, + } + ); + + return true; + }); + + const brElement = node.getElementsByTagName('br')[0]; + if (brElement == null) { + fail('no br element'); + return false; + } + const brElementRect = brElement.getBoundingClientRect(); + + const event = new MouseEvent('mouseup', { + clientX: brElementRect.left + 1, + clientY: brElementRect.bottom + 5, + bubbles: false, + }); + spyOnProperty(event, 'target').and.returnValue(node); + + //Act + const dropResult = onDragEnd(context, event, initValue); + + //Assert + const finalModel = editor.getContentModelCopy('disconnected'); + expect(finalModel.blocks[0]?.blockType).toEqual('Paragraph'); + expect(finalModel.blocks[1]?.blockType).toEqual('Table'); + finalModel.blocks[2].blockType == 'Paragraph' + ? expect(finalModel.blocks[2]?.segments[0]?.segmentType).toEqual('Br') + : fail('Last block is not paragraph'); + expect(dropResult).toBe(true); + expect(onFinishDraggingSpy).not.toHaveBeenCalled(); + expect(onEndSpy).toHaveBeenCalled(); + }); + }); + function runTest( scrollTop: number, isNotNull: boolean | null, @@ -267,6 +784,8 @@ describe('Table Mover Tests', () => { editor, false, () => {}, + () => {}, + () => false, node, undefined, onTableEditorCreatedCallback diff --git a/packages/roosterjs-content-model-plugins/test/watermark/isModelEmptyFastTest.ts b/packages/roosterjs-content-model-plugins/test/watermark/isModelEmptyFastTest.ts index dac437b0c1a..0549fb805ae 100644 --- a/packages/roosterjs-content-model-plugins/test/watermark/isModelEmptyFastTest.ts +++ b/packages/roosterjs-content-model-plugins/test/watermark/isModelEmptyFastTest.ts @@ -99,6 +99,32 @@ describe('isModelEmptyFast', () => { expect(result).toBeTrue(); }); + it('Single Paragraph block - right margin 10px', () => { + const model = createContentModelDocument(); + const para = createParagraph(); + para.format.marginRight = '10px'; + + para.segments.push(createBr()); + model.blocks.push(para); + + const result = isModelEmptyFast(model); + + expect(result).toBeFalse(); + }); + + it('Single Paragraph block - left margin 0.25rem', () => { + const model = createContentModelDocument(); + const para = createParagraph(); + para.format.marginLeft = '0.25rem'; + + para.segments.push(createBr()); + model.blocks.push(para); + + const result = isModelEmptyFast(model); + + expect(result).toBeFalse(); + }); + it('Single Paragraph block - two BR segment', () => { const model = createContentModelDocument(); const para = createParagraph(); diff --git a/packages/roosterjs-content-model-types/lib/enum/BlockType.ts b/packages/roosterjs-content-model-types/lib/contentModel/block/BlockType.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/enum/BlockType.ts rename to packages/roosterjs-content-model-types/lib/contentModel/block/BlockType.ts diff --git a/packages/roosterjs-content-model-types/lib/block/ContentModelBlock.ts b/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelBlock.ts similarity index 67% rename from packages/roosterjs-content-model-types/lib/block/ContentModelBlock.ts rename to packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelBlock.ts index 0224a7c15c3..59179bdd73c 100644 --- a/packages/roosterjs-content-model-types/lib/block/ContentModelBlock.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelBlock.ts @@ -1,8 +1,8 @@ import type { ContentModelDivider } from './ContentModelDivider'; import type { ContentModelEntity } from '../entity/ContentModelEntity'; -import type { ContentModelFormatContainer } from '../group/ContentModelFormatContainer'; -import type { ContentModelGeneralBlock } from '../group/ContentModelGeneralBlock'; -import type { ContentModelListItem } from '../group/ContentModelListItem'; +import type { ContentModelFormatContainer } from '../blockGroup/ContentModelFormatContainer'; +import type { ContentModelGeneralBlock } from '../blockGroup/ContentModelGeneralBlock'; +import type { ContentModelListItem } from '../blockGroup/ContentModelListItem'; import type { ContentModelParagraph } from './ContentModelParagraph'; import type { ContentModelTable } from './ContentModelTable'; diff --git a/packages/roosterjs-content-model-types/lib/block/ContentModelBlockBase.ts b/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelBlockBase.ts similarity index 87% rename from packages/roosterjs-content-model-types/lib/block/ContentModelBlockBase.ts rename to packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelBlockBase.ts index 352f8f06353..8e05b45151f 100644 --- a/packages/roosterjs-content-model-types/lib/block/ContentModelBlockBase.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelBlockBase.ts @@ -1,5 +1,5 @@ import type { ContentModelBlockFormat } from '../format/ContentModelBlockFormat'; -import type { ContentModelBlockType } from '../enum/BlockType'; +import type { ContentModelBlockType } from './BlockType'; import type { ContentModelWithFormat } from '../format/ContentModelWithFormat'; /** diff --git a/packages/roosterjs-content-model-types/lib/block/ContentModelDivider.ts b/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelDivider.ts similarity index 78% rename from packages/roosterjs-content-model-types/lib/block/ContentModelDivider.ts rename to packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelDivider.ts index ba7742b3d7f..23fe4386273 100644 --- a/packages/roosterjs-content-model-types/lib/block/ContentModelDivider.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelDivider.ts @@ -1,7 +1,7 @@ import type { ContentModelBlockBase } from './ContentModelBlockBase'; -import type { ContentModelBlockWithCache } from './ContentModelBlockWithCache'; +import type { ContentModelBlockWithCache } from '../common/ContentModelBlockWithCache'; import type { ContentModelDividerFormat } from '../format/ContentModelDividerFormat'; -import type { Selectable } from '../selection/Selectable'; +import type { Selectable } from '../common/Selectable'; /** * Content Model of horizontal divider diff --git a/packages/roosterjs-content-model-types/lib/block/ContentModelParagraph.ts b/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelParagraph.ts similarity index 92% rename from packages/roosterjs-content-model-types/lib/block/ContentModelParagraph.ts rename to packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelParagraph.ts index 042dbcb140c..cb47999a4a4 100644 --- a/packages/roosterjs-content-model-types/lib/block/ContentModelParagraph.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelParagraph.ts @@ -1,5 +1,5 @@ import type { ContentModelBlockBase } from './ContentModelBlockBase'; -import type { ContentModelBlockWithCache } from './ContentModelBlockWithCache'; +import type { ContentModelBlockWithCache } from '../common/ContentModelBlockWithCache'; import type { ContentModelParagraphDecorator } from '../decorator/ContentModelParagraphDecorator'; import type { ContentModelSegment } from '../segment/ContentModelSegment'; import type { ContentModelSegmentFormat } from '../format/ContentModelSegmentFormat'; diff --git a/packages/roosterjs-content-model-types/lib/block/ContentModelTable.ts b/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelTable.ts similarity index 89% rename from packages/roosterjs-content-model-types/lib/block/ContentModelTable.ts rename to packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelTable.ts index 568f6f7de48..c9c70ebe6c4 100644 --- a/packages/roosterjs-content-model-types/lib/block/ContentModelTable.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelTable.ts @@ -1,5 +1,5 @@ import type { ContentModelBlockBase } from './ContentModelBlockBase'; -import type { ContentModelBlockWithCache } from './ContentModelBlockWithCache'; +import type { ContentModelBlockWithCache } from '../common/ContentModelBlockWithCache'; import type { ContentModelTableFormat } from '../format/ContentModelTableFormat'; import type { ContentModelTableRow } from './ContentModelTableRow'; import type { ContentModelWithDataset } from '../format/ContentModelWithDataset'; diff --git a/packages/roosterjs-content-model-types/lib/block/ContentModelTableRow.ts b/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelTableRow.ts similarity index 74% rename from packages/roosterjs-content-model-types/lib/block/ContentModelTableRow.ts rename to packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelTableRow.ts index e9dc6e4f188..7a5327feb01 100644 --- a/packages/roosterjs-content-model-types/lib/block/ContentModelTableRow.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelTableRow.ts @@ -1,6 +1,6 @@ import type { ContentModelBlockFormat } from '../format/ContentModelBlockFormat'; -import type { ContentModelBlockWithCache } from './ContentModelBlockWithCache'; -import type { ContentModelTableCell } from '../group/ContentModelTableCell'; +import type { ContentModelBlockWithCache } from '../common/ContentModelBlockWithCache'; +import type { ContentModelTableCell } from '../blockGroup/ContentModelTableCell'; import type { ContentModelWithFormat } from '../format/ContentModelWithFormat'; /** diff --git a/packages/roosterjs-content-model-types/lib/enum/BlockGroupType.ts b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/BlockGroupType.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/enum/BlockGroupType.ts rename to packages/roosterjs-content-model-types/lib/contentModel/blockGroup/BlockGroupType.ts diff --git a/packages/roosterjs-content-model-types/lib/group/ContentModelBlockGroup.ts b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelBlockGroup.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/group/ContentModelBlockGroup.ts rename to packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelBlockGroup.ts diff --git a/packages/roosterjs-content-model-types/lib/group/ContentModelBlockGroupBase.ts b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelBlockGroupBase.ts similarity index 82% rename from packages/roosterjs-content-model-types/lib/group/ContentModelBlockGroupBase.ts rename to packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelBlockGroupBase.ts index afb1a07f30e..648f9b1bd70 100644 --- a/packages/roosterjs-content-model-types/lib/group/ContentModelBlockGroupBase.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelBlockGroupBase.ts @@ -1,5 +1,5 @@ import type { ContentModelBlock } from '../block/ContentModelBlock'; -import type { ContentModelBlockGroupType } from '../enum/BlockGroupType'; +import type { ContentModelBlockGroupType } from './BlockGroupType'; /** * Base type of Content Model Block Group diff --git a/packages/roosterjs-content-model-types/lib/group/ContentModelDocument.ts b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelDocument.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/group/ContentModelDocument.ts rename to packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelDocument.ts diff --git a/packages/roosterjs-content-model-types/lib/group/ContentModelFormatContainer.ts b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelFormatContainer.ts similarity index 90% rename from packages/roosterjs-content-model-types/lib/group/ContentModelFormatContainer.ts rename to packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelFormatContainer.ts index 2f0f987bb40..5abe704bdea 100644 --- a/packages/roosterjs-content-model-types/lib/group/ContentModelFormatContainer.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelFormatContainer.ts @@ -1,6 +1,6 @@ import type { ContentModelBlockBase } from '../block/ContentModelBlockBase'; import type { ContentModelBlockGroupBase } from './ContentModelBlockGroupBase'; -import type { ContentModelBlockWithCache } from '../block/ContentModelBlockWithCache'; +import type { ContentModelBlockWithCache } from '../common/ContentModelBlockWithCache'; import type { ContentModelFormatContainerFormat } from '../format/ContentModelFormatContainerFormat'; /** diff --git a/packages/roosterjs-content-model-types/lib/group/ContentModelGeneralBlock.ts b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelGeneralBlock.ts similarity index 92% rename from packages/roosterjs-content-model-types/lib/group/ContentModelGeneralBlock.ts rename to packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelGeneralBlock.ts index 800cce2481f..ff3899baa4f 100644 --- a/packages/roosterjs-content-model-types/lib/group/ContentModelGeneralBlock.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelGeneralBlock.ts @@ -2,7 +2,7 @@ import type { ContentModelBlockBase } from '../block/ContentModelBlockBase'; import type { ContentModelBlockFormat } from '../format/ContentModelBlockFormat'; import type { ContentModelBlockGroupBase } from './ContentModelBlockGroupBase'; import type { ContentModelSegmentFormat } from '../format/ContentModelSegmentFormat'; -import type { Selectable } from '../selection/Selectable'; +import type { Selectable } from '../common/Selectable'; /** * Content Model for general Block element diff --git a/packages/roosterjs-content-model-types/lib/group/ContentModelListItem.ts b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelListItem.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/group/ContentModelListItem.ts rename to packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelListItem.ts diff --git a/packages/roosterjs-content-model-types/lib/group/ContentModelTableCell.ts b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelTableCell.ts similarity index 87% rename from packages/roosterjs-content-model-types/lib/group/ContentModelTableCell.ts rename to packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelTableCell.ts index 19d6d9b6d91..4e51590c48b 100644 --- a/packages/roosterjs-content-model-types/lib/group/ContentModelTableCell.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelTableCell.ts @@ -1,10 +1,10 @@ import type { TableCellMetadataFormat } from '../format/metadata/TableCellMetadataFormat'; import type { ContentModelBlockGroupBase } from './ContentModelBlockGroupBase'; -import type { ContentModelBlockWithCache } from '../block/ContentModelBlockWithCache'; +import type { ContentModelBlockWithCache } from '../common/ContentModelBlockWithCache'; import type { ContentModelTableCellFormat } from '../format/ContentModelTableCellFormat'; import type { ContentModelWithDataset } from '../format/ContentModelWithDataset'; import type { ContentModelWithFormat } from '../format/ContentModelWithFormat'; -import type { Selectable } from '../selection/Selectable'; +import type { Selectable } from '../common/Selectable'; /** * Content Model of Table Cell diff --git a/packages/roosterjs-content-model-types/lib/block/ContentModelBlockWithCache.ts b/packages/roosterjs-content-model-types/lib/contentModel/common/ContentModelBlockWithCache.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/block/ContentModelBlockWithCache.ts rename to packages/roosterjs-content-model-types/lib/contentModel/common/ContentModelBlockWithCache.ts diff --git a/packages/roosterjs-content-model-types/lib/selection/Selectable.ts b/packages/roosterjs-content-model-types/lib/contentModel/common/Selectable.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/selection/Selectable.ts rename to packages/roosterjs-content-model-types/lib/contentModel/common/Selectable.ts diff --git a/packages/roosterjs-content-model-types/lib/decorator/ContentModelCode.ts b/packages/roosterjs-content-model-types/lib/contentModel/decorator/ContentModelCode.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/decorator/ContentModelCode.ts rename to packages/roosterjs-content-model-types/lib/contentModel/decorator/ContentModelCode.ts diff --git a/packages/roosterjs-content-model-types/lib/decorator/ContentModelDecorator.ts b/packages/roosterjs-content-model-types/lib/contentModel/decorator/ContentModelDecorator.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/decorator/ContentModelDecorator.ts rename to packages/roosterjs-content-model-types/lib/contentModel/decorator/ContentModelDecorator.ts diff --git a/packages/roosterjs-content-model-types/lib/decorator/ContentModelLink.ts b/packages/roosterjs-content-model-types/lib/contentModel/decorator/ContentModelLink.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/decorator/ContentModelLink.ts rename to packages/roosterjs-content-model-types/lib/contentModel/decorator/ContentModelLink.ts diff --git a/packages/roosterjs-content-model-types/lib/decorator/ContentModelListLevel.ts b/packages/roosterjs-content-model-types/lib/contentModel/decorator/ContentModelListLevel.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/decorator/ContentModelListLevel.ts rename to packages/roosterjs-content-model-types/lib/contentModel/decorator/ContentModelListLevel.ts diff --git a/packages/roosterjs-content-model-types/lib/decorator/ContentModelParagraphDecorator.ts b/packages/roosterjs-content-model-types/lib/contentModel/decorator/ContentModelParagraphDecorator.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/decorator/ContentModelParagraphDecorator.ts rename to packages/roosterjs-content-model-types/lib/contentModel/decorator/ContentModelParagraphDecorator.ts diff --git a/packages/roosterjs-content-model-types/lib/entity/ContentModelEntity.ts b/packages/roosterjs-content-model-types/lib/contentModel/entity/ContentModelEntity.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/entity/ContentModelEntity.ts rename to packages/roosterjs-content-model-types/lib/contentModel/entity/ContentModelEntity.ts diff --git a/packages/roosterjs-content-model-types/lib/format/ContentModelBlockFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelBlockFormat.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/format/ContentModelBlockFormat.ts rename to packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelBlockFormat.ts diff --git a/packages/roosterjs-content-model-types/lib/format/ContentModelCodeFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelCodeFormat.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/format/ContentModelCodeFormat.ts rename to packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelCodeFormat.ts diff --git a/packages/roosterjs-content-model-types/lib/format/ContentModelDividerFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelDividerFormat.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/format/ContentModelDividerFormat.ts rename to packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelDividerFormat.ts diff --git a/packages/roosterjs-content-model-types/lib/format/ContentModelEntityFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelEntityFormat.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/format/ContentModelEntityFormat.ts rename to packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelEntityFormat.ts diff --git a/packages/roosterjs-content-model-types/lib/format/ContentModelFormatBase.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelFormatBase.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/format/ContentModelFormatBase.ts rename to packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelFormatBase.ts diff --git a/packages/roosterjs-content-model-types/lib/format/ContentModelFormatContainerFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelFormatContainerFormat.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/format/ContentModelFormatContainerFormat.ts rename to packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelFormatContainerFormat.ts diff --git a/packages/roosterjs-content-model-types/lib/format/ContentModelFormatMap.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelFormatMap.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/format/ContentModelFormatMap.ts rename to packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelFormatMap.ts diff --git a/packages/roosterjs-content-model-types/lib/format/ContentModelHyperLinkFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelHyperLinkFormat.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/format/ContentModelHyperLinkFormat.ts rename to packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelHyperLinkFormat.ts diff --git a/packages/roosterjs-content-model-types/lib/format/ContentModelImageFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelImageFormat.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/format/ContentModelImageFormat.ts rename to packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelImageFormat.ts diff --git a/packages/roosterjs-content-model-types/lib/format/ContentModelListItemFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelListItemFormat.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/format/ContentModelListItemFormat.ts rename to packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelListItemFormat.ts diff --git a/packages/roosterjs-content-model-types/lib/format/ContentModelListItemLevelFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelListItemLevelFormat.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/format/ContentModelListItemLevelFormat.ts rename to packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelListItemLevelFormat.ts diff --git a/packages/roosterjs-content-model-types/lib/format/ContentModelSegmentFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelSegmentFormat.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/format/ContentModelSegmentFormat.ts rename to packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelSegmentFormat.ts diff --git a/packages/roosterjs-content-model-types/lib/format/ContentModelTableCellFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelTableCellFormat.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/format/ContentModelTableCellFormat.ts rename to packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelTableCellFormat.ts diff --git a/packages/roosterjs-content-model-types/lib/format/ContentModelTableFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelTableFormat.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/format/ContentModelTableFormat.ts rename to packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelTableFormat.ts diff --git a/packages/roosterjs-content-model-types/lib/format/ContentModelWithDataset.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelWithDataset.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/format/ContentModelWithDataset.ts rename to packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelWithDataset.ts diff --git a/packages/roosterjs-content-model-types/lib/format/ContentModelWithFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelWithFormat.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/format/ContentModelWithFormat.ts rename to packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelWithFormat.ts diff --git a/packages/roosterjs-content-model-types/lib/format/FormatHandlerTypeMap.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/FormatHandlerTypeMap.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/format/FormatHandlerTypeMap.ts rename to packages/roosterjs-content-model-types/lib/contentModel/format/FormatHandlerTypeMap.ts diff --git a/packages/roosterjs-content-model-types/lib/format/formatParts/BackgroundColorFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/BackgroundColorFormat.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/format/formatParts/BackgroundColorFormat.ts rename to packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/BackgroundColorFormat.ts diff --git a/packages/roosterjs-content-model-types/lib/format/formatParts/BoldFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/BoldFormat.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/format/formatParts/BoldFormat.ts rename to packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/BoldFormat.ts diff --git a/packages/roosterjs-content-model-types/lib/format/formatParts/BorderBoxFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/BorderBoxFormat.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/format/formatParts/BorderBoxFormat.ts rename to packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/BorderBoxFormat.ts diff --git a/packages/roosterjs-content-model-types/lib/format/formatParts/BorderFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/BorderFormat.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/format/formatParts/BorderFormat.ts rename to packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/BorderFormat.ts diff --git a/packages/roosterjs-content-model-types/lib/format/formatParts/BoxShadowFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/BoxShadowFormat.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/format/formatParts/BoxShadowFormat.ts rename to packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/BoxShadowFormat.ts diff --git a/packages/roosterjs-content-model-types/lib/format/formatParts/DirectionFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/DirectionFormat.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/format/formatParts/DirectionFormat.ts rename to packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/DirectionFormat.ts diff --git a/packages/roosterjs-content-model-types/lib/format/formatParts/DisplayFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/DisplayFormat.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/format/formatParts/DisplayFormat.ts rename to packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/DisplayFormat.ts diff --git a/packages/roosterjs-content-model-types/lib/format/formatParts/EntityInfoFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/EntityInfoFormat.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/format/formatParts/EntityInfoFormat.ts rename to packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/EntityInfoFormat.ts diff --git a/packages/roosterjs-content-model-types/lib/format/formatParts/FloatFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/FloatFormat.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/format/formatParts/FloatFormat.ts rename to packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/FloatFormat.ts diff --git a/packages/roosterjs-content-model-types/lib/format/formatParts/FontFamilyFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/FontFamilyFormat.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/format/formatParts/FontFamilyFormat.ts rename to packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/FontFamilyFormat.ts diff --git a/packages/roosterjs-content-model-types/lib/format/formatParts/FontSizeFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/FontSizeFormat.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/format/formatParts/FontSizeFormat.ts rename to packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/FontSizeFormat.ts diff --git a/packages/roosterjs-content-model-types/lib/format/formatParts/HtmlAlignFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/HtmlAlignFormat.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/format/formatParts/HtmlAlignFormat.ts rename to packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/HtmlAlignFormat.ts diff --git a/packages/roosterjs-content-model-types/lib/format/formatParts/IdFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/IdFormat.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/format/formatParts/IdFormat.ts rename to packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/IdFormat.ts diff --git a/packages/roosterjs-content-model-types/lib/format/formatParts/ItalicFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/ItalicFormat.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/format/formatParts/ItalicFormat.ts rename to packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/ItalicFormat.ts diff --git a/packages/roosterjs-content-model-types/lib/format/formatParts/LetterSpacingFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/LetterSpacingFormat.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/format/formatParts/LetterSpacingFormat.ts rename to packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/LetterSpacingFormat.ts diff --git a/packages/roosterjs-content-model-types/lib/format/formatParts/LineHeightFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/LineHeightFormat.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/format/formatParts/LineHeightFormat.ts rename to packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/LineHeightFormat.ts diff --git a/packages/roosterjs-content-model-types/lib/format/formatParts/LinkFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/LinkFormat.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/format/formatParts/LinkFormat.ts rename to packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/LinkFormat.ts diff --git a/packages/roosterjs-content-model-types/lib/format/formatParts/ListStyleFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/ListStyleFormat.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/format/formatParts/ListStyleFormat.ts rename to packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/ListStyleFormat.ts diff --git a/packages/roosterjs-content-model-types/lib/format/formatParts/ListThreadFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/ListThreadFormat.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/format/formatParts/ListThreadFormat.ts rename to packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/ListThreadFormat.ts diff --git a/packages/roosterjs-content-model-types/lib/format/formatParts/MarginFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/MarginFormat.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/format/formatParts/MarginFormat.ts rename to packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/MarginFormat.ts diff --git a/packages/roosterjs-content-model-types/lib/format/formatParts/PaddingFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/PaddingFormat.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/format/formatParts/PaddingFormat.ts rename to packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/PaddingFormat.ts diff --git a/packages/roosterjs-content-model-types/lib/format/formatParts/SizeFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/SizeFormat.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/format/formatParts/SizeFormat.ts rename to packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/SizeFormat.ts diff --git a/packages/roosterjs-content-model-types/lib/format/formatParts/SpacingFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/SpacingFormat.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/format/formatParts/SpacingFormat.ts rename to packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/SpacingFormat.ts diff --git a/packages/roosterjs-content-model-types/lib/format/formatParts/StrikeFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/StrikeFormat.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/format/formatParts/StrikeFormat.ts rename to packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/StrikeFormat.ts diff --git a/packages/roosterjs-content-model-types/lib/format/formatParts/SuperOrSubScriptFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/SuperOrSubScriptFormat.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/format/formatParts/SuperOrSubScriptFormat.ts rename to packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/SuperOrSubScriptFormat.ts diff --git a/packages/roosterjs-content-model-types/lib/format/formatParts/TableLayoutFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/TableLayoutFormat.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/format/formatParts/TableLayoutFormat.ts rename to packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/TableLayoutFormat.ts diff --git a/packages/roosterjs-content-model-types/lib/format/formatParts/TextAlignFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/TextAlignFormat.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/format/formatParts/TextAlignFormat.ts rename to packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/TextAlignFormat.ts diff --git a/packages/roosterjs-content-model-types/lib/format/formatParts/TextColorFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/TextColorFormat.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/format/formatParts/TextColorFormat.ts rename to packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/TextColorFormat.ts diff --git a/packages/roosterjs-content-model-types/lib/format/formatParts/TextIndentFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/TextIndentFormat.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/format/formatParts/TextIndentFormat.ts rename to packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/TextIndentFormat.ts diff --git a/packages/roosterjs-content-model-types/lib/format/formatParts/UnderlineFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/UnderlineFormat.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/format/formatParts/UnderlineFormat.ts rename to packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/UnderlineFormat.ts diff --git a/packages/roosterjs-content-model-types/lib/format/formatParts/VerticalAlignFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/VerticalAlignFormat.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/format/formatParts/VerticalAlignFormat.ts rename to packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/VerticalAlignFormat.ts diff --git a/packages/roosterjs-content-model-types/lib/format/formatParts/WhiteSpaceFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/WhiteSpaceFormat.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/format/formatParts/WhiteSpaceFormat.ts rename to packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/WhiteSpaceFormat.ts diff --git a/packages/roosterjs-content-model-types/lib/format/formatParts/WordBreakFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/WordBreakFormat.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/format/formatParts/WordBreakFormat.ts rename to packages/roosterjs-content-model-types/lib/contentModel/format/formatParts/WordBreakFormat.ts diff --git a/packages/roosterjs-content-model-types/lib/format/metadata/DatasetFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/metadata/DatasetFormat.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/format/metadata/DatasetFormat.ts rename to packages/roosterjs-content-model-types/lib/contentModel/format/metadata/DatasetFormat.ts diff --git a/packages/roosterjs-content-model-types/lib/format/metadata/ImageMetadataFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/metadata/ImageMetadataFormat.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/format/metadata/ImageMetadataFormat.ts rename to packages/roosterjs-content-model-types/lib/contentModel/format/metadata/ImageMetadataFormat.ts diff --git a/packages/roosterjs-content-model-types/lib/format/metadata/ListMetadataFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/metadata/ListMetadataFormat.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/format/metadata/ListMetadataFormat.ts rename to packages/roosterjs-content-model-types/lib/contentModel/format/metadata/ListMetadataFormat.ts diff --git a/packages/roosterjs-content-model-types/lib/format/metadata/TableCellMetadataFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/metadata/TableCellMetadataFormat.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/format/metadata/TableCellMetadataFormat.ts rename to packages/roosterjs-content-model-types/lib/contentModel/format/metadata/TableCellMetadataFormat.ts diff --git a/packages/roosterjs-content-model-types/lib/format/metadata/TableMetadataFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/metadata/TableMetadataFormat.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/format/metadata/TableMetadataFormat.ts rename to packages/roosterjs-content-model-types/lib/contentModel/format/metadata/TableMetadataFormat.ts diff --git a/packages/roosterjs-content-model-types/lib/segment/ContentModelBr.ts b/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelBr.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/segment/ContentModelBr.ts rename to packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelBr.ts diff --git a/packages/roosterjs-content-model-types/lib/segment/ContentModelGeneralSegment.ts b/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelGeneralSegment.ts similarity index 84% rename from packages/roosterjs-content-model-types/lib/segment/ContentModelGeneralSegment.ts rename to packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelGeneralSegment.ts index a0efb7ef80a..bbd0741c7ac 100644 --- a/packages/roosterjs-content-model-types/lib/segment/ContentModelGeneralSegment.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelGeneralSegment.ts @@ -1,5 +1,5 @@ import type { ContentModelBlockFormat } from '../format/ContentModelBlockFormat'; -import type { ContentModelGeneralBlock } from '../group/ContentModelGeneralBlock'; +import type { ContentModelGeneralBlock } from '../blockGroup/ContentModelGeneralBlock'; import type { ContentModelSegmentBase } from './ContentModelSegmentBase'; import type { ContentModelSegmentFormat } from '../format/ContentModelSegmentFormat'; diff --git a/packages/roosterjs-content-model-types/lib/segment/ContentModelImage.ts b/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelImage.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/segment/ContentModelImage.ts rename to packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelImage.ts diff --git a/packages/roosterjs-content-model-types/lib/segment/ContentModelSegment.ts b/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelSegment.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/segment/ContentModelSegment.ts rename to packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelSegment.ts diff --git a/packages/roosterjs-content-model-types/lib/segment/ContentModelSegmentBase.ts b/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelSegmentBase.ts similarity index 85% rename from packages/roosterjs-content-model-types/lib/segment/ContentModelSegmentBase.ts rename to packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelSegmentBase.ts index 2e06a7d46ae..22df2695372 100644 --- a/packages/roosterjs-content-model-types/lib/segment/ContentModelSegmentBase.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelSegmentBase.ts @@ -1,9 +1,9 @@ import type { ContentModelCode } from '../decorator/ContentModelCode'; import type { ContentModelLink } from '../decorator/ContentModelLink'; import type { ContentModelSegmentFormat } from '../format/ContentModelSegmentFormat'; -import type { ContentModelSegmentType } from '../enum/SegmentType'; +import type { ContentModelSegmentType } from './SegmentType'; import type { ContentModelWithFormat } from '../format/ContentModelWithFormat'; -import type { Selectable } from '../selection/Selectable'; +import type { Selectable } from '../common/Selectable'; /** * Base type of Content Model Segment diff --git a/packages/roosterjs-content-model-types/lib/segment/ContentModelSelectionMarker.ts b/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelSelectionMarker.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/segment/ContentModelSelectionMarker.ts rename to packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelSelectionMarker.ts diff --git a/packages/roosterjs-content-model-types/lib/segment/ContentModelText.ts b/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelText.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/segment/ContentModelText.ts rename to packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelText.ts diff --git a/packages/roosterjs-content-model-types/lib/enum/SegmentType.ts b/packages/roosterjs-content-model-types/lib/contentModel/segment/SegmentType.ts similarity index 100% rename from packages/roosterjs-content-model-types/lib/enum/SegmentType.ts rename to packages/roosterjs-content-model-types/lib/contentModel/segment/SegmentType.ts diff --git a/packages/roosterjs-content-model-types/lib/context/ContentModelHandler.ts b/packages/roosterjs-content-model-types/lib/context/ContentModelHandler.ts index d50ad80e3f8..4c8fcdf099f 100644 --- a/packages/roosterjs-content-model-types/lib/context/ContentModelHandler.ts +++ b/packages/roosterjs-content-model-types/lib/context/ContentModelHandler.ts @@ -1,7 +1,7 @@ -import type { ContentModelBlock } from '../block/ContentModelBlock'; -import type { ContentModelBlockGroup } from '../group/ContentModelBlockGroup'; -import type { ContentModelDecorator } from '../decorator/ContentModelDecorator'; -import type { ContentModelSegment } from '../segment/ContentModelSegment'; +import type { ContentModelBlock } from '../contentModel/block/ContentModelBlock'; +import type { ContentModelBlockGroup } from '../contentModel/blockGroup/ContentModelBlockGroup'; +import type { ContentModelDecorator } from '../contentModel/decorator/ContentModelDecorator'; +import type { ContentModelSegment } from '../contentModel/segment/ContentModelSegment'; import type { ModelToDomContext } from './ModelToDomContext'; /** diff --git a/packages/roosterjs-content-model-types/lib/context/DomIndexer.ts b/packages/roosterjs-content-model-types/lib/context/DomIndexer.ts index 0b8c5125b47..ad3b5d3f1e1 100644 --- a/packages/roosterjs-content-model-types/lib/context/DomIndexer.ts +++ b/packages/roosterjs-content-model-types/lib/context/DomIndexer.ts @@ -1,8 +1,8 @@ import type { CacheSelection } from '../pluginState/CachePluginState'; -import type { ContentModelDocument } from '../group/ContentModelDocument'; -import type { ContentModelParagraph } from '../block/ContentModelParagraph'; -import type { ContentModelSegment } from '../segment/ContentModelSegment'; -import type { ContentModelTable } from '../block/ContentModelTable'; +import type { ContentModelDocument } from '../contentModel/blockGroup/ContentModelDocument'; +import type { ContentModelParagraph } from '../contentModel/block/ContentModelParagraph'; +import type { ContentModelSegment } from '../contentModel/segment/ContentModelSegment'; +import type { ContentModelTable } from '../contentModel/block/ContentModelTable'; import type { DOMSelection } from '../selection/DOMSelection'; /** diff --git a/packages/roosterjs-content-model-types/lib/context/DomToModelFormatContext.ts b/packages/roosterjs-content-model-types/lib/context/DomToModelFormatContext.ts index 34e233e2d4e..dbe1884ab1e 100644 --- a/packages/roosterjs-content-model-types/lib/context/DomToModelFormatContext.ts +++ b/packages/roosterjs-content-model-types/lib/context/DomToModelFormatContext.ts @@ -1,10 +1,10 @@ -import type { ContentModelBlockFormat } from '../format/ContentModelBlockFormat'; -import type { ContentModelBlockGroup } from '../group/ContentModelBlockGroup'; -import type { ContentModelCode } from '../decorator/ContentModelCode'; -import type { ContentModelLink } from '../decorator/ContentModelLink'; -import type { ContentModelListLevel } from '../decorator/ContentModelListLevel'; -import type { ContentModelParagraphDecorator } from '../decorator/ContentModelParagraphDecorator'; -import type { ContentModelSegmentFormat } from '../format/ContentModelSegmentFormat'; +import type { ContentModelBlockFormat } from '../contentModel/format/ContentModelBlockFormat'; +import type { ContentModelBlockGroup } from '../contentModel/blockGroup/ContentModelBlockGroup'; +import type { ContentModelCode } from '../contentModel/decorator/ContentModelCode'; +import type { ContentModelLink } from '../contentModel/decorator/ContentModelLink'; +import type { ContentModelListLevel } from '../contentModel/decorator/ContentModelListLevel'; +import type { ContentModelParagraphDecorator } from '../contentModel/decorator/ContentModelParagraphDecorator'; +import type { ContentModelSegmentFormat } from '../contentModel/format/ContentModelSegmentFormat'; /** * Represents the context object used when do DOM to Content Model conversion and processing a List diff --git a/packages/roosterjs-content-model-types/lib/context/DomToModelSettings.ts b/packages/roosterjs-content-model-types/lib/context/DomToModelSettings.ts index 042f7ab0532..79269764346 100644 --- a/packages/roosterjs-content-model-types/lib/context/DomToModelSettings.ts +++ b/packages/roosterjs-content-model-types/lib/context/DomToModelSettings.ts @@ -1,9 +1,9 @@ -import type { ContentModelSegmentFormat } from '../format/ContentModelSegmentFormat'; -import type { ContentModelFormatBase } from '../format/ContentModelFormatBase'; -import type { ContentModelFormatMap } from '../format/ContentModelFormatMap'; +import type { ContentModelSegmentFormat } from '../contentModel/format/ContentModelSegmentFormat'; +import type { ContentModelFormatBase } from '../contentModel/format/ContentModelFormatBase'; +import type { ContentModelFormatMap } from '../contentModel/format/ContentModelFormatMap'; import type { DomToModelContext } from './DomToModelContext'; import type { ElementProcessor } from './ElementProcessor'; -import type { FormatHandlerTypeMap, FormatKey } from '../format/FormatHandlerTypeMap'; +import type { FormatHandlerTypeMap, FormatKey } from '../contentModel/format/FormatHandlerTypeMap'; /** * A type of Default style map, from tag name string (in upper case) to a static style object diff --git a/packages/roosterjs-content-model-types/lib/context/EditorContext.ts b/packages/roosterjs-content-model-types/lib/context/EditorContext.ts index 4ea984e21d6..27ff10f399e 100644 --- a/packages/roosterjs-content-model-types/lib/context/EditorContext.ts +++ b/packages/roosterjs-content-model-types/lib/context/EditorContext.ts @@ -1,6 +1,6 @@ import type { DarkColorHandler } from './DarkColorHandler'; import type { DomIndexer } from './DomIndexer'; -import type { ContentModelSegmentFormat } from '../format/ContentModelSegmentFormat'; +import type { ContentModelSegmentFormat } from '../contentModel/format/ContentModelSegmentFormat'; import type { PendingFormat } from '../pluginState/FormatPluginState'; /** diff --git a/packages/roosterjs-content-model-types/lib/context/ElementProcessor.ts b/packages/roosterjs-content-model-types/lib/context/ElementProcessor.ts index e3e009d58a6..8ecfd004cf5 100644 --- a/packages/roosterjs-content-model-types/lib/context/ElementProcessor.ts +++ b/packages/roosterjs-content-model-types/lib/context/ElementProcessor.ts @@ -1,4 +1,4 @@ -import type { ContentModelBlockGroup } from '../group/ContentModelBlockGroup'; +import type { ContentModelBlockGroup } from '../contentModel/blockGroup/ContentModelBlockGroup'; import type { DomToModelContext } from './DomToModelContext'; /** diff --git a/packages/roosterjs-content-model-types/lib/context/ModelToDomFormatContext.ts b/packages/roosterjs-content-model-types/lib/context/ModelToDomFormatContext.ts index 5a279de2684..d1fff72ef9c 100644 --- a/packages/roosterjs-content-model-types/lib/context/ModelToDomFormatContext.ts +++ b/packages/roosterjs-content-model-types/lib/context/ModelToDomFormatContext.ts @@ -1,6 +1,6 @@ -import type { ContentModelBlockFormat } from '../format/ContentModelBlockFormat'; -import type { ContentModelListLevel } from '../decorator/ContentModelListLevel'; -import type { ContentModelSegmentFormat } from '../format/ContentModelSegmentFormat'; +import type { ContentModelBlockFormat } from '../contentModel/format/ContentModelBlockFormat'; +import type { ContentModelListLevel } from '../contentModel/decorator/ContentModelListLevel'; +import type { ContentModelSegmentFormat } from '../contentModel/format/ContentModelSegmentFormat'; /** * Represents a list stack item used by Content Model to DOM conversion diff --git a/packages/roosterjs-content-model-types/lib/context/ModelToDomSettings.ts b/packages/roosterjs-content-model-types/lib/context/ModelToDomSettings.ts index 144569beb2e..61899252ce6 100644 --- a/packages/roosterjs-content-model-types/lib/context/ModelToDomSettings.ts +++ b/packages/roosterjs-content-model-types/lib/context/ModelToDomSettings.ts @@ -1,29 +1,29 @@ import type { Definition } from '../metadata/Definition'; -import type { ContentModelBlock } from '../block/ContentModelBlock'; -import type { ContentModelBlockFormat } from '../format/ContentModelBlockFormat'; -import type { ContentModelBlockGroup } from '../group/ContentModelBlockGroup'; -import type { ContentModelBr } from '../segment/ContentModelBr'; -import type { ContentModelDecorator } from '../decorator/ContentModelDecorator'; -import type { ContentModelDivider } from '../block/ContentModelDivider'; -import type { ContentModelEntity } from '../entity/ContentModelEntity'; -import type { ContentModelFormatBase } from '../format/ContentModelFormatBase'; -import type { ContentModelFormatContainer } from '../group/ContentModelFormatContainer'; -import type { ContentModelFormatMap } from '../format/ContentModelFormatMap'; -import type { ContentModelGeneralBlock } from '../group/ContentModelGeneralBlock'; -import type { ContentModelGeneralSegment } from '../segment/ContentModelGeneralSegment'; -import type { ContentModelImage } from '../segment/ContentModelImage'; -import type { ContentModelListItem } from '../group/ContentModelListItem'; -import type { ContentModelListItemFormat } from '../format/ContentModelListItemFormat'; -import type { ContentModelListItemLevelFormat } from '../format/ContentModelListItemLevelFormat'; -import type { ContentModelParagraph } from '../block/ContentModelParagraph'; -import type { ContentModelSegment } from '../segment/ContentModelSegment'; -import type { ContentModelSegmentFormat } from '../format/ContentModelSegmentFormat'; -import type { ContentModelTable } from '../block/ContentModelTable'; -import type { ContentModelTableRow } from '../block/ContentModelTableRow'; -import type { ContentModelText } from '../segment/ContentModelText'; -import type { FormatHandlerTypeMap, FormatKey } from '../format/FormatHandlerTypeMap'; +import type { ContentModelBlock } from '../contentModel/block/ContentModelBlock'; +import type { ContentModelBlockFormat } from '../contentModel/format/ContentModelBlockFormat'; +import type { ContentModelBlockGroup } from '../contentModel/blockGroup/ContentModelBlockGroup'; +import type { ContentModelBr } from '../contentModel/segment/ContentModelBr'; +import type { ContentModelDecorator } from '../contentModel/decorator/ContentModelDecorator'; +import type { ContentModelDivider } from '../contentModel/block/ContentModelDivider'; +import type { ContentModelEntity } from '../contentModel/entity/ContentModelEntity'; +import type { ContentModelFormatBase } from '../contentModel/format/ContentModelFormatBase'; +import type { ContentModelFormatContainer } from '../contentModel/blockGroup/ContentModelFormatContainer'; +import type { ContentModelFormatMap } from '../contentModel/format/ContentModelFormatMap'; +import type { ContentModelGeneralBlock } from '../contentModel/blockGroup/ContentModelGeneralBlock'; +import type { ContentModelGeneralSegment } from '../contentModel/segment/ContentModelGeneralSegment'; +import type { ContentModelImage } from '../contentModel/segment/ContentModelImage'; +import type { ContentModelListItem } from '../contentModel/blockGroup/ContentModelListItem'; +import type { ContentModelListItemFormat } from '../contentModel/format/ContentModelListItemFormat'; +import type { ContentModelListItemLevelFormat } from '../contentModel/format/ContentModelListItemLevelFormat'; +import type { ContentModelParagraph } from '../contentModel/block/ContentModelParagraph'; +import type { ContentModelSegment } from '../contentModel/segment/ContentModelSegment'; +import type { ContentModelSegmentFormat } from '../contentModel/format/ContentModelSegmentFormat'; +import type { ContentModelTable } from '../contentModel/block/ContentModelTable'; +import type { ContentModelTableRow } from '../contentModel/block/ContentModelTableRow'; +import type { ContentModelText } from '../contentModel/segment/ContentModelText'; +import type { FormatHandlerTypeMap, FormatKey } from '../contentModel/format/FormatHandlerTypeMap'; import type { ModelToDomContext } from './ModelToDomContext'; -import type { ListMetadataFormat } from '../format/metadata/ListMetadataFormat'; +import type { ListMetadataFormat } from '../contentModel/format/metadata/ListMetadataFormat'; import type { ContentModelHandler, ContentModelBlockHandler, diff --git a/packages/roosterjs-content-model-types/lib/editor/EditorCore.ts b/packages/roosterjs-content-model-types/lib/editor/EditorCore.ts index d292bb2e714..59f0600764c 100644 --- a/packages/roosterjs-content-model-types/lib/editor/EditorCore.ts +++ b/packages/roosterjs-content-model-types/lib/editor/EditorCore.ts @@ -7,7 +7,7 @@ import type { DOMEventRecord } from '../parameter/DOMEventRecord'; import type { Snapshot } from '../parameter/Snapshot'; import type { EntityState } from '../parameter/FormatContentModelContext'; import type { DarkColorHandler } from '../context/DarkColorHandler'; -import type { ContentModelDocument } from '../group/ContentModelDocument'; +import type { ContentModelDocument } from '../contentModel/blockGroup/ContentModelDocument'; import type { DOMSelection } from '../selection/DOMSelection'; import type { DomToModelOptionForCreateModel } from '../context/DomToModelOption'; import type { EditorContext } from '../context/EditorContext'; diff --git a/packages/roosterjs-content-model-types/lib/editor/EditorOptions.ts b/packages/roosterjs-content-model-types/lib/editor/EditorOptions.ts index ac038940178..82996e62c06 100644 --- a/packages/roosterjs-content-model-types/lib/editor/EditorOptions.ts +++ b/packages/roosterjs-content-model-types/lib/editor/EditorOptions.ts @@ -2,11 +2,11 @@ import type { KnownAnnounceStrings } from '../parameter/AnnounceData'; import type { PasteType } from '../enum/PasteType'; import type { Colors, ColorTransformFunction } from '../context/DarkColorHandler'; import type { EditorPlugin } from './EditorPlugin'; -import type { ContentModelSegmentFormat } from '../format/ContentModelSegmentFormat'; +import type { ContentModelSegmentFormat } from '../contentModel/format/ContentModelSegmentFormat'; import type { CoreApiMap } from './EditorCore'; import type { DomToModelOption } from '../context/DomToModelOption'; import type { ModelToDomOption } from '../context/ModelToDomOption'; -import type { ContentModelDocument } from '../group/ContentModelDocument'; +import type { ContentModelDocument } from '../contentModel/blockGroup/ContentModelDocument'; import type { Snapshots } from '../parameter/Snapshot'; import type { TrustedHTMLHandler } from '../parameter/TrustedHTMLHandler'; diff --git a/packages/roosterjs-content-model-types/lib/editor/IEditor.ts b/packages/roosterjs-content-model-types/lib/editor/IEditor.ts index f32b2ae797a..2ff5e28bad0 100644 --- a/packages/roosterjs-content-model-types/lib/editor/IEditor.ts +++ b/packages/roosterjs-content-model-types/lib/editor/IEditor.ts @@ -6,8 +6,8 @@ import type { PluginEventType } from '../event/PluginEventType'; import type { DOMEventRecord } from '../parameter/DOMEventRecord'; import type { SnapshotsManager } from '../parameter/SnapshotsManager'; import type { Snapshot } from '../parameter/Snapshot'; -import type { ContentModelDocument } from '../group/ContentModelDocument'; -import type { ContentModelSegmentFormat } from '../format/ContentModelSegmentFormat'; +import type { ContentModelDocument } from '../contentModel/blockGroup/ContentModelDocument'; +import type { ContentModelSegmentFormat } from '../contentModel/format/ContentModelSegmentFormat'; import type { DOMSelection } from '../selection/DOMSelection'; import type { EditorEnvironment } from '../parameter/EditorEnvironment'; import type { @@ -23,19 +23,21 @@ import type { EntityState } from '../parameter/FormatContentModelContext'; * An interface of Editor, built on top of Content Model */ export interface IEditor { + /** + * @deprecated Use formatContentModel() instead + */ + getContentModelCopy(mode: 'connected'): ContentModelDocument; + /** * Create Content Model from DOM tree in this editor * @param mode What kind of Content Model we want. Currently we support the following values: - * - connected: Returns a connect Content Model object. "Connected" means if there is any entity inside editor, the returned Content Model will - * contain the same wrapper element for entity. This option should only be used in some special cases. In most cases we should use "disconnected" - * to get a fully disconnected Content Model so that any change to the model will not impact editor content. * - disconnected: Returns a disconnected clone of Content Model from editor which you can do any change on it and it won't impact the editor content. * If there is any entity in editor, the returned object will contain cloned copy of entity wrapper element. * If editor is in dark mode, the cloned entity will be converted back to light mode. * - clean: Similar with disconnected, this will return a disconnected model, the difference is "clean" mode will not include any selection info. * This is usually used for exporting content */ - getContentModelCopy(mode: 'connected' | 'disconnected' | 'clean'): ContentModelDocument; + getContentModelCopy(mode: 'disconnected' | 'clean'): ContentModelDocument; /** * Get current running environment, such as if editor is running on Mac diff --git a/packages/roosterjs-content-model-types/lib/event/BeforePasteEvent.ts b/packages/roosterjs-content-model-types/lib/event/BeforePasteEvent.ts index d08e76db901..79ccc8ff3aa 100644 --- a/packages/roosterjs-content-model-types/lib/event/BeforePasteEvent.ts +++ b/packages/roosterjs-content-model-types/lib/event/BeforePasteEvent.ts @@ -2,7 +2,7 @@ import type { DomToModelOptionForSanitizing } from '../context/DomToModelOption' import type { PasteType } from '../enum/PasteType'; import type { ClipboardData } from '../parameter/ClipboardData'; import type { BasePluginEvent } from './BasePluginEvent'; -import type { ContentModelDocument } from '../group/ContentModelDocument'; +import type { ContentModelDocument } from '../contentModel/blockGroup/ContentModelDocument'; import type { InsertPoint } from '../selection/InsertPoint'; /** diff --git a/packages/roosterjs-content-model-types/lib/event/ContentChangedEvent.ts b/packages/roosterjs-content-model-types/lib/event/ContentChangedEvent.ts index 1f1de8bccc3..c83aa3a8a45 100644 --- a/packages/roosterjs-content-model-types/lib/event/ContentChangedEvent.ts +++ b/packages/roosterjs-content-model-types/lib/event/ContentChangedEvent.ts @@ -1,9 +1,9 @@ import type { AnnounceData } from '../parameter/AnnounceData'; import type { BasePluginEvent } from './BasePluginEvent'; import type { EntityState } from '../parameter/FormatContentModelContext'; -import type { ContentModelEntity } from '../entity/ContentModelEntity'; +import type { ContentModelEntity } from '../contentModel/entity/ContentModelEntity'; import type { EntityRemovalOperation } from '../enum/EntityOperation'; -import type { ContentModelDocument } from '../group/ContentModelDocument'; +import type { ContentModelDocument } from '../contentModel/blockGroup/ContentModelDocument'; import type { DOMSelection } from '../selection/DOMSelection'; /** diff --git a/packages/roosterjs-content-model-types/lib/index.ts b/packages/roosterjs-content-model-types/lib/index.ts index df428bb8d0a..a6b748c4c68 100644 --- a/packages/roosterjs-content-model-types/lib/index.ts +++ b/packages/roosterjs-content-model-types/lib/index.ts @@ -1,69 +1,70 @@ -export { ContentModelSegmentFormat } from './format/ContentModelSegmentFormat'; -export { ContentModelWithFormat } from './format/ContentModelWithFormat'; -export { ContentModelTableFormat } from './format/ContentModelTableFormat'; -export { ContentModelWithDataset } from './format/ContentModelWithDataset'; -export { ContentModelBlockFormat } from './format/ContentModelBlockFormat'; -export { ContentModelTableCellFormat } from './format/ContentModelTableCellFormat'; -export { ContentModelListItemFormat } from './format/ContentModelListItemFormat'; -export { ContentModelListItemLevelFormat } from './format/ContentModelListItemLevelFormat'; -export { ContentModelHyperLinkFormat } from './format/ContentModelHyperLinkFormat'; -export { ContentModelCodeFormat } from './format/ContentModelCodeFormat'; -export { ContentModelFormatContainerFormat } from './format/ContentModelFormatContainerFormat'; -export { ContentModelDividerFormat } from './format/ContentModelDividerFormat'; -export { ContentModelFormatBase } from './format/ContentModelFormatBase'; -export { ContentModelFormatMap } from './format/ContentModelFormatMap'; -export { ContentModelImageFormat } from './format/ContentModelImageFormat'; -export { ContentModelEntityFormat } from './format/ContentModelEntityFormat'; -export { FormatHandlerTypeMap, FormatKey } from './format/FormatHandlerTypeMap'; +export { ContentModelSegmentFormat } from './contentModel/format/ContentModelSegmentFormat'; +export { ContentModelWithFormat } from './contentModel/format/ContentModelWithFormat'; +export { ContentModelTableFormat } from './contentModel/format/ContentModelTableFormat'; +export { ContentModelWithDataset } from './contentModel/format/ContentModelWithDataset'; +export { ContentModelBlockFormat } from './contentModel/format/ContentModelBlockFormat'; +export { ContentModelTableCellFormat } from './contentModel/format/ContentModelTableCellFormat'; +export { ContentModelListItemFormat } from './contentModel/format/ContentModelListItemFormat'; +export { ContentModelListItemLevelFormat } from './contentModel/format/ContentModelListItemLevelFormat'; +export { ContentModelHyperLinkFormat } from './contentModel/format/ContentModelHyperLinkFormat'; +export { ContentModelCodeFormat } from './contentModel/format/ContentModelCodeFormat'; +export { ContentModelFormatContainerFormat } from './contentModel/format/ContentModelFormatContainerFormat'; +export { ContentModelDividerFormat } from './contentModel/format/ContentModelDividerFormat'; +export { ContentModelFormatBase } from './contentModel/format/ContentModelFormatBase'; +export { ContentModelFormatMap } from './contentModel/format/ContentModelFormatMap'; +export { ContentModelImageFormat } from './contentModel/format/ContentModelImageFormat'; +export { ContentModelEntityFormat } from './contentModel/format/ContentModelEntityFormat'; +export { FormatHandlerTypeMap, FormatKey } from './contentModel/format/FormatHandlerTypeMap'; -export { BackgroundColorFormat } from './format/formatParts/BackgroundColorFormat'; -export { BoldFormat } from './format/formatParts/BoldFormat'; -export { FontFamilyFormat } from './format/formatParts/FontFamilyFormat'; -export { FontSizeFormat } from './format/formatParts/FontSizeFormat'; -export { ItalicFormat } from './format/formatParts/ItalicFormat'; -export { LetterSpacingFormat } from './format/formatParts/LetterSpacingFormat'; -export { LineHeightFormat } from './format/formatParts/LineHeightFormat'; -export { StrikeFormat } from './format/formatParts/StrikeFormat'; -export { SuperOrSubScriptFormat } from './format/formatParts/SuperOrSubScriptFormat'; -export { TextColorFormat } from './format/formatParts/TextColorFormat'; -export { UnderlineFormat } from './format/formatParts/UnderlineFormat'; -export { BorderBoxFormat } from './format/formatParts/BorderBoxFormat'; -export { VerticalAlignFormat } from './format/formatParts/VerticalAlignFormat'; -export { WordBreakFormat } from './format/formatParts/WordBreakFormat'; -export { BorderFormat } from './format/formatParts/BorderFormat'; -export { DirectionFormat } from './format/formatParts/DirectionFormat'; -export { HtmlAlignFormat } from './format/formatParts/HtmlAlignFormat'; -export { MarginFormat } from './format/formatParts/MarginFormat'; -export { PaddingFormat } from './format/formatParts/PaddingFormat'; -export { TextAlignFormat } from './format/formatParts/TextAlignFormat'; -export { TextIndentFormat } from './format/formatParts/TextIndentFormat'; -export { WhiteSpaceFormat } from './format/formatParts/WhiteSpaceFormat'; -export { DisplayFormat } from './format/formatParts/DisplayFormat'; -export { IdFormat } from './format/formatParts/IdFormat'; -export { SpacingFormat } from './format/formatParts/SpacingFormat'; -export { TableLayoutFormat } from './format/formatParts/TableLayoutFormat'; -export { LinkFormat } from './format/formatParts/LinkFormat'; -export { SizeFormat } from './format/formatParts/SizeFormat'; -export { BoxShadowFormat } from './format/formatParts/BoxShadowFormat'; -export { ListThreadFormat } from './format/formatParts/ListThreadFormat'; -export { ListStyleFormat } from './format/formatParts/ListStyleFormat'; -export { FloatFormat } from './format/formatParts/FloatFormat'; -export { EntityInfoFormat } from './format/formatParts/EntityInfoFormat'; +export { BackgroundColorFormat } from './contentModel/format/formatParts/BackgroundColorFormat'; +export { BoldFormat } from './contentModel/format/formatParts/BoldFormat'; +export { FontFamilyFormat } from './contentModel/format/formatParts/FontFamilyFormat'; +export { FontSizeFormat } from './contentModel/format/formatParts/FontSizeFormat'; +export { ItalicFormat } from './contentModel/format/formatParts/ItalicFormat'; +export { LetterSpacingFormat } from './contentModel/format/formatParts/LetterSpacingFormat'; +export { LineHeightFormat } from './contentModel/format/formatParts/LineHeightFormat'; +export { StrikeFormat } from './contentModel/format/formatParts/StrikeFormat'; +export { SuperOrSubScriptFormat } from './contentModel/format/formatParts/SuperOrSubScriptFormat'; +export { TextColorFormat } from './contentModel/format/formatParts/TextColorFormat'; +export { UnderlineFormat } from './contentModel/format/formatParts/UnderlineFormat'; +export { BorderBoxFormat } from './contentModel/format/formatParts/BorderBoxFormat'; +export { VerticalAlignFormat } from './contentModel/format/formatParts/VerticalAlignFormat'; +export { WordBreakFormat } from './contentModel/format/formatParts/WordBreakFormat'; +export { BorderFormat } from './contentModel/format/formatParts/BorderFormat'; +export { DirectionFormat } from './contentModel/format/formatParts/DirectionFormat'; +export { HtmlAlignFormat } from './contentModel/format/formatParts/HtmlAlignFormat'; +export { MarginFormat } from './contentModel/format/formatParts/MarginFormat'; +export { PaddingFormat } from './contentModel/format/formatParts/PaddingFormat'; +export { TextAlignFormat } from './contentModel/format/formatParts/TextAlignFormat'; +export { TextIndentFormat } from './contentModel/format/formatParts/TextIndentFormat'; +export { WhiteSpaceFormat } from './contentModel/format/formatParts/WhiteSpaceFormat'; +export { DisplayFormat } from './contentModel/format/formatParts/DisplayFormat'; +export { IdFormat } from './contentModel/format/formatParts/IdFormat'; +export { SpacingFormat } from './contentModel/format/formatParts/SpacingFormat'; +export { TableLayoutFormat } from './contentModel/format/formatParts/TableLayoutFormat'; +export { LinkFormat } from './contentModel/format/formatParts/LinkFormat'; +export { SizeFormat } from './contentModel/format/formatParts/SizeFormat'; +export { BoxShadowFormat } from './contentModel/format/formatParts/BoxShadowFormat'; +export { ListThreadFormat } from './contentModel/format/formatParts/ListThreadFormat'; +export { ListStyleFormat } from './contentModel/format/formatParts/ListStyleFormat'; +export { FloatFormat } from './contentModel/format/formatParts/FloatFormat'; +export { EntityInfoFormat } from './contentModel/format/formatParts/EntityInfoFormat'; -export { DatasetFormat } from './format/metadata/DatasetFormat'; -export { TableMetadataFormat } from './format/metadata/TableMetadataFormat'; -export { ListMetadataFormat } from './format/metadata/ListMetadataFormat'; +export { DatasetFormat } from './contentModel/format/metadata/DatasetFormat'; +export { TableMetadataFormat } from './contentModel/format/metadata/TableMetadataFormat'; +export { ListMetadataFormat } from './contentModel/format/metadata/ListMetadataFormat'; export { ImageResizeMetadataFormat, ImageCropMetadataFormat, ImageMetadataFormat, ImageRotateMetadataFormat, -} from './format/metadata/ImageMetadataFormat'; -export { TableCellMetadataFormat } from './format/metadata/TableCellMetadataFormat'; +} from './contentModel/format/metadata/ImageMetadataFormat'; +export { TableCellMetadataFormat } from './contentModel/format/metadata/TableCellMetadataFormat'; + +export { ContentModelBlockGroupType } from './contentModel/blockGroup/BlockGroupType'; +export { ContentModelBlockType } from './contentModel/block/BlockType'; +export { ContentModelSegmentType } from './contentModel/segment/SegmentType'; -export { ContentModelBlockGroupType } from './enum/BlockGroupType'; -export { ContentModelBlockType } from './enum/BlockType'; -export { ContentModelSegmentType } from './enum/SegmentType'; export { EntityLifecycleOperation, EntityOperation, @@ -88,39 +89,39 @@ export { DeleteResult } from './enum/DeleteResult'; export { InsertEntityPosition } from './enum/InsertEntityPosition'; export { ExportContentMode } from './enum/ExportContentMode'; -export { ContentModelBlock } from './block/ContentModelBlock'; -export { ContentModelParagraph } from './block/ContentModelParagraph'; -export { ContentModelTable } from './block/ContentModelTable'; -export { ContentModelDivider } from './block/ContentModelDivider'; -export { ContentModelBlockBase } from './block/ContentModelBlockBase'; -export { ContentModelBlockWithCache } from './block/ContentModelBlockWithCache'; -export { ContentModelTableRow } from './block/ContentModelTableRow'; +export { ContentModelBlock } from './contentModel/block/ContentModelBlock'; +export { ContentModelParagraph } from './contentModel/block/ContentModelParagraph'; +export { ContentModelTable } from './contentModel/block/ContentModelTable'; +export { ContentModelDivider } from './contentModel/block/ContentModelDivider'; +export { ContentModelBlockBase } from './contentModel/block/ContentModelBlockBase'; +export { ContentModelBlockWithCache } from './contentModel/common/ContentModelBlockWithCache'; +export { ContentModelTableRow } from './contentModel/block/ContentModelTableRow'; -export { ContentModelEntity } from './entity/ContentModelEntity'; +export { ContentModelEntity } from './contentModel/entity/ContentModelEntity'; -export { ContentModelDocument } from './group/ContentModelDocument'; -export { ContentModelBlockGroupBase } from './group/ContentModelBlockGroupBase'; -export { ContentModelFormatContainer } from './group/ContentModelFormatContainer'; -export { ContentModelGeneralBlock } from './group/ContentModelGeneralBlock'; -export { ContentModelListItem } from './group/ContentModelListItem'; -export { ContentModelTableCell } from './group/ContentModelTableCell'; -export { ContentModelBlockGroup } from './group/ContentModelBlockGroup'; +export { ContentModelDocument } from './contentModel/blockGroup/ContentModelDocument'; +export { ContentModelBlockGroupBase } from './contentModel/blockGroup/ContentModelBlockGroupBase'; +export { ContentModelFormatContainer } from './contentModel/blockGroup/ContentModelFormatContainer'; +export { ContentModelGeneralBlock } from './contentModel/blockGroup/ContentModelGeneralBlock'; +export { ContentModelListItem } from './contentModel/blockGroup/ContentModelListItem'; +export { ContentModelTableCell } from './contentModel/blockGroup/ContentModelTableCell'; +export { ContentModelBlockGroup } from './contentModel/blockGroup/ContentModelBlockGroup'; -export { ContentModelBr } from './segment/ContentModelBr'; -export { ContentModelGeneralSegment } from './segment/ContentModelGeneralSegment'; -export { ContentModelImage } from './segment/ContentModelImage'; -export { ContentModelText } from './segment/ContentModelText'; -export { ContentModelSelectionMarker } from './segment/ContentModelSelectionMarker'; -export { ContentModelSegmentBase } from './segment/ContentModelSegmentBase'; -export { ContentModelSegment } from './segment/ContentModelSegment'; +export { ContentModelBr } from './contentModel/segment/ContentModelBr'; +export { ContentModelGeneralSegment } from './contentModel/segment/ContentModelGeneralSegment'; +export { ContentModelImage } from './contentModel/segment/ContentModelImage'; +export { ContentModelText } from './contentModel/segment/ContentModelText'; +export { ContentModelSelectionMarker } from './contentModel/segment/ContentModelSelectionMarker'; +export { ContentModelSegmentBase } from './contentModel/segment/ContentModelSegmentBase'; +export { ContentModelSegment } from './contentModel/segment/ContentModelSegment'; -export { ContentModelCode } from './decorator/ContentModelCode'; -export { ContentModelLink } from './decorator/ContentModelLink'; -export { ContentModelParagraphDecorator } from './decorator/ContentModelParagraphDecorator'; -export { ContentModelDecorator } from './decorator/ContentModelDecorator'; -export { ContentModelListLevel } from './decorator/ContentModelListLevel'; +export { ContentModelCode } from './contentModel/decorator/ContentModelCode'; +export { ContentModelLink } from './contentModel/decorator/ContentModelLink'; +export { ContentModelParagraphDecorator } from './contentModel/decorator/ContentModelParagraphDecorator'; +export { ContentModelDecorator } from './contentModel/decorator/ContentModelDecorator'; +export { ContentModelListLevel } from './contentModel/decorator/ContentModelListLevel'; -export { Selectable } from './selection/Selectable'; +export { Selectable } from './contentModel/common/Selectable'; export { DOMSelection, SelectionType, @@ -309,6 +310,11 @@ export { NodeTypeMap } from './parameter/NodeTypeMap'; export { TypeOfBlockGroup } from './parameter/TypeOfBlockGroup'; export { OperationalBlocks } from './parameter/OperationalBlocks'; export { ParsedTable, ParsedTableCell } from './parameter/ParsedTable'; +export { + ModelToTextCallback, + ModelToTextCallbacks, + ModelToTextChecker, +} from './parameter/ModelToTextCallbacks'; export { BasePluginEvent, BasePluginDomEvent } from './event/BasePluginEvent'; export { BeforeCutCopyEvent } from './event/BeforeCutCopyEvent'; diff --git a/packages/roosterjs-content-model-types/lib/parameter/ClipboardData.ts b/packages/roosterjs-content-model-types/lib/parameter/ClipboardData.ts index a81bfe39f26..d2cf59062ea 100644 --- a/packages/roosterjs-content-model-types/lib/parameter/ClipboardData.ts +++ b/packages/roosterjs-content-model-types/lib/parameter/ClipboardData.ts @@ -1,4 +1,4 @@ -import type { ContentModelDocument } from '../group/ContentModelDocument'; +import type { ContentModelDocument } from '../contentModel/blockGroup/ContentModelDocument'; import type { EdgeLinkPreview } from './EdgeLinkPreview'; /** diff --git a/packages/roosterjs-content-model-types/lib/parameter/ContentModelFormatState.ts b/packages/roosterjs-content-model-types/lib/parameter/ContentModelFormatState.ts index f1fddbd08b6..ae284720077 100644 --- a/packages/roosterjs-content-model-types/lib/parameter/ContentModelFormatState.ts +++ b/packages/roosterjs-content-model-types/lib/parameter/ContentModelFormatState.ts @@ -1,4 +1,4 @@ -import type { TableMetadataFormat } from '../format/metadata/TableMetadataFormat'; +import type { TableMetadataFormat } from '../contentModel/format/metadata/TableMetadataFormat'; import type { ImageFormatState } from './ImageFormatState'; /** diff --git a/packages/roosterjs-content-model-types/lib/parameter/DeleteSelectionStep.ts b/packages/roosterjs-content-model-types/lib/parameter/DeleteSelectionStep.ts index b6801a01148..f396642b126 100644 --- a/packages/roosterjs-content-model-types/lib/parameter/DeleteSelectionStep.ts +++ b/packages/roosterjs-content-model-types/lib/parameter/DeleteSelectionStep.ts @@ -1,4 +1,4 @@ -import type { ContentModelParagraph } from '../block/ContentModelParagraph'; +import type { ContentModelParagraph } from '../contentModel/block/ContentModelParagraph'; import type { DeleteResult } from '../enum/DeleteResult'; import type { FormatContentModelContext } from './FormatContentModelContext'; import type { InsertPoint } from '../selection/InsertPoint'; diff --git a/packages/roosterjs-content-model-types/lib/parameter/FormatContentModelContext.ts b/packages/roosterjs-content-model-types/lib/parameter/FormatContentModelContext.ts index a993e3302ef..0cc6e7f63c2 100644 --- a/packages/roosterjs-content-model-types/lib/parameter/FormatContentModelContext.ts +++ b/packages/roosterjs-content-model-types/lib/parameter/FormatContentModelContext.ts @@ -1,7 +1,7 @@ import type { AnnounceData } from './AnnounceData'; -import type { ContentModelEntity } from '../entity/ContentModelEntity'; -import type { ContentModelImage } from '../segment/ContentModelImage'; -import type { ContentModelSegmentFormat } from '../format/ContentModelSegmentFormat'; +import type { ContentModelEntity } from '../contentModel/entity/ContentModelEntity'; +import type { ContentModelImage } from '../contentModel/segment/ContentModelImage'; +import type { ContentModelSegmentFormat } from '../contentModel/format/ContentModelSegmentFormat'; import type { EntityRemovalOperation } from '../enum/EntityOperation'; /** diff --git a/packages/roosterjs-content-model-types/lib/parameter/FormatContentModelOptions.ts b/packages/roosterjs-content-model-types/lib/parameter/FormatContentModelOptions.ts index 33bf1158b8b..ec43eae1ed1 100644 --- a/packages/roosterjs-content-model-types/lib/parameter/FormatContentModelOptions.ts +++ b/packages/roosterjs-content-model-types/lib/parameter/FormatContentModelOptions.ts @@ -1,4 +1,4 @@ -import type { ContentModelDocument } from '../group/ContentModelDocument'; +import type { ContentModelDocument } from '../contentModel/blockGroup/ContentModelDocument'; import type { DOMSelection } from '../selection/DOMSelection'; import type { FormatContentModelContext } from './FormatContentModelContext'; import type { OnNodeCreated } from '../context/ModelToDomSettings'; @@ -38,6 +38,11 @@ export interface FormatContentModelOptions { * When specified, use this selection range to override current selection inside editor */ selectionOverride?: DOMSelection; + + /** + * When pass to true, scroll the editing caret into view after write DOM tree if need + */ + scrollCaretIntoView?: boolean; } /** diff --git a/packages/roosterjs-content-model-types/lib/parameter/IterateSelectionsOption.ts b/packages/roosterjs-content-model-types/lib/parameter/IterateSelectionsOption.ts index 49b182f71be..7cce60f6fba 100644 --- a/packages/roosterjs-content-model-types/lib/parameter/IterateSelectionsOption.ts +++ b/packages/roosterjs-content-model-types/lib/parameter/IterateSelectionsOption.ts @@ -1,6 +1,6 @@ -import type { ContentModelBlock } from '../block/ContentModelBlock'; -import type { ContentModelBlockGroup } from '../group/ContentModelBlockGroup'; -import type { ContentModelSegment } from '../segment/ContentModelSegment'; +import type { ContentModelBlock } from '../contentModel/block/ContentModelBlock'; +import type { ContentModelBlockGroup } from '../contentModel/blockGroup/ContentModelBlockGroup'; +import type { ContentModelSegment } from '../contentModel/segment/ContentModelSegment'; import type { TableSelectionContext } from '../selection/TableSelectionContext'; /** diff --git a/packages/roosterjs-content-model-types/lib/parameter/ModelToTextCallbacks.ts b/packages/roosterjs-content-model-types/lib/parameter/ModelToTextCallbacks.ts new file mode 100644 index 00000000000..3ea41e57b31 --- /dev/null +++ b/packages/roosterjs-content-model-types/lib/parameter/ModelToTextCallbacks.ts @@ -0,0 +1,70 @@ +import type { ContentModelBlockGroup } from '../contentModel/blockGroup/ContentModelBlockGroup'; +import type { ContentModelDivider } from '../contentModel/block/ContentModelDivider'; +import type { ContentModelEntity } from '../contentModel/entity/ContentModelEntity'; +import type { ContentModelGeneralSegment } from '../contentModel/segment/ContentModelGeneralSegment'; +import type { ContentModelImage } from '../contentModel/segment/ContentModelImage'; +import type { ContentModelParagraph } from '../contentModel/block/ContentModelParagraph'; +import type { ContentModelTable } from '../contentModel/block/ContentModelTable'; +import type { ContentModelText } from '../contentModel/segment/ContentModelText'; + +/** + * Callback function type for converting a given Content Model object to plain text + * @param model The source model object to be converted to plain text + */ +export type ModelToTextCallback = (model: T) => string; + +/** + * Callback function type for checking if we should convert to text for the given content model object + * @param model The source model to check if we should convert it to plain text + */ +export type ModelToTextChecker = (model: T) => boolean; + +/** + * Callbacks to customize the behavior of contentModelToText function + */ +export interface ModelToTextCallbacks { + /** + * Customize the behavior of converting entity segment to plain text + */ + onEntitySegment?: ModelToTextCallback; + + /** + * Customize the behavior of converting entity block to plain text + */ + onEntityBlock?: ModelToTextCallback; + + /** + * Customize the behavior of converting general segment to plain text + */ + onGeneralSegment?: ModelToTextCallback; + + /** + * Customize the behavior of converting text model to plain text + */ + onText?: ModelToTextCallback; + + /** + * Customize the behavior of converting image model to plain text + */ + onImage?: ModelToTextCallback; + + /** + * Customize the behavior of converting divider model to plain text + */ + onDivider?: ModelToTextCallback; + + /** + * Customize the check if we should convert a paragraph model to plain text + */ + onParagraph?: ModelToTextChecker; + + /** + * Customize the check if we should convert a table model to plain text + */ + onTable?: ModelToTextChecker; + + /** + * Customize the check if we should convert a block group model to plain text + */ + onBlockGroup?: ModelToTextChecker; +} diff --git a/packages/roosterjs-content-model-types/lib/parameter/OperationalBlocks.ts b/packages/roosterjs-content-model-types/lib/parameter/OperationalBlocks.ts index 9f29d320eaa..b83e3e515d8 100644 --- a/packages/roosterjs-content-model-types/lib/parameter/OperationalBlocks.ts +++ b/packages/roosterjs-content-model-types/lib/parameter/OperationalBlocks.ts @@ -1,5 +1,5 @@ -import type { ContentModelBlock } from '../block/ContentModelBlock'; -import type { ContentModelBlockGroup } from '../group/ContentModelBlockGroup'; +import type { ContentModelBlock } from '../contentModel/block/ContentModelBlock'; +import type { ContentModelBlockGroup } from '../contentModel/blockGroup/ContentModelBlockGroup'; /** * Represent a pair of parent block group and child block diff --git a/packages/roosterjs-content-model-types/lib/parameter/TypeOfBlockGroup.ts b/packages/roosterjs-content-model-types/lib/parameter/TypeOfBlockGroup.ts index 2a1dd3e01e2..5cc421f7ce3 100644 --- a/packages/roosterjs-content-model-types/lib/parameter/TypeOfBlockGroup.ts +++ b/packages/roosterjs-content-model-types/lib/parameter/TypeOfBlockGroup.ts @@ -1,5 +1,5 @@ -import type { ContentModelBlockGroup } from '../group/ContentModelBlockGroup'; -import type { ContentModelBlockGroupBase } from '../group/ContentModelBlockGroupBase'; +import type { ContentModelBlockGroup } from '../contentModel/blockGroup/ContentModelBlockGroup'; +import type { ContentModelBlockGroupBase } from '../contentModel/blockGroup/ContentModelBlockGroupBase'; /** * Retrieve block group type string from a given block group diff --git a/packages/roosterjs-content-model-types/lib/pluginState/CachePluginState.ts b/packages/roosterjs-content-model-types/lib/pluginState/CachePluginState.ts index e9a32fd6a2b..92a82c36a0d 100644 --- a/packages/roosterjs-content-model-types/lib/pluginState/CachePluginState.ts +++ b/packages/roosterjs-content-model-types/lib/pluginState/CachePluginState.ts @@ -1,5 +1,5 @@ import type { TextMutationObserver } from '../context/TextMutationObserver'; -import type { ContentModelDocument } from '../group/ContentModelDocument'; +import type { ContentModelDocument } from '../contentModel/blockGroup/ContentModelDocument'; import type { DomIndexer } from '../context/DomIndexer'; import type { DOMInsertPoint, diff --git a/packages/roosterjs-content-model-types/lib/pluginState/FormatPluginState.ts b/packages/roosterjs-content-model-types/lib/pluginState/FormatPluginState.ts index e0581094541..5286fc09d71 100644 --- a/packages/roosterjs-content-model-types/lib/pluginState/FormatPluginState.ts +++ b/packages/roosterjs-content-model-types/lib/pluginState/FormatPluginState.ts @@ -1,5 +1,5 @@ import type { DOMInsertPoint } from '../selection/DOMSelection'; -import type { ContentModelSegmentFormat } from '../format/ContentModelSegmentFormat'; +import type { ContentModelSegmentFormat } from '../contentModel/format/ContentModelSegmentFormat'; /** * Pending format holder interface diff --git a/packages/roosterjs-content-model-types/lib/selection/InsertPoint.ts b/packages/roosterjs-content-model-types/lib/selection/InsertPoint.ts index e7de09ee7d2..49f1057d918 100644 --- a/packages/roosterjs-content-model-types/lib/selection/InsertPoint.ts +++ b/packages/roosterjs-content-model-types/lib/selection/InsertPoint.ts @@ -1,6 +1,6 @@ -import type { ContentModelBlockGroup } from '../group/ContentModelBlockGroup'; -import type { ContentModelParagraph } from '../block/ContentModelParagraph'; -import type { ContentModelSelectionMarker } from '../segment/ContentModelSelectionMarker'; +import type { ContentModelBlockGroup } from '../contentModel/blockGroup/ContentModelBlockGroup'; +import type { ContentModelParagraph } from '../contentModel/block/ContentModelParagraph'; +import type { ContentModelSelectionMarker } from '../contentModel/segment/ContentModelSelectionMarker'; import type { TableSelectionContext } from './TableSelectionContext'; /** diff --git a/packages/roosterjs-content-model-types/lib/selection/TableSelectionContext.ts b/packages/roosterjs-content-model-types/lib/selection/TableSelectionContext.ts index af2befb6bd8..50d85d6f108 100644 --- a/packages/roosterjs-content-model-types/lib/selection/TableSelectionContext.ts +++ b/packages/roosterjs-content-model-types/lib/selection/TableSelectionContext.ts @@ -1,4 +1,4 @@ -import type { ContentModelTable } from '../block/ContentModelTable'; +import type { ContentModelTable } from '../contentModel/block/ContentModelTable'; /** * Context object for table in a selection diff --git a/packages/roosterjs-editor-adapter/lib/editor/EditorAdapter.ts b/packages/roosterjs-editor-adapter/lib/editor/EditorAdapter.ts index d6ac3946185..cc9f977ce6a 100644 --- a/packages/roosterjs-editor-adapter/lib/editor/EditorAdapter.ts +++ b/packages/roosterjs-editor-adapter/lib/editor/EditorAdapter.ts @@ -350,11 +350,22 @@ export class EditorAdapter extends Editor implements ILegacyEditor { * @returns HTML string representing current editor content */ getContent(mode: GetContentMode | CompatibleGetContentMode = GetContentMode.CleanHTML): string { - return exportContent( - this, - GetContentModeMap[mode], - this.getCore().environment.modelToDomSettings.customized - ); + const exportMode = GetContentModeMap[mode] ?? 'HTML'; + + switch (exportMode) { + case 'HTML': + return exportContent( + this, + 'HTML', + this.getCore().environment.modelToDomSettings.customized + ); + + case 'PlainText': + return exportContent(this, 'PlainText'); + + case 'PlainTextFast': + return exportContent(this, 'PlainTextFast'); + } } /** diff --git a/packages/roosterjs-editor-adapter/test/editor/EditorAdapterTest.ts b/packages/roosterjs-editor-adapter/test/editor/EditorAdapterTest.ts index 0036c9aafe8..89a8b6cf892 100644 --- a/packages/roosterjs-editor-adapter/test/editor/EditorAdapterTest.ts +++ b/packages/roosterjs-editor-adapter/test/editor/EditorAdapterTest.ts @@ -1,157 +1,7 @@ -import * as createDomToModelContext from 'roosterjs-content-model-dom/lib/domToModel/context/createDomToModelContext'; -import * as domToContentModel from 'roosterjs-content-model-dom/lib/domToModel/domToContentModel'; -import * as findAllEntities from 'roosterjs-content-model-core/lib/corePlugin/entity/findAllEntities'; -import { ContentModelDocument, EditorContext, EditorCore } from 'roosterjs-content-model-types'; import { EditorAdapter } from '../../lib/editor/EditorAdapter'; -import { EditorPlugin, PluginEventType } from 'roosterjs-editor-types'; - -const editorContext: EditorContext = { - isDarkMode: false, -}; +import { EditorCore } from 'roosterjs-content-model-types'; describe('EditorAdapter', () => { - it('domToContentModel', () => { - const mockedResult = 'Result' as any; - const mockedContext = 'MockedContext' as any; - const mockedConfig = 'MockedConfig' as any; - - spyOn(domToContentModel, 'domToContentModel').and.returnValue(mockedResult); - spyOn(createDomToModelContext, 'createDomToModelContextWithConfig').and.returnValue( - mockedContext - ); - spyOn(createDomToModelContext, 'createDomToModelConfig').and.returnValue(mockedConfig); - spyOn(findAllEntities, 'findAllEntities'); - - const div = document.createElement('div'); - const editor = new EditorAdapter(div, { - coreApiOverride: { - createEditorContext: jasmine - .createSpy('createEditorContext') - .and.returnValue(editorContext), - setContentModel: jasmine.createSpy('setContentModel'), - }, - }); - - const model = editor.getContentModelCopy('connected'); - - expect(model).toBe(mockedResult); - expect(domToContentModel.domToContentModel).toHaveBeenCalledTimes(1); - expect(domToContentModel.domToContentModel).toHaveBeenCalledWith(div, mockedContext); - expect(createDomToModelContext.createDomToModelContextWithConfig).toHaveBeenCalledWith( - mockedConfig, - editorContext - ); - }); - - it('domToContentModel, with Reuse Content Model do not add disableCacheElement option', () => { - const mockedResult = 'Result' as any; - const mockedContext = 'MockedContext' as any; - const mockedConfig = 'MockedConfig' as any; - - spyOn(domToContentModel, 'domToContentModel').and.returnValue(mockedResult); - spyOn(createDomToModelContext, 'createDomToModelContextWithConfig').and.returnValue( - mockedContext - ); - spyOn(createDomToModelContext, 'createDomToModelConfig').and.returnValue(mockedConfig); - spyOn(findAllEntities, 'findAllEntities'); - - const div = document.createElement('div'); - const editor = new EditorAdapter(div, { - coreApiOverride: { - createEditorContext: jasmine - .createSpy('createEditorContext') - .and.returnValue(editorContext), - setContentModel: jasmine.createSpy('setContentModel'), - }, - }); - - const model = editor.getContentModelCopy('connected'); - - expect(model).toBe(mockedResult); - expect(domToContentModel.domToContentModel).toHaveBeenCalledTimes(1); - expect(domToContentModel.domToContentModel).toHaveBeenCalledWith(div, mockedContext); - expect(createDomToModelContext.createDomToModelContextWithConfig).toHaveBeenCalledWith( - mockedConfig, - editorContext - ); - }); - - it('createContentModel in EditorReady event', () => { - let model: ContentModelDocument | undefined; - let pluginEditor: any; - - const div = document.createElement('div'); - const plugin: EditorPlugin = { - getName: () => '', - initialize: e => { - pluginEditor = e; - }, - dispose: () => { - pluginEditor = undefined; - }, - onPluginEvent: event => { - if (event.eventType == PluginEventType.EditorReady) { - model = pluginEditor.getContentModelCopy('connected'); - } - }, - }; - const editor = new EditorAdapter(div, { - legacyPlugins: [plugin], - disableCache: true, - }); - editor.dispose(); - - expect(model).toEqual({ - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - format: {}, - segments: [ - { - segmentType: 'SelectionMarker', - format: {}, - isSelected: true, - }, - { - segmentType: 'Br', - format: {}, - }, - ], - cachedElement: jasmine.anything(), - }, - ], - }); - }); - - it('get model with cache', () => { - const div = document.createElement('div'); - const editor = new EditorAdapter(div); - const cachedModel = 'MODEL' as any; - - (editor as any).core.cache.cachedModel = cachedModel; - - spyOn(domToContentModel, 'domToContentModel'); - - const model = editor.getContentModelCopy('connected'); - - expect(model).toBe(cachedModel); - expect(domToContentModel.domToContentModel).not.toHaveBeenCalled(); - }); - - it('formatContentModel', () => { - const div = document.createElement('div'); - const editor = new EditorAdapter(div); - const core = (editor as any).core; - const formatContentModelSpy = spyOn(core.api, 'formatContentModel'); - const callback = jasmine.createSpy('callback'); - const options = 'Options' as any; - - editor.formatContentModel(callback, options); - - expect(formatContentModelSpy).toHaveBeenCalledWith(core, callback, options, undefined); - }); - it('default format', () => { const div = document.createElement('div'); const editor = new EditorAdapter(div, { diff --git a/versions.json b/versions.json index 01511d6a32f..c21bb46201f 100644 --- a/versions.json +++ b/versions.json @@ -1,6 +1,6 @@ { "legacy": "8.62.0", "react": "8.56.0", - "main": "9.2.0", + "main": "9.3.0", "legacyAdapter": "8.62.0" }