Avoid applying CSS multiple times (#4784)

* Move the applyStyleFromScope to onLoaded, when the views are created and id or className properties are set the CSS selectors are queried and applied multiple times

* Condense the changes when applying properties
This commit is contained in:
Panayot Cankov
2017-09-25 18:32:00 +03:00
committed by SvetoslavTsenov
parent b0577728be
commit 6d7c1ff295
25 changed files with 536 additions and 524 deletions

View File

@ -359,7 +359,7 @@ function showReportPage(finalMessage: string) {
setTimeout(() => { setTimeout(() => {
messageContainer.dismissSoftInput(); messageContainer.dismissSoftInput();
(<android.view.View>messageContainer.nativeViewProtected).scrollTo(0, 0); (<android.view.View>messageContainer.nativeViewProtected).scrollTo(0, 0);
}); }, 10);
} }
} }

View File

@ -1,7 +1,7 @@
import * as helper from "../helper"; import * as helper from "../helper";
import * as btnCounter from "./pages/button-counter"; import * as btnCounter from "./pages/button-counter";
import * as TKUnit from "../../TKUnit"; import * as TKUnit from "../../TKUnit";
import { isIOS } from "tns-core-modules/platform"; import { isIOS, isAndroid } from "tns-core-modules/platform";
// Integration tests that asser sertain runtime behavior, lifecycle events atc. // Integration tests that asser sertain runtime behavior, lifecycle events atc.
@ -119,3 +119,50 @@ export function test_navigating_away_does_not_excessively_reset() {
// but ensure a reasonable amount of native setters were called when the views navigate away // but ensure a reasonable amount of native setters were called when the views navigate away
assert(1); assert(1);
} }
export function test_css_sets_properties() {
const page = helper.navigateToModule("ui/lifecycle/pages/page-two");
const buttons = ["btn1", "btn2", "btn3", "btn4"].map(id => page.getViewById<btnCounter.Button>(id));
buttons.forEach(btn => {
TKUnit.assertEqual(btn.colorSetNativeCount, 1, `Expected ${btn.id}'s native color to propagated exactly once when inflating from xml.`);
TKUnit.assertEqual(btn.colorPropertyChangeCount, 1, `Expected ${btn.id}'s colorChange to be fired exactly once when inflating from xml.`);
});
buttons.forEach(btn => {
btn.className = "";
});
const expectedChangesAfterClearingClasses = [1, 2, 2, 2];
for (var i = 0; i < buttons.length; i++) {
TKUnit.assertEqual(buttons[i].colorSetNativeCount, expectedChangesAfterClearingClasses[i], `Expected ${buttons[i].id} native set after clear.`);
TKUnit.assertEqual(buttons[i].colorPropertyChangeCount, expectedChangesAfterClearingClasses[i], `Expected ${buttons[i].id} change notifications after clear.`);
}
buttons[0].className = "nocolor";
buttons[1].className = "red";
buttons[2].className = "blue";
buttons[3].className = "red blue";
const expectedChangesAfterResettingClasses = [1, 3, 3, 3];
for (let i = 0; i < buttons.length; i++) {
TKUnit.assertEqual(buttons[i].colorSetNativeCount, expectedChangesAfterResettingClasses[i], `Expected ${buttons[i].id} native set after classes are reapplied.`);
TKUnit.assertEqual(buttons[i].colorPropertyChangeCount, expectedChangesAfterResettingClasses[i], `Expected ${buttons[i].id} change notifications classes are reapplied.`);
}
const stack: any = page.getViewById("stack");
page.content = null;
for (let i = 0; i < buttons.length; i++) {
TKUnit.assertEqual(buttons[i].colorSetNativeCount, expectedChangesAfterResettingClasses[i], `Expected ${buttons[i].id} native set to not be called when removed from page.`);
TKUnit.assertEqual(buttons[i].colorPropertyChangeCount, expectedChangesAfterResettingClasses[i], `Expected ${buttons[i].id} change notifications for css properties to not occur when removed from page.`);
}
page.content = stack;
// TODO: The check counts here should be the same as the counts before removing from the page.
const expectedNativeSettersAfterReaddedToPage = isAndroid ? [2, 4, 4, 4] : expectedChangesAfterResettingClasses;
for (let i = 0; i < buttons.length; i++) {
TKUnit.assertEqual(buttons[i].colorSetNativeCount, expectedNativeSettersAfterReaddedToPage[i], `Expected ${buttons[i].id} native set to not be called when added to page.`);
TKUnit.assertEqual(buttons[i].colorPropertyChangeCount, expectedChangesAfterResettingClasses[i], `Expected ${buttons[i].id} change notifications for css properties to not occur when added to page.`);
}
}

View File

@ -5,6 +5,13 @@ export class Button extends button.Button {
nativeBackgroundRedraws = 0; nativeBackgroundRedraws = 0;
backgroundInternalSetNativeCount = 0; backgroundInternalSetNativeCount = 0;
fontInternalSetNativeCount = 0; fontInternalSetNativeCount = 0;
colorSetNativeCount = 0;
colorPropertyChangeCount = 0;
constructor() {
super();
this.style.on("colorChange", () => this.colorPropertyChangeCount++);
}
[view.backgroundInternalProperty.setNative](value) { [view.backgroundInternalProperty.setNative](value) {
this.backgroundInternalSetNativeCount++; this.backgroundInternalSetNativeCount++;
@ -18,5 +25,9 @@ export class Button extends button.Button {
this.nativeBackgroundRedraws++; this.nativeBackgroundRedraws++;
super._redrawNativeBackground(value); super._redrawNativeBackground(value);
} }
[view.colorProperty.setNative](value) {
this.colorSetNativeCount++;
return super[view.colorProperty.setNative](value);
}
} }
Button.prototype.recycleNativeView = "never"; Button.prototype.recycleNativeView = "never";

View File

@ -0,0 +1,15 @@
Button {
color: orange;
}
.red {
color: red;
}
.blue {
color: blue;
}
.nocolor {
background: red;
}

View File

@ -0,0 +1,8 @@
<Page xmlns:btnCount="ui/lifecycle/pages/button-counter">
<StackLayout id="stack">
<btnCount:Button id="btn1" text="one" class="nocolor" />
<btnCount:Button id="btn2" text="two" class="red" />
<btnCount:Button id="btn3" text="three" class="blue" />
<btnCount:Button id="btn4" text="four" class="red blue" />
</StackLayout>
</Page>

View File

@ -5,12 +5,10 @@ export function getNativeItemsCount(bar: segmentedBarModule.SegmentedBar): numbe
} }
export function checkNativeItemsTextColor(bar: segmentedBarModule.SegmentedBar): boolean { export function checkNativeItemsTextColor(bar: segmentedBarModule.SegmentedBar): boolean {
var isValid = true;
var attrs = (<UISegmentedControl>bar.nativeViewProtected).titleTextAttributesForState(UIControlState.Normal); var attrs = (<UISegmentedControl>bar.nativeViewProtected).titleTextAttributesForState(UIControlState.Normal);
isValid = bar.color && attrs && attrs.valueForKey(NSForegroundColorAttributeName) === bar.color.ios; var nativeViewColor = bar.color && attrs && attrs.valueForKey(NSForegroundColorAttributeName);
var barColor = bar.color.ios;
return isValid; return barColor.isEqual(nativeViewColor);
} }
export function setNativeSelectedIndex(bar: segmentedBarModule.SegmentedBar, index: number): void { export function setNativeSelectedIndex(bar: segmentedBarModule.SegmentedBar, index: number): void {

View File

@ -67,6 +67,7 @@ export function test_applies_css_changes_to_application_rules_after_page_load()
helper.buildUIAndRunTest(label1, function (views: Array<viewModule.View>) { helper.buildUIAndRunTest(label1, function (views: Array<viewModule.View>) {
application.addCss(".applicationChangedLabelAfter { color: blue; }"); application.addCss(".applicationChangedLabelAfter { color: blue; }");
label1.className = "applicationChangedLabelAfter"; label1.className = "applicationChangedLabelAfter";
console.log("IsLoaded: " + label1.isLoaded);
helper.assertViewColor(label1, "#0000FF"); helper.assertViewColor(label1, "#0000FF");
}); });
} }
@ -615,7 +616,7 @@ export function test_setInlineStyle_setsLocalValues() {
stack.addChild(testButton); stack.addChild(testButton);
helper.buildUIAndRunTest(stack, function (views: Array<viewModule.View>) { helper.buildUIAndRunTest(stack, function (views: Array<viewModule.View>) {
(<any>testButton)._applyInlineStyle("color: red;"); (<any>testButton).style = "color: red;";
helper.assertViewColor(testButton, "#FF0000"); helper.assertViewColor(testButton, "#FF0000");
}); });
} }
@ -624,7 +625,7 @@ export function test_setStyle_throws() {
const testButton = new buttonModule.Button(); const testButton = new buttonModule.Button();
TKUnit.assertThrows(function () { TKUnit.assertThrows(function () {
(<any>testButton).style = "background-color: red;"; (<any>testButton).style = {};
}, "View.style property is read-only."); }, "View.style property is read-only.");
} }

