From 9c69ea0d40a68bbf3ab500db29334473883fd95d Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Thu, 25 Jan 2024 11:45:56 -0800 Subject: [PATCH] Standalone Editor: Provide a DOMHelper to allow access DOM tree (#2363) --- .../lib/editor/DOMHelperImpl.ts | 17 +++++++++++++ .../lib/editor/StandaloneEditor.ts | 8 ++++++ .../lib/editor/createStandaloneEditorCore.ts | 2 ++ .../test/editor/DOMHelperImplTest.ts | 20 +++++++++++++++ .../test/editor/StandaloneEditorTest.ts | 25 +++++++++++++++++++ .../editor/createStandaloneEditorCoreTest.ts | 4 +++ .../lib/editor/IStandaloneEditor.ts | 6 +++++ .../lib/editor/StandaloneEditorCore.ts | 6 +++++ .../lib/index.ts | 1 + .../lib/parameter/DOMHelper.ts | 22 ++++++++++++++++ 10 files changed, 111 insertions(+) create mode 100644 packages-content-model/roosterjs-content-model-core/lib/editor/DOMHelperImpl.ts create mode 100644 packages-content-model/roosterjs-content-model-core/test/editor/DOMHelperImplTest.ts create mode 100644 packages-content-model/roosterjs-content-model-types/lib/parameter/DOMHelper.ts diff --git a/packages-content-model/roosterjs-content-model-core/lib/editor/DOMHelperImpl.ts b/packages-content-model/roosterjs-content-model-core/lib/editor/DOMHelperImpl.ts new file mode 100644 index 00000000000..4f68d601711 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-core/lib/editor/DOMHelperImpl.ts @@ -0,0 +1,17 @@ +import { toArray } from 'roosterjs-content-model-dom'; +import type { DOMHelper } from 'roosterjs-content-model-types'; + +class DOMHelperImpl implements DOMHelper { + constructor(private contentDiv: HTMLElement) {} + + queryElements(selector: string): HTMLElement[] { + return toArray(this.contentDiv.querySelectorAll(selector)) as HTMLElement[]; + } +} + +/** + * @internal Create new instance of DOMHelper + */ +export function createDOMHelper(contentDiv: HTMLElement): DOMHelper { + return new DOMHelperImpl(contentDiv); +} diff --git a/packages-content-model/roosterjs-content-model-core/lib/editor/StandaloneEditor.ts b/packages-content-model/roosterjs-content-model-core/lib/editor/StandaloneEditor.ts index 00e76f285a0..494898daa15 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/editor/StandaloneEditor.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/editor/StandaloneEditor.ts @@ -8,6 +8,7 @@ import type { ContentModelSegmentFormat, DarkColorHandler, DOMEventRecord, + DOMHelper, DOMSelection, DomToModelOption, EditorEnvironment, @@ -158,6 +159,13 @@ export class StandaloneEditor implements IStandaloneEditor { return this.getCore().format.pendingFormat?.format ?? null; } + /** + * Get a DOM Helper object to help access DOM tree in editor + */ + getDOMHelper(): DOMHelper { + return this.getCore().domHelper; + } + /** * Add a single undo snapshot to undo stack */ diff --git a/packages-content-model/roosterjs-content-model-core/lib/editor/createStandaloneEditorCore.ts b/packages-content-model/roosterjs-content-model-core/lib/editor/createStandaloneEditorCore.ts index d208bc568b4..f6cfdbc86fc 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/editor/createStandaloneEditorCore.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/editor/createStandaloneEditorCore.ts @@ -1,4 +1,5 @@ import { createDarkColorHandler } from './DarkColorHandlerImpl'; +import { createDOMHelper } from './DOMHelperImpl'; import { createStandaloneEditorCorePlugins } from '../corePlugin/createStandaloneEditorCorePlugins'; import { standaloneCoreApiMap } from './standaloneCoreApiMap'; import { @@ -49,6 +50,7 @@ export function createStandaloneEditorCore( trustedHTMLHandler: options.trustedHTMLHandler || defaultTrustHtmlHandler, domToModelSettings: createDomToModelSettings(options), modelToDomSettings: createModelToDomSettings(options), + domHelper: createDOMHelper(contentDiv), ...getPluginState(corePlugins), disposeErrorHandler: options.disposeErrorHandler, zoomScale: (options.zoomScale ?? -1) > 0 ? options.zoomScale! : 1, diff --git a/packages-content-model/roosterjs-content-model-core/test/editor/DOMHelperImplTest.ts b/packages-content-model/roosterjs-content-model-core/test/editor/DOMHelperImplTest.ts new file mode 100644 index 00000000000..e5d3b243f96 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-core/test/editor/DOMHelperImplTest.ts @@ -0,0 +1,20 @@ +import { createDOMHelper } from '../../lib/editor/DOMHelperImpl'; + +describe('DOMHelperImpl', () => { + it('queryElements', () => { + const mockedResult = ['RESULT'] as any; + const querySelectorAllSpy = jasmine + .createSpy('querySelectorAll') + .and.returnValue(mockedResult); + const mockedDiv: HTMLElement = { + querySelectorAll: querySelectorAllSpy, + } as any; + const mockedSelector = 'SELECTOR'; + const domHelper = createDOMHelper(mockedDiv); + + const result = domHelper.queryElements(mockedSelector); + + expect(result).toEqual(mockedResult); + expect(querySelectorAllSpy).toHaveBeenCalledWith(mockedSelector); + }); +}); diff --git a/packages-content-model/roosterjs-content-model-core/test/editor/StandaloneEditorTest.ts b/packages-content-model/roosterjs-content-model-core/test/editor/StandaloneEditorTest.ts index 7cc680eee62..2d10bc22ed0 100644 --- a/packages-content-model/roosterjs-content-model-core/test/editor/StandaloneEditorTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/editor/StandaloneEditorTest.ts @@ -368,6 +368,31 @@ describe('StandaloneEditor', () => { expect(() => editor.takeSnapshot()).toThrow(); }); + it('getDOMHelper', () => { + const div = document.createElement('div'); + const mockedDOMHelper = 'DOMHELPER' as any; + const resetSpy = jasmine.createSpy('reset'); + const mockedCore = { + plugins: [], + domHelper: mockedDOMHelper, + darkColorHandler: { + updateKnownColor: updateKnownColorSpy, + reset: resetSpy, + }, + } as any; + + createEditorCoreSpy.and.returnValue(mockedCore); + + const editor = new StandaloneEditor(div); + const domHelper = editor.getDOMHelper(); + + expect(domHelper).toBe(mockedDOMHelper); + + editor.dispose(); + expect(resetSpy).toHaveBeenCalledWith(); + expect(() => editor.takeSnapshot()).toThrow(); + }); + it('restoreSnapshot', () => { const div = document.createElement('div'); const mockedSnapshot = 'SNAPSHOT' as any; diff --git a/packages-content-model/roosterjs-content-model-core/test/editor/createStandaloneEditorCoreTest.ts b/packages-content-model/roosterjs-content-model-core/test/editor/createStandaloneEditorCoreTest.ts index cd6e4dfa861..6f50bb315a8 100644 --- a/packages-content-model/roosterjs-content-model-core/test/editor/createStandaloneEditorCoreTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/editor/createStandaloneEditorCoreTest.ts @@ -1,6 +1,7 @@ import * as createDefaultSettings from '../../lib/editor/createStandaloneEditorDefaultSettings'; import * as createStandaloneEditorCorePlugins from '../../lib/corePlugin/createStandaloneEditorCorePlugins'; import * as DarkColorHandlerImpl from '../../lib/editor/DarkColorHandlerImpl'; +import * as DOMHelperImpl from '../../lib/editor/DOMHelperImpl'; import { standaloneCoreApiMap } from '../../lib/editor/standaloneCoreApiMap'; import { StandaloneEditorCore, StandaloneEditorOptions } from 'roosterjs-content-model-types'; import { @@ -37,6 +38,7 @@ describe('createEditorCore', () => { const mockedDarkColorHandler = 'DARKCOLOR' as any; const mockedDomToModelSettings = 'DOMTOMODEL' as any; const mockedModelToDomSettings = 'MODELTODOM' as any; + const mockedDOMHelper = 'DOMHELPER' as any; beforeEach(() => { spyOn( @@ -52,6 +54,7 @@ describe('createEditorCore', () => { spyOn(createDefaultSettings, 'createModelToDomSettings').and.returnValue( mockedModelToDomSettings ); + spyOn(DOMHelperImpl, 'createDOMHelper').and.returnValue(mockedDOMHelper); }); function runTest( @@ -92,6 +95,7 @@ describe('createEditorCore', () => { entity: 'entity' as any, selection: 'selection' as any, undo: 'undo' as any, + domHelper: mockedDOMHelper, disposeErrorHandler: undefined, zoomScale: 1, ...additionalResult, diff --git a/packages-content-model/roosterjs-content-model-types/lib/editor/IStandaloneEditor.ts b/packages-content-model/roosterjs-content-model-types/lib/editor/IStandaloneEditor.ts index bee62884242..3d22af37544 100644 --- a/packages-content-model/roosterjs-content-model-types/lib/editor/IStandaloneEditor.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/editor/IStandaloneEditor.ts @@ -1,3 +1,4 @@ +import type { DOMHelper } from '../parameter/DOMHelper'; import type { PluginEventData, PluginEventFromType } from '../event/PluginEventData'; import type { PluginEventType } from '../event/PluginEventType'; import type { PasteType } from '../enum/PasteType'; @@ -90,6 +91,11 @@ export interface IStandaloneEditor { */ isDisposed(): boolean; + /** + * Get a DOM Helper object to help access DOM tree in editor + */ + getDOMHelper(): DOMHelper; + /** * Get document which contains this editor * @returns The HTML document which contains this editor diff --git a/packages-content-model/roosterjs-content-model-types/lib/editor/StandaloneEditorCore.ts b/packages-content-model/roosterjs-content-model-types/lib/editor/StandaloneEditorCore.ts index c256bbe7435..60035289328 100644 --- a/packages-content-model/roosterjs-content-model-types/lib/editor/StandaloneEditorCore.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/editor/StandaloneEditorCore.ts @@ -1,3 +1,4 @@ +import type { DOMHelper } from '../parameter/DOMHelper'; import type { PluginEvent } from '../event/PluginEvent'; import type { PluginState } from '../pluginState/PluginState'; import type { EditorPlugin } from './EditorPlugin'; @@ -340,6 +341,11 @@ export interface StandaloneEditorCore extends PluginState { */ readonly trustedHTMLHandler: TrustedHTMLHandler; + /** + * A helper class to provide DOM access APIs + */ + readonly domHelper: DOMHelper; + /** * A callback to be invoked when any exception is thrown during disposing editor * @param plugin The plugin that causes exception diff --git a/packages-content-model/roosterjs-content-model-types/lib/index.ts b/packages-content-model/roosterjs-content-model-types/lib/index.ts index e43dcd77bc4..47efd401494 100644 --- a/packages-content-model/roosterjs-content-model-types/lib/index.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/index.ts @@ -284,6 +284,7 @@ export { AnnounceData, KnownAnnounceStrings } from './parameter/AnnounceData'; export { TrustedHTMLHandler } from './parameter/TrustedHTMLHandler'; export { Rect } from './parameter/Rect'; export { ValueSanitizer } from './parameter/ValueSanitizer'; +export { DOMHelper } from './parameter/DOMHelper'; export { BasePluginEvent, BasePluginDomEvent } from './event/BasePluginEvent'; export { BeforeCutCopyEvent } from './event/BeforeCutCopyEvent'; diff --git a/packages-content-model/roosterjs-content-model-types/lib/parameter/DOMHelper.ts b/packages-content-model/roosterjs-content-model-types/lib/parameter/DOMHelper.ts new file mode 100644 index 00000000000..ceb73564a90 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-types/lib/parameter/DOMHelper.ts @@ -0,0 +1,22 @@ +/** + * A helper class to provide DOM access APIs + */ +export interface DOMHelper { + /** + * Query HTML elements in editor by tag name. + * Be careful of this function since it will also return element under entity. + * @param tag Tag name of the element to query + * @returns HTML Element array of the query result + */ + queryElements( + tag: TTag + ): HTMLElementTagNameMap[TTag][]; + + /** + * Query HTML elements in editor by a selector string + * Be careful of this function since it will also return element under entity. + * @param selector Selector string to query + * @returns HTML Element array of the query result + */ + queryElements(selector: string): HTMLElement[]; +}