// Definitions. import { ViewBase as ViewBaseDefinition } from "."; import { Page } from "../../page"; import { SelectorCore } from "../../styling/css-selector"; import { Order, FlexGrow, FlexShrink, FlexWrapBefore, AlignSelf } from "../../layouts/flexbox-layout"; import { KeyframeAnimation } from "../../animation/keyframe-animation"; // Types. import { Property, InheritedProperty, Style, clearInheritedProperties, propagateInheritableProperties, propagateInheritableCssProperties, resetCSSProperties, initNativeView, resetNativeView } from "../properties"; import { Binding, BindingOptions, Observable, WrappedValue, PropertyChangeData, traceEnabled, traceWrite, traceCategories, traceNotifyEvent } from "../bindable"; import { isIOS, isAndroid } from "../../../platform"; import { layout } from "../../../utils/utils"; import { Length, paddingTopProperty, paddingRightProperty, paddingBottomProperty, paddingLeftProperty } from "../../styling/style-properties"; // TODO: Remove this import! import * as types from "../../../utils/types"; import { Color } from "../../../color"; export { isIOS, isAndroid, layout, Color }; export * from "../bindable"; export * from "../properties"; import * as ssm from "../../styling/style-scope"; let styleScopeModule: typeof ssm; function ensureStyleScopeModule() { if (!styleScopeModule) { styleScopeModule = require("ui/styling/style-scope"); } } let defaultBindingSource = {}; export function getAncestor(view: ViewBaseDefinition, criterion: string | Function): ViewBaseDefinition { let matcher: (view: ViewBaseDefinition) => boolean = null; if (typeof criterion === "string") { matcher = (view: ViewBaseDefinition) => view.typeName === criterion; } else { matcher = (view: ViewBaseDefinition) => view instanceof criterion; } for (let parent = view.parent; parent != null; parent = parent.parent) { if (matcher(parent)) { return parent; } } return null; } export function getViewById(view: ViewBaseDefinition, id: string): ViewBaseDefinition { if (!view) { return undefined; } if (view.id === id) { return view; } let retVal: ViewBaseDefinition; const descendantsCallback = function (child: ViewBaseDefinition): boolean { if (child.id === id) { retVal = child; // break the iteration by returning false return false; } return true; }; eachDescendant(view, descendantsCallback); return retVal; } export function eachDescendant(view: ViewBaseDefinition, callback: (child: ViewBaseDefinition) => boolean) { if (!callback || !view) { return; } let continueIteration: boolean; let localCallback = function (child: ViewBaseDefinition): boolean { continueIteration = callback(child); if (continueIteration) { child.eachChild(localCallback); } return continueIteration; }; view.eachChild(localCallback); } let viewIdCounter = 1; export class ViewBase extends Observable implements ViewBaseDefinition { public static loadedEvent = "loaded"; public static unloadedEvent = "unloaded"; public recycleNativeView: boolean; private _iosView: Object; private _androidView: Object; private _style: Style; private _isLoaded: boolean; private _registeredAnimations: Array; private _visualState: string; private _inlineStyleSelector: SelectorCore; public bindingContext: any; public nativeView: any; public parent: ViewBase; public isCollapsed; // Default(false) set in prototype public id: string; public className: string; public _domId: number; public _context: any; public _isAddedToNativeVisualTree: boolean; public _cssState: ssm.CssState; public _styleScope: ssm.StyleScope; // Dynamic properties. left: Length; top: Length; effectiveLeft: number; effectiveTop: number; dock: "left" | "top" | "right" | "bottom"; row: number; col: number; rowSpan: number; colSpan: number; order: Order; flexGrow: FlexGrow; flexShrink: FlexShrink; flexWrapBefore: FlexWrapBefore; alignSelf: AlignSelf; _oldLeft: number; _oldTop: number; _oldRight: number; _oldBottom: number; public effectiveMinWidth: number; public effectiveMinHeight: number; public effectiveWidth: number; public effectiveHeight: number; public effectiveMarginTop: number; public effectiveMarginRight: number; public effectiveMarginBottom: number; public effectiveMarginLeft: number; public effectivePaddingTop: number; public effectivePaddingRight: number; public effectivePaddingBottom: number; public effectivePaddingLeft: number; public effectiveBorderTopWidth: number; public effectiveBorderRightWidth: number; public effectiveBorderBottomWidth: number; public effectiveBorderLeftWidth: number; public _defaultPaddingTop: number; public _defaultPaddingRight: number; public _defaultPaddingBottom: number; public _defaultPaddingLeft: number; constructor() { super(); this._domId = viewIdCounter++; this._style = new Style(this); } // TODO: Use Type.prototype.typeName instead. get typeName(): string { return types.getClass(this); } get style(): Style { return this._style; } set style(value) { throw new Error("View.style property is read-only."); } get android(): any { return this._androidView; } get ios(): any { return this._iosView; } get isLoaded(): boolean { return this._isLoaded; } get class(): string { return this.className; } set class(v: string) { this.className = v; } public get inlineStyleSelector(): SelectorCore { return this._inlineStyleSelector; } public set inlineStyleSelector(value: SelectorCore) { this._inlineStyleSelector = value; } getViewById(id: string): T { return getViewById(this, id); } get page(): Page { if (this.parent) { return this.parent.page; } return null; } // Overriden so we don't raise `poropertyChange` // The property will raise its own event. public set(name: string, value: any) { this[name] = WrappedValue.unwrap(value); } public onLoaded() { this._isLoaded = true; this._loadEachChild(); this._emit("loaded"); } public _loadEachChild() { this.eachChild((child) => { child.onLoaded(); return true; }); } public onUnloaded() { this._unloadEachChild(); this._isLoaded = false; this._emit("unloaded"); } private _unloadEachChild() { this.eachChild((child) => { if (child.isLoaded) { child.onUnloaded(); } return true; }); } 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... _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; private applyCssState(): void { if (!this._cssState) { return; } // this.style._beginUpdate(); this._cssState.apply(); // this.style._endUpdate(); } private pseudoClassAliases = { 'highlighted': [ 'active', 'pressed' ] }; public cssClasses: Set = new Set(); public cssPseudoClasses: Set = new Set(); private getAllAliasedStates(name: string): Array { let allStates = []; allStates.push(name); if (name in this.pseudoClassAliases) { for (let i = 0; i < this.pseudoClassAliases[name].length; i++) { allStates.push(this.pseudoClassAliases[name][i]); } } return allStates; } public addPseudoClass(name: string): void { let allStates = this.getAllAliasedStates(name); for (let i = 0; i < allStates.length; i++) { if (!this.cssPseudoClasses.has(allStates[i])) { this.cssPseudoClasses.add(allStates[i]); this.notifyPseudoClassChanged(allStates[i]); } } } public deletePseudoClass(name: string): void { let allStates = this.getAllAliasedStates(name); for (let i = 0; i < allStates.length; i++) { if (this.cssPseudoClasses.has(allStates[i])) { this.cssPseudoClasses.delete(allStates[i]); this.notifyPseudoClassChanged(allStates[i]); } } } 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); } private bindings: Map; private shouldAddHandlerToParentBindingContextChanged: boolean; private bindingContextBoundToParentBindingContextChanged: boolean; public bind(options: BindingOptions, source: Object = defaultBindingSource): void { const targetProperty = options.targetProperty; this.unbind(targetProperty); if (!this.bindings) { this.bindings = new Map(); } const binding = new Binding(this, options); this.bindings.set(targetProperty, binding); let bindingSource = source; if (bindingSource === defaultBindingSource) { bindingSource = this.bindingContext; binding.sourceIsBindingContext = true; if (targetProperty === "bindingContext") { this.bindingContextBoundToParentBindingContextChanged = true; const parent = this.parent; if (parent) { parent.on("bindingContextChange", this.bindingContextChanged, this); } else { this.shouldAddHandlerToParentBindingContextChanged = true; } } } binding.bind(bindingSource); } public unbind(property: string): void { const bindings = this.bindings; if (!bindings) { return; } const binding = bindings.get(property); if (binding) { binding.unbind(); bindings.delete(property); if (binding.sourceIsBindingContext) { if (property === "bindingContext") { this.shouldAddHandlerToParentBindingContextChanged = false; this.bindingContextBoundToParentBindingContextChanged = false; const parent = this.parent; if (parent) { parent.off("bindingContextChange", this.bindingContextChanged, this); } } } } } public requestLayout(): void { let parent = this.parent; if (parent) { parent.requestLayout(); } } public eachChild(callback: (child: ViewBase) => boolean) { // } public _addView(view: ViewBase, atIndex?: number) { if (traceEnabled()) { traceWrite(`${this}._addView(${view}, ${atIndex})`, traceCategories.ViewHierarchy); } if (!view) { throw new Error("Expecting a valid View instance."); } if (!(view instanceof ViewBase)) { throw new Error(view + " is not a valid View instance."); } if (view.parent) { throw new Error("View already has a parent. View: " + view + " Parent: " + view.parent); } view.parent = this; this._addViewCore(view, atIndex); view._parentChanged(null); } 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); const styleScope = this._styleScope; if (styleScope) { view._setStyleScope(styleScope); } propagateInheritableCssProperties(this.style); if (this._context) { view._setupUI(this._context, atIndex); } if (this._isLoaded) { view.onLoaded(); } } /** * Core logic for removing a child view from this instance. Used by the framework to handle lifecycle events more centralized. Do not outside the UI Stack implementation. */ public _removeView(view: ViewBase) { if (traceEnabled()) { traceWrite(`${this}._removeView(${view})`, traceCategories.ViewHierarchy); } if (view.parent !== this) { throw new Error("View not added to this instance. View: " + view + " CurrentParent: " + view.parent + " ExpectedParent: " + this); } this._removeViewCore(view); view.parent = undefined; view._parentChanged(this); } /** * 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(); } } public _createNativeView(): Object { return undefined; } public _disposeNativeView() { // } public _initNativeView(): void { // } public _resetNativeView(): void { if (this.nativeView && this.recycleNativeView) { resetNativeView(this); } } public _setupUI(context: android.content.Context, atIndex?: number, parentIsLoaded?: boolean) { traceNotifyEvent(this, "_setupUI"); if (traceEnabled()) { traceWrite(`${this}._setupUI(${context})`, traceCategories.VisualTreeEvents); } if (this._context === context) { return; } this._context = context; traceNotifyEvent(this, "_onContextChanged"); if (isAndroid) { const nativeView = this._androidView = this.nativeView = this._createNativeView(); if (nativeView) { let result: android.graphics.Rect = (nativeView).defaultPaddings; if (result === undefined) { result = org.nativescript.widgets.ViewHelper.getPadding(nativeView); (nativeView).defaultPaddings = result; } this._defaultPaddingTop = result.top; this._defaultPaddingRight = result.right; this._defaultPaddingBottom = result.bottom; this._defaultPaddingLeft = result.left; const style = this.style; if (!paddingTopProperty.isSet(style)) { this.effectivePaddingTop = this._defaultPaddingTop; } if (!paddingRightProperty.isSet(style)) { this.effectivePaddingRight = this._defaultPaddingRight; } if (!paddingBottomProperty.isSet(style)) { this.effectivePaddingBottom = this._defaultPaddingBottom; } if (!paddingLeftProperty.isSet(style)) { this.effectivePaddingLeft = this._defaultPaddingLeft; } } } else { // TODO: Implement _createNativeView for iOS this._createNativeView(); this.nativeView = this._iosView = (this)._nativeView; } this._initNativeView(); if (this.parent) { let nativeIndex = this.parent._childIndexToNativeChildIndex(atIndex); this._isAddedToNativeVisualTree = this.parent._addViewToNativeVisualTree(this, nativeIndex); } if (this.nativeView) { initNativeView(this); } this.eachChild((child) => { child._setupUI(context); return true; }); } public _tearDownUI(force?: boolean) { // No context means we are already teared down. if (!this._context) { return; } if (traceEnabled()) { traceWrite(`${this}._tearDownUI(${force})`, traceCategories.VisualTreeEvents); } this._resetNativeView(); this.eachChild((child) => { child._tearDownUI(force); return true; }); if (this.parent) { this.parent._removeViewFromNativeVisualTree(this); } this._disposeNativeView(); this._context = null; traceNotifyEvent(this, "_onContextChanged"); traceNotifyEvent(this, "_tearDownUI"); } _childIndexToNativeChildIndex(index?: number): number { return index; } /** * Method is intended to be overridden by inheritors and used as "protected". */ public _addViewToNativeVisualTree(view: ViewBase, atIndex?: number): boolean { if (view._isAddedToNativeVisualTree) { throw new Error("Child already added to the native visual tree."); } return true; } /** * Method is intended to be overridden by inheritors and used as "protected" */ public _removeViewFromNativeVisualTree(view: ViewBase) { traceNotifyEvent(view, "_removeViewFromNativeVisualTree"); view._isAddedToNativeVisualTree = false; } public _goToVisualState(state: string) { if (traceEnabled()) { traceWrite(this + " going to state: " + state, traceCategories.Style); } if (state === this._visualState) { return; } this.deletePseudoClass(this._visualState); this._visualState = state; this.addPseudoClass(state); } public _applyXmlAttribute(attribute, value): boolean { if (attribute === "style") { this._applyInlineStyle(value); return true; } return false; } public setInlineStyle(style: string): void { if (typeof style !== "string") { throw new Error("Parameter should be valid CSS string!"); } this._applyInlineStyle(style); } public _parentChanged(oldParent: ViewBase): void { //Overridden if (oldParent) { clearInheritedProperties(this); if (this.bindingContextBoundToParentBindingContextChanged) { oldParent.off("bindingContextChange", this.bindingContextChanged, this); } } else if (this.shouldAddHandlerToParentBindingContextChanged) { const parent = this.parent; parent.on("bindingContextChange", this.bindingContextChanged, this); this.bindings.get("bindingContext").bind(parent.bindingContext); } } public _registerAnimation(animation: KeyframeAnimation) { if (this._registeredAnimations === undefined) { this._registeredAnimations = new Array(); } 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(); } } } } ViewBase.prototype.isCollapsed = false; ViewBase.prototype._oldLeft = 0; ViewBase.prototype._oldTop = 0; ViewBase.prototype._oldRight = 0; ViewBase.prototype._oldBottom = 0; ViewBase.prototype.effectiveMinWidth = 0; ViewBase.prototype.effectiveMinHeight = 0; ViewBase.prototype.effectiveWidth = 0; ViewBase.prototype.effectiveHeight = 0; ViewBase.prototype.effectiveMarginTop = 0; ViewBase.prototype.effectiveMarginRight = 0; ViewBase.prototype.effectiveMarginBottom = 0; ViewBase.prototype.effectiveMarginLeft = 0; ViewBase.prototype.effectivePaddingTop = 0; ViewBase.prototype.effectivePaddingRight = 0; ViewBase.prototype.effectivePaddingBottom = 0; ViewBase.prototype.effectivePaddingLeft = 0; ViewBase.prototype.effectiveBorderTopWidth = 0; ViewBase.prototype.effectiveBorderRightWidth = 0; ViewBase.prototype.effectiveBorderBottomWidth = 0; ViewBase.prototype.effectiveBorderLeftWidth = 0; ViewBase.prototype._defaultPaddingTop = 0; ViewBase.prototype._defaultPaddingRight = 0; ViewBase.prototype._defaultPaddingBottom = 0; ViewBase.prototype._defaultPaddingLeft = 0; export const bindingContextProperty = new InheritedProperty({ name: "bindingContext" }); bindingContextProperty.register(ViewBase); export const classNameProperty = new Property({ name: "className", valueChanged(view: ViewBase, oldValue: string, newValue: string) { let classes = view.cssClasses; classes.clear(); if (typeof newValue === "string") { newValue.split(" ").forEach(c => classes.add(c)); } resetStyles(view); } }); classNameProperty.register(ViewBase); function resetStyles(view: ViewBase): void { view._cancelAllAnimations(); resetCSSProperties(view.style); view._applyStyleFromScope(); view.eachChild((child) => { resetStyles(child); return true; }); } export const idProperty = new Property({ name: "id", valueChanged: (view, oldValue, newValue) => resetStyles(view) }); idProperty.register(ViewBase); export function booleanConverter(v: string): boolean { let lowercase = (v + '').toLowerCase(); if (lowercase === "true") { return true; } else if (lowercase === "false") { return false; } throw new Error(`Invalid boolean: ${v}`); }