mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-08-15 19:26:42 +08:00
Inital by-type split
Split type.class from CssTypeSelector to CssCompositeSelector, probably support type#id.class selectors Apply review comments, refactor css-selectors internally Applied refactoring, all tests pass, button does not notify changes Add tests for the css selectors parser. Added tests for css-selectors Added basic implementation of mayMatch and changeMap for css match state Implemented TKUnit.assertDeepEqual to check key and key/values in Map and Set Watch for property and pseudoClass changes Add one child group test Add typings for animations Added mechanism to enable/disable listeners for pseudo classes Count listeners instead of checking handlers, reverse subscription and unsubscription
This commit is contained in:
@ -227,6 +227,82 @@ export function assertEqual(actual: any, expected: any, message?: string) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert two json like objects are deep equal.
|
||||||
|
*/
|
||||||
|
export function assertDeepEqual(actual, expected, path: any[] = []): void {
|
||||||
|
let typeofActual = typeof actual;
|
||||||
|
let typeofExpected = typeof expected;
|
||||||
|
if (typeofActual !== typeofExpected) {
|
||||||
|
throw new Error("At /" + path.join("/") + " types of actual " + typeofActual + " and expected " + typeofExpected + " differ.");
|
||||||
|
} else if (typeofActual === "object" || typeofActual === "array") {
|
||||||
|
if (expected instanceof Map) {
|
||||||
|
if (actual instanceof Map) {
|
||||||
|
expected.forEach((value, key) => {
|
||||||
|
if (actual.has(key)) {
|
||||||
|
assertDeepEqual(actual.get(key), value, path.concat([key]));
|
||||||
|
} else {
|
||||||
|
throw new Error("At /" + path.join("/") + " expected Map has key '" + key + "' but actual does not.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
actual.forEach((value, key) => {
|
||||||
|
if (!expected.has(key)) {
|
||||||
|
throw new Error("At /" + path.join("/") + " actual Map has key '" + key + "' but expected does not.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new Error("At /" + path.join("/") + " expected is Map but actual is not.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (expected instanceof Set) {
|
||||||
|
if (actual instanceof Set) {
|
||||||
|
expected.forEach(i => {
|
||||||
|
if (!actual.has(i)) {
|
||||||
|
throw new Error("At /" + path.join("/") + " expected Set has item '" + i + "' but actual does not.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
actual.forEach(i => {
|
||||||
|
if (!expected.has(i)) {
|
||||||
|
throw new Error("At /" + path.join("/") + " actual Set has item '" + i + "' but expected does not.");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
throw new Error("At /" + path.join("/") + " expected is Set but actual is not.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let key in actual) {
|
||||||
|
if (!(key in expected)) {
|
||||||
|
throw new Error("At /" + path.join("/") + " found unexpected key " + key + ".");
|
||||||
|
}
|
||||||
|
assertDeepEqual(actual[key], expected[key], path.concat([key]));
|
||||||
|
}
|
||||||
|
for (let key in expected) {
|
||||||
|
if (!(key in actual)) {
|
||||||
|
throw new Error("At /" + path.join("/") + " expected a key " + key + ".");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (actual !== expected) {
|
||||||
|
throw new Error("At /" + path.join("/") + " actual: '" + actual + "' and expected: '" + expected + "' differ.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function assertDeepSuperset(actual, expected, path: any[] = []): void {
|
||||||
|
let typeofActual = typeof actual;
|
||||||
|
let typeofExpected = typeof expected;
|
||||||
|
if (typeofActual !== typeofExpected) {
|
||||||
|
throw new Error("At /" + path.join("/") + " types of actual " + typeofActual + " and expected " + typeofExpected + " differ.");
|
||||||
|
} else if (typeofActual === "object" || typeofActual === "array") {
|
||||||
|
for (let key in expected) {
|
||||||
|
if (!(key in actual)) {
|
||||||
|
throw new Error("At /" + path.join("/") + " expected a key " + key + ".");
|
||||||
|
}
|
||||||
|
assertDeepSuperset(actual[key], expected[key], path.concat([key]));
|
||||||
|
}
|
||||||
|
} else if (actual !== expected) {
|
||||||
|
throw new Error("At /" + path.join("/") + " actual: '" + actual + "' and expected: '" + expected + "' differ.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function assertNull(actual: any, message?: string) {
|
export function assertNull(actual: any, message?: string) {
|
||||||
if (actual !== null && actual !== undefined) {
|
if (actual !== null && actual !== undefined) {
|
||||||
throw new Error(message + " Actual: " + actual + " is not null/undefined");
|
throw new Error(message + " Actual: " + actual + " is not null/undefined");
|
||||||
|
@ -68,6 +68,8 @@ allTests["VIEW"] = require("./ui/view/view-tests");
|
|||||||
allTests["STYLE"] = require("./ui/styling/style-tests");
|
allTests["STYLE"] = require("./ui/styling/style-tests");
|
||||||
allTests["VISUAL-STATE"] = require("./ui/styling/visual-state-tests");
|
allTests["VISUAL-STATE"] = require("./ui/styling/visual-state-tests");
|
||||||
allTests["VALUE-SOURCE"] = require("./ui/styling/value-source-tests");
|
allTests["VALUE-SOURCE"] = require("./ui/styling/value-source-tests");
|
||||||
|
allTests["CSS-SELECTOR-PARSER"] = require("./ui/styling/css-selector-parser");
|
||||||
|
allTests["CSS-SELECTOR"] = require("./ui/styling/css-selector");
|
||||||
allTests["BUTTON"] = require("./ui/button/button-tests");
|
allTests["BUTTON"] = require("./ui/button/button-tests");
|
||||||
allTests["BORDER"] = require("./ui/border/border-tests");
|
allTests["BORDER"] = require("./ui/border/border-tests");
|
||||||
allTests["LABEL"] = require("./ui/label/label-tests");
|
allTests["LABEL"] = require("./ui/label/label-tests");
|
||||||
|
@ -6,7 +6,9 @@ import helper = require("../../ui/helper");
|
|||||||
import stackModule = require("ui/layouts/stack-layout");
|
import stackModule = require("ui/layouts/stack-layout");
|
||||||
import labelModule = require("ui/label");
|
import labelModule = require("ui/label");
|
||||||
import color = require("color");
|
import color = require("color");
|
||||||
import selectorModule = require("ui/styling/css-selector");
|
|
||||||
|
import {SelectorCore} from "ui/styling/css-selector";
|
||||||
|
|
||||||
//import styling = require("ui/styling");
|
//import styling = require("ui/styling");
|
||||||
|
|
||||||
function createAnimationFromCSS(css: string, name: string): keyframeAnimation.KeyframeAnimationInfo {
|
function createAnimationFromCSS(css: string, name: string): keyframeAnimation.KeyframeAnimationInfo {
|
||||||
@ -15,21 +17,15 @@ function createAnimationFromCSS(css: string, name: string): keyframeAnimation.Ke
|
|||||||
scope.ensureSelectors();
|
scope.ensureSelectors();
|
||||||
let selector = findSelectorInScope(scope, name);
|
let selector = findSelectorInScope(scope, name);
|
||||||
if (selector !== undefined) {
|
if (selector !== undefined) {
|
||||||
let animation = selector.animations[0];
|
let animation = scope.getAnimations(selector.ruleset)[0];
|
||||||
return animation;
|
return animation;
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function findSelectorInScope(scope: styleScope.StyleScope, name: string): selectorModule.CssSelector {
|
function findSelectorInScope(scope: styleScope.StyleScope, cssClass: string): SelectorCore {
|
||||||
let selector = undefined;
|
let selectors = scope.query({cssClasses: new Set([cssClass])});
|
||||||
for (let sel of (<any>scope)._mergedCssSelectors) {
|
return selectors[0];
|
||||||
if (sel.expression === name) {
|
|
||||||
selector = sel;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return selector;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function test_ReadAnimationProperties() {
|
export function test_ReadAnimationProperties() {
|
||||||
@ -108,7 +104,7 @@ export function test_ReadKeyframe() {
|
|||||||
scope.ensureSelectors();
|
scope.ensureSelectors();
|
||||||
let selector = findSelectorInScope(scope, "test");
|
let selector = findSelectorInScope(scope, "test");
|
||||||
TKUnit.assert(selector !== undefined, "CSS selector was not created!");
|
TKUnit.assert(selector !== undefined, "CSS selector was not created!");
|
||||||
let animation = selector.animations[0];
|
let animation = scope.getAnimations(selector.ruleset)[0];
|
||||||
TKUnit.assertEqual(animation.name, "test", "Wrong animation name!");
|
TKUnit.assertEqual(animation.name, "test", "Wrong animation name!");
|
||||||
TKUnit.assertEqual(animation.keyframes.length, 2, "Keyframes not parsed correctly!");
|
TKUnit.assertEqual(animation.keyframes.length, 2, "Keyframes not parsed correctly!");
|
||||||
TKUnit.assertEqual(animation.keyframes[0].duration, 0, "First keyframe duration should be 0");
|
TKUnit.assertEqual(animation.keyframes[0].duration, 0, "First keyframe duration should be 0");
|
||||||
@ -221,15 +217,15 @@ export function test_LoadTwoAnimationsWithTheSameName() {
|
|||||||
scope.css = "@keyframes a1 { from { opacity: 0; } to { opacity: 1; } } @keyframes a1 { from { opacity: 0; } to { opacity: 0.5; } } .a { animation-name: a1; }";
|
scope.css = "@keyframes a1 { from { opacity: 0; } to { opacity: 1; } } @keyframes a1 { from { opacity: 0; } to { opacity: 0.5; } } .a { animation-name: a1; }";
|
||||||
scope.ensureSelectors();
|
scope.ensureSelectors();
|
||||||
let selector = findSelectorInScope(scope, "a");
|
let selector = findSelectorInScope(scope, "a");
|
||||||
let animation = selector.animations[0];
|
let animation = scope.getAnimations(selector.ruleset)[0];
|
||||||
TKUnit.assertEqual(animation.keyframes.length, 2);
|
TKUnit.assertEqual(animation.keyframes.length, 2);
|
||||||
TKUnit.assertEqual(animation.keyframes[1].declarations[0].value, 0.5);
|
TKUnit.assertEqual(animation.keyframes[1].declarations[0].value, 0.5);
|
||||||
scope = new styleScope.StyleScope();
|
scope = new styleScope.StyleScope();
|
||||||
scope.css = "@keyframes k { from { opacity: 0; } to { opacity: 1; } } .a { animation-name: k; animation-duration: 2; } .a { animation-name: k; animation-duration: 3; }";
|
scope.css = "@keyframes k { from { opacity: 0; } to { opacity: 1; } } .a { animation-name: k; animation-duration: 2; } .a { animation-name: k; animation-duration: 3; }";
|
||||||
scope.ensureSelectors();
|
scope.ensureSelectors();
|
||||||
selector = findSelectorInScope(scope, "a");
|
selector = findSelectorInScope(scope, "a");
|
||||||
TKUnit.assertEqual(selector.animations[0].keyframes.length, 2);
|
TKUnit.assertEqual(scope.getAnimations(selector.ruleset)[0].keyframes.length, 2);
|
||||||
TKUnit.assertEqual(selector.animations[0].keyframes.length, 2);
|
TKUnit.assertEqual(scope.getAnimations(selector.ruleset)[0].keyframes.length, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function test_LoadAnimationProgrammatically() {
|
export function test_LoadAnimationProgrammatically() {
|
||||||
@ -295,11 +291,11 @@ export function test_ReadTwoAnimations() {
|
|||||||
scope.css = ".test { animation: one 0.2s ease-out 1 2, two 2s ease-in; }";
|
scope.css = ".test { animation: one 0.2s ease-out 1 2, two 2s ease-in; }";
|
||||||
scope.ensureSelectors();
|
scope.ensureSelectors();
|
||||||
let selector = findSelectorInScope(scope, "test");
|
let selector = findSelectorInScope(scope, "test");
|
||||||
TKUnit.assertEqual(selector.animations.length, 2);
|
TKUnit.assertEqual(scope.getAnimations(selector.ruleset).length, 2);
|
||||||
TKUnit.assertEqual(selector.animations[0].curve, enums.AnimationCurve.easeOut);
|
TKUnit.assertEqual(scope.getAnimations(selector.ruleset)[0].curve, enums.AnimationCurve.easeOut);
|
||||||
TKUnit.assertEqual(selector.animations[1].curve, enums.AnimationCurve.easeIn);
|
TKUnit.assertEqual(scope.getAnimations(selector.ruleset)[1].curve, enums.AnimationCurve.easeIn);
|
||||||
TKUnit.assertEqual(selector.animations[1].name, "two");
|
TKUnit.assertEqual(scope.getAnimations(selector.ruleset)[1].name, "two");
|
||||||
TKUnit.assertEqual(selector.animations[1].duration, 2000);
|
TKUnit.assertEqual(scope.getAnimations(selector.ruleset)[1].duration, 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function test_AnimationCurveInKeyframes() {
|
export function test_AnimationCurveInKeyframes() {
|
||||||
@ -307,7 +303,7 @@ export function test_AnimationCurveInKeyframes() {
|
|||||||
scope.css = "@keyframes an { from { animation-timing-function: linear; background-color: red; } 50% { background-color: green; } to { background-color: black; } } .test { animation-name: an; animation-timing-function: ease-in; }";
|
scope.css = "@keyframes an { from { animation-timing-function: linear; background-color: red; } 50% { background-color: green; } to { background-color: black; } } .test { animation-name: an; animation-timing-function: ease-in; }";
|
||||||
scope.ensureSelectors();
|
scope.ensureSelectors();
|
||||||
let selector = findSelectorInScope(scope, "test");
|
let selector = findSelectorInScope(scope, "test");
|
||||||
let animation = selector.animations[0];
|
let animation = scope.getAnimations(selector.ruleset)[0];
|
||||||
TKUnit.assertEqual(animation.keyframes[0].curve, enums.AnimationCurve.linear);
|
TKUnit.assertEqual(animation.keyframes[0].curve, enums.AnimationCurve.linear);
|
||||||
TKUnit.assertEqual(animation.keyframes[1].curve, undefined);
|
TKUnit.assertEqual(animation.keyframes[1].curve, undefined);
|
||||||
TKUnit.assertEqual(animation.keyframes[1].curve, undefined);
|
TKUnit.assertEqual(animation.keyframes[1].curve, undefined);
|
||||||
|
141
tests/app/ui/styling/css-selector-parser.ts
Normal file
141
tests/app/ui/styling/css-selector-parser.ts
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
import * as parser from "ui/styling/css-selector-parser";
|
||||||
|
import * as TKUnit from "../../TKUnit";
|
||||||
|
|
||||||
|
function test(css: string, expected: {}): void {
|
||||||
|
let result = parser.parse(css);
|
||||||
|
TKUnit.assertDeepEqual(result, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function test_fairly_complex_selector(): void {
|
||||||
|
test(` listview#products.mark gridlayout:selected[row="2"] a> b > c >d>e *[src] `, [
|
||||||
|
{ pos: 2, type: "", ident: "listview" },
|
||||||
|
{ pos: 10, type: "#", ident: "products" },
|
||||||
|
{ pos: 19, type: ".", ident: "mark", comb: " " },
|
||||||
|
{ pos: 25, type: "", ident: "gridlayout" },
|
||||||
|
{ pos: 35, type: ":", ident: "selected" },
|
||||||
|
{ pos: 44, type: "[]", prop: "row", test: "=", value: "2", comb: " " },
|
||||||
|
{ pos: 54, type: "", ident: "a", comb: ">" },
|
||||||
|
{ pos: 57, type: "", ident: "b", comb: ">" },
|
||||||
|
{ pos: 63, type: "", ident: "c", comb: ">" },
|
||||||
|
{ pos: 66, type: "", ident: "d", comb: ">" },
|
||||||
|
{ pos: 68, type: "", ident: "e", comb: " " },
|
||||||
|
{ pos: 70, type: "*" },
|
||||||
|
{ pos: 71, type: "[]", prop: "src" }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function test_typeguard_isUniversal(): void {
|
||||||
|
let selector = parser.parse("*")[0];
|
||||||
|
TKUnit.assertTrue(parser.isUniversal(selector));
|
||||||
|
TKUnit.assertFalse(parser.isType(selector));
|
||||||
|
TKUnit.assertFalse(parser.isClass(selector));
|
||||||
|
TKUnit.assertFalse(parser.isId(selector));
|
||||||
|
TKUnit.assertFalse(parser.isPseudo(selector));
|
||||||
|
TKUnit.assertFalse(parser.isAttribute(selector));
|
||||||
|
}
|
||||||
|
export function test_typeguard_isType(): void {
|
||||||
|
let selector = parser.parse("button")[0];
|
||||||
|
TKUnit.assertFalse(parser.isUniversal(selector));
|
||||||
|
TKUnit.assertTrue(parser.isType(selector));
|
||||||
|
TKUnit.assertFalse(parser.isClass(selector));
|
||||||
|
TKUnit.assertFalse(parser.isId(selector));
|
||||||
|
TKUnit.assertFalse(parser.isPseudo(selector));
|
||||||
|
TKUnit.assertFalse(parser.isAttribute(selector));
|
||||||
|
}
|
||||||
|
export function test_typeguard_isClass(): void {
|
||||||
|
let selector = parser.parse(".login")[0];
|
||||||
|
TKUnit.assertFalse(parser.isUniversal(selector));
|
||||||
|
TKUnit.assertFalse(parser.isType(selector));
|
||||||
|
TKUnit.assertTrue(parser.isClass(selector));
|
||||||
|
TKUnit.assertFalse(parser.isId(selector));
|
||||||
|
TKUnit.assertFalse(parser.isPseudo(selector));
|
||||||
|
TKUnit.assertFalse(parser.isAttribute(selector));
|
||||||
|
}
|
||||||
|
export function test_typeguard_isId(): void {
|
||||||
|
let selector = parser.parse("#login")[0];
|
||||||
|
TKUnit.assertFalse(parser.isUniversal(selector));
|
||||||
|
TKUnit.assertFalse(parser.isType(selector));
|
||||||
|
TKUnit.assertFalse(parser.isClass(selector));
|
||||||
|
TKUnit.assertTrue(parser.isId(selector));
|
||||||
|
TKUnit.assertFalse(parser.isPseudo(selector));
|
||||||
|
TKUnit.assertFalse(parser.isAttribute(selector));
|
||||||
|
}
|
||||||
|
export function test_typeguard_isPseudo(): void {
|
||||||
|
let selector = parser.parse(":hover")[0];
|
||||||
|
TKUnit.assertFalse(parser.isUniversal(selector));
|
||||||
|
TKUnit.assertFalse(parser.isType(selector));
|
||||||
|
TKUnit.assertFalse(parser.isClass(selector));
|
||||||
|
TKUnit.assertFalse(parser.isId(selector));
|
||||||
|
TKUnit.assertTrue(parser.isPseudo(selector));
|
||||||
|
TKUnit.assertFalse(parser.isAttribute(selector));
|
||||||
|
}
|
||||||
|
export function test_typeguard_isAttribute(): void {
|
||||||
|
let selector = parser.parse("[src]")[0];
|
||||||
|
TKUnit.assertFalse(parser.isUniversal(selector));
|
||||||
|
TKUnit.assertFalse(parser.isType(selector));
|
||||||
|
TKUnit.assertFalse(parser.isClass(selector));
|
||||||
|
TKUnit.assertFalse(parser.isId(selector));
|
||||||
|
TKUnit.assertFalse(parser.isPseudo(selector));
|
||||||
|
TKUnit.assertTrue(parser.isAttribute(selector));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function test_universal_selector(): void {
|
||||||
|
test(`*`, [{ pos: 0, type: "*" }]);
|
||||||
|
}
|
||||||
|
export function test_type_selector(): void {
|
||||||
|
test(`button`, [{ pos: 0, type: "", ident: "button" }]);
|
||||||
|
}
|
||||||
|
export function test_class_selector(): void {
|
||||||
|
test(`.red`, [{ pos: 0, type: ".", ident: "red" }]);
|
||||||
|
}
|
||||||
|
export function test_id_selector(): void {
|
||||||
|
test(`#login`, [{ pos: 0, type: "#", ident: "login" }]);
|
||||||
|
}
|
||||||
|
export function test_pseudoClass(): void {
|
||||||
|
test(`:hover`, [{ pos: 0, type: ":", ident: "hover" }]);
|
||||||
|
}
|
||||||
|
export function test_attribute_no_value(): void {
|
||||||
|
test(`[src]`, [{ pos: 0, type: "[]", prop: "src" }]);
|
||||||
|
}
|
||||||
|
export function test_attribute_equal(): void {
|
||||||
|
test(`[src = "res://"]`, [{ pos: 0, type: "[]", prop: "src", test: "=", value: `res://` }]);
|
||||||
|
}
|
||||||
|
export function test_attribute_all_tests(): void {
|
||||||
|
["=", "^=", "$=", "*=", "=", "~=", "|="].forEach(t => test(`[src ${t} "val"]`, [{ pos: 0, type: "[]", prop: "src", test: t, value: "val"}]));
|
||||||
|
}
|
||||||
|
export function test_direct_parent_comb(): void {
|
||||||
|
test(`listview > .image`, [
|
||||||
|
{ pos: 0, type: "", ident: "listview", comb: ">" },
|
||||||
|
{ pos: 11, type: ".", ident: "image" }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
export function test_ancestor_comb(): void {
|
||||||
|
test(`listview .image`, [
|
||||||
|
{ pos: 0, type: "", ident: "listview", comb: " " },
|
||||||
|
{ pos: 10, type: ".", ident: "image" }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
export function test_single_sequence(): void {
|
||||||
|
test(`button:hover`, [
|
||||||
|
{ pos: 0, type: "", ident: "button" },
|
||||||
|
{ pos: 6, type: ":", ident: "hover" }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
export function test_multiple_sequences(): void {
|
||||||
|
test(`listview>:selected image.product`, [
|
||||||
|
{ pos: 0, type: "", ident: "listview", comb: ">" },
|
||||||
|
{ pos: 9, type: ":", ident: "selected", comb: " " },
|
||||||
|
{ pos: 19, type: "", ident: "image" },
|
||||||
|
{ pos: 24, type: ".", ident: "product" }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
export function test_multiple_attribute_and_pseudo_classes(): void {
|
||||||
|
test(`button#login[user][pass]:focused:hovered`, [
|
||||||
|
{ pos: 0, type: "", ident: "button" },
|
||||||
|
{ pos: 6, type: "#", ident: "login" },
|
||||||
|
{ pos: 12, type: "[]", prop: "user" },
|
||||||
|
{ pos: 18, type: "[]", prop: "pass" },
|
||||||
|
{ pos: 24, type: ":", ident: "focused" },
|
||||||
|
{ pos: 32, type: ":", ident: "hovered" }
|
||||||
|
]);
|
||||||
|
}
|
235
tests/app/ui/styling/css-selector.ts
Normal file
235
tests/app/ui/styling/css-selector.ts
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
import * as selector from "ui/styling/css-selector";
|
||||||
|
import * as parser from "css";
|
||||||
|
|
||||||
|
import * as TKUnit from "../../TKUnit";
|
||||||
|
|
||||||
|
function create(css: string, source: string = "css-selectors.ts@test"): { rules: selector.RuleSet[], map: selector.SelectorsMap } {
|
||||||
|
let parse = parser.parse(css, { source });
|
||||||
|
let rulesAst = parse.stylesheet.rules.filter(n => n.type === "rule");
|
||||||
|
let rules = selector.fromAstNodes(rulesAst);
|
||||||
|
let map = new selector.SelectorsMap(rules);
|
||||||
|
return { rules, map };
|
||||||
|
}
|
||||||
|
|
||||||
|
function createOne(css: string, source: string = "css-selectors.ts@test"): selector.RuleSet {
|
||||||
|
let {rules} = create(css, source);
|
||||||
|
TKUnit.assertEqual(rules.length, 1);
|
||||||
|
return rules[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function test_single_selector() {
|
||||||
|
let rule = createOne(`* { color: red; }`);
|
||||||
|
TKUnit.assertTrue(rule.selectors[0].match({ cssType: "button" }));
|
||||||
|
TKUnit.assertTrue(rule.selectors[0].match({ cssType: "image" }));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function test_two_selectors() {
|
||||||
|
let rule = createOne(`button, image { color: red; }`);
|
||||||
|
TKUnit.assertTrue(rule.selectors[0].match({ cssType: "button" }));
|
||||||
|
TKUnit.assertTrue(rule.selectors[1].match({ cssType: "image" }));
|
||||||
|
TKUnit.assertFalse(rule.selectors[0].match({ cssType: "stacklayout" }));
|
||||||
|
TKUnit.assertFalse(rule.selectors[1].match({ cssType: "stacklayout" }));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function test_narrow_selection() {
|
||||||
|
let {map} = create(`
|
||||||
|
.login { color: blue; }
|
||||||
|
button { color: red; }
|
||||||
|
image { color: green; }
|
||||||
|
`);
|
||||||
|
|
||||||
|
let buttonQuerry = map.query({ cssType: "button" }).selectors;
|
||||||
|
TKUnit.assertEqual(buttonQuerry.length, 1);
|
||||||
|
TKUnit.assertDeepSuperset(buttonQuerry[0].ruleset.declarations, [
|
||||||
|
{ property: "color", value: "red" }
|
||||||
|
]);
|
||||||
|
|
||||||
|
let imageQuerry = map.query({ cssType: "image", cssClasses: new Set(["login"]) }).selectors;
|
||||||
|
TKUnit.assertEqual(imageQuerry.length, 2);
|
||||||
|
// Note class before type
|
||||||
|
TKUnit.assertDeepSuperset(imageQuerry[0].ruleset.declarations, [
|
||||||
|
{ property: "color", value: "green" }
|
||||||
|
]);
|
||||||
|
TKUnit.assertDeepSuperset(imageQuerry[1].ruleset.declarations, [
|
||||||
|
{ property: "color", value: "blue" }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let positiveMatches = {
|
||||||
|
"*": (view) => true,
|
||||||
|
"type": (view) => view.cssType === "type",
|
||||||
|
"#id": (view) => view.id === "id",
|
||||||
|
".class": (view) => view.cssClasses.has("class"),
|
||||||
|
":pseudo": (view) => view.cssPseudoClasses.has("pseudo"),
|
||||||
|
"[src1]": (view) => "src1" in view,
|
||||||
|
"[src2='src-value']": (view) => view['src2'] === 'src-value'
|
||||||
|
}
|
||||||
|
|
||||||
|
let positivelyMatchingView = {
|
||||||
|
cssType: "type",
|
||||||
|
id: "id",
|
||||||
|
cssClasses: new Set(["class"]),
|
||||||
|
cssPseudoClasses: new Set(["pseudo"]),
|
||||||
|
"src1": "src",
|
||||||
|
"src2": "src-value"
|
||||||
|
}
|
||||||
|
|
||||||
|
let negativelyMatchingView = {
|
||||||
|
cssType: "nottype",
|
||||||
|
id: "notid",
|
||||||
|
cssClasses: new Set(["notclass"]),
|
||||||
|
cssPseudoClasses: new Set(["notpseudo"]),
|
||||||
|
// Has no "src1"
|
||||||
|
"src2": "not-src-value"
|
||||||
|
}
|
||||||
|
|
||||||
|
export function test_simple_selectors_match() {
|
||||||
|
for (let sel in positiveMatches) {
|
||||||
|
let css = sel + " { color: red; }";
|
||||||
|
let rule = createOne(css);
|
||||||
|
TKUnit.assertTrue(rule.selectors[0].match(positivelyMatchingView), "Expected successful match for: " + css);
|
||||||
|
if (sel !== "*") {
|
||||||
|
TKUnit.assertFalse(rule.selectors[0].match(negativelyMatchingView), "Expected match failure for: " + css);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function test_two_selector_sequence_positive_match() {
|
||||||
|
for (let firstStr in positiveMatches) {
|
||||||
|
for (let secondStr in positiveMatches) {
|
||||||
|
if (secondStr !== firstStr && secondStr !== "*" && secondStr !== "type") {
|
||||||
|
let css = firstStr + secondStr + " { color: red; }";
|
||||||
|
let rule = createOne(css);
|
||||||
|
TKUnit.assertTrue(rule.selectors[0].match(positivelyMatchingView), "Expected successful match for: " + css);
|
||||||
|
if (firstStr !== "*") {
|
||||||
|
TKUnit.assertFalse(rule.selectors[0].match(negativelyMatchingView), "Expected match failure for: " + css);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function test_direct_parent_combinator() {
|
||||||
|
let rule = createOne(`listview > item:selected { color: red; }`);
|
||||||
|
TKUnit.assertTrue(rule.selectors[0].match({
|
||||||
|
cssType: "item",
|
||||||
|
cssPseudoClasses: new Set(["selected"]),
|
||||||
|
parent: {
|
||||||
|
cssType: "listview"
|
||||||
|
}
|
||||||
|
}), "Item in list view expected to match");
|
||||||
|
TKUnit.assertFalse(rule.selectors[0].match({
|
||||||
|
cssType: "item",
|
||||||
|
cssPseudoClasses: new Set(["selected"]),
|
||||||
|
parent: {
|
||||||
|
cssType: "stacklayout",
|
||||||
|
parent: {
|
||||||
|
cssType: "listview"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}), "Item in stack in list view NOT expected to match.");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function test_ancestor_combinator() {
|
||||||
|
let rule = createOne(`listview item:selected { color: red; }`);
|
||||||
|
TKUnit.assertTrue(rule.selectors[0].match({
|
||||||
|
cssType: "item",
|
||||||
|
cssPseudoClasses: new Set(["selected"]),
|
||||||
|
parent: {
|
||||||
|
cssType: "listview"
|
||||||
|
}
|
||||||
|
}), "Item in list view expected to match");
|
||||||
|
TKUnit.assertTrue(rule.selectors[0].match({
|
||||||
|
cssType: "item",
|
||||||
|
cssPseudoClasses: new Set(["selected"]),
|
||||||
|
parent: {
|
||||||
|
cssType: "stacklayout",
|
||||||
|
parent: {
|
||||||
|
cssType: "listview"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}), "Item in stack in list view expected to match.");
|
||||||
|
TKUnit.assertFalse(rule.selectors[0].match({
|
||||||
|
cssType: "item",
|
||||||
|
cssPseudoClasses: new Set(["selected"]),
|
||||||
|
parent: {
|
||||||
|
cssType: "stacklayout",
|
||||||
|
parent: {
|
||||||
|
cssType: "page"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}), "Item in stack in page NOT expected to match.");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function test_backtracking_css_selector() {
|
||||||
|
let sel = createOne(`a>b c { color: red; }`).selectors[0];
|
||||||
|
let child = {
|
||||||
|
cssType: "c",
|
||||||
|
parent: {
|
||||||
|
cssType: "b",
|
||||||
|
parent: {
|
||||||
|
cssType: "fail",
|
||||||
|
parent: {
|
||||||
|
cssType: "b",
|
||||||
|
parent: {
|
||||||
|
cssType: "a"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TKUnit.assertTrue(sel.match(child));
|
||||||
|
}
|
||||||
|
|
||||||
|
function toString() { return this.cssType; }
|
||||||
|
|
||||||
|
export function test_simple_query_match() {
|
||||||
|
let {map} = create(`list grid[promotion] button:highlighted { color: red; }`);
|
||||||
|
|
||||||
|
let list, grid, button;
|
||||||
|
|
||||||
|
button = {
|
||||||
|
cssType: "button",
|
||||||
|
cssPseudoClasses: new Set<string>(["highlighted"]),
|
||||||
|
toString,
|
||||||
|
parent: grid = {
|
||||||
|
cssType: "grid",
|
||||||
|
promotion: true,
|
||||||
|
toString,
|
||||||
|
parent: list = {
|
||||||
|
cssType: "list",
|
||||||
|
toString
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let match = map.query(button);
|
||||||
|
TKUnit.assertEqual(match.selectors.length, 1, "Expected match to have one selector.");
|
||||||
|
|
||||||
|
let expected = new Map<Node, selector.Changes>()
|
||||||
|
.set(grid, { attributes: new Set(["promotion"]) })
|
||||||
|
.set(button, { pseudoClasses: new Set(["highlighted"]) });
|
||||||
|
|
||||||
|
TKUnit.assertDeepEqual(match.changeMap, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function test_query_match_one_child_group() {
|
||||||
|
let {map} = create(`#prod[special] > gridlayout { color: red; }`);
|
||||||
|
let gridlayout, prod;
|
||||||
|
|
||||||
|
gridlayout = {
|
||||||
|
cssType: "gridlayout",
|
||||||
|
toString,
|
||||||
|
parent: prod = {
|
||||||
|
id: "prod",
|
||||||
|
cssType: "listview",
|
||||||
|
toString
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let match = map.query(gridlayout);
|
||||||
|
TKUnit.assertEqual(match.selectors.length, 1, "Expected match to have one selector.");
|
||||||
|
|
||||||
|
let expected = new Map<Node, selector.Changes>().set(prod, { attributes: new Set(["special"])} );
|
||||||
|
TKUnit.assertDeepEqual(match.changeMap, expected);
|
||||||
|
}
|
@ -12,7 +12,6 @@ import types = require("utils/types");
|
|||||||
import viewModule = require("ui/core/view");
|
import viewModule = require("ui/core/view");
|
||||||
import styleModule = require("ui/styling/style");
|
import styleModule = require("ui/styling/style");
|
||||||
import dependencyObservableModule = require("ui/core/dependency-observable");
|
import dependencyObservableModule = require("ui/core/dependency-observable");
|
||||||
import {StyleScope} from 'ui/styling/style-scope';
|
|
||||||
|
|
||||||
export function test_css_dataURI_is_applied_to_backgroundImageSource() {
|
export function test_css_dataURI_is_applied_to_backgroundImageSource() {
|
||||||
var stack = new stackModule.StackLayout();
|
var stack = new stackModule.StackLayout();
|
||||||
@ -358,10 +357,8 @@ export var test_composite_selector_type_and_class = function () {
|
|||||||
let testFunc = function (views: Array<viewModule.View>) {
|
let testFunc = function (views: Array<viewModule.View>) {
|
||||||
TKUnit.assert(btnWithClass.style.color, "Color property no applied correctly.");
|
TKUnit.assert(btnWithClass.style.color, "Color property no applied correctly.");
|
||||||
TKUnit.assert(btnWithClass.style.color.hex === "#FF0000", "Color property no applied correctly.");
|
TKUnit.assert(btnWithClass.style.color.hex === "#FF0000", "Color property no applied correctly.");
|
||||||
|
TKUnit.assert(btnWithNoClass.style.color === undefined, "btnWithNoClass color should not have a value");
|
||||||
TKUnit.assert(btnWithNoClass.style.color === undefined, "Color should not have a value");
|
TKUnit.assert(lblWithClass.style.color === undefined, "lblWithClass color should not have a value");
|
||||||
|
|
||||||
TKUnit.assert(lblWithClass.style.color === undefined, "Color should not have a value");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
helper.buildUIAndRunTest(testStack, testFunc, testCss);
|
helper.buildUIAndRunTest(testStack, testFunc, testCss);
|
||||||
@ -630,13 +627,6 @@ export function test_styling_properties_are_defined() {
|
|||||||
TKUnit.assert(types.isFunction(styling.properties.getPropertyByName), "properties.getPropertyByName function is not defined");
|
TKUnit.assert(types.isFunction(styling.properties.getPropertyByName), "properties.getPropertyByName function is not defined");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function test_styling_visualStates_are_defined() {
|
|
||||||
TKUnit.assert(types.isDefined(styling.visualStates), "visualStates module is not defined");
|
|
||||||
TKUnit.assert(types.isString(styling.visualStates.Hovered), "Hovered state is not defined");
|
|
||||||
TKUnit.assert(types.isString(styling.visualStates.Normal), "Normal state is not defined");
|
|
||||||
TKUnit.assert(types.isString(styling.visualStates.Pressed), "Pressed state is not defined");
|
|
||||||
}
|
|
||||||
|
|
||||||
export function test_styling_stylers_are_defined() {
|
export function test_styling_stylers_are_defined() {
|
||||||
TKUnit.assert(types.isFunction(styleModule.registerHandler), "registerHandler function is not defined");
|
TKUnit.assert(types.isFunction(styleModule.registerHandler), "registerHandler function is not defined");
|
||||||
TKUnit.assert(types.isFunction(styleModule.StylePropertyChangedHandler), "StylePropertyChangedHandler class is not defined");
|
TKUnit.assert(types.isFunction(styleModule.StylePropertyChangedHandler), "StylePropertyChangedHandler class is not defined");
|
||||||
@ -1445,16 +1435,6 @@ export function test_CascadingClassNamesAppliesAfterPageLoad() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function test_SortingOfCssSelectorsWithSameSpecificity() {
|
|
||||||
let scope = new StyleScope();
|
|
||||||
scope.css = ".button { border-color: #b2b2b2; background-color: hotpink; color: #444; margin: 5; padding: 7 2; border-width: 1; border-style: solid; border-radius: 2; text-align: center; font-size: 18; line-height: 42; } .button-small { background-color: salmon; } .button-large { font-size: 26; } .button-light { border-color: #ddd; background-color: #fff; color: #444; } .button-stable { border-color: #b2b2b2; background-color: #f8f8f8; color: #444; } .button-positive { border-color: #0c60ee; background-color: #387ef5; color: #fff; } .button-calm { border-color: #0a9dc7;background-color: #11c1f3; color: #fff; } .button-balanced { border-color: #28a54c; background-color: #33cd5f; color: #fff; } .button-energized { border-color: #e6b500; background-color: #ffc900; color: #fff; } .button-assertive { border-color: #e42112; background-color: #ef473a; color: #fff; } .button-royal { border-color: #6b46e5; background-color: #886aea; color: #fff; } .button-dark { border-color: #111; background-color: #444; color: #fff; }";
|
|
||||||
scope.ensureSelectors();
|
|
||||||
let expressions = [];
|
|
||||||
(<any>scope)._mergedCssSelectors.forEach((v) => {
|
|
||||||
expressions.push(v.expression);
|
|
||||||
});
|
|
||||||
TKUnit.assertTrue(expressions.indexOf('button') < expressions.indexOf('button-calm'), "button class selector should be before button-calm selector.");
|
|
||||||
}
|
|
||||||
// <snippet module="ui/styling" title="styling">
|
// <snippet module="ui/styling" title="styling">
|
||||||
// For information and example how to use style properties please refer to special [**Styling**](../../../styling.md) topic.
|
// For information and example how to use style properties please refer to special [**Styling**](../../../styling.md) topic.
|
||||||
// </snippet>
|
// </snippet>
|
||||||
|
@ -42,21 +42,3 @@ export function test_value_Local_stronger_than_Css() {
|
|||||||
btn.style.color = undefined;
|
btn.style.color = undefined;
|
||||||
TKUnit.assertEqual(btn.style.color, undefined, "style.color should be undefined when set locally.");
|
TKUnit.assertEqual(btn.style.color, undefined, "style.color should be undefined when set locally.");
|
||||||
}
|
}
|
||||||
|
|
||||||
export var test_value_VisualState_stronger_than_Local = function () {
|
|
||||||
let testPage = helper.getCurrentPage();
|
|
||||||
|
|
||||||
let testStack = new stack.StackLayout();
|
|
||||||
testPage.content = testStack;
|
|
||||||
|
|
||||||
let btn = new button.Button();
|
|
||||||
btn.style.color = new color.Color("#FF0000");
|
|
||||||
testStack.addChild(btn);
|
|
||||||
testPage.css = "button:pressed { color: #0000FF; }";
|
|
||||||
|
|
||||||
helper.assertViewColor(btn, "#FF0000");
|
|
||||||
btn._goToVisualState("pressed");
|
|
||||||
helper.assertViewColor(btn, "#0000FF");
|
|
||||||
btn._goToVisualState("normal");
|
|
||||||
helper.assertViewColor(btn, "#FF0000");
|
|
||||||
}
|
|
@ -1,37 +1,17 @@
|
|||||||
import TKUnit = require("../../TKUnit");
|
import TKUnit = require("../../TKUnit");
|
||||||
import view = require("ui/core/view");
|
import view = require("ui/core/view");
|
||||||
import page = require("ui/page");
|
import page = require("ui/page");
|
||||||
import vsConstants = require("ui/styling/visual-state-constants");
|
|
||||||
import types = require("utils/types");
|
import types = require("utils/types");
|
||||||
import helper = require("../helper");
|
import helper = require("../helper");
|
||||||
|
|
||||||
export var test_VisualStates_Parsed = function () {
|
function assertInState(view: view.View, state: string, knownStates: string[]): void {
|
||||||
var test = function (views: Array<view.View>) {
|
let pseudo = view.cssPseudoClasses;
|
||||||
var page = <page.Page>views[0];
|
if (state) {
|
||||||
page.css = "button:hovered { color: red; background-color: orange } button:pressed { color: white; background-color: black }";
|
TKUnit.assert(pseudo.has(state), "Expected view " + view + " to have pseudo class " + state);
|
||||||
|
|
||||||
var states = page._getStyleScope().getVisualStates(views[1]);
|
|
||||||
TKUnit.assert(types.isDefined(states));
|
|
||||||
|
|
||||||
var counter = 0,
|
|
||||||
hoveredFound = false,
|
|
||||||
pressedFound = false;
|
|
||||||
|
|
||||||
for (var p in states) {
|
|
||||||
counter++;
|
|
||||||
if (p === vsConstants.Hovered) {
|
|
||||||
hoveredFound = true;
|
|
||||||
} else if (p === vsConstants.Pressed) {
|
|
||||||
pressedFound = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TKUnit.assert(counter === 2);
|
|
||||||
TKUnit.assert(hoveredFound);
|
|
||||||
TKUnit.assert(pressedFound);
|
|
||||||
}
|
}
|
||||||
|
knownStates.filter(s => s !== state).forEach(s => {
|
||||||
helper.do_PageTest_WithButton(test);
|
TKUnit.assert(!pseudo.has(s), "Expected view " + view + " not to have pseudo class " + s + (state ? " expected just " + state + "." : ""));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export var test_goToVisualState = function () {
|
export var test_goToVisualState = function () {
|
||||||
@ -40,15 +20,19 @@ export var test_goToVisualState = function () {
|
|||||||
|
|
||||||
var btn = views[1];
|
var btn = views[1];
|
||||||
|
|
||||||
|
assertInState(btn, null, ["hovered", "pressed"]);
|
||||||
|
|
||||||
btn._goToVisualState("hovered");
|
btn._goToVisualState("hovered");
|
||||||
|
|
||||||
TKUnit.assert(btn.visualState === "hovered");
|
assertInState(btn, "hovered", ["hovered", "pressed"]);
|
||||||
|
|
||||||
TKUnit.assert(types.isDefined(btn.style.color) && btn.style.color.name === "red");
|
TKUnit.assert(types.isDefined(btn.style.color) && btn.style.color.name === "red");
|
||||||
TKUnit.assert(types.isDefined(btn.style.backgroundColor) && btn.style.backgroundColor.name === "orange");
|
TKUnit.assert(types.isDefined(btn.style.backgroundColor) && btn.style.backgroundColor.name === "orange");
|
||||||
|
|
||||||
btn._goToVisualState("pressed");
|
btn._goToVisualState("pressed");
|
||||||
|
|
||||||
TKUnit.assert(btn.visualState === "pressed");
|
assertInState(btn, "pressed", ["hovered", "pressed"]);
|
||||||
|
|
||||||
TKUnit.assert(types.isDefined(btn.style.color) && btn.style.color.name === "white");
|
TKUnit.assert(types.isDefined(btn.style.color) && btn.style.color.name === "white");
|
||||||
TKUnit.assert(types.isDefined(btn.style.backgroundColor) && btn.style.backgroundColor.name === "black");
|
TKUnit.assert(types.isDefined(btn.style.backgroundColor) && btn.style.backgroundColor.name === "black");
|
||||||
}
|
}
|
||||||
@ -61,17 +45,18 @@ export var test_goToVisualState_NoState_ShouldResetStyledProperties = function (
|
|||||||
(<page.Page>views[0]).css = "button:hovered { color: red; background-color: orange }";
|
(<page.Page>views[0]).css = "button:hovered { color: red; background-color: orange }";
|
||||||
|
|
||||||
var btn = views[1];
|
var btn = views[1];
|
||||||
|
assertInState(btn, null, ["hovered", "pressed"]);
|
||||||
|
|
||||||
btn._goToVisualState("hovered");
|
btn._goToVisualState("hovered");
|
||||||
|
|
||||||
TKUnit.assert(btn.visualState === "hovered");
|
assertInState(btn, "hovered", ["hovered", "pressed"]);
|
||||||
TKUnit.assert(types.isDefined(btn.style.color) && btn.style.color.name === "red");
|
TKUnit.assert(types.isDefined(btn.style.color) && btn.style.color.name === "red");
|
||||||
TKUnit.assert(types.isDefined(btn.style.backgroundColor) && btn.style.backgroundColor.name === "orange");
|
TKUnit.assert(types.isDefined(btn.style.backgroundColor) && btn.style.backgroundColor.name === "orange");
|
||||||
|
|
||||||
btn._goToVisualState("pressed");
|
btn._goToVisualState("pressed");
|
||||||
|
|
||||||
// since there are no modifiers for the "Pressed" state, the "Normal" state is returned.
|
// since there are no modifiers for the "Pressed" state, the "Normal" state is returned.
|
||||||
TKUnit.assert(btn.visualState === "normal");
|
assertInState(btn, "pressed", ["hovered", "pressed"]);
|
||||||
|
|
||||||
// properties are reset (set to undefined)
|
// properties are reset (set to undefined)
|
||||||
TKUnit.assert(types.isUndefined(btn.style.color));
|
TKUnit.assert(types.isUndefined(btn.style.color));
|
||||||
@ -87,16 +72,18 @@ export var test_goToVisualState_NoState_ShouldGoToNormal = function () {
|
|||||||
|
|
||||||
var btn = views[1];
|
var btn = views[1];
|
||||||
|
|
||||||
|
assertInState(btn, null, ["hovered", "pressed"]);
|
||||||
|
|
||||||
btn._goToVisualState("hovered");
|
btn._goToVisualState("hovered");
|
||||||
|
|
||||||
TKUnit.assert(btn.visualState === "hovered");
|
assertInState(btn, "hovered", ["hovered", "pressed"]);
|
||||||
TKUnit.assert(types.isDefined(btn.style.color) && btn.style.color.name === "red");
|
TKUnit.assert(types.isDefined(btn.style.color) && btn.style.color.name === "red");
|
||||||
TKUnit.assert(types.isDefined(btn.style.backgroundColor) && btn.style.backgroundColor.name === "orange");
|
TKUnit.assert(types.isDefined(btn.style.backgroundColor) && btn.style.backgroundColor.name === "orange");
|
||||||
|
|
||||||
btn._goToVisualState("pressed");
|
btn._goToVisualState("pressed");
|
||||||
|
|
||||||
// since there are no modifiers for the "Pressed" state, the "Normal" state is returned.
|
// since there are no modifiers for the "Pressed" state, the "Normal" state is returned.
|
||||||
TKUnit.assert(btn.visualState === "normal");
|
assertInState(btn, "pressed", ["hovered", "pressed"]);
|
||||||
|
|
||||||
// the actual state is "normal" and properties are reverted to these settings (if any)
|
// the actual state is "normal" and properties are reverted to these settings (if any)
|
||||||
TKUnit.assert(types.isDefined(btn.style.color) && btn.style.color.name === "orange");
|
TKUnit.assert(types.isDefined(btn.style.color) && btn.style.color.name === "orange");
|
||||||
|
@ -455,7 +455,7 @@ export function test_parse_ShouldParseBindingToSpecialProperty() {
|
|||||||
p.bindingContext = obj;
|
p.bindingContext = obj;
|
||||||
|
|
||||||
TKUnit.assertEqual(p.content.className, classProp);
|
TKUnit.assertEqual(p.content.className, classProp);
|
||||||
TKUnit.assertEqual(p.content._cssClasses.length, 1);
|
TKUnit.assertEqual(p.content.cssClasses.size, 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
export function test_parse_ShouldParseBindingsWithCommaInsideSingleQuote() {
|
export function test_parse_ShouldParseBindingsWithCommaInsideSingleQuote() {
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
import definition = require("application");
|
import definition = require("application");
|
||||||
import observable = require("data/observable");
|
import observable = require("data/observable");
|
||||||
import frame = require("ui/frame");
|
import frame = require("ui/frame");
|
||||||
import cssSelector = require("ui/styling/css-selector");
|
import {RuleSet} from "ui/styling/css-selector";
|
||||||
import * as fileSystemModule from "file-system";
|
import * as fileSystemModule from "file-system";
|
||||||
import * as styleScopeModule from "ui/styling/style-scope";
|
import * as styleScopeModule from "ui/styling/style-scope";
|
||||||
import * as fileResolverModule from "file-system/file-name-resolver";
|
import * as fileResolverModule from "file-system/file-name-resolver";
|
||||||
@ -34,9 +34,9 @@ export var mainEntry: frame.NavigationEntry;
|
|||||||
|
|
||||||
export var cssFile: string = "app.css"
|
export var cssFile: string = "app.css"
|
||||||
|
|
||||||
export var appSelectors: Array<cssSelector.CssSelector> = [];
|
export var appSelectors: RuleSet[] = [];
|
||||||
export var additionalSelectors: Array<cssSelector.CssSelector> = [];
|
export var additionalSelectors: RuleSet[] = [];
|
||||||
export var cssSelectors: Array<cssSelector.CssSelector> = [];
|
export var cssSelectors: RuleSet[] = [];
|
||||||
export var cssSelectorVersion: number = 0;
|
export var cssSelectorVersion: number = 0;
|
||||||
export var keyframes: any = {};
|
export var keyframes: any = {};
|
||||||
|
|
||||||
@ -58,12 +58,12 @@ export var android = undefined;
|
|||||||
|
|
||||||
export var ios = undefined;
|
export var ios = undefined;
|
||||||
|
|
||||||
export function loadCss(cssFile?: string): Array<cssSelector.CssSelector> {
|
export function loadCss(cssFile?: string): RuleSet[] {
|
||||||
if (!cssFile) {
|
if (!cssFile) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
var result: Array<cssSelector.CssSelector>;
|
var result: RuleSet[];
|
||||||
|
|
||||||
var fs: typeof fileSystemModule = require("file-system");
|
var fs: typeof fileSystemModule = require("file-system");
|
||||||
if (!styleScope) {
|
if (!styleScope) {
|
||||||
@ -89,7 +89,7 @@ export function mergeCssSelectors(module: any): void {
|
|||||||
module.cssSelectorVersion++;
|
module.cssSelectorVersion++;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseCss(cssText: string, cssFileName?: string): Array<cssSelector.CssSelector> {
|
export function parseCss(cssText: string, cssFileName?: string): RuleSet[] {
|
||||||
if (!styleScope) {
|
if (!styleScope) {
|
||||||
styleScope = require("ui/styling/style-scope");
|
styleScope = require("ui/styling/style-scope");
|
||||||
}
|
}
|
||||||
|
12
tns-core-modules/application/application.d.ts
vendored
12
tns-core-modules/application/application.d.ts
vendored
@ -2,7 +2,7 @@
|
|||||||
* Contains the application abstraction with all related methods.
|
* Contains the application abstraction with all related methods.
|
||||||
*/
|
*/
|
||||||
declare module "application" {
|
declare module "application" {
|
||||||
import cssSelector = require("ui/styling/css-selector");
|
import {RuleSet} from "ui/styling/css-selector";
|
||||||
import observable = require("data/observable");
|
import observable = require("data/observable");
|
||||||
import frame = require("ui/frame");
|
import frame = require("ui/frame");
|
||||||
import {View} from "ui/core/view";
|
import {View} from "ui/core/view";
|
||||||
@ -123,15 +123,15 @@ declare module "application" {
|
|||||||
export var cssFile: string;
|
export var cssFile: string;
|
||||||
|
|
||||||
//@private
|
//@private
|
||||||
export var appSelectors: Array<cssSelector.CssSelector>;
|
export var appSelectors: RuleSet[];
|
||||||
export var additionalSelectors: Array<cssSelector.CssSelector>;
|
export var additionalSelectors: RuleSet[];
|
||||||
/**
|
/**
|
||||||
* Cached css selectors created from the content of the css file.
|
* Cached css selectors created from the content of the css file.
|
||||||
*/
|
*/
|
||||||
export var cssSelectors: Array<cssSelector.CssSelector>;
|
export var cssSelectors: RuleSet[];
|
||||||
export var cssSelectorVersion: number;
|
export var cssSelectorVersion: number;
|
||||||
export var keyframes: any;
|
export var keyframes: any;
|
||||||
export function parseCss(cssText: string, cssFileName?: string): Array<cssSelector.CssSelector>;
|
export function parseCss(cssText: string, cssFileName?: string): RuleSet[];
|
||||||
export function mergeCssSelectors(module: any): void;
|
export function mergeCssSelectors(module: any): void;
|
||||||
//@endprivate
|
//@endprivate
|
||||||
|
|
||||||
@ -141,7 +141,7 @@ declare module "application" {
|
|||||||
* Loads css file and parses to a css syntax tree.
|
* Loads css file and parses to a css syntax tree.
|
||||||
* @param cssFile Optional parameter to point to an arbitrary css file. If not specified, the cssFile property is used.
|
* @param cssFile Optional parameter to point to an arbitrary css file. If not specified, the cssFile property is used.
|
||||||
*/
|
*/
|
||||||
export function loadCss(cssFile?: string): Array<cssSelector.CssSelector>;
|
export function loadCss(cssFile?: string): RuleSet[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Call this method to start the application. Important: All code after this method call will not be executed!
|
* Call this method to start the application. Important: All code after this method call will not be executed!
|
||||||
|
10
tns-core-modules/css/reworkcss.d.ts
vendored
10
tns-core-modules/css/reworkcss.d.ts
vendored
@ -5,7 +5,7 @@ declare module "css" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface Node {
|
export interface Node {
|
||||||
type: string;
|
type: "rule" | "keyframes" | "declaration";
|
||||||
position: Position;
|
position: Position;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -16,11 +16,15 @@ declare module "css" {
|
|||||||
|
|
||||||
export interface Rule extends Node {
|
export interface Rule extends Node {
|
||||||
selectors: string[];
|
selectors: string[];
|
||||||
declarations: Declaration[];
|
declarations: Node[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Keyframes extends Rule {
|
||||||
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StyleSheet {
|
export interface StyleSheet {
|
||||||
rules: Rule[];
|
rules: Node[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SyntaxTree {
|
export interface SyntaxTree {
|
||||||
|
4
tns-core-modules/declarations.d.ts
vendored
4
tns-core-modules/declarations.d.ts
vendored
@ -157,3 +157,7 @@ declare class WeakRef<T> {
|
|||||||
declare var module: NativeScriptModule;
|
declare var module: NativeScriptModule;
|
||||||
// Same as module.exports
|
// Same as module.exports
|
||||||
declare var exports: any;
|
declare var exports: any;
|
||||||
|
|
||||||
|
interface Array<T> {
|
||||||
|
filter<U extends T>(pred: (a: T) => a is U): U[];
|
||||||
|
}
|
1
tns-core-modules/tns-core-modules.base.d.ts
vendored
1
tns-core-modules/tns-core-modules.base.d.ts
vendored
@ -81,7 +81,6 @@
|
|||||||
/// <reference path="ui/styling/style-scope.d.ts" />
|
/// <reference path="ui/styling/style-scope.d.ts" />
|
||||||
/// <reference path="ui/styling/style.d.ts" />
|
/// <reference path="ui/styling/style.d.ts" />
|
||||||
/// <reference path="ui/styling/styling.d.ts" />
|
/// <reference path="ui/styling/styling.d.ts" />
|
||||||
/// <reference path="ui/styling/visual-state-constants.d.ts" />
|
|
||||||
/// <reference path="ui/switch/switch.d.ts" />
|
/// <reference path="ui/switch/switch.d.ts" />
|
||||||
/// <reference path="ui/tab-view/tab-view.d.ts" />
|
/// <reference path="ui/tab-view/tab-view.d.ts" />
|
||||||
/// <reference path="ui/text-base/text-base-styler.d.ts" />
|
/// <reference path="ui/text-base/text-base-styler.d.ts" />
|
||||||
|
@ -6,8 +6,7 @@ import view = require("ui/core/view");
|
|||||||
import utils = require("utils/utils");
|
import utils = require("utils/utils");
|
||||||
import enums = require("ui/enums");
|
import enums = require("ui/enums");
|
||||||
import dependencyObservable = require("ui/core/dependency-observable");
|
import dependencyObservable = require("ui/core/dependency-observable");
|
||||||
import styleScope = require("../styling/style-scope");
|
import {PseudoClassHandler} from "ui/core/view";
|
||||||
import {Property} from "ui/core/dependency-observable";
|
|
||||||
|
|
||||||
class TapHandlerImpl extends NSObject {
|
class TapHandlerImpl extends NSObject {
|
||||||
private _owner: WeakRef<Button>;
|
private _owner: WeakRef<Button>;
|
||||||
@ -43,27 +42,6 @@ export class Button extends common.Button {
|
|||||||
|
|
||||||
this._tapHandler = TapHandlerImpl.initWithOwner(new WeakRef(this));
|
this._tapHandler = TapHandlerImpl.initWithOwner(new WeakRef(this));
|
||||||
this._ios.addTargetActionForControlEvents(this._tapHandler, "tap", UIControlEvents.UIControlEventTouchUpInside);
|
this._ios.addTargetActionForControlEvents(this._tapHandler, "tap", UIControlEvents.UIControlEventTouchUpInside);
|
||||||
|
|
||||||
this._stateChangedHandler = new stateChanged.ControlStateChangeListener(this._ios, (s: string) => {
|
|
||||||
this._goToVisualState(s);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public onLoaded() {
|
|
||||||
super.onLoaded();
|
|
||||||
this._updateHandler();
|
|
||||||
}
|
|
||||||
|
|
||||||
public onUnloaded() {
|
|
||||||
super.onUnloaded();
|
|
||||||
this._stateChangedHandler.stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
public _onPropertyChanged(property: Property, oldValue: any, newValue: any) {
|
|
||||||
super._onPropertyChanged(property, oldValue, newValue);
|
|
||||||
if (property.affectsStyle) {
|
|
||||||
this._updateHandler();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get ios(): UIButton {
|
get ios(): UIButton {
|
||||||
@ -88,16 +66,17 @@ export class Button extends common.Button {
|
|||||||
this.style._updateTextDecoration();
|
this.style._updateTextDecoration();
|
||||||
}
|
}
|
||||||
|
|
||||||
private _updateHandler() {
|
@PseudoClassHandler("normal", "highlighted")
|
||||||
if (this.parent !== null && this.page !== null) {
|
_updateHandler(subscribe: boolean) {
|
||||||
let rootPage = this.page;
|
if (subscribe) {
|
||||||
let scope: styleScope.StyleScope = (<any>rootPage)._getStyleScope();
|
if (!this._stateChangedHandler) {
|
||||||
if (scope.getVisualStates(this) !== undefined) {
|
this._stateChangedHandler = new stateChanged.ControlStateChangeListener(this._ios, (s: string) => {
|
||||||
this._stateChangedHandler.start();
|
this._goToVisualState(s);
|
||||||
}
|
});
|
||||||
else {
|
|
||||||
this._stateChangedHandler.stop();
|
|
||||||
}
|
}
|
||||||
|
this._stateChangedHandler.start();
|
||||||
|
} else {
|
||||||
|
this._stateChangedHandler.stop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
/* tslint:disable:no-unused-variable */
|
/* tslint:disable:no-unused-variable */
|
||||||
import definition = require("ui/core/control-state-change");
|
import definition = require("ui/core/control-state-change");
|
||||||
import * as visualStateConstants from "ui/styling/visual-state-constants";
|
|
||||||
|
|
||||||
var ObserverClass = NSObject.extend(
|
var ObserverClass = NSObject.extend(
|
||||||
{
|
{
|
||||||
@ -36,6 +35,7 @@ export class ControlStateChangeListener implements definition.ControlStateChange
|
|||||||
if (!this._observing) {
|
if (!this._observing) {
|
||||||
this._control.addObserverForKeyPathOptionsContext(this._observer, "highlighted", NSKeyValueObservingOptions.NSKeyValueObservingOptionNew, null);
|
this._control.addObserverForKeyPathOptionsContext(this._observer, "highlighted", NSKeyValueObservingOptions.NSKeyValueObservingOptionNew, null);
|
||||||
this._observing = true;
|
this._observing = true;
|
||||||
|
this._updateState();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,7 +59,7 @@ export class ControlStateChangeListener implements definition.ControlStateChange
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _updateState() {
|
private _updateState() {
|
||||||
var state = visualStateConstants.Normal;
|
var state = "normal";
|
||||||
if (this._control.highlighted) {
|
if (this._control.highlighted) {
|
||||||
state = "highlighted";
|
state = "highlighted";
|
||||||
}
|
}
|
||||||
|
@ -14,17 +14,9 @@ import {PropertyMetadata, ProxyObject} from "ui/core/proxy";
|
|||||||
import {PropertyMetadataSettings, PropertyChangeData, Property, ValueSource, PropertyMetadata as doPropertyMetadata} from "ui/core/dependency-observable";
|
import {PropertyMetadataSettings, PropertyChangeData, Property, ValueSource, PropertyMetadata as doPropertyMetadata} from "ui/core/dependency-observable";
|
||||||
import {registerSpecialProperty} from "ui/builder/special-properties";
|
import {registerSpecialProperty} from "ui/builder/special-properties";
|
||||||
import {CommonLayoutParams, nativeLayoutParamsProperty} from "ui/styling/style";
|
import {CommonLayoutParams, nativeLayoutParamsProperty} from "ui/styling/style";
|
||||||
import * as visualStateConstants from "ui/styling/visual-state-constants";
|
|
||||||
import * as visualStateModule from "../styling/visual-state";
|
|
||||||
import * as animModule from "ui/animation";
|
import * as animModule from "ui/animation";
|
||||||
import { Source } from "utils/debug";
|
import {CssState} from "ui/styling/style-scope";
|
||||||
|
import {Source} from "utils/debug";
|
||||||
var visualState: typeof visualStateModule;
|
|
||||||
function ensureVisualState() {
|
|
||||||
if (!visualState) {
|
|
||||||
visualState = require("../styling/visual-state");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
registerSpecialProperty("class", (instance: definition.View, propertyValue: string) => {
|
registerSpecialProperty("class", (instance: definition.View, propertyValue: string) => {
|
||||||
instance.className = propertyValue;
|
instance.className = propertyValue;
|
||||||
@ -104,16 +96,31 @@ export function getAncestor(view: View, criterion: string | Function): definitio
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function PseudoClassHandler(... pseudoClasses: string[]): MethodDecorator {
|
||||||
|
let stateEventNames = pseudoClasses.map(s => ":" + s);
|
||||||
|
let listeners = Symbol("listeners");
|
||||||
|
return <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => {
|
||||||
|
function update(change: number) {
|
||||||
|
let prev = this[listeners] || 0;
|
||||||
|
let next = prev + change;
|
||||||
|
if (prev <= 0 && next > 0) {
|
||||||
|
this[propertyKey](true);
|
||||||
|
} else if (prev > 0 && next <= 0) {
|
||||||
|
this[propertyKey](false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stateEventNames.forEach(s => target[s] = update);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var viewIdCounter = 0;
|
var viewIdCounter = 0;
|
||||||
|
|
||||||
function onCssClassPropertyChanged(data: PropertyChangeData) {
|
function onCssClassPropertyChanged(data: PropertyChangeData) {
|
||||||
var view = <View>data.object;
|
var view = <View>data.object;
|
||||||
|
var classes = view.cssClasses;
|
||||||
|
classes.clear();
|
||||||
if (types.isString(data.newValue)) {
|
if (types.isString(data.newValue)) {
|
||||||
view._cssClasses = (<string>data.newValue).split(" ");
|
data.newValue.split(" ").forEach(c => classes.add(c));
|
||||||
}
|
|
||||||
else {
|
|
||||||
view._cssClasses.length = 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -153,18 +160,22 @@ export class View extends ProxyObject implements definition.View {
|
|||||||
private _parent: definition.View;
|
private _parent: definition.View;
|
||||||
private _style: style.Style;
|
private _style: style.Style;
|
||||||
private _visualState: string;
|
private _visualState: string;
|
||||||
private _requestedVisualState: string;
|
|
||||||
private _isLoaded: boolean;
|
private _isLoaded: boolean;
|
||||||
private _isLayoutValid: boolean = false;
|
private _isLayoutValid: boolean = false;
|
||||||
|
private _cssType: string;
|
||||||
|
|
||||||
private _updatingInheritedProperties: boolean;
|
private _updatingInheritedProperties: boolean;
|
||||||
private _registeredAnimations: Array<keyframeAnimationModule.KeyframeAnimation>;
|
private _registeredAnimations: Array<keyframeAnimationModule.KeyframeAnimation>;
|
||||||
|
|
||||||
public _domId: number;
|
public _domId: number;
|
||||||
public _isAddedToNativeVisualTree = false;
|
public _isAddedToNativeVisualTree = false;
|
||||||
public _cssClasses: Array<string> = [];
|
|
||||||
public _gestureObservers = {};
|
public _gestureObservers = {};
|
||||||
|
|
||||||
|
public cssClasses: Set<string> = new Set();
|
||||||
|
public cssPseudoClasses: Set<string> = new Set();
|
||||||
|
|
||||||
|
public _cssState: CssState;
|
||||||
|
|
||||||
public getGestureObservers(type: gestures.GestureTypes): Array<gestures.GesturesObserver> {
|
public getGestureObservers(type: gestures.GestureTypes): Array<gestures.GesturesObserver> {
|
||||||
return this._gestureObservers[type];
|
return this._gestureObservers[type];
|
||||||
}
|
}
|
||||||
@ -174,7 +185,7 @@ export class View extends ProxyObject implements definition.View {
|
|||||||
|
|
||||||
this._style = new style.Style(this);
|
this._style = new style.Style(this);
|
||||||
this._domId = viewIdCounter++;
|
this._domId = viewIdCounter++;
|
||||||
this._visualState = visualStateConstants.Normal;
|
this._goToVisualState("normal");
|
||||||
}
|
}
|
||||||
|
|
||||||
observe(type: gestures.GestureTypes, callback: (args: gestures.GestureEventData) => void, thisArg?: any): void {
|
observe(type: gestures.GestureTypes, callback: (args: gestures.GestureEventData) => void, thisArg?: any): void {
|
||||||
@ -510,12 +521,11 @@ export class View extends ProxyObject implements definition.View {
|
|||||||
throw new Error("isLayoutValid is read-only property.");
|
throw new Error("isLayoutValid is read-only property.");
|
||||||
}
|
}
|
||||||
|
|
||||||
get visualState(): string {
|
|
||||||
return this._visualState;
|
|
||||||
}
|
|
||||||
|
|
||||||
get cssType(): string {
|
get cssType(): string {
|
||||||
return this.typeName.toLowerCase();
|
if (!this._cssType) {
|
||||||
|
this._cssType = this.typeName.toLowerCase();
|
||||||
|
}
|
||||||
|
return this._cssType;
|
||||||
}
|
}
|
||||||
|
|
||||||
get parent(): definition.View {
|
get parent(): definition.View {
|
||||||
@ -545,6 +555,9 @@ export class View extends ProxyObject implements definition.View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public onUnloaded() {
|
public onUnloaded() {
|
||||||
|
this._onCssStateChange(this._cssState, null);
|
||||||
|
this._cssState = null;
|
||||||
|
|
||||||
this._unloadEachChildView();
|
this._unloadEachChildView();
|
||||||
|
|
||||||
this._isLoaded = false;
|
this._isLoaded = false;
|
||||||
@ -651,6 +664,20 @@ export class View extends ProxyObject implements definition.View {
|
|||||||
//
|
//
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public addPseudoClass(name: string): void {
|
||||||
|
if (!this.cssPseudoClasses.has(name)) {
|
||||||
|
this.cssPseudoClasses.add(name);
|
||||||
|
this.notifyPseudoClassChanged(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public deletePseudoClass(name: string): void {
|
||||||
|
if (this.cssPseudoClasses.has(name)) {
|
||||||
|
this.cssPseudoClasses.delete(name);
|
||||||
|
this.notifyPseudoClassChanged(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static resolveSizeAndState(size: number, specSize: number, specMode: number, childMeasuredState: number): number {
|
public static resolveSizeAndState(size: number, specSize: number, specMode: number, childMeasuredState: number): number {
|
||||||
var result = size;
|
var result = size;
|
||||||
switch (specMode) {
|
switch (specMode) {
|
||||||
@ -1071,15 +1098,13 @@ export class View extends ProxyObject implements definition.View {
|
|||||||
if (trace.enabled) {
|
if (trace.enabled) {
|
||||||
trace.write(this + " going to state: " + state, trace.categories.Style);
|
trace.write(this + " going to state: " + state, trace.categories.Style);
|
||||||
}
|
}
|
||||||
if (state === this._visualState || this._requestedVisualState === state) {
|
if (state === this._visualState) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// we use lazy require to prevent cyclic dependencies issues
|
|
||||||
ensureVisualState();
|
|
||||||
this._visualState = visualState.goToState(this, state);
|
|
||||||
|
|
||||||
// TODO: What state should we set here - the requested or the actual one?
|
this.deletePseudoClass(this._visualState);
|
||||||
this._requestedVisualState = state;
|
this._visualState = state;
|
||||||
|
this.addPseudoClass(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
public _applyXmlAttribute(attribute, value): boolean {
|
public _applyXmlAttribute(attribute, value): boolean {
|
||||||
@ -1206,4 +1231,84 @@ export class View extends ProxyObject implements definition.View {
|
|||||||
// Check for a valid _nativeView instance
|
// Check for a valid _nativeView instance
|
||||||
return !!this._nativeView;
|
return !!this._nativeView;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private notifyPseudoClassChanged(pseudoClass: string): void {
|
||||||
|
this.notify({ eventName: ":" + pseudoClass, object: this });
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Make sure the state is set to null and this is called on unloaded to clean up change listeners...
|
||||||
|
_onCssStateChange(previous: CssState, next: CssState): void {
|
||||||
|
|
||||||
|
if (!this._invalidateCssHandler) {
|
||||||
|
this._invalidateCssHandler = () => {
|
||||||
|
if (this._invalidateCssHandlerSuspended) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.applyCssState();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this._invalidateCssHandlerSuspended = true;
|
||||||
|
|
||||||
|
if (next) {
|
||||||
|
next.changeMap.forEach((changes, view) => {
|
||||||
|
if (changes.attributes) {
|
||||||
|
changes.attributes.forEach(attribute => {
|
||||||
|
view.addEventListener(attribute + "Change", this._invalidateCssHandler)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (changes.pseudoClasses) {
|
||||||
|
changes.pseudoClasses.forEach(pseudoClass => {
|
||||||
|
let eventName = ":" + pseudoClass;
|
||||||
|
view.addEventListener(":" + pseudoClass, this._invalidateCssHandler);
|
||||||
|
if (view[eventName]) {
|
||||||
|
view[eventName](+1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (previous) {
|
||||||
|
previous.changeMap.forEach((changes, view) => {
|
||||||
|
if (changes.attributes) {
|
||||||
|
changes.attributes.forEach(attribute => {
|
||||||
|
view.removeEventListener("onPropertyChanged:" + attribute, this._invalidateCssHandler)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (changes.pseudoClasses) {
|
||||||
|
changes.pseudoClasses.forEach(pseudoClass => {
|
||||||
|
let eventName = ":" + pseudoClass;
|
||||||
|
view.removeEventListener(eventName, this._invalidateCssHandler)
|
||||||
|
if (view[eventName]) {
|
||||||
|
view[eventName](-1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
this._invalidateCssHandlerSuspended = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.applyCssState();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notify that some attributes or pseudo classes that may affect the current CssState had changed.
|
||||||
|
*/
|
||||||
|
private _invalidateCssHandler;
|
||||||
|
private _invalidateCssHandlerSuspended: boolean;
|
||||||
|
|
||||||
|
private applyCssState(): void {
|
||||||
|
if (!this._cssState) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.style._beginUpdate();
|
||||||
|
this._cssState.apply();
|
||||||
|
this.style._endUpdate();
|
||||||
|
}
|
||||||
}
|
}
|
29
tns-core-modules/ui/core/view.d.ts
vendored
29
tns-core-modules/ui/core/view.d.ts
vendored
@ -33,6 +33,8 @@ declare module "ui/core/view" {
|
|||||||
|
|
||||||
export function isEventOrGesture(name: string, view: View): boolean;
|
export function isEventOrGesture(name: string, view: View): boolean;
|
||||||
|
|
||||||
|
export function PseudoClassHandler(... pseudoClasses: string[]): MethodDecorator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Point interface describes a two dimensional location.
|
* The Point interface describes a two dimensional location.
|
||||||
* It has two properties x and y, representing the x and y coordinate of the location.
|
* It has two properties x and y, representing the x and y coordinate of the location.
|
||||||
@ -289,9 +291,14 @@ declare module "ui/core/view" {
|
|||||||
*/
|
*/
|
||||||
isLayoutValid: boolean;
|
isLayoutValid: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the CSS fully qualified type name.
|
||||||
|
* Using this as element type should allow for PascalCase and kebap-case selectors, when fully qualified, to match the element.
|
||||||
|
*/
|
||||||
cssType: string;
|
cssType: string;
|
||||||
|
|
||||||
visualState: string;
|
cssClasses: Set<string>;
|
||||||
|
cssPseudoClasses: Set<string>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets owner page. This is a read-only property.
|
* Gets owner page. This is a read-only property.
|
||||||
@ -477,6 +484,20 @@ declare module "ui/core/view" {
|
|||||||
* Returns the actual size of the view in device-independent pixels.
|
* Returns the actual size of the view in device-independent pixels.
|
||||||
*/
|
*/
|
||||||
public getActualSize(): Size;
|
public getActualSize(): Size;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @protected
|
||||||
|
* @unstable
|
||||||
|
* A widget can call this method to add a matching css pseudo class.
|
||||||
|
*/
|
||||||
|
public addPseudoClass(name: string): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @protected
|
||||||
|
* @unstable
|
||||||
|
* A widget can call this method to discard mathing css pseudo class.
|
||||||
|
*/
|
||||||
|
public deletePseudoClass(name: string): void;
|
||||||
|
|
||||||
// Lifecycle events
|
// Lifecycle events
|
||||||
onLoaded(): void;
|
onLoaded(): void;
|
||||||
@ -505,7 +526,9 @@ declare module "ui/core/view" {
|
|||||||
_gestureObservers: any;
|
_gestureObservers: any;
|
||||||
_isInheritedChange(): boolean;
|
_isInheritedChange(): boolean;
|
||||||
_domId: number;
|
_domId: number;
|
||||||
_cssClasses: Array<string>;
|
|
||||||
|
_cssState: any /* "ui/styling/style-scope" */;
|
||||||
|
_onCssStateChange(previous: any /* "ui/styling/style-scope" */, any /* "ui/styling/style-scope" */);
|
||||||
|
|
||||||
_registerAnimation(animation: keyframeAnimationModule.KeyframeAnimation);
|
_registerAnimation(animation: keyframeAnimationModule.KeyframeAnimation);
|
||||||
_unregisterAnimation(animation: keyframeAnimationModule.KeyframeAnimation);
|
_unregisterAnimation(animation: keyframeAnimationModule.KeyframeAnimation);
|
||||||
@ -560,7 +583,7 @@ declare module "ui/core/view" {
|
|||||||
*/
|
*/
|
||||||
export class CustomLayoutView extends View {
|
export class CustomLayoutView extends View {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defines an interface for a View factory function.
|
* Defines an interface for a View factory function.
|
||||||
* Commonly used to specify the visualization of data objects.
|
* Commonly used to specify the visualization of data objects.
|
||||||
|
@ -177,11 +177,6 @@ export class Page extends ContentView implements dts.Page {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public removeCssSelectors(selectorExpression: string) {
|
|
||||||
this._styleScope.removeSelectors(selectorExpression);
|
|
||||||
this._refreshCss();
|
|
||||||
}
|
|
||||||
|
|
||||||
public getKeyframeAnimationWithName(animationName: string): keyframeAnimation.KeyframeAnimationInfo {
|
public getKeyframeAnimationWithName(animationName: string): keyframeAnimation.KeyframeAnimationInfo {
|
||||||
return this._styleScope.getKeyframeAnimationWithName(animationName);
|
return this._styleScope.getKeyframeAnimationWithName(animationName);
|
||||||
}
|
}
|
||||||
|
6
tns-core-modules/ui/page/page.d.ts
vendored
6
tns-core-modules/ui/page/page.d.ts
vendored
@ -118,12 +118,6 @@ declare module "ui/page" {
|
|||||||
*/
|
*/
|
||||||
addCssFile(cssFileName: string): void;
|
addCssFile(cssFileName: string): void;
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes all selectors matching the specified selector expression.
|
|
||||||
* @param selectorExpression - A valid selector expression.
|
|
||||||
*/
|
|
||||||
removeCssSelectors(selectorExpression: string): void;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a CSS keyframe animation with the specified name, if it exists.
|
* Returns a CSS keyframe animation with the specified name, if it exists.
|
||||||
*/
|
*/
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import animationModule = require("ui/animation");
|
import animationModule = require("ui/animation");
|
||||||
import keyframeAnimationModule = require("ui/animation/keyframe-animation");
|
import keyframeAnimationModule = require("ui/animation/keyframe-animation");
|
||||||
import cssParser = require("css");
|
|
||||||
import converters = require("../styling/converters");
|
import converters = require("../styling/converters");
|
||||||
import types = require("utils/types");
|
import types = require("utils/types");
|
||||||
import colorModule = require("color");
|
import colorModule = require("color");
|
||||||
@ -22,8 +21,7 @@ let animationProperties = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export class CssAnimationParser {
|
export class CssAnimationParser {
|
||||||
|
public static keyframeAnimationsFromCSSDeclarations(declarations: { property: string, value: string }[]): Array<keyframeAnimationModule.KeyframeAnimationInfo> {
|
||||||
public static keyframeAnimationsFromCSSDeclarations(declarations: cssParser.Declaration[]): Array<keyframeAnimationModule.KeyframeAnimationInfo> {
|
|
||||||
let animations: Array<keyframeAnimationModule.KeyframeAnimationInfo> = new Array<keyframeAnimationModule.KeyframeAnimationInfo>();
|
let animations: Array<keyframeAnimationModule.KeyframeAnimationInfo> = new Array<keyframeAnimationModule.KeyframeAnimationInfo>();
|
||||||
let animationInfo: keyframeAnimationModule.KeyframeAnimationInfo = undefined;
|
let animationInfo: keyframeAnimationModule.KeyframeAnimationInfo = undefined;
|
||||||
if (declarations === null || declarations === undefined) {
|
if (declarations === null || declarations === undefined) {
|
||||||
|
39
tns-core-modules/ui/styling/css-selector-parser.d.ts
vendored
Normal file
39
tns-core-modules/ui/styling/css-selector-parser.d.ts
vendored
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
//@private
|
||||||
|
declare module "ui/styling/css-selector-parser" {
|
||||||
|
export interface SimpleSelector {
|
||||||
|
pos: number;
|
||||||
|
type: "" | "*" | "#" | "." | ":" | "[]";
|
||||||
|
comb?: "+" | "~" | ">" | " ";
|
||||||
|
}
|
||||||
|
export interface SimpleIdentifierSelector extends SimpleSelector {
|
||||||
|
ident: string;
|
||||||
|
}
|
||||||
|
export interface UniversalSelector extends SimpleSelector {
|
||||||
|
type: "*";
|
||||||
|
}
|
||||||
|
export interface TypeSelector extends SimpleIdentifierSelector {
|
||||||
|
type: "";
|
||||||
|
}
|
||||||
|
export interface ClassSelector extends SimpleIdentifierSelector {
|
||||||
|
type: ".";
|
||||||
|
}
|
||||||
|
export interface IdSelector extends SimpleIdentifierSelector {
|
||||||
|
type: "#";
|
||||||
|
}
|
||||||
|
export interface PseudoClassSelector extends SimpleIdentifierSelector {
|
||||||
|
type: ":";
|
||||||
|
}
|
||||||
|
export interface AttributeSelector extends SimpleSelector {
|
||||||
|
type: "[]";
|
||||||
|
prop: string;
|
||||||
|
test?: "=" | "^=" | "$=" | "*=" | "=" | "~=" | "|=";
|
||||||
|
value?: string;
|
||||||
|
}
|
||||||
|
export function isUniversal(sel: SimpleSelector): sel is UniversalSelector;
|
||||||
|
export function isType(sel: SimpleSelector): sel is TypeSelector;
|
||||||
|
export function isClass(sel: SimpleSelector): sel is ClassSelector;
|
||||||
|
export function isId(sel: SimpleSelector): sel is IdSelector;
|
||||||
|
export function isPseudo(sel: SimpleSelector): sel is PseudoClassSelector;
|
||||||
|
export function isAttribute(sel: SimpleSelector): sel is AttributeSelector;
|
||||||
|
export function parse(selector: string): SimpleSelector[];
|
||||||
|
}
|
125
tns-core-modules/ui/styling/css-selector-parser.ts
Normal file
125
tns-core-modules/ui/styling/css-selector-parser.ts
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
/// <reference path="./css-selector-parser.d.ts" />
|
||||||
|
export interface SimpleSelector {
|
||||||
|
pos: number;
|
||||||
|
type: "" | "*" | "#" | "." | ":" | "[]";
|
||||||
|
comb?: "+" | "~" | ">" | " ";
|
||||||
|
}
|
||||||
|
export interface SimpleIdentifierSelector extends SimpleSelector {
|
||||||
|
ident: string;
|
||||||
|
}
|
||||||
|
export interface UniversalSelector extends SimpleSelector {
|
||||||
|
type: "*";
|
||||||
|
}
|
||||||
|
export interface TypeSelector extends SimpleIdentifierSelector {
|
||||||
|
type: "";
|
||||||
|
}
|
||||||
|
export interface ClassSelector extends SimpleIdentifierSelector {
|
||||||
|
type: ".";
|
||||||
|
}
|
||||||
|
export interface IdSelector extends SimpleIdentifierSelector {
|
||||||
|
type: "#";
|
||||||
|
}
|
||||||
|
export interface PseudoClassSelector extends SimpleIdentifierSelector {
|
||||||
|
type: ":";
|
||||||
|
}
|
||||||
|
export interface AttributeSelector extends SimpleSelector {
|
||||||
|
type: "[]";
|
||||||
|
prop: string;
|
||||||
|
test?: "=" | "^=" | "$=" | "*=" | "=" | "~=" | "|=";
|
||||||
|
value?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isUniversal(sel: SimpleSelector): sel is UniversalSelector {
|
||||||
|
return sel.type === "*";
|
||||||
|
}
|
||||||
|
export function isType(sel: SimpleSelector): sel is TypeSelector {
|
||||||
|
return sel.type === "";
|
||||||
|
}
|
||||||
|
export function isClass(sel: SimpleSelector): sel is ClassSelector {
|
||||||
|
return sel.type === ".";
|
||||||
|
}
|
||||||
|
export function isId(sel: SimpleSelector): sel is IdSelector {
|
||||||
|
return sel.type === "#";
|
||||||
|
}
|
||||||
|
export function isPseudo(sel: SimpleSelector): sel is PseudoClassSelector {
|
||||||
|
return sel.type === ":";
|
||||||
|
}
|
||||||
|
export function isAttribute(sel: SimpleSelector): sel is AttributeSelector {
|
||||||
|
return sel.type === "[]";
|
||||||
|
}
|
||||||
|
|
||||||
|
var regex = /(\s*)(?:(\*)|(#|\.|:|\b)([_-\w][_-\w\d]*)|\[\s*([_-\w][_-\w\d]*)\s*(?:(=|\^=|\$=|\*=|\~=|\|=)\s*(?:([_-\w][_-\w\d]*)|"((?:[^\\"]|\\(?:"|n|r|f|\\|0-9a-f))*)"|'((?:[^\\']|\\(?:'|n|r|f|\\|0-9a-f))*)')\s*)?\])(?:\s*(\+|~|>|\s))?/g;
|
||||||
|
// no lead ws univ type pref and ident [ prop = ident -or- "string escapes \" \00aaff" -or- 'string escapes \' urf-8: \00aaff' ] combinator
|
||||||
|
|
||||||
|
export function parse(selector: string): SimpleSelector[] {
|
||||||
|
let selectors: any[] = [];
|
||||||
|
|
||||||
|
var result: RegExpExecArray;
|
||||||
|
var lastIndex = regex.lastIndex = 0;
|
||||||
|
while(result = regex.exec(selector)) {
|
||||||
|
let pos = result.index;
|
||||||
|
if (lastIndex !== pos) {
|
||||||
|
throw new Error(`Unexpected characters at index, near: ${lastIndex}: ${result.input.substr(lastIndex, 32)}`);
|
||||||
|
} else if (!result[0] || result[0].length === 0) {
|
||||||
|
throw new Error(`Last selector match got zero character result at index ${lastIndex}, near: ${result.input.substr(lastIndex, 32)}`);
|
||||||
|
}
|
||||||
|
pos += getLeadingWhiteSpace(result).length;
|
||||||
|
lastIndex = regex.lastIndex;
|
||||||
|
|
||||||
|
var type = getType(result);
|
||||||
|
let selector: SimpleSelector | SimpleIdentifierSelector | AttributeSelector;
|
||||||
|
switch(type) {
|
||||||
|
case "*":
|
||||||
|
selector = { pos, type };
|
||||||
|
break;
|
||||||
|
case "#":
|
||||||
|
case ".":
|
||||||
|
case ":":
|
||||||
|
case "":
|
||||||
|
let ident = getIdentifier(result);
|
||||||
|
selector = { pos, type, ident };
|
||||||
|
break;
|
||||||
|
case "[]":
|
||||||
|
let prop = getProperty(result);
|
||||||
|
let test = getPropertyTest(result);
|
||||||
|
// TODO: Unescape escape sequences. Unescape UTF-8 characters.
|
||||||
|
let value = getPropertyValue(result);
|
||||||
|
selector = test ? { pos, type, prop, test, value } : { pos, type, prop };
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error("Unhandled type.");
|
||||||
|
}
|
||||||
|
|
||||||
|
let comb = getCombinator(result);
|
||||||
|
if (comb) {
|
||||||
|
selector.comb = comb;
|
||||||
|
}
|
||||||
|
selectors.push(selector);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectors.length > 0) {
|
||||||
|
delete selectors[selectors.length - 1].comb;
|
||||||
|
}
|
||||||
|
return selectors;
|
||||||
|
}
|
||||||
|
function getLeadingWhiteSpace(result: RegExpExecArray): string {
|
||||||
|
return result[1] || "";
|
||||||
|
}
|
||||||
|
function getType(result: RegExpExecArray): "" | "*" | "." | "#" | ":" | "[]" {
|
||||||
|
return <"[]">(result[5] && "[]") || <"*">result[2] || <"" | "." | "#" | ":">result[3];
|
||||||
|
}
|
||||||
|
function getIdentifier(result: RegExpExecArray): string {
|
||||||
|
return result[4];
|
||||||
|
}
|
||||||
|
function getProperty(result: RegExpExecArray): string {
|
||||||
|
return result[5];
|
||||||
|
}
|
||||||
|
function getPropertyTest(result: RegExpExecArray): string {
|
||||||
|
return result[6] || undefined;
|
||||||
|
}
|
||||||
|
function getPropertyValue(result: RegExpExecArray): string {
|
||||||
|
return result[7] || result[8] || result[9];
|
||||||
|
}
|
||||||
|
function getCombinator(result: RegExpExecArray): "+" | "~" | ">" | " " {
|
||||||
|
return <("+" | "~" | ">" | " ")>result[result.length - 1] || undefined;
|
||||||
|
}
|
96
tns-core-modules/ui/styling/css-selector.d.ts
vendored
96
tns-core-modules/ui/styling/css-selector.d.ts
vendored
@ -1,60 +1,72 @@
|
|||||||
declare module "ui/styling/css-selector" {
|
declare module "ui/styling/css-selector" {
|
||||||
import view = require("ui/core/view");
|
import * as parser from "css";
|
||||||
import cssParser = require("css");
|
|
||||||
import styleProperty = require("ui/styling/style-property");
|
|
||||||
import keyframeAnimation = require("ui/animation/keyframe-animation");
|
|
||||||
|
|
||||||
export class CssSelector {
|
/**
|
||||||
constructor(expression: string, declarations: cssParser.Declaration[]);
|
* An interface describing the shape of a type on which the selectors may apply.
|
||||||
|
* Note, the ui/core/view.View implements Node.
|
||||||
|
*/
|
||||||
|
interface Node {
|
||||||
|
parent?: Node;
|
||||||
|
|
||||||
expression: string;
|
id?: string;
|
||||||
attrExpression: string;
|
cssType?: string;
|
||||||
|
cssClasses?: Set<string>;
|
||||||
declarations(): Array<{ property: string; value: any }>;
|
cssPseudoClasses?: Set<string>;
|
||||||
|
|
||||||
specificity: number;
|
|
||||||
|
|
||||||
animations: Array<keyframeAnimation.KeyframeAnimationInfo>;
|
|
||||||
|
|
||||||
matches(view: view.View): boolean;
|
|
||||||
|
|
||||||
apply(view: view.View, valueSourceModifier: number);
|
|
||||||
|
|
||||||
eachSetter(callback: (property: styleProperty.Property, resolvedValue: any) => void);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class CssTypeSelector extends CssSelector {
|
interface Declaration {
|
||||||
specificity: number;
|
property: string;
|
||||||
matches(view: view.View): boolean;
|
value: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class CssIdSelector extends CssSelector {
|
class SelectorCore {
|
||||||
specificity: number;
|
/**
|
||||||
matches(view: view.View): boolean;
|
* Dynamic selectors depend on attributes and pseudo classes.
|
||||||
|
*/
|
||||||
|
dynamic: boolean;
|
||||||
|
match(node: Node): boolean;
|
||||||
|
ruleset: RuleSet;
|
||||||
}
|
}
|
||||||
|
|
||||||
class CssClassSelector extends CssSelector {
|
class RuleSet {
|
||||||
specificity: number;
|
/**
|
||||||
matches(view: view.View): boolean;
|
* Gets the selectors in this ruleset's selector group.
|
||||||
|
*/
|
||||||
|
selectors: SelectorCore[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the key-value list of declarations for the ruleset.
|
||||||
|
*/
|
||||||
|
declarations: Declaration[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CssVisualStateSelector extends CssSelector {
|
class SelectorsMap {
|
||||||
specificity: number;
|
constructor(rules: RuleSet[]);
|
||||||
|
|
||||||
key: string;
|
/**
|
||||||
|
* Get a list of selectors that are likely to match the node.
|
||||||
state: string;
|
*/
|
||||||
|
query<T extends Node>(node: T): SelectorsMatch<T>;
|
||||||
constructor(expression: string, declarations: cssParser.Declaration[]);
|
|
||||||
matches(view: view.View): boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createSelector(expression: string, declarations: cssParser.Declaration[]): CssSelector;
|
type ChangeMap<T extends Node> = Map<T, Changes>;
|
||||||
|
|
||||||
class InlineStyleSelector extends CssSelector {
|
interface Changes {
|
||||||
constructor(declarations: cssParser.Declaration[]);
|
attributes?: Set<string>;
|
||||||
apply(view: view.View);
|
pseudoClasses?: Set<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function applyInlineSyle(view: view.View, declarations: cssParser.Declaration[]);
|
class SelectorsMatch<T extends Node> {
|
||||||
|
/**
|
||||||
|
* Gets the static selectors that match the queried node and the dynamic selectors that may potentially match the queried node.
|
||||||
|
*/
|
||||||
|
selectors: SelectorCore[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a map of nodes to attributes and pseudo classes, that may affect the state of the dynamic
|
||||||
|
*/
|
||||||
|
changeMap: ChangeMap<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fromAstNodes(astRules: parser.Node[]): RuleSet[];
|
||||||
}
|
}
|
||||||
|
@ -1,485 +1,541 @@
|
|||||||
import view = require("ui/core/view");
|
import {Node, Declaration, Changes, ChangeMap} from "ui/styling/css-selector";
|
||||||
import observable = require("ui/core/dependency-observable");
|
import {isNullOrUndefined} from "utils/types";
|
||||||
import cssParser = require("css");
|
import {escapeRegexSymbols} from "utils/utils";
|
||||||
import * as trace from "trace";
|
|
||||||
import {StyleProperty, ResolvedStylePropertyHandler, withStyleProperty} from "ui/styling/style-property";
|
|
||||||
import * as types from "utils/types";
|
|
||||||
import * as utils from "utils/utils";
|
|
||||||
import keyframeAnimation = require("ui/animation/keyframe-animation");
|
|
||||||
import cssAnimationParser = require("./css-animation-parser");
|
|
||||||
import {getSpecialPropertySetter} from "ui/builder/special-properties";
|
|
||||||
|
|
||||||
let ID_SPECIFICITY = 1000000;
|
import * as cssParser from "css";
|
||||||
let ATTR_SPECIFITY = 10000;
|
import * as selectorParser from "./css-selector-parser";
|
||||||
let CLASS_SPECIFICITY = 100;
|
|
||||||
let TYPE_SPECIFICITY = 1;
|
|
||||||
|
|
||||||
export class CssSelector {
|
const enum Specificity {
|
||||||
public animations: Array<keyframeAnimation.KeyframeAnimationInfo>;
|
Inline = 0x01000000,
|
||||||
|
Id = 0x00010000,
|
||||||
|
Attribute = 0x00000100,
|
||||||
|
Class = 0x00000100,
|
||||||
|
PseudoClass = 0x00000100,
|
||||||
|
Type = 0x00000001,
|
||||||
|
Universal = 0x00000000,
|
||||||
|
Invalid = 0x00000000
|
||||||
|
}
|
||||||
|
|
||||||
private _expression: string;
|
const enum Rarity {
|
||||||
private _declarations: cssParser.Declaration[];
|
Invalid = 4,
|
||||||
private _attrExpression: string;
|
Id = 3,
|
||||||
|
Class = 2,
|
||||||
|
Type = 1,
|
||||||
|
PseudoClass = 0,
|
||||||
|
Attribute = 0,
|
||||||
|
Universal = 0,
|
||||||
|
Inline = 0
|
||||||
|
}
|
||||||
|
|
||||||
constructor(expression: string, declarations: cssParser.Declaration[]) {
|
interface LookupSorter {
|
||||||
if (expression) {
|
sortById(id: string, sel: SelectorCore);
|
||||||
let leftSquareBracketIndex = expression.indexOf(LSBRACKET);
|
sortByClass(cssClass: string, sel: SelectorCore);
|
||||||
if (leftSquareBracketIndex >= 0) {
|
sortByType(cssType: string, sel: SelectorCore);
|
||||||
// extracts what is inside square brackets ([target = 'test'] will extract "target = 'test'")
|
sortAsUniversal(sel: SelectorCore);
|
||||||
let paramsRegex = /\[\s*(.*)\s*\]/;
|
}
|
||||||
let attrParams = paramsRegex.exec(expression);
|
|
||||||
if (attrParams && attrParams.length > 1) {
|
namespace Match {
|
||||||
this._attrExpression = attrParams[1].trim();
|
/**
|
||||||
}
|
* Depends on attributes or pseudoclasses state;
|
||||||
this._expression = expression.substr(0, leftSquareBracketIndex);
|
*/
|
||||||
}
|
export var Dynamic = true;
|
||||||
else {
|
/**
|
||||||
this._expression = expression;
|
* Depends only on the tree structure.
|
||||||
}
|
*/
|
||||||
|
export var Static = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectorProperties(specificity: Specificity, rarity: Rarity, dynamic: boolean = false): ClassDecorator {
|
||||||
|
return cls => {
|
||||||
|
cls.prototype.specificity = specificity;
|
||||||
|
cls.prototype.rarity = rarity;
|
||||||
|
cls.prototype.combinator = "";
|
||||||
|
cls.prototype.dynamic = dynamic;
|
||||||
|
return cls;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare type Combinator = "+" | ">" | "~" | " ";
|
||||||
|
@SelectorProperties(Specificity.Universal, Rarity.Universal, Match.Static)
|
||||||
|
export abstract class SelectorCore {
|
||||||
|
public specificity: number;
|
||||||
|
public rarity: Rarity;
|
||||||
|
public combinator: Combinator;
|
||||||
|
public ruleset: RuleSet;
|
||||||
|
public dynamic: boolean;
|
||||||
|
public abstract match(node: Node): boolean;
|
||||||
|
/**
|
||||||
|
* If the selector is static returns if it matches the node.
|
||||||
|
* If the selector is dynamic returns if it may match the node, and accumulates any changes that may affect its state.
|
||||||
|
*/
|
||||||
|
public abstract accumulateChanges(node: Node, map: ChangeAccumulator): boolean;
|
||||||
|
public lookupSort(sorter: LookupSorter, base?: SelectorCore): void { sorter.sortAsUniversal(base || this); }
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class SimpleSelector extends SelectorCore {
|
||||||
|
public accumulateChanges(node: Node, map?: ChangeAccumulator): boolean {
|
||||||
|
if (!this.dynamic) {
|
||||||
|
return this.match(node);
|
||||||
|
} else if (this.mayMatch(node)) {
|
||||||
|
this.trackChanges(node, map);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
this._declarations = declarations;
|
|
||||||
this.animations = cssAnimationParser.CssAnimationParser.keyframeAnimationsFromCSSDeclarations(declarations);
|
|
||||||
}
|
|
||||||
|
|
||||||
get expression(): string {
|
|
||||||
return this._expression;
|
|
||||||
}
|
|
||||||
|
|
||||||
get attrExpression(): string {
|
|
||||||
return this._attrExpression;
|
|
||||||
}
|
|
||||||
|
|
||||||
get declarations(): Array<{ property: string; value: any }> {
|
|
||||||
return this._declarations;
|
|
||||||
}
|
|
||||||
|
|
||||||
get specificity(): number {
|
|
||||||
throw "Specificity property is abstract";
|
|
||||||
}
|
|
||||||
|
|
||||||
protected get valueSourceModifier(): number {
|
|
||||||
return observable.ValueSource.Css;
|
|
||||||
}
|
|
||||||
|
|
||||||
public matches(view: view.View): boolean {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
public mayMatch(node: Node): boolean { return this.match(node); }
|
||||||
|
public trackChanges(node: Node, map: ChangeAccumulator): void {
|
||||||
|
// No-op, silence the tslint 'block is empty'.
|
||||||
|
// Some derived classes (dynamic) will actually fill the map with stuff here, some (static) won't do anything.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public apply(view: view.View, valueSourceModifier: number) {
|
function wrap(text: string): string {
|
||||||
let modifier = valueSourceModifier || this.valueSourceModifier;
|
return text ? ` ${text} ` : "";
|
||||||
this.eachSetter((property, value) => {
|
}
|
||||||
if (types.isString(property)) {
|
|
||||||
const propertyName = <string>property;
|
|
||||||
let attrHandled = false;
|
|
||||||
let specialSetter = getSpecialPropertySetter(propertyName);
|
|
||||||
|
|
||||||
if (!attrHandled && specialSetter) {
|
@SelectorProperties(Specificity.Invalid, Rarity.Invalid, Match.Static)
|
||||||
specialSetter(view, value);
|
export class InvalidSelector extends SimpleSelector {
|
||||||
attrHandled = true;
|
constructor(public e: Error) { super(); }
|
||||||
|
public toString(): string { return `<error: ${this.e}>`; }
|
||||||
|
public match(node: Node): boolean { return false; }
|
||||||
|
public lookupSort(sorter: LookupSorter, base?: SelectorCore): void {
|
||||||
|
// No-op, silence the tslint 'block is empty'.
|
||||||
|
// It feels like tslint has problems with simple polymorphism...
|
||||||
|
// This selector is invalid and will never match so we won't bother sorting it to further appear in queries.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SelectorProperties(Specificity.Universal, Rarity.Universal, Match.Static)
|
||||||
|
export class UniversalSelector extends SimpleSelector {
|
||||||
|
public toString(): string { return `*${wrap(this.combinator)}`; }
|
||||||
|
public match(node: Node): boolean { return true; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@SelectorProperties(Specificity.Id, Rarity.Id, Match.Static)
|
||||||
|
export class IdSelector extends SimpleSelector {
|
||||||
|
constructor(public id: string) { super(); }
|
||||||
|
public toString(): string { return `#${this.id}${wrap(this.combinator)}`; }
|
||||||
|
public match(node: Node): boolean { return node.id === this.id; }
|
||||||
|
public lookupSort(sorter: LookupSorter, base?: SelectorCore): void { sorter.sortById(this.id, base || this); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@SelectorProperties(Specificity.Type, Rarity.Type, Match.Static)
|
||||||
|
export class TypeSelector extends SimpleSelector {
|
||||||
|
constructor(public cssType: string) { super(); }
|
||||||
|
public toString(): string { return `${this.cssType}${wrap(this.combinator)}`; }
|
||||||
|
public match(node: Node): boolean { return node.cssType === this.cssType; }
|
||||||
|
public lookupSort(sorter: LookupSorter, base?: SelectorCore): void { sorter.sortByType(this.cssType, base || this); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@SelectorProperties(Specificity.Class, Rarity.Class, Match.Static)
|
||||||
|
export class ClassSelector extends SimpleSelector {
|
||||||
|
constructor(public cssClass: string) { super(); }
|
||||||
|
public toString(): string { return `.${this.cssClass}${wrap(this.combinator)}`; }
|
||||||
|
public match(node: Node): boolean { return node.cssClasses && node.cssClasses.has(this.cssClass); }
|
||||||
|
public lookupSort(sorter: LookupSorter, base?: SelectorCore): void { sorter.sortByClass(this.cssClass, base || this); }
|
||||||
|
}
|
||||||
|
|
||||||
|
declare type AttributeTest = "=" | "^=" | "$=" | "*=" | "=" | "~=" | "|=";
|
||||||
|
@SelectorProperties(Specificity.Attribute, Rarity.Attribute, Match.Dynamic)
|
||||||
|
export class AttributeSelector extends SimpleSelector {
|
||||||
|
constructor(public attribute: string, public test?: AttributeTest, public value?: string) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
if (!test) {
|
||||||
|
// HasAttribute
|
||||||
|
this.match = node => !isNullOrUndefined(node[attribute]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!value) {
|
||||||
|
this.match = node => false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let escapedValue = escapeRegexSymbols(value);
|
||||||
|
let regexp: RegExp = null;
|
||||||
|
switch(test) {
|
||||||
|
case "^=": // PrefixMatch
|
||||||
|
regexp = new RegExp("^" + escapedValue);
|
||||||
|
break;
|
||||||
|
case "$=": // SuffixMatch
|
||||||
|
regexp = new RegExp(escapedValue + "$");
|
||||||
|
break;
|
||||||
|
case "*=": // SubstringMatch
|
||||||
|
regexp = new RegExp(escapedValue);
|
||||||
|
break;
|
||||||
|
case "=": // Equals
|
||||||
|
regexp = new RegExp("^" + escapedValue + "$");
|
||||||
|
break;
|
||||||
|
case "~=": // Includes
|
||||||
|
if (/\s/.test(value)) {
|
||||||
|
this.match = node => false;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
regexp = new RegExp("(^|\\s)" + escapedValue + "(\\s|$)");
|
||||||
|
break;
|
||||||
|
case "|=": // DashMatch
|
||||||
|
regexp = new RegExp("^" + escapedValue + "(-|$)");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if (!attrHandled && propertyName in view) {
|
if (regexp) {
|
||||||
view[propertyName] = utils.convertString(value);
|
this.match = node => regexp.test(node[attribute] + "");
|
||||||
}
|
return;
|
||||||
|
} else {
|
||||||
|
this.match = node => false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public get specificity(): number { return Specificity.Attribute; }
|
||||||
|
public get rarity(): number { return Specificity.Attribute; }
|
||||||
|
public toString(): string { return `[${this.attribute}${wrap(this.test)}${(this.test && this.value) || ''}]${wrap(this.combinator)}`; }
|
||||||
|
public match(node: Node): boolean { return false; }
|
||||||
|
public mayMatch(node: Node): boolean { return true; }
|
||||||
|
public trackChanges(node: Node, map: ChangeAccumulator): void { map.addAttribute(node, this.attribute); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@SelectorProperties(Specificity.PseudoClass, Rarity.PseudoClass, Match.Dynamic)
|
||||||
|
export class PseudoClassSelector extends SimpleSelector {
|
||||||
|
constructor(public cssPseudoClass: string) { super(); }
|
||||||
|
public toString(): string { return `:${this.cssPseudoClass}${wrap(this.combinator)}`; }
|
||||||
|
public match(node: Node): boolean { return node.cssPseudoClasses && node.cssPseudoClasses.has(this.cssPseudoClass); }
|
||||||
|
public mayMatch(node: Node): boolean { return true; }
|
||||||
|
public trackChanges(node: Node, map: ChangeAccumulator): void { map.addPseudoClass(node, this.cssPseudoClass); }
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SimpleSelectorSequence extends SimpleSelector {
|
||||||
|
private head: SimpleSelector;
|
||||||
|
constructor(public selectors: SimpleSelector[]) {
|
||||||
|
super();
|
||||||
|
this.specificity = selectors.reduce((sum, sel) => sel.specificity + sum, 0);
|
||||||
|
this.head = this.selectors.reduce((prev, curr) => !prev || (curr.rarity > prev.rarity) ? curr : prev, null);
|
||||||
|
this.dynamic = selectors.some(sel => sel.dynamic);
|
||||||
|
}
|
||||||
|
public toString(): string { return `${this.selectors.join("")}${wrap(this.combinator)}`; }
|
||||||
|
public match(node: Node): boolean { return this.selectors.every(sel => sel.match(node)); }
|
||||||
|
public mayMatch(node: Node): boolean {
|
||||||
|
return this.selectors.every(sel => sel.mayMatch(node));
|
||||||
|
}
|
||||||
|
public trackChanges(node, map): void {
|
||||||
|
this.selectors.forEach(sel => sel.trackChanges(node, map));
|
||||||
|
}
|
||||||
|
public lookupSort(sorter: LookupSorter, base?: SelectorCore): void {
|
||||||
|
this.head.lookupSort(sorter, base || this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Selector extends SelectorCore {
|
||||||
|
// Grouped by ancestor combinators, then by direct child combinators.
|
||||||
|
private groups: Selector.ChildGroup[];
|
||||||
|
private last: SelectorCore;
|
||||||
|
|
||||||
|
constructor(public selectors: SimpleSelector[]) {
|
||||||
|
super();
|
||||||
|
let lastGroup: SimpleSelector[];
|
||||||
|
let groups: SimpleSelector[][] = [];
|
||||||
|
selectors.reverse().forEach(sel => {
|
||||||
|
switch(sel.combinator) {
|
||||||
|
case undefined:
|
||||||
|
case " ":
|
||||||
|
groups.push(lastGroup = []);
|
||||||
|
case ">":
|
||||||
|
lastGroup.push(sel);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported combinator "${sel.combinator}".`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.groups = groups.map(g => new Selector.ChildGroup(g));
|
||||||
|
this.last = selectors[0];
|
||||||
|
this.specificity = selectors.reduce((sum, sel) => sel.specificity + sum, 0);
|
||||||
|
this.dynamic = selectors.some(sel => sel.dynamic);
|
||||||
|
}
|
||||||
|
|
||||||
|
public toString(): string { return this.selectors.join(""); }
|
||||||
|
|
||||||
|
public match(node: Node): boolean {
|
||||||
|
return this.groups.every((group, i) => {
|
||||||
|
if (i === 0) {
|
||||||
|
node = group.match(node);
|
||||||
|
return !!node;
|
||||||
} else {
|
} else {
|
||||||
const resolvedProperty = <StyleProperty>property;
|
let ancestor = node;
|
||||||
try {
|
while(ancestor = ancestor.parent) {
|
||||||
view.style._setValue(resolvedProperty, value, modifier);
|
if (node = group.match(ancestor)) {
|
||||||
} catch (ex) {
|
return true;
|
||||||
if (trace.enabled) {
|
|
||||||
trace.write("Error setting property: " + resolvedProperty.name + " view: " + view + " value: " + value + " " + ex, trace.categories.Style, trace.messageType.error);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (this.animations && view.isLoaded && view._nativeView !== undefined) {
|
}
|
||||||
for (let animationInfo of this.animations) {
|
|
||||||
let animation = keyframeAnimation.KeyframeAnimation.keyframeAnimationFromInfo(animationInfo, modifier);
|
public lookupSort(sorter: LookupSorter, base?: SelectorCore): void {
|
||||||
if (animation) {
|
this.last.lookupSort(sorter, this);
|
||||||
view._registerAnimation(animation);
|
}
|
||||||
animation.play(view)
|
|
||||||
.then(() => { view._unregisterAnimation(animation); })
|
public accumulateChanges(node: Node, map?: ChangeAccumulator): boolean {
|
||||||
.catch((e) => { view._unregisterAnimation(animation); });
|
if (!this.dynamic) {
|
||||||
|
return this.match(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
let bounds: Selector.Bound[] = [];
|
||||||
|
let mayMatch = this.groups.every((group, i) => {
|
||||||
|
if (i === 0) {
|
||||||
|
let nextNode = group.mayMatch(node);
|
||||||
|
bounds.push({ left: node, right: node });
|
||||||
|
node = nextNode;
|
||||||
|
return !!node;
|
||||||
|
} else {
|
||||||
|
let ancestor = node;
|
||||||
|
while(ancestor = ancestor.parent) {
|
||||||
|
let nextNode = group.mayMatch(ancestor);
|
||||||
|
if (nextNode) {
|
||||||
|
bounds.push({ left: ancestor, right: null });
|
||||||
|
node = nextNode;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Calculating the right bounds for each selectors won't save much
|
||||||
|
if (!mayMatch) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!map) {
|
||||||
|
return mayMatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < this.groups.length; i++) {
|
||||||
|
let group = this.groups[i];
|
||||||
|
if (!group.dynamic) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let bound = bounds[i];
|
||||||
|
let node = bound.left;
|
||||||
|
do {
|
||||||
|
if (group.mayMatch(node)) {
|
||||||
|
group.trackChanges(node, map);
|
||||||
|
}
|
||||||
|
} while((node !== bound.right) && (node = node.parent));
|
||||||
|
}
|
||||||
|
|
||||||
|
return mayMatch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export namespace Selector {
|
||||||
|
// Non-spec. Selector sequences are grouped by ancestor then by child combinators for easier backtracking.
|
||||||
|
export class ChildGroup {
|
||||||
|
public dynamic: boolean;
|
||||||
|
|
||||||
|
constructor(private selectors: SimpleSelector[]) {
|
||||||
|
this.dynamic = selectors.some(sel => sel.dynamic);
|
||||||
|
}
|
||||||
|
|
||||||
|
public match(node: Node): Node {
|
||||||
|
return this.selectors.every((sel, i) => (i === 0 ? node : node = node.parent) && sel.match(node)) ? node : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public mayMatch(node: Node): Node {
|
||||||
|
return this.selectors.every((sel, i) => (i === 0 ? node : node = node.parent) && sel.mayMatch(node)) ? node : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public trackChanges(node: Node, map: ChangeAccumulator) {
|
||||||
|
this.selectors.forEach((sel, i) => (i === 0 ? node : node = node.parent) && sel.trackChanges(node, map));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export interface Bound {
|
||||||
|
left: Node;
|
||||||
|
right: Node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RuleSet {
|
||||||
|
constructor(public selectors: SelectorCore[], private declarations: Declaration[]) {
|
||||||
|
this.selectors.forEach(sel => sel.ruleset = this);
|
||||||
|
}
|
||||||
|
public toString(): string { return `${this.selectors.join(", ")} {${this.declarations.map((d, i) => `${i === 0 ? " ": ""}${d.property}: ${d.value}`).join("; ")} }`; }
|
||||||
|
public lookupSort(sorter: LookupSorter): void { this.selectors.forEach(sel => sel.lookupSort(sorter)); }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fromAstNodes(astRules: cssParser.Node[]): RuleSet[] {
|
||||||
|
return astRules.filter(isRule).map(rule => {
|
||||||
|
let declarations = rule.declarations.filter(isDeclaration).map(createDeclaration);
|
||||||
|
let selectors = rule.selectors.map(createSelector);
|
||||||
|
let ruleset = new RuleSet(selectors, declarations);
|
||||||
|
return ruleset;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createDeclaration(decl: cssParser.Declaration): any {
|
||||||
|
return { property: decl.property.toLowerCase(), value: decl.value };
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSelector(sel: string): SimpleSelector | SimpleSelectorSequence | Selector {
|
||||||
|
try {
|
||||||
|
let ast = selectorParser.parse(sel);
|
||||||
|
if (ast.length === 0) {
|
||||||
|
return new InvalidSelector(new Error("Empty selector"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let selectors = ast.map(createSimpleSelector);
|
||||||
|
let sequences: (SimpleSelector | SimpleSelectorSequence)[] = [];
|
||||||
|
|
||||||
|
// Join simple selectors into sequences, set combinators
|
||||||
|
for (let seqStart = 0, seqEnd = 0, last = selectors.length - 1; seqEnd <= last; seqEnd++) {
|
||||||
|
let sel = selectors[seqEnd];
|
||||||
|
let astComb = ast[seqEnd].comb;
|
||||||
|
if (astComb || seqEnd === last) {
|
||||||
|
if (seqStart === seqEnd) {
|
||||||
|
// This is a sequnce with single SimpleSelector, so we will not combine it into SimpleSelectorSequence.
|
||||||
|
sel.combinator = astComb;
|
||||||
|
sequences.push(sel);
|
||||||
|
} else {
|
||||||
|
let sequence = new SimpleSelectorSequence(selectors.slice(seqStart, seqEnd + 1));
|
||||||
|
sequence.combinator = astComb;
|
||||||
|
sequences.push(sequence);
|
||||||
|
}
|
||||||
|
seqStart = seqEnd + 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public eachSetter(callback: ResolvedStylePropertyHandler) {
|
if (sequences.length === 1) {
|
||||||
for (let i = 0; i < this._declarations.length; i++) {
|
// This is a selector with a single SinmpleSelectorSequence so we will not combine it into Selector.
|
||||||
let declaration = this._declarations[i];
|
return sequences[0];
|
||||||
let name = declaration.property;
|
|
||||||
let resolvedValue = declaration.value;
|
|
||||||
withStyleProperty(name, resolvedValue, callback);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public get declarationText(): string {
|
|
||||||
return this.declarations.map((declaration) => `${declaration.property}: ${declaration.value}`).join("; ");
|
|
||||||
}
|
|
||||||
|
|
||||||
public get attrExpressionText(): string {
|
|
||||||
if (this.attrExpression) {
|
|
||||||
return `[${this.attrExpression}]`;
|
|
||||||
} else {
|
} else {
|
||||||
return "";
|
return new Selector(sequences);
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
return new InvalidSelector(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSimpleSelector(sel: selectorParser.SimpleSelector): SimpleSelector {
|
||||||
|
if (selectorParser.isUniversal(sel)) {
|
||||||
|
return new UniversalSelector();
|
||||||
|
} else if (selectorParser.isId(sel)) {
|
||||||
|
return new IdSelector(sel.ident);
|
||||||
|
} else if (selectorParser.isType(sel)) {
|
||||||
|
return new TypeSelector(sel.ident.replace(/-/, '').toLowerCase());
|
||||||
|
} else if (selectorParser.isClass(sel)) {
|
||||||
|
return new ClassSelector(sel.ident);
|
||||||
|
} else if (selectorParser.isPseudo(sel)) {
|
||||||
|
return new PseudoClassSelector(sel.ident);
|
||||||
|
} else if (selectorParser.isAttribute(sel)) {
|
||||||
|
if (sel.test) {
|
||||||
|
return new AttributeSelector(sel.prop, sel.test, sel.value);
|
||||||
|
} else {
|
||||||
|
return new AttributeSelector(sel.prop)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class CssTypeSelector extends CssSelector {
|
function isRule(node: cssParser.Node): node is cssParser.Rule {
|
||||||
get specificity(): number {
|
return node.type === "rule";
|
||||||
let result = TYPE_SPECIFICITY;
|
}
|
||||||
let dotIndex = this.expression.indexOf(DOT);
|
function isDeclaration(node: cssParser.Node): node is cssParser.Declaration {
|
||||||
if (dotIndex > -1) {
|
return node.type === "declaration";
|
||||||
result += CLASS_SPECIFICITY;
|
}
|
||||||
}
|
|
||||||
return result;
|
interface SelectorInDocument {
|
||||||
|
pos: number;
|
||||||
|
sel: SelectorCore;
|
||||||
|
}
|
||||||
|
interface SelectorMap {
|
||||||
|
[key: string]: SelectorInDocument[]
|
||||||
|
}
|
||||||
|
export class SelectorsMap<T extends Node> implements LookupSorter {
|
||||||
|
private id: SelectorMap = {};
|
||||||
|
private class: SelectorMap = {};
|
||||||
|
private type: SelectorMap = {};
|
||||||
|
private universal: SelectorInDocument[] = [];
|
||||||
|
|
||||||
|
private position = 0;
|
||||||
|
|
||||||
|
constructor(rulesets: RuleSet[]) {
|
||||||
|
rulesets.forEach(rule => rule.lookupSort(this));
|
||||||
}
|
}
|
||||||
public matches(view: view.View): boolean {
|
|
||||||
let result = matchesType(this.expression, view);
|
query(node: T): SelectorsMatch<T> {
|
||||||
if (result && this.attrExpression) {
|
let selectorClasses = [
|
||||||
return matchesAttr(this.attrExpression, view);
|
this.universal,
|
||||||
|
this.id[node.id],
|
||||||
|
this.type[node.cssType]
|
||||||
|
];
|
||||||
|
if (node.cssClasses) {
|
||||||
|
node.cssClasses.forEach(c => selectorClasses.push(this.class[c]));
|
||||||
}
|
}
|
||||||
return result;
|
let selectors = selectorClasses
|
||||||
|
.filter(arr => !!arr)
|
||||||
|
.reduce((cur, next) => cur.concat(next), []);
|
||||||
|
|
||||||
|
let selectorsMatch = new SelectorsMatch<T>();
|
||||||
|
|
||||||
|
selectorsMatch.selectors = selectors
|
||||||
|
.filter(sel => sel.sel.accumulateChanges(node, selectorsMatch))
|
||||||
|
.sort((a, b) => a.sel.specificity - b.sel.specificity || a.pos - b.pos)
|
||||||
|
.map(docSel => docSel.sel);
|
||||||
|
|
||||||
|
return selectorsMatch;
|
||||||
}
|
}
|
||||||
public toString(): string {
|
|
||||||
return `CssTypeSelector ${this.expression}${this.attrExpressionText} { ${this.declarationText} }`;
|
sortById(id: string, sel: SelectorCore): void { this.addToMap(this.id, id, sel); }
|
||||||
|
sortByClass(cssClass: string, sel: SelectorCore): void {
|
||||||
|
this.addToMap(this.class, cssClass, sel);
|
||||||
|
}
|
||||||
|
sortByType(cssType: string, sel: SelectorCore): void {
|
||||||
|
this.addToMap(this.type, cssType, sel);
|
||||||
|
}
|
||||||
|
sortAsUniversal(sel: SelectorCore): void { this.universal.push(this.makeDocSelector(sel)); }
|
||||||
|
|
||||||
|
private addToMap(map: SelectorMap, head: string, sel: SelectorCore): void {
|
||||||
|
this.position++;
|
||||||
|
let list = map[head];
|
||||||
|
if (list) {
|
||||||
|
list.push(this.makeDocSelector(sel));
|
||||||
|
} else {
|
||||||
|
map[head] = [this.makeDocSelector(sel)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private makeDocSelector(sel: SelectorCore): SelectorInDocument {
|
||||||
|
return { sel, pos: this.position++ };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function matchesType(expression: string, view: view.View): boolean {
|
interface ChangeAccumulator {
|
||||||
let exprArr = expression.split(".");
|
addAttribute(node: Node, attribute: string): void;
|
||||||
let exprTypeName = exprArr[0];
|
addPseudoClass(node: Node, pseudoClass: string): void;
|
||||||
let exprClassName = exprArr[1];
|
|
||||||
|
|
||||||
let typeCheck = exprTypeName.toLowerCase() === view.typeName.toLowerCase() ||
|
|
||||||
exprTypeName.toLowerCase() === view.typeName.split(/(?=[A-Z])/).join("-").toLowerCase();
|
|
||||||
|
|
||||||
if (typeCheck) {
|
|
||||||
if (exprClassName) {
|
|
||||||
return view._cssClasses.some((cssClass, i, arr) => { return cssClass === exprClassName });
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return typeCheck;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class CssIdSelector extends CssSelector {
|
export class SelectorsMatch<T extends Node> implements ChangeAccumulator {
|
||||||
get specificity(): number {
|
public changeMap: ChangeMap<T> = new Map<T, Changes>();
|
||||||
return ID_SPECIFICITY;
|
public selectors;
|
||||||
}
|
|
||||||
public matches(view: view.View): boolean {
|
public addAttribute(node: T, attribute: string): void {
|
||||||
let result = this.expression === view.id;
|
let deps: Changes = this.properties(node);
|
||||||
if (result && this.attrExpression) {
|
if (!deps.attributes) {
|
||||||
return matchesAttr(this.attrExpression, view);
|
deps.attributes = new Set();
|
||||||
}
|
}
|
||||||
return result;
|
deps.attributes.add(attribute);
|
||||||
}
|
}
|
||||||
|
|
||||||
public toString(): string {
|
public addPseudoClass(node: T, pseudoClass: string): void {
|
||||||
return `CssIdSelector ${this.expression}${this.attrExpressionText} { ${this.declarationText} }`;
|
let deps: Changes = this.properties(node);
|
||||||
|
if (!deps.pseudoClasses) {
|
||||||
|
deps.pseudoClasses = new Set();
|
||||||
|
}
|
||||||
|
deps.pseudoClasses.add(pseudoClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
private properties(node: T): Changes {
|
||||||
|
let set = this.changeMap.get(node);
|
||||||
|
if (!set) {
|
||||||
|
this.changeMap.set(node, set = {});
|
||||||
|
}
|
||||||
|
return set;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class CssClassSelector extends CssSelector {
|
|
||||||
get specificity(): number {
|
|
||||||
return CLASS_SPECIFICITY;
|
|
||||||
}
|
|
||||||
public matches(view: view.View): boolean {
|
|
||||||
let expectedClass = this.expression;
|
|
||||||
let result = view._cssClasses.some((cssClass, i, arr) => { return cssClass === expectedClass });
|
|
||||||
if (result && this.attrExpression) {
|
|
||||||
return matchesAttr(this.attrExpression, view);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
public toString(): string {
|
|
||||||
return `CssClassSelector ${this.expression}${this.attrExpressionText} { ${this.declarationText} }`;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class CssCompositeSelector extends CssSelector {
|
|
||||||
get specificity(): number {
|
|
||||||
let result = 0;
|
|
||||||
for (let i = 0; i < this.parentCssSelectors.length; i++) {
|
|
||||||
result += this.parentCssSelectors[i].selector.specificity;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private parentCssSelectors: [{ selector: CssSelector, onlyDirectParent: boolean }];
|
|
||||||
|
|
||||||
private splitExpression(expression) {
|
|
||||||
let result = [];
|
|
||||||
let tempArr = [];
|
|
||||||
let validSpace = true;
|
|
||||||
for (let i = 0; i < expression.length; i++) {
|
|
||||||
if (expression[i] === LSBRACKET) {
|
|
||||||
validSpace = false;
|
|
||||||
}
|
|
||||||
if (expression[i] === RSBRACKET) {
|
|
||||||
validSpace = true;
|
|
||||||
}
|
|
||||||
if ((expression[i] === SPACE && validSpace) || (expression[i] === GTHAN)) {
|
|
||||||
if (tempArr.length > 0) {
|
|
||||||
result.push(tempArr.join(""));
|
|
||||||
tempArr = [];
|
|
||||||
}
|
|
||||||
if (expression[i] === GTHAN) {
|
|
||||||
result.push(GTHAN);
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
tempArr.push(expression[i]);
|
|
||||||
}
|
|
||||||
if (tempArr.length > 0) {
|
|
||||||
result.push(tempArr.join(""));
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(expr: string, declarations: cssParser.Declaration[]) {
|
|
||||||
super(expr, declarations);
|
|
||||||
let expressions = this.splitExpression(expr);
|
|
||||||
let onlyParent = false;
|
|
||||||
this.parentCssSelectors = <any>[];
|
|
||||||
for (let i = expressions.length - 1; i >= 0; i--) {
|
|
||||||
if (expressions[i].trim() === GTHAN) {
|
|
||||||
onlyParent = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
this.parentCssSelectors.push({ selector: createSelector(expressions[i].trim(), null), onlyDirectParent: onlyParent });
|
|
||||||
onlyParent = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public matches(view: view.View): boolean {
|
|
||||||
let result = this.parentCssSelectors[0].selector.matches(view);
|
|
||||||
if (!result) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
let tempView = view.parent;
|
|
||||||
for (let i = 1; i < this.parentCssSelectors.length; i++) {
|
|
||||||
let parentCounter = 0;
|
|
||||||
while (tempView && parentCounter === 0) {
|
|
||||||
result = this.parentCssSelectors[i].selector.matches(tempView);
|
|
||||||
if (result) {
|
|
||||||
tempView = tempView.parent;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (this.parentCssSelectors[i].onlyDirectParent) {
|
|
||||||
parentCounter++;
|
|
||||||
}
|
|
||||||
tempView = tempView.parent;
|
|
||||||
}
|
|
||||||
if (!result) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public toString(): string {
|
|
||||||
return `CssCompositeSelector ${this.expression}${this.attrExpressionText} { ${this.declarationText} }`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class CssAttrSelector extends CssSelector {
|
|
||||||
get specificity(): number {
|
|
||||||
return ATTR_SPECIFITY;
|
|
||||||
}
|
|
||||||
|
|
||||||
public matches(view: view.View): boolean {
|
|
||||||
return matchesAttr(this.attrExpression, view);
|
|
||||||
}
|
|
||||||
|
|
||||||
public toString(): string {
|
|
||||||
return `CssAttrSelector ${this.expression}${this.attrExpressionText} { ${this.declarationText} }`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function matchesAttr(attrExpression: string, view: view.View): boolean {
|
|
||||||
let equalSignIndex = attrExpression.indexOf(EQUAL);
|
|
||||||
if (equalSignIndex > 0) {
|
|
||||||
let nameValueRegex = /(.*[^~|\^\$\*])[~|\^\$\*]?=(.*)/;
|
|
||||||
let nameValueRegexRes = nameValueRegex.exec(attrExpression);
|
|
||||||
let attrName;
|
|
||||||
let attrValue;
|
|
||||||
if (nameValueRegexRes && nameValueRegexRes.length > 2) {
|
|
||||||
attrName = nameValueRegexRes[1].trim();
|
|
||||||
attrValue = nameValueRegexRes[2].trim().replace(/^(["'])*(.*)\1$/, '$2');
|
|
||||||
}
|
|
||||||
// extract entire sign (=, ~=, |=, ^=, $=, *=)
|
|
||||||
let escapedAttrValue = utils.escapeRegexSymbols(attrValue);
|
|
||||||
let attrCheckRegex;
|
|
||||||
switch (attrExpression.charAt(equalSignIndex - 1)) {
|
|
||||||
case "~":
|
|
||||||
attrCheckRegex = new RegExp("(^|[^a-zA-Z-])" + escapedAttrValue + "([^a-zA-Z-]|$)");
|
|
||||||
break;
|
|
||||||
case "|":
|
|
||||||
attrCheckRegex = new RegExp("^" + escapedAttrValue + "\\b");
|
|
||||||
break;
|
|
||||||
case "^":
|
|
||||||
attrCheckRegex = new RegExp("^" + escapedAttrValue);
|
|
||||||
break;
|
|
||||||
case "$":
|
|
||||||
attrCheckRegex = new RegExp(escapedAttrValue + "$");
|
|
||||||
break;
|
|
||||||
case "*":
|
|
||||||
attrCheckRegex = new RegExp(escapedAttrValue);
|
|
||||||
break;
|
|
||||||
|
|
||||||
// only = (EQUAL)
|
|
||||||
default:
|
|
||||||
attrCheckRegex = new RegExp("^" + escapedAttrValue + "$");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return !types.isNullOrUndefined(view[attrName]) && attrCheckRegex.test(view[attrName] + "");
|
|
||||||
} else {
|
|
||||||
return !types.isNullOrUndefined(view[attrExpression]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class CssVisualStateSelector extends CssSelector {
|
|
||||||
private _key: string;
|
|
||||||
private _match: string;
|
|
||||||
private _state: string;
|
|
||||||
private _isById: boolean;
|
|
||||||
private _isByClass: boolean;
|
|
||||||
private _isByType: boolean;
|
|
||||||
private _isByAttr: boolean;
|
|
||||||
|
|
||||||
get specificity(): number {
|
|
||||||
return (this._isById ? ID_SPECIFICITY : 0) +
|
|
||||||
(this._isByAttr ? ATTR_SPECIFITY : 0) +
|
|
||||||
(this._isByClass ? CLASS_SPECIFICITY : 0) +
|
|
||||||
(this._isByType ? TYPE_SPECIFICITY : 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
get key(): string {
|
|
||||||
return this._key;
|
|
||||||
}
|
|
||||||
|
|
||||||
get state(): string {
|
|
||||||
return this._state;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected get valueSourceModifier(): number {
|
|
||||||
return observable.ValueSource.VisualState;
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(expression: string, declarations: cssParser.Declaration[]) {
|
|
||||||
super(expression, declarations);
|
|
||||||
|
|
||||||
let args = expression.split(COLON);
|
|
||||||
this._key = args[0];
|
|
||||||
this._state = args[1];
|
|
||||||
|
|
||||||
if (this._key.charAt(0) === HASH) {
|
|
||||||
this._match = this._key.substring(1);
|
|
||||||
this._isById = true;
|
|
||||||
} else if (this._key.charAt(0) === DOT) {
|
|
||||||
this._match = this._key.substring(1);
|
|
||||||
this._isByClass = true;
|
|
||||||
} else if (this._key.charAt(0) === LSBRACKET) {
|
|
||||||
this._match = this._key;
|
|
||||||
this._isByAttr = true;
|
|
||||||
}
|
|
||||||
else if (this._key.length > 0) { // handle the case when there is no key. E.x. ":pressed" selector
|
|
||||||
this._match = this._key;
|
|
||||||
this._isByType = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public matches(view: view.View): boolean {
|
|
||||||
let matches = true;
|
|
||||||
if (this._isById) {
|
|
||||||
matches = this._match === view.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._isByClass) {
|
|
||||||
let expectedClass = this._match;
|
|
||||||
matches = view._cssClasses.some((cssClass, i, arr) => { return cssClass === expectedClass });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._isByType) {
|
|
||||||
matches = matchesType(this._match, view);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._isByAttr) {
|
|
||||||
matches = matchesAttr(this._key, view);
|
|
||||||
}
|
|
||||||
|
|
||||||
return matches;
|
|
||||||
}
|
|
||||||
|
|
||||||
public toString(): string {
|
|
||||||
return `CssVisualStateSelector ${this.expression}${this.attrExpressionText} { ${this.declarationText} }`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let HASH = "#";
|
|
||||||
let DOT = ".";
|
|
||||||
let COLON = ":";
|
|
||||||
let SPACE = " ";
|
|
||||||
let GTHAN = ">";
|
|
||||||
let LSBRACKET = "[";
|
|
||||||
let RSBRACKET = "]";
|
|
||||||
let EQUAL = "=";
|
|
||||||
|
|
||||||
export function createSelector(expression: string, declarations: cssParser.Declaration[]): CssSelector {
|
|
||||||
let goodExpr = expression.replace(/>/g, " > ").replace(/\s\s+/g, " ");
|
|
||||||
let spaceIndex = goodExpr.indexOf(SPACE);
|
|
||||||
if (spaceIndex >= 0) {
|
|
||||||
return new CssCompositeSelector(goodExpr, declarations);
|
|
||||||
}
|
|
||||||
|
|
||||||
let leftSquareBracketIndex = goodExpr.indexOf(LSBRACKET);
|
|
||||||
if (leftSquareBracketIndex === 0) {
|
|
||||||
return new CssAttrSelector(goodExpr, declarations);
|
|
||||||
}
|
|
||||||
|
|
||||||
var colonIndex = goodExpr.indexOf(COLON);
|
|
||||||
if (colonIndex >= 0) {
|
|
||||||
return new CssVisualStateSelector(goodExpr, declarations);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (goodExpr.charAt(0) === HASH) {
|
|
||||||
return new CssIdSelector(goodExpr.substring(1), declarations);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (goodExpr.charAt(0) === DOT) {
|
|
||||||
// TODO: Combinations like label.center
|
|
||||||
return new CssClassSelector(goodExpr.substring(1), declarations);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new CssTypeSelector(goodExpr, declarations);
|
|
||||||
}
|
|
||||||
|
|
||||||
class InlineStyleSelector extends CssSelector {
|
|
||||||
constructor(declarations: cssParser.Declaration[]) {
|
|
||||||
super(undefined, declarations)
|
|
||||||
}
|
|
||||||
|
|
||||||
public apply(view: view.View) {
|
|
||||||
this.eachSetter((property, value) => {
|
|
||||||
const resolvedProperty = <StyleProperty>property;
|
|
||||||
view.style._setValue(resolvedProperty, value, observable.ValueSource.Local);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public toString(): string {
|
|
||||||
return `InlineStyleSelector ${this.expression}${this.attrExpressionText} { ${this.declarationText} }`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function applyInlineSyle(view: view.View, declarations: cssParser.Declaration[]) {
|
|
||||||
let localStyleSelector = new InlineStyleSelector(declarations);
|
|
||||||
localStyleSelector.apply(view);
|
|
||||||
}
|
|
||||||
|
26
tns-core-modules/ui/styling/style-scope.d.ts
vendored
26
tns-core-modules/ui/styling/style-scope.d.ts
vendored
@ -1,23 +1,35 @@
|
|||||||
//@private
|
//@private
|
||||||
declare module "ui/styling/style-scope" {
|
declare module "ui/styling/style-scope" {
|
||||||
import view = require("ui/core/view");
|
import view = require("ui/core/view");
|
||||||
import cssSelector = require("ui/styling/css-selector");
|
|
||||||
import cssParser = require("css");
|
import cssParser = require("css");
|
||||||
import keyframeAnimation = require("ui/animation/keyframe-animation");
|
import {RuleSet, Node, SelectorCore, ChangeMap} from "ui/styling/css-selector";
|
||||||
|
import {KeyframeAnimationInfo} from "ui/animation/keyframe-animation";
|
||||||
|
|
||||||
|
export class CssState {
|
||||||
|
/**
|
||||||
|
* Re-evaluate the selectors and apply any changes to the underlying view.
|
||||||
|
*/
|
||||||
|
public apply(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the static selectors that match the view and the dynamic selectors that may potentially match the view.
|
||||||
|
*/
|
||||||
|
public changeMap: ChangeMap<view.View>;
|
||||||
|
}
|
||||||
|
|
||||||
export class StyleScope {
|
export class StyleScope {
|
||||||
public css: string;
|
public css: string;
|
||||||
public addCss(cssString: string, cssFileName: string): void;
|
public addCss(cssString: string, cssFileName: string): void;
|
||||||
|
|
||||||
public static createSelectorsFromCss(css: string, cssFileName: string, keyframes: Object): cssSelector.CssSelector[];
|
public static createSelectorsFromCss(css: string, cssFileName: string, keyframes: Object): RuleSet[];
|
||||||
public static createSelectorsFromImports(tree: cssParser.SyntaxTree, keyframes: Object): cssSelector.CssSelector[];
|
public static createSelectorsFromImports(tree: cssParser.SyntaxTree, keyframes: Object): RuleSet[];
|
||||||
public ensureSelectors(): boolean;
|
public ensureSelectors(): boolean;
|
||||||
|
|
||||||
public applySelectors(view: view.View): void
|
public applySelectors(view: view.View): void
|
||||||
public getVisualStates(view: view.View): Object;
|
public query(options: Node): SelectorCore[];
|
||||||
|
|
||||||
public removeSelectors(selectorExpression: string);
|
public getKeyframeAnimationWithName(animationName: string): KeyframeAnimationInfo;
|
||||||
public getKeyframeAnimationWithName(animationName: string): keyframeAnimation.KeyframeAnimationInfo;
|
public getAnimations(ruleset: RuleSet): KeyframeAnimationInfo[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function applyInlineSyle(view: view.View, style: string): void;
|
export function applyInlineSyle(view: view.View, style: string): void;
|
||||||
|
@ -6,11 +6,18 @@ import application = require("application");
|
|||||||
import * as typesModule from "utils/types";
|
import * as typesModule from "utils/types";
|
||||||
import * as utilsModule from "utils/utils";
|
import * as utilsModule from "utils/utils";
|
||||||
import * as fileSystemModule from "file-system";
|
import * as fileSystemModule from "file-system";
|
||||||
import * as visualStateModule from "./visual-state";
|
|
||||||
import keyframeAnimation = require("ui/animation/keyframe-animation");
|
import keyframeAnimation = require("ui/animation/keyframe-animation");
|
||||||
import cssAnimationParser = require("./css-animation-parser");
|
import cssAnimationParser = require("./css-animation-parser");
|
||||||
import observable = require("ui/core/dependency-observable");
|
import observable = require("ui/core/dependency-observable");
|
||||||
|
|
||||||
|
import {convertString} from "utils/utils";
|
||||||
|
import {RuleSet, SelectorsMap, SelectorCore, SelectorsMatch, ChangeMap } from "ui/styling/css-selector";
|
||||||
|
import {StyleProperty, withStyleProperty} from "ui/styling/style-property";
|
||||||
|
import {getSpecialPropertySetter} from "ui/builder/special-properties";
|
||||||
|
|
||||||
|
const animationsSymbol: symbol = Symbol("animations");
|
||||||
|
|
||||||
var types: typeof typesModule;
|
var types: typeof typesModule;
|
||||||
function ensureTypes() {
|
function ensureTypes() {
|
||||||
if (!types) {
|
if (!types) {
|
||||||
@ -32,24 +39,35 @@ function ensureFS() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var vs: typeof visualStateModule;
|
var pattern: RegExp = /('|")(.*?)\1/;
|
||||||
function ensureVisualState() {
|
|
||||||
if (!vs) {
|
export class CssState {
|
||||||
vs = require("./visual-state");
|
constructor(private view: view.View, private match: SelectorsMatch<view.View>) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public get changeMap(): ChangeMap<view.View> {
|
||||||
|
return this.match.changeMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
public apply(): void {
|
||||||
|
this.view.style._resetCssValues();
|
||||||
|
let matchingSelectors = this.match.selectors.filter(sel => sel.dynamic ? sel.match(this.view) : true);
|
||||||
|
matchingSelectors.forEach(s => applyDescriptors(this.view, s.ruleset));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var pattern: RegExp = /('|")(.*?)\1/;
|
|
||||||
|
|
||||||
export class StyleScope {
|
export class StyleScope {
|
||||||
|
|
||||||
|
private _selectors: SelectorsMap;
|
||||||
|
|
||||||
// caches all the visual states by the key of the visual state selectors
|
// caches all the visual states by the key of the visual state selectors
|
||||||
private _statesByKey = {};
|
private _statesByKey = {};
|
||||||
private _viewIdToKey = {};
|
private _viewIdToKey = {};
|
||||||
|
|
||||||
private _css: string;
|
private _css: string;
|
||||||
private _cssFileName: string;
|
private _cssFileName: string;
|
||||||
private _mergedCssSelectors: Array<cssSelector.CssSelector>;
|
private _mergedCssSelectors: cssSelector.RuleSet[];
|
||||||
private _localCssSelectors: Array<cssSelector.CssSelector> = [];
|
private _localCssSelectors: cssSelector.RuleSet[] = [];
|
||||||
private _localCssSelectorVersion: number = 0;
|
private _localCssSelectorVersion: number = 0;
|
||||||
private _localCssSelectorsAppliedVersion: number = 0;
|
private _localCssSelectorsAppliedVersion: number = 0;
|
||||||
private _applicationCssSelectorsAppliedVersion: number = 0;
|
private _applicationCssSelectorsAppliedVersion: number = 0;
|
||||||
@ -77,6 +95,7 @@ export class StyleScope {
|
|||||||
this._reset();
|
this._reset();
|
||||||
|
|
||||||
const parsedSelectors = StyleScope.createSelectorsFromCss(this._css, cssFileName, this._keyframes);
|
const parsedSelectors = StyleScope.createSelectorsFromCss(this._css, cssFileName, this._keyframes);
|
||||||
|
|
||||||
if (append) {
|
if (append) {
|
||||||
this._localCssSelectors.push.apply(this._localCssSelectors, parsedSelectors);
|
this._localCssSelectors.push.apply(this._localCssSelectors, parsedSelectors);
|
||||||
} else {
|
} else {
|
||||||
@ -87,15 +106,6 @@ export class StyleScope {
|
|||||||
this.ensureSelectors();
|
this.ensureSelectors();
|
||||||
}
|
}
|
||||||
|
|
||||||
public removeSelectors(selectorExpression: string) {
|
|
||||||
for (let i = this._mergedCssSelectors.length - 1; i >= 0; i--) {
|
|
||||||
let selector = this._mergedCssSelectors[i];
|
|
||||||
if (selector.expression === selectorExpression) {
|
|
||||||
this._mergedCssSelectors.splice(i, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public getKeyframeAnimationWithName(animationName: string): keyframeAnimation.KeyframeAnimationInfo {
|
public getKeyframeAnimationWithName(animationName: string): keyframeAnimation.KeyframeAnimationInfo {
|
||||||
let keyframes = this._keyframes[animationName];
|
let keyframes = this._keyframes[animationName];
|
||||||
if (keyframes !== undefined) {
|
if (keyframes !== undefined) {
|
||||||
@ -106,13 +116,13 @@ export class StyleScope {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static createSelectorsFromCss(css: string, cssFileName: string, keyframes: Object): cssSelector.CssSelector[] {
|
public static createSelectorsFromCss(css: string, cssFileName: string, keyframes: Object): cssSelector.RuleSet[] {
|
||||||
try {
|
try {
|
||||||
let pageCssSyntaxTree = css ? cssParser.parse(css, { source: cssFileName }) : null;
|
let pageCssSyntaxTree = css ? cssParser.parse(css, { source: cssFileName }) : null;
|
||||||
let pageCssSelectors = new Array<cssSelector.CssSelector>();
|
let pageCssSelectors: cssSelector.RuleSet[] = [];
|
||||||
if (pageCssSyntaxTree) {
|
if (pageCssSyntaxTree) {
|
||||||
pageCssSelectors = StyleScope._joinCssSelectorsArrays([pageCssSelectors, StyleScope.createSelectorsFromImports(pageCssSyntaxTree, keyframes)]);
|
pageCssSelectors = pageCssSelectors.concat(StyleScope.createSelectorsFromImports(pageCssSyntaxTree, keyframes));
|
||||||
pageCssSelectors = StyleScope._joinCssSelectorsArrays([pageCssSelectors, StyleScope.createSelectorsFromSyntaxTree(pageCssSyntaxTree, keyframes)]);
|
pageCssSelectors = pageCssSelectors.concat(StyleScope.createSelectorsFromSyntaxTree(pageCssSyntaxTree, keyframes));
|
||||||
}
|
}
|
||||||
return pageCssSelectors;
|
return pageCssSelectors;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -120,12 +130,12 @@ export class StyleScope {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static createSelectorsFromImports(tree: cssParser.SyntaxTree, keyframes: Object): cssSelector.CssSelector[] {
|
public static createSelectorsFromImports(tree: cssParser.SyntaxTree, keyframes: Object): cssSelector.RuleSet[] {
|
||||||
let selectors = new Array<cssSelector.CssSelector>();
|
let selectors: cssSelector.RuleSet[] = [];
|
||||||
ensureTypes();
|
ensureTypes();
|
||||||
|
|
||||||
if (!types.isNullOrUndefined(tree)) {
|
if (!types.isNullOrUndefined(tree)) {
|
||||||
let imports = tree["stylesheet"]["rules"].filter(r=> r.type === "import");
|
let imports = tree["stylesheet"]["rules"].filter(r => r.type === "import");
|
||||||
|
|
||||||
for (let i = 0; i < imports.length; i++) {
|
for (let i = 0; i < imports.length; i++) {
|
||||||
let importItem = imports[i]["import"];
|
let importItem = imports[i]["import"];
|
||||||
@ -148,7 +158,7 @@ export class StyleScope {
|
|||||||
let file = fs.File.fromPath(fileName);
|
let file = fs.File.fromPath(fileName);
|
||||||
let text = file.readTextSync();
|
let text = file.readTextSync();
|
||||||
if (text) {
|
if (text) {
|
||||||
selectors = StyleScope._joinCssSelectorsArrays([selectors, StyleScope.createSelectorsFromCss(text, fileName, keyframes)]);
|
selectors = selectors.concat(StyleScope.createSelectorsFromCss(text, fileName, keyframes));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -163,8 +173,8 @@ export class StyleScope {
|
|||||||
let toMerge = []
|
let toMerge = []
|
||||||
if ((this._applicationCssSelectorsAppliedVersion !== application.cssSelectorVersion) ||
|
if ((this._applicationCssSelectorsAppliedVersion !== application.cssSelectorVersion) ||
|
||||||
(this._localCssSelectorVersion !== this._localCssSelectorsAppliedVersion) ||
|
(this._localCssSelectorVersion !== this._localCssSelectorsAppliedVersion) ||
|
||||||
|
|
||||||
(!this._mergedCssSelectors)) {
|
(!this._mergedCssSelectors)) {
|
||||||
|
|
||||||
toMerge.push(application.cssSelectors);
|
toMerge.push(application.cssSelectors);
|
||||||
this._applicationCssSelectorsAppliedVersion = application.cssSelectorVersion;
|
this._applicationCssSelectorsAppliedVersion = application.cssSelectorVersion;
|
||||||
toMerge.push(this._localCssSelectors);
|
toMerge.push(this._localCssSelectors);
|
||||||
@ -175,139 +185,41 @@ export class StyleScope {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (toMerge.length > 0) {
|
if (toMerge.length > 0) {
|
||||||
this._mergedCssSelectors = StyleScope._joinCssSelectorsArrays(toMerge);
|
this._mergedCssSelectors = toMerge.filter(m => !!m).reduce((merged, next) => merged.concat(next), []);
|
||||||
this._applyKeyframesOnSelectors();
|
this._applyKeyframesOnSelectors();
|
||||||
return true;
|
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._selectors = new SelectorsMap(this._mergedCssSelectors);
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static _joinCssSelectorsArrays(arrays: Array<Array<cssSelector.CssSelector>>): Array<cssSelector.CssSelector> {
|
public applySelectors(view: view.View): void {
|
||||||
let mergedResult = [];
|
this.ensureSelectors();
|
||||||
let i;
|
|
||||||
for (i = 0; i < arrays.length; i++) {
|
|
||||||
if (arrays[i]) {
|
|
||||||
mergedResult.push.apply(mergedResult, arrays[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ensureUtils();
|
|
||||||
mergedResult = utils.mergeSort(mergedResult, (a, b) => { return a.specificity - b.specificity; });
|
|
||||||
|
|
||||||
return mergedResult;
|
let state = this._selectors.query(view);
|
||||||
|
|
||||||
|
let previousState = view._cssState;
|
||||||
|
let nextState = new CssState(view, state);
|
||||||
|
view._cssState = nextState;
|
||||||
|
view._onCssStateChange(previousState, nextState);
|
||||||
}
|
}
|
||||||
|
|
||||||
public applySelectors(view: view.View) {
|
public query(node: Node): SelectorCore[] {
|
||||||
this.ensureSelectors();
|
this.ensureSelectors();
|
||||||
|
return this._selectors.query(node).selectors;
|
||||||
view.style._beginUpdate();
|
|
||||||
let i;
|
|
||||||
let selector: cssSelector.CssSelector;
|
|
||||||
let matchedStateSelectors = new Array<cssSelector.CssVisualStateSelector>();
|
|
||||||
|
|
||||||
// Go trough all selectors - and directly apply all non-state selectors
|
|
||||||
for (i = 0; i < this._mergedCssSelectors.length; i++) {
|
|
||||||
selector = this._mergedCssSelectors[i];
|
|
||||||
if (selector.matches(view)) {
|
|
||||||
if (selector instanceof cssSelector.CssVisualStateSelector) {
|
|
||||||
matchedStateSelectors.push(<cssSelector.CssVisualStateSelector>selector);
|
|
||||||
} else {
|
|
||||||
selector.apply(view, observable.ValueSource.Css);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (matchedStateSelectors.length > 0) {
|
|
||||||
// Create a key for all matched selectors for this element
|
|
||||||
let key: string = "";
|
|
||||||
matchedStateSelectors.forEach((s) => key += s.key + "|");
|
|
||||||
|
|
||||||
// Associate the view to the created key
|
|
||||||
this._viewIdToKey[view._domId] = key;
|
|
||||||
|
|
||||||
// Create visual states for this key if there aren't already created
|
|
||||||
if (!this._statesByKey[key]) {
|
|
||||||
this._createVisualsStatesForSelectors(key, matchedStateSelectors);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
view.style._endUpdate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getVisualStates(view: view.View): Object {
|
private static createSelectorsFromSyntaxTree(ast: cssParser.SyntaxTree, keyframes: Object): RuleSet[] {
|
||||||
let key = this._viewIdToKey[view._domId];
|
let nodes = ast.stylesheet.rules;
|
||||||
if (key === undefined) {
|
nodes.filter(isKeyframe).forEach(node => keyframes[node.name] = node);
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this._statesByKey[key];
|
let rulesets = cssSelector.fromAstNodes(nodes);
|
||||||
}
|
rulesets.forEach(rule => rule[animationsSymbol] = cssAnimationParser.CssAnimationParser.keyframeAnimationsFromCSSDeclarations(rule.declarations));
|
||||||
|
|
||||||
private _createVisualsStatesForSelectors(key: string, matchedStateSelectors: Array<cssSelector.CssVisualStateSelector>) {
|
return rulesets;
|
||||||
let i,
|
|
||||||
allStates = {},
|
|
||||||
stateSelector: cssSelector.CssVisualStateSelector;
|
|
||||||
|
|
||||||
this._statesByKey[key] = allStates;
|
|
||||||
ensureVisualState();
|
|
||||||
|
|
||||||
for (i = 0; i < matchedStateSelectors.length; i++) {
|
|
||||||
stateSelector = matchedStateSelectors[i];
|
|
||||||
|
|
||||||
let visualState = allStates[stateSelector.state];
|
|
||||||
if (!visualState) {
|
|
||||||
visualState = new vs.VisualState();
|
|
||||||
allStates[stateSelector.state] = visualState;
|
|
||||||
}
|
|
||||||
|
|
||||||
// add all stateSelectors instead of adding setters
|
|
||||||
if (stateSelector.animations && stateSelector.animations.length > 0) {
|
|
||||||
visualState.animatedSelectors.push(stateSelector);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
stateSelector.eachSetter((property, value) => {
|
|
||||||
visualState.setters[property.name] = value;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static createSelectorsFromSyntaxTree(ast: cssParser.SyntaxTree, keyframes: Object): Array<cssSelector.CssSelector> {
|
|
||||||
let result: Array<cssSelector.CssSelector> = [];
|
|
||||||
let rules = ast.stylesheet.rules;
|
|
||||||
let rule: cssParser.Rule;
|
|
||||||
let i;
|
|
||||||
let j;
|
|
||||||
|
|
||||||
// Create selectors form AST
|
|
||||||
for (i = 0; i < rules.length; i++) {
|
|
||||||
rule = rules[i];
|
|
||||||
// Skip comment nodes.
|
|
||||||
if (rule.type === "rule") {
|
|
||||||
|
|
||||||
// Filter comment nodes.
|
|
||||||
let filteredDeclarations = [];
|
|
||||||
if (rule.declarations) {
|
|
||||||
for (j = 0; j < rule.declarations.length; j++) {
|
|
||||||
let declaration = rule.declarations[j];
|
|
||||||
if (declaration.type === "declaration") {
|
|
||||||
filteredDeclarations.push({
|
|
||||||
property: declaration.property.toLowerCase(),
|
|
||||||
value: declaration.value
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (j = 0; j < rule.selectors.length; j++) {
|
|
||||||
result.push(cssSelector.createSelector(rule.selectors[j], filteredDeclarations));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (rule.type === "keyframes") {
|
|
||||||
keyframes[(<any>rule).name] = rule;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _reset() {
|
private _reset() {
|
||||||
@ -317,9 +229,10 @@ export class StyleScope {
|
|||||||
|
|
||||||
private _applyKeyframesOnSelectors() {
|
private _applyKeyframesOnSelectors() {
|
||||||
for (let i = this._mergedCssSelectors.length - 1; i >= 0; i--) {
|
for (let i = this._mergedCssSelectors.length - 1; i >= 0; i--) {
|
||||||
let selector = this._mergedCssSelectors[i];
|
let ruleset = this._mergedCssSelectors[i];
|
||||||
if (selector.animations !== undefined) {
|
let animations = ruleset[animationsSymbol];
|
||||||
for (let animation of selector.animations) {
|
if (animations !== undefined) {
|
||||||
|
for (let animation of animations) {
|
||||||
let keyframe = this._keyframes[animation.name];
|
let keyframe = this._keyframes[animation.name];
|
||||||
if (keyframe !== undefined) {
|
if (keyframe !== undefined) {
|
||||||
animation.keyframes = cssAnimationParser.CssAnimationParser.keyframesArrayFromCSS(keyframe);
|
animation.keyframes = cssAnimationParser.CssAnimationParser.keyframesArrayFromCSS(keyframe);
|
||||||
@ -328,14 +241,78 @@ export class StyleScope {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getAnimations(ruleset: RuleSet): keyframeAnimation.KeyframeAnimationInfo[] {
|
||||||
|
return ruleset[animationsSymbol];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function applyInlineSyle(view: view.View, style: string) {
|
export function applyInlineSyle(view: view.View, style: string) {
|
||||||
try {
|
try {
|
||||||
let syntaxTree = cssParser.parse("local { " + style + " }", undefined);
|
let syntaxTree = cssParser.parse("local { " + style + " }", undefined);
|
||||||
let filteredDeclarations = syntaxTree.stylesheet.rules[0].declarations.filter((val, i, arr) => { return val.type === "declaration" });
|
let filteredDeclarations = syntaxTree.stylesheet.rules.filter(isRule)[0].declarations.filter(isDeclaration);
|
||||||
cssSelector.applyInlineSyle(view, filteredDeclarations);
|
applyInlineStyle(view, filteredDeclarations);
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
trace.write("Applying local style failed: " + ex, trace.categories.Error, trace.messageType.error);
|
trace.write("Applying local style failed: " + ex, trace.categories.Error, trace.messageType.error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isRule(node: cssParser.Node): node is cssParser.Rule {
|
||||||
|
return node.type === "rule";
|
||||||
|
}
|
||||||
|
function isDeclaration(node: cssParser.Node): node is cssParser.Declaration {
|
||||||
|
return node.type === "declaration";
|
||||||
|
}
|
||||||
|
function isKeyframe(node: cssParser.Node): node is cssParser.Keyframes {
|
||||||
|
return node.type === "keyframes";
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyDescriptors(view: view.View, ruleset: RuleSet): void {
|
||||||
|
let modifier = observable.ValueSource.Css;
|
||||||
|
|
||||||
|
ruleset.declarations.forEach(d => withStyleProperty(d.property, d.value, (property, value) => {
|
||||||
|
if (types.isString(property)) {
|
||||||
|
const propertyName = <string>property;
|
||||||
|
let attrHandled = false;
|
||||||
|
let specialSetter = getSpecialPropertySetter(propertyName);
|
||||||
|
|
||||||
|
if (!attrHandled && specialSetter) {
|
||||||
|
specialSetter(view, value);
|
||||||
|
attrHandled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!attrHandled && propertyName in view) {
|
||||||
|
view[propertyName] = convertString(value);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const resolvedProperty = <StyleProperty>property;
|
||||||
|
try {
|
||||||
|
view.style._setValue(resolvedProperty, value, modifier);
|
||||||
|
} catch (ex) {
|
||||||
|
if (trace.enabled) {
|
||||||
|
trace.write("Error setting property: " + resolvedProperty.name + " view: " + view + " value: " + value + " " + ex, trace.categories.Style, trace.messageType.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
let ruleAnimations: keyframeAnimation.KeyframeAnimationInfo[] = ruleset[animationsSymbol];
|
||||||
|
if (ruleAnimations && view.isLoaded && view._nativeView !== undefined) {
|
||||||
|
for (let animationInfo of ruleAnimations) {
|
||||||
|
let animation = keyframeAnimation.KeyframeAnimation.keyframeAnimationFromInfo(animationInfo, modifier);
|
||||||
|
if (animation) {
|
||||||
|
view._registerAnimation(animation);
|
||||||
|
animation.play(view)
|
||||||
|
.then(() => { view._unregisterAnimation(animation); })
|
||||||
|
.catch((e) => { view._unregisterAnimation(animation); });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyInlineStyle(view: view.View, declarations: cssSelector.Declaration[]): void {
|
||||||
|
declarations.forEach(d => withStyleProperty(d.property, d.value, (property, value) => {
|
||||||
|
const resolvedProperty = <StyleProperty>property;
|
||||||
|
view.style._setValue(resolvedProperty, value, observable.ValueSource.Local);
|
||||||
|
}));
|
||||||
|
}
|
20
tns-core-modules/ui/styling/styling.d.ts
vendored
20
tns-core-modules/ui/styling/styling.d.ts
vendored
@ -307,24 +307,4 @@
|
|||||||
*/
|
*/
|
||||||
export function visibilityConverter(cssValue: any): number;
|
export function visibilityConverter(cssValue: any): number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Encapsulates visual states names.
|
|
||||||
*/
|
|
||||||
module visualStates {
|
|
||||||
/**
|
|
||||||
* The normal visual state.
|
|
||||||
*/
|
|
||||||
export var Normal: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The hovered visual state.
|
|
||||||
*/
|
|
||||||
export var Hovered: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The pressed visual state.
|
|
||||||
*/
|
|
||||||
export var Pressed: string;
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -1,6 +1,5 @@
|
|||||||
import styleModule = require("./style");
|
import styleModule = require("./style");
|
||||||
import stylePropertyModule = require("./style-property");
|
import stylePropertyModule = require("./style-property");
|
||||||
import visualStateConstatnsModule = require("./visual-state-constants");
|
|
||||||
import convertersModule = require("./converters");
|
import convertersModule = require("./converters");
|
||||||
|
|
||||||
// Exports form style-property module.
|
// Exports form style-property module.
|
||||||
@ -30,10 +29,3 @@ export module converters {
|
|||||||
export var visibilityConverter = convertersModule.visibilityConverter;
|
export var visibilityConverter = convertersModule.visibilityConverter;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Exports form visual states module
|
|
||||||
export module visualStates {
|
|
||||||
export var Normal = visualStateConstatnsModule.Normal;
|
|
||||||
export var Hovered = visualStateConstatnsModule.Hovered;
|
|
||||||
export var Pressed = visualStateConstatnsModule.Pressed;
|
|
||||||
};
|
|
@ -1,16 +0,0 @@
|
|||||||
declare module "ui/styling/visual-state-constants" {
|
|
||||||
/**
|
|
||||||
* Denotes Normal state of an UIControl.
|
|
||||||
*/
|
|
||||||
export var Normal: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Denotes Hovered state of an UIControl.
|
|
||||||
*/
|
|
||||||
export var Hovered: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Denotes Pressed state of an UIControl.
|
|
||||||
*/
|
|
||||||
export var Pressed: string;
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
export var Normal = "normal";
|
|
||||||
export var Hovered = "hovered";
|
|
||||||
export var Pressed = "pressed";
|
|
@ -1,119 +0,0 @@
|
|||||||
import viewModule = require("ui/core/view");
|
|
||||||
import observable = require("ui/core/dependency-observable");
|
|
||||||
import styleProperty = require("ui/styling/style-property");
|
|
||||||
import cssSelector = require("ui/styling/css-selector");
|
|
||||||
import * as visualStateConstants from "ui/styling/visual-state-constants";
|
|
||||||
|
|
||||||
export class VisualState {
|
|
||||||
private _setters: {}; // use css selector instead
|
|
||||||
private _animatedSelectors: cssSelector.CssVisualStateSelector[];
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this._setters = {};
|
|
||||||
this._animatedSelectors = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
get setters(): {} {
|
|
||||||
return this._setters;
|
|
||||||
}
|
|
||||||
|
|
||||||
get animatedSelectors(): cssSelector.CssVisualStateSelector[] {
|
|
||||||
return this._animatedSelectors;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export function goToState(view: viewModule.View, state: string): string {
|
|
||||||
let root = <any>view.page;
|
|
||||||
if (!root) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
//TODO: this of optimization
|
|
||||||
let allStates = root._getStyleScope().getVisualStates(view);
|
|
||||||
if (!allStates) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
// logic here is:
|
|
||||||
// 1. Verify the new state, whether we have setters for it and rollback to Normal(default) if not.
|
|
||||||
// 2. If the new visual state is the same as the current one, do nothing
|
|
||||||
// 3. Else, remove the visual state value for all the properties from the current state that are not present in the new one
|
|
||||||
// 4. Apply all the properties from the new state.
|
|
||||||
|
|
||||||
// Step 1
|
|
||||||
if (!(state in allStates)) {
|
|
||||||
// TODO: Directly go to normal?
|
|
||||||
state = visualStateConstants.Normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 2
|
|
||||||
if (state !== view.visualState) {
|
|
||||||
let newState: VisualState = allStates[state];
|
|
||||||
let oldState: VisualState = allStates[view.visualState];
|
|
||||||
|
|
||||||
// Step 3
|
|
||||||
resetProperties(view, oldState, newState);
|
|
||||||
|
|
||||||
// Step 4
|
|
||||||
applyProperties(view, newState);
|
|
||||||
}
|
|
||||||
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetProperties(view: viewModule.View, oldState: VisualState, newState: VisualState) {
|
|
||||||
if (!oldState) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let property: styleProperty.Property;
|
|
||||||
|
|
||||||
for (let name in oldState.setters) {
|
|
||||||
if (newState && (name in newState.setters)) {
|
|
||||||
// Property will be altered by the new state, no need to reset it.
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
property = styleProperty.getPropertyByName(name);
|
|
||||||
if (property) {
|
|
||||||
view.style._resetValue(property, observable.ValueSource.VisualState);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
view._unregisterAllAnimations();
|
|
||||||
|
|
||||||
for (let selector of oldState.animatedSelectors) {
|
|
||||||
for (let animationInfo of selector.animations) {
|
|
||||||
for (let keyframe of animationInfo.keyframes) {
|
|
||||||
for (let declaration of keyframe.declarations) {
|
|
||||||
property = styleProperty.getPropertyByName(declaration.property);
|
|
||||||
if (property) {
|
|
||||||
view.style._resetValue(property, observable.ValueSource.VisualState);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyProperties(view: viewModule.View, state: VisualState) {
|
|
||||||
if (!state) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let property: styleProperty.Property;
|
|
||||||
|
|
||||||
for (let name in state.setters) {
|
|
||||||
property = styleProperty.getPropertyByName(name);
|
|
||||||
if (property) {
|
|
||||||
view.style._setValue(property, state.setters[name], observable.ValueSource.VisualState);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let selector of state.animatedSelectors) {
|
|
||||||
selector.apply(view, observable.ValueSource.VisualState);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,5 +1,4 @@
|
|||||||
{
|
{
|
||||||
"version": "1.6.2",
|
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"noEmitOnError": true,
|
"noEmitOnError": true,
|
||||||
"noEmitHelpers": true,
|
"noEmitHelpers": true,
|
||||||
|
Reference in New Issue
Block a user