View File

@ -5,6 +5,19 @@ import * as helper from "../helper";
import * as TKUnit from "../../TKUnit"; import * as TKUnit from "../../TKUnit";
import { unsetValue } from "tns-core-modules/ui/core/view"; import { unsetValue } from "tns-core-modules/ui/core/view";
export var test_value_Inherited_after_unset = function () {
let page = helper.getCurrentPage();
page.css = "StackLayout { color: #FF0000; } .blue { color: #0000FF; }";
let btn = new button.Button();
let testStack = new stack.StackLayout();
page.content = testStack;
testStack.addChild(btn);
btn.className = "blue";
helper.assertViewColor(btn, "#0000FF");
btn.className = "";
helper.assertViewColor(btn, "#FF0000");
}
export var test_value_Inherited_stronger_than_Default = function () { export var test_value_Inherited_stronger_than_Default = function () {
let page = helper.getCurrentPage(); let page = helper.getCurrentPage();
let btn = new button.Button(); let btn = new button.Button();

View File

@ -125,12 +125,6 @@ const applyComponentCss = profile("applyComponentCss", (instance: View, moduleNa
cssApplied = true; cssApplied = true;
} }
} }
if (!cssApplied) {
// Called only to apply application css.
// If we have page css (through file or cssAttribute) we have appCss applied.
(<any>instance)._refreshCss();
}
} }
}); });
@ -211,15 +205,9 @@ export function setPropertyValue(instance: View, instanceModule: Object, exports
instance[propertyName] = exports[propertyValue]; instance[propertyName] = exports[propertyValue];
} }
else { else {
let attrHandled = false;
if (!attrHandled && instance._applyXmlAttribute) {
attrHandled = instance._applyXmlAttribute(propertyName, propertyValue);
}
if (!attrHandled) {
instance[propertyName] = propertyValue; instance[propertyName] = propertyValue;
} }
} }
}
function getBindingExpressionFromAttribute(value: string): string { function getBindingExpressionFromAttribute(value: string): string {
return value.replace("{{", "").replace("}}", "").trim(); return value.replace("{{", "").replace("}}", "").trim();

View File

@ -77,6 +77,7 @@ export class CssProperty<T extends Style, U> {
public readonly setNative: symbol; public readonly setNative: symbol;
public readonly name: string; public readonly name: string;
public readonly cssName: string; public readonly cssName: string;
public readonly cssLocalName: string;
public readonly defaultValue: U; public readonly defaultValue: U;
public register(cls: { prototype: T }): void; public register(cls: { prototype: T }): void;
public isSet(instance: T): boolean; public isSet(instance: T): boolean;
@ -92,7 +93,7 @@ export class ShorthandProperty<T extends Style, P> {
public readonly name: string; public readonly name: string;
public readonly cssName: string; public readonly cssName: string;
public register(cls: { prototype: T }): void; public register(cls: typeof Style): void;
} }
export class CssAnimationProperty<T extends Style, U> { export class CssAnimationProperty<T extends Style, U> {
@ -100,11 +101,10 @@ export class CssAnimationProperty<T extends Style, U> {
public readonly getDefault: symbol; public readonly getDefault: symbol;
public readonly setNative: symbol; public readonly setNative: symbol;
public readonly key: symbol;
public readonly name: string; public readonly name: string;
public readonly cssName: string; public readonly cssName: string;
public readonly native: symbol; public readonly cssLocalName: string;
readonly keyframe: string; readonly keyframe: string;

View File

@ -646,9 +646,10 @@ export class CssProperty<T extends Style, U> implements definitions.CssProperty<
} }
CssProperty.prototype.isStyleProperty = true; CssProperty.prototype.isStyleProperty = true;
export class CssAnimationProperty<T extends Style, U> { export class CssAnimationProperty<T extends Style, U> implements definitions.CssAnimationProperty<T, U> {
public readonly name: string; public readonly name: string;
public readonly cssName: string; public readonly cssName: string;
public readonly cssLocalName: string;
public readonly getDefault: symbol; public readonly getDefault: symbol;
public readonly setNative: symbol; public readonly setNative: symbol;
@ -682,7 +683,9 @@ export class CssAnimationProperty<T extends Style, U> {
this._valueConverter = options.valueConverter; this._valueConverter = options.valueConverter;
const cssName = "css:" + (options.cssName || propertyName); const cssLocalName = (options.cssName || propertyName);
this.cssLocalName = cssLocalName;
const cssName = "css:" + cssLocalName;
this.cssName = cssName; this.cssName = cssName;
const keyframeName = "keyframe:" + propertyName; const keyframeName = "keyframe:" + propertyName;
@ -866,8 +869,10 @@ export class InheritedCssProperty<T extends Style, U> extends CssProperty<T, U>
} }
} }
const oldValue: U = key in this ? this[key] : defaultValue;
const view = this.view; const view = this.view;
let value: U; let value: U;
let unsetNativeValue = false;
if (reset) { if (reset) {
// If unsetValue - we want to reset this property. // If unsetValue - we want to reset this property.
let parent = view.parent; let parent = view.parent;
@ -876,9 +881,12 @@ export class InheritedCssProperty<T extends Style, U> extends CssProperty<T, U>
if (style && style[sourceKey] > ValueSource.Default) { if (style && style[sourceKey] > ValueSource.Default) {
value = style[propertyName]; value = style[propertyName];
this[sourceKey] = ValueSource.Inherited; this[sourceKey] = ValueSource.Inherited;
this[key] = value;
} else { } else {
value = defaultValue; value = defaultValue;
delete this[sourceKey]; delete this[sourceKey];
delete this[key];
unsetNativeValue = true;
} }
} else { } else {
this[sourceKey] = valueSource; this[sourceKey] = valueSource;
@ -887,15 +895,13 @@ export class InheritedCssProperty<T extends Style, U> extends CssProperty<T, U>
} else { } else {
value = boxedValue; value = boxedValue;
} }
this[key] = value;
} }
const oldValue: U = key in this ? this[key] : defaultValue;
const changed: boolean = equalityComparer ? !equalityComparer(oldValue, value) : oldValue !== value; const changed: boolean = equalityComparer ? !equalityComparer(oldValue, value) : oldValue !== value;
if (changed) { if (changed) {
const view = this.view; const view = this.view;
if (reset) {
delete this[key];
if (valueChanged) { if (valueChanged) {
valueChanged(this, oldValue, value); valueChanged(this, oldValue, value);
} }
@ -906,25 +912,13 @@ export class InheritedCssProperty<T extends Style, U> extends CssProperty<T, U>
view._suspendedUpdates[propertyName] = property; view._suspendedUpdates[propertyName] = property;
} }
} else { } else {
if (unsetNativeValue) {
if (defaultValueKey in this) { if (defaultValueKey in this) {
view[setNative](this[defaultValueKey]); view[setNative](this[defaultValueKey]);
delete this[defaultValueKey]; delete this[defaultValueKey];
} else { } else {
view[setNative](defaultValue); view[setNative](defaultValue);
} }
}
}
} else {
this[key] = value;
if (valueChanged) {
valueChanged(this, oldValue, value);
}
if (view[setNative]) {
if (view._suspendNativeUpdatesCount) {
if (view._suspendedUpdates) {
view._suspendedUpdates[propertyName] = property;
}
} else { } else {
if (!(defaultValueKey in this)) { if (!(defaultValueKey in this)) {
this[defaultValueKey] = view[getDefault] ? view[getDefault]() : defaultValue; this[defaultValueKey] = view[getDefault] ? view[getDefault]() : defaultValue;
@ -981,6 +975,7 @@ export class ShorthandProperty<T extends Style, P> implements definitions.Shorth
protected readonly cssValueDescriptor: PropertyDescriptor; protected readonly cssValueDescriptor: PropertyDescriptor;
protected readonly localValueDescriptor: PropertyDescriptor; protected readonly localValueDescriptor: PropertyDescriptor;
protected readonly propertyBagDescriptor: PropertyDescriptor;
public readonly sourceKey: symbol; public readonly sourceKey: symbol;
@ -1025,19 +1020,32 @@ export class ShorthandProperty<T extends Style, P> implements definitions.Shorth
set: setLocalValue set: setLocalValue
}; };
this.propertyBagDescriptor = {
enumerable: false,
configurable: true,
set(value: string) {
converter(value).forEach(([property, value]) => {
this[property.cssLocalName] = value;
});
}
}
cssSymbolPropertyMap[key] = this; cssSymbolPropertyMap[key] = this;
} }
public register(cls: { prototype: T }): void { public register(cls: typeof Style): void {
if (this.registered) { if (this.registered) {
throw new Error(`Property ${this.name} already registered.`); throw new Error(`Property ${this.name} already registered.`);
} }
this.registered = true; this.registered = true;
Object.defineProperty(cls.prototype, this.name, this.localValueDescriptor); Object.defineProperty(cls.prototype, this.name, this.localValueDescriptor);
Object.defineProperty(cls.prototype, this.cssName, this.cssValueDescriptor); Object.defineProperty(cls.prototype, this.cssName, this.cssValueDescriptor);
if (this.cssLocalName !== this.cssName) { if (this.cssLocalName !== this.cssName) {
Object.defineProperty(cls.prototype, this.cssLocalName, this.localValueDescriptor); Object.defineProperty(cls.prototype, this.cssLocalName, this.localValueDescriptor);
} }
Object.defineProperty(cls.prototype.PropertyBag, this.cssLocalName, this.propertyBagDescriptor);
} }
} }

