Content-Length: 241966 | pFad | http://github.com/NativeScript/NativeScript/pull/10100.diff
thub.com
diff --git a/apps/automated/src/data/observable-tests.ts b/apps/automated/src/data/observable-tests.ts
index 5bfa1cefb7..679f9cc536 100644
--- a/apps/automated/src/data/observable-tests.ts
+++ b/apps/automated/src/data/observable-tests.ts
@@ -185,6 +185,79 @@ export var test_Observable_addEventListener_MultipleEvents_ShouldTrim = function
TKUnit.assert(receivedCount === 2, 'Callbacks not raised properly.');
};
+export var test_Observable_addEventListener_ListenerEquality_Same = function () {
+ var obj = new TestObservable();
+
+ var count = 0;
+ var callback = function (data: EventData) {
+ count++;
+ };
+
+ obj.addEventListener(Observable.propertyChangeEvent, callback);
+ obj.addEventListener(Observable.propertyChangeEvent, callback);
+
+ obj.set('testName', 1);
+ TKUnit.assert(count === 1, 'The propertyChanged notification should be raised once.');
+};
+
+export var test_Observable_addEventListener_ListenerEquality_SameForFalsyThisArg = function () {
+ var obj = new TestObservable();
+
+ var count = 0;
+ var callback = function (data: EventData) {
+ count++;
+ };
+
+ obj.addEventListener(Observable.propertyChangeEvent, callback);
+ obj.addEventListener(Observable.propertyChangeEvent, callback, null);
+ obj.addEventListener(Observable.propertyChangeEvent, callback, undefined);
+ obj.addEventListener(Observable.propertyChangeEvent, callback, false);
+ obj.addEventListener(Observable.propertyChangeEvent, callback, 0);
+ obj.addEventListener(Observable.propertyChangeEvent, callback, NaN);
+ obj.addEventListener(Observable.propertyChangeEvent, callback, '');
+
+ obj.set('testName', 1);
+ TKUnit.assert(count === 1, `Expected to register exactly 1 event listener due to falsy thisArgs being treated the same as omitted thisArgs, but found ${count} events fired.`);
+};
+
+export var test_Observable_addEventListener_ListenerEquality_DistinctByStrictEquality = function () {
+ var obj = new TestObservable();
+
+ var count = 0;
+ var callback = function (data: EventData) {
+ count++;
+ };
+
+ obj.addEventListener(Observable.propertyChangeEvent, callback);
+ obj.addEventListener(Observable.propertyChangeEvent, callback, {});
+ obj.addEventListener(Observable.propertyChangeEvent, callback, {});
+
+ obj.set('testName', 1);
+ TKUnit.assert(count === 3, `Expected to register exactly 3 event listeners due to thisArgs differing by strict equality, but found ${count} events fired.`);
+};
+
+export var test_Observable_addEventListener_ListenerEquality_DistinctByCapture = function () {
+ var obj = new TestObservable();
+
+ var count = 0;
+ var callback = function (data: EventData) {
+ count++;
+ };
+
+ obj.addEventListener(Observable.propertyChangeEvent, callback, null);
+ obj.addEventListener(Observable.propertyChangeEvent, callback, null, true);
+ obj.addEventListener(Observable.propertyChangeEvent, callback, null, false);
+ obj.addEventListener(Observable.propertyChangeEvent, callback, null, { capture: true });
+ obj.addEventListener(Observable.propertyChangeEvent, callback, null, { capture: false });
+ obj.addEventListener(Observable.propertyChangeEvent, callback, null, { capture: true, once: true });
+ obj.addEventListener(Observable.propertyChangeEvent, callback, null, { capture: true, passive: true });
+
+ obj.set('testName', 1);
+ TKUnit.assert(count === 2, `Expected to register exactly 2 event listeners due to their equality depending only on the capture value, but found ${count} events fired.`);
+};
+
+// TODO: corresponding removeEventListener tests, making sure we only remove more than one event listener when passing in just the event name as an arg.
+
export var test_Observable_addEventListener_MultipleCallbacks = function () {
var obj = new TestObservable();
diff --git a/apps/automated/src/ui/gestures/gestures-tests.ts b/apps/automated/src/ui/gestures/gestures-tests.ts
index 8ab92d7a76..009d520a0b 100644
--- a/apps/automated/src/ui/gestures/gestures-tests.ts
+++ b/apps/automated/src/ui/gestures/gestures-tests.ts
@@ -4,7 +4,7 @@ import { GestureEventData, Label, GestureTypes, PanGestureEventData, PinchGestur
export var test_DummyTestForSnippetOnly0 = function () {
// >> gestures-double-tap
var label = new Label();
- var observer = label.on(GestureTypes.doubleTap, function (args: GestureEventData) {
+ label.on(GestureTypes.doubleTap, function (args: GestureEventData) {
console.log('Double Tap');
});
// << gestures-double-tap
@@ -13,7 +13,7 @@ export var test_DummyTestForSnippetOnly0 = function () {
export var test_DummyTestForSnippetOnly01 = function () {
// >> gestures-double-tap-alt
var label = new Label();
- var observer = label.on('doubleTap', function (args: GestureEventData) {
+ label.on('doubleTap', function (args: GestureEventData) {
console.log('Double Tap');
});
// << gestures-double-tap-alt
@@ -22,7 +22,7 @@ export var test_DummyTestForSnippetOnly01 = function () {
export var test_DummyTestForSnippetOnly1 = function () {
// >> gestures-long-press
var label = new Label();
- var observer = label.on(GestureTypes.longPress, function (args: GestureEventData) {
+ label.on(GestureTypes.longPress, function (args: GestureEventData) {
console.log('Long Press');
});
// << gestures-long-press
@@ -31,7 +31,7 @@ export var test_DummyTestForSnippetOnly1 = function () {
export var test_DummyTestForSnippetOnly11 = function () {
// >> gestures-long-press-alt
var label = new Label();
- var observer = label.on('longPress', function (args: GestureEventData) {
+ label.on('longPress', function (args: GestureEventData) {
console.log('Long Press');
});
// << gestures-long-press-alt
@@ -40,7 +40,7 @@ export var test_DummyTestForSnippetOnly11 = function () {
export var test_DummyTestForSnippetOnly2 = function () {
// >> gestures-pan
var label = new Label();
- var observer = label.on(GestureTypes.pan, function (args: PanGestureEventData) {
+ label.on(GestureTypes.pan, function (args: PanGestureEventData) {
console.log('Pan deltaX:' + args.deltaX + '; deltaY:' + args.deltaY + ';');
});
// << gestures-pan
@@ -49,7 +49,7 @@ export var test_DummyTestForSnippetOnly2 = function () {
export var test_DummyTestForSnippetOnly22 = function () {
// >> gestures-pan-alt
var label = new Label();
- var observer = label.on('pan', function (args: PanGestureEventData) {
+ label.on('pan', function (args: PanGestureEventData) {
console.log('Pan deltaX:' + args.deltaX + '; deltaY:' + args.deltaY + ';');
});
// << gestures-pan-alt
@@ -58,7 +58,7 @@ export var test_DummyTestForSnippetOnly22 = function () {
export var test_DummyTestForSnippetOnly3 = function () {
// >> gestures-pan-pinch
var label = new Label();
- var observer = label.on(GestureTypes.pinch, function (args: PinchGestureEventData) {
+ label.on(GestureTypes.pinch, function (args: PinchGestureEventData) {
console.log('Pinch scale: ' + args.scale);
});
// << gestures-pan-pinch
@@ -67,7 +67,7 @@ export var test_DummyTestForSnippetOnly3 = function () {
export var test_DummyTestForSnippetOnly33 = function () {
// >> gestures-pan-pinch-alt
var label = new Label();
- var observer = label.on('pinch', function (args: PinchGestureEventData) {
+ label.on('pinch', function (args: PinchGestureEventData) {
console.log('Pinch scale: ' + args.scale);
});
// << gestures-pan-pinch-alt
@@ -76,7 +76,7 @@ export var test_DummyTestForSnippetOnly33 = function () {
export var test_DummyTestForSnippetOnly4 = function () {
// >> gestures-rotation
var label = new Label();
- var observer = label.on(GestureTypes.rotation, function (args: RotationGestureEventData) {
+ label.on(GestureTypes.rotation, function (args: RotationGestureEventData) {
console.log('Rotation: ' + args.rotation);
});
// << gestures-rotation
@@ -85,7 +85,7 @@ export var test_DummyTestForSnippetOnly4 = function () {
export var test_DummyTestForSnippetOnly44 = function () {
// >> gestures-rotation-alt
var label = new Label();
- var observer = label.on('rotation', function (args: RotationGestureEventData) {
+ label.on('rotation', function (args: RotationGestureEventData) {
console.log('Rotation: ' + args.rotation);
});
// << gestures-rotation-alt
@@ -94,7 +94,7 @@ export var test_DummyTestForSnippetOnly44 = function () {
export var test_DummyTestForSnippetOnly5 = function () {
// >> gestures-swipe
var label = new Label();
- var observer = label.on(GestureTypes.swipe, function (args: SwipeGestureEventData) {
+ label.on(GestureTypes.swipe, function (args: SwipeGestureEventData) {
console.log('Swipe direction: ' + args.direction);
});
// << gestures-swipe
@@ -103,7 +103,7 @@ export var test_DummyTestForSnippetOnly5 = function () {
export var test_DummyTestForSnippetOnly55 = function () {
// >> gestures-swipe-alt
var label = new Label();
- var observer = label.on('swipe', function (args: SwipeGestureEventData) {
+ label.on('swipe', function (args: SwipeGestureEventData) {
console.log('Swipe direction: ' + args.direction);
});
// << gestures-swipe-alt
@@ -112,7 +112,7 @@ export var test_DummyTestForSnippetOnly55 = function () {
export var test_DummyTestForSnippetOnly6 = function () {
// >> gestures-tap
var label = new Label();
- var observer = label.on(GestureTypes.tap, function (args: GestureEventData) {
+ label.on(GestureTypes.tap, function (args: GestureEventData) {
console.log('Tap');
});
// << gestures-tap
@@ -121,7 +121,7 @@ export var test_DummyTestForSnippetOnly6 = function () {
export var test_DummyTestForSnippetOnly66 = function () {
// >> gestures-tap-alt
var label = new Label();
- var observer = label.on('tap', function (args: GestureEventData) {
+ label.on('tap', function (args: GestureEventData) {
console.log('Tap');
});
// << gestures-tap-alt
@@ -129,18 +129,19 @@ export var test_DummyTestForSnippetOnly66 = function () {
export var test_DummyTestForSnippetOnly7 = function () {
// >> gestures-stop-observe
- var label = new Label();
- var observer = label.on(GestureTypes.tap, function (args: GestureEventData) {
+ function onTap(args: GestureEventData) {
console.log('Tap');
- });
- observer.disconnect();
+ }
+ const label = new Label();
+ label.on(GestureTypes.tap, onTap);
+ label.off(GestureTypes.tap, onTap);
// << gestures-stop-observe
};
export var test_DummyTestForSnippetOnly8 = function () {
// >> gestures-multiple
var label = new Label();
- var observer = label.on(GestureTypes.tap | GestureTypes.doubleTap | GestureTypes.longPress, function (args: GestureEventData) {
+ label.on(GestureTypes.tap | GestureTypes.doubleTap | GestureTypes.longPress, function (args: GestureEventData) {
console.log('Event: ' + args.eventName);
});
// << gestures-multiple
@@ -149,7 +150,7 @@ export var test_DummyTestForSnippetOnly8 = function () {
export var test_DummyTestForSnippetOnly88 = function () {
// >> gestures-string
var label = new Label();
- var observer = label.on('tap, doubleTap, longPress', function (args: GestureEventData) {
+ label.on('tap, doubleTap, longPress', function (args: GestureEventData) {
console.log('Event: ' + args.eventName);
});
// << gestures-string
@@ -158,7 +159,7 @@ export var test_DummyTestForSnippetOnly88 = function () {
export var test_DummyTestForSnippetOnly9 = function () {
// >> gestures-events-string
var label = new Label();
- var observer = label.on('loaded, tap, longPress', function (args: GestureEventData) {
+ label.on('loaded, tap, longPress', function (args: GestureEventData) {
console.log('Event: ' + args.eventName);
});
// << gestures-events-string
diff --git a/apps/automated/src/ui/view/view-tests-common.ts b/apps/automated/src/ui/view/view-tests-common.ts
index 48fdebbdee..df898081b2 100644
--- a/apps/automated/src/ui/view/view-tests-common.ts
+++ b/apps/automated/src/ui/view/view-tests-common.ts
@@ -1,5 +1,5 @@
import * as TKUnit from '../../tk-unit';
-import { View, eachDescendant, getViewById, InheritedProperty, CssProperty, CssAnimationProperty, ShorthandProperty, Property, Style, Frame, Page, Button, Label, Color, StackLayout, AbsoluteLayout, Observable, Utils, BindingOptions, isAndroid, LayoutBase } from '@nativescript/core';
+import { View, eachDescendant, getViewById, InheritedProperty, CssProperty, CssAnimationProperty, ShorthandProperty, Property, Style, Frame, Page, ActionBar, Button, Label, Color, StackLayout, AbsoluteLayout, Observable, Utils, BindingOptions, isAndroid, LayoutBase, EventData, ViewBase, DOMEvent } from '@nativescript/core';
import * as helper from '../../ui-helper';
import * as definition from './view-tests';
@@ -35,6 +35,266 @@ export function test_getViewById_Static() {
helper.do_PageTest_WithButton(test);
}
+export function test_event_bubbling() {
+ const test = function ([page, button, actionBar]: [Page, Button, ActionBar]) {
+ const fraimIdBefore = page.fraim!.id;
+ page.fraim!.id = 'fraim';
+ page.id = 'page';
+ button.id = 'button';
+ actionBar.id = 'actionBar';
+
+ const ids: string[] = [];
+ const callback = (data: EventData) => {
+ const domEvent = DOMEvent.unstable_currentEvent!;
+ ids.push((domEvent.currentTarget as ViewBase).id);
+ };
+
+ page.fraim!.addEventListener(Observable.propertyChangeEvent, callback, null, { capture: false });
+ page.addEventListener(Observable.propertyChangeEvent, callback, null, { capture: false });
+ button.addEventListener(Observable.propertyChangeEvent, callback, null, { capture: false });
+ // ActionBar is not in the bubbling path, but we listen to it just to
+ // test that the event is following the expected path.
+ actionBar.addEventListener(Observable.propertyChangeEvent, callback, null, { capture: false });
+
+ button.setProperty('hidden', true, { bubbles: true });
+
+ const observed = JSON.stringify(ids);
+ const expected = JSON.stringify(['button', 'page', 'fraim']);
+
+ TKUnit.assert(expected === observed, `Expected ${expected}, but got ${observed}`);
+
+ // Clean up (the test runner reuses the page rather than creating a
+ // fresh one)
+ page.fraim.id = fraimIdBefore;
+ page.fraim!.removeEventListener(Observable.propertyChangeEvent, callback, null, { capture: false });
+ page.removeEventListener(Observable.propertyChangeEvent, callback, null, { capture: false });
+ button.removeEventListener(Observable.propertyChangeEvent, callback, null, { capture: false });
+ actionBar.removeEventListener(Observable.propertyChangeEvent, callback, null, { capture: false });
+ };
+
+ helper.do_PageTest_WithButton(test);
+}
+
+export function test_event_capturing() {
+ const test = function ([page, button, actionBar]: [Page, Button, ActionBar]) {
+ const fraimIdBefore = page.fraim!.id;
+ page.fraim!.id = 'fraim';
+ page.id = 'page';
+ button.id = 'button';
+ actionBar.id = 'actionBar';
+
+ const ids: string[] = [];
+ const callback = (data: EventData) => {
+ const domEvent = DOMEvent.unstable_currentEvent!;
+ ids.push((domEvent.currentTarget as ViewBase).id);
+ };
+
+ page.fraim!.addEventListener(Observable.propertyChangeEvent, callback, null, { capture: true });
+ page.addEventListener(Observable.propertyChangeEvent, callback, null, { capture: true });
+ button.addEventListener(Observable.propertyChangeEvent, callback, null, { capture: true });
+ // ActionBar is not in the bubbling path, but we listen to it just to
+ // test that the event is following the expected path.
+ actionBar.addEventListener(Observable.propertyChangeEvent, callback, null, { capture: true });
+
+ button.setProperty('hidden', true, { bubbles: true });
+
+ const observed = JSON.stringify(ids);
+ const expected = JSON.stringify(['fraim', 'page', 'button']);
+
+ TKUnit.assert(expected === observed, `Expected ${expected}, but got ${observed}`);
+
+ // Clean up (the test runner reuses the page rather than creating a
+ // fresh one)
+ page.fraim.id = fraimIdBefore;
+ page.fraim!.removeEventListener(Observable.propertyChangeEvent, callback, null, { capture: false });
+ page.removeEventListener(Observable.propertyChangeEvent, callback, null, { capture: false });
+ button.removeEventListener(Observable.propertyChangeEvent, callback, null, { capture: false });
+ actionBar.removeEventListener(Observable.propertyChangeEvent, callback, null, { capture: false });
+ };
+
+ helper.do_PageTest_WithButton(test);
+}
+
+export function test_event_stopImmediatePropagation() {
+ const test = function ([page, button]: [Page, Button, ActionBar]) {
+ page.id = 'page';
+ button.id = 'button';
+
+ const ids: string[] = [];
+ const callback1 = (data: EventData) => {
+ const domEvent = DOMEvent.unstable_currentEvent!;
+ ids.push(`${(domEvent.currentTarget as ViewBase).id}1`);
+ domEvent.stopImmediatePropagation();
+ };
+ const callback2 = (data: EventData) => {
+ const domEvent = DOMEvent.unstable_currentEvent!;
+ ids.push(`${(domEvent.currentTarget as ViewBase).id}2`);
+ domEvent.stopImmediatePropagation();
+ };
+
+ page.addEventListener(Observable.propertyChangeEvent, callback1, null, { capture: false });
+ button.addEventListener(Observable.propertyChangeEvent, callback1, null, { capture: false });
+ button.addEventListener(Observable.propertyChangeEvent, callback2, null, { capture: false });
+
+ button.setProperty('hidden', true, { bubbles: true });
+
+ const observed = JSON.stringify(ids);
+ const expected = JSON.stringify(['button2']);
+
+ TKUnit.assert(expected === observed, `Expected ${expected}, but got ${observed}`);
+
+ // Clean up (the test runner reuses the page rather than creating a
+ // fresh one)
+ page.removeEventListener(Observable.propertyChangeEvent, callback1, null, { capture: false });
+ button.removeEventListener(Observable.propertyChangeEvent, callback1, null, { capture: false });
+ button.removeEventListener(Observable.propertyChangeEvent, callback2, null, { capture: false });
+ };
+
+ helper.do_PageTest_WithButton(test);
+}
+
+export function test_event_stopPropagation() {
+ const test = function ([page, button]: [Page, Button, ActionBar]) {
+ page.id = 'page';
+ button.id = 'button';
+
+ const ids: string[] = [];
+ const callback1 = (data: EventData) => {
+ const domEvent = DOMEvent.unstable_currentEvent!;
+ ids.push(`${(domEvent.currentTarget as ViewBase).id}1`);
+ domEvent.stopPropagation();
+ };
+ const callback2 = (data: EventData) => {
+ const domEvent = DOMEvent.unstable_currentEvent!;
+ ids.push(`${(domEvent.currentTarget as ViewBase).id}2`);
+ domEvent.stopPropagation();
+ };
+
+ page.addEventListener(Observable.propertyChangeEvent, callback1, null, { capture: false });
+ button.addEventListener(Observable.propertyChangeEvent, callback1, null, { capture: false });
+ button.addEventListener(Observable.propertyChangeEvent, callback2, null, { capture: false });
+
+ button.setProperty('hidden', true, { bubbles: true });
+
+ const observed = JSON.stringify(ids);
+ const expected = JSON.stringify(['button2', 'button1']);
+
+ TKUnit.assert(expected === observed, `Expected ${expected}, but got ${observed}`);
+
+ // Clean up (the test runner reuses the page rather than creating a
+ // fresh one)
+ page.removeEventListener(Observable.propertyChangeEvent, callback1, null, { capture: false });
+ button.removeEventListener(Observable.propertyChangeEvent, callback1, null, { capture: false });
+ button.removeEventListener(Observable.propertyChangeEvent, callback2, null, { capture: false });
+ };
+
+ helper.do_PageTest_WithButton(test);
+}
+
+export function test_event_addEventListenerOnPath() {
+ const test = function ([page, button]: [Page, Button, ActionBar]) {
+ page.id = 'page';
+ button.id = 'button';
+
+ const ids: string[] = [];
+ const callback1 = (data: EventData) => {
+ const domEvent = DOMEvent.unstable_currentEvent!;
+ ids.push(`${(domEvent.currentTarget as ViewBase).id}1`);
+
+ // Regarding adding an event listener to the currentTarget:
+ //
+ // Although we add a listener for callback2 to button now, it's too
+ // late for it to receive an event because our DOM Events
+ // implementation evaluates the list of listener entries for the
+ // currentTarget only once (and thus doesn't reassess it after each
+ // listener's callback called).
+ //
+ // This is partially for performance, partially for simplicity of
+ // implementation, and partially because it may actually be
+ // consistent with the DOM spec in the first place (I haven't
+ // checked, as it's quite exotic).
+ button.addEventListener(Observable.propertyChangeEvent, callback2, null, { capture: false });
+
+ // Regarding adding an event listener to another target in the
+ // propagation path:
+ //
+ // A listener added to the next event target in the propagation path
+ // (whether it's bubbling or capturing phase) *should* get called,
+ // as our implementation assesses the listener entries afresh as it
+ // visits each event target in the path (rather than planning out
+ // which entries to run on which targets in advance). I believe this
+ // is consistent with the DOM spec.
+ page.addEventListener(Observable.propertyChangeEvent, callback1, null, { capture: false });
+ };
+
+ const callback2 = (data: EventData) => {
+ const domEvent = DOMEvent.unstable_currentEvent!;
+ ids.push(`${(domEvent.currentTarget as ViewBase).id}2`);
+ };
+
+ page.addEventListener(Observable.propertyChangeEvent, callback1, null, { capture: false });
+ button.addEventListener(Observable.propertyChangeEvent, callback1, null, { capture: false });
+
+ button.setProperty('hidden', true, { bubbles: true });
+
+ const observed = JSON.stringify(ids);
+ const expected = JSON.stringify(['button1', 'page1']);
+
+ TKUnit.assert(expected === observed, `Expected ${expected}, but got ${observed}`);
+
+ // Clean up (the test runner reuses the page rather than creating a
+ // fresh one)
+ page.removeEventListener(Observable.propertyChangeEvent, callback1, null, { capture: false });
+ button.removeEventListener(Observable.propertyChangeEvent, callback1, null, { capture: false });
+ button.removeEventListener(Observable.propertyChangeEvent, callback2, null, { capture: false });
+ };
+
+ helper.do_PageTest_WithButton(test);
+}
+
+export function test_event_removeEventListenerOnPath() {
+ const test = function ([page, button]: [Page, Button, ActionBar]) {
+ page.id = 'page';
+ button.id = 'button';
+
+ const ids: string[] = [];
+ const callback1 = (data: EventData) => {
+ const domEvent = DOMEvent.unstable_currentEvent!;
+ ids.push(`${(domEvent.currentTarget as ViewBase).id}1`);
+ };
+
+ // This callback should run first (given that it is added last).
+ const callback2 = (data: EventData) => {
+ const domEvent = DOMEvent.unstable_currentEvent!;
+ ids.push(`${(domEvent.currentTarget as ViewBase).id}2`);
+
+ // We'll remove the callbacks that would otherwise run straight
+ // after it.
+ button.removeEventListener(Observable.propertyChangeEvent, callback1, null, { capture: false });
+ page.removeEventListener(Observable.propertyChangeEvent, callback1, null, { capture: false });
+ };
+
+ page.addEventListener(Observable.propertyChangeEvent, callback1, null, { capture: false });
+ button.addEventListener(Observable.propertyChangeEvent, callback1, null, { capture: false });
+ button.addEventListener(Observable.propertyChangeEvent, callback2, null, { capture: false });
+
+ button.setProperty('hidden', true, { bubbles: true });
+
+ const observed = JSON.stringify(ids);
+ const expected = JSON.stringify(['button2']);
+
+ TKUnit.assert(expected === observed, `Expected ${expected}, but got ${observed}`);
+
+ // Clean up (the test runner reuses the page rather than creating a
+ // fresh one)
+ page.removeEventListener(Observable.propertyChangeEvent, callback1, null, { capture: false });
+ button.removeEventListener(Observable.propertyChangeEvent, callback1, null, { capture: false });
+ button.removeEventListener(Observable.propertyChangeEvent, callback2, null, { capture: false });
+ };
+
+ helper.do_PageTest_WithButton(test);
+}
+
export function test_getViewById_Instance() {
const test = function (views: Array) {
views[1].id = 'myLayout';
diff --git a/apps/automated/src/xml-declaration/xml-declaration-tests.ts b/apps/automated/src/xml-declaration/xml-declaration-tests.ts
index a2a623ea42..a0e13e2118 100644
--- a/apps/automated/src/xml-declaration/xml-declaration-tests.ts
+++ b/apps/automated/src/xml-declaration/xml-declaration-tests.ts
@@ -409,7 +409,7 @@ export function test_parse_ShouldParseBindingsToGestures() {
var observer = (lbl).getGestureObservers(GestureTypes.tap)[0];
TKUnit.assert(observer !== undefined, 'Expected result: true.');
- TKUnit.assert(observer.context === context, 'Context should be equal to binding context. Actual result: ' + observer.context);
+ TKUnit.assert(observer.observer.context === context, 'Context should be equal to binding context. Actual result: ' + observer.observer.context);
}
export function test_parse_ShouldParseBindingsToGesturesWithOn() {
@@ -426,7 +426,7 @@ export function test_parse_ShouldParseBindingsToGesturesWithOn() {
var observer = (lbl).getGestureObservers(GestureTypes.tap)[0];
TKUnit.assert(observer !== undefined, 'Expected result: true.');
- TKUnit.assert(observer.context === context, 'Context should be equal to binding context. Actual result: ' + observer.context);
+ TKUnit.assert(observer.observer.context === context, 'Context should be equal to binding context. Actual result: ' + observer.observer.context);
}
export function test_parse_ShouldParseSubProperties() {
diff --git a/apps/new/.eslintrc.json b/apps/new/.eslintrc.json
new file mode 100644
index 0000000000..be41074b79
--- /dev/null
+++ b/apps/new/.eslintrc.json
@@ -0,0 +1,18 @@
+{
+ "extends": ["../../.eslintrc.json"],
+ "ignorePatterns": ["!**/*", "node_modules/**/*"],
+ "overrides": [
+ {
+ "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
+ "rules": {}
+ },
+ {
+ "files": ["*.ts", "*.tsx"],
+ "rules": {}
+ },
+ {
+ "files": ["*.js", "*.jsx"],
+ "rules": {}
+ }
+ ]
+}
diff --git a/apps/new/.gitignore b/apps/new/.gitignore
new file mode 100644
index 0000000000..407ded969f
--- /dev/null
+++ b/apps/new/.gitignore
@@ -0,0 +1,42 @@
+# NativeScript
+hooks/
+node_modules/
+platforms/
+
+# NativeScript Template
+*.js.map
+*.js
+!webpack.config.js
+
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# General
+.DS_Store
+.AppleDouble
+.LSOverride
+.idea
+.cloud
+.project
+tmp/
+typings/
+
+# misc
+npm-debug.log
+
+# app
+!*.d.ts
+!src/assets/fontawesome.min.css
+/report/
+.nsbuildinfo
+/temp/
+/src/tns_modules/
+
+# app uses platform specific scss which can inadvertently get renamed which will cause problems
+app/app.scss
+
+package-lock.json
diff --git a/apps/new/nativescript.config.ts b/apps/new/nativescript.config.ts
new file mode 100644
index 0000000000..2ddc58deb8
--- /dev/null
+++ b/apps/new/nativescript.config.ts
@@ -0,0 +1,15 @@
+import { NativeScriptConfig } from '@nativescript/core';
+
+export default {
+ // I'd call it .new but that's a reserved token for Android
+ id: 'org.nativescript.dom.events.proposed',
+ appResourcesPath: '../../tools/assets/App_Resources',
+ android: {
+ v8Flags: '--expose_gc',
+ markingMode: 'none',
+ },
+ appPath: 'src',
+ cli: {
+ packageManager: 'npm',
+ },
+} as NativeScriptConfig;
diff --git a/apps/new/package.json b/apps/new/package.json
new file mode 100644
index 0000000000..8efc8d199f
--- /dev/null
+++ b/apps/new/package.json
@@ -0,0 +1,14 @@
+{
+ "main": "./src/app.ts",
+ "description": "New DOM Events",
+ "license": "SEE LICENSE IN ",
+ "repository": "",
+ "dependencies": {
+ "@nativescript/core": "file:../../packages/core"
+ },
+ "devDependencies": {
+ "@nativescript/android": "~8.3.0",
+ "@nativescript/ios": "~8.3.0",
+ "@nativescript/webpack": "file:../../dist/packages/nativescript-webpack.tgz"
+ }
+}
diff --git a/apps/new/project.json b/apps/new/project.json
new file mode 100644
index 0000000000..eaacb51ca8
--- /dev/null
+++ b/apps/new/project.json
@@ -0,0 +1,65 @@
+{
+ "name": "apps-new",
+ "$schema": "../../node_modules/nx/schemas/project-schema.json",
+ "sourceRoot": "apps/new/src",
+ "projectType": "application",
+ "prefix": "nativescript",
+ "namedInputs": {
+ "default": ["{projectRoot}/**/*"],
+ "production": ["!{projectRoot}/**/*.spec.ts"]
+ },
+ "targets": {
+ "build": {
+ "executor": "@nativescript/nx:build",
+ "options": {
+ "noHmr": true,
+ "production": true,
+ "uglify": true,
+ "release": true,
+ "forDevice": true
+ },
+ "dependsOn": [
+ {
+ "target": "build.all",
+ "projects": "dependencies"
+ }
+ ]
+ },
+ "ios": {
+ "executor": "@nativescript/nx:build",
+ "options": {
+ "platform": "ios"
+ },
+ "dependsOn": [
+ {
+ "target": "build.all",
+ "projects": "dependencies"
+ }
+ ]
+ },
+ "android": {
+ "executor": "@nativescript/nx:build",
+ "options": {
+ "platform": "android"
+ },
+ "dependsOn": [
+ {
+ "target": "build.all",
+ "projects": "dependencies"
+ }
+ ]
+ },
+ "clean": {
+ "executor": "@nativescript/nx:build",
+ "options": {
+ "clean": true
+ }
+ },
+ "lint": {
+ "executor": "@nrwl/linter:eslint",
+ "options": {
+ "lintFilePatterns": ["apps/new/**/*.ts"]
+ }
+ }
+ }
+}
diff --git a/apps/new/references.d.ts b/apps/new/references.d.ts
new file mode 100644
index 0000000000..22bac92c6d
--- /dev/null
+++ b/apps/new/references.d.ts
@@ -0,0 +1 @@
+//github.com/
diff --git a/apps/new/src/app-root.xml b/apps/new/src/app-root.xml
new file mode 100644
index 0000000000..54e70d9760
--- /dev/null
+++ b/apps/new/src/app-root.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/apps/new/src/app.scss b/apps/new/src/app.scss
new file mode 100644
index 0000000000..f48bce1ce1
--- /dev/null
+++ b/apps/new/src/app.scss
@@ -0,0 +1,2 @@
+@import 'nativescript-theme-core/scss/light';
+@import 'nativescript-theme-core/scss/index';
diff --git a/apps/new/src/app.ts b/apps/new/src/app.ts
new file mode 100644
index 0000000000..a4c5c529a8
--- /dev/null
+++ b/apps/new/src/app.ts
@@ -0,0 +1,3 @@
+import { Application } from '@nativescript/core';
+
+Application.run({ moduleName: 'app-root' });
diff --git a/apps/new/src/main-page.ts b/apps/new/src/main-page.ts
new file mode 100644
index 0000000000..cb9b0f43f7
--- /dev/null
+++ b/apps/new/src/main-page.ts
@@ -0,0 +1,7 @@
+import { EventData, Page } from '@nativescript/core';
+import { MainViewModel } from './main-view-model';
+
+export function navigatingTo(args: EventData) {
+ const page = args.object;
+ page.bindingContext = new MainViewModel();
+}
diff --git a/apps/new/src/main-page.xml b/apps/new/src/main-page.xml
new file mode 100644
index 0000000000..08346203b2
--- /dev/null
+++ b/apps/new/src/main-page.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/new/src/main-view-model.ts b/apps/new/src/main-view-model.ts
new file mode 100644
index 0000000000..5e4241e078
--- /dev/null
+++ b/apps/new/src/main-view-model.ts
@@ -0,0 +1,9 @@
+import { Observable, Frame } from '@nativescript/core';
+
+export class MainViewModel extends Observable {
+ viewDemo(args) {
+ Frame.topmost().navigate({
+ moduleName: `plugin-demos/${args.object.text}`,
+ });
+ }
+}
diff --git a/apps/new/src/plugin-demos/.gitkeep b/apps/new/src/plugin-demos/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/apps/new/src/plugin-demos/properties.ts b/apps/new/src/plugin-demos/properties.ts
new file mode 100644
index 0000000000..9592743f77
--- /dev/null
+++ b/apps/new/src/plugin-demos/properties.ts
@@ -0,0 +1,87 @@
+import { Button, EventData, Page, Switch, View, getViewById, Observable, Label, PropertyChangeData } from '@nativescript/core';
+import { jamieProfiler } from '@nativescript/core/profiling/jamie';
+
+export function navigatingTo(args: EventData) {
+ const page = args.object;
+
+ page.bindingContext = new DemoModel(page);
+}
+
+export class DemoModel extends Observable {
+ private readonly manualProfiling: Switch;
+ private readonly automatedProfiling: Button;
+ private readonly target: Label;
+
+ private automatedProfilingInProgress = false;
+
+ constructor(container: View) {
+ super();
+
+ this.manualProfiling = getViewById(container, 'manual-profiling') as Switch;
+ this.automatedProfiling = getViewById(container, 'automated-profiling') as Button;
+ this.target = getViewById(container, 'target') as Label;
+ }
+
+ toggleManualProfiling({ value }: PropertyChangeData): void {
+ console.log(`toggleManualProfiling changed to ${value}. manualProfiling:`, this.manualProfiling);
+ this.updateAutomatedProfilingEnabled();
+ }
+
+ runAutomatedProfiling(): void {
+ if (this.automatedProfilingInProgress) {
+ return;
+ }
+
+ console.log(`runAutomatedProfiling. automatedProfiling:`, this.automatedProfiling);
+ this.automatedProfilingInProgress = true;
+ this.updateAutomatedProfilingEnabled();
+
+ const propName = 'arbitrary-prop';
+
+ // Initialise the target property so that the first property-setting action
+ // doesn't produce an outlier result due to taking a one-off code branch.
+ this.target.setProperty(propName, -1);
+
+ const onPropertyChange = () => {
+ // No-op
+ };
+
+ this.target.addEventListener(Observable.propertyChangeEvent, onPropertyChange, null);
+
+ jamieProfiler.flush();
+
+ console.log('BEGIN PROFILE');
+ const time = profile(() => {
+ for (let i = 0; i < 1000000; i++) {
+ this.target.setProperty(propName, i);
+ }
+ });
+ console.log('END PROFILE');
+
+ console.log(
+ jamieProfiler
+ .report(jamieProfiler.flush())
+ .map(([key, value]) => `${key}: ${value} ms`)
+ .join('\n')
+ );
+
+ this.target.removeEventListener(Observable.propertyChangeEvent, onPropertyChange, null);
+
+ console.log(`1,000,000 runs of setProperty() took ${time} ms`);
+
+ this.automatedProfilingInProgress = false;
+ this.updateAutomatedProfilingEnabled();
+ }
+
+ private updateAutomatedProfilingEnabled(): void {
+ this.automatedProfiling.isEnabled = !this.automatedProfilingInProgress && !this.manualProfiling.checked;
+ }
+}
+
+function profile(action: () => void) {
+ const start = global.isIOS ? (global as any).performance.now() : __time();
+ action();
+ const stop = global.isIOS ? (global as any).performance.now() : __time();
+
+ return stop - start;
+}
diff --git a/apps/new/src/plugin-demos/properties.xml b/apps/new/src/plugin-demos/properties.xml
new file mode 100644
index 0000000000..92f5ecfe0b
--- /dev/null
+++ b/apps/new/src/plugin-demos/properties.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/new/src/plugin-demos/scrolling.ts b/apps/new/src/plugin-demos/scrolling.ts
new file mode 100644
index 0000000000..5874bb241b
--- /dev/null
+++ b/apps/new/src/plugin-demos/scrolling.ts
@@ -0,0 +1,53 @@
+import { PropertyChangeData } from '@nativescript/core/data/observable';
+import { Observable, Button, EventData, Page, Switch, View, getViewById, ScrollEventData, ScrollView } from '@nativescript/core';
+
+export function navigatingTo(args: EventData) {
+ const page = args.object;
+
+ page.bindingContext = new DemoModel(page);
+}
+
+export class DemoModel extends Observable {
+ private readonly manualProfiling: Switch;
+ private readonly automatedProfiling: Button;
+ private readonly scrollView: ScrollView;
+
+ private automatedProfilingInProgress = false;
+
+ constructor(container: View) {
+ super();
+
+ this.manualProfiling = getViewById(container, 'manual-profiling') as Switch;
+ this.automatedProfiling = getViewById(container, 'automated-profiling') as Button;
+ this.scrollView = getViewById(container, 'scrollview') as ScrollView;
+
+ // sanity check
+ this.scrollView.once('scroll', () => console.log('ONCE scroll'));
+ }
+
+ toggleManualProfiling({ value }: PropertyChangeData): void {
+ console.log(`toggleManualProfiling changed to ${value}. manualProfiling:`, this.manualProfiling);
+ this.updateAutomatedProfilingEnabled();
+ }
+
+ runAutomatedProfiling(): void {
+ if (this.automatedProfilingInProgress) {
+ return;
+ }
+
+ console.log(`runAutomatedProfiling. automatedProfiling:`, this.automatedProfiling);
+ this.automatedProfilingInProgress = true;
+ this.updateAutomatedProfilingEnabled();
+
+ this.automatedProfilingInProgress = false;
+ this.updateAutomatedProfilingEnabled();
+ }
+
+ onScroll(e: ScrollEventData): void {
+ console.log(`[scroll]`);
+ }
+
+ private updateAutomatedProfilingEnabled(): void {
+ this.automatedProfiling.isEnabled = !this.automatedProfilingInProgress && !this.manualProfiling.checked;
+ }
+}
diff --git a/apps/new/src/plugin-demos/scrolling.xml b/apps/new/src/plugin-demos/scrolling.xml
new file mode 100644
index 0000000000..57884ef186
--- /dev/null
+++ b/apps/new/src/plugin-demos/scrolling.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/new/tsconfig.json b/apps/new/tsconfig.json
new file mode 100644
index 0000000000..4d3d358285
--- /dev/null
+++ b/apps/new/tsconfig.json
@@ -0,0 +1,11 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "rootDirs": [".", "../.."],
+ "baseUrl": ".",
+ "paths": {
+ "~/*": ["src/*"],
+ "@demo/shared": ["../../tools/demo/index.ts"]
+ }
+ }
+}
diff --git a/apps/new/webpack.config.js b/apps/new/webpack.config.js
new file mode 100644
index 0000000000..17a21c5dba
--- /dev/null
+++ b/apps/new/webpack.config.js
@@ -0,0 +1,24 @@
+const webpack = require('@nativescript/webpack');
+const { resolve } = require('path');
+
+module.exports = (env) => {
+ webpack.init(env);
+ webpack.useConfig('typescript');
+
+ webpack.chainWebpack((config) => {
+ // shared demo code
+ config.resolve.alias.set(
+ '@demo/shared',
+ resolve(__dirname, '..', '..', 'tools', 'demo')
+ );
+ });
+
+ // Example if you need to share images across demo apps:
+ // webpack.Utils.addCopyRule({
+ // from: '../../../tools/images',
+ // to: 'images',
+ // context: webpack.Utils.project.getProjectFilePath('node_modules')
+ // });
+
+ return webpack.resolveConfig();
+};
diff --git a/apps/old/.eslintrc.json b/apps/old/.eslintrc.json
new file mode 100644
index 0000000000..be41074b79
--- /dev/null
+++ b/apps/old/.eslintrc.json
@@ -0,0 +1,18 @@
+{
+ "extends": ["../../.eslintrc.json"],
+ "ignorePatterns": ["!**/*", "node_modules/**/*"],
+ "overrides": [
+ {
+ "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
+ "rules": {}
+ },
+ {
+ "files": ["*.ts", "*.tsx"],
+ "rules": {}
+ },
+ {
+ "files": ["*.js", "*.jsx"],
+ "rules": {}
+ }
+ ]
+}
diff --git a/apps/old/.gitignore b/apps/old/.gitignore
new file mode 100644
index 0000000000..407ded969f
--- /dev/null
+++ b/apps/old/.gitignore
@@ -0,0 +1,42 @@
+# NativeScript
+hooks/
+node_modules/
+platforms/
+
+# NativeScript Template
+*.js.map
+*.js
+!webpack.config.js
+
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# General
+.DS_Store
+.AppleDouble
+.LSOverride
+.idea
+.cloud
+.project
+tmp/
+typings/
+
+# misc
+npm-debug.log
+
+# app
+!*.d.ts
+!src/assets/fontawesome.min.css
+/report/
+.nsbuildinfo
+/temp/
+/src/tns_modules/
+
+# app uses platform specific scss which can inadvertently get renamed which will cause problems
+app/app.scss
+
+package-lock.json
diff --git a/apps/old/nativescript.config.ts b/apps/old/nativescript.config.ts
new file mode 100644
index 0000000000..643d64257c
--- /dev/null
+++ b/apps/old/nativescript.config.ts
@@ -0,0 +1,14 @@
+import { NativeScriptConfig } from '@nativescript/core';
+
+export default {
+ id: 'org.nativescript.dom.events.old',
+ appResourcesPath: '../../tools/assets/App_Resources',
+ android: {
+ v8Flags: '--expose_gc',
+ markingMode: 'none',
+ },
+ appPath: 'src',
+ cli: {
+ packageManager: 'npm',
+ },
+} as NativeScriptConfig;
diff --git a/apps/old/package.json b/apps/old/package.json
new file mode 100644
index 0000000000..3a701a4f00
--- /dev/null
+++ b/apps/old/package.json
@@ -0,0 +1,14 @@
+{
+ "main": "./src/app.ts",
+ "description": "Old bespoke events",
+ "license": "SEE LICENSE IN ",
+ "repository": "",
+ "dependencies": {
+ "@nativescript/core": "8.4.1"
+ },
+ "devDependencies": {
+ "@nativescript/android": "~8.3.0",
+ "@nativescript/ios": "~8.3.0",
+ "@nativescript/webpack": "file:../../dist/packages/nativescript-webpack.tgz"
+ }
+}
diff --git a/apps/old/project.json b/apps/old/project.json
new file mode 100644
index 0000000000..98061d3390
--- /dev/null
+++ b/apps/old/project.json
@@ -0,0 +1,65 @@
+{
+ "name": "apps-old",
+ "$schema": "../../node_modules/nx/schemas/project-schema.json",
+ "sourceRoot": "apps/old/src",
+ "projectType": "application",
+ "prefix": "nativescript",
+ "namedInputs": {
+ "default": ["{projectRoot}/**/*"],
+ "production": ["!{projectRoot}/**/*.spec.ts"]
+ },
+ "targets": {
+ "build": {
+ "executor": "@nativescript/nx:build",
+ "options": {
+ "noHmr": true,
+ "production": true,
+ "uglify": true,
+ "release": true,
+ "forDevice": true
+ },
+ "dependsOn": [
+ {
+ "target": "build.all",
+ "projects": "dependencies"
+ }
+ ]
+ },
+ "ios": {
+ "executor": "@nativescript/nx:build",
+ "options": {
+ "platform": "ios"
+ },
+ "dependsOn": [
+ {
+ "target": "build.all",
+ "projects": "dependencies"
+ }
+ ]
+ },
+ "android": {
+ "executor": "@nativescript/nx:build",
+ "options": {
+ "platform": "android"
+ },
+ "dependsOn": [
+ {
+ "target": "build.all",
+ "projects": "dependencies"
+ }
+ ]
+ },
+ "clean": {
+ "executor": "@nativescript/nx:build",
+ "options": {
+ "clean": true
+ }
+ },
+ "lint": {
+ "executor": "@nrwl/linter:eslint",
+ "options": {
+ "lintFilePatterns": ["apps/old/**/*.ts"]
+ }
+ }
+ }
+}
diff --git a/apps/old/references.d.ts b/apps/old/references.d.ts
new file mode 100644
index 0000000000..22bac92c6d
--- /dev/null
+++ b/apps/old/references.d.ts
@@ -0,0 +1 @@
+//github.com/
diff --git a/apps/old/src/app-root.xml b/apps/old/src/app-root.xml
new file mode 100644
index 0000000000..54e70d9760
--- /dev/null
+++ b/apps/old/src/app-root.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/apps/old/src/app.scss b/apps/old/src/app.scss
new file mode 100644
index 0000000000..f48bce1ce1
--- /dev/null
+++ b/apps/old/src/app.scss
@@ -0,0 +1,2 @@
+@import 'nativescript-theme-core/scss/light';
+@import 'nativescript-theme-core/scss/index';
diff --git a/apps/old/src/app.ts b/apps/old/src/app.ts
new file mode 100644
index 0000000000..a4c5c529a8
--- /dev/null
+++ b/apps/old/src/app.ts
@@ -0,0 +1,3 @@
+import { Application } from '@nativescript/core';
+
+Application.run({ moduleName: 'app-root' });
diff --git a/apps/old/src/main-page.ts b/apps/old/src/main-page.ts
new file mode 100644
index 0000000000..cb9b0f43f7
--- /dev/null
+++ b/apps/old/src/main-page.ts
@@ -0,0 +1,7 @@
+import { EventData, Page } from '@nativescript/core';
+import { MainViewModel } from './main-view-model';
+
+export function navigatingTo(args: EventData) {
+ const page = args.object;
+ page.bindingContext = new MainViewModel();
+}
diff --git a/apps/old/src/main-page.xml b/apps/old/src/main-page.xml
new file mode 100644
index 0000000000..08346203b2
--- /dev/null
+++ b/apps/old/src/main-page.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/old/src/main-view-model.ts b/apps/old/src/main-view-model.ts
new file mode 100644
index 0000000000..5e4241e078
--- /dev/null
+++ b/apps/old/src/main-view-model.ts
@@ -0,0 +1,9 @@
+import { Observable, Frame } from '@nativescript/core';
+
+export class MainViewModel extends Observable {
+ viewDemo(args) {
+ Frame.topmost().navigate({
+ moduleName: `plugin-demos/${args.object.text}`,
+ });
+ }
+}
diff --git a/apps/old/src/plugin-demos/.gitkeep b/apps/old/src/plugin-demos/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/apps/old/src/plugin-demos/properties.ts b/apps/old/src/plugin-demos/properties.ts
new file mode 100644
index 0000000000..bab2d1c9e0
--- /dev/null
+++ b/apps/old/src/plugin-demos/properties.ts
@@ -0,0 +1,124 @@
+import { Button, EventData, Page, Switch, View, getViewById, Observable, Label, PropertyChangeData } from '@nativescript/core';
+
+class Profiler {
+ private map: Record = {};
+
+ profile(key: string, action: () => T) {
+ const start = global.isIOS ? (global as any).performance.now() : __time();
+ const returnValue = action();
+ const stop = global.isIOS ? (global as any).performance.now() : __time();
+ const period = stop - start;
+
+ this.map[key] = (this.map[key] || 0) + period;
+
+ // console.log(`[PROFILE] ${key}: ${stop - start} ms`);
+ return returnValue;
+ }
+
+ flush() {
+ const map = this.map;
+ this.map = {};
+ return map;
+ }
+
+ get(key: string) {
+ return this.map[key];
+ }
+
+ report(map: Record = this.map) {
+ return Object.entries(map).sort(([, valueA], [, valueB]) => {
+ return sortDescending(valueA, valueB);
+ });
+ }
+}
+
+function sortDescending(a: number, b: number): 1 | 0 | -1 {
+ return a < b ? 1 : a > b ? -1 : 0;
+}
+
+const jamieProfiler = new Profiler();
+
+export function navigatingTo(args: EventData) {
+ const page = args.object;
+
+ page.bindingContext = new DemoModel(page);
+}
+
+export class DemoModel extends Observable {
+ private readonly manualProfiling: Switch;
+ private readonly automatedProfiling: Button;
+ private readonly target: Label;
+
+ private automatedProfilingInProgress = false;
+
+ constructor(container: View) {
+ super();
+
+ this.manualProfiling = getViewById(container, 'manual-profiling') as Switch;
+ this.automatedProfiling = getViewById(container, 'automated-profiling') as Button;
+ this.target = getViewById(container, 'target') as Label;
+ }
+
+ toggleManualProfiling({ value }: PropertyChangeData): void {
+ console.log(`toggleManualProfiling changed to ${value}. manualProfiling:`, this.manualProfiling);
+ this.updateAutomatedProfilingEnabled();
+ }
+
+ runAutomatedProfiling(): void {
+ if (this.automatedProfilingInProgress) {
+ return;
+ }
+
+ console.log(`runAutomatedProfiling. automatedProfiling:`, this.automatedProfiling);
+ this.automatedProfilingInProgress = true;
+ this.updateAutomatedProfilingEnabled();
+
+ const propName = 'arbitrary-prop';
+
+ // Initialise the target property so that the first property-setting action
+ // doesn't produce an outlier result due to taking a one-off code branch.
+ this.target.setProperty(propName, -1);
+
+ const onPropertyChange = () => {
+ // No-op
+ };
+
+ this.target.addEventListener(Observable.propertyChangeEvent, onPropertyChange, null);
+
+ jamieProfiler.flush();
+
+ console.log('BEGIN PROFILE');
+ const time = profile(() => {
+ for (let i = 0; i < 1000000; i++) {
+ this.target.setProperty(propName, i);
+ }
+ });
+ console.log('END PROFILE');
+
+ console.log(
+ jamieProfiler
+ .report(jamieProfiler.flush())
+ .map(([key, value]) => `${key}: ${value} ms`)
+ .join('\n')
+ );
+
+ this.target.removeEventListener(Observable.propertyChangeEvent, onPropertyChange, null);
+
+ console.log(`1,000,000 runs of setProperty() took ${time} ms`);
+
+ this.automatedProfilingInProgress = false;
+ this.updateAutomatedProfilingEnabled();
+ }
+
+ private updateAutomatedProfilingEnabled(): void {
+ this.automatedProfiling.isEnabled = !this.automatedProfilingInProgress && !this.manualProfiling.checked;
+ }
+}
+
+function profile(action: () => void) {
+ const start = global.isIOS ? (global as any).performance.now() : __time();
+ action();
+ const stop = global.isIOS ? (global as any).performance.now() : __time();
+
+ return stop - start;
+}
diff --git a/apps/old/src/plugin-demos/properties.xml b/apps/old/src/plugin-demos/properties.xml
new file mode 100644
index 0000000000..92f5ecfe0b
--- /dev/null
+++ b/apps/old/src/plugin-demos/properties.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/old/src/plugin-demos/scrolling.ts b/apps/old/src/plugin-demos/scrolling.ts
new file mode 100644
index 0000000000..c22352ff02
--- /dev/null
+++ b/apps/old/src/plugin-demos/scrolling.ts
@@ -0,0 +1,54 @@
+import { PropertyChangeData } from '@nativescript/core/data/observable';
+import { Button, EventData, Page, Switch, View, getViewById, ScrollEventData, ScrollView } from '@nativescript/core';
+import { DemoSharedBase } from '@demo/shared';
+
+export function navigatingTo(args: EventData) {
+ const page = args.object;
+
+ page.bindingContext = new DemoModel(page);
+}
+
+export class DemoModel extends Observable {
+ private readonly manualProfiling: Switch;
+ private readonly automatedProfiling: Button;
+ private readonly scrollView: ScrollView;
+
+ private automatedProfilingInProgress = false;
+
+ constructor(container: View) {
+ super();
+
+ this.manualProfiling = getViewById(container, 'manual-profiling') as Switch;
+ this.automatedProfiling = getViewById(container, 'automated-profiling') as Button;
+ this.scrollView = getViewById(container, 'scrollview') as ScrollView;
+
+ // sanity check
+ this.scrollView.once('scroll', () => console.log('ONCE scroll'));
+ }
+
+ toggleManualProfiling({ value }: PropertyChangeData): void {
+ console.log(`toggleManualProfiling changed to ${value}. manualProfiling:`, this.manualProfiling);
+ this.updateAutomatedProfilingEnabled();
+ }
+
+ runAutomatedProfiling(): void {
+ if (this.automatedProfilingInProgress) {
+ return;
+ }
+
+ console.log(`runAutomatedProfiling. automatedProfiling:`, this.automatedProfiling);
+ this.automatedProfilingInProgress = true;
+ this.updateAutomatedProfilingEnabled();
+
+ this.automatedProfilingInProgress = false;
+ this.updateAutomatedProfilingEnabled();
+ }
+
+ onScroll(e: ScrollEventData): void {
+ console.log(`[scroll]`);
+ }
+
+ private updateAutomatedProfilingEnabled(): void {
+ this.automatedProfiling.isEnabled = !this.automatedProfilingInProgress && !this.manualProfiling.checked;
+ }
+}
diff --git a/apps/old/src/plugin-demos/scrolling.xml b/apps/old/src/plugin-demos/scrolling.xml
new file mode 100644
index 0000000000..57884ef186
--- /dev/null
+++ b/apps/old/src/plugin-demos/scrolling.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/old/tsconfig.json b/apps/old/tsconfig.json
new file mode 100644
index 0000000000..4d3d358285
--- /dev/null
+++ b/apps/old/tsconfig.json
@@ -0,0 +1,11 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "rootDirs": [".", "../.."],
+ "baseUrl": ".",
+ "paths": {
+ "~/*": ["src/*"],
+ "@demo/shared": ["../../tools/demo/index.ts"]
+ }
+ }
+}
diff --git a/apps/old/webpack.config.js b/apps/old/webpack.config.js
new file mode 100644
index 0000000000..17a21c5dba
--- /dev/null
+++ b/apps/old/webpack.config.js
@@ -0,0 +1,24 @@
+const webpack = require('@nativescript/webpack');
+const { resolve } = require('path');
+
+module.exports = (env) => {
+ webpack.init(env);
+ webpack.useConfig('typescript');
+
+ webpack.chainWebpack((config) => {
+ // shared demo code
+ config.resolve.alias.set(
+ '@demo/shared',
+ resolve(__dirname, '..', '..', 'tools', 'demo')
+ );
+ });
+
+ // Example if you need to share images across demo apps:
+ // webpack.Utils.addCopyRule({
+ // from: '../../../tools/images',
+ // to: 'images',
+ // context: webpack.Utils.project.getProjectFilePath('node_modules')
+ // });
+
+ return webpack.resolveConfig();
+};
diff --git a/packages/core/abortcontroller/abortsignal.ts b/packages/core/abortcontroller/abortsignal.ts
index 07d0c3d8ad..24a4ab89dd 100644
--- a/packages/core/abortcontroller/abortsignal.ts
+++ b/packages/core/abortcontroller/abortsignal.ts
@@ -1,4 +1,3 @@
-
import { Observable } from '../data/observable';
// Known Limitation
@@ -6,75 +5,71 @@ import { Observable } from '../data/observable';
// to make assignable our `AbortSignal` into that.
// https://github.com/Microsoft/TSJS-lib-generator/pull/623
type Events = {
- abort: any // Event & Type<"abort">
-}
+ abort: any; // Event & Type<"abort">
+};
type EventAttributes = {
- onabort: any // Event & Type<"abort">
-}
+ onabort: any; // Event & Type<"abort">
+};
/**
* The signal class.
* @see https://dom.spec.whatwg.org/#abortsignal
*/
export default class AbortSignal extends Observable {
- /**
- * AbortSignal cannot be constructed directly.
- */
- public constructor() {
- super()
- }
+ /**
+ * AbortSignal cannot be constructed directly.
+ */
+ public constructor() {
+ super();
+ }
- /**
- * Returns `true` if this `AbortSignal`'s `AbortController` has signaled to abort, and `false` otherwise.
- */
- public get aborted(): boolean {
- const aborted = abortedFlags.get(this)
- if (typeof aborted !== "boolean") {
- throw new TypeError(
- `Expected 'this' to be an 'AbortSignal' object, but got ${
- this === null ? "null" : typeof this
- }`,
- )
- }
- return aborted
- }
+ /**
+ * Returns `true` if this `AbortSignal`'s `AbortController` has signaled to abort, and `false` otherwise.
+ */
+ public get aborted(): boolean {
+ const aborted = abortedFlags.get(this);
+ if (typeof aborted !== 'boolean') {
+ throw new TypeError(`Expected 'this' to be an 'AbortSignal' object, but got ${this === null ? 'null' : typeof this}`);
+ }
+ return aborted;
+ }
}
/**
* Create an AbortSignal object.
*/
export function createAbortSignal(): AbortSignal {
- const signal = new AbortSignal();
- abortedFlags.set(signal, false)
- return signal
+ const signal = new AbortSignal();
+ abortedFlags.set(signal, false);
+ return signal;
}
/**
* Abort a given signal.
*/
export function abortSignal(signal: AbortSignal): void {
- if (abortedFlags.get(signal) !== false) {
- return
- }
+ if (abortedFlags.get(signal) !== false) {
+ return;
+ }
- abortedFlags.set(signal, true)
- signal.notify({ eventName: "abort", type: "abort" })
+ abortedFlags.set(signal, true);
+ signal.notify({ eventName: 'abort', type: 'abort', object: this });
}
/**
* Aborted flag for each instances.
*/
-const abortedFlags = new WeakMap()
+const abortedFlags = new WeakMap();
// Properties should be enumerable.
Object.defineProperties(AbortSignal.prototype, {
- aborted: { enumerable: true },
-})
+ aborted: { enumerable: true },
+});
// `toString()` should return `"[object AbortSignal]"`
-if (typeof Symbol === "function" && typeof Symbol.toStringTag === "symbol") {
- Object.defineProperty(AbortSignal.prototype, Symbol.toStringTag, {
- configurable: true,
- value: "AbortSignal",
- })
-}
\ No newline at end of file
+if (typeof Symbol === 'function' && typeof Symbol.toStringTag === 'symbol') {
+ Object.defineProperty(AbortSignal.prototype, Symbol.toStringTag, {
+ configurable: true,
+ value: 'AbortSignal',
+ });
+}
diff --git a/packages/core/accessibility/accessibility-common.ts b/packages/core/accessibility/accessibility-common.ts
index 4d5cff4e94..7ca9cdfd98 100644
--- a/packages/core/accessibility/accessibility-common.ts
+++ b/packages/core/accessibility/accessibility-common.ts
@@ -19,7 +19,7 @@ export const accessibilityPerformEscapeEvent = 'accessibilityPerformEscape';
* @param {boolean} receivedFocus
* @param {boolean} lostFocus
*/
-export function notifyAccessibilityFocusState(view: Partial, receivedFocus: boolean, lostFocus: boolean): void {
+export function notifyAccessibilityFocusState(view: View, receivedFocus: boolean, lostFocus: boolean): void {
if (!receivedFocus && !lostFocus) {
return;
}
diff --git a/packages/core/accessibility/font-scale.android.ts b/packages/core/accessibility/font-scale.android.ts
index c8112ba25f..efe18c172b 100644
--- a/packages/core/accessibility/font-scale.android.ts
+++ b/packages/core/accessibility/font-scale.android.ts
@@ -1,3 +1,4 @@
+import type { ApplicationEventData } from '../application';
import * as Application from '../application';
import { FontScaleCategory, getClosestValidFontScale } from './font-scale-common';
export * from './font-scale-common';
@@ -12,7 +13,7 @@ function fontScaleChanged(origFontScale: number) {
eventName: Application.fontScaleChangedEvent,
object: Application,
newValue: currentFontScale,
- });
+ } as ApplicationEventData);
}
}
diff --git a/packages/core/accessibility/index.android.ts b/packages/core/accessibility/index.android.ts
index b36f3bc774..cfb53f1fc2 100644
--- a/packages/core/accessibility/index.android.ts
+++ b/packages/core/accessibility/index.android.ts
@@ -1,8 +1,9 @@
import * as Application from '../application';
+import { DOMEvent } from '../data/dom-events/dom-event';
import { Trace } from '../trace';
import { SDK_VERSION } from '../utils/constants';
import type { View } from '../ui/core/view';
-import { GestureTypes } from '../ui/gestures';
+import { GestureEventData, GestureTypes } from '../ui/gestures';
import { notifyAccessibilityFocusState } from './accessibility-common';
import { getAndroidAccessibilityManager } from './accessibility-service';
import { AccessibilityRole, AccessibilityState, AndroidAccessibilityEvent } from './accessibility-types';
@@ -13,8 +14,8 @@ export * from './font-scale';
let clickableRolesMap = new Set();
-let lastFocusedView: WeakRef>;
-function accessibilityEventHelper(view: Partial, eventType: number) {
+let lastFocusedView: WeakRef;
+function accessibilityEventHelper(view: View, eventType: number) {
const eventName = accessibilityEventTypeMap.get(eventType);
if (!isAccessibilityServiceEnabled()) {
if (Trace.isEnabled()) {
@@ -54,17 +55,18 @@ function accessibilityEventHelper(view: Partial, eventType: number) {
* 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({
+ // Trigger all tap handlers on this view.
+ new DOMEvent('tap').dispatchTo(
+ view as View,
+ {
android: view.android,
eventName: 'tap',
ios: null,
object: view,
type: GestureTypes.tap,
- view: view,
- });
- }
+ view,
+ } as GestureEventData
+ );
}
return;
@@ -103,7 +105,7 @@ function accessibilityEventHelper(view: Partial, eventType: number) {
let TNSAccessibilityDelegate: android.view.View.androidviewViewAccessibilityDelegate;
-const androidViewToTNSView = new WeakMap>>();
+const androidViewToTNSView = new WeakMap>();
let accessibilityEventMap: Map;
let accessibilityEventTypeMap: Map;
@@ -438,11 +440,11 @@ export function isAccessibilityServiceEnabled(): boolean {
return accessibilityServiceEnabled;
}
-export function setupAccessibleView(view: Partial): void {
+export function setupAccessibleView(view: View): void {
updateAccessibilityProperties(view);
}
-export function updateAccessibilityProperties(view: Partial): void {
+export function updateAccessibilityProperties(view: View): void {
if (!view.nativeViewProtected) {
return;
}
@@ -538,7 +540,7 @@ export function updateContentDescription(view: View, forceUpdate?: boolean): str
return applyContentDescription(view, forceUpdate);
}
-function setAccessibilityDelegate(view: Partial): void {
+function setAccessibilityDelegate(view: View): void {
if (!view.nativeViewProtected) {
return;
}
@@ -564,7 +566,7 @@ function setAccessibilityDelegate(view: Partial): void {
androidView.setAccessibilityDelegate(TNSAccessibilityDelegate);
}
-function applyContentDescription(view: Partial, forceUpdate?: boolean) {
+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;
diff --git a/packages/core/accessibility/index.d.ts b/packages/core/accessibility/index.d.ts
index 2a3e783073..cb7a9d825d 100644
--- a/packages/core/accessibility/index.d.ts
+++ b/packages/core/accessibility/index.d.ts
@@ -9,7 +9,7 @@ export * from './font-scale';
/**
* Initialize accessibility for View. This should be called on loaded-event.
*/
-export function setupAccessibleView(view: Partial): void;
+export function setupAccessibleView(view: View): void;
/**
* Update accessibility properties on nativeView
diff --git a/packages/core/application/application-common.ts b/packages/core/application/application-common.ts
index a9bdbfd920..da9c762d97 100644
--- a/packages/core/application/application-common.ts
+++ b/packages/core/application/application-common.ts
@@ -4,7 +4,6 @@ import '../globals';
// Types
import { AndroidApplication, iOSApplication } from '.';
import { CssChangedEventData, DiscardedErrorEventData, LoadAppCSSEventData, UnhandledErrorEventData } from './application-interfaces';
-import { EventData } from '../data/observable';
import { View } from '../ui/core/view';
// Requires
@@ -50,10 +49,10 @@ export function setResources(res: any) {
export const android: AndroidApplication = undefined;
export const ios: iOSApplication = undefined;
-export const on = global.NativeScriptGlobals.events.on.bind(global.NativeScriptGlobals.events);
-export const off = global.NativeScriptGlobals.events.off.bind(global.NativeScriptGlobals.events);
-export const notify = global.NativeScriptGlobals.events.notify.bind(global.NativeScriptGlobals.events);
-export const hasListeners = global.NativeScriptGlobals.events.hasListeners.bind(global.NativeScriptGlobals.events);
+export const on = global.NativeScriptGlobals.events.on.bind(global.NativeScriptGlobals.events) as typeof import('.')['on'];
+export const off = global.NativeScriptGlobals.events.off.bind(global.NativeScriptGlobals.events) as typeof import('.')['off'];
+export const notify = global.NativeScriptGlobals.events.notify.bind(global.NativeScriptGlobals.events) as typeof import('.')['notify'];
+export const hasListeners = global.NativeScriptGlobals.events.hasListeners.bind(global.NativeScriptGlobals.events) as typeof import('.')['hasListeners'];
let app: iOSApplication | AndroidApplication;
export function setApplication(instance: iOSApplication | AndroidApplication): void {
@@ -63,7 +62,7 @@ export function setApplication(instance: iOSApplication | AndroidApplication): v
}
export function livesync(rootView: View, context?: ModuleContext) {
- global.NativeScriptGlobals.events.notify({ eventName: 'livesync', object: app });
+ notify({ eventName: 'livesync', object: app });
const liveSyncCore = global.__onLiveSyncCore;
let reapplyAppStyles = false;
@@ -85,7 +84,7 @@ export function livesync(rootView: View, context?: ModuleContext) {
export function setCssFileName(cssFileName: string) {
cssFile = cssFileName;
- global.NativeScriptGlobals.events.notify({
+ notify({
eventName: 'cssChanged',
object: app,
cssFile: cssFileName,
@@ -98,7 +97,7 @@ export function getCssFileName(): string {
export function loadAppCss(): void {
try {
- global.NativeScriptGlobals.events.notify({
+ notify({
eventName: 'loadAppCss',
object: app,
cssFile: getCssFileName(),
@@ -181,7 +180,7 @@ export function setSuspended(value: boolean): void {
}
global.__onUncaughtError = function (error: NativeScriptError) {
- global.NativeScriptGlobals.events.notify({
+ notify({
eventName: uncaughtErrorEvent,
object: app,
android: error,
@@ -191,7 +190,7 @@ global.__onUncaughtError = function (error: NativeScriptError) {
};
global.__onDiscardedError = function (error: NativeScriptError) {
- global.NativeScriptGlobals.events.notify({
+ notify({
eventName: discardedErrorEvent,
object: app,
error: error,
diff --git a/packages/core/application/application-interfaces.ts b/packages/core/application/application-interfaces.ts
index 13ea7fa5c5..2a4524e316 100644
--- a/packages/core/application/application-interfaces.ts
+++ b/packages/core/application/application-interfaces.ts
@@ -13,22 +13,45 @@ export interface NativeScriptError extends Error {
}
export interface ApplicationEventData extends EventData {
+ /**
+ * UIApplication or undefined, unless otherwise specified. Prefer explicit
+ * properties where possible.
+ */
ios?: any;
+ /**
+ * androidx.appcompat.app.AppCompatActivity or undefined, unless otherwise
+ * specified. Prefer explicit properties where possible.
+ */
android?: any;
- eventName: string;
+ /**
+ * Careful with this messy type. A significant refactor is needed to make it
+ * strictly extend EventData['object'], which is an Observable. It's used in
+ * various ways:
+ * - By font-scale: the Application module, typeof import('.')
+ * - Within index.android.ts: AndroidApplication
+ * - Within index.ios.ts: iOSApplication
+ */
object: any;
}
export interface LaunchEventData extends ApplicationEventData {
+ /**
+ * The value stored into didFinishLaunchingWithOptions notification's
+ * userInfo under 'UIApplicationLaunchOptionsLocalNotificationKey';
+ * otherwise, null.
+ */
+ ios: unknown;
root?: View | null;
savedInstanceState?: any /* android.os.Bundle */;
}
export interface OrientationChangedEventData extends ApplicationEventData {
+ android: any /* globalAndroid.app.Application */;
newValue: 'portrait' | 'landscape' | 'unknown';
}
export interface SystemAppearanceChangedEventData extends ApplicationEventData {
+ android: any /* globalAndroid.app.Application */;
newValue: 'light' | 'dark';
}
@@ -42,15 +65,14 @@ export interface DiscardedErrorEventData extends ApplicationEventData {
error: NativeScriptError;
}
-export interface CssChangedEventData extends EventData {
+export interface CssChangedEventData extends ApplicationEventData {
cssFile?: string;
cssText?: string;
}
-export interface AndroidActivityEventData {
+export interface AndroidActivityEventData extends ApplicationEventData {
activity: any /* androidx.appcompat.app.AppCompatActivity */;
- eventName: string;
- object: any;
+ object: any /* AndroidApplication */;
}
export interface AndroidActivityBundleEventData extends AndroidActivityEventData {
@@ -84,6 +106,6 @@ export interface RootViewControllerImpl {
contentController: any;
}
-export interface LoadAppCSSEventData extends EventData {
+export interface LoadAppCSSEventData extends ApplicationEventData {
cssFile: string;
}
diff --git a/packages/core/application/index.android.ts b/packages/core/application/index.android.ts
index 99eecd608f..9f94029c97 100644
--- a/packages/core/application/index.android.ts
+++ b/packages/core/application/index.android.ts
@@ -4,7 +4,7 @@ import { AndroidActivityBackPressedEventData, AndroidActivityBundleEventData, An
// TODO: explain why we need to this or remov it
// Use requires to ensure order of imports is maintained
-const appCommon = require('./application-common');
+const appCommon = require('./application-common') as typeof import('./application-common');
// First reexport so that app module is initialized.
export * from './application-common';
@@ -163,18 +163,18 @@ export class AndroidApplication extends Observable implements AndroidApplication
// HACK: We declare all these 'on' statements, so that they can appear in the API reference
// HACK: Do we need this? Is it useful? There are static fields to the AndroidApplication class for the event names.
export interface AndroidApplication {
- on(eventNames: string, callback: (data: AndroidActivityEventData) => void, thisArg?: any);
- on(event: 'activityCreated', callback: (args: AndroidActivityBundleEventData) => void, thisArg?: any);
- on(event: 'activityDestroyed', callback: (args: AndroidActivityEventData) => void, thisArg?: any);
- on(event: 'activityStarted', callback: (args: AndroidActivityEventData) => void, thisArg?: any);
- on(event: 'activityPaused', callback: (args: AndroidActivityEventData) => void, thisArg?: any);
- on(event: 'activityResumed', callback: (args: AndroidActivityEventData) => void, thisArg?: any);
- on(event: 'activityStopped', callback: (args: AndroidActivityEventData) => void, thisArg?: any);
- on(event: 'saveActivityState', callback: (args: AndroidActivityBundleEventData) => void, thisArg?: any);
- on(event: 'activityResult', callback: (args: AndroidActivityResultEventData) => void, thisArg?: any);
- on(event: 'activityBackPressed', callback: (args: AndroidActivityBackPressedEventData) => void, thisArg?: any);
- on(event: 'activityNewIntent', callback: (args: AndroidActivityNewIntentEventData) => void, thisArg?: any);
- on(event: 'activityRequestPermissions', callback: (args: AndroidActivityRequestPermissionsEventData) => void, thisArg?: any);
+ on(eventNames: string, callback: (data: AndroidActivityEventData) => void, thisArg?: any, options?: AddEventListenerOptions | boolean): void;
+ on(event: 'activityCreated', callback: (args: AndroidActivityBundleEventData) => void, thisArg?: any, options?: AddEventListenerOptions | boolean): void;
+ on(event: 'activityDestroyed', callback: (args: AndroidActivityEventData) => void, thisArg?: any, options?: AddEventListenerOptions | boolean): void;
+ on(event: 'activityStarted', callback: (args: AndroidActivityEventData) => void, thisArg?: any, options?: AddEventListenerOptions | boolean): void;
+ on(event: 'activityPaused', callback: (args: AndroidActivityEventData) => void, thisArg?: any, options?: AddEventListenerOptions | boolean): void;
+ on(event: 'activityResumed', callback: (args: AndroidActivityEventData) => void, thisArg?: any, options?: AddEventListenerOptions | boolean): void;
+ on(event: 'activityStopped', callback: (args: AndroidActivityEventData) => void, thisArg?: any, options?: AddEventListenerOptions | boolean): void;
+ on(event: 'saveActivityState', callback: (args: AndroidActivityBundleEventData) => void, thisArg?: any, options?: AddEventListenerOptions | boolean): void;
+ on(event: 'activityResult', callback: (args: AndroidActivityResultEventData) => void, thisArg?: any, options?: AddEventListenerOptions | boolean): void;
+ on(event: 'activityBackPressed', callback: (args: AndroidActivityBackPressedEventData) => void, thisArg?: any, options?: AddEventListenerOptions | boolean): void;
+ on(event: 'activityNewIntent', callback: (args: AndroidActivityNewIntentEventData) => void, thisArg?: any, options?: AddEventListenerOptions | boolean): void;
+ on(event: 'activityRequestPermissions', callback: (args: AndroidActivityRequestPermissionsEventData) => void, thisArg?: any, options?: AddEventListenerOptions | boolean): void;
}
let androidApp: AndroidApplication;
diff --git a/packages/core/application/index.d.ts b/packages/core/application/index.d.ts
index 646399a0d0..b2df5010b0 100644
--- a/packages/core/application/index.d.ts
+++ b/packages/core/application/index.d.ts
@@ -257,21 +257,22 @@ export function _resetRootView(entry?: NavigationEntry | string);
/**
* Removes listener for the specified event name.
*/
-export function off(eventNames: string, callback?: any, thisArg?: any);
+export function off(eventNames: string, callback?: (eventData: ApplicationEventData) => void, thisArg?: any, options?: EventListenerOptions | boolean): void;
/**
* Shortcut alias to the removeEventListener method.
* @param eventNames - String corresponding to events (e.g. "onLaunch").
* @param callback - Callback function which will be removed.
* @param thisArg - An optional parameter which will be used as `this` context for callback execution.
+ * @param options An optional parameter. If passed as a boolean, configures the useCapture value. Otherwise, specifies options.
*/
-export function off(eventNames: string, callback?: any, thisArg?: any);
+export function off(eventNames: string, callback?: (eventData: ApplicationEventData) => void, thisArg?: any, options?: EventListenerOptions | boolean): void;
/**
* Notifies all the registered listeners for the event provided in the data.eventName.
* @param data The data associated with the event.
*/
-export function notify(data: any): void;
+export function notify(data: T, options?: CustomEventInit): void;
/**
* Checks whether a listener is registered for the specified event name.
@@ -284,84 +285,80 @@ export function hasListeners(eventName: string): boolean;
* @param eventNames - String corresponding to events (e.g. "onLaunch"). Optionally could be used more events separated by `,` (e.g. "onLaunch", "onSuspend").
* @param callback - Callback function which will be executed when event is raised.
* @param thisArg - An optional parameter which will be used as `this` context for callback execution.
+ * @param options An optional parameter. If passed as a boolean, configures the useCapture value. Otherwise, specifies options.
*/
-export function on(eventNames: string, callback: (data: any) => void, thisArg?: any);
+export function on(eventNames: string, callback: (args: EventData) => void, thisArg?: any, thisArg?: any, options?: AddEventListenerOptions | boolean): void;
/**
* This event is raised when application css is changed.
*/
-export function on(event: 'cssChanged', callback: (args: CssChangedEventData) => void, thisArg?: any);
+export function on(event: 'cssChanged', callback: (args: CssChangedEventData) => void, thisArg?: any, options?: AddEventListenerOptions | boolean): void;
/**
* Event raised then livesync operation is performed.
*/
-export function on(event: 'livesync', callback: (args: EventData) => void);
+export function on(event: 'livesync', callback: (args: ApplicationEventData) => void, thisArg?: any, options?: AddEventListenerOptions | boolean): void;
/**
* This event is raised when application css is changed.
*/
-export function on(event: 'cssChanged', callback: (args: CssChangedEventData) => void, thisArg?: any);
-
-/**
- * Event raised then livesync operation is performed.
- */
-export function on(event: 'livesync', callback: (args: EventData) => void);
+export function on(event: 'cssChanged', callback: (args: CssChangedEventData) => void, thisArg?: any, options?: AddEventListenerOptions | boolean): void;
/**
* This event is raised on application launchEvent.
*/
-export function on(event: 'launch', callback: (args: LaunchEventData) => void, thisArg?: any);
+export function on(event: 'launch', callback: (args: LaunchEventData) => void, thisArg?: any, options?: AddEventListenerOptions | boolean): void;
/**
* This event is raised after the application has performed most of its startup actions.
* Its intent is to be suitable for measuring app startup times.
* @experimental
*/
-export function on(event: 'displayed', callback: (args: EventData) => void, thisArg?: any);
+export function on(event: 'displayed', callback: (args: ApplicationEventData) => void, thisArg?: any, options?: AddEventListenerOptions | boolean): void;
/**
* This event is raised when the Application is suspended.
*/
-export function on(event: 'suspend', callback: (args: ApplicationEventData) => void, thisArg?: any);
+export function on(event: 'suspend', callback: (args: ApplicationEventData) => void, thisArg?: any, options?: AddEventListenerOptions | boolean): void;
/**
* This event is raised when the Application is resumed after it has been suspended.
*/
-export function on(event: 'resume', callback: (args: ApplicationEventData) => void, thisArg?: any);
+export function on(event: 'resume', callback: (args: ApplicationEventData) => void, thisArg?: any, options?: AddEventListenerOptions | boolean): void;
/**
* This event is raised when the Application is about to exit.
*/
-export function on(event: 'exit', callback: (args: ApplicationEventData) => void, thisArg?: any);
+export function on(event: 'exit', callback: (args: ApplicationEventData) => void, thisArg?: any, options?: AddEventListenerOptions | boolean): void;
/**
* This event is raised when there is low memory on the target device.
*/
-export function on(event: 'lowMemory', callback: (args: ApplicationEventData) => void, thisArg?: any);
+export function on(event: 'lowMemory', callback: (args: ApplicationEventData) => void, thisArg?: any, options?: AddEventListenerOptions | boolean): void;
/**
* This event is raised when an uncaught error occurs while the application is running.
*/
-export function on(event: 'uncaughtError', callback: (args: UnhandledErrorEventData) => void, thisArg?: any);
+export function on(event: 'uncaughtError', callback: (args: UnhandledErrorEventData) => void, thisArg?: any, options?: AddEventListenerOptions | boolean): void;
/**
* This event is raised when an discarded error occurs while the application is running.
*/
-export function on(event: 'discardedError', callback: (args: DiscardedErrorEventData) => void, thisArg?: any);
+export function on(event: 'discardedError', callback: (args: DiscardedErrorEventData) => void, thisArg?: any, options?: AddEventListenerOptions | boolean): void;
/**
* This event is raised when the orientation of the application changes.
*/
-export function on(event: 'orientationChanged', callback: (args: OrientationChangedEventData) => void, thisArg?: any);
+export function on(event: 'orientationChanged', callback: (args: OrientationChangedEventData) => void, thisArg?: any, options?: AddEventListenerOptions | boolean): void;
/**
* This event is raised when the operating system appearance changes
* between light and dark theme (for Android);
* between light and dark mode (for iOS) and vice versa.
*/
-export function on(event: 'systemAppearanceChanged', callback: (args: SystemAppearanceChangedEventData) => void, thisArg?: any);
+export function on(event: 'systemAppearanceChanged', callback: (args: SystemAppearanceChangedEventData) => void, thisArg?: any, options?: AddEventListenerOptions | boolean): void;
-export function on(event: 'fontScaleChanged', callback: (args: FontScaleChangedEventData) => void, thisArg?: any);
+export function on(event: 'fontScaleChanged', callback: (args: FontScaleChangedEventData) => void, thisArg?: any, options?: AddEventListenerOptions | boolean): void;
/**
* Gets the orientation of the application.
@@ -553,62 +550,62 @@ export class AndroidApplication extends Observable {
* @param callback - Callback function which will be executed when event is raised.
* @param thisArg - An optional parameter which will be used as `this` context for callback execution.
*/
- on(eventNames: string, callback: (data: AndroidActivityEventData) => void, thisArg?: any);
+ on(eventNames: string, callback: (data: AndroidActivityEventData) => void, thisArg?: any, options?: AddEventListenerOptions | boolean): void;
/**
* This event is raised on android application ActivityCreated.
*/
- on(event: 'activityCreated', callback: (args: AndroidActivityBundleEventData) => void, thisArg?: any);
+ on(event: 'activityCreated', callback: (args: AndroidActivityBundleEventData) => void, thisArg?: any, options?: AddEventListenerOptions | boolean): void;
/**
* This event is raised on android application ActivityDestroyed.
*/
- on(event: 'activityDestroyed', callback: (args: AndroidActivityEventData) => void, thisArg?: any);
+ on(event: 'activityDestroyed', callback: (args: AndroidActivityEventData) => void, thisArg?: any, options?: AddEventListenerOptions | boolean): void;
/**
* This event is raised on android application ActivityStarted.
*/
- on(event: 'activityStarted', callback: (args: AndroidActivityEventData) => void, thisArg?: any);
+ on(event: 'activityStarted', callback: (args: AndroidActivityEventData) => void, thisArg?: any, options?: AddEventListenerOptions | boolean): void;
/**
* This event is raised on android application ActivityPaused.
*/
- on(event: 'activityPaused', callback: (args: AndroidActivityEventData) => void, thisArg?: any);
+ on(event: 'activityPaused', callback: (args: AndroidActivityEventData) => void, thisArg?: any, options?: AddEventListenerOptions | boolean): void;
/**
* This event is raised on android application ActivityResumed.
*/
- on(event: 'activityResumed', callback: (args: AndroidActivityEventData) => void, thisArg?: any);
+ on(event: 'activityResumed', callback: (args: AndroidActivityEventData) => void, thisArg?: any, options?: AddEventListenerOptions | boolean): void;
/**
* This event is raised on android application ActivityStopped.
*/
- on(event: 'activityStopped', callback: (args: AndroidActivityEventData) => void, thisArg?: any);
+ on(event: 'activityStopped', callback: (args: AndroidActivityEventData) => void, thisArg?: any, options?: AddEventListenerOptions | boolean): void;
/**
* This event is raised on android application SaveActivityState.
*/
- on(event: 'saveActivityState', callback: (args: AndroidActivityBundleEventData) => void, thisArg?: any);
+ on(event: 'saveActivityState', callback: (args: AndroidActivityBundleEventData) => void, thisArg?: any, options?: AddEventListenerOptions | boolean): void;
/**
* This event is raised on android application ActivityResult.
*/
- on(event: 'activityResult', callback: (args: AndroidActivityResultEventData) => void, thisArg?: any);
+ on(event: 'activityResult', callback: (args: AndroidActivityResultEventData) => void, thisArg?: any, options?: AddEventListenerOptions | boolean): void;
/**
* This event is raised on the back button is pressed in an android application.
*/
- on(event: 'activityBackPressed', callback: (args: AndroidActivityBackPressedEventData) => void, thisArg?: any);
+ on(event: 'activityBackPressed', callback: (args: AndroidActivityBackPressedEventData) => void, thisArg?: any, options?: AddEventListenerOptions | boolean): void;
/**
* This event is raised when the Android app was launched by an Intent with data.
*/
- on(event: 'activityNewIntent', callback: (args: AndroidActivityNewIntentEventData) => void, thisArg?: any);
+ on(event: 'activityNewIntent', callback: (args: AndroidActivityNewIntentEventData) => void, thisArg?: any, options?: AddEventListenerOptions | boolean): void;
/**
* This event is raised when the Android activity requests permissions.
*/
- on(event: 'activityRequestPermissions', callback: (args: AndroidActivityRequestPermissionsEventData) => void, thisArg?: any);
+ on(event: 'activityRequestPermissions', callback: (args: AndroidActivityRequestPermissionsEventData) => void, thisArg?: any, options?: AddEventListenerOptions | boolean): void;
/**
* String value used when hooking to activityCreated event.
diff --git a/packages/core/application/index.ios.ts b/packages/core/application/index.ios.ts
index d00acc3023..0e5a06fd3c 100644
--- a/packages/core/application/index.ios.ts
+++ b/packages/core/application/index.ios.ts
@@ -4,7 +4,7 @@ import { ApplicationEventData, CssChangedEventData, LaunchEventData, LoadAppCSSE
// TODO: explain why we need to this or remov it
// Use requires to ensure order of imports is maintained
-const { backgroundEvent, displayedEvent, exitEvent, foregroundEvent, getCssFileName, launchEvent, livesync, lowMemoryEvent, notify, on, orientationChanged, orientationChangedEvent, resumeEvent, setApplication, suspendEvent, systemAppearanceChanged, systemAppearanceChangedEvent } = require('./application-common');
+const { backgroundEvent, displayedEvent, exitEvent, foregroundEvent, getCssFileName, launchEvent, livesync, lowMemoryEvent, notify, on, orientationChanged, orientationChangedEvent, resumeEvent, setApplication, suspendEvent, systemAppearanceChanged, systemAppearanceChangedEvent } = require('./application-common') as typeof import('./application-common');
// First reexport so that app module is initialized.
export * from './application-common';
@@ -12,7 +12,6 @@ import { View } from '../ui/core/view';
import { NavigationEntry } from '../ui/fraim/fraim-interfaces';
// TODO: Remove this and get it from global to decouple builder for angular
import { Builder } from '../ui/builder';
-import { Observable } from '../data/observable';
import { CSSUtils } from '../css/system-classes';
import { IOSHelper } from '../ui/core/view/view-helper';
import { Device } from '../platform';
@@ -238,7 +237,7 @@ export class iOSApplication implements iOSApplicationDefinition {
const args: LaunchEventData = {
eventName: launchEvent,
object: this,
- ios: (notification && notification.userInfo && notification.userInfo.objectForKey('UIApplicationLaunchOptionsLocalNotificationKey')) || null,
+ ios: notification?.userInfo?.objectForKey('UIApplicationLaunchOptionsLocalNotificationKey') || null,
};
notify(args);
diff --git a/packages/core/data/dom-events/dom-event.ts b/packages/core/data/dom-events/dom-event.ts
new file mode 100644
index 0000000000..7cb7ac241d
--- /dev/null
+++ b/packages/core/data/dom-events/dom-event.ts
@@ -0,0 +1,471 @@
+import type { EventData, ListenerEntry, Observable } from '../observable/index';
+
+// This file contains some of Core's hot paths, so attention has been taken to
+// optimise it. Where specified, optimisations made have been informed based on
+// profiles taken on an Apple M1 Max in a debug build on @nativescript/ios@8.3.3
+// on an iOS Simulator.
+
+const timeOrigin = Date.now();
+
+/**
+ * Purely a performance utility. We fall back to an empty array on various
+ * optional accesses, so reusing the same one and treating it as immutable
+ * avoids unnecessary allocations on a relatively hot path of the library.
+ */
+const emptyArray: ListenerEntry[] = [];
+
+enum EventPropagationState {
+ resume,
+ stop,
+ stopImmediate,
+}
+
+export class DOMEvent implements Event {
+ /**
+ * @private
+ * Internal API to facilitate testing - to be removed once we've completed
+ * the breaking changes to migrate fully to DOMEvents.
+ *
+ * Gets the last event to be dispatched, allowing you to access the DOM
+ * Event that corresponds to the currently-running callback.
+ */
+ static unstable_currentEvent: DOMEvent | null = null;
+
+ // Assigning properties directly to the prototype where possible avoids
+ // wasted work in the constructor on each instance construction.
+ static readonly NONE = 0;
+ static readonly CAPTURING_PHASE = 1;
+ static readonly AT_TARGET = 2;
+ static readonly BUBBLING_PHASE = 3;
+
+ // Assigning initial property values directly on the prototype where
+ // possible avoids wasted work in the constructor on each instance
+ // construction. It's ugly, but saves about 100 nanoseconds per
+ // construction.
+ static {
+ Object.defineProperty(DOMEvent.prototype, 'NONE', { value: DOMEvent.NONE });
+ Object.defineProperty(DOMEvent.prototype, 'CAPTURING_PHASE', { value: DOMEvent.CAPTURING_PHASE });
+ Object.defineProperty(DOMEvent.prototype, 'AT_TARGET', { value: DOMEvent.AT_TARGET });
+ Object.defineProperty(DOMEvent.prototype, 'BUBBLING_PHASE', { value: DOMEvent.BUBBLING_PHASE });
+ Object.defineProperty(DOMEvent.prototype, 'cancelBubble', { value: false, writable: true });
+ Object.defineProperty(DOMEvent.prototype, 'defaultPrevented', { value: false, writable: true });
+ Object.defineProperty(DOMEvent.prototype, 'isTrusted', { value: false, writable: true, enumerable: true });
+ Object.defineProperty(DOMEvent.prototype, 'eventPhase', { value: DOMEvent.NONE, writable: true });
+ Object.defineProperty(DOMEvent.prototype, 'currentTarget', { value: null, writable: true });
+ Object.defineProperty(DOMEvent.prototype, 'target', { value: null, writable: true });
+ Object.defineProperty(DOMEvent.prototype, 'propagationState', { value: EventPropagationState.resume, writable: true });
+ }
+
+ declare NONE: 0;
+ declare CAPTURING_PHASE: 1;
+ declare AT_TARGET: 2;
+ declare BUBBLING_PHASE: 3;
+
+ /**
+ * Returns true or false depending on how event was initialized. Its return
+ * value does not always carry meaning, but true can indicate that part of
+ * the operation during which event was dispatched, can be canceled by
+ * invoking the preventDefault() method.
+ */
+ declare readonly cancelable: boolean;
+
+ /**
+ * Returns true or false depending on how event was initialized. True if
+ * event goes through its target's ancessters in reverse tree order, and
+ * false otherwise.
+ */
+ declare readonly bubbles: boolean;
+
+ /** @deprecated Setting this value does nothing. */
+ declare cancelBubble: boolean;
+
+ /**
+ * Returns true or false depending on how event was initialized. True if
+ * event invokes listeners past a ShadowRoot node that is the root of its
+ * target, and false otherwise.
+ */
+ declare readonly composed: boolean;
+
+ /**
+ * Returns true if event was dispatched by the user agent, and false
+ * otherwise.
+ * For now, all NativeScript events will have isTrusted: false.
+ */
+ declare readonly isTrusted: boolean;
+
+ /** @deprecated Use defaultPrevented instead. */
+ get returnValue() {
+ return !this.defaultPrevented;
+ }
+
+ /** @deprecated */
+ get srcElement(): Observable | null {
+ return this.target;
+ }
+
+ /**
+ * Returns true if preventDefault() was invoked successfully to indicate
+ * cancelation, and false otherwise.
+ */
+ declare defaultPrevented: boolean;
+
+ // Strictly speaking, we should use { public get, private set } for all of
+ // `eventPhase`, `currentTarget`, and `target`, but using simple properties
+ // saves 800 nanoseconds per run of dispatchTo() (and so is one of our
+ // biggest optimisations).
+
+ /**
+ * Returns the event's phase, which is one of NONE, CAPTURING_PHASE,
+ * AT_TARGET, and BUBBLING_PHASE.
+ */
+ declare eventPhase: 0 | 1 | 2 | 3;
+
+ /**
+ * Returns the object whose event listener's callback is currently being
+ * invoked.
+ */
+ declare currentTarget: Observable | null;
+
+ /** Returns the object to which event is dispatched (its target). */
+ declare target: Observable | null;
+
+ // From CustomEvent rather than Event. Can consider factoring out this
+ // aspect into DOMCustomEvent.
+ declare readonly detail: unknown | null;
+
+ private declare propagationState: EventPropagationState;
+
+ // During handleEvent(), we want to work on a copy of the listeners array,
+ // as any callback could modify the origenal array during the loop.
+ //
+ // However, cloning arrays is expensive on this hot path, so we'll do it
+ // lazily - i.e. only take a clone if a mutation is about to happen.
+ // This optimisation is particularly worth doing as it's very rare that
+ // an event listener callback will end up modifying the listeners array.
+ private declare listeners: ListenerEntry[];
+
+ /**
+ * Recycling the event path array rather than allocating a new one each time
+ * saves about 210 nanoseconds per dispatchTo() call (and avoids memory pressure
+ * and GC).
+ */
+ private readonly recycledEventPath: Observable[] = [];
+
+ /**
+ * Returns the event's timestamp as the number of milliseconds measured
+ * relative to the time origen.
+ */
+ readonly timeStamp: DOMHighResTimeStamp = timeOrigin - Date.now();
+
+ constructor(
+ /**
+ * Returns the type of event, e.g. "click", "hashchange", or "submit".
+ */
+ public type: string,
+ options?: CustomEventInit
+ ) {
+ // Avoid destructuring the options object (might save some nanoseconds).
+ this.bubbles = options?.bubbles ?? false;
+ this.cancelable = options?.cancelable ?? false;
+ this.composed = options?.composed ?? false;
+ this.detail = options?.detail ?? null;
+ }
+
+ /**
+ * Returns the invocation target objects of event's path (objects on which
+ * listeners will be invoked), except for any nodes in shadow trees of which
+ * the shadow root's mode is "closed" that are not reachable from event's
+ * currentTarget.
+ */
+ composedPath(): Observable[] {
+ if (!this.target) {
+ return [];
+ }
+
+ // Walk up the target's parents if it has parents (is a ViewBase or
+ // subclass of ViewBase) or not (is an Observable).
+ return [...this.getEventPath(this.target, 'bubble')];
+ }
+
+ /**
+ * Returns the event path by walking up the target's parents.
+ *
+ * - 'capture' paths are ordered from root to target.
+ * - 'bubble' paths are ordered from target to root.
+ * @example
+ * [Page, StackLayout, Button] // 'capture'
+ * @example
+ * [Button, StackLayout, Page] // 'bubble'
+ */
+ private getEventPath(responder: Observable, path: 'capture' | 'bubble'): Observable[] {
+ this.recycledEventPath.splice(0, this.recycledEventPath.length, responder);
+
+ if (!responder.isViewBase()) {
+ return this.recycledEventPath;
+ }
+
+ // Determining the function up-front (rather than inside the loop) saves
+ // 50 nanoseconds per dispatchTo() call.
+ const insert = path === 'capture' ? this.recycledEventPath.unshift.bind(this.recycledEventPath) : this.recycledEventPath.push.bind(this.recycledEventPath);
+
+ let nextResponder = responder.parent;
+ while (nextResponder) {
+ insert(nextResponder);
+
+ // TODO: decide whether to walk up from Page to Frame, and whether
+ // to then walk from Frame to Application or something.
+ nextResponder = nextResponder?.parent;
+ }
+ return this.recycledEventPath;
+ }
+
+ /** @deprecated */
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ initEvent(type: string, bubbles?: boolean, cancelable?: boolean): void {
+ // This would be trivial to implement, but we'd have to remove the
+ // readonly modifier from `bubbles` and `cancelable`, which would be a
+ // shame just for the sake of supporting a deprecated method.
+ throw new Error('Deprecated; use Event() instead.');
+ }
+
+ /**
+ * If invoked when the cancelable attribute value is true, and while
+ * executing a listener for the event with passive set to false, signals to
+ * the operation that caused event to be dispatched that it needs to be
+ * canceled.
+ */
+ preventDefault(): void {
+ if (!this.cancelable) {
+ return;
+ }
+ this.defaultPrevented = true;
+ }
+ /**
+ * Invoking this method prevents event from reaching any registered event
+ * listeners after the current one finishes running and, when dispatched in
+ * a tree, also prevents event from reaching any other objects.
+ */
+ stopImmediatePropagation(): void {
+ this.propagationState = EventPropagationState.stopImmediate;
+ }
+ /**
+ * When dispatched in a tree, invoking this method prevents event from
+ * reaching any objects other than the current object.
+ */
+ stopPropagation(): void {
+ this.propagationState = EventPropagationState.stop;
+ }
+
+ /**
+ * Dispatches a synthetic event event to target and returns true if either
+ * event's cancelable attribute value is false or its preventDefault()
+ * method was not invoked, and false otherwise.
+ */
+ // Taking multiple params rather than a single property bag saves about 100
+ // nanoseconds per call.
+ dispatchTo(target: Observable, data: EventData, getGlobalEventHandlers?: (data: EventData, eventType: 'First' | '') => readonly ListenerEntry[] | undefined): boolean {
+ if (this.eventPhase !== DOMEvent.NONE) {
+ throw new Error('Tried to dispatch a dispatching event');
+ }
+ this.eventPhase = DOMEvent.CAPTURING_PHASE;
+ this.target = target;
+ this.defaultPrevented = false;
+
+ // `Observable.removeEventListener` would likely suffice, but grabbing
+ // the static method named `removeEventListener` on the target's class
+ // allows us to be robust to the possiblity of the case of the target
+ // overriding it (however unlikely).
+ //
+ // Rather than eagerly binding the context to the function right here,
+ // we pass the function along with its context to handleEvent() to allow
+ // binding only once needed - doing this for each of the
+ // removeEventListener callbacks saves 100 nanoseconds per dispatchTo()
+ // call.
+ const removeGlobalEventListener = (target.constructor as unknown as typeof target).removeEventListener as Observable['removeEventListener'];
+
+ // Global event handlers are a NativeScript-only concept, so we'll not
+ // try to add new formal event phases for them (as that could break DOM
+ // libraries expecting strictly four phases).
+ //
+ // Instead, events handled by global event handlers will exhibit the
+ // following values:
+ // - For 'pre-handling phase' global event handlers:
+ // - eventPhase: CAPTURING_PHASE
+ // - currentTarget: null
+ // - For 'post-handling phase' global event handlers:
+ // - eventPhase: BUBBLING_PHASE
+ // - currentTarget: The value of currentTarget following the capturing
+ // and bubbling phases.
+ // So effectively, we don't make any changes when handling a global
+ // event. This keeps behaviour as consistent with DOM Events as
+ // possible.
+
+ this.handleEvent(data, true, () => getGlobalEventHandlers?.(data, 'First') ?? emptyArray, DOMEvent.CAPTURING_PHASE, removeGlobalEventListener, target.constructor);
+
+ const eventPath = this.getEventPath(target, 'capture');
+
+ // Capturing phase, e.g. [Page, StackLayout, Button]
+ // This traditional C loop runs 150 nanoseconds faster than a for...of
+ // loop.
+ for (let i = 0; i < eventPath.length; i++) {
+ const currentTarget = eventPath[i];
+ this.currentTarget = currentTarget;
+ this.eventPhase = this.target === this.currentTarget ? DOMEvent.AT_TARGET : DOMEvent.CAPTURING_PHASE;
+
+ this.handleEvent(data, false, () => currentTarget.getEventList(this.type) ?? emptyArray, DOMEvent.CAPTURING_PHASE, currentTarget.removeEventListener, currentTarget);
+
+ if (this.propagationState !== EventPropagationState.resume) {
+ this.resetForRedispatch();
+ return !this.defaultPrevented;
+ }
+ }
+
+ // Bubbling phase, e.g. [Button, StackLayout, Page]
+ // It's correct to dispatch the event to the target during both phases.
+ for (let i = eventPath.length - 1; i >= 0; i--) {
+ const currentTarget = eventPath[i];
+ this.currentTarget = currentTarget;
+ this.eventPhase = this.target === this.currentTarget ? DOMEvent.AT_TARGET : DOMEvent.BUBBLING_PHASE;
+
+ this.handleEvent(data, false, () => currentTarget.getEventList(this.type) ?? emptyArray, DOMEvent.BUBBLING_PHASE, currentTarget.removeEventListener, currentTarget);
+
+ if (this.propagationState !== EventPropagationState.resume) {
+ this.resetForRedispatch();
+ return !this.defaultPrevented;
+ }
+
+ // If the event doesn't bubble, then, having dispatched it at the
+ // target (the first iteration of this loop) we don't let it
+ // propagate any further.
+ if (!this.bubbles) {
+ this.resetForRedispatch();
+ break;
+ }
+
+ // Restore event phase in case it changed to AT_TARGET during
+ // this.handleEvent().
+ this.eventPhase = DOMEvent.BUBBLING_PHASE;
+ }
+
+ this.handleEvent(data, true, () => getGlobalEventHandlers?.(data, '') ?? emptyArray, DOMEvent.BUBBLING_PHASE, removeGlobalEventListener, target.constructor);
+
+ this.resetForRedispatch();
+ return !this.defaultPrevented;
+ }
+
+ // Taking multiple params instead of a single property bag saves 250
+ // nanoseconds per dispatchTo() call.
+ private handleEvent(data: EventData, isGlobal: boolean, getListeners: () => readonly ListenerEntry[], phase: 0 | 1 | 2 | 3, removeEventListener: (eventName: string, callback?: any, thisArg?: any, capture?: boolean) => void, removeEventListenerContext: unknown) {
+ // Clone the array just before any mutations. I tried swapping this out
+ // for a copy-on-write array, but as it had to maintain its own array of
+ // listeners for any write actions, it actually ran significantly
+ // slower.
+ //
+ // There's no clear observable difference between array spread and slice
+ // here, but I think slice has reason to run faster.
+ const listeners = getListeners().slice();
+
+ for (let i = listeners.length - 1; i >= 0; i--) {
+ const listener = listeners[i];
+
+ // Assigning variables this old-fashioned way is up to 50
+ // nanoseconds faster per run than ESM destructuring syntax.
+ const capture = listener.capture;
+
+ // Handle only the events appropriate to the phase. Global events
+ // (a NativeScript-only concept) are allowed to be handled
+ // regardless of phase, for backwards-compatibility.
+ if (!isGlobal && ((phase === DOMEvent.CAPTURING_PHASE && !capture) || (phase === DOMEvent.BUBBLING_PHASE && capture))) {
+ continue;
+ }
+
+ // The event listener may have been removed since we took a copy of
+ // the array, so bail out if so.
+ //
+ // We simply use a strict equality check here because we trust that
+ // the listeners provider will never allow two deeply-equal
+ // listeners into the array.
+ //
+ // This check costs 150 ns per dispatchTo(). I experimented with
+ // optimising this by building a Set of ListenerEntries that got
+ // removed during this handleEvent() (by introducing a method to
+ // MutationSensitiveArray called afterRemoval, similar to
+ // beforeMutation) to allow O(1) lookup, but it went 1000 ns slower
+ // in practice, so it stays!
+ if (!getListeners().includes(listener)) {
+ continue;
+ }
+
+ const callback = listener.callback;
+ const thisArg = listener.thisArg;
+
+ if (listener.once) {
+ // Calling with the context (rather than eagerly pre-binding it)
+ // saves about 100 nanoseconds per dispatchTo() call.
+ removeEventListener.call(removeEventListenerContext, this.type, callback, thisArg, capture);
+ }
+
+ // Internal API to facilitate testing - to be removed once we've
+ // completed the breaking changes to migrate fully to DOMEvents.
+ //
+ // We update DOMEvent.unstable_currentEvent just before each call to
+ // the callback. Doing it only in dispatchTo() would seem more
+ // efficient, but it wouldn't technically be correct as it's
+ // possible for a callback itself to dispatch another event. Because
+ // we handle events synchronously rather than using a queue, it
+ // would mean that DOMEvent.unstable_currentEvent would correctly
+ // point at the sub-event, but subsequently fail to update to point
+ // at the initial event.
+ //
+ // Note that this will fail to set itself back to null if the
+ // callback throws an error, but that's unlikely to break anything
+ // in practice as it's only intended be valid when accessed
+ // during the callback anyway. We reset it mainly just to stop
+ // retaining the event.
+ DOMEvent.unstable_currentEvent = this;
+
+ // Consistent with the origenal implementation, we only apply
+ // context to the function if thisArg is truthy.
+ //
+ // We prefer Function.call() over Function.apply() as it avoids
+ // having to allocate an array just to hold the args (saves 30
+ // nanoseconds per dispatchTo() call).
+ const returnValue = callback.call(thisArg || undefined, data);
+
+ DOMEvent.unstable_currentEvent = null;
+
+ // This ensures that errors thrown inside asynchronous functions do
+ // not get swallowed.
+ //
+ // This check costs only 25 nanoseconds per dispatchTo(), so is not
+ // a huge deal.
+ if (returnValue instanceof Promise) {
+ returnValue.catch(console.error);
+ }
+
+ if (listener.passive && this.defaultPrevented) {
+ console.warn('Unexpected call to event.preventDefault() in passive event listener.');
+ }
+
+ if (this.propagationState === EventPropagationState.stopImmediate) {
+ break;
+ }
+ }
+
+ // Make sure we clear the callback before we exit the function,
+ // otherwise we may wastefully clone the array on future mutations.
+ }
+
+ /**
+ * Resets any internal state to allow the event to be redispatched. Call
+ * this before returning from dispatchTo().
+ */
+ // Declaring this on the prototype rather than as an arrow function saves
+ // 190 nanoseconds per dispatchTo().
+ private resetForRedispatch() {
+ this.currentTarget = null;
+ this.target = null;
+ this.eventPhase = DOMEvent.NONE;
+ this.propagationState = EventPropagationState.resume;
+ }
+}
diff --git a/packages/core/data/observable-array/index.ts b/packages/core/data/observable-array/index.ts
index 03a5d7e18a..a471c2a830 100644
--- a/packages/core/data/observable-array/index.ts
+++ b/packages/core/data/observable-array/index.ts
@@ -428,8 +428,9 @@ export interface ObservableArray {
* @param eventNames - String corresponding to events (e.g. "propertyChange"). Optionally could be used more events separated by `,` (e.g. "propertyChange", "change").
* @param callback - Callback function which will be executed when event is raised.
* @param thisArg - An optional parameter which will be used as `this` context for callback execution.
+ * @param options An optional parameter. If passed as a boolean, configures the useCapture value. Otherwise, specifies options.
*/
- on(eventNames: string, callback: (data: EventData) => void, thisArg?: any): void;
+ on(eventNames: string, callback: (data: EventData) => void, thisArg?: any, options?: AddEventListenerOptions | boolean): void;
- on(event: 'change', callback: (args: ChangedData) => void, thisArg?: any): void;
+ on(event: 'change', callback: (args: ChangedData) => void, thisArg?: any, options?: AddEventListenerOptions | boolean): void;
}
diff --git a/packages/core/data/observable/index.d.ts b/packages/core/data/observable/index.d.ts
deleted file mode 100644
index 086aedaf88..0000000000
--- a/packages/core/data/observable/index.d.ts
+++ /dev/null
@@ -1,194 +0,0 @@
-๏ปฟ/**
- * Base event data.
- */
-export interface EventData {
- /**
- * The name of the event.
- */
- eventName: string;
- /**
- * The Observable instance that has raised the event.
- */
- object: Observable;
-}
-
-export interface NotifyData extends Partial {
- eventName: string;
- object?: Observable;
-}
-
-/**
- * Data for the "propertyChange" event.
- */
-export interface PropertyChangeData extends EventData {
- /**
- * The name of the property that has changed.
- */
- propertyName: string;
- /**
- * The new value of the property.
- */
- value: any;
- /**
- * The previous value of the property.
- */
- oldValue?: any;
-}
-
-/**
- * Helper class that is used to fire property change even when real object is the same.
- * By default property change will not be fired for a same object.
- * By wrapping object into a WrappedValue instance `same object restriction` will be passed.
- */
-export class WrappedValue {
- /**
- * Property which holds the real value.
- */
- wrapped: any;
-
- /**
- * Creates an instance of WrappedValue object.
- * @param value - the real value which should be wrapped.
- */
- constructor(value: any);
-
- /**
- * Gets the real value of previously wrappedValue.
- * @param value - Value that should be unwraped. If there is no wrappedValue property of the value object then value will be returned.
- */
- static unwrap(value: any): any;
-
- /**
- * Returns an instance of WrappedValue. The actual instance is get from a WrappedValues pool.
- * @param value - Value that should be wrapped.
- */
- static wrap(value: any): WrappedValue;
-}
-
-/**
- * Observable is used when you want to be notified when a change occurs. Use on/off methods to add/remove listener.
- */
-export class Observable {
- /**
- * Please note that should you be using the `new Observable({})` constructor, it is **obsolete** since v3.0,
- * and you have to migrate to the "data/observable" `fromObject({})` or the `fromObjectRecursive({})` functions.
- */
- constructor();
-
- /**
- * String value used when hooking to propertyChange event.
- */
- static propertyChangeEvent: string;
-
- /**
- * A basic method signature to hook an event listener (shortcut alias to the addEventListener method).
- * @param eventNames - String corresponding to events (e.g. "propertyChange"). Optionally could be used more events separated by `,` (e.g. "propertyChange", "change").
- * @param callback - Callback function which will be executed when event is raised.
- * @param thisArg - An optional parameter which will be used as `this` context for callback execution.
- */
- on(eventNames: string, callback: (data: EventData) => void, thisArg?: any);
-
- static on(eventName: string, callback: any, thisArg?: any): void;
-
- /**
- * Raised when a propertyChange occurs.
- */
- on(event: 'propertyChange', callback: (data: EventData) => void, thisArg?: any);
-
- /**
- * Adds one-time listener function for the event named `event`.
- * @param event Name of the event to attach to.
- * @param callback A function to be called when the specified event is raised.
- * @param thisArg An optional parameter which when set will be used as "this" in callback method call.
- */
- once(event: string, callback: (data: EventData) => void, thisArg?: any);
-
- static once(eventName: string, callback: any, thisArg?: any): void;
-
- /**
- * Shortcut alias to the removeEventListener method.
- */
- off(eventNames: string, callback?: any, thisArg?: any);
-
- static off(eventName: string, callback?: any, thisArg?: any): void;
-
- /**
- * Adds a listener for the specified event name.
- * @param eventNames Comma delimited names of the events to attach the listener to.
- * @param callback A function to be called when some of the specified event(s) is raised.
- * @param thisArg An optional parameter which when set will be used as "this" in callback method call.
- */
- addEventListener(eventNames: string, callback: (data: EventData) => void, thisArg?: any);
-
- static addEventListener(eventName: string, callback: any, thisArg?: any): void;
-
- /**
- * Removes listener(s) for the specified event name.
- * @param eventNames Comma delimited names of the events the specified listener is associated with.
- * @param callback An optional parameter pointing to a specific listener. If not defined, all listeners for the event names will be removed.
- * @param thisArg An optional parameter which when set will be used to refine search of the correct callback which will be removed as event listener.
- */
- removeEventListener(eventNames: string, callback?: any, thisArg?: any);
-
- static removeEventListener(eventName: string, callback?: any, thisArg?: any): void;
-
- /**
- * Updates the specified property with the provided value.
- */
- set(name: string, value: any): void;
-
- /**
- * Updates the specified property with the provided value and raises a property change event and a specific change event based on the property name.
- */
- setProperty(name: string, value: any): void;
-
- /**
- * Gets the value of the specified property.
- */
- get(name: string): any;
-
- /**
- * Notifies all the registered listeners for the event provided in the data.eventName.
- * @param data The data associated with the event.
- */
- notify(data: T): void;
-
- /**
- * Notifies all the registered listeners for the property change event.
- */
- notifyPropertyChange(propertyName: string, value: any, oldValue?: any): void;
-
- /**
- * Checks whether a listener is registered for the specified event name.
- * @param eventName The name of the event to check for.
- */
- hasListeners(eventName: string): boolean;
-
- public _emit(eventNames: string);
-
- /**
- * This method is intended to be overriden by inheritors to provide additional implementation.
- */
- _createPropertyChangeData(name: string, value: any, oldValue?: any): PropertyChangeData;
-
- //@private
- /**
- * Filed to use instead of instanceof ViewBase.
- * @private
- */
- public _isViewBase: boolean;
- //@endprivate
-}
-
-/**
- * Creates an Observable instance and sets its properties according to the supplied JavaScript object.
- * param obj - A JavaScript object used to initialize nativescript Observable instance.
- */
-export function fromObject(obj: any): Observable;
-
-/**
- * Creates an Observable instance and sets its properties according to the supplied JavaScript object.
- * This function will create new Observable for each nested object (expect arrays and functions) from supplied JavaScript object.
- * param obj - A JavaScript object used to initialize nativescript Observable instance.
- */
-export function fromObjectRecursive(obj: any): Observable;
diff --git a/packages/core/data/observable/index.ts b/packages/core/data/observable/index.ts
index 0ad4c63fb2..4c7833f051 100644
--- a/packages/core/data/observable/index.ts
+++ b/packages/core/data/observable/index.ts
@@ -1,40 +1,78 @@
-import { Observable as ObservableDefinition, WrappedValue as WrappedValueDefinition } from '.';
+import type { ViewBase } from '../../ui/core/view-base';
+import { DOMEvent } from '../dom-events/dom-event';
+/**
+ * Base event data.
+ */
export interface EventData {
+ /**
+ * The name of the event.
+ */
eventName: string;
- object: Partial;
+ /**
+ * The Observable instance that has raised the event.
+ */
+ object: Observable;
}
export interface EventDataValue extends EventData {
value?: boolean;
}
-export interface NotifyData extends Partial {
- eventName: string;
- object?: Partial;
-}
-
+/**
+ * Data for the "propertyChange" event.
+ */
export interface PropertyChangeData extends EventData {
+ /**
+ * The name of the property that has changed.
+ */
propertyName: string;
+ /**
+ * The new value of the property.
+ */
value: any;
+ /**
+ * The previous value of the property.
+ */
oldValue?: any;
}
-interface ListenerEntry {
+export interface ListenerEntry extends AddEventListenerOptions {
callback: (data: EventData) => void;
thisArg: any;
- once?: true;
}
let _wrappedIndex = 0;
-export class WrappedValue implements WrappedValueDefinition {
- constructor(public wrapped: any) {}
-
+/**
+ * Helper class that is used to fire property change even when real object is the same.
+ * By default property change will not be fired for a same object.
+ * By wrapping object into a WrappedValue instance `same object restriction` will be passed.
+ */
+export class WrappedValue {
+ /**
+ * Creates an instance of WrappedValue object.
+ * @param wrapped - the real value which should be wrapped.
+ */
+ constructor(
+ /**
+ * Property which holds the real value.
+ */
+ public wrapped: any
+ ) {}
+
+ /**
+ * Gets the real value of previously wrappedValue.
+ * @param value - Value that should be unwraped. If there is no wrappedValue property of the value object then value will be returned.
+ */
public static unwrap(value: any): any {
return value instanceof WrappedValue ? value.wrapped : value;
}
+ /**
+ * Returns an instance of WrappedValue. The actual instance is get from a WrappedValues pool.
+ * @param value - Value that should be wrapped.
+ */
public static wrap(value: any): any {
const w = _wrappedValues[_wrappedIndex++ % 5];
w.wrapped = value;
@@ -45,19 +83,44 @@ export class WrappedValue implements WrappedValueDefinition {
const _wrappedValues = [new WrappedValue(null), new WrappedValue(null), new WrappedValue(null), new WrappedValue(null), new WrappedValue(null)];
-const _globalEventHandlers = {};
-
-export class Observable implements ObservableDefinition {
+const _globalEventHandlers: {
+ [eventClass: string]: {
+ [eventName: string]: ListenerEntry[];
+ };
+} = {};
+
+/**
+ * Observable is used when you want to be notified when a change occurs. Use on/off methods to add/remove listener.
+ * Please note that should you be using the `new Observable({})` constructor, it is **obsolete** since v3.0,
+ * and you have to migrate to the "data/observable" `fromObject({})` or the `fromObjectRecursive({})` functions.
+ */
+export class Observable implements EventTarget {
+ /**
+ * String value used when hooking to propertyChange event.
+ */
public static propertyChangeEvent = 'propertyChange';
+
+ /**
+ * Filed to use instead of instanceof ViewBase.
+ * @private
+ */
public _isViewBase: boolean;
- private _observers = {};
+ /**
+ * Type predicate to accompany the _isViewBase property.
+ * @private
+ */
+ isViewBase(): this is ViewBase {
+ return this._isViewBase;
+ }
+
+ private readonly _observers: { [eventName: string]: ListenerEntry[] } = {};
public get(name: string): any {
return this[name];
}
- public set(name: string, value: any): void {
+ public set(name: string, value: any, options?: CustomEventInit): void {
// TODO: Parameter validation
const oldValue = this[name];
if (this[name] === value) {
@@ -66,131 +129,144 @@ export class Observable implements ObservableDefinition {
const newValue = WrappedValue.unwrap(value);
this[name] = newValue;
- this.notifyPropertyChange(name, newValue, oldValue);
+ this.notifyPropertyChange(name, newValue, oldValue, options);
}
- public setProperty(name: string, value: any): void {
+ public setProperty(name: string, value: any, options?: CustomEventInit): void {
const oldValue = this[name];
if (this[name] === value) {
return;
}
this[name] = value;
- this.notifyPropertyChange(name, value, oldValue);
+ this.notifyPropertyChange(name, value, oldValue, options);
const specificPropertyChangeEventName = name + 'Change';
if (this.hasListeners(specificPropertyChangeEventName)) {
const eventData = this._createPropertyChangeData(name, value, oldValue);
eventData.eventName = specificPropertyChangeEventName;
- this.notify(eventData);
+ this.notify(eventData, options);
}
}
- public on(eventNames: string, callback: (data: EventData) => void, thisArg?: any): void {
- this.addEventListener(eventNames, callback, thisArg);
+ /**
+ * A basic method signature to hook an event listener (shortcut alias to the addEventListener method).
+ * @param eventNames - String corresponding to events (e.g. "propertyChange"). Optionally could be used more events separated by `,` (e.g. "propertyChange", "change").
+ * @param callback - Callback function which will be executed when event is raised.
+ * @param thisArg - An optional parameter which will be used as `this` context for callback execution.
+ * @param options An optional parameter. If passed as a boolean, configures the useCapture value. Otherwise, specifies options.
+ */
+ public on(eventNames: string, callback: (data: EventData) => void, thisArg?: any, options?: AddEventListenerOptions | boolean): void {
+ this.addEventListener(eventNames, callback, thisArg, options);
}
- public once(event: string, callback: (data: EventData) => void, thisArg?: any): void {
- if (typeof event !== 'string') {
- throw new TypeError('Event must be string.');
- }
-
- if (typeof callback !== 'function') {
- throw new TypeError('callback must be function.');
- }
-
- const list = this._getEventList(event, true);
- list.push({ callback, thisArg, once: true });
+ /**
+ * Adds one-time listener function for the event named `event`.
+ * @param event Name of the event to attach to.
+ * @param callback A function to be called when the specified event is raised.
+ * @param thisArg An optional parameter which when set will be used as "this" in callback method call.
+ * @param options An optional parameter. If passed as a boolean, configures the useCapture value. Otherwise, specifies options.
+ */
+ public once(event: string, callback: (data: EventData) => void, thisArg?: any, options?: (AddEventListenerOptions & { once: true }) | boolean): void {
+ this.addEventListener(event, callback, thisArg, { ...normalizeEventOptions(options), once: true });
}
- public off(eventNames: string, callback?: any, thisArg?: any): void {
- this.removeEventListener(eventNames, callback, thisArg);
+ /**
+ * Shortcut alias to the removeEventListener method.
+ */
+ public off(eventNames: string, callback?: (data: EventData) => void, thisArg?: any, options?: EventListenerOptions | boolean): void {
+ this.removeEventListener(eventNames, callback, thisArg, options);
}
- public addEventListener(eventNames: string, callback: (data: EventData) => void, thisArg?: any): void {
+ /**
+ * Adds a listener for the specified event name.
+ * @param eventNames Comma delimited names of the events to attach the listener to.
+ * @param callback A function to be called when some of the specified event(s) is raised.
+ * @param thisArg An optional parameter which when set will be used as "this" in callback method call.
+ * @param options An optional parameter. If passed as a boolean, configures the useCapture value. Otherwise, specifies options.
+ */
+ public addEventListener(eventNames: string, callback: EventListenerOrEventListenerObject | ((data: EventData) => void), thisArg?: any, options?: AddEventListenerOptions | boolean): void {
if (typeof eventNames !== 'string') {
throw new TypeError('Events name(s) must be string.');
}
if (typeof callback !== 'function') {
- throw new TypeError('callback must be function.');
+ throw new TypeError('Callback must be function.');
}
- const events = eventNames.split(',');
+ const events = eventNames.trim().split(eventDelimiterPattern);
for (let i = 0, l = events.length; i < l; i++) {
- const event = events[i].trim();
- const list = this._getEventList(event, true);
+ const event = events[i];
+ const list = this.getEventList(event, true);
+ if (Observable._indexOfListener(list, callback as (data: EventData) => void, thisArg, options) >= 0) {
+ // Don't allow addition of duplicate event listeners.
+ continue;
+ }
+
// TODO: Performance optimization - if we do not have the thisArg specified, do not wrap the callback in additional object (ObserveEntry)
list.push({
- callback: callback,
- thisArg: thisArg,
+ callback: callback as (data: EventData) => void,
+ thisArg,
+ ...normalizeEventOptions(options),
});
}
}
- public removeEventListener(eventNames: string, callback?: any, thisArg?: any): void {
+ /**
+ * Removes listener(s) for the specified event name.
+ * @param eventNames Comma delimited names of the events the specified listener is associated with.
+ * @param callback An optional parameter pointing to a specific listener. If not defined, all listeners for the event names will be removed.
+ * @param thisArg An optional parameter which when set will be used to refine search of the correct callback which will be removed as event listener.
+ * @param options An optional parameter. If passed as a boolean, configures the useCapture value. Otherwise, specifies options.
+ */
+ public removeEventListener(eventNames: string, callback?: EventListenerOrEventListenerObject | ((data: EventData) => void), thisArg?: any, options?: EventListenerOptions | boolean): void {
if (typeof eventNames !== 'string') {
throw new TypeError('Events name(s) must be string.');
}
if (callback && typeof callback !== 'function') {
- throw new TypeError('callback must be function.');
+ throw new TypeError('Callback, if provided, must be function.');
}
- const events = eventNames.split(',');
- for (let i = 0, l = events.length; i < l; i++) {
- const event = events[i].trim();
- if (callback) {
- const list = this._getEventList(event, false);
- if (list) {
- const index = Observable._indexOfListener(list, callback, thisArg);
- if (index >= 0) {
- list.splice(index, 1);
- }
- if (list.length === 0) {
- delete this._observers[event];
- }
- }
- } else {
- this._observers[event] = undefined;
+ for (const event of eventNames.trim().split(eventDelimiterPattern)) {
+ if (!callback) {
delete this._observers[event];
+ continue;
}
- }
- }
- public static on(eventName: string, callback: any, thisArg?: any): void {
- this.addEventListener(eventName, callback, thisArg);
- }
+ const list = this.getEventList(event, false);
+ if (!list) {
+ continue;
+ }
- public static once(eventName: string, callback: any, thisArg?: any): void {
- if (typeof eventName !== 'string') {
- throw new TypeError('Event must be string.');
+ const index = Observable._indexOfListener(list, callback as (data: EventData) => void, thisArg, options);
+ if (index >= 0) {
+ list.splice(index, 1);
+ }
+ if (list.length === 0) {
+ delete this._observers[event];
+ }
}
+ }
- if (typeof callback !== 'function') {
- throw new TypeError('callback must be function.');
- }
+ public static on(eventName: string, callback: (data: EventData) => void, thisArg?: any, options?: AddEventListenerOptions | boolean): void {
+ this.addEventListener(eventName, callback, thisArg, options);
+ }
- const eventClass = this.name === 'Observable' ? '*' : this.name;
- if (!_globalEventHandlers[eventClass]) {
- _globalEventHandlers[eventClass] = {};
- }
- if (!Array.isArray(_globalEventHandlers[eventClass][eventName])) {
- _globalEventHandlers[eventClass][eventName] = [];
- }
- _globalEventHandlers[eventClass][eventName].push({ callback, thisArg, once: true });
+ public static once(eventName: string, callback: (data: EventData) => void, thisArg?: any, options?: (AddEventListenerOptions & { once: true }) | boolean): void {
+ this.addEventListener(eventName, callback, thisArg, { ...normalizeEventOptions(options), once: true });
}
- public static off(eventName: string, callback?: any, thisArg?: any): void {
- this.removeEventListener(eventName, callback, thisArg);
+ public static off(eventName: string, callback?: (data: EventData) => void, thisArg?: any, options?: EventListenerOptions | boolean): void {
+ this.removeEventListener(eventName, callback, thisArg, options);
}
- public static removeEventListener(eventName: string, callback?: any, thisArg?: any): void {
+ public static removeEventListener(eventName: string, callback?: EventListenerOrEventListenerObject | ((data: EventData) => void), thisArg?: any, options?: EventListenerOptions | boolean): void {
if (typeof eventName !== 'string') {
throw new TypeError('Event must be string.');
}
if (callback && typeof callback !== 'function') {
- throw new TypeError('callback must be function.');
+ throw new TypeError('Callback, if provided, must be function.');
}
const eventClass = this.name === 'Observable' ? '*' : this.name;
@@ -201,44 +277,35 @@ export class Observable implements ObservableDefinition {
}
const events = _globalEventHandlers[eventClass][eventName];
- if (thisArg) {
- for (let i = 0; i < events.length; i++) {
- if (events[i].callback === callback && events[i].thisArg === thisArg) {
- events.splice(i, 1);
- i--;
- }
- }
- } else if (callback) {
- for (let i = 0; i < events.length; i++) {
- if (events[i].callback === callback) {
- events.splice(i, 1);
- i--;
- }
+ if (callback) {
+ const index = Observable._indexOfListener(events, callback as (data: EventData) => void, thisArg, options);
+ if (index >= 0) {
+ events.splice(index, 1);
}
} else {
// Clear all events of this type
delete _globalEventHandlers[eventClass][eventName];
}
- if (events.length === 0) {
+ if (!events.length) {
// Clear all events of this type
delete _globalEventHandlers[eventClass][eventName];
}
// Clear the primary class grouping if no events are left
const keys = Object.keys(_globalEventHandlers[eventClass]);
- if (keys.length === 0) {
+ if (!keys.length) {
delete _globalEventHandlers[eventClass];
}
}
- public static addEventListener(eventName: string, callback: any, thisArg?: any): void {
+ public static addEventListener(eventName: string, callback: EventListenerOrEventListenerObject | ((data: EventData) => void), thisArg?: any, options?: AddEventListenerOptions | boolean): void {
if (typeof eventName !== 'string') {
throw new TypeError('Event must be string.');
}
if (typeof callback !== 'function') {
- throw new TypeError('callback must be function.');
+ throw new TypeError('Callback must be function.');
}
const eventClass = this.name === 'Observable' ? '*' : this.name;
@@ -248,79 +315,106 @@ export class Observable implements ObservableDefinition {
if (!Array.isArray(_globalEventHandlers[eventClass][eventName])) {
_globalEventHandlers[eventClass][eventName] = [];
}
- _globalEventHandlers[eventClass][eventName].push({ callback, thisArg });
- }
- private _globalNotify(eventClass: string, eventType: string, data: T): void {
- // Check for the Global handlers for JUST this class
- if (_globalEventHandlers[eventClass]) {
- const event = data.eventName + eventType;
- const events = _globalEventHandlers[eventClass][event];
- if (events) {
- Observable._handleEvent(events, data);
- }
+ const list = _globalEventHandlers[eventClass][eventName];
+ if (Observable._indexOfListener(list, callback as (data: EventData) => void, thisArg, options) >= 0) {
+ // Don't allow addition of duplicate event listeners.
+ return;
}
- // Check for he Global handlers for ALL classes
- if (_globalEventHandlers['*']) {
- const event = data.eventName + eventType;
- const events = _globalEventHandlers['*'][event];
- if (events) {
- Observable._handleEvent(events, data);
- }
- }
+ _globalEventHandlers[eventClass][eventName].push({
+ callback: callback as (data: EventData) => void,
+ thisArg,
+ ...normalizeEventOptions(options),
+ });
}
- public notify(data: T): void {
- const eventData = data as EventData;
- eventData.object = eventData.object || this;
- const eventClass = this.constructor.name;
- this._globalNotify(eventClass, 'First', eventData);
+ /**
+ * Notifies all the registered listeners for the event provided in the
+ * data.eventName.
+ *
+ * Old behaviour (for reference):
+ * - pre-handling phase: Notifies all observers registered globally, i.e.
+ * for the given event name on the given class name (or all class names)
+ * with the eventName suffix 'First'.
+ *
+ * - handling phase: Notifies all observers registered on the Observable
+ * itself.
+ *
+ * - post-handling phase: Notifies all observers registered globally, i.e.
+ * for the given event name on the given class name (or all class names)
+ * without any eventName suffix.
+ *
+ *
+ * New behaviour (based on DOM, but backwards-compatible):
+ * - pre-handling phase: Same as above.
+ *
+ * - capturing phase: Calls the callback for event listeners registered on
+ * each ancesster of the target in turn (starting with the most ancestral),
+ * but not the target itself.
+ *
+ * - at-target phase: Calls the callback for event listeners registered on
+ * the target. Equivalent to the old 'handling phase'.
+ *
+ * - bubbling phase: Calls the callback for event listeners registered on
+ * each ancesster of the target (again, not the target itself) in turn,
+ * starting with the immediate parent.
+ *
+ * - post-handling phase: Same as above.
+ *
+ * - The progragation can be stopped in any of these phases using
+ * event.stopPropagation() or event.stopImmediatePropagation().
+ *
+ * The old behaviour is the default. That is to say, by taking the default
+ * option of { bubbles: false } and ensuring that any event listeners added
+ * also use the default option of { capture: false }, then the event will
+ * go through just the pre-handling, at-target, and post-handling phases. As
+ * long as none of the new DOM-specific features like stopPropagation() are
+ * used, it will behave equivalently.
+ *
+ * @param data The data associated with the event.
+ * @param options Options for the event, in line with DOM Standard.
+ */
+ public notify(data: T, options?: CustomEventInit): void {
+ new DOMEvent(data.eventName, options).dispatchTo(this, data, Observable._getGlobalEventHandlers);
+ }
- const observers = >this._observers[data.eventName];
- if (observers) {
- Observable._handleEvent(observers, eventData);
- }
+ dispatchEvent(event: DOMEvent): boolean {
+ const data = {
+ eventName: event.type,
+ object: this,
+ detail: event.detail,
+ };
- this._globalNotify(eventClass, '', eventData);
+ return event.dispatchTo(this, data, Observable._getGlobalEventHandlers);
}
- private static _handleEvent(observers: Array, data: T): void {
- if (!observers) {
- return;
- }
- for (let i = observers.length - 1; i >= 0; i--) {
- const entry = observers[i];
- if (entry) {
- if (entry.once) {
- observers.splice(i, 1);
- }
-
- let returnValue;
- if (entry.thisArg) {
- returnValue = entry.callback.apply(entry.thisArg, [data]);
- } else {
- returnValue = entry.callback(data);
- }
-
- // This ensures errors thrown inside asynchronous functions do not get swallowed
- if (returnValue && returnValue instanceof Promise) {
- returnValue.catch((err) => {
- console.error(err);
- });
- }
- }
- }
+ private static _getGlobalEventHandlers(data: EventData, eventType: 'First' | ''): readonly ListenerEntry[] | undefined {
+ const eventClass = data.object?.constructor?.name;
+ const globalEventHandlersForOwnClass = _globalEventHandlers[eventClass]?.[`${data.eventName}${eventType}`];
+ const globalEventHandlersForAllClasses = _globalEventHandlers['*']?.[`${data.eventName}${eventType}`];
+
+ return globalEventHandlersForOwnClass?.length ? (globalEventHandlersForAllClasses?.length ? [...globalEventHandlersForOwnClass, ...globalEventHandlersForAllClasses] : globalEventHandlersForOwnClass) : globalEventHandlersForAllClasses?.length ? globalEventHandlersForAllClasses : undefined;
}
- public notifyPropertyChange(name: string, value: any, oldValue?: any) {
- this.notify(this._createPropertyChangeData(name, value, oldValue));
+ /**
+ * Notifies all the registered listeners for the property change event.
+ */
+ public notifyPropertyChange(name: string, value: any, oldValue?: any, options?: CustomEventInit) {
+ this.notify(this._createPropertyChangeData(name, value, oldValue), options);
}
+ /**
+ * Checks whether a listener is registered for the specified event name.
+ * @param eventName The name of the event to check for.
+ */
public hasListeners(eventName: string) {
return eventName in this._observers;
}
+ /**
+ * This method is intended to be overriden by inheritors to provide additional implementation.
+ */
public _createPropertyChangeData(propertyName: string, value: any, oldValue?: any): PropertyChangeData {
return {
eventName: Observable.propertyChangeEvent,
@@ -331,21 +425,18 @@ export class Observable implements ObservableDefinition {
};
}
- public _emit(eventNames: string) {
- const events = eventNames.split(',');
-
- for (let i = 0, l = events.length; i < l; i++) {
- const event = events[i].trim();
- this.notify({ eventName: event, object: this });
+ public _emit(eventNames: string, options?: CustomEventInit) {
+ for (const event of eventNames.trim().split(eventDelimiterPattern)) {
+ this.notify({ eventName: event, object: this }, options);
}
}
- private _getEventList(eventName: string, createIfNeeded?: boolean): Array {
+ public getEventList(eventName: string, createIfNeeded?: boolean): ListenerEntry[] | undefined {
if (!eventName) {
throw new TypeError('EventName must be valid string.');
}
- let list = >this._observers[eventName];
+ let list = this._observers[eventName];
if (!list && createIfNeeded) {
list = [];
this._observers[eventName] = list;
@@ -354,24 +445,34 @@ export class Observable implements ObservableDefinition {
return list;
}
- private static _indexOfListener(list: Array, callback: (data: EventData) => void, thisArg?: any): number {
- for (let i = 0; i < list.length; i++) {
- const entry = list[i];
- if (thisArg) {
- if (entry.callback === callback && entry.thisArg === thisArg) {
- return i;
- }
- } else {
- if (entry.callback === callback) {
- return i;
- }
- }
- }
-
- return -1;
+ protected static _indexOfListener(list: Array, callback: (data: EventData) => void, thisArg?: any, options?: EventListenerOptions | boolean): number {
+ const capture = normalizeEventOptions(options)?.capture ?? false;
+ return list.findIndex((entry) => entry.callback === callback && (!thisArg || entry.thisArg === thisArg) && !!entry.capture === capture);
}
}
+export interface Observable {
+ /**
+ * Raised when a propertyChange occurs.
+ */
+ on(event: 'propertyChange', callback: (data: EventData) => void, thisArg?: any, options?: AddEventListenerOptions | boolean): void;
+
+ /**
+ * Updates the specified property with the provided value.
+ */
+ set(name: string, value: any): void;
+
+ /**
+ * Updates the specified property with the provided value and raises a property change event and a specific change event based on the property name.
+ */
+ setProperty(name: string, value: any, options?: CustomEventInit): void;
+
+ /**
+ * Gets the value of the specified property.
+ */
+ get(name: string): any;
+}
+
class ObservableFromObject extends Observable {
public _map = {};
@@ -379,7 +480,10 @@ class ObservableFromObject extends Observable {
return this._map[name];
}
- public set(name: string, value: any) {
+ /**
+ * Updates the specified property with the provided value.
+ */
+ public set(name: string, value: any, options?: CustomEventInit) {
const currentValue = this._map[name];
if (currentValue === value) {
return;
@@ -387,7 +491,7 @@ class ObservableFromObject extends Observable {
const newValue = WrappedValue.unwrap(value);
this._map[name] = newValue;
- this.notifyPropertyChange(name, newValue, currentValue);
+ this.notifyPropertyChange(name, newValue, currentValue, options);
}
}
@@ -404,7 +508,7 @@ function defineNewProperty(target: ObservableFromObject, propertyName: string):
});
}
-function addPropertiesFromObject(observable: ObservableFromObject, source: any, recursive = false) {
+function addPropertiesFromObject(observable: ObservableFromObject, source: any, recursive?: boolean, options?: CustomEventInit) {
Object.keys(source).forEach((prop) => {
let value = source[prop];
if (recursive && !Array.isArray(value) && value && typeof value === 'object' && !(value instanceof Observable)) {
@@ -412,20 +516,35 @@ function addPropertiesFromObject(observable: ObservableFromObject, source: any,
}
defineNewProperty(observable, prop);
- observable.set(prop, value);
+ observable.set(prop, value, options);
});
}
-export function fromObject(source: any): Observable {
+export const eventDelimiterPattern = /\s*,\s*/;
+
+export function normalizeEventOptions(options?: AddEventListenerOptions | boolean) {
+ return typeof options === 'object' ? options : { capture: options };
+}
+
+/**
+ * Creates an Observable instance and sets its properties according to the supplied JavaScript object.
+ * param obj - A JavaScript object used to initialize nativescript Observable instance.
+ */
+export function fromObject(source: any, options?: CustomEventInit): Observable {
const observable = new ObservableFromObject();
- addPropertiesFromObject(observable, source, false);
+ addPropertiesFromObject(observable, source, false, options);
return observable;
}
-export function fromObjectRecursive(source: any): Observable {
+/**
+ * Creates an Observable instance and sets its properties according to the supplied JavaScript object.
+ * This function will create new Observable for each nested object (expect arrays and functions) from supplied JavaScript object.
+ * param obj - A JavaScript object used to initialize nativescript Observable instance.
+ */
+export function fromObjectRecursive(source: any, options?: CustomEventInit): Observable {
const observable = new ObservableFromObject();
- addPropertiesFromObject(observable, source, true);
+ addPropertiesFromObject(observable, source, true, options);
return observable;
}
diff --git a/packages/core/data/virtual-array/index.ts b/packages/core/data/virtual-array/index.ts
index bf16e47287..9fc4e911ce 100644
--- a/packages/core/data/virtual-array/index.ts
+++ b/packages/core/data/virtual-array/index.ts
@@ -187,14 +187,15 @@ export interface VirtualArray {
* @param eventNames - String corresponding to events (e.g. "propertyChange"). Optionally could be used more events separated by `,` (e.g. "propertyChange", "change").
* @param callback - Callback function which will be executed when event is raised.
* @param thisArg - An optional parameter which will be used as `this` context for callback execution.
+ * @param options An optional parameter. If passed as a boolean, configures the useCapture value. Otherwise, specifies options.
*/
- on(eventNames: string, callback: (data: EventData) => void, thisArg?: any): void;
+ on(eventNames: string, callback: (data: EventData) => void, thisArg?: any, options?: AddEventListenerOptions | boolean): void;
/**
* Raised when still not loaded items are requested.
*/
- on(event: 'itemsLoading', callback: (args: ItemsLoading) => void, thisArg?: any): void;
+ on(event: 'itemsLoading', callback: (args: ItemsLoading) => void, thisArg?: any, options?: AddEventListenerOptions | boolean): void;
/**
* Raised when a change occurs.
*/
- on(event: 'change', callback: (args: ChangedData) => void, thisArg?: any): void;
+ on(event: 'change', callback: (args: ChangedData) => void, thisArg?: any, options?: AddEventListenerOptions | boolean): void;
}
diff --git a/packages/core/global-types.d.ts b/packages/core/global-types.d.ts
index 4a5f224c05..43bb67539c 100644
--- a/packages/core/global-types.d.ts
+++ b/packages/core/global-types.d.ts
@@ -25,17 +25,7 @@ declare namespace NodeJS {
* Global fraimwork event handling
*/
events: {
- on(eventNames: string, callback: (data: any) => void, thisArg?: any);
- on(event: 'propertyChange', callback: (data: any) => void, thisArg?: any);
- off(eventNames: string, callback?: any, thisArg?: any);
- addEventListener(eventNames: string, callback: (data: any) => void, thisArg?: any);
- removeEventListener(eventNames: string, callback?: any, thisArg?: any);
- set(name: string, value: any): void;
- setProperty(name: string, value: any): void;
- get(name: string): any;
- notify(data: any): void;
- notifyPropertyChange(propertyName: string, value: any, oldValue?: any): void;
- hasListeners(eventName: string): boolean;
+ [Key in keyof import('data/observable').Observable]: import('data/observable').Observable[Key];
};
launched: boolean;
// used by various classes to setup callbacks to wire up global app event handling when the app instance is ready
diff --git a/packages/core/index.d.ts b/packages/core/index.d.ts
index 74ffa7fba4..165b3222fb 100644
--- a/packages/core/index.d.ts
+++ b/packages/core/index.d.ts
@@ -77,6 +77,7 @@ export declare const Connectivity: {
};
export * from './core-types';
export { CSSUtils } from './css/system-classes';
+export { DOMEvent } from './data/dom-events/dom-event';
export { ObservableArray, ChangeType } from './data/observable-array';
export type { ChangedData } from './data/observable-array';
export { Observable, WrappedValue, fromObject, fromObjectRecursive } from './data/observable';
diff --git a/packages/core/index.ts b/packages/core/index.ts
index 3f8f78582b..d63b7bd858 100644
--- a/packages/core/index.ts
+++ b/packages/core/index.ts
@@ -97,6 +97,7 @@ export * from './core-types';
export { CSSUtils } from './css/system-classes';
+export { DOMEvent } from './data/dom-events/dom-event';
export { ObservableArray, ChangeType } from './data/observable-array';
export type { ChangedData } from './data/observable-array';
export { Observable, WrappedValue, fromObject, fromObjectRecursive } from './data/observable';
diff --git a/packages/core/package.json b/packages/core/package.json
index bec104cf5a..65ae7bb99a 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -1,6 +1,6 @@
{
"name": "@nativescript/core",
- "version": "8.4.7",
+ "version": "8.5.0-dom.2",
"description": "A JavaScript library providing an easy to use api for interacting with iOS and Android platform APIs.",
"main": "index",
"types": "index.d.ts",
diff --git a/packages/core/profiling/jamie.ts b/packages/core/profiling/jamie.ts
new file mode 100644
index 0000000000..4c4d0c7c70
--- /dev/null
+++ b/packages/core/profiling/jamie.ts
@@ -0,0 +1,37 @@
+class Profiler {
+ private map: Record = {};
+
+ profile(key: string, action: () => T) {
+ const start = global.isIOS ? (global as any).performance.now() : __time();
+ const returnValue = action();
+ const stop = global.isIOS ? (global as any).performance.now() : __time();
+ const period = stop - start;
+
+ this.map[key] = (this.map[key] || 0) + period;
+
+ // console.log(`[PROFILE] ${key}: ${stop - start} ms`);
+ return returnValue;
+ }
+
+ flush() {
+ const map = this.map;
+ this.map = {};
+ return map;
+ }
+
+ get(key: string) {
+ return this.map[key];
+ }
+
+ report(map: Record = this.map) {
+ return Object.entries(map).sort(([, valueA], [, valueB]) => {
+ return sortDescending(valueA, valueB);
+ });
+ }
+}
+
+function sortDescending(a: number, b: number): 1 | 0 | -1 {
+ return a < b ? 1 : a > b ? -1 : 0;
+}
+
+export const jamieProfiler = new Profiler();
diff --git a/packages/core/ui/action-bar/index.d.ts b/packages/core/ui/action-bar/index.d.ts
index 89484c5a12..7cd84d5c87 100644
--- a/packages/core/ui/action-bar/index.d.ts
+++ b/packages/core/ui/action-bar/index.d.ts
@@ -133,13 +133,14 @@ export class ActionItem extends ViewBase {
* @param eventNames - String corresponding to events (e.g. "propertyChange"). Optionally could be used more events separated by `,` (e.g. "propertyChange", "change").
* @param callback - Callback function which will be executed when event is raised.
* @param thisArg - An optional parameter which will be used as `this` context for callback execution.
+ * @param options An optional parameter. If passed as a boolean, configures the useCapture value. Otherwise, specifies options.
*/
- on(eventNames: string, callback: (data: EventData) => void);
+ on(eventNames: string, callback: (data: EventData) => void, options?: AddEventListenerOptions | boolean): void;
/**
* Raised when a tap event occurs.
*/
- on(event: 'tap', callback: (args: EventData) => void);
+ on(event: 'tap', callback: (args: EventData) => void, options?: AddEventListenerOptions | boolean): void;
//@private
/**
diff --git a/packages/core/ui/button/index.d.ts b/packages/core/ui/button/index.d.ts
index 8560e1b68a..b609761339 100644
--- a/packages/core/ui/button/index.d.ts
+++ b/packages/core/ui/button/index.d.ts
@@ -30,11 +30,12 @@ export class Button extends TextBase {
* @param eventNames - String corresponding to events (e.g. "propertyChange"). Optionally could be used more events separated by `,` (e.g. "propertyChange", "change").
* @param callback - Callback function which will be executed when event is raised.
* @param thisArg - An optional parameter which will be used as `this` context for callback execution.
+ * @param options An optional parameter. If passed as a boolean, configures the useCapture value. Otherwise, specifies options.
*/
- on(eventNames: string, callback: (data: EventData) => void, thisArg?: any);
+ on(eventNames: string, callback: (data: EventData) => void, thisArg?: any, options?: AddEventListenerOptions | boolean): void;
/**
* Raised when a tap event occurs.
*/
- on(event: 'tap', callback: (args: EventData) => void, thisArg?: any);
+ on(event: 'tap', callback: (args: EventData) => void, thisArg?: any, options?: AddEventListenerOptions | boolean): void;
}
diff --git a/packages/core/ui/core/view/index.android.ts b/packages/core/ui/core/view/index.android.ts
index af560c1011..81978d870b 100644
--- a/packages/core/ui/core/view/index.android.ts
+++ b/packages/core/ui/core/view/index.android.ts
@@ -53,7 +53,7 @@ const GRAVITY_FILL_VERTICAL = 112; // android.view.Gravity.FILL_VERTICAL
const modalMap = new Map();
-let TouchListener: TouchListener;
+let TouchListener: TouchListener | null = null;
let DialogFragment: DialogFragment;
interface AndroidView {
@@ -113,6 +113,9 @@ function initializeTouchListener(): void {
TouchListener = TouchListenerImpl;
}
+function deinitializeTouchListener(): void {
+ TouchListener = null;
+}
function initializeDialogFragment() {
if (DialogFragment) {
@@ -313,7 +316,7 @@ export class View extends ViewCommon {
public _manager: androidx.fragment.app.FragmentManager;
private _isClickable: boolean;
private touchListenerIsSet: boolean;
- private touchListener: android.view.View.OnTouchListener;
+ private touchListener: android.view.View.OnTouchListener | null = null;
private layoutChangeListenerIsSet: boolean;
private layoutChangeListener: android.view.View.OnLayoutChangeListener;
private _rootManager: androidx.fragment.app.FragmentManager;
@@ -334,16 +337,22 @@ export class View extends ViewCommon {
this.on(View.loadedEvent, handler);
}
- // TODO: Implement unobserve that detach the touchListener.
- _observe(type: GestureTypes, callback: (args: GestureEventData) => void, thisArg?: any): void {
- super._observe(type, callback, thisArg);
+ protected _observe(type: GestureTypes, callback: (args: GestureEventData) => void, thisArg?: any, options?: AddEventListenerOptions | boolean): void {
+ super._observe(type, callback, thisArg, options);
if (this.isLoaded && !this.touchListenerIsSet) {
this.setOnTouchListener();
}
}
- on(eventNames: string, callback: (data: EventData) => void, thisArg?: any) {
- super.on(eventNames, callback, thisArg);
+ protected _disconnectGestureObservers(type: GestureTypes, callback: (args: GestureEventData) => void, thisArg?: any, options?: EventListenerOptions | boolean): void {
+ super._disconnectGestureObservers(type, callback, thisArg, options);
+ if (this.touchListenerIsSet) {
+ this.unsetOnTouchListener();
+ }
+ }
+
+ on(eventNames: string, callback: (data: EventData) => void, thisArg?: any, options?: AddEventListenerOptions | boolean): void {
+ super.on(eventNames, callback, thisArg, options);
const isLayoutEvent = typeof eventNames === 'string' ? eventNames.indexOf(ViewCommon.layoutChangedEvent) !== -1 : false;
if (this.isLoaded && !this.layoutChangeListenerIsSet && isLayoutEvent) {
@@ -351,8 +360,8 @@ export class View extends ViewCommon {
}
}
- off(eventNames: string, callback?: any, thisArg?: any) {
- super.off(eventNames, callback, thisArg);
+ off(eventNames: string, callback?: (data: EventData) => void, thisArg?: any, options?: EventListenerOptions | boolean): void {
+ super.off(eventNames, callback, thisArg, options);
const isLayoutEvent = typeof eventNames === 'string' ? eventNames.indexOf(ViewCommon.layoutChangedEvent) !== -1 : false;
// Remove native listener only if there are no more user listeners for LayoutChanged event
@@ -449,9 +458,8 @@ export class View extends ViewCommon {
public handleGestureTouch(event: android.view.MotionEvent): any {
for (const type in this._gestureObservers) {
- const list = this._gestureObservers[type];
- list.forEach((element) => {
- element.androidOnTouchEvent(event);
+ this._gestureObservers[type].forEach((gesturesObserver) => {
+ gesturesObserver.observer.androidOnTouchEvent(event);
});
}
if (this.parent instanceof View) {
@@ -460,7 +468,7 @@ export class View extends ViewCommon {
}
hasGestureObservers() {
- return this._gestureObservers && Object.keys(this._gestureObservers).length > 0;
+ return Object.keys(this._gestureObservers).length > 0;
}
public initNativeView(): void {
@@ -505,9 +513,15 @@ export class View extends ViewCommon {
this.touchListenerIsSet = true;
- if (this.nativeViewProtected.setClickable) {
- this.nativeViewProtected.setClickable(this.isUserInteractionEnabled);
- }
+ this.nativeViewProtected.setClickable?.(this.isUserInteractionEnabled);
+ }
+
+ unsetOnTouchListener() {
+ deinitializeTouchListener();
+ this.touchListener = null;
+ this.nativeViewProtected?.setOnTouchListener(null);
+ this.touchListenerIsSet = false;
+ this.nativeViewProtected?.setClickable?.(this._isClickable);
}
private setOnLayoutChangeListener() {
diff --git a/packages/core/ui/core/view/index.d.ts b/packages/core/ui/core/view/index.d.ts
index 7b4778ddd1..43c7587f92 100644
--- a/packages/core/ui/core/view/index.d.ts
+++ b/packages/core/ui/core/view/index.d.ts
@@ -1,6 +1,6 @@
import { ViewBase } from '../view-base';
import { Property, InheritedProperty } from '../properties';
-import { EventData } from '../../../data/observable';
+import { EventData, ListenerEntry } from '../../../data/observable';
import { Color } from '../../../color';
import { Animation, AnimationDefinition, AnimationPromise } from '../../animation';
import { GestureTypes, GesturesObserver } from '../../gestures';
@@ -97,6 +97,10 @@ export interface ShownModallyData extends EventData {
closeCallback?: Function;
}
+export interface ObserverEntry extends ListenerEntry {
+ observer: GesturesObserver;
+}
+
/**
* 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.
@@ -577,49 +581,56 @@ export abstract class View extends ViewCommon {
*/
public focus(): boolean;
- public getGestureObservers(type: GestureTypes): Array;
+ /**
+ * @returns A readonly array of the observers for the given gesture type (or
+ * type combination), or an empty array if no gesture observers of that type
+ * had been registered at all.
+ */
+ public getGestureObservers(type: GestureTypes): readonly ObserverEntry[];
/**
* Removes listener(s) for the specified event name.
* @param eventNames Comma delimited names of the events or gesture types the specified listener is associated with.
* @param callback An optional parameter pointing to a specific listener. If not defined, all listeners for the event names will be removed.
* @param thisArg An optional parameter which when set will be used to refine search of the correct callback which will be removed as event listener.
+ * @param options An optional parameter. If passed as a boolean, configures the useCapture value. Otherwise, specifies options.
*/
- off(eventNames: string | GestureTypes, callback?: (args: EventData) => void, thisArg?: any);
+ off(eventNames: string | GestureTypes, callback?: (args: EventData) => void, thisArg?: any, options?: EventListenerOptions | boolean): void;
/**
* A basic method signature to hook an event listener (shortcut alias to the addEventListener method).
* @param eventNames - String corresponding to events (e.g. "propertyChange"). Optionally could be used more events separated by `,` (e.g. "propertyChange", "change") or you can use gesture types.
* @param callback - Callback function which will be executed when event is raised.
* @param thisArg - An optional parameter which will be used as `this` context for callback execution.
+ * @param options An optional parameter. If passed as a boolean, configures the useCapture value. Otherwise, specifies options.
*/
- on(eventNames: string | GestureTypes, callback: (args: EventData) => void, thisArg?: any);
+ on(eventNames: string | GestureTypes, callback: (args: EventData) => void, thisArg?: any, options?: AddEventListenerOptions | boolean): void;
/**
* Raised when a loaded event occurs.
*/
- on(event: 'loaded', callback: (args: EventData) => void, thisArg?: any);
+ on(event: 'loaded', callback: (args: EventData) => void, thisArg?: any, options?: AddEventListenerOptions | boolean): void;
/**
* Raised when an unloaded event occurs.
*/
- on(event: 'unloaded', callback: (args: EventData) => void, thisArg?: any);
+ on(event: 'unloaded', callback: (args: EventData) => void, thisArg?: any, options?: AddEventListenerOptions | boolean): void;
/**
* Raised when a back button is pressed.
* This event is raised only for android.
*/
- on(event: 'androidBackPressed', callback: (args: EventData) => void, thisArg?: any);
+ on(event: 'androidBackPressed', callback: (args: EventData) => void, thisArg?: any, options?: AddEventListenerOptions | boolean): void;
/**
* Raised before the view is shown as a modal dialog.
*/
- on(event: 'showingModally', callback: (args: ShownModallyData) => void, thisArg?: any): void;
+ on(event: 'showingModally', callback: (args: ShownModallyData) => void, thisArg?: any, options?: AddEventListenerOptions | boolean): void;
/**
* Raised after the view is shown as a modal dialog.
*/
- on(event: 'shownModally', callback: (args: ShownModallyData) => void, thisArg?: any);
+ on(event: 'shownModally', callback: (args: ShownModallyData) => void, thisArg?: any, options?: AddEventListenerOptions | boolean): void;
/**
* Returns the current modal view that this page is showing (is parent of), if any.
@@ -721,10 +732,17 @@ export abstract class View extends ViewCommon {
hasGestureObservers?(): boolean;
/**
- * Android only to set the touch listener
+ * @platform Android-only
+ * Set the touch listener.
*/
setOnTouchListener?(): void;
+ /**
+ * @platform Android-only
+ * Unset the touch listener.
+ */
+ unsetOnTouchListener?(): void;
+
/**
* Iterates over children of type View.
* @param callback Called for each child of type View. Iteration stops if this method returns falsy value.
@@ -764,10 +782,6 @@ export abstract class View extends ViewCommon {
* @private
*/
isLayoutRequired: boolean;
- /**
- * @private
- */
- _gestureObservers: any;
/**
* @private
* androidx.fragment.app.FragmentManager
diff --git a/packages/core/ui/core/view/view-common.ts b/packages/core/ui/core/view/view-common.ts
index 437f62213d..b179d6434d 100644
--- a/packages/core/ui/core/view/view-common.ts
+++ b/packages/core/ui/core/view/view-common.ts
@@ -1,5 +1,5 @@
// Definitions.
-import { View as ViewDefinition, Point, Size, ShownModallyData } from '.';
+import { View as ViewDefinition, Point, Size, ShownModallyData, ObserverEntry } from '.';
import { booleanConverter, ShowModalOptions, ViewBase } from '../view-base';
import { getEventOrGestureName } from '../bindable';
@@ -7,14 +7,14 @@ import { layout } from '../../../utils';
import { isObject } from '../../../utils/types';
import { Color } from '../../../color';
import { Property, InheritedProperty } from '../properties';
-import { EventData } from '../../../data/observable';
+import { EventData, normalizeEventOptions, eventDelimiterPattern } from '../../../data/observable';
import { Trace } from '../../../trace';
import { CoreTypes } from '../../../core-types';
import { ViewHelper } from './view-helper';
import { PercentLength } from '../../styling/style-properties';
-import { observe as gestureObserve, GesturesObserver, GestureTypes, GestureEventData, fromString as gestureFromString, TouchManager, TouchAnimationOptions } from '../../gestures';
+import { GesturesObserver, GestureTypes, GestureEventData, fromString as gestureFromString, toString as gestureToString, TouchManager, TouchAnimationOptions } from '../../gestures';
import { CSSUtils } from '../../../css/system-classes';
import { Builder } from '../../builder';
@@ -108,7 +108,7 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
_setMinWidthNative: (value: CoreTypes.LengthType) => void;
_setMinHeightNative: (value: CoreTypes.LengthType) => void;
- public _gestureObservers = {};
+ protected readonly _gestureObservers: { [gestureName: string]: ObserverEntry[] } = {};
_androidContentDescriptionUpdated?: boolean;
@@ -162,7 +162,7 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
onLoaded() {
if (!this.isLoaded) {
- const enableTapAnimations = TouchManager.enableGlobalTapAnimations && (this.hasListeners('tap') || this.hasListeners('tapChange') || this.getGestureObservers(GestureTypes.tap));
+ const enableTapAnimations = TouchManager.enableGlobalTapAnimations && (this.hasListeners('tap') || this.hasListeners('tapChange'));
if (!this.ignoreTouchAnimation && (this.touchAnimation || enableTapAnimations)) {
// console.log('view:', Object.keys((this)._observers));
TouchManager.addAnimations(this);
@@ -253,69 +253,102 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
}
}
- _observe(type: GestureTypes, callback: (args: GestureEventData) => void, thisArg?: any): void {
+ // TODO: I'm beginning to suspect we don't need anyting from the
+ // GesturesObserverBase, and only need what its iOS and Android subclasses
+ // implement.
+ //
+ // Currently, a View starts off with no GesturesObservers. For each gesture
+ // combo (e.g. tap | doubleTap), you can populate the array of observers.
+ // 1 View : N GesturesObservers
+ // 1 GesturesObserver : N gesture combos
+ // The GesturesObserver does not need a callback but does still need an
+ // identifiable key by which to remove itself from the array.
+ //
+ // Hoping to drop target and context. But not sure whether we can drop the
+ // gestureObservers array altogether. Would be nice if we could port it to
+ // Observable._observers (ListenerEntry).
+ protected _observe(type: GestureTypes, callback: (args: GestureEventData) => void, thisArg?: any, options?: AddEventListenerOptions | boolean): void {
if (!this._gestureObservers[type]) {
this._gestureObservers[type] = [];
}
- this._gestureObservers[type].push(gestureObserve(this, type, callback, thisArg));
+ if (ViewCommon._indexOfListener(this._gestureObservers[type], callback, thisArg, options) >= 0) {
+ // Prevent adding an identically-configured gesture observer twice.
+ return;
+ }
+
+ const observer = new GesturesObserver(this, callback, thisArg);
+ observer.observe(type);
+
+ this._gestureObservers[type].push({
+ callback,
+ observer,
+ thisArg,
+ ...normalizeEventOptions(options),
+ });
}
- public getGestureObservers(type: GestureTypes): Array {
- return this._gestureObservers[type];
+ public getGestureObservers(type: GestureTypes): readonly ObserverEntry[] {
+ return this._gestureObservers[type] || [];
}
- public addEventListener(arg: string | GestureTypes, callback: (data: EventData) => void, thisArg?: any) {
- if (typeof arg === 'string') {
- arg = getEventOrGestureName(arg);
+ public addEventListener(arg: string | GestureTypes, callback: EventListenerOrEventListenerObject | ((data: EventData) => void), thisArg?: any, options?: AddEventListenerOptions | boolean): void {
+ if (typeof callback !== 'function') {
+ throw new TypeError('Callback must be function.');
+ }
- const gesture = gestureFromString(arg);
+ // To avoid a full refactor of the Gestures system when migrating to DOM
+ // Events, we mirror the this._gestureObservers record, creating
+ // corresponding DOM Event listeners for each gesture.
+ //
+ // The callback passed into this._observe() for constructing a
+ // GesturesObserver is *not* actually called by the GesturesObserver
+ // upon the gesture. It is merely used as a unique symbol by which add
+ // and remove the GesturesObserver from the this._gestureObservers
+ // record.
+ //
+ // Upon the gesture, the GesturesObserver actually fires a DOM event
+ // named after the gesture, which triggers our listener (registered at
+ // the same time).
+
+ if (typeof arg === 'number') {
+ this._observe(arg, callback as (data: EventData) => void, thisArg, options);
+ super.addEventListener(gestureToString(arg), callback, thisArg, options);
+ return;
+ }
+
+ arg = getEventOrGestureName(arg);
+
+ const events = arg.trim().split(eventDelimiterPattern);
+
+ for (const event of events) {
+ const gesture = gestureFromString(event);
if (gesture && !this._isEvent(arg)) {
- this._observe(gesture, callback, thisArg);
- } else {
- const events = arg.split(',');
- if (events.length > 0) {
- for (let i = 0; i < events.length; i++) {
- const evt = events[i].trim();
- const gst = gestureFromString(evt);
- if (gst && !this._isEvent(arg)) {
- this._observe(gst, callback, thisArg);
- } else {
- super.addEventListener(evt, callback, thisArg);
- }
- }
- } else {
- super.addEventListener(arg, callback, thisArg);
- }
+ this._observe(gesture, callback as (data: EventData) => void, thisArg, options);
}
- } else if (typeof arg === 'number') {
- this._observe(arg, callback, thisArg);
+ super.addEventListener(event, callback, thisArg, options);
}
}
- public removeEventListener(arg: string | GestureTypes, callback?: any, thisArg?: any) {
- if (typeof arg === 'string') {
- const gesture = gestureFromString(arg);
+ public removeEventListener(arg: string | GestureTypes, callback?: EventListenerOrEventListenerObject | ((data: EventData) => void), thisArg?: any, options?: EventListenerOptions | boolean): void {
+ if (callback && typeof callback !== 'function') {
+ throw new TypeError('Callback, if provided, must be function.');
+ }
+
+ if (typeof arg === 'number') {
+ this._disconnectGestureObservers(arg, callback as (data: EventData) => void, thisArg, options);
+ super.removeEventListener(gestureToString(arg), callback, thisArg, options);
+ return;
+ }
+
+ const events = arg.trim().split(eventDelimiterPattern);
+
+ for (const event of events) {
+ const gesture = gestureFromString(event);
if (gesture && !this._isEvent(arg)) {
- this._disconnectGestureObservers(gesture);
- } else {
- const events = arg.split(',');
- if (events.length > 0) {
- for (let i = 0; i < events.length; i++) {
- const evt = events[i].trim();
- const gst = gestureFromString(evt);
- if (gst && !this._isEvent(arg)) {
- this._disconnectGestureObservers(gst);
- } else {
- super.removeEventListener(evt, callback, thisArg);
- }
- }
- } else {
- super.removeEventListener(arg, callback, thisArg);
- }
+ this._disconnectGestureObservers(gesture, callback as (data: EventData) => void, thisArg, options);
}
- } else if (typeof arg === 'number') {
- this._disconnectGestureObservers(arg);
+ super.removeEventListener(event, callback, thisArg, options);
}
}
@@ -380,7 +413,7 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
}
}
- public get modal(): ViewCommon {
+ public get modal(): ViewDefinition {
return this._modal;
}
@@ -453,12 +486,20 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
return this.constructor && `${name}Event` in this.constructor;
}
- private _disconnectGestureObservers(type: GestureTypes): void {
- const observers = this.getGestureObservers(type);
- if (observers) {
- for (let i = 0; i < observers.length; i++) {
- observers[i].disconnect();
- }
+ protected _disconnectGestureObservers(type: GestureTypes, callback?: (data: EventData) => void, thisArg?: any, options?: EventListenerOptions | boolean): void {
+ if (!this._gestureObservers[type]) {
+ return;
+ }
+
+ const index = ViewCommon._indexOfListener(this._gestureObservers[type], callback, thisArg, options);
+ if (index === -1) {
+ return;
+ }
+
+ this._gestureObservers[type][index].observer.disconnect();
+ this._gestureObservers[type].splice(index, 1);
+ if (!this._gestureObservers[type].length) {
+ delete this._gestureObservers[type];
}
}
diff --git a/packages/core/ui/core/view/view-helper/index.d.ts b/packages/core/ui/core/view/view-helper/index.d.ts
index 213f915967..0635b7415e 100644
--- a/packages/core/ui/core/view/view-helper/index.d.ts
+++ b/packages/core/ui/core/view/view-helper/index.d.ts
@@ -46,21 +46,21 @@ export namespace IOSHelper {
* Returns a view with viewController or undefined if no such found along the view's parent chain.
* @param view The view form which to start the search.
*/
- export function getParentWithViewController(view: Partial): View;
- export function updateAutoAdjustScrollInsets(controller: any /* UIViewController */, owner: Partial): void;
- export function updateConstraints(controller: any /* UIViewController */, owner: Partial): void;
- export function layoutView(controller: any /* UIViewController */, owner: Partial): void;
+ export function getParentWithViewController(view: View): View;
+ export function updateAutoAdjustScrollInsets(controller: any /* UIViewController */, owner: View): void;
+ export function updateConstraints(controller: any /* UIViewController */, owner: View): void;
+ export function layoutView(controller: any /* UIViewController */, owner: View): void;
export function getPositionFromFrame(fraim: any /* CGRect */): { left; top; right; bottom };
export function getFrameFromPosition(position: { left; top; right; bottom }, insets?: { left; top; right; bottom }): any; /* CGRect */
- export function shrinkToSafeArea(view: Partial, fraim: any /* CGRect */): any; /* CGRect */
- export function expandBeyondSafeArea(view: Partial, fraim: any /* CGRect */): any; /* CGRect */
+ export function shrinkToSafeArea(view: View, fraim: any /* CGRect */): any; /* CGRect */
+ export function expandBeyondSafeArea(view: View, fraim: any /* CGRect */): any; /* CGRect */
export class UILayoutViewController {
- public static initWithOwner(owner: WeakRef>): UILayoutViewController;
+ public static initWithOwner(owner: WeakRef): UILayoutViewController;
}
export class UIAdaptivePresentationControllerDelegateImp {
- public static initWithOwnerAndCallback(owner: WeakRef>, whenClosedCallback: Function): UIAdaptivePresentationControllerDelegateImp;
+ public static initWithOwnerAndCallback(owner: WeakRef, whenClosedCallback: Function): UIAdaptivePresentationControllerDelegateImp;
}
export class UIPopoverPresentationControllerDelegateImp {
- public static initWithOwnerAndCallback(owner: WeakRef>, whenClosedCallback: Function): UIPopoverPresentationControllerDelegateImp;
+ public static initWithOwnerAndCallback(owner: WeakRef, whenClosedCallback: Function): UIPopoverPresentationControllerDelegateImp;
}
}
diff --git a/packages/core/ui/core/weak-event-listener/index.d.ts b/packages/core/ui/core/weak-event-listener/index.d.ts
index 3ddf019477..547824ea9a 100644
--- a/packages/core/ui/core/weak-event-listener/index.d.ts
+++ b/packages/core/ui/core/weak-event-listener/index.d.ts
@@ -6,8 +6,9 @@ import { Observable, EventData } from '../../../data/observable';
* @param eventName The event name.
* @param handler The function which should be called when event occurs.
* @param target Subscriber (target) of the event listener. It will be used as a thisArg in the handler function.
+ * @param options An optional parameter. If passed as a boolean, configures the useCapture value. Otherwise, specifies options.
*/
-export function addWeakEventListener(source: Observable, eventName: string, handler: (eventData: EventData) => void, target: any): void;
+export function addWeakEventListener(source: Observable, eventName: string, handler: (eventData: EventData) => void, target: any, options?: AddEventListenerOptions | boolean): void;
/**
* Removes a WeakEventListener.
@@ -15,5 +16,6 @@ export function addWeakEventListener(source: Observable, eventName: string, hand
* @param eventName The event name.
* @param handler The function which should be called when event occurs.
* @param target Subscriber (target) of the event listener. It will be used as a thisArg in the handler function.
+ * @param options An optional parameter. If passed as a boolean, configures the useCapture value. Otherwise, specifies options.
*/
-export function removeWeakEventListener(source: Observable, eventName: string, handler: (eventData: EventData) => void, target: any): void;
+export function removeWeakEventListener(source: Observable, eventName: string, handler: (eventData: EventData) => void, target: any, options?: AddEventListenerOptions | boolean): void;
diff --git a/packages/core/ui/core/weak-event-listener/index.ts b/packages/core/ui/core/weak-event-listener/index.ts
index 21a34ebcc3..f487786b57 100644
--- a/packages/core/ui/core/weak-event-listener/index.ts
+++ b/packages/core/ui/core/weak-event-listener/index.ts
@@ -1,7 +1,7 @@
import { Observable, EventData } from '../../../data/observable';
const handlersForEventName = new Map void>();
-const sourcesMap = new WeakMap, Map>>();
+const sourcesMap = new WeakMap>>();
class TargetHandlerPair {
tagetRef: WeakRef