mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-08-17 21:01:34 +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(() => {
|
setTimeout(() => {
|
||||||
messageContainer.dismissSoftInput();
|
messageContainer.dismissSoftInput();
|
||||||
(<android.view.View>messageContainer.nativeViewProtected).scrollTo(0, 0);
|
(<android.view.View>messageContainer.nativeViewProtected).scrollTo(0, 0);
|
||||||
});
|
}, 10);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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";
|
||||||
|
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 {
|
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 {
|
||||||
|
@ -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.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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();
|
||||||
|
@ -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();
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.
|
* 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>;
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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";
|
||||||
|
@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.
|
* @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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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";
|
||||||
|
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 {
|
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;
|
||||||
}
|
}
|
@ -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; }
|
@ -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": [
|
||||||
|
Reference in New Issue
Block a user