Convert theme to TS
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
This commit is contained in:
parent
6adf762569
commit
ff30dacc84
2 changed files with 35 additions and 9 deletions
258
src/theme.ts
Normal file
258
src/theme.ts
Normal file
|
@ -0,0 +1,258 @@
|
|||
/*
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { _t } from "./languageHandler";
|
||||
|
||||
import SettingsStore from "./settings/SettingsStore";
|
||||
import ThemeWatcher from "./settings/watchers/ThemeWatcher";
|
||||
|
||||
export const DEFAULT_THEME = "light";
|
||||
|
||||
interface IFontFaces {
|
||||
src: {
|
||||
format: string;
|
||||
url: string;
|
||||
local: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
interface ICustomTheme {
|
||||
colors: {
|
||||
[key: string]: string;
|
||||
};
|
||||
fonts: {
|
||||
faces: IFontFaces[];
|
||||
general: string;
|
||||
monospace: string;
|
||||
};
|
||||
is_dark?: boolean; // eslint-disable-line camelcase
|
||||
}
|
||||
|
||||
export function enumerateThemes(): {[key: string]: string} {
|
||||
const BUILTIN_THEMES = {
|
||||
"light": _t("Light"),
|
||||
"dark": _t("Dark"),
|
||||
};
|
||||
const customThemes = SettingsStore.getValue("custom_themes");
|
||||
const customThemeNames = {};
|
||||
for (const { name } of customThemes) {
|
||||
customThemeNames[`custom-${name}`] = name;
|
||||
}
|
||||
return Object.assign({}, customThemeNames, BUILTIN_THEMES);
|
||||
}
|
||||
|
||||
function clearCustomTheme(): void {
|
||||
// remove all css variables, we assume these are there because of the custom theme
|
||||
const inlineStyleProps = Object.values(document.body.style);
|
||||
for (const prop of inlineStyleProps) {
|
||||
if (prop.startsWith("--")) {
|
||||
document.body.style.removeProperty(prop);
|
||||
}
|
||||
}
|
||||
const customFontFaceStyle = document.querySelector("head > style[title='custom-theme-font-faces']");
|
||||
if (customFontFaceStyle) {
|
||||
customFontFaceStyle.remove();
|
||||
}
|
||||
}
|
||||
|
||||
const allowedFontFaceProps = [
|
||||
"font-display",
|
||||
"font-family",
|
||||
"font-stretch",
|
||||
"font-style",
|
||||
"font-weight",
|
||||
"font-variant",
|
||||
"font-feature-settings",
|
||||
"font-variation-settings",
|
||||
"src",
|
||||
"unicode-range",
|
||||
];
|
||||
|
||||
function generateCustomFontFaceCSS(faces: IFontFaces[]): string {
|
||||
return faces.map(face => {
|
||||
const src = face.src && face.src.map(srcElement => {
|
||||
let format;
|
||||
if (srcElement.format) {
|
||||
format = `format("${srcElement.format}")`;
|
||||
}
|
||||
if (srcElement.url) {
|
||||
return `url("${srcElement.url}") ${format}`;
|
||||
} else if (srcElement.local) {
|
||||
return `local("${srcElement.local}") ${format}`;
|
||||
}
|
||||
return "";
|
||||
}).join(", ");
|
||||
const props = Object.keys(face).filter(prop => allowedFontFaceProps.includes(prop));
|
||||
const body = props.map(prop => {
|
||||
let value;
|
||||
if (prop === "src") {
|
||||
value = src;
|
||||
} else if (prop === "font-family") {
|
||||
value = `"${face[prop]}"`;
|
||||
} else {
|
||||
value = face[prop];
|
||||
}
|
||||
return `${prop}: ${value}`;
|
||||
}).join(";");
|
||||
return `@font-face {${body}}`;
|
||||
}).join("\n");
|
||||
}
|
||||
|
||||
function setCustomThemeVars(customTheme: ICustomTheme): void {
|
||||
const { style } = document.body;
|
||||
|
||||
function setCSSColorVariable(name, hexColor, doPct = true) {
|
||||
style.setProperty(`--${name}`, hexColor);
|
||||
if (doPct) {
|
||||
// uses #rrggbbaa to define the color with alpha values at 0%, 15% and 50%
|
||||
style.setProperty(`--${name}-0pct`, hexColor + "00");
|
||||
style.setProperty(`--${name}-15pct`, hexColor + "26");
|
||||
style.setProperty(`--${name}-50pct`, hexColor + "7F");
|
||||
}
|
||||
}
|
||||
|
||||
if (customTheme.colors) {
|
||||
for (const [name, value] of Object.entries(customTheme.colors)) {
|
||||
if (Array.isArray(value)) {
|
||||
for (let i = 0; i < value.length; i += 1) {
|
||||
setCSSColorVariable(`${name}_${i}`, value[i], false);
|
||||
}
|
||||
} else {
|
||||
setCSSColorVariable(name, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (customTheme.fonts) {
|
||||
const { fonts } = customTheme;
|
||||
if (fonts.faces) {
|
||||
const css = generateCustomFontFaceCSS(fonts.faces);
|
||||
const style = document.createElement("style");
|
||||
style.setAttribute("title", "custom-theme-font-faces");
|
||||
style.setAttribute("type", "text/css");
|
||||
style.appendChild(document.createTextNode(css));
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
if (fonts.general) {
|
||||
style.setProperty("--font-family", fonts.general);
|
||||
}
|
||||
if (fonts.monospace) {
|
||||
style.setProperty("--font-family-monospace", fonts.monospace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getCustomTheme(themeName: string): ICustomTheme {
|
||||
// set css variables
|
||||
const customThemes = SettingsStore.getValue("custom_themes");
|
||||
if (!customThemes) {
|
||||
throw new Error(`No custom themes set, can't set custom theme "${themeName}"`);
|
||||
}
|
||||
const customTheme = customThemes.find(t => t.name === themeName);
|
||||
if (!customTheme) {
|
||||
const knownNames = customThemes.map(t => t.name).join(", ");
|
||||
throw new Error(`Can't find custom theme "${themeName}", only know ${knownNames}`);
|
||||
}
|
||||
return customTheme;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called whenever someone changes the theme
|
||||
* Async function that returns once the theme has been set
|
||||
* (ie. the CSS has been loaded)
|
||||
*
|
||||
* @param {string} theme new theme
|
||||
*/
|
||||
export async function setTheme(theme: string): Promise<void> {
|
||||
if (!theme) {
|
||||
const themeWatcher = new ThemeWatcher();
|
||||
theme = themeWatcher.getEffectiveTheme();
|
||||
}
|
||||
clearCustomTheme();
|
||||
let stylesheetName = theme;
|
||||
if (theme.startsWith("custom-")) {
|
||||
const customTheme = getCustomTheme(theme.substr(7));
|
||||
stylesheetName = customTheme.is_dark ? "dark-custom" : "light-custom";
|
||||
setCustomThemeVars(customTheme);
|
||||
}
|
||||
|
||||
// look for the stylesheet elements.
|
||||
// styleElements is a map from style name to HTMLLinkElement.
|
||||
const styleElements = Object.create(null);
|
||||
const themes = Array.from(document.querySelectorAll('[data-mx-theme]'));
|
||||
themes.forEach(theme => {
|
||||
styleElements[theme.attributes['data-mx-theme'].value.toLowerCase()] = theme;
|
||||
});
|
||||
|
||||
if (!(stylesheetName in styleElements)) {
|
||||
throw new Error("Unknown theme " + stylesheetName);
|
||||
}
|
||||
|
||||
// disable all of them first, then enable the one we want. Chrome only
|
||||
// bothers to do an update on a true->false transition, so this ensures
|
||||
// that we get exactly one update, at the right time.
|
||||
//
|
||||
// ^ This comment was true when we used to use alternative stylesheets
|
||||
// for the CSS. Nowadays we just set them all as disabled in index.html
|
||||
// and enable them as needed. It might be cleaner to disable them all
|
||||
// at the same time to prevent loading two themes simultaneously and
|
||||
// having them interact badly... but this causes a flash of unstyled app
|
||||
// which is even uglier. So we don't.
|
||||
|
||||
styleElements[stylesheetName].disabled = false;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const switchTheme = function() {
|
||||
// we re-enable our theme here just in case we raced with another
|
||||
// theme set request as per https://github.com/vector-im/element-web/issues/5601.
|
||||
// We could alternatively lock or similar to stop the race, but
|
||||
// this is probably good enough for now.
|
||||
styleElements[stylesheetName].disabled = false;
|
||||
Object.values(styleElements).forEach((a: HTMLStyleElement) => {
|
||||
if (a == styleElements[stylesheetName]) return;
|
||||
a.disabled = true;
|
||||
});
|
||||
const bodyStyles = global.getComputedStyle(document.body);
|
||||
if (bodyStyles.backgroundColor) {
|
||||
const metaElement: HTMLMetaElement = document.querySelector('meta[name="theme-color"]');
|
||||
metaElement.content = bodyStyles.backgroundColor;
|
||||
}
|
||||
resolve();
|
||||
};
|
||||
|
||||
// turns out that Firefox preloads the CSS for link elements with
|
||||
// the disabled attribute, but Chrome doesn't.
|
||||
|
||||
let cssLoaded = false;
|
||||
|
||||
styleElements[stylesheetName].onload = () => {
|
||||
switchTheme();
|
||||
};
|
||||
|
||||
for (let i = 0; i < document.styleSheets.length; i++) {
|
||||
const ss = document.styleSheets[i];
|
||||
if (ss && ss.href === styleElements[stylesheetName].href) {
|
||||
cssLoaded = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (cssLoaded) {
|
||||
styleElements[stylesheetName].onload = undefined;
|
||||
switchTheme();
|
||||
}
|
||||
});
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue