refactor: circular deps part 7

This commit is contained in:
Nathan Walker
2025-07-08 22:18:43 -07:00
parent 18e75b66ec
commit 779def7609
42 changed files with 2798 additions and 2636 deletions

View File

@@ -1,4 +1,5 @@
import { EventData, EventDataValue } from '../data/observable';
import type { EventData, EventDataValue } from '../data/observable';
import { Observable } from '../data/observable';
import type { View } from '../ui/core/view';
import type { Page } from '../ui/page';
@@ -72,3 +73,443 @@ export function getLastFocusedViewOnPage(page: Page): View | null {
return null;
}
export class SharedA11YObservable extends Observable {
accessibilityServiceEnabled?: boolean;
}
export const AccessibilityServiceEnabledPropName = 'accessibilityServiceEnabled';
export class CommonA11YServiceEnabledObservable extends SharedA11YObservable {
constructor(sharedA11YObservable: SharedA11YObservable) {
super();
const ref = new WeakRef(this);
let lastValue: boolean;
function callback() {
const self = ref?.get();
if (!self) {
sharedA11YObservable.off(Observable.propertyChangeEvent, callback);
return;
}
const newValue = !!sharedA11YObservable.accessibilityServiceEnabled;
if (newValue !== lastValue) {
self.set(AccessibilityServiceEnabledPropName, newValue);
lastValue = newValue;
}
}
sharedA11YObservable.on(Observable.propertyChangeEvent, callback);
this.set(AccessibilityServiceEnabledPropName, !!sharedA11YObservable.accessibilityServiceEnabled);
}
}
let a11yServiceEnabled: boolean;
export function isA11yEnabled(): boolean {
if (typeof a11yServiceEnabled === 'boolean') {
return a11yServiceEnabled;
}
return undefined;
}
export function setA11yEnabled(value: boolean): void {
a11yServiceEnabled = value;
}
export function enforceArray(val: string | string[]): string[] {
if (Array.isArray(val)) {
return val;
}
if (typeof val === 'string') {
return val.split(/[, ]/g).filter((v: string) => !!v);
}
return [];
}
export const VALID_FONT_SCALES = __APPLE__ // Apple supports a wider number of font scales than Android does.
? [0.5, 0.7, 0.85, 1, 1.15, 1.3, 1.5, 2, 2.5, 3, 3.5, 4]
: [0.85, 1, 1.15, 1.3];
export function getClosestValidFontScale(fontScale: number): number {
fontScale = Number(fontScale) || 1;
return VALID_FONT_SCALES.sort((a, b) => Math.abs(fontScale - a) - Math.abs(fontScale - b))[0];
}
export enum FontScaleCategory {
ExtraSmall = 'extra-small',
Medium = 'medium',
ExtraLarge = 'extra-large',
}
export const fontScaleExtraSmallCategoryClass = `a11y-fontscale-xs`;
export const fontScaleMediumCategoryClass = `a11y-fontscale-m`;
export const fontScaleExtraLargeCategoryClass = `a11y-fontscale-xl`;
export const fontScaleCategoryClasses = [fontScaleExtraSmallCategoryClass, fontScaleMediumCategoryClass, fontScaleExtraLargeCategoryClass];
export const a11yServiceEnabledClass = `a11y-service-enabled`;
export const a11yServiceDisabledClass = `a11y-service-disabled`;
export const a11yServiceClasses = [a11yServiceEnabledClass, a11yServiceDisabledClass];
let currentFontScale: number = null;
export function setFontScale(scale: number) {
currentFontScale = scale;
}
export function getFontScale() {
return currentFontScale;
}
export function getFontScaleCategory(): FontScaleCategory {
if (__ANDROID__) {
return FontScaleCategory.Medium;
}
if (getFontScale() < 0.85) {
return FontScaleCategory.ExtraSmall;
}
if (getFontScale() > 1.5) {
return FontScaleCategory.ExtraLarge;
}
return FontScaleCategory.Medium;
}
let initAccessibilityCssHelperCallback: () => void;
export function setInitAccessibilityCssHelper(callback: () => void) {
initAccessibilityCssHelperCallback = callback;
}
export function readyInitAccessibilityCssHelper() {
if (initAccessibilityCssHelperCallback) {
initAccessibilityCssHelperCallback();
initAccessibilityCssHelperCallback = null;
}
}
let initFontScaleCallback: () => void;
export function setInitFontScale(callback: () => void) {
initFontScaleCallback = callback;
}
export function readyInitFontScale() {
if (initFontScaleCallback) {
initFontScaleCallback();
initFontScaleCallback = null;
}
}
let fontScaleCssClasses: Map<number, string>;
export function setFontScaleCssClasses(value: Map<number, string>) {
fontScaleCssClasses = value;
}
export function getFontScaleCssClasses() {
return fontScaleCssClasses;
}
let currentFontScaleClass = '';
export function setCurrentFontScaleClass(value: string) {
currentFontScaleClass = value;
}
export function getCurrentFontScaleClass() {
return currentFontScaleClass;
}
let currentFontScaleCategory = '';
export function setCurrentFontScaleCategory(value: string) {
currentFontScaleCategory = value;
}
export function getCurrentFontScaleCategory() {
return currentFontScaleCategory;
}
let currentA11YServiceClass = '';
export function setCurrentA11YServiceClass(value: string) {
currentA11YServiceClass = value;
}
export function getCurrentA11YServiceClass() {
return currentA11YServiceClass;
}
export enum AccessibilityTrait {
/**
* The element allows direct touch interaction for VoiceOver users.
*/
AllowsDirectInteraction = 'allowsDirectInteraction',
/**
* The element should cause an automatic page turn when VoiceOver finishes reading the text within it.
* Note: Requires custom view with accessibilityScroll(...)
*/
CausesPageTurn = 'pageTurn',
/**
* The element is not enabled and does not respond to user interaction.
*/
NotEnabled = 'disabled',
/**
* The element is currently selected.
*/
Selected = 'selected',
/**
* The element frequently updates its label or value.
*/
UpdatesFrequently = 'frequentUpdates',
}
export enum AccessibilityRole {
/**
* The element allows continuous adjustment through a range of values.
*/
Adjustable = 'adjustable',
/**
* The element should be treated as a button.
*/
Button = 'button',
/**
* The element behaves like a Checkbox
*/
Checkbox = 'checkbox',
/**
* The element is a header that divides content into sections, such as the title of a navigation bar.
*/
Header = 'header',
/**
* The element should be treated as an image.
*/
Image = 'image',
/**
* The element should be treated as a image button.
*/
ImageButton = 'imageButton',
/**
* The element behaves as a keyboard key.
*/
KeyboardKey = 'keyboardKey',
/**
* The element should be treated as a link.
*/
Link = 'link',
/**
* The element has no traits.
*/
None = 'none',
/**
* The element plays its own sound when activated.
*/
PlaysSound = 'plays',
/**
* The element behaves like a ProgressBar
*/
ProgressBar = 'progressBar',
/**
* The element behaves like a RadioButton
*/
RadioButton = 'radioButton',
/**
* The element should be treated as a search field.
*/
Search = 'search',
/**
* The element behaves like a SpinButton
*/
SpinButton = 'spinButton',
/**
* The element starts a media session when it is activated.
*/
StartsMediaSession = 'startsMedia',
/**
* The element should be treated as static text that cannot change.
*/
StaticText = 'text',
/**
* The element provides summary information when the application starts.
*/
Summary = 'summary',
/**
* The element behaves like a switch
*/
Switch = 'switch',
}
export enum AccessibilityState {
Selected = 'selected',
Checked = 'checked',
Unchecked = 'unchecked',
Disabled = 'disabled',
}
export enum AccessibilityLiveRegion {
None = 'none',
Polite = 'polite',
Assertive = 'assertive',
}
export enum IOSPostAccessibilityNotificationType {
Announcement = 'announcement',
Screen = 'screen',
Layout = 'layout',
}
export enum AndroidAccessibilityEvent {
/**
* Invalid selection/focus position.
*/
INVALID_POSITION = 'invalid_position',
/**
* Maximum length of the text fields.
*/
MAX_TEXT_LENGTH = 'max_text_length',
/**
* Represents the event of clicking on a android.view.View like android.widget.Button, android.widget.CompoundButton, etc.
*/
VIEW_CLICKED = 'view_clicked',
/**
* Represents the event of long clicking on a android.view.View like android.widget.Button, android.widget.CompoundButton, etc.
*/
VIEW_LONG_CLICKED = 'view_long_clicked',
/**
* Represents the event of selecting an item usually in the context of an android.widget.AdapterView.
*/
VIEW_SELECTED = 'view_selected',
/**
* Represents the event of setting input focus of a android.view.View.
*/
VIEW_FOCUSED = 'view_focused',
/**
* Represents the event of changing the text of an android.widget.EditText.
*/
VIEW_TEXT_CHANGED = 'view_text_changed',
/**
* Represents the event of opening a android.widget.PopupWindow, android.view.Menu, android.app.Dialog, etc.
*/
WINDOW_STATE_CHANGED = 'window_state_changed',
/**
* Represents the event showing a android.app.Notification.
*/
NOTIFICATION_STATE_CHANGED = 'notification_state_changed',
/**
* Represents the event of a hover enter over a android.view.View.
*/
VIEW_HOVER_ENTER = 'view_hover_enter',
/**
* Represents the event of a hover exit over a android.view.View.
*/
VIEW_HOVER_EXIT = 'view_hover_exit',
/**
* Represents the event of starting a touch exploration gesture.
*/
TOUCH_EXPLORATION_GESTURE_START = 'touch_exploration_gesture_start',
/**
* Represents the event of ending a touch exploration gesture.
*/
TOUCH_EXPLORATION_GESTURE_END = 'touch_exploration_gesture_end',
/**
* Represents the event of changing the content of a window and more specifically the sub-tree rooted at the event's source.
*/
WINDOW_CONTENT_CHANGED = 'window_content_changed',
/**
* Represents the event of scrolling a view.
*/
VIEW_SCROLLED = 'view_scrolled',
/**
* Represents the event of changing the selection in an android.widget.EditText.
*/
VIEW_TEXT_SELECTION_CHANGED = 'view_text_selection_changed',
/**
* Represents the event of an application making an announcement.
*/
ANNOUNCEMENT = 'announcement',
/**
* Represents the event of gaining accessibility focus.
*/
VIEW_ACCESSIBILITY_FOCUSED = 'view_accessibility_focused',
/**
* Represents the event of clearing accessibility focus.
*/
VIEW_ACCESSIBILITY_FOCUS_CLEARED = 'view_accessibility_focus_cleared',
/**
* Represents the event of traversing the text of a view at a given movement granularity.
*/
VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY = 'view_text_traversed_at_movement_granularity',
/**
* Represents the event of beginning gesture detection.
*/
GESTURE_DETECTION_START = 'gesture_detection_start',
/**
* Represents the event of ending gesture detection.
*/
GESTURE_DETECTION_END = 'gesture_detection_end',
/**
* Represents the event of the user starting to touch the screen.
*/
TOUCH_INTERACTION_START = 'touch_interaction_start',
/**
* Represents the event of the user ending to touch the screen.
*/
TOUCH_INTERACTION_END = 'touch_interaction_end',
/**
* Mask for AccessibilityEvent all types.
*/
ALL_MASK = 'all',
}
export interface AccessibilityEventPerformEscape extends EventData {
cancel?: boolean;
}
export interface AccessibilityEventOptions {
androidAccessibilityEvent?: AndroidAccessibilityEvent;
iosNotificationType?: IOSPostAccessibilityNotificationType;
message?: string;
}

View File

@@ -1,4 +0,0 @@
// Only keep helpers that do not import from application here.
// If any code remains that needs Application, move it to accessibility-css-init.ts.
export { initAccessibilityCssHelper } from './accessibility-css-init';

View File

@@ -1,121 +0,0 @@
import { Application } from '../application';
import type { View } from '../ui/core/view';
import { AccessibilityServiceEnabledObservable } from './accessibility-service';
import { FontScaleCategory, getCurrentFontScale, getFontScaleCategory, VALID_FONT_SCALES } from './font-scale';
// CSS-classes
const fontScaleExtraSmallCategoryClass = `a11y-fontscale-xs`;
const fontScaleMediumCategoryClass = `a11y-fontscale-m`;
const fontScaleExtraLargeCategoryClass = `a11y-fontscale-xl`;
const fontScaleCategoryClasses = [fontScaleExtraSmallCategoryClass, fontScaleMediumCategoryClass, fontScaleExtraLargeCategoryClass];
const a11yServiceEnabledClass = `a11y-service-enabled`;
const a11yServiceDisabledClass = `a11y-service-disabled`;
const a11yServiceClasses = [a11yServiceEnabledClass, a11yServiceDisabledClass];
let accessibilityServiceObservable: AccessibilityServiceEnabledObservable;
let fontScaleCssClasses: Map<number, string>;
let currentFontScaleClass = '';
let currentFontScaleCategory = '';
let currentA11YServiceClass = '';
function ensureClasses() {
if (accessibilityServiceObservable) {
return;
}
fontScaleCssClasses = new Map(VALID_FONT_SCALES.map((fs) => [fs, `a11y-fontscale-${Number(fs * 100).toFixed(0)}`]));
accessibilityServiceObservable = new AccessibilityServiceEnabledObservable();
}
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));
}
function updateCurrentHelperClasses(): void {
const fontScale = getCurrentFontScale();
const fontScaleCategory = getFontScaleCategory();
const oldFontScaleClass = currentFontScaleClass;
if (fontScaleCssClasses.has(fontScale)) {
currentFontScaleClass = fontScaleCssClasses.get(fontScale);
} else {
currentFontScaleClass = fontScaleCssClasses.get(1);
}
if (oldFontScaleClass !== currentFontScaleClass) {
applyRootCssClass([...fontScaleCssClasses.values()], currentFontScaleClass);
}
const oldActiveFontScaleCategory = currentFontScaleCategory;
switch (fontScaleCategory) {
case FontScaleCategory.ExtraSmall: {
currentFontScaleCategory = fontScaleExtraSmallCategoryClass;
break;
}
case FontScaleCategory.Medium: {
currentFontScaleCategory = fontScaleMediumCategoryClass;
break;
}
case FontScaleCategory.ExtraLarge: {
currentFontScaleCategory = fontScaleExtraLargeCategoryClass;
break;
}
default: {
currentFontScaleCategory = fontScaleMediumCategoryClass;
break;
}
}
if (oldActiveFontScaleCategory !== currentFontScaleCategory) {
applyRootCssClass(fontScaleCategoryClasses, currentFontScaleCategory);
}
const oldA11YStatusClass = currentA11YServiceClass;
if (accessibilityServiceObservable.accessibilityServiceEnabled) {
currentA11YServiceClass = a11yServiceEnabledClass;
} else {
currentA11YServiceClass = a11yServiceDisabledClass;
}
if (oldA11YStatusClass !== currentA11YServiceClass) {
applyRootCssClass(a11yServiceClasses, currentA11YServiceClass);
}
}
export function initAccessibilityCssHelper(): void {
ensureClasses();
Application.on(Application.fontScaleChangedEvent, () => {
updateCurrentHelperClasses();
applyFontScaleToRootViews();
});
accessibilityServiceObservable.on(AccessibilityServiceEnabledObservable.propertyChangeEvent, updateCurrentHelperClasses);
}

