diff --git a/.gitignore b/.gitignore index bfaaf29ce..9a3d7ca49 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ package-lock.json .DS_Store .nsbuildinfo tsdoc-metadata.json +thumbs.db # IDEs and editors /.idea diff --git a/.travis.yml b/.travis.yml index b9b29624e..768ac73dc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,6 +7,9 @@ script: - npm run setup - npm run unit-test +# public API changes check +- npm run api-extractor-ci + # circular references check - cd tests && npm i - node_modules/.bin/webpack --env.android diff --git a/.vscode/launch.json b/.vscode/launch.json index 38041cb91..0b611bc76 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -7,7 +7,7 @@ { "type": "node", "request": "launch", - "name": "Unit Tests", + "name": "Launch mocha tests", "program": "${workspaceRoot}/node_modules/mocha/bin/_mocha", "args": [ "--timeout", @@ -15,82 +15,67 @@ "--opts", "unit-tests/mocha.opts" ], - "internalConsoleOptions": "openOnSessionStart", + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", "preLaunchTask": "tsc-unit-tests" }, { - "name": "Launch on iOS", - "type": "nativescript", - "request": "launch", - "platform": "ios", - "appRoot": "${workspaceRoot}", - "sourceMaps": true, - "watch": true - }, - // { - // "name": "Test on iOS", - // "type": "nativescript", - // "request": "launch", - // "platform": "ios", - // "appRoot": "${workspaceRoot}", - // "sourceMaps": true, - // "watch": false, - // "stopOnEntry": true, - // "launchTests": true, - // "tnsArgs": [ - // "--justlaunch" - // ] - // }, - { - "name": "Attach on iOS", - "type": "nativescript", - "request": "attach", - "platform": "ios", - "appRoot": "${workspaceRoot}", - "sourceMaps": true, - "watch": false - }, - { - "name": "Launch on Android", - "type": "nativescript", - "request": "launch", - "platform": "android", - "appRoot": "${workspaceRoot}", - "sourceMaps": true, - "watch": true - }, - { - "name": "Debug tests on Android", + "name": "Launch tests on Android", "type": "nativescript", "request": "launch", "platform": "android", "appRoot": "${workspaceRoot}/tests", "sourceMaps": true, "stopOnEntry": true, - "watch": true, + "watch": true }, - // { - // "name": "Test on Android", - // "type": "nativescript", - // "request": "launch", - // "platform": "android", - // "appRoot": "${workspaceRoot}", - // "sourceMaps": true, - // "watch": false, - // "stopOnEntry": true, - // "launchTests": true, - // "tnsArgs": [ - // "--justlaunch" - // ] - // }, { - "name": "Attach on Android", + "name": "Launch tests on iOS", + "type": "nativescript", + "request": "launch", + "platform": "ios", + "appRoot": "${workspaceRoot}/tests", + "sourceMaps": true, + "stopOnEntry": true, + "watch": true + }, + { + "name": "Attach ui tests on Android", "type": "nativescript", "request": "attach", "platform": "android", - "appRoot": "${workspaceRoot}", + "appRoot": "${workspaceRoot}/e2e/ui-tests-app", "sourceMaps": true, "watch": false + }, + { + "name": "Launch ui tests on Android", + "type": "nativescript", + "request": "launch", + "platform": "android", + "appRoot": "${workspaceRoot}/e2e/ui-tests-app", + "sourceMaps": true, + "stopOnEntry": true, + "watch": true + }, + { + "name": "Attach ui tests on iOS", + "type": "nativescript", + "request": "attach", + "platform": "ios", + "appRoot": "${workspaceRoot}/e2e/ui-tests-app", + "sourceMaps": true, + "watch": false + }, + { + "name": "Launch ui tests on iOS", + "type": "nativescript", + "request": "launch", + "platform": "ios", + "appRoot": "${workspaceRoot}/e2e/ui-tests-app", + "sourceMaps": true, + "stopOnEntry": true, + "watch": true } ] -} +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 7dc223a06..7781a38a2 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,28 +1,14 @@ { - // See http://go.microsoft.com/fwlink/?LinkId=733558 - // for the documentation about the tasks.json format - "version": "0.1.0", - "command": "tsc", - "isShellCommand": true, - "args": ["-p", "."], - "showOutput": "always", - "problemMatcher": "$tsc", - "tasks": [ - { - "taskName": "tsc-unit-tests", + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "tsc-unit-tests", + "type": "shell", "problemMatcher": "$tsc", "command": "./node_modules/.bin/tsc", - "args": [ "-p", "tsconfig.unit-tests.json" ] - }, - { - "taskName": "unit-tests", - "command": "npm", - "args": ["run", "unit-test"] - }, - { - "taskName": "unit-tests-watch", - "command": "npm", - "args": ["run", "unit-test-watch"] - } - ] + "args": [ "-p", "unit-tests/tsconfig.json" ], + } + ] } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index ad14ac758..e72aaab8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1172,7 +1172,7 @@ A full list of breaking changes could be found [here](https://github.com/NativeS - [(#2834)](https://github.com/NativeScript/NativeScript/issues/2834) Animations: scale() syntax does not support only one argument -- [(#2813)](https://github.com/NativeScript/NativeScript/issues/2813) The Camera module in Android doesn't handle pemissions for you +- [(#2813)](https://github.com/NativeScript/NativeScript/issues/2813) The Camera module in Android doesn't handle permissions for you - [(#2789)](https://github.com/NativeScript/NativeScript/issues/2789) CSS border-width causes text to overflow TextView diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0da6639d9..a7f5d5ca0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,6 +7,7 @@ Here are some guides on how to do that: - [Reporting Bugs](#bugs) - [Requesting New Features](#features) - [Submitting a PR](#pr) + - [Check test report](#test-report) - [Commit Message Guidelines](#commit-messages) - [Releasing new versions](#release) @@ -69,6 +70,7 @@ git checkout -b master - Rebase your changes to the latest master: `git pull --rebase upstream master`. - Ensure all unit test are green for Android and iOS. Check [running unit tests](DevelopmentWorkflow.md#running-unit-tests). - Ensure your changes pass tslint validation. (run `npm run tslint` in the root of the repo). + - If you've made changes to a public API, make sure you update and add the `api-reports/NativeScript.api.md` file to the PR. (run `npm run api-extractor` to update the api-report and definitions). 6. Push your fork. If you have rebased you might have to use force-push your branch: ``` @@ -81,6 +83,23 @@ It's our turn from there on! We will review the PR and discuss changes you might >Note: Sometimes you will see someone from the contributors team writing strange comments like: `test` or `test branch_functional_tests#css-gradients-tests branch_widgets#vultix/css-gradients` - don't worry about it, these are just phrases that trigger the internal CI builds. +## Check test report + +The purpose of the test report view is to show the tests' results for the PRs for the external contributors. When a NativeScript team member triggers the tests, you can review the result by selecting the `Details` button next to the `ci/jenkins/core-modules-tests` task. + +> Note: the `Details` button will be available when the execution of the test completes and there is at least one failing test. + +When you click on the button, you will be redirected to the report page. On the left pane you can find a list of all failed jobs. + +> Note: Each item name consists of the application name, type of device and platform version: `pr-e2e-tests-[application-name]-[device-type]-[platform-version]`. Usually, the test applications, that are executed for PRs are part of NativeScript repository. + +Based on the executed suite, one of the following or all of the following files will be generated: `mochawesome.html` | `index.html` | `unit-tests.log`. Some of the reports also might include `*.png`, `*.logs` or `[page source].xml` files that can help in understanding where is the problem. + +For example: +1. When you select the `index.html` page, an additional `TestNG Results` sidebar will be displayed. There you can find a list of all failures. +2. When you select one of them, you will see on the right side all tests, that have been executed. The problematic ones will be coloured in red. +3. If you click on one of them, detailed info or error log will be displayed. As we've mentioned above in some of the test reports, you will also find screenshots, that demonstrates the problem visually. Those images can be found below the info/ error log. + ## Commit Message Guidelines Please follow the git commit message format described below when committing your changes or submitting a PR. That allows us to use the commit messages to generate a change log for every new release. diff --git a/README.md b/README.md index 6a808e93f..f73f597f7 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Check out the links below to get started: ## For CTOs -Making the right technology choices is key to success. Our [CTO’s guide to NativeScript](https://www.nativescript.org/ctos-guide) helps you understand why NativeScript is the right choice for your next mobile project. +Making the right technology choices is the key to success. Our [CTO’s guide to NativeScript](https://www.nativescript.org/ctos-guide) helps you understand why NativeScript is the right choice for your next mobile project. ## Getting Started and Installation diff --git a/api-reports/NativeScript.api.md b/api-reports/NativeScript.api.md index ecb5ddee9..c047e2f30 100644 --- a/api-reports/NativeScript.api.md +++ b/api-reports/NativeScript.api.md @@ -170,6 +170,16 @@ export class AndroidApplication extends Observable { nativeApp: any /* android.app.Application */; + on(eventNames: string, callback: (data: AndroidActivityEventData) => void, thisArg?: any); + + on(event: "activityCreated", callback: (args: AndroidActivityBundleEventData) => void, thisArg?: any); + + on(event: "activityDestroyed", callback: (args: AndroidActivityEventData) => void, thisArg?: any); + + on(event: "activityStarted", callback: (args: AndroidActivityEventData) => void, thisArg?: any); + + on(event: "activityPaused", callback: (args: AndroidActivityEventData) => void, thisArg?: any); + on(event: "activityResumed", callback: (args: AndroidActivityEventData) => void, thisArg?: any); on(event: "activityStopped", callback: (args: AndroidActivityEventData) => void, thisArg?: any); @@ -184,16 +194,6 @@ export class AndroidApplication extends Observable { on(event: "activityRequestPermissions", callback: (args: AndroidActivityRequestPermissionsEventData) => void, thisArg?: any); - on(eventNames: string, callback: (data: AndroidActivityEventData) => void, thisArg?: any); - - on(event: "activityCreated", callback: (args: AndroidActivityBundleEventData) => void, thisArg?: any); - - on(event: "activityDestroyed", callback: (args: AndroidActivityEventData) => void, thisArg?: any); - - on(event: "activityStarted", callback: (args: AndroidActivityEventData) => void, thisArg?: any); - - on(event: "activityPaused", callback: (args: AndroidActivityEventData) => void, thisArg?: any); - orientation: "portrait" | "landscape" | "unknown"; packageName: string; @@ -219,7 +219,7 @@ export class Animation { // (undocumented) public isPlaying: boolean; // Warning: (ae-forgotten-export) The symbol "AnimationPromise" needs to be exported by the entry point index.d.ts - // + // // (undocumented) public play: (resetOnFinish?: boolean) => AnimationPromise; // (undocumented) @@ -404,15 +404,15 @@ export class ChangeType { // @public export class Color { constructor(knownColor: string); - constructor(alpha: number, red: number, green: number, blue: number); constructor(hex: string); constructor(argb: number); + constructor(alpha: number, red: number, green: number, blue: number); public a: number; android: number; public argb: number; public b: number; - public static equals(value1: Color, value2: Color): boolean; public equals(value: Color): boolean; + public static equals(value1: Color, value2: Color): boolean; public g: number; public hex: string; ios: any /* UIColor */; @@ -432,7 +432,7 @@ export interface CommonLayoutParams { // (undocumented) heightPercent: number; // Warning: (ae-forgotten-export) The symbol "HorizontalAlignment" needs to be exported by the entry point index.d.ts - // + // // (undocumented) horizontalAlignment: HorizontalAlignment; // (undocumented) @@ -448,7 +448,7 @@ export interface CommonLayoutParams { // (undocumented) topMarginPercent: number; // Warning: (ae-forgotten-export) The symbol "VerticalAlignment" needs to be exported by the entry point index.d.ts - // + // // (undocumented) verticalAlignment: VerticalAlignment; // (undocumented) @@ -466,7 +466,7 @@ export const Connectivity: { }; // Warning: (ae-forgotten-export) The symbol "AddChildFromBuilder" needs to be exported by the entry point index.d.ts -// +// // @public export class ContentView extends View implements AddChildFromBuilder { // (undocumented) @@ -524,7 +524,7 @@ export class DefaultErrorHandler implements ErrorHandler { } // Warning: (ae-forgotten-export) The symbol "Device" needs to be exported by the entry point index.d.ts -// +// // @public export const Device: Device_2; @@ -668,19 +668,19 @@ export class FileSystemEntity { // @public (undocumented) export class FlexboxLayout extends LayoutBase { // Warning: (ae-forgotten-export) The symbol "AlignContent" needs to be exported by the entry point index.d.ts - // + // // (undocumented) public alignContent: AlignContent; // Warning: (ae-forgotten-export) The symbol "AlignItems" needs to be exported by the entry point index.d.ts - // + // // (undocumented) public alignItems: AlignItems; // Warning: (ae-forgotten-export) The symbol "FlexDirection" needs to be exported by the entry point index.d.ts - // + // // (undocumented) public flexDirection: FlexDirection; // Warning: (ae-forgotten-export) The symbol "FlexWrap" needs to be exported by the entry point index.d.ts - // + // // (undocumented) public flexWrap: FlexWrap; // (undocumented) @@ -694,11 +694,11 @@ export class FlexboxLayout extends LayoutBase { // (undocumented) public static getOrder(view: View): number; // Warning: (ae-forgotten-export) The symbol "JustifyContent" needs to be exported by the entry point index.d.ts - // + // // (undocumented) public justifyContent: JustifyContent; // Warning: (ae-forgotten-export) The symbol "AlignSelf" needs to be exported by the entry point index.d.ts - // + // // (undocumented) public static setAlignSelf(view: View, align: AlignSelf); // (undocumented) @@ -845,7 +845,7 @@ export class Frame extends View { _removeFromFrameStack(); // Warning: (ae-forgotten-export) The symbol "NavigationType" needs to be exported by the entry point index.d.ts - // + // // (undocumented) setCurrent(entry: BackstackEntry, navigationType: NavigationType): void; @@ -1112,9 +1112,9 @@ export class ImageCache extends Observable { enqueue(request: DownloadRequest); get(key: string): any; maxRequests: number; - on(event: "downloadError", callback: (args: DownloadError) => void, thisArg?: any); on(eventNames: string, callback: (args: EventData) => void, thisArg?: any); on(event: "downloaded", callback: (args: DownloadedData) => void, thisArg?: any); + on(event: "downloadError", callback: (args: DownloadError) => void, thisArg?: any); // (undocumented) _onDownloadCompleted(key: string, image: any); // (undocumented) @@ -1144,11 +1144,11 @@ export class ImageSource { static fromBase64Sync(source: string): ImageSource; + static fromData(data: any): Promise; + // @deprecated (undocumented) fromData(data: any): Promise; - static fromData(data: any): Promise; - static fromDataSync(data: any): ImageSource; static fromFile(path: string): Promise; @@ -1206,7 +1206,7 @@ export class ImageSource { export type InstrumentationMode = "counters" | "timeline" | "lifecycle"; // @public -export interface iOSApplication { +export class iOSApplication { /* tslint:enable */ addNotificationObserver(notificationName: string, onReceiveCallback: (notification: any /* NSNotification */) => void): any; @@ -1309,7 +1309,7 @@ export interface LaunchEventData extends ApplicationEventData { } // Warning: (ae-forgotten-export) The symbol "CustomLayoutView" needs to be exported by the entry point index.d.ts -// +// // @public export class LayoutBase extends CustomLayoutView { addChild(view: View): void; @@ -1372,10 +1372,10 @@ export class ListView extends View { itemTemplates: string | Array; itemTemplateSelector: string | ((item: any, index: number, items: any) => string); public static loadMoreItemsEvent: string; - on(event: "loadMoreItems", callback: (args: EventData) => void, thisArg?: any); - on(event: "itemLoading", callback: (args: ItemEventData) => void, thisArg?: any); on(eventNames: string, callback: (data: EventData) => void, thisArg?: any); + on(event: "itemLoading", callback: (args: ItemEventData) => void, thisArg?: any); on(event: "itemTap", callback: (args: ItemEventData) => void, thisArg?: any); + on(event: "loadMoreItems", callback: (args: EventData) => void, thisArg?: any); refresh(); rowHeight: Length; scrollToIndex(index: number); @@ -1482,10 +1482,10 @@ export class Observable { off(eventNames: string, callback?: any, thisArg?: any); - on(event: "propertyChange", callback: (data: EventData) => void, thisArg?: any); - on(eventNames: string, callback: (data: EventData) => void, thisArg?: any); + on(event: "propertyChange", callback: (data: EventData) => void, thisArg?: any); + once(event: string, callback: (data: EventData) => void, thisArg?: any); public static propertyChangeEvent: string; @@ -1506,10 +1506,10 @@ export class ObservableArray extends Observable { public static changeEvent: string; - concat(...items: T[]): T[]; - concat(...items: U[]): T[]; + concat(...items: T[]): T[]; + every(callbackfn: (value: T, index: number, array: T[]) => boolean, thisArg?: any): boolean; filter(callbackfn: (value: T, index: number, array: T[]) => boolean, thisArg?: any): T[]; @@ -1558,10 +1558,10 @@ export class ObservableArray extends Observable { sort(compareFn?: (a: T, b: T) => number): T[]; - splice(start: number, deleteCount: number, ...items: T[]): T[]; - splice(start: number): T[]; + splice(start: number, deleteCount: number, ...items: T[]): T[]; + // (undocumented) toLocaleString(): string; @@ -1610,14 +1610,14 @@ export class Page extends ContentView { public on(eventNames: string, callback: (data: EventData) => void, thisArg?: any): void; + public on(event: "navigatingTo", callback: (args: NavigatedData) => void, thisArg?: any): void; + public on(event: "navigatedTo", callback: (args: NavigatedData) => void, thisArg?: any): void; public on(event: "navigatingFrom", callback: (args: NavigatedData) => void, thisArg?: any): void; public on(event: "navigatedFrom", callback: (args: NavigatedData) => void, thisArg?: any): void; - public on(event: "navigatingTo", callback: (args: NavigatedData) => void, thisArg?: any): void; - public onNavigatedFrom(isBackNavigation: boolean): void; public onNavigatedTo(isBackNavigation: boolean): void; @@ -1779,10 +1779,10 @@ export class ScrollView extends ContentView { isScrollEnabled: boolean; - on(event: "scroll", callback: (args: ScrollEventData) => void, thisArg?: any); - on(eventNames: string, callback: (data: EventData) => void, thisArg?: any); + on(event: "scroll", callback: (args: ScrollEventData) => void, thisArg?: any); + // (undocumented) _onOrientationChanged(); @@ -1816,11 +1816,11 @@ export class SearchBar extends View { ios: any /* UISearchBar */; - on(event: "close", callback: (args: EventData) => void, thisArg?: any); + on(eventNames: string, callback: (data: EventData) => void, thisArg?: any); on(event: "submit", callback: (args: EventData) => void, thisArg?: any); - on(eventNames: string, callback: (data: EventData) => void, thisArg?: any); + on(event: "close", callback: (args: EventData) => void, thisArg?: any); public static submitEvent: string; @@ -1832,7 +1832,7 @@ export class SearchBar extends View { } // Warning: (ae-forgotten-export) The symbol "AddArrayFromBuilder" needs to be exported by the entry point index.d.ts -// +// // @public export class SegmentedBar extends View implements AddChildFromBuilder, AddArrayFromBuilder { // (undocumented) @@ -1875,6 +1875,8 @@ export interface ShowModalOptions { animated?: boolean; + cancelable?: boolean + closeCallback: Function; context: any; @@ -1962,17 +1964,17 @@ export class Style extends Observable { // (undocumented) public backgroundColor: Color; // Warning: (ae-forgotten-export) The symbol "LinearGradient" needs to be exported by the entry point index.d.ts - // + // // (undocumented) public backgroundImage: string | LinearGradient; // Warning: (ae-forgotten-export) The symbol "Background" needs to be exported by the entry point index.d.ts - // + // // (undocumented) public backgroundInternal: Background; // (undocumented) public backgroundPosition: string; // Warning: (ae-forgotten-export) The symbol "BackgroundRepeat" needs to be exported by the entry point index.d.ts - // + // // (undocumented) public backgroundRepeat: BackgroundRepeat; // (undocumented) @@ -2014,17 +2016,17 @@ export class Style extends Observable { // (undocumented) public flexDirection: FlexDirection; // Warning: (ae-forgotten-export) The symbol "FlexGrow" needs to be exported by the entry point index.d.ts - // + // // (undocumented) public flexGrow: FlexGrow; // Warning: (ae-forgotten-export) The symbol "FlexShrink" needs to be exported by the entry point index.d.ts - // + // // (undocumented) public flexShrink: FlexShrink; // (undocumented) public flexWrap: FlexWrap; // Warning: (ae-forgotten-export) The symbol "FlexWrapBefore" needs to be exported by the entry point index.d.ts - // + // // (undocumented) public flexWrapBefore: FlexWrapBefore; // (undocumented) @@ -2067,7 +2069,7 @@ export class Style extends Observable { // (undocumented) public opacity: number; // Warning: (ae-forgotten-export) The symbol "Order" needs to be exported by the entry point index.d.ts - // + // // (undocumented) public order: Order; // (undocumented) @@ -2109,19 +2111,19 @@ export class Style extends Observable { // (undocumented) public tabTextFontSize: number; // Warning: (ae-forgotten-export) The symbol "TextAlignment" needs to be exported by the entry point index.d.ts - // + // // (undocumented) public textAlignment: TextAlignment; // (undocumented) public textDecoration: TextDecoration; // Warning: (ae-forgotten-export) The symbol "TextTransform" needs to be exported by the entry point index.d.ts - // + // // (undocumented) public textTransform: TextTransform; // (undocumented) public tintColor: Color; // Warning: (ae-forgotten-export) The symbol "dip" needs to be exported by the entry point index.d.ts - // + // // (undocumented) public translateX: dip; // (undocumented) @@ -2133,11 +2135,11 @@ export class Style extends Observable { // (undocumented) public viewRef: WeakRef; // Warning: (ae-forgotten-export) The symbol "Visibility" needs to be exported by the entry point index.d.ts - // + // // (undocumented) public visibility: Visibility; // Warning: (ae-forgotten-export) The symbol "WhiteSpace" needs to be exported by the entry point index.d.ts - // + // // (undocumented) public whiteSpace: WhiteSpace; // (undocumented) @@ -2681,12 +2683,12 @@ export abstract class View extends ViewBase { // (undocumented) _modalParent?: View; off(eventNames: string | GestureTypes, callback?: (args: EventData) => void, thisArg?: any); - on(event: "showingModally", callback: (args: ShownModallyData) => void, thisArg?: any): void; - on(event: "androidBackPressed", callback: (args: EventData) => void, thisArg?: any); - on(event: "shownModally", callback: (args: ShownModallyData) => void, thisArg?: any); + on(eventNames: string | GestureTypes, callback: (args: EventData) => void, thisArg?: any); on(event: "loaded", callback: (args: EventData) => void, thisArg?: any); on(event: "unloaded", callback: (args: EventData) => void, thisArg?: any); - on(eventNames: string | GestureTypes, callback: (args: EventData) => void, thisArg?: any); + on(event: "androidBackPressed", callback: (args: EventData) => void, thisArg?: any); + on(event: "showingModally", callback: (args: ShownModallyData) => void, thisArg?: any): void; + on(event: "shownModally", callback: (args: ShownModallyData) => void, thisArg?: any); _onAttachedToWindow(): void; onBackPressed(): boolean; _onDetachedFromWindow(): void; @@ -2749,7 +2751,7 @@ export abstract class ViewBase extends Observable { public _automaticallyAdjustsScrollViewInsets: boolean; public _batchUpdate(callback: () => T): T; // Warning: (ae-forgotten-export) The symbol "BindingOptions" needs to be exported by the entry point index.d.ts - // + // // (undocumented) public bind(options: BindingOptions, source?: Object): void; // (undocumented) @@ -2793,7 +2795,7 @@ export abstract class ViewBase extends Observable { // (undocumented) _domId: number; // Warning: (ae-forgotten-export) The symbol "DOMNode" needs to be exported by the entry point index.d.ts - // + // // (undocumented) domNode: DOMNode; public eachChild(callback: (child: ViewBase) => boolean): void; @@ -2859,6 +2861,8 @@ export abstract class ViewBase extends Observable { // (undocumented) _isStyleScopeHost: boolean; // (undocumented) + public _layoutParent(): void; + // (undocumented) left: Length; public static loadedEvent: string; public loadView(view: ViewBase): void; @@ -2952,13 +2956,13 @@ export class WebView extends View { public static loadStartedEvent: string; + on(eventNames: string, callback: (data: EventData) => void, thisArg?: any); + // Warning: (ae-forgotten-export) The symbol "LoadEventData" needs to be exported by the entry point index.d.ts on(event: "loadFinished", callback: (args: LoadEventData) => void, thisArg?: any); on(event: "loadStarted", callback: (args: LoadEventData) => void, thisArg?: any); - on(eventNames: string, callback: (data: EventData) => void, thisArg?: any); - reload(); src: string; @@ -2987,7 +2991,7 @@ export class XmlParser { // Warnings were encountered during analysis: -// +// // nativescript-core/index.d.ts:15:5 - (ae-forgotten-export) The symbol "getMainEntry" needs to be exported by the entry point index.d.ts // nativescript-core/index.d.ts:16:5 - (ae-forgotten-export) The symbol "getRootView" needs to be exported by the entry point index.d.ts // nativescript-core/index.d.ts:17:5 - (ae-forgotten-export) The symbol "setResources" needs to be exported by the entry point index.d.ts @@ -3061,9 +3065,9 @@ export class XmlParser { // nativescript-core/index.d.ts:117:5 - (ae-forgotten-export) The symbol "layout" needs to be exported by the entry point index.d.ts // nativescript-core/index.d.ts:118:5 - (ae-forgotten-export) The symbol "ad" needs to be exported by the entry point index.d.ts // nativescript-core/index.d.ts:119:5 - (ae-forgotten-export) The symbol "ios" needs to be exported by the entry point index.d.ts -// nativescript-core/ui/core/view-base/view-base.d.ts:166:26 - (ae-forgotten-export) The symbol "Property" needs to be exported by the entry point index.d.ts -// nativescript-core/ui/core/view-base/view-base.d.ts:166:26 - (ae-forgotten-export) The symbol "CssProperty" needs to be exported by the entry point index.d.ts -// nativescript-core/ui/core/view-base/view-base.d.ts:166:26 - (ae-forgotten-export) The symbol "CssAnimationProperty" needs to be exported by the entry point index.d.ts +// nativescript-core/ui/core/view-base/view-base.d.ts:171:26 - (ae-forgotten-export) The symbol "Property" needs to be exported by the entry point index.d.ts +// nativescript-core/ui/core/view-base/view-base.d.ts:171:26 - (ae-forgotten-export) The symbol "CssProperty" needs to be exported by the entry point index.d.ts +// nativescript-core/ui/core/view-base/view-base.d.ts:171:26 - (ae-forgotten-export) The symbol "CssAnimationProperty" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/build-docs.sh b/build-docs.sh index def09eb11..dcf6cd329 100755 --- a/build-docs.sh +++ b/build-docs.sh @@ -27,7 +27,7 @@ extract_snippets() { npm install markdown-snippet-injector - for SNIPPET_DIR in {tests/app,apps/app,tns-core-modules} ; do + for SNIPPET_DIR in {tests/app,apps/app,nativescript-core} ; do echo "Extracting snippets from: $SNIPPET_DIR" node "$BIN" --root="$SNIPPET_DIR" --target="$TARGET_DIR" \ --sourceext=".js|.ts|.xml|.html|.css" diff --git a/build/prepare-core.sh b/build/prepare-core.sh index 803f5c0d3..f564093c1 100755 --- a/build/prepare-core.sh +++ b/build/prepare-core.sh @@ -51,7 +51,7 @@ mkdir -p "$DIST" npx rimraf "$DIST/$PACKAGE" npx rimraf "$DIST/$PACKAGE*.tgz" - npm run api-extractor + npm run api-extractor-ci echo "Copying $PACKAGE $DIST/$PACKAGE..." npx ncp "$PACKAGE" "$DIST/$PACKAGE" diff --git a/build/tslint.json b/build/tslint.json index 4180d428a..34f6da9af 100644 --- a/build/tslint.json +++ b/build/tslint.json @@ -1,4 +1,7 @@ { + "rulesDirectory": [ + "@nativescript/tslint-rules" + ], "rules": { "arrow-return-shorthand": true, "class-name": true, @@ -9,6 +12,7 @@ "jsdoc-format": false, "max-line-length": [false, 120], "newline-before-return": true, + "no-android-resources": true, "no-arg": true, "no-bitwise": false, "no-consecutive-blank-lines": true, diff --git a/e2e/animation/app/package.json b/e2e/animation/app/package.json index 0d6fdb754..d4e0596bc 100644 --- a/e2e/animation/app/package.json +++ b/e2e/animation/app/package.json @@ -1,6 +1,7 @@ { "android": { - "v8Flags": "--expose_gc" + "v8Flags": "--expose_gc", + "markingMode": "none" }, "main": "app.js", "name": "tns-template-hello-world-ts", diff --git a/e2e/cuteness.io/app/package.json b/e2e/cuteness.io/app/package.json index ab73df24b..7d2fc03cc 100644 --- a/e2e/cuteness.io/app/package.json +++ b/e2e/cuteness.io/app/package.json @@ -1,6 +1,7 @@ { "main": "app-page.js", "android": { - "v8Flags": "--expose_gc" + "v8Flags": "--expose_gc", + "markingMode": "none" } } diff --git a/e2e/file-qualifiers/app/package.json b/e2e/file-qualifiers/app/package.json index 3414e605f..6970ce225 100644 --- a/e2e/file-qualifiers/app/package.json +++ b/e2e/file-qualifiers/app/package.json @@ -1,6 +1,7 @@ { "android": { - "v8Flags": "--expose_gc" + "v8Flags": "--expose_gc", + "markingMode": "none" }, "main": "app.js" } \ No newline at end of file diff --git a/e2e/modal-navigation/app/package.json b/e2e/modal-navigation/app/package.json index 0d6fdb754..d4e0596bc 100644 --- a/e2e/modal-navigation/app/package.json +++ b/e2e/modal-navigation/app/package.json @@ -1,6 +1,7 @@ { "android": { - "v8Flags": "--expose_gc" + "v8Flags": "--expose_gc", + "markingMode": "none" }, "main": "app.js", "name": "tns-template-hello-world-ts", diff --git a/e2e/ui-tests-app/app/app.ts b/e2e/ui-tests-app/app/app.ts index b2819d214..169e9b56a 100644 --- a/e2e/ui-tests-app/app/app.ts +++ b/e2e/ui-tests-app/app/app.ts @@ -88,9 +88,7 @@ application.on(application.discardedErrorEvent, function (args: application.Disc console.log("### [Discarded] stack: " + args.error.stack); }); -application.setCssFileName("app.css"); -application._start({ moduleName: "main-page" }); +application.run({ moduleName: "app-root" }); // TODO: investigate tab-view -> tabviewcss test crash // TODO: investigate css -> layouts border overlap failure -// application.run({ moduleName: "app-root" }); diff --git a/e2e/ui-tests-app/app/bottom-navigation/bottom-navigation-css-page.css b/e2e/ui-tests-app/app/bottom-navigation/bottom-navigation-css-page.css new file mode 100644 index 000000000..d688ad6e7 --- /dev/null +++ b/e2e/ui-tests-app/app/bottom-navigation/bottom-navigation-css-page.css @@ -0,0 +1 @@ +/* Empty CSS to avoid styles leaking from the page - https://github.com/NativeScript/NativeScript/issues/8143 */ diff --git a/e2e/ui-tests-app/app/button/background-page.css b/e2e/ui-tests-app/app/button/background-page.css new file mode 100644 index 000000000..d688ad6e7 --- /dev/null +++ b/e2e/ui-tests-app/app/button/background-page.css @@ -0,0 +1 @@ +/* Empty CSS to avoid styles leaking from the page - https://github.com/NativeScript/NativeScript/issues/8143 */ diff --git a/e2e/ui-tests-app/app/css/margins-paddings-with-percentage-page.css b/e2e/ui-tests-app/app/css/margins-paddings-with-percentage-page.css new file mode 100644 index 000000000..d67a49649 --- /dev/null +++ b/e2e/ui-tests-app/app/css/margins-paddings-with-percentage-page.css @@ -0,0 +1,21 @@ +.with-percent { background-color: orange; font-size: 8; } +.with-percent GridLayout { margin: 3%; background-color: lightgreen; font-size: 8; } +.with-percent StackLayout { border-color: red; border-width: 1; } +.with-percent StackLayout * { border-color: blue; border-width: 1; } +.with-percent GridLayout { border-color: green; border-width: 1; } +.with-percent .test1 { padding: 10%; } +.with-percent .test2 { padding: 10%; background-color: lightblue; } +.with-percent .test3 { margin: 1% 2% 3% 4%; } +.with-percent WrapLayout { orientation: vertical; width: 75%; height: 45% } +.with-percent Button { color: black } + +.without-percent { background-color: orange; font-size: 8; } +.without-percent GridLayout { margin:3; background-color: lightgreen; font-size: 8; } +.without-percent StackLayout { border-color: red; border-width: 1; } +.without-percent StackLayout * { border-color: blue; border-width: 1; } +.without-percent GridLayout { border-color: green; border-width: 1; } +.without-percent .test1 { padding: 10; } +.without-percent .test2 { padding: 10; background-color: lightblue; } +.without-percent .test3 { margin: 10 20 30 40; } +.without-percent WrapLayout { orientation: vertical; width: 100; height: 120; } +.without-percent Button { color: black } \ No newline at end of file diff --git a/e2e/ui-tests-app/app/css/margins-paddings-with-percentage-page.ts b/e2e/ui-tests-app/app/css/margins-paddings-with-percentage-page.ts index 016ea3269..9ef74f237 100644 --- a/e2e/ui-tests-app/app/css/margins-paddings-with-percentage-page.ts +++ b/e2e/ui-tests-app/app/css/margins-paddings-with-percentage-page.ts @@ -1,53 +1,26 @@ -import * as view from "tns-core-modules/ui/core/view"; -import * as pages from "tns-core-modules/ui/page"; +import { View, getViewById } from "tns-core-modules/ui/core/view"; import { EventData } from "tns-core-modules/data/observable"; -import * as button from "tns-core-modules/ui/button"; +import { Button } from "tns-core-modules/ui/button"; -const cssPercentage = ` - Page { background-color: orange; font-size: 8; } - GridLayout { margin: 3%; background-color: lightgreen; font-size: 8; } - StackLayout { border-color: red; border-width: 1; } - StackLayout * { border-color: blue; border-width: 1; } - GridLayout { border-color: green; border-width: 1; } - .test1 { padding: 10%; } - .test2 { padding: 10%; background-color: lightblue; } - .test3 { margin: 1% 2% 3% 4%; } - WrapLayout { orientation: vertical; width: 75%; height: 45% } - Button { color: black }`; - -const cssWithouPercentage = ` - Page { background-color: orange; font-size: 8; } - GridLayout { margin:3; background-color: lightgreen; font-size: 8; } - StackLayout { border-color: red; border-width: 1; } - StackLayout * { border-color: blue; border-width: 1; } - GridLayout { border-color: green; border-width: 1; } - .test1 { padding: 10; } - .test2 { padding: 10; background-color: lightblue; } - .test3 { margin: 10 20 30 40; } - WrapLayout { orientation: vertical; width: 100; height: 120; } - Button { color: black }`; - -var isSCCWithPercentage = true; +let isSCCWithPercentage = true; export function pageLoaded(args: EventData) { - let page = args.object; - page.css = cssPercentage; - getBtnText(args); + setBtnText(args); } export function applyTap(args: EventData) { - let page = (args.object).page; - let css = isSCCWithPercentage ? cssWithouPercentage : cssPercentage; + let page = (args.object).page; + let cssClass = isSCCWithPercentage ? "without-percent" : "with-percent"; isSCCWithPercentage = !isSCCWithPercentage; - console.log(css); - page.css = css; - getBtnText(args); + console.log(cssClass); + page.className = cssClass; + setBtnText(args); } -function getBtnText(args: EventData) { - var parent = (args.object).parent; +function setBtnText(args: EventData) { + let parent = (args.object).parent; if (parent) { - var btn = view.getViewById(parent, "button"); + var btn = diff --git a/e2e/ui-tests-app/app/flexbox/flexbox-css-page.css b/e2e/ui-tests-app/app/flexbox/flexbox-css-page.css new file mode 100644 index 000000000..d688ad6e7 --- /dev/null +++ b/e2e/ui-tests-app/app/flexbox/flexbox-css-page.css @@ -0,0 +1 @@ +/* Empty CSS to avoid styles leaking from the page - https://github.com/NativeScript/NativeScript/issues/8143 */ diff --git a/e2e/ui-tests-app/app/page/status-bar-css-page.css b/e2e/ui-tests-app/app/page/status-bar-css-page.css new file mode 100644 index 000000000..d688ad6e7 --- /dev/null +++ b/e2e/ui-tests-app/app/page/status-bar-css-page.css @@ -0,0 +1 @@ +/* Empty CSS to avoid styles leaking from the page - https://github.com/NativeScript/NativeScript/issues/8143 */ diff --git a/e2e/ui-tests-app/app/tab-view/tab-view-css-page.css b/e2e/ui-tests-app/app/tab-view/tab-view-css-page.css new file mode 100644 index 000000000..6e1469711 --- /dev/null +++ b/e2e/ui-tests-app/app/tab-view/tab-view-css-page.css @@ -0,0 +1,2 @@ +/* Currently need an empty css file to make this module a style scope host. +This tests sets css on the module and without this empty file the css is set on the frame on top of it */ \ No newline at end of file diff --git a/e2e/ui-tests-app/app/tab-view/text-transform-page.css b/e2e/ui-tests-app/app/tab-view/text-transform-page.css new file mode 100644 index 000000000..d688ad6e7 --- /dev/null +++ b/e2e/ui-tests-app/app/tab-view/text-transform-page.css @@ -0,0 +1 @@ +/* Empty CSS to avoid styles leaking from the page - https://github.com/NativeScript/NativeScript/issues/8143 */ diff --git a/nativescript-core/animation-frame/animation-frame.d.ts b/nativescript-core/animation-frame/animation-frame.d.ts new file mode 100644 index 000000000..c899d0901 --- /dev/null +++ b/nativescript-core/animation-frame/animation-frame.d.ts @@ -0,0 +1,24 @@ +/** + * Allows you to create, cancel and react to listeners to animation frames. + * @module "animation-frame" + */ /** */ + +/** + * Callback called on frame rendered + * @argument time Time of the current frame in milliseconds + */ +export interface FrameRequestCallback { + (time: number): void; +} + +/** + * Requests an animation frame and returns the timer ID + * @param cb Callback to be called on frame + */ +export function requestAnimationFrame(cb: FrameRequestCallback): number + +/** + * Cancels a previously scheduled animation frame request + * @param id timer ID to cancel + */ +export function cancelAnimationFrame(id: number): void; diff --git a/nativescript-core/animation-frame/animation-frame.ts b/nativescript-core/animation-frame/animation-frame.ts new file mode 100644 index 000000000..6a23bc771 --- /dev/null +++ b/nativescript-core/animation-frame/animation-frame.ts @@ -0,0 +1,62 @@ +import { FPSCallback } from "../fps-meter/fps-native"; +import { getTimeInFrameBase } from "./animation-native"; + +export interface FrameRequestCallback { + (time: number): void; +} + +let animationId = 0; +let nextFrameAnimationCallbacks: { [key: string]: FrameRequestCallback } = {}; +let shouldStop = true; +let inAnimationFrame = false; +let fpsCallback: FPSCallback; +let lastFrameTime = 0; + +function getNewId() { + return animationId++; +} + +function ensureNative() { + if (fpsCallback) { + return; + } + fpsCallback = new FPSCallback(doFrame); +} + +function doFrame(currentTimeMillis: number) { + lastFrameTime = currentTimeMillis; + shouldStop = true; + const thisFrameCbs = nextFrameAnimationCallbacks; + nextFrameAnimationCallbacks = {}; + inAnimationFrame = true; + for (const animationId in thisFrameCbs) { + if (thisFrameCbs[animationId]) { + thisFrameCbs[animationId](lastFrameTime); + } + } + inAnimationFrame = false; + if (shouldStop) { + fpsCallback.stop(); // TODO: check performance without stopping to allow consistent frame times + } +} + +export function requestAnimationFrame(cb: FrameRequestCallback): number { + if (!inAnimationFrame) { + inAnimationFrame = true; + zonedCallback(cb)(getTimeInFrameBase()); // TODO: store and use lastFrameTime + inAnimationFrame = false; + + return getNewId(); + } + ensureNative(); + const animId = getNewId(); + nextFrameAnimationCallbacks[animId] = zonedCallback(cb) as FrameRequestCallback; + shouldStop = false; + fpsCallback.start(); + + return animId; +} + +export function cancelAnimationFrame(id: number) { + delete nextFrameAnimationCallbacks[id]; +} diff --git a/nativescript-core/animation-frame/animation-native.android.ts b/nativescript-core/animation-frame/animation-native.android.ts new file mode 100644 index 000000000..1dee2bbd0 --- /dev/null +++ b/nativescript-core/animation-frame/animation-native.android.ts @@ -0,0 +1,4 @@ + +export function getTimeInFrameBase(): number { + return java.lang.System.nanoTime() / 1000000; +} diff --git a/nativescript-core/animation-frame/animation-native.d.ts b/nativescript-core/animation-frame/animation-native.d.ts new file mode 100644 index 000000000..884449e1d --- /dev/null +++ b/nativescript-core/animation-frame/animation-native.d.ts @@ -0,0 +1,4 @@ +/** + * Gets the time in millisseconds in the same base as frames + */ +export function getTimeInFrameBase(): number; \ No newline at end of file diff --git a/nativescript-core/animation-frame/animation-native.ios.ts b/nativescript-core/animation-frame/animation-native.ios.ts new file mode 100644 index 000000000..45cac4da7 --- /dev/null +++ b/nativescript-core/animation-frame/animation-native.ios.ts @@ -0,0 +1,3 @@ +import { time } from "../profiling"; + +export const getTimeInFrameBase = time; diff --git a/nativescript-core/animation-frame/package.json b/nativescript-core/animation-frame/package.json new file mode 100644 index 000000000..a50f09ab4 --- /dev/null +++ b/nativescript-core/animation-frame/package.json @@ -0,0 +1,6 @@ +{ + "name": "animation-frame", + "main": "animation-frame", + "types": "animation-frame.d.ts", + "nativescript": {} +} diff --git a/nativescript-core/application/application-common.ts b/nativescript-core/application/application-common.ts index 10de66855..287aa6161 100644 --- a/nativescript-core/application/application-common.ts +++ b/nativescript-core/application/application-common.ts @@ -1,13 +1,29 @@ // Require globals first so that snapshot takes __extends function. import "../globals"; -import { Observable, EventData } from "../data/observable"; + +// Types +import { AndroidApplication, iOSApplication } from "."; +import { + CssChangedEventData, DiscardedErrorEventData, + LoadAppCSSEventData, UnhandledErrorEventData +} from "./application-interfaces"; +import { EventData } from "../data/observable/observable-interfaces"; import { View } from "../ui/core/view"; + +// Requires +import { Observable } from "../data/observable"; import { trace as profilingTrace, time, uptime, level as profilingLevel, } from "../profiling"; +import * as bindableResources from "../ui/core/bindable/bindable-resources"; +import { CLASS_PREFIX, pushToSystemCssClasses, removeSystemCssClass } from "../css/system-classes"; +import { DeviceOrientation, SystemAppearance } from "../ui/enums/enums"; + +export { Observable }; +export * from "./application-interfaces"; const events = new Observable(); let launched = false; @@ -30,22 +46,6 @@ export function hasLaunched(): boolean { return launched; } -export { Observable }; - -import { - AndroidApplication, - CssChangedEventData, - DiscardedErrorEventData, - iOSApplication, - LoadAppCSSEventData, - UnhandledErrorEventData -} from "./application"; - -import { CLASS_PREFIX, pushToRootViewCssClasses, removeFromRootViewCssClasses } from "../css/system-classes"; -import { DeviceOrientation, SystemAppearance } from "../ui/enums/enums"; - -export { UnhandledErrorEventData, DiscardedErrorEventData, CssChangedEventData, LoadAppCSSEventData }; - export const launchEvent = "launch"; export const suspendEvent = "suspend"; export const displayedEvent = "displayed"; @@ -70,18 +70,16 @@ const SYSTEM_APPEARANCE_CSS_CLASSES = [ let cssFile: string = "./app.css"; -let resources: any = {}; - export function getResources() { - return resources; + return bindableResources.get(); } export function setResources(res: any) { - resources = res; + bindableResources.set(res); } -export let android = undefined; -export let ios = undefined; +export let android: AndroidApplication = undefined; +export let ios: iOSApplication = undefined; export const on: typeof events.on = events.on.bind(events); export const off: typeof events.off = events.off.bind(events); @@ -127,18 +125,17 @@ export function loadAppCss(): void { try { events.notify({ eventName: "loadAppCss", object: app, cssFile: getCssFileName() }); } catch (e) { - throw new Error(`The file ${getCssFileName()} couldn't be loaded! ` + - `You may need to register it inside ./app/vendor.ts.`); + throw new Error(`The app CSS file ${getCssFileName()} couldn't be loaded!`); } } -function applyCssClass(rootView: View, cssClass: string) { - pushToRootViewCssClasses(cssClass); +function addCssClass(rootView: View, cssClass: string) { + pushToSystemCssClasses(cssClass); rootView.cssClasses.add(cssClass); } function removeCssClass(rootView: View, cssClass: string) { - removeFromRootViewCssClasses(cssClass); + removeSystemCssClass(cssClass); rootView.cssClasses.delete(cssClass); } @@ -150,18 +147,27 @@ function increaseStyleScopeApplicationCssSelectorVersion(rootView: View) { } } +function applyCssClass(rootView: View, cssClasses: string[], newCssClass: string) { + if (!rootView.cssClasses.has(newCssClass)) { + cssClasses.forEach(cssClass => removeCssClass(rootView, cssClass)); + addCssClass(rootView, newCssClass); + increaseStyleScopeApplicationCssSelectorVersion(rootView); + rootView._onCssStateChange(); + } +} + export function orientationChanged(rootView: View, newOrientation: "portrait" | "landscape" | "unknown"): void { if (!rootView) { return; } const newOrientationCssClass = `${CLASS_PREFIX}${newOrientation}`; - if (!rootView.cssClasses.has(newOrientationCssClass)) { - ORIENTATION_CSS_CLASSES.forEach(cssClass => removeCssClass(rootView, cssClass)); - applyCssClass(rootView, newOrientationCssClass); - increaseStyleScopeApplicationCssSelectorVersion(rootView); - rootView._onCssStateChange(); - } + applyCssClass(rootView, ORIENTATION_CSS_CLASSES, newOrientationCssClass); + + const rootModalViews = >rootView._getRootModalViews(); + rootModalViews.forEach(rootModalView => { + applyCssClass(rootModalView, ORIENTATION_CSS_CLASSES, newOrientationCssClass); + }); } export function systemAppearanceChanged(rootView: View, newSystemAppearance: "dark" | "light"): void { @@ -170,12 +176,12 @@ export function systemAppearanceChanged(rootView: View, newSystemAppearance: "da } const newSystemAppearanceCssClass = `${CLASS_PREFIX}${newSystemAppearance}`; - if (!rootView.cssClasses.has(newSystemAppearanceCssClass)) { - SYSTEM_APPEARANCE_CSS_CLASSES.forEach(cssClass => removeCssClass(rootView, cssClass)); - applyCssClass(rootView, newSystemAppearanceCssClass); - increaseStyleScopeApplicationCssSelectorVersion(rootView); - rootView._onCssStateChange(); - } + applyCssClass(rootView, SYSTEM_APPEARANCE_CSS_CLASSES, newSystemAppearanceCssClass); + + const rootModalViews = >rootView._getRootModalViews(); + rootModalViews.forEach(rootModalView => { + applyCssClass(rootModalView, SYSTEM_APPEARANCE_CSS_CLASSES, newSystemAppearanceCssClass); + }); } global.__onUncaughtError = function (error: NativeScriptError) { diff --git a/nativescript-core/application/application-interfaces.ts b/nativescript-core/application/application-interfaces.ts new file mode 100644 index 000000000..eb35c77ba --- /dev/null +++ b/nativescript-core/application/application-interfaces.ts @@ -0,0 +1,79 @@ +// Types +import { EventData } from "../data/observable/observable-interfaces"; +import { View } from "../ui/core/view"; + +export interface ApplicationEventData extends EventData { + ios?: any; + android?: any; + eventName: string; + object: any; +} + +export interface LaunchEventData extends ApplicationEventData { + root?: View; + savedInstanceState?: any /* android.os.Bundle */; +} + +export interface OrientationChangedEventData extends ApplicationEventData { + newValue: "portrait" | "landscape" | "unknown"; +} + +export interface SystemAppearanceChangedEventData extends ApplicationEventData { + newValue: "light" | "dark"; +} + +export interface UnhandledErrorEventData extends ApplicationEventData { + ios?: NativeScriptError; + android?: NativeScriptError; + error: NativeScriptError; +} + +export interface DiscardedErrorEventData extends ApplicationEventData { + error: NativeScriptError; +} + +export interface CssChangedEventData extends EventData { + cssFile?: string; + cssText?: string; +} + +export interface AndroidActivityEventData { + activity: any /* androidx.appcompat.app.AppCompatActivity */; + eventName: string; + object: any; +} + +export interface AndroidActivityBundleEventData extends AndroidActivityEventData { + bundle: any /* android.os.Bundle */; +} + +export interface AndroidActivityRequestPermissionsEventData extends AndroidActivityEventData { + requestCode: number; + permissions: Array; + grantResults: Array; +} + +export interface AndroidActivityResultEventData extends AndroidActivityEventData { + requestCode: number; + resultCode: number; + intent: any /* android.content.Intent */; +} + +export interface AndroidActivityNewIntentEventData extends AndroidActivityEventData { + intent: any /* android.content.Intent */; +} + +export interface AndroidActivityBackPressedEventData extends AndroidActivityEventData { + cancel: boolean; +} + +/** +* @deprecated +*/ +export interface RootViewControllerImpl { + contentController: any; +} + +export interface LoadAppCSSEventData extends EventData { + cssFile: string; +} \ No newline at end of file diff --git a/nativescript-core/application/application.android.ts b/nativescript-core/application/application.android.ts index b14ac1632..bf45c01c1 100644 --- a/nativescript-core/application/application.android.ts +++ b/nativescript-core/application/application.android.ts @@ -1,4 +1,5 @@ -// Definitions. +// Types. +import { AndroidApplication as AndroidApplicationDefinition } from "."; import { AndroidActivityBackPressedEventData, AndroidActivityBundleEventData, @@ -6,26 +7,24 @@ import { AndroidActivityNewIntentEventData, AndroidActivityRequestPermissionsEventData, AndroidActivityResultEventData, - AndroidApplication as AndroidApplicationDefinition, ApplicationEventData, CssChangedEventData, OrientationChangedEventData, SystemAppearanceChangedEventData -} from "."; +} from "./application-interfaces"; +import { View } from "../ui/core/view"; +import { NavigationEntry, AndroidActivityCallbacks } from "../ui/frame/frame-interfaces"; +// Requires import { - displayedEvent, hasListeners, livesync, lowMemoryEvent, notify, Observable, on, + displayedEvent, hasListeners, livesync, lowMemoryEvent, notify, Observable, orientationChanged, orientationChangedEvent, setApplication, suspendEvent, systemAppearanceChanged, systemAppearanceChangedEvent } from "./application-common"; - -import { profile } from "../profiling"; - // First reexport so that app module is initialized. export * from "./application-common"; -// Types. -import { NavigationEntry, View, AndroidActivityCallbacks } from "../ui/frame"; +import { profile } from "../profiling"; const ActivityCreated = "activityCreated"; const ActivityDestroyed = "activityDestroyed"; @@ -149,6 +148,10 @@ export class AndroidApplication extends Observable implements AndroidApplication } } +// HACK: Use an interface with the same name, so that the class above fulfills the 'implements' requirement +// HACK: We use the 'implements' to verify the class above is the same as the one declared in the d.ts +// HACK: We declare all these 'on' statements, so that they can appear in the API reference +// HACK: Do we need this? Is it useful? There are static fields to the AndroidApplication class for the event names. export interface AndroidApplication { on(eventNames: string, callback: (data: AndroidActivityEventData) => void, thisArg?: any); on(event: "activityCreated", callback: (args: AndroidActivityBundleEventData) => void, thisArg?: any); @@ -171,9 +174,8 @@ setApplication(androidApp); let mainEntry: NavigationEntry; let started = false; -const createRootFrame = { value: true }; -export function _start(entry?: NavigationEntry | string) { +export function run(entry?: NavigationEntry | string) { if (started) { throw new Error("Application is already started."); } @@ -186,15 +188,6 @@ export function _start(entry?: NavigationEntry | string) { } } -export function _shouldCreateRootFrame(): boolean { - return createRootFrame.value; -} - -export function run(entry?: NavigationEntry | string) { - createRootFrame.value = false; - _start(entry); -} - export function addCss(cssText: string, attributeScoped?: boolean): void { notify({ eventName: "cssChanged", object: androidApp, cssText: cssText }); if (!attributeScoped) { @@ -213,7 +206,6 @@ export function _resetRootView(entry?: NavigationEntry | string) { throw new Error("Cannot find android activity."); } - createRootFrame.value = false; mainEntry = typeof entry === "string" ? { moduleName: entry } : entry; const callbacks: AndroidActivityCallbacks = activity[CALLBACKS]; if (!callbacks) { diff --git a/nativescript-core/application/application.d.ts b/nativescript-core/application/application.d.ts index ddab72616..6649b8d92 100644 --- a/nativescript-core/application/application.d.ts +++ b/nativescript-core/application/application.d.ts @@ -211,15 +211,6 @@ export function run(entry?: NavigationEntry | string); */ export function _resetRootView(entry?: NavigationEntry | string); -/** - * @private - */ -export function _shouldCreateRootFrame(): boolean; -/** - * @private - */ -export function _start(entry?: NavigationEntry | string); - /** * A basic method signature to hook an event listener (shortcut alias to the addEventListener method). * @param eventNames - String corresponding to events (e.g. "onLaunch"). Optionally could be used more events separated by `,` (e.g. "onLaunch", "onSuspend"). @@ -610,7 +601,7 @@ export class AndroidApplication extends Observable { /** * The abstraction of an iOS-specific application object. */ -export interface iOSApplication { +export class iOSApplication { /* tslint:enable */ /** * The root view controller for the application. diff --git a/nativescript-core/application/application.ios.ts b/nativescript-core/application/application.ios.ts index 5a4191d56..f156cda3d 100644 --- a/nativescript-core/application/application.ios.ts +++ b/nativescript-core/application/application.ios.ts @@ -1,19 +1,22 @@ +// Types +import { iOSApplication as iOSApplicationDefinition } from "."; import { ApplicationEventData, CssChangedEventData, - iOSApplication as IOSApplicationDefinition, LaunchEventData, LoadAppCSSEventData, OrientationChangedEventData, SystemAppearanceChangedEventData -} from "."; +} from "./application-interfaces"; +import { View } from "../ui/core/view"; +import { NavigationEntry } from "../ui/frame/frame-interfaces"; +// Require import { displayedEvent, exitEvent, getCssFileName, launchEvent, livesync, lowMemoryEvent, notify, on, orientationChanged, orientationChangedEvent, resumeEvent, setApplication, suspendEvent, systemAppearanceChanged, systemAppearanceChangedEvent } from "./application-common"; - // First reexport so that app module is initialized. export * from "./application-common"; @@ -21,12 +24,11 @@ export * from "./application-common"; import { Builder } from "../ui/builder"; import { CLASS_PREFIX, - getRootViewCssClasses, - pushToRootViewCssClasses + getSystemCssClasses, + pushToSystemCssClasses, + ROOT_VIEW_CSS_CLASS } from "../css/system-classes"; - -import { ios as iosView, View } from "../ui/core/view"; -import { Frame, NavigationEntry } from "../ui/frame"; +import { ios as iosViewHelper } from "../ui/core/view/view-helper"; import { device } from "../platform/platform"; import { profile } from "../profiling"; import { ios } from "../utils/utils"; @@ -88,7 +90,9 @@ class CADisplayLinkTarget extends NSObject { }; } -class IOSApplication implements IOSApplicationDefinition { +/* tslint:disable */ +export class iOSApplication implements iOSApplicationDefinition { + /* tslint:enable */ private _backgroundColor = (majorVersion <= 12 || !UIColor.systemBackgroundColor) ? UIColor.whiteColor : UIColor.systemBackgroundColor; private _delegate: typeof UIApplicationDelegate; private _window: UIWindow; @@ -292,26 +296,21 @@ class IOSApplication implements IOSApplicationDefinition { this._rootView = rootView; - if (createRootFrame.value) { - // Don't setup as styleScopeHost - rootView._setupUI({}); - } else { - // setup view as styleScopeHost - rootView._setupAsRootView({}); - } + // setup view as styleScopeHost + rootView._setupAsRootView({}); setViewControllerView(rootView); const haveController = this._window.rootViewController !== null; this._window.rootViewController = controller; - setRootViewSystemAppearanceCssClass(rootView); + setRootViewsSystemAppearanceCssClass(rootView); if (!haveController) { this._window.makeKeyAndVisible(); } - rootView.on(iosView.traitCollectionColorAppearanceChangedEvent, () => { + rootView.on(iosViewHelper.traitCollectionColorAppearanceChangedEvent, () => { const userInterfaceStyle = controller.traitCollection.userInterfaceStyle; const newSystemAppearance = getSystemAppearanceValue(userInterfaceStyle); @@ -330,8 +329,9 @@ class IOSApplication implements IOSApplicationDefinition { } } -const iosApp = new IOSApplication(); - +/* tslint:disable */ +const iosApp = new iOSApplication(); +/* tslint:enable */ export { iosApp as ios }; setApplication(iosApp); @@ -349,16 +349,11 @@ function createRootView(v?: View) { if (!mainEntry) { throw new Error("Main entry is missing. App cannot be started. Verify app bootstrap."); } else { - if (createRootFrame.value) { - const frame = rootView = new Frame(); - frame.navigate(mainEntry); - } else { - rootView = Builder.createViewFromEntry(mainEntry); - } + rootView = Builder.createViewFromEntry(mainEntry); } } - setRootViewCssClasses(rootView); + setRootViewsCssClasses(rootView); return rootView; } @@ -371,10 +366,8 @@ export function getRootView() { return iosApp.rootView; } -// NOTE: for backwards compatibility. Remove for 4.0.0. -const createRootFrame = { value: true }; let started: boolean = false; -export function _start(entry?: string | NavigationEntry) { +export function run(entry?: string | NavigationEntry) { mainEntry = typeof entry === "string" ? { moduleName: entry } : entry; started = true; @@ -402,8 +395,8 @@ export function _start(entry?: string | NavigationEntry) { // Mind root view CSS classes in future work // on embedding NativeScript applications - setRootViewSystemAppearanceCssClass(rootView); - rootView.on(iosView.traitCollectionColorAppearanceChangedEvent, () => { + setRootViewsSystemAppearanceCssClass(rootView); + rootView.on(iosViewHelper.traitCollectionColorAppearanceChangedEvent, () => { const userInterfaceStyle = controller.traitCollection.userInterfaceStyle; const newSystemAppearance = getSystemAppearanceValue(userInterfaceStyle); @@ -425,11 +418,6 @@ export function _start(entry?: string | NavigationEntry) { } } -export function run(entry?: string | NavigationEntry) { - createRootFrame.value = false; - _start(entry); -} - export function addCss(cssText: string, attributeScoped?: boolean): void { notify({ eventName: "cssChanged", object: iosApp, cssText: cssText }); if (!attributeScoped) { @@ -441,7 +429,6 @@ export function addCss(cssText: string, attributeScoped?: boolean): void { } export function _resetRootView(entry?: NavigationEntry | string) { - createRootFrame.value = false; mainEntry = typeof entry === "string" ? { moduleName: entry } : entry; iosApp.setWindowContent(); } @@ -466,7 +453,7 @@ function getViewController(rootView: View): UIViewController { if (!(viewController instanceof UIViewController)) { // We set UILayoutViewController dynamically to the root view if it doesn't have a view controller // At the moment the root view doesn't have its native view created. We set it in the setViewControllerView func - viewController = iosView.UILayoutViewController.initWithOwner(new WeakRef(rootView)) as UIViewController; + viewController = iosViewHelper.UILayoutViewController.initWithOwner(new WeakRef(rootView)) as UIViewController; rootView.viewController = viewController; } @@ -481,25 +468,27 @@ function setViewControllerView(view: View): void { throw new Error("Root should be either UIViewController or UIView"); } - if (viewController instanceof iosView.UILayoutViewController) { + if (viewController instanceof iosViewHelper.UILayoutViewController) { viewController.view.addSubview(nativeView); } } -function setRootViewCssClasses(rootView: View): void { +function setRootViewsCssClasses(rootView: View): void { const deviceType = device.deviceType.toLowerCase(); - pushToRootViewCssClasses(`${CLASS_PREFIX}${IOS_PLATFORM}`); - pushToRootViewCssClasses(`${CLASS_PREFIX}${deviceType}`); - pushToRootViewCssClasses(`${CLASS_PREFIX}${iosApp.orientation}`); - const rootViewCssClasses = getRootViewCssClasses(); + pushToSystemCssClasses(`${CLASS_PREFIX}${IOS_PLATFORM}`); + pushToSystemCssClasses(`${CLASS_PREFIX}${deviceType}`); + pushToSystemCssClasses(`${CLASS_PREFIX}${iosApp.orientation}`); + + rootView.cssClasses.add(ROOT_VIEW_CSS_CLASS); + const rootViewCssClasses = getSystemCssClasses(); rootViewCssClasses.forEach(c => rootView.cssClasses.add(c)); } -function setRootViewSystemAppearanceCssClass(rootView: View): void { +function setRootViewsSystemAppearanceCssClass(rootView: View): void { if (majorVersion >= 13) { const systemAppearanceCssClass = `${CLASS_PREFIX}${iosApp.systemAppearance}`; - pushToRootViewCssClasses(systemAppearanceCssClass); + pushToSystemCssClasses(systemAppearanceCssClass); rootView.cssClasses.add(systemAppearanceCssClass); } } diff --git a/nativescript-core/css/system-classes.d.ts b/nativescript-core/css/system-classes.d.ts index cbef8e46d..43edcfee9 100644 --- a/nativescript-core/css/system-classes.d.ts +++ b/nativescript-core/css/system-classes.d.ts @@ -1,30 +1,66 @@ /** * @module "system-classes" + * This is an internal module. */ /** */ /** - * String value "ns-" used for CSS system class prefix. - */ +* String value "ns-" used for CSS system class prefix. +*/ export const CLASS_PREFIX: string; /** * Gets CSS system class for modal root view. */ -export function getModalRootViewCssClass(): string; +export const MODAL_ROOT_VIEW_CSS_CLASS; /** * Gets CSS system classes for root view. */ +export const ROOT_VIEW_CSS_CLASS; + +/** + * Gets a list of the current system classes. + * Intended for internal use only + */ +export function getSystemCssClasses(): string[]; + +/** + * Pushes to the list of the current system classes. + * Intended for internal use only + */ +export function pushToSystemCssClasses(value: string): number; + +/** + * Removes value from the list of current system classes + * Intended for internal use only + * @param value + */ +export function removeSystemCssClass(value: string): string; + +/** + * Same as MODAL_ROOT_VIEW_CSS_CLASS + */ +export function getModalRootViewCssClass(): string; + +/** + * Gets CSS system classes for root view. Same as ROOT_VIEW_CSS_CLASS + _getCssClasses + * Intended for internal use only + * @deprecated Use ROOT_VIEW_CSS_CLASS or getCssClasses() instead + */ export function getRootViewCssClasses(): string[]; /** * Appends new CSS class to the system classes and returns the new length of the array. + * Intended for internal use only + * @deprecated Use pushToCssClasses() instead * @param value New CSS system class. */ export function pushToRootViewCssClasses(value: string): number; /** * Removes CSS class from the system classes and returns it. + * Intended for internal use only + * @deprecated Use removeCssClass() instead * @param value */ -export function removeFromRootViewCssClasses(value: string): string; +export function removeFromRootViewCssClasses(value: string): string; \ No newline at end of file diff --git a/nativescript-core/css/system-classes.ts b/nativescript-core/css/system-classes.ts index 6e2f7e80c..a51ca0a85 100644 --- a/nativescript-core/css/system-classes.ts +++ b/nativescript-core/css/system-classes.ts @@ -1,32 +1,44 @@ const MODAL = "modal"; const ROOT = "root"; +const cssClasses = []; export const CLASS_PREFIX = "ns-"; +export const MODAL_ROOT_VIEW_CSS_CLASS = `${CLASS_PREFIX}${MODAL}`; +export const ROOT_VIEW_CSS_CLASS = `${CLASS_PREFIX}${ROOT}`; -const modalRootViewCssClass = `${CLASS_PREFIX}${MODAL}`; -const rootViewCssClasses = [`${CLASS_PREFIX}${ROOT}`]; - -export function getModalRootViewCssClass(): string { - return modalRootViewCssClass; +export function getSystemCssClasses(): string[] { + return cssClasses; } -export function getRootViewCssClasses(): string[] { - return rootViewCssClasses; +export function pushToSystemCssClasses(value: string): number { + cssClasses.push(value); + + return cssClasses.length; } -export function pushToRootViewCssClasses(value: string): number { - rootViewCssClasses.push(value); - - return rootViewCssClasses.length; -} - -export function removeFromRootViewCssClasses(value: string): string { - const index = rootViewCssClasses.indexOf(value); +export function removeSystemCssClass(value: string): string { + const index = cssClasses.indexOf(value); let removedElement; if (index > -1) { - removedElement = rootViewCssClasses.splice(index, 1); + removedElement = cssClasses.splice(index, 1); } return removedElement; } + +export function getModalRootViewCssClass(): string { + return MODAL_ROOT_VIEW_CSS_CLASS; +} + +export function getRootViewCssClasses(): string[] { + return [ROOT_VIEW_CSS_CLASS, ...cssClasses]; +} + +export function pushToRootViewCssClasses(value: string): number { + return pushToSystemCssClasses(value) + 1; // because of ROOT_VIEW_CSS_CLASS +} + +export function removeFromRootViewCssClasses(value: string): string { + return removeSystemCssClass(value); +} \ No newline at end of file diff --git a/nativescript-core/data/observable/observable-interfaces.ts b/nativescript-core/data/observable/observable-interfaces.ts new file mode 100644 index 000000000..4e2ac5559 --- /dev/null +++ b/nativescript-core/data/observable/observable-interfaces.ts @@ -0,0 +1,13 @@ +// Types +import { Observable } from "."; + +export interface EventData { + eventName: string; + object: Observable; +} + +export interface PropertyChangeData extends EventData { + propertyName: string; + value: any; + oldValue?: any; +} \ No newline at end of file diff --git a/nativescript-core/data/observable/observable.ts b/nativescript-core/data/observable/observable.ts index adb8ab39c..18f4699b9 100644 --- a/nativescript-core/data/observable/observable.ts +++ b/nativescript-core/data/observable/observable.ts @@ -1,10 +1,7 @@ -import { Observable as ObservableDefinition, WrappedValue as WrappedValueDefinition, PropertyChangeData } from "."; +import { Observable as ObservableDefinition, WrappedValue as WrappedValueDefinition } from "."; +import { EventData, PropertyChangeData } from "./observable-interfaces"; -// TODO: Remove this. It is the same export as in d.ts to fix failing build when modules are linked -export interface EventData { - eventName: string; - object: ObservableDefinition; -} +export * from "./observable-interfaces"; interface ListenerEntry { callback: (data: EventData) => void; diff --git a/nativescript-core/debugger/devtools-elements-interfaces.ts b/nativescript-core/debugger/devtools-elements-interfaces.ts new file mode 100644 index 000000000..c7bf5eee2 --- /dev/null +++ b/nativescript-core/debugger/devtools-elements-interfaces.ts @@ -0,0 +1,18 @@ +//Types +import { DOMNode } from "./dom-node"; + +export interface InspectorCommands { + // DevTools -> Application communication. Methods that devtools calls when needed. + getDocument(): string | DOMNode; + removeNode(nodeId: number): void; + getComputedStylesForNode(nodeId: number): string | Array<{ name: string, value: string }>; + setAttributeAsText(nodeId: number, text: string, name: string): void; +} + +export interface InspectorEvents { + // Application -> DevTools communication. Methods that the app should call when needed. + childNodeInserted(parentId: number, lastId: number, node: DOMNode): void; + childNodeRemoved(parentId: number, nodeId: number): void; + attributeModified(nodeId: number, attrName: string, attrValue: string): void; + attributeRemoved(nodeId: number, attrName: string): void; +} \ No newline at end of file diff --git a/nativescript-core/debugger/devtools-elements.android.ts b/nativescript-core/debugger/devtools-elements.android.ts index be0753b3f..96889f6ad 100644 --- a/nativescript-core/debugger/devtools-elements.android.ts +++ b/nativescript-core/debugger/devtools-elements.android.ts @@ -1,7 +1,12 @@ -import { InspectorEvents, InspectorCommands } from "./devtools-elements"; +// Types +import { InspectorEvents, InspectorCommands } from "./devtools-elements-interfaces"; + +// Requires import { getDocument, getComputedStylesForNode, removeNode, setAttributeAsText } from "./devtools-elements.common"; import { registerInspectorEvents, DOMNode } from "./dom-node"; +export * from "./devtools-elements-interfaces"; + export function attachDOMInspectorEventCallbacks(DOMDomainFrontend: InspectorEvents) { registerInspectorEvents(DOMDomainFrontend); diff --git a/nativescript-core/debugger/devtools-elements.common.ts b/nativescript-core/debugger/devtools-elements.common.ts index 71473ef5a..3fa3bbf40 100644 --- a/nativescript-core/debugger/devtools-elements.common.ts +++ b/nativescript-core/debugger/devtools-elements.common.ts @@ -1,7 +1,8 @@ -import { getNodeById } from "./dom-node"; - -// Needed for typings only +// Types import { ViewBase } from "../ui/core/view-base"; + +//Requires +import { getNodeById } from "./dom-node"; import { mainThreadify } from "../utils/utils"; // Use lazy requires for core modules diff --git a/nativescript-core/debugger/devtools-elements.ios.ts b/nativescript-core/debugger/devtools-elements.ios.ts index 2d7046639..43e9dec19 100644 --- a/nativescript-core/debugger/devtools-elements.ios.ts +++ b/nativescript-core/debugger/devtools-elements.ios.ts @@ -1,7 +1,12 @@ -import { InspectorEvents, InspectorCommands } from "./devtools-elements"; +// Types +import { InspectorEvents, InspectorCommands } from "./devtools-elements-interfaces"; + +// Requires import { getDocument, getComputedStylesForNode, removeNode, setAttributeAsText } from "./devtools-elements.common"; import { registerInspectorEvents, DOMNode } from "./dom-node"; +export * from "./devtools-elements-interfaces"; + export function attachDOMInspectorEventCallbacks(DOMDomainFrontend: InspectorEvents) { registerInspectorEvents(DOMDomainFrontend); diff --git a/nativescript-core/debugger/dom-node.ts b/nativescript-core/debugger/dom-node.ts index f8ca58288..9966ac0ed 100644 --- a/nativescript-core/debugger/dom-node.ts +++ b/nativescript-core/debugger/dom-node.ts @@ -1,5 +1,5 @@ import { CSSComputedStyleProperty } from "./css-agent"; -import { InspectorEvents } from "./devtools-elements"; +import { InspectorEvents } from "./devtools-elements-interfaces"; // Needed for typings only import { ViewBase } from "../ui/core/view"; diff --git a/nativescript-core/debugger/webinspector-network.ios.ts b/nativescript-core/debugger/webinspector-network.ios.ts index 6832f6acd..ddf2c6b97 100644 --- a/nativescript-core/debugger/webinspector-network.ios.ts +++ b/nativescript-core/debugger/webinspector-network.ios.ts @@ -247,14 +247,3 @@ export class NetworkDomainDebugger implements inspectorCommandTypes.NetworkDomai return resourceData; } } - -@inspectorCommands.DomainDispatcher("Runtime") -export class RuntimeDomainDebugger { - constructor() { - __inspectorSendEvent(`{"method":"Runtime.executionContextCreated","params":{"context":{"id":1,"origin":"http://main.xml","name":"","auxData":{"isDefault":true,"frameId":"${frameId}"}}}}`); - } - - compileScript(): { scriptId?: string, exceptionDetails?: Object } { - return {}; - } -} diff --git a/nativescript-core/file-system/file-name-resolver/file-name-resolver.ts b/nativescript-core/file-system/file-name-resolver/file-name-resolver.ts index 608e275d0..6dae02d0c 100644 --- a/nativescript-core/file-system/file-name-resolver/file-name-resolver.ts +++ b/nativescript-core/file-system/file-name-resolver/file-name-resolver.ts @@ -1,4 +1,4 @@ -import { PlatformContext, FileNameResolver as FileNameResolverDefinition } from "../file-name-resolver"; +import { PlatformContext, FileNameResolver as FileNameResolverDefinition } from "."; import { screen, device } from "../../platform"; import { path as fsPath, Folder, File } from "../file-system"; import * as trace from "../../trace"; @@ -6,7 +6,6 @@ import * as appCommonModule from "../../application/application-common"; import { findMatch } from "../../module-name-resolver/qualifier-matcher/qualifier-matcher"; -@Deprecated export class FileNameResolver implements FileNameResolverDefinition { private _context: PlatformContext; private _cache = {}; diff --git a/nativescript-core/file-system/file-system-access.android.ts b/nativescript-core/file-system/file-system-access.android.ts index 8c2a7077f..9d73e96ec 100644 --- a/nativescript-core/file-system/file-system-access.android.ts +++ b/nativescript-core/file-system/file-system-access.android.ts @@ -495,7 +495,7 @@ export class FileSystemAccess { // TODO: This method is the same as in the iOS implementation. // Make it in a separate file / module so it can be reused from both implementations. - private getFileExtension(path: string): string { + public getFileExtension(path: string): string { const dotIndex = path.lastIndexOf("."); if (dotIndex && dotIndex >= 0 && dotIndex < path.length) { return path.substring(dotIndex); diff --git a/nativescript-core/file-system/file-system-access.ios.ts b/nativescript-core/file-system/file-system-access.ios.ts index d63f4b4a8..7bca2d71d 100644 --- a/nativescript-core/file-system/file-system-access.ios.ts +++ b/nativescript-core/file-system/file-system-access.ios.ts @@ -395,7 +395,7 @@ export class FileSystemAccess { // TODO: This method is the same as in the iOS implementation. // Make it in a separate file / module so it can be reused from both implementations. - private getFileExtension(path: string): string { + public getFileExtension(path: string): string { // TODO [For Panata]: The definitions currently specify "any" as a return value of this method //const nsString = Foundation.NSString.stringWithString(path); //const extension = nsString.pathExtension(); diff --git a/nativescript-core/globals/globals.ts b/nativescript-core/globals/globals.ts index e88a68496..005216cfb 100644 --- a/nativescript-core/globals/globals.ts +++ b/nativescript-core/globals/globals.ts @@ -1,6 +1,7 @@ import "./core"; import "./polyfills/timers"; +import "./polyfills/animation"; import "./polyfills/dialogs"; import "./polyfills/xhr"; import "./polyfills/fetch"; diff --git a/nativescript-core/globals/polyfills/animation/animation.d.ts b/nativescript-core/globals/polyfills/animation/animation.d.ts new file mode 100644 index 000000000..e149a301d --- /dev/null +++ b/nativescript-core/globals/polyfills/animation/animation.d.ts @@ -0,0 +1,5 @@ +/** + * Installs animations polyfill. + * @module "globals/polyfills/animations" + */ /** */ +import "../../core"; diff --git a/nativescript-core/globals/polyfills/animation/animation.ts b/nativescript-core/globals/polyfills/animation/animation.ts new file mode 100644 index 000000000..6c38fa34b --- /dev/null +++ b/nativescript-core/globals/polyfills/animation/animation.ts @@ -0,0 +1,6 @@ +import "../../core"; +import { installPolyfills } from "../polyfill-helpers"; + +global.registerModule("animation", () => require("../../../animation-frame")); + +installPolyfills("animation", ["requestAnimationFrame", "cancelAnimationFrame"]); diff --git a/nativescript-core/globals/polyfills/animation/package.json b/nativescript-core/globals/polyfills/animation/package.json new file mode 100644 index 000000000..a647c1dd4 --- /dev/null +++ b/nativescript-core/globals/polyfills/animation/package.json @@ -0,0 +1,6 @@ +{ + "name": "animation", + "main": "animation", + "types": "animation.d.ts", + "nativescript": {} +} \ No newline at end of file diff --git a/nativescript-core/index.ts b/nativescript-core/index.ts index 9ba41154a..6088694f4 100644 --- a/nativescript-core/index.ts +++ b/nativescript-core/index.ts @@ -47,7 +47,7 @@ export { File, FileSystemEntity, Folder, knownFolders, path } from "./file-syste // Export all interfaces from "http" module export { HttpRequestOptions, HttpResponse, Headers, HttpResponseEncoding, HttpContent } from "./http"; -// Export all methods from "http" as ApplicationSettings +// Export all methods from "http" as Http import { getFile, getImage, getJSON, getString as httpGetString, request } from "./http"; export const Http = { getFile, getImage, getJSON, getString: httpGetString, request }; diff --git a/nativescript-core/package.json b/nativescript-core/package.json index fad73d89d..a0441e28b 100644 --- a/nativescript-core/package.json +++ b/nativescript-core/package.json @@ -3,7 +3,7 @@ "main": "index", "types": "index.d.ts", "description": "Telerik NativeScript Core Modules", - "version": "6.2.3", + "version": "6.3.0", "homepage": "https://www.nativescript.org", "repository": { "type": "git", diff --git a/nativescript-core/ui/animation/animation-common.ts b/nativescript-core/ui/animation/animation-common.ts index 1c6e5bf42..9a7ace2ef 100644 --- a/nativescript-core/ui/animation/animation-common.ts +++ b/nativescript-core/ui/animation/animation-common.ts @@ -1,19 +1,23 @@ -// Definitions. +// Types. import { CubicBezierAnimationCurve as CubicBezierAnimationCurveDefinition, - AnimationPromise as AnimationPromiseDefinition, - Animation as AnimationBaseDefinition, - AnimationDefinition, - Pair + Animation as AnimationBaseDefinition } from "."; -import { View } from "../core/view"; +import { + AnimationDefinition, AnimationPromise as AnimationPromiseDefinition, + Pair, PropertyAnimation +} from "./animation-interfaces"; -// Types. +// Requires. import { Color } from "../../color"; -import { isEnabled as traceEnabled, write as traceWrite, categories as traceCategories, messageType as traceType } from "../../trace"; +import { + isEnabled as traceEnabled, write as traceWrite, + categories as traceCategories, messageType as traceType +} from "../../trace"; import { PercentLength } from "../styling/style-properties"; export { Color, traceEnabled, traceWrite, traceCategories, traceType }; +export * from "./animation-interfaces"; export module Properties { export const opacity = "opacity"; @@ -25,16 +29,6 @@ export module Properties { export const width = "width"; } -export interface PropertyAnimation { - target: View; - property: string; - value: any; - duration?: number; - delay?: number; - iterations?: number; - curve?: any; -} - export class CubicBezierAnimationCurve implements CubicBezierAnimationCurveDefinition { public x1: number; diff --git a/nativescript-core/ui/animation/animation-interfaces.ts b/nativescript-core/ui/animation/animation-interfaces.ts new file mode 100644 index 000000000..d086c7e42 --- /dev/null +++ b/nativescript-core/ui/animation/animation-interfaces.ts @@ -0,0 +1,72 @@ +// Types +import { View } from "../core/view"; +import { PercentLength } from "../styling/style-properties"; +import { Color } from "../../color"; + +export type Transformation = { + property: TransformationType; + value: TransformationValue; +}; + +export type TransformationType = "rotate" | + "translate" | "translateX" | "translateY" | + "scale" | "scaleX" | "scaleY"; + +export type TransformationValue = Pair | number; + +export type TransformFunctionsInfo = { + translate: Pair, + rotate: number, + scale: Pair, +}; + +export type AnimationPromise = Promise & Cancelable; + +export interface Pair { + x: number; + y: number; +} + +export interface Cancelable { + cancel(): void; +} + +export interface PropertyAnimation { + target: View; + property: string; + value: any; + duration?: number; + delay?: number; + iterations?: number; + curve?: any; +} + +export interface PropertyAnimationInfo extends PropertyAnimation { + _propertyResetCallback?: any; + _originalValue?: any; +} + +export interface AnimationDefinition { + target?: View; + opacity?: number; + backgroundColor?: Color; + translate?: Pair; + scale?: Pair; + height?: PercentLength | string; + width?: PercentLength | string; + rotate?: number; + duration?: number; + delay?: number; + iterations?: number; + curve?: any; +} + +export interface AnimationDefinitionInternal extends AnimationDefinition { + valueSource?: "animation" | "keyframe"; +} + +export interface IOSView extends View { + _suspendPresentationLayerUpdates(); + _resumePresentationLayerUpdates(); + _isPresentationLayerUpdateSuspeneded(); +} diff --git a/nativescript-core/ui/animation/animation.android.ts b/nativescript-core/ui/animation/animation.android.ts index 3aef429c9..ee4097b29 100644 --- a/nativescript-core/ui/animation/animation.android.ts +++ b/nativescript-core/ui/animation/animation.android.ts @@ -1,22 +1,22 @@ -// Definitions. -import { AnimationDefinition, AnimationPromise } from "."; +// Types. +import { AnimationDefinitionInternal, AnimationPromise, PropertyAnimation } from "./animation-common"; import { View } from "../core/view"; -import { AnimationBase, Properties, PropertyAnimation, CubicBezierAnimationCurve, Color, traceWrite, traceEnabled, traceCategories, traceType } from "./animation-common"; +// Requires +import { + AnimationBase, Properties, CubicBezierAnimationCurve, Color, traceWrite, + traceEnabled, traceCategories, traceType +} from "./animation-common"; import { opacityProperty, backgroundColorProperty, rotateProperty, translateXProperty, translateYProperty, scaleXProperty, scaleYProperty, heightProperty, widthProperty, PercentLength } from "../styling/style-properties"; - import { layout } from "../../utils/utils"; import { device, screen } from "../../platform"; import lazy from "../../utils/lazy"; -export * from "./animation-common"; -interface AnimationDefinitionInternal extends AnimationDefinition { - valueSource?: "animation" | "keyframe"; -} +export * from "./animation-common"; let argbEvaluator: android.animation.ArgbEvaluator; function ensureArgbEvaluator() { diff --git a/nativescript-core/ui/animation/animation.ios.ts b/nativescript-core/ui/animation/animation.ios.ts index 29f854055..2cf0c31cb 100644 --- a/nativescript-core/ui/animation/animation.ios.ts +++ b/nativescript-core/ui/animation/animation.ios.ts @@ -1,7 +1,15 @@ -import { AnimationDefinition, AnimationPromise } from "."; +// Types +import { + AnimationDefinitionInternal, AnimationPromise, IOSView, + PropertyAnimation, PropertyAnimationInfo +} from "./animation-common"; import { View } from "../core/view"; -import { AnimationBase, Properties, PropertyAnimation, CubicBezierAnimationCurve, traceWrite, traceEnabled, traceCategories, traceType } from "./animation-common"; +// Requires +import { + AnimationBase, Properties, CubicBezierAnimationCurve, + traceWrite, traceEnabled, traceCategories, traceType +} from "./animation-common"; import { opacityProperty, backgroundColorProperty, rotateProperty, translateXProperty, translateYProperty, scaleXProperty, scaleYProperty, @@ -26,21 +34,6 @@ class AnimationInfo { public delay: number; } -interface PropertyAnimationInfo extends PropertyAnimation { - _propertyResetCallback?: any; - _originalValue?: any; -} - -interface AnimationDefinitionInternal extends AnimationDefinition { - valueSource?: "animation" | "keyframe"; -} - -interface IOSView extends View { - _suspendPresentationLayerUpdates(); - _resumePresentationLayerUpdates(); - _isPresentationLayerUpdateSuspeneded(); -} - class AnimationDelegateImpl extends NSObject implements CAAnimationDelegate { public nextAnimation: Function; diff --git a/nativescript-core/ui/core/bindable/bindable-resources.ts b/nativescript-core/ui/core/bindable/bindable-resources.ts new file mode 100644 index 000000000..ef9aae8c0 --- /dev/null +++ b/nativescript-core/ui/core/bindable/bindable-resources.ts @@ -0,0 +1,9 @@ +let resources: any = {}; + +export function get() { + return resources; +} + +export function set(res: any) { + resources = res; +} \ No newline at end of file diff --git a/nativescript-core/ui/core/bindable/bindable.ts b/nativescript-core/ui/core/bindable/bindable.ts index 9dd65d386..46428f254 100644 --- a/nativescript-core/ui/core/bindable/bindable.ts +++ b/nativescript-core/ui/core/bindable/bindable.ts @@ -1,6 +1,8 @@ +// Types import { BindingOptions } from "."; import { ViewBase } from "../view-base"; +// Requires import { unsetValue } from "../properties"; import { Observable, WrappedValue, PropertyChangeData, EventData } from "../../../data/observable"; import { addWeakEventListener, removeWeakEventListener } from "../weak-event-listener"; @@ -16,8 +18,7 @@ import { messageType as traceMessageType } from "../../../trace"; import * as types from "../../../utils/types"; - -import * as applicationCommon from "../../../application/application-common"; +import * as bindableResources from "./bindable-resources"; import * as polymerExpressions from "../../../js-libs/polymer-expressions"; export { @@ -368,7 +369,7 @@ export class Binding { let context = this.source && this.source.get && this.source.get() || global; let model = {}; let addedProps = []; - const resources = applicationCommon.getResources(); + const resources = bindableResources.get(); for (let prop in resources) { if (resources.hasOwnProperty(prop) && !context.hasOwnProperty(prop)) { context[prop] = resources[prop]; diff --git a/nativescript-core/ui/core/properties/properties.ts b/nativescript-core/ui/core/properties/properties.ts index 24cea4c99..8ecaeaab9 100644 --- a/nativescript-core/ui/core/properties/properties.ts +++ b/nativescript-core/ui/core/properties/properties.ts @@ -58,19 +58,16 @@ export function _getStyleProperties(): CssProperty[] { return getPropertiesFromMap(cssSymbolPropertyMap) as CssProperty[]; } -const cssVariableExpressionRegexp = /\bvar\(\s*(--[^,\s]+?)(?:\s*,\s*(.+))?\s*\)/; -const cssVariableAllExpressionsRegexp = /\bvar\(\s*(--[^,\s]+?)(?:\s*,\s*(.+))?\s*\)/g; - export function isCssVariable(property: string) { return /^--[^,\s]+?$/.test(property); } export function isCssCalcExpression(value: string) { - return /\bcalc\(/.test(value); + return value.includes("calc("); } export function isCssVariableExpression(value: string) { - return cssVariableExpressionRegexp.test(value); + return value.includes("var(--"); } export function _evaluateCssVariableExpression(view: ViewBase, cssName: string, value: string): string { @@ -90,26 +87,28 @@ export function _evaluateCssVariableExpression(view: ViewBase, cssName: string, while (lastValue !== output) { lastValue = output; - output = output.replace(cssVariableAllExpressionsRegexp, (matchStr, cssVariableName: string, fallbackStr: string) => { - const cssVariableValue = view.style.getCssVariable(cssVariableName); - if (cssVariableValue !== null) { - return cssVariableValue; - } + const idx = output.lastIndexOf("var("); + if (idx === -1) { + continue; + } - if (fallbackStr) { - // css-variable not found, using fallback-string. - const fallbackOutput = _evaluateCssVariableExpression(view, cssName, fallbackStr); - if (fallbackOutput) { - // If the fallback have multiple values, return the first of them. - return fallbackOutput.split(",")[0]; - } - } + const endIdx = output.indexOf(")", idx); + if (endIdx === -1) { + continue; + } - // Couldn't find a value for the css-variable or the fallback, return "unset" - traceWrite(`Failed to get value for css-variable "${cssVariableName}" used in "${cssName}"=[${value}] to ${view}`, traceCategories.Style, traceMessageType.error); + const matched = output.substring(idx + 4, endIdx).split(",").map((v) => v.trim()).filter((v) => !!v); + const cssVariableName = matched.shift(); + let cssVariableValue = view.style.getCssVariable(cssVariableName); + if (cssVariableValue === null && matched.length) { + cssVariableValue = _evaluateCssVariableExpression(view, cssName, matched.join(", ")).split(",")[0]; + } - return "unset"; - }); + if (!cssVariableValue) { + cssVariableValue = "unset"; + } + + output = `${output.substring(0, idx)}${cssVariableValue}${output.substring(endIdx + 1)}`; } return output; diff --git a/nativescript-core/ui/core/view-base/view-base.d.ts b/nativescript-core/ui/core/view-base/view-base.d.ts index d0618064e..81723081f 100644 --- a/nativescript-core/ui/core/view-base/view-base.d.ts +++ b/nativescript-core/ui/core/view-base/view-base.d.ts @@ -83,10 +83,15 @@ export interface ShowModalOptions { } android?: { /** + * @deprecated Use ShowModalOptions.cancelable instead. * An optional parameter specifying whether the modal view can be dismissed when not in full-screen mode. */ cancelable?: boolean } + /** + * An optional parameter specifying whether the modal view can be dismissed when not in full-screen mode. + */ + cancelable?: boolean } export abstract class ViewBase extends Observable { @@ -434,6 +439,11 @@ export abstract class ViewBase extends Observable { */ _isStyleScopeHost: boolean; + /** + * @private + */ + public _layoutParent(): void; + /** * Determines the depth of suspended updates. * When the value is 0 the current property updates are not batched nor scoped and must be immediately applied. diff --git a/nativescript-core/ui/core/view-base/view-base.ts b/nativescript-core/ui/core/view-base/view-base.ts index 89dc23607..092eec6fc 100644 --- a/nativescript-core/ui/core/view-base/view-base.ts +++ b/nativescript-core/ui/core/view-base/view-base.ts @@ -7,7 +7,7 @@ import { Page } from "../../page"; // Types. import { Property, CssProperty, CssAnimationProperty, InheritedProperty, Style, clearInheritedProperties, propagateInheritableProperties, propagateInheritableCssProperties, initNativeView } from "../properties"; -import { getModalRootViewCssClass, getRootViewCssClasses } from "../../../css/system-classes"; +import { getSystemCssClasses, MODAL_ROOT_VIEW_CSS_CLASS, ROOT_VIEW_CSS_CLASS } from "../../../css/system-classes"; import { Source } from "../../../utils/debug"; import { Binding, BindingOptions, Observable, WrappedValue, PropertyChangeData, traceEnabled, traceWrite, traceCategories } from "../bindable"; import { isIOS, isAndroid } from "../../../platform"; @@ -377,6 +377,12 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition this._emit("unloaded"); } + public _layoutParent() { + if (this.parent) { + this.parent._layoutParent(); + } + } + public _suspendNativeUpdates(type: SuspendType): void { if (type) { this._suspendNativeUpdatesCount = this._suspendNativeUpdatesCount | type; @@ -1039,21 +1045,21 @@ export const classNameProperty = new Property({ name: "className", valueChanged(view: ViewBase, oldValue: string, newValue: string) { const cssClasses = view.cssClasses; + const rootViewsCssClasses = getSystemCssClasses(); - const modalViewCssClass = getModalRootViewCssClass(); - const rootViewCssClasses = getRootViewCssClasses(); - - const shouldAddModalRootViewCssClass = cssClasses.has(modalViewCssClass); - const shouldAddRootViewCssClasses = cssClasses.has(rootViewCssClasses[0]); + const shouldAddModalRootViewCssClasses = cssClasses.has(MODAL_ROOT_VIEW_CSS_CLASS); + const shouldAddRootViewCssClasses = cssClasses.has(ROOT_VIEW_CSS_CLASS); cssClasses.clear(); - if (shouldAddModalRootViewCssClass) { - cssClasses.add(modalViewCssClass); + if (shouldAddModalRootViewCssClasses) { + cssClasses.add(MODAL_ROOT_VIEW_CSS_CLASS); } else if (shouldAddRootViewCssClasses) { - rootViewCssClasses.forEach(c => cssClasses.add(c)); + cssClasses.add(ROOT_VIEW_CSS_CLASS); } + rootViewsCssClasses.forEach(c => cssClasses.add(c)); + if (typeof newValue === "string" && newValue !== "") { newValue.split(" ").forEach(c => cssClasses.add(c)); } diff --git a/nativescript-core/ui/core/view/view-common.ts b/nativescript-core/ui/core/view/view-common.ts index 1618a2090..e304a3aac 100644 --- a/nativescript-core/ui/core/view/view-common.ts +++ b/nativescript-core/ui/core/view/view-common.ts @@ -8,6 +8,7 @@ import { booleanConverter, EventData, getEventOrGestureName, InheritedProperty, layout, Property, ShowModalOptions, traceCategories, traceEnabled, traceWrite, ViewBase } from "../view-base"; +import { ViewHelper } from "./view-helper"; import { HorizontalAlignment, VerticalAlignment, Visibility, Length, PercentLength } from "../../styling/style-properties"; @@ -19,7 +20,7 @@ import { fromString as gestureFromString } from "../../gestures"; -import { getModalRootViewCssClass } from "../../../css/system-classes"; +import { getSystemCssClasses, MODAL_ROOT_VIEW_CSS_CLASS } from "../../../css/system-classes"; import { Builder } from "../../builder"; import { sanitizeModuleName } from "../../builder/module-name-sanitizer"; import { StyleScope } from "../../styling/style-scope"; @@ -371,8 +372,9 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition { protected _showNativeModalView(parent: ViewCommon, options: ShowModalOptions) { _rootModalViews.push(this); - const modalRootViewCssClass = getModalRootViewCssClass(); - this.cssClasses.add(modalRootViewCssClass); + this.cssClasses.add(MODAL_ROOT_VIEW_CSS_CLASS); + const modalRootViewCssClasses = getSystemCssClasses(); + modalRootViewCssClasses.forEach(c => this.cssClasses.add(c)); parent._modal = this; this._modalParent = parent; @@ -806,193 +808,19 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition { public abstract layoutNativeView(left: number, top: number, right: number, bottom: number): void; public static resolveSizeAndState(size: number, specSize: number, specMode: number, childMeasuredState: number): number { - let result = size; - switch (specMode) { - case layout.UNSPECIFIED: - result = Math.ceil(size); - break; - - case layout.AT_MOST: - if (specSize < size) { - result = Math.ceil(specSize) | layout.MEASURED_STATE_TOO_SMALL; - } - break; - - case layout.EXACTLY: - result = Math.ceil(specSize); - break; - } - - return result | (childMeasuredState & layout.MEASURED_STATE_MASK); + return ViewHelper.resolveSizeAndState(size, specSize, specMode, childMeasuredState); } public static combineMeasuredStates(curState: number, newState): number { - return curState | newState; + return ViewHelper.combineMeasuredStates(curState, newState); } public static layoutChild(parent: ViewDefinition, child: ViewDefinition, left: number, top: number, right: number, bottom: number, setFrame: boolean = true): 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 = child.effectiveMarginTop; - let effectiveMarginBottom = child.effectiveMarginBottom; - - let vAlignment: VerticalAlignment; - if (child.effectiveHeight >= 0 && childStyle.verticalAlignment === "stretch") { - vAlignment = "middle"; - } - else { - vAlignment = childStyle.verticalAlignment; - } - - switch (vAlignment) { - case "top": - childTop = top + effectiveMarginTop; - break; - - 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 = child.effectiveMarginLeft; - let effectiveMarginRight = child.effectiveMarginRight; - - let hAlignment: HorizontalAlignment; - if (child.effectiveWidth >= 0 && childStyle.horizontalAlignment === "stretch") { - hAlignment = "center"; - } - else { - hAlignment = childStyle.horizontalAlignment; - } - - switch (hAlignment) { - case "left": - childLeft = left + effectiveMarginLeft; - break; - - case "center": - 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, setFrame); + ViewHelper.layoutChild(parent, child, left, top, right, bottom); } 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) { - - const widthSpec = parent ? parent._currentWidthMeasureSpec : widthMeasureSpec; - const heightSpec = parent ? parent._currentHeightMeasureSpec : heightMeasureSpec; - - const width = layout.getMeasureSpecSize(widthSpec); - const widthMode = layout.getMeasureSpecMode(widthSpec); - - const height = layout.getMeasureSpecSize(heightSpec); - const heightMode = layout.getMeasureSpecMode(heightSpec); - - child._updateEffectiveLayoutValues(width, widthMode, height, heightMode); - - const style = child.style; - const horizontalMargins = child.effectiveMarginLeft + child.effectiveMarginRight; - const verticalMargins = child.effectiveMarginTop + child.effectiveMarginBottom; - - const childWidthMeasureSpec = ViewCommon.getMeasureSpec(widthMeasureSpec, horizontalMargins, child.effectiveWidth, style.horizontalAlignment === "stretch"); - const childHeightMeasureSpec = ViewCommon.getMeasureSpec(heightMeasureSpec, verticalMargins, child.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(parentSpec: number, margins: number, childLength: number, stretched: boolean): number { - const parentLength = layout.getMeasureSpecSize(parentSpec); - const parentSpecMode = layout.getMeasureSpecMode(parentSpec); - - let resultSize: number; - let resultMode: number; - - // 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. - // TODO: remove Math.min(parentLength, childLength) - 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); + return ViewHelper.measureChild(parent, child, widthMeasureSpec, heightMeasureSpec); } _setCurrentMeasureSpecs(widthMeasureSpec: number, heightMeasureSpec: number): boolean { diff --git a/nativescript-core/ui/core/view/view-helper/package.json b/nativescript-core/ui/core/view/view-helper/package.json new file mode 100644 index 000000000..0561420d6 --- /dev/null +++ b/nativescript-core/ui/core/view/view-helper/package.json @@ -0,0 +1,5 @@ +{ + "name": "view-helper", + "main": "view-helper", + "types": "view-helper.d.ts" +} diff --git a/nativescript-core/ui/core/view/view-helper/view-helper-common.ts b/nativescript-core/ui/core/view/view-helper/view-helper-common.ts new file mode 100644 index 000000000..f41213e65 --- /dev/null +++ b/nativescript-core/ui/core/view/view-helper/view-helper-common.ts @@ -0,0 +1,207 @@ +// Types +import { View as ViewDefinition } from ".."; +import { + HorizontalAlignment as HorizontalAlignmentDefinition, + VerticalAlignment as VerticalAlignmentDefinition +} from "../../../styling/style-properties"; + +// Requires +import { layout } from "../../../../utils/utils"; +import { + isEnabled as traceEnabled, + categories as traceCategories, + write as traceWrite +} from "../../../../trace"; + +export class ViewHelper { + + public static measureChild(parent: ViewDefinition, child: ViewDefinition, widthMeasureSpec: number, heightMeasureSpec: number): { measuredWidth: number; measuredHeight: number } { + let measureWidth = 0; + let measureHeight = 0; + + if (child && !child.isCollapsed) { + + const widthSpec = parent ? parent._currentWidthMeasureSpec : widthMeasureSpec; + const heightSpec = parent ? parent._currentHeightMeasureSpec : heightMeasureSpec; + + const width = layout.getMeasureSpecSize(widthSpec); + const widthMode = layout.getMeasureSpecMode(widthSpec); + + const height = layout.getMeasureSpecSize(heightSpec); + const heightMode = layout.getMeasureSpecMode(heightSpec); + + child._updateEffectiveLayoutValues(width, widthMode, height, heightMode); + + const style = child.style; + const horizontalMargins = child.effectiveMarginLeft + child.effectiveMarginRight; + const verticalMargins = child.effectiveMarginTop + child.effectiveMarginBottom; + + const childWidthMeasureSpec = ViewHelper.getMeasureSpec(widthMeasureSpec, horizontalMargins, child.effectiveWidth, style.horizontalAlignment === "stretch"); + const childHeightMeasureSpec = ViewHelper.getMeasureSpec(heightMeasureSpec, verticalMargins, child.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 }; + } + + public static layoutChild(parent: ViewDefinition, child: ViewDefinition, left: number, top: number, right: number, bottom: number, setFrame: boolean = true): 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 = child.effectiveMarginTop; + let effectiveMarginBottom = child.effectiveMarginBottom; + + let vAlignment: VerticalAlignmentDefinition; + if (child.effectiveHeight >= 0 && childStyle.verticalAlignment === "stretch") { + vAlignment = "middle"; + } + else { + vAlignment = childStyle.verticalAlignment; + } + + switch (vAlignment) { + case "top": + childTop = top + effectiveMarginTop; + break; + + 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 = child.effectiveMarginLeft; + let effectiveMarginRight = child.effectiveMarginRight; + + let hAlignment: HorizontalAlignmentDefinition; + if (child.effectiveWidth >= 0 && childStyle.horizontalAlignment === "stretch") { + hAlignment = "center"; + } + else { + hAlignment = childStyle.horizontalAlignment; + } + + switch (hAlignment) { + case "left": + childLeft = left + effectiveMarginLeft; + break; + + case "center": + 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, setFrame); + } + + public static resolveSizeAndState(size: number, specSize: number, specMode: number, childMeasuredState: number): number { + let result = size; + switch (specMode) { + case layout.UNSPECIFIED: + result = Math.ceil(size); + break; + + case layout.AT_MOST: + if (specSize < size) { + result = Math.ceil(specSize) | layout.MEASURED_STATE_TOO_SMALL; + } + break; + + case layout.EXACTLY: + result = Math.ceil(specSize); + break; + } + + return result | (childMeasuredState & layout.MEASURED_STATE_MASK); + } + + public static combineMeasuredStates(curState: number, newState): number { + return curState | newState; + } + + private static getMeasureSpec(parentSpec: number, margins: number, childLength: number, stretched: boolean): number { + const parentLength = layout.getMeasureSpecSize(parentSpec); + const parentSpecMode = layout.getMeasureSpecMode(parentSpec); + + let resultSize: number; + let resultMode: number; + + // 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. + // TODO: remove Math.min(parentLength, childLength) + 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); + } +} diff --git a/nativescript-core/ui/core/view/view-helper/view-helper.android.ts b/nativescript-core/ui/core/view/view-helper/view-helper.android.ts new file mode 100644 index 000000000..d95f1c4ff --- /dev/null +++ b/nativescript-core/ui/core/view/view-helper/view-helper.android.ts @@ -0,0 +1 @@ +export * from "./view-helper-common"; diff --git a/nativescript-core/ui/core/view/view-helper/view-helper.d.ts b/nativescript-core/ui/core/view/view-helper/view-helper.d.ts new file mode 100644 index 000000000..377b5474d --- /dev/null +++ b/nativescript-core/ui/core/view/view-helper/view-helper.d.ts @@ -0,0 +1,63 @@ +import { View } from ".."; + +export class ViewHelper { + /** + * Measure a child by taking into account its margins and a given measureSpecs. + * @param parent This parameter is not used. You can pass null. + * @param child The view to be measured. + * @param measuredWidth The measured width that the parent layout specifies for this view. + * @param measuredHeight The measured height that the parent layout specifies for this view. + */ + public static measureChild(parent: View, child: View, widthMeasureSpec: number, heightMeasureSpec: number): { measuredWidth: number; measuredHeight: number }; + + /** + * Layout a child by taking into account its margins, horizontal and vertical alignments and a given bounds. + * @param parent This parameter is not used. You can pass null. + * @param left Left position, relative to parent + * @param top Top position, relative to parent + * @param right Right position, relative to parent + * @param bottom Bottom position, relative to parent + */ + public static layoutChild(parent: View, child: View, left: number, top: number, right: number, bottom: number): void; + + /** + * Utility to reconcile a desired size and state, with constraints imposed + * by a MeasureSpec. Will take the desired size, unless a different size + * is imposed by the constraints. The returned value is a compound integer, + * with the resolved size in the MEASURED_SIZE_MASK bits and + * optionally the bit MEASURED_STATE_TOO_SMALL set if the resulting + * size is smaller than the size the view wants to be. + */ + public static resolveSizeAndState(size: number, specSize: number, specMode: number, childMeasuredState: number): number; + + public static combineMeasuredStates(curState: number, newState): number; +} + +export namespace ios { + /** + * String value used when hooking to traitCollectionColorAppearanceChangedEvent event. + */ + export const traitCollectionColorAppearanceChangedEvent: string; + + /** + * Returns a view with viewController or undefined if no such found along the view's parent chain. + * @param view The view form which to start the search. + */ + export function getParentWithViewController(view: View): View + export function updateAutoAdjustScrollInsets(controller: any /* UIViewController */, owner: View): void + export function updateConstraints(controller: any /* UIViewController */, owner: View): void; + export function layoutView(controller: any /* UIViewController */, owner: View): void; + export function getPositionFromFrame(frame: any /* CGRect */): { left, top, right, bottom }; + export function getFrameFromPosition(position: { left, top, right, bottom }, insets?: { left, top, right, bottom }): any /* CGRect */; + export function shrinkToSafeArea(view: View, frame: any /* CGRect */): any /* CGRect */; + export function expandBeyondSafeArea(view: View, frame: any /* CGRect */): any /* CGRect */; + export class UILayoutViewController { + public static initWithOwner(owner: WeakRef): UILayoutViewController; + } + export class UIAdaptivePresentationControllerDelegateImp { + public static initWithOwnerAndCallback(owner: WeakRef, whenClosedCallback: Function): UIAdaptivePresentationControllerDelegateImp; + } + export class UIPopoverPresentationControllerDelegateImp { + public static initWithOwnerAndCallback(owner: WeakRef, whenClosedCallback: Function): UIPopoverPresentationControllerDelegateImp; + } +} diff --git a/nativescript-core/ui/core/view/view-helper/view-helper.ios.ts b/nativescript-core/ui/core/view/view-helper/view-helper.ios.ts new file mode 100644 index 000000000..b52f28620 --- /dev/null +++ b/nativescript-core/ui/core/view/view-helper/view-helper.ios.ts @@ -0,0 +1,367 @@ +// Types +import { View } from ".."; + +// Requires +import { ViewHelper } from "./view-helper-common"; +import { + ios as iosUtils, + layout +} from "../../../../utils/utils"; +import { + isEnabled as traceEnabled, + messageType as traceMessageType, + categories as traceCategories, + write as traceWrite +} from "../../../../trace"; + +export * from "./view-helper-common"; + +const majorVersion = iosUtils.MajorVersion; + +export namespace ios { + export const traitCollectionColorAppearanceChangedEvent = "traitCollectionColorAppearanceChanged"; + + export function getParentWithViewController(view: View): View { + while (view && !view.viewController) { + view = view.parent as View; + } + + // Note: Might return undefined if no parent with viewController is found + return view; + } + + export function updateAutoAdjustScrollInsets(controller: UIViewController, owner: View): void { + if (majorVersion <= 10) { + owner._automaticallyAdjustsScrollViewInsets = false; + // This API is deprecated, but has no alternative for <= iOS 10 + // Defaults to true and results to appliyng the insets twice together with our logic + // for iOS 11+ we use the contentInsetAdjustmentBehavior property in scrollview + // https://developer.apple.com/documentation/uikit/uiviewcontroller/1621372-automaticallyadjustsscrollviewin + controller.automaticallyAdjustsScrollViewInsets = false; + } + } + + export function updateConstraints(controller: UIViewController, owner: View): void { + if (majorVersion <= 10) { + const layoutGuide = initLayoutGuide(controller); + (controller.view).safeAreaLayoutGuide = layoutGuide; + } + } + + function initLayoutGuide(controller: UIViewController) { + const rootView = controller.view; + const layoutGuide = UILayoutGuide.alloc().init(); + rootView.addLayoutGuide(layoutGuide); + NSLayoutConstraint.activateConstraints([ + layoutGuide.topAnchor.constraintEqualToAnchor(controller.topLayoutGuide.bottomAnchor), + layoutGuide.bottomAnchor.constraintEqualToAnchor(controller.bottomLayoutGuide.topAnchor), + layoutGuide.leadingAnchor.constraintEqualToAnchor(rootView.leadingAnchor), + layoutGuide.trailingAnchor.constraintEqualToAnchor(rootView.trailingAnchor) + ]); + + return layoutGuide; + } + + export function layoutView(controller: UIViewController, owner: View): void { + let layoutGuide = controller.view.safeAreaLayoutGuide; + if (!layoutGuide) { + traceWrite(`safeAreaLayoutGuide during layout of ${owner}. Creating fallback constraints, but layout might be wrong.`, + traceCategories.Layout, traceMessageType.error); + + layoutGuide = initLayoutGuide(controller); + } + const safeArea = layoutGuide.layoutFrame; + let position = ios.getPositionFromFrame(safeArea); + const safeAreaSize = safeArea.size; + + const hasChildViewControllers = controller.childViewControllers.count > 0; + if (hasChildViewControllers) { + const fullscreen = controller.view.frame; + position = ios.getPositionFromFrame(fullscreen); + } + + const safeAreaWidth = layout.round(layout.toDevicePixels(safeAreaSize.width)); + const safeAreaHeight = layout.round(layout.toDevicePixels(safeAreaSize.height)); + + const widthSpec = layout.makeMeasureSpec(safeAreaWidth, layout.EXACTLY); + const heightSpec = layout.makeMeasureSpec(safeAreaHeight, layout.EXACTLY); + + ViewHelper.measureChild(null, owner, widthSpec, heightSpec); + ViewHelper.layoutChild(null, owner, position.left, position.top, position.right, position.bottom); + + if (owner.parent) { + owner.parent._layoutParent(); + } + } + + export function getPositionFromFrame(frame: CGRect): { left, top, right, bottom } { + const left = layout.round(layout.toDevicePixels(frame.origin.x)); + const top = layout.round(layout.toDevicePixels(frame.origin.y)); + const right = layout.round(layout.toDevicePixels(frame.origin.x + frame.size.width)); + const bottom = layout.round(layout.toDevicePixels(frame.origin.y + frame.size.height)); + + return { left, right, top, bottom }; + } + + export function getFrameFromPosition(position: { left, top, right, bottom }, insets?: { left, top, right, bottom }): CGRect { + insets = insets || { left: 0, top: 0, right: 0, bottom: 0 }; + + const left = layout.toDeviceIndependentPixels(position.left + insets.left); + const top = layout.toDeviceIndependentPixels(position.top + insets.top); + const width = layout.toDeviceIndependentPixels(position.right - position.left - insets.left - insets.right); + const height = layout.toDeviceIndependentPixels(position.bottom - position.top - insets.top - insets.bottom); + + return CGRectMake(left, top, width, height); + } + + export function shrinkToSafeArea(view: View, frame: CGRect): CGRect { + const insets = view.getSafeAreaInsets(); + if (insets.left || insets.top) { + const position = ios.getPositionFromFrame(frame); + const adjustedFrame = ios.getFrameFromPosition(position, insets); + + if (traceEnabled()) { + traceWrite(this + " :shrinkToSafeArea: " + JSON.stringify(ios.getPositionFromFrame(adjustedFrame)), traceCategories.Layout); + } + + return adjustedFrame; + } + + return null; + } + + export function expandBeyondSafeArea(view: View, frame: CGRect): CGRect { + const availableSpace = getAvailableSpaceFromParent(view, frame); + const safeArea = availableSpace.safeArea; + const fullscreen = availableSpace.fullscreen; + const inWindow = availableSpace.inWindow; + + const position = ios.getPositionFromFrame(frame); + const safeAreaPosition = ios.getPositionFromFrame(safeArea); + const fullscreenPosition = ios.getPositionFromFrame(fullscreen); + const inWindowPosition = ios.getPositionFromFrame(inWindow); + + const adjustedPosition = position; + + if (position.left && inWindowPosition.left <= safeAreaPosition.left) { + adjustedPosition.left = fullscreenPosition.left; + } + + if (position.top && inWindowPosition.top <= safeAreaPosition.top) { + adjustedPosition.top = fullscreenPosition.top; + } + + if (inWindowPosition.right < fullscreenPosition.right && inWindowPosition.right >= safeAreaPosition.right + fullscreenPosition.left) { + adjustedPosition.right += fullscreenPosition.right - inWindowPosition.right; + } + + if (inWindowPosition.bottom < fullscreenPosition.bottom && inWindowPosition.bottom >= safeAreaPosition.bottom + fullscreenPosition.top) { + adjustedPosition.bottom += fullscreenPosition.bottom - inWindowPosition.bottom; + } + + const adjustedFrame = CGRectMake(layout.toDeviceIndependentPixels(adjustedPosition.left), layout.toDeviceIndependentPixels(adjustedPosition.top), layout.toDeviceIndependentPixels(adjustedPosition.right - adjustedPosition.left), layout.toDeviceIndependentPixels(adjustedPosition.bottom - adjustedPosition.top)); + + if (traceEnabled()) { + traceWrite(view + " :expandBeyondSafeArea: " + JSON.stringify(ios.getPositionFromFrame(adjustedFrame)), traceCategories.Layout); + } + + return adjustedFrame; + } + + function getAvailableSpaceFromParent(view: View, frame: CGRect): { safeArea: CGRect, fullscreen: CGRect, inWindow: CGRect } { + if (!view) { + return; + } + + let scrollView = null; + let viewControllerView = null; + + if (view.viewController) { + viewControllerView = view.viewController.view; + } else { + let parent = view.parent as View; + while (parent && !parent.viewController && !(parent.nativeViewProtected instanceof UIScrollView)) { + parent = parent.parent as View; + } + + if (parent.nativeViewProtected instanceof UIScrollView) { + scrollView = parent.nativeViewProtected; + } else if (parent.viewController) { + viewControllerView = parent.viewController.view; + } + } + + let fullscreen = null; + let safeArea = null; + + if (viewControllerView) { + safeArea = viewControllerView.safeAreaLayoutGuide.layoutFrame; + fullscreen = viewControllerView.frame; + } + else if (scrollView) { + const insets = scrollView.safeAreaInsets; + safeArea = CGRectMake(insets.left, insets.top, scrollView.contentSize.width - insets.left - insets.right, scrollView.contentSize.height - insets.top - insets.bottom); + fullscreen = CGRectMake(0, 0, scrollView.contentSize.width, scrollView.contentSize.height); + } + + const locationInWindow = view.getLocationInWindow(); + let inWindowLeft = locationInWindow.x; + let inWindowTop = locationInWindow.y; + + if (scrollView) { + inWindowLeft += scrollView.contentOffset.x; + inWindowTop += scrollView.contentOffset.y; + } + + const inWindow = CGRectMake(inWindowLeft, inWindowTop, frame.size.width, frame.size.height); + + return { safeArea: safeArea, fullscreen: fullscreen, inWindow: inWindow }; + } + + export class UILayoutViewController extends UIViewController { + public owner: WeakRef; + + public static initWithOwner(owner: WeakRef): UILayoutViewController { + const controller = UILayoutViewController.new(); + controller.owner = owner; + + return controller; + } + + public viewDidLoad(): void { + super.viewDidLoad(); + + // Unify translucent and opaque bars layout + // this.edgesForExtendedLayout = UIRectEdgeBottom; + this.extendedLayoutIncludesOpaqueBars = true; + } + + public viewWillLayoutSubviews(): void { + super.viewWillLayoutSubviews(); + const owner = this.owner.get(); + if (owner) { + updateConstraints(this, owner); + } + } + + public viewDidLayoutSubviews(): void { + super.viewDidLayoutSubviews(); + const owner = this.owner.get(); + if (owner) { + if (majorVersion >= 11) { + // Handle nested UILayoutViewController safe area application. + // Currently, UILayoutViewController can be nested only in a TabView. + // The TabView itself is handled by the OS, so we check the TabView's parent (usually a Page, but can be a Layout). + const tabViewItem = owner.parent; + const tabView = tabViewItem && tabViewItem.parent; + let parent = tabView && tabView.parent; + + // Handle Angular scenario where TabView is in a ProxyViewContainer + // It is possible to wrap components in ProxyViewContainers indefinitely + // Not using instanceof ProxyViewContainer to avoid circular dependency + // TODO: Try moving UILayoutViewController out of view module + while (parent && !parent.nativeViewProtected) { + parent = parent.parent; + } + + if (parent) { + const parentPageInsetsTop = parent.nativeViewProtected.safeAreaInsets.top; + const currentInsetsTop = this.view.safeAreaInsets.top; + const additionalInsetsTop = Math.max(parentPageInsetsTop - currentInsetsTop, 0); + + const parentPageInsetsBottom = parent.nativeViewProtected.safeAreaInsets.bottom; + const currentInsetsBottom = this.view.safeAreaInsets.bottom; + const additionalInsetsBottom = Math.max(parentPageInsetsBottom - currentInsetsBottom, 0); + + if (additionalInsetsTop > 0 || additionalInsetsBottom > 0) { + const additionalInsets = new UIEdgeInsets({ top: additionalInsetsTop, left: 0, bottom: additionalInsetsBottom, right: 0 }); + this.additionalSafeAreaInsets = additionalInsets; + } + } + } + + layoutView(this, owner); + } + } + + public viewWillAppear(animated: boolean): void { + super.viewWillAppear(animated); + const owner = this.owner.get(); + if (!owner) { + return; + } + + updateAutoAdjustScrollInsets(this, owner); + + if (!owner.parent) { + owner.callLoaded(); + } + } + + public viewDidDisappear(animated: boolean): void { + super.viewDidDisappear(animated); + const owner = this.owner.get(); + if (owner && !owner.parent) { + owner.callUnloaded(); + } + } + + // Mind implementation for other controllers + public traitCollectionDidChange(previousTraitCollection: UITraitCollection): void { + super.traitCollectionDidChange(previousTraitCollection); + + if (majorVersion >= 13) { + const owner = this.owner.get(); + if (owner && + this.traitCollection.hasDifferentColorAppearanceComparedToTraitCollection && + this.traitCollection.hasDifferentColorAppearanceComparedToTraitCollection(previousTraitCollection)) { + owner.notify({ eventName: traitCollectionColorAppearanceChangedEvent, object: owner }); + } + } + } + } + + export class UIAdaptivePresentationControllerDelegateImp extends NSObject implements UIAdaptivePresentationControllerDelegate { + public static ObjCProtocols = [UIAdaptivePresentationControllerDelegate]; + + private owner: WeakRef; + private closedCallback: Function; + + public static initWithOwnerAndCallback(owner: WeakRef, whenClosedCallback: Function): UIAdaptivePresentationControllerDelegateImp { + const instance = super.new(); + instance.owner = owner; + instance.closedCallback = whenClosedCallback; + + return instance; + } + + public presentationControllerDidDismiss(presentationController: UIPresentationController) { + const owner = this.owner.get(); + if (owner && typeof this.closedCallback === "function") { + this.closedCallback(); + } + } + } + + export class UIPopoverPresentationControllerDelegateImp extends NSObject implements UIPopoverPresentationControllerDelegate { + public static ObjCProtocols = [UIPopoverPresentationControllerDelegate]; + + private owner: WeakRef; + private closedCallback: Function; + + public static initWithOwnerAndCallback(owner: WeakRef, whenClosedCallback: Function): UIPopoverPresentationControllerDelegateImp { + const instance = super.new(); + instance.owner = owner; + instance.closedCallback = whenClosedCallback; + + return instance; + } + + public popoverPresentationControllerDidDismissPopover(popoverPresentationController: UIPopoverPresentationController) { + const owner = this.owner.get(); + if (owner && typeof this.closedCallback === "function") { + this.closedCallback(); + } + } + } +} \ No newline at end of file diff --git a/nativescript-core/ui/core/view/view.android.ts b/nativescript-core/ui/core/view/view.android.ts index 5b42e0047..c6a3132f2 100644 --- a/nativescript-core/ui/core/view/view.android.ts +++ b/nativescript-core/ui/core/view/view.android.ts @@ -640,12 +640,21 @@ export class View extends ViewCommon { args.putInt(DOMID, this._domId); df.setArguments(args); + let cancelable = true; + + if (options.android && (options).android.cancelable !== undefined) { + cancelable = !!(options).android.cancelable; + console.log("ShowModalOptions.android.cancelable is deprecated. Use ShowModalOptions.cancelable instead."); + } + + cancelable = options.cancelable !== undefined ? !!options.cancelable : cancelable; + const dialogOptions: DialogOptions = { owner: this, fullscreen: !!options.fullscreen, animated: !!options.animated, stretched: !!options.stretched, - cancelable: options.android ? !!options.android.cancelable : true, + cancelable: cancelable, shownCallback: () => this._raiseShownModallyEvent(), dismissCallback: () => this.closeModal() }; diff --git a/nativescript-core/ui/core/view/view.ios.ts b/nativescript-core/ui/core/view/view.ios.ts index 30c58eb69..822b160b7 100644 --- a/nativescript-core/ui/core/view/view.ios.ts +++ b/nativescript-core/ui/core/view/view.ios.ts @@ -1,12 +1,12 @@ -// Definitions. +// Types. import { Point, View as ViewDefinition, dip } from "."; -import { ViewBase } from "../view-base"; +// Requires import { ViewCommon, layout, isEnabledProperty, originXProperty, originYProperty, automationTextProperty, isUserInteractionEnabledProperty, traceEnabled, traceWrite, traceCategories, traceError, traceMessageType, ShowModalOptions } from "./view-common"; - +import { ios } from "./view-helper"; import { ios as iosBackground, Background } from "../../styling/background"; import { ios as iosUtils } from "../../../utils/utils"; import { @@ -19,6 +19,7 @@ import { import { profile } from "../../../profiling"; export * from "./view-common"; +export { ios }; const PFLAG_FORCE_LAYOUT = 1; const PFLAG_MEASURED_DIMENSION_SET = 1 << 1; @@ -26,10 +27,11 @@ const PFLAG_LAYOUT_REQUIRED = 1 << 2; const majorVersion = iosUtils.MajorVersion; -export class View extends ViewCommon { +export class View extends ViewCommon implements ViewDefinition { nativeViewProtected: UIView; viewController: UIViewController; private _popoverPresentationDelegate: ios.UIPopoverPresentationControllerDelegateImp; + private _adaptivePresentationDelegate: ios.UIAdaptivePresentationControllerDelegateImp; private _isLaidOut = false; private _hasTransfrom = false; @@ -214,6 +216,21 @@ export class View extends ViewCommon { this._setNativeViewFrame(nativeView, frame); } + public _layoutParent() { + if (this.nativeViewProtected) { + const frame = this.nativeViewProtected.frame; + const origin = frame.origin; + const size = frame.size; + const left = layout.toDevicePixels(origin.x); + const top = layout.toDevicePixels(origin.y); + const width = layout.toDevicePixels(size.width); + const height = layout.toDevicePixels(size.height); + this._setLayoutFlags(left, top, width + left, height + top); + } + + super._layoutParent(); + } + public _setLayoutFlags(left: number, top: number, right: number, bottom: number): void { const width = right - left; const height = bottom - top; @@ -398,11 +415,11 @@ export class View extends ViewCommon { this._setupAsRootView({}); - super._showNativeModalView(parentWithController, options); + super._showNativeModalView(parentWithController, options); let controller = this.viewController; if (!controller) { const nativeView = this.ios || this.nativeViewProtected; - controller = ios.UILayoutViewController.initWithOwner(new WeakRef(this)); + controller = ios.UILayoutViewController.initWithOwner(new WeakRef(this)); if (nativeView instanceof UIView) { controller.view.addSubview(nativeView); @@ -422,14 +439,19 @@ export class View extends ViewCommon { controller.modalPresentationStyle = presentationStyle; if (presentationStyle === UIModalPresentationStyle.Popover) { - const popoverPresentationController = controller.popoverPresentationController; - this._popoverPresentationDelegate = ios.UIPopoverPresentationControllerDelegateImp.initWithOwnerAndCallback(new WeakRef(this), this._closeModalCallback); - popoverPresentationController.delegate = this._popoverPresentationDelegate; - const view = parent.nativeViewProtected; - // Note: sourceView and sourceRect are needed to specify the anchor location for the popover. - // Note: sourceView should be the button triggering the modal. If it the Page the popover might appear "behind" the page content - popoverPresentationController.sourceView = view; - popoverPresentationController.sourceRect = CGRectMake(0, 0, view.frame.size.width, view.frame.size.height); + this._setupPopoverControllerDelegate(controller, parent); + } + } + + const cancelable = options.cancelable !== undefined ? !!options.cancelable : true; + + if (majorVersion >= 13) { + if (cancelable) { + // Listen for dismiss modal callback. + this._setupAdaptiveControllerDelegate(controller); + } else { + // Prevent users from dismissing the modal. + (controller).modalInPresentation = true; } } @@ -644,6 +666,22 @@ export class View extends ViewCommon { backgroundInternal.hasBorderWidth() || backgroundInternal.hasBorderRadius(); } + + private _setupPopoverControllerDelegate(controller: UIViewController, parent: View) { + const popoverPresentationController = controller.popoverPresentationController; + this._popoverPresentationDelegate = ios.UIPopoverPresentationControllerDelegateImp.initWithOwnerAndCallback(new WeakRef(this), this._closeModalCallback); + popoverPresentationController.delegate = this._popoverPresentationDelegate; + const view = parent.nativeViewProtected; + // Note: sourceView and sourceRect are needed to specify the anchor location for the popover. + // Note: sourceView should be the button triggering the modal. If it the Page the popover might appear "behind" the page content + popoverPresentationController.sourceView = view; + popoverPresentationController.sourceRect = CGRectMake(0, 0, view.frame.size.width, view.frame.size.height); + } + + private _setupAdaptiveControllerDelegate(controller: UIViewController) { + this._adaptivePresentationDelegate = ios.UIAdaptivePresentationControllerDelegateImp.initWithOwnerAndCallback(new WeakRef(this), this._closeModalCallback); + controller.presentationController.delegate = this._adaptivePresentationDelegate; + } } View.prototype._nativeBackgroundState = "unset"; @@ -700,346 +738,3 @@ export class CustomLayoutView extends ContainerView { } } } - -export namespace ios { - export const traitCollectionColorAppearanceChangedEvent = "traitCollectionColorAppearanceChanged"; - - export function getParentWithViewController(view: View): View { - while (view && !view.viewController) { - view = view.parent as View; - } - - // Note: Might return undefined if no parent with viewController is found - return view; - } - - export function updateAutoAdjustScrollInsets(controller: UIViewController, owner: View): void { - if (majorVersion <= 10) { - owner._automaticallyAdjustsScrollViewInsets = false; - // This API is deprecated, but has no alternative for <= iOS 10 - // Defaults to true and results to appliyng the insets twice together with our logic - // for iOS 11+ we use the contentInsetAdjustmentBehavior property in scrollview - // https://developer.apple.com/documentation/uikit/uiviewcontroller/1621372-automaticallyadjustsscrollviewin - controller.automaticallyAdjustsScrollViewInsets = false; - } - } - - export function updateConstraints(controller: UIViewController, owner: View): void { - if (majorVersion <= 10) { - const layoutGuide = initLayoutGuide(controller); - (controller.view).safeAreaLayoutGuide = layoutGuide; - } - } - - function initLayoutGuide(controller: UIViewController) { - const rootView = controller.view; - const layoutGuide = UILayoutGuide.alloc().init(); - rootView.addLayoutGuide(layoutGuide); - NSLayoutConstraint.activateConstraints([ - layoutGuide.topAnchor.constraintEqualToAnchor(controller.topLayoutGuide.bottomAnchor), - layoutGuide.bottomAnchor.constraintEqualToAnchor(controller.bottomLayoutGuide.topAnchor), - layoutGuide.leadingAnchor.constraintEqualToAnchor(rootView.leadingAnchor), - layoutGuide.trailingAnchor.constraintEqualToAnchor(rootView.trailingAnchor) - ]); - - return layoutGuide; - } - - export function layoutView(controller: UIViewController, owner: View): void { - let layoutGuide = controller.view.safeAreaLayoutGuide; - if (!layoutGuide) { - traceWrite(`safeAreaLayoutGuide during layout of ${owner}. Creating fallback constraints, but layout might be wrong.`, - traceCategories.Layout, traceMessageType.error); - - layoutGuide = initLayoutGuide(controller); - } - const safeArea = layoutGuide.layoutFrame; - let position = ios.getPositionFromFrame(safeArea); - const safeAreaSize = safeArea.size; - - const hasChildViewControllers = controller.childViewControllers.count > 0; - if (hasChildViewControllers) { - const fullscreen = controller.view.frame; - position = ios.getPositionFromFrame(fullscreen); - } - - const safeAreaWidth = layout.round(layout.toDevicePixels(safeAreaSize.width)); - const safeAreaHeight = layout.round(layout.toDevicePixels(safeAreaSize.height)); - - const widthSpec = layout.makeMeasureSpec(safeAreaWidth, layout.EXACTLY); - const heightSpec = layout.makeMeasureSpec(safeAreaHeight, layout.EXACTLY); - - View.measureChild(null, owner, widthSpec, heightSpec); - View.layoutChild(null, owner, position.left, position.top, position.right, position.bottom); - - layoutParent(owner.parent); - } - - export function getPositionFromFrame(frame: CGRect): { left, top, right, bottom } { - const left = layout.round(layout.toDevicePixels(frame.origin.x)); - const top = layout.round(layout.toDevicePixels(frame.origin.y)); - const right = layout.round(layout.toDevicePixels(frame.origin.x + frame.size.width)); - const bottom = layout.round(layout.toDevicePixels(frame.origin.y + frame.size.height)); - - return { left, right, top, bottom }; - } - - export function getFrameFromPosition(position: { left, top, right, bottom }, insets?: { left, top, right, bottom }): CGRect { - insets = insets || { left: 0, top: 0, right: 0, bottom: 0 }; - - const left = layout.toDeviceIndependentPixels(position.left + insets.left); - const top = layout.toDeviceIndependentPixels(position.top + insets.top); - const width = layout.toDeviceIndependentPixels(position.right - position.left - insets.left - insets.right); - const height = layout.toDeviceIndependentPixels(position.bottom - position.top - insets.top - insets.bottom); - - return CGRectMake(left, top, width, height); - } - - export function shrinkToSafeArea(view: View, frame: CGRect): CGRect { - const insets = view.getSafeAreaInsets(); - if (insets.left || insets.top) { - const position = ios.getPositionFromFrame(frame); - const adjustedFrame = ios.getFrameFromPosition(position, insets); - - if (traceEnabled()) { - traceWrite(this + " :shrinkToSafeArea: " + JSON.stringify(ios.getPositionFromFrame(adjustedFrame)), traceCategories.Layout); - } - - return adjustedFrame; - } - - return null; - } - - export function expandBeyondSafeArea(view: View, frame: CGRect): CGRect { - const availableSpace = getAvailableSpaceFromParent(view, frame); - const safeArea = availableSpace.safeArea; - const fullscreen = availableSpace.fullscreen; - const inWindow = availableSpace.inWindow; - - const position = ios.getPositionFromFrame(frame); - const safeAreaPosition = ios.getPositionFromFrame(safeArea); - const fullscreenPosition = ios.getPositionFromFrame(fullscreen); - const inWindowPosition = ios.getPositionFromFrame(inWindow); - - const adjustedPosition = position; - - if (position.left && inWindowPosition.left <= safeAreaPosition.left) { - adjustedPosition.left = fullscreenPosition.left; - } - - if (position.top && inWindowPosition.top <= safeAreaPosition.top) { - adjustedPosition.top = fullscreenPosition.top; - } - - if (inWindowPosition.right < fullscreenPosition.right && inWindowPosition.right >= safeAreaPosition.right + fullscreenPosition.left) { - adjustedPosition.right += fullscreenPosition.right - inWindowPosition.right; - } - - if (inWindowPosition.bottom < fullscreenPosition.bottom && inWindowPosition.bottom >= safeAreaPosition.bottom + fullscreenPosition.top) { - adjustedPosition.bottom += fullscreenPosition.bottom - inWindowPosition.bottom; - } - - const adjustedFrame = CGRectMake(layout.toDeviceIndependentPixels(adjustedPosition.left), layout.toDeviceIndependentPixels(adjustedPosition.top), layout.toDeviceIndependentPixels(adjustedPosition.right - adjustedPosition.left), layout.toDeviceIndependentPixels(adjustedPosition.bottom - adjustedPosition.top)); - - if (traceEnabled()) { - traceWrite(view + " :expandBeyondSafeArea: " + JSON.stringify(ios.getPositionFromFrame(adjustedFrame)), traceCategories.Layout); - } - - return adjustedFrame; - } - - function layoutParent(view: ViewBase): void { - if (!view) { - return; - } - - if (view instanceof View && view.nativeViewProtected) { - const frame = view.nativeViewProtected.frame; - const origin = frame.origin; - const size = frame.size; - const left = layout.toDevicePixels(origin.x); - const top = layout.toDevicePixels(origin.y); - const width = layout.toDevicePixels(size.width); - const height = layout.toDevicePixels(size.height); - view._setLayoutFlags(left, top, width + left, height + top); - } - - layoutParent(view.parent); - } - - function getAvailableSpaceFromParent(view: View, frame: CGRect): { safeArea: CGRect, fullscreen: CGRect, inWindow: CGRect } { - if (!view) { - return; - } - - let scrollView = null; - let viewControllerView = null; - - if (view.viewController) { - viewControllerView = view.viewController.view; - } else { - let parent = view.parent as View; - while (parent && !parent.viewController && !(parent.nativeViewProtected instanceof UIScrollView)) { - parent = parent.parent as View; - } - - if (parent.nativeViewProtected instanceof UIScrollView) { - scrollView = parent.nativeViewProtected; - } else if (parent.viewController) { - viewControllerView = parent.viewController.view; - } - } - - let fullscreen = null; - let safeArea = null; - - if (viewControllerView) { - safeArea = viewControllerView.safeAreaLayoutGuide.layoutFrame; - fullscreen = viewControllerView.frame; - } - else if (scrollView) { - const insets = scrollView.safeAreaInsets; - safeArea = CGRectMake(insets.left, insets.top, scrollView.contentSize.width - insets.left - insets.right, scrollView.contentSize.height - insets.top - insets.bottom); - fullscreen = CGRectMake(0, 0, scrollView.contentSize.width, scrollView.contentSize.height); - } - - const locationInWindow = view.getLocationInWindow(); - let inWindowLeft = locationInWindow.x; - let inWindowTop = locationInWindow.y; - - if (scrollView) { - inWindowLeft += scrollView.contentOffset.x; - inWindowTop += scrollView.contentOffset.y; - } - - const inWindow = CGRectMake(inWindowLeft, inWindowTop, frame.size.width, frame.size.height); - - return { safeArea: safeArea, fullscreen: fullscreen, inWindow: inWindow }; - } - - export class UILayoutViewController extends UIViewController { - public owner: WeakRef; - - public static initWithOwner(owner: WeakRef): UILayoutViewController { - const controller = UILayoutViewController.new(); - controller.owner = owner; - - return controller; - } - - public viewDidLoad(): void { - super.viewDidLoad(); - - // Unify translucent and opaque bars layout - // this.edgesForExtendedLayout = UIRectEdgeBottom; - this.extendedLayoutIncludesOpaqueBars = true; - } - - public viewWillLayoutSubviews(): void { - super.viewWillLayoutSubviews(); - const owner = this.owner.get(); - if (owner) { - updateConstraints(this, owner); - } - } - - public viewDidLayoutSubviews(): void { - super.viewDidLayoutSubviews(); - const owner = this.owner.get(); - if (owner) { - if (majorVersion >= 11) { - // Handle nested UILayoutViewController safe area application. - // Currently, UILayoutViewController can be nested only in a TabView. - // The TabView itself is handled by the OS, so we check the TabView's parent (usually a Page, but can be a Layout). - const tabViewItem = owner.parent; - const tabView = tabViewItem && tabViewItem.parent; - let parent = tabView && tabView.parent; - - // Handle Angular scenario where TabView is in a ProxyViewContainer - // It is possible to wrap components in ProxyViewContainers indefinitely - // Not using instanceof ProxyViewContainer to avoid circular dependency - // TODO: Try moving UILayoutViewController out of view module - while (parent && !parent.nativeViewProtected) { - parent = parent.parent; - } - - if (parent) { - const parentPageInsetsTop = parent.nativeViewProtected.safeAreaInsets.top; - const currentInsetsTop = this.view.safeAreaInsets.top; - const additionalInsetsTop = Math.max(parentPageInsetsTop - currentInsetsTop, 0); - - const parentPageInsetsBottom = parent.nativeViewProtected.safeAreaInsets.bottom; - const currentInsetsBottom = this.view.safeAreaInsets.bottom; - const additionalInsetsBottom = Math.max(parentPageInsetsBottom - currentInsetsBottom, 0); - - if (additionalInsetsTop > 0 || additionalInsetsBottom > 0) { - const additionalInsets = new UIEdgeInsets({ top: additionalInsetsTop, left: 0, bottom: additionalInsetsBottom, right: 0 }); - this.additionalSafeAreaInsets = additionalInsets; - } - } - } - - layoutView(this, owner); - } - } - - public viewWillAppear(animated: boolean): void { - super.viewWillAppear(animated); - const owner = this.owner.get(); - if (!owner) { - return; - } - - updateAutoAdjustScrollInsets(this, owner); - - if (!owner.parent) { - owner.callLoaded(); - } - } - - public viewDidDisappear(animated: boolean): void { - super.viewDidDisappear(animated); - const owner = this.owner.get(); - if (owner && !owner.parent) { - owner.callUnloaded(); - } - } - - // Mind implementation for other controllers - public traitCollectionDidChange(previousTraitCollection: UITraitCollection): void { - super.traitCollectionDidChange(previousTraitCollection); - - if (majorVersion >= 13) { - const owner = this.owner.get(); - if (owner && - this.traitCollection.hasDifferentColorAppearanceComparedToTraitCollection && - this.traitCollection.hasDifferentColorAppearanceComparedToTraitCollection(previousTraitCollection)) { - owner.notify({ eventName: traitCollectionColorAppearanceChangedEvent, object: owner }); - } - } - } - } - - export class UIPopoverPresentationControllerDelegateImp extends NSObject implements UIPopoverPresentationControllerDelegate { - public static ObjCProtocols = [UIPopoverPresentationControllerDelegate]; - - private owner: WeakRef; - private closedCallback: Function; - - public static initWithOwnerAndCallback(owner: WeakRef, whenClosedCallback: Function): UIPopoverPresentationControllerDelegateImp { - const instance = super.new(); - instance.owner = owner; - instance.closedCallback = whenClosedCallback; - - return instance; - } - - public popoverPresentationControllerDidDismissPopover(popoverPresentationController: UIPopoverPresentationController) { - const owner = this.owner.get(); - if (owner && typeof this.closedCallback === "function") { - this.closedCallback(); - } - } - } -} diff --git a/nativescript-core/ui/dialogs/dialogs.ios.ts b/nativescript-core/ui/dialogs/dialogs.ios.ts index d8276adeb..f7eb1dbe6 100644 --- a/nativescript-core/ui/dialogs/dialogs.ios.ts +++ b/nativescript-core/ui/dialogs/dialogs.ios.ts @@ -205,6 +205,11 @@ function showUIAlertController(alertController: UIAlertController) { if (currentView) { currentView = currentView.modal || currentView; + //get to the top most view controller on the stack + while (currentView && currentView.modal) { + currentView = currentView.modal; + } + let viewController: UIViewController = currentView.ios; if (viewController.presentedViewController) { diff --git a/nativescript-core/ui/frame/fragment.transitions.ios.ts b/nativescript-core/ui/frame/fragment.transitions.ios.ts index 7599a6093..6aa8f4c2c 100644 --- a/nativescript-core/ui/frame/fragment.transitions.ios.ts +++ b/nativescript-core/ui/frame/fragment.transitions.ios.ts @@ -69,7 +69,7 @@ class AnimatedTransitioning extends NSObject implements UIViewControllerAnimated } export function _createIOSAnimatedTransitioning(navigationTransition: NavigationTransition, nativeCurve: UIViewAnimationCurve, operation: UINavigationControllerOperation, fromVC: UIViewController, toVC: UIViewController): UIViewControllerAnimatedTransitioning { - const instance = navigationTransition.instance; + const instance = navigationTransition.instance; let transition: Transition; if (instance) { diff --git a/nativescript-core/ui/frame/frame-common.ts b/nativescript-core/ui/frame/frame-common.ts index 37b8b12c4..7abe9f168 100644 --- a/nativescript-core/ui/frame/frame-common.ts +++ b/nativescript-core/ui/frame/frame-common.ts @@ -1,23 +1,19 @@ -// Definitions. -import { Frame as FrameDefinition, NavigationEntry, BackstackEntry, NavigationTransition } from "."; -import { Page } from "../page"; - // Types. -import { getAncestor, viewMatchesModuleContext } from "../core/view/view-common"; +import { Frame as FrameDefinition } from "."; +import { BackstackEntry, NavigationContext, NavigationEntry, NavigationTransition, NavigationType } from "./frame-interfaces"; +import { Page } from "../page"; import { View, CustomLayoutView, isIOS, isAndroid, traceEnabled, traceWrite, traceCategories, Property, CSSType } from "../core/view"; + +// Requires. +import { frameStack, topmost as frameStackTopmost, _pushInFrameStack, _popFromFrameStack, _removeFromFrameStack } from "./frame-stack"; +import { getAncestor, viewMatchesModuleContext } from "../core/view/view-common"; import { Builder } from "../builder"; +import { sanitizeModuleName } from "../builder/module-name-sanitizer"; import { profile } from "../../profiling"; -import { frameStack, topmost as frameStackTopmost, _pushInFrameStack, _popFromFrameStack, _removeFromFrameStack } from "./frame-stack"; -import { sanitizeModuleName } from "../builder/module-name-sanitizer"; +export * from "./frame-interfaces"; export * from "../core/view"; -export enum NavigationType { - back, - forward, - replace -} - function buildEntryFromArgs(arg: any): NavigationEntry { let entry: NavigationEntry; if (typeof arg === "string") { @@ -35,13 +31,6 @@ function buildEntryFromArgs(arg: any): NavigationEntry { return entry; } -export interface NavigationContext { - entry: BackstackEntry; - // TODO: remove isBackNavigation for NativeScript 6.0 - isBackNavigation: boolean; - navigationType: NavigationType; -} - @CSSType("Frame") export class FrameBase extends CustomLayoutView implements FrameDefinition { public static androidOptionSelectedEvent = "optionSelected"; diff --git a/nativescript-core/ui/frame/frame-interfaces.ts b/nativescript-core/ui/frame/frame-interfaces.ts new file mode 100644 index 000000000..9c52509ad --- /dev/null +++ b/nativescript-core/ui/frame/frame-interfaces.ts @@ -0,0 +1,107 @@ +// Types +import { View } from "../core/view"; +import { Page } from "../page"; +import { Transition } from "../transition"; +import { Observable } from "../../data/observable"; + +export enum NavigationType { + back, + forward, + replace +} + +export interface TransitionState { + enterTransitionListener: any; + exitTransitionListener: any; + reenterTransitionListener: any; + returnTransitionListener: any; + transitionName: string; + entry: BackstackEntry; +} + +export interface ViewEntry { + moduleName?: string; + create?: () => View; +} + +export interface NavigationEntry extends ViewEntry { + context?: any; + bindingContext?: any; + animated?: boolean; + transition?: NavigationTransition; + transitioniOS?: NavigationTransition; + transitionAndroid?: NavigationTransition; + backstackVisible?: boolean; + clearHistory?: boolean; +} + +export interface NavigationContext { + entry: BackstackEntry; + // TODO: remove isBackNavigation for NativeScript 7.0 + isBackNavigation: boolean; + navigationType: NavigationType; +} + +export interface NavigationTransition { + name?: string; + instance?: Transition; + duration?: number; + curve?: any; +} + +export interface BackstackEntry { + entry: NavigationEntry; + resolvedPage: Page; + navDepth: number; + fragmentTag: string; + fragment?: any; + viewSavedState?: any; + frameId?: number; + recreated?: boolean; +} + +export interface AndroidFrame extends Observable { + rootViewGroup: any /* android.view.ViewGroup */; + activity: any /* androidx.appcompat.app.AppCompatActivity */; + currentActivity: any /* androidx.appcompat.app.AppCompatActivity */; + actionBar: any /* android.app.ActionBar */; + showActionBar: boolean; + fragmentForPage(entry: BackstackEntry): any; +} + +export interface AndroidActivityCallbacks { + getRootView(): View; + resetActivityContent(activity: any): void; + + onCreate(activity: any, savedInstanceState: any, intent: any, superFunc: Function): void; + onSaveInstanceState(activity: any, outState: any, superFunc: Function): void; + onStart(activity: any, superFunc: Function): void; + onStop(activity: any, superFunc: Function): void; + onPostResume(activity: any, superFunc: Function): void; + onDestroy(activity: any, superFunc: Function): void; + onBackPressed(activity: any, superFunc: Function): void; + onRequestPermissionsResult(activity: any, requestCode: number, permissions: Array, grantResults: Array, superFunc: Function): void; + onActivityResult(activity: any, requestCode: number, resultCode: number, data: any, superFunc: Function); + onNewIntent(activity: any, intent: any, superSetIntentFunc: Function, superFunc: Function): void; +} + +export interface AndroidFragmentCallbacks { + onHiddenChanged(fragment: any, hidden: boolean, superFunc: Function): void; + onCreateAnimator(fragment: any, transit: number, enter: boolean, nextAnim: number, superFunc: Function): any; + onCreate(fragment: any, savedInstanceState: any, superFunc: Function): void; + onCreateView(fragment: any, inflater: any, container: any, savedInstanceState: any, superFunc: Function): any; + onSaveInstanceState(fragment: any, outState: any, superFunc: Function): void; + onDestroyView(fragment: any, superFunc: Function): void; + onDestroy(fragment: any, superFunc: Function): void; + onPause(fragment: any, superFunc: Function): void; + onStop(fragment: any, superFunc: Function): void; + toStringOverride(fragment: any, superFunc: Function): string; +} + +/* tslint:disable */ +export interface iOSFrame { + /* tslint:enable */ + controller: any /* UINavigationController */; + navBarVisibility: "auto" | "never" | "always"; + _disableNavBarAnimation: boolean; +} diff --git a/nativescript-core/ui/frame/frame.android.ts b/nativescript-core/ui/frame/frame.android.ts index 8bad9a26b..e06248e64 100644 --- a/nativescript-core/ui/frame/frame.android.ts +++ b/nativescript-core/ui/frame/frame.android.ts @@ -3,6 +3,7 @@ import { AndroidFrame as AndroidFrameDefinition, AndroidActivityCallbacks, AndroidFragmentCallbacks, BackstackEntry, NavigationTransition } from "."; +import { TransitionState } from "./frame-common"; import { Page } from "../page"; // Types. @@ -20,21 +21,17 @@ import { // TODO: Remove this and get it from global to decouple builder for angular import { Builder } from "../builder"; -import { CLASS_PREFIX, getRootViewCssClasses, pushToRootViewCssClasses } from "../../css/system-classes"; +import { + CLASS_PREFIX, + getSystemCssClasses, + pushToSystemCssClasses, + ROOT_VIEW_CSS_CLASS +} from "../../css/system-classes"; import { device } from "../../platform/platform"; import { profile } from "../../profiling"; export * from "./frame-common"; -interface TransitionState { - enterTransitionListener: any; - exitTransitionListener: any; - reenterTransitionListener: any; - returnTransitionListener: any; - transitionName: string; - entry: BackstackEntry; -} - const ANDROID_PLATFORM = "android"; const INTENT_EXTRA = "com.tns.activity"; @@ -383,8 +380,10 @@ export class Frame extends FrameBase { return false; } + // HACK: This @profile decorator creates a circular dependency + // HACK: because the function parameter type is evaluated with 'typeof' @profile - public _navigateCore(newEntry: BackstackEntry) { + public _navigateCore(newEntry: any) { // should be (newEntry: BackstackEntry) super._navigateCore(newEntry); // set frameId here so that we could use it in fragment.transitions @@ -1304,12 +1303,11 @@ class ActivityCallbacksImplementation implements AndroidActivityCallbacks { savedInstanceState: android.os.Bundle, fireLaunchEvent: boolean ): void { - const shouldCreateRootFrame = application._shouldCreateRootFrame(); let rootView = this._rootView; if (traceEnabled()) { traceWrite( - `Frame.setActivityContent rootView: ${rootView} shouldCreateRootFrame: ${shouldCreateRootFrame} fireLaunchEvent: ${fireLaunchEvent}`, + `Frame.setActivityContent rootView: ${rootView} shouldCreateRootFrame: false fireLaunchEvent: ${fireLaunchEvent}`, traceCategories.NativeLifecycle ); } @@ -1330,59 +1328,26 @@ class ActivityCallbacksImplementation implements AndroidActivityCallbacks { throw new Error("Main entry is missing. App cannot be started. Verify app bootstrap."); } - if (shouldCreateRootFrame) { - const extras = intent.getExtras(); - let frameId = -1; - - // We have extras when we call - new Frame().navigate(); - // savedInstanceState is used when activity is recreated. - // NOTE: On API 23+ we get extras on first run. - // Check changed - first try to get frameId from Extras if not from saveInstanceState. - if (extras) { - frameId = extras.getInt(INTENT_EXTRA, -1); - } - - if (savedInstanceState && frameId < 0) { - frameId = savedInstanceState.getInt(INTENT_EXTRA, -1); - } - - if (!rootView) { - // If we have frameId from extras - we are starting a new activity from navigation (e.g. new Frame().navigate())) - // Then we check if we have frameId from savedInstanceState - this happens when Activity is destroyed but app was not (e.g. suspend) - rootView = getFrameByNumberId(frameId) || new Frame(); - } - - if (rootView instanceof Frame) { - rootView.navigate(mainEntry); - } else { - throw new Error("A Frame must be used to navigate to a Page."); - } - } else { - rootView = Builder.createViewFromEntry(mainEntry); - } + rootView = Builder.createViewFromEntry(mainEntry); } this._rootView = rootView; activityRootViewsMap.set(rootView._domId, new WeakRef(rootView)); const deviceType = device.deviceType.toLowerCase(); - pushToRootViewCssClasses(`${CLASS_PREFIX}${ANDROID_PLATFORM}`); - pushToRootViewCssClasses(`${CLASS_PREFIX}${deviceType}`); - pushToRootViewCssClasses(`${CLASS_PREFIX}${application.android.orientation}`); - pushToRootViewCssClasses(`${CLASS_PREFIX}${application.android.systemAppearance}`); - const rootViewCssClasses = getRootViewCssClasses(); + pushToSystemCssClasses(`${CLASS_PREFIX}${ANDROID_PLATFORM}`); + pushToSystemCssClasses(`${CLASS_PREFIX}${deviceType}`); + pushToSystemCssClasses(`${CLASS_PREFIX}${application.android.orientation}`); + pushToSystemCssClasses(`${CLASS_PREFIX}${application.android.systemAppearance}`); + + this._rootView.cssClasses.add(ROOT_VIEW_CSS_CLASS); + const rootViewCssClasses = getSystemCssClasses(); rootViewCssClasses.forEach(c => this._rootView.cssClasses.add(c)); } - // Initialize native visual tree; - if (shouldCreateRootFrame) { - // Don't setup as styleScopeHost - rootView._setupUI(activity); - } else { - // setup view as styleScopeHost - rootView._setupAsRootView(activity); - } + // setup view as styleScopeHost + rootView._setupAsRootView(activity); activity.setContentView(rootView.nativeViewProtected, new org.nativescript.widgets.CommonLayoutParams()); } diff --git a/nativescript-core/ui/frame/frame.ios.ts b/nativescript-core/ui/frame/frame.ios.ts index bd3b1e1e1..774c67c00 100644 --- a/nativescript-core/ui/frame/frame.ios.ts +++ b/nativescript-core/ui/frame/frame.ios.ts @@ -1,23 +1,22 @@ -// Definitions. -import { - iOSFrame as iOSFrameDefinition, BackstackEntry, NavigationTransition -} from "."; -import { ios as iosView } from "../core/view"; +//Types +import { iOSFrame as iOSFrameDefinition, BackstackEntry, NavigationTransition } from "."; +import { FrameBase, NavigationType } from "./frame-common"; import { Page } from "../page"; -import { profile } from "../../profiling"; +import { View } from "../core/view"; -//Types. -import { - FrameBase, View, isCategorySet, layout, - NavigationType, traceCategories, traceEnabled, traceWrite -} from "./frame-common"; +// Requires import { _createIOSAnimatedTransitioning } from "./fragment.transitions"; - -import * as utils from "../../utils/utils"; +import { ios as iosViewHelper } from "../core/view/view-helper"; +import { profile } from "../../profiling"; +import { ios as iosUtils, layout } from "../../utils/utils"; +import { + isCategorySet, isEnabled as traceEnabled, + categories as traceCategories, write as traceWrite +} from "../../trace"; export * from "./frame-common"; -const majorVersion = utils.ios.MajorVersion; +const majorVersion = iosUtils.MajorVersion; const ENTRY = "_entry"; const DELEGATE = "_delegate"; @@ -64,8 +63,10 @@ export class Frame extends FrameBase { } } + // !!! THIS PROFILE DECORATOR CREATES A CIRCULAR DEPENDENCY + // !!! BECAUSE THE PARAMETER TYPE IS EVALUATED WITH TYPEOF @profile - public _navigateCore(backstackEntry: BackstackEntry) { + public _navigateCore(backstackEntry: any) { super._navigateCore(backstackEntry); let viewController: UIViewController = backstackEntry.resolvedPage.ios; @@ -543,7 +544,7 @@ class UINavigationControllerImpl extends UINavigationController { if (owner && this.traitCollection.hasDifferentColorAppearanceComparedToTraitCollection && this.traitCollection.hasDifferentColorAppearanceComparedToTraitCollection(previousTraitCollection)) { - owner.notify({ eventName: iosView.traitCollectionColorAppearanceChangedEvent, object: owner }); + owner.notify({ eventName: iosViewHelper.traitCollectionColorAppearanceChangedEvent, object: owner }); } } } diff --git a/nativescript-core/ui/layouts/grid-layout/grid-layout-common.ts b/nativescript-core/ui/layouts/grid-layout/grid-layout-common.ts index 538c0666e..ce1b5f279 100644 --- a/nativescript-core/ui/layouts/grid-layout/grid-layout-common.ts +++ b/nativescript-core/ui/layouts/grid-layout/grid-layout-common.ts @@ -58,7 +58,8 @@ function convertGridLength(value: string): ItemSpec { } function parseAndAddItemSpecs(value: string, func: (itemSpec: ItemSpec) => void): void { - const arr = value.split(/[\s,]+/); + // ensure value is a string since view bindings could be parsed as number/int's here + const arr = `${value}`.split(/[\s,]+/); for (let i = 0, length = arr.length; i < length; i++) { const str = arr[i].trim(); if (str.length > 0) { diff --git a/nativescript-core/ui/proxy-view-container/proxy-view-container.ts b/nativescript-core/ui/proxy-view-container/proxy-view-container.ts index 411d8f8eb..abc20df11 100644 --- a/nativescript-core/ui/proxy-view-container/proxy-view-container.ts +++ b/nativescript-core/ui/proxy-view-container/proxy-view-container.ts @@ -1,5 +1,8 @@ import { ProxyViewContainer as ProxyViewContainerDefinition } from "."; import { LayoutBase, View, traceEnabled, traceWrite, traceCategories, CSSType } from "../layouts/layout-base"; +import { Property } from "../core/properties/properties"; +import { messageType } from "../../trace/trace"; + /** * Proxy view container that adds all its native children directly to the parent. * To be used as a logical grouping container of views. @@ -11,6 +14,7 @@ import { LayoutBase, View, traceEnabled, traceWrite, traceCategories, CSSType } // * Proxy (with children) is removed form the DOM. In _removeViewFromNativeVisualTree recursively when the proxy is removed from its parent. @CSSType("ProxyViewContainer") export class ProxyViewContainer extends LayoutBase implements ProxyViewContainerDefinition { + private proxiedLayoutProperties = new Set(); constructor() { super(); @@ -62,10 +66,19 @@ export class ProxyViewContainer extends LayoutBase implements ProxyViewContainer public _addViewToNativeVisualTree(child: View, atIndex?: number): boolean { if (traceEnabled()) { - traceWrite("ViewContainer._addViewToNativeVisualTree for a child " + child + " ViewContainer.parent: " + this.parent, traceCategories.ViewHierarchy); + traceWrite("ProxyViewContainer._addViewToNativeVisualTree for a child " + child + " ViewContainer.parent: " + this.parent, traceCategories.ViewHierarchy); } super._addViewToNativeVisualTree(child); + layoutProperties.forEach((propName) => { + const proxyPropName = makeProxyPropName(propName); + child[proxyPropName] = child[propName]; + + if (this.proxiedLayoutProperties.has(propName)) { + this._applyLayoutPropertyToChild(child, propName, this[propName]); + } + }); + const parent = this.parent; if (parent instanceof View) { let baseIndex = 0; @@ -147,4 +160,90 @@ export class ProxyViewContainer extends LayoutBase implements ProxyViewContainer }); } } + + /** + * Layout property changed, proxy the new value to the child view(s) + */ + public _changedLayoutProperty(propName: string, value: string) { + const numChildren = this._getNativeViewsCount(); + if (numChildren > 1) { + traceWrite("ProxyViewContainer._changeLayoutProperty - you're setting '" + propName + "' for " + this + " with more than one child. Probably this is not what you want, consider wrapping it in a StackLayout ", traceCategories.ViewHierarchy, messageType.error); + } + + this.eachLayoutChild((child) => { + this._applyLayoutPropertyToChild(child, propName, value); + + return true; + }); + + this.proxiedLayoutProperties.add(propName); + } + + /** + * Apply the layout property to the child view. + */ + private _applyLayoutPropertyToChild(child: View, propName: string, value: any) { + const proxyPropName = makeProxyPropName(propName); + if (proxyPropName in child) { + if (child[propName] !== child[proxyPropName]) { + // Value was set directly on the child view, don't override. + if (traceEnabled()) { + traceWrite("ProxyViewContainer._applyLayoutPropertyToChild child " + child + " has its own value [" + child[propName] + "] for [" + propName + "]", traceCategories.ViewHierarchy); + } + + return; + } + } + + child[propName] = value; + child[proxyPropName] = value; + } +} + +// Layout propeties to be proxyed to the child views +const layoutProperties = [ + // AbsoluteLayout + "left", + "top", + + // DockLayout + "dock", + + // FlexLayout + "flexDirection", + "flexWrap", + "justifyContent", + "alignItems", + "alignContent", + "order", + "flexGrow", + "flexShrink", + "flexWrapBefore", + "alignSelf", + "flexFlow", + "flex", + + // GridLayout + "column", + "columnSpan", + "col", + "colSpan", + "row", + "rowSpan", +]; + +// Override the inherited layout properties +for (const name of layoutProperties) { + const proxyProperty = new Property({ + name, + valueChanged(target, oldValue, value) { + target._changedLayoutProperty(name, value); + } + }); + + proxyProperty.register(ProxyViewContainer); +} + +function makeProxyPropName(propName) { + return `_proxy:${propName}`; } diff --git a/nativescript-core/ui/styling/font-common.ts b/nativescript-core/ui/styling/font-common.ts index 3d910dc90..fca83e612 100644 --- a/nativescript-core/ui/styling/font-common.ts +++ b/nativescript-core/ui/styling/font-common.ts @@ -1,6 +1,9 @@ -import { Font as FontDefinition, ParsedFont } from "./font"; +import { Font as FontDefinition } from "./font"; +import { ParsedFont } from "./font-interfaces"; import { makeValidator, makeParser } from "../core/properties"; +export * from "./font-interfaces"; + export abstract class Font implements FontDefinition { public static default = undefined; diff --git a/nativescript-core/ui/styling/font-interfaces.ts b/nativescript-core/ui/styling/font-interfaces.ts new file mode 100644 index 000000000..fab26d0e3 --- /dev/null +++ b/nativescript-core/ui/styling/font-interfaces.ts @@ -0,0 +1,12 @@ +export type FontStyle = "normal" | "italic"; + +export type FontWeight = "100" | "200" | "300" | "normal" | "400" | "500" | "600" | "bold" | "700" | "800" | "900"; + +export interface ParsedFont { + fontStyle?: FontStyle; + fontVariant?: string; + fontWeight?: FontWeight; + lineHeight?: string; + fontSize?: string; + fontFamily?: string; +} diff --git a/nativescript-core/ui/styling/font.d.ts b/nativescript-core/ui/styling/font.d.ts index c17925d4b..25753d884 100644 --- a/nativescript-core/ui/styling/font.d.ts +++ b/nativescript-core/ui/styling/font.d.ts @@ -49,7 +49,7 @@ export namespace FontWeight { export function parse(value: string): FontWeight; } -interface ParsedFont { +export interface ParsedFont { fontStyle?: FontStyle; fontVariant?: string; fontWeight?: FontWeight, diff --git a/nativescript-core/ui/styling/style-scope.ts b/nativescript-core/ui/styling/style-scope.ts index 6105cff89..ed585227d 100644 --- a/nativescript-core/ui/styling/style-scope.ts +++ b/nativescript-core/ui/styling/style-scope.ts @@ -28,7 +28,7 @@ import { messageType as traceMessageType, } from "../../trace"; import { File, knownFolders, path } from "../../file-system"; -import * as applicationCommon from "../../application/application-common"; +import * as application from "../../application"; import { profile } from "../../profiling"; import * as kam from "../animation/keyframe-animation"; @@ -330,7 +330,7 @@ export function addTaggedAdditionalCSS(cssText: string, tag?: string | Number): return changed; } -const onCssChanged = profile("\"style-scope\".onCssChanged", (args: applicationCommon.CssChangedEventData) => { +const onCssChanged = profile("\"style-scope\".onCssChanged", (args: application.CssChangedEventData) => { if (args.cssText) { const parsed = CSSSource.fromSource(args.cssText, applicationKeyframes, args.cssFile).selectors; if (parsed) { @@ -342,8 +342,8 @@ const onCssChanged = profile("\"style-scope\".onCssChanged", (args: applicationC } }); -function onLiveSync(args: applicationCommon.CssChangedEventData): void { - loadCss(applicationCommon.getCssFileName()); +function onLiveSync(args: application.CssChangedEventData): void { + loadCss(application.getCssFileName()); } const loadCss = profile(`"style-scope".loadCss`, (cssModule: string) => { @@ -363,23 +363,23 @@ const loadCss = profile(`"style-scope".loadCss`, (cssModule: string) => { } }); -applicationCommon.on("cssChanged", onCssChanged); -applicationCommon.on("livesync", onLiveSync); +application.on("cssChanged", onCssChanged); +application.on("livesync", onLiveSync); // Call to this method is injected in the application in: // - no-snapshot - code injected in app.ts by [bundle-config-loader](https://github.com/NativeScript/nativescript-dev-webpack/blob/9b1e34d8ef838006c9b575285c42d2304f5f02b5/bundle-config-loader.ts#L85-L92) // - with-snapshot - code injected in snapshot bundle by [NativeScriptSnapshotPlugin](https://github.com/NativeScript/nativescript-dev-webpack/blob/48b26f412fd70c19dc0b9c7763e08e9505a0ae11/plugins/NativeScriptSnapshotPlugin/index.js#L48-L56) // Having the app.css loaded in snapshot provides significant boost in startup (when using the ns-theme ~150 ms). However, because app.css is resolved at build-time, // when the snapshot is created - there is no way to use file qualifiers or change the name of on app.css -export const loadAppCSS = profile("\"style-scope\".loadAppCSS", (args: applicationCommon.LoadAppCSSEventData) => { +export const loadAppCSS = profile("\"style-scope\".loadAppCSS", (args: application.LoadAppCSSEventData) => { loadCss(args.cssFile); - applicationCommon.off("loadAppCss", loadAppCSS); + application.off("loadAppCss", loadAppCSS); }); -if (applicationCommon.hasLaunched()) { - loadAppCSS({ eventName: "loadAppCss", object: applicationCommon, cssFile: applicationCommon.getCssFileName() }); +if (application.hasLaunched()) { + loadAppCSS({ eventName: "loadAppCss", object: application, cssFile: application.getCssFileName() }); } else { - applicationCommon.on("loadAppCss", loadAppCSS); + application.on("loadAppCss", loadAppCSS); } export class CssState { @@ -790,8 +790,10 @@ export class StyleScope { } } + // HACK: This @profile decorator creates a circular dependency + // HACK: because the function parameter type is evaluated with 'typeof' @profile - public matchSelectors(view: ViewBase): SelectorsMatch { + public matchSelectors(view: any): SelectorsMatch { // should be (view: ViewBase): SelectorsMatch this.ensureSelectors(); return this._selectors.query(view); diff --git a/nativescript-core/ui/text-base/text-base-common.ts b/nativescript-core/ui/text-base/text-base-common.ts index 3115711e7..a5da6f3c3 100644 --- a/nativescript-core/ui/text-base/text-base-common.ts +++ b/nativescript-core/ui/text-base/text-base-common.ts @@ -1,13 +1,19 @@ -// Definitions. -import { TextBase as TextBaseDefinition, TextAlignment, TextDecoration, TextTransform, WhiteSpace } from "."; -import { FontStyle, FontWeight } from "../styling/font"; -import { PropertyChangeData } from "../../data/observable"; +// Types +import { TextBase as TextBaseDefinition } from "."; +import { TextAlignment, TextDecoration, TextTransform, WhiteSpace } from "./text-base-interfaces"; +import { Length, ViewBase } from "../core/view"; +import { FontStyle, FontWeight } from "../styling/font-interfaces"; +import { PropertyChangeData } from "../../data/observable/observable-interfaces"; -// Types. -import { View, ViewBase, Property, CssProperty, InheritedCssProperty, Style, isAndroid, isIOS, Observable, makeValidator, makeParser, Length } from "../core/view"; +// Requires. import { FormattedString, Span } from "./formatted-string"; +import { + View, Property, CssProperty, InheritedCssProperty, Style, isAndroid, isIOS, Observable, + makeValidator, makeParser +} from "../core/view"; export { FormattedString, Span }; +export * from "./text-base-interfaces"; export * from "../core/view"; const CHILD_SPAN = "Span"; diff --git a/nativescript-core/ui/text-base/text-base-interfaces.ts b/nativescript-core/ui/text-base/text-base-interfaces.ts new file mode 100644 index 000000000..2115210ec --- /dev/null +++ b/nativescript-core/ui/text-base/text-base-interfaces.ts @@ -0,0 +1,11 @@ +// Types +import { TextBase } from "./text-base"; + +export interface TextTransformation { + new(owner: TextBase): any /* android.text.method.TransformationMethod */; +} + +export type WhiteSpace = "initial" | "normal" | "nowrap"; +export type TextAlignment = "initial" | "left" | "center" | "right"; +export type TextTransform = "initial" | "none" | "capitalize" | "uppercase" | "lowercase"; +export type TextDecoration = "none" | "underline" | "line-through" | "underline line-through"; diff --git a/nativescript-core/ui/text-base/text-base.android.ts b/nativescript-core/ui/text-base/text-base.android.ts index 258a1ab4a..d7cf1bfa7 100644 --- a/nativescript-core/ui/text-base/text-base.android.ts +++ b/nativescript-core/ui/text-base/text-base.android.ts @@ -1,4 +1,7 @@ -import { TextDecoration, TextAlignment, TextTransform, WhiteSpace } from "./text-base"; +// Types +import { TextTransformation, TextDecoration, TextAlignment, TextTransform, WhiteSpace } from "./text-base-common"; + +// Requires import { Font } from "../styling/font"; import { backgroundColorProperty } from "../styling/style-properties"; import { @@ -11,10 +14,6 @@ import { isString } from "../../utils/types"; export * from "./text-base-common"; -interface TextTransformation { - new(owner: TextBase): android.text.method.TransformationMethod; -} - let TextTransformation: TextTransformation; function initializeTextTransformation(): void { diff --git a/nativescript-core/ui/text-base/text-base.ios.ts b/nativescript-core/ui/text-base/text-base.ios.ts index 12cb12e30..e47cf139b 100644 --- a/nativescript-core/ui/text-base/text-base.ios.ts +++ b/nativescript-core/ui/text-base/text-base.ios.ts @@ -1,16 +1,18 @@ -import { TextDecoration, TextAlignment, TextTransform } from "./text-base"; +// Types +import { TextDecoration, TextAlignment, TextTransform } from "./text-base-common"; + +// Requires import { Font } from "../styling/font"; import { TextBaseCommon, textProperty, formattedTextProperty, textAlignmentProperty, textDecorationProperty, textTransformProperty, letterSpacingProperty, colorProperty, fontInternalProperty, lineHeightProperty, FormattedString, Span, Color, isBold, resetSymbol } from "./text-base-common"; - -export * from "./text-base-common"; - import { isString } from "../../utils/types"; import { ios } from "../../utils/utils"; +export * from "./text-base-common"; + const majorVersion = ios.MajorVersion; export class TextBase extends TextBaseCommon { diff --git a/nativescript-core/ui/web-view/web-view-common.ts b/nativescript-core/ui/web-view/web-view-common.ts index 36acc9da0..bced3983b 100644 --- a/nativescript-core/ui/web-view/web-view-common.ts +++ b/nativescript-core/ui/web-view/web-view-common.ts @@ -1,8 +1,10 @@ -import { WebView as WebViewDefinition, LoadEventData, NavigationType } from "."; +import { WebView as WebViewDefinition } from "."; +import { LoadEventData, NavigationType } from "./web-view-interfaces"; import { ContainerView, Property, EventData, CSSType } from "../core/view"; import { File, knownFolders, path } from "../../file-system"; -export { File, knownFolders, path, NavigationType }; +export { File, knownFolders, path }; +export * from "./web-view-interfaces"; export * from "../core/view"; export const srcProperty = new Property({ name: "src" }); @@ -87,6 +89,10 @@ export abstract class WebViewBase extends ContainerView implements WebViewDefini } } +// HACK: Use an interface with the same name, so that the class above fulfills the 'implements' requirement +// HACK: We use the 'implements' to verify the class above is the same as the one declared in the d.ts +// HACK: We declare all these 'on' statements, so that they can appear in the API reference +// HACK: Do we need this? Is it useful? There are static fields to the WebViewBase class for the event names. export interface WebViewBase { on(eventNames: string, callback: (data: EventData) => void, thisArg?: any); on(event: "loadFinished", callback: (args: LoadEventData) => void, thisArg?: any); diff --git a/nativescript-core/ui/web-view/web-view-interfaces.ts b/nativescript-core/ui/web-view/web-view-interfaces.ts new file mode 100644 index 000000000..a70343458 --- /dev/null +++ b/nativescript-core/ui/web-view/web-view-interfaces.ts @@ -0,0 +1,14 @@ +import { WebView } from "."; +import { EventData } from "../core/view"; + +export type NavigationType = "linkClicked" | "formSubmitted" | "backForward" | "reload" | "formResubmitted" | "other" | undefined; + +export interface LoadEventData extends EventData { + url: string; + navigationType: NavigationType; + error: string; +} + +export interface WebViewClient { + new(owner: WebView): any /* android.webkit.WebViewClient */; +} diff --git a/nativescript-core/ui/web-view/web-view.android.ts b/nativescript-core/ui/web-view/web-view.android.ts index 497d0e14f..3b3aacdec 100644 --- a/nativescript-core/ui/web-view/web-view.android.ts +++ b/nativescript-core/ui/web-view/web-view.android.ts @@ -1,11 +1,7 @@ -import { WebViewBase, knownFolders, traceEnabled, traceWrite, traceCategories } from "./web-view-common"; +import { WebViewBase, knownFolders, traceEnabled, traceWrite, traceCategories, WebViewClient } from "./web-view-common"; export * from "./web-view-common"; -interface WebViewClient { - new(owner: WebView): android.webkit.WebViewClient; -} - let WebViewClient: WebViewClient; function initializeWebViewClient(): void { diff --git a/nativescript-core/ui/web-view/web-view.ios.ts b/nativescript-core/ui/web-view/web-view.ios.ts index 71b39ab58..534cd7497 100644 --- a/nativescript-core/ui/web-view/web-view.ios.ts +++ b/nativescript-core/ui/web-view/web-view.ios.ts @@ -1,4 +1,5 @@ -import { WebViewBase, knownFolders, traceWrite, traceEnabled, traceCategories, NavigationType } from "./web-view-common"; +import { NavigationType } from "."; +import { WebViewBase, knownFolders, traceWrite, traceEnabled, traceCategories } from "./web-view-common"; import { profile } from "../../profiling"; export * from "./web-view-common"; diff --git a/nativescript-core/utils/layout-helper/layout-helper-common.ts b/nativescript-core/utils/layout-helper/layout-helper-common.ts new file mode 100644 index 000000000..eea75d82c --- /dev/null +++ b/nativescript-core/utils/layout-helper/layout-helper-common.ts @@ -0,0 +1,63 @@ +// cache the MeasureSpec constants here, to prevent extensive marshaling calls to and from Java +// TODO: While this boosts the performance it is error-prone in case Google changes these constants +export const MODE_SHIFT = 30; +export 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 getMode(mode: number): string { + switch (mode) { + case EXACTLY: + return "Exact"; + case AT_MOST: + return "AtMost"; + default: + return "Unspecified"; + } +} + +export function getMeasureSpecMode(spec: number): number { + return (spec & MODE_MASK); +} + +export function getMeasureSpecSize(spec: number): number { + return (spec & ~MODE_MASK); +} + +export function measureSpecToString(measureSpec: number): string { + const mode = getMeasureSpecMode(measureSpec); + const 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 "; + } + + text += size; + + return text; +} + +export function round(value: number): number { + const res = Math.floor(value + 0.5); + if (res !== 0) { + return res; + } else if (value === 0) { + return 0; + } else if (value > 0) { + return 1; + } + + return -1; +} diff --git a/nativescript-core/utils/layout-helper/layout-helper.android.ts b/nativescript-core/utils/layout-helper/layout-helper.android.ts new file mode 100644 index 000000000..c5135e94b --- /dev/null +++ b/nativescript-core/utils/layout-helper/layout-helper.android.ts @@ -0,0 +1,49 @@ +import { MODE_MASK } from "./layout-helper-common"; +import { ad } from "../native-helper"; + +export * from "./layout-helper-common"; + +let density: number; + +let sdkVersion: number; +let useOldMeasureSpec = false; + +export function makeMeasureSpec(size: number, mode: number): number { + if (sdkVersion === undefined) { + // check whether the old layout is needed + sdkVersion = ad.getApplicationContext().getApplicationInfo().targetSdkVersion; + useOldMeasureSpec = sdkVersion <= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1; + } + + if (useOldMeasureSpec) { + return size + mode; + } + + return (size & ~MODE_MASK) | (mode & MODE_MASK); +} + +export function getDisplayDensity(): number { + if (density === undefined) { + density = ad.getResources().getDisplayMetrics().density; + } + + return density; +} + +export function toDevicePixels(value: number): number { + return value * getDisplayDensity(); +} + +export function toDeviceIndependentPixels(value: number): number { + return value / getDisplayDensity(); +} + +export function measureNativeView(nativeView: any /* android.view.View */, width: number, widthMode: number, height: number, heightMode: number): { width: number, height: number } { + const view = nativeView; + view.measure(makeMeasureSpec(width, widthMode), makeMeasureSpec(height, heightMode)); + + return { + width: view.getMeasuredWidth(), + height: view.getMeasuredHeight() + }; +} diff --git a/nativescript-core/utils/layout-helper/layout-helper.d.ts b/nativescript-core/utils/layout-helper/layout-helper.d.ts new file mode 100644 index 000000000..5ff316060 --- /dev/null +++ b/nativescript-core/utils/layout-helper/layout-helper.d.ts @@ -0,0 +1,77 @@ +import { dip, px } from "../../ui/core/view"; + +/** + * Bits that provide the actual measured size. + */ +export const MEASURED_HEIGHT_STATE_SHIFT: number; +export const MEASURED_SIZE_MASK: number; +export const MEASURED_STATE_MASK: number; +export const MEASURED_STATE_TOO_SMALL: number; +export const UNSPECIFIED: number; +export const EXACTLY: number; +export const AT_MOST: number; + +/** + * Gets layout mode from a given specification as string. + * @param mode - The measure specification mode. + */ +export function getMode(mode: number): string; + +/** + * Gets measure specification mode from a given specification. + * @param spec - The measure specification. + */ +export function getMeasureSpecMode(spec: number): number; + +/** + * Gets measure specification size from a given specification. + * @param spec - The measure specification. + */ +export function getMeasureSpecSize(spec: number): number; + +/** + * Creates measure specification size from size and mode. + * @param size - The size component of measure specification. + * @param mode - The mode component of measure specification. + */ +export function makeMeasureSpec(px: number, mode: number): number; + +/** + * Gets display density for the current device. + */ +export function getDisplayDensity(): number; + +/** + * Convert device independent pixels to device pixels - dip to px. + * @param value - The pixel to convert. + */ +export function toDevicePixels(value: dip): px; + +/** + * Convert device pixels to device independent pixels - px to dip. + * @param value - The pixel to convert. + */ +export function toDeviceIndependentPixels(value: px): dip; + +/** + * Rounds value used in layout. + * @param px to round. + */ +export function round(px: px): px; + +/** + * Converts device pixels to device independent pixes and measure the nativeView. + * Returns the desired size of the nativeView in device pixels. + * @param nativeView the nativeView to measure (UIView or android.view.View) + * @param width the available width + * @param widthMode width mode - UNSPECIFIED, EXACTLY or AT_MOST + * @param height the available hegiht + * @param heightMode height mode - UNSPECIFIED, EXACTLY or AT_MOST + */ +export function measureNativeView(nativeView: any /* UIView or android.view.View */, width: number, widthMode: number, height: number, heightMode: number): { width: number, height: number }; + +/** + * Prints user friendly version of the measureSpec. + * @param measureSpec the spec to print + */ +export function measureSpecToString(measureSpec: number): string; diff --git a/nativescript-core/utils/layout-helper/layout-helper.ios.ts b/nativescript-core/utils/layout-helper/layout-helper.ios.ts new file mode 100644 index 000000000..d3450bbfb --- /dev/null +++ b/nativescript-core/utils/layout-helper/layout-helper.ios.ts @@ -0,0 +1,36 @@ +import { round, MODE_MASK } from "./layout-helper-common"; + +export * from "./layout-helper-common"; + +let mainScreenScale; + +export function makeMeasureSpec(size: number, mode: number): number { + return (Math.round(Math.max(0, size)) & ~MODE_MASK) | (mode & MODE_MASK); +} + +export function getDisplayDensity(): number { + return mainScreenScale; +} + +export function toDevicePixels(value: number): number { + return value * mainScreenScale; +} + +export function toDeviceIndependentPixels(value: number): number { + return value / mainScreenScale; +} + +export function measureNativeView(nativeView: any /* UIView */, width: number, widthMode: number, height: number, heightMode: number): { width: number, height: number } { + const view = nativeView; + const nativeSize = view.sizeThatFits({ + width: widthMode === 0 /* layout.UNSPECIFIED */ ? Number.POSITIVE_INFINITY : toDeviceIndependentPixels(width), + height: heightMode === 0 /* layout.UNSPECIFIED */ ? Number.POSITIVE_INFINITY : toDeviceIndependentPixels(height) + }); + + nativeSize.width = round(toDevicePixels(nativeSize.width)); + nativeSize.height = round(toDevicePixels(nativeSize.height)); + + return nativeSize; +} + +mainScreenScale = UIScreen.mainScreen.scale; diff --git a/nativescript-core/utils/layout-helper/package.json b/nativescript-core/utils/layout-helper/package.json new file mode 100644 index 000000000..4d020b6a2 --- /dev/null +++ b/nativescript-core/utils/layout-helper/package.json @@ -0,0 +1,6 @@ +{ + "name": "layout-helper", + "main": "layout-helper", + "types": "layout-helper.d.ts", + "nativescript": {} +} \ No newline at end of file diff --git a/nativescript-core/utils/native-helper.android.ts b/nativescript-core/utils/native-helper.android.ts new file mode 100644 index 000000000..6d848179c --- /dev/null +++ b/nativescript-core/utils/native-helper.android.ts @@ -0,0 +1,156 @@ +import { getNativeApplication, android as androidApp } from "../application"; +import { + messageType as traceMessageType, + categories as traceCategories, + write as traceWrite +} from "../trace"; + +// We are using "ad" here to avoid namespace collision with the global android object +export module ad { + + let application: android.app.Application; + let applicationContext: android.content.Context; + let contextResources: android.content.res.Resources; + let packageName: string; + export function getApplicationContext() { + if (!applicationContext) { + applicationContext = getApplication().getApplicationContext(); + } + + return applicationContext; + } + export function getApplication() { + if (!application) { + application = (getNativeApplication()); + } + + return application; + } + export function getResources() { + if (!contextResources) { + contextResources = getApplication().getResources(); + } + + return contextResources; + } + function getPackageName() { + if (!packageName) { + packageName = getApplicationContext().getPackageName(); + } + + return packageName; + } + + let inputMethodManager: android.view.inputmethod.InputMethodManager; + export function getInputMethodManager(): android.view.inputmethod.InputMethodManager { + if (!inputMethodManager) { + inputMethodManager = getApplicationContext().getSystemService(android.content.Context.INPUT_METHOD_SERVICE); + } + + return inputMethodManager; + } + + export function showSoftInput(nativeView: android.view.View): void { + const inputManager = getInputMethodManager(); + if (inputManager && nativeView instanceof android.view.View) { + inputManager.showSoftInput(nativeView, android.view.inputmethod.InputMethodManager.SHOW_IMPLICIT); + } + } + + export function dismissSoftInput(nativeView?: android.view.View): void { + const inputManager = getInputMethodManager(); + let windowToken: android.os.IBinder; + + if (nativeView instanceof android.view.View) { + windowToken = nativeView.getWindowToken(); + } else if (androidApp.foregroundActivity instanceof androidx.appcompat.app.AppCompatActivity) { + const decorView = androidApp.foregroundActivity.getWindow().getDecorView(); + windowToken = decorView ? decorView.getWindowToken() : null; + } + + if (inputManager && windowToken) { + inputManager.hideSoftInputFromWindow(windowToken, 0); + } + } + + export module collections { + export function stringArrayToStringSet(str: string[]): java.util.HashSet { + const hashSet = new java.util.HashSet(); + if (str !== undefined) { + for (let element in str) { + hashSet.add("" + str[element]); + } + } + + return hashSet; + } + + export function stringSetToStringArray(stringSet: any): string[] { + const arr = []; + if (stringSet !== undefined) { + const it = stringSet.iterator(); + while (it.hasNext()) { + const element = "" + it.next(); + arr.push(element); + } + } + + return arr; + } + } + + export module resources { + let attr; + const attrCache = new Map(); + + export function getDrawableId(name) { + return getId(":drawable/" + name); + } + + export function getStringId(name) { + return getId(":string/" + name); + } + + export function getId(name: string): number { + const resources = getResources(); + const packageName = getPackageName(); + const uri = packageName + name; + + return resources.getIdentifier(uri, null, null); + } + export function getPalleteColor(name: string, context: android.content.Context): number { + return getPaletteColor(name, context); + } + export function getPaletteColor(name: string, context: android.content.Context): number { + if (attrCache.has(name)) { + return attrCache.get(name); + } + + let result = 0; + try { + if (!attr) { + attr = java.lang.Class.forName("androidx.appcompat.R$attr"); + } + + let colorID = 0; + let field = attr.getField(name); + if (field) { + colorID = field.getInt(null); + } + + if (colorID) { + let typedValue = new android.util.TypedValue(); + context.getTheme().resolveAttribute(colorID, typedValue, true); + result = typedValue.data; + } + } + catch (ex) { + traceWrite("Cannot get pallete color: " + name, traceCategories.Error, traceMessageType.error); + } + + attrCache.set(name, result); + + return result; + } + } +} \ No newline at end of file diff --git a/nativescript-core/utils/native-helper.d.ts b/nativescript-core/utils/native-helper.d.ts new file mode 100644 index 000000000..3dc4ff457 --- /dev/null +++ b/nativescript-core/utils/native-helper.d.ts @@ -0,0 +1,159 @@ +/** + * Module with android specific utilities. + */ +export module ad { + /** + * Gets the native Android application instance. + */ + export function getApplication(): any /* android.app.Application */; + + /** + * Gets the native Android application resources. + */ + export function getResources(): any /* android.content.res.Resources */; + + /** + * Gets the Android application context. + */ + export function getApplicationContext(): any /* android.content.Context */; + + /** + * Gets the native Android input method manager. + */ + export function getInputMethodManager(): any /* android.view.inputmethod.InputMethodManager */; + + /** + * Hides the soft input method, usually a soft keyboard. + */ + export function dismissSoftInput(nativeView?: any /* android.view.View */): void; + + /** + * Shows the soft input method, usually a soft keyboard. + */ + export function showSoftInput(nativeView: any /* android.view.View */): void; + + /** + * Utility module dealing with some android collections. + */ + module collections { + /** + * Converts string array into a String [hash set](http://developer.android.com/reference/java/util/HashSet.html). + * @param str - An array of strings to convert. + */ + export function stringArrayToStringSet(str: string[]): any; + + /** + * Converts string hash set into array of strings. + * @param stringSet - A string hash set to convert. + */ + export function stringSetToStringArray(stringSet: any): string[]; + } + + /** + * Utility module related to android resources. + */ + export module resources { + /** + * Gets the drawable id from a given name. + * @param name - Name of the resource. + */ + export function getDrawableId(name); + + /** + * Gets the string id from a given name. + * @param name - Name of the resource. + */ + export function getStringId(name) + + /** + * Gets the id from a given name. + * @param name - Name of the resource. + */ + export function getId(name: string): number; + + /** + * [Obsolete - please use getPaletteColor] Gets a color from current theme. + * @param name - Name of the color + */ + export function getPalleteColor(); + + /** + * Gets a color from the current theme. + * @param name - Name of the color resource. + */ + export function getPaletteColor(name: string, context: any /* android.content.Context */): number; + } +} +/** + * Module with ios specific utilities. + */ +export module ios { + + // Common properties between UILabel, UITextView and UITextField + export interface TextUIView { + font: any; + textAlignment: number; + textColor: any; + text: string; + attributedText: any; + lineBreakMode: number; + numberOfLines: number; + } + + /** + * Utility module dealing with some iOS collections. + */ + module collections { + /** + * Converts JavaScript array to [NSArray](https://developer.apple.com/library/ios/documentation/Cocoa/Reference/Foundation/Classes/NSArray_Class/). + * @param str - JavaScript string array to convert. + */ + export function jsArrayToNSArray(str: string[]): any; + + /** + * Converts NSArray to JavaScript array. + * @param a - NSArray to convert. + */ + export function nsArrayToJSArray(a: any): string[]; + } + + /** + * @deprecated use application.orientation instead + * + * Gets an information about if current mode is Landscape. + */ + export function isLandscape(): boolean; + + /** + * Gets the iOS device major version (for 8.1 will return 8). + */ + export const MajorVersion: number; + + /** + * Opens file with associated application. + * @param filePath The file path. + */ + export function openFile(filePath: string): boolean; + + /** + * Joins an array of file paths. + * @param paths An array of paths. + * Returns the joined path. + */ + export function joinPaths(...paths: string[]): string; + + /** + * Gets the root folder for the current application. This Folder is private for the application and not accessible from Users/External apps. + * iOS - this folder is read-only and contains the app and all its resources. + */ + export function getCurrentAppPath(): string; + + /** + * Gets the currently visible(topmost) UIViewController. + * @param rootViewController The root UIViewController instance to start searching from (normally window.rootViewController). + * Returns the visible UIViewController. + */ + export function getVisibleViewController(rootViewController: any/* UIViewController*/): any/* UIViewController*/; + + export class UIDocumentInteractionControllerDelegateImpl {} +} diff --git a/nativescript-core/utils/native-helper.ios.ts b/nativescript-core/utils/native-helper.ios.ts new file mode 100644 index 000000000..219f7c3db --- /dev/null +++ b/nativescript-core/utils/native-helper.ios.ts @@ -0,0 +1,138 @@ +import { + messageType as traceMessageType, + categories as traceCategories, + write as traceWrite +} from "../trace"; + +function isOrientationLandscape(orientation: number) { + return orientation === UIDeviceOrientation.LandscapeLeft /* 3 */ || + orientation === UIDeviceOrientation.LandscapeRight /* 4 */; +} + +function openFileAtRootModule(filePath: string): boolean { + try { + const appPath = ios.getCurrentAppPath(); + const path = filePath.replace("~", appPath); + + const controller = UIDocumentInteractionController.interactionControllerWithURL(NSURL.fileURLWithPath(path)); + controller.delegate = new ios.UIDocumentInteractionControllerDelegateImpl(); + + return controller.presentPreviewAnimated(true); + } + catch (e) { + traceWrite("Error in openFile", traceCategories.Error, traceMessageType.error); + } + + return false; +} + +export module ios { + // TODO: remove for NativeScript 7.0 + export function getter(_this: any, property: T | { (): T }): T { + console.log("utils.ios.getter() is deprecated; use the respective native property instead"); + if (typeof property === "function") { + return (<{ (): T }>property).call(_this); + } else { + return property; + } + } + + export module collections { + export function jsArrayToNSArray(str: string[]): NSArray { + return NSArray.arrayWithArray(str); + } + + export function nsArrayToJSArray(a: NSArray): Array { + const arr = []; + if (a !== undefined) { + let count = a.count; + for (let i = 0; i < count; i++) { + arr.push(a.objectAtIndex(i)); + } + } + + return arr; + } + } + + export function isLandscape(): boolean { + console.log("utils.ios.isLandscape() is deprecated; use application.orientation instead"); + + const deviceOrientation = UIDevice.currentDevice.orientation; + const statusBarOrientation = UIApplication.sharedApplication.statusBarOrientation; + + const isDeviceOrientationLandscape = isOrientationLandscape(deviceOrientation); + const isStatusBarOrientationLandscape = isOrientationLandscape(statusBarOrientation); + + return isDeviceOrientationLandscape || isStatusBarOrientationLandscape; + } + + export const MajorVersion = NSString.stringWithString(UIDevice.currentDevice.systemVersion).intValue; + + export function openFile(filePath: string): boolean { + console.log("utils.ios.openFile() is deprecated; use utils.openFile() instead"); + + return openFileAtRootModule(filePath); + } + + export function getCurrentAppPath(): string { + const currentDir = __dirname; + const tnsModulesIndex = currentDir.indexOf("/tns_modules"); + + // Module not hosted in ~/tns_modules when bundled. Use current dir. + let appPath = currentDir; + if (tnsModulesIndex !== -1) { + // Strip part after tns_modules to obtain app root + appPath = currentDir.substring(0, tnsModulesIndex); + } + + return appPath; + } + + export function joinPaths(...paths: string[]): string { + if (!paths || paths.length === 0) { + return ""; + } + + return NSString.stringWithString(NSString.pathWithComponents(paths)).stringByStandardizingPath; + } + + export function getVisibleViewController(rootViewController: UIViewController): UIViewController { + if (rootViewController.presentedViewController) { + return getVisibleViewController(rootViewController.presentedViewController); + } + + if (rootViewController.isKindOfClass(UINavigationController.class())) { + return getVisibleViewController((rootViewController).visibleViewController); + } + + if (rootViewController.isKindOfClass(UITabBarController.class())) { + return getVisibleViewController(rootViewController); + } + + return rootViewController; + + } + + export class UIDocumentInteractionControllerDelegateImpl extends NSObject implements UIDocumentInteractionControllerDelegate { + public static ObjCProtocols = [UIDocumentInteractionControllerDelegate]; + + public getViewController(): UIViewController { + const app = UIApplication.sharedApplication; + + return app.keyWindow.rootViewController; + } + + public documentInteractionControllerViewControllerForPreview(controller: UIDocumentInteractionController) { + return this.getViewController(); + } + + public documentInteractionControllerViewForPreview(controller: UIDocumentInteractionController) { + return this.getViewController().view; + } + + public documentInteractionControllerRectForPreview(controller: UIDocumentInteractionController): CGRect { + return this.getViewController().view.frame; + } + } +} diff --git a/nativescript-core/utils/utils-common.ts b/nativescript-core/utils/utils-common.ts index 8fe05c99f..f785482c4 100644 --- a/nativescript-core/utils/utils-common.ts +++ b/nativescript-core/utils/utils-common.ts @@ -1,7 +1,9 @@ import * as types from "./types"; import { dispatchToMainThread, isMainThread } from "./mainthread-helper"; import { sanitizeModuleName } from "../ui/builder/module-name-sanitizer"; +import * as layout from "./layout-helper"; +export { layout }; export * from "./mainthread-helper"; export const RESOURCE_PREFIX = "res://"; @@ -39,70 +41,6 @@ export function getModuleName(path: string): string { return sanitizeModuleName(moduleName); } -export module layoutCommon { - 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 getMode(mode: number): string { - switch (mode) { - case layoutCommon.EXACTLY: - return "Exact"; - case layoutCommon.AT_MOST: - return "AtMost"; - default: - return "Unspecified"; - } - } - - export function getMeasureSpecMode(spec: number): number { - return (spec & MODE_MASK); - } - - export function getMeasureSpecSize(spec: number): number { - return (spec & ~MODE_MASK); - } - - export function measureSpecToString(measureSpec: number): string { - const mode = getMeasureSpecMode(measureSpec); - const 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 "; - } - - text += size; - - return text; - } - - export function round(value: number): number { - const res = Math.floor(value + 0.5); - if (res !== 0) { - return res; - } else if (value === 0) { - return 0; - } else if (value > 0) { - return 1; - } - - return -1; - } -} - export function isFileOrResourcePath(path: string): boolean { if (!types.isString(path)) { return false; diff --git a/nativescript-core/utils/utils.android.ts b/nativescript-core/utils/utils.android.ts index 58bd26fc4..933b2e942 100644 --- a/nativescript-core/utils/utils.android.ts +++ b/nativescript-core/utils/utils.android.ts @@ -1,224 +1,17 @@ +import { ad } from "./native-helper"; +import { device } from "../platform"; +import { FileSystemAccess } from "../file-system/file-system-access"; import { write as traceWrite, categories as traceCategories, messageType as traceMessageType, } from "../trace"; -import { layoutCommon } from "./utils-common"; - +export { ad }; export * from "./utils-common"; -import { getNativeApplication, android as androidApp } from "../application"; -import { device } from "../platform"; -import { FileSystemAccess } from "../file-system/file-system-access"; - const MIN_URI_SHARE_RESTRICTED_APK_VERSION = 24; -export module layout { - let density: number; - - // cache the MeasureSpec constants here, to prevent extensive marshaling calls to and from Java - // TODO: While this boosts the performance it is error-prone in case Google changes these constants - const MODE_SHIFT = 30; - const MODE_MASK = 0x3 << MODE_SHIFT; - let sdkVersion: number; - let useOldMeasureSpec = false; - - export function makeMeasureSpec(size: number, mode: number): number { - if (sdkVersion === undefined) { - // check whether the old layout is needed - sdkVersion = ad.getApplicationContext().getApplicationInfo().targetSdkVersion; - useOldMeasureSpec = sdkVersion <= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1; - } - - if (useOldMeasureSpec) { - return size + mode; - } - - return (size & ~MODE_MASK) | (mode & MODE_MASK); - } - - export function getDisplayDensity(): number { - if (density === undefined) { - density = ad.getResources().getDisplayMetrics().density; - } - - return density; - } - - export function toDevicePixels(value: number): number { - return value * getDisplayDensity(); - } - - export function toDeviceIndependentPixels(value: number): number { - return value / getDisplayDensity(); - } - - export function measureNativeView(nativeView: any /* android.view.View */, width: number, widthMode: number, height: number, heightMode: number): { width: number, height: number } { - const view = nativeView; - view.measure(makeMeasureSpec(width, widthMode), makeMeasureSpec(height, heightMode)); - - return { - width: view.getMeasuredWidth(), - height: view.getMeasuredHeight() - }; - } -} - -// TODO(webpack-workflow): Export all methods from layoutCommon -// Think of a cleaner way to do that -Object.assign(layout, layoutCommon); - -// We are using "ad" here to avoid namespace collision with the global android object -export module ad { - - let application: android.app.Application; - let applicationContext: android.content.Context; - let contextResources: android.content.res.Resources; - let packageName: string; - export function getApplicationContext() { - if (!applicationContext) { - applicationContext = getApplication().getApplicationContext(); - } - - return applicationContext; - } - export function getApplication() { - if (!application) { - application = (getNativeApplication()); - } - - return application; - } - export function getResources() { - if (!contextResources) { - contextResources = getApplication().getResources(); - } - - return contextResources; - } - function getPackageName() { - if (!packageName) { - packageName = getApplicationContext().getPackageName(); - } - - return packageName; - } - - let inputMethodManager: android.view.inputmethod.InputMethodManager; - export function getInputMethodManager(): android.view.inputmethod.InputMethodManager { - if (!inputMethodManager) { - inputMethodManager = getApplicationContext().getSystemService(android.content.Context.INPUT_METHOD_SERVICE); - } - - return inputMethodManager; - } - - export function showSoftInput(nativeView: android.view.View): void { - const inputManager = getInputMethodManager(); - if (inputManager && nativeView instanceof android.view.View) { - inputManager.showSoftInput(nativeView, android.view.inputmethod.InputMethodManager.SHOW_IMPLICIT); - } - } - - export function dismissSoftInput(nativeView?: android.view.View): void { - const inputManager = getInputMethodManager(); - let windowToken: android.os.IBinder; - - if (nativeView instanceof android.view.View) { - windowToken = nativeView.getWindowToken(); - } else if (androidApp.foregroundActivity instanceof androidx.appcompat.app.AppCompatActivity) { - const decorView = androidApp.foregroundActivity.getWindow().getDecorView(); - windowToken = decorView ? decorView.getWindowToken() : null; - } - - if (inputManager && windowToken) { - inputManager.hideSoftInputFromWindow(windowToken, 0); - } - } - - export module collections { - export function stringArrayToStringSet(str: string[]): java.util.HashSet { - const hashSet = new java.util.HashSet(); - if (str !== undefined) { - for (let element in str) { - hashSet.add("" + str[element]); - } - } - - return hashSet; - } - - export function stringSetToStringArray(stringSet: any): string[] { - const arr = []; - if (stringSet !== undefined) { - const it = stringSet.iterator(); - while (it.hasNext()) { - const element = "" + it.next(); - arr.push(element); - } - } - - return arr; - } - } - - export module resources { - let attr; - const attrCache = new Map(); - - export function getDrawableId(name) { - return getId(":drawable/" + name); - } - - export function getStringId(name) { - return getId(":string/" + name); - } - - export function getId(name: string): number { - const resources = getResources(); - const packageName = getPackageName(); - const uri = packageName + name; - - return resources.getIdentifier(uri, null, null); - } - export function getPalleteColor(name: string, context: android.content.Context): number { - return getPaletteColor(name, context); - } - export function getPaletteColor(name: string, context: android.content.Context): number { - if (attrCache.has(name)) { - return attrCache.get(name); - } - - let result = 0; - try { - if (!attr) { - attr = java.lang.Class.forName("androidx.appcompat.R$attr"); - } - - let colorID = 0; - let field = attr.getField(name); - if (field) { - colorID = field.getInt(null); - } - - if (colorID) { - let typedValue = new android.util.TypedValue(); - context.getTheme().resolveAttribute(colorID, typedValue, true); - result = typedValue.data; - } - } - catch (ex) { - traceWrite("Cannot get pallete color: " + name, traceCategories.Error, traceMessageType.error); - } - - attrCache.set(name, result); - - return result; - } - } -} - export function GC() { gc(); } diff --git a/nativescript-core/utils/utils.ios.ts b/nativescript-core/utils/utils.ios.ts index 5c075eb73..17474fd8d 100644 --- a/nativescript-core/utils/utils.ios.ts +++ b/nativescript-core/utils/utils.ios.ts @@ -1,152 +1,20 @@ +import { ios } from "./native-helper"; import { write as traceWrite, categories as traceCategories, messageType as traceMessageType } from "../trace"; -import { layoutCommon } from "./utils-common"; +export { ios }; export * from "./utils-common"; let mainScreenScale; -function isOrientationLandscape(orientation: number) { - return orientation === UIDeviceOrientation.LandscapeLeft /* 3 */ || - orientation === UIDeviceOrientation.LandscapeRight /* 4 */; -} - -export module layout { - const MODE_SHIFT = 30; - const MODE_MASK = 0x3 << MODE_SHIFT; - - export function makeMeasureSpec(size: number, mode: number): number { - return (Math.round(Math.max(0, size)) & ~MODE_MASK) | (mode & MODE_MASK); - } - - export function getDisplayDensity(): number { - return mainScreenScale; - } - - export function toDevicePixels(value: number): number { - return value * mainScreenScale; - } - - export function toDeviceIndependentPixels(value: number): number { - return value / mainScreenScale; - } - - export function measureNativeView(nativeView: any /* UIView */, width: number, widthMode: number, height: number, heightMode: number): { width: number, height: number } { - const view = nativeView; - const nativeSize = view.sizeThatFits({ - width: widthMode === 0 /* layout.UNSPECIFIED */ ? Number.POSITIVE_INFINITY : toDeviceIndependentPixels(width), - height: heightMode === 0 /* layout.UNSPECIFIED */ ? Number.POSITIVE_INFINITY : toDeviceIndependentPixels(height) - }); - - nativeSize.width = layoutCommon.round(toDevicePixels(nativeSize.width)); - nativeSize.height = layoutCommon.round(toDevicePixels(nativeSize.height)); - - return nativeSize; - } -} - -// TODO(webpack-workflow): Export all methods from layoutCommon -// Think of a cleaner way to do that -Object.assign(layout, layoutCommon); - -export module ios { - // TODO: remove for NativeScript 7.0 - export function getter(_this: any, property: T | { (): T }): T { - console.log("utils.ios.getter() is deprecated; use the respective native property instead"); - if (typeof property === "function") { - return (<{ (): T }>property).call(_this); - } else { - return property; - } - } - - export module collections { - export function jsArrayToNSArray(str: string[]): NSArray { - return NSArray.arrayWithArray(str); - } - - export function nsArrayToJSArray(a: NSArray): Array { - const arr = []; - if (a !== undefined) { - let count = a.count; - for (let i = 0; i < count; i++) { - arr.push(a.objectAtIndex(i)); - } - } - - return arr; - } - } - - export function isLandscape(): boolean { - console.log("utils.ios.isLandscape() is deprecated; use application.orientation instead"); - - const deviceOrientation = UIDevice.currentDevice.orientation; - const statusBarOrientation = UIApplication.sharedApplication.statusBarOrientation; - - const isDeviceOrientationLandscape = isOrientationLandscape(deviceOrientation); - const isStatusBarOrientationLandscape = isOrientationLandscape(statusBarOrientation); - - return isDeviceOrientationLandscape || isStatusBarOrientationLandscape; - } - - export const MajorVersion = NSString.stringWithString(UIDevice.currentDevice.systemVersion).intValue; - - export function openFile(filePath: string): boolean { - console.log("utils.ios.openFile() is deprecated; use utils.openFile() instead"); - - return openFileAtRootModule(filePath); - } - - export function getCurrentAppPath(): string { - const currentDir = __dirname; - const tnsModulesIndex = currentDir.indexOf("/tns_modules"); - - // Module not hosted in ~/tns_modules when bundled. Use current dir. - let appPath = currentDir; - if (tnsModulesIndex !== -1) { - // Strip part after tns_modules to obtain app root - appPath = currentDir.substring(0, tnsModulesIndex); - } - - return appPath; - } - - export function joinPaths(...paths: string[]): string { - if (!paths || paths.length === 0) { - return ""; - } - - return NSString.stringWithString(NSString.pathWithComponents(paths)).stringByStandardizingPath; - } - - export function getVisibleViewController(rootViewController: UIViewController): UIViewController { - if (rootViewController.presentedViewController) { - return getVisibleViewController(rootViewController.presentedViewController); - } - - if (rootViewController.isKindOfClass(UINavigationController.class())) { - return getVisibleViewController((rootViewController).visibleViewController); - } - - if (rootViewController.isKindOfClass(UITabBarController.class())) { - return getVisibleViewController(rootViewController); - } - - return rootViewController; - - } - -} - export function openFile(filePath: string): boolean { try { const appPath = ios.getCurrentAppPath(); const path = filePath.replace("~", appPath); const controller = UIDocumentInteractionController.interactionControllerWithURL(NSURL.fileURLWithPath(path)); - controller.delegate = new UIDocumentInteractionControllerDelegateImpl(); + controller.delegate = new ios.UIDocumentInteractionControllerDelegateImpl(); return controller.presentPreviewAnimated(true); } @@ -157,9 +25,6 @@ export function openFile(filePath: string): boolean { return false; } -// Need this so that we can use this function inside the ios module (avoid name clashing). -const openFileAtRootModule = openFile; - export function GC() { __collect(); } @@ -183,26 +48,4 @@ export function openUrl(location: string): boolean { return false; } -class UIDocumentInteractionControllerDelegateImpl extends NSObject implements UIDocumentInteractionControllerDelegate { - public static ObjCProtocols = [UIDocumentInteractionControllerDelegate]; - - public getViewController(): UIViewController { - const app = UIApplication.sharedApplication; - - return app.keyWindow.rootViewController; - } - - public documentInteractionControllerViewControllerForPreview(controller: UIDocumentInteractionController) { - return this.getViewController(); - } - - public documentInteractionControllerViewForPreview(controller: UIDocumentInteractionController) { - return this.getViewController().view; - } - - public documentInteractionControllerRectForPreview(controller: UIDocumentInteractionController): CGRect { - return this.getViewController().view.frame; - } -} - mainScreenScale = UIScreen.mainScreen.scale; diff --git a/package.json b/package.json index 5c18e302d..e68f0c506 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ }, "license": "Apache-2.0", "devDependencies": { - "@microsoft/api-extractor": "^7.5.0", + "@microsoft/api-extractor": "7.6.1", + "@nativescript/tslint-rules": "0.0.5", "@types/chai": "^4.0.4", "@types/mocha": "^2.2.42", "@types/node": "~10.12.18", @@ -47,6 +48,7 @@ "unit-test-watch": "mocha-typescript-watch -p unit-tests/tsconfig.json --opts unit-tests/mocha.opts", "dev-link-tns-core-modules-widgets": "(cd tns-core-modules-widgets/dist/package && npm link) && (cd nativescript-core && npm i ../tns-core-modules-widgets/dist/package --save)", "api-extractor": "api-extractor run --local --verbose && (cd nativescript-core && cat nativescript-core.header nativescript-core.d.ts > tmp_file && mv tmp_file nativescript-core.d.ts)", + "api-extractor-ci": "api-extractor run --verbose", "typedoc": "npm run api-extractor && typedoc --tsconfig tsconfig.typedoc.json", "typedoc-dev": "npm run typedoc && cd bin/dist/apiref && npx http-server", "build-core": "sh ./build/build-core.sh", diff --git a/tests/app/animation-frame/animation-frame.ts b/tests/app/animation-frame/animation-frame.ts new file mode 100644 index 000000000..a0d6b87c9 --- /dev/null +++ b/tests/app/animation-frame/animation-frame.ts @@ -0,0 +1,85 @@ +import * as TKUnit from "../tk-unit"; +import * as animationFrame from "@nativescript/core/animation-frame"; +import * as fpsNative from "@nativescript/core/fps-meter/fps-native"; + +export function test_requestAnimationFrame_isDefined() { + TKUnit.assertNotEqual(animationFrame.requestAnimationFrame, undefined, "Method animationFrame.requestAnimationFrame() should be defined!"); +} + +export function test_cancelAnimationFrame_isDefined() { + TKUnit.assertNotEqual(animationFrame.cancelAnimationFrame, undefined, "Method animationFrame.cancelAnimationFrame() should be defined!"); +} + +export function test_requestAnimationFrame() { + let completed: boolean; + + const id = animationFrame.requestAnimationFrame(() => { + completed = true; + }); + + TKUnit.waitUntilReady(() => completed, 0.5, false); + animationFrame.cancelAnimationFrame(id); + TKUnit.assert(completed, "Callback should be called!"); +} + +export function test_requestAnimationFrame_callbackCalledInCurrentFrame() { + let completed: boolean; + let currentFrameTime = 0; + const frameCb = new fpsNative.FPSCallback((time) => { + currentFrameTime = time; + }); + frameCb.start(); + + TKUnit.waitUntilReady(() => currentFrameTime > 0, 0.5); + let calledTime = 0; + animationFrame.requestAnimationFrame((frameTime) => { + calledTime = frameTime; + completed = calledTime >= frameTime; + }); + + TKUnit.waitUntilReady(() => completed, 0.5, false); + frameCb.stop(); + TKUnit.assert(completed, "Callback should be called in current frame!"); +} + +export function test_requestAnimationFrame_nextCallbackCalledInNextFrame() { + let completed: boolean; + let currentFrameTime = 0; + const frameCb = new fpsNative.FPSCallback((time) => { + currentFrameTime = time; + }); + frameCb.start(); + + TKUnit.waitUntilReady(() => currentFrameTime > 0, 0.5); + animationFrame.requestAnimationFrame((firstFrameTime) => { + animationFrame.requestAnimationFrame((frameTime) => { + frameCb.stop(); + completed = frameTime > firstFrameTime && frameTime === currentFrameTime; + }); + }); + + TKUnit.waitUntilReady(() => completed, 0.5, false); + frameCb.stop(); + TKUnit.assert(completed, "Callback should be called in next frame!"); +} + +export function test_requestAnimationFrame_shouldBeCancelled() { + let completed: boolean; + let currentFrameTime = 0; + const frameCb = new fpsNative.FPSCallback((time) => { + currentFrameTime = time; + }); + frameCb.start(); + + TKUnit.waitUntilReady(() => currentFrameTime > 0, 0.5); + animationFrame.requestAnimationFrame((firstFrameTime) => { + const cbId = animationFrame.requestAnimationFrame((frameTime) => { + completed = true; + }); + animationFrame.cancelAnimationFrame(cbId); + }); + + TKUnit.wait(1); + frameCb.stop(); + TKUnit.assert(!completed, "Callback should not be called"); +} diff --git a/tests/app/livesync/livesync-tests.ts b/tests/app/livesync/livesync-tests.ts index 0fdafb9b3..f4839ada8 100644 --- a/tests/app/livesync/livesync-tests.ts +++ b/tests/app/livesync/livesync-tests.ts @@ -249,7 +249,7 @@ function _test_onLiveSync_ModalViewClosed(context: ModuleContext) { } function livesync(context: ModuleContext) { - const ls = (global).__hmrSyncBackup || global.__onLiveSync; + const ls = (global).__coreModulesLiveSync || global.__onLiveSync; ls(context); } diff --git a/tests/app/test-runner.ts b/tests/app/test-runner.ts index bc3ca4f35..1e2a966ed 100644 --- a/tests/app/test-runner.ts +++ b/tests/app/test-runner.ts @@ -76,6 +76,9 @@ allTests["OBSERVABLE"] = observableTests; import * as timerTests from "./timer/timer-tests"; allTests["TIMER"] = timerTests; +import * as animationFrameTests from "./animation-frame/animation-frame"; +allTests["ANIMATION-FRAME"] = animationFrameTests; + import * as colorTests from "./color/color-tests"; allTests["COLOR"] = colorTests; diff --git a/tests/app/ui/proxy-view-container/proxy-view-container-tests.ts b/tests/app/ui/proxy-view-container/proxy-view-container-tests.ts index 278519cda..e890abb16 100644 --- a/tests/app/ui/proxy-view-container/proxy-view-container-tests.ts +++ b/tests/app/ui/proxy-view-container/proxy-view-container-tests.ts @@ -43,6 +43,35 @@ export function test_children_immediately_registered_in_parent_grid_layout() { helper.buildUIAndRunTest(outer, testAction); } +export function test_proxy_layout_properties() { + const outer = new GridLayout(); + const proxy = new ProxyViewContainer(); + + function testAction(views: Array) { + outer.addChild(proxy); + + const btn = createBtn("1"); + proxy.addChild(btn); + + proxy.row = 1; + TKUnit.assertEqual(proxy.row, btn.row, "Proxy row value to existing child"); + + const btn2 = createBtn("2"); + proxy.addChild(btn2); + TKUnit.assertEqual(proxy.row, btn2.row, "Proxy row value to new child"); + + proxy.removeChild(btn2); + + btn.row = 0; + TKUnit.assertNotEqual(proxy.row, btn.row, "Child value changed"); + + proxy.row = 1; + TKUnit.assertNotEqual(proxy.row, btn.row, "Changed child value not overridden"); + } + + helper.buildUIAndRunTest(outer, testAction); +} + export function test_children_registered_in_parent_grid_layout_on_attach() { const outer = new GridLayout(); const proxy = new ProxyViewContainer(); diff --git a/tests/app/ui/styling/root-views-css-classes-tests.ts b/tests/app/ui/styling/root-views-css-classes-tests.ts index b7987b892..26f4fff88 100644 --- a/tests/app/ui/styling/root-views-css-classes-tests.ts +++ b/tests/app/ui/styling/root-views-css-classes-tests.ts @@ -35,254 +35,178 @@ const UNKNOWN_ORIENTATION_CSS_CLASS = "ns-unknown"; const DARK_SYSTEM_APPEARANCE_CSS_CLASS = "ns-dark"; const LIGHT_SYSTEM_APPEARANCE_CSS_CLASS = "ns-light"; -function _test_root_view_root_css_class(shouldSetClassName: boolean) { - const rootView = getRootView(); +function _test_root_css_class(view: View, isModal: boolean, shouldSetClassName: boolean) { + if (shouldSetClassName) { + view.className = CLASS_NAME; + } + + const cssClass = isModal ? MODAL_CSS_CLASS : ROOT_CSS_CLASS; + const viewCssClasses = view.cssClasses; + TKUnit.assertTrue(viewCssClasses.has(cssClass), `${cssClass} CSS class is missing`); + + if (shouldSetClassName) { + TKUnit.assertTrue(viewCssClasses.has(CLASS_NAME), `${CLASS_NAME} CSS class is missing`); + } +} + +function _test_platform_css_class(rootView: View, shouldSetClassName: boolean) { if (shouldSetClassName) { rootView.className = CLASS_NAME; } - const rootViewCssClasses = rootView.cssClasses; - TKUnit.assertTrue(rootViewCssClasses.has( - ROOT_CSS_CLASS), - `${ROOT_CSS_CLASS} CSS class is missing` - ); + const cssClasses = rootView.cssClasses; + if (isAndroid) { + TKUnit.assertTrue(cssClasses.has(ANDROID_PLATFORM_CSS_CLASS), `${ANDROID_PLATFORM_CSS_CLASS} CSS class is missing`); + TKUnit.assertFalse(cssClasses.has(IOS_PLATFORM_CSS_CLASS), `${IOS_PLATFORM_CSS_CLASS} CSS class is present`); + } + else { + TKUnit.assertTrue(cssClasses.has(IOS_PLATFORM_CSS_CLASS), `${IOS_PLATFORM_CSS_CLASS} CSS class is missing`); + TKUnit.assertFalse(cssClasses.has(ANDROID_PLATFORM_CSS_CLASS), `${ANDROID_PLATFORM_CSS_CLASS} CSS class is present`); + } if (shouldSetClassName) { - TKUnit.assertTrue(rootViewCssClasses.has( - CLASS_NAME), - `${CLASS_NAME} CSS class is missing` - ); + TKUnit.assertTrue(cssClasses.has(CLASS_NAME), `${CLASS_NAME} CSS class is missing`); } } +function _test_device_type_css_class(rootView: View, shouldSetClassName: boolean) { + if (shouldSetClassName) { + rootView.className = CLASS_NAME; + } + + const cssClasses = rootView.cssClasses; + const deviceType = device.deviceType; + if (deviceType === DeviceType.Phone) { + TKUnit.assertTrue(cssClasses.has(PHONE_DEVICE_TYPE_CSS_CLASS), `${PHONE_DEVICE_TYPE_CSS_CLASS} CSS class is missing`); + TKUnit.assertFalse(cssClasses.has(TABLET_DEVICE_TYPE_CSS_CLASS), `${TABLET_DEVICE_TYPE_CSS_CLASS} CSS class is present`); + } + else { + TKUnit.assertTrue(cssClasses.has(TABLET_DEVICE_TYPE_CSS_CLASS), `${TABLET_DEVICE_TYPE_CSS_CLASS} CSS class is missing`); + TKUnit.assertFalse(cssClasses.has(PHONE_DEVICE_TYPE_CSS_CLASS), `${PHONE_DEVICE_TYPE_CSS_CLASS} CSS class is present`); + } + + if (shouldSetClassName) { + TKUnit.assertTrue(cssClasses.has(CLASS_NAME), `${CLASS_NAME} CSS class is missing`); + } +} + +function _test_orientation_css_class(rootView: View, shouldSetClassName: boolean) { + if (shouldSetClassName) { + rootView.className = CLASS_NAME; + } + + const cssClasses = rootView.cssClasses; + let appOrientation; + if (isAndroid) { + appOrientation = android.orientation; + } + else { + appOrientation = ios.orientation; + } + if (appOrientation === "portrait") { + TKUnit.assertTrue(cssClasses.has(PORTRAIT_ORIENTATION_CSS_CLASS), `${PORTRAIT_ORIENTATION_CSS_CLASS} CSS class is missing`); + TKUnit.assertFalse(cssClasses.has(LANDSCAPE_ORIENTATION_CSS_CLASS), `${LANDSCAPE_ORIENTATION_CSS_CLASS} CSS class is present`); + TKUnit.assertFalse(cssClasses.has(UNKNOWN_ORIENTATION_CSS_CLASS), `${UNKNOWN_ORIENTATION_CSS_CLASS} CSS class is present`); + } + else if (appOrientation === "landscape") { + TKUnit.assertTrue(cssClasses.has(LANDSCAPE_ORIENTATION_CSS_CLASS), `${LANDSCAPE_ORIENTATION_CSS_CLASS} CSS class is missing`); + TKUnit.assertFalse(cssClasses.has(PORTRAIT_ORIENTATION_CSS_CLASS), `${PORTRAIT_ORIENTATION_CSS_CLASS} CSS class is present`); + TKUnit.assertFalse(cssClasses.has(UNKNOWN_ORIENTATION_CSS_CLASS), `${UNKNOWN_ORIENTATION_CSS_CLASS} CSS class is present`); + } + else if (appOrientation === "landscape") { + TKUnit.assertTrue(cssClasses.has(UNKNOWN_ORIENTATION_CSS_CLASS), `${UNKNOWN_ORIENTATION_CSS_CLASS} CSS class is missing`); + TKUnit.assertFalse(cssClasses.has(LANDSCAPE_ORIENTATION_CSS_CLASS), `${LANDSCAPE_ORIENTATION_CSS_CLASS} CSS class is present`); + TKUnit.assertFalse(cssClasses.has(PORTRAIT_ORIENTATION_CSS_CLASS), `${PORTRAIT_ORIENTATION_CSS_CLASS} CSS class is present`); + } + + if (shouldSetClassName) { + TKUnit.assertTrue(cssClasses.has(CLASS_NAME), `${CLASS_NAME} CSS class is missing`); + } +} + +function _test_system_appearance_css_class(rootView: View, shouldSetClassName: boolean) { + if (shouldSetClassName) { + rootView.className = CLASS_NAME; + } + + const cssClasses = rootView.cssClasses; + let systemAppearance; + if (isAndroid) { + systemAppearance = android.systemAppearance; + } + else { + systemAppearance = ios.systemAppearance; + } + if (isIOS && iosUtils.MajorVersion <= 12) { + TKUnit.assertFalse(cssClasses.has(DARK_SYSTEM_APPEARANCE_CSS_CLASS), `${DARK_SYSTEM_APPEARANCE_CSS_CLASS} CSS class is present`); + TKUnit.assertFalse(cssClasses.has(LIGHT_SYSTEM_APPEARANCE_CSS_CLASS), `${LIGHT_SYSTEM_APPEARANCE_CSS_CLASS} CSS class is present`); + } + else if (systemAppearance === "dark") { + TKUnit.assertTrue(cssClasses.has(DARK_SYSTEM_APPEARANCE_CSS_CLASS), `${DARK_SYSTEM_APPEARANCE_CSS_CLASS} CSS class is missing`); + TKUnit.assertFalse(cssClasses.has(LIGHT_SYSTEM_APPEARANCE_CSS_CLASS), `${LIGHT_SYSTEM_APPEARANCE_CSS_CLASS} CSS class is present`); + } + else if (systemAppearance === "light") { + TKUnit.assertTrue(cssClasses.has(LIGHT_SYSTEM_APPEARANCE_CSS_CLASS), `${LIGHT_SYSTEM_APPEARANCE_CSS_CLASS} CSS class is missing`); + TKUnit.assertFalse(cssClasses.has(DARK_SYSTEM_APPEARANCE_CSS_CLASS), `${DARK_SYSTEM_APPEARANCE_CSS_CLASS} CSS class is present`); + } + + if (shouldSetClassName) { + TKUnit.assertTrue(cssClasses.has(CLASS_NAME), `${CLASS_NAME} CSS class is missing`); + } +} + +// Application root view export function test_root_view_root_css_class() { - _test_root_view_root_css_class(false); + const rootView = getRootView(); + _test_root_css_class(rootView, false, false); } export function test_root_view_class_name_preserve_root_css_class() { - _test_root_view_root_css_class(true); -} - -function _test_root_view_platform_css_class(shouldSetClassName: boolean) { const rootView = getRootView(); - if (shouldSetClassName) { - rootView.className = CLASS_NAME; - } - - const rootViewCssClasses = rootView.cssClasses; - if (isAndroid) { - TKUnit.assertTrue(rootViewCssClasses.has( - ANDROID_PLATFORM_CSS_CLASS), - `${ANDROID_PLATFORM_CSS_CLASS} CSS class is missing` - ); - TKUnit.assertFalse(rootViewCssClasses.has( - IOS_PLATFORM_CSS_CLASS), - `${IOS_PLATFORM_CSS_CLASS} CSS class is present` - ); - } else { - TKUnit.assertTrue(rootViewCssClasses.has( - IOS_PLATFORM_CSS_CLASS), - `${IOS_PLATFORM_CSS_CLASS} CSS class is missing` - ); - TKUnit.assertFalse(rootViewCssClasses.has( - ANDROID_PLATFORM_CSS_CLASS), - `${ANDROID_PLATFORM_CSS_CLASS} CSS class is present` - ); - } - - if (shouldSetClassName) { - TKUnit.assertTrue(rootViewCssClasses.has( - CLASS_NAME), - `${CLASS_NAME} CSS class is missing` - ); - } + _test_root_css_class(rootView, false, true); } export function test_root_view_platform_css_class() { - _test_root_view_platform_css_class(false); + const rootView = getRootView(); + _test_platform_css_class(rootView, false); } export function test_root_view_class_name_preserve_platform_css_class() { - _test_root_view_platform_css_class(true); -} - -function _test_root_view_device_type_css_class(shouldSetClassName: boolean) { const rootView = getRootView(); - if (shouldSetClassName) { - rootView.className = CLASS_NAME; - } - - const rootViewCssClasses = rootView.cssClasses; - const deviceType = device.deviceType; - - if (deviceType === DeviceType.Phone) { - TKUnit.assertTrue(rootViewCssClasses.has( - PHONE_DEVICE_TYPE_CSS_CLASS), - `${PHONE_DEVICE_TYPE_CSS_CLASS} CSS class is missing` - ); - TKUnit.assertFalse(rootViewCssClasses.has( - TABLET_DEVICE_TYPE_CSS_CLASS), - `${TABLET_DEVICE_TYPE_CSS_CLASS} CSS class is present` - ); - } else { - TKUnit.assertTrue(rootViewCssClasses.has( - TABLET_DEVICE_TYPE_CSS_CLASS), - `${TABLET_DEVICE_TYPE_CSS_CLASS} CSS class is missing` - ); - TKUnit.assertFalse(rootViewCssClasses.has( - PHONE_DEVICE_TYPE_CSS_CLASS), - `${PHONE_DEVICE_TYPE_CSS_CLASS} CSS class is present` - ); - } - - if (shouldSetClassName) { - TKUnit.assertTrue(rootViewCssClasses.has( - CLASS_NAME), - `${CLASS_NAME} CSS class is missing` - ); - } + _test_platform_css_class(rootView, true); } export function test_root_view_device_type_css_class() { - _test_root_view_device_type_css_class(false); + const rootView = getRootView(); + _test_device_type_css_class(rootView, false); } export function test_root_view_class_name_preserve_device_type_css_class() { - _test_root_view_device_type_css_class(true); -} - -function _test_root_view_orientation_css_class(shouldSetClassName: boolean) { const rootView = getRootView(); - if (shouldSetClassName) { - rootView.className = CLASS_NAME; - } - - const rootViewCssClasses = rootView.cssClasses; - let appOrientation; - - if (isAndroid) { - appOrientation = android.orientation; - } else { - appOrientation = ios.orientation; - } - - if (appOrientation === "portrait") { - TKUnit.assertTrue(rootViewCssClasses.has( - PORTRAIT_ORIENTATION_CSS_CLASS), - `${PORTRAIT_ORIENTATION_CSS_CLASS} CSS class is missing` - ); - TKUnit.assertFalse(rootViewCssClasses.has( - LANDSCAPE_ORIENTATION_CSS_CLASS), - `${LANDSCAPE_ORIENTATION_CSS_CLASS} CSS class is present` - ); - TKUnit.assertFalse(rootViewCssClasses.has( - UNKNOWN_ORIENTATION_CSS_CLASS), - `${UNKNOWN_ORIENTATION_CSS_CLASS} CSS class is present` - ); - } else if (appOrientation === "landscape") { - TKUnit.assertTrue(rootViewCssClasses.has( - LANDSCAPE_ORIENTATION_CSS_CLASS), - `${LANDSCAPE_ORIENTATION_CSS_CLASS} CSS class is missing` - ); - TKUnit.assertFalse(rootViewCssClasses.has( - PORTRAIT_ORIENTATION_CSS_CLASS), - `${PORTRAIT_ORIENTATION_CSS_CLASS} CSS class is present` - ); - TKUnit.assertFalse(rootViewCssClasses.has( - UNKNOWN_ORIENTATION_CSS_CLASS), - `${UNKNOWN_ORIENTATION_CSS_CLASS} CSS class is present` - ); - } else if (appOrientation === "landscape") { - TKUnit.assertTrue(rootViewCssClasses.has( - UNKNOWN_ORIENTATION_CSS_CLASS), - `${UNKNOWN_ORIENTATION_CSS_CLASS} CSS class is missing` - ); - TKUnit.assertFalse(rootViewCssClasses.has( - LANDSCAPE_ORIENTATION_CSS_CLASS), - `${LANDSCAPE_ORIENTATION_CSS_CLASS} CSS class is present` - ); - TKUnit.assertFalse(rootViewCssClasses.has( - PORTRAIT_ORIENTATION_CSS_CLASS), - `${PORTRAIT_ORIENTATION_CSS_CLASS} CSS class is present` - ); - } - - if (shouldSetClassName) { - TKUnit.assertTrue(rootViewCssClasses.has( - CLASS_NAME), - `${CLASS_NAME} CSS class is missing` - ); - } + _test_device_type_css_class(rootView, true); } export function test_root_view_orientation_css_class() { - _test_root_view_orientation_css_class(false); + const rootView = getRootView(); + _test_orientation_css_class(rootView, false); } export function test_root_view_class_name_preserve_orientation_css_class() { - _test_root_view_orientation_css_class(true); -} - -function _test_root_view_system_appearance_css_class(shouldSetClassName: boolean) { const rootView = getRootView(); - if (shouldSetClassName) { - rootView.className = CLASS_NAME; - } - - const rootViewCssClasses = rootView.cssClasses; - let systemAppearance; - - if (isAndroid) { - systemAppearance = android.systemAppearance; - } else { - systemAppearance = ios.systemAppearance; - } - - if (isIOS && iosUtils.MajorVersion <= 12) { - TKUnit.assertFalse(rootViewCssClasses.has( - DARK_SYSTEM_APPEARANCE_CSS_CLASS), - `${DARK_SYSTEM_APPEARANCE_CSS_CLASS} CSS class is present` - ); - TKUnit.assertFalse(rootViewCssClasses.has( - LIGHT_SYSTEM_APPEARANCE_CSS_CLASS), - `${LIGHT_SYSTEM_APPEARANCE_CSS_CLASS} CSS class is present` - ); - } else if (systemAppearance === "dark") { - TKUnit.assertTrue(rootViewCssClasses.has( - DARK_SYSTEM_APPEARANCE_CSS_CLASS), - `${DARK_SYSTEM_APPEARANCE_CSS_CLASS} CSS class is missing` - ); - TKUnit.assertFalse(rootViewCssClasses.has( - LIGHT_SYSTEM_APPEARANCE_CSS_CLASS), - `${LIGHT_SYSTEM_APPEARANCE_CSS_CLASS} CSS class is present` - ); - } else if (systemAppearance === "light") { - TKUnit.assertTrue(rootViewCssClasses.has( - LIGHT_SYSTEM_APPEARANCE_CSS_CLASS), - `${LIGHT_SYSTEM_APPEARANCE_CSS_CLASS} CSS class is missing` - ); - TKUnit.assertFalse(rootViewCssClasses.has( - DARK_SYSTEM_APPEARANCE_CSS_CLASS), - `${DARK_SYSTEM_APPEARANCE_CSS_CLASS} CSS class is present` - ); - } - - if (shouldSetClassName) { - TKUnit.assertTrue(rootViewCssClasses.has( - CLASS_NAME), - `${CLASS_NAME} CSS class is missing` - ); - } + _test_orientation_css_class(rootView, true); } export function test_root_view_system_appearance_css_class() { - _test_root_view_system_appearance_css_class(false); + const rootView = getRootView(); + _test_system_appearance_css_class(rootView, false); } export function test_root_view_class_name_preserve_system_appearance_css_class() { - _test_root_view_system_appearance_css_class(true); + const rootView = getRootView(); + _test_system_appearance_css_class(rootView, true); } +// Modal root view function _test_modal_root_view_modal_css_class(shouldSetClassName: boolean) { let modalClosed = false; @@ -294,20 +218,8 @@ function _test_modal_root_view_modal_css_class(shouldSetClassName: boolean) { const page = args.object; page.off(View.shownModallyEvent, modalPageShownModallyEventHandler); - const rootModalView = _rootModalViews[0]; - if (shouldSetClassName) { - rootModalView.className = CLASS_NAME; - } - - const rootModalViewCssClasses = rootModalView.cssClasses; - TKUnit.assertTrue(rootModalViewCssClasses.has(MODAL_CSS_CLASS), - `${MODAL_CSS_CLASS} CSS class is missing`); - - if (shouldSetClassName) { - TKUnit.assertTrue(rootModalViewCssClasses.has(CLASS_NAME), - `${CLASS_NAME} CSS class is missing`); - } - + const rootModalView = _rootModalViews[0]; + _test_root_css_class(rootModalView, true, shouldSetClassName); args.closeCallback(); }; @@ -348,3 +260,219 @@ export function test_modal_root_view_modal_css_class() { export function test_modal_root_view_class_name_preserve_modal_css_class() { _test_modal_root_view_modal_css_class(true); } + +function _test_root_modal_view_platform_css_class(shouldSetClassName: boolean) { + let modalClosed = false; + + const modalCloseCallback = function () { + modalClosed = true; + }; + + const modalPageShownModallyEventHandler = function (args: ShownModallyData) { + const page = args.object; + page.off(View.shownModallyEvent, modalPageShownModallyEventHandler); + + const rootModalView = _rootModalViews[0]; + _test_platform_css_class(rootModalView, shouldSetClassName); + args.closeCallback(); + }; + + const hostNavigatedToEventHandler = function (args) { + const page = args.object; + page.off(Page.navigatedToEvent, hostNavigatedToEventHandler); + + const modalPage = new Page(); + modalPage.on(View.shownModallyEvent, modalPageShownModallyEventHandler); + const button =