Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix color issue with Font tag #1595

Merged
merged 3 commits into from
Feb 23, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions packages/roosterjs-content-model/lib/formatHandlers/utils/color.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { DarkColorHandler, DarkModeDatasetNames } from 'roosterjs-editor-types';
import { getTagOfNode } from 'roosterjs-editor-dom';

/**
* @internal
Expand Down Expand Up @@ -26,6 +27,8 @@ export function getColor(

if (!color) {
color =
(darkColorHandler &&
tryGetFontColor(element, isDarkMode, darkColorHandler, isBackground)) ||
(isBackground ? element.style.backgroundColor : element.style.color) ||
element.getAttribute(isBackground ? 'bgcolor' : 'color') ||
undefined;
Expand Down Expand Up @@ -73,3 +76,26 @@ export function setColor(
element.style.color = effectiveColor;
}
}

/**
* There is a known issue that when input with IME in Chrome, it is possible Chrome insert a new FONT tag with colors.
* If editor is in dark mode, this color will cause the FONT tag doesn't have light mode color info so that after convert
* to light mode the color will be wrong.
* To workaround it, we check if this is a known color (for light mode with VariableBasedDarkColor enabled, all used colors
* are stored in darkColorHandler), then use the related light mode color instead.
*/
function tryGetFontColor(
element: HTMLElement,
isDarkMode: boolean,
darkColorHandler: DarkColorHandler,
isBackground: boolean
) {
let darkColor: string | null;

return getTagOfNode(element) == 'FONT' &&
!element.style.getPropertyValue(isBackground ? 'background-color' : 'color') &&
isDarkMode &&
(darkColor = element.getAttribute(isBackground ? 'bgcolor' : 'color'))
? darkColorHandler.findLightColorFromDarkColor(darkColor)
: null;
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ContentModelTableCell } from '../../publicTypes/group/ContentModelTableCell';
import { parseColor } from 'roosterjs-editor-dom';
import { updateTableCellMetadata } from '../../domUtils/metadata/updateTableCellMetadata';

// Using the HSL (hue, saturation and lightness) representation for RGB color values.
Expand All @@ -8,7 +9,6 @@ const DARK_COLORS_LIGHTNESS = 20;
const BRIGHT_COLORS_LIGHTNESS = 80;
const White = '#ffffff';
const Black = '#000000';
const RGBA_REGEX = /^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d+)?))?\)$/;

/**
* @internal
Expand Down Expand Up @@ -45,49 +45,18 @@ export function setTableCellBackgroundColor(
}

function calculateLightness(color: string) {
let r: number;
let g: number;
let b: number;

if (color.substring(0, 1) == '#') {
[r, g, b] = parseHexColor(color);
} else {
[r, g, b] = parseRGBColor(color);
}
const colorValues = parseColor(color);

// Use the values of r,g,b to calculate the lightness in the HSl representation
//First calculate the fraction of the light in each color, since in css the value of r,g,b is in the interval of [0,255], we have
if (colorValues) {
const red = colorValues[0] / 255;
const green = colorValues[1] / 255;
const blue = colorValues[2] / 255;

const red = r / 255;
const green = g / 255;
const blue = b / 255;

//Then the lightness in the HSL representation is the average between maximum fraction of r,g,b and the minimum fraction
return (Math.max(red, green, blue) + Math.min(red, green, blue)) * 50;
}

function parseHexColor(color: string) {
if (color.length === 4) {
color = color.replace(/(.)/g, '$1$1');
}

const colors = color.replace('#', '');
const r = parseInt(colors.substr(0, 2), 16);
const g = parseInt(colors.substr(2, 2), 16);
const b = parseInt(colors.substr(4, 2), 16);

return [r, g, b];
}

function parseRGBColor(color: string) {
const colors = color.match(RGBA_REGEX);

if (colors) {
const r = parseInt(colors[1]);
const g = parseInt(colors[2]);
const b = parseInt(colors[3]);
return [r, g, b];
//Then the lightness in the HSL representation is the average between maximum fraction of r,g,b and the minimum fraction
return (Math.max(red, green, blue) + Math.min(red, green, blue)) * 50;
} else {
return [255, 255, 255];
return 255;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,28 @@ describe('getColor with darkColorHandler', () => {
expect(parseColorValue).toHaveBeenCalledTimes(1);
expect(parseColorValue).toHaveBeenCalledWith('red');
});

it('get color from FONT tag in dark mode', () => {
const parseColorValue = jasmine.createSpy().and.returnValue({
lightModeColor: 'green',
});
const findLightColorFromDarkColor = jasmine.createSpy().and.returnValue('red');
const darkColorHandler = ({
parseColorValue,
findLightColorFromDarkColor,
} as any) as DarkColorHandler;
const font = document.createElement('font');

font.color = '#112233';

const color = getColor(font, false, darkColorHandler, true);

expect(color).toBe('green');
expect(parseColorValue).toHaveBeenCalledTimes(1);
expect(parseColorValue).toHaveBeenCalledWith('red');
expect(findLightColorFromDarkColor).toHaveBeenCalledTimes(1);
expect(findLightColorFromDarkColor).toHaveBeenCalledWith('#112233');
});
});

describe('setColor', () => {
Expand Down
44 changes: 39 additions & 5 deletions packages/roosterjs-editor-core/lib/coreApi/transformColor.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { arrayPush, safeInstanceOf, setColor, toArray } from 'roosterjs-editor-dom';
import { arrayPush, getTagOfNode, safeInstanceOf, setColor, toArray } from 'roosterjs-editor-dom';
import {
ColorTransformDirection,
DarkColorHandler,
Expand Down Expand Up @@ -73,10 +73,12 @@ export const transformColor: TransformColor = (
function transformV2(elements: HTMLElement[], darkColorHandler: DarkColorHandler, toDark: boolean) {
elements.forEach(element => {
ColorAttributeName.forEach((names, i) => {
const color = darkColorHandler.parseColorValue(
element.style.getPropertyValue(names[ColorAttributeEnum.CssColor]) ||
element.getAttribute(names[ColorAttributeEnum.HtmlColor])
).lightModeColor;
const color =
tryFetchAndClearFontColor(element, toDark, darkColorHandler, names) ||
darkColorHandler.parseColorValue(
element.style.getPropertyValue(names[ColorAttributeEnum.CssColor]) ||
element.getAttribute(names[ColorAttributeEnum.HtmlColor])
).lightModeColor;

if (color && color != 'inherit') {
setColor(
Expand Down Expand Up @@ -200,3 +202,35 @@ function isHTMLElement(element: Element): element is HTMLElement {
const htmlElement = <HTMLElement>element;
return !!htmlElement.style && !!htmlElement.dataset;
}

/**
* There is a known issue that when input with IME in Chrome, it is possible Chrome insert a new FONT tag with colors.
* If editor is in dark mode, this color will cause the FONT tag doesn't have light mode color info so that after convert
* to light mode the color will be wrong.
* To workaround it, we check if this is a known color (for light mode with VariableBasedDarkColor enabled, all used colors
* are stored in darkColorHandler), then use the related light mode color instead.
*/
function tryFetchAndClearFontColor(
element: HTMLElement,
toDark: boolean,
darkColorHandler: DarkColorHandler,
names: { [key in ColorAttributeEnum]: string }
) {
let darkColor: string | null;

if (
getTagOfNode(element) == 'FONT' &&
!element.style.getPropertyValue(names[ColorAttributeEnum.CssColor]) &&
!toDark &&
(darkColor = element.getAttribute(names[ColorAttributeEnum.HtmlColor]))
) {
const lightColor = darkColorHandler.findLightColorFromDarkColor(darkColor);

if (lightColor) {
element.removeAttribute(names[ColorAttributeEnum.HtmlColor]);
return lightColor;
}
}

return null;
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ColorKeyAndValue, DarkColorHandler, ModeIndependentColor } from 'roosterjs-editor-types';
import { getObjectKeys } from 'roosterjs-editor-dom';
import { getObjectKeys, parseColor } from 'roosterjs-editor-dom';