View File

@@ -2,7 +2,7 @@ import { CssProperty, InheritedCssProperty, Property } from '../ui/core/properti
import type { View } from '../ui/core/view';
import { booleanConverter } from '../ui/core/view-base/utils';
import { Style } from '../ui/styling/style';
import { AccessibilityLiveRegion, AccessibilityRole, AccessibilityState, AccessibilityTrait } from './accessibility-types';
import { AccessibilityLiveRegion, AccessibilityRole, AccessibilityState, AccessibilityTrait } from './accessibility-common';
function makePropertyEnumConverter<T>(enumValues) {
return (value: string): T | null => {

View File

@@ -1,35 +0,0 @@
import { Observable } from '../data/observable';
export class SharedA11YObservable extends Observable {
accessibilityServiceEnabled?: boolean;
}
export const AccessibilityServiceEnabledPropName = 'accessibilityServiceEnabled';
export class CommonA11YServiceEnabledObservable extends SharedA11YObservable {
constructor(sharedA11YObservable: SharedA11YObservable) {
super();
const ref = new WeakRef(this);
let lastValue: boolean;
function callback() {
const self = ref?.get();
if (!self) {
sharedA11YObservable.off(Observable.propertyChangeEvent, callback);
return;
}
const newValue = !!sharedA11YObservable.accessibilityServiceEnabled;
if (newValue !== lastValue) {
self.set(AccessibilityServiceEnabledPropName, newValue);
lastValue = newValue;
}
}
sharedA11YObservable.on(Observable.propertyChangeEvent, callback);
this.set(AccessibilityServiceEnabledPropName, !!sharedA11YObservable.accessibilityServiceEnabled);
}
}

View File

@@ -1,132 +0,0 @@
import { Application, ApplicationEventData } from '../application';
import { Observable } from '../data/observable';
import { Trace } from '../trace';
import * as Utils from '../utils';
import { SDK_VERSION } from '../utils/constants';
import { CommonA11YServiceEnabledObservable, SharedA11YObservable } from './accessibility-service-common';
export function getAndroidAccessibilityManager(): android.view.accessibility.AccessibilityManager | null {
const context = Utils.ad.getApplicationContext() as android.content.Context;
if (!context) {
return null;
}
return context.getSystemService(android.content.Context.ACCESSIBILITY_SERVICE) as android.view.accessibility.AccessibilityManager;
}
const accessibilityStateEnabledPropName = 'accessibilityStateEnabled';
const touchExplorationStateEnabledPropName = 'touchExplorationStateEnabled';
class AndroidSharedA11YObservable extends SharedA11YObservable {
[accessibilityStateEnabledPropName]: boolean;
[touchExplorationStateEnabledPropName]: boolean;
// @ts-ignore todo: fix
get accessibilityServiceEnabled(): boolean {
return !!this[accessibilityStateEnabledPropName] && !!this[touchExplorationStateEnabledPropName];
}
set accessibilityServiceEnabled(v) {
return;
}
}
let accessibilityStateChangeListener: android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener;
let touchExplorationStateChangeListener: android.view.accessibility.AccessibilityManager.TouchExplorationStateChangeListener;
let sharedA11YObservable: AndroidSharedA11YObservable;
function updateAccessibilityState(): void {
const accessibilityManager = getAndroidAccessibilityManager();
if (!accessibilityManager) {
sharedA11YObservable.set(accessibilityStateEnabledPropName, false);
sharedA11YObservable.set(touchExplorationStateEnabledPropName, false);
return;
}
sharedA11YObservable.set(accessibilityStateEnabledPropName, !!accessibilityManager.isEnabled());
sharedA11YObservable.set(touchExplorationStateEnabledPropName, !!accessibilityManager.isTouchExplorationEnabled());
}
function ensureStateListener(): SharedA11YObservable {
if (sharedA11YObservable) {
return sharedA11YObservable;
}
const accessibilityManager = getAndroidAccessibilityManager();
sharedA11YObservable = new AndroidSharedA11YObservable();
if (!accessibilityManager) {
sharedA11YObservable.set(accessibilityStateEnabledPropName, false);
sharedA11YObservable.set(touchExplorationStateEnabledPropName, false);
return sharedA11YObservable;
}
accessibilityStateChangeListener = new android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener({
onAccessibilityStateChanged(enabled) {
updateAccessibilityState();
if (Trace.isEnabled()) {
Trace.write(`AccessibilityStateChangeListener state changed to: ${!!enabled}`, Trace.categories.Accessibility);
}
},
});
accessibilityManager.addAccessibilityStateChangeListener(accessibilityStateChangeListener);
if (SDK_VERSION >= 19) {
touchExplorationStateChangeListener = new android.view.accessibility.AccessibilityManager.TouchExplorationStateChangeListener({
onTouchExplorationStateChanged(enabled) {
updateAccessibilityState();
if (Trace.isEnabled()) {
Trace.write(`TouchExplorationStateChangeListener state changed to: ${!!enabled}`, Trace.categories.Accessibility);
}
},
});
accessibilityManager.addTouchExplorationStateChangeListener(touchExplorationStateChangeListener);
}
updateAccessibilityState();
Application.on(Application.resumeEvent, updateAccessibilityState);
Application.on(Application.exitEvent, (args: ApplicationEventData) => {
const activity = args.android as android.app.Activity;
if (activity && !activity.isFinishing()) {
return;
}
const accessibilityManager = getAndroidAccessibilityManager();
if (accessibilityManager) {
if (accessibilityStateChangeListener) {
accessibilityManager.removeAccessibilityStateChangeListener(accessibilityStateChangeListener);
}
if (touchExplorationStateChangeListener) {
accessibilityManager.removeTouchExplorationStateChangeListener(touchExplorationStateChangeListener);
}
}
accessibilityStateChangeListener = null;
touchExplorationStateChangeListener = null;
if (sharedA11YObservable) {
sharedA11YObservable.removeEventListener(Observable.propertyChangeEvent);
sharedA11YObservable = null;
}
Application.off(Application.resumeEvent, updateAccessibilityState);
});
return sharedA11YObservable;
}
export function isAccessibilityServiceEnabled(): boolean {
return ensureStateListener().accessibilityServiceEnabled;
}
export class AccessibilityServiceEnabledObservable extends CommonA11YServiceEnabledObservable {
constructor() {
super(ensureStateListener());
}
}

View File

@@ -1,10 +0,0 @@
import { Observable } from '../data/observable';
export class AccessibilityServiceEnabledObservable extends Observable {
public readonly accessibilityServiceEnabled?: boolean;
}
/**
* Get the Android platform's AccessibilityManager.
*/
export function getAndroidAccessibilityManager(): /* android.view.accessibility.AccessibilityManager */ any | null;

View File

@@ -1,75 +0,0 @@
import { Application } from '../application';
import { Observable } from '../data/observable';
import { Trace } from '../trace';
import { AccessibilityServiceEnabledPropName, CommonA11YServiceEnabledObservable, SharedA11YObservable } from './accessibility-service-common';
export function isAccessibilityServiceEnabled(): boolean {
return getSharedA11YObservable().accessibilityServiceEnabled;
}
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());
}
}

View File

@@ -1,275 +0,0 @@
import type { EventData } from '../data/observable';
export enum AccessibilityTrait {
/**
* The element allows direct touch interaction for VoiceOver users.
*/
AllowsDirectInteraction = 'allowsDirectInteraction',
/**
* The element should cause an automatic page turn when VoiceOver finishes reading the text within it.
* Note: Requires custom view with accessibilityScroll(...)
*/
CausesPageTurn = 'pageTurn',
/**
* The element is not enabled and does not respond to user interaction.
*/
NotEnabled = 'disabled',
/**
* The element is currently selected.
*/
Selected = 'selected',
/**
* The element frequently updates its label or value.
*/
UpdatesFrequently = 'frequentUpdates',
}
export enum AccessibilityRole {
/**
* The element allows continuous adjustment through a range of values.
*/
Adjustable = 'adjustable',
/**
* The element should be treated as a button.
*/
Button = 'button',
/**
* The element behaves like a Checkbox
*/
Checkbox = 'checkbox',
/**
* The element is a header that divides content into sections, such as the title of a navigation bar.
*/
Header = 'header',
/**
* The element should be treated as an image.
*/
Image = 'image',
/**
* The element should be treated as a image button.
*/
ImageButton = 'imageButton',
/**
* The element behaves as a keyboard key.
*/
KeyboardKey = 'keyboardKey',
/**
* The element should be treated as a link.
*/
Link = 'link',
/**
* The element has no traits.
*/
None = 'none',
/**
* The element plays its own sound when activated.
*/
PlaysSound = 'plays',
/**
* The element behaves like a ProgressBar
*/
ProgressBar = 'progressBar',
/**
* The element behaves like a RadioButton
*/
RadioButton = 'radioButton',
/**
* The element should be treated as a search field.
*/
Search = 'search',
/**
* The element behaves like a SpinButton
*/
SpinButton = 'spinButton',
/**
* The element starts a media session when it is activated.
*/
StartsMediaSession = 'startsMedia',
/**
* The element should be treated as static text that cannot change.
*/
StaticText = 'text',
/**
* The element provides summary information when the application starts.
*/
Summary = 'summary',
/**
* The element behaves like a switch
*/
Switch = 'switch',
}
export enum AccessibilityState {
Selected = 'selected',
Checked = 'checked',
Unchecked = 'unchecked',
Disabled = 'disabled',
}
export enum AccessibilityLiveRegion {
None = 'none',
Polite = 'polite',
Assertive = 'assertive',
}
export enum IOSPostAccessibilityNotificationType {
Announcement = 'announcement',
Screen = 'screen',
Layout = 'layout',
}
export enum AndroidAccessibilityEvent {
/**
* Invalid selection/focus position.
*/
INVALID_POSITION = 'invalid_position',
/**
* Maximum length of the text fields.
*/
MAX_TEXT_LENGTH = 'max_text_length',
/**
* Represents the event of clicking on a android.view.View like android.widget.Button, android.widget.CompoundButton, etc.
*/
VIEW_CLICKED = 'view_clicked',
/**
* Represents the event of long clicking on a android.view.View like android.widget.Button, android.widget.CompoundButton, etc.
*/
VIEW_LONG_CLICKED = 'view_long_clicked',
/**
* Represents the event of selecting an item usually in the context of an android.widget.AdapterView.
*/
VIEW_SELECTED = 'view_selected',
/**
* Represents the event of setting input focus of a android.view.View.
*/
VIEW_FOCUSED = 'view_focused',
/**
* Represents the event of changing the text of an android.widget.EditText.
*/
VIEW_TEXT_CHANGED = 'view_text_changed',
/**
* Represents the event of opening a android.widget.PopupWindow, android.view.Menu, android.app.Dialog, etc.
*/
WINDOW_STATE_CHANGED = 'window_state_changed',
/**
* Represents the event showing a android.app.Notification.
*/
NOTIFICATION_STATE_CHANGED = 'notification_state_changed',
/**
* Represents the event of a hover enter over a android.view.View.
*/
VIEW_HOVER_ENTER = 'view_hover_enter',
/**
* Represents the event of a hover exit over a android.view.View.
*/
VIEW_HOVER_EXIT = 'view_hover_exit',
/**
* Represents the event of starting a touch exploration gesture.
*/
TOUCH_EXPLORATION_GESTURE_START = 'touch_exploration_gesture_start',
/**
* Represents the event of ending a touch exploration gesture.
*/
TOUCH_EXPLORATION_GESTURE_END = 'touch_exploration_gesture_end',
/**
* Represents the event of changing the content of a window and more specifically the sub-tree rooted at the event's source.
*/
WINDOW_CONTENT_CHANGED = 'window_content_changed',
/**
* Represents the event of scrolling a view.
*/
VIEW_SCROLLED = 'view_scrolled',
/**
* Represents the event of changing the selection in an android.widget.EditText.
*/
VIEW_TEXT_SELECTION_CHANGED = 'view_text_selection_changed',
/**
* Represents the event of an application making an announcement.
*/
ANNOUNCEMENT = 'announcement',
/**
* Represents the event of gaining accessibility focus.
*/
VIEW_ACCESSIBILITY_FOCUSED = 'view_accessibility_focused',
/**
* Represents the event of clearing accessibility focus.
*/
VIEW_ACCESSIBILITY_FOCUS_CLEARED = 'view_accessibility_focus_cleared',
/**
* Represents the event of traversing the text of a view at a given movement granularity.
*/
VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY = 'view_text_traversed_at_movement_granularity',
/**
* Represents the event of beginning gesture detection.
*/
GESTURE_DETECTION_START = 'gesture_detection_start',
/**
* Represents the event of ending gesture detection.
*/
GESTURE_DETECTION_END = 'gesture_detection_end',
/**
* Represents the event of the user starting to touch the screen.
*/
TOUCH_INTERACTION_START = 'touch_interaction_start',
/**
* Represents the event of the user ending to touch the screen.
*/
TOUCH_INTERACTION_END = 'touch_interaction_end',
/**
* Mask for AccessibilityEvent all types.
*/
ALL_MASK = 'all',
}
export interface AccessibilityEventPerformEscape extends EventData {
cancel?: boolean;
}
export interface AccessibilityEventOptions {
androidAccessibilityEvent?: AndroidAccessibilityEvent;
iosNotificationType?: IOSPostAccessibilityNotificationType;
message?: string;
}

View File

@@ -1,15 +0,0 @@
export const VALID_FONT_SCALES = __APPLE__ // Apple supports a wider number of font scales than Android does.
? [0.5, 0.7, 0.85, 1, 1.15, 1.3, 1.5, 2, 2.5, 3, 3.5, 4]
: [0.85, 1, 1.15, 1.3];
export function getClosestValidFontScale(fontScale: number): number {
fontScale = Number(fontScale) || 1;
return VALID_FONT_SCALES.sort((a, b) => Math.abs(fontScale - a) - Math.abs(fontScale - b))[0];
}
export enum FontScaleCategory {
ExtraSmall = 'extra-small',
Medium = 'medium',
ExtraLarge = 'extra-large',
}

View File

