mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-08-17 12:57: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:
@ -1,485 +1,541 @@
|
||||
import view = require("ui/core/view");
|
||||
import observable = require("ui/core/dependency-observable");
|
||||
import cssParser = require("css");
|
||||
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";
|
||||
import {Node, Declaration, Changes, ChangeMap} from "ui/styling/css-selector";
|
||||
import {isNullOrUndefined} from "utils/types";
|
||||
import {escapeRegexSymbols} from "utils/utils";
|
||||
|
||||
let ID_SPECIFICITY = 1000000;
|
||||
let ATTR_SPECIFITY = 10000;
|
||||
let CLASS_SPECIFICITY = 100;
|
||||
let TYPE_SPECIFICITY = 1;
|
||||
import * as cssParser from "css";
|
||||
import * as selectorParser from "./css-selector-parser";
|
||||
|
||||
export class CssSelector {
|
||||
public animations: Array<keyframeAnimation.KeyframeAnimationInfo>;
|
||||
const enum Specificity {
|
||||
Inline = 0x01000000,
|
||||
Id = 0x00010000,
|
||||
Attribute = 0x00000100,
|
||||
Class = 0x00000100,
|
||||
PseudoClass = 0x00000100,
|
||||
Type = 0x00000001,
|
||||
Universal = 0x00000000,
|
||||
Invalid = 0x00000000
|
||||
}
|
||||
|
||||
private _expression: string;
|
||||
private _declarations: cssParser.Declaration[];
|
||||
private _attrExpression: string;
|
||||
const enum Rarity {
|
||||
Invalid = 4,
|
||||
Id = 3,
|
||||
Class = 2,
|
||||
Type = 1,
|
||||
PseudoClass = 0,
|
||||
Attribute = 0,
|
||||
Universal = 0,
|
||||
Inline = 0
|
||||
}
|
||||
|
||||
constructor(expression: string, declarations: cssParser.Declaration[]) {
|
||||
if (expression) {
|
||||
let leftSquareBracketIndex = expression.indexOf(LSBRACKET);
|
||||
if (leftSquareBracketIndex >= 0) {
|
||||
// extracts what is inside square brackets ([target = 'test'] will extract "target = 'test'")
|
||||
let paramsRegex = /\[\s*(.*)\s*\]/;
|
||||
let attrParams = paramsRegex.exec(expression);
|
||||
if (attrParams && attrParams.length > 1) {
|
||||
this._attrExpression = attrParams[1].trim();
|
||||
}
|
||||
this._expression = expression.substr(0, leftSquareBracketIndex);
|
||||
}
|
||||
else {
|
||||
this._expression = expression;
|
||||
}
|
||||
interface LookupSorter {
|
||||
sortById(id: string, sel: SelectorCore);
|
||||
sortByClass(cssClass: string, sel: SelectorCore);
|
||||
sortByType(cssType: string, sel: SelectorCore);
|
||||
sortAsUniversal(sel: SelectorCore);
|
||||
}
|
||||
|
||||
namespace Match {
|
||||
/**
|
||||
* Depends on attributes or pseudoclasses state;
|
||||
*/
|
||||
export var Dynamic = true;
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
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) {
|
||||
let modifier = valueSourceModifier || this.valueSourceModifier;
|
||||
this.eachSetter((property, value) => {
|
||||
if (types.isString(property)) {
|
||||
const propertyName = <string>property;
|
||||
let attrHandled = false;
|
||||
let specialSetter = getSpecialPropertySetter(propertyName);
|
||||
function wrap(text: string): string {
|
||||
return text ? ` ${text} ` : "";
|
||||
}
|
||||
|
||||
if (!attrHandled && specialSetter) {
|
||||
specialSetter(view, value);
|
||||
attrHandled = true;
|
||||
@SelectorProperties(Specificity.Invalid, Rarity.Invalid, Match.Static)
|
||||
export class InvalidSelector extends SimpleSelector {
|
||||
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) {
|
||||
view[propertyName] = utils.convertString(value);
|
||||
}
|
||||
if (regexp) {
|
||||
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 {
|
||||
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 ancestor = node;
|
||||
while(ancestor = ancestor.parent) {
|
||||
if (node = group.match(ancestor)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
if (this.animations && view.isLoaded && view._nativeView !== undefined) {
|
||||
for (let animationInfo of this.animations) {
|
||||
let animation = keyframeAnimation.KeyframeAnimation.keyframeAnimationFromInfo(animationInfo, modifier);
|
||||
if (animation) {
|
||||
view._registerAnimation(animation);
|
||||
animation.play(view)
|
||||
.then(() => { view._unregisterAnimation(animation); })
|
||||
.catch((e) => { view._unregisterAnimation(animation); });
|
||||
}
|
||||
|
||||
public lookupSort(sorter: LookupSorter, base?: SelectorCore): void {
|
||||
this.last.lookupSort(sorter, this);
|
||||
}
|
||||
|
||||
public accumulateChanges(node: Node, map?: ChangeAccumulator): boolean {
|
||||
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) {
|
||||
for (let i = 0; i < this._declarations.length; i++) {
|
||||
let declaration = this._declarations[i];
|
||||
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}]`;
|
||||
if (sequences.length === 1) {
|
||||
// This is a selector with a single SinmpleSelectorSequence so we will not combine it into Selector.
|
||||
return sequences[0];
|
||||
} 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 {
|
||||
get specificity(): number {
|
||||
let result = TYPE_SPECIFICITY;
|
||||
let dotIndex = this.expression.indexOf(DOT);
|
||||
if (dotIndex > -1) {
|
||||
result += CLASS_SPECIFICITY;
|
||||
}
|
||||
return result;
|
||||
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";
|
||||
}
|
||||
|
||||
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);
|
||||
if (result && this.attrExpression) {
|
||||
return matchesAttr(this.attrExpression, view);
|
||||
|
||||
query(node: T): SelectorsMatch<T> {
|
||||
let selectorClasses = [
|
||||
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 {
|
||||
let exprArr = expression.split(".");
|
||||
let exprTypeName = exprArr[0];
|
||||
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;
|
||||
}
|
||||
interface ChangeAccumulator {
|
||||
addAttribute(node: Node, attribute: string): void;
|
||||
addPseudoClass(node: Node, pseudoClass: string): void;
|
||||
}
|
||||
|
||||
class CssIdSelector extends CssSelector {
|
||||
get specificity(): number {
|
||||
return ID_SPECIFICITY;
|
||||
}
|
||||
public matches(view: view.View): boolean {
|
||||
let result = this.expression === view.id;
|
||||
if (result && this.attrExpression) {
|
||||
return matchesAttr(this.attrExpression, view);
|
||||
export class SelectorsMatch<T extends Node> implements ChangeAccumulator {
|
||||
public changeMap: ChangeMap<T> = new Map<T, Changes>();
|
||||
public selectors;
|
||||
|
||||
public addAttribute(node: T, attribute: string): void {
|
||||
let deps: Changes = this.properties(node);
|
||||
if (!deps.attributes) {
|
||||
deps.attributes = new Set();
|
||||
}
|
||||
return result;
|
||||
deps.attributes.add(attribute);
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return `CssIdSelector ${this.expression}${this.attrExpressionText} { ${this.declarationText} }`;
|
||||
public addPseudoClass(node: T, pseudoClass: string): void {
|
||||
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);
|
||||
}
|
||||
|
Reference in New Issue
Block a user