mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-11-05 13:26:48 +08:00
refactor(core): zero circulars + esm ready (#10770)
This commit is contained in:
@ -1,18 +1,21 @@
|
||||
import { initAccessibilityCssHelper } from '../accessibility/accessibility-css-helper';
|
||||
import { initAccessibilityFontScale } from '../accessibility/font-scale';
|
||||
import { CoreTypes } from '../core-types';
|
||||
import { CSSUtils } from '../css/system-classes';
|
||||
import { Device, Screen } from '../platform';
|
||||
import { profile } from '../profiling';
|
||||
import { Trace } from '../trace';
|
||||
import { clearResolverCache, prepareAppForModuleResolver, _setResolver } from '../module-name-resolver/helpers';
|
||||
import { Builder } from '../ui/builder';
|
||||
import * as bindableResources from '../ui/core/bindable/bindable-resources';
|
||||
import type { View } from '../ui/core/view';
|
||||
import type { Frame } from '../ui/frame';
|
||||
import type { NavigationEntry } from '../ui/frame/frame-interfaces';
|
||||
import type { StyleScope } from '../ui/styling/style-scope';
|
||||
import type { AndroidApplication as IAndroidApplication, iOSApplication as IiOSApplication } from './';
|
||||
import type { AndroidApplication as AndroidApplicationType, iOSApplication as iOSApplicationType } from '.';
|
||||
import type { ApplicationEventData, CssChangedEventData, DiscardedErrorEventData, FontScaleChangedEventData, InitRootViewEventData, LaunchEventData, LoadAppCSSEventData, NativeScriptError, OrientationChangedEventData, SystemAppearanceChangedEventData, UnhandledErrorEventData } from './application-interfaces';
|
||||
import { readyInitAccessibilityCssHelper, readyInitFontScale } from '../accessibility/accessibility-common';
|
||||
import { getAppMainEntry, isAppInBackground, setAppInBackground, setAppMainEntry } from './helpers-common';
|
||||
import { getNativeScriptGlobals } from '../globals/global-utils';
|
||||
import { SDK_VERSION } from '../utils/constants';
|
||||
|
||||
// prettier-ignore
|
||||
const ORIENTATION_CSS_CLASSES = [
|
||||
@ -27,7 +30,55 @@ const SYSTEM_APPEARANCE_CSS_CLASSES = [
|
||||
`${CSSUtils.CLASS_PREFIX}${CoreTypes.SystemAppearance.dark}`,
|
||||
];
|
||||
|
||||
const globalEvents = global.NativeScriptGlobals.events;
|
||||
// SDK Version CSS classes
|
||||
let sdkVersionClasses: string[] = [];
|
||||
|
||||
export function initializeSdkVersionClass(rootView: View): void {
|
||||
const majorVersion = Math.floor(SDK_VERSION);
|
||||
sdkVersionClasses = [];
|
||||
|
||||
let platformPrefix = '';
|
||||
if (__APPLE__) {
|
||||
platformPrefix = __VISIONOS__ ? 'ns-visionos' : 'ns-ios';
|
||||
} else if (__ANDROID__) {
|
||||
platformPrefix = 'ns-android';
|
||||
}
|
||||
|
||||
if (platformPrefix) {
|
||||
// Add exact version class (e.g., .ns-ios-26 or .ns-android-36)
|
||||
// this acts like 'gte' for that major version range
|
||||
// e.g., if user wants iOS 27, they can add .ns-ios-27 specifiers
|
||||
sdkVersionClasses.push(`${platformPrefix}-${majorVersion}`);
|
||||
}
|
||||
|
||||
// Apply the SDK version classes to root views
|
||||
applySdkVersionClass(rootView);
|
||||
}
|
||||
|
||||
function applySdkVersionClass(rootView: View): void {
|
||||
if (!sdkVersionClasses.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!rootView) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Batch apply all SDK version classes to root view for better performance
|
||||
const classesToAdd = sdkVersionClasses.filter((className) => !rootView.cssClasses.has(className));
|
||||
classesToAdd.forEach((className) => rootView.cssClasses.add(className));
|
||||
|
||||
// Apply to modal views only if there are any
|
||||
const rootModalViews = <Array<View>>rootView._getRootModalViews();
|
||||
if (rootModalViews.length > 0) {
|
||||
rootModalViews.forEach((rootModalView) => {
|
||||
const modalClassesToAdd = sdkVersionClasses.filter((className) => !rootModalView.cssClasses.has(className));
|
||||
modalClassesToAdd.forEach((className) => rootModalView.cssClasses.add(className));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const globalEvents = getNativeScriptGlobals().events;
|
||||
|
||||
// helper interface to correctly type Application event handlers
|
||||
interface ApplicationEvents {
|
||||
@ -160,7 +211,7 @@ export class ApplicationCommon {
|
||||
* @internal - should not be constructed by the user.
|
||||
*/
|
||||
constructor() {
|
||||
global.NativeScriptGlobals.appInstanceReady = true;
|
||||
getNativeScriptGlobals().appInstanceReady = true;
|
||||
|
||||
global.__onUncaughtError = (error: NativeScriptError) => {
|
||||
this.notify({
|
||||
@ -309,13 +360,11 @@ export class ApplicationCommon {
|
||||
// implement in platform specific files (iOS only for now)
|
||||
}
|
||||
|
||||
protected mainEntry: NavigationEntry;
|
||||
|
||||
/**
|
||||
* @returns The main entry of the application
|
||||
*/
|
||||
getMainEntry() {
|
||||
return this.mainEntry;
|
||||
return getAppMainEntry();
|
||||
}
|
||||
|
||||
@profile
|
||||
@ -349,11 +398,11 @@ export class ApplicationCommon {
|
||||
|
||||
if (!rootView) {
|
||||
// try to navigate to the mainEntry (if specified)
|
||||
if (!this.mainEntry) {
|
||||
if (!getAppMainEntry()) {
|
||||
throw new Error('Main entry is missing. App cannot be started. Verify app bootstrap.');
|
||||
}
|
||||
|
||||
rootView = Builder.createViewFromEntry(this.mainEntry);
|
||||
rootView = Builder.createViewFromEntry(getAppMainEntry());
|
||||
}
|
||||
}
|
||||
|
||||
@ -365,14 +414,14 @@ export class ApplicationCommon {
|
||||
}
|
||||
|
||||
resetRootView(entry?: NavigationEntry | string) {
|
||||
this.mainEntry = typeof entry === 'string' ? { moduleName: entry } : entry;
|
||||
setAppMainEntry(typeof entry === 'string' ? { moduleName: entry } : entry);
|
||||
// rest of implementation is platform specific
|
||||
}
|
||||
|
||||
initRootView(rootView: View) {
|
||||
this.setRootViewCSSClasses(rootView);
|
||||
initAccessibilityCssHelper();
|
||||
initAccessibilityFontScale();
|
||||
readyInitAccessibilityCssHelper();
|
||||
readyInitFontScale();
|
||||
this.notify(<InitRootViewEventData>{ eventName: this.initRootViewEvent, rootView });
|
||||
}
|
||||
|
||||
@ -508,7 +557,7 @@ export class ApplicationCommon {
|
||||
}
|
||||
|
||||
hasLaunched(): boolean {
|
||||
return global.NativeScriptGlobals && global.NativeScriptGlobals.launched;
|
||||
return getNativeScriptGlobals().launched;
|
||||
}
|
||||
|
||||
private _systemAppearance: 'dark' | 'light' | null;
|
||||
@ -575,14 +624,12 @@ export class ApplicationCommon {
|
||||
rootView._onCssStateChange();
|
||||
}
|
||||
|
||||
private _inBackground: boolean = false;
|
||||
|
||||
get inBackground() {
|
||||
return this._inBackground;
|
||||
return isAppInBackground();
|
||||
}
|
||||
|
||||
setInBackground(value: boolean, additonalData?: any) {
|
||||
this._inBackground = value;
|
||||
setAppInBackground(value);
|
||||
|
||||
this.notify(<ApplicationEventData>{
|
||||
eventName: value ? this.backgroundEvent : this.foregroundEvent,
|
||||
@ -614,11 +661,11 @@ export class ApplicationCommon {
|
||||
|
||||
public started = false;
|
||||
|
||||
get android(): IAndroidApplication {
|
||||
get android(): AndroidApplicationType {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
get ios(): IiOSApplication {
|
||||
get ios(): iOSApplicationType {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@ -630,3 +677,10 @@ export class ApplicationCommon {
|
||||
return this.ios;
|
||||
}
|
||||
}
|
||||
|
||||
prepareAppForModuleResolver(() => {
|
||||
ApplicationCommon.on('livesync', (args) => clearResolverCache());
|
||||
ApplicationCommon.on('orientationChanged', (args) => {
|
||||
_setResolver(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import type { ApplicationCommon } from './application-common';
|
||||
import type { EventData, Observable } from '../data/observable';
|
||||
import type { View } from '../ui/core/view';
|
||||
|
||||
@ -42,7 +41,7 @@ export interface ApplicationEventData {
|
||||
/**
|
||||
* The instance that has raised the event.
|
||||
*/
|
||||
object: ApplicationCommon | Observable;
|
||||
object: any; // Application;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
25
packages/core/application/application.d.ts
vendored
25
packages/core/application/application.d.ts
vendored
@ -1,4 +1,5 @@
|
||||
import { ApplicationCommon } from './application-common';
|
||||
import { FontScaleCategory } from '../accessibility/font-scale-common';
|
||||
|
||||
export * from './application-common';
|
||||
export * from './application-interfaces';
|
||||
@ -192,3 +193,27 @@ export class iOSApplication extends ApplicationCommon {
|
||||
*/
|
||||
removeNotificationObserver(observer: any, notificationName: string);
|
||||
}
|
||||
|
||||
export const VALID_FONT_SCALES: number[];
|
||||
export function getCurrentFontScale(): number;
|
||||
export function getAndroidAccessibilityManager(): android.view.accessibility.AccessibilityManager | null;
|
||||
|
||||
/**
|
||||
* Update accessibility properties on nativeView
|
||||
*/
|
||||
export function updateAccessibilityProperties(view: View): void;
|
||||
|
||||
/**
|
||||
* Android: helper function for triggering accessibility events
|
||||
*/
|
||||
export function sendAccessibilityEvent(View: View, eventName: AndroidAccessibilityEvent, text?: string): void;
|
||||
|
||||
/**
|
||||
* Is Android TalkBack or iOS VoiceOver enabled?
|
||||
*/
|
||||
export function isAccessibilityServiceEnabled(): boolean;
|
||||
|
||||
/**
|
||||
* Find the last view focused on a page.
|
||||
*/
|
||||
export function getLastFocusedViewOnPage(page: Page): View | null;
|
||||
|
||||
@ -1,12 +1,53 @@
|
||||
import { profile } from '../profiling';
|
||||
import { View } from '../ui/core/view';
|
||||
import type { View } from '../ui/core/view';
|
||||
import { isEmbedded } from '../ui/embedding';
|
||||
import { IOSHelper } from '../ui/core/view/view-helper';
|
||||
import { NavigationEntry } from '../ui/frame/frame-interfaces';
|
||||
import * as Utils from '../utils';
|
||||
import type { iOSApplication as IiOSApplication } from './application';
|
||||
import { ApplicationCommon } from './application-common';
|
||||
import type { NavigationEntry } from '../ui/frame/frame-interfaces';
|
||||
import { getWindow } from '../utils/native-helper';
|
||||
import { SDK_VERSION } from '../utils/constants';
|
||||
import { ios as iosUtils } from '../utils/native-helper';
|
||||
import { ApplicationCommon, initializeSdkVersionClass } from './application-common';
|
||||
import { ApplicationEventData } from './application-interfaces';
|
||||
import { Observable } from '../data/observable';
|
||||
import { Trace } from '../trace';
|
||||
import {
|
||||
AccessibilityServiceEnabledPropName,
|
||||
CommonA11YServiceEnabledObservable,
|
||||
SharedA11YObservable,
|
||||
a11yServiceClasses,
|
||||
a11yServiceDisabledClass,
|
||||
a11yServiceEnabledClass,
|
||||
fontScaleCategoryClasses,
|
||||
fontScaleExtraLargeCategoryClass,
|
||||
fontScaleExtraSmallCategoryClass,
|
||||
fontScaleMediumCategoryClass,
|
||||
getCurrentA11YServiceClass,
|
||||
getCurrentFontScaleCategory,
|
||||
getCurrentFontScaleClass,
|
||||
getFontScaleCssClasses,
|
||||
setCurrentA11YServiceClass,
|
||||
setCurrentFontScaleCategory,
|
||||
setCurrentFontScaleClass,
|
||||
setFontScaleCssClasses,
|
||||
FontScaleCategory,
|
||||
getClosestValidFontScale,
|
||||
VALID_FONT_SCALES,
|
||||
setFontScale,
|
||||
getFontScale,
|
||||
setInitFontScale,
|
||||
getFontScaleCategory,
|
||||
setInitAccessibilityCssHelper,
|
||||
notifyAccessibilityFocusState,
|
||||
AccessibilityLiveRegion,
|
||||
AccessibilityRole,
|
||||
AccessibilityState,
|
||||
AccessibilityTrait,
|
||||
isA11yEnabled,
|
||||
setA11yEnabled,
|
||||
enforceArray,
|
||||
} from '../accessibility/accessibility-common';
|
||||
import { iosAddNotificationObserver, iosRemoveNotificationObserver } from './helpers';
|
||||
import { getiOSWindow, setA11yUpdatePropertiesCallback, setApplicationPropertiesCallback, setAppMainEntry, setiOSWindow, setRootView, setToggleApplicationEventListenersCallback } from './helpers-common';
|
||||
|
||||
@NativeClass
|
||||
class CADisplayLinkTarget extends NSObject {
|
||||
@ -40,26 +81,6 @@ class CADisplayLinkTarget extends NSObject {
|
||||
};
|
||||
}
|
||||
|
||||
@NativeClass
|
||||
class NotificationObserver extends NSObject {
|
||||
private _onReceiveCallback: (notification: NSNotification) => void;
|
||||
|
||||
public static initWithCallback(onReceiveCallback: (notification: NSNotification) => void): NotificationObserver {
|
||||
const observer = <NotificationObserver>super.new();
|
||||
observer._onReceiveCallback = onReceiveCallback;
|
||||
|
||||
return observer;
|
||||
}
|
||||
|
||||
public onReceive(notification: NSNotification): void {
|
||||
this._onReceiveCallback(notification);
|
||||
}
|
||||
|
||||
public static ObjCExposedMethods = {
|
||||
onReceive: { returns: interop.types.void, params: [NSNotification] },
|
||||
};
|
||||
}
|
||||
|
||||
@NativeClass
|
||||
class Responder extends UIResponder implements UIApplicationDelegate {
|
||||
get window(): UIWindow {
|
||||
@ -73,11 +94,9 @@ class Responder extends UIResponder implements UIApplicationDelegate {
|
||||
static ObjCProtocols = [UIApplicationDelegate];
|
||||
}
|
||||
|
||||
export class iOSApplication extends ApplicationCommon implements IiOSApplication {
|
||||
export class iOSApplication extends ApplicationCommon {
|
||||
private _delegate: UIApplicationDelegate;
|
||||
private _delegateHandlers = new Map<string, Array<Function>>();
|
||||
private _window: UIWindow;
|
||||
private _notificationObservers: NotificationObserver[] = [];
|
||||
private _rootView: View;
|
||||
|
||||
displayedOnce = false;
|
||||
@ -108,7 +127,7 @@ export class iOSApplication extends ApplicationCommon implements IiOSApplication
|
||||
}
|
||||
|
||||
run(entry?: string | NavigationEntry): void {
|
||||
this.mainEntry = typeof entry === 'string' ? { moduleName: entry } : entry;
|
||||
setAppMainEntry(typeof entry === 'string' ? { moduleName: entry } : entry);
|
||||
this.started = true;
|
||||
|
||||
if (this.nativeApp) {
|
||||
@ -129,8 +148,9 @@ export class iOSApplication extends ApplicationCommon implements IiOSApplication
|
||||
return;
|
||||
}
|
||||
this._rootView = rootView;
|
||||
setRootView(rootView);
|
||||
// Attach to the existing iOS app
|
||||
const window = Utils.ios.getWindow();
|
||||
const window = getWindow() as UIWindow;
|
||||
|
||||
if (!window) {
|
||||
return;
|
||||
@ -155,7 +175,7 @@ export class iOSApplication extends ApplicationCommon implements IiOSApplication
|
||||
this.setViewControllerView(rootView);
|
||||
embedderDelegate.presentNativeScriptApp(controller);
|
||||
} else {
|
||||
const visibleVC = Utils.ios.getVisibleViewController(rootController);
|
||||
const visibleVC = iosUtils.getVisibleViewController(rootController);
|
||||
visibleVC.presentViewControllerAnimatedCompletion(controller, true, null);
|
||||
}
|
||||
|
||||
@ -198,7 +218,7 @@ export class iOSApplication extends ApplicationCommon implements IiOSApplication
|
||||
|
||||
if (minFrameRateDisabled) {
|
||||
let max = 120;
|
||||
const deviceMaxFrames = Utils.ios.getMainScreen().maximumFramesPerSecond;
|
||||
const deviceMaxFrames = iosUtils.getMainScreen().maximumFramesPerSecond;
|
||||
if (options?.max) {
|
||||
if (deviceMaxFrames) {
|
||||
// iOS 10.3
|
||||
@ -209,7 +229,7 @@ export class iOSApplication extends ApplicationCommon implements IiOSApplication
|
||||
}
|
||||
}
|
||||
|
||||
if (Utils.SDK_VERSION >= 15 || __VISIONOS__) {
|
||||
if (SDK_VERSION >= 15 || __VISIONOS__) {
|
||||
const min = options?.min || max / 2;
|
||||
const preferred = options?.preferred || max;
|
||||
this.displayedLink.preferredFrameRateRange = CAFrameRateRangeMake(min, max, preferred);
|
||||
@ -243,12 +263,12 @@ export class iOSApplication extends ApplicationCommon implements IiOSApplication
|
||||
// TODO: consideration
|
||||
// may not want to cache this value given the potential of multiple scenes
|
||||
// particularly with SwiftUI app lifecycle based apps
|
||||
if (!this._window) {
|
||||
if (!getiOSWindow()) {
|
||||
// Note: NativeScriptViewFactory.getKeyWindow will always be used in SwiftUI app lifecycle based apps
|
||||
this._window = Utils.ios.getWindow();
|
||||
setiOSWindow(getWindow() as UIWindow);
|
||||
}
|
||||
|
||||
return this._window;
|
||||
return getiOSWindow();
|
||||
}
|
||||
|
||||
get delegate(): UIApplicationDelegate & { prototype: UIApplicationDelegate } {
|
||||
@ -304,24 +324,16 @@ export class iOSApplication extends ApplicationCommon implements IiOSApplication
|
||||
}
|
||||
|
||||
addNotificationObserver(notificationName: string, onReceiveCallback: (notification: NSNotification) => void) {
|
||||
const observer = NotificationObserver.initWithCallback(onReceiveCallback);
|
||||
NSNotificationCenter.defaultCenter.addObserverSelectorNameObject(observer, 'onReceive', notificationName, null);
|
||||
this._notificationObservers.push(observer);
|
||||
|
||||
return observer;
|
||||
return iosAddNotificationObserver(notificationName, onReceiveCallback);
|
||||
}
|
||||
|
||||
removeNotificationObserver(observer: any, notificationName: string) {
|
||||
const index = this._notificationObservers.indexOf(observer);
|
||||
if (index >= 0) {
|
||||
this._notificationObservers.splice(index, 1);
|
||||
NSNotificationCenter.defaultCenter.removeObserverNameObject(observer, notificationName, null);
|
||||
}
|
||||
removeNotificationObserver(observer: any /* NotificationObserver */, notificationName: string) {
|
||||
iosRemoveNotificationObserver(observer, notificationName);
|
||||
}
|
||||
|
||||
protected getSystemAppearance(): 'light' | 'dark' {
|
||||
// userInterfaceStyle is available on UITraitCollection since iOS 12.
|
||||
if ((!__VISIONOS__ && Utils.SDK_VERSION <= 11) || !this.rootController) {
|
||||
if ((!__VISIONOS__ && SDK_VERSION <= 11) || !this.rootController) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -367,12 +379,12 @@ export class iOSApplication extends ApplicationCommon implements IiOSApplication
|
||||
ios: notification?.userInfo?.objectForKey('UIApplicationLaunchOptionsLocalNotificationKey') ?? null,
|
||||
});
|
||||
|
||||
if (this._window) {
|
||||
if (getiOSWindow()) {
|
||||
if (root !== null && !isEmbedded()) {
|
||||
this.setWindowContent(root);
|
||||
}
|
||||
} else {
|
||||
this._window = this.window; // UIApplication.sharedApplication.keyWindow;
|
||||
setiOSWindow(this.window);
|
||||
}
|
||||
}
|
||||
|
||||
@ -400,6 +412,7 @@ export class iOSApplication extends ApplicationCommon implements IiOSApplication
|
||||
const controller = this.getViewController(rootView);
|
||||
|
||||
this._rootView = rootView;
|
||||
setRootView(rootView);
|
||||
|
||||
// setup view as styleScopeHost
|
||||
rootView._setupAsRootView({});
|
||||
@ -428,17 +441,31 @@ export class iOSApplication extends ApplicationCommon implements IiOSApplication
|
||||
// Observers
|
||||
@profile
|
||||
private didFinishLaunchingWithOptions(notification: NSNotification) {
|
||||
if (__DEV__) {
|
||||
/**
|
||||
* v9+ runtime crash handling
|
||||
* When crash occurs during boot, we let runtime take over
|
||||
*/
|
||||
if (notification.userInfo) {
|
||||
const isBootCrash = notification.userInfo.objectForKey('NativeScriptBootCrash');
|
||||
if (isBootCrash) {
|
||||
// fatal crash will show in console without app exiting
|
||||
// allowing hot reload fixes to continue
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.setMaxRefreshRate();
|
||||
// ensures window is assigned to proper window scene
|
||||
this._window = this.window;
|
||||
setiOSWindow(this.window);
|
||||
|
||||
if (!this._window) {
|
||||
if (!getiOSWindow()) {
|
||||
// if still no window, create one
|
||||
this._window = UIWindow.alloc().initWithFrame(UIScreen.mainScreen.bounds);
|
||||
setiOSWindow(UIWindow.alloc().initWithFrame(UIScreen.mainScreen.bounds));
|
||||
}
|
||||
|
||||
if (!__VISIONOS__) {
|
||||
this.window.backgroundColor = Utils.SDK_VERSION <= 12 || !UIColor.systemBackgroundColor ? UIColor.whiteColor : UIColor.systemBackgroundColor;
|
||||
this.window.backgroundColor = SDK_VERSION <= 12 || !UIColor.systemBackgroundColor ? UIColor.whiteColor : UIColor.systemBackgroundColor;
|
||||
}
|
||||
|
||||
this.notifyAppStarted(notification);
|
||||
@ -514,3 +541,523 @@ global.__onLiveSyncCore = function (context?: ModuleContext) {
|
||||
export * from './application-common';
|
||||
export const Application = iosApp;
|
||||
export const AndroidApplication = undefined;
|
||||
|
||||
function fontScaleChanged(origFontScale: number) {
|
||||
const oldValue = getFontScale();
|
||||
setFontScale(getClosestValidFontScale(origFontScale));
|
||||
const currentFontScale = getFontScale();
|
||||
|
||||
if (oldValue !== currentFontScale) {
|
||||
Application.notify({
|
||||
eventName: Application.fontScaleChangedEvent,
|
||||
object: Application,
|
||||
newValue: currentFontScale,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function getCurrentFontScale(): number {
|
||||
setupConfigListener();
|
||||
|
||||
return getFontScale();
|
||||
}
|
||||
|
||||
const sizeMap = new Map<string, number>([
|
||||
[UIContentSizeCategoryExtraSmall, 0.5],
|
||||
[UIContentSizeCategorySmall, 0.7],
|
||||
[UIContentSizeCategoryMedium, 0.85],
|
||||
[UIContentSizeCategoryLarge, 1],
|
||||
[UIContentSizeCategoryExtraLarge, 1.15],
|
||||
[UIContentSizeCategoryExtraExtraLarge, 1.3],
|
||||
[UIContentSizeCategoryExtraExtraExtraLarge, 1.5],
|
||||
[UIContentSizeCategoryAccessibilityMedium, 2],
|
||||
[UIContentSizeCategoryAccessibilityLarge, 2.5],
|
||||
[UIContentSizeCategoryAccessibilityExtraLarge, 3],
|
||||
[UIContentSizeCategoryAccessibilityExtraExtraLarge, 3.5],
|
||||
[UIContentSizeCategoryAccessibilityExtraExtraExtraLarge, 4],
|
||||
]);
|
||||
|
||||
function contentSizeUpdated(fontSize: string) {
|
||||
if (sizeMap.has(fontSize)) {
|
||||
fontScaleChanged(sizeMap.get(fontSize));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
fontScaleChanged(1);
|
||||
}
|
||||
|
||||
function useIOSFontScale() {
|
||||
if (Application.ios.nativeApp) {
|
||||
contentSizeUpdated(Application.ios.nativeApp.preferredContentSizeCategory);
|
||||
} else {
|
||||
fontScaleChanged(1);
|
||||
}
|
||||
}
|
||||
|
||||
let fontSizeObserver;
|
||||
function setupConfigListener(attempt = 0) {
|
||||
if (fontSizeObserver) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Application.ios.nativeApp) {
|
||||
if (attempt > 100) {
|
||||
fontScaleChanged(1);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Couldn't get launchEvent to trigger.
|
||||
setTimeout(() => setupConfigListener(attempt + 1), 1);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
fontSizeObserver = Application.ios.addNotificationObserver(UIContentSizeCategoryDidChangeNotification, (args) => {
|
||||
const fontSize = args.userInfo.valueForKey(UIContentSizeCategoryNewValueKey);
|
||||
contentSizeUpdated(fontSize);
|
||||
});
|
||||
|
||||
Application.on(Application.exitEvent, () => {
|
||||
if (fontSizeObserver) {
|
||||
Application.ios.removeNotificationObserver(fontSizeObserver, UIContentSizeCategoryDidChangeNotification);
|
||||
fontSizeObserver = null;
|
||||
}
|
||||
|
||||
Application.off(Application.resumeEvent, useIOSFontScale);
|
||||
});
|
||||
|
||||
Application.on(Application.resumeEvent, useIOSFontScale);
|
||||
|
||||
useIOSFontScale();
|
||||
}
|
||||
setInitFontScale(setupConfigListener);
|
||||
|
||||
/**
|
||||
* Convert array of values into a bitmask.
|
||||
*
|
||||
* @param values string values
|
||||
* @param map map lower-case name to integer value.
|
||||
*/
|
||||
function inputArrayToBitMask(values: string | string[], map: Map<string, number>): number {
|
||||
return (
|
||||
enforceArray(values)
|
||||
.filter((value) => !!value)
|
||||
.map((value) => `${value}`.toLocaleLowerCase())
|
||||
.filter((value) => map.has(value))
|
||||
.reduce((res, value) => res | map.get(value), 0) || 0
|
||||
);
|
||||
}
|
||||
|
||||
let AccessibilityTraitsMap: Map<string, number>;
|
||||
let RoleTypeMap: Map<AccessibilityRole, number>;
|
||||
|
||||
let nativeFocusedNotificationObserver;
|
||||
let lastFocusedView: WeakRef<View>;
|
||||
function ensureNativeClasses() {
|
||||
if (AccessibilityTraitsMap && nativeFocusedNotificationObserver) {
|
||||
return;
|
||||
}
|
||||
|
||||
AccessibilityTraitsMap = new Map<AccessibilityTrait, number>([
|
||||
[AccessibilityTrait.AllowsDirectInteraction, UIAccessibilityTraitAllowsDirectInteraction],
|
||||
[AccessibilityTrait.CausesPageTurn, UIAccessibilityTraitCausesPageTurn],
|
||||
[AccessibilityTrait.NotEnabled, UIAccessibilityTraitNotEnabled],
|
||||
[AccessibilityTrait.Selected, UIAccessibilityTraitSelected],
|
||||
[AccessibilityTrait.UpdatesFrequently, UIAccessibilityTraitUpdatesFrequently],
|
||||
]);
|
||||
|
||||
RoleTypeMap = new Map<AccessibilityRole, number>([
|
||||
[AccessibilityRole.Adjustable, UIAccessibilityTraitAdjustable],
|
||||
[AccessibilityRole.Button, UIAccessibilityTraitButton],
|
||||
[AccessibilityRole.Checkbox, UIAccessibilityTraitButton],
|
||||
[AccessibilityRole.Header, UIAccessibilityTraitHeader],
|
||||
[AccessibilityRole.KeyboardKey, UIAccessibilityTraitKeyboardKey],
|
||||
[AccessibilityRole.Image, UIAccessibilityTraitImage],
|
||||
[AccessibilityRole.ImageButton, UIAccessibilityTraitImage | UIAccessibilityTraitButton],
|
||||
[AccessibilityRole.Link, UIAccessibilityTraitLink],
|
||||
[AccessibilityRole.None, UIAccessibilityTraitNone],
|
||||
[AccessibilityRole.PlaysSound, UIAccessibilityTraitPlaysSound],
|
||||
[AccessibilityRole.RadioButton, UIAccessibilityTraitButton],
|
||||
[AccessibilityRole.Search, UIAccessibilityTraitSearchField],
|
||||
[AccessibilityRole.StaticText, UIAccessibilityTraitStaticText],
|
||||
[AccessibilityRole.StartsMediaSession, UIAccessibilityTraitStartsMediaSession],
|
||||
[AccessibilityRole.Summary, UIAccessibilityTraitSummaryElement],
|
||||
[AccessibilityRole.Switch, UIAccessibilityTraitButton],
|
||||
]);
|
||||
|
||||
nativeFocusedNotificationObserver = Application.ios.addNotificationObserver(UIAccessibilityElementFocusedNotification, (args: NSNotification) => {
|
||||
const uiView = args.userInfo?.objectForKey(UIAccessibilityFocusedElementKey) as UIView;
|
||||
if (!uiView?.tag) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rootView = Application.getRootView();
|
||||
|
||||
// We use the UIView's tag to find the NativeScript View by its domId.
|
||||
let view = rootView.getViewByDomId<View>(uiView?.tag);
|
||||
if (!view) {
|
||||
for (const modalView of <Array<View>>rootView._getRootModalViews()) {
|
||||
view = modalView.getViewByDomId(uiView?.tag);
|
||||
if (view) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!view) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lastView = lastFocusedView?.deref();
|
||||
if (lastView && view !== lastView) {
|
||||
const lastFocusedUIView = lastView.nativeViewProtected as UIView;
|
||||
if (lastFocusedUIView) {
|
||||
lastFocusedView = null;
|
||||
|
||||
notifyAccessibilityFocusState(lastView, false, true);
|
||||
}
|
||||
}
|
||||
|
||||
lastFocusedView = new WeakRef(view);
|
||||
|
||||
notifyAccessibilityFocusState(view, true, false);
|
||||
});
|
||||
|
||||
Application.on(Application.exitEvent, () => {
|
||||
if (nativeFocusedNotificationObserver) {
|
||||
Application.ios.removeNotificationObserver(nativeFocusedNotificationObserver, UIAccessibilityElementFocusedNotification);
|
||||
}
|
||||
|
||||
nativeFocusedNotificationObserver = null;
|
||||
lastFocusedView = null;
|
||||
});
|
||||
}
|
||||
|
||||
export function updateAccessibilityProperties(view: View): void {
|
||||
const uiView = view.nativeViewProtected as UIView;
|
||||
if (!uiView) {
|
||||
return;
|
||||
}
|
||||
|
||||
ensureNativeClasses();
|
||||
|
||||
const accessibilityRole = view.accessibilityRole;
|
||||
const accessibilityState = view.accessibilityState;
|
||||
|
||||
if (!view.accessible || view.accessibilityHidden) {
|
||||
uiView.accessibilityTraits = UIAccessibilityTraitNone;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// NOTE: left here for various core inspection passes while running the toolbox app
|
||||
// console.log('--- Accessible element: ', view.constructor.name);
|
||||
// console.log('accessibilityLabel: ', view.accessibilityLabel);
|
||||
// console.log('accessibilityRole: ', accessibilityRole);
|
||||
// console.log('accessibilityState: ', accessibilityState);
|
||||
// console.log('accessibilityValue: ', view.accessibilityValue);
|
||||
|
||||
let a11yTraits = UIAccessibilityTraitNone;
|
||||
if (RoleTypeMap.has(accessibilityRole)) {
|
||||
a11yTraits |= RoleTypeMap.get(accessibilityRole);
|
||||
}
|
||||
|
||||
switch (accessibilityRole) {
|
||||
case AccessibilityRole.Checkbox:
|
||||
case AccessibilityRole.RadioButton:
|
||||
case AccessibilityRole.Switch: {
|
||||
if (accessibilityState === AccessibilityState.Checked) {
|
||||
a11yTraits |= AccessibilityTraitsMap.get(AccessibilityTrait.Selected);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
if (accessibilityState === AccessibilityState.Selected) {
|
||||
a11yTraits |= AccessibilityTraitsMap.get(AccessibilityTrait.Selected);
|
||||
}
|
||||
if (accessibilityState === AccessibilityState.Disabled) {
|
||||
a11yTraits |= AccessibilityTraitsMap.get(AccessibilityTrait.NotEnabled);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const UpdatesFrequentlyTrait = AccessibilityTraitsMap.get(AccessibilityTrait.UpdatesFrequently);
|
||||
|
||||
switch (view.accessibilityLiveRegion) {
|
||||
case AccessibilityLiveRegion.Polite:
|
||||
case AccessibilityLiveRegion.Assertive: {
|
||||
a11yTraits |= UpdatesFrequentlyTrait;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
a11yTraits &= ~UpdatesFrequentlyTrait;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: left here for various core inspection passes while running the toolbox app
|
||||
// if (view.accessibilityLiveRegion) {
|
||||
// console.log('accessibilityLiveRegion:', view.accessibilityLiveRegion);
|
||||
// }
|
||||
|
||||
if (view.accessibilityMediaSession) {
|
||||
a11yTraits |= RoleTypeMap.get(AccessibilityRole.StartsMediaSession);
|
||||
}
|
||||
|
||||
// NOTE: There were duplicated types in traits and roles previously which we conslidated
|
||||
// not sure if this is still needed
|
||||
// accessibilityTraits used to be stored on {N} view component but if the above
|
||||
// is combining all traits fresh each time through, don't believe we need to keep track or previous traits
|
||||
// if (view.accessibilityTraits) {
|
||||
// a11yTraits |= inputArrayToBitMask(view.accessibilityTraits, AccessibilityTraitsMap);
|
||||
// }
|
||||
|
||||
// NOTE: left here for various core inspection passes while running the toolbox app
|
||||
// console.log('a11yTraits:', a11yTraits);
|
||||
// console.log(' ');
|
||||
|
||||
uiView.accessibilityTraits = a11yTraits;
|
||||
}
|
||||
setA11yUpdatePropertiesCallback(updateAccessibilityProperties);
|
||||
|
||||
export const sendAccessibilityEvent = (): void => {};
|
||||
|
||||
export function isAccessibilityServiceEnabled(): boolean {
|
||||
const accessibilityServiceEnabled = isA11yEnabled();
|
||||
if (typeof accessibilityServiceEnabled === 'boolean') {
|
||||
return accessibilityServiceEnabled;
|
||||
}
|
||||
|
||||
let isVoiceOverRunning: () => boolean;
|
||||
if (typeof UIAccessibilityIsVoiceOverRunning === 'function') {
|
||||
isVoiceOverRunning = UIAccessibilityIsVoiceOverRunning;
|
||||
} else {
|
||||
// iOS is too old to tell us if voice over is enabled
|
||||
if (typeof UIAccessibilityIsVoiceOverRunning !== 'function') {
|
||||
setA11yEnabled(false);
|
||||
return isA11yEnabled();
|
||||
}
|
||||
}
|
||||
|
||||
setA11yEnabled(isVoiceOverRunning());
|
||||
|
||||
let voiceOverStatusChangedNotificationName: string | null = null;
|
||||
if (typeof UIAccessibilityVoiceOverStatusDidChangeNotification !== 'undefined') {
|
||||
voiceOverStatusChangedNotificationName = UIAccessibilityVoiceOverStatusDidChangeNotification;
|
||||
} else if (typeof UIAccessibilityVoiceOverStatusChanged !== 'undefined') {
|
||||
voiceOverStatusChangedNotificationName = UIAccessibilityVoiceOverStatusChanged;
|
||||
}
|
||||
|
||||
if (voiceOverStatusChangedNotificationName) {
|
||||
nativeObserver = Application.ios.addNotificationObserver(voiceOverStatusChangedNotificationName, () => {
|
||||
setA11yEnabled(isVoiceOverRunning());
|
||||
});
|
||||
|
||||
Application.on(Application.exitEvent, () => {
|
||||
if (nativeObserver) {
|
||||
Application.ios.removeNotificationObserver(nativeObserver, voiceOverStatusChangedNotificationName);
|
||||
}
|
||||
|
||||
setA11yEnabled(undefined);
|
||||
nativeObserver = null;
|
||||
});
|
||||
}
|
||||
|
||||
Application.on(Application.resumeEvent, () => {
|
||||
setA11yEnabled(isVoiceOverRunning());
|
||||
});
|
||||
|
||||
return isA11yEnabled();
|
||||
}
|
||||
|
||||
export function getAndroidAccessibilityManager(): null {
|
||||
return null;
|
||||
}
|
||||
|
||||
let sharedA11YObservable: SharedA11YObservable;
|
||||
let nativeObserver;
|
||||
|
||||
function getSharedA11YObservable(): SharedA11YObservable {
|
||||
if (sharedA11YObservable) {
|
||||
return sharedA11YObservable;
|
||||
}
|
||||
|
||||
sharedA11YObservable = new SharedA11YObservable();
|
||||
|
||||
let isVoiceOverRunning: () => boolean;
|
||||
if (typeof UIAccessibilityIsVoiceOverRunning === 'function') {
|
||||
isVoiceOverRunning = UIAccessibilityIsVoiceOverRunning;
|
||||
} else {
|
||||
if (typeof UIAccessibilityIsVoiceOverRunning !== 'function') {
|
||||
Trace.write(`UIAccessibilityIsVoiceOverRunning() - is not a function`, Trace.categories.Accessibility, Trace.messageType.error);
|
||||
|
||||
isVoiceOverRunning = () => false;
|
||||
}
|
||||
}
|
||||
|
||||
sharedA11YObservable.set(AccessibilityServiceEnabledPropName, isVoiceOverRunning());
|
||||
|
||||
let voiceOverStatusChangedNotificationName: string | null = null;
|
||||
if (typeof UIAccessibilityVoiceOverStatusDidChangeNotification !== 'undefined') {
|
||||
// iOS 11+
|
||||
voiceOverStatusChangedNotificationName = UIAccessibilityVoiceOverStatusDidChangeNotification;
|
||||
} else if (typeof UIAccessibilityVoiceOverStatusChanged !== 'undefined') {
|
||||
// iOS <11
|
||||
voiceOverStatusChangedNotificationName = UIAccessibilityVoiceOverStatusChanged;
|
||||
}
|
||||
|
||||
if (voiceOverStatusChangedNotificationName) {
|
||||
nativeObserver = Application.ios.addNotificationObserver(voiceOverStatusChangedNotificationName, () => {
|
||||
sharedA11YObservable?.set(AccessibilityServiceEnabledPropName, isVoiceOverRunning());
|
||||
});
|
||||
|
||||
Application.on(Application.exitEvent, () => {
|
||||
if (nativeObserver) {
|
||||
Application.ios.removeNotificationObserver(nativeObserver, voiceOverStatusChangedNotificationName);
|
||||
}
|
||||
|
||||
nativeObserver = null;
|
||||
|
||||
if (sharedA11YObservable) {
|
||||
sharedA11YObservable.removeEventListener(Observable.propertyChangeEvent);
|
||||
|
||||
sharedA11YObservable = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Application.on(Application.resumeEvent, () => sharedA11YObservable.set(AccessibilityServiceEnabledPropName, isVoiceOverRunning()));
|
||||
|
||||
return sharedA11YObservable;
|
||||
}
|
||||
|
||||
export class AccessibilityServiceEnabledObservable extends CommonA11YServiceEnabledObservable {
|
||||
constructor() {
|
||||
super(getSharedA11YObservable());
|
||||
}
|
||||
}
|
||||
|
||||
let accessibilityServiceObservable: AccessibilityServiceEnabledObservable;
|
||||
export function ensureClasses() {
|
||||
if (accessibilityServiceObservable) {
|
||||
return;
|
||||
}
|
||||
|
||||
setFontScaleCssClasses(new Map(VALID_FONT_SCALES.map((fs) => [fs, `a11y-fontscale-${Number(fs * 100).toFixed(0)}`])));
|
||||
|
||||
accessibilityServiceObservable = new AccessibilityServiceEnabledObservable();
|
||||
|
||||
// Initialize SDK version CSS class once
|
||||
initializeSdkVersionClass(Application.getRootView());
|
||||
}
|
||||
|
||||
export function updateCurrentHelperClasses(applyRootCssClass: (cssClasses: string[], newCssClass: string) => void): void {
|
||||
const fontScale = getFontScale();
|
||||
const fontScaleCategory = getFontScaleCategory();
|
||||
const fontScaleCssClasses = getFontScaleCssClasses();
|
||||
const oldFontScaleClass = getCurrentFontScaleClass();
|
||||
if (fontScaleCssClasses.has(fontScale)) {
|
||||
setCurrentFontScaleClass(fontScaleCssClasses.get(fontScale));
|
||||
} else {
|
||||
setCurrentFontScaleClass(fontScaleCssClasses.get(1));
|
||||
}
|
||||
|
||||
if (oldFontScaleClass !== getCurrentFontScaleClass()) {
|
||||
applyRootCssClass([...fontScaleCssClasses.values()], getCurrentFontScaleClass());
|
||||
}
|
||||
|
||||
const oldActiveFontScaleCategory = getCurrentFontScaleCategory();
|
||||
switch (fontScaleCategory) {
|
||||
case FontScaleCategory.ExtraSmall: {
|
||||
setCurrentFontScaleCategory(fontScaleExtraSmallCategoryClass);
|
||||
break;
|
||||
}
|
||||
case FontScaleCategory.Medium: {
|
||||
setCurrentFontScaleCategory(fontScaleMediumCategoryClass);
|
||||
break;
|
||||
}
|
||||
case FontScaleCategory.ExtraLarge: {
|
||||
setCurrentFontScaleCategory(fontScaleExtraLargeCategoryClass);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
setCurrentFontScaleCategory(fontScaleMediumCategoryClass);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (oldActiveFontScaleCategory !== getCurrentFontScaleCategory()) {
|
||||
applyRootCssClass(fontScaleCategoryClasses, getCurrentFontScaleCategory());
|
||||
}
|
||||
|
||||
const oldA11YStatusClass = getCurrentA11YServiceClass();
|
||||
if (accessibilityServiceObservable.accessibilityServiceEnabled) {
|
||||
setCurrentA11YServiceClass(a11yServiceEnabledClass);
|
||||
} else {
|
||||
setCurrentA11YServiceClass(a11yServiceDisabledClass);
|
||||
}
|
||||
|
||||
if (oldA11YStatusClass !== getCurrentA11YServiceClass()) {
|
||||
applyRootCssClass(a11yServiceClasses, getCurrentA11YServiceClass());
|
||||
}
|
||||
}
|
||||
|
||||
function applyRootCssClass(cssClasses: string[], newCssClass: string): void {
|
||||
const rootView = Application.getRootView();
|
||||
if (!rootView) {
|
||||
return;
|
||||
}
|
||||
|
||||
Application.applyCssClass(rootView, cssClasses, newCssClass);
|
||||
|
||||
const rootModalViews = <Array<View>>rootView._getRootModalViews();
|
||||
rootModalViews.forEach((rootModalView) => Application.applyCssClass(rootModalView, cssClasses, newCssClass));
|
||||
}
|
||||
|
||||
function applyFontScaleToRootViews(): void {
|
||||
const rootView = Application.getRootView();
|
||||
if (!rootView) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fontScale = getCurrentFontScale();
|
||||
|
||||
rootView.style.fontScaleInternal = fontScale;
|
||||
|
||||
const rootModalViews = <Array<View>>rootView._getRootModalViews();
|
||||
rootModalViews.forEach((rootModalView) => (rootModalView.style.fontScaleInternal = fontScale));
|
||||
}
|
||||
|
||||
export function initAccessibilityCssHelper(): void {
|
||||
ensureClasses();
|
||||
|
||||
Application.on(Application.fontScaleChangedEvent, () => {
|
||||
updateCurrentHelperClasses(applyRootCssClass);
|
||||
applyFontScaleToRootViews();
|
||||
});
|
||||
|
||||
accessibilityServiceObservable.on(AccessibilityServiceEnabledObservable.propertyChangeEvent, () => updateCurrentHelperClasses(applyRootCssClass));
|
||||
}
|
||||
setInitAccessibilityCssHelper(initAccessibilityCssHelper);
|
||||
|
||||
const applicationEvents: string[] = [Application.orientationChangedEvent, Application.systemAppearanceChangedEvent];
|
||||
function toggleApplicationEventListeners(toAdd: boolean, callback: (args: ApplicationEventData) => void) {
|
||||
for (const eventName of applicationEvents) {
|
||||
if (toAdd) {
|
||||
Application.on(eventName, callback);
|
||||
} else {
|
||||
Application.off(eventName, callback);
|
||||
}
|
||||
}
|
||||
}
|
||||
setToggleApplicationEventListenersCallback(toggleApplicationEventListeners);
|
||||
|
||||
setApplicationPropertiesCallback(() => {
|
||||
return {
|
||||
orientation: Application.orientation(),
|
||||
systemAppearance: Application.systemAppearance(),
|
||||
};
|
||||
});
|
||||
|
||||
191
packages/core/application/helpers-common.ts
Normal file
191
packages/core/application/helpers-common.ts
Normal file
@ -0,0 +1,191 @@
|
||||
/**
|
||||
* Do not import other files here to avoid circular dependencies.
|
||||
* Used to define helper functions and variables that are shared between Android and iOS.
|
||||
* This file can declare cross platform types and use bundler platform checks.
|
||||
* For example, use `__ANDROID__` or `__APPLE__` to check the platform.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Application type. UIApplication on iOS or android.app.Application on Android.
|
||||
*/
|
||||
type NativeApp = UIApplication | android.app.Application;
|
||||
|
||||
let nativeApp: NativeApp;
|
||||
|
||||
declare namespace com {
|
||||
namespace tns {
|
||||
class NativeScriptApplication extends android.app.Application {
|
||||
static getInstance(): NativeScriptApplication;
|
||||
}
|
||||
|
||||
namespace embedding {
|
||||
class ApplicationHolder {
|
||||
static getInstance(): android.app.Application;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isEmbedded(): boolean {
|
||||
if (__APPLE__) {
|
||||
return !!NativeScriptEmbedder.sharedInstance().delegate;
|
||||
} else {
|
||||
// @ts-ignore
|
||||
// Check if the Bootstrap class exists and has the isEmbeddedNativeScript property
|
||||
// This is a way to determine if the app is embedded in a host project.
|
||||
return org.nativescript?.Bootstrap?.isEmbeddedNativeScript;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current application instance.
|
||||
* @returns current application instance, UIApplication on iOS or android.app.Application on Android.
|
||||
*/
|
||||
export function getNativeApp<T extends NativeApp>(): T {
|
||||
if (__ANDROID__) {
|
||||
if (!nativeApp) {
|
||||
// Try getting it from module - check whether application.android.init has been explicitly called
|
||||
// check whether the com.tns.NativeScriptApplication type exists
|
||||
if (com.tns.NativeScriptApplication) {
|
||||
nativeApp = com.tns.NativeScriptApplication.getInstance();
|
||||
}
|
||||
|
||||
if (!nativeApp && isEmbedded()) {
|
||||
nativeApp = com.tns.embedding.ApplicationHolder.getInstance();
|
||||
}
|
||||
|
||||
// the getInstance might return null if com.tns.NativeScriptApplication exists but is not the starting app type
|
||||
if (!nativeApp) {
|
||||
// TODO: Should we handle the case when a custom application type is provided and the user has not explicitly initialized the application module?
|
||||
const clazz = java.lang.Class.forName('android.app.ActivityThread');
|
||||
if (clazz) {
|
||||
const method = clazz.getMethod('currentApplication', null);
|
||||
if (method) {
|
||||
nativeApp = method.invoke(null, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nativeApp! as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is called internally to set the native application instance.
|
||||
* You typically do not need to call this directly.
|
||||
* However, it's exposed for special case purposes, such as custom application initialization.
|
||||
* @param app The native application instance to set.
|
||||
* This should be called once during application startup to set the native app instance.
|
||||
*/
|
||||
export function setNativeApp(app: NativeApp) {
|
||||
nativeApp = app;
|
||||
}
|
||||
|
||||
let rootView: any;
|
||||
|
||||
export function getRootView() {
|
||||
return rootView;
|
||||
}
|
||||
|
||||
export function setRootView(view: any) {
|
||||
rootView = view;
|
||||
}
|
||||
|
||||
let _appInBackground: boolean = false;
|
||||
export function isAppInBackground() {
|
||||
return _appInBackground;
|
||||
}
|
||||
export function setAppInBackground(value: boolean) {
|
||||
_appInBackground = value;
|
||||
}
|
||||
|
||||
let _iosWindow: UIWindow;
|
||||
export function getiOSWindow(): UIWindow {
|
||||
return _iosWindow;
|
||||
}
|
||||
export function setiOSWindow(value: UIWindow) {
|
||||
_iosWindow = value;
|
||||
}
|
||||
|
||||
let _appMainEntry: any /* NavigationEntry */;
|
||||
|
||||
export function getAppMainEntry(): any /* NavigationEntry */ {
|
||||
return _appMainEntry;
|
||||
}
|
||||
export function setAppMainEntry(entry: any /* NavigationEntry */) {
|
||||
_appMainEntry = entry;
|
||||
}
|
||||
|
||||
// Aids avoiding circular dependencies by allowing the application event listeners to be toggled
|
||||
let _toggleApplicationEventListenersHandler: (toAdd: boolean, callback: (args: any) => void) => void;
|
||||
export function toggleApplicationEventListeners(toAdd: boolean, callback: (args: any) => void) {
|
||||
if (_toggleApplicationEventListenersHandler) {
|
||||
_toggleApplicationEventListenersHandler(toAdd, callback);
|
||||
}
|
||||
}
|
||||
export function setToggleApplicationEventListenersCallback(callback: (toAdd: boolean, callback: (args: any) => void) => void) {
|
||||
_toggleApplicationEventListenersHandler = callback;
|
||||
}
|
||||
|
||||
// Aids avoiding circular dependencies by allowing the application properties to be retrieved
|
||||
type ApplicationPropertyValues = { orientation: 'portrait' | 'landscape' | 'unknown'; systemAppearance: 'dark' | 'light' | null };
|
||||
let _applicationPropertiesCallback: () => ApplicationPropertyValues;
|
||||
export function getApplicationProperties(): ApplicationPropertyValues {
|
||||
if (_applicationPropertiesCallback) {
|
||||
return _applicationPropertiesCallback();
|
||||
}
|
||||
return { orientation: 'unknown', systemAppearance: null };
|
||||
}
|
||||
export function setApplicationPropertiesCallback(callback: () => ApplicationPropertyValues) {
|
||||
_applicationPropertiesCallback = callback;
|
||||
}
|
||||
|
||||
let _a11yUpdatePropertiesCallback: (view: any /* View */) => void;
|
||||
export function setA11yUpdatePropertiesCallback(callback: (view: any /* View */) => void) {
|
||||
_a11yUpdatePropertiesCallback = callback;
|
||||
}
|
||||
export function updateA11yPropertiesCallback(view: any /* View */) {
|
||||
if (_a11yUpdatePropertiesCallback) {
|
||||
_a11yUpdatePropertiesCallback(view);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal Android app helpers
|
||||
*/
|
||||
let _imageFetcher: org.nativescript.widgets.image.Fetcher;
|
||||
export function getImageFetcher(): org.nativescript.widgets.image.Fetcher {
|
||||
return _imageFetcher;
|
||||
}
|
||||
export function setImageFetcher(fetcher: org.nativescript.widgets.image.Fetcher) {
|
||||
_imageFetcher = fetcher;
|
||||
}
|
||||
export enum CacheMode {
|
||||
none,
|
||||
memory,
|
||||
diskAndMemory,
|
||||
}
|
||||
|
||||
let _currentCacheMode: CacheMode;
|
||||
|
||||
export function initImageCache(context: android.content.Context, mode = CacheMode.diskAndMemory, memoryCacheSize = 0.25, diskCacheSize: number = 10 * 1024 * 1024): void {
|
||||
if (_currentCacheMode === mode) {
|
||||
return;
|
||||
}
|
||||
|
||||
_currentCacheMode = mode;
|
||||
if (!getImageFetcher()) {
|
||||
setImageFetcher(org.nativescript.widgets.image.Fetcher.getInstance(context));
|
||||
} else {
|
||||
getImageFetcher().clearCache();
|
||||
}
|
||||
|
||||
const params = new org.nativescript.widgets.image.Cache.CacheParams();
|
||||
params.memoryCacheEnabled = mode !== CacheMode.none;
|
||||
params.setMemCacheSizePercent(memoryCacheSize); // Set memory cache to % of app memory
|
||||
params.diskCacheEnabled = mode === CacheMode.diskAndMemory;
|
||||
params.diskCacheSize = diskCacheSize;
|
||||
const imageCache = org.nativescript.widgets.image.Cache.getInstance(params);
|
||||
getImageFetcher().addImageCache(imageCache);
|
||||
getImageFetcher().initCache();
|
||||
}
|
||||
209
packages/core/application/helpers.android.ts
Normal file
209
packages/core/application/helpers.android.ts
Normal file
@ -0,0 +1,209 @@
|
||||
import { SDK_VERSION } from '../utils/constants';
|
||||
import { getNativeApp, updateA11yPropertiesCallback } from './helpers-common';
|
||||
import { AccessibilityRole, AccessibilityState } from '../accessibility/accessibility-common';
|
||||
import { Trace } from '../trace';
|
||||
|
||||
let _startActivity: androidx.appcompat.app.AppCompatActivity;
|
||||
let _foregroundActivity: androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
export function androidGetCurrentActivity(): androidx.appcompat.app.AppCompatActivity {
|
||||
return _foregroundActivity || _startActivity;
|
||||
}
|
||||
export function androidGetForegroundActivity(): androidx.appcompat.app.AppCompatActivity {
|
||||
return _foregroundActivity;
|
||||
}
|
||||
export function androidSetForegroundActivity(activity: androidx.appcompat.app.AppCompatActivity): void {
|
||||
_foregroundActivity = activity;
|
||||
}
|
||||
export function androidGetStartActivity(): androidx.appcompat.app.AppCompatActivity {
|
||||
return _startActivity;
|
||||
}
|
||||
export function androidSetStartActivity(activity: androidx.appcompat.app.AppCompatActivity): void {
|
||||
_startActivity = activity;
|
||||
}
|
||||
|
||||
function getApplicationContext(): android.content.Context {
|
||||
return getNativeApp<android.app.Application>().getApplicationContext();
|
||||
}
|
||||
|
||||
export const androidRegisteredReceivers: { [key: string]: android.content.BroadcastReceiver } = {};
|
||||
export const androidPendingReceiverRegistrations = new Array<(context: android.content.Context) => void>();
|
||||
|
||||
declare class BroadcastReceiver extends android.content.BroadcastReceiver {
|
||||
constructor(onReceiveCallback: (context: android.content.Context, intent: android.content.Intent) => void);
|
||||
}
|
||||
|
||||
let BroadcastReceiver_: typeof BroadcastReceiver;
|
||||
function initBroadcastReceiver() {
|
||||
if (BroadcastReceiver_) {
|
||||
return BroadcastReceiver_;
|
||||
}
|
||||
|
||||
@NativeClass
|
||||
class BroadcastReceiverImpl extends android.content.BroadcastReceiver {
|
||||
private _onReceiveCallback: (context: android.content.Context, intent: android.content.Intent) => void;
|
||||
|
||||
constructor(onReceiveCallback: (context: android.content.Context, intent: android.content.Intent) => void) {
|
||||
super();
|
||||
this._onReceiveCallback = onReceiveCallback;
|
||||
|
||||
return global.__native(this);
|
||||
}
|
||||
|
||||
public onReceive(context: android.content.Context, intent: android.content.Intent) {
|
||||
if (this._onReceiveCallback) {
|
||||
this._onReceiveCallback(context, intent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BroadcastReceiver_ = BroadcastReceiverImpl;
|
||||
return BroadcastReceiver_;
|
||||
}
|
||||
|
||||
export function androidRegisterBroadcastReceiver(intentFilter: string, onReceiveCallback: (context: android.content.Context, intent: android.content.Intent) => void, flags = 2): void {
|
||||
const registerFunc = (context: android.content.Context) => {
|
||||
const receiver: android.content.BroadcastReceiver = new (initBroadcastReceiver())(onReceiveCallback);
|
||||
if (SDK_VERSION >= 26) {
|
||||
context.registerReceiver(receiver, new android.content.IntentFilter(intentFilter), flags);
|
||||
} else {
|
||||
context.registerReceiver(receiver, new android.content.IntentFilter(intentFilter));
|
||||
}
|
||||
androidRegisteredReceivers[intentFilter] = receiver;
|
||||
};
|
||||
|
||||
if (getApplicationContext()) {
|
||||
registerFunc(getApplicationContext());
|
||||
} else {
|
||||
androidPendingReceiverRegistrations.push(registerFunc);
|
||||
}
|
||||
}
|
||||
|
||||
export function androidUnregisterBroadcastReceiver(intentFilter: string): void {
|
||||
const receiver = androidRegisteredReceivers[intentFilter];
|
||||
if (receiver) {
|
||||
getApplicationContext().unregisterReceiver(receiver);
|
||||
androidRegisteredReceivers[intentFilter] = undefined;
|
||||
delete androidRegisteredReceivers[intentFilter];
|
||||
}
|
||||
}
|
||||
|
||||
export function updateContentDescription(view: any /* View */, forceUpdate?: boolean): string | null {
|
||||
if (!view.nativeViewProtected) {
|
||||
return;
|
||||
}
|
||||
|
||||
return applyContentDescription(view, forceUpdate);
|
||||
}
|
||||
|
||||
export function applyContentDescription(view: any /* View */, forceUpdate?: boolean) {
|
||||
let androidView = view.nativeViewProtected as android.view.View;
|
||||
if (!androidView || (androidView instanceof android.widget.TextView && !view._androidContentDescriptionUpdated)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (androidView instanceof androidx.appcompat.widget.Toolbar) {
|
||||
const numChildren = androidView.getChildCount();
|
||||
|
||||
for (let i = 0; i < numChildren; i += 1) {
|
||||
const childAndroidView = androidView.getChildAt(i);
|
||||
if (childAndroidView instanceof androidx.appcompat.widget.AppCompatTextView) {
|
||||
androidView = childAndroidView;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const cls = `applyContentDescription(${view})`;
|
||||
|
||||
const titleValue = view['title'] as string;
|
||||
const textValue = view['text'] as string;
|
||||
|
||||
if (!forceUpdate && view._androidContentDescriptionUpdated === false && textValue === view['_lastText'] && titleValue === view['_lastTitle']) {
|
||||
// prevent updating this too much
|
||||
return androidView.getContentDescription();
|
||||
}
|
||||
|
||||
const contentDescriptionBuilder = new Array<string>();
|
||||
|
||||
// Workaround: TalkBack won't read the checked state for fake Switch.
|
||||
if (view.accessibilityRole === AccessibilityRole.Switch) {
|
||||
const androidSwitch = new android.widget.Switch(getApplicationContext());
|
||||
if (view.accessibilityState === AccessibilityState.Checked) {
|
||||
contentDescriptionBuilder.push(androidSwitch.getTextOn());
|
||||
} else {
|
||||
contentDescriptionBuilder.push(androidSwitch.getTextOff());
|
||||
}
|
||||
}
|
||||
|
||||
if (view.accessibilityLabel) {
|
||||
if (Trace.isEnabled()) {
|
||||
Trace.write(`${cls} - have accessibilityLabel`, Trace.categories.Accessibility);
|
||||
}
|
||||
|
||||
contentDescriptionBuilder.push(`${view.accessibilityLabel}`);
|
||||
}
|
||||
|
||||
if (view.accessibilityValue) {
|
||||
if (Trace.isEnabled()) {
|
||||
Trace.write(`${cls} - have accessibilityValue`, Trace.categories.Accessibility);
|
||||
}
|
||||
|
||||
contentDescriptionBuilder.push(`${view.accessibilityValue}`);
|
||||
} else if (textValue) {
|
||||
if (textValue !== view.accessibilityLabel) {
|
||||
if (Trace.isEnabled()) {
|
||||
Trace.write(`${cls} - don't have accessibilityValue - use 'text' value`, Trace.categories.Accessibility);
|
||||
}
|
||||
|
||||
contentDescriptionBuilder.push(`${textValue}`);
|
||||
}
|
||||
} else if (titleValue) {
|
||||
if (titleValue !== view.accessibilityLabel) {
|
||||
if (Trace.isEnabled()) {
|
||||
Trace.write(`${cls} - don't have accessibilityValue - use 'title' value`, Trace.categories.Accessibility);
|
||||
}
|
||||
|
||||
contentDescriptionBuilder.push(`${titleValue}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (view.accessibilityHint) {
|
||||
if (Trace.isEnabled()) {
|
||||
Trace.write(`${cls} - have accessibilityHint`, Trace.categories.Accessibility);
|
||||
}
|
||||
|
||||
contentDescriptionBuilder.push(`${view.accessibilityHint}`);
|
||||
}
|
||||
|
||||
const contentDescription = contentDescriptionBuilder.join('. ').trim().replace(/^\.$/, '');
|
||||
|
||||
if (contentDescription) {
|
||||
if (Trace.isEnabled()) {
|
||||
Trace.write(`${cls} - set to "${contentDescription}"`, Trace.categories.Accessibility);
|
||||
}
|
||||
|
||||
androidView.setContentDescription(contentDescription);
|
||||
} else {
|
||||
if (Trace.isEnabled()) {
|
||||
Trace.write(`${cls} - remove value`, Trace.categories.Accessibility);
|
||||
}
|
||||
|
||||
androidView.setContentDescription(null);
|
||||
}
|
||||
|
||||
view['_lastTitle'] = titleValue;
|
||||
view['_lastText'] = textValue;
|
||||
view._androidContentDescriptionUpdated = false;
|
||||
|
||||
return contentDescription;
|
||||
}
|
||||
|
||||
export function setupAccessibleView(view: any /* any */): void {
|
||||
updateA11yPropertiesCallback(view);
|
||||
}
|
||||
|
||||
// stubs
|
||||
export const iosNotificationObservers: Array<any> = [];
|
||||
export function iosAddNotificationObserver(notificationName: string, onReceiveCallback: (notification: any) => void) {}
|
||||
export function iosRemoveNotificationObserver(observer: any, notificationName: string) {}
|
||||
25
packages/core/application/helpers.d.ts
vendored
Normal file
25
packages/core/application/helpers.d.ts
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Initialize accessibility for View. This should be called on loaded-event.
|
||||
*/
|
||||
export function setupAccessibleView(view: View): void;
|
||||
/**
|
||||
* Android: Update the content description for views
|
||||
*/
|
||||
export const updateContentDescription: (view: any /* View */, forceUpdate?: boolean) => string | null;
|
||||
export function applyContentDescription(view: any /* View */, forceUpdate?: boolean);
|
||||
/* Android app-wide helpers */
|
||||
export const androidRegisteredReceivers: { [key: string]: android.content.BroadcastReceiver };
|
||||
export const androidPendingReceiverRegistrations: Array<(context: android.content.Context) => void>;
|
||||
export function androidRegisterBroadcastReceiver(intentFilter: string, onReceiveCallback: (context: android.content.Context, intent: android.content.Intent) => void, flags = 2): void;
|
||||
export function androidUnregisterBroadcastReceiver(intentFilter: string): void;
|
||||
export function androidGetCurrentActivity(): androidx.appcompat.app.AppCompatActivity;
|
||||
export function androidGetForegroundActivity(): androidx.appcompat.app.AppCompatActivity;
|
||||
export function androidSetForegroundActivity(activity: androidx.appcompat.app.AppCompatActivity): void;
|
||||
export function androidGetStartActivity(): androidx.appcompat.app.AppCompatActivity;
|
||||
export function androidSetStartActivity(activity: androidx.appcompat.app.AppCompatActivity): void;
|
||||
|
||||
/* iOS app-wide helpers */
|
||||
export const iosNotificationObservers: NotificationObserver[];
|
||||
class NotificationObserver extends NSObject {}
|
||||
export function iosAddNotificationObserver(notificationName: string, onReceiveCallback: (notification: NSNotification) => void): NotificationObserver;
|
||||
export function iosRemoveNotificationObserver(observer: NotificationObserver, notificationName: string): void;
|
||||
68
packages/core/application/helpers.ios.ts
Normal file
68
packages/core/application/helpers.ios.ts
Normal file
@ -0,0 +1,68 @@
|
||||
// stubs to avoid bundler warnings
|
||||
export const updateContentDescription = (view: any /* View */, forceUpdate?: boolean): string | null => null;
|
||||
export function applyContentDescription(view: any /* View */, forceUpdate?: boolean) {
|
||||
return null;
|
||||
}
|
||||
export const androidRegisteredReceivers = undefined;
|
||||
export const androidPendingReceiverRegistrations = undefined;
|
||||
export function androidRegisterBroadcastReceiver(intentFilter: string, onReceiveCallback: (context: android.content.Context, intent: android.content.Intent) => void, flags = 2): void {}
|
||||
export function androidUnregisterBroadcastReceiver(intentFilter: string): void {}
|
||||
export function androidGetCurrentActivity() {}
|
||||
export function androidGetForegroundActivity() {}
|
||||
export function androidSetForegroundActivity(activity: androidx.appcompat.app.AppCompatActivity): void {}
|
||||
export function androidGetStartActivity() {}
|
||||
export function androidSetStartActivity(activity: androidx.appcompat.app.AppCompatActivity): void {}
|
||||
|
||||
@NativeClass
|
||||
class NotificationObserver extends NSObject {
|
||||
private _onReceiveCallback: (notification: NSNotification) => void;
|
||||
|
||||
public static initWithCallback(onReceiveCallback: (notification: NSNotification) => void): NotificationObserver {
|
||||
const observer = <NotificationObserver>super.new();
|
||||
observer._onReceiveCallback = onReceiveCallback;
|
||||
|
||||
return observer;
|
||||
}
|
||||
|
||||
public onReceive(notification: NSNotification): void {
|
||||
this._onReceiveCallback(notification);
|
||||
}
|
||||
|
||||
public static ObjCExposedMethods = {
|
||||
onReceive: { returns: interop.types.void, params: [NSNotification] },
|
||||
};
|
||||
}
|
||||
|
||||
export const iosNotificationObservers: NotificationObserver[] = [];
|
||||
export function iosAddNotificationObserver(notificationName: string, onReceiveCallback: (notification: NSNotification) => void) {
|
||||
const observer = NotificationObserver.initWithCallback(onReceiveCallback);
|
||||
NSNotificationCenter.defaultCenter.addObserverSelectorNameObject(observer, 'onReceive', notificationName, null);
|
||||
iosNotificationObservers.push(observer);
|
||||
|
||||
return observer;
|
||||
}
|
||||
|
||||
export function iosRemoveNotificationObserver(observer: NotificationObserver, notificationName: string) {
|
||||
// TODO: test if this finds the right observer instance match everytime
|
||||
// after circular dependencies are resolved
|
||||
const index = iosNotificationObservers.indexOf(observer);
|
||||
if (index >= 0) {
|
||||
iosNotificationObservers.splice(index, 1);
|
||||
NSNotificationCenter.defaultCenter.removeObserverNameObject(observer, notificationName, null);
|
||||
}
|
||||
}
|
||||
|
||||
export function setupAccessibleView(view: any /* any */): void {
|
||||
const uiView = view.nativeViewProtected as UIView;
|
||||
if (!uiView) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* We need to map back from the UIView to the NativeScript View.
|
||||
*
|
||||
* We do that by setting the uiView's tag to the View's domId.
|
||||
* This way we can do reverse lookup.
|
||||
*/
|
||||
uiView.tag = view._domId;
|
||||
}
|
||||
Reference in New Issue
Block a user