Files
NativeScript/ui/styling/style-scope.ts
Hristo Deshev 38f243de5c Move css and css-value modules to the toplevel.
This is the default location when installing from npm, and Angular uses
that when parsing CSS.

Updated module paths, requires, csproj, tsconfig and grunt tasks.
2015-08-03 12:01:09 +03:00

236 lines
8.7 KiB
TypeScript

import view = require("ui/core/view");
import trace = require("trace");
import cssSelector = require("ui/styling/css-selector");
import cssParser = require("css");
import {VisualState} from "ui/styling/visual-state";
import application = require("application");
import utils = require("utils/utils");
import types = require("utils/types");
import fs = require("file-system");
import file_access_module = require("file-system/file-system-access");
var fileAccess = new file_access_module.FileSystemAccess();
var pattern: RegExp = /url\(('|")(.*?)\1\)/;
export class StyleScope {
// caches all the visual states by the key of the visual state selectors
private _statesByKey = {};
private _viewIdToKey = {};
private _css: string;
private _cssFileName: string;
private _cssSelectors: Array<cssSelector.CssSelector>;
get css(): string {
return this._css;
}
set css(value: string) {
this._css = value;
this._cssFileName = undefined;
this._cssSelectors = undefined;
this._reset();
}
public addCss(cssString: string, cssFileName: string): void {
this._css = this._css ? this._css + cssString : cssString;
this._cssFileName = cssFileName
this._reset();
if (!this._cssSelectors) {
// Always add app.css when initializing selectors
if (application.cssSelectorsCache) {
this._cssSelectors = StyleScope._joinCssSelectorsArrays([application.cssSelectorsCache]);
}
else {
this._cssSelectors = new Array<cssSelector.CssSelector>();
}
}
var selectorsFromFile = StyleScope.createSelectorsFromCss(cssString, cssFileName);
this._cssSelectors = StyleScope._joinCssSelectorsArrays([this._cssSelectors, selectorsFromFile]);
}
public static createSelectorsFromCss(css: string, cssFileName: string): cssSelector.CssSelector[] {
try {
var pageCssSyntaxTree = css ? cssParser.parse(css, { source: cssFileName }) : null;
var pageCssSelectors = new Array<cssSelector.CssSelector>();
if (pageCssSyntaxTree) {
pageCssSelectors = StyleScope._joinCssSelectorsArrays([pageCssSelectors, StyleScope.createSelectorsFromImports(pageCssSyntaxTree)]);
pageCssSelectors = StyleScope._joinCssSelectorsArrays([pageCssSelectors, StyleScope.createSelectorsFromSyntaxTree(pageCssSyntaxTree)]);
}
return pageCssSelectors;
}
catch (e) {
trace.write("Css styling failed: " + e, trace.categories.Error, trace.messageType.error);
}
}
public static createSelectorsFromImports(tree: cssParser.SyntaxTree): cssSelector.CssSelector[] {
var selectors = new Array<cssSelector.CssSelector>();
if (!types.isNullOrUndefined(tree)) {
var imports = tree["stylesheet"]["rules"].filter(r=> r.type === "import");
for (var i = 0; i < imports.length; i++) {
var importItem = imports[i]["import"];
var match = importItem && (<string>importItem).match(pattern);
var url = match && match[2];
if (!types.isNullOrUndefined(url)) {
if (utils.isFileOrResourcePath(url)) {
var fileName = types.isString(url) ? url.trim() : "";
if (fileName.indexOf("~/") === 0) {
fileName = fs.path.join(fs.knownFolders.currentApp().path, fileName.replace("~/", ""));
}
fileAccess.readText(fileName, result => {
selectors = StyleScope._joinCssSelectorsArrays([selectors, StyleScope.createSelectorsFromCss(result, fileName)]);
});
}
}
}
}
return selectors;
}
public ensureSelectors() {
if (!this._cssSelectors && (this._css || application.cssSelectorsCache)) {
var applicationCssSelectors = application.cssSelectorsCache ? application.cssSelectorsCache : null;
var pageCssSelectors = StyleScope.createSelectorsFromCss(this._css, this._cssFileName);
this._cssSelectors = StyleScope._joinCssSelectorsArrays([applicationCssSelectors, pageCssSelectors]);
}
}
private static _joinCssSelectorsArrays(arrays: Array<Array<cssSelector.CssSelector>>): Array<cssSelector.CssSelector> {
var mergedResult = [];
var i;
for (i = 0; i < arrays.length; i++) {
if (arrays[i]) {
mergedResult.push.apply(mergedResult, arrays[i]);
}
}
mergedResult.sort((a, b) => a.specificity - b.specificity);
return mergedResult;
}
public applySelectors(view: view.View) {
if (!this._cssSelectors) {
return;
}
view.style._beginUpdate();
var i,
selector: cssSelector.CssSelector,
matchedStateSelectors = new Array<cssSelector.CssVisualStateSelector>()
// Go trough all selectors - and directly apply all non-state selectors
for (i = 0; i < this._cssSelectors.length; i++) {
selector = this._cssSelectors[i];
if (selector.matches(view)) {
if (selector instanceof cssSelector.CssVisualStateSelector) {
matchedStateSelectors.push(<cssSelector.CssVisualStateSelector>selector);
} else {
selector.apply(view);
}
}
}
if (matchedStateSelectors.length > 0) {
// Create a key for all matched selectors for this element
var key: string = "";
matchedStateSelectors.forEach((s) => key += s.key + "|");
//console.log("Created key: " + key + " for " + matchedStateSelectors.length + " state selectors");
// 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 {
var key = this._viewIdToKey[view._domId];
if (key === undefined) {
return undefined;
}
return this._statesByKey[key];
}
private _createVisualsStatesForSelectors(key: string, matchedStateSelectors: Array<cssSelector.CssVisualStateSelector>) {
var i,
allStates = {},
stateSelector: cssSelector.CssVisualStateSelector;
this._statesByKey[key] = allStates;
for (i = 0; i < matchedStateSelectors.length; i++) {
stateSelector = matchedStateSelectors[i];
var visualState: VisualState = allStates[stateSelector.state];
if (!visualState) {
visualState = new VisualState();
allStates[stateSelector.state] = visualState;
}
stateSelector.eachSetter((property, value) => {
visualState.setters[property.name] = value;
});
}
}
private static createSelectorsFromSyntaxTree(ast: cssParser.SyntaxTree): Array<cssSelector.CssSelector> {
var result: Array<cssSelector.CssSelector> = [];
var rules = ast.stylesheet.rules;
var rule: cssParser.Rule;
var filteredDeclarations: cssParser.Declaration[];
var i;
var 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.
filteredDeclarations = rule.declarations.filter((val, i, arr) => { return val.type === "declaration" });
for (j = 0; j < rule.selectors.length; j++) {
result.push(cssSelector.createSelector(rule.selectors[j], filteredDeclarations));
}
}
}
return result;
}
private _reset() {
this._statesByKey = {};
this._viewIdToKey = {};
}
}
export function applyInlineSyle(view: view.View, style: string) {
try {
var syntaxTree = cssParser.parse("local { " + style + " }", undefined);
var filteredDeclarations = syntaxTree.stylesheet.rules[0].declarations.filter((val, i, arr) => { return val.type === "declaration" });
cssSelector.applyInlineSyle(view, filteredDeclarations);
} catch (ex) {
trace.write("Applying local style failed: " + ex, trace.categories.Error, trace.messageType.error);
}
}