diff --git a/packages/roosterjs-editor-core/lib/coreApi/transformColor.ts b/packages/roosterjs-editor-core/lib/coreApi/transformColor.ts
index 2353f5a635f..96bf5fc8575 100644
--- a/packages/roosterjs-editor-core/lib/coreApi/transformColor.ts
+++ b/packages/roosterjs-editor-core/lib/coreApi/transformColor.ts
@@ -1,23 +1,6 @@
import { ColorTransformDirection, EditorCore, TransformColor } from 'roosterjs-editor-types';
-import { setColor } from 'roosterjs-editor-dom';
import type { CompatibleColorTransformDirection } from 'roosterjs-editor-types/lib/compatibleTypes';
-const enum ColorAttributeEnum {
- CssColor = 0,
- HtmlColor = 1,
-}
-
-const ColorAttributeName: { [key in ColorAttributeEnum]: string }[] = [
- {
- [ColorAttributeEnum.CssColor]: 'color',
- [ColorAttributeEnum.HtmlColor]: 'color',
- },
- {
- [ColorAttributeEnum.CssColor]: 'background-color',
- [ColorAttributeEnum.HtmlColor]: 'bgcolor',
- },
-];
-
/**
* @internal
* Edit and transform color of elements between light mode and dark mode
@@ -36,37 +19,21 @@ export const transformColor: TransformColor = (
callback: (() => void) | null,
direction: ColorTransformDirection | CompatibleColorTransformDirection,
forceTransform?: boolean,
- fromDarkMode?: boolean
+ fromDarkMode: boolean = false
) => {
- const { darkColorHandler } = core;
- const toDark = direction == ColorTransformDirection.LightToDark;
-
+ const {
+ darkColorHandler,
+ lifecycle: { onExternalContentTransform },
+ } = core;
+ const toDarkMode = direction == ColorTransformDirection.LightToDark;
if (rootNode && (forceTransform || core.lifecycle.isDarkMode)) {
- const transformer =
- core.lifecycle.onExternalContentTransform ||
- ((element: HTMLElement) => {
- ColorAttributeName.forEach((names, i) => {
- const color = darkColorHandler.parseColorValue(
- element.style.getPropertyValue(names[ColorAttributeEnum.CssColor]) ||
- element.getAttribute(names[ColorAttributeEnum.HtmlColor]),
- !!fromDarkMode
- ).lightModeColor;
-
- element.style.setProperty(names[ColorAttributeEnum.CssColor], null);
- element.removeAttribute(names[ColorAttributeEnum.HtmlColor]);
-
- if (color && color != 'inherit') {
- setColor(
- element,
- color,
- i != 0,
- toDark,
- false /*shouldAdaptFontColor*/,
- darkColorHandler
- );
- }
- });
- });
+ const transformer = onExternalContentTransform
+ ? (element: HTMLElement) => {
+ onExternalContentTransform(element, fromDarkMode, toDarkMode, darkColorHandler);
+ }
+ : (element: HTMLElement) => {
+ darkColorHandler.transformElementColor(element, fromDarkMode, toDarkMode);
+ };
iterateElements(rootNode, transformer, includeSelf);
}
diff --git a/packages/roosterjs-editor-core/lib/editor/DarkColorHandlerImpl.ts b/packages/roosterjs-editor-core/lib/editor/DarkColorHandlerImpl.ts
index 704d2bbc04b..e10f1b5c17b 100644
--- a/packages/roosterjs-editor-core/lib/editor/DarkColorHandlerImpl.ts
+++ b/packages/roosterjs-editor-core/lib/editor/DarkColorHandlerImpl.ts
@@ -1,9 +1,23 @@
import { ColorKeyAndValue, DarkColorHandler, ModeIndependentColor } from 'roosterjs-editor-types';
-import { getObjectKeys, parseColor } from 'roosterjs-editor-dom';
+import { getObjectKeys, parseColor, setColor } from 'roosterjs-editor-dom';
const VARIABLE_REGEX = /^\s*var\(\s*(\-\-[a-zA-Z0-9\-_]+)\s*(?:,\s*(.*))?\)\s*$/;
const VARIABLE_PREFIX = 'var(';
const COLOR_VAR_PREFIX = 'darkColor';
+const enum ColorAttributeEnum {
+ CssColor = 0,
+ HtmlColor = 1,
+}
+const ColorAttributeName: { [key in ColorAttributeEnum]: string }[] = [
+ {
+ [ColorAttributeEnum.CssColor]: 'color',
+ [ColorAttributeEnum.HtmlColor]: 'color',
+ },
+ {
+ [ColorAttributeEnum.CssColor]: 'background-color',
+ [ColorAttributeEnum.HtmlColor]: 'bgcolor',
+ },
+];
/**
* @internal
@@ -129,4 +143,27 @@ export default class DarkColorHandlerImpl implements DarkColorHandler {
return null;
}
+
+ /**
+ * Transform element color, from dark to light or from light to dark
+ * @param element The element to transform color
+ * @param fromDarkMode Whether this is transforming color from dark mode
+ * @param toDarkMode Whether this is transforming color to dark mode
+ */
+ transformElementColor(element: HTMLElement, fromDarkMode: boolean, toDarkMode: boolean): void {
+ ColorAttributeName.forEach((names, i) => {
+ const color = this.parseColorValue(
+ element.style.getPropertyValue(names[ColorAttributeEnum.CssColor]) ||
+ element.getAttribute(names[ColorAttributeEnum.HtmlColor]),
+ !!fromDarkMode
+ ).lightModeColor;
+
+ element.style.setProperty(names[ColorAttributeEnum.CssColor], null);
+ element.removeAttribute(names[ColorAttributeEnum.HtmlColor]);
+
+ if (color && color != 'inherit') {
+ setColor(element, color, i != 0, toDarkMode, false /*shouldAdaptFontColor*/, this);
+ }
+ });
+ }
}
diff --git a/packages/roosterjs-editor-core/test/coreApi/transformColorTest.ts b/packages/roosterjs-editor-core/test/coreApi/transformColorTest.ts
index 6a7b0db98f8..5efa0c47c94 100644
--- a/packages/roosterjs-editor-core/test/coreApi/transformColorTest.ts
+++ b/packages/roosterjs-editor-core/test/coreApi/transformColorTest.ts
@@ -1,5 +1,6 @@
import createEditorCore from './createMockEditorCore';
-import { ColorTransformDirection, DarkColorHandler } from 'roosterjs-editor-types';
+import DarkColorHandlerImpl from '../../lib/editor/DarkColorHandlerImpl';
+import { ColorTransformDirection } from 'roosterjs-editor-types';
import { getDarkColor } from 'roosterjs-color-utils';
import { itChromeOnly } from '../TestHelper';
import { transformColor } from '../../lib/coreApi/transformColor';
@@ -23,20 +24,19 @@ describe('transform to dark mode v2', () => {
expectedParseValueCalls: string[],
expectedRegisterColorCalls: [string, boolean, string][]
) {
+ const handler = new DarkColorHandlerImpl(div, getDarkColor);
const core = createEditorCore(div, {
inDarkMode: false,
getDarkColor,
});
- const parseColorValue = jasmine
- .createSpy('parseColorValue')
- .and.callFake((color: string) => ({
- lightModeColor: color == 'red' ? 'blue' : color == 'green' ? 'yellow' : '',
- }));
- const registerColor = jasmine
- .createSpy('registerColor')
- .and.callFake((color: string) => color);
+ core.darkColorHandler = handler;
- core.darkColorHandler = ({ parseColorValue, registerColor } as any) as DarkColorHandler;
+ const parseColorValue = spyOn(handler, 'parseColorValue').and.callFake((color: string) => ({
+ lightModeColor: color == 'red' ? 'blue' : color == 'green' ? 'yellow' : '',
+ }));
+ const registerColor = spyOn(handler, 'registerColor').and.callFake(
+ (color: string) => color
+ );
transformColor(core, element, true, null, ColorTransformDirection.LightToDark, true);
@@ -128,19 +128,18 @@ describe('transform to light mode v2', () => {
expectedParseValueCalls: string[],
expectedRegisterColorCalls: [string, boolean, string][]
) {
+ const handler = new DarkColorHandlerImpl(div, getDarkColor);
const core = createEditorCore(div, {
getDarkColor,
});
- const parseColorValue = jasmine
- .createSpy('parseColorValue')
- .and.callFake((color: string) => ({
- lightModeColor: color == 'red' ? 'blue' : color == 'green' ? 'yellow' : '',
- }));
- const registerColor = jasmine
- .createSpy('registerColor')
- .and.callFake((color: string) => color);
-
- core.darkColorHandler = ({ parseColorValue, registerColor } as any) as DarkColorHandler;
+ const parseColorValue = spyOn(handler, 'parseColorValue').and.callFake((color: string) => ({
+ lightModeColor: color == 'red' ? 'blue' : color == 'green' ? 'yellow' : '',
+ }));
+ const registerColor = spyOn(handler, 'registerColor').and.callFake(
+ (color: string) => color
+ );
+
+ core.darkColorHandler = handler;
transformColor(
core,
diff --git a/packages/roosterjs-editor-core/test/editor/DarkColorHandlerImplTest.ts b/packages/roosterjs-editor-core/test/editor/DarkColorHandlerImplTest.ts
index d74087234df..5675abe7621 100644
--- a/packages/roosterjs-editor-core/test/editor/DarkColorHandlerImplTest.ts
+++ b/packages/roosterjs-editor-core/test/editor/DarkColorHandlerImplTest.ts
@@ -361,3 +361,165 @@ describe('DarkColorHandlerImpl.findLightColorFromDarkColor', () => {
expect(result).toEqual('aa');
});
});
+
+describe('DarkColorHandlerImpl.transformElementColor', () => {
+ let parseColorSpy: jasmine.Spy;
+ let registerColorSpy: jasmine.Spy;
+ let handler: DarkColorHandlerImpl;
+ let contentDiv: HTMLDivElement;
+
+ beforeEach(() => {
+ const getDarkColor = (color: string) => 'mocked_' + color;
+ contentDiv = document.createElement('div');
+ handler = new DarkColorHandlerImpl(contentDiv, getDarkColor);
+
+ 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/roosterjs-editor-types/lib/corePluginState/LifecyclePluginState.ts b/packages/roosterjs-editor-types/lib/corePluginState/LifecyclePluginState.ts
index b5e3da07936..0f60fd7e0da 100644
--- a/packages/roosterjs-editor-types/lib/corePluginState/LifecyclePluginState.ts
+++ b/packages/roosterjs-editor-types/lib/corePluginState/LifecyclePluginState.ts
@@ -1,4 +1,5 @@
import CustomData from '../interface/CustomData';
+import DarkColorHandler from '../interface/DarkColorHandler';
import DefaultFormat from '../interface/DefaultFormat';
import SelectionPath from '../interface/SelectionPath';
import { ExperimentalFeatures } from '../enum/ExperimentalFeatures';
@@ -31,7 +32,14 @@ export default interface LifecyclePluginState {
/**
* External content transform function to help do color transform for existing content
*/
- onExternalContentTransform: ((htmlIn: HTMLElement) => void) | null;
+ onExternalContentTransform:
+ | ((
+ element: HTMLElement,
+ fromDarkMode: boolean,
+ toDarkMode: boolean,
+ darkColorHandler: DarkColorHandler
+ ) => void)
+ | null;
/**
* Enabled experimental features
diff --git a/packages/roosterjs-editor-types/lib/interface/DarkColorHandler.ts b/packages/roosterjs-editor-types/lib/interface/DarkColorHandler.ts
index e8b158593a9..eeb600ce45d 100644
--- a/packages/roosterjs-editor-types/lib/interface/DarkColorHandler.ts
+++ b/packages/roosterjs-editor-types/lib/interface/DarkColorHandler.ts
@@ -57,4 +57,12 @@ export default interface DarkColorHandler {
* @param darkColor The existing dark color
*/
findLightColorFromDarkColor(darkColor: string): string | null;
+
+ /**
+ * Transform element color, from dark to light or from light to dark
+ * @param element The element to transform color
+ * @param fromDarkMode Whether this is transforming color from dark mode
+ * @param toDarkMode Whether this is transforming color to dark mode
+ */
+ transformElementColor(element: HTMLElement, fromDarkMode: boolean, toDarkMode: boolean): void;
}
diff --git a/packages/roosterjs-editor-types/lib/interface/EditorOptions.ts b/packages/roosterjs-editor-types/lib/interface/EditorOptions.ts
index 987af512318..5a48b426145 100644
--- a/packages/roosterjs-editor-types/lib/interface/EditorOptions.ts
+++ b/packages/roosterjs-editor-types/lib/interface/EditorOptions.ts
@@ -1,4 +1,5 @@
import CorePlugins from './CorePlugins';
+import DarkColorHandler from './DarkColorHandler';
import DefaultFormat from './DefaultFormat';
import EditorPlugin from './EditorPlugin';
import Rect from './Rect';
@@ -72,7 +73,12 @@ export default interface EditorOptions {
* If you want to change this behavior, you may define a different function here.
* It takes in the impacted HTMLElement
*/
- onExternalContentTransform?: (htmlIn: HTMLElement) => void;
+ onExternalContentTransform?: (
+ element: HTMLElement,
+ fromDarkMode: boolean,
+ toDarkMode: boolean,
+ darkColorHandler: DarkColorHandler
+ ) => void;
/**
* A util function to transform light mode color to dark mode color