From ee25e975777fc6b67e3f70339e20eed9fc24b244 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Fri, 17 Nov 2023 14:21:54 -0800 Subject: [PATCH] Standalone Editor: CreateStandaloneEditorCore --- .../lib/coreApi/formatContentModel.ts | 2 +- .../lib/coreApi/getVisibleViewport.ts | 69 +++ .../createStandaloneEditorCorePlugins.ts | 1 + .../lib/editor/DarkColorHandlerImpl.ts | 36 +- .../lib/editor/createStandaloneEditorCore.ts | 76 +++ .../createStandaloneEditorDefaultSettings.ts | 1 + .../lib/editor/standaloneCoreApiMap.ts | 3 + .../roosterjs-content-model-core/lib/index.ts | 5 +- .../roosterjs-content-model-core/package.json | 1 + .../test/coreApi/formatContentModelTest.ts | 2 +- .../test/coreApi/getVisibleViewportTest.ts | 38 ++ .../test/editor/DarkColorHandlerImplTest.ts | 549 ++++++++++++++++++ .../backgroundColorFormatHandlerTest.ts | 11 +- .../segment/textColorFormatHandlerTest.ts | 11 +- .../lib/coreApi/coreApiMap.ts | 6 +- .../lib/corePlugins/createCorePlugins.ts | 14 +- .../lib/editor/ContentModelEditor.ts | 4 +- .../lib/editor/createEditorCore.ts | 78 +-- .../lib/index.ts | 5 +- .../publicTypes/ContentModelCorePlugins.ts | 15 +- .../lib/publicTypes/IContentModelEditor.ts | 31 +- .../test/editor/createEditorCoreTest.ts | 14 +- .../test/paste/e2e/testUtils.ts | 16 +- .../lib/editor/StandaloneEditorCore.ts | 29 +- .../lib/editor/StandaloneEditorOptions.ts | 27 +- .../lib/index.ts | 6 +- ...tate.ts => StandaloneEditorPluginState.ts} | 11 +- 27 files changed, 902 insertions(+), 159 deletions(-) create mode 100644 packages-content-model/roosterjs-content-model-core/lib/coreApi/getVisibleViewport.ts rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-core}/lib/editor/DarkColorHandlerImpl.ts (85%) create mode 100644 packages-content-model/roosterjs-content-model-core/lib/editor/createStandaloneEditorCore.ts create mode 100644 packages-content-model/roosterjs-content-model-core/test/coreApi/getVisibleViewportTest.ts create mode 100644 packages-content-model/roosterjs-content-model-core/test/editor/DarkColorHandlerImplTest.ts rename packages-content-model/roosterjs-content-model-types/lib/pluginState/{ContentModelPluginState.ts => StandaloneEditorPluginState.ts} (79%) diff --git a/packages-content-model/roosterjs-content-model-core/lib/coreApi/formatContentModel.ts b/packages-content-model/roosterjs-content-model-core/lib/coreApi/formatContentModel.ts index 4c3f48a7166..08bd752028d 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/coreApi/formatContentModel.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/coreApi/formatContentModel.ts @@ -141,7 +141,7 @@ function handleDeletedEntities(core: StandaloneEditorCore, context: FormatWithCo function handleImages(core: StandaloneEditorCore, context: FormatWithContentModelContext) { if (context.newImages.length > 0) { - const viewport = core.getVisibleViewport(); + const viewport = core.api.getVisibleViewport(core); if (viewport) { const { left, right } = viewport; diff --git a/packages-content-model/roosterjs-content-model-core/lib/coreApi/getVisibleViewport.ts b/packages-content-model/roosterjs-content-model-core/lib/coreApi/getVisibleViewport.ts new file mode 100644 index 00000000000..de6dc3d30c3 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-core/lib/coreApi/getVisibleViewport.ts @@ -0,0 +1,69 @@ +import type { Rect } from 'roosterjs-editor-types'; +import type { GetVisibleViewport } from 'roosterjs-content-model-types'; + +/** + * @internal + * Retrieves the rect of the visible viewport of the editor. + * @param core The StandaloneEditorCore object + */ +export const getVisibleViewport: GetVisibleViewport = core => { + const scrollContainer = core.domEvent.scrollContainer; + + return getIntersectedRect( + scrollContainer == core.contentDiv ? [scrollContainer] : [scrollContainer, core.contentDiv] + ); +}; + +/** + * Get the intersected Rect of elements provided + * + * @example + * The result of the following Elements Rects would be: + { + top: Element2.top, + bottom: Element1.bottom, + left: Element2.left, + right: Element2.right + } + +-------------------------+ + | Element 1 | + | +-----------------+ | + | | Element2 | | + | | | | + | | | | + +-------------------------+ + | | + +-----------------+ + + * @param elements Elements to use. + * @param additionalRects additional rects to use + * @returns If the Rect is valid return the rect, if not, return null. + */ +function getIntersectedRect(elements: HTMLElement[], additionalRects: Rect[] = []): Rect | null { + const rects = elements + .map(element => normalizeRect(element.getBoundingClientRect())) + .concat(additionalRects) + .filter((rect: Rect | null): rect is Rect => !!rect); + + const result: Rect = { + top: Math.max(...rects.map(r => r.top)), + bottom: Math.min(...rects.map(r => r.bottom)), + left: Math.max(...rects.map(r => r.left)), + right: Math.min(...rects.map(r => r.right)), + }; + + return result.top < result.bottom && result.left < result.right ? result : null; +} + +function normalizeRect(clientRect: DOMRect): Rect | null { + const { left, right, top, bottom } = + clientRect || { left: 0, right: 0, top: 0, bottom: 0 }; + return left === 0 && right === 0 && top === 0 && bottom === 0 + ? null + : { + left: Math.round(left), + right: Math.round(right), + top: Math.round(top), + bottom: Math.round(bottom), + }; +} diff --git a/packages-content-model/roosterjs-content-model-core/lib/corePlugin/createStandaloneEditorCorePlugins.ts b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/createStandaloneEditorCorePlugins.ts index 64b800c7592..5e7e38c683c 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/corePlugin/createStandaloneEditorCorePlugins.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/createStandaloneEditorCorePlugins.ts @@ -8,6 +8,7 @@ import type { } from 'roosterjs-content-model-types'; /** + * @internal * Create core plugins for standalone editor * @param options Options of editor */ diff --git a/packages-content-model/roosterjs-content-model-editor/lib/editor/DarkColorHandlerImpl.ts b/packages-content-model/roosterjs-content-model-core/lib/editor/DarkColorHandlerImpl.ts similarity index 85% rename from packages-content-model/roosterjs-content-model-editor/lib/editor/DarkColorHandlerImpl.ts rename to packages-content-model/roosterjs-content-model-core/lib/editor/DarkColorHandlerImpl.ts index 0f9687fcc6e..cda3a6bb61d 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/editor/DarkColorHandlerImpl.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/editor/DarkColorHandlerImpl.ts @@ -1,3 +1,4 @@ +import * as Color from 'color'; import { getObjectKeys, parseColor, setColor } from 'roosterjs-editor-dom'; import type { ColorKeyAndValue, @@ -5,6 +6,7 @@ import type { ModeIndependentColor, } from 'roosterjs-editor-types'; +const DefaultLightness = 21.25; // Lightness for #333333 const VARIABLE_REGEX = /^\s*var\(\s*(\-\-[a-zA-Z0-9\-_]+)\s*(?:,\s*(.*))?\)\s*$/; const VARIABLE_PREFIX = 'var('; const COLOR_VAR_PREFIX = 'darkColor'; @@ -28,8 +30,11 @@ const ColorAttributeName: { [key in ColorAttributeEnum]: string }[] = [ */ export class DarkColorHandlerImpl implements DarkColorHandler { private knownColors: Record> = {}; + readonly baseLightness: number; - constructor(private contentDiv: HTMLElement, private getDarkColor: (color: string) => string) {} + constructor(private contentDiv: HTMLElement, baseDarkColor?: string) { + this.baseLightness = getLightness(baseDarkColor); + } /** * Get a copy of known colors @@ -61,7 +66,7 @@ export class DarkColorHandlerImpl implements DarkColorHandler { colorKey || `--${COLOR_VAR_PREFIX}_${lightModeColor.replace(/[^\d\w]/g, '_')}`; if (!this.knownColors[colorKey]) { - darkModeColor = darkModeColor || this.getDarkColor(lightModeColor); + darkModeColor = darkModeColor || getDarkColor(lightModeColor, this.baseLightness); this.knownColors[colorKey] = { lightModeColor, darkModeColor }; this.contentDiv.style.setProperty(colorKey, darkModeColor); @@ -171,3 +176,30 @@ export class DarkColorHandlerImpl implements DarkColorHandler { }); } } + +function getDarkColor(color: string, baseLightness: number): string { + try { + const computedColor = Color(color || undefined); + const colorLab = computedColor.lab().array(); + const newLValue = (100 - colorLab[0]) * ((100 - baseLightness) / 100) + baseLightness; + color = Color.lab(newLValue, colorLab[1], colorLab[2]) + .rgb() + .alpha(computedColor.alpha()) + .toString(); + } catch {} + + return color; +} + +function getLightness(color?: string): number { + let result = DefaultLightness; + + if (color) { + try { + const computedColor = Color(color || undefined); + result = computedColor.lab().array()[0]; + } catch {} + } + + return result; +} 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 new file mode 100644 index 00000000000..d23409d641a --- /dev/null +++ b/packages-content-model/roosterjs-content-model-core/lib/editor/createStandaloneEditorCore.ts @@ -0,0 +1,76 @@ +import { createStandaloneEditorCorePlugins } from '../corePlugin/createStandaloneEditorCorePlugins'; +import { createStandaloneEditorDefaultSettings } from './createStandaloneEditorDefaultSettings'; +import { DarkColorHandlerImpl } from './DarkColorHandlerImpl'; +import { standaloneCoreApiMap } from './standaloneCoreApiMap'; +import type { + EditorEnvironment, + StandaloneEditorCore, + StandaloneEditorCorePlugins, + StandaloneEditorOptions, + UnportedCoreApiMap, + UnportedCorePluginState, +} from 'roosterjs-content-model-types'; + +/** + * A temporary function to create Standalone Editor core + * @param contentDiv Editor content DIV + * @param options Editor options + */ +export function createStandaloneEditorCore( + contentDiv: HTMLDivElement, + options: StandaloneEditorOptions, + unportedCoreApiMap: UnportedCoreApiMap, + unportedCorePluginState: UnportedCorePluginState +): StandaloneEditorCore { + const corePlugins = createStandaloneEditorCorePlugins(options, contentDiv); + + return { + contentDiv, + api: { ...standaloneCoreApiMap, ...unportedCoreApiMap, ...options.coreApiOverride }, + originalApi: { ...standaloneCoreApiMap, ...unportedCoreApiMap }, + plugins: [ + corePlugins.cache, + corePlugins.format, + corePlugins.copyPaste, + corePlugins.domEvent, + // TODO: Add additional plugins here + ], + environment: createEditorEnvironment(), + darkColorHandler: new DarkColorHandlerImpl(contentDiv, options.baseDarkColor), + imageSelectionBorderColor: options.imageSelectionBorderColor, // TODO: Move to Selection core plugin + trustedHTMLHandler: options.trustedHTMLHandler || defaultTrustHtmlHandler, + ...createStandaloneEditorDefaultSettings(options), + ...getPluginState(corePlugins), + ...unportedCorePluginState, + }; +} + +function createEditorEnvironment(): EditorEnvironment { + // It is ok to use global window here since the environment should always be the same for all windows in one session + const userAgent = window.navigator.userAgent; + + return { + isMac: window.navigator.appVersion.indexOf('Mac') != -1, + isAndroid: /android/i.test(userAgent), + isSafari: + userAgent.indexOf('Safari') >= 0 && + userAgent.indexOf('Chrome') < 0 && + userAgent.indexOf('Android') < 0, + }; +} + +/** + * @internal export for test only + */ +export function defaultTrustHtmlHandler(html: string) { + return html; +} + +function getPluginState(corePlugins: StandaloneEditorCorePlugins) { + return { + domEvent: corePlugins.domEvent.getState(), + copyPaste: corePlugins.copyPaste.getState(), + cache: corePlugins.cache.getState(), + format: corePlugins.format.getState(), + }; +} diff --git a/packages-content-model/roosterjs-content-model-core/lib/editor/createStandaloneEditorDefaultSettings.ts b/packages-content-model/roosterjs-content-model-core/lib/editor/createStandaloneEditorDefaultSettings.ts index b46db72068f..e7a600fd4bd 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/editor/createStandaloneEditorDefaultSettings.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/editor/createStandaloneEditorDefaultSettings.ts @@ -9,6 +9,7 @@ import type { } from 'roosterjs-content-model-types'; /** + * @internal * Create default DOM and Content Model conversion settings for a standalone editor * @param options The editor options */ diff --git a/packages-content-model/roosterjs-content-model-core/lib/editor/standaloneCoreApiMap.ts b/packages-content-model/roosterjs-content-model-core/lib/editor/standaloneCoreApiMap.ts index 059d317eccc..5092a757f71 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/editor/standaloneCoreApiMap.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/editor/standaloneCoreApiMap.ts @@ -2,12 +2,14 @@ import { createContentModel } from '../coreApi/createContentModel'; import { createEditorContext } from '../coreApi/createEditorContext'; import { formatContentModel } from '../coreApi/formatContentModel'; import { getDOMSelection } from '../coreApi/getDOMSelection'; +import { getVisibleViewport } from '../coreApi/getVisibleViewport'; import { setContentModel } from '../coreApi/setContentModel'; import { setDOMSelection } from '../coreApi/setDOMSelection'; import { switchShadowEdit } from '../coreApi/switchShadowEdit'; import type { PortedCoreApiMap } from 'roosterjs-content-model-types'; /** + * @internal * Core API map for Standalone Content Model Editor */ export const standaloneCoreApiMap: PortedCoreApiMap = { @@ -18,4 +20,5 @@ export const standaloneCoreApiMap: PortedCoreApiMap = { setContentModel: setContentModel, setDOMSelection: setDOMSelection, switchShadowEdit: switchShadowEdit, + getVisibleViewport: getVisibleViewport, }; diff --git a/packages-content-model/roosterjs-content-model-core/lib/index.ts b/packages-content-model/roosterjs-content-model-core/lib/index.ts index 149f6c5c67b..ec683666bc8 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/index.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/index.ts @@ -40,12 +40,9 @@ export { updateTableCellMetadata } from './metadata/updateTableCellMetadata'; export { updateTableMetadata } from './metadata/updateTableMetadata'; export { updateListMetadata } from './metadata/updateListMetadata'; -export { standaloneCoreApiMap } from './editor/standaloneCoreApiMap'; -export { createStandaloneEditorDefaultSettings } from './editor/createStandaloneEditorDefaultSettings'; - export { ChangeSource } from './constants/ChangeSource'; export { BulletListType } from './constants/BulletListType'; export { NumberingListType } from './constants/NumberingListType'; export { TableBorderFormat } from './constants/TableBorderFormat'; -export { createStandaloneEditorCorePlugins } from './corePlugin/createStandaloneEditorCorePlugins'; +export { createStandaloneEditorCore } from './editor/createStandaloneEditorCore'; diff --git a/packages-content-model/roosterjs-content-model-core/package.json b/packages-content-model/roosterjs-content-model-core/package.json index c037037dea2..40d2aab4f68 100644 --- a/packages-content-model/roosterjs-content-model-core/package.json +++ b/packages-content-model/roosterjs-content-model-core/package.json @@ -3,6 +3,7 @@ "description": "Content Model for roosterjs (Under development)", "dependencies": { "tslib": "^2.3.1", + "color": "^3.0.0", "roosterjs-editor-types": "", "roosterjs-editor-dom": "", "roosterjs-content-model-dom": "", diff --git a/packages-content-model/roosterjs-content-model-core/test/coreApi/formatContentModelTest.ts b/packages-content-model/roosterjs-content-model-core/test/coreApi/formatContentModelTest.ts index b518e96e7e2..fab0ddf50ec 100644 --- a/packages-content-model/roosterjs-content-model-core/test/coreApi/formatContentModelTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/coreApi/formatContentModelTest.ts @@ -429,7 +429,7 @@ describe('formatContentModel', () => { const getVisibleViewportSpy = jasmine .createSpy('getVisibleViewport') .and.returnValue({ top: 100, bottom: 200, left: 100, right: 200 }); - core.getVisibleViewport = getVisibleViewportSpy; + core.api.getVisibleViewport = getVisibleViewportSpy; formatContentModel( core, diff --git a/packages-content-model/roosterjs-content-model-core/test/coreApi/getVisibleViewportTest.ts b/packages-content-model/roosterjs-content-model-core/test/coreApi/getVisibleViewportTest.ts new file mode 100644 index 00000000000..785895374bb --- /dev/null +++ b/packages-content-model/roosterjs-content-model-core/test/coreApi/getVisibleViewportTest.ts @@ -0,0 +1,38 @@ +import { getVisibleViewport } from '../../lib/coreApi/getVisibleViewport'; + +describe('getVisibleViewport', () => { + it('scrollContainer is same with contentDiv', () => { + const div = { + getBoundingClientRect: () => ({ left: 100, right: 200, top: 300, bottom: 400 }), + }; + const core = { + contentDiv: div, + domEvent: { + scrollContainer: div, + }, + } as any; + + const result = getVisibleViewport(core); + + expect(result).toEqual({ left: 100, right: 200, top: 300, bottom: 400 }); + }); + + it('scrollContainer is different than contentDiv', () => { + const div1 = { + getBoundingClientRect: () => ({ left: 100, right: 200, top: 300, bottom: 400 }), + }; + const div2 = { + getBoundingClientRect: () => ({ left: 150, right: 250, top: 350, bottom: 450 }), + }; + const core = { + contentDiv: div1, + domEvent: { + scrollContainer: div2, + }, + } as any; + + const result = getVisibleViewport(core); + + expect(result).toEqual({ left: 150, right: 200, top: 350, bottom: 400 }); + }); +}); diff --git a/packages-content-model/roosterjs-content-model-core/test/editor/DarkColorHandlerImplTest.ts b/packages-content-model/roosterjs-content-model-core/test/editor/DarkColorHandlerImplTest.ts new file mode 100644 index 00000000000..0372c39c164 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-core/test/editor/DarkColorHandlerImplTest.ts @@ -0,0 +1,549 @@ +import { ColorKeyAndValue } from 'roosterjs-editor-types'; +import { DarkColorHandlerImpl } from '../../lib/editor/DarkColorHandlerImpl'; + +describe('DarkColorHandlerImpl.ctor', () => { + it('No additional param', () => { + const div = document.createElement('div'); + const handler = new DarkColorHandlerImpl(div); + + expect(handler).toBeDefined(); + expect(handler.baseLightness).toBe(21.25); + }); + + it('With customized base color', () => { + const div = document.createElement('div'); + const handler = new DarkColorHandlerImpl(div, '#555555'); + + expect(handler).toBeDefined(); + expect(Math.round(handler.baseLightness)).toBe(36); + }); + + it('Calculate color using customized base color', () => { + const div = document.createElement('div'); + const handler = new DarkColorHandlerImpl(div, '#555555'); + + const darkColor = handler.registerColor('red', true); + const parsedColor = handler.parseColorValue(darkColor); + + expect(darkColor).toBe('var(--darkColor_red, red)'); + expect(parsedColor).toEqual({ + key: '--darkColor_red', + lightModeColor: 'red', + darkModeColor: 'rgb(255, 72, 40)', + }); + }); +}); + +describe('DarkColorHandlerImpl.parseColorValue', () => { + let div: HTMLElement; + let handler: DarkColorHandlerImpl; + + beforeEach(() => { + div = document.createElement('div'); + handler = new DarkColorHandlerImpl(div); + }); + + function runTest(input: string, expectedOutput: ColorKeyAndValue) { + const result = handler.parseColorValue(input); + + expect(result).toEqual(expectedOutput); + } + + it('empty color', () => { + runTest(null!, { + key: undefined, + lightModeColor: '', + darkModeColor: undefined, + }); + }); + + it('simple color', () => { + runTest('aa', { + key: undefined, + lightModeColor: 'aa', + darkModeColor: undefined, + }); + }); + + it('var color without fallback', () => { + runTest('var(--bb)', { + key: undefined, + lightModeColor: '', + darkModeColor: undefined, + }); + }); + + it('var color with fallback', () => { + runTest('var(--bb,cc)', { + key: '--bb', + lightModeColor: 'cc', + darkModeColor: undefined, + }); + }); + + it('var color with fallback, has dark color', () => { + (handler as any).knownColors = { + '--bb': { + lightModeColor: 'dd', + darkModeColor: 'ee', + }, + }; + runTest('var(--bb,cc)', { + key: '--bb', + lightModeColor: 'cc', + darkModeColor: 'ee', + }); + }); + + function runDarkTest(input: string, expectedOutput: ColorKeyAndValue) { + const result = handler.parseColorValue(input, true); + + expect(result).toEqual(expectedOutput); + } + + it('simple color in dark mode', () => { + runDarkTest('aa', { + key: undefined, + lightModeColor: '', + darkModeColor: undefined, + }); + }); + + it('var color in dark mode', () => { + runDarkTest('var(--aa, bb)', { + key: '--aa', + lightModeColor: 'bb', + darkModeColor: undefined, + }); + }); + + it('known simple color in dark mode', () => { + (handler as any).knownColors = { + '--bb': { + lightModeColor: '#ff0000', + darkModeColor: '#00ffff', + }, + }; + runDarkTest('#00ffff', { + key: undefined, + lightModeColor: '#ff0000', + darkModeColor: '#00ffff', + }); + }); +}); + +describe('DarkColorHandlerImpl.registerColor', () => { + let setProperty: jasmine.Spy; + let handler: DarkColorHandlerImpl; + + beforeEach(() => { + setProperty = jasmine.createSpy('setProperty'); + const div = ({ + style: { + setProperty, + }, + } as any) as HTMLElement; + handler = new DarkColorHandlerImpl(div); + }); + + function runTest( + input: string, + isDark: boolean, + darkColor: string | undefined, + expectedOutput: string, + expectedKnownColors: Record, + expectedSetPropertyCalls: [string, string][] + ) { + const result = handler.registerColor(input, isDark, darkColor); + + expect(result).toEqual(expectedOutput); + expect((handler as any).knownColors).toEqual(expectedKnownColors); + expect(setProperty).toHaveBeenCalledTimes(expectedSetPropertyCalls.length); + + expectedSetPropertyCalls.forEach(v => { + expect(setProperty).toHaveBeenCalledWith(...v); + }); + } + + it('empty color, light mode', () => { + runTest('', false, undefined, '', {}, []); + }); + + it('simple color, light mode', () => { + runTest('red', false, undefined, 'red', {}, []); + }); + + it('empty color, dark mode', () => { + runTest('', true, undefined, '', {}, []); + }); + + it('simple color, dark mode', () => { + runTest( + 'red', + true, + undefined, + 'var(--darkColor_red, red)', + { + '--darkColor_red': { + lightModeColor: 'red', + darkModeColor: 'rgb(255, 39, 17)', + }, + }, + [['--darkColor_red', 'rgb(255, 39, 17)']] + ); + }); + + it('simple color, dark mode, with dark color', () => { + runTest( + 'red', + true, + 'blue', + 'var(--darkColor_red, red)', + { + '--darkColor_red': { + lightModeColor: 'red', + darkModeColor: 'blue', + }, + }, + [['--darkColor_red', 'blue']] + ); + }); + + it('var color, light mode', () => { + runTest('var(--aa, bb)', false, undefined, 'bb', {}, []); + }); + + it('var color, dark mode', () => { + runTest( + 'var(--aa, red)', + true, + undefined, + 'var(--aa, red)', + { + '--aa': { + lightModeColor: 'red', + darkModeColor: 'rgb(255, 39, 17)', + }, + }, + [['--aa', 'rgb(255, 39, 17)']] + ); + }); + + it('var color, dark mode with dark color', () => { + runTest( + 'var(--aa, bb)', + true, + 'cc', + 'var(--aa, bb)', + { + '--aa': { + lightModeColor: 'bb', + darkModeColor: 'cc', + }, + }, + [['--aa', 'cc']] + ); + }); + + it('var color, dark mode with dark color and existing dark color', () => { + (handler as any).knownColors['--aa'] = { + lightModeColor: 'dd', + darkModeColor: 'ee', + }; + runTest( + 'var(--aa, bb)', + true, + 'cc', + 'var(--aa, bb)', + { + '--aa': { + lightModeColor: 'dd', + darkModeColor: 'ee', + }, + }, + [] + ); + }); +}); + +describe('DarkColorHandlerImpl.reset', () => { + it('Reset', () => { + const removeProperty = jasmine.createSpy('removeProperty'); + const div = ({ + style: { + removeProperty, + }, + } as any) as HTMLElement; + const handler = new DarkColorHandlerImpl(div, null!); + + (handler as any).knownColors = { + '--aa': { + lightModeColor: 'bb', + darkModeColor: 'cc', + }, + '--dd': { + lightModeColor: 'ee', + darkModeColor: 'ff', + }, + }; + + handler.reset(); + + expect((handler as any).knownColors).toEqual({}); + expect(removeProperty).toHaveBeenCalledTimes(2); + expect(removeProperty).toHaveBeenCalledWith('--aa'); + expect(removeProperty).toHaveBeenCalledWith('--dd'); + }); +}); + +describe('DarkColorHandlerImpl.findLightColorFromDarkColor', () => { + it('Not found', () => { + const div = ({} as any) as HTMLElement; + const handler = new DarkColorHandlerImpl(div, null!); + + const result = handler.findLightColorFromDarkColor('#010203'); + + expect(result).toEqual(null); + }); + + it('Found: HEX to RGB', () => { + const div = ({} as any) as HTMLElement; + const handler = new DarkColorHandlerImpl(div, null!); + + (handler as any).knownColors = { + '--bb': { + lightModeColor: 'bb', + darkModeColor: 'rgb(4,5,6)', + }, + '--aa': { + lightModeColor: 'aa', + darkModeColor: 'rgb(1,2,3)', + }, + }; + + const result = handler.findLightColorFromDarkColor('#010203'); + + expect(result).toEqual('aa'); + }); + + it('Found: HEX to HEX', () => { + const div = ({} as any) as HTMLElement; + const handler = new DarkColorHandlerImpl(div, null!); + + (handler as any).knownColors = { + '--bb': { + lightModeColor: 'bb', + darkModeColor: 'rgb(4,5,6)', + }, + '--aa': { + lightModeColor: 'aa', + darkModeColor: '#010203', + }, + }; + + const result = handler.findLightColorFromDarkColor('#010203'); + + expect(result).toEqual('aa'); + }); + + it('Found: RGB to HEX', () => { + const div = ({} as any) as HTMLElement; + const handler = new DarkColorHandlerImpl(div, null!); + + (handler as any).knownColors = { + '--bb': { + lightModeColor: 'bb', + darkModeColor: 'rgb(4,5,6)', + }, + '--aa': { + lightModeColor: 'aa', + darkModeColor: '#010203', + }, + }; + + const result = handler.findLightColorFromDarkColor('rgb(1,2,3)'); + + expect(result).toEqual('aa'); + }); + + it('Found: RGB to RGB', () => { + const div = ({} as any) as HTMLElement; + const handler = new DarkColorHandlerImpl(div, null!); + + (handler as any).knownColors = { + '--bb': { + lightModeColor: 'bb', + darkModeColor: 'rgb(4,5,6)', + }, + '--aa': { + lightModeColor: 'aa', + darkModeColor: 'rgb(1, 2, 3)', + }, + }; + + const result = handler.findLightColorFromDarkColor('rgb(1,2,3)'); + + expect(result).toEqual('aa'); + }); +}); + +describe('DarkColorHandlerImpl.transformElementColor', () => { + let parseColorSpy: jasmine.Spy; + let registerColorSpy: jasmine.Spy; + let handler: DarkColorHandlerImpl; + let contentDiv: HTMLDivElement; + + beforeEach(() => { + contentDiv = document.createElement('div'); + handler = new DarkColorHandlerImpl(contentDiv); + + parseColorSpy = spyOn(handler, 'parseColorValue').and.callThrough(); + registerColorSpy = spyOn(handler, 'registerColor').and.callThrough(); + }); + + it('No color, light to dark', () => { + const span = document.createElement('span'); + handler.transformElementColor(span, false, true); + + expect(span.outerHTML).toBe(''); + expect(parseColorSpy).toHaveBeenCalledTimes(2); + expect(parseColorSpy).toHaveBeenCalledWith(null, false); + expect(registerColorSpy).not.toHaveBeenCalled(); + }); + + it('Has simple color in HTML, light to dark', () => { + const span = document.createElement('span'); + + span.setAttribute('color', 'red'); + + handler.transformElementColor(span, false, true); + + expect(span.outerHTML).toBe(''); + expect(parseColorSpy).toHaveBeenCalledTimes(3); + expect(parseColorSpy).toHaveBeenCalledWith('red', false); + expect(parseColorSpy).toHaveBeenCalledWith(null, false); + expect(registerColorSpy).toHaveBeenCalledTimes(1); + expect(registerColorSpy).toHaveBeenCalledWith('red', true, undefined); + }); + + it('Has simple color in CSS, light to dark', () => { + const span = document.createElement('span'); + + span.style.color = 'red'; + + handler.transformElementColor(span, false, true); + + expect(span.outerHTML).toBe(''); + expect(parseColorSpy).toHaveBeenCalledTimes(3); + expect(parseColorSpy).toHaveBeenCalledWith('red', false); + expect(parseColorSpy).toHaveBeenCalledWith(null, false); + expect(registerColorSpy).toHaveBeenCalledTimes(1); + expect(registerColorSpy).toHaveBeenCalledWith('red', true, undefined); + }); + + it('Has color in both text and background, light to dark', () => { + const span = document.createElement('span'); + + span.style.color = 'red'; + span.style.backgroundColor = 'green'; + + handler.transformElementColor(span, false, true); + + expect(span.outerHTML).toBe( + '' + ); + expect(parseColorSpy).toHaveBeenCalledTimes(4); + expect(parseColorSpy).toHaveBeenCalledWith('red', false); + expect(parseColorSpy).toHaveBeenCalledWith('green', false); + expect(parseColorSpy).toHaveBeenCalledWith('red'); + expect(parseColorSpy).toHaveBeenCalledWith('green'); + expect(registerColorSpy).toHaveBeenCalledTimes(2); + expect(registerColorSpy).toHaveBeenCalledWith('red', true, undefined); + expect(registerColorSpy).toHaveBeenCalledWith('green', true, undefined); + }); + + it('Has var-based color, light to dark', () => { + const span = document.createElement('span'); + + span.style.color = 'var(--darkColor_red, red)'; + + handler.transformElementColor(span, false, true); + + expect(span.outerHTML).toBe(''); + expect(parseColorSpy).toHaveBeenCalledTimes(3); + expect(parseColorSpy).toHaveBeenCalledWith('var(--darkColor_red, red)', false); + expect(parseColorSpy).toHaveBeenCalledWith('red'); + expect(parseColorSpy).toHaveBeenCalledWith(null, false); + expect(registerColorSpy).toHaveBeenCalledTimes(1); + expect(registerColorSpy).toHaveBeenCalledWith('red', true, undefined); + }); + + it('No color, dark to light', () => { + const span = document.createElement('span'); + handler.transformElementColor(span, true, false); + + expect(span.outerHTML).toBe(''); + expect(parseColorSpy).toHaveBeenCalledTimes(2); + expect(parseColorSpy).toHaveBeenCalledWith(null, true); + expect(registerColorSpy).not.toHaveBeenCalled(); + }); + + it('Has simple color in HTML, dark to light', () => { + const span = document.createElement('span'); + + span.setAttribute('color', 'red'); + + handler.transformElementColor(span, true, false); + + expect(span.outerHTML).toBe(''); + expect(parseColorSpy).toHaveBeenCalledTimes(2); + expect(parseColorSpy).toHaveBeenCalledWith('red', true); + expect(parseColorSpy).toHaveBeenCalledWith(null, true); + expect(registerColorSpy).not.toHaveBeenCalled(); + }); + + it('Has simple color in CSS, dark to light', () => { + const span = document.createElement('span'); + + span.style.color = 'red'; + + handler.transformElementColor(span, true, false); + + expect(span.outerHTML).toBe(''); + expect(parseColorSpy).toHaveBeenCalledTimes(2); + expect(parseColorSpy).toHaveBeenCalledWith('red', true); + expect(parseColorSpy).toHaveBeenCalledWith(null, true); + expect(registerColorSpy).not.toHaveBeenCalled(); + }); + + it('Has color in both text and background, dark to light', () => { + const span = document.createElement('span'); + + span.style.color = 'red'; + span.style.backgroundColor = 'green'; + + handler.transformElementColor(span, true, false); + + expect(span.outerHTML).toBe(''); + expect(parseColorSpy).toHaveBeenCalledTimes(2); + expect(parseColorSpy).toHaveBeenCalledWith('red', true); + expect(parseColorSpy).toHaveBeenCalledWith('green', true); + expect(registerColorSpy).not.toHaveBeenCalled(); + }); + + it('Has var-based color, dark to light', () => { + const span = document.createElement('span'); + + span.style.color = 'var(--darkColor_red, red)'; + + handler.transformElementColor(span, true, false); + + expect(span.outerHTML).toBe(''); + expect(parseColorSpy).toHaveBeenCalledTimes(3); + expect(parseColorSpy).toHaveBeenCalledWith('var(--darkColor_red, red)', true); + expect(parseColorSpy).toHaveBeenCalledWith('red'); + expect(parseColorSpy).toHaveBeenCalledWith(null, true); + expect(registerColorSpy).toHaveBeenCalledTimes(1); + expect(registerColorSpy).toHaveBeenCalledWith('red', false, undefined); + }); +}); diff --git a/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/common/backgroundColorFormatHandlerTest.ts b/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/common/backgroundColorFormatHandlerTest.ts index be3f5925e60..959beb060f1 100644 --- a/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/common/backgroundColorFormatHandlerTest.ts +++ b/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/common/backgroundColorFormatHandlerTest.ts @@ -1,7 +1,6 @@ import { backgroundColorFormatHandler } from '../../../lib/formatHandlers/common/backgroundColorFormatHandler'; import { createDomToModelContext } from '../../../lib/domToModel/context/createDomToModelContext'; import { createModelToDomContext } from '../../../lib/modelToDom/context/createModelToDomContext'; -import { DarkColorHandlerImpl } from 'roosterjs-content-model-editor/lib/editor/DarkColorHandlerImpl'; import { DeprecatedColors } from '../../../lib/formatHandlers/utils/color'; import { expectHtml } from 'roosterjs-editor-dom/test/DomTestHelper'; import { @@ -104,14 +103,14 @@ describe('backgroundColorFormatHandler.apply', () => { it('Simple color in dark mode', () => { format.backgroundColor = 'red'; context.isDarkMode = true; - context.darkColorHandler = new DarkColorHandlerImpl(div, s => 'darkMock:' + s); + context.darkColorHandler = { + registerColor: (color: string, isDarkMode: boolean) => + isDarkMode ? `var(--darkColor_${color}, ${color})` : color, + } as any; backgroundColorFormatHandler.apply(format, div, context); - const expectedResult = [ - '
', - '
', - ]; + const expectedResult = ['
']; expectHtml(div.outerHTML, expectedResult); }); diff --git a/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/segment/textColorFormatHandlerTest.ts b/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/segment/textColorFormatHandlerTest.ts index 74ded2e190c..68bc1ecad41 100644 --- a/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/segment/textColorFormatHandlerTest.ts +++ b/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/segment/textColorFormatHandlerTest.ts @@ -1,6 +1,5 @@ import { createDomToModelContext } from '../../../lib/domToModel/context/createDomToModelContext'; import { createModelToDomContext } from '../../../lib/modelToDom/context/createModelToDomContext'; -import { DarkColorHandlerImpl } from 'roosterjs-content-model-editor/lib/editor/DarkColorHandlerImpl'; import { defaultHTMLStyleMap } from '../../../lib/config/defaultHTMLStyleMap'; import { DeprecatedColors } from '../../../lib'; import { expectHtml } from 'roosterjs-editor-dom/test/DomTestHelper'; @@ -108,7 +107,10 @@ describe('textColorFormatHandler.apply', () => { beforeEach(() => { div = document.createElement('div'); context = createModelToDomContext(); - context.darkColorHandler = new DarkColorHandlerImpl(div, s => 'darkMock: ' + s); + context.darkColorHandler = { + registerColor: (color: string, isDarkMode: boolean) => + isDarkMode ? `var(--darkColor_${color}, ${color})` : color, + } as any; format = {}; }); @@ -133,10 +135,7 @@ describe('textColorFormatHandler.apply', () => { textColorFormatHandler.apply(format, div, context); - const expectedResult = [ - '
', - '
', - ]; + const expectedResult = ['
']; expectHtml(div.outerHTML, expectedResult); }); diff --git a/packages-content-model/roosterjs-content-model-editor/lib/coreApi/coreApiMap.ts b/packages-content-model/roosterjs-content-model-editor/lib/coreApi/coreApiMap.ts index 0cc7b7d5b34..f0b17b15c2f 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/coreApi/coreApiMap.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/coreApi/coreApiMap.ts @@ -14,16 +14,14 @@ import { selectImage } from './selectImage'; import { selectRange } from './selectRange'; import { selectTable } from './selectTable'; import { setContent } from './setContent'; -import { standaloneCoreApiMap } from 'roosterjs-content-model-core'; import { transformColor } from './transformColor'; import { triggerEvent } from './triggerEvent'; -import type { StandaloneCoreApiMap } from 'roosterjs-content-model-types'; +import type { UnportedCoreApiMap } from 'roosterjs-content-model-types'; /** * @internal */ -export const coreApiMap: StandaloneCoreApiMap = { - ...standaloneCoreApiMap, +export const coreApiMap: UnportedCoreApiMap = { attachDomEvent, addUndoSnapshot, ensureTypeInContainer, diff --git a/packages-content-model/roosterjs-content-model-editor/lib/corePlugins/createCorePlugins.ts b/packages-content-model/roosterjs-content-model-editor/lib/corePlugins/createCorePlugins.ts index 7e6943ebc95..216b3d23fdd 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/corePlugins/createCorePlugins.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/corePlugins/createCorePlugins.ts @@ -3,16 +3,15 @@ import { createEntityPlugin } from './EntityPlugin'; import { createImageSelection } from './ImageSelection'; import { createLifecyclePlugin } from './LifecyclePlugin'; import { createNormalizeTablePlugin } from './NormalizeTablePlugin'; -import { createStandaloneEditorCorePlugins } from 'roosterjs-content-model-core'; import { createUndoPlugin } from './UndoPlugin'; -import type { ContentModelCorePlugins } from '../publicTypes/ContentModelCorePlugins'; +import type { UnportedCorePlugins } from '../publicTypes/ContentModelCorePlugins'; +import type { UnportedCorePluginState } from 'roosterjs-content-model-types'; import type { ContentModelEditorOptions } from '../publicTypes/IContentModelEditor'; -import type { ContentModelPluginState } from 'roosterjs-content-model-types'; /** * @internal */ -export interface CreateCorePluginResponse extends ContentModelCorePlugins { +export interface CreateCorePluginResponse extends UnportedCorePlugins { _placeholder: null; } @@ -31,7 +30,6 @@ export function createCorePlugins( // The order matters, some plugin needs to be put before/after others to make sure event // can be handled in right order return { - ...createStandaloneEditorCorePlugins(options, contentDiv), edit: map.edit || createEditPlugin(), _placeholder: null, undo: map.undo || createUndoPlugin(options), @@ -47,15 +45,11 @@ export function createCorePlugins( * Get plugin state of core plugins * @param corePlugins ContentModelCorePlugins object */ -export function getPluginState(corePlugins: ContentModelCorePlugins): ContentModelPluginState { +export function getPluginState(corePlugins: UnportedCorePlugins): UnportedCorePluginState { return { - domEvent: corePlugins.domEvent.getState(), edit: corePlugins.edit.getState(), lifecycle: corePlugins.lifecycle.getState(), undo: corePlugins.undo.getState(), entity: corePlugins.entity.getState(), - copyPaste: corePlugins.copyPaste.getState(), - cache: corePlugins.cache.getState(), - format: corePlugins.format.getState(), }; } 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 5504c9bfa1b..e9bf1d56ff0 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 @@ -1074,7 +1074,9 @@ export class ContentModelEditor implements IContentModelEditor { * Retrieves the rect of the visible viewport of the editor. */ getVisibleViewport(): Rect | null { - return this.getCore().getVisibleViewport(); + const core = this.getCore(); + + return core.api.getVisibleViewport(core); } /** diff --git a/packages-content-model/roosterjs-content-model-editor/lib/editor/createEditorCore.ts b/packages-content-model/roosterjs-content-model-editor/lib/editor/createEditorCore.ts index ac4241f7cc6..d8fa0d0b6b4 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/editor/createEditorCore.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/editor/createEditorCore.ts @@ -1,9 +1,7 @@ -import { arrayPush, getIntersectedRect, getObjectKeys } from 'roosterjs-editor-dom'; import { coreApiMap } from '../coreApi/coreApiMap'; import { createCorePlugins, getPluginState } from '../corePlugins/createCorePlugins'; -import { createStandaloneEditorDefaultSettings } from 'roosterjs-content-model-core'; -import { DarkColorHandlerImpl } from './DarkColorHandlerImpl'; -import type { EditorPlugin } from 'roosterjs-editor-types'; +import { createStandaloneEditorCore } from 'roosterjs-content-model-core'; +import { getObjectKeys } from 'roosterjs-content-model-dom'; import type { ContentModelEditorCore } from '../publicTypes/ContentModelEditorCore'; import type { ContentModelEditorOptions } from '../publicTypes/IContentModelEditor'; @@ -18,70 +16,34 @@ export function createEditorCore( options: ContentModelEditorOptions ): ContentModelEditorCore { const corePlugins = createCorePlugins(contentDiv, options); - const plugins: EditorPlugin[] = []; - - getObjectKeys(corePlugins).forEach(name => { - if (name == '_placeholder') { - if (options.plugins) { - arrayPush(plugins, options.plugins); - } - } else { - plugins.push(corePlugins[name]); - } - }); - const pluginState = getPluginState(corePlugins); - const zoomScale: number = (options.zoomScale ?? -1) > 0 ? options.zoomScale! : 1; - const getVisibleViewport = - options.getVisibleViewport || - (() => { - const scrollContainer = pluginState.domEvent.scrollContainer; - return getIntersectedRect( - scrollContainer == core.contentDiv - ? [scrollContainer] - : [scrollContainer, core.contentDiv] - ); - }); + const zoomScale: number = (options.zoomScale ?? -1) > 0 ? options.zoomScale! : 1; - // It is ok to use global window here since the environment should always be the same for all windows in one session - const userAgent = window.navigator.userAgent; + const standaloneEditorCore = createStandaloneEditorCore( + contentDiv, + options, + coreApiMap, + pluginState + ); const core: ContentModelEditorCore = { - contentDiv, - api: { - ...coreApiMap, - ...(options.coreApiOverride || {}), - }, - originalApi: { ...coreApiMap }, - plugins: plugins.filter(x => !!x), + ...standaloneEditorCore, ...pluginState, - trustedHTMLHandler: options.trustedHTMLHandler || defaultTrustHtmlHandler, zoomScale: zoomScale, sizeTransformer: (size: number) => size / zoomScale, - getVisibleViewport, - imageSelectionBorderColor: options.imageSelectionBorderColor, - darkColorHandler: new DarkColorHandlerImpl(contentDiv, pluginState.lifecycle.getDarkColor), disposeErrorHandler: options.disposeErrorHandler, - - ...createStandaloneEditorDefaultSettings(options), - - environment: { - isMac: window.navigator.appVersion.indexOf('Mac') != -1, - isAndroid: /android/i.test(userAgent), - isSafari: - userAgent.indexOf('Safari') >= 0 && - userAgent.indexOf('Chrome') < 0 && - userAgent.indexOf('Android') < 0, - }, }; - return core; -} + getObjectKeys(corePlugins).forEach(name => { + if (name == '_placeholder') { + if (options.plugins) { + core.plugins.push(...options.plugins.filter(x => !!x)); + } + } else if (corePlugins[name]) { + core.plugins.push(corePlugins[name]); + } + }); -/** - * @internal Export for test only - */ -export function defaultTrustHtmlHandler(html: string) { - return html; + return core; } 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 43a31ab5983..ac1905956cb 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/index.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/index.ts @@ -1,6 +1,9 @@ export { ContentModelEditorCore } from './publicTypes/ContentModelEditorCore'; export { IContentModelEditor, ContentModelEditorOptions } from './publicTypes/IContentModelEditor'; -export { ContentModelCorePlugins } from './publicTypes/ContentModelCorePlugins'; +export { + ContentModelCorePlugins, + UnportedCorePlugins, +} from './publicTypes/ContentModelCorePlugins'; export { ContentModelEditor } from './editor/ContentModelEditor'; export { isContentModelEditor } from './editor/isContentModelEditor'; diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/ContentModelCorePlugins.ts b/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/ContentModelCorePlugins.ts index c02378cfd7a..4c8cd1948a4 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/ContentModelCorePlugins.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/ContentModelCorePlugins.ts @@ -1,6 +1,5 @@ import type { StandaloneEditorCorePlugins } from 'roosterjs-content-model-types'; import type { - CopyPastePluginState, EditPluginState, EditorPlugin, EntityPluginState, @@ -10,9 +9,10 @@ import type { } from 'roosterjs-editor-types'; /** - * An interface for Content Model editor core plugins. + * An interface for unported core plugins + * TODO: Port these plugins */ -export interface ContentModelCorePlugins extends StandaloneEditorCorePlugins { +export interface UnportedCorePlugins { /** * Edit plugin handles ContentEditFeatures */ @@ -23,10 +23,6 @@ export interface ContentModelCorePlugins extends StandaloneEditorCorePlugins { */ readonly undo: PluginWithState; - /** - * Copy and paste plugin for handling onCopy and onPaste event - */ - readonly copyPaste: PluginWithState; /** * Entity Plugin handles all operations related to an entity and generate entity specified events */ @@ -49,3 +45,8 @@ export interface ContentModelCorePlugins extends StandaloneEditorCorePlugins { */ readonly lifecycle: PluginWithState; } + +/** + * An interface for Content Model editor core plugins. + */ +export interface ContentModelCorePlugins extends StandaloneEditorCorePlugins, UnportedCorePlugins {} 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 9be82c2b66f..34e8bb630e1 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 @@ -4,16 +4,10 @@ import type { EditorPlugin, ExperimentalFeatures, IEditor, - Rect, Snapshot, - TrustedHTMLHandler, UndoSnapshotsService, } from 'roosterjs-editor-types'; -import type { - StandaloneEditorOptions, - IStandaloneEditor, - StandaloneCoreApiMap, -} from 'roosterjs-content-model-types'; +import type { StandaloneEditorOptions, IStandaloneEditor } from 'roosterjs-content-model-types'; /** * An interface of editor with Content Model support. @@ -44,12 +38,6 @@ export interface ContentModelEditorOptions extends StandaloneEditorOptions { */ initialContent?: string; - /** - * A function map to override default core API implementation - * Default value is null - */ - coreApiOverride?: Partial; - /** * A plugin map to override default core Plugin implementation * Default value is null @@ -77,13 +65,6 @@ export interface ContentModelEditorOptions extends StandaloneEditorOptions { */ experimentalFeatures?: ExperimentalFeatures[]; - /** - * Customized trusted type handler used for sanitizing HTML string before assign to DOM tree - * This is required when trusted-type Content-Security-Policy (CSP) is enabled. - * See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/trusted-types - */ - trustedHTMLHandler?: TrustedHTMLHandler; - /** * Current zoom scale, @default value is 1 * When editor is put under a zoomed container, need to pass the zoom scale number using this property @@ -91,16 +72,6 @@ export interface ContentModelEditorOptions extends StandaloneEditorOptions { */ zoomScale?: number; - /** - * Retrieves the visible viewport of the Editor. The default viewport is the Rect of the scrollContainer. - */ - getVisibleViewport?: () => Rect | null; - - /** - * Color of the border of a selectedImage. Default color: '#DB626C' - */ - imageSelectionBorderColor?: string; - /** * 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-editor/test/editor/createEditorCoreTest.ts b/packages-content-model/roosterjs-content-model-editor/test/editor/createEditorCoreTest.ts index 1f597bdf9f5..7d6ab21182b 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/editor/createEditorCoreTest.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/editor/createEditorCoreTest.ts @@ -10,7 +10,9 @@ import * as LifecyclePlugin from '../../lib/corePlugins/LifecyclePlugin'; import * as NormalizeTablePlugin from '../../lib/corePlugins/NormalizeTablePlugin'; import * as UndoPlugin from '../../lib/corePlugins/UndoPlugin'; import { coreApiMap } from '../../lib/coreApi/coreApiMap'; -import { createEditorCore, defaultTrustHtmlHandler } from '../../lib/editor/createEditorCore'; +import { createEditorCore } from '../../lib/editor/createEditorCore'; +import { defaultTrustHtmlHandler } from 'roosterjs-content-model-core/lib/editor/createStandaloneEditorCore'; +import { standaloneCoreApiMap } from 'roosterjs-content-model-core/lib/editor/standaloneCoreApiMap'; const mockedDomEventState = 'DOMEVENTSTATE' as any; const mockedEditState = 'EDITSTATE' as any; @@ -87,8 +89,8 @@ describe('createEditorCore', () => { const core = createEditorCore(contentDiv, {}); expect(core).toEqual({ contentDiv, - api: coreApiMap, - originalApi: coreApiMap, + api: { ...coreApiMap, ...standaloneCoreApiMap }, + originalApi: { ...coreApiMap, ...standaloneCoreApiMap }, plugins: [ mockedCachePlugin, mockedFormatPlugin, @@ -112,7 +114,6 @@ describe('createEditorCore', () => { trustedHTMLHandler: defaultTrustHtmlHandler, zoomScale: 1, sizeTransformer: jasmine.anything(), - getVisibleViewport: jasmine.anything(), imageSelectionBorderColor: undefined, darkColorHandler: jasmine.anything(), disposeErrorHandler: undefined, @@ -141,8 +142,8 @@ describe('createEditorCore', () => { expect(core).toEqual({ contentDiv, - api: coreApiMap, - originalApi: coreApiMap, + api: { ...coreApiMap, ...standaloneCoreApiMap }, + originalApi: { ...coreApiMap, ...standaloneCoreApiMap }, plugins: [ mockedCachePlugin, mockedFormatPlugin, @@ -166,7 +167,6 @@ describe('createEditorCore', () => { trustedHTMLHandler: defaultTrustHtmlHandler, zoomScale: 1, sizeTransformer: jasmine.anything(), - getVisibleViewport: jasmine.anything(), imageSelectionBorderColor: undefined, darkColorHandler: jasmine.anything(), disposeErrorHandler: undefined, diff --git a/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/testUtils.ts b/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/testUtils.ts index 025c0d7882e..69579d81fb6 100644 --- a/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/testUtils.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/testUtils.ts @@ -14,13 +14,15 @@ export function initEditor(id: string): IContentModelEditor { let options: ContentModelEditorOptions = { plugins: [new ContentModelPastePlugin()], - getVisibleViewport: () => { - return { - top: 100, - bottom: 200, - left: 100, - right: 200, - }; + coreApiOverride: { + getVisibleViewport: () => { + return { + top: 100, + bottom: 200, + left: 100, + right: 200, + }; + }, }, }; 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 3b8dcbd6d2a..d18116e8707 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 @@ -24,7 +24,10 @@ import type { TrustedHTMLHandler, } from 'roosterjs-editor-types'; import type { ContentModelDocument } from '../group/ContentModelDocument'; -import type { ContentModelPluginState } from '../pluginState/ContentModelPluginState'; +import type { + StandaloneEditorCorePluginState, + UnportedCorePluginState, +} from '../pluginState/StandaloneEditorPluginState'; import type { DOMSelection } from '../selection/DOMSelection'; import type { DomToModelOption } from '../context/DomToModelOption'; import type { DomToModelSettings } from '../context/DomToModelSettings'; @@ -179,6 +182,12 @@ export type AddUndoSnapshot = ( additionalData?: ContentChangedData ) => void; +/** + * Retrieves the rect of the visible viewport of the editor. + * @param core The StandaloneEditorCore object + */ +export type GetVisibleViewport = (core: StandaloneEditorCore) => Rect | null; + /** * Change the editor selection to the given range * @param core The StandaloneEditorCore object @@ -375,6 +384,12 @@ export interface PortedCoreApiMap { * @param isOn True to switch On, False to switch Off */ switchShadowEdit: SwitchShadowEdit; + + /** + * Retrieves the rect of the visible viewport of the editor. + * @param core The StandaloneEditorCore object + */ + getVisibleViewport: GetVisibleViewport; } /** @@ -550,7 +565,8 @@ export interface StandaloneCoreApiMap extends PortedCoreApiMap, UnportedCoreApiM * Represents the core data structure of a Content Model editor */ export interface StandaloneEditorCore - extends ContentModelPluginState, + extends StandaloneEditorCorePluginState, + UnportedCorePluginState, StandaloneEditorDefaultSettings { /** * The content DIV element of this editor @@ -581,17 +597,12 @@ export interface StandaloneEditorCore * Dark model handler for the editor, used for variable-based solution. * If keep it null, editor will still use original dataset-based dark mode solution. */ - darkColorHandler: DarkColorHandler; - - /** - * Retrieves the Visible Viewport of the editor. - */ - getVisibleViewport: () => Rect | null; + readonly darkColorHandler: DarkColorHandler; /** * Color of the border of a selectedImage. Default color: '#DB626C' */ - imageSelectionBorderColor?: string; + readonly imageSelectionBorderColor?: string; /** * A handler to convert HTML string to a trust HTML string. diff --git a/packages-content-model/roosterjs-content-model-types/lib/editor/StandaloneEditorOptions.ts b/packages-content-model/roosterjs-content-model-types/lib/editor/StandaloneEditorOptions.ts index fe3d8beac6f..b82b5056f09 100644 --- a/packages-content-model/roosterjs-content-model-types/lib/editor/StandaloneEditorOptions.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/editor/StandaloneEditorOptions.ts @@ -1,4 +1,5 @@ -import type { DefaultFormat, EditorPlugin } from 'roosterjs-editor-types'; +import type { StandaloneCoreApiMap } from './StandaloneEditorCore'; +import type { DefaultFormat, EditorPlugin, TrustedHTMLHandler } from 'roosterjs-editor-types'; import type { DomToModelOption } from '../context/DomToModelOption'; import type { ModelToDomOption } from '../context/ModelToDomOption'; @@ -47,4 +48,28 @@ export interface StandaloneEditorOptions { * By default, the scroll container will be the same with editor content DIV */ scrollContainer?: HTMLElement; + + /** + * Base dark mode color. We will use this color to calculate the dark mode color from a given light mode color + * @default #333333 + */ + baseDarkColor?: string; + + /** + * Customized trusted type handler used for sanitizing HTML string before assign to DOM tree + * This is required when trusted-type Content-Security-Policy (CSP) is enabled. + * See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/trusted-types + */ + trustedHTMLHandler?: TrustedHTMLHandler; + + /** + * A function map to override default core API implementation + * Default value is null + */ + coreApiOverride?: Partial; + + /** + * Color of the border of a selectedImage. Default color: '#DB626C' + */ + imageSelectionBorderColor?: string; } 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 c3b562ef901..0c9d0ce1754 100644 --- a/packages-content-model/roosterjs-content-model-types/lib/index.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/index.ts @@ -226,11 +226,15 @@ export { GetStyleBasedFormatState, RestoreUndoSnapshot, EnsureTypeInContainer, + GetVisibleViewport, } from './editor/StandaloneEditorCore'; export { StandaloneEditorCorePlugins } from './editor/StandaloneEditorCorePlugins'; export { ContentModelCachePluginState } from './pluginState/ContentModelCachePluginState'; -export { ContentModelPluginState } from './pluginState/ContentModelPluginState'; +export { + StandaloneEditorCorePluginState, + UnportedCorePluginState, +} from './pluginState/StandaloneEditorPluginState'; export { ContentModelFormatPluginState, PendingFormat, diff --git a/packages-content-model/roosterjs-content-model-types/lib/pluginState/ContentModelPluginState.ts b/packages-content-model/roosterjs-content-model-types/lib/pluginState/StandaloneEditorPluginState.ts similarity index 79% rename from packages-content-model/roosterjs-content-model-types/lib/pluginState/ContentModelPluginState.ts rename to packages-content-model/roosterjs-content-model-types/lib/pluginState/StandaloneEditorPluginState.ts index 5d725106a7f..fdf29ed64fd 100644 --- a/packages-content-model/roosterjs-content-model-types/lib/pluginState/ContentModelPluginState.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/pluginState/StandaloneEditorPluginState.ts @@ -10,10 +10,10 @@ import type { ContentModelFormatPluginState } from './ContentModelFormatPluginSt import type { DOMEventPluginState } from './DOMEventPluginState'; /** - * Temporary core plugin state for Content Model editor + * Temporary core plugin state for Content Model editor (ported part) * TODO: Create Content Model plugin state from all core plugins once we have standalone Content Model Editor */ -export interface ContentModelPluginState { +export interface StandaloneEditorCorePluginState { /** * Plugin state for ContentModelCachePlugin */ @@ -33,8 +33,13 @@ export interface ContentModelPluginState { * Plugin state for DOMEventPlugin */ domEvent: DOMEventPluginState; +} - // Plugins copied from legacy editor +/** + * Temporary core plugin state for Content Model editor (unported part) + * TODO: Port these plugins + */ +export interface UnportedCorePluginState { lifecycle: LifecyclePluginState; entity: EntityPluginState; undo: UndoPluginState;