Skip to content

Commit

Permalink
Fix onExternalContentTransform, allow customize dark color handling (#…
Browse files Browse the repository at this point in the history
…1907)

* Fix onExternalContentTransform again

* fix build

* fix test

* remove temp code
  • Loading branch information
JiuqingSong authored Jun 27, 2023
1 parent e0da6ba commit ce31cc7
Show file tree
Hide file tree
Showing 7 changed files with 256 additions and 69 deletions.
59 changes: 13 additions & 46 deletions packages/roosterjs-editor-core/lib/coreApi/transformColor.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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);
}
});
}
}
39 changes: 19 additions & 20 deletions packages/roosterjs-editor-core/test/coreApi/transformColorTest.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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);

Expand Down Expand Up @@ -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,
Expand Down
162 changes: 162 additions & 0 deletions packages/roosterjs-editor-core/test/editor/DarkColorHandlerImplTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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('<span></span>');
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('<span style="color: var(--darkColor_red, red);"></span>');
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('<span style="color: var(--darkColor_red, red);"></span>');
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(
'<span style="color: var(--darkColor_red, red); background-color: var(--darkColor_green, green);"></span>'
);
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('<span style="color: var(--darkColor_red, red);"></span>');
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('<span></span>');
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('<span></span>');
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('<span style=""></span>');
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('<span style=""></span>');
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('<span style="color: red;"></span>');
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);
});
});
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Loading

0 comments on commit ce31cc7

Please sign in to comment.