@@ -1,67 +0,0 @@
import { Application, ApplicationEventData } from '../application';
import { FontScaleCategory, getClosestValidFontScale } from './font-scale-common';
export * from './font-scale-common';
let currentFontScale: number = null;
function fontScaleChanged(origFontScale: number) {
const oldValue = currentFontScale;
currentFontScale = getClosestValidFontScale(origFontScale);
if (oldValue !== currentFontScale) {
Application.notify({
eventName: Application.fontScaleChangedEvent,
object: Application,
newValue: currentFontScale,
} as ApplicationEventData);
}
}
export function getCurrentFontScale(): number {
setupConfigListener();
return currentFontScale;
}
export function getFontScaleCategory(): FontScaleCategory {
return FontScaleCategory.Medium;
}
function useAndroidFontScale() {
fontScaleChanged(Number(Application.android.context.getResources().getConfiguration().fontScale));
}
let configChangedCallback: android.content.ComponentCallbacks2;
function setupConfigListener() {
if (configChangedCallback) {
return;
}
Application.off(Application.launchEvent, setupConfigListener);
const context = Application.android?.context as android.content.Context;
if (!context) {
Application.on(Application.launchEvent, setupConfigListener);
return;
}
useAndroidFontScale();
configChangedCallback = new android.content.ComponentCallbacks2({
onLowMemory() {
// Dummy
},
onTrimMemory() {
// Dummy
},
onConfigurationChanged(newConfig: android.content.res.Configuration) {
fontScaleChanged(Number(newConfig.fontScale));
},
});
context.registerComponentCallbacks(configChangedCallback);
Application.on(Application.resumeEvent, useAndroidFontScale);
}
export function initAccessibilityFontScale(): void {
setupConfigListener();
}

View File

@@ -1,9 +0,0 @@
export const VALID_FONT_SCALES: number[];
export function getCurrentFontScale(): number;
export enum FontScaleCategory {
ExtraSmall = 'extra-small',
Medium = 'medium',
ExtraLarge = 'extra-large',
}
export function getFontScaleCategory(): FontScaleCategory;
export function initAccessibilityFontScale(): void;

View File

@@ -1,110 +0,0 @@
import { Application } from '../application';
import { FontScaleCategory, getClosestValidFontScale } from './font-scale-common';
export * from './font-scale-common';
let currentFontScale: number = null;
function fontScaleChanged(origFontScale: number) {
const oldValue = currentFontScale;
currentFontScale = getClosestValidFontScale(origFontScale);
if (oldValue !== currentFontScale) {
Application.notify({
eventName: Application.fontScaleChangedEvent,
object: Application,
newValue: currentFontScale,
});
}
}
export function getCurrentFontScale(): number {
setupConfigListener();
return currentFontScale;
}
export function getFontScaleCategory(): FontScaleCategory {
if (currentFontScale < 0.85) {
return FontScaleCategory.ExtraSmall;
}
if (currentFontScale > 1.5) {
return FontScaleCategory.ExtraLarge;
}
return FontScaleCategory.Medium;
}
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();
}
export function initAccessibilityFontScale(): void {
setupConfigListener();
}

View File

@@ -1,690 +0,0 @@
import { Application, ApplicationEventData } from '../application';
import { Trace } from '../trace';
import { SDK_VERSION } from '../utils/constants';
import { android as androidUtils } from '../utils';
import type { View } from '../ui/core/view';
import { GestureTypes } from '../ui/gestures';
import { notifyAccessibilityFocusState } from './accessibility-common';
import { getAndroidAccessibilityManager } from './accessibility-service';
import { AccessibilityRole, AccessibilityState, AndroidAccessibilityEvent } from './accessibility-types';
export * from './accessibility-common';
export * from './accessibility-types';
export * from './font-scale';
let clickableRolesMap = new Set<string>();
let lastFocusedView: WeakRef<View>;
function accessibilityEventHelper(view: View, eventType: number) {
const eventName = accessibilityEventTypeMap.get(eventType);
if (!isAccessibilityServiceEnabled()) {
if (Trace.isEnabled()) {
Trace.write(`accessibilityEventHelper: Service not active`, Trace.categories.Accessibility);
}
return;
}
if (!eventName) {
Trace.write(`accessibilityEventHelper: unknown eventType: ${eventType}`, Trace.categories.Accessibility, Trace.messageType.error);
return;
}
if (!view) {
if (Trace.isEnabled()) {
Trace.write(`accessibilityEventHelper: no owner: ${eventName}`, Trace.categories.Accessibility);
}
return;
}
const androidView = view.nativeViewProtected as android.view.View;
if (!androidView) {
if (Trace.isEnabled()) {
Trace.write(`accessibilityEventHelper: no nativeView`, Trace.categories.Accessibility);
}
return;
}
switch (eventType) {
case android.view.accessibility.AccessibilityEvent.TYPE_VIEW_CLICKED: {
/**
* Android API >= 26 handles accessibility tap-events by converting them to TYPE_VIEW_CLICKED
* These aren't triggered for custom tap events in NativeScript.
*/
if (SDK_VERSION >= 26) {
// Find all tap gestures and trigger them.
for (const tapGesture of view.getGestureObservers(GestureTypes.tap) ?? []) {
tapGesture.callback({
android: view.android,
eventName: 'tap',
ios: null,
object: view,
type: GestureTypes.tap,
view: view,
});
}
}
return;
}
case android.view.accessibility.AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED: {
const lastView = lastFocusedView?.get();
if (lastView && view !== lastView) {
const lastAndroidView = lastView.nativeViewProtected as android.view.View;
if (lastAndroidView) {
lastAndroidView.clearFocus();
lastFocusedView = null;
notifyAccessibilityFocusState(lastView, false, true);
}
}
lastFocusedView = new WeakRef(view);
notifyAccessibilityFocusState(view, true, false);
return;
}
case android.view.accessibility.AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED: {
const lastView = lastFocusedView?.get();
if (lastView && view === lastView) {
lastFocusedView = null;
androidView.clearFocus();
}
notifyAccessibilityFocusState(view, false, true);
return;
}
}
}
let TNSAccessibilityDelegate: android.view.View.androidviewViewAccessibilityDelegate;
const androidViewToTNSView = new WeakMap<android.view.View, WeakRef<View>>();
let accessibilityEventMap: Map<AndroidAccessibilityEvent, number>;
let accessibilityEventTypeMap: Map<number, string>;
function ensureNativeClasses() {
if (TNSAccessibilityDelegate) {
return;
}
// WORKAROUND: Typing refers to android.view.View.androidviewViewAccessibilityDelegate but it is called android.view.View.AccessibilityDelegate at runtime
const AccessibilityDelegate: typeof android.view.View.androidviewViewAccessibilityDelegate = android.view.View['AccessibilityDelegate'];
const RoleTypeMap = new Map<AccessibilityRole, string>([
[AccessibilityRole.Button, android.widget.Button.class.getName()],
[AccessibilityRole.Search, android.widget.EditText.class.getName()],
[AccessibilityRole.Image, android.widget.ImageView.class.getName()],
[AccessibilityRole.ImageButton, android.widget.ImageButton.class.getName()],
[AccessibilityRole.KeyboardKey, android.inputmethodservice.Keyboard.Key.class.getName()],
[AccessibilityRole.StaticText, android.widget.TextView.class.getName()],
[AccessibilityRole.Adjustable, android.widget.SeekBar.class.getName()],
[AccessibilityRole.Checkbox, android.widget.CheckBox.class.getName()],
[AccessibilityRole.RadioButton, android.widget.RadioButton.class.getName()],
[AccessibilityRole.SpinButton, android.widget.Spinner.class.getName()],
[AccessibilityRole.Switch, android.widget.Switch.class.getName()],
[AccessibilityRole.ProgressBar, android.widget.ProgressBar.class.getName()],
]);
clickableRolesMap = new Set<string>([AccessibilityRole.Button, AccessibilityRole.ImageButton]);
const ignoreRoleTypesForTrace = new Set([AccessibilityRole.Header, AccessibilityRole.Link, AccessibilityRole.None, AccessibilityRole.Summary]);
@NativeClass()
class TNSAccessibilityDelegateImpl extends AccessibilityDelegate {
constructor() {
super();
return global.__native(this);
}
private getTnsView(androidView: android.view.View) {
const view = androidViewToTNSView.get(androidView)?.get();
if (!view) {
androidViewToTNSView.delete(androidView);
return null;
}
return view;
}
public onInitializeAccessibilityNodeInfo(host: android.view.View, info: android.view.accessibility.AccessibilityNodeInfo) {
super.onInitializeAccessibilityNodeInfo(host, info);
const view = this.getTnsView(host);
if (!view) {
if (Trace.isEnabled()) {
Trace.write(`onInitializeAccessibilityNodeInfo ${host} ${info} no tns-view`, Trace.categories.Accessibility);
}
return;
}
// Set resource id that can be used with test frameworks without polluting the content description.
const id = host.getTag(androidUtils.resources.getId(`:id/nativescript_accessibility_id`));
if (id != null) {
info.setViewIdResourceName(id);
}
const accessibilityRole = view.accessibilityRole;
if (accessibilityRole) {
const androidClassName = RoleTypeMap.get(accessibilityRole);
if (androidClassName) {
const oldClassName = info.getClassName() || (SDK_VERSION >= 28 && host.getAccessibilityClassName()) || null;
info.setClassName(androidClassName);
if (Trace.isEnabled()) {
Trace.write(`${view}.accessibilityRole = "${accessibilityRole}" is mapped to "${androidClassName}" (was ${oldClassName}). ${info.getClassName()}`, Trace.categories.Accessibility);
}
} else if (!ignoreRoleTypesForTrace.has(accessibilityRole)) {
if (Trace.isEnabled()) {
Trace.write(`${view}.accessibilityRole = "${accessibilityRole}" is unknown`, Trace.categories.Accessibility);
}
}
if (clickableRolesMap.has(accessibilityRole)) {
if (Trace.isEnabled()) {
Trace.write(`onInitializeAccessibilityNodeInfo ${view} - set clickable role=${accessibilityRole}`, Trace.categories.Accessibility);
}
info.setClickable(true);
}
if (SDK_VERSION >= 28) {
if (accessibilityRole === AccessibilityRole.Header) {
if (Trace.isEnabled()) {
Trace.write(`onInitializeAccessibilityNodeInfo ${view} - set heading role=${accessibilityRole}`, Trace.categories.Accessibility);
}
info.setHeading(true);
} else if (host.isAccessibilityHeading()) {
if (Trace.isEnabled()) {
Trace.write(`onInitializeAccessibilityNodeInfo ${view} - set heading from host`, Trace.categories.Accessibility);
}
info.setHeading(true);
} else {
if (Trace.isEnabled()) {
Trace.write(`onInitializeAccessibilityNodeInfo ${view} - set not heading`, Trace.categories.Accessibility);
}
info.setHeading(false);
}
}
switch (accessibilityRole) {
case AccessibilityRole.Switch:
case AccessibilityRole.RadioButton:
case AccessibilityRole.Checkbox: {
if (Trace.isEnabled()) {
Trace.write(`onInitializeAccessibilityNodeInfo ${view} - set checkable and check=${view.accessibilityState === AccessibilityState.Checked}`, Trace.categories.Accessibility);
}
info.setCheckable(true);
info.setChecked(view.accessibilityState === AccessibilityState.Checked);
break;
}
default: {
if (Trace.isEnabled()) {
Trace.write(`onInitializeAccessibilityNodeInfo ${view} - set enabled=${view.accessibilityState !== AccessibilityState.Disabled} and selected=${view.accessibilityState === AccessibilityState.Selected}`, Trace.categories.Accessibility);
}
info.setEnabled(view.accessibilityState !== AccessibilityState.Disabled);
info.setSelected(view.accessibilityState === AccessibilityState.Selected);
break;
}
}
}
if (view.accessible) {
info.setFocusable(true);
}
}
public sendAccessibilityEvent(host: android.view.ViewGroup, eventType: number) {
super.sendAccessibilityEvent(host, eventType);
const view = this.getTnsView(host);
if (!view) {
console.log(`skip - ${host} - ${accessibilityEventTypeMap.get(eventType)}`);
return;
}
try {
accessibilityEventHelper(view, eventType);
} catch (err) {
console.error(err);
}
}
}
TNSAccessibilityDelegate = new TNSAccessibilityDelegateImpl();
accessibilityEventMap = new Map<AndroidAccessibilityEvent, number>([
/**
* Invalid selection/focus position.
*/
[AndroidAccessibilityEvent.INVALID_POSITION, android.view.accessibility.AccessibilityEvent.INVALID_POSITION],
/**
* Maximum length of the text fields.
*/
[AndroidAccessibilityEvent.MAX_TEXT_LENGTH, android.view.accessibility.AccessibilityEvent.MAX_TEXT_LENGTH],
/**
* Represents the event of clicking on a android.view.View like android.widget.Button, android.widget.CompoundButton, etc.
*/
[AndroidAccessibilityEvent.VIEW_CLICKED, android.view.accessibility.AccessibilityEvent.TYPE_VIEW_CLICKED],
/**
* Represents the event of long clicking on a android.view.View like android.widget.Button, android.widget.CompoundButton, etc.
*/
[AndroidAccessibilityEvent.VIEW_LONG_CLICKED, android.view.accessibility.AccessibilityEvent.TYPE_VIEW_LONG_CLICKED],
/**
* Represents the event of selecting an item usually in the context of an android.widget.AdapterView.
*/
[AndroidAccessibilityEvent.VIEW_SELECTED, android.view.accessibility.AccessibilityEvent.TYPE_VIEW_SELECTED],
/**
* Represents the event of setting input focus of a android.view.View.
*/
[AndroidAccessibilityEvent.VIEW_FOCUSED, android.view.accessibility.AccessibilityEvent.TYPE_VIEW_FOCUSED],
/**
* Represents the event of changing the text of an android.widget.EditText.
*/
[AndroidAccessibilityEvent.VIEW_TEXT_CHANGED, android.view.accessibility.AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED],
/**
* Represents the event of opening a android.widget.PopupWindow, android.view.Menu, android.app.Dialog, etc.
*/
[AndroidAccessibilityEvent.WINDOW_STATE_CHANGED, android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED],
/**
* Represents the event showing a android.app.Notification.
*/
[AndroidAccessibilityEvent.NOTIFICATION_STATE_CHANGED, android.view.accessibility.AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED],
/**
* Represents the event of a hover enter over a android.view.View.
*/
[AndroidAccessibilityEvent.VIEW_HOVER_ENTER, android.view.accessibility.AccessibilityEvent.TYPE_VIEW_HOVER_ENTER],
/**
* Represents the event of a hover exit over a android.view.View.
*/
[AndroidAccessibilityEvent.VIEW_HOVER_EXIT, android.view.accessibility.AccessibilityEvent.TYPE_VIEW_HOVER_EXIT],
/**
* Represents the event of starting a touch exploration gesture.
*/
[AndroidAccessibilityEvent.TOUCH_EXPLORATION_GESTURE_START, android.view.accessibility.AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_START],
/**
* Represents the event of ending a touch exploration gesture.
*/
[AndroidAccessibilityEvent.TOUCH_EXPLORATION_GESTURE_END, android.view.accessibility.AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_END],
/**
* Represents the event of changing the content of a window and more specifically the sub-tree rooted at the event's source.
*/
[AndroidAccessibilityEvent.WINDOW_CONTENT_CHANGED, android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED],
/**
* Represents the event of scrolling a view.
*/
[AndroidAccessibilityEvent.VIEW_SCROLLED, android.view.accessibility.AccessibilityEvent.TYPE_VIEW_SCROLLED],
/**
* Represents the event of changing the selection in an android.widget.EditText.
*/
[AndroidAccessibilityEvent.VIEW_TEXT_SELECTION_CHANGED, android.view.accessibility.AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED],
/**
* Represents the event of an application making an announcement.
*/
[AndroidAccessibilityEvent.ANNOUNCEMENT, android.view.accessibility.AccessibilityEvent.TYPE_ANNOUNCEMENT],
/**
* Represents the event of gaining accessibility focus.
*/
[AndroidAccessibilityEvent.VIEW_ACCESSIBILITY_FOCUSED, android.view.accessibility.AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED],
/**
* Represents the event of clearing accessibility focus.
*/
[AndroidAccessibilityEvent.VIEW_ACCESSIBILITY_FOCUS_CLEARED, android.view.accessibility.AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED],
/**
* Represents the event of traversing the text of a view at a given movement granularity.
*/
[AndroidAccessibilityEvent.VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY, android.view.accessibility.AccessibilityEvent.TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY],
/**
* Represents the event of beginning gesture detection.
*/
[AndroidAccessibilityEvent.GESTURE_DETECTION_START, android.view.accessibility.AccessibilityEvent.TYPE_GESTURE_DETECTION_START],
/**
* Represents the event of ending gesture detection.
*/
[AndroidAccessibilityEvent.GESTURE_DETECTION_END, android.view.accessibility.AccessibilityEvent.TYPE_GESTURE_DETECTION_END],
/**
* Represents the event of the user starting to touch the screen.
*/
[AndroidAccessibilityEvent.TOUCH_INTERACTION_START, android.view.accessibility.AccessibilityEvent.TYPE_TOUCH_INTERACTION_START],
/**
* Represents the event of the user ending to touch the screen.
*/
[AndroidAccessibilityEvent.TOUCH_INTERACTION_END, android.view.accessibility.AccessibilityEvent.TYPE_TOUCH_INTERACTION_END],
/**
* Mask for AccessibilityEvent all types.
*/
[AndroidAccessibilityEvent.ALL_MASK, android.view.accessibility.AccessibilityEvent.TYPES_ALL_MASK],
]);
accessibilityEventTypeMap = new Map([...accessibilityEventMap].map(([k, v]) => [v, k]));
}
let accessibilityStateChangeListener: androidx.core.view.accessibility.AccessibilityManagerCompat.AccessibilityStateChangeListener;
let touchExplorationStateChangeListener: androidx.core.view.accessibility.AccessibilityManagerCompat.TouchExplorationStateChangeListener;
function updateAccessibilityServiceState() {
const accessibilityManager = getAndroidAccessibilityManager();
if (!accessibilityManager) {
return;
}
accessibilityServiceEnabled = !!accessibilityManager.isEnabled() && !!accessibilityManager.isTouchExplorationEnabled();
}
let accessibilityServiceEnabled: boolean;
export function isAccessibilityServiceEnabled(): boolean {
if (typeof accessibilityServiceEnabled === 'boolean') {
return accessibilityServiceEnabled;
}
const accessibilityManager = getAndroidAccessibilityManager();
accessibilityStateChangeListener = new androidx.core.view.accessibility.AccessibilityManagerCompat.AccessibilityStateChangeListener({
onAccessibilityStateChanged(enabled) {
updateAccessibilityServiceState();
if (Trace.isEnabled()) {
Trace.write(`AccessibilityStateChangeListener state changed to: ${!!enabled}`, Trace.categories.Accessibility);
}
},
});
touchExplorationStateChangeListener = new androidx.core.view.accessibility.AccessibilityManagerCompat.TouchExplorationStateChangeListener({
onTouchExplorationStateChanged(enabled) {
updateAccessibilityServiceState();
if (Trace.isEnabled()) {
Trace.write(`TouchExplorationStateChangeListener state changed to: ${!!enabled}`, Trace.categories.Accessibility);
}
},
});
androidx.core.view.accessibility.AccessibilityManagerCompat.addAccessibilityStateChangeListener(accessibilityManager, accessibilityStateChangeListener);
androidx.core.view.accessibility.AccessibilityManagerCompat.addTouchExplorationStateChangeListener(accessibilityManager, touchExplorationStateChangeListener);
updateAccessibilityServiceState();
Application.on(Application.exitEvent, (args: ApplicationEventData) => {
const activity = args.android as android.app.Activity;
if (activity && !activity.isFinishing()) {
return;
}
const accessibilityManager = getAndroidAccessibilityManager();
if (accessibilityManager) {
if (accessibilityStateChangeListener) {
androidx.core.view.accessibility.AccessibilityManagerCompat.removeAccessibilityStateChangeListener(accessibilityManager, accessibilityStateChangeListener);
}
if (touchExplorationStateChangeListener) {
androidx.core.view.accessibility.AccessibilityManagerCompat.removeTouchExplorationStateChangeListener(accessibilityManager, touchExplorationStateChangeListener);
}
}
accessibilityStateChangeListener = null;
touchExplorationStateChangeListener = null;
Application.off(Application.resumeEvent, updateAccessibilityServiceState);
});
Application.on(Application.resumeEvent, updateAccessibilityServiceState);
return accessibilityServiceEnabled;
}
export function setupAccessibleView(view: View): void {
updateAccessibilityProperties(view);
}
let updateAccessibilityPropertiesMicroTask;
let pendingViews = new Set<View>();
export function updateAccessibilityProperties(view: View) {
if (!view.nativeViewProtected) {
return;
}
pendingViews.add(view);
if (updateAccessibilityPropertiesMicroTask) return;
updateAccessibilityPropertiesMicroTask = true;
Promise.resolve().then(() => {
updateAccessibilityPropertiesMicroTask = false;
let _pendingViews = Array.from(pendingViews);
pendingViews = new Set();
for (const view of _pendingViews) {
if (!view.nativeViewProtected) continue;
setAccessibilityDelegate(view);
applyContentDescription(view);
}
_pendingViews = [];
});
}
export function sendAccessibilityEvent(view: View, eventType: AndroidAccessibilityEvent, text?: string): void {
if (!isAccessibilityServiceEnabled()) {
return;
}
const cls = `sendAccessibilityEvent(${view}, ${eventType}, ${text})`;
const androidView = view.nativeViewProtected as android.view.View;
if (!androidView) {
if (Trace.isEnabled()) {
Trace.write(`${cls}: no nativeView`, Trace.categories.Accessibility);
}
return;
}
if (!eventType) {
if (Trace.isEnabled()) {
Trace.write(`${cls}: no eventName provided`, Trace.categories.Accessibility);
}
return;
}
if (!isAccessibilityServiceEnabled()) {
if (Trace.isEnabled()) {
Trace.write(`${cls} - TalkBack not enabled`, Trace.categories.Accessibility);
}
return;
}
const accessibilityManager = getAndroidAccessibilityManager();
if (!accessibilityManager?.isEnabled()) {
if (Trace.isEnabled()) {
Trace.write(`${cls} - accessibility service not enabled`, Trace.categories.Accessibility);
}
return;
}
if (!accessibilityEventMap.has(eventType)) {
if (Trace.isEnabled()) {
Trace.write(`${cls} - unknown event`, Trace.categories.Accessibility);
}
return;
}
const eventInt = accessibilityEventMap.get(eventType);
if (!text) {
return androidView.sendAccessibilityEvent(eventInt);
}
const accessibilityEvent = android.view.accessibility.AccessibilityEvent.obtain(eventInt);
accessibilityEvent.setSource(androidView);
accessibilityEvent.getText().clear();
if (!text) {
applyContentDescription(view);
text = androidView.getContentDescription() || view['title'];
if (Trace.isEnabled()) {
Trace.write(`${cls} - text not provided use androidView.getContentDescription() - ${text}`, Trace.categories.Accessibility);
}
}
if (Trace.isEnabled()) {
Trace.write(`${cls}: send event with text: '${JSON.stringify(text)}'`, Trace.categories.Accessibility);
}
if (text) {
accessibilityEvent.getText().add(text);
}
accessibilityManager.sendAccessibilityEvent(accessibilityEvent);
}
export function updateContentDescription(view: View, forceUpdate?: boolean): string | null {
if (!view.nativeViewProtected) {
return;
}
return applyContentDescription(view, forceUpdate);
}
function setAccessibilityDelegate(view: View): void {
if (!view.nativeViewProtected) {
return;
}
ensureNativeClasses();
const androidView = view.nativeViewProtected as android.view.View;
if (!androidView || !androidView.setAccessibilityDelegate) {
return;
}
androidViewToTNSView.set(androidView, new WeakRef(view));
let hasOldDelegate = false;
if (typeof androidView.getAccessibilityDelegate === 'function') {
hasOldDelegate = androidView.getAccessibilityDelegate() === TNSAccessibilityDelegate;
}
if (hasOldDelegate) {
return;
}
androidView.setAccessibilityDelegate(TNSAccessibilityDelegate);
}
function applyContentDescription(view: 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(Application.android.context);
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;
}