View File

@ -167,11 +167,6 @@ export abstract class ViewBase extends Observable {
*/ */
public className: string; public className: string;
/**
* Gets or sets inline style selectors for this view.
*/
public inlineStyleSelector: SelectorCore;
/** /**
* Gets owner page. This is a read-only property. * Gets owner page. This is a read-only property.
*/ */
@ -220,15 +215,22 @@ export abstract class ViewBase extends Observable {
_domId: number; _domId: number;
_cssState: any /* "ui/styling/style-scope" */; _cssState: any /* "ui/styling/style-scope" */;
_setCssState(next: any /* "ui/styling/style-scope" */); /**
_registerAnimation(animation: KeyframeAnimation); * @private
_unregisterAnimation(animation: KeyframeAnimation); * Notifies each child's css state for change, recursively.
_cancelAllAnimations(); * Either the style scope, className or id properties were changed.
*/
_onCssStateChange(): void;
public cssClasses: Set<string>; public cssClasses: Set<string>;
public cssPseudoClasses: Set<string>; public cssPseudoClasses: Set<string>;
public _goToVisualState(state: string): void; public _goToVisualState(state: string): void;
/**
* This used to be the way to set attribute values in early {N} versions.
* Now attributes are expected to be set as plain properties on the view instances.
* @deprecated
*/
public _applyXmlAttribute(attribute, value): boolean; public _applyXmlAttribute(attribute, value): boolean;
public setInlineStyle(style: string): void; public setInlineStyle(style: string): void;
@ -293,7 +295,7 @@ export abstract class ViewBase extends Observable {
/** /**
* @protected * @protected
* @unstable * @unstable
* A widget can call this method to discard mathing css pseudo class. * A widget can call this method to discard matching css pseudo class.
*/ */
public deletePseudoClass(name: string): void; public deletePseudoClass(name: string): void;
@ -331,6 +333,11 @@ export abstract class ViewBase extends Observable {
* @private * @private
*/ */
_setupAsRootView(context: any): void; _setupAsRootView(context: any): void;
/**
* @private
*/
_inheritStyleScope(styleScope: any /* StyleScope */): void;
//@endprivate //@endprivate
} }

View File

@ -138,9 +138,7 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
private _androidView: Object; private _androidView: Object;
private _style: Style; private _style: Style;
private _isLoaded: boolean; private _isLoaded: boolean;
private _registeredAnimations: Array<KeyframeAnimation>;
private _visualState: string; private _visualState: string;
private _inlineStyleSelector: SelectorCore;
private __nativeView: any; private __nativeView: any;
// private _disableNativeViewRecycling: boolean; // private _disableNativeViewRecycling: boolean;
public domNode: DOMNode; public domNode: DOMNode;
@ -157,7 +155,7 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
public _domId: number; public _domId: number;
public _context: any; public _context: any;
public _isAddedToNativeVisualTree: boolean; public _isAddedToNativeVisualTree: boolean;
public _cssState: ssm.CssState; public _cssState: ssm.CssState = new ssm.CssState(this);
public _styleScope: ssm.StyleScope; public _styleScope: ssm.StyleScope;
public _suspendedUpdates: { [propertyName: string]: Property<ViewBase, any> | CssProperty<Style, any> | CssAnimationProperty<Style, any> }; public _suspendedUpdates: { [propertyName: string]: Property<ViewBase, any> | CssProperty<Style, any> | CssAnimationProperty<Style, any> };
public _suspendNativeUpdatesCount: number; public _suspendNativeUpdatesCount: number;
@ -229,9 +227,13 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
get style(): Style { get style(): Style {
return this._style; return this._style;
} }
set style(value) { set style(inlineStyle: Style /* | string */) {
if (typeof inlineStyle === "string") {
this.setInlineStyle(inlineStyle);
} else {
throw new Error("View.style property is read-only."); throw new Error("View.style property is read-only.");
} }
}
get android(): any { get android(): any {
// this._disableNativeViewRecycling = true; // this._disableNativeViewRecycling = true;
@ -254,13 +256,6 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
this.className = v; this.className = v;
} }
get inlineStyleSelector(): SelectorCore {
return this._inlineStyleSelector;
}
set inlineStyleSelector(value: SelectorCore) {
this._inlineStyleSelector = value;
}
getViewById<T extends ViewBaseDefinition>(id: string): T { getViewById<T extends ViewBaseDefinition>(id: string): T {
return <T>getViewById(this, id); return <T>getViewById(this, id);
} }
@ -288,6 +283,7 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
@profile @profile
public onLoaded() { public onLoaded() {
this._isLoaded = true; this._isLoaded = true;
this._cssState.onLoaded();
this._resumeNativeUpdates(); this._resumeNativeUpdates();
this._loadEachChild(); this._loadEachChild();
this._emit("loaded"); this._emit("loaded");
@ -305,6 +301,7 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
this._suspendNativeUpdates(); this._suspendNativeUpdates();
this._unloadEachChild(); this._unloadEachChild();
this._isLoaded = false; this._isLoaded = false;
this._cssState.onUnloaded();
this._emit("unloaded"); this._emit("unloaded");
} }
@ -336,101 +333,10 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
}); });
} }
@profile
public _applyStyleFromScope() {
const scope = this._styleScope;
if (scope) {
scope.applySelectors(this);
} else {
this._setCssState(null);
}
}
// TODO: Make sure the state is set to null and this is called on unloaded to clean up change listeners...
@profile
_setCssState(next: ssm.CssState): void {
const previous = this._cssState;
this._cssState = next;
if (!this._invalidateCssHandler) {
this._invalidateCssHandler = () => {
if (this._invalidateCssHandlerSuspended) {
return;
}
this.applyCssState();
};
}
try {
this._invalidateCssHandlerSuspended = true;
if (next) {
next.changeMap.forEach((changes, view) => {
if (changes.attributes) {
changes.attributes.forEach(attribute => {
view.addEventListener(attribute + "Change", this._invalidateCssHandler);
});
}
if (changes.pseudoClasses) {
changes.pseudoClasses.forEach(pseudoClass => {
let eventName = ":" + pseudoClass;
view.addEventListener(":" + pseudoClass, this._invalidateCssHandler);
if (view[eventName]) {
view[eventName](+1);
}
});
}
});
}
if (previous) {
previous.changeMap.forEach((changes, view) => {
if (changes.attributes) {
changes.attributes.forEach(attribute => {
view.removeEventListener("onPropertyChanged:" + attribute, this._invalidateCssHandler);
});
}
if (changes.pseudoClasses) {
changes.pseudoClasses.forEach(pseudoClass => {
let eventName = ":" + pseudoClass;
view.removeEventListener(eventName, this._invalidateCssHandler);
if (view[eventName]) {
view[eventName](-1);
}
});
}
});
}
} finally {
this._invalidateCssHandlerSuspended = false;
}
this.applyCssState();
}
private notifyPseudoClassChanged(pseudoClass: string): void { private notifyPseudoClassChanged(pseudoClass: string): void {
this.notify({ eventName: ":" + pseudoClass, object: this }); this.notify({ eventName: ":" + pseudoClass, object: this });
} }
/**
* Notify that some attributes or pseudo classes that may affect the current CssState had changed.
*/
private _invalidateCssHandler;
private _invalidateCssHandlerSuspended: boolean;
@profile
private applyCssState(): void {
this._batchUpdate(() => {
if (!this._cssState) {
this._cancelAllAnimations();
resetCSSProperties(this.style);
return;
}
this._cssState.apply();
});
}
private pseudoClassAliases = { private pseudoClassAliases = {
'highlighted': [ 'highlighted': [
'active', 'active',
@ -474,19 +380,6 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
} }
} }
@profile
private _applyInlineStyle(inlineStyle) {
if (typeof inlineStyle === "string") {
try {
// this.style._beginUpdate();
ensureStyleScopeModule();
styleScopeModule.applyInlineStyle(this, inlineStyle);
} finally {
// this.style._endUpdate();
}
}
}
private bindingContextChanged(data: PropertyChangeData): void { private bindingContextChanged(data: PropertyChangeData): void {
this.bindings.get("bindingContext").bind(data.value); this.bindings.get("bindingContext").bind(data.value);
} }
@ -584,24 +477,9 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
} }
} }
@profile
private _setStyleScope(scope: ssm.StyleScope): void {
this._styleScope = scope;
this._applyStyleFromScope();
this.eachChild((v) => {
v._setStyleScope(scope);
return true;
});
}
public _addViewCore(view: ViewBase, atIndex?: number) { public _addViewCore(view: ViewBase, atIndex?: number) {
propagateInheritableProperties(this, view); propagateInheritableProperties(this, view);
view._inheritStyleScope(this._styleScope);
const styleScope = this._styleScope;
if (styleScope) {
view._setStyleScope(styleScope);
}
propagateInheritableCssProperties(this.style, view.style); propagateInheritableCssProperties(this.style, view.style);
if (this._context) { if (this._context) {
@ -638,17 +516,10 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
* Method is intended to be overridden by inheritors and used as "protected" * Method is intended to be overridden by inheritors and used as "protected"
*/ */
public _removeViewCore(view: ViewBase) { public _removeViewCore(view: ViewBase) {
// TODO: Discuss this.
if (this._styleScope === view._styleScope) {
view._setStyleScope(null);
}
if (view.isLoaded) { if (view.isLoaded) {
view.onUnloaded(); view.onUnloaded();
} }
// view.unsetInheritedProperties();
if (view._context) { if (view._context) {
view._tearDownUI(); view._tearDownUI();
} }
@ -663,10 +534,7 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
} }
public initNativeView(): void { public initNativeView(): void {
// No initNativeView(this)? //
if (this._cssState) {
this._cssState.playPendingKeyframeAnimations();
}
} }
public resetNativeView(): void { public resetNativeView(): void {
@ -688,9 +556,9 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
// } // }
// } // }
if (this._cssState) { // if (this._cssState) {
this._cancelAllAnimations(); // this._cancelAllAnimations();
} // }
} }
_setupAsRootView(context: any): void { _setupAsRootView(context: any): void {
@ -895,12 +763,12 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
this.addPseudoClass(state); this.addPseudoClass(state);
} }
public _applyXmlAttribute(attribute, value): boolean { /**
if (attribute === "style") { * This used to be the way to set attribute values in early {N} versions.
this._applyInlineStyle(value); * Now attributes are expected to be set as plain properties on the view instances.
return true; * @deprecated
} */
public _applyXmlAttribute(): boolean {
return false; return false;
} }
@ -909,7 +777,8 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
throw new Error("Parameter should be valid CSS string!"); throw new Error("Parameter should be valid CSS string!");
} }
this._applyInlineStyle(style); ensureStyleScopeModule();
styleScopeModule.applyInlineStyle(this, style);
} }
public _parentChanged(oldParent: ViewBase): void { public _parentChanged(oldParent: ViewBase): void {
@ -932,30 +801,6 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
initNativeView(this); initNativeView(this);
} }
public _registerAnimation(animation: KeyframeAnimation) {
if (this._registeredAnimations === undefined) {
this._registeredAnimations = new Array<KeyframeAnimation>();
}
this._registeredAnimations.push(animation);
}
public _unregisterAnimation(animation: KeyframeAnimation) {
if (this._registeredAnimations) {
let index = this._registeredAnimations.indexOf(animation);
if (index >= 0) {
this._registeredAnimations.splice(index, 1);
}
}
}
public _cancelAllAnimations() {
if (this._registeredAnimations) {
for (let animation of this._registeredAnimations) {
animation.cancel();
}
}
}
public toString(): string { public toString(): string {
let str = this.typeName; let str = this.typeName;
if (this.id) { if (this.id) {
@ -970,6 +815,25 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
return str; return str;
} }
_onCssStateChange(): void {
this._cssState.onChange();
eachDescendant(this, (child: ViewBase) => {
child._cssState.onChange();
return true;
});
}
_inheritStyleScope(styleScope: ssm.StyleScope): void {
if (this._styleScope !== styleScope) {
this._styleScope = styleScope;
this._onCssStateChange();
this.eachChild(child => {
child._inheritStyleScope(styleScope);
return true
});
}
}
} }
ViewBase.prototype.isCollapsed = false; ViewBase.prototype.isCollapsed = false;
@ -1019,20 +883,12 @@ export const classNameProperty = new Property<ViewBase, string>({
if (typeof newValue === "string") { if (typeof newValue === "string") {
newValue.split(" ").forEach(c => classes.add(c)); newValue.split(" ").forEach(c => classes.add(c));
} }
resetStyles(view); view._onCssStateChange();
} }
}); });
classNameProperty.register(ViewBase); classNameProperty.register(ViewBase);
function resetStyles(view: ViewBase): void { export const idProperty = new Property<ViewBase, string>({ name: "id", valueChanged: (view, oldValue, newValue) => view._onCssStateChange() });
view._applyStyleFromScope();
view.eachChild((child) => {
resetStyles(child);
return true;
});
}
export const idProperty = new Property<ViewBase, string>({ name: "id", valueChanged: (view, oldValue, newValue) => resetStyles(view) });
idProperty.register(ViewBase); idProperty.register(ViewBase);
export function booleanConverter(v: string): boolean { export function booleanConverter(v: string): boolean {

View File

@ -100,7 +100,6 @@ export class View extends ViewCommon {
this.nativeViewProtected.setClickable(this._isClickable); this.nativeViewProtected.setClickable(this._isClickable);
} }
this._cancelAllAnimations();
super.onUnloaded(); super.onUnloaded();
} }