const VARIABLE_REGEX = /^\s*var\(\s*(\-\-[a-zA-Z0-9\-_]+)\s*(?:,\s*(.*))?\)\s*$/;
const VARIABLE_PREFIX = 'var(';
Expand Down Expand Up @@ -89,4 +89,31 @@ export default class DarkColorHandlerImpl implements DarkColorHandler {

return { key, lightModeColor, darkModeColor };
}

/**
* Find related light mode color from dark mode color.
* @param darkColor The existing dark color
*/
findLightColorFromDarkColor(darkColor: string): string | null {
const rgbSearch = parseColor(darkColor);

if (rgbSearch) {
const key = getObjectKeys(this.knownColors).find(key => {
const rgbCurrent = parseColor(this.knownColors[key].darkModeColor);

return (
rgbCurrent &&
rgbCurrent[0] == rgbSearch[0] &&
rgbCurrent[1] == rgbSearch[1] &&
rgbCurrent[2] == rgbSearch[2]
);
});

if (key) {
return this.knownColors[key].lightModeColor;
}
}

return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -234,3 +234,94 @@ describe('DarkColorHandlerImpl.reset', () => {
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');
});
});
1 change: 1 addition & 0 deletions packages/roosterjs-editor-dom/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export { default as createElement, KnownCreateElementData } from './utils/create
export { default as moveChildNodes } from './utils/moveChildNodes';
export { default as getIntersectedRect } from './utils/getIntersectedRect';
export { default as isNodeAfter } from './utils/isNodeAfter';
export { default as parseColor } from './utils/parseColor';

export { default as VTable } from './table/VTable';
export { default as isWholeTableSelected } from './table/isWholeTableSelected';
Expand Down
29 changes: 29 additions & 0 deletions packages/roosterjs-editor-dom/lib/utils/parseColor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
const HEX3_REGEX = /^#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])$/;
const HEX6_REGEX = /^#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})$/;
const RGB_REGEX = /^rgb\(\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\s*\)$/;
const RGBA_REGEX = /^rgba\(\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\s*\)$/;

/**
* Parse color string to r/g/b value.
* If the given color is not in a recognized format, return null
*/
export default function parseColor(color: string): [number, number, number] | null {
color = (color || '').trim();

let match: RegExpMatchArray | null;
if ((match = color.match(HEX3_REGEX))) {
return [
parseInt(match[1] + match[1], 16),
parseInt(match[2] + match[2], 16),
parseInt(match[3] + match[3], 16),
];
} else if ((match = color.match(HEX6_REGEX))) {
return [parseInt(match[1], 16), parseInt(match[2], 16), parseInt(match[3], 16)];
} else if ((match = color.match(RGB_REGEX) || color.match(RGBA_REGEX))) {
return [parseInt(match[1]), parseInt(match[2]), parseInt(match[3])];
} else {
// CSS color names such as red, green is not included for now.
// If need, we can add those colors from https://www.w3.org/wiki/CSS/Properties/color/keywords
return null;
}
}
Loading