mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-08-17 04:41:36 +08:00
Merge pull request #4146 from NativeScript/sibling-css-selector
Sibling css selector
This commit is contained in:
36
apps/app/ui-tests-app/css/combinators.css
Normal file
36
apps/app/ui-tests-app/css/combinators.css
Normal file
@ -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;
|
||||||
|
}
|
66
apps/app/ui-tests-app/css/combinators.xml
Normal file
66
apps/app/ui-tests-app/css/combinators.xml
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
<Page>
|
||||||
|
<ScrollView>
|
||||||
|
<StackLayout>
|
||||||
|
|
||||||
|
<StackLayout class="direct-child--type">
|
||||||
|
<Label text="Direct child test by type"/>
|
||||||
|
<Button text="I'm a direct child!"/>
|
||||||
|
<StackLayout>
|
||||||
|
<Button text="I'm not!"/>
|
||||||
|
</StackLayout>
|
||||||
|
</StackLayout>
|
||||||
|
|
||||||
|
<StackLayout class="direct-child--class">
|
||||||
|
<Label text="Direct child test by class"/>
|
||||||
|
<Button class="test-child" text="I'm a direct child!"/>
|
||||||
|
<StackLayout>
|
||||||
|
<Button class="test-child" text="I'm not!"/>
|
||||||
|
</StackLayout>
|
||||||
|
</StackLayout>
|
||||||
|
|
||||||
|
<StackLayout class="direct-sibling--type">
|
||||||
|
<Label text="Direct sibling test by type"/>
|
||||||
|
<Button text="I'm the ref"/>
|
||||||
|
<Label class="sibling-test-label" text="I'm a direct sibling!"/>
|
||||||
|
<Label text="I'm not!" class="sibling-test-label"/>
|
||||||
|
<Button text="I'm not either!"/>
|
||||||
|
<Label class="sibling-test-label" text="But I am!"/>
|
||||||
|
</StackLayout>
|
||||||
|
|
||||||
|
<StackLayout class="direct-sibling--id">
|
||||||
|
<Label text="Direct sibling test by id"/>
|
||||||
|
<Button id="test-child" text="I'm the ref"/>
|
||||||
|
<Button id="test-child-2" text="I'm a direct sibling!"/>
|
||||||
|
<Button id="test-child-3" text="I'm not!"/>
|
||||||
|
</StackLayout>
|
||||||
|
|
||||||
|
<StackLayout class="direct-sibling--class">
|
||||||
|
<Label text="Direct sibling test by class"/>
|
||||||
|
<Button class="test-child" text="I'm the ref"/>
|
||||||
|
<Button class="test-child-2" text="I'm a direct sibling!"/>
|
||||||
|
<Button class="test-child-2" text="I'm not!"/>
|
||||||
|
<Button class="test-child" text="I'm not either!"/>
|
||||||
|
<Label class="test-child-2 sibling-test-label" text="But I am!"/>
|
||||||
|
</StackLayout>
|
||||||
|
|
||||||
|
<StackLayout class="direct-sibling--attribute">
|
||||||
|
<Label text="Direct sibling test by attribute"/>
|
||||||
|
<Button data="test-child" text="I'm the ref"/>
|
||||||
|
<Button data="test-child-2" text="I'm a direct sibling!"/>
|
||||||
|
<Button data="test-child-2" text="I'm not!"/>
|
||||||
|
<Button data="test-child" text="I'm not either!"/>
|
||||||
|
<Button data="test-child-2" text="But I am!"/>
|
||||||
|
</StackLayout>
|
||||||
|
|
||||||
|
<StackLayout class="direct-sibling--pseudo-selector">
|
||||||
|
<Label text="Direct sibling test by pseudo-selector"/>
|
||||||
|
<Button text="I'm the ref"/>
|
||||||
|
<Button isEnabled="false" text="I'm a direct sibling!"/>
|
||||||
|
<Button text="I'm not!"/>
|
||||||
|
<Button text="I'm not either!"/>
|
||||||
|
<Button isEnabled="false" text="But I am!"/>
|
||||||
|
</StackLayout>
|
||||||
|
|
||||||
|
</StackLayout>
|
||||||
|
</ScrollView>
|
||||||
|
</Page>
|
@ -45,6 +45,7 @@ export function pageLoaded(args: EventData) {
|
|||||||
examples.set("border-playground", "css/border-playground");
|
examples.set("border-playground", "css/border-playground");
|
||||||
examples.set("textview-hint-color", "css/textview-hint-color");
|
examples.set("textview-hint-color", "css/textview-hint-color");
|
||||||
examples.set("hint-text-color", "css/hint-text-color");
|
examples.set("hint-text-color", "css/hint-text-color");
|
||||||
|
examples.set("combinators", "css/combinators");
|
||||||
|
|
||||||
let viewModel = new SubMainPageViewModel(wrapLayout, examples);
|
let viewModel = new SubMainPageViewModel(wrapLayout, examples);
|
||||||
page.bindingContext = viewModel;
|
page.bindingContext = viewModel;
|
||||||
|
@ -232,3 +232,38 @@ export function test_query_match_one_child_group() {
|
|||||||
let expected = new Map<selector.Node, selector.Changes>().set(prod, { attributes: new Set(["special"])} );
|
let expected = new Map<selector.Node, selector.Changes>().set(prod, { attributes: new Set(["special"])} );
|
||||||
TKUnit.assertDeepEqual(match.changeMap, expected);
|
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<string>(["highlighted"]),
|
||||||
|
toString,
|
||||||
|
parent: list
|
||||||
|
};
|
||||||
|
|
||||||
|
disabledButton = {
|
||||||
|
cssType: "button",
|
||||||
|
cssPseudoClasses: new Set<string>(["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<selector.Node, selector.Changes>()
|
||||||
|
.set(button, { pseudoClasses: new Set(["highlighted"]) })
|
||||||
|
.set(disabledButton, { pseudoClasses: new Set(["disabled"]) });
|
||||||
|
|
||||||
|
TKUnit.assertDeepEqual(match.changeMap, expected);
|
||||||
|
}
|
||||||
|
@ -15,6 +15,8 @@ export interface Node {
|
|||||||
cssType?: string;
|
cssType?: string;
|
||||||
cssClasses?: Set<string>;
|
cssClasses?: Set<string>;
|
||||||
cssPseudoClasses?: Set<string>;
|
cssPseudoClasses?: Set<string>;
|
||||||
|
getChildIndex?(node: Node): number
|
||||||
|
getChildAt?(index: number): Node
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Declaration {
|
export interface Declaration {
|
||||||
|
@ -45,6 +45,17 @@ namespace Match {
|
|||||||
export var Static = false;
|
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 {
|
function SelectorProperties(specificity: Specificity, rarity: Rarity, dynamic: boolean = false): ClassDecorator {
|
||||||
return cls => {
|
return cls => {
|
||||||
cls.prototype.specificity = specificity;
|
cls.prototype.specificity = specificity;
|
||||||
@ -229,21 +240,27 @@ export class Selector extends SelectorCore {
|
|||||||
|
|
||||||
constructor(public selectors: SimpleSelector[]) {
|
constructor(public selectors: SimpleSelector[]) {
|
||||||
super();
|
super();
|
||||||
let lastGroup: SimpleSelector[];
|
const supportedCombinator = [undefined, " ", ">", "+"];
|
||||||
let groups: SimpleSelector[][] = [];
|
let siblingGroup: SimpleSelector[];
|
||||||
|
let lastGroup: SimpleSelector[][];
|
||||||
|
let groups: SimpleSelector[][][] = [];
|
||||||
selectors.reverse().forEach(sel => {
|
selectors.reverse().forEach(sel => {
|
||||||
switch(sel.combinator) {
|
if (supportedCombinator.indexOf(sel.combinator) === -1) {
|
||||||
case undefined:
|
throw new Error(`Unsupported combinator "${sel.combinator}".`);
|
||||||
case " ":
|
|
||||||
groups.push(lastGroup = []);
|
|
||||||
case ">":
|
|
||||||
lastGroup.push(sel);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
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.last = selectors[0];
|
||||||
this.specificity = selectors.reduce((sum, sel) => sel.specificity + sum, 0);
|
this.specificity = selectors.reduce((sum, sel) => sel.specificity + sum, 0);
|
||||||
this.dynamic = selectors.some(sel => sel.dynamic);
|
this.dynamic = selectors.some(sel => sel.dynamic);
|
||||||
@ -329,20 +346,39 @@ export namespace Selector {
|
|||||||
export class ChildGroup {
|
export class ChildGroup {
|
||||||
public dynamic: boolean;
|
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[]) {
|
constructor(private selectors: SimpleSelector[]) {
|
||||||
this.dynamic = selectors.some(sel => sel.dynamic);
|
this.dynamic = selectors.some(sel => sel.dynamic);
|
||||||
}
|
}
|
||||||
|
|
||||||
public match(node: Node): Node {
|
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 {
|
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) {
|
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 {
|
export interface Bound {
|
||||||
@ -381,7 +417,6 @@ function createSelector(sel: string): SimpleSelector | SimpleSelectorSequence |
|
|||||||
|
|
||||||
let selectors = ast.map(createSimpleSelector);
|
let selectors = ast.map(createSimpleSelector);
|
||||||
let sequences: (SimpleSelector | SimpleSelectorSequence)[] = [];
|
let sequences: (SimpleSelector | SimpleSelectorSequence)[] = [];
|
||||||
|
|
||||||
// Join simple selectors into sequences, set combinators
|
// Join simple selectors into sequences, set combinators
|
||||||
for (let seqStart = 0, seqEnd = 0, last = selectors.length - 1; seqEnd <= last; seqEnd++) {
|
for (let seqStart = 0, seqEnd = 0, last = selectors.length - 1; seqEnd <= last; seqEnd++) {
|
||||||
let sel = selectors[seqEnd];
|
let sel = selectors[seqEnd];
|
||||||
|
Reference in New Issue
Block a user