// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License.txt in the project root for license information.
//High Level
// - Injects and maintains a dynamic stylesheet to style elements for user
// accent colors.The RGB values of the colors are not available until run
// time and may also change at any time during run time.
// - Allows CSS syntax styling of elements to apply user accent colors via
// code, e.g.:
// Accents.createAccentRule(".myControl input", [
// { name: "color", value: Accents.ColorTypes.accent },
// { name: "border-color", value: Accents.ColorTypes.accent }
// ]);
// Note: The Accents Module is currently only available internally to
// WinJS controls.
// - Supports
// - Primary Accent color
// - Redline specific accent shades
// - Picks the right shading depending on ui- light or ui- dark
//Theme detection
// Since accent color shades vary depending on the theme of the app
// (light vs.dark), we first need to detect which theme is currently applied.
// This detection is done once, during app launch, and the scenario where the
// theme stylesheet is swapped at runtime is not supported.
// The ui- dark stylesheet contains the following rule that ui- light does not:
// winjs - themedetection - tag {
// opacity: 0;
// }
// The accent color implementation injects an element
// into the < head > at launch and examines if the computed opacity is 0 to see if
// we are in the dark theme.This tag is removed after examination.
//Specificity Concerns
// WinJS apps headers look like this:
//
//
//
//
//
//
//
//The dynamic stylesheet is always on the bottom of the head section of the
// document, which means that it will trump WinJS and app developer styles that
// have the same specificity.This is intended as we consider accent colors as
// part of a control's feature implementation. If the app developer wants to
// forcefully overwrite the accent color of a specific element, they are expected
// to write a more specific selector.
//Availability
// Real accent colors are only available when WinRT is present.We depend on the
// _WinRT.Windows.UI.ViewManagement.UISettings.colorValuesChanged event to
// set/update the accent colors.
// When WinRT is unavailable, we use a hard- coded value as the accent color as
// a fallback.WinJS doesn’t currently support a way to change this fallback value.
//Implementation
// The Accent Color module injects a dynamic stylesheet into the head of the
// document and updates that style when new accent rules are added.It batches
// updates so that multiple, synchronous additions only trigger one stylesheet
// rewrite.The full implementation consists of many, many string concatenations
// which are much better documented in the actual source code.
import _Global = require("./Core/_Global");
import _WinRT = require("./Core/_WinRT");
import _Base = require("./Core/_Base");
import _BaseUtils = require("./Core/_BaseUtils");
import _ElementUtilities = require('./Utilities/_ElementUtilities');
var Constants = {
accentStyleId: "WinJSAccentsStyle",
themeDetectionTag: "winjs-themedetection-tag",
hoverSelector: "html.win-hoverable",
lightThemeSelector: ".win-ui-light",
darkThemeSelector: ".win-ui-dark"
};
var CSSSelectorTokens = [".", "#", ":"];
var UISettings: _WinRT.Windows.UI.ViewManagement.UISettings = null;
var colors: string[] = [];
var isDarkTheme = false;
var rules: { selector: string; props: { name: string; value: ColorTypes; }[]; }[] = [];
var writeRulesTOHandle = -1;
// Public APIs
//
// Enum values align with the colors array indices
export enum ColorTypes {
accent = 0,
listSelectRest = 1,
listSelectHover = 2,
listSelectPress = 3,
_listSelectRestInverse = 4,
_listSelectHoverInverse = 5,
_listSelectPressInverse = 6,
}
export function createAccentRule(selector: string, props: { name: string; value: ColorTypes; }[]) {
rules.push({ selector: selector, props: props });
scheduleWriteRules();
}
// Private helpers
//
function scheduleWriteRules() {
if (rules.length === 0 || writeRulesTOHandle !== -1) {
return;
}
writeRulesTOHandle = _BaseUtils._setImmediate(() => {
writeRulesTOHandle = -1;
cleanup();
var inverseThemeSelector = isDarkTheme ? Constants.lightThemeSelector : Constants.darkThemeSelector;
var inverseThemeHoverSelector = Constants.hoverSelector + " " + inverseThemeSelector;
var style = _Global.document.createElement("style");
style.id = Constants.accentStyleId;
style.textContent = rules.map(rule => {
// example rule: { selector: " .foo, html.win-hoverable .bar:hover , div:hover ", props: [{ name: "color", value: 0 }, { name: "background-color", value: 1 } }
var body = " " + rule.props.map(prop => prop.name + ": " + colors[prop.value] + ";").join("\n ");
// body = color: *accent*; background-color: *listSelectHover*
var selectorSplit = rule.selector.split(",").map(str => sanitizeSpaces(str)); // [".foo", ".bar:hover", "div"]
var selector = selectorSplit.join(",\n"); // ".foo, html.win-hoverable .bar:hover, div:hover"
var css = selector + " {\n" + body + "\n}";
// css = .foo, html.win-hoverable .bar:hover, div:hover { *body* }
// Inverse Theme Selectors
var isThemedColor = rule.props.some(prop => prop.value !== ColorTypes.accent)
if (isThemedColor) {
var inverseBody = " " + rule.props.map(prop => prop.name + ": " + colors[(prop.value ? (prop.value + 3) : prop.value)] + ";").join("\n ");
// inverseBody = "color: *accent*; background-color: *listSelectHoverInverse"
var themedSelectors: string[] = [];
selectorSplit.forEach(sel => {
if (sel.indexOf(Constants.hoverSelector) !== -1 && sel.indexOf(inverseThemeHoverSelector) === -1) {
themedSelectors.push(sel.replace(Constants.hoverSelector, inverseThemeHoverSelector));
var selWithoutHover = sel.replace(Constants.hoverSelector, "").trim();
if (CSSSelectorTokens.indexOf(selWithoutHover[0]) !== -1) {
themedSelectors.push(sel.replace(Constants.hoverSelector + " ", inverseThemeHoverSelector));
}
} else {
themedSelectors.push(inverseThemeSelector + " " + sel);
if (CSSSelectorTokens.indexOf(sel[0]) !== -1) {
themedSelectors.push(inverseThemeSelector + sel);
}
}
css += "\n" + themedSelectors.join(",\n") + " {\n" + inverseBody + "\n}";
});
// css
//.foo, html.win-hoverable .bar:hover, div:hover, { *body* }
//.win-ui-light .foo,
//.win-ui-light.foo,
//html.win-hoverable .win-ui-light .bar:hover,
//html.win-hoverable .win-ui-light.bar:hover,
//.win-ui-light div:hover { *inverseBody* }
}
return css;
}).join("\n");
_Global.document.head.appendChild(style);
});
}
function handleColorsChanged() {
var UIColorType = _WinRT.Windows.UI.ViewManagement.UIColorType;
var uiColor = UISettings.getColorValue(_WinRT.Windows.UI.ViewManagement.UIColorType.accent);
var accent = colorToString(uiColor, 1);
if (colors[0] === accent) {
return;
}
// Establish colors
// The order of the colors align with the ColorTypes enum values
colors.length = 0;
colors.push(
accent,
colorToString(uiColor, (isDarkTheme ? 0.6 : 0.4)),
colorToString(uiColor, (isDarkTheme ? 0.8 : 0.6)),
colorToString(uiColor, (isDarkTheme ? 0.9 : 0.7)),
colorToString(uiColor, (isDarkTheme ? 0.4 : 0.6)),
colorToString(uiColor, (isDarkTheme ? 0.6 : 0.8)),
colorToString(uiColor, (isDarkTheme ? 0.7 : 0.9)));
scheduleWriteRules();
}
function colorToString(color: _WinRT.Windows.UI.Color, alpha: number) {
return "rgba(" + color.r + "," + color.g + "," + color.b + "," + alpha + ")";
}
function sanitizeSpaces(str: string) {
return str.replace(/ /g, " ").replace(/ /g, " ").trim();
}
function cleanup() {
var style = _Global.document.head.querySelector("#" + Constants.accentStyleId);
style && style.parentNode.removeChild(style);
}
function _reset() {
rules.length = 0;
cleanup();
}
// Module initialization
//
// Figure out color theme
var tag = _Global.document.createElement(Constants.themeDetectionTag);
_Global.document.head.appendChild(tag);
var cs = _ElementUtilities._getComputedStyle(tag);
isDarkTheme = cs.opacity === "0";
tag.parentElement.removeChild(tag);
try {
UISettings = new _WinRT.Windows.UI.ViewManagement.UISettings();
UISettings.addEventListener("colorvalueschanged", handleColorsChanged);
handleColorsChanged();
} catch (e) {
// No WinRT - use hardcoded blue accent color
// The order of the colors align with the ColorTypes enum values
colors.push(
"rgb(0, 120, 215)",
"rgba(0, 120, 215, " + (isDarkTheme ? "0.6" : "0.4") + ")",
"rgba(0, 120, 215, " + (isDarkTheme ? "0.8" : "0.6") + ")",
"rgba(0, 120, 215, " + (isDarkTheme ? "0.9" : "0.7") + ")",
"rgba(0, 120, 215, " + (isDarkTheme ? "0.4" : "0.6") + ")",
"rgba(0, 120, 215, " + (isDarkTheme ? "0.6" : "0.8") + ")",
"rgba(0, 120, 215, " + (isDarkTheme ? "0.7" : "0.9") + ")");
}
// Publish to WinJS namespace
var toPublish = {
ColorTypes: ColorTypes,
createAccentRule: createAccentRule,
// Exposed for tests
_colors: colors,
_reset: _reset,
_isDarkTheme: isDarkTheme
};
_Base.Namespace.define("WinJS.UI._Accents", toPublish);