View File

@ -67,7 +67,7 @@ export interface Size {
* This class is the base class for all UI components. * This class is the base class for all UI components.
* A View occupies a rectangular area on the screen and is responsible for drawing and layouting of all UI components within. * A View occupies a rectangular area on the screen and is responsible for drawing and layouting of all UI components within.
*/ */
export abstract class View extends ViewBase implements ApplyXmlAttributes { export abstract class View extends ViewBase {
/** /**
* Gets the android-specific native instance that lies behind this proxy. Will be available if running on an Android platform. * Gets the android-specific native instance that lies behind this proxy. Will be available if running on an Android platform.
*/ */
@ -83,8 +83,6 @@ export abstract class View extends ViewBase implements ApplyXmlAttributes {
*/ */
bindingContext: any; bindingContext: any;
//----------Style property shortcuts----------
/** /**
* Gets or sets the border color of the view. * Gets or sets the border color of the view.
*/ */
@ -413,12 +411,6 @@ export abstract class View extends ViewBase implements ApplyXmlAttributes {
*/ */
public focus(): boolean; public focus(): boolean;
/**
* Sets in-line CSS string as style.
* @param style - In-line CSS string.
*/
public setInlineStyle(style: string): void;
public getGestureObservers(type: GestureTypes): Array<GesturesObserver>; public getGestureObservers(type: GestureTypes): Array<GesturesObserver>;
/** /**
@ -490,7 +482,6 @@ export abstract class View extends ViewBase implements ApplyXmlAttributes {
_eachLayoutView(callback: (View) => void): void; _eachLayoutView(callback: (View) => void): void;
public _applyXmlAttribute(attribute: string, value: any): boolean;
public eachChildView(callback: (view: View) => boolean): void; public eachChildView(callback: (view: View) => boolean): void;
//@private //@private
@ -647,19 +638,6 @@ export interface AddChildFromBuilder {
_addChildFromBuilder(name: string, value: any): void; _addChildFromBuilder(name: string, value: any): void;
} }
/**
* Defines an interface used to create a member of a class from string representation (used in xml declaration).
*/
export interface ApplyXmlAttributes {
/**
* Called for every attribute in xml declaration. <... fontWeight="bold" ../>
* @param attributeName - the name of the attribute (fontAttributes)
* @param attrValue - the value of the attribute (bold)
* Should return true if this attribute is handled and there is no need default handler to process it.
*/
_applyXmlAttribute(attributeName: string, attrValue: any): boolean;
}
export const automationTextProperty: Property<View, string>; export const automationTextProperty: Property<View, string>;
export const originXProperty: Property<View, number>; export const originXProperty: Property<View, number>;
export const originYProperty: Property<View, number>; export const originYProperty: Property<View, number>;

View File

@ -49,9 +49,9 @@ export function getCurrentPage(): Page {
function applySelectors(view: View) { function applySelectors(view: View) {
let currentPage = getCurrentPage(); let currentPage = getCurrentPage();
if (currentPage) { if (currentPage) {
let styleScope = currentPage._getStyleScope(); let styleScope = currentPage._styleScope;
if (styleScope) { if (styleScope) {
styleScope.applySelectors(view); styleScope.matchSelectors(view);
} }
} }
} }

View File

@ -89,7 +89,6 @@ const entryCreatePage = profile("entry.create", (entry: NavigationEntry): Page =
throw new Error("Failed to create Page with entry.create() function."); throw new Error("Failed to create Page with entry.create() function.");
} }
page._refreshCss();
return page; return page;
}); });

View File

@ -281,28 +281,15 @@ export class GridLayoutBase extends LayoutBase implements GridLayoutDefinition {
this.requestLayout(); this.requestLayout();
} }
_applyXmlAttribute(attributeName: string, attributeValue: any): boolean { set rows(value: string) {
if (attributeName === "columns") {
this._setColumns(attributeValue);
return true;
}
else if (attributeName === "rows") {
this._setRows(attributeValue);
return true;
}
return super._applyXmlAttribute(attributeName, attributeValue);
}
private _setColumns(value: string) {
this.removeColumns();
parseAndAddItemSpecs(value, (spec: ItemSpec) => this.addColumn(spec));
}
private _setRows(value: string) {
this.removeRows(); this.removeRows();
parseAndAddItemSpecs(value, (spec: ItemSpec) => this.addRow(spec)); parseAndAddItemSpecs(value, (spec: ItemSpec) => this.addRow(spec));
} }
set columns(value: string) {
this.removeColumns();
parseAndAddItemSpecs(value, (spec: ItemSpec) => this.addColumn(spec));
}
} }
GridLayoutBase.prototype.recycleNativeView = "auto"; GridLayoutBase.prototype.recycleNativeView = "auto";