View File

@@ -1,37 +0,0 @@
import type { Page } from '../ui/page';
import type { View } from '../ui/core/view';
import type { AndroidAccessibilityEvent } from './accessibility-types';
export * from './accessibility-common';
export * from './accessibility-types';
export * from './font-scale';
/**
* Initialize accessibility for View. This should be called on loaded-event.
*/
export function setupAccessibleView(view: View): void;
/**
* 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;
/**
* Android: Update the content description for views
*/
export function updateContentDescription(View: View, forceUpdate?: boolean): string | null;
/**
* 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;

View File

@@ -1,276 +0,0 @@
import { Application } from '../application';
import type { ViewBase } from '../ui/core/view-base';
import type { View } from '../ui/core/view';
import { notifyAccessibilityFocusState } from './accessibility-common';
import { AccessibilityLiveRegion, AccessibilityRole, AccessibilityState, AccessibilityTrait } from './accessibility-types';
export * from './accessibility-common';
export * from './accessibility-types';
export * from './font-scale';
function enforceArray(val: string | string[]): string[] {
if (Array.isArray(val)) {
return val;
}
if (typeof val === 'string') {
return val.split(/[, ]/g).filter((v: string) => !!v);
}
return [];
}
/**
* 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 setupAccessibleView(view: View): 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;
}
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;
}
export const sendAccessibilityEvent = (): void => {};
export const updateContentDescription = (): string | null => null;
let accessibilityServiceEnabled: boolean;
let nativeObserver;
export function isAccessibilityServiceEnabled(): boolean {
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') {
accessibilityServiceEnabled = false;
return accessibilityServiceEnabled;
}
}
accessibilityServiceEnabled = 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, () => {
accessibilityServiceEnabled = isVoiceOverRunning();
});
Application.on(Application.exitEvent, () => {
if (nativeObserver) {
Application.ios.removeNotificationObserver(nativeObserver, voiceOverStatusChangedNotificationName);
}
accessibilityServiceEnabled = undefined;
nativeObserver = null;
});
}
Application.on(Application.resumeEvent, () => {
accessibilityServiceEnabled = isVoiceOverRunning();
});
return accessibilityServiceEnabled;
}

View File

@@ -0,0 +1 @@
export * from './accessibility-common';

View File

@@ -1,5 +1,3 @@
import { initAccessibilityCssHelper } from '../accessibility/accessibility-css-init';
import { initAccessibilityFontScale } from '../accessibility/font-scale';
import { CoreTypes } from '../core-types';
import { CSSUtils } from '../css/system-classes';
import { Device, Screen } from '../platform';
@@ -11,8 +9,9 @@ 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';
// prettier-ignore
const ORIENTATION_CSS_CLASSES = [
@@ -371,8 +370,8 @@ export class ApplicationCommon {
initRootView(rootView: View) {
this.setRootViewCSSClasses(rootView);
initAccessibilityCssHelper();
initAccessibilityFontScale();
readyInitAccessibilityCssHelper();
readyInitFontScale();
this.notify(<InitRootViewEventData>{ eventName: this.initRootViewEvent, rootView });
}
@@ -614,11 +613,11 @@ export class ApplicationCommon {
public started = false;
get android(): IAndroidApplication {
get android(): AndroidApplicationType {
return undefined;
}
get ios(): IiOSApplication {
get ios(): iOSApplicationType {
return undefined;
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -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,37 @@ 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;
/**
* Initialize accessibility for View. This should be called on loaded-event.
*/
export function setupAccessibleView(view: View): void;
/**
* 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;
/**
* Android: Update the content description for views
*/
export function updateContentDescription(View: View, forceUpdate?: boolean): string | null;
/**
* 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;

View File

@@ -1,12 +1,49 @@
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 { 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';
@NativeClass
class CADisplayLinkTarget extends NSObject {
@@ -73,7 +110,7 @@ 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;
@@ -514,3 +551,516 @@ 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 setupAccessibleView(view: View): 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;
}
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;
}
export const sendAccessibilityEvent = (): void => {};
export const updateContentDescription = (): string | null => null;
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();
}
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);

View File

@@ -1,641 +0,0 @@
var g =
(typeof globalThis !== 'undefined' && globalThis) ||
(typeof self !== 'undefined' && self) ||
// eslint-disable-next-line no-undef
(typeof global !== 'undefined' && global) ||
{}
var support = {
searchParams: 'URLSearchParams' in g,
iterable: 'Symbol' in g && 'iterator' in Symbol,
blob:
'FileReader' in g &&
'Blob' in g &&
(function() {
try {
new Blob()
return true
} catch (e) {
return false
}
})(),
formData: 'FormData' in g,
arrayBuffer: 'ArrayBuffer' in g
}
function isDataView(obj) {
return obj && DataView.prototype.isPrototypeOf(obj)
}
if (support.arrayBuffer) {
var viewClasses = [
'[object Int8Array]',
'[object Uint8Array]',
'[object Uint8ClampedArray]',
'[object Int16Array]',
'[object Uint16Array]',
'[object Int32Array]',
'[object Uint32Array]',
'[object Float32Array]',
'[object Float64Array]'
]
var isArrayBufferView =
ArrayBuffer.isView ||
function(obj) {
return obj && viewClasses.indexOf(Object.prototype.toString.call(obj)) > -1
}
}
function normalizeName(name) {
if (typeof name !== 'string') {
name = String(name)
}
if (/[^a-z0-9\-#$%&'*+.^_`|~!]/i.test(name) || name === '') {
throw new TypeError('Invalid character in header field name: "' + name + '"')
}
return name.toLowerCase()
}
function normalizeValue(value) {
if (typeof value !== 'string') {
value = String(value)
}
return value
}
// Build a destructive iterator for the value list
function iteratorFor(items) {
var iterator = {
next: function() {
var value = items.shift()
return {done: value === undefined, value: value}
}
}
if (support.iterable) {
iterator[Symbol.iterator] = function() {
return iterator
}
}
return iterator
}
export function Headers(headers) {
this.map = {}
if (headers instanceof Headers) {
headers.forEach(function(value, name) {
this.append(name, value)
}, this)
} else if (Array.isArray(headers)) {
headers.forEach(function(header) {
if (header.length != 2) {
throw new TypeError('Headers constructor: expected name/value pair to be length 2, found' + header.length)
}
this.append(header[0], header[1])
}, this)
} else if (headers) {
Object.getOwnPropertyNames(headers).forEach(function(name) {
this.append(name, headers[name])
}, this)
}
}
Headers.prototype.append = function(name, value) {
name = normalizeName(name)
value = normalizeValue(value)
var oldValue = this.map[name]
this.map[name] = oldValue ? oldValue + ', ' + value : value
}
Headers.prototype['delete'] = function(name) {
delete this.map[normalizeName(name)]
}
Headers.prototype.get = function(name) {
name = normalizeName(name)
return this.has(name) ? this.map[name] : null
}
Headers.prototype.has = function(name) {
return this.map.hasOwnProperty(normalizeName(name))
}
Headers.prototype.set = function(name, value) {
this.map[normalizeName(name)] = normalizeValue(value)
}
Headers.prototype.forEach = function(callback, thisArg) {
for (var name in this.map) {
if (this.map.hasOwnProperty(name)) {
callback.call(thisArg, this.map[name], name, this)
}
}
}
Headers.prototype.keys = function() {
var items = []
this.forEach(function(value, name) {
items.push(name)
})
return iteratorFor(items)
}
Headers.prototype.values = function() {
var items = []
this.forEach(function(value) {
items.push(value)
})
return iteratorFor(items)
}
Headers.prototype.entries = function() {
var items = []
this.forEach(function(value, name) {
items.push([name, value])
})
return iteratorFor(items)
}
if (support.iterable) {
Headers.prototype[Symbol.iterator] = Headers.prototype.entries
}
function consumed(body) {
if (body._noBody) return
if (body.bodyUsed) {
return Promise.reject(new TypeError('Already read'))
}
body.bodyUsed = true
}
function fileReaderReady(reader) {
return new Promise(function(resolve, reject) {
reader.onload = function() {
resolve(reader.result)
}
reader.onerror = function() {
reject(reader.error)
}
})
}
function readBlobAsArrayBuffer(blob) {
var reader = new FileReader()
var promise = fileReaderReady(reader)
reader.readAsArrayBuffer(blob)
return promise
}
function readBlobAsText(blob) {
var reader = new FileReader()
var promise = fileReaderReady(reader)
var match = /charset=([A-Za-z0-9_-]+)/.exec(blob.type)
var encoding = match ? match[1] : 'utf-8'
reader.readAsText(blob, encoding)
return promise
}
function readArrayBufferAsText(buf) {
var view = new Uint8Array(buf)
var chars = new Array(view.length)
for (var i = 0; i < view.length; i++) {
chars[i] = String.fromCharCode(view[i])
}
return chars.join('')
}
function bufferClone(buf) {
if (buf.slice) {
return buf.slice(0)
} else {
var view = new Uint8Array(buf.byteLength)
view.set(new Uint8Array(buf))
return view.buffer
}
}
function Body() {
this.bodyUsed = false
this._initBody = function(body) {
/*
fetch-mock wraps the Response object in an ES6 Proxy to
provide useful test harness features such as flush. However, on
ES5 browsers without fetch or Proxy support pollyfills must be used;
the proxy-pollyfill is unable to proxy an attribute unless it exists
on the object before the Proxy is created. This change ensures
Response.bodyUsed exists on the instance, while maintaining the
semantic of setting Request.bodyUsed in the constructor before
_initBody is called.
*/
// eslint-disable-next-line no-self-assign
this.bodyUsed = this.bodyUsed
this._bodyInit = body
if (!body) {
this._noBody = true;
this._bodyText = ''
} else if (typeof body === 'string') {
this._bodyText = body
} else if (support.blob && Blob.prototype.isPrototypeOf(body)) {
this._bodyBlob = body
} else if (support.formData && FormData.prototype.isPrototypeOf(body)) {
this._bodyFormData = body
} else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) {
this._bodyText = body.toString()
} else if (support.arrayBuffer && support.blob && isDataView(body)) {
this._bodyArrayBuffer = bufferClone(body.buffer)
// IE 10-11 can't handle a DataView body.
this._bodyInit = new Blob([this._bodyArrayBuffer])
} else if (support.arrayBuffer && (ArrayBuffer.prototype.isPrototypeOf(body) || isArrayBufferView(body))) {
this._bodyArrayBuffer = bufferClone(body)
} else {
this._bodyText = body = Object.prototype.toString.call(body)
}
if (!this.headers.get('content-type')) {
if (typeof body === 'string') {
this.headers.set('content-type', 'text/plain;charset=UTF-8')
} else if (this._bodyBlob && this._bodyBlob.type) {
this.headers.set('content-type', this._bodyBlob.type)
} else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) {
this.headers.set('content-type', 'application/x-www-form-urlencoded;charset=UTF-8')
}
}
}
if (support.blob) {
this.blob = function() {
var rejected = consumed(this)
if (rejected) {
return rejected
}
if (this._bodyBlob) {
return Promise.resolve(this._bodyBlob)
} else if (this._bodyArrayBuffer) {
return Promise.resolve(new Blob([this._bodyArrayBuffer]))
} else if (this._bodyFormData) {
throw new Error('could not read FormData body as blob')
} else {
return Promise.resolve(new Blob([this._bodyText]))
}
}
}
this.arrayBuffer = function() {
if (this._bodyArrayBuffer) {
var isConsumed = consumed(this)
if (isConsumed) {
return isConsumed
} else if (ArrayBuffer.isView(this._bodyArrayBuffer)) {
return Promise.resolve(
this._bodyArrayBuffer.buffer.slice(
this._bodyArrayBuffer.byteOffset,
this._bodyArrayBuffer.byteOffset + this._bodyArrayBuffer.byteLength
)
)
} else {
return Promise.resolve(this._bodyArrayBuffer)
}
} else if (support.blob) {
return this.blob().then(readBlobAsArrayBuffer)
} else {
throw new Error('could not read as ArrayBuffer')
}
}
this.text = function() {
var rejected = consumed(this)
if (rejected) {
return rejected
}
if (this._bodyBlob) {
return readBlobAsText(this._bodyBlob)
} else if (this._bodyArrayBuffer) {
return Promise.resolve(readArrayBufferAsText(this._bodyArrayBuffer))
} else if (this._bodyFormData) {
throw new Error('could not read FormData body as text')
} else {
return Promise.resolve(this._bodyText)
}
}
if (support.formData) {
this.formData = function() {
return this.text().then(decode)
}
}
this.json = function() {
return this.text().then(JSON.parse)
}
return this
}
// HTTP methods whose capitalization should be normalized
var methods = ['CONNECT', 'DELETE', 'GET', 'HEAD', 'OPTIONS', 'PATCH', 'POST', 'PUT', 'TRACE']
function normalizeMethod(method) {
var upcased = method.toUpperCase()
return methods.indexOf(upcased) > -1 ? upcased : method
}
export function Request(input, options) {
if (!(this instanceof Request)) {
throw new TypeError('Please use the "new" operator, this DOM object constructor cannot be called as a function.')
}
options = options || {}
var body = options.body
if (input instanceof Request) {
if (input.bodyUsed) {
throw new TypeError('Already read')
}
this.url = input.url
this.credentials = input.credentials
if (!options.headers) {
this.headers = new Headers(input.headers)
}
this.method = input.method
this.mode = input.mode
this.signal = input.signal
if (!body && input._bodyInit != null) {
body = input._bodyInit
input.bodyUsed = true
}
} else {
this.url = String(input)
}
this.credentials = options.credentials || this.credentials || 'same-origin'
if (options.headers || !this.headers) {
this.headers = new Headers(options.headers)
}
this.method = normalizeMethod(options.method || this.method || 'GET')
this.mode = options.mode || this.mode || null
this.signal = options.signal || this.signal || (function () {
if ('AbortController' in g) {
var ctrl = new AbortController();
return ctrl.signal;
}
}());
this.referrer = null
if ((this.method === 'GET' || this.method === 'HEAD') && body) {
throw new TypeError('Body not allowed for GET or HEAD requests')
}
this._initBody(body)
if (this.method === 'GET' || this.method === 'HEAD') {
if (options.cache === 'no-store' || options.cache === 'no-cache') {
// Search for a '_' parameter in the query string
var reParamSearch = /([?&])_=[^&]*/
if (reParamSearch.test(this.url)) {
// If it already exists then set the value with the current time
this.url = this.url.replace(reParamSearch, '$1_=' + new Date().getTime())
} else {
// Otherwise add a new '_' parameter to the end with the current time
var reQueryString = /\?/
this.url += (reQueryString.test(this.url) ? '&' : '?') + '_=' + new Date().getTime()
}
}
}
}
Request.prototype.clone = function() {
return new Request(this, {body: this._bodyInit})
}
function decode(body) {
var form = new FormData()
body
.trim()
.split('&')
.forEach(function(bytes) {
if (bytes) {
var split = bytes.split('=')
var name = split.shift().replace(/\+/g, ' ')
var value = split.join('=').replace(/\+/g, ' ')
form.append(decodeURIComponent(name), decodeURIComponent(value))
}
})
return form
}
function parseHeaders(rawHeaders) {
var headers = new Headers()
// Replace instances of \r\n and \n followed by at least one space or horizontal tab with a space
// https://tools.ietf.org/html/rfc7230#section-3.2
var preProcessedHeaders = rawHeaders.replace(/\r?\n[\t ]+/g, ' ')
// Avoiding split via regex to work around a common IE11 bug with the core-js 3.6.0 regex polyfill
// https://github.com/github/fetch/issues/748
// https://github.com/zloirock/core-js/issues/751
preProcessedHeaders
.split('\r')
.map(function(header) {
return header.indexOf('\n') === 0 ? header.substr(1, header.length) : header
})
.forEach(function(line) {
var parts = line.split(':')
var key = parts.shift().trim()
if (key) {
var value = parts.join(':').trim()
try {
headers.append(key, value)
} catch (error) {
console.warn('Response ' + error.message)
}
}
})
return headers
}
Body.call(Request.prototype)
export function Response(bodyInit, options) {
if (!(this instanceof Response)) {
throw new TypeError('Please use the "new" operator, this DOM object constructor cannot be called as a function.')
}
if (!options) {
options = {}
}
this.type = 'default'
this.status = options.status === undefined ? 200 : options.status
if (this.status < 200 || this.status > 599) {
throw new RangeError("Failed to construct 'Response': The status provided (0) is outside the range [200, 599].")
}
this.ok = this.status >= 200 && this.status < 300
this.statusText = options.statusText === undefined ? '' : '' + options.statusText
this.headers = new Headers(options.headers)
this.url = options.url || ''
this._initBody(bodyInit)
}
Body.call(Response.prototype)
Response.prototype.clone = function() {
return new Response(this._bodyInit, {
status: this.status,
statusText: this.statusText,
headers: new Headers(this.headers),
url: this.url
})
}
Response.error = function() {
var response = new Response(null, {status: 200, statusText: ''})
response.ok = false
response.status = 0
response.type = 'error'
return response
}
var redirectStatuses = [301, 302, 303, 307, 308]
Response.redirect = function(url, status) {
if (redirectStatuses.indexOf(status) === -1) {
throw new RangeError('Invalid status code')
}
return new Response(null, {status: status, headers: {location: url}})
}
export var DOMException = g.DOMException
try {
new DOMException()
} catch (err) {
DOMException = function(message, name) {
this.message = message
this.name = name
var error = Error(message)
this.stack = error.stack
}
DOMException.prototype = Object.create(Error.prototype)
DOMException.prototype.constructor = DOMException
}
export function fetch(input, init) {
return new Promise(function(resolve, reject) {
var request = new Request(input, init)
if (request.signal && request.signal.aborted) {
return reject(new DOMException('Aborted', 'AbortError'))
}
var xhr = new XMLHttpRequest()
function abortXhr() {
xhr.abort()
}
xhr.onload = function() {
var options = {
statusText: xhr.statusText,
headers: parseHeaders(xhr.getAllResponseHeaders() || '')
}
// This check if specifically for when a user fetches a file locally from the file system
// Only if the status is out of a normal range
if (request.url.indexOf('file://') === 0 && (xhr.status < 200 || xhr.status > 599)) {
options.status = 200;
} else {
options.status = xhr.status;
}
options.url = 'responseURL' in xhr ? xhr.responseURL : options.headers.get('X-Request-URL')
var body = 'response' in xhr ? xhr.response : xhr.responseText
setTimeout(function() {
resolve(new Response(body, options))
}, 0)
}
xhr.onerror = function() {
setTimeout(function() {
reject(new TypeError('Network request failed'))
}, 0)
}
xhr.ontimeout = function() {
setTimeout(function() {
reject(new TypeError('Network request timed out'))
}, 0)
}
xhr.onabort = function() {
setTimeout(function() {
reject(new DOMException('Aborted', 'AbortError'))
}, 0)
}
function fixUrl(url) {
try {
return url === '' && g.location.href ? g.location.href : url
} catch (e) {
return url
}
}
xhr.open(request.method, fixUrl(request.url), true)
if (request.credentials === 'include') {
xhr.withCredentials = true
} else if (request.credentials === 'omit') {
xhr.withCredentials = false
}
if ('responseType' in xhr) {
if (support.blob) {
xhr.responseType = 'blob'
} else if (
support.arrayBuffer
) {
xhr.responseType = 'arraybuffer'
}
}
if (init && typeof init.headers === 'object' && !(init.headers instanceof Headers || (g.Headers && init.headers instanceof g.Headers))) {
var names = [];
Object.getOwnPropertyNames(init.headers).forEach(function(name) {
names.push(normalizeName(name))
xhr.setRequestHeader(name, normalizeValue(init.headers[name]))
})
request.headers.forEach(function(value, name) {
if (names.indexOf(name) === -1) {
xhr.setRequestHeader(name, value)
}
})
} else {
request.headers.forEach(function(value, name) {
xhr.setRequestHeader(name, value)
})
}
if (request.signal) {
request.signal.addEventListener('abort', abortXhr)
xhr.onreadystatechange = function() {
// DONE (success or failure)
if (xhr.readyState === 4) {
request.signal.removeEventListener('abort', abortXhr)
}
}
}
xhr.send(typeof request._bodyInit === 'undefined' ? null : request._bodyInit)
})
}
fetch.polyfill = true
if (!g.fetch) {
g.fetch = fetch
g.Headers = Headers
g.Request = Request
g.Response = Response
}

