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