From a7a78534669f54636d13344a54f74258b367a44f Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Tue, 17 Oct 2023 09:25:43 -0700 Subject: [PATCH] Standalone editor: Remove dependency to DOM utils (#2151) * Standalone editor: TableOperation * fix build * Standalone editor: Remove dependency to DOM utils * fix build --- .../lib/domUtils/eventUtils.ts | 28 ++++++++++++ .../lib/domUtils/readFile.ts | 19 ++++++++ .../lib/editor/ContentModelEditor.ts | 8 ++++ .../corePlugins/ContentModelCachePlugin.ts | 2 +- .../ContentModelCopyPastePlugin.ts | 23 +++++----- .../corePlugins/ContentModelFormatPlugin.ts | 2 +- .../editor/createContentModelEditorCore.ts | 7 +++ .../lib/editor/overrides/tablePreProcessor.ts | 4 +- .../processPastedContentWacComponents.ts | 12 ++---- .../lib/index.ts | 7 ++- .../lib/publicApi/editing/keyboardDelete.ts | 14 +++--- .../lib/publicApi/format/getFormatState.ts | 3 +- .../lib/publicApi/image/changeImage.ts | 39 ++++++++--------- .../lib/publicApi/image/insertImage.ts | 2 +- .../utils/formatImageWithContentModel.ts | 8 +--- .../lib/publicTypes/ContentModelEditorCore.ts | 6 +++ .../lib/publicTypes/IContentModelEditor.ts | 15 +++++++ .../createContentModelEditorCoreTest.ts | 5 +++ .../publicApi/editing/editingTestCommon.ts | 1 + .../test/publicApi/image/changeImageTest.ts | 43 ++++++++++++++++--- .../test/publicApi/image/insertImageTest.ts | 4 +- .../utils/formatImageWithContentModelTest.ts | 13 +----- 22 files changed, 185 insertions(+), 80 deletions(-) create mode 100644 packages-content-model/roosterjs-content-model-editor/lib/domUtils/eventUtils.ts create mode 100644 packages-content-model/roosterjs-content-model-editor/lib/domUtils/readFile.ts diff --git a/packages-content-model/roosterjs-content-model-editor/lib/domUtils/eventUtils.ts b/packages-content-model/roosterjs-content-model-editor/lib/domUtils/eventUtils.ts new file mode 100644 index 00000000000..b652765096a --- /dev/null +++ b/packages-content-model/roosterjs-content-model-editor/lib/domUtils/eventUtils.ts @@ -0,0 +1,28 @@ +const CTRL_CHAR_CODE = 'Control'; +const ALT_CHAR_CODE = 'Alt'; +const META_CHAR_CODE = 'Meta'; + +/** + * @internal + * Returns true when the event was fired from a modifier key, otherwise false + * @param event The keyboard event object + */ +export function isModifierKey(event: KeyboardEvent): boolean { + const isCtrlKey = event.ctrlKey || event.key === CTRL_CHAR_CODE; + const isAltKey = event.altKey || event.key === ALT_CHAR_CODE; + const isMetaKey = event.metaKey || event.key === META_CHAR_CODE; + + return isCtrlKey || isAltKey || isMetaKey; +} + +/** + * @internal + * Returns true when the event was fired from a key that produces a character value, otherwise false + * This detection is not 100% accurate. event.key is not fully supported by all browsers, and in some browsers (e.g. IE), + * event.key is longer than 1 for num pad input. But here we just want to improve performance as much as possible. + * So if we missed some case here it is still acceptable. + * @param event The keyboard event object + */ +export function isCharacterValue(event: KeyboardEvent): boolean { + return !isModifierKey(event) && !!event.key && event.key.length == 1; +} diff --git a/packages-content-model/roosterjs-content-model-editor/lib/domUtils/readFile.ts b/packages-content-model/roosterjs-content-model-editor/lib/domUtils/readFile.ts new file mode 100644 index 00000000000..73d24290e40 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-editor/lib/domUtils/readFile.ts @@ -0,0 +1,19 @@ +/** + * @internal + * Read a file object and invoke a callback function with the data url of this file + * @param file The file to read + * @param callback the callback to invoke with data url of the file. + * If fail to read, dataUrl will be null + */ +export function readFile(file: File, callback: (dataUrl: string | null) => void) { + if (file) { + const reader = new FileReader(); + reader.onload = () => { + callback(reader.result as string); + }; + reader.onerror = () => { + callback(null); + }; + reader.readAsDataURL(file); + } +} diff --git a/packages-content-model/roosterjs-content-model-editor/lib/editor/ContentModelEditor.ts b/packages-content-model/roosterjs-content-model-editor/lib/editor/ContentModelEditor.ts index 3659d6af7c6..ea0a3a626e6 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/editor/ContentModelEditor.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/editor/ContentModelEditor.ts @@ -3,6 +3,7 @@ import { EditorBase } from 'roosterjs-editor-core'; import type { ContentModelEditorCore } from '../publicTypes/ContentModelEditorCore'; import type { ContentModelEditorOptions, + EditorEnvironment, IContentModelEditor, } from '../publicTypes/IContentModelEditor'; import type { @@ -65,6 +66,13 @@ export default class ContentModelEditor return core.api.setContentModel(core, model, option, onNodeCreated); } + /** + * Get current running environment, such as if editor is running on Mac + */ + getEnvironment(): EditorEnvironment { + return this.getCore().environment; + } + /** * Get current DOM selection */ diff --git a/packages-content-model/roosterjs-content-model-editor/lib/editor/corePlugins/ContentModelCachePlugin.ts b/packages-content-model/roosterjs-content-model-editor/lib/editor/corePlugins/ContentModelCachePlugin.ts index b839a0e5558..eaeebe7cccd 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/editor/corePlugins/ContentModelCachePlugin.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/editor/corePlugins/ContentModelCachePlugin.ts @@ -1,5 +1,5 @@ import { areSameRangeEx } from '../../modelApi/selection/areSameRangeEx'; -import { isCharacterValue } from 'roosterjs-editor-dom'; +import { isCharacterValue } from '../../domUtils/eventUtils'; import { Keys, PluginEventType } from 'roosterjs-editor-types'; import type ContentModelContentChangedEvent from '../../publicTypes/event/ContentModelContentChangedEvent'; import type { ContentModelCachePluginState } from '../../publicTypes/pluginState/ContentModelCachePluginState'; diff --git a/packages-content-model/roosterjs-content-model-editor/lib/editor/corePlugins/ContentModelCopyPastePlugin.ts b/packages-content-model/roosterjs-content-model-editor/lib/editor/corePlugins/ContentModelCopyPastePlugin.ts index c0f09eee411..2e385cda3ce 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/editor/corePlugins/ContentModelCopyPastePlugin.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/editor/corePlugins/ContentModelCopyPastePlugin.ts @@ -1,5 +1,6 @@ import paste from '../../publicApi/utils/paste'; -import { addRangeToSelection, createElement, extractClipboardItems } from 'roosterjs-editor-dom'; +import { addRangeToSelection, extractClipboardItems } from 'roosterjs-editor-dom'; +import { ChangeSource, ColorTransformDirection, PluginEventType } from 'roosterjs-editor-types'; import { cloneModel } from '../../modelApi/common/cloneModel'; import { DeleteResult } from '../../modelApi/edit/utils/DeleteSelectionStep'; import { deleteSelection } from '../../modelApi/edit/deleteSelection'; @@ -23,12 +24,6 @@ import type { PluginWithState, ClipboardData, } from 'roosterjs-editor-types'; -import { - ChangeSource, - PluginEventType, - KnownCreateElementDataIndex, - ColorTransformDirection, -} from 'roosterjs-editor-types'; /** * Copy and paste plugin for handling onCopy and onPaste event @@ -213,10 +208,16 @@ export default class ContentModelCopyPastePlugin implements PluginWithState { - const tempDiv = createElement( - KnownCreateElementDataIndex.CopyPasteTempDiv, - editor.getDocument() - ) as HTMLDivElement; + const tempDiv = editor.getDocument().createElement('div'); + + tempDiv.style.width = '600px'; + tempDiv.style.height = '1px'; + tempDiv.style.overflow = 'hidden'; + tempDiv.style.position = 'fixed'; + tempDiv.style.top = '0'; + tempDiv.style.left = '0'; + tempDiv.style.userSelect = 'text'; + tempDiv.contentEditable = 'true'; editor.getDocument().body.appendChild(tempDiv); diff --git a/packages-content-model/roosterjs-content-model-editor/lib/editor/corePlugins/ContentModelFormatPlugin.ts b/packages-content-model/roosterjs-content-model-editor/lib/editor/corePlugins/ContentModelFormatPlugin.ts index a2940ea4734..ede1aaf8cce 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/editor/corePlugins/ContentModelFormatPlugin.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/editor/corePlugins/ContentModelFormatPlugin.ts @@ -2,7 +2,7 @@ import applyDefaultFormat from '../../publicApi/format/applyDefaultFormat'; import applyPendingFormat from '../../publicApi/format/applyPendingFormat'; import { canApplyPendingFormat, clearPendingFormat } from '../../modelApi/format/pendingFormat'; import { getObjectKeys } from 'roosterjs-content-model-dom'; -import { isCharacterValue } from 'roosterjs-editor-dom'; +import { isCharacterValue } from '../../domUtils/eventUtils'; import { Keys, PluginEventType } from 'roosterjs-editor-types'; import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; import type { IEditor, PluginEvent, PluginWithState } from 'roosterjs-editor-types'; diff --git a/packages-content-model/roosterjs-content-model-editor/lib/editor/createContentModelEditorCore.ts b/packages-content-model/roosterjs-content-model-editor/lib/editor/createContentModelEditorCore.ts index ca919e5612b..b3eba12c47f 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/editor/createContentModelEditorCore.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/editor/createContentModelEditorCore.ts @@ -47,6 +47,8 @@ export const createContentModelEditorCore: CoreCreator< const core = createEditorCore(contentDiv, modifiedOptions) as ContentModelEditorCore; + core.environment = {}; + promoteToContentModelEditorCore(core, modifiedOptions, pluginState); return core; @@ -67,6 +69,7 @@ export function promoteToContentModelEditorCore( promoteCorePluginState(cmCore, pluginState); promoteContentModelInfo(cmCore, options); promoteCoreApi(cmCore); + promoteEnvironment(cmCore); } function promoteCorePluginState( @@ -115,6 +118,10 @@ function promoteCoreApi(cmCore: ContentModelEditorCore) { cmCore.originalApi.setDOMSelection = setDOMSelection; } +function promoteEnvironment(cmCore: ContentModelEditorCore) { + cmCore.environment.isMac = window.navigator.appVersion.indexOf('Mac') != -1; +} + function getPluginState(options: ContentModelEditorOptions): ContentModelPluginState { const format = options.defaultFormat || {}; return { diff --git a/packages-content-model/roosterjs-content-model-editor/lib/editor/overrides/tablePreProcessor.ts b/packages-content-model/roosterjs-content-model-editor/lib/editor/overrides/tablePreProcessor.ts index 5d5c9831c95..08c09a384d4 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/editor/overrides/tablePreProcessor.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/editor/overrides/tablePreProcessor.ts @@ -1,4 +1,3 @@ -import { contains } from 'roosterjs-editor-dom'; import { entityProcessor, hasMetadata, tableProcessor } from 'roosterjs-content-model-dom'; import { getSelectionRootNode } from '../../modelApi/selection/getSelectionRootNode'; import type { DomToModelContext, ElementProcessor } from 'roosterjs-content-model-types'; @@ -13,6 +12,7 @@ export const tablePreProcessor: ElementProcessor = (group, ele }; function shouldUseTableProcessor(element: HTMLTableElement, context: DomToModelContext) { + const selectionRoot = getSelectionRootNode(context.selection); // Treat table as a real table when: // 1. It is a roosterjs table (has metadata) // 2. Table is in selection @@ -21,6 +21,6 @@ function shouldUseTableProcessor(element: HTMLTableElement, context: DomToModelC return ( hasMetadata(element) || context.isInSelection || - contains(element, getSelectionRootNode(context.selection), true /*treatSameNodeAsContain*/) + (selectionRoot && element.contains(selectionRoot)) ); } diff --git a/packages-content-model/roosterjs-content-model-editor/lib/editor/plugins/PastePlugin/WacComponents/processPastedContentWacComponents.ts b/packages-content-model/roosterjs-content-model-editor/lib/editor/plugins/PastePlugin/WacComponents/processPastedContentWacComponents.ts index b65ef004375..24e50482002 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/editor/plugins/PastePlugin/WacComponents/processPastedContentWacComponents.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/editor/plugins/PastePlugin/WacComponents/processPastedContentWacComponents.ts @@ -1,5 +1,4 @@ import addParser from '../utils/addParser'; -import { findClosestElementAncestor, matchesSelector } from 'roosterjs-editor-dom'; import { setProcessor } from '../utils/setProcessor'; import type ContentModelBeforePasteEvent from '../../../../publicTypes/event/ContentModelBeforePasteEvent'; import type { @@ -78,7 +77,8 @@ const wacElementProcessor: ElementProcessor = ( context: DomToModelContext ): void => { const elementTag = element.tagName; - if (matchesSelector(element, WAC_IDENTIFY_SELECTOR)) { + + if (element.matches(WAC_IDENTIFY_SELECTOR)) { element.style.removeProperty('display'); element.style.removeProperty('margin'); } @@ -187,7 +187,7 @@ function shouldClearListContext( return ( context.listFormat.levels.length > 0 && LIST_ELEMENT_TAGS.every(tag => tag != elementTag) && - !findClosestElementAncestor(element, undefined, LIST_ELEMENT_SELECTOR) + !element.closest(LIST_ELEMENT_SELECTOR) ); } @@ -232,11 +232,7 @@ const wacListProcessor: ElementProcessor = context: DomToModelContext ): void => { const lastBlock = group.blocks[group.blocks.length - 1]; - const isWrappedInContainer = findClosestElementAncestor( - element, - undefined, - `.${LIST_CONTAINER_ELEMENT_CLASS_NAME}` - ); + const isWrappedInContainer = element.closest(`.${LIST_CONTAINER_ELEMENT_CLASS_NAME}`); if ( isWrappedInContainer?.previousElementSibling?.classList.contains( LIST_CONTAINER_ELEMENT_CLASS_NAME diff --git a/packages-content-model/roosterjs-content-model-editor/lib/index.ts b/packages-content-model/roosterjs-content-model-editor/lib/index.ts index 18da70422c4..ac90ffdecdf 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/index.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/index.ts @@ -21,7 +21,11 @@ export { ContentModelContentChangedEventData, } from './publicTypes/event/ContentModelContentChangedEvent'; -export { IContentModelEditor, ContentModelEditorOptions } from './publicTypes/IContentModelEditor'; +export { + IContentModelEditor, + ContentModelEditorOptions, + EditorEnvironment, +} from './publicTypes/IContentModelEditor'; export { InsertPoint } from './publicTypes/selection/InsertPoint'; export { TableSelectionContext } from './publicTypes/selection/TableSelectionContext'; export { @@ -96,7 +100,6 @@ export { default as toggleCode } from './publicApi/segment/toggleCode'; export { default as paste } from './publicApi/utils/paste'; export { default as insertEntity } from './publicApi/entity/insertEntity'; export { formatWithContentModel } from './publicApi/utils/formatWithContentModel'; -export { default as keyboardDelete } from './publicApi/editing/keyboardDelete'; export { default as ContentModelEditor } from './editor/ContentModelEditor'; export { default as isContentModelEditor } from './editor/isContentModelEditor'; diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/editing/keyboardDelete.ts b/packages-content-model/roosterjs-content-model-editor/lib/publicApi/editing/keyboardDelete.ts index 305db2020e8..05c3f0580b2 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/editing/keyboardDelete.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/publicApi/editing/keyboardDelete.ts @@ -1,9 +1,9 @@ -import { Browser, isModifierKey } from 'roosterjs-editor-dom'; import { ChangeSource, Keys } from 'roosterjs-editor-types'; import { deleteAllSegmentBefore } from '../../modelApi/edit/deleteSteps/deleteAllSegmentBefore'; import { DeleteResult } from '../../modelApi/edit/utils/DeleteSelectionStep'; import { deleteSelection } from '../../modelApi/edit/deleteSelection'; import { formatWithContentModel } from '../utils/formatWithContentModel'; +import { isModifierKey } from '../../domUtils/eventUtils'; import { isNodeOfType } from 'roosterjs-content-model-dom'; import type { DeleteSelectionStep } from '../../modelApi/edit/utils/DeleteSelectionStep'; import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; @@ -22,6 +22,7 @@ import { } from '../../modelApi/edit/deleteSteps/deleteCollapsedSelection'; /** + * @internal * Do keyboard event handling for DELETE/BACKSPACE key * @param editor The Content Model Editor * @param rawEvent DOM keyboard event @@ -41,8 +42,11 @@ export default function keyboardDelete( editor, which == Keys.DELETE ? 'handleDeleteKey' : 'handleBackspaceKey', (model, context) => { - const result = deleteSelection(model, getDeleteSteps(rawEvent), context) - .deleteResult; + const result = deleteSelection( + model, + getDeleteSteps(rawEvent, !!editor.getEnvironment().isMac), + context + ).deleteResult; isDeleted = result != DeleteResult.NotDeleted; @@ -61,11 +65,11 @@ export default function keyboardDelete( return isDeleted; } -function getDeleteSteps(rawEvent: KeyboardEvent): (DeleteSelectionStep | null)[] { +function getDeleteSteps(rawEvent: KeyboardEvent, isMac: boolean): (DeleteSelectionStep | null)[] { const isForward = rawEvent.which == Keys.DELETE; const deleteAllSegmentBeforeStep = shouldDeleteAllSegmentsBefore(rawEvent) && !isForward ? deleteAllSegmentBefore : null; - const deleteWordSelection = shouldDeleteWord(rawEvent, !!Browser.isMac) + const deleteWordSelection = shouldDeleteWord(rawEvent, isMac) ? isForward ? forwardDeleteWordSelection : backwardDeleteWordSelection diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/format/getFormatState.ts b/packages-content-model/roosterjs-content-model-editor/lib/publicApi/format/getFormatState.ts index 4ec030cbbcf..4e2b4b64cc4 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/format/getFormatState.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/publicApi/format/getFormatState.ts @@ -1,4 +1,3 @@ -import { contains } from 'roosterjs-editor-dom'; import { getPendingFormat } from '../../modelApi/format/pendingFormat'; import { getSelectionRootNode } from '../../modelApi/selection/getSelectionRootNode'; import { retrieveModelFormatState } from '../../modelApi/common/retrieveModelFormatState'; @@ -96,7 +95,7 @@ function createNodeStack(root: Node, startNode: Node): Node[] { const result: Node[] = []; let node: Node | null = startNode; - while (node && contains(root, node)) { + while (node && root != node && root.contains(node)) { if (isNodeOfType(node, 'ELEMENT_NODE') && node.tagName == 'TABLE') { // For table, we can't do a reduced model creation since we need to handle their cells and indexes, // so clean up whatever we already have, and just put table into the stack diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/image/changeImage.ts b/packages-content-model/roosterjs-content-model-editor/lib/publicApi/image/changeImage.ts index b87c7db6cc8..20187fd008b 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/image/changeImage.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/publicApi/image/changeImage.ts @@ -1,5 +1,7 @@ import formatImageWithContentModel from '../utils/formatImageWithContentModel'; -import { getMetadata, readFile } from 'roosterjs-editor-dom'; +import { PluginEventType } from 'roosterjs-editor-types'; +import { readFile } from '../../domUtils/readFile'; +import { updateImageMetadata } from '../../domUtils/metadata/updateImageMetadata'; import type { ContentModelImage } from 'roosterjs-content-model-types'; import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; @@ -14,28 +16,23 @@ export default function changeImage(editor: IContentModelEditor, file: File) { const selection = editor.getDOMSelection(); readFile(file, dataUrl => { if (dataUrl && !editor.isDisposed() && selection?.type === 'image') { - formatImageWithContentModel( - editor, - 'changeImage', - (image: ContentModelImage) => { - image.src = dataUrl; - image.dataset = {}; - image.format.width = ''; - image.format.height = ''; - image.alt = ''; - }, - { + formatImageWithContentModel(editor, 'changeImage', (image: ContentModelImage) => { + const originalSrc = updateImageMetadata(image)?.src ?? ''; + const previousSrc = image.src; + + image.src = dataUrl; + image.dataset = {}; + image.format.width = ''; + image.format.height = ''; + image.alt = ''; + + editor.triggerPluginEvent(PluginEventType.EditImage, { image: selection.image, - previousSrc: selection.image.src, + previousSrc, newSrc: dataUrl, - originalSrc: getImageSrc(selection.image), - } - ); + originalSrc, + }); + }); } }); } - -const getImageSrc = (image: HTMLImageElement) => { - const obj = getMetadata<{ src: string }>(image); - return (obj && obj.src) || ''; -}; diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/image/insertImage.ts b/packages-content-model/roosterjs-content-model-editor/lib/publicApi/image/insertImage.ts index 32d60e9b203..b815bd51fa7 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/image/insertImage.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/publicApi/image/insertImage.ts @@ -1,7 +1,7 @@ import { addSegment, createContentModelDocument, createImage } from 'roosterjs-content-model-dom'; import { formatWithContentModel } from '../utils/formatWithContentModel'; import { mergeModel } from '../../modelApi/common/mergeModel'; -import { readFile } from 'roosterjs-editor-dom'; +import { readFile } from '../../domUtils/readFile'; import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; /** diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/utils/formatImageWithContentModel.ts b/packages-content-model/roosterjs-content-model-editor/lib/publicApi/utils/formatImageWithContentModel.ts index 8fbbced3103..a0f5e4ceb05 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/utils/formatImageWithContentModel.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/publicApi/utils/formatImageWithContentModel.ts @@ -1,7 +1,5 @@ import { formatSegmentWithContentModel } from './formatSegmentWithContentModel'; -import { PluginEventType } from 'roosterjs-editor-types'; import type { ContentModelImage } from 'roosterjs-content-model-types'; -import type { EditImageEventData } from 'roosterjs-editor-types'; import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; /** @@ -10,8 +8,7 @@ import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor' export default function formatImageWithContentModel( editor: IContentModelEditor, apiName: string, - callback: (segment: ContentModelImage) => void, - eventChangeData?: EditImageEventData + callback: (segment: ContentModelImage) => void ) { formatSegmentWithContentModel( editor, @@ -19,9 +16,6 @@ export default function formatImageWithContentModel( (_, __, segment) => { if (segment?.segmentType == 'Image') { callback(segment); - if (eventChangeData) { - editor.triggerPluginEvent(PluginEventType.EditImage, eventChangeData); - } } }, undefined /** segmentHasStyleCallback **/, diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/ContentModelEditorCore.ts b/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/ContentModelEditorCore.ts index 9f0954cbeb2..77c718a272a 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/ContentModelEditorCore.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/ContentModelEditorCore.ts @@ -1,3 +1,4 @@ +import type { EditorEnvironment } from './IContentModelEditor'; import type { ContentModelPluginState } from './pluginState/ContentModelPluginState'; import type { CoreApiMap, EditorCore } from 'roosterjs-editor-types'; import type { @@ -131,4 +132,9 @@ export interface ContentModelEditorCore extends EditorCore, ContentModelPluginSt * will be used for setting content model if there is no other customized options */ defaultModelToDomConfig: ModelToDomSettings; + + /** + * Editor running environment + */ + environment: EditorEnvironment; } diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/IContentModelEditor.ts b/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/IContentModelEditor.ts index b4af108f8b4..95777221d44 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/IContentModelEditor.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/IContentModelEditor.ts @@ -7,6 +7,16 @@ import type { OnNodeCreated, } from 'roosterjs-content-model-types'; +/** + * Current running environment + */ +export interface EditorEnvironment { + /** + * Whether editor is running on Mac + */ + isMac?: boolean; +} + /** * An interface of editor with Content Model support. * (This interface is still under development, and may still be changed in the future with some breaking changes) @@ -36,6 +46,11 @@ export interface IContentModelEditor extends IEditor { onNodeCreated?: OnNodeCreated ): DOMSelection | null; + /** + * Get current running environment, such as if editor is running on Mac + */ + getEnvironment(): EditorEnvironment; + /** * Get current DOM selection. * This is the replacement of IEditor.getSelectionRangeEx. diff --git a/packages-content-model/roosterjs-content-model-editor/test/editor/createContentModelEditorCoreTest.ts b/packages-content-model/roosterjs-content-model-editor/test/editor/createContentModelEditorCoreTest.ts index 6aa479b79b3..b76d619f47a 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/editor/createContentModelEditorCoreTest.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/editor/createContentModelEditorCoreTest.ts @@ -143,6 +143,7 @@ describe('createContentModelEditorCore', () => { }, cache: { domIndexer: undefined }, copyPaste: { allowedCustomPasteType: [] }, + environment: { isMac: false }, } as any); }); @@ -222,6 +223,7 @@ describe('createContentModelEditorCore', () => { domIndexer: undefined, }, copyPaste: { allowedCustomPasteType: [] }, + environment: { isMac: false }, } as any); }); @@ -310,6 +312,7 @@ describe('createContentModelEditorCore', () => { }, cache: { domIndexer: undefined }, copyPaste: { allowedCustomPasteType: [] }, + environment: { isMac: false }, } as any); }); @@ -381,6 +384,7 @@ describe('createContentModelEditorCore', () => { }, cache: { domIndexer: undefined }, copyPaste: { allowedCustomPasteType: [] }, + environment: { isMac: false }, } as any); }); @@ -453,6 +457,7 @@ describe('createContentModelEditorCore', () => { }, cache: { domIndexer: contentModelDomIndexer }, copyPaste: { allowedCustomPasteType: [] }, + environment: { isMac: false }, } as any); }); }); diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/editing/editingTestCommon.ts b/packages-content-model/roosterjs-content-model-editor/test/publicApi/editing/editingTestCommon.ts index f8de272c86a..0a1a3834a15 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/publicApi/editing/editingTestCommon.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/publicApi/editing/editingTestCommon.ts @@ -41,6 +41,7 @@ export function editingTestCommon( triggerContentChangedEvent, getVisibleViewport, isDarkMode: () => false, + getEnvironment: () => ({}), } as any) as IContentModelEditor; executionCallback(editor); diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/image/changeImageTest.ts b/packages-content-model/roosterjs-content-model-editor/test/publicApi/image/changeImageTest.ts index 8e6207581a6..a2fec41de95 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/publicApi/image/changeImageTest.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/publicApi/image/changeImageTest.ts @@ -1,8 +1,9 @@ import * as pendingFormat from '../../../lib/modelApi/format/pendingFormat'; -import * as readFile from 'roosterjs-editor-dom/lib/utils/readFile'; +import * as readFile from '../../../lib/domUtils/readFile'; import changeImage from '../../../lib/publicApi/image/changeImage'; import { ContentModelDocument } from 'roosterjs-content-model-types'; import { IContentModelEditor } from '../../../lib/publicTypes/IContentModelEditor'; +import { PluginEventType } from 'roosterjs-editor-types'; import { addSegment, createContentModelDocument, @@ -13,6 +14,8 @@ import { describe('changeImage', () => { const testUrl = 'http://test.com/test'; const blob = ({ a: 1 } as any) as File; + let imageNode: HTMLImageElement; + let triggerPluginEvent: jasmine.Spy; function runTest( model: ContentModelDocument, @@ -36,12 +39,10 @@ describe('changeImage', () => { spyOn(pendingFormat, 'setPendingFormat'); spyOn(pendingFormat, 'getPendingFormat').and.returnValue(null); - const image = document.createElement('img'); - const getDOMSelection = jasmine .createSpy() - .and.returnValues({ type: 'image', image: image }); - const triggerPluginEvent = jasmine.createSpy().and.callThrough(); + .and.returnValues({ type: 'image', image: imageNode }); + triggerPluginEvent = jasmine.createSpy('triggerPluginEvent').and.callThrough(); const getVisibleViewport = jasmine.createSpy().and.callThrough(); const editor = ({ @@ -65,7 +66,9 @@ describe('changeImage', () => { } beforeEach(() => { - spyOn(readFile, 'default').and.callFake((_, callback) => { + imageNode = document.createElement('img'); + + spyOn(readFile, 'readFile').and.callFake((_, callback) => { callback(testUrl); }); }); @@ -82,6 +85,8 @@ describe('changeImage', () => { }, 0 ); + + expect(triggerPluginEvent).toHaveBeenCalledTimes(0); }); it('Doc without selection', () => { @@ -114,6 +119,8 @@ describe('changeImage', () => { }, 0 ); + + expect(triggerPluginEvent).toHaveBeenCalledTimes(0); }); it('Doc with selection, but no image', () => { @@ -147,6 +154,15 @@ describe('changeImage', () => { }, 1 ); + + expect(triggerPluginEvent).toHaveBeenCalledTimes(1); + expect(triggerPluginEvent).toHaveBeenCalledWith(PluginEventType.ContentChanged, { + contentModel: doc, + selection: undefined, + source: 'Format', + data: undefined, + additionalData: { formatApiName: 'changeImage' }, + }); }); it('Doc with selection and image', () => { @@ -190,5 +206,20 @@ describe('changeImage', () => { }, 1 ); + + expect(triggerPluginEvent).toHaveBeenCalledTimes(2); + expect(triggerPluginEvent).toHaveBeenCalledWith(PluginEventType.EditImage, { + image: imageNode, + newSrc: testUrl, + previousSrc: 'test', + originalSrc: '', + }); + expect(triggerPluginEvent).toHaveBeenCalledWith(PluginEventType.ContentChanged, { + contentModel: doc, + selection: undefined, + source: 'Format', + data: undefined, + additionalData: { formatApiName: 'changeImage' }, + }); }); }); diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/image/insertImageTest.ts b/packages-content-model/roosterjs-content-model-editor/test/publicApi/image/insertImageTest.ts index be0bc6b4518..2beb29181ab 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/publicApi/image/insertImageTest.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/publicApi/image/insertImageTest.ts @@ -1,4 +1,4 @@ -import * as readFile from 'roosterjs-editor-dom/lib/utils/readFile'; +import * as readFile from '../../../lib/domUtils/readFile'; import insertImage from '../../../lib/publicApi/image/insertImage'; import { ContentModelDocument } from 'roosterjs-content-model-types'; import { IContentModelEditor } from '../../../lib/publicTypes/IContentModelEditor'; @@ -52,7 +52,7 @@ describe('insertImage', () => { } beforeEach(() => { - spyOn(readFile, 'default').and.callFake((_, callback) => { + spyOn(readFile, 'readFile').and.callFake((_, callback) => { callback(testUrl); }); }); diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/utils/formatImageWithContentModelTest.ts b/packages-content-model/roosterjs-content-model-editor/test/publicApi/utils/formatImageWithContentModelTest.ts index c27f03bb44e..0cd533b469e 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/publicApi/utils/formatImageWithContentModelTest.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/publicApi/utils/formatImageWithContentModelTest.ts @@ -15,17 +15,15 @@ describe('formatImageWithContentModel', () => { model: ContentModelDocument, result: ContentModelDocument, calledTimes: number, - callback: (image: ContentModelImage) => void, - shouldCallPluginEvent: boolean | any = false + callback: (image: ContentModelImage) => void ) { segmentTestForPluginEvent( 'apiTest', editor => { - formatImageWithContentModel(editor, 'apiTest', callback, shouldCallPluginEvent); + formatImageWithContentModel(editor, 'apiTest', callback); }, model, result, - shouldCallPluginEvent, calledTimes ); } @@ -188,9 +186,6 @@ describe('formatImageWithContentModel', () => { (image: ContentModelImage) => { image.format.borderTop = '1px solid green'; image.format.boxShadow = '0px 0px 3px 3px #aaaaaa'; - }, - { - test: 'test', } ); }); @@ -201,7 +196,6 @@ function segmentTestForPluginEvent( executionCallback: (editor: IContentModelEditor) => void, model: ContentModelDocument, result: ContentModelDocument, - shouldCallPluginEvent: boolean, calledTimes: number ) { spyOn(pendingFormat, 'setPendingFormat'); @@ -232,9 +226,6 @@ function segmentTestForPluginEvent( } as any) as IContentModelEditor; executionCallback(editor); - if (shouldCallPluginEvent) { - expect(triggerPluginEvent).toHaveBeenCalled(); - } expect(addUndoSnapshot).toHaveBeenCalledTimes(calledTimes); expect(setContentModel).toHaveBeenCalledTimes(calledTimes); expect(model).toEqual(result);