Skip to content

Commit 3a5c21e

Browse files
committed
global event subscriptions support options
1 parent aff9955 commit 3a5c21e

2 files changed

Lines changed: 82 additions & 152 deletions

File tree

tea-cup/src/TeaCup/DocumentEvents.test.tsx

Lines changed: 19 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -51,30 +51,30 @@ describe('DocumentEvents Test', () => {
5151
expect(addSpy.mock.calls.length).toBe(1);
5252
});
5353

54-
it('second sub adds no listener', () => {
54+
it('second sub adds another listener', () => {
5555
const sub = documentEvents.on('click', (e) => 'clicked');
5656
sub.init(() => ({}));
5757

5858
expect(addSpy.mock.calls.length).toBe(1);
5959

60-
const sub2 = documentEvents.on('click', (e) => 'clicked2');
60+
const sub2 = documentEvents.on('click', (e) => 'clicked2', true);
6161
sub2.init(() => ({}));
6262

63-
expect(addSpy.mock.calls.length).toBe(1);
63+
expect(addSpy.mock.calls.length).toBe(2);
6464
});
6565

66-
it('last sub removes listener', () => {
66+
it('all subs remove listeners', () => {
6767
const sub = documentEvents.on('click', (e) => 'clicked');
6868
sub.init(() => ({}));
6969

70-
const sub2 = documentEvents.on('click', (e) => 'clicked2');
70+
const sub2 = documentEvents.on('click', (e) => 'clicked2', { capture: true });
7171
sub2.init(() => ({}));
7272

7373
sub.release();
74-
expect(removeSpy.mock.calls.length).toBe(0);
74+
expect(removeSpy.mock.calls.length).toBe(1);
7575

7676
sub2.release();
77-
expect(removeSpy.mock.calls.length).toBe(1);
77+
expect(removeSpy.mock.calls.length).toBe(2);
7878
});
7979

8080
it('sub receives event from listener', () => {
@@ -93,23 +93,30 @@ describe('DocumentEvents Test', () => {
9393
expect(msgs).toEqual(['clicked1']);
9494
});
9595

96-
it('two subs receive events from listener', () => {
96+
it('each sub receive events its listener', () => {
9797
const msgs: string[] = [];
9898
const collectMsgs = (msg: string): void => {
9999
msgs.push(msg);
100100
};
101+
const msgs2: string[] = [];
102+
const collectMsgs2 = (msg: string): void => {
103+
msgs2.push(msg);
104+
};
101105

102-
const sub = documentEvents.on('click', (e) => 'clicked1');
106+
const sub = documentEvents.on('click', (e) => 'clicked');
103107
const sub2 = documentEvents.on('click', (e) => 'clicked2');
104108

105109
sub.init(collectMsgs);
106-
sub2.init(collectMsgs);
110+
sub2.init(collectMsgs2);
107111

108-
expect(addSpy.mock.calls.length).toBe(1);
112+
expect(addSpy.mock.calls.length).toBe(2);
109113
const listener = addSpy.mock.calls[0][1];
114+
const listener2 = addSpy.mock.calls[1][1];
110115

111116
listener({ event: 'event' });
112-
expect(msgs).toEqual(['clicked1', 'clicked2']);
117+
expect(msgs).toEqual(['clicked']);
118+
listener2({ event: 'event' });
119+
expect(msgs2).toEqual(['clicked2']);
113120
});
114121

115122
it('sub stops receiving events from listener', () => {
@@ -132,16 +139,4 @@ describe('DocumentEvents Test', () => {
132139
expect(msgs).toEqual(['clicked1', 'clicked1']);
133140
});
134141

135-
it('release removes all listeners', () => {
136-
const sub = documentEvents.on('click', (e) => 'clicked1');
137-
sub.init(() => ({}));
138-
const sub2 = documentEvents.on('click', (e) => 'clicked2');
139-
sub2.init(() => ({}));
140-
const sub3 = documentEvents.on('mousemove', (e) => 'moved3');
141-
sub3.init(() => ({}));
142-
expect(addSpy.mock.calls.length).toBe(2);
143-
144-
documentEvents.release();
145-
expect(removeSpy.mock.calls.length).toBe(2);
146-
});
147142
});

tea-cup/src/TeaCup/DocumentEvents.ts

Lines changed: 63 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -23,158 +23,93 @@
2323
*
2424
*/
2525

26-
import { Sub } from 'tea-cup-core';
26+
import {Sub} from 'tea-cup-core';
2727

2828
type Listener<E> = (ev: E) => any;
29-
30-
type ListenerMap<T> = {
31-
[K in keyof T]?: Listener<T[K]>;
32-
};
33-
34-
function setListener<T, K extends keyof T>(o: ListenerMap<T>, k: K, v: Listener<T[K]> | undefined) {
35-
o[k] = v;
36-
}
37-
38-
function getListener<T, K extends keyof T>(o: ListenerMap<T>, k: K): Listener<T[K]> | undefined {
39-
return o[k];
40-
}
41-
42-
type SubsMap<Map, Msg> = {
43-
[K in keyof Map]?: ReadonlyArray<DocSub<K, Map, Msg>>;
44-
};
45-
46-
function addOneSub<K extends keyof Map, Map, Msg>(
47-
sub: DocSub<K, Map, Msg>,
48-
subs: SubsMap<Map, Msg>[K],
49-
): SubsMap<Map, Msg>[K] {
50-
const result = new Array().concat(subs);
51-
result.push(sub);
52-
return result;
53-
}
54-
55-
function removeOneSub<K extends keyof Map, Map, Msg>(
56-
sub: DocSub<K, Map, Msg>,
57-
subs: SubsMap<Map, Msg>[K],
58-
): SubsMap<Map, Msg>[K] | undefined {
59-
const result = new Array().concat(subs).filter((s) => s !== sub);
60-
return result.length !== 0 ? result : undefined;
61-
}
62-
63-
function getSubs<K extends keyof Map, Map, Msg>(o: SubsMap<Map, Msg>, k: K): SubsMap<Map, Msg>[K] | undefined {
64-
return o[k];
65-
}
66-
67-
function setSubs<K extends keyof Map, Map, Msg>(o: SubsMap<Map, Msg>, k: K, v: SubsMap<Map, Msg>[K] | undefined) {
68-
o[k] = v;
69-
}
29+
type ListenerOptions = boolean | AddEventListenerOptions;
30+
type Mapper<E, Msg> = (ev: E) => Msg;
7031

7132
class DocSub<K extends keyof Map, Map, Msg> extends Sub<Msg> {
72-
constructor(
73-
private readonly documentEvents: EventMapEvents<Map, Msg>,
74-
private readonly key: K,
75-
private readonly mapper: (t: Map[K]) => Msg,
76-
) {
77-
super();
78-
}
33+
private readonly listener: Listener<Map[K]>;
34+
35+
constructor(
36+
private readonly documentEvents: EventMapEvents<Map, Msg>,
37+
private readonly key: K,
38+
private readonly mapper: Mapper<Map[K], Msg>,
39+
private readonly options?: ListenerOptions
40+
) {
41+
super();
42+
this.listener = (e) => this.dispatch(this.mapper(e));
43+
}
7944

80-
protected onInit() {
81-
super.onInit();
82-
this.documentEvents.addSub({ key: this.key, sub: this });
83-
}
45+
protected onInit() {
46+
super.onInit();
47+
this.documentEvents.doAddListener(this.key, this.listener, this.options)
48+
}
8449

85-
protected onRelease() {
86-
super.onRelease();
87-
this.documentEvents.removeSub(this.key, this);
88-
}
50+
protected onRelease() {
51+
super.onRelease();
52+
this.documentEvents.doRemoveListener(this.key, this.listener, this.options)
53+
}
8954

90-
event(e: Map[K]) {
91-
this.dispatch(this.mapper(e));
92-
}
55+
event(e: Map[K]) {
56+
this.dispatch(this.mapper(e));
57+
}
9358
}
9459

9560
abstract class EventMapEvents<Map, Msg> {
96-
private readonly listeners: ListenerMap<Map> = {};
97-
private readonly subs: SubsMap<Map, Msg> = {};
98-
99-
constructor() {}
100-
101-
public release() {
102-
const listeners = this.listeners;
103-
const keys = Object.keys(listeners) as Array<keyof typeof listeners>;
104-
keys.forEach((key) => this.releaseListener(key));
105-
}
106-
107-
abstract doAddListener<K extends keyof Map>(key: K, listener: (ev: Map[K]) => any): void;
108-
109-
abstract doRemoveListener<K extends keyof Map>(key: K, listener: (ev: Map[K]) => any): void;
110-
111-
addSub<K extends keyof Map>({ key, sub }: { key: K; sub: DocSub<K, Map, Msg> }) {
112-
this.initListener(key);
113-
const subs: SubsMap<Map, Msg>[K] = getSubs(this.subs, key) ?? [];
114-
setSubs(this.subs, key, addOneSub(sub, subs));
115-
}
116-
117-
removeSub<K extends keyof Map, M>(key: K, sub: DocSub<K, Map, Msg>) {
118-
const list: SubsMap<Map, Msg>[K] = getSubs(this.subs, key) ?? [];
119-
const list1 = removeOneSub(sub, list);
120-
setSubs(this.subs, key, list1);
121-
if (!list1) {
122-
this.releaseListener(key);
123-
}
124-
}
125-
126-
private initListener<K extends keyof Map>(key: K) {
127-
if (!this.listeners[key]) {
128-
const listener = (ev: Map[K]) => {
129-
const subs: SubsMap<Map, Msg>[K] = getSubs(this.subs, key) ?? [];
130-
new Array().concat(subs).forEach((s: DocSub<K, Map, Msg>) => s.event(ev));
131-
return {};
132-
};
133-
setListener(this.listeners, key, listener);
134-
this.doAddListener(key, listener);
135-
}
136-
}
13761

138-
private releaseListener<K extends keyof Map>(key: K) {
139-
const listener: Listener<Map[K]> | undefined = getListener(this.listeners, key);
140-
if (listener) {
141-
setListener(this.listeners, key, undefined);
142-
this.doRemoveListener(key, listener);
62+
abstract doAddListener<K extends keyof Map>(key: K,
63+
listener: (ev: Map[K]) => any,
64+
options?: boolean | AddEventListenerOptions): void;
65+
66+
abstract doRemoveListener<K extends keyof Map>(key: K,
67+
listener: (ev: Map[K]) => any,
68+
options?: boolean | AddEventListenerOptions): void;
69+
70+
/**
71+
* Subscribe to an event.
72+
* @param key the event type to subscribe to.
73+
* @param mapper map the event to a message.
74+
* @param options options for this listener
75+
*/
76+
public on<K extends keyof Map, Msg>(key: K,
77+
mapper: (e: Map[K]) => Msg,
78+
options?: boolean | AddEventListenerOptions): Sub<Msg> {
79+
return new DocSub<K, Map, Msg>(this, key, mapper, options);
14380
}
144-
}
145-
146-
/**
147-
* Subscribe to an event.
148-
* @param key the event type to subscribe to.
149-
* @param mapper map the event to a message.
150-
*/
151-
public on<K extends keyof Map, Msg>(key: K, mapper: (e: Map[K]) => Msg): Sub<Msg> {
152-
return new DocSub<K, Map, Msg>(this, key, mapper);
153-
}
15481
}
15582

15683
/**
15784
* Subscribe to document events.
15885
*/
15986
export class DocumentEvents<Msg> extends EventMapEvents<DocumentEventMap, Msg> {
160-
doAddListener<K extends keyof DocumentEventMap>(key: K, listener: (ev: DocumentEventMap[K]) => any): void {
161-
document.addEventListener(key, listener);
162-
}
87+
doAddListener<K extends keyof DocumentEventMap>(key: K,
88+
listener: (ev: DocumentEventMap[K]) => any,
89+
options?: boolean | AddEventListenerOptions): void {
90+
document.addEventListener(key, listener, options);
91+
}
16392

164-
doRemoveListener<K extends keyof DocumentEventMap>(key: K, listener: (ev: DocumentEventMap[K]) => any): void {
165-
document.removeEventListener(key, listener);
166-
}
93+
doRemoveListener<K extends keyof DocumentEventMap>(key: K,
94+
listener: (ev: DocumentEventMap[K]) => any,
95+
options?: boolean | AddEventListenerOptions): void {
96+
document.removeEventListener(key, listener, options);
97+
}
16798
}
16899

169100
/**
170101
* Bonus, WindowEvents
171102
*/
172103
export class WindowEvents<Msg> extends EventMapEvents<WindowEventMap, Msg> {
173-
doAddListener<K extends keyof WindowEventMap>(key: K, listener: (ev: WindowEventMap[K]) => any): void {
174-
window.addEventListener(key, listener);
175-
}
104+
doAddListener<K extends keyof WindowEventMap>(key: K,
105+
listener: (ev: WindowEventMap[K]) => any,
106+
options?: boolean | AddEventListenerOptions): void {
107+
window.addEventListener(key, listener, options);
108+
}
176109

177-
doRemoveListener<K extends keyof WindowEventMap>(key: K, listener: (ev: WindowEventMap[K]) => any): void {
178-
window.removeEventListener(key, listener);
179-
}
110+
doRemoveListener<K extends keyof WindowEventMap>(key: K,
111+
listener: (ev: WindowEventMap[K]) => any,
112+
options?: boolean | AddEventListenerOptions): void {
113+
window.removeEventListener(key, listener, options);
114+
}
180115
}

0 commit comments

Comments
 (0)