mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-11-05 13:26:48 +08:00
Merge branch 'main' into feat/simpler-image-scale-ios
This commit is contained in:
@@ -31,20 +31,11 @@ export const accessibilityEnabledProperty = new CssProperty<Style, boolean>({
|
||||
});
|
||||
accessibilityEnabledProperty.register(Style);
|
||||
|
||||
const accessibilityHiddenPropertyName = 'accessibilityHidden';
|
||||
const accessibilityHiddenCssName = 'a11y-hidden';
|
||||
|
||||
export const accessibilityHiddenProperty = global.isIOS
|
||||
? new InheritedCssProperty({
|
||||
name: accessibilityHiddenPropertyName,
|
||||
cssName: accessibilityHiddenCssName,
|
||||
valueConverter: booleanConverter,
|
||||
})
|
||||
: new CssProperty({
|
||||
name: accessibilityHiddenPropertyName,
|
||||
cssName: accessibilityHiddenCssName,
|
||||
valueConverter: booleanConverter,
|
||||
});
|
||||
export const accessibilityHiddenProperty = new (global.isIOS ? InheritedCssProperty : CssProperty)({
|
||||
name: 'accessibilityHidden',
|
||||
cssName: 'a11y-hidden',
|
||||
valueConverter: booleanConverter,
|
||||
});
|
||||
accessibilityHiddenProperty.register(Style);
|
||||
|
||||
export const accessibilityIdentifierProperty = new Property<View, string>({
|
||||
|
||||
@@ -206,6 +206,19 @@ export class iOSApplication implements iOSApplicationDefinition {
|
||||
return this._rootView;
|
||||
}
|
||||
|
||||
public setSystemAppearance(value: 'light' | 'dark' | null) {
|
||||
if (this.systemAppearance !== value) {
|
||||
this._systemAppearance = value;
|
||||
systemAppearanceChanged(this.rootView, value);
|
||||
notify(<SystemAppearanceChangedEventData>{
|
||||
eventName: systemAppearanceChangedEvent,
|
||||
ios: this,
|
||||
newValue: iosApp.systemAppearance,
|
||||
object: this,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public addNotificationObserver(notificationName: string, onReceiveCallback: (notification: NSNotification) => void): NotificationObserver {
|
||||
const observer = NotificationObserver.initWithCallback(onReceiveCallback);
|
||||
NSNotificationCenter.defaultCenter.addObserverSelectorNameObject(observer, 'onReceive', notificationName, null);
|
||||
@@ -376,17 +389,7 @@ export class iOSApplication implements iOSApplicationDefinition {
|
||||
const userInterfaceStyle = controller.traitCollection.userInterfaceStyle;
|
||||
const newSystemAppearance = getSystemAppearanceValue(userInterfaceStyle);
|
||||
|
||||
if (this._systemAppearance !== newSystemAppearance) {
|
||||
this._systemAppearance = newSystemAppearance;
|
||||
systemAppearanceChanged(rootView, newSystemAppearance);
|
||||
|
||||
notify(<SystemAppearanceChangedEventData>{
|
||||
eventName: systemAppearanceChangedEvent,
|
||||
ios: this,
|
||||
newValue: this._systemAppearance,
|
||||
object: this,
|
||||
});
|
||||
}
|
||||
this.setSystemAppearance(newSystemAppearance);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -404,7 +407,7 @@ export function ensureNativeApplication() {
|
||||
}
|
||||
|
||||
// attach on global, so it can be overwritten in NativeScript Angular
|
||||
(<any>global).__onLiveSyncCore = function (context?: ModuleContext) {
|
||||
global.__onLiveSyncCore = function (context?: ModuleContext) {
|
||||
ensureNativeApplication();
|
||||
iosApp._onLivesync(context);
|
||||
};
|
||||
@@ -461,6 +464,7 @@ export function run(entry?: string | NavigationEntry) {
|
||||
rootView._setupAsRootView({});
|
||||
const embedderDelegate = NativeScriptEmbedder.sharedInstance().delegate;
|
||||
if (embedderDelegate) {
|
||||
setViewControllerView(rootView);
|
||||
embedderDelegate.presentNativeScriptApp(controller);
|
||||
} else {
|
||||
const visibleVC = getVisibleViewController(rootController);
|
||||
@@ -474,16 +478,7 @@ export function run(entry?: string | NavigationEntry) {
|
||||
const userInterfaceStyle = controller.traitCollection.userInterfaceStyle;
|
||||
const newSystemAppearance = getSystemAppearanceValue(userInterfaceStyle);
|
||||
|
||||
if (this._systemAppearance !== newSystemAppearance) {
|
||||
this._systemAppearance = newSystemAppearance;
|
||||
|
||||
notify(<SystemAppearanceChangedEventData>{
|
||||
eventName: systemAppearanceChangedEvent,
|
||||
ios: this,
|
||||
newValue: this._systemAppearance,
|
||||
object: this,
|
||||
});
|
||||
}
|
||||
iosApp.setSystemAppearance(newSystemAppearance);
|
||||
});
|
||||
iosApp.notifyAppStarted();
|
||||
}
|
||||
|
||||
@@ -254,6 +254,44 @@ export class FileSystemAccess implements IFileSystemAccess {
|
||||
return this.getLogicalRootPath() + '/app';
|
||||
}
|
||||
|
||||
public readBuffer = this.readBufferSync.bind(this);
|
||||
|
||||
public readBufferAsync(path: string): Promise<ArrayBuffer> {
|
||||
return new Promise<ArrayBuffer>((resolve, reject) => {
|
||||
try {
|
||||
org.nativescript.widgets.Async.File.readBuffer(
|
||||
path,
|
||||
new org.nativescript.widgets.Async.CompleteCallback({
|
||||
onComplete: (result: java.nio.ByteBuffer) => {
|
||||
resolve((ArrayBuffer as any).from(result));
|
||||
},
|
||||
onError: (err) => {
|
||||
reject(new Error(err));
|
||||
},
|
||||
}),
|
||||
null
|
||||
);
|
||||
} catch (ex) {
|
||||
reject(ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public readBufferSync(path: string, onError?: (error: any) => any) {
|
||||
try {
|
||||
const javaFile = new java.io.File(path);
|
||||
const stream = new java.io.FileInputStream(javaFile);
|
||||
const channel = stream.getChannel();
|
||||
const buffer = new ArrayBuffer(javaFile.length());
|
||||
channel.read(buffer as any);
|
||||
return buffer;
|
||||
} catch (exception) {
|
||||
if (onError) {
|
||||
onError(exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public read = this.readSync.bind(this);
|
||||
|
||||
public readAsync(path: string): Promise<number[]> {
|
||||
@@ -293,6 +331,52 @@ export class FileSystemAccess implements IFileSystemAccess {
|
||||
}
|
||||
}
|
||||
|
||||
static getBuffer(buffer: ArrayBuffer | Uint8Array | Uint8ClampedArray): any {
|
||||
if (buffer instanceof ArrayBuffer) {
|
||||
return (buffer as any).nativeObject || buffer;
|
||||
} else {
|
||||
return (buffer?.buffer as any)?.nativeObject || buffer;
|
||||
}
|
||||
}
|
||||
|
||||
public writeBuffer = this.writeBufferSync.bind(this);
|
||||
|
||||
public writeBufferAsync(path: string, buffer: ArrayBuffer | Uint8Array | Uint8ClampedArray): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
try {
|
||||
org.nativescript.widgets.Async.File.writeBuffer(
|
||||
path,
|
||||
FileSystemAccess.getBuffer(buffer),
|
||||
new org.nativescript.widgets.Async.CompleteCallback({
|
||||
onComplete: () => {
|
||||
resolve();
|
||||
},
|
||||
onError: (err) => {
|
||||
reject(new Error(err));
|
||||
},
|
||||
}),
|
||||
null
|
||||
);
|
||||
} catch (ex) {
|
||||
reject(ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public writeBufferSync(path: string, buffer: ArrayBuffer | Uint8Array | Uint8ClampedArray, onError?: (error: any) => any) {
|
||||
try {
|
||||
const javaFile = new java.io.File(path);
|
||||
const stream = new java.io.FileOutputStream(javaFile);
|
||||
const channel = stream.getChannel();
|
||||
channel.write(FileSystemAccess.getBuffer(buffer));
|
||||
stream.close();
|
||||
} catch (exception) {
|
||||
if (onError) {
|
||||
onError(exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public write = this.writeSync.bind(this);
|
||||
|
||||
public writeAsync(path: string, bytes: androidNative.Array<number>): Promise<void> {
|
||||
@@ -757,6 +841,7 @@ export class FileSystemAccess29 extends FileSystemAccess {
|
||||
getCurrentAppPath(): string {
|
||||
return super.getCurrentAppPath();
|
||||
}
|
||||
|
||||
public readText = this.readTextSync.bind(this);
|
||||
|
||||
readTextAsync(path: string, encoding?: any): Promise<string> {
|
||||
@@ -795,6 +880,47 @@ export class FileSystemAccess29 extends FileSystemAccess {
|
||||
}
|
||||
}
|
||||
|
||||
readBuffer = this.readBufferSync.bind(this);
|
||||
|
||||
readBufferAsync(path: string): Promise<any> {
|
||||
if (isContentUri(path)) {
|
||||
return new Promise((resolve, reject) => {
|
||||
getOrSetHelper(path).readBuffer(
|
||||
applicationContext,
|
||||
new org.nativescript.widgets.FileHelper.Callback({
|
||||
onSuccess(result) {
|
||||
resolve(result);
|
||||
},
|
||||
onError(error) {
|
||||
reject(error);
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
return super.readBufferAsync(path);
|
||||
}
|
||||
|
||||
readBufferSync(path: string, onError?: (error: any) => any) {
|
||||
if (isContentUri(path)) {
|
||||
let callback = null;
|
||||
if (typeof onError === 'function') {
|
||||
callback = new org.nativescript.widgets.FileHelper.Callback({
|
||||
onSuccess(result) {},
|
||||
onError(error) {
|
||||
onError(error);
|
||||
},
|
||||
});
|
||||
}
|
||||
const ret = getOrSetHelper(path).readBufferSync(applicationContext, callback);
|
||||
if (ret) {
|
||||
return null;
|
||||
}
|
||||
return (ArrayBuffer as any).from(ret);
|
||||
}
|
||||
return super.readBufferSync(path, onError);
|
||||
}
|
||||
|
||||
read = this.readSync.bind(this);
|
||||
|
||||
readAsync(path: string): Promise<any> {
|
||||
@@ -872,6 +998,45 @@ export class FileSystemAccess29 extends FileSystemAccess {
|
||||
}
|
||||
}
|
||||
|
||||
writeBuffer = this.writeBufferSync.bind(this);
|
||||
|
||||
writeBufferAsync(path: string, content: any): Promise<void> {
|
||||
if (isContentUri(path)) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
getOrSetHelper(path).writeBuffer(
|
||||
applicationContext,
|
||||
FileSystemAccess.getBuffer(content),
|
||||
new org.nativescript.widgets.FileHelper.Callback({
|
||||
onSuccess(result) {
|
||||
resolve();
|
||||
},
|
||||
onError(error) {
|
||||
reject(error);
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
return super.writeAsync(path, content);
|
||||
}
|
||||
|
||||
writeBufferSync(path: string, content: any, onError?: (error: any) => any) {
|
||||
if (isContentUri(path)) {
|
||||
let callback = null;
|
||||
if (typeof onError === 'function') {
|
||||
callback = new org.nativescript.widgets.FileHelper.Callback({
|
||||
onSuccess(result) {},
|
||||
onError(error) {
|
||||
onError(error);
|
||||
},
|
||||
});
|
||||
}
|
||||
getOrSetHelper(path).writeSync(applicationContext, FileSystemAccess.getBuffer(content), callback);
|
||||
} else {
|
||||
super.writeSync(path, content, onError);
|
||||
}
|
||||
}
|
||||
|
||||
write = this.writeSync.bind(this);
|
||||
|
||||
writeAsync(path: string, content: any): Promise<void> {
|
||||
|
||||
@@ -298,6 +298,12 @@ export class FileSystemAccess implements IFileSystemAccess {
|
||||
|
||||
readTextSync(path: string, onError?: (error: any) => any, encoding?: any): string;
|
||||
|
||||
readBuffer(path: string, onError?: (error: any) => any): ArrayBuffer;
|
||||
|
||||
readBufferAsync(path: string): Promise<ArrayBuffer>;
|
||||
|
||||
readBufferSync(path: string, onError?: (error: any) => any): ArrayBuffer;
|
||||
|
||||
read(path: string, onError?: (error: any) => any): any;
|
||||
|
||||
readAsync(path: string): Promise<any>;
|
||||
@@ -310,6 +316,12 @@ export class FileSystemAccess implements IFileSystemAccess {
|
||||
|
||||
writeTextSync(path: string, content: string, onError?: (error: any) => any, encoding?: any);
|
||||
|
||||
writeBuffer(path: string, content: ArrayBuffer | Uint8Array | Uint8ClampedArray, onError?: (error: any) => any);
|
||||
|
||||
writeBufferAsync(path: string, content: ArrayBuffer | Uint8Array | Uint8ClampedArray): Promise<void>;
|
||||
|
||||
writeBufferSync(path: string, content: ArrayBuffer | Uint8Array | Uint8ClampedArray, onError?: (error: any) => any);
|
||||
|
||||
write(path: string, content: any, onError?: (error: any) => any);
|
||||
|
||||
writeAsync(path: string, content: any): Promise<void>;
|
||||
|
||||
@@ -294,6 +294,30 @@ export class FileSystemAccess {
|
||||
}
|
||||
}
|
||||
|
||||
public readBuffer = this.readBufferSync.bind(this);
|
||||
|
||||
public readBufferAsync(path: string): Promise<ArrayBuffer> {
|
||||
return new Promise<ArrayBuffer>((resolve, reject) => {
|
||||
try {
|
||||
(NSData as any).dataWithContentsOfFileCompletion(path, (data) => {
|
||||
resolve(interop.bufferFromData(data));
|
||||
});
|
||||
} catch (ex) {
|
||||
reject(new Error("Failed to read file at path '" + path + "': " + ex));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public readBufferSync(path: string, onError?: (error: any) => any): ArrayBuffer {
|
||||
try {
|
||||
return interop.bufferFromData(NSData.dataWithContentsOfFile(path));
|
||||
} catch (ex) {
|
||||
if (onError) {
|
||||
onError(new Error("Failed to read file at path '" + path + "': " + ex));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public read = this.readSync.bind(this);
|
||||
|
||||
public readAsync(path: string): Promise<NSData> {
|
||||
@@ -352,6 +376,40 @@ export class FileSystemAccess {
|
||||
}
|
||||
}
|
||||
|
||||
static getBuffer(buffer: ArrayBuffer | Uint8Array | Uint8ClampedArray): NSData {
|
||||
if (buffer instanceof ArrayBuffer) {
|
||||
return NSData.dataWithData(buffer as any);
|
||||
} else {
|
||||
const buf = NSData.dataWithData(buffer?.buffer as any);
|
||||
const len = buffer.byteLength;
|
||||
return NSData.dataWithBytesNoCopyLength((buf.bytes as interop.Pointer).add(buffer?.byteOffset ?? 0), len);
|
||||
}
|
||||
}
|
||||
|
||||
public writeBuffer = this.writeBufferSync.bind(this);
|
||||
|
||||
public writeBufferAsync(path: string, content: ArrayBuffer | Uint8Array | Uint8ClampedArray): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
try {
|
||||
FileSystemAccess.getBuffer(content).writeToFileAtomicallyCompletion(path, true, () => {
|
||||
resolve();
|
||||
});
|
||||
} catch (ex) {
|
||||
reject(new Error("Failed to write file at path '" + path + "': " + ex));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public writeBufferSync(path: string, content: ArrayBuffer | Uint8Array | Uint8ClampedArray, onError?: (error: any) => any) {
|
||||
try {
|
||||
FileSystemAccess.getBuffer(content).writeToFileAtomically(path, true);
|
||||
} catch (ex) {
|
||||
if (onError) {
|
||||
onError(new Error("Failed to write to file '" + path + "': " + ex));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public write = this.writeSync.bind(this);
|
||||
|
||||
public writeAsync(path: string, content: NSData): Promise<void> {
|
||||
|
||||
@@ -27,7 +27,7 @@ export class FPSCallback implements definition.FPSCallback {
|
||||
}
|
||||
|
||||
private _isNativeFramesSupported() {
|
||||
return typeof (<any>global).__postFrameCallback === 'function' && typeof (<any>global).__removeFrameCallback === 'function';
|
||||
return typeof (global as any).__postFrameCallback === 'function' && typeof global.__removeFrameCallback === 'function';
|
||||
}
|
||||
|
||||
public start() {
|
||||
|
||||
176
packages/core/global-types.d.ts
vendored
176
packages/core/global-types.d.ts
vendored
@@ -17,106 +17,106 @@ declare interface NativeScriptError extends Error {
|
||||
}
|
||||
|
||||
//Augment the NodeJS global type with our own extensions
|
||||
declare namespace NodeJS {
|
||||
interface Global {
|
||||
NativeScriptHasInitGlobal?: boolean;
|
||||
NativeScriptGlobals?: {
|
||||
/**
|
||||
* Global framework event handling
|
||||
*/
|
||||
events: {
|
||||
[Key in keyof import('data/observable').Observable]: import('data/observable').Observable[Key];
|
||||
};
|
||||
launched: boolean;
|
||||
// used by various classes to setup callbacks to wire up global app event handling when the app instance is ready
|
||||
appEventWiring: Array<any>;
|
||||
// determines if the app instance is ready upon bootstrap
|
||||
appInstanceReady: boolean;
|
||||
|
||||
/**
|
||||
* Ability for classes to initialize app event handling early even before the app instance is ready during boot cycle avoiding boot race conditions
|
||||
* @param callback wire up any global event handling inside the callback
|
||||
*/
|
||||
addEventWiring(callback: () => void): void;
|
||||
declare module globalThis {
|
||||
var NativeScriptHasInitGlobal: boolean;
|
||||
var NativeScriptGlobals: {
|
||||
/**
|
||||
* Global framework event handling
|
||||
*/
|
||||
events: {
|
||||
[Key in keyof import('data/observable').Observable]: import('data/observable').Observable[Key];
|
||||
};
|
||||
android?: any;
|
||||
require(id: string): any;
|
||||
|
||||
moduleMerge(sourceExports: any, destExports: any): void;
|
||||
|
||||
registerModule(name: string, loader: (name: string) => any): void;
|
||||
/**
|
||||
* Register all modules from a webpack context.
|
||||
* The context is one created using the following webpack utility:
|
||||
* https://webpack.js.org/guides/dependency-management/#requirecontext
|
||||
*
|
||||
* The extension map is optional, modules in the webpack context will have their original file extension (e.g. may be ".ts" or ".scss" etc.),
|
||||
* while the built-in module builders in {N} will look for ".js", ".css" or ".xml" files. Adding a map such as:
|
||||
* ```
|
||||
* { ".ts": ".js" }
|
||||
* ```
|
||||
* Will resolve lookups for .js to the .ts file.
|
||||
* By default scss and ts files are mapped.
|
||||
*/
|
||||
registerWebpackModules(context: { keys(): string[]; (key: string): any }, extensionMap?: { [originalFileExtension: string]: string });
|
||||
launched: boolean;
|
||||
// used by various classes to setup callbacks to wire up global app event handling when the app instance is ready
|
||||
appEventWiring: Array<any>;
|
||||
// determines if the app instance is ready upon bootstrap
|
||||
appInstanceReady: boolean;
|
||||
|
||||
/**
|
||||
* The NativeScript XML builder, style-scope, application modules use various resources such as:
|
||||
* app.css, page.xml files and modules during the application life-cycle.
|
||||
* The moduleResolvers can be used to provide additional mechanisms to locate such resources.
|
||||
* For example:
|
||||
* ```
|
||||
* global.moduleResolvers.unshift(uri => uri === "main-page" ? require("main-page") : null);
|
||||
* ```
|
||||
* More advanced scenarios will allow for specific bundlers to integrate their module resolving mechanisms.
|
||||
* When adding resolvers at the start of the array, avoid throwing and return null instead so subsequent resolvers may try to resolve the resource.
|
||||
* By default the only member of the array is global.require, as last resort - if it fails to find a module it will throw.
|
||||
* Ability for classes to initialize app event handling early even before the app instance is ready during boot cycle avoiding boot race conditions
|
||||
* @param callback wire up any global event handling inside the callback
|
||||
*/
|
||||
readonly moduleResolvers: ModuleResolver[];
|
||||
addEventWiring(callback: () => void): void;
|
||||
};
|
||||
// var android: any;
|
||||
function require(id: string): any;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param name Name of the module to be loaded
|
||||
* @param loadForUI Is this UI module is being loaded for UI from @nativescript/core/ui/builder.
|
||||
* Xml, css/scss and js/ts modules for pages and custom-components should load with loadForUI=true.
|
||||
* Passing "true" will enable the HMR mechanics this module. Default value is false.
|
||||
*/
|
||||
loadModule(name: string, loadForUI?: boolean): any;
|
||||
function moduleMerge(sourceExports: any, destExports: any): void;
|
||||
|
||||
/**
|
||||
* Checks if the module has been registered with `registerModule` or in `registerWebpackModules`
|
||||
* @param name Name of the module
|
||||
*/
|
||||
moduleExists(name: string): boolean;
|
||||
function registerModule(name: string, loader: (name: string) => any): void;
|
||||
/**
|
||||
* Register all modules from a webpack context.
|
||||
* The context is one created using the following webpack utility:
|
||||
* https://webpack.js.org/guides/dependency-management/#requirecontext
|
||||
*
|
||||
* The extension map is optional, modules in the webpack context will have their original file extension (e.g. may be ".ts" or ".scss" etc.),
|
||||
* while the built-in module builders in {N} will look for ".js", ".css" or ".xml" files. Adding a map such as:
|
||||
* ```
|
||||
* { ".ts": ".js" }
|
||||
* ```
|
||||
* Will resolve lookups for .js to the .ts file.
|
||||
* By default scss and ts files are mapped.
|
||||
*/
|
||||
function registerWebpackModules(context: { keys(): string[]; (key: string): any }, extensionMap?: { [originalFileExtension: string]: string });
|
||||
|
||||
getRegisteredModules(): string[];
|
||||
/**
|
||||
* The NativeScript XML builder, style-scope, application modules use various resources such as:
|
||||
* app.css, page.xml files and modules during the application life-cycle.
|
||||
* The moduleResolvers can be used to provide additional mechanisms to locate such resources.
|
||||
* For example:
|
||||
* ```
|
||||
* global.moduleResolvers.unshift(uri => uri === "main-page" ? require("main-page") : null);
|
||||
* ```
|
||||
* More advanced scenarios will allow for specific bundlers to integrate their module resolving mechanisms.
|
||||
* When adding resolvers at the start of the array, avoid throwing and return null instead so subsequent resolvers may try to resolve the resource.
|
||||
* By default the only member of the array is global.require, as last resort - if it fails to find a module it will throw.
|
||||
*/
|
||||
var moduleResolvers: ModuleResolver[];
|
||||
|
||||
_unregisterModule(name: string): void;
|
||||
/**
|
||||
*
|
||||
* @param name Name of the module to be loaded
|
||||
* @param loadForUI Is this UI module is being loaded for UI from @nativescript/core/ui/builder.
|
||||
* Xml, css/scss and js/ts modules for pages and custom-components should load with loadForUI=true.
|
||||
* Passing "true" will enable the HMR mechanics this module. Default value is false.
|
||||
*/
|
||||
function loadModule(name: string, loadForUI?: boolean): any;
|
||||
|
||||
_isModuleLoadedForUI(moduleName: string): boolean;
|
||||
/**
|
||||
* Checks if the module has been registered with `registerModule` or in `registerWebpackModules`
|
||||
* @param name Name of the module
|
||||
*/
|
||||
function moduleExists(name: string): boolean;
|
||||
|
||||
onGlobalLayoutListener: any;
|
||||
zonedCallback(callback: Function): Function;
|
||||
Reflect?: any;
|
||||
Deprecated(target: Object, key?: string | symbol, descriptor?: any): any;
|
||||
Experimental(target: Object, key?: string | symbol, descriptor?: any): any;
|
||||
function getRegisteredModules(): string[];
|
||||
|
||||
__native?: any;
|
||||
__inspector?: any;
|
||||
__extends: any;
|
||||
__onLiveSync: (context?: { type: string; path: string }) => void;
|
||||
__onLiveSyncCore: (context?: { type: string; path: string }) => void;
|
||||
__onUncaughtError: (error: NativeScriptError) => void;
|
||||
__onDiscardedError: (error: NativeScriptError) => void;
|
||||
__snapshot?: boolean;
|
||||
TNS_WEBPACK?: boolean;
|
||||
isIOS?: boolean;
|
||||
isAndroid?: boolean;
|
||||
__requireOverride?: (name: string, dir: string) => any;
|
||||
function _unregisterModule(name: string): void;
|
||||
|
||||
// used to get the rootlayout instance to add/remove childviews
|
||||
rootLayout: any;
|
||||
}
|
||||
function _isModuleLoadedForUI(moduleName: string): boolean;
|
||||
|
||||
var onGlobalLayoutListener: any;
|
||||
function zonedCallback(callback: Function): Function;
|
||||
var Reflect: any;
|
||||
function Deprecated(target: Object, key?: string | symbol, descriptor?: any): any;
|
||||
function Experimental(target: Object, key?: string | symbol, descriptor?: any): any;
|
||||
|
||||
var __native: any;
|
||||
var __inspector: any;
|
||||
var __extends: any;
|
||||
var __onLiveSync: (context?: { type: string; path: string }) => void;
|
||||
var __onLiveSyncCore: (context?: { type: string; path: string }) => void;
|
||||
var __onUncaughtError: (error: NativeScriptError) => void;
|
||||
var __onDiscardedError: (error: NativeScriptError) => void;
|
||||
var __snapshot: boolean;
|
||||
var TNS_WEBPACK: boolean;
|
||||
var isIOS: boolean;
|
||||
var isAndroid: boolean;
|
||||
var isDisplayedEventFired: boolean;
|
||||
var autoLoadPolyfills: boolean;
|
||||
var __requireOverride: (name: string, dir: string) => any;
|
||||
|
||||
// used to get the rootlayout instance to add/remove childviews
|
||||
var rootLayout: any;
|
||||
}
|
||||
declare const __DEV__: boolean;
|
||||
declare const __CSS_PARSER__: string;
|
||||
|
||||
@@ -115,8 +115,8 @@ export function initGlobal() {
|
||||
|
||||
// ts-helpers
|
||||
// Required by V8 snapshot generator
|
||||
if (!(<any>global).__extends) {
|
||||
(<any>global).__extends = function (d, b) {
|
||||
if (!global.__extends) {
|
||||
global.__extends = function (d, b) {
|
||||
for (const p in b) {
|
||||
if (b.hasOwnProperty(p)) {
|
||||
d[p] = b[p];
|
||||
@@ -160,7 +160,7 @@ export function initGlobal() {
|
||||
};
|
||||
|
||||
// Cast to <any> because moduleResolvers is read-only in definitions
|
||||
(<any>global).moduleResolvers = [global.require];
|
||||
global.moduleResolvers = [global.require];
|
||||
|
||||
global.registerModule = function (name: string, loader: ModuleLoader): void {
|
||||
modules.set(name, { loader, moduleId: name });
|
||||
@@ -250,7 +250,7 @@ export function initGlobal() {
|
||||
return result;
|
||||
}
|
||||
|
||||
for (const resolver of (<any>global).moduleResolvers) {
|
||||
for (const resolver of global.moduleResolvers) {
|
||||
const result = resolver(name);
|
||||
if (result) {
|
||||
modules.set(name, { moduleId: name, loader: () => result });
|
||||
@@ -276,19 +276,19 @@ export function initGlobal() {
|
||||
};
|
||||
|
||||
global.zonedCallback = function (callback: Function): Function {
|
||||
if ((<any>global).zone) {
|
||||
if (global.zone) {
|
||||
// Zone v0.5.* style callback wrapping
|
||||
return (<any>global).zone.bind(callback);
|
||||
return global.zone.bind(callback);
|
||||
}
|
||||
if ((<any>global).Zone) {
|
||||
if (global.Zone) {
|
||||
// Zone v0.6.* style callback wrapping
|
||||
return (<any>global).Zone.current.wrap(callback);
|
||||
return global.Zone.current.wrap(callback);
|
||||
} else {
|
||||
return callback;
|
||||
}
|
||||
};
|
||||
|
||||
(<any>global).System = {
|
||||
global.System = {
|
||||
import(path) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
|
||||
2
packages/core/index.d.ts
vendored
2
packages/core/index.d.ts
vendored
@@ -17,6 +17,8 @@ export declare const Application: {
|
||||
suspendEvent: string;
|
||||
resumeEvent: string;
|
||||
exitEvent: string;
|
||||
foregroundEvent: string;
|
||||
backgroundEvent: string;
|
||||
lowMemoryEvent: string;
|
||||
orientationChangedEvent: string;
|
||||
systemAppearanceChangedEvent: string;
|
||||
|
||||
@@ -6,7 +6,7 @@ export { iOSApplication, AndroidApplication } from './application';
|
||||
export type { ApplicationEventData, LaunchEventData, OrientationChangedEventData, UnhandledErrorEventData, DiscardedErrorEventData, CssChangedEventData, LoadAppCSSEventData, AndroidActivityEventData, AndroidActivityBundleEventData, AndroidActivityRequestPermissionsEventData, AndroidActivityResultEventData, AndroidActivityNewIntentEventData, AndroidActivityBackPressedEventData, SystemAppearanceChangedEventData } from './application';
|
||||
|
||||
import { fontScaleChangedEvent, launchEvent, displayedEvent, uncaughtErrorEvent, discardedErrorEvent, suspendEvent, resumeEvent, exitEvent, lowMemoryEvent, orientationChangedEvent, systemAppearanceChanged, systemAppearanceChangedEvent, getMainEntry, getRootView, _resetRootView, getResources, setResources, setCssFileName, getCssFileName, loadAppCss, addCss, on, off, notify, hasListeners, run, orientation, getNativeApplication, hasLaunched, android as appAndroid, ios as iosApp, systemAppearance, setAutoSystemAppearanceChanged, ensureNativeApplication, setMaxRefreshRate } from './application';
|
||||
import { inBackground, suspended } from './application/application-common';
|
||||
import { inBackground, suspended, foregroundEvent, backgroundEvent } from './application/application-common';
|
||||
|
||||
export const Application = {
|
||||
launchEvent,
|
||||
@@ -16,6 +16,8 @@ export const Application = {
|
||||
suspendEvent,
|
||||
resumeEvent,
|
||||
exitEvent,
|
||||
foregroundEvent,
|
||||
backgroundEvent,
|
||||
lowMemoryEvent,
|
||||
orientationChangedEvent,
|
||||
systemAppearanceChangedEvent,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// @ts-nocheck
|
||||
global.WeakRef.prototype.get = global.WeakRef.prototype.deref;
|
||||
global.NativeClass = function () {};
|
||||
global.NSObject = class NSObject {};
|
||||
global.NSString = {
|
||||
stringWithString() {
|
||||
return {
|
||||
|
||||
Binary file not shown.
@@ -2,13 +2,13 @@
|
||||
declare let __startCPUProfiler: any;
|
||||
declare let __stopCPUProfiler: any;
|
||||
|
||||
export function uptime() {
|
||||
return global.android ? (<any>org).nativescript.Process.getUpTime() : (<any>global).__tns_uptime();
|
||||
export function uptime(): number {
|
||||
return global.android ? (<any>org).nativescript.Process.getUpTime() : global.__tns_uptime();
|
||||
}
|
||||
|
||||
export function log(message: string, ...optionalParams: any[]): void {
|
||||
if ((<any>global).__nslog) {
|
||||
(<any>global).__nslog('CONSOLE LOG: ' + message);
|
||||
if (global.__nslog) {
|
||||
global.__nslog('CONSOLE LOG: ' + message);
|
||||
}
|
||||
console.log(message, ...optionalParams);
|
||||
}
|
||||
@@ -31,7 +31,7 @@ const timers: { [index: string]: TimerInfo } = {};
|
||||
const anyGlobal = <any>global;
|
||||
const profileNames: string[] = [];
|
||||
|
||||
export const time = (<any>global).__time || Date.now;
|
||||
export const time = (global.__time || Date.now) as () => number;
|
||||
|
||||
export function start(name: string): void {
|
||||
let info = timers[name];
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"noEmitOnError": true,
|
||||
"noEmitHelpers": true,
|
||||
"declaration": true,
|
||||
"noImplicitAny": false,
|
||||
"noImplicitUseStrict": true,
|
||||
"removeComments": false,
|
||||
"emitDecoratorMetadata": true,
|
||||
|
||||
@@ -34,7 +34,7 @@ class AnimationDelegateImpl extends NSObject implements CAAnimationDelegate {
|
||||
public nextAnimation: Function;
|
||||
|
||||
// The CAAnimationDelegate protocol has been introduced in the iOS 10 SDK
|
||||
static ObjCProtocols = (<any>global).CAAnimationDelegate ? [(<any>global).CAAnimationDelegate] : [];
|
||||
static ObjCProtocols = global.CAAnimationDelegate ? [global.CAAnimationDelegate] : [];
|
||||
|
||||
private _finishedCallback: Function;
|
||||
private _propertyAnimation: PropertyAnimationInfo;
|
||||
|
||||
@@ -211,7 +211,6 @@ export class View extends ViewCommon implements ViewDefinition {
|
||||
const boundsOrigin = nativeView.bounds.origin;
|
||||
const boundsFrame = adjustedFrame || frame;
|
||||
nativeView.bounds = CGRectMake(boundsOrigin.x, boundsOrigin.y, boundsFrame.size.width, boundsFrame.size.height);
|
||||
nativeView.layoutIfNeeded();
|
||||
|
||||
this._raiseLayoutChangedEvent();
|
||||
this._isLaidOut = true;
|
||||
@@ -889,6 +888,9 @@ export class View extends ViewCommon implements ViewDefinition {
|
||||
}
|
||||
|
||||
_setNativeClipToBounds() {
|
||||
if (!this.nativeViewProtected) {
|
||||
return;
|
||||
}
|
||||
const backgroundInternal = this.style.backgroundInternal;
|
||||
this.nativeViewProtected.clipsToBounds = (this.nativeViewProtected instanceof UIScrollView || backgroundInternal.hasBorderWidth() || backgroundInternal.hasBorderRadius()) && !backgroundInternal.hasBoxShadow();
|
||||
}
|
||||
|
||||
@@ -14,9 +14,51 @@ const majorVersion = iOSNativeHelper.MajorVersion;
|
||||
class UILayoutViewController extends UIViewController {
|
||||
public owner: WeakRef<View>;
|
||||
|
||||
private _isRunningLayout: number;
|
||||
private get isRunningLayout() {
|
||||
return this._isRunningLayout !== 0;
|
||||
}
|
||||
private startRunningLayout() {
|
||||
this._isRunningLayout++;
|
||||
}
|
||||
private finishRunningLayout() {
|
||||
this._isRunningLayout--;
|
||||
this.clearScheduledLayout();
|
||||
}
|
||||
private runLayout(cb: () => void) {
|
||||
try {
|
||||
this.startRunningLayout();
|
||||
cb();
|
||||
} finally {
|
||||
this.finishRunningLayout();
|
||||
}
|
||||
}
|
||||
|
||||
layoutTimer: number;
|
||||
|
||||
private clearScheduledLayout() {
|
||||
if (this.layoutTimer) {
|
||||
clearTimeout(this.layoutTimer);
|
||||
this.layoutTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
private scheduleLayout() {
|
||||
if (this.layoutTimer) {
|
||||
return;
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.layoutTimer = null;
|
||||
if (!this.isRunningLayout) {
|
||||
this.runLayout(() => this.layoutOwner());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static initWithOwner(owner: WeakRef<View>): UILayoutViewController {
|
||||
const controller = <UILayoutViewController>UILayoutViewController.new();
|
||||
controller.owner = owner;
|
||||
controller._isRunningLayout = 0;
|
||||
|
||||
return controller;
|
||||
}
|
||||
@@ -29,6 +71,11 @@ class UILayoutViewController extends UIViewController {
|
||||
this.extendedLayoutIncludesOpaqueBars = true;
|
||||
}
|
||||
|
||||
public viewSafeAreaInsetsDidChange(): void {
|
||||
super.viewSafeAreaInsetsDidChange();
|
||||
this.scheduleLayout();
|
||||
}
|
||||
|
||||
public viewWillLayoutSubviews(): void {
|
||||
super.viewWillLayoutSubviews();
|
||||
const owner = this.owner?.deref();
|
||||
@@ -38,56 +85,69 @@ class UILayoutViewController extends UIViewController {
|
||||
}
|
||||
|
||||
public viewDidLayoutSubviews(): void {
|
||||
this.startRunningLayout();
|
||||
super.viewDidLayoutSubviews();
|
||||
this.layoutOwner();
|
||||
this.finishRunningLayout();
|
||||
}
|
||||
layoutOwner(force = false) {
|
||||
const owner = this.owner?.deref();
|
||||
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;
|
||||
if (!owner) {
|
||||
return;
|
||||
}
|
||||
if (!force && owner.isLayoutValid && !owner.nativeViewProtected?.layer.needsLayout?.()) {
|
||||
// we skip layout if the view is not yet laid out yet
|
||||
// this usually means that viewDidLayoutSubviews will be called again
|
||||
// so doing a layout pass now will layout with the wrong parameters
|
||||
return;
|
||||
}
|
||||
|
||||
// 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 (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;
|
||||
|
||||
if (parent) {
|
||||
const parentPageInsetsTop = parent.nativeViewProtected.safeAreaInsets.top;
|
||||
const parentPageInsetsBottom = parent.nativeViewProtected.safeAreaInsets.bottom;
|
||||
let currentInsetsTop = this.view.safeAreaInsets.top;
|
||||
let currentInsetsBottom = this.view.safeAreaInsets.bottom;
|
||||
|
||||
// Safe area insets include additional safe area insets too, so subtract old values
|
||||
if (this.additionalSafeAreaInsets) {
|
||||
currentInsetsTop -= this.additionalSafeAreaInsets.top;
|
||||
currentInsetsBottom -= this.additionalSafeAreaInsets.bottom;
|
||||
}
|
||||
|
||||
const additionalInsetsTop = Math.max(parentPageInsetsTop - currentInsetsTop, 0);
|
||||
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;
|
||||
} else {
|
||||
this.additionalSafeAreaInsets = null;
|
||||
}
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
|
||||
IOSHelper.layoutView(this, owner);
|
||||
if (parent) {
|
||||
const parentPageInsetsTop = parent.nativeViewProtected.safeAreaInsets.top;
|
||||
const parentPageInsetsBottom = parent.nativeViewProtected.safeAreaInsets.bottom;
|
||||
let currentInsetsTop = this.view.safeAreaInsets.top;
|
||||
let currentInsetsBottom = this.view.safeAreaInsets.bottom;
|
||||
|
||||
// Safe area insets include additional safe area insets too, so subtract old values
|
||||
if (this.additionalSafeAreaInsets) {
|
||||
currentInsetsTop -= this.additionalSafeAreaInsets.top;
|
||||
currentInsetsBottom -= this.additionalSafeAreaInsets.bottom;
|
||||
}
|
||||
|
||||
const additionalInsetsTop = Math.max(parentPageInsetsTop - currentInsetsTop, 0);
|
||||
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;
|
||||
} else {
|
||||
this.additionalSafeAreaInsets = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IOSHelper.layoutView(this, owner);
|
||||
}
|
||||
|
||||
public viewWillAppear(animated: boolean): void {
|
||||
|
||||
@@ -254,6 +254,7 @@ function applySelectors<T extends View>(view: T, callback: (view: T) => void) {
|
||||
if (currentPage) {
|
||||
const styleScope = currentPage._styleScope;
|
||||
if (styleScope) {
|
||||
view.parent = currentPage;
|
||||
view._inheritStyleScope(styleScope);
|
||||
view.onLoaded();
|
||||
callback(view);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
import { DialogOptions, ConfirmOptions, PromptOptions, PromptResult, LoginOptions, LoginResult, ActionOptions } from './dialogs-common';
|
||||
import { getLabelColor, getButtonColors, isDialogOptions, inputType, capitalizationType, DialogStrings, parseLoginOptions } from './dialogs-common';
|
||||
import { android as androidApp } from '../../application';
|
||||
import { ad } from '../../utils/native-helper';
|
||||
|
||||
export * from './dialogs-common';
|
||||
|
||||
@@ -12,7 +12,7 @@ function isString(value): value is string {
|
||||
}
|
||||
|
||||
function createAlertDialog(options?: DialogOptions): android.app.AlertDialog.Builder {
|
||||
const alert = new android.app.AlertDialog.Builder(androidApp.foregroundActivity, options.theme ? options.theme : -1);
|
||||
const alert = new android.app.AlertDialog.Builder(ad.getCurrentActivity(), options.theme ? options.theme : -1);
|
||||
alert.setTitle(options && isString(options.title) ? options.title : '');
|
||||
alert.setMessage(options && isString(options.message) ? options.message : '');
|
||||
if (options && options.cancelable === false) {
|
||||
@@ -202,7 +202,7 @@ export function prompt(...args): Promise<PromptResult> {
|
||||
try {
|
||||
const alert = createAlertDialog(options);
|
||||
|
||||
const input = new android.widget.EditText(androidApp.foregroundActivity);
|
||||
const input = new android.widget.EditText(ad.getCurrentActivity());
|
||||
|
||||
if (options) {
|
||||
if (options.inputType === inputType.password) {
|
||||
@@ -257,23 +257,21 @@ export function login(...args: any[]): Promise<LoginResult> {
|
||||
|
||||
return new Promise<LoginResult>((resolve, reject) => {
|
||||
try {
|
||||
const context = androidApp.foregroundActivity;
|
||||
|
||||
const alert = createAlertDialog(options);
|
||||
|
||||
const userNameInput = new android.widget.EditText(context);
|
||||
const userNameInput = new android.widget.EditText(ad.getApplicationContext());
|
||||
|
||||
userNameInput.setHint(options.userNameHint ? options.userNameHint : '');
|
||||
userNameInput.setText(options.userName ? options.userName : '');
|
||||
|
||||
const passwordInput = new android.widget.EditText(context);
|
||||
const passwordInput = new android.widget.EditText(ad.getApplicationContext());
|
||||
passwordInput.setInputType(android.text.InputType.TYPE_CLASS_TEXT | android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD);
|
||||
passwordInput.setTypeface(android.graphics.Typeface.DEFAULT);
|
||||
|
||||
passwordInput.setHint(options.passwordHint ? options.passwordHint : '');
|
||||
passwordInput.setText(options.password ? options.password : '');
|
||||
|
||||
const layout = new android.widget.LinearLayout(context);
|
||||
const layout = new android.widget.LinearLayout(ad.getApplicationContext());
|
||||
layout.setOrientation(1);
|
||||
layout.addView(userNameInput);
|
||||
layout.addView(passwordInput);
|
||||
@@ -324,8 +322,7 @@ export function action(...args): Promise<string> {
|
||||
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
try {
|
||||
const activity = androidApp.foregroundActivity || androidApp.startActivity;
|
||||
const alert = new android.app.AlertDialog.Builder(activity, options.theme ? options.theme : -1);
|
||||
const alert = new android.app.AlertDialog.Builder(ad.getCurrentActivity(), options.theme ? options.theme : -1);
|
||||
const message = options && isString(options.message) ? options.message : '';
|
||||
const title = options && isString(options.title) ? options.title : '';
|
||||
if (options && options.cancelable === false) {
|
||||
|
||||
@@ -51,9 +51,7 @@ placeholderColorProperty.register(Style);
|
||||
|
||||
const keyboardTypeConverter = makeParser<CoreTypes.KeyboardInputType>(makeValidator<CoreTypes.KeyboardInputType>(CoreTypes.KeyboardType.datetime, CoreTypes.KeyboardType.phone, CoreTypes.KeyboardType.number, CoreTypes.KeyboardType.url, CoreTypes.KeyboardType.email, CoreTypes.KeyboardType.integer), true);
|
||||
|
||||
const autofillTypeConverter = makeParser<CoreTypes.AutofillType>(makeValidator<CoreTypes.AutofillType>(CoreTypes.AutofillType.username, CoreTypes.AutofillType.password, CoreTypes.AutofillType.none), true);
|
||||
|
||||
export const autofillTypeProperty = new Property<EditableTextBase, CoreTypes.AutofillType>({ name: 'autofillType', valueConverter: autofillTypeConverter });
|
||||
export const autofillTypeProperty = new Property<EditableTextBase, CoreTypes.AutofillType>({ name: 'autofillType' });
|
||||
autofillTypeProperty.register(EditableTextBase);
|
||||
|
||||
export const keyboardTypeProperty = new Property<EditableTextBase, CoreTypes.KeyboardInputType>({ name: 'keyboardType', valueConverter: keyboardTypeConverter });
|
||||
|
||||
@@ -43,6 +43,14 @@ export class FrameBase extends CustomLayoutView {
|
||||
|
||||
public actionBarVisibility: 'auto' | 'never' | 'always';
|
||||
public _currentEntry: BackstackEntry;
|
||||
|
||||
/**
|
||||
* A reference of current page that is set earlier than current entry.
|
||||
* Using this property, methods like 'eachChildView' and '_childrenCount' gain access to page view
|
||||
* just in time for calls like '_addView' to perform view-tree iterations.
|
||||
*/
|
||||
public _resolvedPage: Page;
|
||||
|
||||
public _animationInProgress = false;
|
||||
public _executingContext: NavigationContext;
|
||||
public _isInFrameStack = false;
|
||||
@@ -238,6 +246,8 @@ export class FrameBase extends CustomLayoutView {
|
||||
// In case we navigated forward to a page that was in the backstack
|
||||
// with clearHistory: true
|
||||
if (!newPage.frame) {
|
||||
this._resolvedPage = newPage;
|
||||
|
||||
this._addView(newPage);
|
||||
newPage._frame = this;
|
||||
}
|
||||
@@ -456,7 +466,6 @@ export class FrameBase extends CustomLayoutView {
|
||||
if (this._currentEntry) {
|
||||
return this._currentEntry.resolvedPage;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -508,7 +517,7 @@ export class FrameBase extends CustomLayoutView {
|
||||
}
|
||||
|
||||
get _childrenCount(): number {
|
||||
if (this.currentPage) {
|
||||
if (this._resolvedPage) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -516,7 +525,7 @@ export class FrameBase extends CustomLayoutView {
|
||||
}
|
||||
|
||||
public eachChildView(callback: (child: View) => boolean) {
|
||||
const page = this.currentPage;
|
||||
const page = this._resolvedPage;
|
||||
if (page) {
|
||||
callback(page);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import { Device } from '../../platform';
|
||||
import { profile } from '../../profiling';
|
||||
import { android as androidApplication } from '../../application';
|
||||
import { setSuspended } from '../../application/application-common';
|
||||
import { ad } from '../../utils/native-helper';
|
||||
|
||||
export * from './frame-common';
|
||||
|
||||
@@ -94,7 +95,7 @@ export class Frame extends FrameBase {
|
||||
}
|
||||
|
||||
public static reloadPage(context?: ModuleContext): void {
|
||||
const activity = application.android.foregroundActivity;
|
||||
const activity = ad.getCurrentActivity();
|
||||
const callbacks: AndroidActivityCallbacks = activity[CALLBACKS];
|
||||
if (callbacks) {
|
||||
const rootView: View = callbacks.getRootView();
|
||||
@@ -147,7 +148,8 @@ export class Frame extends FrameBase {
|
||||
|
||||
// _onAttachedToWindow called from OS again after it was detach
|
||||
// still happens with androidx.fragment:1.3.2
|
||||
const lifecycleState = (androidApplication.foregroundActivity?.getLifecycle?.() || androidApplication.startActivity?.getLifecycle?.())?.getCurrentState() || androidx.lifecycle.Lifecycle.State.CREATED;
|
||||
const activity = ad.getCurrentActivity();
|
||||
const lifecycleState = activity?.getLifecycle?.()?.getCurrentState() || androidx.lifecycle.Lifecycle.State.CREATED;
|
||||
if ((this._manager && this._manager.isDestroyed()) || !lifecycleState.isAtLeast(androidx.lifecycle.Lifecycle.State.CREATED)) {
|
||||
return;
|
||||
}
|
||||
@@ -595,7 +597,7 @@ export function reloadPage(context?: ModuleContext): void {
|
||||
}
|
||||
|
||||
// attach on global, so it can be overwritten in NativeScript Angular
|
||||
(<any>global).__onLiveSyncCore = Frame.reloadPage;
|
||||
global.__onLiveSyncCore = Frame.reloadPage;
|
||||
|
||||
function cloneExpandedTransitionListener(expandedTransitionListener: any) {
|
||||
if (!expandedTransitionListener) {
|
||||
@@ -913,6 +915,8 @@ class FragmentCallbacksImplementation implements AndroidFragmentCallbacks {
|
||||
return null;
|
||||
}
|
||||
|
||||
frame._resolvedPage = page;
|
||||
|
||||
if (page.parent === frame) {
|
||||
// If we are navigating to a page that was destroyed
|
||||
// reinitialize its UI.
|
||||
@@ -920,6 +924,10 @@ class FragmentCallbacksImplementation implements AndroidFragmentCallbacks {
|
||||
const context = (container && container.getContext()) || (inflater && inflater.getContext());
|
||||
page._setupUI(context);
|
||||
}
|
||||
|
||||
if (frame.isLoaded && !page.isLoaded) {
|
||||
page.callLoaded();
|
||||
}
|
||||
} else {
|
||||
if (!frame._styleScope) {
|
||||
// Make sure page will have styleScope even if parents don't.
|
||||
@@ -929,10 +937,6 @@ class FragmentCallbacksImplementation implements AndroidFragmentCallbacks {
|
||||
frame._addView(page);
|
||||
}
|
||||
|
||||
if (frame.isLoaded && !page.isLoaded) {
|
||||
page.callLoaded();
|
||||
}
|
||||
|
||||
const savedState = entry.viewSavedState;
|
||||
if (savedState) {
|
||||
(<android.view.View>page.nativeViewProtected).restoreHierarchyState(savedState);
|
||||
|
||||
@@ -39,6 +39,7 @@ export class Label extends TextBase implements LabelDefinition {
|
||||
const textView = this.nativeTextViewProtected;
|
||||
textView.setSingleLine(true);
|
||||
textView.setEllipsize(android.text.TextUtils.TruncateAt.END);
|
||||
textView.setGravity(android.view.Gravity.CENTER_VERTICAL);
|
||||
}
|
||||
|
||||
[whiteSpaceProperty.setNative](value: CoreTypes.WhiteSpaceType) {
|
||||
|
||||
@@ -42,7 +42,9 @@ export class RootLayoutBase extends GridLayout {
|
||||
|
||||
// ability to add any view instance to composite views like layers
|
||||
open(view: View, options: RootLayoutOptions = {}): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const enterAnimationDefinition = options.animation ? options.animation.enterFrom : null;
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
if (!(view instanceof View)) {
|
||||
return reject(new Error(`Invalid open view: ${view}`));
|
||||
}
|
||||
@@ -51,46 +53,57 @@ export class RootLayoutBase extends GridLayout {
|
||||
return reject(new Error(`${view} has already been added`));
|
||||
}
|
||||
|
||||
const enterAnimationDefinition = options.animation ? options.animation.enterFrom : null;
|
||||
resolve();
|
||||
})
|
||||
.then(() => {
|
||||
// keep track of the views locally to be able to use their options later
|
||||
this.popupViews.push({ view: view, options: options });
|
||||
|
||||
// keep track of the views locally to be able to use their options later
|
||||
this.popupViews.push({ view: view, options: options });
|
||||
|
||||
if (options.shadeCover) {
|
||||
// perf optimization note: we only need 1 layer of shade cover
|
||||
// we just update properties if needed by additional overlaid views
|
||||
if (this.shadeCover) {
|
||||
// overwrite current shadeCover options if topmost popupview has additional shadeCover configurations
|
||||
this.updateShadeCover(this.shadeCover, options.shadeCover);
|
||||
} else {
|
||||
this.openShadeCover(options.shadeCover);
|
||||
if (options.shadeCover) {
|
||||
// perf optimization note: we only need 1 layer of shade cover
|
||||
// we just update properties if needed by additional overlaid views
|
||||
if (this.shadeCover) {
|
||||
// overwrite current shadeCover options if topmost popupview has additional shadeCover configurations
|
||||
return this.updateShadeCover(this.shadeCover, options.shadeCover);
|
||||
}
|
||||
return this.openShadeCover(options.shadeCover);
|
||||
}
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
view.opacity = 0; // always begin with view invisible when adding dynamically
|
||||
this.insertChild(view, this.getChildrenCount() + 1);
|
||||
|
||||
view.opacity = 0; // always begin with view invisible when adding dynamically
|
||||
this.insertChild(view, this.getChildrenCount() + 1);
|
||||
|
||||
setTimeout(() => {
|
||||
// only apply initial state and animate after the first tick - ensures safe areas and other measurements apply correctly
|
||||
this.applyInitialState(view, enterAnimationDefinition);
|
||||
this.getEnterAnimation(view, enterAnimationDefinition)
|
||||
.play()
|
||||
.then(() => {
|
||||
this.applyDefaultState(view);
|
||||
view.notify({ eventName: 'opened', object: view });
|
||||
resolve();
|
||||
})
|
||||
.catch((ex) => {
|
||||
reject(new Error(`Error playing enter animation: ${ex}`));
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
// only apply initial state and animate after the first tick - ensures safe areas and other measurements apply correctly
|
||||
this.applyInitialState(view, enterAnimationDefinition);
|
||||
this.getEnterAnimation(view, enterAnimationDefinition)
|
||||
.play()
|
||||
.then(() => {
|
||||
this.applyDefaultState(view);
|
||||
view.notify({ eventName: 'opened', object: view });
|
||||
resolve();
|
||||
})
|
||||
.catch((ex) => {
|
||||
reject(new Error(`Error playing enter animation: ${ex}`));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// optional animation parameter to overwrite close animation declared when opening popup
|
||||
// ability to remove any view instance from composite views
|
||||
close(view: View, exitTo?: TransitionAnimation): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const cleanupAndFinish = () => {
|
||||
view.notify({ eventName: 'closed', object: view });
|
||||
this.removeChild(view);
|
||||
};
|
||||
|
||||
// use exitAnimation that is passed in and fallback to the exitAnimation passed in when opening
|
||||
let exitAnimationDefinition = exitTo;
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
if (!(view instanceof View)) {
|
||||
return reject(new Error(`Invalid close view: ${view}`));
|
||||
}
|
||||
@@ -99,51 +112,51 @@ export class RootLayoutBase extends GridLayout {
|
||||
return reject(new Error(`Unable to close popup. ${view} not found`));
|
||||
}
|
||||
|
||||
const popupIndex = this.getPopupIndex(view);
|
||||
const poppedView = this.popupViews[popupIndex];
|
||||
const cleanupAndFinish = () => {
|
||||
view.notify({ eventName: 'closed', object: view });
|
||||
this.removeChild(view);
|
||||
resolve();
|
||||
};
|
||||
// use exitAnimation that is passed in and fallback to the exitAnimation passed in when opening
|
||||
const exitAnimationDefinition = exitTo || poppedView?.options?.animation?.exitTo;
|
||||
resolve();
|
||||
})
|
||||
.then(() => {
|
||||
const popupIndex = this.getPopupIndex(view);
|
||||
const poppedView = this.popupViews[popupIndex];
|
||||
|
||||
// Remove view from tracked popupviews
|
||||
this.popupViews.splice(popupIndex, 1);
|
||||
if (!exitAnimationDefinition) {
|
||||
exitAnimationDefinition = poppedView?.options?.animation?.exitTo;
|
||||
}
|
||||
|
||||
if (this.shadeCover) {
|
||||
// update shade cover with the topmost popupView options (if not specifically told to ignore)
|
||||
if (!poppedView?.options?.shadeCover?.ignoreShadeRestore) {
|
||||
const shadeCoverOptions = this.popupViews[this.popupViews.length - 1]?.options?.shadeCover;
|
||||
if (shadeCoverOptions) {
|
||||
this.updateShadeCover(this.shadeCover, shadeCoverOptions);
|
||||
// Remove view from tracked popupviews
|
||||
this.popupViews.splice(popupIndex, 1);
|
||||
|
||||
if (this.shadeCover) {
|
||||
// update shade cover with the topmost popupView options (if not specifically told to ignore)
|
||||
if (!poppedView?.options?.shadeCover?.ignoreShadeRestore) {
|
||||
const shadeCoverOptions = this.popupViews[this.popupViews.length - 1]?.options?.shadeCover;
|
||||
if (shadeCoverOptions) {
|
||||
return this.updateShadeCover(this.shadeCover, shadeCoverOptions);
|
||||
}
|
||||
}
|
||||
// remove shade cover animation if this is the last opened popup view
|
||||
if (this.popupViews.length === 0) {
|
||||
return this.closeShadeCover(poppedView?.options?.shadeCover);
|
||||
}
|
||||
}
|
||||
// remove shade cover animation if this is the last opened popup view
|
||||
if (this.popupViews.length === 0) {
|
||||
this.closeShadeCover(poppedView?.options?.shadeCover);
|
||||
})
|
||||
.then(() => {
|
||||
if (exitAnimationDefinition) {
|
||||
return this.getExitAnimation(view, exitAnimationDefinition)
|
||||
.play()
|
||||
.then(cleanupAndFinish.bind(this))
|
||||
.catch((ex) => Promise.reject(new Error(`Error playing exit animation: ${ex}`)));
|
||||
}
|
||||
}
|
||||
|
||||
if (exitAnimationDefinition) {
|
||||
this.getExitAnimation(view, exitAnimationDefinition)
|
||||
.play()
|
||||
.then(cleanupAndFinish.bind(this))
|
||||
.catch((ex) => {
|
||||
reject(new Error(`Error playing exit animation: ${ex}`));
|
||||
});
|
||||
} else {
|
||||
cleanupAndFinish();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
closeAll(): Promise<void[]> {
|
||||
const toClose = [];
|
||||
const views = this.popupViews.map((popupView) => popupView.view);
|
||||
|
||||
// Close all views at the same time and wait for all of them
|
||||
while (this.popupViews.length > 0) {
|
||||
toClose.push(this.close(this.popupViews[this.popupViews.length - 1].view));
|
||||
for (const view of views) {
|
||||
toClose.push(this.close(view));
|
||||
}
|
||||
return Promise.all(toClose);
|
||||
}
|
||||
@@ -342,7 +355,7 @@ export class RootLayoutBase extends GridLayout {
|
||||
return shadeCover;
|
||||
}
|
||||
|
||||
private updateShadeCover(shade: View, shadeOptions: ShadeCoverOptions = {}): void {
|
||||
private updateShadeCover(shade: View, shadeOptions: ShadeCoverOptions = {}): Promise<void> {
|
||||
if (shadeOptions.tapToClose !== undefined && shadeOptions.tapToClose !== null) {
|
||||
shade.off('tap');
|
||||
if (shadeOptions.tapToClose) {
|
||||
@@ -351,7 +364,7 @@ export class RootLayoutBase extends GridLayout {
|
||||
});
|
||||
}
|
||||
}
|
||||
this._updateShadeCover(shade, shadeOptions);
|
||||
return this._updateShadeCover(shade, shadeOptions);
|
||||
}
|
||||
|
||||
private hasChild(view: View): boolean {
|
||||
|
||||
@@ -74,7 +74,6 @@ class UIViewControllerImpl extends UIViewController {
|
||||
|
||||
public isBackstackSkipped: boolean;
|
||||
public isBackstackCleared: boolean;
|
||||
private didFirstLayout: boolean;
|
||||
// this is initialized in initWithOwner since the constructor doesn't run on native classes
|
||||
private _isRunningLayout: number;
|
||||
private get isRunningLayout() {
|
||||
@@ -85,7 +84,7 @@ class UIViewControllerImpl extends UIViewController {
|
||||
}
|
||||
private finishRunningLayout() {
|
||||
this._isRunningLayout--;
|
||||
this.didFirstLayout = true;
|
||||
this.clearScheduledLayout();
|
||||
}
|
||||
private runLayout(cb: () => void) {
|
||||
try {
|
||||
@@ -96,11 +95,31 @@ class UIViewControllerImpl extends UIViewController {
|
||||
}
|
||||
}
|
||||
|
||||
layoutTimer: number;
|
||||
|
||||
private clearScheduledLayout() {
|
||||
if (this.layoutTimer) {
|
||||
clearTimeout(this.layoutTimer);
|
||||
this.layoutTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
private scheduleLayout() {
|
||||
if (this.layoutTimer) {
|
||||
return;
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.layoutTimer = null;
|
||||
if (!this.isRunningLayout) {
|
||||
this.runLayout(() => this.layoutOwner());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static initWithOwner(owner: WeakRef<Page>): UIViewControllerImpl {
|
||||
const controller = <UIViewControllerImpl>UIViewControllerImpl.new();
|
||||
controller._owner = owner;
|
||||
controller._isRunningLayout = 0;
|
||||
controller.didFirstLayout = false;
|
||||
|
||||
return controller;
|
||||
}
|
||||
@@ -120,7 +139,7 @@ class UIViewControllerImpl extends UIViewController {
|
||||
return;
|
||||
}
|
||||
|
||||
const frame = this.navigationController ? (<any>this.navigationController).owner : null;
|
||||
const frame: Frame = this.navigationController ? (<any>this.navigationController).owner : null;
|
||||
const newEntry = this[ENTRY];
|
||||
|
||||
// Don't raise event if currentPage was showing modal page.
|
||||
@@ -130,6 +149,8 @@ class UIViewControllerImpl extends UIViewController {
|
||||
}
|
||||
|
||||
if (frame) {
|
||||
frame._resolvedPage = owner;
|
||||
|
||||
if (!owner.parent) {
|
||||
owner._frame = frame;
|
||||
if (!frame._styleScope) {
|
||||
@@ -279,71 +300,77 @@ class UIViewControllerImpl extends UIViewController {
|
||||
|
||||
public viewSafeAreaInsetsDidChange(): void {
|
||||
super.viewSafeAreaInsetsDidChange();
|
||||
if (this.isRunningLayout || !this.didFirstLayout) {
|
||||
return;
|
||||
}
|
||||
const owner = this._owner?.deref();
|
||||
if (owner) {
|
||||
this.runLayout(() => IOSHelper.layoutView(this, owner));
|
||||
}
|
||||
this.scheduleLayout();
|
||||
}
|
||||
|
||||
public viewDidLayoutSubviews(): void {
|
||||
this.startRunningLayout();
|
||||
super.viewDidLayoutSubviews();
|
||||
this.layoutOwner();
|
||||
this.finishRunningLayout();
|
||||
}
|
||||
|
||||
layoutOwner(force = false) {
|
||||
const owner = this._owner?.deref();
|
||||
if (owner) {
|
||||
// layout(owner.actionBar)
|
||||
// layout(owner.content)
|
||||
if (!owner) {
|
||||
return;
|
||||
}
|
||||
if (!force && owner.isLayoutValid && !owner.nativeViewProtected?.layer.needsLayout?.()) {
|
||||
// we skip layout if the view is not yet laid out yet
|
||||
// this usually means that viewDidLayoutSubviews will be called again
|
||||
// so doing a layout pass now will layout with the wrong parameters
|
||||
return;
|
||||
}
|
||||
|
||||
if (majorVersion >= 11) {
|
||||
// Handle nested Page safe area insets application.
|
||||
// A Page is nested if its Frame has a parent.
|
||||
// If the Page is nested, cross check safe area insets on top and bottom with Frame parent.
|
||||
const frame = owner.parent;
|
||||
// There is a legacy scenario where Page is not in a Frame - the root of a Modal View, so it has no parent.
|
||||
let frameParent = frame && frame.parent;
|
||||
// layout(owner.actionBar)
|
||||
// layout(owner.content)
|
||||
|
||||
// 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 UIViewControllerImpl out of page module
|
||||
while (frameParent && !frameParent.nativeViewProtected) {
|
||||
frameParent = frameParent.parent;
|
||||
}
|
||||
if (majorVersion >= 11) {
|
||||
// Handle nested Page safe area insets application.
|
||||
// A Page is nested if its Frame has a parent.
|
||||
// If the Page is nested, cross check safe area insets on top and bottom with Frame parent.
|
||||
const frame = owner.parent;
|
||||
// There is a legacy scenario where Page is not in a Frame - the root of a Modal View, so it has no parent.
|
||||
let frameParent = frame && frame.parent;
|
||||
|
||||
if (frameParent) {
|
||||
const parentPageInsetsTop = frameParent.nativeViewProtected.safeAreaInsets.top;
|
||||
const parentPageInsetsBottom = frameParent.nativeViewProtected.safeAreaInsets.bottom;
|
||||
let currentInsetsTop = this.view.safeAreaInsets.top;
|
||||
let currentInsetsBottom = this.view.safeAreaInsets.bottom;
|
||||
|
||||
// Safe area insets include additional safe area insets too, so subtract old values
|
||||
if (this.additionalSafeAreaInsets) {
|
||||
currentInsetsTop -= this.additionalSafeAreaInsets.top;
|
||||
currentInsetsBottom -= this.additionalSafeAreaInsets.bottom;
|
||||
}
|
||||
|
||||
const additionalInsetsTop = Math.max(parentPageInsetsTop - currentInsetsTop, 0);
|
||||
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;
|
||||
} else {
|
||||
this.additionalSafeAreaInsets = null;
|
||||
}
|
||||
}
|
||||
// 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 UIViewControllerImpl out of page module
|
||||
while (frameParent && !frameParent.nativeViewProtected) {
|
||||
frameParent = frameParent.parent;
|
||||
}
|
||||
|
||||
IOSHelper.layoutView(this, owner);
|
||||
if (frameParent) {
|
||||
const parentPageInsetsTop = frameParent.nativeViewProtected.safeAreaInsets.top;
|
||||
const parentPageInsetsBottom = frameParent.nativeViewProtected.safeAreaInsets.bottom;
|
||||
let currentInsetsTop = this.view.safeAreaInsets.top;
|
||||
let currentInsetsBottom = this.view.safeAreaInsets.bottom;
|
||||
|
||||
// Safe area insets include additional safe area insets too, so subtract old values
|
||||
if (this.additionalSafeAreaInsets) {
|
||||
currentInsetsTop -= this.additionalSafeAreaInsets.top;
|
||||
currentInsetsBottom -= this.additionalSafeAreaInsets.bottom;
|
||||
}
|
||||
|
||||
const additionalInsetsTop = Math.max(parentPageInsetsTop - currentInsetsTop, 0);
|
||||
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;
|
||||
} else {
|
||||
this.additionalSafeAreaInsets = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.finishRunningLayout();
|
||||
|
||||
IOSHelper.layoutView(this, owner);
|
||||
}
|
||||
|
||||
// Mind implementation for other controllerss
|
||||
|
||||
@@ -33,6 +33,10 @@ export namespace ios {
|
||||
const background = view.style.backgroundInternal;
|
||||
const nativeView = <NativeScriptUIView>view.nativeViewProtected;
|
||||
|
||||
if (!nativeView) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (background.clearFlags & BackgroundClearFlags.CLEAR_BOX_SHADOW) {
|
||||
// clear box shadow if it has been removed!
|
||||
view.setProperty('clipToBounds', true);
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import { KeyframeAnimationInfo, KeyframeDeclaration, KeyframeInfo, UnparsedKeyframe } from '../animation/keyframe-animation';
|
||||
|
||||
export class CssAnimationParser {
|
||||
public static keyframeAnimationsFromCSSDeclarations(declarations: KeyframeDeclaration[]): KeyframeAnimationInfo[];
|
||||
|
||||
public static keyframesArrayFromCSS(keyframes: UnparsedKeyframe[]): KeyframeInfo[];
|
||||
}
|
||||
|
||||
export function parseKeyframeDeclarations(unparsedKeyframeDeclarations: KeyframeDeclaration[]): KeyframeDeclaration[];
|
||||
523
packages/core/ui/styling/css-animation-parser.spec.ts
Normal file
523
packages/core/ui/styling/css-animation-parser.spec.ts
Normal file
@@ -0,0 +1,523 @@
|
||||
import { CoreTypes } from '../../core-types';
|
||||
import type { KeyframeAnimationInfo, KeyframeInfo } from '../animation';
|
||||
import { CssAnimationParser, keyframeAnimationsFromCSSProperty } from './css-animation-parser';
|
||||
import { cssTreeParse } from '../../css/css-tree-parser';
|
||||
|
||||
describe('css-animation-parser', () => {
|
||||
describe('shorthand-property-parser', () => {
|
||||
// helper functions
|
||||
function testSingleAnimation(css: string): KeyframeAnimationInfo {
|
||||
const animations: KeyframeAnimationInfo[] = [];
|
||||
keyframeAnimationsFromCSSProperty(css, animations);
|
||||
|
||||
return animations[0];
|
||||
}
|
||||
|
||||
function testMultipleAnimations(css: string): KeyframeAnimationInfo[] {
|
||||
const animations: KeyframeAnimationInfo[] = [];
|
||||
keyframeAnimationsFromCSSProperty(css, animations);
|
||||
|
||||
return animations;
|
||||
}
|
||||
|
||||
it('empty', () => {
|
||||
const animation = testSingleAnimation('');
|
||||
expect(animation).toBeUndefined();
|
||||
});
|
||||
|
||||
// times to test for
|
||||
const times = {
|
||||
'0s': 0,
|
||||
'0ms': 0,
|
||||
'250ms': 250,
|
||||
'0.5s': 500,
|
||||
'1500ms': 1500,
|
||||
'1s': 1000,
|
||||
'3s': 3000,
|
||||
};
|
||||
|
||||
const curves = {
|
||||
ease: CoreTypes.AnimationCurve.ease,
|
||||
linear: CoreTypes.AnimationCurve.linear,
|
||||
'ease-in': CoreTypes.AnimationCurve.easeIn,
|
||||
'ease-out': CoreTypes.AnimationCurve.easeOut,
|
||||
'ease-in-out': CoreTypes.AnimationCurve.easeInOut,
|
||||
spring: CoreTypes.AnimationCurve.spring,
|
||||
'cubic-bezier(0.1, 1.0, 0.5, 0.5)': CoreTypes.AnimationCurve.cubicBezier(0.1, 1.0, 0.5, 0.5),
|
||||
'cubic-bezier(0.42, 0.0, 1.0, 1.0);': CoreTypes.AnimationCurve.cubicBezier(0.42, 0.0, 1.0, 1.0),
|
||||
};
|
||||
|
||||
it('parses duration', () => {
|
||||
Object.entries(times).forEach(([timeString, ms]) => {
|
||||
expect(testSingleAnimation(`${timeString}`).duration).toBe(ms);
|
||||
});
|
||||
});
|
||||
|
||||
it('parses delay', () => {
|
||||
Object.entries(times).forEach(([timeString, ms]) => {
|
||||
const animation = testSingleAnimation(`0s ${timeString}`);
|
||||
expect(animation.duration).toBe(0);
|
||||
expect(animation.delay).toBe(ms);
|
||||
});
|
||||
});
|
||||
|
||||
it('parses duration and delay', () => {
|
||||
Object.entries(times).forEach(([timeString, ms]) => {
|
||||
const animation = testSingleAnimation(`${timeString} ${timeString}`);
|
||||
expect(animation.duration).toBe(ms);
|
||||
expect(animation.delay).toBe(ms);
|
||||
});
|
||||
});
|
||||
|
||||
it('parses curve', () => {
|
||||
Object.entries(curves).forEach(([curveString, curve]) => {
|
||||
const animation = testSingleAnimation(`${curveString}`);
|
||||
expect(animation.curve).toEqual(curve);
|
||||
});
|
||||
});
|
||||
|
||||
it('parses duration, curve and delay', () => {
|
||||
Object.entries(curves).forEach(([curveString, curve]) => {
|
||||
const animation1 = testSingleAnimation(`225ms 300ms ${curveString}`);
|
||||
expect(animation1.duration).toBe(225);
|
||||
expect(animation1.delay).toBe(300);
|
||||
expect(animation1.curve).toEqual(curve);
|
||||
|
||||
// curve and delay can be swapped
|
||||
const animation2 = testSingleAnimation(`225ms ${curveString} 300ms`);
|
||||
expect(animation2.duration).toBe(225);
|
||||
expect(animation2.delay).toBe(300);
|
||||
expect(animation2.curve).toEqual(curve);
|
||||
});
|
||||
});
|
||||
|
||||
it('parses iteration count', () => {
|
||||
expect(testSingleAnimation(`0s 0s ease 2`).iterations).toBe(2);
|
||||
expect(testSingleAnimation(`0s 0s ease 2.5`).iterations).toBe(2.5);
|
||||
expect(testSingleAnimation(`0s 0s ease infinite`).iterations).toBe(Infinity);
|
||||
expect(testSingleAnimation(`2`).iterations).toBe(2);
|
||||
expect(testSingleAnimation(`2.5`).iterations).toBe(2.5);
|
||||
expect(testSingleAnimation(`infinite`).iterations).toBe(Infinity);
|
||||
expect(testSingleAnimation(`1s 2`).iterations).toBe(2);
|
||||
expect(testSingleAnimation(`1s 2.5`).iterations).toBe(2.5);
|
||||
expect(testSingleAnimation(`1s infinite`).iterations).toBe(Infinity);
|
||||
expect(testSingleAnimation(`ease 2`).iterations).toBe(2);
|
||||
expect(testSingleAnimation(`ease 2.5`).iterations).toBe(2.5);
|
||||
expect(testSingleAnimation(`ease infinite`).iterations).toBe(Infinity);
|
||||
});
|
||||
|
||||
it('parses direction', () => {
|
||||
expect(testSingleAnimation(`1s`).isReverse).toBe(false);
|
||||
expect(testSingleAnimation(`1s normal`).isReverse).toBe(false);
|
||||
expect(testSingleAnimation(`1s reverse`).isReverse).toBe(true);
|
||||
expect(testSingleAnimation(`1s 1s reverse`).isReverse).toBe(true);
|
||||
expect(testSingleAnimation(`1s 1s ease reverse`).isReverse).toBe(true);
|
||||
expect(testSingleAnimation(`1s 1s ease 2 reverse`).isReverse).toBe(true);
|
||||
expect(testSingleAnimation(`1s 1s ease infinite reverse`).isReverse).toBe(true);
|
||||
expect(testSingleAnimation(`1s ease reverse`).isReverse).toBe(true);
|
||||
expect(testSingleAnimation(`1s ease 1s reverse`).isReverse).toBe(true);
|
||||
expect(testSingleAnimation(`1s ease 1s 2 reverse`).isReverse).toBe(true);
|
||||
|
||||
// unsupported values should still work
|
||||
expect(testSingleAnimation(`1s alternate`).isReverse).toBe(false);
|
||||
expect(testSingleAnimation(`1s alternate-reverse`).isReverse).toBe(false);
|
||||
});
|
||||
|
||||
it('parses fill-mode', () => {
|
||||
expect(testSingleAnimation(`1s`).isForwards).toBe(false);
|
||||
expect(testSingleAnimation(`1s none`).isForwards).toBe(false);
|
||||
expect(testSingleAnimation(`1s backwards`).isForwards).toBe(false);
|
||||
|
||||
expect(testSingleAnimation(`1s both`).isForwards).toBe(true);
|
||||
expect(testSingleAnimation(`1s forwards`).isForwards).toBe(true);
|
||||
expect(testSingleAnimation(`1s 1s forwards`).isForwards).toBe(true);
|
||||
expect(testSingleAnimation(`1s 1s ease forwards`).isForwards).toBe(true);
|
||||
expect(testSingleAnimation(`1s 1s ease 2 forwards`).isForwards).toBe(true);
|
||||
expect(testSingleAnimation(`1s 1s ease infinite forwards`).isForwards).toBe(true);
|
||||
expect(testSingleAnimation(`1s ease forwards`).isForwards).toBe(true);
|
||||
expect(testSingleAnimation(`1s ease 1s forwards`).isForwards).toBe(true);
|
||||
});
|
||||
|
||||
it('parses play-state', () => {
|
||||
// TODO: implement play-state?
|
||||
|
||||
expect(testSingleAnimation(`1s`)).not.toBeUndefined();
|
||||
expect(testSingleAnimation(`1s running`)).not.toBeUndefined();
|
||||
expect(testSingleAnimation(`1s paused`)).not.toBeUndefined();
|
||||
expect(testSingleAnimation(`1s 1s paused`)).not.toBeUndefined();
|
||||
expect(testSingleAnimation(`1s 1s ease paused`)).not.toBeUndefined();
|
||||
expect(testSingleAnimation(`1s 1s ease 2 paused`)).not.toBeUndefined();
|
||||
expect(testSingleAnimation(`1s 1s ease infinite paused`)).not.toBeUndefined();
|
||||
expect(testSingleAnimation(`1s ease paused`)).not.toBeUndefined();
|
||||
expect(testSingleAnimation(`1s ease 1s paused`)).not.toBeUndefined();
|
||||
});
|
||||
|
||||
it('parses animation name', () => {
|
||||
expect(testSingleAnimation(`1s`).name).toBe('');
|
||||
expect(testSingleAnimation(`1s fade`).name).toBe('fade');
|
||||
expect(testSingleAnimation(`1s 'fade'`).name).toBe('fade');
|
||||
expect(testSingleAnimation(`1s "fade"`).name).toBe('fade');
|
||||
|
||||
expect(testSingleAnimation(`1s fade-in`).name).toBe('fade-in');
|
||||
expect(testSingleAnimation(`1s 'fade-in'`).name).toBe('fade-in');
|
||||
expect(testSingleAnimation(`1s "fade-in"`).name).toBe('fade-in');
|
||||
|
||||
expect(testSingleAnimation(`1s fade_in`).name).toBe('fade_in');
|
||||
expect(testSingleAnimation(`1s 'fade_in'`).name).toBe('fade_in');
|
||||
expect(testSingleAnimation(`1s "fade_in"`).name).toBe('fade_in');
|
||||
});
|
||||
|
||||
it('parses MDN example: 3s ease-in 1s 2 reverse both paused slidein', () => {
|
||||
const animation = testSingleAnimation(`3s ease-in 1s 2 reverse both paused slidein`);
|
||||
expect(animation.duration).toBe(3000);
|
||||
expect(animation.delay).toBe(1000);
|
||||
expect(animation.curve).toBe(CoreTypes.AnimationCurve.easeIn);
|
||||
expect(animation.iterations).toBe(2);
|
||||
expect(animation.isReverse).toBe(true);
|
||||
expect(animation.isForwards).toBe(true);
|
||||
expect(animation.name).toBe('slidein');
|
||||
});
|
||||
|
||||
it('parses MDN example: 3s linear 1s slidein', () => {
|
||||
const animation = testSingleAnimation(`3s linear 1s slidein`);
|
||||
expect(animation.duration).toBe(3000);
|
||||
expect(animation.delay).toBe(1000);
|
||||
expect(animation.curve).toBe(CoreTypes.AnimationCurve.linear);
|
||||
expect(animation.name).toBe('slidein');
|
||||
});
|
||||
|
||||
it('parses MDN example: 3s linear slidein, 3s ease-out 5s slideout', () => {
|
||||
const [animation1, animation2] = testMultipleAnimations(`3s linear slidein, 3s ease-out 5s slideout`);
|
||||
|
||||
expect(animation1.duration).toBe(3000);
|
||||
expect(animation1.curve).toBe(CoreTypes.AnimationCurve.linear);
|
||||
expect(animation1.name).toBe('slidein');
|
||||
|
||||
expect(animation2.duration).toBe(3000);
|
||||
expect(animation2.delay).toBe(5000);
|
||||
expect(animation2.curve).toBe(CoreTypes.AnimationCurve.easeOut);
|
||||
expect(animation2.name).toBe('slideout');
|
||||
});
|
||||
|
||||
it('parses SPEC example: 3s none backwards', () => {
|
||||
const animation = testSingleAnimation(`3s none backwards`);
|
||||
expect(animation.duration).toBe(3000);
|
||||
expect(animation.isForwards).toBe(false);
|
||||
expect(animation.name).toBe('backwards');
|
||||
});
|
||||
|
||||
it('parses SPEC example: 3s backwards', () => {
|
||||
const animation = testSingleAnimation(`3s backwards`);
|
||||
expect(animation.duration).toBe(3000);
|
||||
expect(animation.isForwards).toBe(false);
|
||||
expect(animation.name).toBe('');
|
||||
});
|
||||
|
||||
it('does not throw on invalid values', () => {
|
||||
// prettier-ignore
|
||||
const invalidValues = [
|
||||
'asd',
|
||||
'$#-1401;lk',
|
||||
'1 1 1 1 1 1 1 1 1 1',
|
||||
'1s 1s 1s 1s',
|
||||
',,,,',
|
||||
'$,1s-_1:s>',
|
||||
Infinity.toString(),
|
||||
NaN.toString(),
|
||||
null,
|
||||
undefined
|
||||
];
|
||||
|
||||
invalidValues.forEach((value) => {
|
||||
expect(() => testSingleAnimation(value)).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('keyframe-parser', () => {
|
||||
// helper function
|
||||
function testKeyframesArrayFromCSS(css: string, expectedName?: string): KeyframeInfo[] {
|
||||
const ast = cssTreeParse(css, 'test.css');
|
||||
const rules = ast.stylesheet.rules;
|
||||
const firstRule = rules[0];
|
||||
|
||||
expect(rules.length).toBe(1);
|
||||
expect(firstRule.type).toBe('keyframes');
|
||||
|
||||
const name = firstRule.name;
|
||||
const keyframes = firstRule.keyframes;
|
||||
|
||||
if (expectedName) {
|
||||
expect(name).toBe(expectedName);
|
||||
}
|
||||
|
||||
return CssAnimationParser.keyframesArrayFromCSS(keyframes);
|
||||
}
|
||||
|
||||
it('parses "from" keyframes', () => {
|
||||
const res = testKeyframesArrayFromCSS(
|
||||
`@keyframes fade {
|
||||
from { opacity: 0; }
|
||||
}`,
|
||||
'fade'
|
||||
);
|
||||
|
||||
expect(res.length).toBe(1);
|
||||
|
||||
const [from] = res;
|
||||
expect(from.duration).toBe(0);
|
||||
expect(from.declarations.length).toBe(1);
|
||||
expect(from.declarations[0].property).toBe('opacity');
|
||||
expect(from.declarations[0].value).toBe(0);
|
||||
});
|
||||
|
||||
it('parses "to" keyframes', () => {
|
||||
const res = testKeyframesArrayFromCSS(
|
||||
`@keyframes fade {
|
||||
to { opacity: 1; }
|
||||
}`,
|
||||
'fade'
|
||||
);
|
||||
|
||||
expect(res.length).toBe(1);
|
||||
|
||||
const [to] = res;
|
||||
expect(to.duration).toBe(1);
|
||||
expect(to.declarations.length).toBe(1);
|
||||
expect(to.declarations[0].property).toBe('opacity');
|
||||
expect(to.declarations[0].value).toBe(1);
|
||||
});
|
||||
|
||||
it('parses "from/to" keyframes', () => {
|
||||
const res = testKeyframesArrayFromCSS(
|
||||
`@keyframes fade {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}`,
|
||||
'fade'
|
||||
);
|
||||
|
||||
expect(res.length).toBe(2);
|
||||
|
||||
const [from, to] = res;
|
||||
expect(from.duration).toBe(0);
|
||||
expect(from.declarations.length).toBe(1);
|
||||
expect(from.declarations[0].property).toBe('opacity');
|
||||
expect(from.declarations[0].value).toBe(0);
|
||||
|
||||
expect(to.duration).toBe(1);
|
||||
expect(to.declarations.length).toBe(1);
|
||||
expect(to.declarations[0].property).toBe('opacity');
|
||||
expect(to.declarations[0].value).toBe(1);
|
||||
});
|
||||
|
||||
it('parses "0%" keyframes', () => {
|
||||
const res = testKeyframesArrayFromCSS(
|
||||
`@keyframes fade {
|
||||
0% { opacity: 0; }
|
||||
}`,
|
||||
'fade'
|
||||
);
|
||||
|
||||
expect(res.length).toBe(1);
|
||||
|
||||
const [from] = res;
|
||||
expect(from.duration).toBe(0);
|
||||
expect(from.declarations.length).toBe(1);
|
||||
expect(from.declarations[0].property).toBe('opacity');
|
||||
expect(from.declarations[0].value).toBe(0);
|
||||
});
|
||||
|
||||
it('parses "100%" keyframes', () => {
|
||||
const res = testKeyframesArrayFromCSS(
|
||||
`@keyframes fade {
|
||||
100% { opacity: 1; }
|
||||
}`,
|
||||
'fade'
|
||||
);
|
||||
|
||||
expect(res.length).toBe(1);
|
||||
|
||||
const [to] = res;
|
||||
expect(to.duration).toBe(1);
|
||||
expect(to.declarations.length).toBe(1);
|
||||
expect(to.declarations[0].property).toBe('opacity');
|
||||
expect(to.declarations[0].value).toBe(1);
|
||||
});
|
||||
|
||||
it('parses "0%/100%" keyframes', () => {
|
||||
const res = testKeyframesArrayFromCSS(
|
||||
`@keyframes fade {
|
||||
0% { opacity: 0; }
|
||||
100% { opacity: 1; }
|
||||
}`,
|
||||
'fade'
|
||||
);
|
||||
|
||||
expect(res.length).toBe(2);
|
||||
|
||||
const [from, to] = res;
|
||||
expect(from.duration).toBe(0);
|
||||
expect(from.declarations.length).toBe(1);
|
||||
expect(from.declarations[0].property).toBe('opacity');
|
||||
expect(from.declarations[0].value).toBe(0);
|
||||
|
||||
expect(to.duration).toBe(1);
|
||||
expect(to.declarations.length).toBe(1);
|
||||
expect(to.declarations[0].property).toBe('opacity');
|
||||
expect(to.declarations[0].value).toBe(1);
|
||||
});
|
||||
|
||||
it('parses "via" keyframes', () => {
|
||||
const res = testKeyframesArrayFromCSS(
|
||||
`@keyframes fade {
|
||||
50% { opacity: 0.5; }
|
||||
}`,
|
||||
'fade'
|
||||
);
|
||||
|
||||
expect(res.length).toBe(1);
|
||||
|
||||
const [via] = res;
|
||||
expect(via.duration).toBe(0.5);
|
||||
expect(via.declarations.length).toBe(1);
|
||||
expect(via.declarations[0].property).toBe('opacity');
|
||||
expect(via.declarations[0].value).toBe(0.5);
|
||||
});
|
||||
|
||||
it('parses multiple keyframes', () => {
|
||||
const res = testKeyframesArrayFromCSS(
|
||||
`@keyframes fade {
|
||||
0% { opacity: 0; }
|
||||
50% { opacity: 0.5; }
|
||||
100% { opacity: 1; }
|
||||
}`,
|
||||
'fade'
|
||||
);
|
||||
|
||||
expect(res.length).toBe(3);
|
||||
|
||||
const [from, via, to] = res;
|
||||
expect(from.duration).toBe(0);
|
||||
expect(from.declarations.length).toBe(1);
|
||||
expect(from.declarations[0].property).toBe('opacity');
|
||||
expect(from.declarations[0].value).toBe(0);
|
||||
|
||||
expect(via.duration).toBe(0.5);
|
||||
expect(via.declarations.length).toBe(1);
|
||||
expect(via.declarations[0].property).toBe('opacity');
|
||||
expect(via.declarations[0].value).toBe(0.5);
|
||||
|
||||
expect(to.duration).toBe(1);
|
||||
expect(to.declarations.length).toBe(1);
|
||||
expect(to.declarations[0].property).toBe('opacity');
|
||||
expect(to.declarations[0].value).toBe(1);
|
||||
});
|
||||
|
||||
it('parses multiple keyframes with mixed stops', () => {
|
||||
const res = testKeyframesArrayFromCSS(
|
||||
`@keyframes fade {
|
||||
from { opacity: 0; }
|
||||
50% { opacity: 0.5; }
|
||||
to { opacity: 1; }
|
||||
}`,
|
||||
'fade'
|
||||
);
|
||||
|
||||
expect(res.length).toBe(3);
|
||||
|
||||
const [from, via, to] = res;
|
||||
expect(from.duration).toBe(0);
|
||||
expect(from.declarations.length).toBe(1);
|
||||
expect(from.declarations[0].property).toBe('opacity');
|
||||
expect(from.declarations[0].value).toBe(0);
|
||||
|
||||
expect(via.duration).toBe(0.5);
|
||||
expect(via.declarations.length).toBe(1);
|
||||
expect(via.declarations[0].property).toBe('opacity');
|
||||
expect(via.declarations[0].value).toBe(0.5);
|
||||
|
||||
expect(to.duration).toBe(1);
|
||||
expect(to.declarations.length).toBe(1);
|
||||
expect(to.declarations[0].property).toBe('opacity');
|
||||
expect(to.declarations[0].value).toBe(1);
|
||||
});
|
||||
|
||||
it('parses duplicate keyframes', () => {
|
||||
const res = testKeyframesArrayFromCSS(
|
||||
`@keyframes fade {
|
||||
0% { opacity: 0; }
|
||||
50% { opacity: 0.5; }
|
||||
50% { translateX: 100; }
|
||||
100% { opacity: 1; }
|
||||
}`,
|
||||
'fade'
|
||||
);
|
||||
|
||||
expect(res.length).toBe(3);
|
||||
|
||||
const [from, via, to] = res;
|
||||
expect(from.duration).toBe(0);
|
||||
expect(from.declarations.length).toBe(1);
|
||||
expect(from.declarations[0].property).toBe('opacity');
|
||||
expect(from.declarations[0].value).toBe(0);
|
||||
|
||||
expect(via.duration).toBe(0.5);
|
||||
expect(via.declarations.length).toBe(2);
|
||||
expect(via.declarations[0].property).toBe('opacity');
|
||||
expect(via.declarations[0].value).toBe(0.5);
|
||||
expect(via.declarations[1].property).toBe('translateX');
|
||||
expect(via.declarations[1].value).toBe(100);
|
||||
|
||||
expect(to.duration).toBe(1);
|
||||
expect(to.declarations.length).toBe(1);
|
||||
expect(to.declarations[0].property).toBe('opacity');
|
||||
expect(to.declarations[0].value).toBe(1);
|
||||
});
|
||||
|
||||
it('parses timing functions in keyframes', () => {
|
||||
const res = testKeyframesArrayFromCSS(
|
||||
`@keyframes fade {
|
||||
from { opacity: 0; animation-timing-function: ease-in; }
|
||||
to { opacity: 1; }
|
||||
}`,
|
||||
'fade'
|
||||
);
|
||||
|
||||
expect(res.length).toBe(2);
|
||||
|
||||
const [from, to] = res;
|
||||
expect(from.curve).toBe(CoreTypes.AnimationCurve.easeIn);
|
||||
expect(to.curve).not.toBeDefined();
|
||||
});
|
||||
|
||||
it('sorts multiple keyframes with mixed order', () => {
|
||||
const res = testKeyframesArrayFromCSS(
|
||||
`@keyframes fade {
|
||||
100% { opacity: 1; }
|
||||
0% { opacity: 0; }
|
||||
50% { opacity: 0.5; }
|
||||
}`,
|
||||
'fade'
|
||||
);
|
||||
|
||||
expect(res.length).toBe(3);
|
||||
|
||||
const [from, via, to] = res;
|
||||
expect(from.duration).toBe(0);
|
||||
expect(from.declarations.length).toBe(1);
|
||||
expect(from.declarations[0].property).toBe('opacity');
|
||||
expect(from.declarations[0].value).toBe(0);
|
||||
|
||||
expect(via.duration).toBe(0.5);
|
||||
expect(via.declarations.length).toBe(1);
|
||||
expect(via.declarations[0].property).toBe('opacity');
|
||||
expect(via.declarations[0].value).toBe(0.5);
|
||||
|
||||
expect(to.duration).toBe(1);
|
||||
expect(to.declarations.length).toBe(1);
|
||||
expect(to.declarations[0].property).toBe('opacity');
|
||||
expect(to.declarations[0].value).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -4,15 +4,16 @@ import { KeyframeAnimationInfo, KeyframeDeclaration, KeyframeInfo, UnparsedKeyfr
|
||||
import { timeConverter, animationTimingFunctionConverter } from '../styling/converters';
|
||||
|
||||
import { transformConverter } from '../styling/style-properties';
|
||||
import { cleanupImportantFlags } from './css-utils';
|
||||
|
||||
const ANIMATION_PROPERTY_HANDLERS = Object.freeze({
|
||||
'animation-name': (info: any, value: any) => (info.name = value),
|
||||
'animation-name': (info: any, value: any) => (info.name = value.replace(/['"]/g, '')),
|
||||
'animation-duration': (info: any, value: any) => (info.duration = timeConverter(value)),
|
||||
'animation-delay': (info: any, value: any) => (info.delay = timeConverter(value)),
|
||||
'animation-timing-function': (info: any, value: any) => (info.curve = animationTimingFunctionConverter(value)),
|
||||
'animation-iteration-count': (info: any, value: any) => (info.iterations = value === 'infinite' ? Number.POSITIVE_INFINITY : parseFloat(value)),
|
||||
'animation-direction': (info: any, value: any) => (info.isReverse = value === 'reverse'),
|
||||
'animation-fill-mode': (info: any, value: any) => (info.isForwards = value === 'forwards'),
|
||||
'animation-fill-mode': (info: any, value: any) => (info.isForwards = value === 'forwards' || value === 'both'),
|
||||
});
|
||||
|
||||
export class CssAnimationParser {
|
||||
@@ -64,6 +65,7 @@ export class CssAnimationParser {
|
||||
if (current === undefined) {
|
||||
current = <KeyframeInfo>{};
|
||||
current.duration = time;
|
||||
current.declarations = [];
|
||||
parsedKeyframes[time] = current;
|
||||
}
|
||||
for (const declaration of keyframe.declarations) {
|
||||
@@ -71,7 +73,7 @@ export class CssAnimationParser {
|
||||
current.curve = animationTimingFunctionConverter(declaration.value);
|
||||
}
|
||||
}
|
||||
current.declarations = declarations;
|
||||
current.declarations = current.declarations.concat(declarations);
|
||||
}
|
||||
}
|
||||
const array = [];
|
||||
@@ -86,49 +88,108 @@ export class CssAnimationParser {
|
||||
}
|
||||
}
|
||||
|
||||
function keyframeAnimationsFromCSSProperty(value: any, animations: KeyframeAnimationInfo[]) {
|
||||
if (typeof value === 'string') {
|
||||
const values = value.split(/[,]+/);
|
||||
for (const parsedValue of values) {
|
||||
const animationInfo = new KeyframeAnimationInfo();
|
||||
const arr = (<string>parsedValue).trim().split(/[ ]+/);
|
||||
/**
|
||||
* @see https://w3c.github.io/csswg-drafts/css-animations/#propdef-animation
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/CSS/animation
|
||||
* @internal - exported for testing
|
||||
* @param value
|
||||
* @param animations
|
||||
*/
|
||||
export function keyframeAnimationsFromCSSProperty(value: any, animations: KeyframeAnimationInfo[]) {
|
||||
if (typeof value !== 'string') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (arr.length > 0) {
|
||||
animationInfo.name = arr[0];
|
||||
}
|
||||
if (arr.length > 1) {
|
||||
animationInfo.duration = timeConverter(arr[1]);
|
||||
}
|
||||
if (arr.length > 2) {
|
||||
animationInfo.curve = animationTimingFunctionConverter(arr[2]);
|
||||
}
|
||||
if (arr.length > 3) {
|
||||
animationInfo.delay = timeConverter(arr[3]);
|
||||
}
|
||||
if (arr.length > 4) {
|
||||
animationInfo.iterations = parseInt(arr[4]);
|
||||
}
|
||||
if (arr.length > 5) {
|
||||
animationInfo.isReverse = arr[4] === 'reverse';
|
||||
}
|
||||
if (arr.length > 6) {
|
||||
animationInfo.isForwards = arr[5] === 'forwards';
|
||||
}
|
||||
if (arr.length > 7) {
|
||||
throw new Error('Invalid value for animation: ' + value);
|
||||
}
|
||||
animations.push(animationInfo);
|
||||
if (value.trim().length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches whitespace except if the whitespace is contained in parenthesis - ex. cubic-bezier(1, 1, 1, 1).
|
||||
*/
|
||||
const VALUE_SPLIT_RE = /\s(?![^(]*\))/;
|
||||
|
||||
/**
|
||||
* Matches commas except if the comma is contained in parenthesis - ex. cubic-bezier(1, 1, 1, 1).
|
||||
*/
|
||||
const MULTIPLE_SPLIT_RE = /,(?![^(]*\))/;
|
||||
|
||||
const isTime = (v: string) => !!v.match(/\dm?s$/g);
|
||||
const isTimingFunction = (v: string) => !!v.match(/ease|linear|ease-in|ease-out|ease-in-out|spring|cubic-bezier/g);
|
||||
const isIterationCount = (v: string) => !!v.match(/infinite|[\d.]+$/g);
|
||||
const isDirection = (v: string) => !!v.match(/normal|reverse|alternate|alternate-reverse/g);
|
||||
const isFillMode = (v: string) => !!v.match(/none|forwards|backwards|both/g);
|
||||
const isPlayState = (v: string) => !!v.match(/running|paused/g);
|
||||
|
||||
const values = value.split(MULTIPLE_SPLIT_RE);
|
||||
for (const parsedValue of values) {
|
||||
const animationInfo = new KeyframeAnimationInfo();
|
||||
const parts = (<string>parsedValue).trim().split(VALUE_SPLIT_RE);
|
||||
|
||||
const [duration, delay] = parts.filter(isTime);
|
||||
const [timing] = parts.filter(isTimingFunction);
|
||||
const [iterationCount] = parts.filter(isIterationCount);
|
||||
const [direction] = parts.filter(isDirection);
|
||||
const [fillMode] = parts.filter(isFillMode);
|
||||
const [playState] = parts.filter(isPlayState);
|
||||
const [name] = parts.filter((v) => {
|
||||
// filter out "consumed" values
|
||||
return ![duration, delay, timing, iterationCount, direction, fillMode, playState].filter(Boolean).includes(v);
|
||||
});
|
||||
|
||||
// console.log({
|
||||
// duration,
|
||||
// delay,
|
||||
// timing,
|
||||
// iterationCount,
|
||||
// direction,
|
||||
// fillMode,
|
||||
// playState,
|
||||
// name,
|
||||
// });
|
||||
|
||||
if (duration) {
|
||||
ANIMATION_PROPERTY_HANDLERS['animation-duration'](animationInfo, duration);
|
||||
}
|
||||
if (delay) {
|
||||
ANIMATION_PROPERTY_HANDLERS['animation-delay'](animationInfo, delay);
|
||||
}
|
||||
if (timing) {
|
||||
ANIMATION_PROPERTY_HANDLERS['animation-timing-function'](animationInfo, timing);
|
||||
}
|
||||
if (iterationCount) {
|
||||
ANIMATION_PROPERTY_HANDLERS['animation-iteration-count'](animationInfo, iterationCount);
|
||||
}
|
||||
if (direction) {
|
||||
ANIMATION_PROPERTY_HANDLERS['animation-direction'](animationInfo, direction);
|
||||
}
|
||||
if (fillMode) {
|
||||
ANIMATION_PROPERTY_HANDLERS['animation-fill-mode'](animationInfo, fillMode);
|
||||
}
|
||||
if (playState) {
|
||||
// TODO: implement play state? Currently not supported...
|
||||
}
|
||||
if (name) {
|
||||
ANIMATION_PROPERTY_HANDLERS['animation-name'](animationInfo, name);
|
||||
} else {
|
||||
// based on the SPEC we should set the name to 'none' if no name is provided
|
||||
// however we just don't set the name at all.
|
||||
// perhaps we should set it to 'none' and handle it accordingly.
|
||||
// animationInfo.name = 'none'
|
||||
}
|
||||
|
||||
animations.push(animationInfo);
|
||||
}
|
||||
}
|
||||
|
||||
export function parseKeyframeDeclarations(unparsedKeyframeDeclarations: KeyframeDeclaration[]): KeyframeDeclaration[] {
|
||||
const declarations = unparsedKeyframeDeclarations.reduce((declarations, { property: unparsedProperty, value: unparsedValue }) => {
|
||||
const property = CssAnimationProperty._getByCssName(unparsedProperty);
|
||||
unparsedValue = cleanupImportantFlags(unparsedValue, property?.cssLocalName);
|
||||
|
||||
if (typeof unparsedProperty === 'string' && property && property._valueConverter) {
|
||||
if (typeof unparsedProperty === 'string' && property?._valueConverter) {
|
||||
declarations[property.name] = property._valueConverter(<string>unparsedValue);
|
||||
} else if (typeof unparsedValue === 'string' && unparsedProperty === 'transform') {
|
||||
} else if (unparsedProperty === 'transform') {
|
||||
const transformations = transformConverter(unparsedValue);
|
||||
Object.assign(declarations, transformations);
|
||||
}
|
||||
|
||||
12
packages/core/ui/styling/css-utils.ts
Normal file
12
packages/core/ui/styling/css-utils.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Trace } from '../../trace';
|
||||
|
||||
export function cleanupImportantFlags(value: string, propertyName: string) {
|
||||
const index = value?.indexOf('!important');
|
||||
if (index >= 0) {
|
||||
if (Trace.isEnabled()) {
|
||||
Trace.write(`The !important css rule is currently not supported. Property: ${propertyName}`, Trace.categories.Style, Trace.messageType.warn);
|
||||
}
|
||||
return value.substring(0, index).trim();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
@@ -91,7 +91,8 @@ export class Font extends FontBase {
|
||||
getUIFont(defaultFont: UIFont): UIFont {
|
||||
return getUIFontCached({
|
||||
fontFamily: parseFontFamily(this.fontFamily),
|
||||
fontSize: this.fontSize || defaultFont.pointSize,
|
||||
// Apply a11y scale and calculate proper font size (avoid applying multiplier to native point size as it's messing calculations)
|
||||
fontSize: this.fontSize ? this.fontSize * this.fontScale : defaultFont.pointSize,
|
||||
fontWeight: getNativeFontWeight(this.fontWeight),
|
||||
fontVariationSettings: this.fontVariationSettings,
|
||||
isBold: this.isBold,
|
||||
|
||||
@@ -1329,20 +1329,7 @@ fontFamilyProperty.register(Style);
|
||||
export const fontScaleProperty = new InheritedCssProperty<Style, number>({
|
||||
name: '_fontScale',
|
||||
cssName: '_fontScale',
|
||||
affectsLayout: global.isIOS,
|
||||
valueChanged: (target, oldValue, newValue) => {
|
||||
if (global.isIOS) {
|
||||
if (target.viewRef['handleFontSize'] === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentFont = target.fontInternal || Font.default;
|
||||
if (currentFont.fontScale !== newValue) {
|
||||
const newFont = currentFont.withFontScale(newValue);
|
||||
target.fontInternal = Font.equals(Font.default, newFont) ? unsetValue : newFont;
|
||||
}
|
||||
}
|
||||
},
|
||||
defaultValue: 1.0,
|
||||
valueConverter: (v) => parseFloat(v),
|
||||
});
|
||||
fontScaleProperty.register(Style);
|
||||
|
||||
@@ -21,6 +21,7 @@ function ensureKeyframeAnimationModule() {
|
||||
import * as capm from './css-animation-parser';
|
||||
import { sanitizeModuleName } from '../builder/module-name-sanitizer';
|
||||
import { resolveModuleName } from '../../module-name-resolver';
|
||||
import { cleanupImportantFlags } from './css-utils';
|
||||
|
||||
let cssAnimationParserModule: typeof capm;
|
||||
function ensureCssAnimationParserModule() {
|
||||
@@ -563,7 +564,6 @@ export class CssState {
|
||||
const view = this.viewRef.get();
|
||||
if (!view) {
|
||||
Trace.write(`${matchingSelectors} not set to view's property because ".viewRef" is cleared`, Trace.categories.Style, Trace.messageType.warn);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -579,7 +579,8 @@ export class CssState {
|
||||
const replacementFunc = (g) => g[1].toUpperCase();
|
||||
|
||||
for (const property in newPropertyValues) {
|
||||
const value = newPropertyValues[property];
|
||||
const value = cleanupImportantFlags(newPropertyValues[property], property);
|
||||
|
||||
const isCssExp = isCssVariableExpression(value) || isCssCalcExpression(value);
|
||||
|
||||
if (isCssExp) {
|
||||
|
||||
@@ -105,6 +105,9 @@ export class Style extends Observable implements StyleDefinition {
|
||||
}
|
||||
|
||||
public fontInternal: Font;
|
||||
/**
|
||||
* This property ensures inheritance of a11y scale among views.
|
||||
*/
|
||||
public _fontScale: number;
|
||||
public backgroundInternal: Background;
|
||||
|
||||
|
||||
@@ -4,16 +4,15 @@ import { CSSShadow } from '../styling/css-shadow';
|
||||
|
||||
// Requires
|
||||
import { Font } from '../styling/font';
|
||||
import { TextBaseCommon, textProperty, formattedTextProperty, textAlignmentProperty, textDecorationProperty, textTransformProperty, textShadowProperty, letterSpacingProperty, lineHeightProperty, resetSymbol } from './text-base-common';
|
||||
import { TextBaseCommon, textProperty, formattedTextProperty, textAlignmentProperty, textDecorationProperty, textTransformProperty, textShadowProperty, letterSpacingProperty, lineHeightProperty, maxLinesProperty, resetSymbol } from './text-base-common';
|
||||
import { Color } from '../../color';
|
||||
import { FormattedString } from './formatted-string';
|
||||
import { Span } from './span';
|
||||
import { colorProperty, fontInternalProperty, Length } from '../styling/style-properties';
|
||||
import { colorProperty, fontInternalProperty, fontScaleProperty, Length } from '../styling/style-properties';
|
||||
import { isString, isNullOrUndefined } from '../../utils/types';
|
||||
import { iOSNativeHelper } from '../../utils';
|
||||
import { Trace } from '../../trace';
|
||||
import { CoreTypes } from '../../core-types';
|
||||
import { maxLinesProperty } from './text-base-common';
|
||||
|
||||
export * from './text-base-common';
|
||||
|
||||
@@ -188,7 +187,26 @@ export class TextBase extends TextBaseCommon {
|
||||
if (!(value instanceof Font) || !this.formattedText) {
|
||||
let nativeView = this.nativeTextViewProtected;
|
||||
nativeView = nativeView instanceof UIButton ? nativeView.titleLabel : nativeView;
|
||||
nativeView.font = value instanceof Font ? value.getUIFont(nativeView.font) : value;
|
||||
|
||||
if (value instanceof Font) {
|
||||
// Apply a11y font scale if not set
|
||||
if (value.fontScale !== this.style._fontScale) {
|
||||
value.fontScale = this.style._fontScale;
|
||||
}
|
||||
nativeView.font = value.getUIFont(nativeView.font);
|
||||
} else {
|
||||
nativeView.font = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[fontScaleProperty.setNative](value: number) {
|
||||
const nativeView = this.nativeTextViewProtected instanceof UIButton ? this.nativeTextViewProtected.titleLabel : this.nativeTextViewProtected;
|
||||
const currentFont = this.style.fontInternal || Font.default.withFontSize(nativeView.font.pointSize);
|
||||
if (currentFont.fontScale !== value) {
|
||||
const newFont = currentFont.withFontScale(value);
|
||||
this.style.fontInternal = newFont;
|
||||
this.requestLayout();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
82
packages/core/utils/index.d.ts
vendored
82
packages/core/utils/index.d.ts
vendored
@@ -21,88 +21,6 @@ interface Owned {
|
||||
}
|
||||
//@endprivate
|
||||
|
||||
/**
|
||||
* Module with android specific utilities.
|
||||
*/
|
||||
export namespace ad {
|
||||
/**
|
||||
* Gets the native Android application instance.
|
||||
*/
|
||||
export function getApplication(): any; /* android.app.Application */
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
namespace 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 namespace 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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An utility function that invokes garbage collection on the JavaScript side.
|
||||
*/
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { android as ad } from '../application';
|
||||
import { ad } from './native-helper';
|
||||
|
||||
export function dispatchToMainThread(func: () => void) {
|
||||
const runOnMainThread = (global as any).__runOnMainThread;
|
||||
@@ -20,7 +20,7 @@ export function isMainThread(): boolean {
|
||||
}
|
||||
|
||||
export function dispatchToUIThread(func: () => void) {
|
||||
const activity: androidx.appcompat.app.AppCompatActivity = ad.foregroundActivity || ad.startActivity;
|
||||
const activity: androidx.appcompat.app.AppCompatActivity = ad.getCurrentActivity();
|
||||
if (activity && func) {
|
||||
activity.runOnUiThread(
|
||||
new java.lang.Runnable({
|
||||
|
||||
@@ -397,7 +397,7 @@ function _generateAmpMap(): any {
|
||||
}
|
||||
|
||||
// android-specific implementation, which pre-populates the map to get it saved into the heap blob
|
||||
if ((<any>global).__snapshot) {
|
||||
if (global.__snapshot) {
|
||||
_ampCodes = _generateAmpMap();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user