View File

@ -22,14 +22,12 @@ export class PageBase extends ContentView implements PageDefinition {
public static showingModallyEvent = "showingModally"; public static showingModallyEvent = "showingModally";
protected _closeModalCallback: Function; protected _closeModalCallback: Function;
private _modalContext: any;
private _modalContext: any;
private _navigationContext: any; private _navigationContext: any;
private _actionBar: ActionBar; private _actionBar: ActionBar;
private _cssAppliedVersion: number;
public _styleScope: StyleScope; // same as in ViewBase, but strongly typed
public _modal: PageBase; public _modal: PageBase;
public _fragmentTag: string; public _fragmentTag: string;
@ -51,8 +49,7 @@ export class PageBase extends ContentView implements PageDefinition {
} }
set css(value: string) { set css(value: string) {
this._styleScope.css = value; this._styleScope.css = value;
this._cssFiles = {}; this._onCssStateChange();
this._refreshCss();
} }
get actionBar(): ActionBar { get actionBar(): ActionBar {
@ -94,59 +91,14 @@ export class PageBase extends ContentView implements PageDefinition {
return this; return this;
} }
@profile
public onLoaded(): void {
this._refreshCss();
super.onLoaded();
}
public onUnloaded() {
const styleScope = this._styleScope;
super.onUnloaded();
this._styleScope = styleScope;
}
public addCss(cssString: string): void { public addCss(cssString: string): void {
this._addCssInternal(cssString); this._styleScope.addCss(cssString);
this._onCssStateChange();
} }
private _addCssInternal(cssString: string, cssFileName?: string): void {
this._styleScope.addCss(cssString, cssFileName);
this._refreshCss();
}
private _cssFiles = {};
public addCssFile(cssFileName: string) { public addCssFile(cssFileName: string) {
if (cssFileName.indexOf("~/") === 0) { this._styleScope.addCssFile(cssFileName);
cssFileName = path.join(knownFolders.currentApp().path, cssFileName.replace("~/", "")); this._onCssStateChange();
}
if (!this._cssFiles[cssFileName]) {
if (File.exists(cssFileName)) {
const file = File.fromPath(cssFileName);
const text = file.readTextSync();
if (text) {
this._addCssInternal(text, cssFileName);
this._cssFiles[cssFileName] = true;
}
}
}
}
// Used in component-builder.ts
public _refreshCss(): void {
const scopeVersion = this._styleScope.ensureSelectors();
if (scopeVersion !== this._cssAppliedVersion) {
const styleScope = this._styleScope;
this._resetCssValues();
const checkSelectors = (view: View): boolean => {
styleScope.applySelectors(view);
return true;
};
checkSelectors(this);
eachDescendant(this, checkSelectors);
this._cssAppliedVersion = scopeVersion;
}
} }
public getKeyframeAnimationWithName(animationName: string): KeyframeAnimationInfo { public getKeyframeAnimationWithName(animationName: string): KeyframeAnimationInfo {
@ -275,10 +227,6 @@ export class PageBase extends ContentView implements PageDefinition {
this.notify(args); this.notify(args);
} }
public _getStyleScope(): StyleScope {
return this._styleScope;
}
public eachChildView(callback: (child: View) => boolean) { public eachChildView(callback: (child: View) => boolean) {
super.eachChildView(callback); super.eachChildView(callback);
callback(this.actionBar); callback(this.actionBar);
@ -288,17 +236,8 @@ export class PageBase extends ContentView implements PageDefinition {
return (this.content ? 1 : 0) + (this.actionBar ? 1 : 0); return (this.content ? 1 : 0) + (this.actionBar ? 1 : 0);
} }
private _resetCssValues() { _inheritStyleScope(styleScope: StyleScope): void {
const resetCssValuesFunc = (view: View): boolean => { // The Page have its own scope.
view._batchUpdate(() => {
view._cancelAllAnimations();
resetCSSProperties(view.style);
});
return true;
};
resetCssValuesFunc(this);
eachDescendant(this, resetCssValuesFunc);
} }
} }

View File

@ -254,15 +254,6 @@ export class Page extends ContentView {
* @param isBackNavigation - True if the Page is being navigated from using the Frame.goBack() method, false otherwise. * @param isBackNavigation - True if the Page is being navigated from using the Frame.goBack() method, false otherwise.
*/ */
public onNavigatedFrom(isBackNavigation: boolean): void; public onNavigatedFrom(isBackNavigation: boolean): void;
/**
* @private
*/
_refreshCss(): void;
/**
* @private
*/
_getStyleScope(): styleScope.StyleScope;
//@endprivate //@endprivate
} }

