mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-08-16 20:11:24 +08:00
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:

committed by
SvetoslavTsenov

parent
b0577728be
commit
6d7c1ff295
@ -359,7 +359,7 @@ function showReportPage(finalMessage: string) {
|
||||
setTimeout(() => {
|
||||
messageContainer.dismissSoftInput();
|
||||
(<android.view.View>messageContainer.nativeViewProtected).scrollTo(0, 0);
|
||||
});
|
||||
}, 10);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import * as helper from "../helper";
|
||||
import * as btnCounter from "./pages/button-counter";
|
||||
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.
|
||||
|
||||
@ -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
|
||||
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.`);
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,13 @@ export class Button extends button.Button {
|
||||
nativeBackgroundRedraws = 0;
|
||||
backgroundInternalSetNativeCount = 0;
|
||||
fontInternalSetNativeCount = 0;
|
||||
colorSetNativeCount = 0;
|
||||
colorPropertyChangeCount = 0;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.style.on("colorChange", () => this.colorPropertyChangeCount++);
|
||||
}
|
||||
|
||||
[view.backgroundInternalProperty.setNative](value) {
|
||||
this.backgroundInternalSetNativeCount++;
|
||||
@ -18,5 +25,9 @@ export class Button extends button.Button {
|
||||
this.nativeBackgroundRedraws++;
|
||||
super._redrawNativeBackground(value);
|
||||
}
|
||||
[view.colorProperty.setNative](value) {
|
||||
this.colorSetNativeCount++;
|
||||
return super[view.colorProperty.setNative](value);
|
||||
}
|
||||
}
|
||||
Button.prototype.recycleNativeView = "never";
|
||||
|
15
tests/app/ui/lifecycle/pages/page-two.css
Normal file
15
tests/app/ui/lifecycle/pages/page-two.css
Normal file
@ -0,0 +1,15 @@
|
||||
Button {
|
||||
color: orange;
|
||||
}
|
||||
|
||||
.red {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.blue {
|
||||
color: blue;
|
||||
}
|
||||
|
||||
.nocolor {
|
||||
background: red;
|
||||
}
|
8
tests/app/ui/lifecycle/pages/page-two.xml
Normal file
8
tests/app/ui/lifecycle/pages/page-two.xml
Normal 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>
|
@ -5,12 +5,10 @@ export function getNativeItemsCount(bar: segmentedBarModule.SegmentedBar): numbe
|
||||
}
|
||||
|
||||
export function checkNativeItemsTextColor(bar: segmentedBarModule.SegmentedBar): boolean {
|
||||
var isValid = true;
|
||||
|
||||
var attrs = (<UISegmentedControl>bar.nativeViewProtected).titleTextAttributesForState(UIControlState.Normal);
|
||||
isValid = bar.color && attrs && attrs.valueForKey(NSForegroundColorAttributeName) === bar.color.ios;
|
||||
|
||||
return isValid;
|
||||
var nativeViewColor = bar.color && attrs && attrs.valueForKey(NSForegroundColorAttributeName);
|
||||
var barColor = bar.color.ios;
|
||||
return barColor.isEqual(nativeViewColor);
|
||||
}
|
||||
|
||||
export function setNativeSelectedIndex(bar: segmentedBarModule.SegmentedBar, index: number): void {
|
||||
|
@ -67,6 +67,7 @@ export function test_applies_css_changes_to_application_rules_after_page_load()
|
||||
helper.buildUIAndRunTest(label1, function (views: Array<viewModule.View>) {
|
||||
application.addCss(".applicationChangedLabelAfter { color: blue; }");
|
||||
label1.className = "applicationChangedLabelAfter";
|
||||
console.log("IsLoaded: " + label1.isLoaded);
|
||||
helper.assertViewColor(label1, "#0000FF");
|
||||
});
|
||||
}
|
||||
@ -615,7 +616,7 @@ export function test_setInlineStyle_setsLocalValues() {
|
||||
stack.addChild(testButton);
|
||||
|
||||
helper.buildUIAndRunTest(stack, function (views: Array<viewModule.View>) {
|
||||
(<any>testButton)._applyInlineStyle("color: red;");
|
||||
(<any>testButton).style = "color: red;";
|
||||
helper.assertViewColor(testButton, "#FF0000");
|
||||
});
|
||||
}
|
||||
@ -624,7 +625,7 @@ export function test_setStyle_throws() {
|
||||
const testButton = new buttonModule.Button();
|
||||
|
||||
TKUnit.assertThrows(function () {
|
||||
(<any>testButton).style = "background-color: red;";
|
||||
(<any>testButton).style = {};
|
||||
}, "View.style property is read-only.");
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,19 @@ import * as helper from "../helper";
|
||||
import * as TKUnit from "../../TKUnit";
|
||||
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 () {
|
||||
let page = helper.getCurrentPage();
|
||||
let btn = new button.Button();
|
||||
|
@ -125,12 +125,6 @@ const applyComponentCss = profile("applyComponentCss", (instance: View, moduleNa
|
||||
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,14 +205,8 @@ export function setPropertyValue(instance: View, instanceModule: Object, exports
|
||||
instance[propertyName] = exports[propertyValue];
|
||||
}
|
||||
else {
|
||||
let attrHandled = false;
|
||||
if (!attrHandled && instance._applyXmlAttribute) {
|
||||
attrHandled = instance._applyXmlAttribute(propertyName, propertyValue);
|
||||
}
|
||||
if (!attrHandled) {
|
||||
instance[propertyName] = propertyValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getBindingExpressionFromAttribute(value: string): string {
|
||||
|
@ -77,6 +77,7 @@ export class CssProperty<T extends Style, U> {
|
||||
public readonly setNative: symbol;
|
||||
public readonly name: string;
|
||||
public readonly cssName: string;
|
||||
public readonly cssLocalName: string;
|
||||
public readonly defaultValue: U;
|
||||
public register(cls: { prototype: T }): void;
|
||||
public isSet(instance: T): boolean;
|
||||
@ -92,7 +93,7 @@ export class ShorthandProperty<T extends Style, P> {
|
||||
public readonly name: string;
|
||||
public readonly cssName: string;
|
||||
|
||||
public register(cls: { prototype: T }): void;
|
||||
public register(cls: typeof Style): void;
|
||||
}
|
||||
|
||||
export class CssAnimationProperty<T extends Style, U> {
|
||||
@ -100,11 +101,10 @@ export class CssAnimationProperty<T extends Style, U> {
|
||||
|
||||
public readonly getDefault: symbol;
|
||||
public readonly setNative: symbol;
|
||||
public readonly key: symbol;
|
||||
|
||||
public readonly name: string;
|
||||
public readonly cssName: string;
|
||||
public readonly native: symbol;
|
||||
public readonly cssLocalName: string;
|
||||
|
||||
readonly keyframe: string;
|
||||
|
||||
|
@ -646,9 +646,10 @@ export class CssProperty<T extends Style, U> implements definitions.CssProperty<
|
||||
}
|
||||
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 cssName: string;
|
||||
public readonly cssLocalName: string;
|
||||
|
||||
public readonly getDefault: symbol;
|
||||
public readonly setNative: symbol;
|
||||
@ -682,7 +683,9 @@ export class CssAnimationProperty<T extends Style, U> {
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
let value: U;
|
||||
let unsetNativeValue = false;
|
||||
if (reset) {
|
||||
// If unsetValue - we want to reset this property.
|
||||
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) {
|
||||
value = style[propertyName];
|
||||
this[sourceKey] = ValueSource.Inherited;
|
||||
this[key] = value;
|
||||
} else {
|
||||
value = defaultValue;
|
||||
delete this[sourceKey];
|
||||
delete this[key];
|
||||
unsetNativeValue = true;
|
||||
}
|
||||
} else {
|
||||
this[sourceKey] = valueSource;
|
||||
@ -887,15 +895,13 @@ export class InheritedCssProperty<T extends Style, U> extends CssProperty<T, U>
|
||||
} else {
|
||||
value = boxedValue;
|
||||
}
|
||||
this[key] = value;
|
||||
}
|
||||
|
||||
const oldValue: U = key in this ? this[key] : defaultValue;
|
||||
const changed: boolean = equalityComparer ? !equalityComparer(oldValue, value) : oldValue !== value;
|
||||
|
||||
if (changed) {
|
||||
const view = this.view;
|
||||
if (reset) {
|
||||
delete this[key];
|
||||
if (valueChanged) {
|
||||
valueChanged(this, oldValue, value);
|
||||
}
|
||||
@ -906,25 +912,13 @@ export class InheritedCssProperty<T extends Style, U> extends CssProperty<T, U>
|
||||
view._suspendedUpdates[propertyName] = property;
|
||||
}
|
||||
} else {
|
||||
if (unsetNativeValue) {
|
||||
if (defaultValueKey in this) {
|
||||
view[setNative](this[defaultValueKey]);
|
||||
delete this[defaultValueKey];
|
||||
} else {
|
||||
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 {
|
||||
if (!(defaultValueKey in this)) {
|
||||
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 localValueDescriptor: PropertyDescriptor;
|
||||
protected readonly propertyBagDescriptor: PropertyDescriptor;
|
||||
|
||||
public readonly sourceKey: symbol;
|
||||
|
||||
@ -1025,19 +1020,32 @@ export class ShorthandProperty<T extends Style, P> implements definitions.Shorth
|
||||
set: setLocalValue
|
||||
};
|
||||
|
||||
this.propertyBagDescriptor = {
|
||||
enumerable: false,
|
||||
configurable: true,
|
||||
set(value: string) {
|
||||
converter(value).forEach(([property, value]) => {
|
||||
this[property.cssLocalName] = value;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
cssSymbolPropertyMap[key] = this;
|
||||
}
|
||||
|
||||
public register(cls: { prototype: T }): void {
|
||||
public register(cls: typeof Style): void {
|
||||
if (this.registered) {
|
||||
throw new Error(`Property ${this.name} already registered.`);
|
||||
}
|
||||
|
||||
this.registered = true;
|
||||
Object.defineProperty(cls.prototype, this.name, this.localValueDescriptor);
|
||||
Object.defineProperty(cls.prototype, this.cssName, this.cssValueDescriptor);
|
||||
if (this.cssLocalName !== this.cssName) {
|
||||
Object.defineProperty(cls.prototype, this.cssLocalName, this.localValueDescriptor);
|
||||
}
|
||||
|
||||
Object.defineProperty(cls.prototype.PropertyBag, this.cssLocalName, this.propertyBagDescriptor);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -167,11 +167,6 @@ export abstract class ViewBase extends Observable {
|
||||
*/
|
||||
public className: string;
|
||||
|
||||
/**
|
||||
* Gets or sets inline style selectors for this view.
|
||||
*/
|
||||
public inlineStyleSelector: SelectorCore;
|
||||
|
||||
/**
|
||||
* Gets owner page. This is a read-only property.
|
||||
*/
|
||||
@ -220,15 +215,22 @@ export abstract class ViewBase extends Observable {
|
||||
_domId: number;
|
||||
|
||||
_cssState: any /* "ui/styling/style-scope" */;
|
||||
_setCssState(next: any /* "ui/styling/style-scope" */);
|
||||
_registerAnimation(animation: KeyframeAnimation);
|
||||
_unregisterAnimation(animation: KeyframeAnimation);
|
||||
_cancelAllAnimations();
|
||||
/**
|
||||
* @private
|
||||
* Notifies each child's css state for change, recursively.
|
||||
* Either the style scope, className or id properties were changed.
|
||||
*/
|
||||
_onCssStateChange(): void;
|
||||
|
||||
public cssClasses: Set<string>;
|
||||
public cssPseudoClasses: Set<string>;
|
||||
|
||||
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 setInlineStyle(style: string): void;
|
||||
|
||||
@ -293,7 +295,7 @@ export abstract class ViewBase extends Observable {
|
||||
/**
|
||||
* @protected
|
||||
* @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;
|
||||
|
||||
@ -331,6 +333,11 @@ export abstract class ViewBase extends Observable {
|
||||
* @private
|
||||
*/
|
||||
_setupAsRootView(context: any): void;
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_inheritStyleScope(styleScope: any /* StyleScope */): void;
|
||||
//@endprivate
|
||||
}
|
||||
|
||||
|
@ -138,9 +138,7 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
|
||||
private _androidView: Object;
|
||||
private _style: Style;
|
||||
private _isLoaded: boolean;
|
||||
private _registeredAnimations: Array<KeyframeAnimation>;
|
||||
private _visualState: string;
|
||||
private _inlineStyleSelector: SelectorCore;
|
||||
private __nativeView: any;
|
||||
// private _disableNativeViewRecycling: boolean;
|
||||
public domNode: DOMNode;
|
||||
@ -157,7 +155,7 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
|
||||
public _domId: number;
|
||||
public _context: any;
|
||||
public _isAddedToNativeVisualTree: boolean;
|
||||
public _cssState: ssm.CssState;
|
||||
public _cssState: ssm.CssState = new ssm.CssState(this);
|
||||
public _styleScope: ssm.StyleScope;
|
||||
public _suspendedUpdates: { [propertyName: string]: Property<ViewBase, any> | CssProperty<Style, any> | CssAnimationProperty<Style, any> };
|
||||
public _suspendNativeUpdatesCount: number;
|
||||
@ -229,9 +227,13 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
|
||||
get style(): 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.");
|
||||
}
|
||||
}
|
||||
|
||||
get android(): any {
|
||||
// this._disableNativeViewRecycling = true;
|
||||
@ -254,13 +256,6 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
|
||||
this.className = v;
|
||||
}
|
||||
|
||||
get inlineStyleSelector(): SelectorCore {
|
||||
return this._inlineStyleSelector;
|
||||
}
|
||||
set inlineStyleSelector(value: SelectorCore) {
|
||||
this._inlineStyleSelector = value;
|
||||
}
|
||||
|
||||
getViewById<T extends ViewBaseDefinition>(id: string): T {
|
||||
return <T>getViewById(this, id);
|
||||
}
|
||||
@ -288,6 +283,7 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
|
||||
@profile
|
||||
public onLoaded() {
|
||||
this._isLoaded = true;
|
||||
this._cssState.onLoaded();
|
||||
this._resumeNativeUpdates();
|
||||
this._loadEachChild();
|
||||
this._emit("loaded");
|
||||
@ -305,6 +301,7 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
|
||||
this._suspendNativeUpdates();
|
||||
this._unloadEachChild();
|
||||
this._isLoaded = false;
|
||||
this._cssState.onUnloaded();
|
||||
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 {
|
||||
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 = {
|
||||
'highlighted': [
|
||||
'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 {
|
||||
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) {
|
||||
propagateInheritableProperties(this, view);
|
||||
|
||||
const styleScope = this._styleScope;
|
||||
if (styleScope) {
|
||||
view._setStyleScope(styleScope);
|
||||
}
|
||||
|
||||
view._inheritStyleScope(this._styleScope);
|
||||
propagateInheritableCssProperties(this.style, view.style);
|
||||
|
||||
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"
|
||||
*/
|
||||
public _removeViewCore(view: ViewBase) {
|
||||
// TODO: Discuss this.
|
||||
if (this._styleScope === view._styleScope) {
|
||||
view._setStyleScope(null);
|
||||
}
|
||||
|
||||
if (view.isLoaded) {
|
||||
view.onUnloaded();
|
||||
}
|
||||
|
||||
// view.unsetInheritedProperties();
|
||||
|
||||
if (view._context) {
|
||||
view._tearDownUI();
|
||||
}
|
||||
@ -663,10 +534,7 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
|
||||
}
|
||||
|
||||
public initNativeView(): void {
|
||||
// No initNativeView(this)?
|
||||
if (this._cssState) {
|
||||
this._cssState.playPendingKeyframeAnimations();
|
||||
}
|
||||
//
|
||||
}
|
||||
|
||||
public resetNativeView(): void {
|
||||
@ -688,9 +556,9 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
|
||||
// }
|
||||
// }
|
||||
|
||||
if (this._cssState) {
|
||||
this._cancelAllAnimations();
|
||||
}
|
||||
// if (this._cssState) {
|
||||
// this._cancelAllAnimations();
|
||||
// }
|
||||
}
|
||||
|
||||
_setupAsRootView(context: any): void {
|
||||
@ -895,12 +763,12 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
|
||||
this.addPseudoClass(state);
|
||||
}
|
||||
|
||||
public _applyXmlAttribute(attribute, value): boolean {
|
||||
if (attribute === "style") {
|
||||
this._applyInlineStyle(value);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -909,7 +777,8 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
|
||||
throw new Error("Parameter should be valid CSS string!");
|
||||
}
|
||||
|
||||
this._applyInlineStyle(style);
|
||||
ensureStyleScopeModule();
|
||||
styleScopeModule.applyInlineStyle(this, style);
|
||||
}
|
||||
|
||||
public _parentChanged(oldParent: ViewBase): void {
|
||||
@ -932,30 +801,6 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
|
||||
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 {
|
||||
let str = this.typeName;
|
||||
if (this.id) {
|
||||
@ -970,6 +815,25 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
|
||||
|
||||
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;
|
||||
@ -1019,20 +883,12 @@ export const classNameProperty = new Property<ViewBase, string>({
|
||||
if (typeof newValue === "string") {
|
||||
newValue.split(" ").forEach(c => classes.add(c));
|
||||
}
|
||||
resetStyles(view);
|
||||
view._onCssStateChange();
|
||||
}
|
||||
});
|
||||
classNameProperty.register(ViewBase);
|
||||
|
||||
function resetStyles(view: ViewBase): void {
|
||||
view._applyStyleFromScope();
|
||||
view.eachChild((child) => {
|
||||
resetStyles(child);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
export const idProperty = new Property<ViewBase, string>({ name: "id", valueChanged: (view, oldValue, newValue) => resetStyles(view) });
|
||||
export const idProperty = new Property<ViewBase, string>({ name: "id", valueChanged: (view, oldValue, newValue) => view._onCssStateChange() });
|
||||
idProperty.register(ViewBase);
|
||||
|
||||
export function booleanConverter(v: string): boolean {
|
||||
|
@ -100,7 +100,6 @@ export class View extends ViewCommon {
|
||||
this.nativeViewProtected.setClickable(this._isClickable);
|
||||
}
|
||||
|
||||
this._cancelAllAnimations();
|
||||
super.onUnloaded();
|
||||
}
|
||||
|
||||
|
24
tns-core-modules/ui/core/view/view.d.ts
vendored
24
tns-core-modules/ui/core/view/view.d.ts
vendored
@ -67,7 +67,7 @@ export interface Size {
|
||||
* 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.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
@ -83,8 +83,6 @@ export abstract class View extends ViewBase implements ApplyXmlAttributes {
|
||||
*/
|
||||
bindingContext: any;
|
||||
|
||||
//----------Style property shortcuts----------
|
||||
|
||||
/**
|
||||
* Gets or sets the border color of the view.
|
||||
*/
|
||||
@ -413,12 +411,6 @@ export abstract class View extends ViewBase implements ApplyXmlAttributes {
|
||||
*/
|
||||
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>;
|
||||
|
||||
/**
|
||||
@ -490,7 +482,6 @@ export abstract class View extends ViewBase implements ApplyXmlAttributes {
|
||||
|
||||
_eachLayoutView(callback: (View) => void): void;
|
||||
|
||||
public _applyXmlAttribute(attribute: string, value: any): boolean;
|
||||
public eachChildView(callback: (view: View) => boolean): void;
|
||||
|
||||
//@private
|
||||
@ -647,19 +638,6 @@ export interface AddChildFromBuilder {
|
||||
_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 originXProperty: Property<View, number>;
|
||||
export const originYProperty: Property<View, number>;
|
||||
|
@ -49,9 +49,9 @@ export function getCurrentPage(): Page {
|
||||
function applySelectors(view: View) {
|
||||
let currentPage = getCurrentPage();
|
||||
if (currentPage) {
|
||||
let styleScope = currentPage._getStyleScope();
|
||||
let styleScope = currentPage._styleScope;
|
||||
if (styleScope) {
|
||||
styleScope.applySelectors(view);
|
||||
styleScope.matchSelectors(view);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -89,7 +89,6 @@ const entryCreatePage = profile("entry.create", (entry: NavigationEntry): Page =
|
||||
throw new Error("Failed to create Page with entry.create() function.");
|
||||
}
|
||||
|
||||
page._refreshCss();
|
||||
return page;
|
||||
});
|
||||
|
||||
|
@ -281,28 +281,15 @@ export class GridLayoutBase extends LayoutBase implements GridLayoutDefinition {
|
||||
this.requestLayout();
|
||||
}
|
||||
|
||||
_applyXmlAttribute(attributeName: string, attributeValue: any): boolean {
|
||||
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) {
|
||||
set rows(value: string) {
|
||||
this.removeRows();
|
||||
parseAndAddItemSpecs(value, (spec: ItemSpec) => this.addRow(spec));
|
||||
}
|
||||
|
||||
set columns(value: string) {
|
||||
this.removeColumns();
|
||||
parseAndAddItemSpecs(value, (spec: ItemSpec) => this.addColumn(spec));
|
||||
}
|
||||
}
|
||||
|
||||
GridLayoutBase.prototype.recycleNativeView = "auto";
|
||||
|
@ -22,14 +22,12 @@ export class PageBase extends ContentView implements PageDefinition {
|
||||
public static showingModallyEvent = "showingModally";
|
||||
|
||||
protected _closeModalCallback: Function;
|
||||
private _modalContext: any;
|
||||
|
||||
private _modalContext: any;
|
||||
private _navigationContext: any;
|
||||
|
||||
private _actionBar: ActionBar;
|
||||
private _cssAppliedVersion: number;
|
||||
|
||||
public _styleScope: StyleScope; // same as in ViewBase, but strongly typed
|
||||
public _modal: PageBase;
|
||||
public _fragmentTag: string;
|
||||
|
||||
@ -51,8 +49,7 @@ export class PageBase extends ContentView implements PageDefinition {
|
||||
}
|
||||
set css(value: string) {
|
||||
this._styleScope.css = value;
|
||||
this._cssFiles = {};
|
||||
this._refreshCss();
|
||||
this._onCssStateChange();
|
||||
}
|
||||
|
||||
get actionBar(): ActionBar {
|
||||
@ -94,59 +91,14 @@ export class PageBase extends ContentView implements PageDefinition {
|
||||
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 {
|
||||
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) {
|
||||
if (cssFileName.indexOf("~/") === 0) {
|
||||
cssFileName = path.join(knownFolders.currentApp().path, cssFileName.replace("~/", ""));
|
||||
}
|
||||
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;
|
||||
}
|
||||
this._styleScope.addCssFile(cssFileName);
|
||||
this._onCssStateChange();
|
||||
}
|
||||
|
||||
public getKeyframeAnimationWithName(animationName: string): KeyframeAnimationInfo {
|
||||
@ -275,10 +227,6 @@ export class PageBase extends ContentView implements PageDefinition {
|
||||
this.notify(args);
|
||||
}
|
||||
|
||||
public _getStyleScope(): StyleScope {
|
||||
return this._styleScope;
|
||||
}
|
||||
|
||||
public eachChildView(callback: (child: View) => boolean) {
|
||||
super.eachChildView(callback);
|
||||
callback(this.actionBar);
|
||||
@ -288,17 +236,8 @@ export class PageBase extends ContentView implements PageDefinition {
|
||||
return (this.content ? 1 : 0) + (this.actionBar ? 1 : 0);
|
||||
}
|
||||
|
||||
private _resetCssValues() {
|
||||
const resetCssValuesFunc = (view: View): boolean => {
|
||||
view._batchUpdate(() => {
|
||||
view._cancelAllAnimations();
|
||||
resetCSSProperties(view.style);
|
||||
});
|
||||
return true;
|
||||
};
|
||||
|
||||
resetCssValuesFunc(this);
|
||||
eachDescendant(this, resetCssValuesFunc);
|
||||
_inheritStyleScope(styleScope: StyleScope): void {
|
||||
// The Page have its own scope.
|
||||
}
|
||||
}
|
||||
|
||||
|
9
tns-core-modules/ui/page/page.d.ts
vendored
9
tns-core-modules/ui/page/page.d.ts
vendored
@ -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.
|
||||
*/
|
||||
public onNavigatedFrom(isBackNavigation: boolean): void;
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_refreshCss(): void;
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_getStyleScope(): styleScope.StyleScope;
|
||||
//@endprivate
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Keyframes } from "../animation/keyframe-animation";
|
||||
import { ViewBase } from "../core/view-base";
|
||||
import { View } from "../core/view";
|
||||
import { resetCSSProperties } from "../core/properties";
|
||||
import { unsetValue } from "../core/properties";
|
||||
import {
|
||||
SyntaxTree,
|
||||
Keyframes as KeyframesDefinition,
|
||||
@ -56,9 +56,101 @@ const applicationKeyframes: any = {};
|
||||
const animationsSymbol: symbol = Symbol("animations");
|
||||
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) => {
|
||||
if (args.cssText) {
|
||||
const parsed = createSelectorsFromCss(args.cssText, args.cssFile, applicationKeyframes);
|
||||
const parsed = CSSSource.fromSource(args.cssText, applicationKeyframes, args.cssFile).selectors;
|
||||
if (parsed) {
|
||||
applicationAdditionalSelectors.push.apply(applicationAdditionalSelectors, parsed);
|
||||
mergeCssSelectors();
|
||||
@ -72,23 +164,16 @@ function onLiveSync(args: application.CssChangedEventData): void {
|
||||
loadCss(application.getCssFileName());
|
||||
}
|
||||
|
||||
const loadCss = profile(`"style-scope".loadCss`, (cssFile?: string) => {
|
||||
const loadCss = profile(`"style-scope".loadCss`, (cssFile: string) => {
|
||||
if (!cssFile) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let result: RuleSet[];
|
||||
|
||||
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);
|
||||
const result = CSSSource.fromFile(cssFile, applicationKeyframes).selectors;
|
||||
if (result.length > 0) {
|
||||
applicationSelectors = result;
|
||||
mergeCssSelectors();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
application.on("cssChanged", onCssChanged);
|
||||
@ -106,69 +191,186 @@ if (application.hasLaunched()) {
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public apply(): void {
|
||||
this.view._cancelAllAnimations();
|
||||
resetCSSProperties(this.view.style);
|
||||
|
||||
let matchingSelectors = this.match.selectors.filter(sel => sel.dynamic ? sel.match(this.view) : true);
|
||||
if (this.view.inlineStyleSelector) {
|
||||
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;
|
||||
/**
|
||||
* 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 onChange(): void {
|
||||
if (this.view.isLoaded) {
|
||||
this.unsubscribeFromDynamicUpdates();
|
||||
this.updateMatch();
|
||||
this.subscribeForDynamicUpdates();
|
||||
this.updateDynamicState();
|
||||
} 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 {
|
||||
let ruleAnimations: kam.KeyframeAnimationInfo[] = ruleset[animationsSymbol];
|
||||
public onLoaded(): void {
|
||||
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) {
|
||||
ensureKeyframeAnimationModule();
|
||||
for (let animationInfo of ruleAnimations) {
|
||||
let animation = keyframeAnimationModule.KeyframeAnimation.keyframeAnimationFromInfo(animationInfo);
|
||||
if (animation) {
|
||||
this.view._registerAnimation(animation);
|
||||
animation.play(<View>this.view)
|
||||
.then(() => { this.view._unregisterAnimation(animation); })
|
||||
.catch((e) => { this.view._unregisterAnimation(animation); });
|
||||
animations.push(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 {
|
||||
|
||||
@ -200,25 +402,31 @@ export class StyleScope {
|
||||
this.appendCss(cssString, cssFileName)
|
||||
}
|
||||
|
||||
public addCssFile(cssFileName: string): void {
|
||||
this.appendCss(null, cssFileName);
|
||||
}
|
||||
|
||||
@profile
|
||||
private setCss(cssString: string, cssFileName?): void {
|
||||
this._css = cssString;
|
||||
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.ensureSelectors();
|
||||
}
|
||||
|
||||
@profile
|
||||
private appendCss(cssString: string, cssFileName?): void {
|
||||
if (!cssString) {
|
||||
if (!cssString && !cssFileName) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._css = this._css + cssString;
|
||||
this._reset();
|
||||
let parsedCssSelectors = createSelectorsFromCss(cssString, cssFileName, this._keyframes);
|
||||
this._localCssSelectors.push.apply(this._localCssSelectors, parsedCssSelectors);
|
||||
let parsedCssSelectors = cssString ? CSSSource.fromSource(cssString, this._keyframes, cssFileName) : CSSSource.fromFile(cssFileName, this._keyframes);
|
||||
this._css = this._css + parsedCssSelectors.source;
|
||||
this._localCssSelectors.push.apply(this._localCssSelectors, parsedCssSelectors.selectors);
|
||||
this._localCssSelectorVersion++;
|
||||
this.ensureSelectors();
|
||||
}
|
||||
@ -267,13 +475,10 @@ export class StyleScope {
|
||||
}
|
||||
}
|
||||
|
||||
public applySelectors(view: ViewBase): void {
|
||||
@profile
|
||||
public matchSelectors(view: ViewBase): SelectorsMatch<ViewBase> {
|
||||
this.ensureSelectors();
|
||||
|
||||
let state = this._selectors.query(view);
|
||||
|
||||
let nextState = new CssState(view, state);
|
||||
view._setCssState(nextState);
|
||||
return this._selectors.query(view);
|
||||
}
|
||||
|
||||
public query(node: Node): SelectorCore[] {
|
||||
@ -314,66 +519,7 @@ export class StyleScope {
|
||||
}
|
||||
}
|
||||
|
||||
function createSelectorsFromCss(css: string, cssFileName: string, keyframes: Map<string, Keyframes>): RuleSet[] {
|
||||
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;
|
||||
}
|
||||
type KeyframesMap = Map<string, Keyframes>;
|
||||
|
||||
export function resolveFileNameFromUrl(url: string, appDirectory: string, fileExists: (name: string) => boolean): string {
|
||||
let fileName: string = typeof url === "string" ? url.trim() : "";
|
||||
@ -382,22 +528,25 @@ export function resolveFileNameFromUrl(url: string, appDirectory: string, fileEx
|
||||
fileName = fileName.replace("~/", "");
|
||||
}
|
||||
|
||||
let local = path.join(appDirectory, fileName);
|
||||
if (fileExists(local)) {
|
||||
return local;
|
||||
const isAbsolutePath = fileName.indexOf("/") === 0;
|
||||
const absolutePath = isAbsolutePath ? fileName : path.join(appDirectory, fileName);
|
||||
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)) {
|
||||
return external;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function applyInlineStyle(view: ViewBase, styleStr: string) {
|
||||
export const applyInlineStyle = profile(function applyInlineStyle(view: ViewBase, styleStr: string) {
|
||||
let localStyle = `local { ${styleStr} }`;
|
||||
let inlineRuleSet = createSelectorsFromCss(localStyle, null, new Map());
|
||||
let inlineRuleSet = CSSSource.fromSource(localStyle, new Map()).selectors;
|
||||
const style = view.style;
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function isKeyframe(node: CssNode): node is KeyframesDefinition {
|
||||
return node.type === "keyframes";
|
||||
|
17
tns-core-modules/ui/styling/style/style.d.ts
vendored
17
tns-core-modules/ui/styling/style/style.d.ts
vendored
@ -48,7 +48,6 @@ export interface CommonLayoutParams {
|
||||
}
|
||||
|
||||
export class Style extends Observable {
|
||||
|
||||
public fontInternal: Font;
|
||||
public backgroundInternal: Background;
|
||||
|
||||
@ -149,4 +148,20 @@ export class Style extends Observable {
|
||||
public flexShrink: FlexShrink;
|
||||
public flexWrapBefore: FlexWrapBefore;
|
||||
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;
|
||||
}
|
@ -118,4 +118,7 @@ export class Style extends Observable implements StyleDefinition {
|
||||
public flexShrink: FlexShrink;
|
||||
public flexWrapBefore: FlexWrapBefore;
|
||||
public alignSelf: AlignSelf;
|
||||
|
||||
public PropertyBag: { new(): { [property: string]: string }, prototype: { [property: string]: string } };
|
||||
}
|
||||
Style.prototype.PropertyBag = class { [property: string]: string; }
|
@ -10,7 +10,7 @@
|
||||
"removeComments": true,
|
||||
"experimentalDecorators": true,
|
||||
"diagnostics": true,
|
||||
"sourceMap": true,
|
||||
"inlineSourceMap": true,
|
||||
"jsx": "react",
|
||||
"reactNamespace": "UIBuilder",
|
||||
"lib": [
|
||||
|
Reference in New Issue
Block a user