View File

@@ -0,0 +1,620 @@
const g: any = (typeof globalThis !== 'undefined' && globalThis) || (typeof self !== 'undefined' && self) || (typeof global !== 'undefined' && global) || {};
// Feature support detection
interface Support {
searchParams: boolean;
iterable: boolean;
blob: boolean;
formData: boolean;
arrayBuffer: boolean;
}
const support: Support = {
searchParams: 'URLSearchParams' in g,
iterable: 'Symbol' in g && 'iterator' in Symbol,
blob:
'FileReader' in g &&
'Blob' in g &&
(() => {
try {
new Blob();
return true;
} catch (e) {
return false;
}
})(),
formData: 'FormData' in g,
arrayBuffer: 'ArrayBuffer' in g,
};
function isDataView(obj: any): obj is DataView {
return obj && DataView.prototype.isPrototypeOf(obj);
}
let isArrayBufferView: (obj: any) => boolean;
if (support.arrayBuffer) {
const viewClasses = ['[object Int8Array]', '[object Uint8Array]', '[object Uint8ClampedArray]', '[object Int16Array]', '[object Uint16Array]', '[object Int32Array]', '[object Uint32Array]', '[object Float32Array]', '[object Float64Array]'];
isArrayBufferView =
ArrayBuffer.isView ||
((obj: any) => {
return obj && viewClasses.indexOf(Object.prototype.toString.call(obj)) > -1;
});
}
function normalizeName(name: any): string {
if (typeof name !== 'string') {
name = String(name);
}
if (/[^a-z0-9\-#$%&'*+.^_`|~!]/i.test(name) || name === '') {
throw new TypeError('Invalid character in header field name: "' + name + '"');
}
return name.toLowerCase();
}
function normalizeValue(value: any): string {
if (typeof value !== 'string') {
value = String(value);
}
return value;
}
// Build a destructive iterator for the value list
function iteratorFor(items: any[]): IterableIterator<any> {
const iterator: any = {
next: () => {
const value = items.shift();
return { done: value === undefined, value };
},
};
if (support.iterable) {
(iterator as any)[Symbol.iterator] = () => iterator;
}
return iterator;
}
export type HeaderInit = Headers | string[][] | Record<string, string>;
export class Headers {
private map: Record<string, string> = {};
constructor(headers?: HeaderInit) {
if (headers instanceof Headers) {
headers.forEach((value, name) => {
this.append(name, value);
});
} else if (Array.isArray(headers)) {
headers.forEach((header) => {
if (header.length !== 2) {
throw new TypeError('Headers constructor: expected name/value pair to be length 2, found ' + header.length);
}
this.append(header[0], header[1]);
});
} else if (headers) {
Object.getOwnPropertyNames(headers).forEach((name) => {
this.append(name, (headers as Record<string, string>)[name]);
});
}
}
append(name: string, value: string): void {
name = normalizeName(name);
value = normalizeValue(value);
const oldValue = this.map[name];
this.map[name] = oldValue ? oldValue + ', ' + value : value;
}
delete(name: string): void {
delete this.map[normalizeName(name)];
}
get(name: string): string | null {
name = normalizeName(name);
return this.has(name) ? this.map[name] : null;
}
has(name: string): boolean {
return Object.prototype.hasOwnProperty.call(this.map, normalizeName(name));
}
set(name: string, value: string): void {
this.map[normalizeName(name)] = normalizeValue(value);
}
forEach(callback: (value: string, name: string, headers: Headers) => void, thisArg?: any): void {
for (const name in this.map) {
if (Object.prototype.hasOwnProperty.call(this.map, name)) {
callback.call(thisArg, this.map[name], name, this);
}
}
}
keys(): IterableIterator<string> {
const items: string[] = [];
this.forEach((_, name) => items.push(name));
return iteratorFor(items);
}
values(): IterableIterator<string> {
const items: string[] = [];
this.forEach((value) => items.push(value));
return iteratorFor(items);
}
entries(): IterableIterator<[string, string]> {
const items: [string, string][] = [];
this.forEach((value, name) => items.push([name, value]));
return iteratorFor(items);
}
[Symbol.iterator](): IterableIterator<[string, string]> {
return this.entries();
}
}
// Body mixin
export class Body {
bodyUsed = false;
_bodyInit: any;
protected _bodyText?: string;
protected _bodyBlob?: Blob;
protected _bodyFormData?: FormData;
protected _bodyArrayBuffer?: ArrayBuffer;
protected _noBody?: boolean;
protected headers!: Headers;
protected _initBody(body: any): void {
// Ensure bodyUsed property exists
this.bodyUsed = this.bodyUsed;
this._bodyInit = body;
if (!body) {
this._noBody = true;
this._bodyText = '';
} else if (typeof body === 'string') {
this._bodyText = body;
} else if (support.blob && Blob.prototype.isPrototypeOf(body)) {
this._bodyBlob = body as Blob;
} else if (support.formData && FormData.prototype.isPrototypeOf(body)) {
this._bodyFormData = body as FormData;
} else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) {
this._bodyText = body.toString();
} else if (support.arrayBuffer && support.blob && isDataView(body)) {
// @ts-ignore
this._bodyArrayBuffer = bufferClone((body as DataView).buffer);
this._bodyInit = new Blob([this._bodyArrayBuffer]);
} else if (support.arrayBuffer && (ArrayBuffer.prototype.isPrototypeOf(body) || isArrayBufferView(body))) {
this._bodyArrayBuffer = bufferClone(body as ArrayBuffer);
} else {
this._bodyText = body = Object.prototype.toString.call(body);
}
// Set Content-Type header if not set
if (!this.headers.get('content-type')) {
if (typeof body === 'string') {
this.headers.set('content-type', 'text/plain;charset=UTF-8');
} else if (this._bodyBlob && this._bodyBlob.type) {
this.headers.set('content-type', this._bodyBlob.type);
} else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) {
this.headers.set('content-type', 'application/x-www-form-urlencoded;charset=UTF-8');
}
}
}
blob?(): Promise<Blob> {
const rejected = consumed(this);
if (rejected) {
return rejected as Promise<any>;
}
if (this._bodyBlob) {
return Promise.resolve(this._bodyBlob);
} else if (this._bodyArrayBuffer) {
return Promise.resolve(new Blob([this._bodyArrayBuffer]));
} else if (this._bodyFormData) {
throw new Error('could not read FormData body as blob');
} else {
return Promise.resolve(new Blob([this._bodyText!]));
}
}
arrayBuffer(): Promise<ArrayBuffer> {
if (this._bodyArrayBuffer) {
const consumedResult = consumed(this);
if (consumedResult) {
return consumedResult as Promise<any>;
} else if (ArrayBuffer.isView(this._bodyArrayBuffer)) {
return Promise.resolve((this._bodyArrayBuffer as any).buffer.slice((this._bodyArrayBuffer as any).byteOffset, (this._bodyArrayBuffer as any).byteOffset + (this._bodyArrayBuffer as any).byteLength));
} else {
return Promise.resolve(this._bodyArrayBuffer);
}
} else if (support.blob) {
// @ts-ignore
return this.blob!().then(readBlobAsArrayBuffer);
} else {
throw new Error('could not read as ArrayBuffer');
}
}
text(): Promise<string> {
const rejected = consumed(this);
if (rejected) {
return rejected as Promise<any>;
}
if (this._bodyBlob) {
return readBlobAsText(this._bodyBlob);
} else if (this._bodyArrayBuffer) {
return Promise.resolve(readArrayBufferAsText(this._bodyArrayBuffer));
} else if (this._bodyFormData) {
throw new Error('could not read FormData body as text');
} else {
return Promise.resolve(this._bodyText!);
}
}
formData?(): Promise<FormData> {
return this.text().then(decode);
}
json(): Promise<any> {
return this.text().then(JSON.parse);
}
}
// Helper functions for Body
function consumed(body: any): Promise<any> | undefined {
if (body._noBody) return;
if (body.bodyUsed) {
return Promise.reject(new TypeError('Already read'));
}
body.bodyUsed = true;
}
function fileReaderReady(reader: FileReader): Promise<any> {
return new Promise((resolve, reject) => {
reader.onload = () => resolve(reader.result);
reader.onerror = () => reject(reader.error);
});
}
function readBlobAsArrayBuffer(blob: Blob): Promise<ArrayBuffer | string | null> {
const reader = new FileReader();
const promise = fileReaderReady(reader);
reader.readAsArrayBuffer(blob);
return promise;
}
function readBlobAsText(blob: Blob): Promise<string | null> {
const reader = new FileReader();
const promise = fileReaderReady(reader);
const match = /charset=([A-Za-z0-9_-]+)/.exec(blob.type);
const encoding = match ? match[1] : 'utf-8';
reader.readAsText(blob, encoding);
return promise;
}
function readArrayBufferAsText(buf: ArrayBuffer): string {
const view = new Uint8Array(buf);
const chars = new Array(view.length);
for (let i = 0; i < view.length; i++) {
chars[i] = String.fromCharCode(view[i]);
}
return chars.join('');
}
function bufferClone(buf: ArrayBuffer): ArrayBuffer {
if (buf.slice) {
return buf.slice(0);
} else {
const view = new Uint8Array(buf.byteLength);
view.set(new Uint8Array(buf));
return view.buffer;
}
}
// HTTP methods whose capitalization should be normalized
const methods = ['CONNECT', 'DELETE', 'GET', 'HEAD', 'OPTIONS', 'PATCH', 'POST', 'PUT', 'TRACE'];
function normalizeMethod(method: string): string {
const upcased = method.toUpperCase();
return methods.indexOf(upcased) > -1 ? upcased : method;
}
// Request class
export type RequestInfo = string | Request;
export interface RequestInit {
method?: string;
headers?: HeaderInit;
body?: any;
mode?: string | null;
credentials?: RequestCredentials;
cache?: 'default' | 'no-store' | 'reload' | 'no-cache' | 'force-cache' | 'only-if-cached';
signal?: AbortSignal;
}
export class Request extends Body {
url: string;
credentials: RequestCredentials;
headers: Headers;
method: string;
mode: string | null;
signal?: AbortSignal;
referrer: string | null;
constructor(input: RequestInfo, options: RequestInit = {}) {
super();
let body = options.body;
if (input instanceof Request) {
if (input.bodyUsed) {
throw new TypeError('Already read');
}
this.url = input.url;
this.credentials = input.credentials;
if (!options.headers) {
this.headers = new Headers(input.headers);
}
this.method = input.method;
this.mode = input.mode;
this.signal = input.signal;
if (!body && input._bodyInit != null) {
body = input._bodyInit;
input.bodyUsed = true;
}
} else {
this.url = String(input);
}
this.credentials = options.credentials || this.credentials || 'same-origin';
if (options.headers || !this.headers) {
this.headers = new Headers(options.headers);
}
this.method = normalizeMethod(options.method || this.method || 'GET');
this.mode = options.mode || this.mode || null;
this.signal = options.signal || this.signal || ('AbortController' in g ? new AbortController().signal : undefined);
this.referrer = null;
if ((this.method === 'GET' || this.method === 'HEAD') && body) {
throw new TypeError('Body not allowed for GET or HEAD requests');
}
this._initBody(body);
if (this.method === 'GET' || this.method === 'HEAD') {
if (options.cache === 'no-store' || options.cache === 'no-cache') {
const reParamSearch = /([?&])_=[^&]*/;
if (reParamSearch.test(this.url)) {
this.url = this.url.replace(reParamSearch, '$1_=' + new Date().getTime());
} else {
const reQueryString = /\?/;
this.url += (reQueryString.test(this.url) ? '&' : '?') + '_=' + new Date().getTime();
}
}
}
}
clone(): Request {
return new Request(this, { body: this._bodyInit });
}
}
// Decode URL-encoded form data
function decode(body: string): FormData {
const form = new FormData();
body
.trim()
.split('&')
.forEach((bytes) => {
if (bytes) {
const split = bytes.split('=');
const name = split.shift()!.replace(/\+/g, ' ');
const value = split.join('=').replace(/\+/g, ' ');
form.append(decodeURIComponent(name), decodeURIComponent(value));
}
});
return form;
}
// Parse raw headers string into Headers
function parseHeaders(rawHeaders: string): Headers {
const headers = new Headers();
const preProcessed = rawHeaders.replace(/\r?\n[\t ]+/g, ' ');
preProcessed
.split('\r')
.map((header) => (header.indexOf('\n') === 0 ? header.substr(1) : header))
.forEach((line) => {
const parts = line.split(':');
const key = parts.shift()!.trim();
if (key) {
const value = parts.join(':').trim();
try {
headers.append(key, value);
} catch (err) {
console.warn('Response ' + (err as Error).message);
}
}
});
return headers;
}
// Response class
export interface ResponseInit {
status?: number;
statusText?: string;
headers?: HeaderInit;
url?: string;
}
const redirectStatuses = [301, 302, 303, 307, 308];
export class Response extends Body {
type: string;
status: number;
ok: boolean;
statusText: string;
headers: Headers;
url: string;
constructor(bodyInit: any, options: ResponseInit = {}) {
super();
this.type = 'default';
this.status = options.status === undefined ? 200 : options.status!;
if (this.status < 200 || this.status > 599) {
throw new RangeError(`Failed to construct 'Response': The status provided (${this.status}) is outside the range [200, 599].`);
}
this.ok = this.status >= 200 && this.status < 300;
this.statusText = options.statusText === undefined ? '' : String(options.statusText);
this.headers = new Headers(options.headers);
this.url = options.url || '';
this._initBody(bodyInit);
}
clone(): Response {
return new Response(this._bodyInit, {
status: this.status,
statusText: this.statusText,
headers: new Headers(this.headers),
url: this.url,
});
}
static error(): Response {
const response = new Response(null, { status: 200, statusText: '' });
response.ok = false;
response.status = 0;
response.type = 'error';
return response;
}
static redirect(url: string, status: number): Response {
if (!redirectStatuses.includes(status)) {
throw new RangeError('Invalid status code');
}
return new Response(null, { status, headers: { location: url } });
}
}
// DOMException polyfill
export let DOMException: any = g.DOMException;
try {
new (g.DOMException as any)();
} catch (err) {
DOMException = class {
message: string;
name: string;
stack?: string;
constructor(message: string, name: string) {
this.message = message;
this.name = name;
const error = new Error(message);
this.stack = error.stack;
}
};
}
// fetch function
export function fetch(input: RequestInfo, init?: RequestInit): Promise<Response> {
return new Promise((resolve, reject) => {
const request = new Request(input, init as any);
if (request.signal && (request.signal as any).aborted) {
return reject(new DOMException('Aborted', 'AbortError'));
}
const xhr = new XMLHttpRequest();
function abortXhr() {
xhr.abort();
}
xhr.onload = function () {
const options: any = {
statusText: xhr.statusText,
headers: parseHeaders(xhr.getAllResponseHeaders() || ''),
};
// Local file handling
if (request.url.startsWith('file://') && (xhr.status < 200 || xhr.status > 599)) {
options.status = 200;
} else {
options.status = xhr.status;
}
options.url = 'responseURL' in xhr ? xhr.responseURL : options.headers.get('X-Request-URL');
// @ts-ignore
const body = 'response' in xhr ? xhr.response : xhr.responseText;
setTimeout(() => resolve(new Response(body, options)), 0);
};
xhr.onerror = function () {
setTimeout(() => reject(new TypeError('Network request failed')), 0);
};
xhr.ontimeout = function () {
setTimeout(() => reject(new TypeError('Network request timed out')), 0);
};
xhr.onabort = function () {
setTimeout(() => reject(new DOMException('Aborted', 'AbortError')), 0);
};
function fixUrl(url: string): string {
try {
return url === '' && g.location.href ? g.location.href : url;
} catch (e) {
return url;
}
}
xhr.open(request.method, fixUrl(request.url), true);
if (request.credentials === 'include') {
xhr.withCredentials = true;
} else if (request.credentials === 'omit') {
xhr.withCredentials = false;
}
if ('responseType' in xhr) {
if (support.blob) {
(xhr as any).responseType = 'blob';
} else if (support.arrayBuffer) {
(xhr as any).responseType = 'arraybuffer';
}
}
if (init && typeof init.headers === 'object' && !(init.headers instanceof Headers) && !(g.Headers && init.headers instanceof g.Headers)) {
const names: string[] = [];
Object.getOwnPropertyNames(init.headers).forEach((name) => {
names.push(normalizeName(name));
xhr.setRequestHeader(name, normalizeValue((init.headers as any)[name]));
});
request.headers.forEach((value, name) => {
if (!names.includes(name)) {
xhr.setRequestHeader(name, value);
}
});
} else {
request.headers.forEach((value, name) => xhr.setRequestHeader(name, value));
}
if (request.signal) {
request.signal.addEventListener('abort', abortXhr);
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
(request.signal as any).removeEventListener('abort', abortXhr);
}
};
}
xhr.send(typeof request._bodyInit === 'undefined' ? null : request._bodyInit);
});
}
// Attach polyfill to globals
(fetch as any).polyfill = true;
if (!g.fetch) {
g.fetch = fetch;
g.Headers = Headers;
g.Request = Request;
g.Response = Response;
}