View File

@ -1,7 +1,7 @@
import { Keyframes } from "../animation/keyframe-animation"; import { Keyframes } from "../animation/keyframe-animation";
import { ViewBase } from "../core/view-base"; import { ViewBase } from "../core/view-base";
import { View } from "../core/view"; import { View } from "../core/view";
import { resetCSSProperties } from "../core/properties"; import { unsetValue } from "../core/properties";
import { import {
SyntaxTree, SyntaxTree,
Keyframes as KeyframesDefinition, Keyframes as KeyframesDefinition,
@ -56,9 +56,101 @@ const applicationKeyframes: any = {};
const animationsSymbol: symbol = Symbol("animations"); const animationsSymbol: symbol = Symbol("animations");
const pattern: RegExp = /('|")(.*?)\1/; const pattern: RegExp = /('|")(.*?)\1/;
class CSSSource {
private _selectors: RuleSet[] = [];
private _ast: SyntaxTree;
private static cssFilesCache: { [path: string]: CSSSource } = {};
private constructor(private _url: string, private _file: string, private _keyframes: KeyframesMap, private _source?: string) {
if (this._file && !this._source) {
this.load();
}
this.parse();
}
public static fromFile(url: string, keyframes: KeyframesMap): CSSSource {
const app = knownFolders.currentApp().path;
const file = resolveFileNameFromUrl(url, app, File.exists);
return new CSSSource(url, file, keyframes, undefined);
}
public static fromSource(source: string, keyframes: KeyframesMap, url?: string): CSSSource {
return new CSSSource(url, undefined, keyframes, source);
}
get selectors(): RuleSet[] { return this._selectors; }
get source(): string { return this._source; }
@profile
private load(): void {
const file = File.fromPath(this._file);
this._source = file.readTextSync();
}
@profile
private parse(): void {
if (this._source) {
try {
this._ast = this._source ? parseCss(this._source, { source: this._file }) : null;
// TODO: Don't merge arrays, instead chain the css files.
if (this._ast) {
this._selectors = [
...this.createSelectorsFromImports(),
...this.createSelectorsFromSyntaxTree()
];
}
} catch (e) {
traceWrite("Css styling failed: " + e, traceCategories.Error, traceMessageType.error);
}
} else {
this._selectors = [];
}
}
private createSelectorsFromImports(): RuleSet[] {
let selectors: RuleSet[] = [];
const imports = this._ast["stylesheet"]["rules"].filter(r => r.type === "import");
for (let i = 0; i < imports.length; i++) {
const importItem = imports[i]["import"];
const match = importItem && (<string>importItem).match(pattern);
const url = match && match[2];
if (url !== null && url !== undefined) {
const cssFile = CSSSource.fromFile(url, this._keyframes);
selectors = selectors.concat(cssFile.selectors);
}
}
return selectors;
}
private createSelectorsFromSyntaxTree(): RuleSet[] {
const nodes = this._ast.stylesheet.rules;
(<KeyframesDefinition[]>nodes.filter(isKeyframe)).forEach(node => this._keyframes[node.name] = node);
const rulesets = fromAstNodes(nodes);
if (rulesets && rulesets.length) {
ensureCssAnimationParserModule();
rulesets.forEach(rule => {
rule[animationsSymbol] = cssAnimationParserModule.CssAnimationParser
.keyframeAnimationsFromCSSDeclarations(rule.declarations);
});
}
return rulesets;
}
toString(): string {
return this._file || this._url || "(in-memory)";
}
}
const onCssChanged = profile('"style-scope".onCssChanged', (args: application.CssChangedEventData) => { const onCssChanged = profile('"style-scope".onCssChanged', (args: application.CssChangedEventData) => {
if (args.cssText) { if (args.cssText) {
const parsed = createSelectorsFromCss(args.cssText, args.cssFile, applicationKeyframes); const parsed = CSSSource.fromSource(args.cssText, applicationKeyframes, args.cssFile).selectors;
if (parsed) { if (parsed) {
applicationAdditionalSelectors.push.apply(applicationAdditionalSelectors, parsed); applicationAdditionalSelectors.push.apply(applicationAdditionalSelectors, parsed);
mergeCssSelectors(); mergeCssSelectors();
@ -72,23 +164,16 @@ function onLiveSync(args: application.CssChangedEventData): void {
loadCss(application.getCssFileName()); loadCss(application.getCssFileName());
} }
const loadCss = profile(`"style-scope".loadCss`, (cssFile?: string) => { const loadCss = profile(`"style-scope".loadCss`, (cssFile: string) => {
if (!cssFile) { if (!cssFile) {
return undefined; return undefined;
} }
let result: RuleSet[]; const result = CSSSource.fromFile(cssFile, applicationKeyframes).selectors;
if (result.length > 0) {
const cssFileName = path.join(knownFolders.currentApp().path, cssFile);
if (File.exists(cssFileName)) {
const file = File.fromPath(cssFileName);
const applicationCss = file.readTextSync();
if (applicationCss) {
result = createSelectorsFromCss(applicationCss, cssFileName, applicationKeyframes);
applicationSelectors = result; applicationSelectors = result;
mergeCssSelectors(); mergeCssSelectors();
} }
}
}); });
application.on("cssChanged", onCssChanged); application.on("cssChanged", onCssChanged);
@ -106,69 +191,186 @@ if (application.hasLaunched()) {
} }
export class CssState { export class CssState {
private _pendingKeyframeAnimations: SelectorCore[]; static emptyChangeMap: Readonly<ChangeMap<ViewBase>> = Object.freeze(new Map());
static emptyPropertyBag: Readonly<{}> = Object.freeze({});
static emptyAnimationArray: ReadonlyArray<kam.KeyframeAnimation> = Object.freeze([]);
static emptyMatch: Readonly<SelectorsMatch<ViewBase>> = { selectors: [], changeMap: new Map() };
constructor(private view: ViewBase, private match: SelectorsMatch<ViewBase>) { _onDynamicStateChangeHandler: () => void;
_appliedChangeMap: Readonly<ChangeMap<ViewBase>>;
_appliedPropertyValues: Readonly<{}>;
_appliedAnimations: ReadonlyArray<kam.KeyframeAnimation>;
_match: SelectorsMatch<ViewBase>;
_matchInvalid: boolean;
constructor(private view: ViewBase) {
this._onDynamicStateChangeHandler = () => this.updateDynamicState();
} }
public get changeMap(): ChangeMap<ViewBase> { /**
return this.match.changeMap; * Called when a change had occurred that may invalidate the statically matching selectors (class, id, ancestor selectors).
} * As a result, at some point in time, the selectors matched have to be requerried from the style scope and applied to the view.
*/
public apply(): void { public onChange(): void {
this.view._cancelAllAnimations(); if (this.view.isLoaded) {
resetCSSProperties(this.view.style); this.unsubscribeFromDynamicUpdates();
this.updateMatch();
let matchingSelectors = this.match.selectors.filter(sel => sel.dynamic ? sel.match(this.view) : true); this.subscribeForDynamicUpdates();
if (this.view.inlineStyleSelector) { this.updateDynamicState();
matchingSelectors.push(this.view.inlineStyleSelector);
}
matchingSelectors.forEach(s => this.applyDescriptors(s.ruleset));
this._pendingKeyframeAnimations = matchingSelectors;
this.playPendingKeyframeAnimations();
}
public playPendingKeyframeAnimations() {
if (this._pendingKeyframeAnimations && this.view.nativeViewProtected) {
this._pendingKeyframeAnimations.forEach(s => this.playKeyframeAnimationsFromRuleSet(s.ruleset));
this._pendingKeyframeAnimations = null;
}
}
private applyDescriptors(ruleset: RuleSet): void {
let style = this.view.style;
ruleset.declarations.forEach(d => {
try {
// Use the "css:" prefixed name, so that CSS value source is set.
let cssPropName = `css:${d.property}`;
if (cssPropName in style) {
style[cssPropName] = d.value;
} else { } else {
this.view[d.property] = d.value; this._matchInvalid = true;
} }
} catch (e) {
traceWrite(`Failed to apply property [${d.property}] with value [${d.value}] to ${this.view}. ${e}`, traceCategories.Error, traceMessageType.error);
}
});
} }
private playKeyframeAnimationsFromRuleSet(ruleset: RuleSet): void { public onLoaded(): void {
let ruleAnimations: kam.KeyframeAnimationInfo[] = ruleset[animationsSymbol]; if (this._matchInvalid) {
this.updateMatch();
}
this.subscribeForDynamicUpdates();
this.updateDynamicState();
}
public onUnloaded(): void {
this.unsubscribeFromDynamicUpdates();
}
@profile
private updateMatch() {
this._match = this.view._styleScope ? this.view._styleScope.matchSelectors(this.view) : CssState.emptyMatch;
this._matchInvalid = false;
}
@profile
private updateDynamicState(): void {
const matchingSelectors = this._match.selectors.filter(sel => sel.dynamic ? sel.match(this.view) : true);
this.stopKeyframeAnimations();
this.setPropertyValues(matchingSelectors);
this.playKeyframeAnimations(matchingSelectors);
}
private playKeyframeAnimations(matchingSelectors: SelectorCore[]): void {
const animations: kam.KeyframeAnimation[] = [];
matchingSelectors.forEach(selector => {
let ruleAnimations: kam.KeyframeAnimationInfo[] = selector.ruleset[animationsSymbol];
if (ruleAnimations) { if (ruleAnimations) {
ensureKeyframeAnimationModule(); ensureKeyframeAnimationModule();
for (let animationInfo of ruleAnimations) { for (let animationInfo of ruleAnimations) {
let animation = keyframeAnimationModule.KeyframeAnimation.keyframeAnimationFromInfo(animationInfo); let animation = keyframeAnimationModule.KeyframeAnimation.keyframeAnimationFromInfo(animationInfo);
if (animation) { if (animation) {
this.view._registerAnimation(animation); animations.push(animation);
animation.play(<View>this.view)
.then(() => { this.view._unregisterAnimation(animation); })
.catch((e) => { this.view._unregisterAnimation(animation); });
} }
} }
} }
});
animations.forEach(animation => animation.play(<View>this.view));
Object.freeze(animations);
this._appliedAnimations = animations;
}
private stopKeyframeAnimations(): void {
this._appliedAnimations
.filter(animation => animation.isPlaying)
.forEach(animation => animation.cancel());
this._appliedAnimations = CssState.emptyAnimationArray;
}
/**
* Calculate the difference between the previously applied property values,
* and the new set of property values that have to be applied for the provided selectors.
* Apply the values and ensure each property setter is called at most once to avoid excessive change notifications.
* @param matchingSelectors
*/
private setPropertyValues(matchingSelectors: SelectorCore[]): void {
const newPropertyValues = new this.view.style.PropertyBag();
matchingSelectors.forEach(selector =>
selector.ruleset.declarations.forEach(declaration =>
newPropertyValues[declaration.property] = declaration.value));
Object.freeze(newPropertyValues);
this.view._batchUpdate(() => {
const oldProperties = this._appliedPropertyValues;
for(const key in oldProperties) {
if (!(key in newPropertyValues)) {
if (key in this.view.style) {
this.view.style[`css:${key}`] = unsetValue;
} else {
// TRICKY: How do we unset local value?
} }
} }
}
for(const property in newPropertyValues) {
if (oldProperties && property in oldProperties && oldProperties[property] === newPropertyValues[property]) {
continue;
}
const value = newPropertyValues[property];
try {
if (property in this.view.style) {
this.view.style[`css:${property}`] = value;
} else {
this.view[property] = value;
}
} catch (e) {
traceWrite(`Failed to apply property [${property}] with value [${value}] to ${this.view}. ${e}`, traceCategories.Error, traceMessageType.error);
}
}
});
this._appliedPropertyValues = newPropertyValues;
}
private subscribeForDynamicUpdates(): void {
const changeMap = this._match.changeMap;
changeMap.forEach((changes, view) => {
if (changes.attributes) {
changes.attributes.forEach(attribute => {
view.addEventListener(attribute + "Change", this._onDynamicStateChangeHandler);
});
}
if (changes.pseudoClasses) {
changes.pseudoClasses.forEach(pseudoClass => {
let eventName = ":" + pseudoClass;
view.addEventListener(":" + pseudoClass, this._onDynamicStateChangeHandler);
if (view[eventName]) {
view[eventName](+1);
}
});
}
});
this._appliedChangeMap = changeMap;
}
private unsubscribeFromDynamicUpdates(): void {
this._appliedChangeMap.forEach((changes, view) => {
if (changes.attributes) {
changes.attributes.forEach(attribute => {
view.removeEventListener("onPropertyChanged:" + attribute, this._onDynamicStateChangeHandler);
});
}
if (changes.pseudoClasses) {
changes.pseudoClasses.forEach(pseudoClass => {
let eventName = ":" + pseudoClass;
view.removeEventListener(eventName, this._onDynamicStateChangeHandler);
if (view[eventName]) {
view[eventName](-1);
}
});
}
});
this._appliedChangeMap = CssState.emptyChangeMap;
}
toString(): string {
return `${this.view}._cssState`;
}
}
CssState.prototype._appliedChangeMap = CssState.emptyChangeMap;
CssState.prototype._appliedPropertyValues = CssState.emptyPropertyBag;
CssState.prototype._appliedAnimations = CssState.emptyAnimationArray;
CssState.prototype._matchInvalid = true;
export class StyleScope { export class StyleScope {
@ -200,25 +402,31 @@ export class StyleScope {
this.appendCss(cssString, cssFileName) this.appendCss(cssString, cssFileName)
} }
public addCssFile(cssFileName: string): void {
this.appendCss(null, cssFileName);
}
@profile @profile
private setCss(cssString: string, cssFileName?): void { private setCss(cssString: string, cssFileName?): void {
this._css = cssString; this._css = cssString;
this._reset(); this._reset();
this._localCssSelectors = createSelectorsFromCss(this._css, cssFileName, this._keyframes);
const cssFile = CSSSource.fromSource(cssString, this._keyframes, cssFileName);
this._localCssSelectors = cssFile.selectors;
this._localCssSelectorVersion++; this._localCssSelectorVersion++;
this.ensureSelectors(); this.ensureSelectors();
} }
@profile @profile
private appendCss(cssString: string, cssFileName?): void { private appendCss(cssString: string, cssFileName?): void {
if (!cssString) { if (!cssString && !cssFileName) {
return; return;
} }
this._css = this._css + cssString;
this._reset(); this._reset();
let parsedCssSelectors = createSelectorsFromCss(cssString, cssFileName, this._keyframes); let parsedCssSelectors = cssString ? CSSSource.fromSource(cssString, this._keyframes, cssFileName) : CSSSource.fromFile(cssFileName, this._keyframes);
this._localCssSelectors.push.apply(this._localCssSelectors, parsedCssSelectors); this._css = this._css + parsedCssSelectors.source;
this._localCssSelectors.push.apply(this._localCssSelectors, parsedCssSelectors.selectors);
this._localCssSelectorVersion++; this._localCssSelectorVersion++;
this.ensureSelectors(); this.ensureSelectors();
} }
@ -267,13 +475,10 @@ export class StyleScope {
} }
} }
public applySelectors(view: ViewBase): void { @profile
public matchSelectors(view: ViewBase): SelectorsMatch<ViewBase> {
this.ensureSelectors(); this.ensureSelectors();
return this._selectors.query(view);
let state = this._selectors.query(view);
let nextState = new CssState(view, state);
view._setCssState(nextState);
} }
public query(node: Node): SelectorCore[] { public query(node: Node): SelectorCore[] {
@ -314,66 +519,7 @@ export class StyleScope {
} }
} }
function createSelectorsFromCss(css: string, cssFileName: string, keyframes: Map<string, Keyframes>): RuleSet[] { type KeyframesMap = Map<string, Keyframes>;
try {
const pageCssSyntaxTree = css ? parseCss(css, { source: cssFileName }) : null;
let pageCssSelectors: RuleSet[] = [];
if (pageCssSyntaxTree) {
pageCssSelectors = pageCssSelectors.concat(createSelectorsFromImports(pageCssSyntaxTree, keyframes));
pageCssSelectors = pageCssSelectors.concat(createSelectorsFromSyntaxTree(pageCssSyntaxTree, keyframes));
}
return pageCssSelectors;
} catch (e) {
traceWrite("Css styling failed: " + e, traceCategories.Error, traceMessageType.error);
}
}
function createSelectorsFromImports(tree: SyntaxTree, keyframes: Map<string, Keyframes>): RuleSet[] {
let selectors: RuleSet[] = [];
if (tree !== null && tree !== undefined) {
const imports = tree["stylesheet"]["rules"].filter(r => r.type === "import");
for (let i = 0; i < imports.length; i++) {
const importItem = imports[i]["import"];
const match = importItem && (<string>importItem).match(pattern);
const url = match && match[2];
if (url !== null && url !== undefined) {
const appDirectory = knownFolders.currentApp().path;
const fileName = resolveFileNameFromUrl(url, appDirectory, File.exists);
if (fileName !== null) {
const file = File.fromPath(fileName);
const text = file.readTextSync();
if (text) {
selectors = selectors.concat(createSelectorsFromCss(text, fileName, keyframes));
}
}
}
}
}
return selectors;
}
function createSelectorsFromSyntaxTree(ast: SyntaxTree, keyframes: Map<string, Keyframes>): RuleSet[] {
const nodes = ast.stylesheet.rules;
(<KeyframesDefinition[]>nodes.filter(isKeyframe)).forEach(node => keyframes[node.name] = node);
const rulesets = fromAstNodes(nodes);
if (rulesets && rulesets.length) {
ensureCssAnimationParserModule();
rulesets.forEach(rule => {
rule[animationsSymbol] = cssAnimationParserModule.CssAnimationParser
.keyframeAnimationsFromCSSDeclarations(rule.declarations);
});
}
return rulesets;
}
export function resolveFileNameFromUrl(url: string, appDirectory: string, fileExists: (name: string) => boolean): string { export function resolveFileNameFromUrl(url: string, appDirectory: string, fileExists: (name: string) => boolean): string {
let fileName: string = typeof url === "string" ? url.trim() : ""; let fileName: string = typeof url === "string" ? url.trim() : "";
@ -382,22 +528,25 @@ export function resolveFileNameFromUrl(url: string, appDirectory: string, fileEx
fileName = fileName.replace("~/", ""); fileName = fileName.replace("~/", "");
} }
let local = path.join(appDirectory, fileName); const isAbsolutePath = fileName.indexOf("/") === 0;
if (fileExists(local)) { const absolutePath = isAbsolutePath ? fileName : path.join(appDirectory, fileName);
return local; if (fileExists(absolutePath)) {
return absolutePath;
} }
let external = path.join(appDirectory, "tns_modules", fileName); if (!isAbsolutePath) {
const external = path.join(appDirectory, "tns_modules", fileName);
if (fileExists(external)) { if (fileExists(external)) {
return external; return external;
} }
}
return null; return null;
} }
export function applyInlineStyle(view: ViewBase, styleStr: string) { export const applyInlineStyle = profile(function applyInlineStyle(view: ViewBase, styleStr: string) {
let localStyle = `local { ${styleStr} }`; let localStyle = `local { ${styleStr} }`;
let inlineRuleSet = createSelectorsFromCss(localStyle, null, new Map()); let inlineRuleSet = CSSSource.fromSource(localStyle, new Map()).selectors;
const style = view.style; const style = view.style;
inlineRuleSet[0].declarations.forEach(d => { inlineRuleSet[0].declarations.forEach(d => {
@ -413,7 +562,7 @@ export function applyInlineStyle(view: ViewBase, styleStr: string) {
traceWrite(`Failed to apply property [${d.property}] with value [${d.value}] to ${view}. ${e}`, traceCategories.Error, traceMessageType.error); traceWrite(`Failed to apply property [${d.property}] with value [${d.value}] to ${view}. ${e}`, traceCategories.Error, traceMessageType.error);
} }
}); });
} });
function isKeyframe(node: CssNode): node is KeyframesDefinition { function isKeyframe(node: CssNode): node is KeyframesDefinition {
return node.type === "keyframes"; return node.type === "keyframes";

View File

@ -48,7 +48,6 @@ export interface CommonLayoutParams {
} }
export class Style extends Observable { export class Style extends Observable {
public fontInternal: Font; public fontInternal: Font;
public backgroundInternal: Background; public backgroundInternal: Background;
@ -149,4 +148,20 @@ export class Style extends Observable {
public flexShrink: FlexShrink; public flexShrink: FlexShrink;
public flexWrapBefore: FlexWrapBefore; public flexWrapBefore: FlexWrapBefore;
public alignSelf: AlignSelf; public alignSelf: AlignSelf;
/**
* The property bag is a simple class that is paired with the Style class.
* Setting regular css properties on the PropertyBag should simply preserve their values.
* Setting shorthand css properties on the PropertyBag should decompose the provided value, and set each of the shorthand composite properties.
* The shorthand properties are defined as non-enumerable so it should be safe to for-in the keys that are set in the bag.
*/
public readonly PropertyBag: PropertyBagClass;
}
interface PropertyBagClass {
new(): PropertyBag;
prototype: PropertyBag;
}
interface PropertyBag {
[property: string]: string;
} }

View File

@ -118,4 +118,7 @@ export class Style extends Observable implements StyleDefinition {
public flexShrink: FlexShrink; public flexShrink: FlexShrink;
public flexWrapBefore: FlexWrapBefore; public flexWrapBefore: FlexWrapBefore;
public alignSelf: AlignSelf; public alignSelf: AlignSelf;
public PropertyBag: { new(): { [property: string]: string }, prototype: { [property: string]: string } };
} }
Style.prototype.PropertyBag = class { [property: string]: string; }

View File

@ -10,7 +10,7 @@
"removeComments": true, "removeComments": true,
"experimentalDecorators": true, "experimentalDecorators": true,
"diagnostics": true, "diagnostics": true,
"sourceMap": true, "inlineSourceMap": true,
"jsx": "react", "jsx": "react",
"reactNamespace": "UIBuilder", "reactNamespace": "UIBuilder",
"lib": [ "lib": [