import { View as ViewDefinition, Point, Size } from "ui/core/view"; import { Style } from "ui/styling/style"; import { CssState, StyleScope, applyInlineSyle } from "ui/styling/style-scope"; import { Color } from "color"; import { Animation, AnimationPromise } from "ui/animation"; import { KeyframeAnimation } from "ui/animation/keyframe-animation"; import { Source } from "utils/debug"; import { Observable, EventData } from "data/observable"; import { Background } from "ui/styling/background"; import { ViewBase, getEventOrGestureName } from "./view-base"; import { propagateInheritedProperties, clearInheritedProperties, Property, InheritedProperty, CssProperty, ShorthandProperty, InheritedCssProperty } from "./properties"; import { observe, fromString as gestureFromString, GesturesObserver, GestureTypes, GestureEventData } from "ui/gestures"; import { isIOS } from "platform"; import { Font } from "ui/styling/font"; // TODO: Remove this and start using string as source (for android). import { fromFileOrResource, fromBase64, fromUrl } from "image-source"; import { enabled as traceEnabled, write as traceWrite, categories as traceCategories } from "trace"; import { isDataURI, isFileOrResourcePath } from "utils/utils"; export * from "./view-base"; // registerSpecialProperty("class", (instance: ViewDefinition, propertyValue: string) => { // instance.className = propertyValue; // }); // registerSpecialProperty("text", (instance, propertyValue) => { // instance.set("text", propertyValue); // }); declare module "ui/styling/style" { interface Style { effectiveMinWidth: number; effectiveMinHeight: number; effectiveWidth: number; effectiveHeight: number; effectiveMarginTop: number; effectiveMarginRight: number; effectiveMarginBottom: number; effectiveMarginLeft: number; effectivePaddingTop: number; effectivePaddingRight: number; effectivePaddingBottom: number; effectivePaddingLeft: number; effectiveBorderTopWidth: number; effectiveBorderRightWidth: number; effectiveBorderBottomWidth: number; effectiveBorderLeftWidth: number; } } export namespace layout { const MODE_SHIFT = 30; const MODE_MASK = 0x3 << MODE_SHIFT; export const UNSPECIFIED = 0 << MODE_SHIFT; export const EXACTLY = 1 << MODE_SHIFT; export const AT_MOST = 2 << MODE_SHIFT; export const MEASURED_HEIGHT_STATE_SHIFT = 0x00000010; /* 16 */ export const MEASURED_STATE_TOO_SMALL = 0x01000000; export const MEASURED_STATE_MASK = 0xff000000; export const MEASURED_SIZE_MASK = 0x00ffffff; export function getMeasureSpecMode(spec: number): number { return (spec & MODE_MASK); } export function getMeasureSpecSize(spec: number): number { return (spec & ~MODE_MASK); } export function getDisplayDensity(): number { return 1; } export function makeMeasureSpec(size: number, mode: number): number { return (Math.round(size) & ~MODE_MASK) | (mode & MODE_MASK); } export function toDevicePixels(value: number): number { return value * getDisplayDensity(); } export function toDeviceIndependentPixels(value: number): number { return value / getDisplayDensity(); } export function measureSpecToString(measureSpec: number): string { let mode = getMeasureSpecMode(measureSpec); let size = getMeasureSpecSize(measureSpec); let text = "MeasureSpec: "; if (mode === UNSPECIFIED) { text += "UNSPECIFIED "; } else if (mode === EXACTLY) { text += "EXACTLY "; } else if (mode === AT_MOST) { text += "AT_MOST "; } else { text += mode + " "; } text += size; return text; } } export function getViewById(view: ViewDefinition, id: string): ViewDefinition { if (!view) { return undefined; } if (view.id === id) { return view; } let retVal: ViewDefinition; let descendantsCallback = function (child: ViewDefinition): 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: ViewDefinition, callback: (child: ViewDefinition) => boolean) { if (!callback || !view) { return; } let continueIteration: boolean; let localCallback = function (child: ViewDefinition): boolean { continueIteration = callback(child); if (continueIteration) { child._eachChildView(localCallback); } return continueIteration; } view._eachChildView(localCallback); } export function PseudoClassHandler(...pseudoClasses: string[]): MethodDecorator { let stateEventNames = pseudoClasses.map(s => ":" + s); let listeners = Symbol("listeners"); return (target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor) => { function update(change: number) { let prev = this[listeners] || 0; let next = prev + change; if (prev <= 0 && next > 0) { this[propertyKey](true); } else if (prev > 0 && next <= 0) { this[propertyKey](false); } } stateEventNames.forEach(s => target[s] = update); } } let viewIdCounter = 0; export abstract class ViewCommon extends ViewBase implements ViewDefinition { public static loadedEvent = "loaded"; public static unloadedEvent = "unloaded"; private _measuredWidth: number; private _measuredHeight: number; _currentWidthMeasureSpec: number; _currentHeightMeasureSpec: number; private _oldLeft: number; private _oldTop: number; private _oldRight: number; private _oldBottom: number; private _parent: ViewCommon; private _visualState: string; private _isLoaded: boolean; private _isLayoutValid: boolean; private _cssType: string; private _updatingInheritedProperties: boolean; private _registeredAnimations: Array; public _domId: number; public _isAddedToNativeVisualTree: boolean; public _gestureObservers = {}; public cssClasses: Set = new Set(); public cssPseudoClasses: Set = new Set(); public _cssState: CssState; public parent: ViewCommon; constructor() { super(); this._domId = viewIdCounter++; this._goToVisualState("normal"); } observe(type: GestureTypes, callback: (args: GestureEventData) => void, thisArg?: any): void { if (!this._gestureObservers[type]) { this._gestureObservers[type] = []; } this._gestureObservers[type].push(observe(this, type, callback, thisArg)); } public getGestureObservers(type: GestureTypes): Array { return this._gestureObservers[type]; } public addEventListener(arg: string | GestureTypes, callback: (data: EventData) => void, thisArg?: any) { if (typeof arg === "string") { arg = getEventOrGestureName(arg); let gesture = gestureFromString(arg); if (gesture && !this._isEvent(arg)) { this.observe(gesture, callback, thisArg); } else { let events = (arg).split(","); if (events.length > 0) { for (let i = 0; i < events.length; i++) { let evt = events[i].trim(); let gst = gestureFromString(evt); if (gst && !this._isEvent(arg)) { this.observe(gst, callback, thisArg); } else { super.addEventListener(evt, callback, thisArg); } } } else { super.addEventListener(arg, callback, thisArg); } } } else if (typeof arg === "number") { this.observe(arg, callback, thisArg); } } public removeEventListener(arg: string | GestureTypes, callback?: any, thisArg?: any) { if (typeof arg === "string") { let gesture = gestureFromString(arg); if (gesture && !this._isEvent(arg)) { this._disconnectGestureObservers(gesture); } else { let events = (arg).split(","); if (events.length > 0) { for (let i = 0; i < events.length; i++) { let evt = events[i].trim(); let gst = gestureFromString(evt); if (gst && !this._isEvent(arg)) { this._disconnectGestureObservers(gst); } else { super.removeEventListener(evt, callback, thisArg); } } } else { super.removeEventListener(arg, callback, thisArg); } } } else if (typeof arg === "number") { this._disconnectGestureObservers(arg); } } public eachChild(callback: (child: ViewCommon) => boolean): void { this._eachChildView(callback); } private _isEvent(name: string): boolean { return this.constructor && `${name}Event` in this.constructor; } private _disconnectGestureObservers(type: GestureTypes): void { let observers = this.getGestureObservers(type); if (observers) { for (let i = 0; i < observers.length; i++) { observers[i].disconnect(); } } } getViewById(id: string): T { return getViewById(this, id); } // START Style property shortcuts get borderColor(): string | Color { return this.style.borderColor; } set borderColor(value: string | Color) { this.style.borderColor = value; } get borderTopColor(): Color { return this.style.borderTopColor; } set borderTopColor(value: Color) { this.style.borderTopColor = value; } get borderRightColor(): Color { return this.style.borderRightColor; } set borderRightColor(value: Color) { this.style.borderRightColor = value; } get borderBottomColor(): Color { return this.style.borderBottomColor; } set borderBottomColor(value: Color) { this.style.borderBottomColor = value; } get borderLeftColor(): Color { return this.style.borderLeftColor; } set borderLeftColor(value: Color) { this.style.borderLeftColor = value; } get borderWidth(): string | number { return this.style.borderWidth; } set borderWidth(value: string | number) { this.style.borderWidth = value; } get borderTopWidth(): Length { return this.style.borderTopWidth; } set borderTopWidth(value: Length) { this.style.borderTopWidth = value; } get borderRightWidth(): Length { return this.style.borderRightWidth; } set borderRightWidth(value: Length) { this.style.borderRightWidth = value; } get borderBottomWidth(): Length { return this.style.borderBottomWidth; } set borderBottomWidth(value: Length) { this.style.borderBottomWidth = value; } get borderLeftWidth(): Length { return this.style.borderLeftWidth; } set borderLeftWidth(value: Length) { this.style.borderLeftWidth = value; } get borderRadius(): string | number { return this.style.borderRadius; } set borderRadius(value: string | number) { this.style.borderRadius = value; } get borderTopLeftRadius(): number { return this.style.borderTopLeftRadius; } set borderTopLeftRadius(value: number) { this.style.borderTopLeftRadius = value; } get borderTopRightRadius(): number { return this.style.borderTopRightRadius; } set borderTopRightRadius(value: number) { this.style.borderTopRightRadius = value; } get borderBottomRightRadius(): number { return this.style.borderBottomRightRadius; } set borderBottomRightRadius(value: number) { this.style.borderBottomRightRadius = value; } get borderBottomLeftRadius(): number { return this.style.borderBottomLeftRadius; } set borderBottomLeftRadius(value: number) { this.style.borderBottomLeftRadius = value; } get color(): Color { return this.style.color; } set color(value: Color) { this.style.color = value; } get backgroundColor(): Color { return this.style.backgroundColor; } set backgroundColor(value: Color) { this.style.backgroundColor = value; } get backgroundImage(): string { return this.style.backgroundImage; } set backgroundImage(value: string) { this.style.backgroundImage = value; } get minWidth(): Length { return this.style.minWidth; } set minWidth(value: Length) { this.style.minWidth = value; } get minHeight(): Length { return this.style.minHeight; } set minHeight(value: Length) { this.style.minHeight = value; } get width(): Length { return this.style.width; } set width(value: Length) { this.style.width = value; } get height(): Length { return this.style.height; } set height(value: Length) { this.style.height = value; } get margin(): string { return this.style.margin; } set margin(value: string) { this.style.margin = value; } get marginLeft(): Length { return this.style.marginLeft; } set marginLeft(value: Length) { this.style.marginLeft = value; } get marginTop(): Length { return this.style.marginTop; } set marginTop(value: Length) { this.style.marginTop = value; } get marginRight(): Length { return this.style.marginRight; } set marginRight(value: Length) { this.style.marginRight = value; } get marginBottom(): Length { return this.style.marginBottom; } set marginBottom(value: Length) { this.style.marginBottom = value; } get horizontalAlignment(): "left" | "center" | "middle" | "right" | "stretch" { return this.style.horizontalAlignment; } set horizontalAlignment(value: "left" | "center" | "middle" | "right" | "stretch") { this.style.horizontalAlignment = value; } get verticalAlignment(): "top" | "center" | "middle" | "bottom" | "stretch" { return this.style.verticalAlignment; } set verticalAlignment(value: "top" | "center" | "middle" | "bottom" | "stretch") { this.style.verticalAlignment = value; } get visibility(): "visible" | "hidden" | "collapse" | "collapsed" { return this.style.visibility; } set visibility(value: "visible" | "hidden" | "collapse" | "collapsed") { this.style.visibility = value; } get opacity(): number { return this.style.opacity; } set opacity(value: number) { this.style.opacity = value; } get rotate(): number { return this.style.rotate; } set rotate(value: number) { this.style.rotate = value; } get translateX(): number { return this.style.translateX; } set translateX(value: number) { this.style.translateX = value; } get translateY(): number { return this.style.translateY; } set translateY(value: number) { this.style.translateY = value; } get scaleX(): number { return this.style.scaleX; } set scaleX(value: number) { this.style.scaleX = value; } get scaleY(): number { return this.style.scaleY; } set scaleY(value: number) { this.style.scaleY = value; } //END Style property shortcuts get page(): ViewDefinition { if (this.parent) { return this.parent.page; } return null; } public id: string; public automationText: string; public originX: number; public originY: number; public isEnabled: boolean; public isUserInteractionEnabled: boolean; public className: string; get isLayoutValid(): boolean { return this._isLayoutValid; } get cssType(): string { if (!this._cssType) { this._cssType = this.typeName.toLowerCase(); } return this._cssType; } get isLayoutRequired(): boolean { return true; } get isLoaded(): boolean { return this._isLoaded; } public onLoaded() { this._isLoaded = true; this._loadEachChildView(); this._applyStyleFromScope(); this._emit("loaded"); } public _loadEachChildView() { if (this._childrenCount > 0) { // iterate all children and call onLoaded on them first let eachChild = function (child: ViewCommon): boolean { child.onLoaded(); return true; } this._eachChildView(eachChild); } } public onUnloaded() { this._setCssState(null); this._unloadEachChildView(); this._isLoaded = false; this._emit("unloaded"); } public _unloadEachChildView() { if (this._childrenCount > 0) { this._eachChildView((child: ViewCommon) => { if (child.isLoaded) { child.onUnloaded(); } return true; }); } } // public _onPropertyChanged(property: Property, oldValue: any, newValue: any) { // super._onPropertyChanged(property, oldValue, newValue); // if (this._childrenCount > 0) { // let shouldUpdateInheritableProps = (property.inheritable && !(property instanceof styling.Property)); // if (shouldUpdateInheritableProps) { // this._updatingInheritedProperties = true; // this._eachChildView((child) => { // child._setValue(property, this._getValue(property), ValueSource.Inherited); // return true; // }); // this._updatingInheritedProperties = false; // } // } // this._checkMetadataOnPropertyChanged(property.metadata); // } // public _isInheritedChange() { // if (this._updatingInheritedProperties) { // return true; // } // let parentView: ViewDefinition; // parentView = (this.parent); // while (parentView) { // if (parentView._updatingInheritedProperties) { // return true; // } // parentView = (parentView.parent); // } // return false; // } // public _checkMetadataOnPropertyChanged(metadata: doPropertyMetadata) { // if (metadata.affectsLayout) { // this.requestLayout(); // } // if (metadata.affectsStyle) { // this.style._resetCssValues(); // this._applyStyleFromScope(); // this._eachChildView((v) => { // v._checkMetadataOnPropertyChanged(metadata); // return true; // }); // } // } public measure(widthMeasureSpec: number, heightMeasureSpec: number): void { this._setCurrentMeasureSpecs(widthMeasureSpec, heightMeasureSpec); } public layout(left: number, top: number, right: number, bottom: number): void { this._setCurrentLayoutBounds(left, top, right, bottom); } public getMeasuredWidth(): number { return this._measuredWidth & layout.MEASURED_SIZE_MASK || 0; } public getMeasuredHeight(): number { return this._measuredHeight & layout.MEASURED_SIZE_MASK || 0; } public getMeasuredState(): number { return (this._measuredWidth & layout.MEASURED_STATE_MASK) | ((this._measuredHeight >> layout.MEASURED_HEIGHT_STATE_SHIFT) & (layout.MEASURED_STATE_MASK >> layout.MEASURED_HEIGHT_STATE_SHIFT)); } public setMeasuredDimension(measuredWidth: number, measuredHeight: number): void { this._measuredWidth = measuredWidth; this._measuredHeight = measuredHeight; if (traceEnabled) { traceWrite(this + " :setMeasuredDimension: " + measuredWidth + ", " + measuredHeight, traceCategories.Layout); } } public requestLayout(): void { this._isLayoutValid = false; } public abstract onMeasure(widthMeasureSpec: number, heightMeasureSpec: number): void; public abstract onLayout(left: number, top: number, right: number, bottom: number): void; public abstract layoutNativeView(left: number, top: number, right: number, bottom: number): void; private pseudoClassAliases = { 'highlighted': [ 'active', 'pressed' ] }; 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]); } } } public static resolveSizeAndState(size: number, specSize: number, specMode: number, childMeasuredState: number): number { let result = size; switch (specMode) { case layout.UNSPECIFIED: result = size; break; case layout.AT_MOST: if (specSize < size) { result = Math.round(specSize + 0.499) | layout.MEASURED_STATE_TOO_SMALL; } break; case layout.EXACTLY: result = specSize; break; } return Math.round(result + 0.499) | (childMeasuredState & layout.MEASURED_STATE_MASK); } public static combineMeasuredStates(curState: number, newState): number { return curState | newState; } public static layoutChild(parent: ViewDefinition, child: ViewDefinition, left: number, top: number, right: number, bottom: number): void { if (!child || child.isCollapsed) { return; } let childStyle = child.style; let childTop: number; let childLeft: number; let childWidth = child.getMeasuredWidth(); let childHeight = child.getMeasuredHeight(); let effectiveMarginTop = childStyle.effectiveMarginTop; let effectiveMarginBottom = childStyle.effectiveMarginBottom; let vAlignment: string; if (childStyle.effectiveHeight >= 0 && childStyle.verticalAlignment === "stretch") { vAlignment = "center"; } else { vAlignment = childStyle.verticalAlignment; } let marginTop = childStyle.marginTop; let marginBottom = childStyle.marginBottom; let marginLeft = childStyle.marginLeft; let marginRight = childStyle.marginRight; switch (vAlignment) { case "top": childTop = top + effectiveMarginTop; break; case "center": case "middle": childTop = top + (bottom - top - childHeight + (effectiveMarginTop - effectiveMarginBottom)) / 2; break; case "bottom": childTop = bottom - childHeight - effectiveMarginBottom; break; case "stretch": default: childTop = top + effectiveMarginTop; childHeight = bottom - top - (effectiveMarginTop + effectiveMarginBottom); break; } let effectiveMarginLeft = childStyle.effectiveMarginLeft; let effectiveMarginRight = childStyle.effectiveMarginRight; let hAlignment: string; if (childStyle.effectiveWidth >= 0 && childStyle.horizontalAlignment === "stretch") { hAlignment = "center"; } else { hAlignment = childStyle.horizontalAlignment; } switch (hAlignment) { case "left": childLeft = left + effectiveMarginLeft; break; case "center": case "middle": childLeft = left + (right - left - childWidth + (effectiveMarginLeft - effectiveMarginRight)) / 2; break; case "right": childLeft = right - childWidth - effectiveMarginRight; break; case "stretch": default: childLeft = left + effectiveMarginLeft; childWidth = right - left - (effectiveMarginLeft + effectiveMarginRight); break; } let childRight = Math.round(childLeft + childWidth); let childBottom = Math.round(childTop + childHeight); childLeft = Math.round(childLeft); childTop = Math.round(childTop); if (traceEnabled) { traceWrite(child.parent + " :layoutChild: " + child + " " + childLeft + ", " + childTop + ", " + childRight + ", " + childBottom, traceCategories.Layout); } child.layout(childLeft, childTop, childRight, childBottom); } public static measureChild(parent: ViewCommon, child: ViewCommon, widthMeasureSpec: number, heightMeasureSpec: number): { measuredWidth: number; measuredHeight: number } { let measureWidth = 0; let measureHeight = 0; if (child && !child.isCollapsed) { let density = layout.getDisplayDensity(); let width = layout.getMeasureSpecSize(widthMeasureSpec); let widthMode = layout.getMeasureSpecMode(widthMeasureSpec); let height = layout.getMeasureSpecSize(heightMeasureSpec); let heightMode = layout.getMeasureSpecMode(heightMeasureSpec); let parentWidthMeasureSpec = parent._currentWidthMeasureSpec; updateChildLayoutParams(child, parent, density); let style = child.style; let horizontalMargins = style.effectiveMarginLeft + style.effectiveMarginRight; let verticalMargins = style.effectiveMarginTop + style.effectiveMarginRight; let childWidthMeasureSpec = ViewCommon.getMeasureSpec(width, widthMode, horizontalMargins, style.effectiveWidth, style.horizontalAlignment === "stretch"); let childHeightMeasureSpec = ViewCommon.getMeasureSpec(height, heightMode, verticalMargins, style.effectiveHeight, style.verticalAlignment === "stretch"); if (traceEnabled) { traceWrite(child.parent + " :measureChild: " + child + " " + layout.measureSpecToString(childWidthMeasureSpec) + ", " + layout.measureSpecToString(childHeightMeasureSpec), traceCategories.Layout); } child.measure(childWidthMeasureSpec, childHeightMeasureSpec); measureWidth = Math.round(child.getMeasuredWidth() + horizontalMargins); measureHeight = Math.round(child.getMeasuredHeight() + verticalMargins); } return { measuredWidth: measureWidth, measuredHeight: measureHeight }; } private static getMeasureSpec(parentLength: number, parentSpecMode: number, margins: number, childLength: number, stretched: boolean): number { let resultSize = 0; let resultMode = 0; // We want a specific size... let be it. if (childLength >= 0) { // If mode !== UNSPECIFIED we take the smaller of parentLength and childLength // Otherwise we will need to clip the view but this is not possible in all Android API levels. resultSize = parentSpecMode === layout.UNSPECIFIED ? childLength : Math.min(parentLength, childLength); resultMode = layout.EXACTLY; } else { switch (parentSpecMode) { // Parent has imposed an exact size on us case layout.EXACTLY: resultSize = Math.max(0, parentLength - margins); // if stretched - nativeView wants to be our size. So be it. // else - nativeView wants to determine its own size. It can't be bigger than us. resultMode = stretched ? layout.EXACTLY : layout.AT_MOST; break; // Parent has imposed a maximum size on us case layout.AT_MOST: resultSize = Math.max(0, parentLength - margins); resultMode = layout.AT_MOST; break; // Equivalent to measure with Infinity. case layout.UNSPECIFIED: resultSize = 0; resultMode = layout.UNSPECIFIED; break; } } return layout.makeMeasureSpec(resultSize, resultMode); } _setCurrentMeasureSpecs(widthMeasureSpec: number, heightMeasureSpec: number): boolean { let changed: boolean = this._currentWidthMeasureSpec !== widthMeasureSpec || this._currentHeightMeasureSpec !== heightMeasureSpec; this._currentWidthMeasureSpec = widthMeasureSpec; this._currentHeightMeasureSpec = heightMeasureSpec; return changed; } _getCurrentLayoutBounds(): { left: number; top: number; right: number; bottom: number } { return { left: this._oldLeft, top: this._oldTop, right: this._oldRight, bottom: this._oldBottom } } /** * Returns two booleans - the first if "boundsChanged" the second is "sizeChanged". */ _setCurrentLayoutBounds(left: number, top: number, right: number, bottom: number): { boundsChanged: boolean, sizeChanged: boolean } { this._isLayoutValid = true; let boundsChanged: boolean = this._oldLeft !== left || this._oldTop !== top || this._oldRight !== right || this._oldBottom !== bottom; let sizeChanged: boolean = (this._oldRight - this._oldLeft !== right - left) || (this._oldBottom - this._oldTop !== bottom - top); this._oldLeft = left; this._oldTop = top; this._oldRight = right; this._oldBottom = bottom; return { boundsChanged, sizeChanged }; } private _applyStyleFromScope() { let rootPage = this.page; if (!rootPage || !rootPage.isLoaded) { return; } let scope: StyleScope = (rootPage)._getStyleScope(); scope.applySelectors(this); } private _applyInlineStyle(inlineStyle) { if (typeof inlineStyle === "string") { try { // this.style._beginUpdate(); applyInlineSyle(this, inlineStyle); } finally { // this.style._endUpdate(); } } } // TODO: We need to implement some kind of build step that includes these members only when building for Android //@android public _context: android.content.Context; public _onAttached(context: android.content.Context) { // } public _onDetached(force?: boolean) { // } public _createUI() { // } public _onContextChanged() { // } //@endandroid // TODO: We need to implement some kind of build step that includes these members only when building for iOS //@endios get _childrenCount(): number { return 0; } public _eachChildView(callback: (view: ViewCommon) => boolean) { // } _childIndexToNativeChildIndex(index?: number): number { return index; } _getNativeViewsCount(): number { return this._isAddedToNativeVisualTree ? 1 : 0; } _eachLayoutView(callback: (View) => void): void { return callback(this); } _addToSuperview(superview: any, index?: number): boolean { // IOS specific return false; } _removeFromSuperview(): void { // IOS specific } /** * Core logic for adding a child view to this instance. Used by the framework to handle lifecycle events more centralized. Do not outside the UI Stack implementation. * // TODO: Think whether we need the base Layout routine. */ public _addView(view: ViewDefinition, 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); } /** * Method is intended to be overridden by inheritors and used as "protected" */ public _addViewCore(view: ViewDefinition, atIndex?: number) { this._propagateInheritableProperties(view); if (!view._isAddedToNativeVisualTree) { let nativeIndex = this._childIndexToNativeChildIndex(atIndex); view._isAddedToNativeVisualTree = this._addViewToNativeVisualTree(view, nativeIndex); } // TODO: Discuss this. if (this._isLoaded) { view.onLoaded(); } } public _propagateInheritableProperties(view: ViewDefinition) { propagateInheritedProperties(this); // view._inheritProperties(this); // view.style._inheritStyleProperties(this); } // public _inheritProperties(parentView: ViewDefinition) { // parentView._eachSetProperty((property) => { // if (!(property instanceof styling.Property) && property.inheritable) { // let baseValue = parentView._getValue(property); // this._setValue(property, baseValue, ValueSource.Inherited); // } // return true; // }); // } /** * 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: ViewDefinition) { 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: ViewDefinition) { // TODO: Change type from ViewCommon to ViewBase. Probably this // method will need to go to ViewBase class. // Remove the view from the native visual scene first this._removeViewFromNativeVisualTree(view); // TODO: Discuss this. if (view.isLoaded) { view.onUnloaded(); } // view.unsetInheritedProperties(); } public unsetInheritedProperties(): void { // this._setValue(ProxyObject.bindingContextProperty, undefined, ValueSource.Inherited); // this._eachSetProperty((property) => { // if (!(property instanceof styling.Property) && property.inheritable) { // this._resetValue(property, ValueSource.Inherited); // } // return true; // }); } public _parentChanged(oldParent: ViewDefinition): void { //Overridden if (oldParent) { // Move these method in property class. clearInheritedProperties(this); } } /** * Method is intended to be overridden by inheritors and used as "protected". */ public _addViewToNativeVisualTree(view: ViewDefinition, 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: ViewDefinition) { 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 _updateLayout() { // needed for iOS. } get _nativeView(): any { return undefined; } public _shouldApplyStyleHandlers() { // If we have native view we are ready to apply style handelr; return !!this._nativeView; } public focus(): boolean { return undefined; } public getLocationInWindow(): Point { return undefined; } public getLocationOnScreen(): Point { return undefined; } public getLocationRelativeTo(otherView: ViewDefinition): Point { return undefined; } public getActualSize(): Size { let currentBounds = this._getCurrentLayoutBounds(); if (!currentBounds) { return undefined; } return { width: layout.toDeviceIndependentPixels(currentBounds.right - currentBounds.left), height: layout.toDeviceIndependentPixels(currentBounds.bottom - currentBounds.top), } } public animate(animation: any): AnimationPromise { return this.createAnimation(animation).play(); } public createAnimation(animation: any): any { animation.target = this; return new Animation([animation]); } 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 _unregisterAllAnimations() { if (this._registeredAnimations) { for (let animation of this._registeredAnimations) { animation.cancel(); } } } public toString(): string { let str = this.typeName; if (this.id) { str += `<${this.id}>`; } else { str += `(${this._domId})`; } let source = Source.get(this); if (source) { str += `@${source};`; } return str; } public _setNativeViewFrame(nativeView: any, frame: any) { // } // public _onStylePropertyChanged(property: Property): void { // // // } // protected _canApplyNativeProperty(): boolean { // // Check for a valid _nativeView instance // return !!this._nativeView; // } private notifyPseudoClassChanged(pseudoClass: string): void { this.notify({ eventName: ":" + pseudoClass, object: this }); } // TODO: Make sure the state is set to null and this is called on unloaded to clean up change listeners... _setCssState(next: 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(); } /** * 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(); } } function getEffectiveValue(prentAvailableLength: number, density: number, param: Length): number { switch (param.unit) { case "%": return Math.round(prentAvailableLength * param.value); case "px": return Math.round(param.value); default: case "dip": return Math.round(density * param.value); } } function updateChildLayoutParams(child: ViewCommon, parent: ViewCommon, density: number): void { let style = child.style; let parentWidthMeasureSpec = parent._currentWidthMeasureSpec; let parentWidthMeasureSize = layout.getMeasureSpecSize(parentWidthMeasureSpec); let parentWidthMeasureMode = layout.getMeasureSpecMode(parentWidthMeasureSpec); let parentAvailableWidth = parentWidthMeasureMode === layout.UNSPECIFIED ? -1 : parentWidthMeasureSize; style.effectiveWidth = getEffectiveValue(parentAvailableWidth, density, style.width); style.effectiveMarginLeft = getEffectiveValue(parentAvailableWidth, density, style.marginLeft); style.effectiveMarginRight = getEffectiveValue(parentAvailableWidth, density, style.marginRight); let parentHeightMeasureSpec = parent._currentHeightMeasureSpec; let parentHeightMeasureSize = layout.getMeasureSpecSize(parentHeightMeasureSpec); let parentHeightMeasureMode = layout.getMeasureSpecMode(parentHeightMeasureSpec); let parentAvailableHeight = parentHeightMeasureMode === layout.UNSPECIFIED ? -1 : parentHeightMeasureSize; style.effectiveHeight = getEffectiveValue(parentAvailableHeight, density, style.height); style.effectiveMarginTop = getEffectiveValue(parentAvailableHeight, density, style.marginTop); style.effectiveMarginBottom = getEffectiveValue(parentAvailableHeight, density, style.marginBottom); } interface Length { readonly unit: "%" | "dip" | "px"; readonly value: number; } export namespace Length { export function parse(value: string | Length): Length { if (typeof value === "string") { let type: "%" | "dip" | "px"; let numberValue = 0; let stringValue = value.trim(); let percentIndex = stringValue.indexOf("%"); if (percentIndex !== -1) { type = "%"; // if only % or % is not last we treat it as invalid value. if (percentIndex !== (stringValue.length - 1) || percentIndex === 0) { numberValue = Number.NaN; } else { numberValue = parseFloat(stringValue.substring(0, stringValue.length - 1).trim()) / 100; } } else { if (stringValue.indexOf("px") !== -1) { type = "px"; stringValue = stringValue.replace("px", "").trim(); } else { type = "dip"; } numberValue = parseFloat(stringValue); } if (isNaN(numberValue) || !isFinite(numberValue)) { throw new Error("Invalid value: " + value); } return { value: numberValue, unit: type } } else { return value; } } } function onCssClassPropertyChanged(view: ViewCommon, oldValue: string, newValue: string) { let classes = view.cssClasses; classes.clear(); if (typeof newValue === "string") { newValue.split(" ").forEach(c => classes.add(c)); } } export const classNameProperty = new Property({ name: "className", valueChanged: onCssClassPropertyChanged }); classNameProperty.register(ViewCommon); function resetStyles(view: ViewCommon): void { // view.style._resetCssValues(); // view._applyStyleFromScope(); view._eachChildView((child) => { // TODO.. Check old implementation.... resetStyles(child); return true; }); } // let idProperty = new Property("id", "View", new PropertyMetadata(undefined, PropertyMetadataSettings.AffectsStyle)); export const idProperty = new Property({ name: "id", valueChanged: (view, oldValue, newValue) => resetStyles(view) }); idProperty.register(ViewCommon); export const automationTextProperty = new Property({ name: "automationText" }); automationTextProperty.register(ViewCommon); export const originXProperty = new Property({ name: "originX", defaultValue: 0.5 }); originXProperty.register(ViewCommon); export const originYProperty = new Property({ name: "originY", defaultValue: 0.5 }); originYProperty.register(ViewCommon); export const isEnabledProperty = new Property({ name: "isEnabled", defaultValue: true }); isEnabledProperty.register(ViewCommon); export const isUserInteractionEnabledProperty = new Property({ name: "isUserInteractionEnabled", defaultValue: true }); isUserInteractionEnabledProperty.register(ViewCommon); const zeroLength: Length = { value: 0, unit: "px" }; export const minWidthProperty = new CssProperty({ name: "minWidth", cssName: "min-width", defaultValue: zeroLength, affectsLayout: isIOS, valueChanged: (target, newValue) => { target.effectiveMinWidth = getEffectiveValue(0, layout.getDisplayDensity(), newValue); }, valueConverter: Length.parse }); minWidthProperty.register(Style); export const minHeightProperty = new CssProperty({ name: "minHeight", cssName: "min-height", defaultValue: zeroLength, affectsLayout: isIOS, valueChanged: (target, newValue) => { target.effectiveMinHeight = getEffectiveValue(0, layout.getDisplayDensity(), newValue); }, valueConverter: Length.parse }); minHeightProperty.register(Style); const matchParent: Length = { value: -1, unit: "px" }; export const widthProperty = new CssProperty({ name: "width", cssName: "width", defaultValue: matchParent, affectsLayout: isIOS, valueConverter: Length.parse }); widthProperty.register(Style); export const heightProperty = new CssProperty({ name: "height", cssName: "height", defaultValue: matchParent, affectsLayout: isIOS, valueConverter: Length.parse }); heightProperty.register(Style); export const marginProperty = new ShorthandProperty