/
streams.dart
480 lines (412 loc) · 15.7 KB
/
streams.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'dart:js_interop';
import '../../dom.dart' as html;
import '../../helpers.dart' show Device;
/// Helper class used to create streams abstracting DOM events. This is a
/// piece of the helper layer directly derived from a similar feature in
/// `dart:html`.
///
/// A few differences compared to `dart:html`:
/// * The helper layer doesn't have `ElementList` APIs, so this
/// provider omits APIs related to them.
///
/// * Streams returned here behave slighly differently. The timing of when
/// callbacks execute is sometimes differet when using stream to future APIs
/// like `.first`. In particular, when using synchronous browser APIs like
/// `dispatchEvent`, the Dart callbacks that would have executed
/// synchronously in `dart:html`, may now execute asynchronously. This only
/// breaks code that relied on specific timing details of the
/// implementation, but at an API level, the change is non breaking.
class EventStreamProvider<T extends html.Event> {
final String _eventType;
const EventStreamProvider(this._eventType);
/// Gets a [Stream] for this event type, on the specified target.
///
/// This will always return a broadcast stream so multiple listeners can be
/// used simultaneously.
///
/// This may be used to capture DOM events:
///
/// Element.keyDownEvent.forTarget(element, useCapture: true).listen(...);
///
/// // Alternate method:
/// Element.keyDownEvent.forTarget(element).capture(...);
///
/// Or for listening to an event which will bubble through the DOM tree:
///
/// MediaElement.pauseEvent.forTarget(document.body).listen(...);
///
/// See also:
///
/// * [EventTarget.addEventListener](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener)
/// from MDN.
Stream<T> forTarget(html.EventTarget? e, {bool useCapture = false}) =>
_EventStream<T>(e, _eventType, useCapture);
/// Gets a [Stream] for this event type, on the specified element.
///
/// This will always return a broadcast stream so multiple listeners can be
/// used simultaneously.
///
/// This may be used to capture DOM events:
///
/// Element.keyDownEvent.forElement(element, useCapture: true)
/// .listen(...);
///
/// // Alternate method:
/// Element.keyDownEvent.forElement(element).capture(...);
///
/// Or for listening to an event which will bubble through the DOM tree:
///
/// MediaElement.pauseEvent.forElement(document.body).listen(...);
///
/// See also:
///
/// * [EventTarget.addEventListener](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener)
/// from MDN.
ElementStream<T> forElement(html.Element e, {bool useCapture = false}) =>
_ElementEventStreamImpl<T>(e, _eventType, useCapture);
/// Gets the type of the event which this would listen for on the specified
/// event target.
///
/// The target is necessary because some browsers may use different event
/// names for the same purpose and the target allows differentiating browser
/// support.
String getEventType(html.EventTarget target) => _eventType;
}
/// A stream that allows capturing events on a parent element.
///
/// Note, unlike `dart:html`'s `ElementStream`, this implementation doesn't
/// provide the jquery style `matches` filtering functionality.
abstract class ElementStream<T extends html.Event> implements Stream<T> {
/// Adds a capturing subscription to this stream.
///
/// If the target of the event is a descendant of the element from which this
/// stream derives then [onData] is called before the event propagates down to
/// the target. This is the opposite of bubbling behavior, where the event
/// is first processed for the event target and then bubbles upward.
///
/// ## Other resources
///
/// * [Event Capture](http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-flow-capture)
/// from the W3C DOM Events specification.
StreamSubscription<T> capture(void Function(T) onData);
}
/// Adapter for exposing DOM events as Dart streams.
class _EventStream<T extends html.Event> extends Stream<T> {
final html.EventTarget? _target;
final String _eventType;
final bool _useCapture;
_EventStream(this._target, this._eventType, this._useCapture);
// DOM events are inherently multi-subscribers.
@override
Stream<T> asBroadcastStream(
{void Function(StreamSubscription<T>)? onListen,
void Function(StreamSubscription<T>)? onCancel}) =>
this;
@override
bool get isBroadcast => true;
// TODO(9757): Inlining should be smart and inline only when inlining would
// enable scalar replacement of an immediately allocated receiver.
@pragma('dart2js:tryInline')
@override
StreamSubscription<T> listen(void Function(T)? onData,
{Function? onError, void Function()? onDone, bool? cancelOnError}) =>
_EventStreamSubscription<T>(
this._target, this._eventType, onData, this._useCapture);
}
/// Adapter for exposing DOM Element events as streams
class _ElementEventStreamImpl<T extends html.Event> extends _EventStream<T>
implements ElementStream<T> {
_ElementEventStreamImpl(super.target, super.eventType, super.useCapture);
@override
StreamSubscription<T> capture(void Function(T) onData) =>
_EventStreamSubscription<T>(_target, _eventType, onData, true);
}
class _EventStreamSubscription<T extends html.Event>
implements StreamSubscription<T> {
int _pauseCount = 0;
html.EventTarget? _target;
final String _eventType;
html.EventListener? _onData;
final bool _useCapture;
// TODO(leafp): It would be better to write this as
// _onData = onData == null ? null :
// onData is void Function(Event)
// ? _wrapZone<Event>(onData)
// : _wrapZone<Event>((e) => onData(e as T))
// In order to support existing tests which pass the wrong type of events but
// use a more general listener, without causing as much slowdown for things
// which are typed correctly. But this currently runs afoul of restrictions
// on is checks for compatibility with the VM.
_EventStreamSubscription(
this._target, this._eventType, void Function(T)? onData, this._useCapture)
: _onData = onData == null
? null
// ignore: avoid_dynamic_calls
: _wrapZone<html.Event>((e) => (onData as dynamic)(e))?.toJS {
_tryResume();
}
@override
Future<void> cancel() {
// Note: the use of `emptyFuture` here has an indirect effect. The timing of
// when stream listeners get dispatched is different from that of
// `dart:html`.
//
// For example, the following program prints 1, 2, 3, 4, but with
// `dart:html` it would have printed 1, 2, 4, 3
//
// ```dart
// import 'package:web/helpers.dart';
//
// main() {
// print('1');
// final body = document.body!;
// body.onTouchStart.first.whenComplete(() {
// print('4');
// });
//
// print('2');
// body.dispatchEvent(TouchEvent('touchstart'));
// print('3');
// }
// ```
//
// More details can be found on [this change][1].
// [1]: https://dart-review.googlesource.com/c/sdk/+/175323
final emptyFuture = Future<void>.value();
if (_canceled) return emptyFuture;
_unlisten();
// Clear out the target to indicate this is complete.
_target = null;
_onData = null;
return emptyFuture;
}
bool get _canceled => _target == null;
@override
void onData(void Function(T)? handleData) {
if (_canceled) {
throw StateError('Subscription has been canceled.');
}
// Remove current event listener.
_unlisten();
_onData = handleData == null
? null
// ignore: avoid_dynamic_calls
: _wrapZone<html.Event>((e) => (handleData as dynamic)(e))?.toJS;
_tryResume();
}
/// Has no effect.
@override
void onError(Function? handleError) {}
/// Has no effect.
@override
void onDone(void Function()? handleDone) {}
@override
void pause([Future<dynamic>? resumeSignal]) {
if (_canceled) return;
++_pauseCount;
_unlisten();
if (resumeSignal != null) {
resumeSignal.whenComplete(resume);
}
}
@override
bool get isPaused => _pauseCount > 0;
@override
void resume() {
if (_canceled || !isPaused) return;
--_pauseCount;
_tryResume();
}
void _tryResume() {
if (_onData != null && !isPaused) {
_target!.addEventListener(_eventType, _onData, _useCapture.toJS);
}
}
void _unlisten() {
if (_onData != null) {
_target!.removeEventListener(_eventType, _onData, _useCapture.toJS);
}
}
@override
Future<E> asFuture<E>([E? futureValue]) =>
// We just need a future that will never succeed or fail.
Completer<E>().future;
}
/// Base class that supports listening for and dispatching browser events.
///
/// Normally events are accessed via the Stream getter:
///
/// element.onMouseOver.listen((e) => print('Mouse over!'));
///
/// To access bubbling events which are declared on one element, but may bubble
/// up to another element type (common for MediaElement events):
///
/// MediaElement.pauseEvent.forTarget(document.body).listen(...);
///
/// To useCapture on events:
///
/// Element.keyDownEvent.forTarget(element, useCapture: true).listen(...);
///
/// Custom events can be declared as:
///
/// class DataGenerator {
/// static EventStreamProvider<Event> dataEvent =
/// new EventStreamProvider('data');
/// }
///
/// Then listeners should access the event with:
///
/// DataGenerator.dataEvent.forTarget(element).listen(...);
///
/// Custom events can also be accessed as:
///
/// element.on['some_event'].listen(...);
///
/// This approach is generally discouraged as it loses the event typing and
/// some DOM events may have multiple platform-dependent event names under the
/// covers. By using the standard Stream getters you will get the platform
/// specific event name automatically.
class Events {
final html.EventTarget _ptr;
Events(this._ptr);
Stream<html.Event> operator [](String type) =>
_EventStream(_ptr, type, false);
}
class ElementEvents extends Events {
static final _webkitEvents = {
'animationend': 'webkitAnimationEnd',
'animationiteration': 'webkitAnimationIteration',
'animationstart': 'webkitAnimationStart',
'fullscreenchange': 'webkitfullscreenchange',
'fullscreenerror': 'webkitfullscreenerror',
'keyadded': 'webkitkeyadded',
'keyerror': 'webkitkeyerror',
'keymessage': 'webkitkeymessage',
'needkey': 'webkitneedkey',
'pointerlockchange': 'webkitpointerlockchange',
'pointerlockerror': 'webkitpointerlockerror',
'resourcetimingbufferfull': 'webkitresourcetimingbufferfull',
'transitionend': 'webkitTransitionEnd',
'speechchange': 'webkitSpeechChange'
};
ElementEvents(html.Element super.ptr);
@override
Stream<html.Event> operator [](String type) {
if (_webkitEvents.keys.contains(type.toLowerCase())) {
if (Device.isWebKit) {
return _ElementEventStreamImpl(
_ptr, _webkitEvents[type.toLowerCase()]!, false);
}
}
return _ElementEventStreamImpl(_ptr, type, false);
}
}
/// Helper class to implement custom events which wrap DOM events.
// TODO(b/261997228): Add support for CustomEvents now that WrappedEvent is not
// implementing the JS interop html.Event type.
class WrappedEvent {
final html.Event wrapped;
/// The CSS selector involved with event delegation.
String? _selector;
WrappedEvent(this.wrapped);
bool get bubbles => wrapped.bubbles;
bool get cancelable => wrapped.cancelable;
bool get composed => wrapped.composed;
html.EventTarget? get currentTarget => wrapped.currentTarget;
bool get defaultPrevented => wrapped.defaultPrevented;
int get eventPhase => wrapped.eventPhase;
bool get isTrusted => wrapped.isTrusted;
html.EventTarget? get target => wrapped.target;
double get timeStamp => wrapped.timeStamp.toDouble();
String get type => wrapped.type;
void preventDefault() {
wrapped.preventDefault();
}
void stopImmediatePropagation() {
wrapped.stopImmediatePropagation();
}
void stopPropagation() {
wrapped.stopPropagation();
}
List<html.EventTarget> composedPath() =>
wrapped.composedPath().toDart.cast<html.EventTarget>();
html.Element get matchingTarget {
if (_selector == null) {
throw UnsupportedError('Cannot call matchingTarget if this Event did'
' not arise as a result of event delegation.');
}
final currentTarget = this.currentTarget as html.Element?;
var target = this.target as html.Element?;
do {
if (target!.matches(_selector!)) return target;
target = target.parentElement;
} while (target != null && target != currentTarget!.parentElement);
throw StateError('No selector matched for populating matchedTarget.');
}
}
void Function(T)? _wrapZone<T>(void Function(T)? callback) {
// For performance reasons avoid wrapping if we are in the root zone.
if (Zone.current == Zone.root) return callback;
if (callback == null) return null;
return Zone.current.bindUnaryCallbackGuarded(callback);
}
void Function(T1, T2)? wrapBinaryZone<T1, T2>(void Function(T1, T2)? callback) {
// For performance reasons avoid wrapping if we are in the root zone.
if (Zone.current == Zone.root) return callback;
if (callback == null) return null;
return Zone.current.bindBinaryCallbackGuarded(callback);
}
/// A stream of custom events, which enables the user to "fire" (add) their own
/// custom events to a stream.
abstract class CustomStream<T extends html.Event> implements Stream<T> {
/// Add the following custom event to the stream for dispatching to interested
/// listeners.
void add(T event);
}
class CustomEventStreamImpl<T extends html.Event> extends Stream<T>
implements CustomStream<T> {
StreamController<T> streamController;
/// The type of event this stream is providing (e.g. "keydown").
String type;
CustomEventStreamImpl(this.type)
: streamController = StreamController.broadcast(sync: true);
// Delegate all regular Stream behavior to our wrapped Stream.
@override
StreamSubscription<T> listen(void Function(T)? onData,
{Function? onError, void Function()? onDone, bool? cancelOnError}) =>
streamController.stream.listen(onData,
onError: onError, onDone: onDone, cancelOnError: cancelOnError);
@override
Stream<T> asBroadcastStream(
{void Function(StreamSubscription<T>)? onListen,
void Function(StreamSubscription<T>)? onCancel}) =>
streamController.stream;
@override
bool get isBroadcast => true;
@override
void add(T event) {
if (event.type == type) streamController.add(event);
}
}
/// A factory to expose DOM events as streams, where the DOM event name has to
/// be determined on the fly (for example, mouse wheel events).
class CustomEventStreamProvider<T extends html.Event>
implements EventStreamProvider<T> {
final String Function(html.EventTarget) _eventTypeGetter;
const CustomEventStreamProvider(this._eventTypeGetter);
@override
Stream<T> forTarget(html.EventTarget? e, {bool useCapture = false}) =>
_EventStream<T>(e, _eventTypeGetter(e!), useCapture);
@override
ElementStream<T> forElement(html.Element e, {bool useCapture = false}) =>
_ElementEventStreamImpl<T>(e, _eventTypeGetter(e), useCapture);
@override
String getEventType(html.EventTarget target) => _eventTypeGetter(target);
@override
String get _eventType =>
throw UnsupportedError('Access type through getEventType method.');
}