diff --git a/application/application-common.ts b/application/application-common.ts index 2c3c1c3a8..cc47c5139 100644 --- a/application/application-common.ts +++ b/application/application-common.ts @@ -6,6 +6,8 @@ import cssSelector = require("ui/styling/css-selector"); import * as fileSystemModule from "file-system"; import * as styleScopeModule from "ui/styling/style-scope"; +var styleScope: typeof styleScopeModule = undefined; + var events = new observable.Observable(); global.moduleMerge(events, exports); @@ -22,7 +24,10 @@ export var mainEntry: frame.NavigationEntry; export var cssFile: string = "app.css" -export var cssSelectorsCache: Array = undefined; +export var appSelectors: Array = []; +export var additionalSelectors: Array = []; +export var cssSelectors: Array = []; +export var cssSelectorVersion: number = 0; export var resources: any = {}; @@ -50,16 +55,32 @@ export function loadCss(cssFile?: string): Array { var result: Array; var fs: typeof fileSystemModule = require("file-system"); - var styleScope: typeof styleScopeModule = require("ui/styling/style-scope"); + if (!styleScope) { + styleScope = require("ui/styling/style-scope"); + } var cssFileName = fs.path.join(fs.knownFolders.currentApp().path, cssFile); if (fs.File.exists(cssFileName)) { var file = fs.File.fromPath(cssFileName); var applicationCss = file.readTextSync(); if (applicationCss) { - result = styleScope.StyleScope.createSelectorsFromCss(applicationCss, cssFileName); + result = parseCss(applicationCss, cssFileName); } } return result; -} \ No newline at end of file +} + +export function mergeCssSelectors(module: any): void { + //HACK: pass the merged module and work with its exported vars. + module.cssSelectors = module.appSelectors.slice(); + module.cssSelectors.push.apply(module.cssSelectors, module.additionalSelectors); + module.cssSelectorVersion++; +} + +export function parseCss(cssText: string, cssFileName?: string): Array { + if (!styleScope) { + styleScope = require("ui/styling/style-scope"); + } + return styleScope.StyleScope.createSelectorsFromCss(cssText, cssFileName); +} diff --git a/application/application.android.ts b/application/application.android.ts index 864aa6bfc..eb87b1152 100644 --- a/application/application.android.ts +++ b/application/application.android.ts @@ -322,7 +322,20 @@ function onConfigurationChanged(context: android.content.Context, intent: androi } function loadCss() { - typedExports.cssSelectorsCache = typedExports.loadCss(typedExports.cssFile); + //HACK: identical to application.ios.ts + typedExports.appSelectors = typedExports.loadCss(typedExports.cssFile) || []; + if (typedExports.appSelectors.length > 0) { + typedExports.mergeCssSelectors(typedExports); + } +} + +export function addCss(cssText: string) { + //HACK: identical to application.ios.ts + const parsed = typedExports.parseCss(cssText); + if (parsed) { + typedExports.additionalSelectors.push.apply(typedExports.additionalSelectors, parsed); + typedExports.mergeCssSelectors(typedExports); + } } global.__onLiveSync = function () { @@ -351,4 +364,4 @@ global.__onUncaughtError = function (error: definition.NativeScriptError) { } typedExports.notify({ eventName: typedExports.uncaughtErrorEvent, object: appModule.android, android: error }); -} \ No newline at end of file +} diff --git a/application/application.d.ts b/application/application.d.ts index 2c646f139..85e2405bf 100644 --- a/application/application.d.ts +++ b/application/application.d.ts @@ -122,10 +122,19 @@ declare module "application" { */ export var cssFile: string; + //@private + export var appSelectors: Array; + export var additionalSelectors: Array; /** * Cached css selectors created from the content of the css file. */ - export var cssSelectorsCache: Array; + export var cssSelectors: Array; + export var cssSelectorVersion: number; + export function parseCss(cssText: string, cssFileName?: string): Array; + export function mergeCssSelectors(module: any): void; + //@endprivate + + export function addCss(cssText: string): void; /** * Loads css file and parses to a css syntax tree. diff --git a/application/application.ios.ts b/application/application.ios.ts index 9d4892aa2..16e0d86ad 100644 --- a/application/application.ios.ts +++ b/application/application.ios.ts @@ -242,7 +242,20 @@ global.__onUncaughtError = function (error: definition.NativeScriptError) { } function loadCss() { - typedExports.cssSelectorsCache = typedExports.loadCss(typedExports.cssFile); + //HACK: identical to application.ios.ts + typedExports.appSelectors = typedExports.loadCss(typedExports.cssFile) || []; + if (typedExports.appSelectors.length > 0) { + typedExports.mergeCssSelectors(typedExports); + } +} + +export function addCss(cssText: string) { + //HACK: identical to application.android.ts + const parsed = typedExports.parseCss(cssText); + if (parsed) { + typedExports.additionalSelectors.push.apply(typedExports.additionalSelectors, parsed); + typedExports.mergeCssSelectors(typedExports); + } } var started: boolean = false; diff --git a/apps/tests/ui/style/style-tests.ts b/apps/tests/ui/style/style-tests.ts index 059779653..00d40a222 100644 --- a/apps/tests/ui/style/style-tests.ts +++ b/apps/tests/ui/style/style-tests.ts @@ -1,4 +1,5 @@ import TKUnit = require("../../TKUnit"); +import application = require("application"); import buttonModule = require("ui/button"); import labelModule = require("ui/label"); import pageModule = require("ui/page"); @@ -52,6 +53,42 @@ export function test_css_is_applied_to_special_properties() { }); } +export function test_applies_css_changes_to_application_rules_before_page_load() { + application.addCss(".applicationChangedLabelBefore { color: red; }"); + const label = new labelModule.Label(); + label.className = "applicationChangedLabelBefore"; + label.text = "Red color coming from updated application rules"; + + helper.buildUIAndRunTest(label, function (views: Array) { + helper.assertViewColor(label, "#FF0000"); + }); +} + +export function test_applies_css_changes_to_application_rules_after_page_load() { + const label1 = new labelModule.Label(); + label1.text = "Blue color coming from updated application rules"; + + helper.buildUIAndRunTest(label1, function (views: Array) { + application.addCss(".applicationChangedLabelAfter { color: blue; }"); + label1.className = "applicationChangedLabelAfter"; + helper.assertViewColor(label1, "#0000FF"); + }); +} + +export function test_applies_css_changes_to_application_rules_after_page_load_new_views() { + const host = new stackModule.StackLayout(); + + helper.buildUIAndRunTest(host, function (views: Array) { + application.addCss(".applicationChangedLabelAfterNew { color: #00FF00; }"); + + const label = new labelModule.Label(); + label.className = "applicationChangedLabelAfterNew"; + label.text = "Blue color coming from updated application rules"; + host.addChild(label); + helper.assertViewColor(label, "#00FF00"); + }); +} + // Test for inheritance in different containers export function test_css_is_applied_inside_StackLayout() { var testButton = new buttonModule.Button(); diff --git a/ui/styling/css-selector.ts b/ui/styling/css-selector.ts index 90eb5856b..dff0bea47 100644 --- a/ui/styling/css-selector.ts +++ b/ui/styling/css-selector.ts @@ -73,8 +73,7 @@ export class CssSelector { } else { try { view.style._setValue(property, value, observable.ValueSource.Css); - } - catch (ex) { + } catch (ex) { trace.write("Error setting property: " + property.name + " view: " + view + " value: " + value + " " + ex, trace.categories.Style, trace.messageType.error); } } @@ -106,6 +105,18 @@ export class CssSelector { } } } + + 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 { + return ""; + } + } } class CssTypeSelector extends CssSelector { @@ -119,6 +130,9 @@ class CssTypeSelector extends CssSelector { } return result; } + public toString(): string { + return `CssTypeSelector ${this.expression}${this.attrExpressionText} { ${this.declarationText} }`; + } } function matchesType(expression: string, view: view.View): boolean { @@ -153,6 +167,10 @@ class CssIdSelector extends CssSelector { } return result; } + + public toString(): string { + return `CssIdSelector ${this.expression}${this.attrExpressionText} { ${this.declarationText} }`; + } } class CssClassSelector extends CssSelector { @@ -167,6 +185,10 @@ class CssClassSelector extends CssSelector { } return result; } + public toString(): string { + return `CssClassSelector ${this.expression}${this.attrExpressionText} { ${this.declarationText} }`; + } + } class CssCompositeSelector extends CssSelector { @@ -249,6 +271,10 @@ class CssCompositeSelector extends CssSelector { } return result; } + + public toString(): string { + return `CssCompositeSelector ${this.expression}${this.attrExpressionText} { ${this.declarationText} }`; + } } class CssAttrSelector extends CssSelector { @@ -259,6 +285,10 @@ class CssAttrSelector extends CssSelector { 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 { @@ -371,6 +401,10 @@ export class CssVisualStateSelector extends CssSelector { return matches; } + + public toString(): string { + return `CssVisualStateSelector ${this.expression}${this.attrExpressionText} { ${this.declarationText} }`; + } } var HASH = "#"; @@ -421,6 +455,10 @@ class InlineStyleSelector extends CssSelector { view.style._setValue(property, value, observable.ValueSource.Local); }); } + + public toString(): string { + return `InlineStyleSelector ${this.expression}${this.attrExpressionText} { ${this.declarationText} }`; + } } export function applyInlineSyle(view: view.View, declarations: cssParser.Declaration[]) { diff --git a/ui/styling/style-scope.d.ts b/ui/styling/style-scope.d.ts index 8ced6d535..a4afcd65c 100644 --- a/ui/styling/style-scope.d.ts +++ b/ui/styling/style-scope.d.ts @@ -10,7 +10,7 @@ declare module "ui/styling/style-scope" { public static createSelectorsFromCss(css: string, cssFileName: string): cssSelector.CssSelector[]; public static createSelectorsFromImports(tree: cssParser.SyntaxTree): cssSelector.CssSelector[]; - public ensureSelectors(); + public ensureSelectors(): boolean; public applySelectors(view: view.View): void public getVisualStates(view: view.View): Object; diff --git a/ui/styling/style-scope.ts b/ui/styling/style-scope.ts index 3d6207612..196b17633 100644 --- a/ui/styling/style-scope.ts +++ b/ui/styling/style-scope.ts @@ -45,36 +45,41 @@ export class StyleScope { private _css: string; private _cssFileName: string; - private _cssSelectors: Array; + private _mergedCssSelectors: Array; + private _localCssSelectors: Array = []; + private _localCssSelectorVersion: number = 0; + private _localCssSelectorsAppliedVersion: number = 0; + private _applicationCssSelectorsAppliedVersion: number = 0; get css(): string { return this._css; } set css(value: string) { - this._css = value; this._cssFileName = undefined; - this._cssSelectors = undefined; - this._reset(); + this.setCss(value); } - public addCss(cssString: string, cssFileName: string): void { + public addCss(cssString: string, cssFileName?: string): void { + this.setCss(cssString, cssFileName, true); + } + + private setCss(cssString: string, cssFileName?: string, append: boolean = false): void { this._css = this._css ? this._css + cssString : cssString; - this._cssFileName = cssFileName + if (cssFileName) { + 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(); - } + const parsedSelectors = StyleScope.createSelectorsFromCss(cssString, cssFileName); + if (append) { + this._localCssSelectors.push.apply(this._localCssSelectors, parsedSelectors); + } else { + this._localCssSelectors = parsedSelectors; } - var selectorsFromFile = StyleScope.createSelectorsFromCss(cssString, cssFileName); - this._cssSelectors = StyleScope._joinCssSelectorsArrays([this._cssSelectors, selectorsFromFile]); + this._localCssSelectorVersion++; + this.ensureSelectors(); } public static createSelectorsFromCss(css: string, cssFileName: string): cssSelector.CssSelector[] { @@ -89,8 +94,7 @@ export class StyleScope { } return pageCssSelectors; - } - catch (e) { + } catch (e) { trace.write("Css styling failed: " + e, trace.categories.Error, trace.messageType.error); } } @@ -134,12 +138,23 @@ export class StyleScope { 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); + public ensureSelectors(): boolean { + let toMerge = [] + if ((this._applicationCssSelectorsAppliedVersion !== application.cssSelectorVersion) || + (this._localCssSelectorVersion !== this._localCssSelectorsAppliedVersion) || - this._cssSelectors = StyleScope._joinCssSelectorsArrays([applicationCssSelectors, pageCssSelectors]); + (!this._mergedCssSelectors)) { + toMerge.push(application.cssSelectors); + this._applicationCssSelectorsAppliedVersion = application.cssSelectorVersion; + toMerge.push(this._localCssSelectors); + this._localCssSelectorsAppliedVersion = this._localCssSelectorVersion; + } + + if (toMerge.length > 0) { + this._mergedCssSelectors = StyleScope._joinCssSelectorsArrays(toMerge); + return true; + } else { + return false; } } @@ -157,18 +172,17 @@ export class StyleScope { } public applySelectors(view: view.View) { - if (!this._cssSelectors) { - return; - } + this.ensureSelectors(); view.style._beginUpdate(); + var i, selector: cssSelector.CssSelector, matchedStateSelectors = new Array() // Go trough all selectors - and directly apply all non-state selectors - for (i = 0; i < this._cssSelectors.length; i++) { - selector = this._cssSelectors[i]; + for (i = 0; i < this._mergedCssSelectors.length; i++) { + selector = this._mergedCssSelectors[i]; if (selector.matches(view)) { if (selector instanceof cssSelector.CssVisualStateSelector) { matchedStateSelectors.push(selector); @@ -182,7 +196,6 @@ export class StyleScope { // 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;