turbo-spark 0.0.1
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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +28 -0
- data/Rakefile +8 -0
- data/app/assets/javascripts/hotwire_spark.js +3701 -0
- data/app/assets/javascripts/hotwire_spark.js.map +1 -0
- data/app/assets/javascripts/hotwire_spark.min.js +2 -0
- data/app/assets/javascripts/hotwire_spark.min.js.map +1 -0
- data/app/assets/stylesheets/hotwire_spark/application.css +15 -0
- data/app/channels/hotwire_spark/channel.rb +5 -0
- data/app/controllers/hotwire_spark/application_controller.rb +4 -0
- data/app/helpers/hotwire_spark/application_helper.rb +4 -0
- data/app/jobs/hotwire_spark/application_job.rb +4 -0
- data/app/mailers/hotwire_spark/application_mailer.rb +6 -0
- data/app/models/hotwire_spark/application_record.rb +5 -0
- data/app/views/layouts/hotwire_spark/application.html.erb +17 -0
- data/config/routes.rb +2 -0
- data/lib/hotwire_spark/engine.rb +7 -0
- data/lib/hotwire_spark/version.rb +3 -0
- data/lib/hotwire_spark.rb +5 -0
- data/lib/tasks/hotwire_spark_tasks.rake +4 -0
- metadata +80 -0
@@ -0,0 +1,3701 @@
|
|
1
|
+
var HotwireSpark = (function () {
|
2
|
+
'use strict';
|
3
|
+
|
4
|
+
var adapters = {
|
5
|
+
logger: typeof console !== "undefined" ? console : undefined,
|
6
|
+
WebSocket: typeof WebSocket !== "undefined" ? WebSocket : undefined
|
7
|
+
};
|
8
|
+
|
9
|
+
var logger = {
|
10
|
+
log(...messages) {
|
11
|
+
if (this.enabled) {
|
12
|
+
messages.push(Date.now());
|
13
|
+
adapters.logger.log("[ActionCable]", ...messages);
|
14
|
+
}
|
15
|
+
}
|
16
|
+
};
|
17
|
+
|
18
|
+
const now = () => (new Date).getTime();
|
19
|
+
|
20
|
+
const secondsSince = time => (now() - time) / 1e3;
|
21
|
+
|
22
|
+
class ConnectionMonitor {
|
23
|
+
constructor(connection) {
|
24
|
+
this.visibilityDidChange = this.visibilityDidChange.bind(this);
|
25
|
+
this.connection = connection;
|
26
|
+
this.reconnectAttempts = 0;
|
27
|
+
}
|
28
|
+
start() {
|
29
|
+
if (!this.isRunning()) {
|
30
|
+
this.startedAt = now();
|
31
|
+
delete this.stoppedAt;
|
32
|
+
this.startPolling();
|
33
|
+
addEventListener("visibilitychange", this.visibilityDidChange);
|
34
|
+
logger.log(`ConnectionMonitor started. stale threshold = ${this.constructor.staleThreshold} s`);
|
35
|
+
}
|
36
|
+
}
|
37
|
+
stop() {
|
38
|
+
if (this.isRunning()) {
|
39
|
+
this.stoppedAt = now();
|
40
|
+
this.stopPolling();
|
41
|
+
removeEventListener("visibilitychange", this.visibilityDidChange);
|
42
|
+
logger.log("ConnectionMonitor stopped");
|
43
|
+
}
|
44
|
+
}
|
45
|
+
isRunning() {
|
46
|
+
return this.startedAt && !this.stoppedAt;
|
47
|
+
}
|
48
|
+
recordMessage() {
|
49
|
+
this.pingedAt = now();
|
50
|
+
}
|
51
|
+
recordConnect() {
|
52
|
+
this.reconnectAttempts = 0;
|
53
|
+
delete this.disconnectedAt;
|
54
|
+
logger.log("ConnectionMonitor recorded connect");
|
55
|
+
}
|
56
|
+
recordDisconnect() {
|
57
|
+
this.disconnectedAt = now();
|
58
|
+
logger.log("ConnectionMonitor recorded disconnect");
|
59
|
+
}
|
60
|
+
startPolling() {
|
61
|
+
this.stopPolling();
|
62
|
+
this.poll();
|
63
|
+
}
|
64
|
+
stopPolling() {
|
65
|
+
clearTimeout(this.pollTimeout);
|
66
|
+
}
|
67
|
+
poll() {
|
68
|
+
this.pollTimeout = setTimeout((() => {
|
69
|
+
this.reconnectIfStale();
|
70
|
+
this.poll();
|
71
|
+
}), this.getPollInterval());
|
72
|
+
}
|
73
|
+
getPollInterval() {
|
74
|
+
const {staleThreshold: staleThreshold, reconnectionBackoffRate: reconnectionBackoffRate} = this.constructor;
|
75
|
+
const backoff = Math.pow(1 + reconnectionBackoffRate, Math.min(this.reconnectAttempts, 10));
|
76
|
+
const jitterMax = this.reconnectAttempts === 0 ? 1 : reconnectionBackoffRate;
|
77
|
+
const jitter = jitterMax * Math.random();
|
78
|
+
return staleThreshold * 1e3 * backoff * (1 + jitter);
|
79
|
+
}
|
80
|
+
reconnectIfStale() {
|
81
|
+
if (this.connectionIsStale()) {
|
82
|
+
logger.log(`ConnectionMonitor detected stale connection. reconnectAttempts = ${this.reconnectAttempts}, time stale = ${secondsSince(this.refreshedAt)} s, stale threshold = ${this.constructor.staleThreshold} s`);
|
83
|
+
this.reconnectAttempts++;
|
84
|
+
if (this.disconnectedRecently()) {
|
85
|
+
logger.log(`ConnectionMonitor skipping reopening recent disconnect. time disconnected = ${secondsSince(this.disconnectedAt)} s`);
|
86
|
+
} else {
|
87
|
+
logger.log("ConnectionMonitor reopening");
|
88
|
+
this.connection.reopen();
|
89
|
+
}
|
90
|
+
}
|
91
|
+
}
|
92
|
+
get refreshedAt() {
|
93
|
+
return this.pingedAt ? this.pingedAt : this.startedAt;
|
94
|
+
}
|
95
|
+
connectionIsStale() {
|
96
|
+
return secondsSince(this.refreshedAt) > this.constructor.staleThreshold;
|
97
|
+
}
|
98
|
+
disconnectedRecently() {
|
99
|
+
return this.disconnectedAt && secondsSince(this.disconnectedAt) < this.constructor.staleThreshold;
|
100
|
+
}
|
101
|
+
visibilityDidChange() {
|
102
|
+
if (document.visibilityState === "visible") {
|
103
|
+
setTimeout((() => {
|
104
|
+
if (this.connectionIsStale() || !this.connection.isOpen()) {
|
105
|
+
logger.log(`ConnectionMonitor reopening stale connection on visibilitychange. visibilityState = ${document.visibilityState}`);
|
106
|
+
this.connection.reopen();
|
107
|
+
}
|
108
|
+
}), 200);
|
109
|
+
}
|
110
|
+
}
|
111
|
+
}
|
112
|
+
|
113
|
+
ConnectionMonitor.staleThreshold = 6;
|
114
|
+
|
115
|
+
ConnectionMonitor.reconnectionBackoffRate = .15;
|
116
|
+
|
117
|
+
var INTERNAL = {
|
118
|
+
message_types: {
|
119
|
+
welcome: "welcome",
|
120
|
+
disconnect: "disconnect",
|
121
|
+
ping: "ping",
|
122
|
+
confirmation: "confirm_subscription",
|
123
|
+
rejection: "reject_subscription"
|
124
|
+
},
|
125
|
+
disconnect_reasons: {
|
126
|
+
unauthorized: "unauthorized",
|
127
|
+
invalid_request: "invalid_request",
|
128
|
+
server_restart: "server_restart",
|
129
|
+
remote: "remote"
|
130
|
+
},
|
131
|
+
default_mount_path: "/cable",
|
132
|
+
protocols: [ "actioncable-v1-json", "actioncable-unsupported" ]
|
133
|
+
};
|
134
|
+
|
135
|
+
const {message_types: message_types, protocols: protocols} = INTERNAL;
|
136
|
+
|
137
|
+
const supportedProtocols = protocols.slice(0, protocols.length - 1);
|
138
|
+
|
139
|
+
const indexOf = [].indexOf;
|
140
|
+
|
141
|
+
class Connection {
|
142
|
+
constructor(consumer) {
|
143
|
+
this.open = this.open.bind(this);
|
144
|
+
this.consumer = consumer;
|
145
|
+
this.subscriptions = this.consumer.subscriptions;
|
146
|
+
this.monitor = new ConnectionMonitor(this);
|
147
|
+
this.disconnected = true;
|
148
|
+
}
|
149
|
+
send(data) {
|
150
|
+
if (this.isOpen()) {
|
151
|
+
this.webSocket.send(JSON.stringify(data));
|
152
|
+
return true;
|
153
|
+
} else {
|
154
|
+
return false;
|
155
|
+
}
|
156
|
+
}
|
157
|
+
open() {
|
158
|
+
if (this.isActive()) {
|
159
|
+
logger.log(`Attempted to open WebSocket, but existing socket is ${this.getState()}`);
|
160
|
+
return false;
|
161
|
+
} else {
|
162
|
+
const socketProtocols = [ ...protocols, ...this.consumer.subprotocols || [] ];
|
163
|
+
logger.log(`Opening WebSocket, current state is ${this.getState()}, subprotocols: ${socketProtocols}`);
|
164
|
+
if (this.webSocket) {
|
165
|
+
this.uninstallEventHandlers();
|
166
|
+
}
|
167
|
+
this.webSocket = new adapters.WebSocket(this.consumer.url, socketProtocols);
|
168
|
+
this.installEventHandlers();
|
169
|
+
this.monitor.start();
|
170
|
+
return true;
|
171
|
+
}
|
172
|
+
}
|
173
|
+
close({allowReconnect: allowReconnect} = {
|
174
|
+
allowReconnect: true
|
175
|
+
}) {
|
176
|
+
if (!allowReconnect) {
|
177
|
+
this.monitor.stop();
|
178
|
+
}
|
179
|
+
if (this.isOpen()) {
|
180
|
+
return this.webSocket.close();
|
181
|
+
}
|
182
|
+
}
|
183
|
+
reopen() {
|
184
|
+
logger.log(`Reopening WebSocket, current state is ${this.getState()}`);
|
185
|
+
if (this.isActive()) {
|
186
|
+
try {
|
187
|
+
return this.close();
|
188
|
+
} catch (error) {
|
189
|
+
logger.log("Failed to reopen WebSocket", error);
|
190
|
+
} finally {
|
191
|
+
logger.log(`Reopening WebSocket in ${this.constructor.reopenDelay}ms`);
|
192
|
+
setTimeout(this.open, this.constructor.reopenDelay);
|
193
|
+
}
|
194
|
+
} else {
|
195
|
+
return this.open();
|
196
|
+
}
|
197
|
+
}
|
198
|
+
getProtocol() {
|
199
|
+
if (this.webSocket) {
|
200
|
+
return this.webSocket.protocol;
|
201
|
+
}
|
202
|
+
}
|
203
|
+
isOpen() {
|
204
|
+
return this.isState("open");
|
205
|
+
}
|
206
|
+
isActive() {
|
207
|
+
return this.isState("open", "connecting");
|
208
|
+
}
|
209
|
+
triedToReconnect() {
|
210
|
+
return this.monitor.reconnectAttempts > 0;
|
211
|
+
}
|
212
|
+
isProtocolSupported() {
|
213
|
+
return indexOf.call(supportedProtocols, this.getProtocol()) >= 0;
|
214
|
+
}
|
215
|
+
isState(...states) {
|
216
|
+
return indexOf.call(states, this.getState()) >= 0;
|
217
|
+
}
|
218
|
+
getState() {
|
219
|
+
if (this.webSocket) {
|
220
|
+
for (let state in adapters.WebSocket) {
|
221
|
+
if (adapters.WebSocket[state] === this.webSocket.readyState) {
|
222
|
+
return state.toLowerCase();
|
223
|
+
}
|
224
|
+
}
|
225
|
+
}
|
226
|
+
return null;
|
227
|
+
}
|
228
|
+
installEventHandlers() {
|
229
|
+
for (let eventName in this.events) {
|
230
|
+
const handler = this.events[eventName].bind(this);
|
231
|
+
this.webSocket[`on${eventName}`] = handler;
|
232
|
+
}
|
233
|
+
}
|
234
|
+
uninstallEventHandlers() {
|
235
|
+
for (let eventName in this.events) {
|
236
|
+
this.webSocket[`on${eventName}`] = function() {};
|
237
|
+
}
|
238
|
+
}
|
239
|
+
}
|
240
|
+
|
241
|
+
Connection.reopenDelay = 500;
|
242
|
+
|
243
|
+
Connection.prototype.events = {
|
244
|
+
message(event) {
|
245
|
+
if (!this.isProtocolSupported()) {
|
246
|
+
return;
|
247
|
+
}
|
248
|
+
const {identifier: identifier, message: message, reason: reason, reconnect: reconnect, type: type} = JSON.parse(event.data);
|
249
|
+
this.monitor.recordMessage();
|
250
|
+
switch (type) {
|
251
|
+
case message_types.welcome:
|
252
|
+
if (this.triedToReconnect()) {
|
253
|
+
this.reconnectAttempted = true;
|
254
|
+
}
|
255
|
+
this.monitor.recordConnect();
|
256
|
+
return this.subscriptions.reload();
|
257
|
+
|
258
|
+
case message_types.disconnect:
|
259
|
+
logger.log(`Disconnecting. Reason: ${reason}`);
|
260
|
+
return this.close({
|
261
|
+
allowReconnect: reconnect
|
262
|
+
});
|
263
|
+
|
264
|
+
case message_types.ping:
|
265
|
+
return null;
|
266
|
+
|
267
|
+
case message_types.confirmation:
|
268
|
+
this.subscriptions.confirmSubscription(identifier);
|
269
|
+
if (this.reconnectAttempted) {
|
270
|
+
this.reconnectAttempted = false;
|
271
|
+
return this.subscriptions.notify(identifier, "connected", {
|
272
|
+
reconnected: true
|
273
|
+
});
|
274
|
+
} else {
|
275
|
+
return this.subscriptions.notify(identifier, "connected", {
|
276
|
+
reconnected: false
|
277
|
+
});
|
278
|
+
}
|
279
|
+
|
280
|
+
case message_types.rejection:
|
281
|
+
return this.subscriptions.reject(identifier);
|
282
|
+
|
283
|
+
default:
|
284
|
+
return this.subscriptions.notify(identifier, "received", message);
|
285
|
+
}
|
286
|
+
},
|
287
|
+
open() {
|
288
|
+
logger.log(`WebSocket onopen event, using '${this.getProtocol()}' subprotocol`);
|
289
|
+
this.disconnected = false;
|
290
|
+
if (!this.isProtocolSupported()) {
|
291
|
+
logger.log("Protocol is unsupported. Stopping monitor and disconnecting.");
|
292
|
+
return this.close({
|
293
|
+
allowReconnect: false
|
294
|
+
});
|
295
|
+
}
|
296
|
+
},
|
297
|
+
close(event) {
|
298
|
+
logger.log("WebSocket onclose event");
|
299
|
+
if (this.disconnected) {
|
300
|
+
return;
|
301
|
+
}
|
302
|
+
this.disconnected = true;
|
303
|
+
this.monitor.recordDisconnect();
|
304
|
+
return this.subscriptions.notifyAll("disconnected", {
|
305
|
+
willAttemptReconnect: this.monitor.isRunning()
|
306
|
+
});
|
307
|
+
},
|
308
|
+
error() {
|
309
|
+
logger.log("WebSocket onerror event");
|
310
|
+
}
|
311
|
+
};
|
312
|
+
|
313
|
+
const extend$1 = function(object, properties) {
|
314
|
+
if (properties != null) {
|
315
|
+
for (let key in properties) {
|
316
|
+
const value = properties[key];
|
317
|
+
object[key] = value;
|
318
|
+
}
|
319
|
+
}
|
320
|
+
return object;
|
321
|
+
};
|
322
|
+
|
323
|
+
class Subscription {
|
324
|
+
constructor(consumer, params = {}, mixin) {
|
325
|
+
this.consumer = consumer;
|
326
|
+
this.identifier = JSON.stringify(params);
|
327
|
+
extend$1(this, mixin);
|
328
|
+
}
|
329
|
+
perform(action, data = {}) {
|
330
|
+
data.action = action;
|
331
|
+
return this.send(data);
|
332
|
+
}
|
333
|
+
send(data) {
|
334
|
+
return this.consumer.send({
|
335
|
+
command: "message",
|
336
|
+
identifier: this.identifier,
|
337
|
+
data: JSON.stringify(data)
|
338
|
+
});
|
339
|
+
}
|
340
|
+
unsubscribe() {
|
341
|
+
return this.consumer.subscriptions.remove(this);
|
342
|
+
}
|
343
|
+
}
|
344
|
+
|
345
|
+
class SubscriptionGuarantor {
|
346
|
+
constructor(subscriptions) {
|
347
|
+
this.subscriptions = subscriptions;
|
348
|
+
this.pendingSubscriptions = [];
|
349
|
+
}
|
350
|
+
guarantee(subscription) {
|
351
|
+
if (this.pendingSubscriptions.indexOf(subscription) == -1) {
|
352
|
+
logger.log(`SubscriptionGuarantor guaranteeing ${subscription.identifier}`);
|
353
|
+
this.pendingSubscriptions.push(subscription);
|
354
|
+
} else {
|
355
|
+
logger.log(`SubscriptionGuarantor already guaranteeing ${subscription.identifier}`);
|
356
|
+
}
|
357
|
+
this.startGuaranteeing();
|
358
|
+
}
|
359
|
+
forget(subscription) {
|
360
|
+
logger.log(`SubscriptionGuarantor forgetting ${subscription.identifier}`);
|
361
|
+
this.pendingSubscriptions = this.pendingSubscriptions.filter((s => s !== subscription));
|
362
|
+
}
|
363
|
+
startGuaranteeing() {
|
364
|
+
this.stopGuaranteeing();
|
365
|
+
this.retrySubscribing();
|
366
|
+
}
|
367
|
+
stopGuaranteeing() {
|
368
|
+
clearTimeout(this.retryTimeout);
|
369
|
+
}
|
370
|
+
retrySubscribing() {
|
371
|
+
this.retryTimeout = setTimeout((() => {
|
372
|
+
if (this.subscriptions && typeof this.subscriptions.subscribe === "function") {
|
373
|
+
this.pendingSubscriptions.map((subscription => {
|
374
|
+
logger.log(`SubscriptionGuarantor resubscribing ${subscription.identifier}`);
|
375
|
+
this.subscriptions.subscribe(subscription);
|
376
|
+
}));
|
377
|
+
}
|
378
|
+
}), 500);
|
379
|
+
}
|
380
|
+
}
|
381
|
+
|
382
|
+
class Subscriptions {
|
383
|
+
constructor(consumer) {
|
384
|
+
this.consumer = consumer;
|
385
|
+
this.guarantor = new SubscriptionGuarantor(this);
|
386
|
+
this.subscriptions = [];
|
387
|
+
}
|
388
|
+
create(channelName, mixin) {
|
389
|
+
const channel = channelName;
|
390
|
+
const params = typeof channel === "object" ? channel : {
|
391
|
+
channel: channel
|
392
|
+
};
|
393
|
+
const subscription = new Subscription(this.consumer, params, mixin);
|
394
|
+
return this.add(subscription);
|
395
|
+
}
|
396
|
+
add(subscription) {
|
397
|
+
this.subscriptions.push(subscription);
|
398
|
+
this.consumer.ensureActiveConnection();
|
399
|
+
this.notify(subscription, "initialized");
|
400
|
+
this.subscribe(subscription);
|
401
|
+
return subscription;
|
402
|
+
}
|
403
|
+
remove(subscription) {
|
404
|
+
this.forget(subscription);
|
405
|
+
if (!this.findAll(subscription.identifier).length) {
|
406
|
+
this.sendCommand(subscription, "unsubscribe");
|
407
|
+
}
|
408
|
+
return subscription;
|
409
|
+
}
|
410
|
+
reject(identifier) {
|
411
|
+
return this.findAll(identifier).map((subscription => {
|
412
|
+
this.forget(subscription);
|
413
|
+
this.notify(subscription, "rejected");
|
414
|
+
return subscription;
|
415
|
+
}));
|
416
|
+
}
|
417
|
+
forget(subscription) {
|
418
|
+
this.guarantor.forget(subscription);
|
419
|
+
this.subscriptions = this.subscriptions.filter((s => s !== subscription));
|
420
|
+
return subscription;
|
421
|
+
}
|
422
|
+
findAll(identifier) {
|
423
|
+
return this.subscriptions.filter((s => s.identifier === identifier));
|
424
|
+
}
|
425
|
+
reload() {
|
426
|
+
return this.subscriptions.map((subscription => this.subscribe(subscription)));
|
427
|
+
}
|
428
|
+
notifyAll(callbackName, ...args) {
|
429
|
+
return this.subscriptions.map((subscription => this.notify(subscription, callbackName, ...args)));
|
430
|
+
}
|
431
|
+
notify(subscription, callbackName, ...args) {
|
432
|
+
let subscriptions;
|
433
|
+
if (typeof subscription === "string") {
|
434
|
+
subscriptions = this.findAll(subscription);
|
435
|
+
} else {
|
436
|
+
subscriptions = [ subscription ];
|
437
|
+
}
|
438
|
+
return subscriptions.map((subscription => typeof subscription[callbackName] === "function" ? subscription[callbackName](...args) : undefined));
|
439
|
+
}
|
440
|
+
subscribe(subscription) {
|
441
|
+
if (this.sendCommand(subscription, "subscribe")) {
|
442
|
+
this.guarantor.guarantee(subscription);
|
443
|
+
}
|
444
|
+
}
|
445
|
+
confirmSubscription(identifier) {
|
446
|
+
logger.log(`Subscription confirmed ${identifier}`);
|
447
|
+
this.findAll(identifier).map((subscription => this.guarantor.forget(subscription)));
|
448
|
+
}
|
449
|
+
sendCommand(subscription, command) {
|
450
|
+
const {identifier: identifier} = subscription;
|
451
|
+
return this.consumer.send({
|
452
|
+
command: command,
|
453
|
+
identifier: identifier
|
454
|
+
});
|
455
|
+
}
|
456
|
+
}
|
457
|
+
|
458
|
+
class Consumer {
|
459
|
+
constructor(url) {
|
460
|
+
this._url = url;
|
461
|
+
this.subscriptions = new Subscriptions(this);
|
462
|
+
this.connection = new Connection(this);
|
463
|
+
this.subprotocols = [];
|
464
|
+
}
|
465
|
+
get url() {
|
466
|
+
return createWebSocketURL(this._url);
|
467
|
+
}
|
468
|
+
send(data) {
|
469
|
+
return this.connection.send(data);
|
470
|
+
}
|
471
|
+
connect() {
|
472
|
+
return this.connection.open();
|
473
|
+
}
|
474
|
+
disconnect() {
|
475
|
+
return this.connection.close({
|
476
|
+
allowReconnect: false
|
477
|
+
});
|
478
|
+
}
|
479
|
+
ensureActiveConnection() {
|
480
|
+
if (!this.connection.isActive()) {
|
481
|
+
return this.connection.open();
|
482
|
+
}
|
483
|
+
}
|
484
|
+
addSubProtocol(subprotocol) {
|
485
|
+
this.subprotocols = [ ...this.subprotocols, subprotocol ];
|
486
|
+
}
|
487
|
+
}
|
488
|
+
|
489
|
+
function createWebSocketURL(url) {
|
490
|
+
if (typeof url === "function") {
|
491
|
+
url = url();
|
492
|
+
}
|
493
|
+
if (url && !/^wss?:/i.test(url)) {
|
494
|
+
const a = document.createElement("a");
|
495
|
+
a.href = url;
|
496
|
+
a.href = a.href;
|
497
|
+
a.protocol = a.protocol.replace("http", "ws");
|
498
|
+
return a.href;
|
499
|
+
} else {
|
500
|
+
return url;
|
501
|
+
}
|
502
|
+
}
|
503
|
+
|
504
|
+
function createConsumer(url = getConfig("url") || INTERNAL.default_mount_path) {
|
505
|
+
return new Consumer(url);
|
506
|
+
}
|
507
|
+
|
508
|
+
function getConfig(name) {
|
509
|
+
const element = document.head.querySelector(`meta[name='action-cable-${name}']`);
|
510
|
+
if (element) {
|
511
|
+
return element.getAttribute("content");
|
512
|
+
}
|
513
|
+
}
|
514
|
+
|
515
|
+
var consumer = createConsumer();
|
516
|
+
|
517
|
+
function nameFromFilePath(path) {
|
518
|
+
return path.split("/").pop().split(".")[0];
|
519
|
+
}
|
520
|
+
function urlWithParams(urlString, params) {
|
521
|
+
const url = new URL(urlString, window.location.origin);
|
522
|
+
Object.entries(params).forEach(_ref => {
|
523
|
+
let [key, value] = _ref;
|
524
|
+
url.searchParams.set(key, value);
|
525
|
+
});
|
526
|
+
return url.toString();
|
527
|
+
}
|
528
|
+
function cacheBustedUrl(urlString) {
|
529
|
+
return urlWithParams(urlString, {
|
530
|
+
reload: Date.now()
|
531
|
+
});
|
532
|
+
}
|
533
|
+
async function reloadHtmlDocument() {
|
534
|
+
let currentUrl = cacheBustedUrl(urlWithParams(window.location.href, {
|
535
|
+
hotwire_spark: "true"
|
536
|
+
}));
|
537
|
+
const response = await fetch(currentUrl);
|
538
|
+
if (!response.ok) {
|
539
|
+
throw new Error(`${response.status} when fetching ${currentUrl}`);
|
540
|
+
}
|
541
|
+
const fetchedHTML = await response.text();
|
542
|
+
const parser = new DOMParser();
|
543
|
+
return parser.parseFromString(fetchedHTML, "text/html");
|
544
|
+
}
|
545
|
+
|
546
|
+
// base IIFE to define idiomorph
|
547
|
+
var Idiomorph = (function () {
|
548
|
+
|
549
|
+
//=============================================================================
|
550
|
+
// AND NOW IT BEGINS...
|
551
|
+
//=============================================================================
|
552
|
+
let EMPTY_SET = new Set();
|
553
|
+
|
554
|
+
// default configuration values, updatable by users now
|
555
|
+
let defaults = {
|
556
|
+
morphStyle: "outerHTML",
|
557
|
+
callbacks : {
|
558
|
+
beforeNodeAdded: noOp,
|
559
|
+
afterNodeAdded: noOp,
|
560
|
+
beforeNodeMorphed: noOp,
|
561
|
+
afterNodeMorphed: noOp,
|
562
|
+
beforeNodeRemoved: noOp,
|
563
|
+
afterNodeRemoved: noOp,
|
564
|
+
beforeAttributeUpdated: noOp,
|
565
|
+
|
566
|
+
},
|
567
|
+
head: {
|
568
|
+
style: 'merge',
|
569
|
+
shouldPreserve: function (elt) {
|
570
|
+
return elt.getAttribute("im-preserve") === "true";
|
571
|
+
},
|
572
|
+
shouldReAppend: function (elt) {
|
573
|
+
return elt.getAttribute("im-re-append") === "true";
|
574
|
+
},
|
575
|
+
shouldRemove: noOp,
|
576
|
+
afterHeadMorphed: noOp,
|
577
|
+
}
|
578
|
+
};
|
579
|
+
|
580
|
+
//=============================================================================
|
581
|
+
// Core Morphing Algorithm - morph, morphNormalizedContent, morphOldNodeTo, morphChildren
|
582
|
+
//=============================================================================
|
583
|
+
function morph(oldNode, newContent, config = {}) {
|
584
|
+
|
585
|
+
if (oldNode instanceof Document) {
|
586
|
+
oldNode = oldNode.documentElement;
|
587
|
+
}
|
588
|
+
|
589
|
+
if (typeof newContent === 'string') {
|
590
|
+
newContent = parseContent(newContent);
|
591
|
+
}
|
592
|
+
|
593
|
+
let normalizedContent = normalizeContent(newContent);
|
594
|
+
|
595
|
+
let ctx = createMorphContext(oldNode, normalizedContent, config);
|
596
|
+
|
597
|
+
return morphNormalizedContent(oldNode, normalizedContent, ctx);
|
598
|
+
}
|
599
|
+
|
600
|
+
function morphNormalizedContent(oldNode, normalizedNewContent, ctx) {
|
601
|
+
if (ctx.head.block) {
|
602
|
+
let oldHead = oldNode.querySelector('head');
|
603
|
+
let newHead = normalizedNewContent.querySelector('head');
|
604
|
+
if (oldHead && newHead) {
|
605
|
+
let promises = handleHeadElement(newHead, oldHead, ctx);
|
606
|
+
// when head promises resolve, call morph again, ignoring the head tag
|
607
|
+
Promise.all(promises).then(function () {
|
608
|
+
morphNormalizedContent(oldNode, normalizedNewContent, Object.assign(ctx, {
|
609
|
+
head: {
|
610
|
+
block: false,
|
611
|
+
ignore: true
|
612
|
+
}
|
613
|
+
}));
|
614
|
+
});
|
615
|
+
return;
|
616
|
+
}
|
617
|
+
}
|
618
|
+
|
619
|
+
if (ctx.morphStyle === "innerHTML") {
|
620
|
+
|
621
|
+
// innerHTML, so we are only updating the children
|
622
|
+
morphChildren(normalizedNewContent, oldNode, ctx);
|
623
|
+
return oldNode.children;
|
624
|
+
|
625
|
+
} else if (ctx.morphStyle === "outerHTML" || ctx.morphStyle == null) {
|
626
|
+
// otherwise find the best element match in the new content, morph that, and merge its siblings
|
627
|
+
// into either side of the best match
|
628
|
+
let bestMatch = findBestNodeMatch(normalizedNewContent, oldNode, ctx);
|
629
|
+
|
630
|
+
// stash the siblings that will need to be inserted on either side of the best match
|
631
|
+
let previousSibling = bestMatch?.previousSibling;
|
632
|
+
let nextSibling = bestMatch?.nextSibling;
|
633
|
+
|
634
|
+
// morph it
|
635
|
+
let morphedNode = morphOldNodeTo(oldNode, bestMatch, ctx);
|
636
|
+
|
637
|
+
if (bestMatch) {
|
638
|
+
// if there was a best match, merge the siblings in too and return the
|
639
|
+
// whole bunch
|
640
|
+
return insertSiblings(previousSibling, morphedNode, nextSibling);
|
641
|
+
} else {
|
642
|
+
// otherwise nothing was added to the DOM
|
643
|
+
return []
|
644
|
+
}
|
645
|
+
} else {
|
646
|
+
throw "Do not understand how to morph style " + ctx.morphStyle;
|
647
|
+
}
|
648
|
+
}
|
649
|
+
|
650
|
+
|
651
|
+
/**
|
652
|
+
* @param possibleActiveElement
|
653
|
+
* @param ctx
|
654
|
+
* @returns {boolean}
|
655
|
+
*/
|
656
|
+
function ignoreValueOfActiveElement(possibleActiveElement, ctx) {
|
657
|
+
return ctx.ignoreActiveValue && possibleActiveElement === document.activeElement;
|
658
|
+
}
|
659
|
+
|
660
|
+
/**
|
661
|
+
* @param oldNode root node to merge content into
|
662
|
+
* @param newContent new content to merge
|
663
|
+
* @param ctx the merge context
|
664
|
+
* @returns {Element} the element that ended up in the DOM
|
665
|
+
*/
|
666
|
+
function morphOldNodeTo(oldNode, newContent, ctx) {
|
667
|
+
if (ctx.ignoreActive && oldNode === document.activeElement) ; else if (newContent == null) {
|
668
|
+
if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return oldNode;
|
669
|
+
|
670
|
+
oldNode.remove();
|
671
|
+
ctx.callbacks.afterNodeRemoved(oldNode);
|
672
|
+
return null;
|
673
|
+
} else if (!isSoftMatch(oldNode, newContent)) {
|
674
|
+
if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return oldNode;
|
675
|
+
if (ctx.callbacks.beforeNodeAdded(newContent) === false) return oldNode;
|
676
|
+
|
677
|
+
oldNode.parentElement.replaceChild(newContent, oldNode);
|
678
|
+
ctx.callbacks.afterNodeAdded(newContent);
|
679
|
+
ctx.callbacks.afterNodeRemoved(oldNode);
|
680
|
+
return newContent;
|
681
|
+
} else {
|
682
|
+
if (ctx.callbacks.beforeNodeMorphed(oldNode, newContent) === false) return oldNode;
|
683
|
+
|
684
|
+
if (oldNode instanceof HTMLHeadElement && ctx.head.ignore) ; else if (oldNode instanceof HTMLHeadElement && ctx.head.style !== "morph") {
|
685
|
+
handleHeadElement(newContent, oldNode, ctx);
|
686
|
+
} else {
|
687
|
+
syncNodeFrom(newContent, oldNode, ctx);
|
688
|
+
if (!ignoreValueOfActiveElement(oldNode, ctx)) {
|
689
|
+
morphChildren(newContent, oldNode, ctx);
|
690
|
+
}
|
691
|
+
}
|
692
|
+
ctx.callbacks.afterNodeMorphed(oldNode, newContent);
|
693
|
+
return oldNode;
|
694
|
+
}
|
695
|
+
}
|
696
|
+
|
697
|
+
/**
|
698
|
+
* This is the core algorithm for matching up children. The idea is to use id sets to try to match up
|
699
|
+
* nodes as faithfully as possible. We greedily match, which allows us to keep the algorithm fast, but
|
700
|
+
* by using id sets, we are able to better match up with content deeper in the DOM.
|
701
|
+
*
|
702
|
+
* Basic algorithm is, for each node in the new content:
|
703
|
+
*
|
704
|
+
* - if we have reached the end of the old parent, append the new content
|
705
|
+
* - if the new content has an id set match with the current insertion point, morph
|
706
|
+
* - search for an id set match
|
707
|
+
* - if id set match found, morph
|
708
|
+
* - otherwise search for a "soft" match
|
709
|
+
* - if a soft match is found, morph
|
710
|
+
* - otherwise, prepend the new node before the current insertion point
|
711
|
+
*
|
712
|
+
* The two search algorithms terminate if competing node matches appear to outweigh what can be achieved
|
713
|
+
* with the current node. See findIdSetMatch() and findSoftMatch() for details.
|
714
|
+
*
|
715
|
+
* @param {Element} newParent the parent element of the new content
|
716
|
+
* @param {Element } oldParent the old content that we are merging the new content into
|
717
|
+
* @param ctx the merge context
|
718
|
+
*/
|
719
|
+
function morphChildren(newParent, oldParent, ctx) {
|
720
|
+
|
721
|
+
let nextNewChild = newParent.firstChild;
|
722
|
+
let insertionPoint = oldParent.firstChild;
|
723
|
+
let newChild;
|
724
|
+
|
725
|
+
// run through all the new content
|
726
|
+
while (nextNewChild) {
|
727
|
+
|
728
|
+
newChild = nextNewChild;
|
729
|
+
nextNewChild = newChild.nextSibling;
|
730
|
+
|
731
|
+
// if we are at the end of the exiting parent's children, just append
|
732
|
+
if (insertionPoint == null) {
|
733
|
+
if (ctx.callbacks.beforeNodeAdded(newChild) === false) return;
|
734
|
+
|
735
|
+
oldParent.appendChild(newChild);
|
736
|
+
ctx.callbacks.afterNodeAdded(newChild);
|
737
|
+
removeIdsFromConsideration(ctx, newChild);
|
738
|
+
continue;
|
739
|
+
}
|
740
|
+
|
741
|
+
// if the current node has an id set match then morph
|
742
|
+
if (isIdSetMatch(newChild, insertionPoint, ctx)) {
|
743
|
+
morphOldNodeTo(insertionPoint, newChild, ctx);
|
744
|
+
insertionPoint = insertionPoint.nextSibling;
|
745
|
+
removeIdsFromConsideration(ctx, newChild);
|
746
|
+
continue;
|
747
|
+
}
|
748
|
+
|
749
|
+
// otherwise search forward in the existing old children for an id set match
|
750
|
+
let idSetMatch = findIdSetMatch(newParent, oldParent, newChild, insertionPoint, ctx);
|
751
|
+
|
752
|
+
// if we found a potential match, remove the nodes until that point and morph
|
753
|
+
if (idSetMatch) {
|
754
|
+
insertionPoint = removeNodesBetween(insertionPoint, idSetMatch, ctx);
|
755
|
+
morphOldNodeTo(idSetMatch, newChild, ctx);
|
756
|
+
removeIdsFromConsideration(ctx, newChild);
|
757
|
+
continue;
|
758
|
+
}
|
759
|
+
|
760
|
+
// no id set match found, so scan forward for a soft match for the current node
|
761
|
+
let softMatch = findSoftMatch(newParent, oldParent, newChild, insertionPoint, ctx);
|
762
|
+
|
763
|
+
// if we found a soft match for the current node, morph
|
764
|
+
if (softMatch) {
|
765
|
+
insertionPoint = removeNodesBetween(insertionPoint, softMatch, ctx);
|
766
|
+
morphOldNodeTo(softMatch, newChild, ctx);
|
767
|
+
removeIdsFromConsideration(ctx, newChild);
|
768
|
+
continue;
|
769
|
+
}
|
770
|
+
|
771
|
+
// abandon all hope of morphing, just insert the new child before the insertion point
|
772
|
+
// and move on
|
773
|
+
if (ctx.callbacks.beforeNodeAdded(newChild) === false) return;
|
774
|
+
|
775
|
+
oldParent.insertBefore(newChild, insertionPoint);
|
776
|
+
ctx.callbacks.afterNodeAdded(newChild);
|
777
|
+
removeIdsFromConsideration(ctx, newChild);
|
778
|
+
}
|
779
|
+
|
780
|
+
// remove any remaining old nodes that didn't match up with new content
|
781
|
+
while (insertionPoint !== null) {
|
782
|
+
|
783
|
+
let tempNode = insertionPoint;
|
784
|
+
insertionPoint = insertionPoint.nextSibling;
|
785
|
+
removeNode(tempNode, ctx);
|
786
|
+
}
|
787
|
+
}
|
788
|
+
|
789
|
+
//=============================================================================
|
790
|
+
// Attribute Syncing Code
|
791
|
+
//=============================================================================
|
792
|
+
|
793
|
+
/**
|
794
|
+
* @param attr {String} the attribute to be mutated
|
795
|
+
* @param to {Element} the element that is going to be updated
|
796
|
+
* @param updateType {("update"|"remove")}
|
797
|
+
* @param ctx the merge context
|
798
|
+
* @returns {boolean} true if the attribute should be ignored, false otherwise
|
799
|
+
*/
|
800
|
+
function ignoreAttribute(attr, to, updateType, ctx) {
|
801
|
+
if(attr === 'value' && ctx.ignoreActiveValue && to === document.activeElement){
|
802
|
+
return true;
|
803
|
+
}
|
804
|
+
return ctx.callbacks.beforeAttributeUpdated(attr, to, updateType) === false;
|
805
|
+
}
|
806
|
+
|
807
|
+
/**
|
808
|
+
* syncs a given node with another node, copying over all attributes and
|
809
|
+
* inner element state from the 'from' node to the 'to' node
|
810
|
+
*
|
811
|
+
* @param {Element} from the element to copy attributes & state from
|
812
|
+
* @param {Element} to the element to copy attributes & state to
|
813
|
+
* @param ctx the merge context
|
814
|
+
*/
|
815
|
+
function syncNodeFrom(from, to, ctx) {
|
816
|
+
let type = from.nodeType;
|
817
|
+
|
818
|
+
// if is an element type, sync the attributes from the
|
819
|
+
// new node into the new node
|
820
|
+
if (type === 1 /* element type */) {
|
821
|
+
const fromAttributes = from.attributes;
|
822
|
+
const toAttributes = to.attributes;
|
823
|
+
for (const fromAttribute of fromAttributes) {
|
824
|
+
if (ignoreAttribute(fromAttribute.name, to, 'update', ctx)) {
|
825
|
+
continue;
|
826
|
+
}
|
827
|
+
if (to.getAttribute(fromAttribute.name) !== fromAttribute.value) {
|
828
|
+
to.setAttribute(fromAttribute.name, fromAttribute.value);
|
829
|
+
}
|
830
|
+
}
|
831
|
+
// iterate backwards to avoid skipping over items when a delete occurs
|
832
|
+
for (let i = toAttributes.length - 1; 0 <= i; i--) {
|
833
|
+
const toAttribute = toAttributes[i];
|
834
|
+
if (ignoreAttribute(toAttribute.name, to, 'remove', ctx)) {
|
835
|
+
continue;
|
836
|
+
}
|
837
|
+
if (!from.hasAttribute(toAttribute.name)) {
|
838
|
+
to.removeAttribute(toAttribute.name);
|
839
|
+
}
|
840
|
+
}
|
841
|
+
}
|
842
|
+
|
843
|
+
// sync text nodes
|
844
|
+
if (type === 8 /* comment */ || type === 3 /* text */) {
|
845
|
+
if (to.nodeValue !== from.nodeValue) {
|
846
|
+
to.nodeValue = from.nodeValue;
|
847
|
+
}
|
848
|
+
}
|
849
|
+
|
850
|
+
if (!ignoreValueOfActiveElement(to, ctx)) {
|
851
|
+
// sync input values
|
852
|
+
syncInputValue(from, to, ctx);
|
853
|
+
}
|
854
|
+
}
|
855
|
+
|
856
|
+
/**
|
857
|
+
* @param from {Element} element to sync the value from
|
858
|
+
* @param to {Element} element to sync the value to
|
859
|
+
* @param attributeName {String} the attribute name
|
860
|
+
* @param ctx the merge context
|
861
|
+
*/
|
862
|
+
function syncBooleanAttribute(from, to, attributeName, ctx) {
|
863
|
+
if (from[attributeName] !== to[attributeName]) {
|
864
|
+
let ignoreUpdate = ignoreAttribute(attributeName, to, 'update', ctx);
|
865
|
+
if (!ignoreUpdate) {
|
866
|
+
to[attributeName] = from[attributeName];
|
867
|
+
}
|
868
|
+
if (from[attributeName]) {
|
869
|
+
if (!ignoreUpdate) {
|
870
|
+
to.setAttribute(attributeName, from[attributeName]);
|
871
|
+
}
|
872
|
+
} else {
|
873
|
+
if (!ignoreAttribute(attributeName, to, 'remove', ctx)) {
|
874
|
+
to.removeAttribute(attributeName);
|
875
|
+
}
|
876
|
+
}
|
877
|
+
}
|
878
|
+
}
|
879
|
+
|
880
|
+
/**
|
881
|
+
* NB: many bothans died to bring us information:
|
882
|
+
*
|
883
|
+
* https://github.com/patrick-steele-idem/morphdom/blob/master/src/specialElHandlers.js
|
884
|
+
* https://github.com/choojs/nanomorph/blob/master/lib/morph.jsL113
|
885
|
+
*
|
886
|
+
* @param from {Element} the element to sync the input value from
|
887
|
+
* @param to {Element} the element to sync the input value to
|
888
|
+
* @param ctx the merge context
|
889
|
+
*/
|
890
|
+
function syncInputValue(from, to, ctx) {
|
891
|
+
if (from instanceof HTMLInputElement &&
|
892
|
+
to instanceof HTMLInputElement &&
|
893
|
+
from.type !== 'file') {
|
894
|
+
|
895
|
+
let fromValue = from.value;
|
896
|
+
let toValue = to.value;
|
897
|
+
|
898
|
+
// sync boolean attributes
|
899
|
+
syncBooleanAttribute(from, to, 'checked', ctx);
|
900
|
+
syncBooleanAttribute(from, to, 'disabled', ctx);
|
901
|
+
|
902
|
+
if (!from.hasAttribute('value')) {
|
903
|
+
if (!ignoreAttribute('value', to, 'remove', ctx)) {
|
904
|
+
to.value = '';
|
905
|
+
to.removeAttribute('value');
|
906
|
+
}
|
907
|
+
} else if (fromValue !== toValue) {
|
908
|
+
if (!ignoreAttribute('value', to, 'update', ctx)) {
|
909
|
+
to.setAttribute('value', fromValue);
|
910
|
+
to.value = fromValue;
|
911
|
+
}
|
912
|
+
}
|
913
|
+
} else if (from instanceof HTMLOptionElement) {
|
914
|
+
syncBooleanAttribute(from, to, 'selected', ctx);
|
915
|
+
} else if (from instanceof HTMLTextAreaElement && to instanceof HTMLTextAreaElement) {
|
916
|
+
let fromValue = from.value;
|
917
|
+
let toValue = to.value;
|
918
|
+
if (ignoreAttribute('value', to, 'update', ctx)) {
|
919
|
+
return;
|
920
|
+
}
|
921
|
+
if (fromValue !== toValue) {
|
922
|
+
to.value = fromValue;
|
923
|
+
}
|
924
|
+
if (to.firstChild && to.firstChild.nodeValue !== fromValue) {
|
925
|
+
to.firstChild.nodeValue = fromValue;
|
926
|
+
}
|
927
|
+
}
|
928
|
+
}
|
929
|
+
|
930
|
+
//=============================================================================
|
931
|
+
// the HEAD tag can be handled specially, either w/ a 'merge' or 'append' style
|
932
|
+
//=============================================================================
|
933
|
+
function handleHeadElement(newHeadTag, currentHead, ctx) {
|
934
|
+
|
935
|
+
let added = [];
|
936
|
+
let removed = [];
|
937
|
+
let preserved = [];
|
938
|
+
let nodesToAppend = [];
|
939
|
+
|
940
|
+
let headMergeStyle = ctx.head.style;
|
941
|
+
|
942
|
+
// put all new head elements into a Map, by their outerHTML
|
943
|
+
let srcToNewHeadNodes = new Map();
|
944
|
+
for (const newHeadChild of newHeadTag.children) {
|
945
|
+
srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild);
|
946
|
+
}
|
947
|
+
|
948
|
+
// for each elt in the current head
|
949
|
+
for (const currentHeadElt of currentHead.children) {
|
950
|
+
|
951
|
+
// If the current head element is in the map
|
952
|
+
let inNewContent = srcToNewHeadNodes.has(currentHeadElt.outerHTML);
|
953
|
+
let isReAppended = ctx.head.shouldReAppend(currentHeadElt);
|
954
|
+
let isPreserved = ctx.head.shouldPreserve(currentHeadElt);
|
955
|
+
if (inNewContent || isPreserved) {
|
956
|
+
if (isReAppended) {
|
957
|
+
// remove the current version and let the new version replace it and re-execute
|
958
|
+
removed.push(currentHeadElt);
|
959
|
+
} else {
|
960
|
+
// this element already exists and should not be re-appended, so remove it from
|
961
|
+
// the new content map, preserving it in the DOM
|
962
|
+
srcToNewHeadNodes.delete(currentHeadElt.outerHTML);
|
963
|
+
preserved.push(currentHeadElt);
|
964
|
+
}
|
965
|
+
} else {
|
966
|
+
if (headMergeStyle === "append") {
|
967
|
+
// we are appending and this existing element is not new content
|
968
|
+
// so if and only if it is marked for re-append do we do anything
|
969
|
+
if (isReAppended) {
|
970
|
+
removed.push(currentHeadElt);
|
971
|
+
nodesToAppend.push(currentHeadElt);
|
972
|
+
}
|
973
|
+
} else {
|
974
|
+
// if this is a merge, we remove this content since it is not in the new head
|
975
|
+
if (ctx.head.shouldRemove(currentHeadElt) !== false) {
|
976
|
+
removed.push(currentHeadElt);
|
977
|
+
}
|
978
|
+
}
|
979
|
+
}
|
980
|
+
}
|
981
|
+
|
982
|
+
// Push the remaining new head elements in the Map into the
|
983
|
+
// nodes to append to the head tag
|
984
|
+
nodesToAppend.push(...srcToNewHeadNodes.values());
|
985
|
+
|
986
|
+
let promises = [];
|
987
|
+
for (const newNode of nodesToAppend) {
|
988
|
+
let newElt = document.createRange().createContextualFragment(newNode.outerHTML).firstChild;
|
989
|
+
if (ctx.callbacks.beforeNodeAdded(newElt) !== false) {
|
990
|
+
if (newElt.href || newElt.src) {
|
991
|
+
let resolve = null;
|
992
|
+
let promise = new Promise(function (_resolve) {
|
993
|
+
resolve = _resolve;
|
994
|
+
});
|
995
|
+
newElt.addEventListener('load', function () {
|
996
|
+
resolve();
|
997
|
+
});
|
998
|
+
promises.push(promise);
|
999
|
+
}
|
1000
|
+
currentHead.appendChild(newElt);
|
1001
|
+
ctx.callbacks.afterNodeAdded(newElt);
|
1002
|
+
added.push(newElt);
|
1003
|
+
}
|
1004
|
+
}
|
1005
|
+
|
1006
|
+
// remove all removed elements, after we have appended the new elements to avoid
|
1007
|
+
// additional network requests for things like style sheets
|
1008
|
+
for (const removedElement of removed) {
|
1009
|
+
if (ctx.callbacks.beforeNodeRemoved(removedElement) !== false) {
|
1010
|
+
currentHead.removeChild(removedElement);
|
1011
|
+
ctx.callbacks.afterNodeRemoved(removedElement);
|
1012
|
+
}
|
1013
|
+
}
|
1014
|
+
|
1015
|
+
ctx.head.afterHeadMorphed(currentHead, {added: added, kept: preserved, removed: removed});
|
1016
|
+
return promises;
|
1017
|
+
}
|
1018
|
+
|
1019
|
+
function noOp() {
|
1020
|
+
}
|
1021
|
+
|
1022
|
+
/*
|
1023
|
+
Deep merges the config object and the Idiomoroph.defaults object to
|
1024
|
+
produce a final configuration object
|
1025
|
+
*/
|
1026
|
+
function mergeDefaults(config) {
|
1027
|
+
let finalConfig = {};
|
1028
|
+
// copy top level stuff into final config
|
1029
|
+
Object.assign(finalConfig, defaults);
|
1030
|
+
Object.assign(finalConfig, config);
|
1031
|
+
|
1032
|
+
// copy callbacks into final config (do this to deep merge the callbacks)
|
1033
|
+
finalConfig.callbacks = {};
|
1034
|
+
Object.assign(finalConfig.callbacks, defaults.callbacks);
|
1035
|
+
Object.assign(finalConfig.callbacks, config.callbacks);
|
1036
|
+
|
1037
|
+
// copy head config into final config (do this to deep merge the head)
|
1038
|
+
finalConfig.head = {};
|
1039
|
+
Object.assign(finalConfig.head, defaults.head);
|
1040
|
+
Object.assign(finalConfig.head, config.head);
|
1041
|
+
return finalConfig;
|
1042
|
+
}
|
1043
|
+
|
1044
|
+
function createMorphContext(oldNode, newContent, config) {
|
1045
|
+
config = mergeDefaults(config);
|
1046
|
+
return {
|
1047
|
+
target: oldNode,
|
1048
|
+
newContent: newContent,
|
1049
|
+
config: config,
|
1050
|
+
morphStyle: config.morphStyle,
|
1051
|
+
ignoreActive: config.ignoreActive,
|
1052
|
+
ignoreActiveValue: config.ignoreActiveValue,
|
1053
|
+
idMap: createIdMap(oldNode, newContent),
|
1054
|
+
deadIds: new Set(),
|
1055
|
+
callbacks: config.callbacks,
|
1056
|
+
head: config.head
|
1057
|
+
}
|
1058
|
+
}
|
1059
|
+
|
1060
|
+
function isIdSetMatch(node1, node2, ctx) {
|
1061
|
+
if (node1 == null || node2 == null) {
|
1062
|
+
return false;
|
1063
|
+
}
|
1064
|
+
if (node1.nodeType === node2.nodeType && node1.tagName === node2.tagName) {
|
1065
|
+
if (node1.id !== "" && node1.id === node2.id) {
|
1066
|
+
return true;
|
1067
|
+
} else {
|
1068
|
+
return getIdIntersectionCount(ctx, node1, node2) > 0;
|
1069
|
+
}
|
1070
|
+
}
|
1071
|
+
return false;
|
1072
|
+
}
|
1073
|
+
|
1074
|
+
function isSoftMatch(node1, node2) {
|
1075
|
+
if (node1 == null || node2 == null) {
|
1076
|
+
return false;
|
1077
|
+
}
|
1078
|
+
return node1.nodeType === node2.nodeType && node1.tagName === node2.tagName
|
1079
|
+
}
|
1080
|
+
|
1081
|
+
function removeNodesBetween(startInclusive, endExclusive, ctx) {
|
1082
|
+
while (startInclusive !== endExclusive) {
|
1083
|
+
let tempNode = startInclusive;
|
1084
|
+
startInclusive = startInclusive.nextSibling;
|
1085
|
+
removeNode(tempNode, ctx);
|
1086
|
+
}
|
1087
|
+
removeIdsFromConsideration(ctx, endExclusive);
|
1088
|
+
return endExclusive.nextSibling;
|
1089
|
+
}
|
1090
|
+
|
1091
|
+
//=============================================================================
|
1092
|
+
// Scans forward from the insertionPoint in the old parent looking for a potential id match
|
1093
|
+
// for the newChild. We stop if we find a potential id match for the new child OR
|
1094
|
+
// if the number of potential id matches we are discarding is greater than the
|
1095
|
+
// potential id matches for the new child
|
1096
|
+
//=============================================================================
|
1097
|
+
function findIdSetMatch(newContent, oldParent, newChild, insertionPoint, ctx) {
|
1098
|
+
|
1099
|
+
// max id matches we are willing to discard in our search
|
1100
|
+
let newChildPotentialIdCount = getIdIntersectionCount(ctx, newChild, oldParent);
|
1101
|
+
|
1102
|
+
let potentialMatch = null;
|
1103
|
+
|
1104
|
+
// only search forward if there is a possibility of an id match
|
1105
|
+
if (newChildPotentialIdCount > 0) {
|
1106
|
+
let potentialMatch = insertionPoint;
|
1107
|
+
// if there is a possibility of an id match, scan forward
|
1108
|
+
// keep track of the potential id match count we are discarding (the
|
1109
|
+
// newChildPotentialIdCount must be greater than this to make it likely
|
1110
|
+
// worth it)
|
1111
|
+
let otherMatchCount = 0;
|
1112
|
+
while (potentialMatch != null) {
|
1113
|
+
|
1114
|
+
// If we have an id match, return the current potential match
|
1115
|
+
if (isIdSetMatch(newChild, potentialMatch, ctx)) {
|
1116
|
+
return potentialMatch;
|
1117
|
+
}
|
1118
|
+
|
1119
|
+
// computer the other potential matches of this new content
|
1120
|
+
otherMatchCount += getIdIntersectionCount(ctx, potentialMatch, newContent);
|
1121
|
+
if (otherMatchCount > newChildPotentialIdCount) {
|
1122
|
+
// if we have more potential id matches in _other_ content, we
|
1123
|
+
// do not have a good candidate for an id match, so return null
|
1124
|
+
return null;
|
1125
|
+
}
|
1126
|
+
|
1127
|
+
// advanced to the next old content child
|
1128
|
+
potentialMatch = potentialMatch.nextSibling;
|
1129
|
+
}
|
1130
|
+
}
|
1131
|
+
return potentialMatch;
|
1132
|
+
}
|
1133
|
+
|
1134
|
+
//=============================================================================
|
1135
|
+
// Scans forward from the insertionPoint in the old parent looking for a potential soft match
|
1136
|
+
// for the newChild. We stop if we find a potential soft match for the new child OR
|
1137
|
+
// if we find a potential id match in the old parents children OR if we find two
|
1138
|
+
// potential soft matches for the next two pieces of new content
|
1139
|
+
//=============================================================================
|
1140
|
+
function findSoftMatch(newContent, oldParent, newChild, insertionPoint, ctx) {
|
1141
|
+
|
1142
|
+
let potentialSoftMatch = insertionPoint;
|
1143
|
+
let nextSibling = newChild.nextSibling;
|
1144
|
+
let siblingSoftMatchCount = 0;
|
1145
|
+
|
1146
|
+
while (potentialSoftMatch != null) {
|
1147
|
+
|
1148
|
+
if (getIdIntersectionCount(ctx, potentialSoftMatch, newContent) > 0) {
|
1149
|
+
// the current potential soft match has a potential id set match with the remaining new
|
1150
|
+
// content so bail out of looking
|
1151
|
+
return null;
|
1152
|
+
}
|
1153
|
+
|
1154
|
+
// if we have a soft match with the current node, return it
|
1155
|
+
if (isSoftMatch(newChild, potentialSoftMatch)) {
|
1156
|
+
return potentialSoftMatch;
|
1157
|
+
}
|
1158
|
+
|
1159
|
+
if (isSoftMatch(nextSibling, potentialSoftMatch)) {
|
1160
|
+
// the next new node has a soft match with this node, so
|
1161
|
+
// increment the count of future soft matches
|
1162
|
+
siblingSoftMatchCount++;
|
1163
|
+
nextSibling = nextSibling.nextSibling;
|
1164
|
+
|
1165
|
+
// If there are two future soft matches, bail to allow the siblings to soft match
|
1166
|
+
// so that we don't consume future soft matches for the sake of the current node
|
1167
|
+
if (siblingSoftMatchCount >= 2) {
|
1168
|
+
return null;
|
1169
|
+
}
|
1170
|
+
}
|
1171
|
+
|
1172
|
+
// advanced to the next old content child
|
1173
|
+
potentialSoftMatch = potentialSoftMatch.nextSibling;
|
1174
|
+
}
|
1175
|
+
|
1176
|
+
return potentialSoftMatch;
|
1177
|
+
}
|
1178
|
+
|
1179
|
+
function parseContent(newContent) {
|
1180
|
+
let parser = new DOMParser();
|
1181
|
+
|
1182
|
+
// remove svgs to avoid false-positive matches on head, etc.
|
1183
|
+
let contentWithSvgsRemoved = newContent.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim, '');
|
1184
|
+
|
1185
|
+
// if the newContent contains a html, head or body tag, we can simply parse it w/o wrapping
|
1186
|
+
if (contentWithSvgsRemoved.match(/<\/html>/) || contentWithSvgsRemoved.match(/<\/head>/) || contentWithSvgsRemoved.match(/<\/body>/)) {
|
1187
|
+
let content = parser.parseFromString(newContent, "text/html");
|
1188
|
+
// if it is a full HTML document, return the document itself as the parent container
|
1189
|
+
if (contentWithSvgsRemoved.match(/<\/html>/)) {
|
1190
|
+
content.generatedByIdiomorph = true;
|
1191
|
+
return content;
|
1192
|
+
} else {
|
1193
|
+
// otherwise return the html element as the parent container
|
1194
|
+
let htmlElement = content.firstChild;
|
1195
|
+
if (htmlElement) {
|
1196
|
+
htmlElement.generatedByIdiomorph = true;
|
1197
|
+
return htmlElement;
|
1198
|
+
} else {
|
1199
|
+
return null;
|
1200
|
+
}
|
1201
|
+
}
|
1202
|
+
} else {
|
1203
|
+
// if it is partial HTML, wrap it in a template tag to provide a parent element and also to help
|
1204
|
+
// deal with touchy tags like tr, tbody, etc.
|
1205
|
+
let responseDoc = parser.parseFromString("<body><template>" + newContent + "</template></body>", "text/html");
|
1206
|
+
let content = responseDoc.body.querySelector('template').content;
|
1207
|
+
content.generatedByIdiomorph = true;
|
1208
|
+
return content
|
1209
|
+
}
|
1210
|
+
}
|
1211
|
+
|
1212
|
+
function normalizeContent(newContent) {
|
1213
|
+
if (newContent == null) {
|
1214
|
+
// noinspection UnnecessaryLocalVariableJS
|
1215
|
+
const dummyParent = document.createElement('div');
|
1216
|
+
return dummyParent;
|
1217
|
+
} else if (newContent.generatedByIdiomorph) {
|
1218
|
+
// the template tag created by idiomorph parsing can serve as a dummy parent
|
1219
|
+
return newContent;
|
1220
|
+
} else if (newContent instanceof Node) {
|
1221
|
+
// a single node is added as a child to a dummy parent
|
1222
|
+
const dummyParent = document.createElement('div');
|
1223
|
+
dummyParent.append(newContent);
|
1224
|
+
return dummyParent;
|
1225
|
+
} else {
|
1226
|
+
// all nodes in the array or HTMLElement collection are consolidated under
|
1227
|
+
// a single dummy parent element
|
1228
|
+
const dummyParent = document.createElement('div');
|
1229
|
+
for (const elt of [...newContent]) {
|
1230
|
+
dummyParent.append(elt);
|
1231
|
+
}
|
1232
|
+
return dummyParent;
|
1233
|
+
}
|
1234
|
+
}
|
1235
|
+
|
1236
|
+
function insertSiblings(previousSibling, morphedNode, nextSibling) {
|
1237
|
+
let stack = [];
|
1238
|
+
let added = [];
|
1239
|
+
while (previousSibling != null) {
|
1240
|
+
stack.push(previousSibling);
|
1241
|
+
previousSibling = previousSibling.previousSibling;
|
1242
|
+
}
|
1243
|
+
while (stack.length > 0) {
|
1244
|
+
let node = stack.pop();
|
1245
|
+
added.push(node); // push added preceding siblings on in order and insert
|
1246
|
+
morphedNode.parentElement.insertBefore(node, morphedNode);
|
1247
|
+
}
|
1248
|
+
added.push(morphedNode);
|
1249
|
+
while (nextSibling != null) {
|
1250
|
+
stack.push(nextSibling);
|
1251
|
+
added.push(nextSibling); // here we are going in order, so push on as we scan, rather than add
|
1252
|
+
nextSibling = nextSibling.nextSibling;
|
1253
|
+
}
|
1254
|
+
while (stack.length > 0) {
|
1255
|
+
morphedNode.parentElement.insertBefore(stack.pop(), morphedNode.nextSibling);
|
1256
|
+
}
|
1257
|
+
return added;
|
1258
|
+
}
|
1259
|
+
|
1260
|
+
function findBestNodeMatch(newContent, oldNode, ctx) {
|
1261
|
+
let currentElement;
|
1262
|
+
currentElement = newContent.firstChild;
|
1263
|
+
let bestElement = currentElement;
|
1264
|
+
let score = 0;
|
1265
|
+
while (currentElement) {
|
1266
|
+
let newScore = scoreElement(currentElement, oldNode, ctx);
|
1267
|
+
if (newScore > score) {
|
1268
|
+
bestElement = currentElement;
|
1269
|
+
score = newScore;
|
1270
|
+
}
|
1271
|
+
currentElement = currentElement.nextSibling;
|
1272
|
+
}
|
1273
|
+
return bestElement;
|
1274
|
+
}
|
1275
|
+
|
1276
|
+
function scoreElement(node1, node2, ctx) {
|
1277
|
+
if (isSoftMatch(node1, node2)) {
|
1278
|
+
return .5 + getIdIntersectionCount(ctx, node1, node2);
|
1279
|
+
}
|
1280
|
+
return 0;
|
1281
|
+
}
|
1282
|
+
|
1283
|
+
function removeNode(tempNode, ctx) {
|
1284
|
+
removeIdsFromConsideration(ctx, tempNode);
|
1285
|
+
if (ctx.callbacks.beforeNodeRemoved(tempNode) === false) return;
|
1286
|
+
|
1287
|
+
tempNode.remove();
|
1288
|
+
ctx.callbacks.afterNodeRemoved(tempNode);
|
1289
|
+
}
|
1290
|
+
|
1291
|
+
//=============================================================================
|
1292
|
+
// ID Set Functions
|
1293
|
+
//=============================================================================
|
1294
|
+
|
1295
|
+
function isIdInConsideration(ctx, id) {
|
1296
|
+
return !ctx.deadIds.has(id);
|
1297
|
+
}
|
1298
|
+
|
1299
|
+
function idIsWithinNode(ctx, id, targetNode) {
|
1300
|
+
let idSet = ctx.idMap.get(targetNode) || EMPTY_SET;
|
1301
|
+
return idSet.has(id);
|
1302
|
+
}
|
1303
|
+
|
1304
|
+
function removeIdsFromConsideration(ctx, node) {
|
1305
|
+
let idSet = ctx.idMap.get(node) || EMPTY_SET;
|
1306
|
+
for (const id of idSet) {
|
1307
|
+
ctx.deadIds.add(id);
|
1308
|
+
}
|
1309
|
+
}
|
1310
|
+
|
1311
|
+
function getIdIntersectionCount(ctx, node1, node2) {
|
1312
|
+
let sourceSet = ctx.idMap.get(node1) || EMPTY_SET;
|
1313
|
+
let matchCount = 0;
|
1314
|
+
for (const id of sourceSet) {
|
1315
|
+
// a potential match is an id in the source and potentialIdsSet, but
|
1316
|
+
// that has not already been merged into the DOM
|
1317
|
+
if (isIdInConsideration(ctx, id) && idIsWithinNode(ctx, id, node2)) {
|
1318
|
+
++matchCount;
|
1319
|
+
}
|
1320
|
+
}
|
1321
|
+
return matchCount;
|
1322
|
+
}
|
1323
|
+
|
1324
|
+
/**
|
1325
|
+
* A bottom up algorithm that finds all elements with ids inside of the node
|
1326
|
+
* argument and populates id sets for those nodes and all their parents, generating
|
1327
|
+
* a set of ids contained within all nodes for the entire hierarchy in the DOM
|
1328
|
+
*
|
1329
|
+
* @param node {Element}
|
1330
|
+
* @param {Map<Node, Set<String>>} idMap
|
1331
|
+
*/
|
1332
|
+
function populateIdMapForNode(node, idMap) {
|
1333
|
+
let nodeParent = node.parentElement;
|
1334
|
+
// find all elements with an id property
|
1335
|
+
let idElements = node.querySelectorAll('[id]');
|
1336
|
+
for (const elt of idElements) {
|
1337
|
+
let current = elt;
|
1338
|
+
// walk up the parent hierarchy of that element, adding the id
|
1339
|
+
// of element to the parent's id set
|
1340
|
+
while (current !== nodeParent && current != null) {
|
1341
|
+
let idSet = idMap.get(current);
|
1342
|
+
// if the id set doesn't exist, create it and insert it in the map
|
1343
|
+
if (idSet == null) {
|
1344
|
+
idSet = new Set();
|
1345
|
+
idMap.set(current, idSet);
|
1346
|
+
}
|
1347
|
+
idSet.add(elt.id);
|
1348
|
+
current = current.parentElement;
|
1349
|
+
}
|
1350
|
+
}
|
1351
|
+
}
|
1352
|
+
|
1353
|
+
/**
|
1354
|
+
* This function computes a map of nodes to all ids contained within that node (inclusive of the
|
1355
|
+
* node). This map can be used to ask if two nodes have intersecting sets of ids, which allows
|
1356
|
+
* for a looser definition of "matching" than tradition id matching, and allows child nodes
|
1357
|
+
* to contribute to a parent nodes matching.
|
1358
|
+
*
|
1359
|
+
* @param {Element} oldContent the old content that will be morphed
|
1360
|
+
* @param {Element} newContent the new content to morph to
|
1361
|
+
* @returns {Map<Node, Set<String>>} a map of nodes to id sets for the
|
1362
|
+
*/
|
1363
|
+
function createIdMap(oldContent, newContent) {
|
1364
|
+
let idMap = new Map();
|
1365
|
+
populateIdMapForNode(oldContent, idMap);
|
1366
|
+
populateIdMapForNode(newContent, idMap);
|
1367
|
+
return idMap;
|
1368
|
+
}
|
1369
|
+
|
1370
|
+
//=============================================================================
|
1371
|
+
// This is what ends up becoming the Idiomorph global object
|
1372
|
+
//=============================================================================
|
1373
|
+
return {
|
1374
|
+
morph,
|
1375
|
+
defaults
|
1376
|
+
}
|
1377
|
+
})();
|
1378
|
+
|
1379
|
+
function log() {
|
1380
|
+
if (HotwireSpark.config.loggingEnabled) {
|
1381
|
+
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
|
1382
|
+
args[_key] = arguments[_key];
|
1383
|
+
}
|
1384
|
+
console.log(`[hotwire_spark]`, ...args);
|
1385
|
+
}
|
1386
|
+
}
|
1387
|
+
|
1388
|
+
/*
|
1389
|
+
Stimulus 3.2.1
|
1390
|
+
Copyright © 2023 Basecamp, LLC
|
1391
|
+
*/
|
1392
|
+
class EventListener {
|
1393
|
+
constructor(eventTarget, eventName, eventOptions) {
|
1394
|
+
this.eventTarget = eventTarget;
|
1395
|
+
this.eventName = eventName;
|
1396
|
+
this.eventOptions = eventOptions;
|
1397
|
+
this.unorderedBindings = new Set();
|
1398
|
+
}
|
1399
|
+
connect() {
|
1400
|
+
this.eventTarget.addEventListener(this.eventName, this, this.eventOptions);
|
1401
|
+
}
|
1402
|
+
disconnect() {
|
1403
|
+
this.eventTarget.removeEventListener(this.eventName, this, this.eventOptions);
|
1404
|
+
}
|
1405
|
+
bindingConnected(binding) {
|
1406
|
+
this.unorderedBindings.add(binding);
|
1407
|
+
}
|
1408
|
+
bindingDisconnected(binding) {
|
1409
|
+
this.unorderedBindings.delete(binding);
|
1410
|
+
}
|
1411
|
+
handleEvent(event) {
|
1412
|
+
const extendedEvent = extendEvent(event);
|
1413
|
+
for (const binding of this.bindings) {
|
1414
|
+
if (extendedEvent.immediatePropagationStopped) {
|
1415
|
+
break;
|
1416
|
+
}
|
1417
|
+
else {
|
1418
|
+
binding.handleEvent(extendedEvent);
|
1419
|
+
}
|
1420
|
+
}
|
1421
|
+
}
|
1422
|
+
hasBindings() {
|
1423
|
+
return this.unorderedBindings.size > 0;
|
1424
|
+
}
|
1425
|
+
get bindings() {
|
1426
|
+
return Array.from(this.unorderedBindings).sort((left, right) => {
|
1427
|
+
const leftIndex = left.index, rightIndex = right.index;
|
1428
|
+
return leftIndex < rightIndex ? -1 : leftIndex > rightIndex ? 1 : 0;
|
1429
|
+
});
|
1430
|
+
}
|
1431
|
+
}
|
1432
|
+
function extendEvent(event) {
|
1433
|
+
if ("immediatePropagationStopped" in event) {
|
1434
|
+
return event;
|
1435
|
+
}
|
1436
|
+
else {
|
1437
|
+
const { stopImmediatePropagation } = event;
|
1438
|
+
return Object.assign(event, {
|
1439
|
+
immediatePropagationStopped: false,
|
1440
|
+
stopImmediatePropagation() {
|
1441
|
+
this.immediatePropagationStopped = true;
|
1442
|
+
stopImmediatePropagation.call(this);
|
1443
|
+
},
|
1444
|
+
});
|
1445
|
+
}
|
1446
|
+
}
|
1447
|
+
|
1448
|
+
class Dispatcher {
|
1449
|
+
constructor(application) {
|
1450
|
+
this.application = application;
|
1451
|
+
this.eventListenerMaps = new Map();
|
1452
|
+
this.started = false;
|
1453
|
+
}
|
1454
|
+
start() {
|
1455
|
+
if (!this.started) {
|
1456
|
+
this.started = true;
|
1457
|
+
this.eventListeners.forEach((eventListener) => eventListener.connect());
|
1458
|
+
}
|
1459
|
+
}
|
1460
|
+
stop() {
|
1461
|
+
if (this.started) {
|
1462
|
+
this.started = false;
|
1463
|
+
this.eventListeners.forEach((eventListener) => eventListener.disconnect());
|
1464
|
+
}
|
1465
|
+
}
|
1466
|
+
get eventListeners() {
|
1467
|
+
return Array.from(this.eventListenerMaps.values()).reduce((listeners, map) => listeners.concat(Array.from(map.values())), []);
|
1468
|
+
}
|
1469
|
+
bindingConnected(binding) {
|
1470
|
+
this.fetchEventListenerForBinding(binding).bindingConnected(binding);
|
1471
|
+
}
|
1472
|
+
bindingDisconnected(binding, clearEventListeners = false) {
|
1473
|
+
this.fetchEventListenerForBinding(binding).bindingDisconnected(binding);
|
1474
|
+
if (clearEventListeners)
|
1475
|
+
this.clearEventListenersForBinding(binding);
|
1476
|
+
}
|
1477
|
+
handleError(error, message, detail = {}) {
|
1478
|
+
this.application.handleError(error, `Error ${message}`, detail);
|
1479
|
+
}
|
1480
|
+
clearEventListenersForBinding(binding) {
|
1481
|
+
const eventListener = this.fetchEventListenerForBinding(binding);
|
1482
|
+
if (!eventListener.hasBindings()) {
|
1483
|
+
eventListener.disconnect();
|
1484
|
+
this.removeMappedEventListenerFor(binding);
|
1485
|
+
}
|
1486
|
+
}
|
1487
|
+
removeMappedEventListenerFor(binding) {
|
1488
|
+
const { eventTarget, eventName, eventOptions } = binding;
|
1489
|
+
const eventListenerMap = this.fetchEventListenerMapForEventTarget(eventTarget);
|
1490
|
+
const cacheKey = this.cacheKey(eventName, eventOptions);
|
1491
|
+
eventListenerMap.delete(cacheKey);
|
1492
|
+
if (eventListenerMap.size == 0)
|
1493
|
+
this.eventListenerMaps.delete(eventTarget);
|
1494
|
+
}
|
1495
|
+
fetchEventListenerForBinding(binding) {
|
1496
|
+
const { eventTarget, eventName, eventOptions } = binding;
|
1497
|
+
return this.fetchEventListener(eventTarget, eventName, eventOptions);
|
1498
|
+
}
|
1499
|
+
fetchEventListener(eventTarget, eventName, eventOptions) {
|
1500
|
+
const eventListenerMap = this.fetchEventListenerMapForEventTarget(eventTarget);
|
1501
|
+
const cacheKey = this.cacheKey(eventName, eventOptions);
|
1502
|
+
let eventListener = eventListenerMap.get(cacheKey);
|
1503
|
+
if (!eventListener) {
|
1504
|
+
eventListener = this.createEventListener(eventTarget, eventName, eventOptions);
|
1505
|
+
eventListenerMap.set(cacheKey, eventListener);
|
1506
|
+
}
|
1507
|
+
return eventListener;
|
1508
|
+
}
|
1509
|
+
createEventListener(eventTarget, eventName, eventOptions) {
|
1510
|
+
const eventListener = new EventListener(eventTarget, eventName, eventOptions);
|
1511
|
+
if (this.started) {
|
1512
|
+
eventListener.connect();
|
1513
|
+
}
|
1514
|
+
return eventListener;
|
1515
|
+
}
|
1516
|
+
fetchEventListenerMapForEventTarget(eventTarget) {
|
1517
|
+
let eventListenerMap = this.eventListenerMaps.get(eventTarget);
|
1518
|
+
if (!eventListenerMap) {
|
1519
|
+
eventListenerMap = new Map();
|
1520
|
+
this.eventListenerMaps.set(eventTarget, eventListenerMap);
|
1521
|
+
}
|
1522
|
+
return eventListenerMap;
|
1523
|
+
}
|
1524
|
+
cacheKey(eventName, eventOptions) {
|
1525
|
+
const parts = [eventName];
|
1526
|
+
Object.keys(eventOptions)
|
1527
|
+
.sort()
|
1528
|
+
.forEach((key) => {
|
1529
|
+
parts.push(`${eventOptions[key] ? "" : "!"}${key}`);
|
1530
|
+
});
|
1531
|
+
return parts.join(":");
|
1532
|
+
}
|
1533
|
+
}
|
1534
|
+
|
1535
|
+
const defaultActionDescriptorFilters = {
|
1536
|
+
stop({ event, value }) {
|
1537
|
+
if (value)
|
1538
|
+
event.stopPropagation();
|
1539
|
+
return true;
|
1540
|
+
},
|
1541
|
+
prevent({ event, value }) {
|
1542
|
+
if (value)
|
1543
|
+
event.preventDefault();
|
1544
|
+
return true;
|
1545
|
+
},
|
1546
|
+
self({ event, value, element }) {
|
1547
|
+
if (value) {
|
1548
|
+
return element === event.target;
|
1549
|
+
}
|
1550
|
+
else {
|
1551
|
+
return true;
|
1552
|
+
}
|
1553
|
+
},
|
1554
|
+
};
|
1555
|
+
const descriptorPattern = /^(?:(?:([^.]+?)\+)?(.+?)(?:\.(.+?))?(?:@(window|document))?->)?(.+?)(?:#([^:]+?))(?::(.+))?$/;
|
1556
|
+
function parseActionDescriptorString(descriptorString) {
|
1557
|
+
const source = descriptorString.trim();
|
1558
|
+
const matches = source.match(descriptorPattern) || [];
|
1559
|
+
let eventName = matches[2];
|
1560
|
+
let keyFilter = matches[3];
|
1561
|
+
if (keyFilter && !["keydown", "keyup", "keypress"].includes(eventName)) {
|
1562
|
+
eventName += `.${keyFilter}`;
|
1563
|
+
keyFilter = "";
|
1564
|
+
}
|
1565
|
+
return {
|
1566
|
+
eventTarget: parseEventTarget(matches[4]),
|
1567
|
+
eventName,
|
1568
|
+
eventOptions: matches[7] ? parseEventOptions(matches[7]) : {},
|
1569
|
+
identifier: matches[5],
|
1570
|
+
methodName: matches[6],
|
1571
|
+
keyFilter: matches[1] || keyFilter,
|
1572
|
+
};
|
1573
|
+
}
|
1574
|
+
function parseEventTarget(eventTargetName) {
|
1575
|
+
if (eventTargetName == "window") {
|
1576
|
+
return window;
|
1577
|
+
}
|
1578
|
+
else if (eventTargetName == "document") {
|
1579
|
+
return document;
|
1580
|
+
}
|
1581
|
+
}
|
1582
|
+
function parseEventOptions(eventOptions) {
|
1583
|
+
return eventOptions
|
1584
|
+
.split(":")
|
1585
|
+
.reduce((options, token) => Object.assign(options, { [token.replace(/^!/, "")]: !/^!/.test(token) }), {});
|
1586
|
+
}
|
1587
|
+
function stringifyEventTarget(eventTarget) {
|
1588
|
+
if (eventTarget == window) {
|
1589
|
+
return "window";
|
1590
|
+
}
|
1591
|
+
else if (eventTarget == document) {
|
1592
|
+
return "document";
|
1593
|
+
}
|
1594
|
+
}
|
1595
|
+
|
1596
|
+
function camelize(value) {
|
1597
|
+
return value.replace(/(?:[_-])([a-z0-9])/g, (_, char) => char.toUpperCase());
|
1598
|
+
}
|
1599
|
+
function namespaceCamelize(value) {
|
1600
|
+
return camelize(value.replace(/--/g, "-").replace(/__/g, "_"));
|
1601
|
+
}
|
1602
|
+
function capitalize(value) {
|
1603
|
+
return value.charAt(0).toUpperCase() + value.slice(1);
|
1604
|
+
}
|
1605
|
+
function dasherize(value) {
|
1606
|
+
return value.replace(/([A-Z])/g, (_, char) => `-${char.toLowerCase()}`);
|
1607
|
+
}
|
1608
|
+
function tokenize(value) {
|
1609
|
+
return value.match(/[^\s]+/g) || [];
|
1610
|
+
}
|
1611
|
+
function hasProperty(object, property) {
|
1612
|
+
return Object.prototype.hasOwnProperty.call(object, property);
|
1613
|
+
}
|
1614
|
+
|
1615
|
+
const allModifiers = ["meta", "ctrl", "alt", "shift"];
|
1616
|
+
class Action {
|
1617
|
+
constructor(element, index, descriptor, schema) {
|
1618
|
+
this.element = element;
|
1619
|
+
this.index = index;
|
1620
|
+
this.eventTarget = descriptor.eventTarget || element;
|
1621
|
+
this.eventName = descriptor.eventName || getDefaultEventNameForElement(element) || error("missing event name");
|
1622
|
+
this.eventOptions = descriptor.eventOptions || {};
|
1623
|
+
this.identifier = descriptor.identifier || error("missing identifier");
|
1624
|
+
this.methodName = descriptor.methodName || error("missing method name");
|
1625
|
+
this.keyFilter = descriptor.keyFilter || "";
|
1626
|
+
this.schema = schema;
|
1627
|
+
}
|
1628
|
+
static forToken(token, schema) {
|
1629
|
+
return new this(token.element, token.index, parseActionDescriptorString(token.content), schema);
|
1630
|
+
}
|
1631
|
+
toString() {
|
1632
|
+
const eventFilter = this.keyFilter ? `.${this.keyFilter}` : "";
|
1633
|
+
const eventTarget = this.eventTargetName ? `@${this.eventTargetName}` : "";
|
1634
|
+
return `${this.eventName}${eventFilter}${eventTarget}->${this.identifier}#${this.methodName}`;
|
1635
|
+
}
|
1636
|
+
shouldIgnoreKeyboardEvent(event) {
|
1637
|
+
if (!this.keyFilter) {
|
1638
|
+
return false;
|
1639
|
+
}
|
1640
|
+
const filters = this.keyFilter.split("+");
|
1641
|
+
if (this.keyFilterDissatisfied(event, filters)) {
|
1642
|
+
return true;
|
1643
|
+
}
|
1644
|
+
const standardFilter = filters.filter((key) => !allModifiers.includes(key))[0];
|
1645
|
+
if (!standardFilter) {
|
1646
|
+
return false;
|
1647
|
+
}
|
1648
|
+
if (!hasProperty(this.keyMappings, standardFilter)) {
|
1649
|
+
error(`contains unknown key filter: ${this.keyFilter}`);
|
1650
|
+
}
|
1651
|
+
return this.keyMappings[standardFilter].toLowerCase() !== event.key.toLowerCase();
|
1652
|
+
}
|
1653
|
+
shouldIgnoreMouseEvent(event) {
|
1654
|
+
if (!this.keyFilter) {
|
1655
|
+
return false;
|
1656
|
+
}
|
1657
|
+
const filters = [this.keyFilter];
|
1658
|
+
if (this.keyFilterDissatisfied(event, filters)) {
|
1659
|
+
return true;
|
1660
|
+
}
|
1661
|
+
return false;
|
1662
|
+
}
|
1663
|
+
get params() {
|
1664
|
+
const params = {};
|
1665
|
+
const pattern = new RegExp(`^data-${this.identifier}-(.+)-param$`, "i");
|
1666
|
+
for (const { name, value } of Array.from(this.element.attributes)) {
|
1667
|
+
const match = name.match(pattern);
|
1668
|
+
const key = match && match[1];
|
1669
|
+
if (key) {
|
1670
|
+
params[camelize(key)] = typecast(value);
|
1671
|
+
}
|
1672
|
+
}
|
1673
|
+
return params;
|
1674
|
+
}
|
1675
|
+
get eventTargetName() {
|
1676
|
+
return stringifyEventTarget(this.eventTarget);
|
1677
|
+
}
|
1678
|
+
get keyMappings() {
|
1679
|
+
return this.schema.keyMappings;
|
1680
|
+
}
|
1681
|
+
keyFilterDissatisfied(event, filters) {
|
1682
|
+
const [meta, ctrl, alt, shift] = allModifiers.map((modifier) => filters.includes(modifier));
|
1683
|
+
return event.metaKey !== meta || event.ctrlKey !== ctrl || event.altKey !== alt || event.shiftKey !== shift;
|
1684
|
+
}
|
1685
|
+
}
|
1686
|
+
const defaultEventNames = {
|
1687
|
+
a: () => "click",
|
1688
|
+
button: () => "click",
|
1689
|
+
form: () => "submit",
|
1690
|
+
details: () => "toggle",
|
1691
|
+
input: (e) => (e.getAttribute("type") == "submit" ? "click" : "input"),
|
1692
|
+
select: () => "change",
|
1693
|
+
textarea: () => "input",
|
1694
|
+
};
|
1695
|
+
function getDefaultEventNameForElement(element) {
|
1696
|
+
const tagName = element.tagName.toLowerCase();
|
1697
|
+
if (tagName in defaultEventNames) {
|
1698
|
+
return defaultEventNames[tagName](element);
|
1699
|
+
}
|
1700
|
+
}
|
1701
|
+
function error(message) {
|
1702
|
+
throw new Error(message);
|
1703
|
+
}
|
1704
|
+
function typecast(value) {
|
1705
|
+
try {
|
1706
|
+
return JSON.parse(value);
|
1707
|
+
}
|
1708
|
+
catch (o_O) {
|
1709
|
+
return value;
|
1710
|
+
}
|
1711
|
+
}
|
1712
|
+
|
1713
|
+
class Binding {
|
1714
|
+
constructor(context, action) {
|
1715
|
+
this.context = context;
|
1716
|
+
this.action = action;
|
1717
|
+
}
|
1718
|
+
get index() {
|
1719
|
+
return this.action.index;
|
1720
|
+
}
|
1721
|
+
get eventTarget() {
|
1722
|
+
return this.action.eventTarget;
|
1723
|
+
}
|
1724
|
+
get eventOptions() {
|
1725
|
+
return this.action.eventOptions;
|
1726
|
+
}
|
1727
|
+
get identifier() {
|
1728
|
+
return this.context.identifier;
|
1729
|
+
}
|
1730
|
+
handleEvent(event) {
|
1731
|
+
const actionEvent = this.prepareActionEvent(event);
|
1732
|
+
if (this.willBeInvokedByEvent(event) && this.applyEventModifiers(actionEvent)) {
|
1733
|
+
this.invokeWithEvent(actionEvent);
|
1734
|
+
}
|
1735
|
+
}
|
1736
|
+
get eventName() {
|
1737
|
+
return this.action.eventName;
|
1738
|
+
}
|
1739
|
+
get method() {
|
1740
|
+
const method = this.controller[this.methodName];
|
1741
|
+
if (typeof method == "function") {
|
1742
|
+
return method;
|
1743
|
+
}
|
1744
|
+
throw new Error(`Action "${this.action}" references undefined method "${this.methodName}"`);
|
1745
|
+
}
|
1746
|
+
applyEventModifiers(event) {
|
1747
|
+
const { element } = this.action;
|
1748
|
+
const { actionDescriptorFilters } = this.context.application;
|
1749
|
+
const { controller } = this.context;
|
1750
|
+
let passes = true;
|
1751
|
+
for (const [name, value] of Object.entries(this.eventOptions)) {
|
1752
|
+
if (name in actionDescriptorFilters) {
|
1753
|
+
const filter = actionDescriptorFilters[name];
|
1754
|
+
passes = passes && filter({ name, value, event, element, controller });
|
1755
|
+
}
|
1756
|
+
else {
|
1757
|
+
continue;
|
1758
|
+
}
|
1759
|
+
}
|
1760
|
+
return passes;
|
1761
|
+
}
|
1762
|
+
prepareActionEvent(event) {
|
1763
|
+
return Object.assign(event, { params: this.action.params });
|
1764
|
+
}
|
1765
|
+
invokeWithEvent(event) {
|
1766
|
+
const { target, currentTarget } = event;
|
1767
|
+
try {
|
1768
|
+
this.method.call(this.controller, event);
|
1769
|
+
this.context.logDebugActivity(this.methodName, { event, target, currentTarget, action: this.methodName });
|
1770
|
+
}
|
1771
|
+
catch (error) {
|
1772
|
+
const { identifier, controller, element, index } = this;
|
1773
|
+
const detail = { identifier, controller, element, index, event };
|
1774
|
+
this.context.handleError(error, `invoking action "${this.action}"`, detail);
|
1775
|
+
}
|
1776
|
+
}
|
1777
|
+
willBeInvokedByEvent(event) {
|
1778
|
+
const eventTarget = event.target;
|
1779
|
+
if (event instanceof KeyboardEvent && this.action.shouldIgnoreKeyboardEvent(event)) {
|
1780
|
+
return false;
|
1781
|
+
}
|
1782
|
+
if (event instanceof MouseEvent && this.action.shouldIgnoreMouseEvent(event)) {
|
1783
|
+
return false;
|
1784
|
+
}
|
1785
|
+
if (this.element === eventTarget) {
|
1786
|
+
return true;
|
1787
|
+
}
|
1788
|
+
else if (eventTarget instanceof Element && this.element.contains(eventTarget)) {
|
1789
|
+
return this.scope.containsElement(eventTarget);
|
1790
|
+
}
|
1791
|
+
else {
|
1792
|
+
return this.scope.containsElement(this.action.element);
|
1793
|
+
}
|
1794
|
+
}
|
1795
|
+
get controller() {
|
1796
|
+
return this.context.controller;
|
1797
|
+
}
|
1798
|
+
get methodName() {
|
1799
|
+
return this.action.methodName;
|
1800
|
+
}
|
1801
|
+
get element() {
|
1802
|
+
return this.scope.element;
|
1803
|
+
}
|
1804
|
+
get scope() {
|
1805
|
+
return this.context.scope;
|
1806
|
+
}
|
1807
|
+
}
|
1808
|
+
|
1809
|
+
class ElementObserver {
|
1810
|
+
constructor(element, delegate) {
|
1811
|
+
this.mutationObserverInit = { attributes: true, childList: true, subtree: true };
|
1812
|
+
this.element = element;
|
1813
|
+
this.started = false;
|
1814
|
+
this.delegate = delegate;
|
1815
|
+
this.elements = new Set();
|
1816
|
+
this.mutationObserver = new MutationObserver((mutations) => this.processMutations(mutations));
|
1817
|
+
}
|
1818
|
+
start() {
|
1819
|
+
if (!this.started) {
|
1820
|
+
this.started = true;
|
1821
|
+
this.mutationObserver.observe(this.element, this.mutationObserverInit);
|
1822
|
+
this.refresh();
|
1823
|
+
}
|
1824
|
+
}
|
1825
|
+
pause(callback) {
|
1826
|
+
if (this.started) {
|
1827
|
+
this.mutationObserver.disconnect();
|
1828
|
+
this.started = false;
|
1829
|
+
}
|
1830
|
+
callback();
|
1831
|
+
if (!this.started) {
|
1832
|
+
this.mutationObserver.observe(this.element, this.mutationObserverInit);
|
1833
|
+
this.started = true;
|
1834
|
+
}
|
1835
|
+
}
|
1836
|
+
stop() {
|
1837
|
+
if (this.started) {
|
1838
|
+
this.mutationObserver.takeRecords();
|
1839
|
+
this.mutationObserver.disconnect();
|
1840
|
+
this.started = false;
|
1841
|
+
}
|
1842
|
+
}
|
1843
|
+
refresh() {
|
1844
|
+
if (this.started) {
|
1845
|
+
const matches = new Set(this.matchElementsInTree());
|
1846
|
+
for (const element of Array.from(this.elements)) {
|
1847
|
+
if (!matches.has(element)) {
|
1848
|
+
this.removeElement(element);
|
1849
|
+
}
|
1850
|
+
}
|
1851
|
+
for (const element of Array.from(matches)) {
|
1852
|
+
this.addElement(element);
|
1853
|
+
}
|
1854
|
+
}
|
1855
|
+
}
|
1856
|
+
processMutations(mutations) {
|
1857
|
+
if (this.started) {
|
1858
|
+
for (const mutation of mutations) {
|
1859
|
+
this.processMutation(mutation);
|
1860
|
+
}
|
1861
|
+
}
|
1862
|
+
}
|
1863
|
+
processMutation(mutation) {
|
1864
|
+
if (mutation.type == "attributes") {
|
1865
|
+
this.processAttributeChange(mutation.target, mutation.attributeName);
|
1866
|
+
}
|
1867
|
+
else if (mutation.type == "childList") {
|
1868
|
+
this.processRemovedNodes(mutation.removedNodes);
|
1869
|
+
this.processAddedNodes(mutation.addedNodes);
|
1870
|
+
}
|
1871
|
+
}
|
1872
|
+
processAttributeChange(element, attributeName) {
|
1873
|
+
if (this.elements.has(element)) {
|
1874
|
+
if (this.delegate.elementAttributeChanged && this.matchElement(element)) {
|
1875
|
+
this.delegate.elementAttributeChanged(element, attributeName);
|
1876
|
+
}
|
1877
|
+
else {
|
1878
|
+
this.removeElement(element);
|
1879
|
+
}
|
1880
|
+
}
|
1881
|
+
else if (this.matchElement(element)) {
|
1882
|
+
this.addElement(element);
|
1883
|
+
}
|
1884
|
+
}
|
1885
|
+
processRemovedNodes(nodes) {
|
1886
|
+
for (const node of Array.from(nodes)) {
|
1887
|
+
const element = this.elementFromNode(node);
|
1888
|
+
if (element) {
|
1889
|
+
this.processTree(element, this.removeElement);
|
1890
|
+
}
|
1891
|
+
}
|
1892
|
+
}
|
1893
|
+
processAddedNodes(nodes) {
|
1894
|
+
for (const node of Array.from(nodes)) {
|
1895
|
+
const element = this.elementFromNode(node);
|
1896
|
+
if (element && this.elementIsActive(element)) {
|
1897
|
+
this.processTree(element, this.addElement);
|
1898
|
+
}
|
1899
|
+
}
|
1900
|
+
}
|
1901
|
+
matchElement(element) {
|
1902
|
+
return this.delegate.matchElement(element);
|
1903
|
+
}
|
1904
|
+
matchElementsInTree(tree = this.element) {
|
1905
|
+
return this.delegate.matchElementsInTree(tree);
|
1906
|
+
}
|
1907
|
+
processTree(tree, processor) {
|
1908
|
+
for (const element of this.matchElementsInTree(tree)) {
|
1909
|
+
processor.call(this, element);
|
1910
|
+
}
|
1911
|
+
}
|
1912
|
+
elementFromNode(node) {
|
1913
|
+
if (node.nodeType == Node.ELEMENT_NODE) {
|
1914
|
+
return node;
|
1915
|
+
}
|
1916
|
+
}
|
1917
|
+
elementIsActive(element) {
|
1918
|
+
if (element.isConnected != this.element.isConnected) {
|
1919
|
+
return false;
|
1920
|
+
}
|
1921
|
+
else {
|
1922
|
+
return this.element.contains(element);
|
1923
|
+
}
|
1924
|
+
}
|
1925
|
+
addElement(element) {
|
1926
|
+
if (!this.elements.has(element)) {
|
1927
|
+
if (this.elementIsActive(element)) {
|
1928
|
+
this.elements.add(element);
|
1929
|
+
if (this.delegate.elementMatched) {
|
1930
|
+
this.delegate.elementMatched(element);
|
1931
|
+
}
|
1932
|
+
}
|
1933
|
+
}
|
1934
|
+
}
|
1935
|
+
removeElement(element) {
|
1936
|
+
if (this.elements.has(element)) {
|
1937
|
+
this.elements.delete(element);
|
1938
|
+
if (this.delegate.elementUnmatched) {
|
1939
|
+
this.delegate.elementUnmatched(element);
|
1940
|
+
}
|
1941
|
+
}
|
1942
|
+
}
|
1943
|
+
}
|
1944
|
+
|
1945
|
+
class AttributeObserver {
|
1946
|
+
constructor(element, attributeName, delegate) {
|
1947
|
+
this.attributeName = attributeName;
|
1948
|
+
this.delegate = delegate;
|
1949
|
+
this.elementObserver = new ElementObserver(element, this);
|
1950
|
+
}
|
1951
|
+
get element() {
|
1952
|
+
return this.elementObserver.element;
|
1953
|
+
}
|
1954
|
+
get selector() {
|
1955
|
+
return `[${this.attributeName}]`;
|
1956
|
+
}
|
1957
|
+
start() {
|
1958
|
+
this.elementObserver.start();
|
1959
|
+
}
|
1960
|
+
pause(callback) {
|
1961
|
+
this.elementObserver.pause(callback);
|
1962
|
+
}
|
1963
|
+
stop() {
|
1964
|
+
this.elementObserver.stop();
|
1965
|
+
}
|
1966
|
+
refresh() {
|
1967
|
+
this.elementObserver.refresh();
|
1968
|
+
}
|
1969
|
+
get started() {
|
1970
|
+
return this.elementObserver.started;
|
1971
|
+
}
|
1972
|
+
matchElement(element) {
|
1973
|
+
return element.hasAttribute(this.attributeName);
|
1974
|
+
}
|
1975
|
+
matchElementsInTree(tree) {
|
1976
|
+
const match = this.matchElement(tree) ? [tree] : [];
|
1977
|
+
const matches = Array.from(tree.querySelectorAll(this.selector));
|
1978
|
+
return match.concat(matches);
|
1979
|
+
}
|
1980
|
+
elementMatched(element) {
|
1981
|
+
if (this.delegate.elementMatchedAttribute) {
|
1982
|
+
this.delegate.elementMatchedAttribute(element, this.attributeName);
|
1983
|
+
}
|
1984
|
+
}
|
1985
|
+
elementUnmatched(element) {
|
1986
|
+
if (this.delegate.elementUnmatchedAttribute) {
|
1987
|
+
this.delegate.elementUnmatchedAttribute(element, this.attributeName);
|
1988
|
+
}
|
1989
|
+
}
|
1990
|
+
elementAttributeChanged(element, attributeName) {
|
1991
|
+
if (this.delegate.elementAttributeValueChanged && this.attributeName == attributeName) {
|
1992
|
+
this.delegate.elementAttributeValueChanged(element, attributeName);
|
1993
|
+
}
|
1994
|
+
}
|
1995
|
+
}
|
1996
|
+
|
1997
|
+
function add(map, key, value) {
|
1998
|
+
fetch$1(map, key).add(value);
|
1999
|
+
}
|
2000
|
+
function del(map, key, value) {
|
2001
|
+
fetch$1(map, key).delete(value);
|
2002
|
+
prune(map, key);
|
2003
|
+
}
|
2004
|
+
function fetch$1(map, key) {
|
2005
|
+
let values = map.get(key);
|
2006
|
+
if (!values) {
|
2007
|
+
values = new Set();
|
2008
|
+
map.set(key, values);
|
2009
|
+
}
|
2010
|
+
return values;
|
2011
|
+
}
|
2012
|
+
function prune(map, key) {
|
2013
|
+
const values = map.get(key);
|
2014
|
+
if (values != null && values.size == 0) {
|
2015
|
+
map.delete(key);
|
2016
|
+
}
|
2017
|
+
}
|
2018
|
+
|
2019
|
+
class Multimap {
|
2020
|
+
constructor() {
|
2021
|
+
this.valuesByKey = new Map();
|
2022
|
+
}
|
2023
|
+
get keys() {
|
2024
|
+
return Array.from(this.valuesByKey.keys());
|
2025
|
+
}
|
2026
|
+
get values() {
|
2027
|
+
const sets = Array.from(this.valuesByKey.values());
|
2028
|
+
return sets.reduce((values, set) => values.concat(Array.from(set)), []);
|
2029
|
+
}
|
2030
|
+
get size() {
|
2031
|
+
const sets = Array.from(this.valuesByKey.values());
|
2032
|
+
return sets.reduce((size, set) => size + set.size, 0);
|
2033
|
+
}
|
2034
|
+
add(key, value) {
|
2035
|
+
add(this.valuesByKey, key, value);
|
2036
|
+
}
|
2037
|
+
delete(key, value) {
|
2038
|
+
del(this.valuesByKey, key, value);
|
2039
|
+
}
|
2040
|
+
has(key, value) {
|
2041
|
+
const values = this.valuesByKey.get(key);
|
2042
|
+
return values != null && values.has(value);
|
2043
|
+
}
|
2044
|
+
hasKey(key) {
|
2045
|
+
return this.valuesByKey.has(key);
|
2046
|
+
}
|
2047
|
+
hasValue(value) {
|
2048
|
+
const sets = Array.from(this.valuesByKey.values());
|
2049
|
+
return sets.some((set) => set.has(value));
|
2050
|
+
}
|
2051
|
+
getValuesForKey(key) {
|
2052
|
+
const values = this.valuesByKey.get(key);
|
2053
|
+
return values ? Array.from(values) : [];
|
2054
|
+
}
|
2055
|
+
getKeysForValue(value) {
|
2056
|
+
return Array.from(this.valuesByKey)
|
2057
|
+
.filter(([_key, values]) => values.has(value))
|
2058
|
+
.map(([key, _values]) => key);
|
2059
|
+
}
|
2060
|
+
}
|
2061
|
+
|
2062
|
+
class SelectorObserver {
|
2063
|
+
constructor(element, selector, delegate, details) {
|
2064
|
+
this._selector = selector;
|
2065
|
+
this.details = details;
|
2066
|
+
this.elementObserver = new ElementObserver(element, this);
|
2067
|
+
this.delegate = delegate;
|
2068
|
+
this.matchesByElement = new Multimap();
|
2069
|
+
}
|
2070
|
+
get started() {
|
2071
|
+
return this.elementObserver.started;
|
2072
|
+
}
|
2073
|
+
get selector() {
|
2074
|
+
return this._selector;
|
2075
|
+
}
|
2076
|
+
set selector(selector) {
|
2077
|
+
this._selector = selector;
|
2078
|
+
this.refresh();
|
2079
|
+
}
|
2080
|
+
start() {
|
2081
|
+
this.elementObserver.start();
|
2082
|
+
}
|
2083
|
+
pause(callback) {
|
2084
|
+
this.elementObserver.pause(callback);
|
2085
|
+
}
|
2086
|
+
stop() {
|
2087
|
+
this.elementObserver.stop();
|
2088
|
+
}
|
2089
|
+
refresh() {
|
2090
|
+
this.elementObserver.refresh();
|
2091
|
+
}
|
2092
|
+
get element() {
|
2093
|
+
return this.elementObserver.element;
|
2094
|
+
}
|
2095
|
+
matchElement(element) {
|
2096
|
+
const { selector } = this;
|
2097
|
+
if (selector) {
|
2098
|
+
const matches = element.matches(selector);
|
2099
|
+
if (this.delegate.selectorMatchElement) {
|
2100
|
+
return matches && this.delegate.selectorMatchElement(element, this.details);
|
2101
|
+
}
|
2102
|
+
return matches;
|
2103
|
+
}
|
2104
|
+
else {
|
2105
|
+
return false;
|
2106
|
+
}
|
2107
|
+
}
|
2108
|
+
matchElementsInTree(tree) {
|
2109
|
+
const { selector } = this;
|
2110
|
+
if (selector) {
|
2111
|
+
const match = this.matchElement(tree) ? [tree] : [];
|
2112
|
+
const matches = Array.from(tree.querySelectorAll(selector)).filter((match) => this.matchElement(match));
|
2113
|
+
return match.concat(matches);
|
2114
|
+
}
|
2115
|
+
else {
|
2116
|
+
return [];
|
2117
|
+
}
|
2118
|
+
}
|
2119
|
+
elementMatched(element) {
|
2120
|
+
const { selector } = this;
|
2121
|
+
if (selector) {
|
2122
|
+
this.selectorMatched(element, selector);
|
2123
|
+
}
|
2124
|
+
}
|
2125
|
+
elementUnmatched(element) {
|
2126
|
+
const selectors = this.matchesByElement.getKeysForValue(element);
|
2127
|
+
for (const selector of selectors) {
|
2128
|
+
this.selectorUnmatched(element, selector);
|
2129
|
+
}
|
2130
|
+
}
|
2131
|
+
elementAttributeChanged(element, _attributeName) {
|
2132
|
+
const { selector } = this;
|
2133
|
+
if (selector) {
|
2134
|
+
const matches = this.matchElement(element);
|
2135
|
+
const matchedBefore = this.matchesByElement.has(selector, element);
|
2136
|
+
if (matches && !matchedBefore) {
|
2137
|
+
this.selectorMatched(element, selector);
|
2138
|
+
}
|
2139
|
+
else if (!matches && matchedBefore) {
|
2140
|
+
this.selectorUnmatched(element, selector);
|
2141
|
+
}
|
2142
|
+
}
|
2143
|
+
}
|
2144
|
+
selectorMatched(element, selector) {
|
2145
|
+
this.delegate.selectorMatched(element, selector, this.details);
|
2146
|
+
this.matchesByElement.add(selector, element);
|
2147
|
+
}
|
2148
|
+
selectorUnmatched(element, selector) {
|
2149
|
+
this.delegate.selectorUnmatched(element, selector, this.details);
|
2150
|
+
this.matchesByElement.delete(selector, element);
|
2151
|
+
}
|
2152
|
+
}
|
2153
|
+
|
2154
|
+
class StringMapObserver {
|
2155
|
+
constructor(element, delegate) {
|
2156
|
+
this.element = element;
|
2157
|
+
this.delegate = delegate;
|
2158
|
+
this.started = false;
|
2159
|
+
this.stringMap = new Map();
|
2160
|
+
this.mutationObserver = new MutationObserver((mutations) => this.processMutations(mutations));
|
2161
|
+
}
|
2162
|
+
start() {
|
2163
|
+
if (!this.started) {
|
2164
|
+
this.started = true;
|
2165
|
+
this.mutationObserver.observe(this.element, { attributes: true, attributeOldValue: true });
|
2166
|
+
this.refresh();
|
2167
|
+
}
|
2168
|
+
}
|
2169
|
+
stop() {
|
2170
|
+
if (this.started) {
|
2171
|
+
this.mutationObserver.takeRecords();
|
2172
|
+
this.mutationObserver.disconnect();
|
2173
|
+
this.started = false;
|
2174
|
+
}
|
2175
|
+
}
|
2176
|
+
refresh() {
|
2177
|
+
if (this.started) {
|
2178
|
+
for (const attributeName of this.knownAttributeNames) {
|
2179
|
+
this.refreshAttribute(attributeName, null);
|
2180
|
+
}
|
2181
|
+
}
|
2182
|
+
}
|
2183
|
+
processMutations(mutations) {
|
2184
|
+
if (this.started) {
|
2185
|
+
for (const mutation of mutations) {
|
2186
|
+
this.processMutation(mutation);
|
2187
|
+
}
|
2188
|
+
}
|
2189
|
+
}
|
2190
|
+
processMutation(mutation) {
|
2191
|
+
const attributeName = mutation.attributeName;
|
2192
|
+
if (attributeName) {
|
2193
|
+
this.refreshAttribute(attributeName, mutation.oldValue);
|
2194
|
+
}
|
2195
|
+
}
|
2196
|
+
refreshAttribute(attributeName, oldValue) {
|
2197
|
+
const key = this.delegate.getStringMapKeyForAttribute(attributeName);
|
2198
|
+
if (key != null) {
|
2199
|
+
if (!this.stringMap.has(attributeName)) {
|
2200
|
+
this.stringMapKeyAdded(key, attributeName);
|
2201
|
+
}
|
2202
|
+
const value = this.element.getAttribute(attributeName);
|
2203
|
+
if (this.stringMap.get(attributeName) != value) {
|
2204
|
+
this.stringMapValueChanged(value, key, oldValue);
|
2205
|
+
}
|
2206
|
+
if (value == null) {
|
2207
|
+
const oldValue = this.stringMap.get(attributeName);
|
2208
|
+
this.stringMap.delete(attributeName);
|
2209
|
+
if (oldValue)
|
2210
|
+
this.stringMapKeyRemoved(key, attributeName, oldValue);
|
2211
|
+
}
|
2212
|
+
else {
|
2213
|
+
this.stringMap.set(attributeName, value);
|
2214
|
+
}
|
2215
|
+
}
|
2216
|
+
}
|
2217
|
+
stringMapKeyAdded(key, attributeName) {
|
2218
|
+
if (this.delegate.stringMapKeyAdded) {
|
2219
|
+
this.delegate.stringMapKeyAdded(key, attributeName);
|
2220
|
+
}
|
2221
|
+
}
|
2222
|
+
stringMapValueChanged(value, key, oldValue) {
|
2223
|
+
if (this.delegate.stringMapValueChanged) {
|
2224
|
+
this.delegate.stringMapValueChanged(value, key, oldValue);
|
2225
|
+
}
|
2226
|
+
}
|
2227
|
+
stringMapKeyRemoved(key, attributeName, oldValue) {
|
2228
|
+
if (this.delegate.stringMapKeyRemoved) {
|
2229
|
+
this.delegate.stringMapKeyRemoved(key, attributeName, oldValue);
|
2230
|
+
}
|
2231
|
+
}
|
2232
|
+
get knownAttributeNames() {
|
2233
|
+
return Array.from(new Set(this.currentAttributeNames.concat(this.recordedAttributeNames)));
|
2234
|
+
}
|
2235
|
+
get currentAttributeNames() {
|
2236
|
+
return Array.from(this.element.attributes).map((attribute) => attribute.name);
|
2237
|
+
}
|
2238
|
+
get recordedAttributeNames() {
|
2239
|
+
return Array.from(this.stringMap.keys());
|
2240
|
+
}
|
2241
|
+
}
|
2242
|
+
|
2243
|
+
class TokenListObserver {
|
2244
|
+
constructor(element, attributeName, delegate) {
|
2245
|
+
this.attributeObserver = new AttributeObserver(element, attributeName, this);
|
2246
|
+
this.delegate = delegate;
|
2247
|
+
this.tokensByElement = new Multimap();
|
2248
|
+
}
|
2249
|
+
get started() {
|
2250
|
+
return this.attributeObserver.started;
|
2251
|
+
}
|
2252
|
+
start() {
|
2253
|
+
this.attributeObserver.start();
|
2254
|
+
}
|
2255
|
+
pause(callback) {
|
2256
|
+
this.attributeObserver.pause(callback);
|
2257
|
+
}
|
2258
|
+
stop() {
|
2259
|
+
this.attributeObserver.stop();
|
2260
|
+
}
|
2261
|
+
refresh() {
|
2262
|
+
this.attributeObserver.refresh();
|
2263
|
+
}
|
2264
|
+
get element() {
|
2265
|
+
return this.attributeObserver.element;
|
2266
|
+
}
|
2267
|
+
get attributeName() {
|
2268
|
+
return this.attributeObserver.attributeName;
|
2269
|
+
}
|
2270
|
+
elementMatchedAttribute(element) {
|
2271
|
+
this.tokensMatched(this.readTokensForElement(element));
|
2272
|
+
}
|
2273
|
+
elementAttributeValueChanged(element) {
|
2274
|
+
const [unmatchedTokens, matchedTokens] = this.refreshTokensForElement(element);
|
2275
|
+
this.tokensUnmatched(unmatchedTokens);
|
2276
|
+
this.tokensMatched(matchedTokens);
|
2277
|
+
}
|
2278
|
+
elementUnmatchedAttribute(element) {
|
2279
|
+
this.tokensUnmatched(this.tokensByElement.getValuesForKey(element));
|
2280
|
+
}
|
2281
|
+
tokensMatched(tokens) {
|
2282
|
+
tokens.forEach((token) => this.tokenMatched(token));
|
2283
|
+
}
|
2284
|
+
tokensUnmatched(tokens) {
|
2285
|
+
tokens.forEach((token) => this.tokenUnmatched(token));
|
2286
|
+
}
|
2287
|
+
tokenMatched(token) {
|
2288
|
+
this.delegate.tokenMatched(token);
|
2289
|
+
this.tokensByElement.add(token.element, token);
|
2290
|
+
}
|
2291
|
+
tokenUnmatched(token) {
|
2292
|
+
this.delegate.tokenUnmatched(token);
|
2293
|
+
this.tokensByElement.delete(token.element, token);
|
2294
|
+
}
|
2295
|
+
refreshTokensForElement(element) {
|
2296
|
+
const previousTokens = this.tokensByElement.getValuesForKey(element);
|
2297
|
+
const currentTokens = this.readTokensForElement(element);
|
2298
|
+
const firstDifferingIndex = zip(previousTokens, currentTokens).findIndex(([previousToken, currentToken]) => !tokensAreEqual(previousToken, currentToken));
|
2299
|
+
if (firstDifferingIndex == -1) {
|
2300
|
+
return [[], []];
|
2301
|
+
}
|
2302
|
+
else {
|
2303
|
+
return [previousTokens.slice(firstDifferingIndex), currentTokens.slice(firstDifferingIndex)];
|
2304
|
+
}
|
2305
|
+
}
|
2306
|
+
readTokensForElement(element) {
|
2307
|
+
const attributeName = this.attributeName;
|
2308
|
+
const tokenString = element.getAttribute(attributeName) || "";
|
2309
|
+
return parseTokenString(tokenString, element, attributeName);
|
2310
|
+
}
|
2311
|
+
}
|
2312
|
+
function parseTokenString(tokenString, element, attributeName) {
|
2313
|
+
return tokenString
|
2314
|
+
.trim()
|
2315
|
+
.split(/\s+/)
|
2316
|
+
.filter((content) => content.length)
|
2317
|
+
.map((content, index) => ({ element, attributeName, content, index }));
|
2318
|
+
}
|
2319
|
+
function zip(left, right) {
|
2320
|
+
const length = Math.max(left.length, right.length);
|
2321
|
+
return Array.from({ length }, (_, index) => [left[index], right[index]]);
|
2322
|
+
}
|
2323
|
+
function tokensAreEqual(left, right) {
|
2324
|
+
return left && right && left.index == right.index && left.content == right.content;
|
2325
|
+
}
|
2326
|
+
|
2327
|
+
class ValueListObserver {
|
2328
|
+
constructor(element, attributeName, delegate) {
|
2329
|
+
this.tokenListObserver = new TokenListObserver(element, attributeName, this);
|
2330
|
+
this.delegate = delegate;
|
2331
|
+
this.parseResultsByToken = new WeakMap();
|
2332
|
+
this.valuesByTokenByElement = new WeakMap();
|
2333
|
+
}
|
2334
|
+
get started() {
|
2335
|
+
return this.tokenListObserver.started;
|
2336
|
+
}
|
2337
|
+
start() {
|
2338
|
+
this.tokenListObserver.start();
|
2339
|
+
}
|
2340
|
+
stop() {
|
2341
|
+
this.tokenListObserver.stop();
|
2342
|
+
}
|
2343
|
+
refresh() {
|
2344
|
+
this.tokenListObserver.refresh();
|
2345
|
+
}
|
2346
|
+
get element() {
|
2347
|
+
return this.tokenListObserver.element;
|
2348
|
+
}
|
2349
|
+
get attributeName() {
|
2350
|
+
return this.tokenListObserver.attributeName;
|
2351
|
+
}
|
2352
|
+
tokenMatched(token) {
|
2353
|
+
const { element } = token;
|
2354
|
+
const { value } = this.fetchParseResultForToken(token);
|
2355
|
+
if (value) {
|
2356
|
+
this.fetchValuesByTokenForElement(element).set(token, value);
|
2357
|
+
this.delegate.elementMatchedValue(element, value);
|
2358
|
+
}
|
2359
|
+
}
|
2360
|
+
tokenUnmatched(token) {
|
2361
|
+
const { element } = token;
|
2362
|
+
const { value } = this.fetchParseResultForToken(token);
|
2363
|
+
if (value) {
|
2364
|
+
this.fetchValuesByTokenForElement(element).delete(token);
|
2365
|
+
this.delegate.elementUnmatchedValue(element, value);
|
2366
|
+
}
|
2367
|
+
}
|
2368
|
+
fetchParseResultForToken(token) {
|
2369
|
+
let parseResult = this.parseResultsByToken.get(token);
|
2370
|
+
if (!parseResult) {
|
2371
|
+
parseResult = this.parseToken(token);
|
2372
|
+
this.parseResultsByToken.set(token, parseResult);
|
2373
|
+
}
|
2374
|
+
return parseResult;
|
2375
|
+
}
|
2376
|
+
fetchValuesByTokenForElement(element) {
|
2377
|
+
let valuesByToken = this.valuesByTokenByElement.get(element);
|
2378
|
+
if (!valuesByToken) {
|
2379
|
+
valuesByToken = new Map();
|
2380
|
+
this.valuesByTokenByElement.set(element, valuesByToken);
|
2381
|
+
}
|
2382
|
+
return valuesByToken;
|
2383
|
+
}
|
2384
|
+
parseToken(token) {
|
2385
|
+
try {
|
2386
|
+
const value = this.delegate.parseValueForToken(token);
|
2387
|
+
return { value };
|
2388
|
+
}
|
2389
|
+
catch (error) {
|
2390
|
+
return { error };
|
2391
|
+
}
|
2392
|
+
}
|
2393
|
+
}
|
2394
|
+
|
2395
|
+
class BindingObserver {
|
2396
|
+
constructor(context, delegate) {
|
2397
|
+
this.context = context;
|
2398
|
+
this.delegate = delegate;
|
2399
|
+
this.bindingsByAction = new Map();
|
2400
|
+
}
|
2401
|
+
start() {
|
2402
|
+
if (!this.valueListObserver) {
|
2403
|
+
this.valueListObserver = new ValueListObserver(this.element, this.actionAttribute, this);
|
2404
|
+
this.valueListObserver.start();
|
2405
|
+
}
|
2406
|
+
}
|
2407
|
+
stop() {
|
2408
|
+
if (this.valueListObserver) {
|
2409
|
+
this.valueListObserver.stop();
|
2410
|
+
delete this.valueListObserver;
|
2411
|
+
this.disconnectAllActions();
|
2412
|
+
}
|
2413
|
+
}
|
2414
|
+
get element() {
|
2415
|
+
return this.context.element;
|
2416
|
+
}
|
2417
|
+
get identifier() {
|
2418
|
+
return this.context.identifier;
|
2419
|
+
}
|
2420
|
+
get actionAttribute() {
|
2421
|
+
return this.schema.actionAttribute;
|
2422
|
+
}
|
2423
|
+
get schema() {
|
2424
|
+
return this.context.schema;
|
2425
|
+
}
|
2426
|
+
get bindings() {
|
2427
|
+
return Array.from(this.bindingsByAction.values());
|
2428
|
+
}
|
2429
|
+
connectAction(action) {
|
2430
|
+
const binding = new Binding(this.context, action);
|
2431
|
+
this.bindingsByAction.set(action, binding);
|
2432
|
+
this.delegate.bindingConnected(binding);
|
2433
|
+
}
|
2434
|
+
disconnectAction(action) {
|
2435
|
+
const binding = this.bindingsByAction.get(action);
|
2436
|
+
if (binding) {
|
2437
|
+
this.bindingsByAction.delete(action);
|
2438
|
+
this.delegate.bindingDisconnected(binding);
|
2439
|
+
}
|
2440
|
+
}
|
2441
|
+
disconnectAllActions() {
|
2442
|
+
this.bindings.forEach((binding) => this.delegate.bindingDisconnected(binding, true));
|
2443
|
+
this.bindingsByAction.clear();
|
2444
|
+
}
|
2445
|
+
parseValueForToken(token) {
|
2446
|
+
const action = Action.forToken(token, this.schema);
|
2447
|
+
if (action.identifier == this.identifier) {
|
2448
|
+
return action;
|
2449
|
+
}
|
2450
|
+
}
|
2451
|
+
elementMatchedValue(element, action) {
|
2452
|
+
this.connectAction(action);
|
2453
|
+
}
|
2454
|
+
elementUnmatchedValue(element, action) {
|
2455
|
+
this.disconnectAction(action);
|
2456
|
+
}
|
2457
|
+
}
|
2458
|
+
|
2459
|
+
class ValueObserver {
|
2460
|
+
constructor(context, receiver) {
|
2461
|
+
this.context = context;
|
2462
|
+
this.receiver = receiver;
|
2463
|
+
this.stringMapObserver = new StringMapObserver(this.element, this);
|
2464
|
+
this.valueDescriptorMap = this.controller.valueDescriptorMap;
|
2465
|
+
}
|
2466
|
+
start() {
|
2467
|
+
this.stringMapObserver.start();
|
2468
|
+
this.invokeChangedCallbacksForDefaultValues();
|
2469
|
+
}
|
2470
|
+
stop() {
|
2471
|
+
this.stringMapObserver.stop();
|
2472
|
+
}
|
2473
|
+
get element() {
|
2474
|
+
return this.context.element;
|
2475
|
+
}
|
2476
|
+
get controller() {
|
2477
|
+
return this.context.controller;
|
2478
|
+
}
|
2479
|
+
getStringMapKeyForAttribute(attributeName) {
|
2480
|
+
if (attributeName in this.valueDescriptorMap) {
|
2481
|
+
return this.valueDescriptorMap[attributeName].name;
|
2482
|
+
}
|
2483
|
+
}
|
2484
|
+
stringMapKeyAdded(key, attributeName) {
|
2485
|
+
const descriptor = this.valueDescriptorMap[attributeName];
|
2486
|
+
if (!this.hasValue(key)) {
|
2487
|
+
this.invokeChangedCallback(key, descriptor.writer(this.receiver[key]), descriptor.writer(descriptor.defaultValue));
|
2488
|
+
}
|
2489
|
+
}
|
2490
|
+
stringMapValueChanged(value, name, oldValue) {
|
2491
|
+
const descriptor = this.valueDescriptorNameMap[name];
|
2492
|
+
if (value === null)
|
2493
|
+
return;
|
2494
|
+
if (oldValue === null) {
|
2495
|
+
oldValue = descriptor.writer(descriptor.defaultValue);
|
2496
|
+
}
|
2497
|
+
this.invokeChangedCallback(name, value, oldValue);
|
2498
|
+
}
|
2499
|
+
stringMapKeyRemoved(key, attributeName, oldValue) {
|
2500
|
+
const descriptor = this.valueDescriptorNameMap[key];
|
2501
|
+
if (this.hasValue(key)) {
|
2502
|
+
this.invokeChangedCallback(key, descriptor.writer(this.receiver[key]), oldValue);
|
2503
|
+
}
|
2504
|
+
else {
|
2505
|
+
this.invokeChangedCallback(key, descriptor.writer(descriptor.defaultValue), oldValue);
|
2506
|
+
}
|
2507
|
+
}
|
2508
|
+
invokeChangedCallbacksForDefaultValues() {
|
2509
|
+
for (const { key, name, defaultValue, writer } of this.valueDescriptors) {
|
2510
|
+
if (defaultValue != undefined && !this.controller.data.has(key)) {
|
2511
|
+
this.invokeChangedCallback(name, writer(defaultValue), undefined);
|
2512
|
+
}
|
2513
|
+
}
|
2514
|
+
}
|
2515
|
+
invokeChangedCallback(name, rawValue, rawOldValue) {
|
2516
|
+
const changedMethodName = `${name}Changed`;
|
2517
|
+
const changedMethod = this.receiver[changedMethodName];
|
2518
|
+
if (typeof changedMethod == "function") {
|
2519
|
+
const descriptor = this.valueDescriptorNameMap[name];
|
2520
|
+
try {
|
2521
|
+
const value = descriptor.reader(rawValue);
|
2522
|
+
let oldValue = rawOldValue;
|
2523
|
+
if (rawOldValue) {
|
2524
|
+
oldValue = descriptor.reader(rawOldValue);
|
2525
|
+
}
|
2526
|
+
changedMethod.call(this.receiver, value, oldValue);
|
2527
|
+
}
|
2528
|
+
catch (error) {
|
2529
|
+
if (error instanceof TypeError) {
|
2530
|
+
error.message = `Stimulus Value "${this.context.identifier}.${descriptor.name}" - ${error.message}`;
|
2531
|
+
}
|
2532
|
+
throw error;
|
2533
|
+
}
|
2534
|
+
}
|
2535
|
+
}
|
2536
|
+
get valueDescriptors() {
|
2537
|
+
const { valueDescriptorMap } = this;
|
2538
|
+
return Object.keys(valueDescriptorMap).map((key) => valueDescriptorMap[key]);
|
2539
|
+
}
|
2540
|
+
get valueDescriptorNameMap() {
|
2541
|
+
const descriptors = {};
|
2542
|
+
Object.keys(this.valueDescriptorMap).forEach((key) => {
|
2543
|
+
const descriptor = this.valueDescriptorMap[key];
|
2544
|
+
descriptors[descriptor.name] = descriptor;
|
2545
|
+
});
|
2546
|
+
return descriptors;
|
2547
|
+
}
|
2548
|
+
hasValue(attributeName) {
|
2549
|
+
const descriptor = this.valueDescriptorNameMap[attributeName];
|
2550
|
+
const hasMethodName = `has${capitalize(descriptor.name)}`;
|
2551
|
+
return this.receiver[hasMethodName];
|
2552
|
+
}
|
2553
|
+
}
|
2554
|
+
|
2555
|
+
class TargetObserver {
|
2556
|
+
constructor(context, delegate) {
|
2557
|
+
this.context = context;
|
2558
|
+
this.delegate = delegate;
|
2559
|
+
this.targetsByName = new Multimap();
|
2560
|
+
}
|
2561
|
+
start() {
|
2562
|
+
if (!this.tokenListObserver) {
|
2563
|
+
this.tokenListObserver = new TokenListObserver(this.element, this.attributeName, this);
|
2564
|
+
this.tokenListObserver.start();
|
2565
|
+
}
|
2566
|
+
}
|
2567
|
+
stop() {
|
2568
|
+
if (this.tokenListObserver) {
|
2569
|
+
this.disconnectAllTargets();
|
2570
|
+
this.tokenListObserver.stop();
|
2571
|
+
delete this.tokenListObserver;
|
2572
|
+
}
|
2573
|
+
}
|
2574
|
+
tokenMatched({ element, content: name }) {
|
2575
|
+
if (this.scope.containsElement(element)) {
|
2576
|
+
this.connectTarget(element, name);
|
2577
|
+
}
|
2578
|
+
}
|
2579
|
+
tokenUnmatched({ element, content: name }) {
|
2580
|
+
this.disconnectTarget(element, name);
|
2581
|
+
}
|
2582
|
+
connectTarget(element, name) {
|
2583
|
+
var _a;
|
2584
|
+
if (!this.targetsByName.has(name, element)) {
|
2585
|
+
this.targetsByName.add(name, element);
|
2586
|
+
(_a = this.tokenListObserver) === null || _a === void 0 ? void 0 : _a.pause(() => this.delegate.targetConnected(element, name));
|
2587
|
+
}
|
2588
|
+
}
|
2589
|
+
disconnectTarget(element, name) {
|
2590
|
+
var _a;
|
2591
|
+
if (this.targetsByName.has(name, element)) {
|
2592
|
+
this.targetsByName.delete(name, element);
|
2593
|
+
(_a = this.tokenListObserver) === null || _a === void 0 ? void 0 : _a.pause(() => this.delegate.targetDisconnected(element, name));
|
2594
|
+
}
|
2595
|
+
}
|
2596
|
+
disconnectAllTargets() {
|
2597
|
+
for (const name of this.targetsByName.keys) {
|
2598
|
+
for (const element of this.targetsByName.getValuesForKey(name)) {
|
2599
|
+
this.disconnectTarget(element, name);
|
2600
|
+
}
|
2601
|
+
}
|
2602
|
+
}
|
2603
|
+
get attributeName() {
|
2604
|
+
return `data-${this.context.identifier}-target`;
|
2605
|
+
}
|
2606
|
+
get element() {
|
2607
|
+
return this.context.element;
|
2608
|
+
}
|
2609
|
+
get scope() {
|
2610
|
+
return this.context.scope;
|
2611
|
+
}
|
2612
|
+
}
|
2613
|
+
|
2614
|
+
function readInheritableStaticArrayValues(constructor, propertyName) {
|
2615
|
+
const ancestors = getAncestorsForConstructor(constructor);
|
2616
|
+
return Array.from(ancestors.reduce((values, constructor) => {
|
2617
|
+
getOwnStaticArrayValues(constructor, propertyName).forEach((name) => values.add(name));
|
2618
|
+
return values;
|
2619
|
+
}, new Set()));
|
2620
|
+
}
|
2621
|
+
function getAncestorsForConstructor(constructor) {
|
2622
|
+
const ancestors = [];
|
2623
|
+
while (constructor) {
|
2624
|
+
ancestors.push(constructor);
|
2625
|
+
constructor = Object.getPrototypeOf(constructor);
|
2626
|
+
}
|
2627
|
+
return ancestors.reverse();
|
2628
|
+
}
|
2629
|
+
function getOwnStaticArrayValues(constructor, propertyName) {
|
2630
|
+
const definition = constructor[propertyName];
|
2631
|
+
return Array.isArray(definition) ? definition : [];
|
2632
|
+
}
|
2633
|
+
|
2634
|
+
class OutletObserver {
|
2635
|
+
constructor(context, delegate) {
|
2636
|
+
this.started = false;
|
2637
|
+
this.context = context;
|
2638
|
+
this.delegate = delegate;
|
2639
|
+
this.outletsByName = new Multimap();
|
2640
|
+
this.outletElementsByName = new Multimap();
|
2641
|
+
this.selectorObserverMap = new Map();
|
2642
|
+
this.attributeObserverMap = new Map();
|
2643
|
+
}
|
2644
|
+
start() {
|
2645
|
+
if (!this.started) {
|
2646
|
+
this.outletDefinitions.forEach((outletName) => {
|
2647
|
+
this.setupSelectorObserverForOutlet(outletName);
|
2648
|
+
this.setupAttributeObserverForOutlet(outletName);
|
2649
|
+
});
|
2650
|
+
this.started = true;
|
2651
|
+
this.dependentContexts.forEach((context) => context.refresh());
|
2652
|
+
}
|
2653
|
+
}
|
2654
|
+
refresh() {
|
2655
|
+
this.selectorObserverMap.forEach((observer) => observer.refresh());
|
2656
|
+
this.attributeObserverMap.forEach((observer) => observer.refresh());
|
2657
|
+
}
|
2658
|
+
stop() {
|
2659
|
+
if (this.started) {
|
2660
|
+
this.started = false;
|
2661
|
+
this.disconnectAllOutlets();
|
2662
|
+
this.stopSelectorObservers();
|
2663
|
+
this.stopAttributeObservers();
|
2664
|
+
}
|
2665
|
+
}
|
2666
|
+
stopSelectorObservers() {
|
2667
|
+
if (this.selectorObserverMap.size > 0) {
|
2668
|
+
this.selectorObserverMap.forEach((observer) => observer.stop());
|
2669
|
+
this.selectorObserverMap.clear();
|
2670
|
+
}
|
2671
|
+
}
|
2672
|
+
stopAttributeObservers() {
|
2673
|
+
if (this.attributeObserverMap.size > 0) {
|
2674
|
+
this.attributeObserverMap.forEach((observer) => observer.stop());
|
2675
|
+
this.attributeObserverMap.clear();
|
2676
|
+
}
|
2677
|
+
}
|
2678
|
+
selectorMatched(element, _selector, { outletName }) {
|
2679
|
+
const outlet = this.getOutlet(element, outletName);
|
2680
|
+
if (outlet) {
|
2681
|
+
this.connectOutlet(outlet, element, outletName);
|
2682
|
+
}
|
2683
|
+
}
|
2684
|
+
selectorUnmatched(element, _selector, { outletName }) {
|
2685
|
+
const outlet = this.getOutletFromMap(element, outletName);
|
2686
|
+
if (outlet) {
|
2687
|
+
this.disconnectOutlet(outlet, element, outletName);
|
2688
|
+
}
|
2689
|
+
}
|
2690
|
+
selectorMatchElement(element, { outletName }) {
|
2691
|
+
const selector = this.selector(outletName);
|
2692
|
+
const hasOutlet = this.hasOutlet(element, outletName);
|
2693
|
+
const hasOutletController = element.matches(`[${this.schema.controllerAttribute}~=${outletName}]`);
|
2694
|
+
if (selector) {
|
2695
|
+
return hasOutlet && hasOutletController && element.matches(selector);
|
2696
|
+
}
|
2697
|
+
else {
|
2698
|
+
return false;
|
2699
|
+
}
|
2700
|
+
}
|
2701
|
+
elementMatchedAttribute(_element, attributeName) {
|
2702
|
+
const outletName = this.getOutletNameFromOutletAttributeName(attributeName);
|
2703
|
+
if (outletName) {
|
2704
|
+
this.updateSelectorObserverForOutlet(outletName);
|
2705
|
+
}
|
2706
|
+
}
|
2707
|
+
elementAttributeValueChanged(_element, attributeName) {
|
2708
|
+
const outletName = this.getOutletNameFromOutletAttributeName(attributeName);
|
2709
|
+
if (outletName) {
|
2710
|
+
this.updateSelectorObserverForOutlet(outletName);
|
2711
|
+
}
|
2712
|
+
}
|
2713
|
+
elementUnmatchedAttribute(_element, attributeName) {
|
2714
|
+
const outletName = this.getOutletNameFromOutletAttributeName(attributeName);
|
2715
|
+
if (outletName) {
|
2716
|
+
this.updateSelectorObserverForOutlet(outletName);
|
2717
|
+
}
|
2718
|
+
}
|
2719
|
+
connectOutlet(outlet, element, outletName) {
|
2720
|
+
var _a;
|
2721
|
+
if (!this.outletElementsByName.has(outletName, element)) {
|
2722
|
+
this.outletsByName.add(outletName, outlet);
|
2723
|
+
this.outletElementsByName.add(outletName, element);
|
2724
|
+
(_a = this.selectorObserverMap.get(outletName)) === null || _a === void 0 ? void 0 : _a.pause(() => this.delegate.outletConnected(outlet, element, outletName));
|
2725
|
+
}
|
2726
|
+
}
|
2727
|
+
disconnectOutlet(outlet, element, outletName) {
|
2728
|
+
var _a;
|
2729
|
+
if (this.outletElementsByName.has(outletName, element)) {
|
2730
|
+
this.outletsByName.delete(outletName, outlet);
|
2731
|
+
this.outletElementsByName.delete(outletName, element);
|
2732
|
+
(_a = this.selectorObserverMap
|
2733
|
+
.get(outletName)) === null || _a === void 0 ? void 0 : _a.pause(() => this.delegate.outletDisconnected(outlet, element, outletName));
|
2734
|
+
}
|
2735
|
+
}
|
2736
|
+
disconnectAllOutlets() {
|
2737
|
+
for (const outletName of this.outletElementsByName.keys) {
|
2738
|
+
for (const element of this.outletElementsByName.getValuesForKey(outletName)) {
|
2739
|
+
for (const outlet of this.outletsByName.getValuesForKey(outletName)) {
|
2740
|
+
this.disconnectOutlet(outlet, element, outletName);
|
2741
|
+
}
|
2742
|
+
}
|
2743
|
+
}
|
2744
|
+
}
|
2745
|
+
updateSelectorObserverForOutlet(outletName) {
|
2746
|
+
const observer = this.selectorObserverMap.get(outletName);
|
2747
|
+
if (observer) {
|
2748
|
+
observer.selector = this.selector(outletName);
|
2749
|
+
}
|
2750
|
+
}
|
2751
|
+
setupSelectorObserverForOutlet(outletName) {
|
2752
|
+
const selector = this.selector(outletName);
|
2753
|
+
const selectorObserver = new SelectorObserver(document.body, selector, this, { outletName });
|
2754
|
+
this.selectorObserverMap.set(outletName, selectorObserver);
|
2755
|
+
selectorObserver.start();
|
2756
|
+
}
|
2757
|
+
setupAttributeObserverForOutlet(outletName) {
|
2758
|
+
const attributeName = this.attributeNameForOutletName(outletName);
|
2759
|
+
const attributeObserver = new AttributeObserver(this.scope.element, attributeName, this);
|
2760
|
+
this.attributeObserverMap.set(outletName, attributeObserver);
|
2761
|
+
attributeObserver.start();
|
2762
|
+
}
|
2763
|
+
selector(outletName) {
|
2764
|
+
return this.scope.outlets.getSelectorForOutletName(outletName);
|
2765
|
+
}
|
2766
|
+
attributeNameForOutletName(outletName) {
|
2767
|
+
return this.scope.schema.outletAttributeForScope(this.identifier, outletName);
|
2768
|
+
}
|
2769
|
+
getOutletNameFromOutletAttributeName(attributeName) {
|
2770
|
+
return this.outletDefinitions.find((outletName) => this.attributeNameForOutletName(outletName) === attributeName);
|
2771
|
+
}
|
2772
|
+
get outletDependencies() {
|
2773
|
+
const dependencies = new Multimap();
|
2774
|
+
this.router.modules.forEach((module) => {
|
2775
|
+
const constructor = module.definition.controllerConstructor;
|
2776
|
+
const outlets = readInheritableStaticArrayValues(constructor, "outlets");
|
2777
|
+
outlets.forEach((outlet) => dependencies.add(outlet, module.identifier));
|
2778
|
+
});
|
2779
|
+
return dependencies;
|
2780
|
+
}
|
2781
|
+
get outletDefinitions() {
|
2782
|
+
return this.outletDependencies.getKeysForValue(this.identifier);
|
2783
|
+
}
|
2784
|
+
get dependentControllerIdentifiers() {
|
2785
|
+
return this.outletDependencies.getValuesForKey(this.identifier);
|
2786
|
+
}
|
2787
|
+
get dependentContexts() {
|
2788
|
+
const identifiers = this.dependentControllerIdentifiers;
|
2789
|
+
return this.router.contexts.filter((context) => identifiers.includes(context.identifier));
|
2790
|
+
}
|
2791
|
+
hasOutlet(element, outletName) {
|
2792
|
+
return !!this.getOutlet(element, outletName) || !!this.getOutletFromMap(element, outletName);
|
2793
|
+
}
|
2794
|
+
getOutlet(element, outletName) {
|
2795
|
+
return this.application.getControllerForElementAndIdentifier(element, outletName);
|
2796
|
+
}
|
2797
|
+
getOutletFromMap(element, outletName) {
|
2798
|
+
return this.outletsByName.getValuesForKey(outletName).find((outlet) => outlet.element === element);
|
2799
|
+
}
|
2800
|
+
get scope() {
|
2801
|
+
return this.context.scope;
|
2802
|
+
}
|
2803
|
+
get schema() {
|
2804
|
+
return this.context.schema;
|
2805
|
+
}
|
2806
|
+
get identifier() {
|
2807
|
+
return this.context.identifier;
|
2808
|
+
}
|
2809
|
+
get application() {
|
2810
|
+
return this.context.application;
|
2811
|
+
}
|
2812
|
+
get router() {
|
2813
|
+
return this.application.router;
|
2814
|
+
}
|
2815
|
+
}
|
2816
|
+
|
2817
|
+
class Context {
|
2818
|
+
constructor(module, scope) {
|
2819
|
+
this.logDebugActivity = (functionName, detail = {}) => {
|
2820
|
+
const { identifier, controller, element } = this;
|
2821
|
+
detail = Object.assign({ identifier, controller, element }, detail);
|
2822
|
+
this.application.logDebugActivity(this.identifier, functionName, detail);
|
2823
|
+
};
|
2824
|
+
this.module = module;
|
2825
|
+
this.scope = scope;
|
2826
|
+
this.controller = new module.controllerConstructor(this);
|
2827
|
+
this.bindingObserver = new BindingObserver(this, this.dispatcher);
|
2828
|
+
this.valueObserver = new ValueObserver(this, this.controller);
|
2829
|
+
this.targetObserver = new TargetObserver(this, this);
|
2830
|
+
this.outletObserver = new OutletObserver(this, this);
|
2831
|
+
try {
|
2832
|
+
this.controller.initialize();
|
2833
|
+
this.logDebugActivity("initialize");
|
2834
|
+
}
|
2835
|
+
catch (error) {
|
2836
|
+
this.handleError(error, "initializing controller");
|
2837
|
+
}
|
2838
|
+
}
|
2839
|
+
connect() {
|
2840
|
+
this.bindingObserver.start();
|
2841
|
+
this.valueObserver.start();
|
2842
|
+
this.targetObserver.start();
|
2843
|
+
this.outletObserver.start();
|
2844
|
+
try {
|
2845
|
+
this.controller.connect();
|
2846
|
+
this.logDebugActivity("connect");
|
2847
|
+
}
|
2848
|
+
catch (error) {
|
2849
|
+
this.handleError(error, "connecting controller");
|
2850
|
+
}
|
2851
|
+
}
|
2852
|
+
refresh() {
|
2853
|
+
this.outletObserver.refresh();
|
2854
|
+
}
|
2855
|
+
disconnect() {
|
2856
|
+
try {
|
2857
|
+
this.controller.disconnect();
|
2858
|
+
this.logDebugActivity("disconnect");
|
2859
|
+
}
|
2860
|
+
catch (error) {
|
2861
|
+
this.handleError(error, "disconnecting controller");
|
2862
|
+
}
|
2863
|
+
this.outletObserver.stop();
|
2864
|
+
this.targetObserver.stop();
|
2865
|
+
this.valueObserver.stop();
|
2866
|
+
this.bindingObserver.stop();
|
2867
|
+
}
|
2868
|
+
get application() {
|
2869
|
+
return this.module.application;
|
2870
|
+
}
|
2871
|
+
get identifier() {
|
2872
|
+
return this.module.identifier;
|
2873
|
+
}
|
2874
|
+
get schema() {
|
2875
|
+
return this.application.schema;
|
2876
|
+
}
|
2877
|
+
get dispatcher() {
|
2878
|
+
return this.application.dispatcher;
|
2879
|
+
}
|
2880
|
+
get element() {
|
2881
|
+
return this.scope.element;
|
2882
|
+
}
|
2883
|
+
get parentElement() {
|
2884
|
+
return this.element.parentElement;
|
2885
|
+
}
|
2886
|
+
handleError(error, message, detail = {}) {
|
2887
|
+
const { identifier, controller, element } = this;
|
2888
|
+
detail = Object.assign({ identifier, controller, element }, detail);
|
2889
|
+
this.application.handleError(error, `Error ${message}`, detail);
|
2890
|
+
}
|
2891
|
+
targetConnected(element, name) {
|
2892
|
+
this.invokeControllerMethod(`${name}TargetConnected`, element);
|
2893
|
+
}
|
2894
|
+
targetDisconnected(element, name) {
|
2895
|
+
this.invokeControllerMethod(`${name}TargetDisconnected`, element);
|
2896
|
+
}
|
2897
|
+
outletConnected(outlet, element, name) {
|
2898
|
+
this.invokeControllerMethod(`${namespaceCamelize(name)}OutletConnected`, outlet, element);
|
2899
|
+
}
|
2900
|
+
outletDisconnected(outlet, element, name) {
|
2901
|
+
this.invokeControllerMethod(`${namespaceCamelize(name)}OutletDisconnected`, outlet, element);
|
2902
|
+
}
|
2903
|
+
invokeControllerMethod(methodName, ...args) {
|
2904
|
+
const controller = this.controller;
|
2905
|
+
if (typeof controller[methodName] == "function") {
|
2906
|
+
controller[methodName](...args);
|
2907
|
+
}
|
2908
|
+
}
|
2909
|
+
}
|
2910
|
+
|
2911
|
+
function bless(constructor) {
|
2912
|
+
return shadow(constructor, getBlessedProperties(constructor));
|
2913
|
+
}
|
2914
|
+
function shadow(constructor, properties) {
|
2915
|
+
const shadowConstructor = extend(constructor);
|
2916
|
+
const shadowProperties = getShadowProperties(constructor.prototype, properties);
|
2917
|
+
Object.defineProperties(shadowConstructor.prototype, shadowProperties);
|
2918
|
+
return shadowConstructor;
|
2919
|
+
}
|
2920
|
+
function getBlessedProperties(constructor) {
|
2921
|
+
const blessings = readInheritableStaticArrayValues(constructor, "blessings");
|
2922
|
+
return blessings.reduce((blessedProperties, blessing) => {
|
2923
|
+
const properties = blessing(constructor);
|
2924
|
+
for (const key in properties) {
|
2925
|
+
const descriptor = blessedProperties[key] || {};
|
2926
|
+
blessedProperties[key] = Object.assign(descriptor, properties[key]);
|
2927
|
+
}
|
2928
|
+
return blessedProperties;
|
2929
|
+
}, {});
|
2930
|
+
}
|
2931
|
+
function getShadowProperties(prototype, properties) {
|
2932
|
+
return getOwnKeys(properties).reduce((shadowProperties, key) => {
|
2933
|
+
const descriptor = getShadowedDescriptor(prototype, properties, key);
|
2934
|
+
if (descriptor) {
|
2935
|
+
Object.assign(shadowProperties, { [key]: descriptor });
|
2936
|
+
}
|
2937
|
+
return shadowProperties;
|
2938
|
+
}, {});
|
2939
|
+
}
|
2940
|
+
function getShadowedDescriptor(prototype, properties, key) {
|
2941
|
+
const shadowingDescriptor = Object.getOwnPropertyDescriptor(prototype, key);
|
2942
|
+
const shadowedByValue = shadowingDescriptor && "value" in shadowingDescriptor;
|
2943
|
+
if (!shadowedByValue) {
|
2944
|
+
const descriptor = Object.getOwnPropertyDescriptor(properties, key).value;
|
2945
|
+
if (shadowingDescriptor) {
|
2946
|
+
descriptor.get = shadowingDescriptor.get || descriptor.get;
|
2947
|
+
descriptor.set = shadowingDescriptor.set || descriptor.set;
|
2948
|
+
}
|
2949
|
+
return descriptor;
|
2950
|
+
}
|
2951
|
+
}
|
2952
|
+
const getOwnKeys = (() => {
|
2953
|
+
if (typeof Object.getOwnPropertySymbols == "function") {
|
2954
|
+
return (object) => [...Object.getOwnPropertyNames(object), ...Object.getOwnPropertySymbols(object)];
|
2955
|
+
}
|
2956
|
+
else {
|
2957
|
+
return Object.getOwnPropertyNames;
|
2958
|
+
}
|
2959
|
+
})();
|
2960
|
+
const extend = (() => {
|
2961
|
+
function extendWithReflect(constructor) {
|
2962
|
+
function extended() {
|
2963
|
+
return Reflect.construct(constructor, arguments, new.target);
|
2964
|
+
}
|
2965
|
+
extended.prototype = Object.create(constructor.prototype, {
|
2966
|
+
constructor: { value: extended },
|
2967
|
+
});
|
2968
|
+
Reflect.setPrototypeOf(extended, constructor);
|
2969
|
+
return extended;
|
2970
|
+
}
|
2971
|
+
function testReflectExtension() {
|
2972
|
+
const a = function () {
|
2973
|
+
this.a.call(this);
|
2974
|
+
};
|
2975
|
+
const b = extendWithReflect(a);
|
2976
|
+
b.prototype.a = function () { };
|
2977
|
+
return new b();
|
2978
|
+
}
|
2979
|
+
try {
|
2980
|
+
testReflectExtension();
|
2981
|
+
return extendWithReflect;
|
2982
|
+
}
|
2983
|
+
catch (error) {
|
2984
|
+
return (constructor) => class extended extends constructor {
|
2985
|
+
};
|
2986
|
+
}
|
2987
|
+
})();
|
2988
|
+
|
2989
|
+
function blessDefinition(definition) {
|
2990
|
+
return {
|
2991
|
+
identifier: definition.identifier,
|
2992
|
+
controllerConstructor: bless(definition.controllerConstructor),
|
2993
|
+
};
|
2994
|
+
}
|
2995
|
+
|
2996
|
+
class Module {
|
2997
|
+
constructor(application, definition) {
|
2998
|
+
this.application = application;
|
2999
|
+
this.definition = blessDefinition(definition);
|
3000
|
+
this.contextsByScope = new WeakMap();
|
3001
|
+
this.connectedContexts = new Set();
|
3002
|
+
}
|
3003
|
+
get identifier() {
|
3004
|
+
return this.definition.identifier;
|
3005
|
+
}
|
3006
|
+
get controllerConstructor() {
|
3007
|
+
return this.definition.controllerConstructor;
|
3008
|
+
}
|
3009
|
+
get contexts() {
|
3010
|
+
return Array.from(this.connectedContexts);
|
3011
|
+
}
|
3012
|
+
connectContextForScope(scope) {
|
3013
|
+
const context = this.fetchContextForScope(scope);
|
3014
|
+
this.connectedContexts.add(context);
|
3015
|
+
context.connect();
|
3016
|
+
}
|
3017
|
+
disconnectContextForScope(scope) {
|
3018
|
+
const context = this.contextsByScope.get(scope);
|
3019
|
+
if (context) {
|
3020
|
+
this.connectedContexts.delete(context);
|
3021
|
+
context.disconnect();
|
3022
|
+
}
|
3023
|
+
}
|
3024
|
+
fetchContextForScope(scope) {
|
3025
|
+
let context = this.contextsByScope.get(scope);
|
3026
|
+
if (!context) {
|
3027
|
+
context = new Context(this, scope);
|
3028
|
+
this.contextsByScope.set(scope, context);
|
3029
|
+
}
|
3030
|
+
return context;
|
3031
|
+
}
|
3032
|
+
}
|
3033
|
+
|
3034
|
+
class ClassMap {
|
3035
|
+
constructor(scope) {
|
3036
|
+
this.scope = scope;
|
3037
|
+
}
|
3038
|
+
has(name) {
|
3039
|
+
return this.data.has(this.getDataKey(name));
|
3040
|
+
}
|
3041
|
+
get(name) {
|
3042
|
+
return this.getAll(name)[0];
|
3043
|
+
}
|
3044
|
+
getAll(name) {
|
3045
|
+
const tokenString = this.data.get(this.getDataKey(name)) || "";
|
3046
|
+
return tokenize(tokenString);
|
3047
|
+
}
|
3048
|
+
getAttributeName(name) {
|
3049
|
+
return this.data.getAttributeNameForKey(this.getDataKey(name));
|
3050
|
+
}
|
3051
|
+
getDataKey(name) {
|
3052
|
+
return `${name}-class`;
|
3053
|
+
}
|
3054
|
+
get data() {
|
3055
|
+
return this.scope.data;
|
3056
|
+
}
|
3057
|
+
}
|
3058
|
+
|
3059
|
+
class DataMap {
|
3060
|
+
constructor(scope) {
|
3061
|
+
this.scope = scope;
|
3062
|
+
}
|
3063
|
+
get element() {
|
3064
|
+
return this.scope.element;
|
3065
|
+
}
|
3066
|
+
get identifier() {
|
3067
|
+
return this.scope.identifier;
|
3068
|
+
}
|
3069
|
+
get(key) {
|
3070
|
+
const name = this.getAttributeNameForKey(key);
|
3071
|
+
return this.element.getAttribute(name);
|
3072
|
+
}
|
3073
|
+
set(key, value) {
|
3074
|
+
const name = this.getAttributeNameForKey(key);
|
3075
|
+
this.element.setAttribute(name, value);
|
3076
|
+
return this.get(key);
|
3077
|
+
}
|
3078
|
+
has(key) {
|
3079
|
+
const name = this.getAttributeNameForKey(key);
|
3080
|
+
return this.element.hasAttribute(name);
|
3081
|
+
}
|
3082
|
+
delete(key) {
|
3083
|
+
if (this.has(key)) {
|
3084
|
+
const name = this.getAttributeNameForKey(key);
|
3085
|
+
this.element.removeAttribute(name);
|
3086
|
+
return true;
|
3087
|
+
}
|
3088
|
+
else {
|
3089
|
+
return false;
|
3090
|
+
}
|
3091
|
+
}
|
3092
|
+
getAttributeNameForKey(key) {
|
3093
|
+
return `data-${this.identifier}-${dasherize(key)}`;
|
3094
|
+
}
|
3095
|
+
}
|
3096
|
+
|
3097
|
+
class Guide {
|
3098
|
+
constructor(logger) {
|
3099
|
+
this.warnedKeysByObject = new WeakMap();
|
3100
|
+
this.logger = logger;
|
3101
|
+
}
|
3102
|
+
warn(object, key, message) {
|
3103
|
+
let warnedKeys = this.warnedKeysByObject.get(object);
|
3104
|
+
if (!warnedKeys) {
|
3105
|
+
warnedKeys = new Set();
|
3106
|
+
this.warnedKeysByObject.set(object, warnedKeys);
|
3107
|
+
}
|
3108
|
+
if (!warnedKeys.has(key)) {
|
3109
|
+
warnedKeys.add(key);
|
3110
|
+
this.logger.warn(message, object);
|
3111
|
+
}
|
3112
|
+
}
|
3113
|
+
}
|
3114
|
+
|
3115
|
+
function attributeValueContainsToken(attributeName, token) {
|
3116
|
+
return `[${attributeName}~="${token}"]`;
|
3117
|
+
}
|
3118
|
+
|
3119
|
+
class TargetSet {
|
3120
|
+
constructor(scope) {
|
3121
|
+
this.scope = scope;
|
3122
|
+
}
|
3123
|
+
get element() {
|
3124
|
+
return this.scope.element;
|
3125
|
+
}
|
3126
|
+
get identifier() {
|
3127
|
+
return this.scope.identifier;
|
3128
|
+
}
|
3129
|
+
get schema() {
|
3130
|
+
return this.scope.schema;
|
3131
|
+
}
|
3132
|
+
has(targetName) {
|
3133
|
+
return this.find(targetName) != null;
|
3134
|
+
}
|
3135
|
+
find(...targetNames) {
|
3136
|
+
return targetNames.reduce((target, targetName) => target || this.findTarget(targetName) || this.findLegacyTarget(targetName), undefined);
|
3137
|
+
}
|
3138
|
+
findAll(...targetNames) {
|
3139
|
+
return targetNames.reduce((targets, targetName) => [
|
3140
|
+
...targets,
|
3141
|
+
...this.findAllTargets(targetName),
|
3142
|
+
...this.findAllLegacyTargets(targetName),
|
3143
|
+
], []);
|
3144
|
+
}
|
3145
|
+
findTarget(targetName) {
|
3146
|
+
const selector = this.getSelectorForTargetName(targetName);
|
3147
|
+
return this.scope.findElement(selector);
|
3148
|
+
}
|
3149
|
+
findAllTargets(targetName) {
|
3150
|
+
const selector = this.getSelectorForTargetName(targetName);
|
3151
|
+
return this.scope.findAllElements(selector);
|
3152
|
+
}
|
3153
|
+
getSelectorForTargetName(targetName) {
|
3154
|
+
const attributeName = this.schema.targetAttributeForScope(this.identifier);
|
3155
|
+
return attributeValueContainsToken(attributeName, targetName);
|
3156
|
+
}
|
3157
|
+
findLegacyTarget(targetName) {
|
3158
|
+
const selector = this.getLegacySelectorForTargetName(targetName);
|
3159
|
+
return this.deprecate(this.scope.findElement(selector), targetName);
|
3160
|
+
}
|
3161
|
+
findAllLegacyTargets(targetName) {
|
3162
|
+
const selector = this.getLegacySelectorForTargetName(targetName);
|
3163
|
+
return this.scope.findAllElements(selector).map((element) => this.deprecate(element, targetName));
|
3164
|
+
}
|
3165
|
+
getLegacySelectorForTargetName(targetName) {
|
3166
|
+
const targetDescriptor = `${this.identifier}.${targetName}`;
|
3167
|
+
return attributeValueContainsToken(this.schema.targetAttribute, targetDescriptor);
|
3168
|
+
}
|
3169
|
+
deprecate(element, targetName) {
|
3170
|
+
if (element) {
|
3171
|
+
const { identifier } = this;
|
3172
|
+
const attributeName = this.schema.targetAttribute;
|
3173
|
+
const revisedAttributeName = this.schema.targetAttributeForScope(identifier);
|
3174
|
+
this.guide.warn(element, `target:${targetName}`, `Please replace ${attributeName}="${identifier}.${targetName}" with ${revisedAttributeName}="${targetName}". ` +
|
3175
|
+
`The ${attributeName} attribute is deprecated and will be removed in a future version of Stimulus.`);
|
3176
|
+
}
|
3177
|
+
return element;
|
3178
|
+
}
|
3179
|
+
get guide() {
|
3180
|
+
return this.scope.guide;
|
3181
|
+
}
|
3182
|
+
}
|
3183
|
+
|
3184
|
+
class OutletSet {
|
3185
|
+
constructor(scope, controllerElement) {
|
3186
|
+
this.scope = scope;
|
3187
|
+
this.controllerElement = controllerElement;
|
3188
|
+
}
|
3189
|
+
get element() {
|
3190
|
+
return this.scope.element;
|
3191
|
+
}
|
3192
|
+
get identifier() {
|
3193
|
+
return this.scope.identifier;
|
3194
|
+
}
|
3195
|
+
get schema() {
|
3196
|
+
return this.scope.schema;
|
3197
|
+
}
|
3198
|
+
has(outletName) {
|
3199
|
+
return this.find(outletName) != null;
|
3200
|
+
}
|
3201
|
+
find(...outletNames) {
|
3202
|
+
return outletNames.reduce((outlet, outletName) => outlet || this.findOutlet(outletName), undefined);
|
3203
|
+
}
|
3204
|
+
findAll(...outletNames) {
|
3205
|
+
return outletNames.reduce((outlets, outletName) => [...outlets, ...this.findAllOutlets(outletName)], []);
|
3206
|
+
}
|
3207
|
+
getSelectorForOutletName(outletName) {
|
3208
|
+
const attributeName = this.schema.outletAttributeForScope(this.identifier, outletName);
|
3209
|
+
return this.controllerElement.getAttribute(attributeName);
|
3210
|
+
}
|
3211
|
+
findOutlet(outletName) {
|
3212
|
+
const selector = this.getSelectorForOutletName(outletName);
|
3213
|
+
if (selector)
|
3214
|
+
return this.findElement(selector, outletName);
|
3215
|
+
}
|
3216
|
+
findAllOutlets(outletName) {
|
3217
|
+
const selector = this.getSelectorForOutletName(outletName);
|
3218
|
+
return selector ? this.findAllElements(selector, outletName) : [];
|
3219
|
+
}
|
3220
|
+
findElement(selector, outletName) {
|
3221
|
+
const elements = this.scope.queryElements(selector);
|
3222
|
+
return elements.filter((element) => this.matchesElement(element, selector, outletName))[0];
|
3223
|
+
}
|
3224
|
+
findAllElements(selector, outletName) {
|
3225
|
+
const elements = this.scope.queryElements(selector);
|
3226
|
+
return elements.filter((element) => this.matchesElement(element, selector, outletName));
|
3227
|
+
}
|
3228
|
+
matchesElement(element, selector, outletName) {
|
3229
|
+
const controllerAttribute = element.getAttribute(this.scope.schema.controllerAttribute) || "";
|
3230
|
+
return element.matches(selector) && controllerAttribute.split(" ").includes(outletName);
|
3231
|
+
}
|
3232
|
+
}
|
3233
|
+
|
3234
|
+
class Scope {
|
3235
|
+
constructor(schema, element, identifier, logger) {
|
3236
|
+
this.targets = new TargetSet(this);
|
3237
|
+
this.classes = new ClassMap(this);
|
3238
|
+
this.data = new DataMap(this);
|
3239
|
+
this.containsElement = (element) => {
|
3240
|
+
return element.closest(this.controllerSelector) === this.element;
|
3241
|
+
};
|
3242
|
+
this.schema = schema;
|
3243
|
+
this.element = element;
|
3244
|
+
this.identifier = identifier;
|
3245
|
+
this.guide = new Guide(logger);
|
3246
|
+
this.outlets = new OutletSet(this.documentScope, element);
|
3247
|
+
}
|
3248
|
+
findElement(selector) {
|
3249
|
+
return this.element.matches(selector) ? this.element : this.queryElements(selector).find(this.containsElement);
|
3250
|
+
}
|
3251
|
+
findAllElements(selector) {
|
3252
|
+
return [
|
3253
|
+
...(this.element.matches(selector) ? [this.element] : []),
|
3254
|
+
...this.queryElements(selector).filter(this.containsElement),
|
3255
|
+
];
|
3256
|
+
}
|
3257
|
+
queryElements(selector) {
|
3258
|
+
return Array.from(this.element.querySelectorAll(selector));
|
3259
|
+
}
|
3260
|
+
get controllerSelector() {
|
3261
|
+
return attributeValueContainsToken(this.schema.controllerAttribute, this.identifier);
|
3262
|
+
}
|
3263
|
+
get isDocumentScope() {
|
3264
|
+
return this.element === document.documentElement;
|
3265
|
+
}
|
3266
|
+
get documentScope() {
|
3267
|
+
return this.isDocumentScope
|
3268
|
+
? this
|
3269
|
+
: new Scope(this.schema, document.documentElement, this.identifier, this.guide.logger);
|
3270
|
+
}
|
3271
|
+
}
|
3272
|
+
|
3273
|
+
class ScopeObserver {
|
3274
|
+
constructor(element, schema, delegate) {
|
3275
|
+
this.element = element;
|
3276
|
+
this.schema = schema;
|
3277
|
+
this.delegate = delegate;
|
3278
|
+
this.valueListObserver = new ValueListObserver(this.element, this.controllerAttribute, this);
|
3279
|
+
this.scopesByIdentifierByElement = new WeakMap();
|
3280
|
+
this.scopeReferenceCounts = new WeakMap();
|
3281
|
+
}
|
3282
|
+
start() {
|
3283
|
+
this.valueListObserver.start();
|
3284
|
+
}
|
3285
|
+
stop() {
|
3286
|
+
this.valueListObserver.stop();
|
3287
|
+
}
|
3288
|
+
get controllerAttribute() {
|
3289
|
+
return this.schema.controllerAttribute;
|
3290
|
+
}
|
3291
|
+
parseValueForToken(token) {
|
3292
|
+
const { element, content: identifier } = token;
|
3293
|
+
return this.parseValueForElementAndIdentifier(element, identifier);
|
3294
|
+
}
|
3295
|
+
parseValueForElementAndIdentifier(element, identifier) {
|
3296
|
+
const scopesByIdentifier = this.fetchScopesByIdentifierForElement(element);
|
3297
|
+
let scope = scopesByIdentifier.get(identifier);
|
3298
|
+
if (!scope) {
|
3299
|
+
scope = this.delegate.createScopeForElementAndIdentifier(element, identifier);
|
3300
|
+
scopesByIdentifier.set(identifier, scope);
|
3301
|
+
}
|
3302
|
+
return scope;
|
3303
|
+
}
|
3304
|
+
elementMatchedValue(element, value) {
|
3305
|
+
const referenceCount = (this.scopeReferenceCounts.get(value) || 0) + 1;
|
3306
|
+
this.scopeReferenceCounts.set(value, referenceCount);
|
3307
|
+
if (referenceCount == 1) {
|
3308
|
+
this.delegate.scopeConnected(value);
|
3309
|
+
}
|
3310
|
+
}
|
3311
|
+
elementUnmatchedValue(element, value) {
|
3312
|
+
const referenceCount = this.scopeReferenceCounts.get(value);
|
3313
|
+
if (referenceCount) {
|
3314
|
+
this.scopeReferenceCounts.set(value, referenceCount - 1);
|
3315
|
+
if (referenceCount == 1) {
|
3316
|
+
this.delegate.scopeDisconnected(value);
|
3317
|
+
}
|
3318
|
+
}
|
3319
|
+
}
|
3320
|
+
fetchScopesByIdentifierForElement(element) {
|
3321
|
+
let scopesByIdentifier = this.scopesByIdentifierByElement.get(element);
|
3322
|
+
if (!scopesByIdentifier) {
|
3323
|
+
scopesByIdentifier = new Map();
|
3324
|
+
this.scopesByIdentifierByElement.set(element, scopesByIdentifier);
|
3325
|
+
}
|
3326
|
+
return scopesByIdentifier;
|
3327
|
+
}
|
3328
|
+
}
|
3329
|
+
|
3330
|
+
class Router {
|
3331
|
+
constructor(application) {
|
3332
|
+
this.application = application;
|
3333
|
+
this.scopeObserver = new ScopeObserver(this.element, this.schema, this);
|
3334
|
+
this.scopesByIdentifier = new Multimap();
|
3335
|
+
this.modulesByIdentifier = new Map();
|
3336
|
+
}
|
3337
|
+
get element() {
|
3338
|
+
return this.application.element;
|
3339
|
+
}
|
3340
|
+
get schema() {
|
3341
|
+
return this.application.schema;
|
3342
|
+
}
|
3343
|
+
get logger() {
|
3344
|
+
return this.application.logger;
|
3345
|
+
}
|
3346
|
+
get controllerAttribute() {
|
3347
|
+
return this.schema.controllerAttribute;
|
3348
|
+
}
|
3349
|
+
get modules() {
|
3350
|
+
return Array.from(this.modulesByIdentifier.values());
|
3351
|
+
}
|
3352
|
+
get contexts() {
|
3353
|
+
return this.modules.reduce((contexts, module) => contexts.concat(module.contexts), []);
|
3354
|
+
}
|
3355
|
+
start() {
|
3356
|
+
this.scopeObserver.start();
|
3357
|
+
}
|
3358
|
+
stop() {
|
3359
|
+
this.scopeObserver.stop();
|
3360
|
+
}
|
3361
|
+
loadDefinition(definition) {
|
3362
|
+
this.unloadIdentifier(definition.identifier);
|
3363
|
+
const module = new Module(this.application, definition);
|
3364
|
+
this.connectModule(module);
|
3365
|
+
const afterLoad = definition.controllerConstructor.afterLoad;
|
3366
|
+
if (afterLoad) {
|
3367
|
+
afterLoad.call(definition.controllerConstructor, definition.identifier, this.application);
|
3368
|
+
}
|
3369
|
+
}
|
3370
|
+
unloadIdentifier(identifier) {
|
3371
|
+
const module = this.modulesByIdentifier.get(identifier);
|
3372
|
+
if (module) {
|
3373
|
+
this.disconnectModule(module);
|
3374
|
+
}
|
3375
|
+
}
|
3376
|
+
getContextForElementAndIdentifier(element, identifier) {
|
3377
|
+
const module = this.modulesByIdentifier.get(identifier);
|
3378
|
+
if (module) {
|
3379
|
+
return module.contexts.find((context) => context.element == element);
|
3380
|
+
}
|
3381
|
+
}
|
3382
|
+
proposeToConnectScopeForElementAndIdentifier(element, identifier) {
|
3383
|
+
const scope = this.scopeObserver.parseValueForElementAndIdentifier(element, identifier);
|
3384
|
+
if (scope) {
|
3385
|
+
this.scopeObserver.elementMatchedValue(scope.element, scope);
|
3386
|
+
}
|
3387
|
+
else {
|
3388
|
+
console.error(`Couldn't find or create scope for identifier: "${identifier}" and element:`, element);
|
3389
|
+
}
|
3390
|
+
}
|
3391
|
+
handleError(error, message, detail) {
|
3392
|
+
this.application.handleError(error, message, detail);
|
3393
|
+
}
|
3394
|
+
createScopeForElementAndIdentifier(element, identifier) {
|
3395
|
+
return new Scope(this.schema, element, identifier, this.logger);
|
3396
|
+
}
|
3397
|
+
scopeConnected(scope) {
|
3398
|
+
this.scopesByIdentifier.add(scope.identifier, scope);
|
3399
|
+
const module = this.modulesByIdentifier.get(scope.identifier);
|
3400
|
+
if (module) {
|
3401
|
+
module.connectContextForScope(scope);
|
3402
|
+
}
|
3403
|
+
}
|
3404
|
+
scopeDisconnected(scope) {
|
3405
|
+
this.scopesByIdentifier.delete(scope.identifier, scope);
|
3406
|
+
const module = this.modulesByIdentifier.get(scope.identifier);
|
3407
|
+
if (module) {
|
3408
|
+
module.disconnectContextForScope(scope);
|
3409
|
+
}
|
3410
|
+
}
|
3411
|
+
connectModule(module) {
|
3412
|
+
this.modulesByIdentifier.set(module.identifier, module);
|
3413
|
+
const scopes = this.scopesByIdentifier.getValuesForKey(module.identifier);
|
3414
|
+
scopes.forEach((scope) => module.connectContextForScope(scope));
|
3415
|
+
}
|
3416
|
+
disconnectModule(module) {
|
3417
|
+
this.modulesByIdentifier.delete(module.identifier);
|
3418
|
+
const scopes = this.scopesByIdentifier.getValuesForKey(module.identifier);
|
3419
|
+
scopes.forEach((scope) => module.disconnectContextForScope(scope));
|
3420
|
+
}
|
3421
|
+
}
|
3422
|
+
|
3423
|
+
const defaultSchema = {
|
3424
|
+
controllerAttribute: "data-controller",
|
3425
|
+
actionAttribute: "data-action",
|
3426
|
+
targetAttribute: "data-target",
|
3427
|
+
targetAttributeForScope: (identifier) => `data-${identifier}-target`,
|
3428
|
+
outletAttributeForScope: (identifier, outlet) => `data-${identifier}-${outlet}-outlet`,
|
3429
|
+
keyMappings: Object.assign(Object.assign({ enter: "Enter", tab: "Tab", esc: "Escape", space: " ", up: "ArrowUp", down: "ArrowDown", left: "ArrowLeft", right: "ArrowRight", home: "Home", end: "End", page_up: "PageUp", page_down: "PageDown" }, objectFromEntries("abcdefghijklmnopqrstuvwxyz".split("").map((c) => [c, c]))), objectFromEntries("0123456789".split("").map((n) => [n, n]))),
|
3430
|
+
};
|
3431
|
+
function objectFromEntries(array) {
|
3432
|
+
return array.reduce((memo, [k, v]) => (Object.assign(Object.assign({}, memo), { [k]: v })), {});
|
3433
|
+
}
|
3434
|
+
|
3435
|
+
class Application {
|
3436
|
+
constructor(element = document.documentElement, schema = defaultSchema) {
|
3437
|
+
this.logger = console;
|
3438
|
+
this.debug = false;
|
3439
|
+
this.logDebugActivity = (identifier, functionName, detail = {}) => {
|
3440
|
+
if (this.debug) {
|
3441
|
+
this.logFormattedMessage(identifier, functionName, detail);
|
3442
|
+
}
|
3443
|
+
};
|
3444
|
+
this.element = element;
|
3445
|
+
this.schema = schema;
|
3446
|
+
this.dispatcher = new Dispatcher(this);
|
3447
|
+
this.router = new Router(this);
|
3448
|
+
this.actionDescriptorFilters = Object.assign({}, defaultActionDescriptorFilters);
|
3449
|
+
}
|
3450
|
+
static start(element, schema) {
|
3451
|
+
const application = new this(element, schema);
|
3452
|
+
application.start();
|
3453
|
+
return application;
|
3454
|
+
}
|
3455
|
+
async start() {
|
3456
|
+
await domReady();
|
3457
|
+
this.logDebugActivity("application", "starting");
|
3458
|
+
this.dispatcher.start();
|
3459
|
+
this.router.start();
|
3460
|
+
this.logDebugActivity("application", "start");
|
3461
|
+
}
|
3462
|
+
stop() {
|
3463
|
+
this.logDebugActivity("application", "stopping");
|
3464
|
+
this.dispatcher.stop();
|
3465
|
+
this.router.stop();
|
3466
|
+
this.logDebugActivity("application", "stop");
|
3467
|
+
}
|
3468
|
+
register(identifier, controllerConstructor) {
|
3469
|
+
this.load({ identifier, controllerConstructor });
|
3470
|
+
}
|
3471
|
+
registerActionOption(name, filter) {
|
3472
|
+
this.actionDescriptorFilters[name] = filter;
|
3473
|
+
}
|
3474
|
+
load(head, ...rest) {
|
3475
|
+
const definitions = Array.isArray(head) ? head : [head, ...rest];
|
3476
|
+
definitions.forEach((definition) => {
|
3477
|
+
if (definition.controllerConstructor.shouldLoad) {
|
3478
|
+
this.router.loadDefinition(definition);
|
3479
|
+
}
|
3480
|
+
});
|
3481
|
+
}
|
3482
|
+
unload(head, ...rest) {
|
3483
|
+
const identifiers = Array.isArray(head) ? head : [head, ...rest];
|
3484
|
+
identifiers.forEach((identifier) => this.router.unloadIdentifier(identifier));
|
3485
|
+
}
|
3486
|
+
get controllers() {
|
3487
|
+
return this.router.contexts.map((context) => context.controller);
|
3488
|
+
}
|
3489
|
+
getControllerForElementAndIdentifier(element, identifier) {
|
3490
|
+
const context = this.router.getContextForElementAndIdentifier(element, identifier);
|
3491
|
+
return context ? context.controller : null;
|
3492
|
+
}
|
3493
|
+
handleError(error, message, detail) {
|
3494
|
+
var _a;
|
3495
|
+
this.logger.error(`%s\n\n%o\n\n%o`, message, error, detail);
|
3496
|
+
(_a = window.onerror) === null || _a === void 0 ? void 0 : _a.call(window, message, "", 0, 0, error);
|
3497
|
+
}
|
3498
|
+
logFormattedMessage(identifier, functionName, detail = {}) {
|
3499
|
+
detail = Object.assign({ application: this }, detail);
|
3500
|
+
this.logger.groupCollapsed(`${identifier} #${functionName}`);
|
3501
|
+
this.logger.log("details:", Object.assign({}, detail));
|
3502
|
+
this.logger.groupEnd();
|
3503
|
+
}
|
3504
|
+
}
|
3505
|
+
function domReady() {
|
3506
|
+
return new Promise((resolve) => {
|
3507
|
+
if (document.readyState == "loading") {
|
3508
|
+
document.addEventListener("DOMContentLoaded", () => resolve());
|
3509
|
+
}
|
3510
|
+
else {
|
3511
|
+
resolve();
|
3512
|
+
}
|
3513
|
+
});
|
3514
|
+
}
|
3515
|
+
|
3516
|
+
class StimulusReloader {
|
3517
|
+
static async reload(filePattern) {
|
3518
|
+
const document = await reloadHtmlDocument();
|
3519
|
+
return new StimulusReloader(document, filePattern).reload();
|
3520
|
+
}
|
3521
|
+
constructor(document) {
|
3522
|
+
let filePattern = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : /./;
|
3523
|
+
this.document = document;
|
3524
|
+
this.filePattern = filePattern;
|
3525
|
+
this.application = window.Stimulus || Application.start();
|
3526
|
+
}
|
3527
|
+
async reload() {
|
3528
|
+
log("Reload Stimulus controllers...");
|
3529
|
+
this.application.stop();
|
3530
|
+
await this.#reloadStimulusControllers();
|
3531
|
+
this.application.start();
|
3532
|
+
}
|
3533
|
+
async #reloadStimulusControllers() {
|
3534
|
+
await Promise.all(this.#stimulusControllerPaths.map(async moduleName => this.#reloadStimulusController(moduleName)));
|
3535
|
+
}
|
3536
|
+
get #stimulusControllerPaths() {
|
3537
|
+
return Object.keys(this.#stimulusPathsByModule).filter(path => path.endsWith("_controller") && this.#shouldReloadController(path));
|
3538
|
+
}
|
3539
|
+
#shouldReloadController(path) {
|
3540
|
+
return this.filePattern.test(path);
|
3541
|
+
}
|
3542
|
+
get #stimulusPathsByModule() {
|
3543
|
+
this.pathsByModule = this.pathsByModule || this.#parseImportmapJson();
|
3544
|
+
return this.pathsByModule;
|
3545
|
+
}
|
3546
|
+
#parseImportmapJson() {
|
3547
|
+
const importmapScript = this.document.querySelector("script[type=importmap]");
|
3548
|
+
return JSON.parse(importmapScript.text).imports;
|
3549
|
+
}
|
3550
|
+
async #reloadStimulusController(moduleName) {
|
3551
|
+
log(`\t${moduleName}`);
|
3552
|
+
const controllerName = this.#extractControllerName(moduleName);
|
3553
|
+
const path = cacheBustedUrl(this.#pathForModuleName(moduleName));
|
3554
|
+
const module = await import(path);
|
3555
|
+
this.#registerController(controllerName, module);
|
3556
|
+
}
|
3557
|
+
#pathForModuleName(moduleName) {
|
3558
|
+
return this.#stimulusPathsByModule[moduleName];
|
3559
|
+
}
|
3560
|
+
#extractControllerName(path) {
|
3561
|
+
return path.replace(/^.*\//, "").replace("_controller", "").replace(/\//g, "--").replace(/_/g, "-");
|
3562
|
+
}
|
3563
|
+
#registerController(name, module) {
|
3564
|
+
this.application.unload(name);
|
3565
|
+
this.application.register(name, module.default);
|
3566
|
+
}
|
3567
|
+
}
|
3568
|
+
|
3569
|
+
class HtmlReloader {
|
3570
|
+
static async reload() {
|
3571
|
+
return new HtmlReloader().reload();
|
3572
|
+
}
|
3573
|
+
async reload() {
|
3574
|
+
const reloadedDocument = await this.#reloadHtml();
|
3575
|
+
await this.#reloadStimulus(reloadedDocument);
|
3576
|
+
}
|
3577
|
+
async #reloadHtml() {
|
3578
|
+
log("Reload html...");
|
3579
|
+
const reloadedDocument = await reloadHtmlDocument();
|
3580
|
+
this.#updateBody(reloadedDocument.body);
|
3581
|
+
return reloadedDocument;
|
3582
|
+
}
|
3583
|
+
async #reloadStimulus(reloadedDocument) {
|
3584
|
+
return new StimulusReloader(reloadedDocument).reload();
|
3585
|
+
}
|
3586
|
+
#updateBody(newBody) {
|
3587
|
+
Idiomorph.morph(document.body, newBody);
|
3588
|
+
}
|
3589
|
+
}
|
3590
|
+
|
3591
|
+
class CssReloader {
|
3592
|
+
static async reload() {
|
3593
|
+
for (var _len = arguments.length, params = new Array(_len), _key = 0; _key < _len; _key++) {
|
3594
|
+
params[_key] = arguments[_key];
|
3595
|
+
}
|
3596
|
+
return new CssReloader(...params).reload();
|
3597
|
+
}
|
3598
|
+
constructor() {
|
3599
|
+
let filePattern = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : /./;
|
3600
|
+
this.filePattern = filePattern;
|
3601
|
+
}
|
3602
|
+
async reload() {
|
3603
|
+
log("Reload css...");
|
3604
|
+
await Promise.all(await this.#reloadAllLinks());
|
3605
|
+
}
|
3606
|
+
async #reloadAllLinks() {
|
3607
|
+
const newCssLinks = await this.#loadNewCssLinks();
|
3608
|
+
return newCssLinks.map(link => this.#reloadLinkIfNeeded(link));
|
3609
|
+
}
|
3610
|
+
async #loadNewCssLinks() {
|
3611
|
+
const reloadedDocument = await reloadHtmlDocument();
|
3612
|
+
return Array.from(reloadedDocument.head.querySelectorAll("link[rel='stylesheet']"));
|
3613
|
+
}
|
3614
|
+
#reloadLinkIfNeeded(link) {
|
3615
|
+
console.debug("reload if needed", link);
|
3616
|
+
if (this.#shouldReloadLink(link)) {
|
3617
|
+
return this.#reloadLink(link);
|
3618
|
+
} else {
|
3619
|
+
return Promise.resolve();
|
3620
|
+
}
|
3621
|
+
}
|
3622
|
+
#shouldReloadLink(link) {
|
3623
|
+
console.debug("CHECKING ", link.getAttribute("href"), this.filePattern);
|
3624
|
+
return this.filePattern.test(link.getAttribute("href"));
|
3625
|
+
}
|
3626
|
+
async #reloadLink(link) {
|
3627
|
+
return new Promise(resolve => {
|
3628
|
+
const href = link.getAttribute("href");
|
3629
|
+
const newLink = this.#findExistingLinkFor(link) || this.#appendNewLink(link);
|
3630
|
+
newLink.setAttribute("href", cacheBustedUrl(link.getAttribute("href")));
|
3631
|
+
newLink.onload = () => {
|
3632
|
+
log(`\t${href}`);
|
3633
|
+
resolve();
|
3634
|
+
};
|
3635
|
+
});
|
3636
|
+
}
|
3637
|
+
#findExistingLinkFor(link) {
|
3638
|
+
return this.#cssLinks.find(newLink => {
|
3639
|
+
return this.#withoutAssetDigest(link.href) === this.#withoutAssetDigest(newLink.href);
|
3640
|
+
});
|
3641
|
+
}
|
3642
|
+
get #cssLinks() {
|
3643
|
+
return Array.from(document.querySelectorAll("link[rel='stylesheet']"));
|
3644
|
+
}
|
3645
|
+
#withoutAssetDigest(url) {
|
3646
|
+
return url.replace(/-[^-]+\.css.*$/, ".css");
|
3647
|
+
}
|
3648
|
+
#appendNewLink(link) {
|
3649
|
+
document.head.append(link);
|
3650
|
+
return link;
|
3651
|
+
}
|
3652
|
+
}
|
3653
|
+
|
3654
|
+
consumer.subscriptions.create({
|
3655
|
+
channel: "HotwireSpark::Channel"
|
3656
|
+
}, {
|
3657
|
+
connected() {
|
3658
|
+
document.body.setAttribute("data-hotwire-spark-ready", "");
|
3659
|
+
},
|
3660
|
+
async received(data) {
|
3661
|
+
try {
|
3662
|
+
await this.dispatchMessage(data);
|
3663
|
+
} catch (error) {
|
3664
|
+
console.log(`Error on ${data.action}`, error);
|
3665
|
+
}
|
3666
|
+
},
|
3667
|
+
dispatchMessage(_ref) {
|
3668
|
+
let {
|
3669
|
+
action,
|
3670
|
+
path
|
3671
|
+
} = _ref;
|
3672
|
+
const fileName = nameFromFilePath(path);
|
3673
|
+
switch (action) {
|
3674
|
+
case "reload_html":
|
3675
|
+
return this.reloadHtml();
|
3676
|
+
case "reload_css":
|
3677
|
+
return this.reloadCss(fileName);
|
3678
|
+
case "reload_stimulus":
|
3679
|
+
return this.reloadStimulus(fileName);
|
3680
|
+
}
|
3681
|
+
},
|
3682
|
+
reloadHtml() {
|
3683
|
+
return HtmlReloader.reload();
|
3684
|
+
},
|
3685
|
+
reloadCss(path) {
|
3686
|
+
return CssReloader.reload(new RegExp(path));
|
3687
|
+
},
|
3688
|
+
reloadStimulus(path) {
|
3689
|
+
return StimulusReloader.reload(new RegExp(path));
|
3690
|
+
}
|
3691
|
+
});
|
3692
|
+
|
3693
|
+
const HotwireSpark = {
|
3694
|
+
config: {
|
3695
|
+
loggingEnabled: true
|
3696
|
+
}
|
3697
|
+
};
|
3698
|
+
|
3699
|
+
return HotwireSpark;
|
3700
|
+
|
3701
|
+
})();
|