-
Notifications
You must be signed in to change notification settings - Fork 25
/
listenerHelpers.ts
226 lines (196 loc) · 6.31 KB
/
listenerHelpers.ts
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
import { MathQuillField } from "#components";
import type { Calc, DispatchedEvent } from "#globals";
interface DispatchOverridingHandler {
handler: (evt: DispatchedEvent) => boolean | undefined;
priority: number;
id: number;
}
const calcDispatchOverrideHandlers = new WeakMap<
Calc,
DispatchOverridingHandler[]
>();
let dispatchOverridingHandlerId = 0;
// schedule a function to run after every desmos event
// priorities determine which run first
// the handler can return false to force the dispatcher to stop early
// (e.g. to stop desmos from doing a default action upon pressing a key)
export function registerCustomDispatchOverridingHandler(
calc: Calc,
handler: (evt: DispatchedEvent) => boolean | undefined,
priority: number
): number {
const handlers = getDispatchOverrideHandlers(calc);
const id = dispatchOverridingHandlerId++;
// add the handler
handlers.push({ handler, priority, id });
// sort the handlers so that higher priorities are first
// could easily be optimized but prob not a bottleneck
handlers.sort((a, b) => b.priority - a.priority);
return id;
}
// deregisters a function created with registerCustomDispatchOverridingHandler
// uses the id that the former function returns
export function deregisterCustomDispatchOverridingHandler(
calc: Calc,
id: number
): void {
const handlers = getDispatchOverrideHandlers(calc);
// remove all handlers with matching IDs
// This is in general O(ND), but only one handler should be deleted typically.
for (let i = handlers.length - 1; i >= 0; i--) {
if (handlers[i].id === id) {
handlers.splice(i, 1);
}
}
}
function getDispatchOverrideHandlers(calc: Calc) {
const curr = calcDispatchOverrideHandlers.get(calc);
if (curr) return curr;
const newHandlers = setupDispatchOverride(calc);
calcDispatchOverrideHandlers.set(calc, newHandlers);
return newHandlers;
}
// Change calc.handleDispatchedAction to first run a set of custom handlers
export function setupDispatchOverride(calc: Calc) {
const old = calc.controller.handleDispatchedAction;
const handlers: DispatchOverridingHandler[] = [];
calc.controller.handleDispatchedAction = function (evt) {
for (const { handler } of handlers) {
const keepGoing = handler(evt);
if (keepGoing === false) return;
}
old.call(this, evt);
};
return handlers;
}
// "attach" a function onto an existing function, performing some functionality
// and then optionally triggering the existing function.
// Returns an "unsubscribe" function that resets the attached function to its prior state
export function attach<F extends (...args: any) => any>(
getTarget: () => F,
setTarget: (f: F) => void,
handler: (...params: Parameters<F>) => [false, ReturnType<F>] | undefined
): () => void {
const oldTarget = getTarget();
// @ts-expect-error go away
setTarget((...args) => {
const ret = handler(...args);
// intentional
// eslint-disable-next-line @typescript-eslint/no-unnecessary-boolean-literal-compare
if (ret?.[0] === false) return ret[1];
return oldTarget(...args);
});
return () => setTarget(oldTarget);
}
// helper function for attach; makes a getter/setter for an object property
export function propGetSet<Obj extends object, Key extends keyof Obj>(
obj: Obj,
key: Key
) {
return [() => obj[key], (v: Obj[Key]) => (obj[key] = v)] as const;
}
// override all keyboard inputs entering a MathQuill field
// and optionally prevent them from doing their default behavior
export function hookIntoOverrideKeystroke(
// field for which to override kb inputs
mq: MathQuillField,
// callback function
// return false to override all other events
fn: (key: string, evt: KeyboardEvent) => boolean | undefined,
// higher priority --> runs first
priority: number,
// unique key to prevent adding duplicate hooks
key: string
) {
return hookIntoFunction(
mq.__options,
"overrideKeystroke",
key,
priority,
(stop, k, e) => {
const cont = fn(k, e);
if (cont === false) {
stop();
}
}
);
}
type HookedFunctionCallback<Fn extends (...args: any[]) => any> = (
stop: (ret: ReturnType<Fn>) => void,
...args: Parameters<Fn>
) => void;
type HookedFunction<Fn extends (...args: any[]) => any> = Fn & {
__isMonkeypatchedIn: true;
handlers: {
key: string;
fn: HookedFunctionCallback<Fn>;
priority: number;
}[];
revert: () => void;
};
type MaybeHookedFunction<Fn extends (...args: any[]) => any> =
| HookedFunction<Fn>
| (Fn & {
__isMonkeypatchedIn: undefined;
});
export function hookIntoFunction<
Key extends string,
Obj extends { [K in Key]: (...args: any[]) => any },
Fn extends Obj[Key]
>(
obj: Obj,
prop: Key,
key: string,
priority: number,
fn: HookedFunctionCallback<Fn>
) {
const oldfn = obj[prop].bind(obj) as MaybeHookedFunction<Fn>;
// monkeypatch the function if it isn't monkeypatched already
if (!oldfn.__isMonkeypatchedIn) {
const monkeypatchedFunction = function (
...args: Parameters<Fn>
): ReturnType<Fn> {
const handlersArray = (obj[prop] as HookedFunction<Fn>).handlers;
for (const h of handlersArray) {
let stop = false;
let ret: ReturnType<Fn> | undefined;
h.fn((r: ReturnType<Fn>) => {
stop = true;
ret = r;
}, ...args);
if (stop) return ret as ReturnType<Fn>;
}
return oldfn(...args);
};
monkeypatchedFunction.__isMonkeypatchedIn = true;
monkeypatchedFunction.handlers = [] as HookedFunction<Fn>["handlers"];
monkeypatchedFunction.revert = () => {
obj[prop] = oldfn;
};
obj[prop] = monkeypatchedFunction as unknown as any;
}
const monkeypatchedFn = obj[prop] as HookedFunction<Fn>;
// if theres already a handler with this key, update it
const handler = monkeypatchedFn.handlers.find((h) => h.key === key);
if (handler) {
handler.priority = priority;
handler.fn = fn;
return;
}
// if there isn't one, add a new one
monkeypatchedFn.handlers.push({
key,
priority,
fn,
});
monkeypatchedFn.handlers.sort((a, b) => b.priority - a.priority);
// function for removing this handler
return () => {
monkeypatchedFn.handlers = monkeypatchedFn.handlers.filter(
(h) => h.key !== key
);
if (monkeypatchedFn.handlers.length === 0) {
monkeypatchedFn.revert();
}
};
}