Skip to content

Instantly share code, notes, and snippets.

@JaminQ
Forked from hteumeuleu/darkModeHandler.ts
Last active January 3, 2020 06:39
Show Gist options
  • Save JaminQ/68b089b51a6054e5b8169949244d0c46 to your computer and use it in GitHub Desktop.
Save JaminQ/68b089b51a6054e5b8169949244d0c46 to your computer and use it in GitHub Desktop.
Outlook.com darkModeHandler - Support alpha
import ContentHandler from '../schema/ContentHandler';
import { transformElementForDarkMode, AlteredElement } from 'owa-dark-mode-utilities';
import {
ATTR_COLOR,
ATTR_BGCOLOR,
DATA_OG_STYLE_COLOR,
DATA_OG_ATTR_COLOR,
DATA_OG_STYLE_BACKGROUNDCOLOR,
DATA_OG_ATTR_BGCOLOR,
} from 'owa-content-colors';
export const DARK_MODE_HANDLER_NAME = 'darkModeHandler';
const DARK_MODE_SELECTOR = '*';
const EMPTY_STRING = '';
export class DarkModeHandler implements ContentHandler {
public readonly cssSelector = DARK_MODE_SELECTOR;
public readonly keywords = null;
private alteredElements: AlteredElement[];
constructor() {
this.alteredElements = [];
}
public readonly handler = (element: HTMLElement, keyword?: string) => {
const alteredElement = transformElementForDarkMode(element, false /* useSimpleMethod */);
if (alteredElement) {
this.alteredElements.push(alteredElement);
}
};
public readonly undoHandler = (elements: HTMLElement[]) => {
this.alteredElements.forEach(alteredElement => {
const { element, styleColor, attrColor, styleBGColor, attrBGColor } = alteredElement;
// It's possible we set an attribute based on default assumptions, and don't have a cached value to return to.
// Therefore, reset all attributes to their cached values or empty strings.
element.style.color = styleColor ? styleColor : EMPTY_STRING;
attrColor && element.setAttribute(ATTR_COLOR, attrColor);
element.style.backgroundColor = styleBGColor ? styleBGColor : EMPTY_STRING;
attrBGColor && element.setAttribute(ATTR_BGCOLOR, attrBGColor);
// Clean up our custom attr from DOM
element.removeAttribute(DATA_OG_STYLE_COLOR);
element.removeAttribute(DATA_OG_ATTR_COLOR);
element.removeAttribute(DATA_OG_STYLE_BACKGROUNDCOLOR);
element.removeAttribute(DATA_OG_ATTR_BGCOLOR);
});
this.alteredElements = [];
};
}
import Color from 'color';
import { getPaletteAsRawColors } from 'owa-theme';
const VALID_CONTRAST_VALUE = 4.5;
const EMPTY_STRING = '';
const ATTR_COLOR = 'color';
const ATTR_BGCOLOR = 'bgcolor';
const DATA_OG_STYLE_COLOR = 'data-ogsc';
const DATA_OG_ATTR_COLOR = 'data-ogac';
const DATA_OG_STYLE_BACKGROUNDCOLOR = 'data-ogsb';
const DATA_OG_ATTR_BGCOLOR = 'data-ogab';
export interface AlteredElement {
element: HTMLElement;
styleColor: string | null;
attrColor: string | null;
styleBGColor: string | null;
attrBGColor: string | null;
}
export default function transformElementForDarkMode(
element: HTMLElement,
useSimpleMethod?: boolean
): AlteredElement | null {
// If any element has an invalid color value, Color will throw an exception when trying to create a color object.
try {
const baseBGColor = Color(getPaletteAsRawColors().white);
let elementAltered = false;
const computedStyles = window.getComputedStyle(element);
// The color mapped to "white" is always used for the background in the reading pane.
// The actual color will be flipped to a dark value when darkMode is enabled.
const styleColor = element.style.color;
let textColor = Color(computedStyles.color || undefined);
// Legacy color support, do not remove even though HTML5 deprecates it
let attrColor = element.getAttribute(ATTR_COLOR);
// With the simple recolor flag on, recolor only elements with explicit color applied.
// This should be fine since we inline all CSS at the CTS step.
// Else, do our original contrast check logic.
if (useSimpleMethod ? styleColor || attrColor : !isValidContrast(textColor, baseBGColor)) {
textColor = fixContrast(textColor, baseBGColor, !!useSimpleMethod);
element.style.setProperty('color', getHexa(textColor), 'important');
element.setAttribute(DATA_OG_STYLE_COLOR, styleColor ? styleColor : EMPTY_STRING);
// Word seems to prioritize color over CSS color so copy/paste has issues without this
if (attrColor) {
element.setAttribute(ATTR_COLOR, getHexa(textColor));
element.setAttribute(DATA_OG_ATTR_COLOR, attrColor);
}
elementAltered = true;
}
// Get the background color from the element giving priority to style.
// If the element contains no background color, default to dark mode background to past contrast checks.
const styleBGColor = element.style.backgroundColor;
let bgColor = computedStyles.backgroundColor
? Color(computedStyles.backgroundColor)
: Color(styleBGColor || undefined);
// Legacy bgcolor support, do not remove even though HTML5 deprecates it
let attrBGColor = element.getAttribute(ATTR_BGCOLOR);
if (useSimpleMethod ? styleBGColor || attrBGColor : !isValidContrast(bgColor, textColor)) {
bgColor = fixContrast(bgColor, textColor, !!useSimpleMethod);
element.style.setProperty('background-color', getHexa(bgColor), 'important');
element.setAttribute(
DATA_OG_STYLE_BACKGROUNDCOLOR,
styleBGColor ? styleBGColor : EMPTY_STRING
);
// Word seems to prioritize bgcolor over CSS background-color so copy/paste has issues without this
if (attrBGColor) {
element.setAttribute(ATTR_BGCOLOR, getHexa(bgColor));
element.setAttribute(DATA_OG_ATTR_BGCOLOR, attrBGColor);
}
elementAltered = true;
}
// If we altered the element cache its attributes for restoring via the undoHandler.
if (elementAltered) {
return {
element: element,
styleColor,
attrColor,
styleBGColor,
attrBGColor,
};
}
} catch (e) {
// Simply swallow malformed CSS and move on as to avoid component errors.
}
return null;
}
function isValidContrast(color1: Color, color2: Color): boolean {
return color1.contrast(color2) >= VALID_CONTRAST_VALUE;
}
function fixContrast(color: Color, comparisonColor: Color, useSimpleMethod: boolean): Color {
const baseBGColor = Color(getPaletteAsRawColors().white);
const baseLValue = baseBGColor.lab().array()[0];
// Create the CIELAB color array from the provided color
const colorLab = color.lab().array();
let newLValue;
// With simple recolor, we don't try to contrast adjust anymore.
// This means that white text will become background colored.
if (useSimpleMethod) {
// Flipped and scaled L value because baseLValue reduced the range from [0, 100] to [baseLValue, 100]
newLValue = (100 - colorLab[0]) * ((100 - baseLValue) / 100) + baseLValue;
} else {
// L value for the color we need to contrast against
const comparisonLValue = comparisonColor.lab().array()[0];
const ceilingDarkLValue = 50; // values above this can't contrast against 100
const floorLightLValue = 50 + baseLValue; // values below this can't contrast against baseLValue
const midpointLValue = (floorLightLValue + ceilingDarkLValue) / 2;
// Scaled L value because baseLValue reduced the range from [0, 100] to [baseLValue, 100]
newLValue = colorLab[0] * ((100 - baseLValue) / 100) + baseLValue;
// Flip, or not flip, depends on whichever value is more contrasting
// Also avoid the dead zone in the middle caused by baseLValue not being 0
if (comparisonLValue > midpointLValue) {
// comparisonLValue isLight, newLValue should isDark
newLValue = Math.min(newLValue, 2 * midpointLValue - newLValue);
newLValue =
((newLValue - baseLValue) * (ceilingDarkLValue - baseLValue)) /
(midpointLValue - baseLValue) +
baseLValue;
} else {
// comparisonLValue isDark, newLValue should isLight
newLValue = Math.max(newLValue, 2 * midpointLValue - newLValue);
newLValue =
100 - ((100 - newLValue) * (100 - floorLightLValue)) / (100 - midpointLValue);
}
}
// Create a new color from the old color values and new luminance value.
return Color.lab(newLValue, colorLab[1], colorLab[2]).alpha(color.alpha()).rgb();
}
function getHexa(color: Color): string {
// Calculate the hexadecimal value of transparency
let alpha = Math.round(color.alpha() * 255).toString(16).toUpperCase();
// Make up for short length
if (alpha.length === 1) alpha = `0${alpha}`;
// Create the hexa
return `#${color.hex().slice(1)}${alpha}`;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment