diff --git a/apps/app/ui-tests-app/css/combinators.css b/apps/app/ui-tests-app/css/combinators.css
new file mode 100644
index 000000000..a1beb6d33
--- /dev/null
+++ b/apps/app/ui-tests-app/css/combinators.css
@@ -0,0 +1,36 @@
+.direct-child--type > Button {
+ background-color: red;
+ color: white;
+}
+
+.direct-child--class > .test-child {
+ background-color: blue;
+ color: white;
+}
+
+.direct-sibling--type Button + Label {
+ background-color: green;
+ color: white;
+}
+
+.direct-sibling--id #test-child + #test-child-2 {
+ background-color: pink;
+}
+
+.direct-sibling--class .test-child + .test-child-2 {
+ background-color: yellow;
+}
+
+.direct-sibling--attribute Button[data="test-child"] + Button[data="test-child-2"] {
+ background-color: blueviolet;
+ color: white;
+}
+
+.direct-sibling--pseudo-selector Button + Button:disabled {
+ background-color: black;
+ color: white;
+}
+
+.sibling-test-label {
+ text-align: center;
+}
diff --git a/apps/app/ui-tests-app/css/combinators.xml b/apps/app/ui-tests-app/css/combinators.xml
new file mode 100644
index 000000000..b7ee5b367
--- /dev/null
+++ b/apps/app/ui-tests-app/css/combinators.xml
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/app/ui-tests-app/css/main-page.ts b/apps/app/ui-tests-app/css/main-page.ts
index 9f7d83598..24dc260e1 100644
--- a/apps/app/ui-tests-app/css/main-page.ts
+++ b/apps/app/ui-tests-app/css/main-page.ts
@@ -45,6 +45,7 @@ export function pageLoaded(args: EventData) {
examples.set("border-playground", "css/border-playground");
examples.set("textview-hint-color", "css/textview-hint-color");
examples.set("hint-text-color", "css/hint-text-color");
+ examples.set("combinators", "css/combinators");
let viewModel = new SubMainPageViewModel(wrapLayout, examples);
page.bindingContext = viewModel;
diff --git a/tests/app/ui/styling/css-selector.ts b/tests/app/ui/styling/css-selector.ts
index e63ee2909..c5f4467b8 100644
--- a/tests/app/ui/styling/css-selector.ts
+++ b/tests/app/ui/styling/css-selector.ts
@@ -232,3 +232,38 @@ export function test_query_match_one_child_group() {
let expected = new Map().set(prod, { attributes: new Set(["special"])} );
TKUnit.assertDeepEqual(match.changeMap, expected);
}
+
+export function test_query_match_one_sibling_group() {
+ let {map} = create(`list button:highlighted+button:disabled { color: red; }`);
+ let list, button, disabledButton;
+
+ list = {
+ cssType: "list",
+ toString,
+ getChildIndex: () => 1,
+ getChildAt: () => button
+ };
+
+ button = {
+ cssType: "button",
+ cssPseudoClasses: new Set(["highlighted"]),
+ toString,
+ parent: list
+ };
+
+ disabledButton = {
+ cssType: "button",
+ cssPseudoClasses: new Set(["disabled"]),
+ toString,
+ parent: list
+ };
+
+ let match = map.query(disabledButton);
+ TKUnit.assertEqual(match.selectors.length, 1, "Expected match to have one selector.");
+
+ let expected = new Map()
+ .set(button, { pseudoClasses: new Set(["highlighted"]) })
+ .set(disabledButton, { pseudoClasses: new Set(["disabled"]) });
+
+ TKUnit.assertDeepEqual(match.changeMap, expected);
+}
diff --git a/tns-core-modules/ui/styling/css-selector/css-selector.d.ts b/tns-core-modules/ui/styling/css-selector/css-selector.d.ts
index 994bb7226..1ab340549 100644
--- a/tns-core-modules/ui/styling/css-selector/css-selector.d.ts
+++ b/tns-core-modules/ui/styling/css-selector/css-selector.d.ts
@@ -15,6 +15,8 @@ export interface Node {
cssType?: string;
cssClasses?: Set;
cssPseudoClasses?: Set;
+ getChildIndex?(node: Node): number
+ getChildAt?(index: number): Node
}
export interface Declaration {
diff --git a/tns-core-modules/ui/styling/css-selector/css-selector.ts b/tns-core-modules/ui/styling/css-selector/css-selector.ts
index 4bfb73850..7ce0f80a0 100644
--- a/tns-core-modules/ui/styling/css-selector/css-selector.ts
+++ b/tns-core-modules/ui/styling/css-selector/css-selector.ts
@@ -45,6 +45,17 @@ namespace Match {
export var Static = false;
}
+function getNodeDirectSibling(node): null | Node {
+ if (!node.parent || !node.parent.getChildIndex || !node.parent.getChildAt) {
+ return null;
+ }
+ const nodeIndex = node.parent.getChildIndex(node);
+ if (nodeIndex === 0) {
+ return null;
+ }
+ return node.parent.getChildAt(nodeIndex - 1);
+}
+
function SelectorProperties(specificity: Specificity, rarity: Rarity, dynamic: boolean = false): ClassDecorator {
return cls => {
cls.prototype.specificity = specificity;
@@ -229,21 +240,27 @@ export class Selector extends SelectorCore {
constructor(public selectors: SimpleSelector[]) {
super();
- let lastGroup: SimpleSelector[];
- let groups: SimpleSelector[][] = [];
+ const supportedCombinator = [undefined, " ", ">", "+"];
+ let siblingGroup: SimpleSelector[];
+ 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}".`);
+ if (supportedCombinator.indexOf(sel.combinator) === -1) {
+ throw new Error(`Unsupported combinator "${sel.combinator}".`);
}
+ if (sel.combinator === undefined || sel.combinator === " ") {
+ groups.push(lastGroup = [siblingGroup = []]);
+ }
+ if (sel.combinator === ">") {
+ lastGroup.push(siblingGroup = []);
+ }
+ siblingGroup.push(sel);
});
- this.groups = groups.map(g => new Selector.ChildGroup(g));
+ this.groups = groups.map(g =>
+ new Selector.ChildGroup(g.map(sg =>
+ new Selector.SiblingGroup(sg)
+ ))
+ );
this.last = selectors[0];
this.specificity = selectors.reduce((sum, sel) => sel.specificity + sum, 0);
this.dynamic = selectors.some(sel => sel.dynamic);
@@ -329,20 +346,39 @@ export namespace Selector {
export class ChildGroup {
public dynamic: boolean;
+ constructor(private selectors: SiblingGroup[]) {
+ 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 class SiblingGroup {
+ 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;
+ return this.selectors.every((sel, i) => (i === 0 ? node : node = getNodeDirectSibling(node)) && 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;
+ return this.selectors.every((sel, i) => (i === 0 ? node : node = getNodeDirectSibling(node)) && 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));
+ this.selectors.forEach((sel, i) => (i === 0 ? node : node = getNodeDirectSibling(node)) && sel.trackChanges(node, map));
}
}
export interface Bound {
@@ -381,7 +417,6 @@ function createSelector(sel: string): SimpleSelector | SimpleSelectorSequence |
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];