View File

@@ -20,9 +20,9 @@
"{workspaceRoot}/LICENSE",
"{projectRoot}/README.md",
"{projectRoot}/global-types.d.ts",
"{projectRoot}/fetch/LICENSE",
{ "glob": "**/*", "input": "{projectRoot}/js-libs/", "output": "./js-libs/" },
{ "glob": "**/*", "input": "{projectRoot}/cli-hooks/", "output": "./cli-hooks/" },
{ "glob": "**/*", "input": "{projectRoot}/fetch/", "output": "./fetch/" },
{ "glob": "**/*", "input": "{projectRoot}/css/", "output": "./css/" },
{ "glob": "**/*", "input": "{projectRoot}/css-value/", "output": "./css-value/" },
{ "glob": "**/*", "input": "{projectRoot}/platforms/", "output": "./platforms/" },

View File

@@ -1,4 +1,5 @@
import { AndroidActionItemSettings, AndroidActionBarSettings as AndroidActionBarSettingsDefinition, ActionItem as ActionItemDefinition } from '.';
import { isAccessibilityServiceEnabled, updateContentDescription } from '../../application';
import { ActionItemBase, ActionBarBase, isVisible, flatProperty, traceMissingIcon, androidContentInsetLeftProperty, androidContentInsetRightProperty } from './action-bar-common';
import { AndroidHelper, View } from '../core/view';
import { Color } from '../../color';
@@ -6,7 +7,6 @@ import { layout, RESOURCE_PREFIX, isFontIconURI } from '../../utils';
import { colorProperty } from '../styling/style-properties';
import { ImageSource } from '../../image-source';
import { Application } from '../../application';
import { isAccessibilityServiceEnabled, updateContentDescription } from '../../accessibility';
import type { Background } from '../styling/background';
import { SDK_VERSION } from '../../utils/constants';
import { NativeScriptAndroidView } from '../utils';

