Merge pull request #1834 from NativeScript/hdeshev/application-css-update

Allow updates to application CSS rules.
This commit is contained in:
Hristo Deshev
2016-03-24 15:10:56 +02:00
8 changed files with 184 additions and 40 deletions

View File

@ -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<cssSelector.CssSelector> = undefined;
export var appSelectors: Array<cssSelector.CssSelector> = [];
export var additionalSelectors: Array<cssSelector.CssSelector> = [];
export var cssSelectors: Array<cssSelector.CssSelector> = [];
export var cssSelectorVersion: number = 0;
export var resources: any = {};
@ -50,16 +55,32 @@ export function loadCss(cssFile?: string): Array<cssSelector.CssSelector> {
var result: Array<cssSelector.CssSelector>;
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;
}
}
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<cssSelector.CssSelector> {
if (!styleScope) {
styleScope = require("ui/styling/style-scope");
}
return styleScope.StyleScope.createSelectorsFromCss(cssText, cssFileName);
}

View File

@ -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 });
}
}

View File

@ -122,10 +122,19 @@ declare module "application" {
*/
export var cssFile: string;
//@private
export var appSelectors: Array<cssSelector.CssSelector>;
export var additionalSelectors: Array<cssSelector.CssSelector>;
/**
* Cached css selectors created from the content of the css file.
*/
export var cssSelectorsCache: Array<cssSelector.CssSelector>;
export var cssSelectors: Array<cssSelector.CssSelector>;
export var cssSelectorVersion: number;
export function parseCss(cssText: string, cssFileName?: string): Array<cssSelector.CssSelector>;
export function mergeCssSelectors(module: any): void;
//@endprivate
export function addCss(cssText: string): void;
/**
* Loads css file and parses to a css syntax tree.

View File

@ -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;

View File

@ -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<viewModule.View>) {
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<viewModule.View>) {
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<viewModule.View>) {
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();

View File

@ -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[]) {

View File

@ -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;

View File

@ -45,36 +45,41 @@ export class StyleScope {
private _css: string;
private _cssFileName: string;
private _cssSelectors: Array<cssSelector.CssSelector>;
private _mergedCssSelectors: Array<cssSelector.CssSelector>;
private _localCssSelectors: Array<cssSelector.CssSelector> = [];
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<cssSelector.CssSelector>();
}
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<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];
for (i = 0; i < this._mergedCssSelectors.length; i++) {
selector = this._mergedCssSelectors[i];
if (selector.matches(view)) {
if (selector instanceof cssSelector.CssVisualStateSelector) {
matchedStateSelectors.push(<cssSelector.CssVisualStateSelector>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;