@pechynho/stimulus-typescript 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +28 -0
- package/README.md +275 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +6 -0
- package/dist/portal-controller.d.ts +54 -0
- package/dist/portal-controller.js +792 -0
- package/dist/portal.d.ts +13 -0
- package/dist/portal.js +101 -0
- package/dist/resolvable.d.ts +28 -0
- package/dist/resolvable.js +54 -0
- package/dist/test.d.ts +1 -0
- package/dist/test.js +56 -0
- package/dist/typed-stimulus.d.ts +78 -0
- package/dist/typed-stimulus.js +139 -0
- package/dist/typed.d.ts +69 -0
- package/dist/typed.js +60 -0
- package/dist/utils.d.ts +6 -0
- package/dist/utils.js +38 -0
- package/package.json +40 -0
- package/src/index.ts +6 -0
- package/src/portal-controller.ts +821 -0
- package/src/portal.ts +110 -0
- package/src/resolvable.ts +65 -0
- package/src/test.ts +72 -0
- package/src/typed.ts +178 -0
- package/src/utils.ts +41 -0
|
@@ -0,0 +1,821 @@
|
|
|
1
|
+
import {ActionEvent, Controller} from "@hotwired/stimulus";
|
|
2
|
+
import {throttle} from "throttle-debounce";
|
|
3
|
+
import {camelCase, capitalize} from "./utils";
|
|
4
|
+
|
|
5
|
+
const proxyActionPrefix = '__proxyAction__';
|
|
6
|
+
|
|
7
|
+
class Action
|
|
8
|
+
{
|
|
9
|
+
constructor(
|
|
10
|
+
public readonly event: string | undefined,
|
|
11
|
+
public readonly identifier: string,
|
|
12
|
+
public readonly method: string,
|
|
13
|
+
public readonly modifier: string | undefined,
|
|
14
|
+
public stringified: string | null = null,
|
|
15
|
+
) {
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
toString(): string {
|
|
19
|
+
if (this.stringified !== null) {
|
|
20
|
+
return this.stringified;
|
|
21
|
+
}
|
|
22
|
+
let directive = '';
|
|
23
|
+
if (this.event !== undefined) {
|
|
24
|
+
directive += `${this.event}->`;
|
|
25
|
+
}
|
|
26
|
+
directive += `${this.identifier}#${this.method}`;
|
|
27
|
+
if (this.modifier !== undefined) {
|
|
28
|
+
directive += `:${this.modifier}`;
|
|
29
|
+
}
|
|
30
|
+
this.stringified = directive;
|
|
31
|
+
return this.stringified;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default class extends Controller<HTMLElement>
|
|
36
|
+
{
|
|
37
|
+
private observer: MutationObserver | null = null;
|
|
38
|
+
private isConnected: boolean = false;
|
|
39
|
+
private identifiers: Set<string> = new Set();
|
|
40
|
+
private searchedIdentifiersForTargets: Set<string> = new Set();
|
|
41
|
+
private searchedIdentifiersForActions: Set<string> = new Set();
|
|
42
|
+
private controllers: Map<string, Set<Controller>> = new Map();
|
|
43
|
+
private targetsByController: Map<Controller, Set<Element>> = new Map();
|
|
44
|
+
private targetsByIdentifier: Map<string, Set<Element>> = new Map();
|
|
45
|
+
private targetsByTargetName: Map<string, Map<string, Set<Element>>> = new Map();
|
|
46
|
+
private controllerOriginalMethods: Map<Controller, { [key: string]: TypedPropertyDescriptor<Controller> }> = new Map();
|
|
47
|
+
private actionToElementsMap: Map<string, Set<Element>> = new Map();
|
|
48
|
+
private elementToActionsMap: Map<Element, Set<string>> = new Map();
|
|
49
|
+
private identifierToActionElementsMap: Map<string, Set<Element>> = new Map();
|
|
50
|
+
private actionElementToIdentifiersMap: Map<Element, Set<string>> = new Map();
|
|
51
|
+
|
|
52
|
+
public initialize(): void {
|
|
53
|
+
this.searchTargets = throttle(1, this.searchTargets.bind(this));
|
|
54
|
+
this.searchActions = throttle(1, this.searchActions.bind(this));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
public connect(): void {
|
|
58
|
+
this.isConnected = true;
|
|
59
|
+
this.reinitializeObserver();
|
|
60
|
+
this.connectObserver();
|
|
61
|
+
this.searchTargets();
|
|
62
|
+
this.searchActions();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
public disconnect(): void {
|
|
66
|
+
this.isConnected = false;
|
|
67
|
+
this.disconnectAllTargets();
|
|
68
|
+
this.restoreControllersGetTargetMethods();
|
|
69
|
+
this.removeAllProxyActions();
|
|
70
|
+
this.disconnectObserver();
|
|
71
|
+
this.identifiers.clear();
|
|
72
|
+
this.searchedIdentifiersForTargets.clear();
|
|
73
|
+
this.searchedIdentifiersForActions.clear();
|
|
74
|
+
this.controllers.clear();
|
|
75
|
+
this.targetsByController.clear();
|
|
76
|
+
this.targetsByIdentifier.clear();
|
|
77
|
+
this.targetsByTargetName.clear();
|
|
78
|
+
this.controllerOriginalMethods.clear();
|
|
79
|
+
this.actionToElementsMap.clear();
|
|
80
|
+
this.elementToActionsMap.clear();
|
|
81
|
+
this.identifierToActionElementsMap.clear();
|
|
82
|
+
this.actionElementToIdentifiersMap.clear();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
public sync(controller: Controller): void {
|
|
86
|
+
this.identifiers.add(controller.identifier);
|
|
87
|
+
let controllers = this.controllers.get(controller.identifier);
|
|
88
|
+
if (controllers === undefined) {
|
|
89
|
+
controllers = new Set<Controller>();
|
|
90
|
+
this.controllers.set(controller.identifier, controllers);
|
|
91
|
+
}
|
|
92
|
+
controllers.add(controller);
|
|
93
|
+
this.overrideControllerGetTargetMethods(controller);
|
|
94
|
+
if (this.isConnected) {
|
|
95
|
+
this.reinitializeObserver();
|
|
96
|
+
this.connectObserver();
|
|
97
|
+
this.searchTargets();
|
|
98
|
+
this.searchActions();
|
|
99
|
+
}
|
|
100
|
+
const targetsByIdentifier = this.targetsByIdentifier.get(controller.identifier);
|
|
101
|
+
if (targetsByIdentifier !== undefined) {
|
|
102
|
+
for (const target of targetsByIdentifier) {
|
|
103
|
+
this.addTarget(target, controller.identifier);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
public unsync(controller: Controller): void {
|
|
109
|
+
const controllers = this.controllers.get(controller.identifier);
|
|
110
|
+
if (controllers === undefined) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
controllers.delete(controller);
|
|
114
|
+
this.restoreControllerGetTargetMethods(controller);
|
|
115
|
+
if (controllers.size === 0) {
|
|
116
|
+
this.controllers.delete(controller.identifier);
|
|
117
|
+
this.identifiers.delete(controller.identifier);
|
|
118
|
+
this.targetsByIdentifier.delete(controller.identifier);
|
|
119
|
+
this.searchedIdentifiersForTargets.delete(controller.identifier);
|
|
120
|
+
this.searchedIdentifiersForActions.delete(controller.identifier);
|
|
121
|
+
this.targetsByTargetName.delete(controller.identifier);
|
|
122
|
+
if (this.isConnected) {
|
|
123
|
+
this.removeAllProxyActionsByIdentifier(controller.identifier);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
this.targetsByController.delete(controller);
|
|
127
|
+
this.reinitializeObserver();
|
|
128
|
+
this.connectObserver();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private reinitializeObserver() {
|
|
132
|
+
this.disconnectObserver();
|
|
133
|
+
this.observer = new MutationObserver(this.handleMutations.bind(this));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private connectObserver(): void {
|
|
137
|
+
if (this.observer === null) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const attributes = [this.getActionAttributeName()];
|
|
141
|
+
for (const identifier of this.identifiers) {
|
|
142
|
+
attributes.push(this.getTargetAttributeName(identifier));
|
|
143
|
+
}
|
|
144
|
+
this.observer.observe(this.element, {
|
|
145
|
+
childList: true,
|
|
146
|
+
subtree: true,
|
|
147
|
+
attributes: true,
|
|
148
|
+
attributeFilter: attributes,
|
|
149
|
+
attributeOldValue: true,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private disconnectObserver(): void {
|
|
154
|
+
if (this.observer === null) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
this.observer.disconnect();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private handleMutations(mutations: MutationRecord[]) {
|
|
161
|
+
for (const mutation of mutations) {
|
|
162
|
+
if (mutation.type === 'childList') {
|
|
163
|
+
for (const node of mutation.addedNodes) {
|
|
164
|
+
if (node instanceof Element) {
|
|
165
|
+
if (this.isObservedTargetElement(node)) {
|
|
166
|
+
this.addTarget(node);
|
|
167
|
+
}
|
|
168
|
+
if (node.hasAttribute(this.getActionAttributeName())) {
|
|
169
|
+
this.addActionElement(node);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
for (const node of mutation.removedNodes) {
|
|
174
|
+
if (node instanceof Element) {
|
|
175
|
+
if (this.isObservedTargetElement(node)) {
|
|
176
|
+
this.removeTarget(node);
|
|
177
|
+
}
|
|
178
|
+
if (node.hasAttribute(this.getActionAttributeName())) {
|
|
179
|
+
this.removeActionElement(node);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
} else if (mutation.type === 'attributes' && mutation.target instanceof Element && typeof mutation.attributeName === 'string') {
|
|
184
|
+
const oldValue = mutation.oldValue;
|
|
185
|
+
const currentValue = mutation.target.getAttribute(mutation.attributeName);
|
|
186
|
+
if (mutation.attributeName === this.getActionAttributeName()) {
|
|
187
|
+
if (oldValue === null && currentValue !== null) {
|
|
188
|
+
this.addActionElement(mutation.target);
|
|
189
|
+
} else if (oldValue !== null && currentValue !== null && oldValue !== currentValue) {
|
|
190
|
+
this.addActionElement(mutation.target);
|
|
191
|
+
} else if (oldValue !== null && currentValue === null) {
|
|
192
|
+
this.removeActionElement(mutation.target, true);
|
|
193
|
+
}
|
|
194
|
+
} else {
|
|
195
|
+
for (const identifier of this.identifiers) {
|
|
196
|
+
if (mutation.attributeName === this.getTargetAttributeName(identifier)) {
|
|
197
|
+
if (oldValue === null && currentValue !== null) {
|
|
198
|
+
this.addTarget(mutation.target, identifier);
|
|
199
|
+
} else if (oldValue !== null && currentValue === null) {
|
|
200
|
+
this.removeTarget(mutation.target, identifier);
|
|
201
|
+
} else if (oldValue !== null && currentValue !== null && oldValue !== currentValue) {
|
|
202
|
+
this.removeStoredTargetByTargetName(mutation.target, identifier, oldValue);
|
|
203
|
+
this.storeTargetByTargetName(mutation.target, identifier, currentValue);
|
|
204
|
+
}
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private addTarget(target: Element): void;
|
|
214
|
+
private addTarget(target: Element, identifier: string): void;
|
|
215
|
+
private addTarget(firstArg: any, secondArg?: any): void {
|
|
216
|
+
const target = firstArg;
|
|
217
|
+
if (!(target instanceof Element)) {
|
|
218
|
+
throw new Error('Expected first argument to be an Element');
|
|
219
|
+
}
|
|
220
|
+
const identifier = secondArg;
|
|
221
|
+
const addTarget = (target: Element, identifier: string): void => {
|
|
222
|
+
const targetAttributeName = this.getTargetAttributeName(identifier);
|
|
223
|
+
if (!target.hasAttribute(targetAttributeName)) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
let targetsByIdentifier = this.targetsByIdentifier.get(identifier);
|
|
227
|
+
if (targetsByIdentifier === undefined) {
|
|
228
|
+
targetsByIdentifier = new Set();
|
|
229
|
+
this.targetsByIdentifier.set(identifier, targetsByIdentifier);
|
|
230
|
+
}
|
|
231
|
+
targetsByIdentifier.add(target);
|
|
232
|
+
const targetName = target.getAttribute(targetAttributeName)!;
|
|
233
|
+
this.storeTargetByTargetName(target, identifier, targetName);
|
|
234
|
+
const controllers = this.controllers.get(identifier);
|
|
235
|
+
if (controllers === undefined) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
for (const controller of controllers) {
|
|
239
|
+
let targetsByController = this.targetsByController.get(controller);
|
|
240
|
+
if (targetsByController === undefined) {
|
|
241
|
+
targetsByController = new Set();
|
|
242
|
+
this.targetsByController.set(controller, targetsByController);
|
|
243
|
+
}
|
|
244
|
+
if (targetsByController.has(target)) {
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
try {
|
|
248
|
+
controller.context.invokeControllerMethod(this.getTargetConnectedMethodName(targetName), target);
|
|
249
|
+
} catch (e) {
|
|
250
|
+
console.error(e);
|
|
251
|
+
} finally {
|
|
252
|
+
targetsByController.add(target);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (typeof identifier === 'string') {
|
|
257
|
+
addTarget(target, identifier);
|
|
258
|
+
} else if (typeof identifier === 'undefined') {
|
|
259
|
+
for (const identifier of this.identifiers) {
|
|
260
|
+
addTarget(target, identifier);
|
|
261
|
+
}
|
|
262
|
+
} else {
|
|
263
|
+
throw new Error('Expected second argument to be a string or undefined');
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
private removeTarget(target: Element): void;
|
|
268
|
+
private removeTarget(target: Element, identifier: string): void;
|
|
269
|
+
private removeTarget(firstArg: any, secondArg?: any): void {
|
|
270
|
+
const target = firstArg;
|
|
271
|
+
if (!(target instanceof Element)) {
|
|
272
|
+
throw new Error('Expected first argument to be an Element');
|
|
273
|
+
}
|
|
274
|
+
const identifier = secondArg;
|
|
275
|
+
const removeTarget = (target: Element, identifier: string): void => {
|
|
276
|
+
const targetAttributeName = this.getTargetAttributeName(identifier);
|
|
277
|
+
if (!target.hasAttribute(targetAttributeName)) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const targetsByIdentifier = this.targetsByIdentifier.get(identifier);
|
|
281
|
+
if (targetsByIdentifier !== undefined) {
|
|
282
|
+
targetsByIdentifier.delete(target);
|
|
283
|
+
}
|
|
284
|
+
const targetName = target.getAttribute(targetAttributeName)!;
|
|
285
|
+
this.removeStoredTargetByTargetName(target, identifier, targetName);
|
|
286
|
+
const controllers = this.controllers.get(identifier);
|
|
287
|
+
if (controllers === undefined) {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
for (const controller of controllers) {
|
|
291
|
+
let targetsByController = this.targetsByController.get(controller);
|
|
292
|
+
if (targetsByController === undefined) {
|
|
293
|
+
targetsByController = new Set();
|
|
294
|
+
this.targetsByController.set(controller, targetsByController);
|
|
295
|
+
}
|
|
296
|
+
if (!targetsByController.has(target)) {
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
try {
|
|
300
|
+
controller.context.invokeControllerMethod(this.getTargetDisconnectedMethodName(targetName), target);
|
|
301
|
+
} catch (e) {
|
|
302
|
+
console.error(e);
|
|
303
|
+
} finally {
|
|
304
|
+
targetsByController.delete(target);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (typeof identifier === 'string') {
|
|
309
|
+
removeTarget(target, identifier);
|
|
310
|
+
} else if (typeof identifier === 'undefined') {
|
|
311
|
+
for (const identifier of this.identifiers) {
|
|
312
|
+
removeTarget(target, identifier);
|
|
313
|
+
}
|
|
314
|
+
} else {
|
|
315
|
+
throw new Error('Expected second argument to be a string or undefined');
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
private disconnectAllTargets(): void {
|
|
320
|
+
for (const identifier of this.identifiers) {
|
|
321
|
+
const targetsByIdentifier = this.targetsByIdentifier.get(identifier);
|
|
322
|
+
if (targetsByIdentifier !== undefined) {
|
|
323
|
+
targetsByIdentifier.clear();
|
|
324
|
+
}
|
|
325
|
+
const controllers = this.controllers.get(identifier);
|
|
326
|
+
if (controllers === undefined) {
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
for (const controller of controllers) {
|
|
330
|
+
const targetsByController = this.targetsByController.get(controller);
|
|
331
|
+
if (targetsByController === undefined) {
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
for (const target of targetsByController) {
|
|
335
|
+
const targetAttributeName = this.getTargetAttributeName(identifier);
|
|
336
|
+
if (!target.hasAttribute(targetAttributeName)) {
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
const targetName = target.getAttribute(targetAttributeName)!;
|
|
340
|
+
this.removeStoredTargetByTargetName(target, identifier, targetName);
|
|
341
|
+
try {
|
|
342
|
+
controller.context.invokeControllerMethod(this.getTargetDisconnectedMethodName(targetName), target);
|
|
343
|
+
} catch (e) {
|
|
344
|
+
console.error(e);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
targetsByController.clear();
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
private searchTargets(): void {
|
|
353
|
+
if (!this.isConnected) {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
let batch = [];
|
|
357
|
+
const batchSize = 5;
|
|
358
|
+
const identifiers = [...this.identifiers].filter(identifier => !this.searchedIdentifiersForTargets.has(identifier));
|
|
359
|
+
for (let i = 0; i < identifiers.length; i++) {
|
|
360
|
+
batch.push(`[${this.getTargetAttributeName(identifiers[i])}]`);
|
|
361
|
+
if (batch.length == batchSize || i + 1 === identifiers.length) {
|
|
362
|
+
const targets = this.element.querySelectorAll(batch.join(','));
|
|
363
|
+
for (const target of targets) {
|
|
364
|
+
if (target instanceof Element) {
|
|
365
|
+
this.addTarget(target, identifiers[i]);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
batch = [];
|
|
369
|
+
}
|
|
370
|
+
this.searchedIdentifiersForTargets.add(identifiers[i]);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
private searchActions(): void {
|
|
375
|
+
if (!this.isConnected) {
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
const identifiers = [...this.identifiers].filter(identifier => !this.searchedIdentifiersForActions.has(identifier));
|
|
379
|
+
if (identifiers.length === 0) {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
const elements = this.element.querySelectorAll(`[${this.getActionAttributeName()}]`);
|
|
383
|
+
for (const element of elements) {
|
|
384
|
+
this.addActionElement(element);
|
|
385
|
+
}
|
|
386
|
+
for (const identifier of identifiers) {
|
|
387
|
+
this.searchedIdentifiersForActions.add(identifier);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
private getTargetConnectedMethodName(targetName: string): string {
|
|
392
|
+
return camelCase(targetName) + 'TargetConnected';
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
private getTargetDisconnectedMethodName(targetName: string): string {
|
|
396
|
+
return camelCase(targetName) + 'TargetDisconnected';
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
private getTargetAttributeName(identifier: string): string {
|
|
400
|
+
return `data-${identifier}-target`;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
private getActionAttributeName(): string {
|
|
404
|
+
return this.context.schema.actionAttribute;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
private getPortalledActionAttributeName(): string {
|
|
408
|
+
return this.context.schema.actionAttribute + '-portalled';
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
private isObservedTargetElement(element: Element): boolean {
|
|
412
|
+
for (const identifier of this.identifiers) {
|
|
413
|
+
if (element.hasAttribute(this.getTargetAttributeName(identifier))) {
|
|
414
|
+
return true;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return false;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
private overrideControllerGetTargetMethods(controller: Controller): void {
|
|
421
|
+
let originalMethods = this.controllerOriginalMethods.get(controller);
|
|
422
|
+
if (originalMethods !== undefined) {
|
|
423
|
+
throw new Error(`Controller ${controller.identifier} already has overridden target methods`);
|
|
424
|
+
}
|
|
425
|
+
originalMethods = {};
|
|
426
|
+
const targetNames = (controller.constructor as any).targets;
|
|
427
|
+
if (!Array.isArray(targetNames) || targetNames.length === 0) {
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
const targetDescriptorsMap: Map<string, string> = new Map();
|
|
431
|
+
for (const targetName of targetNames) {
|
|
432
|
+
targetDescriptorsMap.set(`${targetName}Target`, targetName);
|
|
433
|
+
targetDescriptorsMap.set(`${targetName}Targets`, targetName);
|
|
434
|
+
targetDescriptorsMap.set(`has${capitalize(targetName)}Target`, targetName);
|
|
435
|
+
}
|
|
436
|
+
const descriptors: { [key: string]: TypedPropertyDescriptor<Controller> } = {};
|
|
437
|
+
let prototype = controller;
|
|
438
|
+
while (prototype !== Object.prototype) {
|
|
439
|
+
const prototypeDescriptors = Object.getOwnPropertyDescriptors(prototype);
|
|
440
|
+
Object.keys(prototypeDescriptors).forEach((descriptorName) => {
|
|
441
|
+
if (!targetDescriptorsMap.has(descriptorName) || descriptors[descriptorName] !== undefined) {
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
descriptors[descriptorName] = prototypeDescriptors[descriptorName] as TypedPropertyDescriptor<Controller>;
|
|
445
|
+
});
|
|
446
|
+
prototype = Object.getPrototypeOf(prototype);
|
|
447
|
+
}
|
|
448
|
+
Object.keys(descriptors).forEach((descriptorName) => {
|
|
449
|
+
const targetName = targetDescriptorsMap.get(descriptorName);
|
|
450
|
+
if (targetName === undefined) {
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
const portal = this;
|
|
454
|
+
const descriptor = descriptors[descriptorName];
|
|
455
|
+
if (descriptorName === `has${capitalize(targetName)}Target`) {
|
|
456
|
+
originalMethods[descriptorName] = descriptor;
|
|
457
|
+
Object.defineProperty(controller, descriptorName, {
|
|
458
|
+
get: function (): boolean {
|
|
459
|
+
if (portal.hasStoredTargetsByTargetName(controller.identifier, targetName)) {
|
|
460
|
+
return true;
|
|
461
|
+
}
|
|
462
|
+
return controller.targets.has(targetName);
|
|
463
|
+
},
|
|
464
|
+
configurable: true,
|
|
465
|
+
enumerable: descriptor.enumerable,
|
|
466
|
+
});
|
|
467
|
+
} else if (descriptorName === `${targetName}Target`) {
|
|
468
|
+
originalMethods[descriptorName] = descriptor;
|
|
469
|
+
Object.defineProperty(controller, descriptorName, {
|
|
470
|
+
get: function (): Element {
|
|
471
|
+
if (portal.hasStoredTargetsByTargetName(controller.identifier, targetName)) {
|
|
472
|
+
return portal.getStoredTargetsByTargetName(controller.identifier, targetName)[0];
|
|
473
|
+
}
|
|
474
|
+
const target = controller.targets.find(targetName);
|
|
475
|
+
if (target === undefined) {
|
|
476
|
+
throw new Error(`Missing target element "${targetName}" for "${controller.identifier}" controller`);
|
|
477
|
+
}
|
|
478
|
+
return target;
|
|
479
|
+
},
|
|
480
|
+
configurable: true,
|
|
481
|
+
enumerable: descriptor.enumerable,
|
|
482
|
+
});
|
|
483
|
+
} else if (descriptorName === `${targetName}Targets`) {
|
|
484
|
+
originalMethods[descriptorName] = descriptor;
|
|
485
|
+
Object.defineProperty(controller, descriptorName, {
|
|
486
|
+
get: function (): Element[] {
|
|
487
|
+
return [
|
|
488
|
+
...portal.getStoredTargetsByTargetName(controller.identifier, targetName),
|
|
489
|
+
...controller.targets.findAll(targetName),
|
|
490
|
+
];
|
|
491
|
+
},
|
|
492
|
+
configurable: true,
|
|
493
|
+
enumerable: descriptor.enumerable,
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
this.controllerOriginalMethods.set(controller, originalMethods);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
private restoreControllerGetTargetMethods(controller: Controller): void {
|
|
501
|
+
const originalMethods = this.controllerOriginalMethods.get(controller);
|
|
502
|
+
if (originalMethods === undefined) {
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
for (const [descriptorName, descriptor] of Object.entries(originalMethods)) {
|
|
506
|
+
Object.defineProperty(controller, descriptorName, {
|
|
507
|
+
...descriptor,
|
|
508
|
+
configurable: true,
|
|
509
|
+
enumerable: descriptor.enumerable,
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
this.controllerOriginalMethods.delete(controller);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
private restoreControllersGetTargetMethods(): void {
|
|
516
|
+
const controllers = this.controllerOriginalMethods.keys();
|
|
517
|
+
for (const controller of controllers) {
|
|
518
|
+
this.restoreControllerGetTargetMethods(controller);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
private storeTargetByTargetName(target: Element, identifier: string, targetName: string): void {
|
|
523
|
+
let targetsByTargetName1 = this.targetsByTargetName.get(identifier);
|
|
524
|
+
if (targetsByTargetName1 === undefined) {
|
|
525
|
+
targetsByTargetName1 = new Map();
|
|
526
|
+
this.targetsByTargetName.set(identifier, targetsByTargetName1);
|
|
527
|
+
}
|
|
528
|
+
let targetsByTargetName2 = targetsByTargetName1.get(targetName);
|
|
529
|
+
if (targetsByTargetName2 === undefined) {
|
|
530
|
+
targetsByTargetName2 = new Set();
|
|
531
|
+
targetsByTargetName1.set(targetName, targetsByTargetName2);
|
|
532
|
+
}
|
|
533
|
+
targetsByTargetName2.add(target);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
private removeStoredTargetByTargetName(target: Element, identifier: string, targetName: string): void {
|
|
537
|
+
const targetsByTargetName1 = this.targetsByTargetName.get(identifier);
|
|
538
|
+
if (targetsByTargetName1 === undefined) {
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
const targetsByTargetName2 = targetsByTargetName1.get(targetName);
|
|
542
|
+
if (targetsByTargetName2 === undefined) {
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
targetsByTargetName2.delete(target);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
private hasStoredTargetsByTargetName(identifier: string, targetName: string): boolean {
|
|
549
|
+
const targetsByTargetName1 = this.targetsByTargetName.get(identifier);
|
|
550
|
+
if (targetsByTargetName1 === undefined) {
|
|
551
|
+
return false;
|
|
552
|
+
}
|
|
553
|
+
const targetsByTargetName2 = targetsByTargetName1.get(targetName);
|
|
554
|
+
if (targetsByTargetName2 === undefined) {
|
|
555
|
+
return false;
|
|
556
|
+
}
|
|
557
|
+
return targetsByTargetName2.size > 0;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
private getStoredTargetsByTargetName(identifier: string, targetName: string): Element[] {
|
|
561
|
+
const targetsByTargetName1 = this.targetsByTargetName.get(identifier);
|
|
562
|
+
if (targetsByTargetName1 === undefined) {
|
|
563
|
+
return [];
|
|
564
|
+
}
|
|
565
|
+
const targetsByTargetName2 = targetsByTargetName1.get(targetName);
|
|
566
|
+
if (targetsByTargetName2 === undefined) {
|
|
567
|
+
return [];
|
|
568
|
+
}
|
|
569
|
+
return [...targetsByTargetName2.values()];
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
private addActionElement(element: Element): void {
|
|
573
|
+
const actionAttributeName = this.getActionAttributeName();
|
|
574
|
+
const portalledActionAttributeName = this.getPortalledActionAttributeName();
|
|
575
|
+
const actionsToProxy = [];
|
|
576
|
+
const actionsToDeleteProxyMethods = [];
|
|
577
|
+
const newAttributeValueTokens = [];
|
|
578
|
+
const newPortalledAttributeValueTokens = [];
|
|
579
|
+
if (!element.hasAttribute(actionAttributeName) && !element.hasAttribute(portalledActionAttributeName)) {
|
|
580
|
+
const elementToActionsSet = this.elementToActionsMap.get(element);
|
|
581
|
+
if (elementToActionsSet === undefined || elementToActionsSet.size === 0) {
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
for (const directive of elementToActionsSet) {
|
|
585
|
+
const actionToElementsSet = this.actionToElementsMap.get(directive);
|
|
586
|
+
if (actionToElementsSet === undefined || actionToElementsSet.size === 0) {
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
actionToElementsSet.delete(element);
|
|
590
|
+
if (actionToElementsSet.size === 0) {
|
|
591
|
+
actionsToDeleteProxyMethods.push(this.parseActionToken(directive));
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
const actions = this.parseActions(element);
|
|
596
|
+
for (const action of actions) {
|
|
597
|
+
if (action.identifier === this.identifier) {
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
const directive = action.toString();
|
|
601
|
+
let actionToElementsSet = this.actionToElementsMap.get(directive);
|
|
602
|
+
if (actionToElementsSet === undefined) {
|
|
603
|
+
actionToElementsSet = new Set<Element>();
|
|
604
|
+
this.actionToElementsMap.set(directive, actionToElementsSet);
|
|
605
|
+
}
|
|
606
|
+
let elementToActionsSet = this.elementToActionsMap.get(element);
|
|
607
|
+
if (elementToActionsSet === undefined) {
|
|
608
|
+
elementToActionsSet = new Set<string>();
|
|
609
|
+
this.elementToActionsMap.set(element, elementToActionsSet);
|
|
610
|
+
}
|
|
611
|
+
if (this.identifiers.has(action.identifier)) {
|
|
612
|
+
actionToElementsSet.add(element);
|
|
613
|
+
elementToActionsSet.add(directive);
|
|
614
|
+
actionsToProxy.push(action);
|
|
615
|
+
newPortalledAttributeValueTokens.push(directive);
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
618
|
+
newAttributeValueTokens.push(directive);
|
|
619
|
+
actionToElementsSet.delete(element);
|
|
620
|
+
elementToActionsSet.delete(directive);
|
|
621
|
+
if (actionToElementsSet.size === 0) {
|
|
622
|
+
actionsToDeleteProxyMethods.push(action);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
for (const action of actionsToDeleteProxyMethods) {
|
|
626
|
+
const proxyActionName = this.getProxyActionName(action.method);
|
|
627
|
+
if (typeof (this as any)[proxyActionName] === 'function') {
|
|
628
|
+
delete (this as any)[proxyActionName];
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
for (const action of actionsToProxy) {
|
|
632
|
+
let identifierToActionElementsSet = this.identifierToActionElementsMap.get(action.identifier);
|
|
633
|
+
if (identifierToActionElementsSet === undefined) {
|
|
634
|
+
identifierToActionElementsSet = new Set<Element>();
|
|
635
|
+
this.identifierToActionElementsMap.set(action.identifier, identifierToActionElementsSet);
|
|
636
|
+
}
|
|
637
|
+
identifierToActionElementsSet.add(element);
|
|
638
|
+
let actionElementToIdentifiersSet = this.actionElementToIdentifiersMap.get(element);
|
|
639
|
+
if (actionElementToIdentifiersSet === undefined) {
|
|
640
|
+
actionElementToIdentifiersSet = new Set<string>();
|
|
641
|
+
this.actionElementToIdentifiersMap.set(element, actionElementToIdentifiersSet);
|
|
642
|
+
}
|
|
643
|
+
actionElementToIdentifiersSet.add(action.identifier);
|
|
644
|
+
const proxyAction = this.toProxyAction(action);
|
|
645
|
+
newAttributeValueTokens.push(proxyAction.toString());
|
|
646
|
+
if (typeof (this as any)[proxyAction.method] === 'function') {
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
(this as any)[proxyAction.method] = (event: ActionEvent): void => {
|
|
650
|
+
const target = event.currentTarget;
|
|
651
|
+
if (!(target instanceof Element)) {
|
|
652
|
+
console.warn(`Proxy action "${proxyAction.method}" called on non-element target`, event);
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
const targetActions = this.parseActions(target);
|
|
656
|
+
for (const targetAction of targetActions) {
|
|
657
|
+
if (targetAction.identifier === this.identifier || targetAction.method !== action.method) {
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
const controllers = this.controllers.get(targetAction.identifier);
|
|
661
|
+
if (controllers === undefined) {
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
event.params = this.getActionParams(target, targetAction.identifier);
|
|
665
|
+
for (const controller of controllers) {
|
|
666
|
+
try {
|
|
667
|
+
controller.context.invokeControllerMethod(targetAction.method, event);
|
|
668
|
+
} catch (e) {
|
|
669
|
+
console.error(e);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
this.setAttributeValue(element, actionAttributeName, newAttributeValueTokens.join(' '));
|
|
676
|
+
this.setAttributeValue(element, portalledActionAttributeName, newPortalledAttributeValueTokens.join(' '));
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
private removeActionElement(element: Element, forceActionsAttributeRemoval: boolean = false): void {
|
|
680
|
+
const actionsToDeleteProxyMethods = [];
|
|
681
|
+
const newAttributeValueTokens = [];
|
|
682
|
+
const actions = this.parseActions(element);
|
|
683
|
+
this.elementToActionsMap.delete(element);
|
|
684
|
+
const actionElementToIdentifiersSet = this.actionElementToIdentifiersMap.get(element);
|
|
685
|
+
if (actionElementToIdentifiersSet !== undefined) {
|
|
686
|
+
for (const identifier of actionElementToIdentifiersSet) {
|
|
687
|
+
const identifierToActionElementsSet = this.identifierToActionElementsMap.get(identifier);
|
|
688
|
+
if (identifierToActionElementsSet !== undefined) {
|
|
689
|
+
identifierToActionElementsSet.delete(element);
|
|
690
|
+
if (identifierToActionElementsSet.size === 0) {
|
|
691
|
+
this.identifierToActionElementsMap.delete(identifier);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
this.actionElementToIdentifiersMap.delete(element);
|
|
696
|
+
}
|
|
697
|
+
for (const action of actions) {
|
|
698
|
+
if (action.identifier === this.identifier) {
|
|
699
|
+
continue;
|
|
700
|
+
}
|
|
701
|
+
const directive = action.toString();
|
|
702
|
+
if (!forceActionsAttributeRemoval) {
|
|
703
|
+
newAttributeValueTokens.push(directive);
|
|
704
|
+
}
|
|
705
|
+
const actionToElementsSet = this.actionToElementsMap.get(directive);
|
|
706
|
+
if (actionToElementsSet !== undefined) {
|
|
707
|
+
actionToElementsSet.delete(element);
|
|
708
|
+
if (actionToElementsSet.size === 0) {
|
|
709
|
+
this.actionToElementsMap.delete(directive);
|
|
710
|
+
actionsToDeleteProxyMethods.push(action);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
for (const action of actionsToDeleteProxyMethods) {
|
|
715
|
+
const proxyActionName = this.getProxyActionName(action.method);
|
|
716
|
+
if (typeof (this as any)[proxyActionName] === 'function') {
|
|
717
|
+
delete (this as any)[proxyActionName];
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
this.setAttributeValue(element, this.getActionAttributeName(), newAttributeValueTokens.join(' '));
|
|
721
|
+
this.setAttributeValue(element, this.getPortalledActionAttributeName(), null);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
private removeAllProxyActions(): void {
|
|
725
|
+
for (const identifier of this.identifiers) {
|
|
726
|
+
this.removeAllProxyActionsByIdentifier(identifier);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
private removeAllProxyActionsByIdentifier(identifier: string): void {
|
|
731
|
+
const elements = this.identifierToActionElementsMap.get(identifier);
|
|
732
|
+
if (elements === undefined) {
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
for (const element of elements) {
|
|
736
|
+
this.removeActionElement(element);
|
|
737
|
+
}
|
|
738
|
+
this.identifierToActionElementsMap.delete(identifier);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
private parseActions(element: Element): Action[] {
|
|
742
|
+
let attributeValue = '';
|
|
743
|
+
const actionAttributeName = this.getActionAttributeName();
|
|
744
|
+
const portalledActionAttributeName = this.getPortalledActionAttributeName();
|
|
745
|
+
if (element.hasAttribute(actionAttributeName)) {
|
|
746
|
+
attributeValue = (attributeValue + ' ' + element.getAttribute(actionAttributeName)!).trim();
|
|
747
|
+
}
|
|
748
|
+
if (element.hasAttribute(portalledActionAttributeName)) {
|
|
749
|
+
attributeValue = (attributeValue + ' ' + element.getAttribute(portalledActionAttributeName)!).trim();
|
|
750
|
+
}
|
|
751
|
+
attributeValue = attributeValue.trim();
|
|
752
|
+
if (attributeValue === '') {
|
|
753
|
+
return [];
|
|
754
|
+
}
|
|
755
|
+
const actions: Action[] = [];
|
|
756
|
+
const tokens = attributeValue.split(' ');
|
|
757
|
+
for (let token of tokens) {
|
|
758
|
+
token = token.trim();
|
|
759
|
+
if (token === '') {
|
|
760
|
+
continue;
|
|
761
|
+
}
|
|
762
|
+
actions.push(this.parseActionToken(token));
|
|
763
|
+
}
|
|
764
|
+
return actions;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
private parseActionToken(token: string): Action {
|
|
768
|
+
let [event, rest]: (string | undefined)[] = token.split('->');
|
|
769
|
+
if (rest === undefined) {
|
|
770
|
+
rest = event;
|
|
771
|
+
event = undefined;
|
|
772
|
+
}
|
|
773
|
+
const [identifier, methodDetails] = rest.split('#');
|
|
774
|
+
const [method, modifier] = methodDetails.split(':');
|
|
775
|
+
return new Action(event, identifier, method, modifier, token);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
private getProxyActionName(action: string): string {
|
|
779
|
+
return `${proxyActionPrefix}${action}`;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
private getActionParams(element: Element, identifier: string): { [_key: string]: any } {
|
|
783
|
+
const params: { [_key: string]: any } = {}
|
|
784
|
+
const parseParam = (value: string): any => {
|
|
785
|
+
try {
|
|
786
|
+
return JSON.parse(value)
|
|
787
|
+
} catch (_) {
|
|
788
|
+
return value
|
|
789
|
+
}
|
|
790
|
+
};
|
|
791
|
+
const pattern = new RegExp(`^data-${identifier}-(.+)-param$`, 'i')
|
|
792
|
+
for (const {name, value} of Array.from(element.attributes)) {
|
|
793
|
+
const match = name.match(pattern)
|
|
794
|
+
if (match === null) {
|
|
795
|
+
continue
|
|
796
|
+
}
|
|
797
|
+
params[camelCase(match[1])] = parseParam(value);
|
|
798
|
+
}
|
|
799
|
+
return params
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
private toProxyAction(action: Action): Action {
|
|
803
|
+
return new Action(
|
|
804
|
+
action.event,
|
|
805
|
+
this.identifier,
|
|
806
|
+
this.getProxyActionName(action.method),
|
|
807
|
+
action.modifier,
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
private setAttributeValue(element: Element, attributeName: string, value: string | null): void {
|
|
812
|
+
value = value === null ? '' : value.trim();
|
|
813
|
+
if (value === '' && element.hasAttribute(attributeName)) {
|
|
814
|
+
element.removeAttribute(attributeName);
|
|
815
|
+
} else if (value !== '' && !element.hasAttribute(attributeName)) {
|
|
816
|
+
element.setAttribute(attributeName, value);
|
|
817
|
+
} else if (value !== '' && element.getAttribute(attributeName) !== value) {
|
|
818
|
+
element.setAttribute(attributeName, value);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
}
|