View File

@@ -19,7 +19,6 @@ import { profile } from '../../../profiling';
import { DOMNode } from '../../../debugger/dom-types';
import { applyInlineStyle, CssState, StyleScope } from '../../styling/style-scope';
import { ViewBase as ViewBaseDefinition } from '.';
import { booleanConverter } from './utils';
export { booleanConverter } from './utils';
@@ -106,12 +105,12 @@ export interface ShowModalOptions {
* @param criterion - The type of ancestor view we are looking for. Could be a string containing a class name or an actual type.
* Returns an instance of a view (if found), otherwise undefined.
*/
export function getAncestor(view: ViewBaseDefinition, criterion: string | { new () }): ViewBaseDefinition {
let matcher: (view: ViewBaseDefinition) => boolean = null;
export function getAncestor(view: ViewBase, criterion: string | { new () }): ViewBase {
let matcher: (view: ViewBase) => boolean = null;
if (typeof criterion === 'string') {
matcher = (view: ViewBaseDefinition) => view.typeName === criterion;
matcher = (view: ViewBase) => view.typeName === criterion;
} else {
matcher = (view: ViewBaseDefinition) => view instanceof criterion;
matcher = (view: ViewBase) => view instanceof criterion;
}
for (let parent = view.parent; parent != null; parent = parent.parent) {
@@ -129,7 +128,7 @@ export function getAncestor(view: ViewBaseDefinition, criterion: string | { new
* @param id - The id of the view to look for.
* Returns an instance of a view (if found), otherwise undefined.
*/
export function getViewById(view: ViewBaseDefinition, id: string): ViewBaseDefinition {
export function getViewById(view: ViewBase, id: string): ViewBase {
if (!view) {
return undefined;
}
@@ -138,8 +137,8 @@ export function getViewById(view: ViewBaseDefinition, id: string): ViewBaseDefin
return view;
}
let retVal: ViewBaseDefinition;
const descendantsCallback = function (child: ViewBaseDefinition): boolean {
let retVal: ViewBase;
const descendantsCallback = function (child: ViewBase): boolean {
if (child.id === id) {
retVal = child;
@@ -161,7 +160,7 @@ export function getViewById(view: ViewBaseDefinition, id: string): ViewBaseDefin
* @param domId - The id of the view to look for.
* Returns an instance of a view (if found), otherwise undefined.
*/
export function getViewByDomId(view: ViewBaseDefinition, domId: number): ViewBaseDefinition {
export function getViewByDomId(view: ViewBase, domId: number): ViewBase {
if (!view) {
return undefined;
}
@@ -170,8 +169,8 @@ export function getViewByDomId(view: ViewBaseDefinition, domId: number): ViewBas
return view;
}
let retVal: ViewBaseDefinition;
const descendantsCallback = function (child: ViewBaseDefinition): boolean {
let retVal: ViewBase;
const descendantsCallback = function (child: ViewBase): boolean {
if (view._domId === domId) {
retVal = child;
@@ -194,17 +193,17 @@ export function getViewByDomId(view: ViewBaseDefinition, domId: number): ViewBas
* @param selector - The selector of the view to look for.
* Returns an instance of a view (if found), otherwise undefined.
*/
export function querySelectorAll(view: ViewBaseDefinition, selector: string): Array<ViewBaseDefinition> {
export function querySelectorAll(view: ViewBase, selector: string): Array<ViewBase> {
if (!view) {
return [];
}
const retVal: Array<ViewBaseDefinition> = [];
const retVal: Array<ViewBase> = [];
if (view[selector]) {
retVal.push(view);
}
const descendantsCallback = function (child: ViewBaseDefinition): boolean {
const descendantsCallback = function (child: ViewBase): boolean {
if (child[selector]) {
retVal.push(child);
}
@@ -222,13 +221,13 @@ export function querySelectorAll(view: ViewBaseDefinition, selector: string): Ar
* @param view - Starting view (parent container).
* @param callback - A function to execute on every child. If function returns false it breaks the iteration.
*/
export function eachDescendant(view: ViewBaseDefinition, callback: (child: ViewBaseDefinition) => boolean) {
export function eachDescendant(view: ViewBase, callback: (child: ViewBase) => boolean) {
if (!callback || !view) {
return;
}
let continueIteration: boolean;
const localCallback = function (child: ViewBaseDefinition): boolean {
const localCallback = function (child: ViewBase): boolean {
continueIteration = callback(child);
if (continueIteration) {
child.eachChild(localCallback);
@@ -302,7 +301,7 @@ const DEFAULT_VIEW_PADDINGS: Map<string, any> = new Map();
*
* @nsView ViewBase
*/
export abstract class ViewBase extends Observable implements ViewBaseDefinition {
export abstract class ViewBase extends Observable {
/**
* String value used when hooking to loaded event.
*
@@ -636,14 +635,14 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
/**
* Returns the child view with the specified id.
*/
getViewById<T extends ViewBaseDefinition>(id: string): T {
getViewById<T extends ViewBase>(id: string): T {
return <T>getViewById(this, id);
}
/**
* Returns the child view with the specified domId.
*/
getViewByDomId<T extends ViewBaseDefinition>(domId: number): T {
getViewByDomId<T extends ViewBase>(domId: number): T {
return <T>getViewByDomId(this, domId);
}

View File

@@ -1,5 +1,4 @@
// Definitions.
import type { Point, CustomLayoutView as CustomLayoutViewDefinition, Position } from '.';
import type { Point, Position } from './view-interfaces';
import type { GestureTypes, GestureEventData } from '../../gestures';
import { ViewCommon, isEnabledProperty, originXProperty, originYProperty, isUserInteractionEnabledProperty, testIDProperty, AndroidHelper } from './view-common';
@@ -17,9 +16,9 @@ import { Background, BackgroundClearFlags, refreshBorderDrawable } from '../../s
import { profile } from '../../../profiling';
import { topmost } from '../../frame/frame-stack';
import { Screen } from '../../../platform';
import { AndroidActivityBackPressedEventData, Application } from '../../../application';
import { AndroidActivityBackPressedEventData, Application, updateAccessibilityProperties, updateContentDescription } from '../../../application';
import { accessibilityEnabledProperty, accessibilityHiddenProperty, accessibilityHintProperty, accessibilityIdentifierProperty, accessibilityLabelProperty, accessibilityLiveRegionProperty, accessibilityMediaSessionProperty, accessibilityRoleProperty, accessibilityStateProperty, accessibilityValueProperty } from '../../../accessibility/accessibility-properties';
import { AccessibilityLiveRegion, AccessibilityRole, AndroidAccessibilityEvent, updateAccessibilityProperties, updateContentDescription, AccessibilityState } from '../../../accessibility';
import { AccessibilityLiveRegion, AccessibilityRole, AndroidAccessibilityEvent, AccessibilityState } from '../../../accessibility';
import * as Utils from '../../../utils';
import { SDK_VERSION } from '../../../utils/constants';
import { BoxShadow } from '../../styling/box-shadow';
@@ -1276,7 +1275,7 @@ export class ContainerView extends View {
public iosOverflowSafeArea: boolean;
}
export class CustomLayoutView extends ContainerView implements CustomLayoutViewDefinition {
export class CustomLayoutView extends ContainerView {
nativeViewProtected: android.view.ViewGroup;
public createNativeView() {

View File

@@ -9,6 +9,7 @@ import { LinearGradient } from '../../styling/linear-gradient';
import { InheritedProperty, Property } from '../properties';
import { ViewBase } from '../view-base';
import { ViewCommon } from './view-common';
import type { Point } from './view-interfaces';
export * from './view-common';
// helpers (these are okay re-exported here)
@@ -43,67 +44,6 @@ export function CSSType(type: string): ClassDecorator;
*/
export function viewMatchesModuleContext(view: View, context: ModuleContext, type: ModuleType[]): boolean;
/**
* The Point interface describes a two dimensional location.
* It has two properties x and y, representing the x and y coordinate of the location.
*/
export interface Point {
/**
* Represents the x coordinate of the location.
*/
x: number;
/**
* Represents the y coordinate of the location.
*/
y: number;
/**
* Represents the z coordinate of the location.
*/
z?: number;
}
export interface Position {
top: number;
right: number;
bottom: number;
left: number;
}
/**
* The Size interface describes abstract dimensions in two dimensional space.
* It has two properties width and height, representing the width and height values of the size.
*/
export interface Size {
/**
* Represents the width of the size.
*/
width: number;
/**
* Represents the height of the size.
*/
height: number;
}
/**
* Defines the data for the shownModally event.
*/
export interface ShownModallyData extends EventData {
/**
* The context (optional, may be undefined) passed to the view when shown modally.
*/
context?: any;
/**
* A callback to call when you want to close the modally shown view.
* Pass in any kind of arguments and you will receive when the callback parameter
* of View.showModal is executed.
*/
closeCallback?: Function;
}
/**
* This class is the base class for all UI components.
* A View occupies a rectangular area on the screen and is responsible for drawing and layouting of all UI components within.

View File

@@ -1,5 +1,6 @@
import type { Point, Position, View as ViewDefinition } from '.';
import type { Point, Position } from './view-interfaces';
import { ViewCommon, isEnabledProperty, originXProperty, originYProperty, isUserInteractionEnabledProperty, testIDProperty } from './view-common';
import { isAccessibilityServiceEnabled, updateAccessibilityProperties } from '../../../application';
import { ShowModalOptions, hiddenProperty } from '../view-base';
import { Trace } from '../../../trace';
import { layout, ios as iosUtils, SDK_VERSION } from '../../../utils';
@@ -8,7 +9,7 @@ import { ios as iosBackground, Background } from '../../styling/background';
import { perspectiveProperty, visibilityProperty, opacityProperty, rotateProperty, rotateXProperty, rotateYProperty, scaleXProperty, scaleYProperty, translateXProperty, translateYProperty, zIndexProperty, backgroundInternalProperty } from '../../styling/style-properties';
import { profile } from '../../../profiling';
import { accessibilityEnabledProperty, accessibilityHiddenProperty, accessibilityHintProperty, accessibilityIdentifierProperty, accessibilityLabelProperty, accessibilityLanguageProperty, accessibilityLiveRegionProperty, accessibilityMediaSessionProperty, accessibilityRoleProperty, accessibilityStateProperty, accessibilityValueProperty, accessibilityIgnoresInvertColorsProperty } from '../../../accessibility/accessibility-properties';
import { IOSPostAccessibilityNotificationType, isAccessibilityServiceEnabled, updateAccessibilityProperties, AccessibilityEventOptions, AccessibilityRole, AccessibilityState } from '../../../accessibility';
import { IOSPostAccessibilityNotificationType, AccessibilityEventOptions, AccessibilityRole, AccessibilityState } from '../../../accessibility';
import { CoreTypes } from '../../../core-types';
import type { ModalTransition } from '../../transition/modal-transition';
import { SharedTransition } from '../../transition/shared-transition';
@@ -23,7 +24,7 @@ const PFLAG_FORCE_LAYOUT = 1;
const PFLAG_MEASURED_DIMENSION_SET = 1 << 1;
const PFLAG_LAYOUT_REQUIRED = 1 << 2;
export class View extends ViewCommon implements ViewDefinition {
export class View extends ViewCommon {
// @ts-ignore
nativeViewProtected: UIView;
// @ts-ignore
@@ -371,7 +372,7 @@ export class View extends ViewCommon implements ViewDefinition {
};
}
public getLocationRelativeTo(otherView: ViewDefinition): Point {
public getLocationRelativeTo(otherView: View): Point {
if (!this.nativeViewProtected || !this.nativeViewProtected.window || !otherView.nativeViewProtected || !otherView.nativeViewProtected.window || this.nativeViewProtected.window !== otherView.nativeViewProtected.window) {
return undefined;
}

View File

@@ -1,6 +1,5 @@
// Definitions.
import { View as ViewDefinition, Point, Size, ShownModallyData, Position } from '.';
import type { View as ViewType } from '.';
import { Point, Size, ShownModallyData, Position } from './view-interfaces';
import { booleanConverter, ShowModalOptions, ViewBase } from '../view-base';
import { getEventOrGestureName } from '../bindable';
import { layout } from '../../../utils';
@@ -12,7 +11,7 @@ import { EventData } from '../../../data/observable';
import { Trace } from '../../../trace';
import { CoreTypes } from '../../../core-types';
import { ViewHelper } from './view-helper';
import { setupAccessibleView } from '../../../accessibility';
import { setupAccessibleView } from '../../../application';
import { PercentLength } from '../../styling/style-properties';
@@ -26,9 +25,9 @@ import { LinearGradient } from '../../styling/linear-gradient';
import { Animation } from '../../animation';
import type { AnimationPromise } from '../../animation/animation-types';
import { AccessibilityEventOptions, AccessibilityLiveRegion, AccessibilityRole, AccessibilityState } from '../../../accessibility/accessibility-types';
import { AccessibilityEventOptions, AccessibilityLiveRegion, AccessibilityRole, AccessibilityState, getFontScale } from '../../../accessibility';
import { accessibilityHintProperty, accessibilityIdentifierProperty, accessibilityLabelProperty, accessibilityValueProperty, accessibilityIgnoresInvertColorsProperty } from '../../../accessibility/accessibility-properties';
import { accessibilityBlurEvent, accessibilityFocusChangedEvent, accessibilityFocusEvent, accessibilityPerformEscapeEvent, getCurrentFontScale } from '../../../accessibility';
import { accessibilityBlurEvent, accessibilityFocusChangedEvent, accessibilityFocusEvent, accessibilityPerformEscapeEvent } from '../../../accessibility';
import { ShadowCSSValues } from '../../styling/css-shadow';
import { SharedTransition, SharedTransitionInteractiveOptions } from '../../transition/shared-transition';
import { Flex, FlexFlow } from '../../layouts/flexbox-layout';
@@ -42,7 +41,7 @@ export function CSSType(type: string): ClassDecorator {
};
}
export function viewMatchesModuleContext(view: ViewDefinition, context: ModuleContext, types: ModuleType[]): boolean {
export function viewMatchesModuleContext(view: ViewCommon, context: ModuleContext, types: ModuleType[]): boolean {
return context && view._moduleName && context.type && types.some((type) => type === context.type) && context.path && context.path.includes(view._moduleName);
}
@@ -73,7 +72,7 @@ type InteractiveTransitionState = { began?: boolean; cancelled?: boolean; option
// TODO: remove once we fully switch to the new event system
const warnedEvent = new Set<string>();
export abstract class ViewCommon extends ViewBase implements ViewDefinition {
export abstract class ViewCommon extends ViewBase {
public static layoutChangedEvent = 'layoutChanged';
public static shownModallyEvent = 'shownModally';
public static showingModallyEvent = 'showingModally';
@@ -102,7 +101,7 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
protected _closeModalCallback: Function;
public _manager: any;
public _modalParent: ViewCommon;
public _modalParent?: ViewCommon;
private _modalContext: any;
private _modal: ViewCommon;
@@ -399,7 +398,7 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
}
}
public showModal(...args): ViewDefinition {
public showModal(...args): ViewType {
const { view, options } = this.getModalOptions(args);
if (options.transition?.instance) {
SharedTransition.updateState(options.transition?.instance.id, {
@@ -436,7 +435,7 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
modalRootViewCssClasses.forEach((c) => this.cssClasses.add(c));
parent._modal = this;
this.style.fontScaleInternal = getCurrentFontScale();
this.style.fontScaleInternal = getFontScale();
this._modalParent = parent;
this._modalContext = options.context;
this._closeModalCallback = (...originalArgs) => {
@@ -1050,7 +1049,7 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
return ViewHelper.combineMeasuredStates(curState, newState);
}
public static layoutChild(parent: ViewDefinition, child: ViewDefinition, left: number, top: number, right: number, bottom: number, setFrame = true): void {
public static layoutChild(parent: ViewCommon, child: ViewCommon, left: number, top: number, right: number, bottom: number, setFrame = true): void {
ViewHelper.layoutChild(parent, child, left, top, right, bottom);
}
@@ -1089,7 +1088,7 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
this.eachChildView(<any>callback);
}
public eachChildView(callback: (view: ViewDefinition) => boolean) {
public eachChildView(callback: (view: ViewCommon) => boolean) {
//
}
@@ -1117,7 +1116,7 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
return undefined;
}
public getLocationRelativeTo(otherView: ViewDefinition): Point {
public getLocationRelativeTo(otherView: ViewCommon): Point {
return undefined;
}
@@ -1224,11 +1223,11 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
//
}
_hasAncestorView(ancestorView: ViewDefinition): boolean {
const matcher = (view: ViewDefinition) => view === ancestorView;
_hasAncestorView(ancestorView: ViewCommon): boolean {
const matcher = (view: ViewCommon) => view === ancestorView;
for (let parent = this.parent; parent != null; parent = parent.parent) {
if (matcher(<ViewDefinition>parent)) {
if (matcher(<ViewCommon>parent)) {
return true;
}
}

View File

@@ -1,5 +1,6 @@
// Types
import { Position, View } from '..';
import { Position } from '../view-interfaces';
import type { View } from '..';
// Requires
import { ViewHelper } from './view-helper-common';

View File

@@ -0,0 +1,61 @@
import type { EventData } from '../../../data/observable';
/**
* Defines the data for the shownModally event.
*/
export interface ShownModallyData extends EventData {
/**
* The context (optional, may be undefined) passed to the view when shown modally.
*/
context?: any;
/**
* A callback to call when you want to close the modally shown view.
* Pass in any kind of arguments and you will receive when the callback parameter
* of View.showModal is executed.
*/
closeCallback?: Function;
}
/**
* The Point interface describes a two dimensional location.
* It has two properties x and y, representing the x and y coordinate of the location.
*/
export interface Point {
/**
* Represents the x coordinate of the location.
*/
x: number;
/**
* Represents the y coordinate of the location.
*/
y: number;
/**
* Represents the z coordinate of the location.
*/
z?: number;
}
export interface Position {
top: number;
right: number;
bottom: number;
left: number;
}
/**
* The Size interface describes abstract dimensions in two dimensional space.
* It has two properties width and height, representing the width and height values of the size.
*/
export interface Size {
/**
* Represents the width of the size.
*/
width: number;
/**
* Represents the height of the size.
*/
height: number;
}

View File

@@ -685,7 +685,7 @@ export class FrameBase extends CustomLayoutView {
}
// Handle markup/script changes in currentPage
if (this.currentPage && viewMatchesModuleContext(this.currentPage, context, ['markup', 'script'])) {
if (this.currentPage && viewMatchesModuleContext(this.currentPage as any, context, ['markup', 'script'])) {
Trace.write(`Change Handled: Replacing page ${context.path}`, Trace.categories.Livesync);
// Replace current page with a default fade transition

View File

@@ -15,7 +15,8 @@ export { ControlStateChangeListener } from './core/control-state-change';
export { ViewBase, eachDescendant, getAncestor, getViewById, booleanConverter, querySelectorAll } from './core/view-base';
export type { ShowModalOptions } from './core/view-base';
export { View, CSSType, ContainerView, ViewHelper, AndroidHelper, IOSHelper, isUserInteractionEnabledProperty, PseudoClassHandler, CustomLayoutView } from './core/view';
export type { Template, KeyedTemplate, ShownModallyData, AddArrayFromBuilder, AddChildFromBuilder, Size } from './core/view';
export type { Template, KeyedTemplate, AddArrayFromBuilder, AddChildFromBuilder } from './core/view';
export type { ShownModallyData, Size } from './core/view/view-interfaces';
export { Property, CoercibleProperty, InheritedProperty, CssProperty, InheritedCssProperty, ShorthandProperty, CssAnimationProperty, unsetValue, makeParser, makeValidator } from './core/properties';
export { addWeakEventListener, removeWeakEventListener } from './core/weak-event-listener';
export { DatePicker } from './date-picker';

View File

@@ -1,5 +1,6 @@
import { FlexDirection, FlexWrap, JustifyContent, AlignItems, AlignContent, FlexboxLayoutBase, FlexBasisPercent, orderProperty, flexGrowProperty, flexShrinkProperty, flexWrapBeforeProperty, alignSelfProperty } from './flexbox-layout-common';
import { Position, View } from '../../core/view';
import { View } from '../../core/view';
import { Position } from '../../core/view/view-interfaces';
import { layout } from '../../../utils';
export * from './flexbox-layout-common';

View File

@@ -1,6 +1,7 @@
import { StackLayoutBase } from './stack-layout-common';
import { CoreTypes } from '../../../core-types';
import { Position, View } from '../../core/view';
import { View } from '../../core/view';
import { Position } from '../../core/view/view-interfaces';
import { layout } from '../../../utils';
import { Trace } from '../../../trace';

View File

@@ -1,12 +1,12 @@
import { isAccessibilityServiceEnabled } from '../../application';
import { PageBase, actionBarHiddenProperty, statusBarStyleProperty, androidStatusBarBackgroundProperty } from './page-common';
import { CoreTypes } from '../../core-types';
import { View } from '../core/view';
import { Color } from '../../color';
import { ActionBar } from '../action-bar';
import { GridLayout } from '../layouts/grid-layout';
import { SDK_VERSION } from '../../utils/constants';
import { profile } from '../../profiling';
import { AndroidAccessibilityEvent, getLastFocusedViewOnPage, isAccessibilityServiceEnabled } from '../../accessibility';
import { AndroidAccessibilityEvent, getLastFocusedViewOnPage } from '../../accessibility';
export * from './page-common';
@@ -23,7 +23,7 @@ export class Page extends PageBase {
JSON.stringify([
{ value: 1, type: 0 /* org.nativescript.widgets.GridUnitType.auto */ },
{ value: 1, type: 2 /* org.nativescript.widgets.GridUnitType.star */ },
])
]),
);
return layout;
}

View File

@@ -1,14 +1,12 @@
// Definitions.
import { isAccessibilityServiceEnabled } from '../../application';
import { Frame, BackstackEntry, NavigationType } from '../frame';
// Types.
import { View, IOSHelper } from '../core/view';
import { PageBase, actionBarHiddenProperty, statusBarStyleProperty } from './page-common';
import { profile } from '../../profiling';
import { layout } from '../../utils';
import { SDK_VERSION } from '../../utils/constants';
import { getLastFocusedViewOnPage, isAccessibilityServiceEnabled } from '../../accessibility';
import { getLastFocusedViewOnPage } from '../../accessibility';
import { SharedTransition } from '../transition/shared-transition';
export * from './page-common';

View File

@@ -1,6 +1,7 @@
import { Page as PageDefinition } from '.';
import { ContentView } from '../content-view';
import { View, CSSType, ShownModallyData } from '../core/view';
import { View, CSSType } from '../core/view';
import { ShownModallyData } from '../core/view/view-interfaces';
import { booleanConverter } from '../core/view-base';
import { Property, CssProperty } from '../core/properties';
import { Style } from '../styling/style';

View File

@@ -1,6 +1,7 @@
import { ScrollEventData } from '../scroll-view';
import { Background as BackgroundDefinition } from './background';
import { View, Point, Position } from '../core/view';
import { View } from '../core/view';
import { Point, Position } from '../core/view/view-interfaces';
import { LinearGradient } from './linear-gradient';
import { Screen } from '../../platform';
import { isDataURI, isFileOrResourcePath, layout } from '../../utils';
@@ -9,7 +10,6 @@ import { ImageSource } from '../../image-source';
import type { CSSValue } from '../../css-value/reworkcss-value';
import { parse as cssParse } from '../../css-value/reworkcss-value.js';
import { BoxShadow } from './box-shadow';
import { Length } from './style-properties';
import { BackgroundClearFlags } from './background-common';
import { ClipPathFunction } from './clip-path-function';

View File

@@ -8,7 +8,7 @@ import { Observable } from '../../../data/observable';
import { FlexDirection, FlexWrap, JustifyContent, AlignItems, AlignContent, Order, FlexGrow, FlexShrink, FlexWrapBefore, AlignSelf, FlexFlow, Flex } from '../../layouts/flexbox-layout';
import { Trace } from '../../../trace';
import { CoreTypes } from '../../../core-types';
import { AccessibilityLiveRegion, AccessibilityRole, AccessibilityState } from '../../../accessibility/accessibility-types';
import { AccessibilityLiveRegion, AccessibilityRole, AccessibilityState } from '../../../accessibility';
import { ShadowCSSValues } from '../css-shadow';
import { StrokeCSSValues } from '../css-stroke';
import { ClipPathFunction } from '../clip-path-function';

View File

@@ -19,6 +19,7 @@ global.NSObject = class NSObject {
return new NSObject();
}
};
global.NSNumber = {};
global.NSString = {
stringWithString() {
return {
@@ -106,6 +107,15 @@ global.NativeScriptUtils = {
return {};
},
};
global.NSURLSessionConfiguration = {
defaultSessionConfiguration: function () {
return {};
},
ephemeralSessionConfiguration: function () {
return {};
},
};
global.NSURLSessionTaskDelegate = function () {};
global.NSOperationQueue = {
mainQueue: {
addOperationWithBlock(fn: Function) {
@@ -148,6 +158,15 @@ global.UIResponder = function () {};
global.UIResponder.extend = function () {};
global.UIViewController = function () {};
global.UIViewControllerTransitioningDelegate = function () {};
global.UINavigationControllerDelegate = function () {};
global.UINavigationController = function () {};
global.UIUserInterfaceIdiom = {
Phone: 0,
Pad: 1,
TV: 2,
CarPlay: 3,
Mac: 4,
};
global.UIGestureRecognizer = function () {};
global.UIGestureRecognizerDelegate = function () {};
global.UIAdaptivePresentationControllerDelegate = function () {};
@@ -164,6 +183,7 @@ global.UIContentSizeCategoryAccessibilityLarge = 2.5;
global.UIContentSizeCategoryAccessibilityExtraLarge = 3;
global.UIContentSizeCategoryAccessibilityExtraExtraLarge = 3.5;
global.UIContentSizeCategoryAccessibilityExtraExtraExtraLarge = 4;
global.UIViewControllerAnimatedTransitioning = function () {};
// global.UIDocumentInteractionController = {
// interactionControllerWithURL(url: any) {
// return null;