feat: implement css-variables and css-calc (#7553)

* feat: implement basic support for css-variables

* fix(test): test-watch-android and test-watch-ios was broken

* fix: processing css-variables belong in CssProperty-classes

Not in the StyleScope.

* fix(css-variables): set style attribute override value from css-classes

* feat: add css calc-support using 'reduce-css-calc'

* fix(tslint): missing semicolon and incorrect quotemark

* feat: move css-variable handling to Style-class

* chor: add comments explaining css-variable implmentation

* chor: set css-variables before other style properties

* chor(css-variables): cleaning up

* chor: code style fixes

* test(CSS-CALC): Add tests for nested css-calc statements

* fix(CSS-CALC): dip-unit not supported by reduce-css-calc

* fix(tslint): use double quotemarks

* test(css-calc): test _cssCalcConverter directly

* chor(css-variables): rename and clean up _cssVariableConverter to _evaluateCssVariable

* chor: rename varname to varName for consistency

* chor: support css-calc and variables for normal properties

* chor: use string.replace to evaluate css-variables

* fix: Missing blank line before return

* chor: rename css-calc functions

* fix: undefined css-variables treated as 'unset'

* fix(tslint): use double quotemarks

* feat(css-variable): handle fallback values

* chor(css-variables): handle unsetValue

* chor: process css-calc and css-variables in style-scope

* chore: clean-up css-calc/variable expressions

* fix(css-calc): handle invalid expressions

* chore(CSSState): update comments

* chore(Style): rename css-variable functions

* chore(css-variables): describe fallback logic

* chore: move reset scoped css-variables to Style-class

* chore(CssState): simplify check for css expressions

* chore: add reduce-css-calc to /package.json
This commit is contained in:
Morten Sjøgren
2019-08-19 23:56:56 +02:00
committed by Svetoslav
parent 0adcb64cc3
commit 673c8087e0
8 changed files with 651 additions and 14 deletions

View File

@@ -34,6 +34,7 @@
"nativescript-typedoc-theme": "git://github.com/NativeScript/nativescript-typedoc-theme.git#master",
"parse-css": "git+https://github.com/tabatkins/parse-css.git",
"parserlib": "^1.1.1",
"reduce-css-calc": "^2.1.6",
"shady-css-parser": "^0.1.0",
"shelljs": "^0.7.0",
"source-map-support": "^0.4.17",
@@ -71,8 +72,8 @@
"pretest": "npm run setup && npm run tsc",
"test-android": "tns run android --path tests --justlaunch --no-watch",
"test-ios": "tns run ios --path tests --justlaunch --no-watch",
"test-watch-android": "npm run pretest && concurrently --kill-others \"npm run tsc-tiw\" \"tns livesync android --path tests --watch\"",
"test-watch-ios": "npm run pretest && concurrently --kill-others \"npm run tsc-tiw\" \"tns livesync ios --path tests --watch\"",
"test-watch-android": "npm run pretest && concurrently --kill-others \"npm run tsc-w\" \"tns run android --path tests --watch\"",
"test-watch-ios": "npm run pretest && concurrently --kill-others \"npm run tsc-w\" \"tns run ios --path tests --watch\"",
"typedoc": "typedoc --tsconfig tsconfig.typedoc.json --out bin/dist/apiref --includeDeclarations --name NativeScript --theme ./node_modules/nativescript-typedoc-theme --excludeExternals --externalPattern \"**/+(tns-core-modules|module).d.ts\"",
"dev-typedoc": "npm run typedoc && cd bin/dist/apiref && npx http-server",
"test-tsc-es2016": "npm run tsc -- -p tsconfig.public.es2016.json",

View File

@@ -13,6 +13,7 @@ import { resolveFileNameFromUrl, removeTaggedAdditionalCSS, addTaggedAdditionalC
import { unsetValue } from "tns-core-modules/ui/core/view";
import * as color from "tns-core-modules/color";
import * as fs from "tns-core-modules/file-system";
import { _evaluateCssCalcExpression } from "tns-core-modules/ui/core/properties/properties";
export function test_css_dataURI_is_applied_to_backgroundImageSource() {
const stack = new stackModule.StackLayout();
@@ -1426,6 +1427,413 @@ export function test_CascadingClassNamesAppliesAfterPageLoad() {
});
}
export function test_evaluateCssCalcExpression() {
TKUnit.assertEqual(_evaluateCssCalcExpression("calc(1px + 1px)"), "2px", "Simple calc (1)");
TKUnit.assertEqual(_evaluateCssCalcExpression("calc(50px - (20px - 30px))"), "60px", "Simple calc (2)");
TKUnit.assertEqual(_evaluateCssCalcExpression("calc(100px - (100px - 100%))"), "100%", "Simple calc (3)");
TKUnit.assertEqual(_evaluateCssCalcExpression("calc(100px + (100px - 100%))"), "calc(200px - 100%)", "Simple calc (4)");
TKUnit.assertEqual(_evaluateCssCalcExpression("calc(100% - 10px + 20px)"), "calc(100% + 10px)", "Simple calc (5)");
TKUnit.assertEqual(_evaluateCssCalcExpression("calc(100% + 10px - 20px)"), "calc(100% - 10px)", "Simple calc (6)");
TKUnit.assertEqual(_evaluateCssCalcExpression("calc(10.px + .0px)"), "10px", "Simple calc (8)");
TKUnit.assertEqual(_evaluateCssCalcExpression("a calc(1px + 1px)"), "a 2px", "Ignore value surrounding calc function (1)");
TKUnit.assertEqual(_evaluateCssCalcExpression("calc(1px + 1px) a"), "2px a", "Ignore value surrounding calc function (2)");
TKUnit.assertEqual(_evaluateCssCalcExpression("a calc(1px + 1px) b"), "a 2px b", "Ignore value surrounding calc function (3)");
TKUnit.assertEqual(_evaluateCssCalcExpression("a calc(1px + 1px) b calc(1em + 2em) c"), "a 2px b 3em c", "Ignore value surrounding calc function (4)");
TKUnit.assertEqual(_evaluateCssCalcExpression(`calc(\n1px \n* 2 \n* 1.5)`), "3px", "Handle new lines");
TKUnit.assertEqual(_evaluateCssCalcExpression("calc(1/100)"), "0.01", "Handle precision correctly (1)");
TKUnit.assertEqual(_evaluateCssCalcExpression("calc(5/1000000)"), "0.00001", "Handle precision correctly (2)");
TKUnit.assertEqual(_evaluateCssCalcExpression("calc(5/100000)"), "0.00005", "Handle precision correctly (3)");
}
export function test_css_calc() {
const page = helper.getClearCurrentPage();
const stack = new stackModule.StackLayout();
stack.css = `
StackLayout.slim {
width: calc(100 * .1);
}
StackLayout.wide {
width: calc(100 * 1.25);
}
StackLayout.invalid-css-calc {
width: calc(asd3 * 1.25);
}
`;
const label = new labelModule.Label();
page.content = stack;
stack.addChild(label);
stack.className = "slim";
TKUnit.assertEqual(stack.width as any, 10, "Stack - width === 10");
stack.className = "wide";
TKUnit.assertEqual(stack.width as any, 125, "Stack - width === 125");
(stack as any).style = `width: calc(100% / 2)`;
TKUnit.assertDeepEqual(stack.width, { unit: "%", value: 0.5 }, "Stack - width === 50%");
// This should log an error for the invalid css-calc expression, but not cause a crash
stack.className = "invalid-css-calc";
}
export function test_css_calc_units() {
const page = helper.getClearCurrentPage();
const stack = new stackModule.StackLayout();
stack.css = `
StackLayout.no_unit {
width: calc(100 * .1);
}
StackLayout.dip_unit {
width: calc(100dip * .1);
}
StackLayout.pct_unit {
width: calc(100% * .1);
}
StackLayout.px_unit {
width: calc(100px * .1);
}
`;
const label = new labelModule.Label();
page.content = stack;
stack.addChild(label);
stack.className = "no_unit";
TKUnit.assertEqual(stack.width as any, 10, "Stack - width === 10");
stack.className = "dip_unit";
TKUnit.assertEqual(stack.width as any, 10, "Stack - width === 10dip");
stack.className = "pct_unit";
TKUnit.assertDeepEqual(stack.width as any, { unit: "%", value: 0.1 }, "Stack - width === 10%");
stack.className = "px_unit";
TKUnit.assertDeepEqual(stack.width as any, { unit: "px", value: 10 }, "Stack - width === 10px");
}
export function test_nested_css_calc() {
const page = helper.getClearCurrentPage();
const stack = new stackModule.StackLayout();
stack.css = `
StackLayout.slim {
width: calc(calc(10 * 10) * .1);
}
StackLayout.wide {
width: calc(calc(10 * 10) * 1.25);
}
`;
const label = new labelModule.Label();
page.content = stack;
stack.addChild(label);
stack.className = "slim";
TKUnit.assertEqual(stack.width as any, 10, "Stack - width === 10");
stack.className = "wide";
TKUnit.assertEqual(stack.width as any, 125, "Stack - width === 125");
(stack as any).style = `width: calc(100% * calc(1 / 2)`;
TKUnit.assertDeepEqual(stack.width, { unit: "%", value: 0.5 }, "Stack - width === 50%");
}
export function test_css_variables() {
const blackColor = "#000000";
const redColor = "#FF0000";
const greenColor = "#00FF00";
const blueColor = "#0000FF";
const page = helper.getClearCurrentPage();
const cssVarName = `--my-background-color-${Date.now()}`;
const stack = new stackModule.StackLayout();
stack.css = `
StackLayout[use-css-vars] {
background-color: var(${cssVarName});
}
StackLayout.make-red {
${cssVarName}: red;
}
StackLayout.make-blue {
${cssVarName}: blue;
}
Label.lab1 {
background-color: var(${cssVarName});
color: black;
}`;
const label = new labelModule.Label();
page.content = stack;
stack.addChild(label);
// This should log an error about not finding the css-variable but not cause a crash
stack["use-css-vars"] = true;
label.className = "lab1";
stack.className = "make-red";
TKUnit.assertEqual(label.color.hex, blackColor, "text color is black");
TKUnit.assertEqual((<color.Color>stack.backgroundColor).hex, redColor, "Stack - background-color is red");
TKUnit.assertEqual((<color.Color>label.backgroundColor).hex, redColor, "Label - background-color is red");
stack.className = "make-blue";
TKUnit.assertEqual(label.color.hex, blackColor, "text color is black");
TKUnit.assertEqual((<color.Color>stack.backgroundColor).hex, blueColor, "Stack - background-color is blue");
TKUnit.assertEqual((<color.Color>label.backgroundColor).hex, blueColor, "Label - background-color is blue");
stack.className = "make-red";
TKUnit.assertEqual(label.color.hex, blackColor, "text color is black");
TKUnit.assertEqual((<color.Color>stack.backgroundColor).hex, redColor, "Stack - background-color is red");
TKUnit.assertEqual((<color.Color>label.backgroundColor).hex, redColor, "Label - background-color is red");
// view.style takes priority over css-classes.
(stack as any).style = `${cssVarName}: ${greenColor}`;
stack.className = "";
TKUnit.assertEqual(label.color.hex, blackColor, "text color is black");
TKUnit.assertEqual((<color.Color>stack.backgroundColor).hex, greenColor, "Stack - background-color is green");
TKUnit.assertEqual((<color.Color>label.backgroundColor).hex, greenColor, "Label - background-color is green");
stack.className = "make-red";
TKUnit.assertEqual(label.color.hex, blackColor, "text color is black");
TKUnit.assertEqual((<color.Color>stack.backgroundColor).hex, greenColor, "Stack - background-color is green");
TKUnit.assertEqual((<color.Color>label.backgroundColor).hex, greenColor, "Label - background-color is green");
(stack as any).style = "";
TKUnit.assertEqual(label.color.hex, blackColor, "text color is black");
TKUnit.assertEqual((<color.Color>stack.backgroundColor).hex, redColor, "Stack - background-color is red");
TKUnit.assertEqual((<color.Color>label.backgroundColor).hex, redColor, "Label - background-color is red");
}
export function test_css_calc_and_variables() {
const page = helper.getClearCurrentPage();
const cssVarName = `--my-width-factor-${Date.now()}`;
const stack = new stackModule.StackLayout();
stack.css = `
StackLayout[use-css-vars] {
${cssVarName}: 1;
width: calc(100% * var(${cssVarName}));
}
StackLayout.slim {
${cssVarName}: 0.1;
}
StackLayout.wide {
${cssVarName}: 1.25;
}
`;
const label = new labelModule.Label();
page.content = stack;
stack["use-css-vars"] = true;
stack.addChild(label);
stack.className = "";
TKUnit.assertDeepEqual(stack.width, { unit: "%", value: 1 }, "Stack - width === 100%");
stack.className = "slim";
TKUnit.assertDeepEqual(stack.width, { unit: "%", value: 0.1 }, "Stack - width === 10%");
stack.className = "wide";
TKUnit.assertDeepEqual(stack.width, { unit: "%", value: 1.25 }, "Stack - width === 125%");
// Test setting the CSS variable via the style-attribute, this should override any value set via css-class
(stack as any).style = `${cssVarName}: 0.5`;
TKUnit.assertDeepEqual(stack.width, { unit: "%", value: 0.5 }, "Stack - width === 50%");
}
export function test_css_variable_fallback() {
const redColor = "#FF0000";
const blueColor = "#0000FF";
const limeColor = new color.Color("lime").hex;
const yellowColor = new color.Color("yellow").hex;
const classToValue = [
{
className: "defined-css-variable",
expectedColor: blueColor,
}, {
className: "defined-css-variable-with-fallback",
expectedColor: blueColor,
}, {
className: "undefined-css-variable-without-fallback",
expectedColor: undefined,
}, {
className: "undefined-css-variable-with-fallback",
expectedColor: redColor,
}, {
className: "undefined-css-variable-with-defined-fallback",
expectedColor: limeColor,
}, {
className: "undefined-css-variable-with-multiple-fallbacks",
expectedColor: limeColor,
}, {
className: "undefined-css-variable-with-missing-fallback-value",
expectedColor: undefined,
}, {
className: "undefined-css-variable-with-nested-fallback",
expectedColor: yellowColor,
},
];
const page = helper.getClearCurrentPage();
const stack = new stackModule.StackLayout();
stack.css = `
.defined-css-variable {
--my-var: blue;
color: var(--my-var); /* resolved as color: blue; */
}
.defined-css-variable-with-fallback {
--my-var: blue;
color: var(--my-var, red); /* resolved as color: blue; */
}
.undefined-css-variable-without-fallback {
color: var(--undefined-var); /* resolved as color: unset; */
}
.undefined-css-variable-with-fallback {
color: var(--undefined-var, red); /* resolved as color: red; */
}
.undefined-css-variable-with-defined-fallback {
--my-fallback-var: lime;
color: var(--undefined-var, var(--my-fallback-var)); /* resolved as color: lime; */
}
.undefined-css-variable-with-multiple-fallbacks {
--my-fallback-var: lime;
color: var(--undefined-var, var(--my-fallback-var), yellow); /* resolved as color: lime; */
}
.undefined-css-variable-with-missing-fallback-value {
color: var(--undefined-var, var(--undefined-fallback-var)); /* resolved as color: unset; */
}
.undefined-css-variable-with-nested-fallback {
color: var(--undefined-var, var(--undefined-fallback-var, yellow)); /* resolved as color: yellow; */
}
`;
const label = new labelModule.Label();
page.content = stack;
stack.addChild(label);
for (const { className, expectedColor } of classToValue) {
label.className = className;
TKUnit.assertEqual(label.color && label.color.hex, expectedColor, className);
}
}
export function test_nested_css_calc_and_variables() {
const page = helper.getClearCurrentPage();
const cssVarName = `--my-width-factor-base-${Date.now()}`;
const cssVarName2 = `--my-width-factor-${Date.now()}`;
const stack = new stackModule.StackLayout();
stack.css = `
StackLayout[use-css-vars] {
${cssVarName}: 0.5;
${cssVarName2}: var(${cssVarName});
width: calc(100% * calc(var(${cssVarName2}) * 2));
}
StackLayout.slim {
${cssVarName}: 0.05;
}
StackLayout.wide {
${cssVarName}: 0.625
}
StackLayout.nested {
${cssVarName2}: calc(var(${cssVarName}) * 2);
}
`;
const label = new labelModule.Label();
page.content = stack;
stack["use-css-vars"] = true;
stack.addChild(label);
stack.className = "";
TKUnit.assertDeepEqual(stack.width, { unit: "%", value: 1 }, "Stack - width === 100%");
stack.className = "nested";
TKUnit.assertDeepEqual(stack.width, { unit: "%", value: 2 }, "Stack - width === 200%");
stack.className = "slim";
TKUnit.assertDeepEqual(stack.width, { unit: "%", value: 0.1 }, "Stack - width === 10%");
stack.className = "slim nested";
TKUnit.assertDeepEqual(stack.width, { unit: "%", value: 0.2 }, "Stack - width === 20%");
stack.className = "wide";
TKUnit.assertDeepEqual(stack.width, { unit: "%", value: 1.25 }, "Stack - width === 125%");
stack.className = "wide nested";
TKUnit.assertDeepEqual(stack.width, { unit: "%", value: 2.5 }, "Stack - width === 250%");
// Test setting the CSS variable via the style-attribute, this should override any value set via css-class
stack.className = "wide";
(stack as any).style = `${cssVarName}: 0.25`;
TKUnit.assertDeepEqual(stack.width, { unit: "%", value: 0.5 }, "Stack - width === 50%");
stack.className = "nested";
TKUnit.assertDeepEqual(stack.width, { unit: "%", value: 1 }, "Stack - width === 100%");
}
export function test_css_variable_is_applied_to_normal_properties() {
const stack = new stackModule.StackLayout();
const cssVarName = `--my-custom-variable-${Date.now()}`;
helper.buildUIAndRunTest(stack, function (views: Array<viewModule.View>) {
const page = <pageModule.Page>views[1];
const expected = "horizontal";
page.css = `StackLayout {
${cssVarName}: ${expected};
orientation: var(${cssVarName});
}`;
TKUnit.assertEqual(stack.orientation, expected);
});
}
export function test_css_variable_is_applied_to_special_properties() {
const stack = new stackModule.StackLayout();
const cssVarName = `--my-custom-variable-${Date.now()}`;
helper.buildUIAndRunTest(stack, function (views: Array<viewModule.View>) {
const page = <pageModule.Page>views[1];
const expected = "test";
page.css = `StackLayout {
${cssVarName}: ${expected};
class: var(${cssVarName});
}`;
TKUnit.assertEqual(stack.className, expected);
});
}
export function test_resolveFileNameFromUrl_local_file_tilda() {
const localFileExistsMock = (fileName: string) => true;
const url = "~/theme/core.css";

View File

@@ -26,6 +26,7 @@
"license": "Apache-2.0",
"typings": "tns-core-modules.d.ts",
"dependencies": {
"reduce-css-calc": "^2.1.6",
"tns-core-modules-widgets": "next",
"tslib": "^1.9.3"
},

View File

@@ -144,6 +144,10 @@ export function makeParser<T>(isValid: (value: any) => boolean): (value: any) =>
export function getSetProperties(view: ViewBase): [string, any][];
export function getComputedCssValues(view: ViewBase): [string, any][];
export function isCssVariable(property: string): boolean;
export function isCssCalcExpression(value: string): boolean;
export function isCssVariableExpression(value: string): boolean;
//@private
/**
* @private get all properties defined on ViewBase
@@ -154,4 +158,8 @@ export function _getProperties(): Property<any, any>[];
* @private get all properties defined on Style
*/
export function _getStyleProperties(): CssProperty<any, any>[];
export function _evaluateCssVariableExpression(view: ViewBase, cssName: string, value: string): string;
export function _evaluateCssCalcExpression(value: string): string;
//@endprivate

View File

@@ -1,3 +1,5 @@
import reduceCSSCalc from "reduce-css-calc";
// Definitions.
import * as definitions from "../view-base";
import { ViewBase } from "../view-base";
@@ -56,6 +58,76 @@ export function _getStyleProperties(): CssProperty<any, any>[] {
return getPropertiesFromMap(cssSymbolPropertyMap) as CssProperty<any, any>[];
}
const cssVariableExpressionRegexp = /\bvar\(\s*(--[^,\s]+?)(?:\s*,\s*(.+))?\s*\)/;
const cssVariableAllExpressionsRegexp = /\bvar\(\s*(--[^,\s]+?)(?:\s*,\s*(.+))?\s*\)/g;
export function isCssVariable(property: string) {
return /^--[^,\s]+?$/.test(property);
}
export function isCssCalcExpression(value: string) {
return /\bcalc\(/.test(value);
}
export function isCssVariableExpression(value: string) {
return cssVariableExpressionRegexp.test(value);
}
export function _evaluateCssVariableExpression(view: ViewBase, cssName: string, value: string): string {
if (typeof value !== "string") {
return value;
}
if (!isCssVariableExpression(value)) {
// Value is not using css-variable(s)
return value;
}
let output = value.trim();
// Evaluate every (and nested) css-variables in the value.
let lastValue: string;
while (lastValue !== output) {
lastValue = output;
output = output.replace(cssVariableAllExpressionsRegexp, (matchStr, cssVariableName: string, fallbackStr: string) => {
const cssVariableValue = view.style.getCssVariable(cssVariableName);
if (cssVariableValue !== null) {
return cssVariableValue;
}
if (fallbackStr) {
// css-variable not found, using fallback-string.
const fallbackOutput = _evaluateCssVariableExpression(view, cssName, fallbackStr);
if (fallbackOutput) {
// If the fallback have multiple values, return the first of them.
return fallbackOutput.split(",")[0];
}
}
// Couldn't find a value for the css-variable or the fallback, return "unset"
traceWrite(`Failed to get value for css-variable "${cssVariableName}" used in "${cssName}"=[${value}] to ${view}`, traceCategories.Style, traceMessageType.error);
return "unset";
});
}
return output;
}
export function _evaluateCssCalcExpression(value: string) {
if (typeof value !== "string") {
return value;
}
if (isCssCalcExpression(value)) {
// WORKAROUND: reduce-css-calc can't handle the dip-unit.
return reduceCSSCalc(value.replace(/([0-9]+(\.[0-9]+)?)dip\b/g, "$1"));
} else {
return value;
}
}
function getPropertiesFromMap(map): Property<any, any>[] | CssProperty<any, any>[] {
const props = [];
Object.getOwnPropertySymbols(map).forEach(symbol => props.push(map[symbol]));

View File

@@ -1,7 +1,7 @@
import { Keyframes } from "../animation/keyframe-animation";
import { ViewBase } from "../core/view-base";
import { View } from "../core/view";
import { unsetValue } from "../core/properties";
import { unsetValue, _evaluateCssVariableExpression, _evaluateCssCalcExpression, isCssVariable, isCssVariableExpression, isCssCalcExpression } from "../core/properties";
import {
SyntaxTree,
Keyframes as KeyframesDefinition,
@@ -42,6 +42,7 @@ function ensureKeyframeAnimationModule() {
import * as capm from "./css-animation-parser";
import { sanitizeModuleName } from "../builder/module-name-sanitizer";
import { resolveModuleName } from "../../module-name-resolver";
let cssAnimationParserModule: typeof capm;
function ensureCssAnimationParserModule() {
if (!cssAnimationParserModule) {
@@ -59,6 +60,28 @@ try {
//
}
/**
* Evaluate css-variable and css-calc expressions
*/
function evaluateCssExpressions(view: ViewBase, property: string, value: string) {
const newValue = _evaluateCssVariableExpression(view, property, value);
if (newValue === "unset") {
return unsetValue;
}
value = newValue;
try {
value = _evaluateCssCalcExpression(value);
} catch (e) {
traceWrite(`Failed to evaluate css-calc for property [${property}] for expression [${value}] to ${view}. ${e.stack}`, traceCategories.Error, traceMessageType.error);
return unsetValue;
}
return value;
}
export function mergeCssSelectors(): void {
applicationCssSelectors = applicationSelectors.slice();
applicationCssSelectors.push.apply(applicationCssSelectors, applicationAdditionalSelectors);
@@ -522,22 +545,60 @@ export class CssState {
matchingSelectors.forEach(selector =>
selector.ruleset.declarations.forEach(declaration =>
newPropertyValues[declaration.property] = declaration.value));
Object.freeze(newPropertyValues);
const oldProperties = this._appliedPropertyValues;
for (const key in oldProperties) {
if (!(key in newPropertyValues)) {
if (key in view.style) {
view.style[`css:${key}`] = unsetValue;
let isCssExpressionInUse = false;
// Update values for the scope's css-variables
view.style.resetScopedCssVariables();
for (const property in newPropertyValues) {
const value = newPropertyValues[property];
if (isCssVariable(property)) {
view.style.setScopedCssVariable(property, value);
delete newPropertyValues[property];
continue;
}
isCssExpressionInUse = isCssExpressionInUse || isCssVariableExpression(value) || isCssCalcExpression(value);
}
if (isCssExpressionInUse) {
// Evalute css-expressions to get the latest values.
for (const property in newPropertyValues) {
const value = evaluateCssExpressions(view, property, newPropertyValues[property]);
if (value === unsetValue) {
delete newPropertyValues[property];
continue;
}
newPropertyValues[property] = value;
}
}
// Property values are fully updated, freeze the object to be used for next update.
Object.freeze(newPropertyValues);
// Unset removed values
for (const property in oldProperties) {
if (!(property in newPropertyValues)) {
if (property in view.style) {
view.style[`css:${property}`] = unsetValue;
} else {
// TRICKY: How do we unset local value?
}
}
}
// Set new values to the style
for (const property in newPropertyValues) {
if (oldProperties && property in oldProperties && oldProperties[property] === newPropertyValues[property]) {
// Skip unchanged values
continue;
}
const value = newPropertyValues[property];
try {
if (property in view.style) {
@@ -547,7 +608,7 @@ export class CssState {
view[camelCasedProperty] = value;
}
} catch (e) {
traceWrite(`Failed to apply property [${property}] with value [${value}] to ${view}. ${e}`, traceCategories.Error, traceMessageType.error);
traceWrite(`Failed to apply property [${property}] with value [${value}] to ${view}. ${e.stack}`, traceCategories.Error, traceMessageType.error);
}
}
@@ -819,21 +880,41 @@ function resolveFilePathFromImport(importSource: string, fileName: string): stri
export const applyInlineStyle = profile(function applyInlineStyle(view: ViewBase, styleStr: string) {
let localStyle = `local { ${styleStr} }`;
let inlineRuleSet = CSSSource.fromSource(localStyle, new Map()).selectors;
const style = view.style;
// Reset unscoped css-variables
view.style.resetUnscopedCssVariables();
// Set all the css-variables first, so we can be sure they are up-to-date
inlineRuleSet[0].declarations.forEach(d => {
// Use the actual property name so that a local value is set.
let property = d.property;
if (isCssVariable(property)) {
view.style.setUnscopedCssVariable(property, d.value);
}
});
inlineRuleSet[0].declarations.forEach(d => {
// Use the actual property name so that a local value is set.
let name = d.property;
let property = d.property;
try {
if (name in style) {
style[name] = d.value;
if (isCssVariable(property)) {
// Skip css-variables, they have been handled
return;
}
const value = evaluateCssExpressions(view, property, d.value);
if (property in view.style) {
view.style[property] = value;
} else {
view[name] = d.value;
view[property] = value;
}
} catch (e) {
traceWrite(`Failed to apply property [${d.property}] with value [${d.value}] to ${view}. ${e}`, traceCategories.Error, traceMessageType.error);
}
});
// This is needed in case of changes to css-variable or css-calc expressions.
view._onCssStateChange();
});
function isCurrentDirectory(uriPart: string): boolean {

View File

@@ -168,6 +168,32 @@ export class Style extends Observable {
* The shorthand properties are defined as non-enumerable so it should be safe to for-in the keys that are set in the bag.
*/
public readonly PropertyBag: PropertyBagClass;
/**
* Set a scoped css-value. These are css-variables set from CssState
*/
public setScopedCssVariable(varName: string, value: string): void;
/**
* Set a unscoped css-value. These are css-variables set on view.style
*/
public setUnscopedCssVariable(varName: string, value: string): void;
/**
* Get value of the css-variable.
* If the value is not set on this style-object, try the parent view.
*/
public getCssVariable(varName: string): string | null;
/**
* Remove all scoped css-variables
*/
public resetScopedCssVariables(): void;
/**
* Remove all unscoped css-variables
*/
public resetUnscopedCssVariables(): void;
}
interface PropertyBagClass {

View File

@@ -18,6 +18,9 @@ import {
import { TextAlignment, TextDecoration, TextTransform, WhiteSpace } from "../../text-base";
export class Style extends Observable implements StyleDefinition {
private unscopedCssVariables = new Map<string, string>();
private scopedCssVariables = new Map<string, string>();
constructor(ownerView: ViewBase | WeakRef<ViewBase>) {
super();
@@ -29,6 +32,43 @@ export class Style extends Observable implements StyleDefinition {
}
}
public setScopedCssVariable(varName: string, value: string): void {
this.scopedCssVariables.set(varName, value);
}
public setUnscopedCssVariable(varName: string, value: string): void {
this.unscopedCssVariables.set(varName, value);
}
public getCssVariable(varName: string): string | null {
const view = this.view;
if (!view) {
return null;
}
if (this.unscopedCssVariables.has(varName)) {
return this.unscopedCssVariables.get(varName);
}
if (this.scopedCssVariables.has(varName)) {
return this.scopedCssVariables.get(varName);
}
if (!view.parent || !view.parent.style) {
return null;
}
return view.parent.style.getCssVariable(varName);
}
public resetScopedCssVariables() {
this.scopedCssVariables.clear();
}
public resetUnscopedCssVariables() {
this.unscopedCssVariables.clear();
}
toString() {
const view = this.viewRef.get();
if (!view) {