atv-rails 0.0.2 → 0.0.4
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 +4 -4
- data/README.md +3 -3
- data/app/assets/javascripts/atv.js +664 -0
- data/app/assets/javascripts/atv.min.js +4 -0
- data/lib/atv/engine.rb +13 -0
- data/lib/atv/version.rb +3 -0
- data/lib/atv-rails.rb +5 -0
- data/lib/generators/atv/USAGE +14 -0
- data/lib/generators/atv/atv_generator.rb +19 -0
- data/lib/generators/atv/templates/controller.js.tt +13 -0
- data/lib/tasks/atv.rake +37 -0
- metadata +44 -11
- data/lib/atv/rails/railtie.rb +0 -6
- data/lib/atv/rails/version.rb +0 -5
- data/lib/atv/rails.rb +0 -8
- data/lib/tasks/atv/rails_tasks.rake +0 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 90eed2550de006c017a7e04a41aa89ffe9d3d0e34740c9cb6d9fc5c17c418537
|
4
|
+
data.tar.gz: d4c9e9bb798ee12cd2eda572d1300deb897fad1eee53bfb949d28cd9f341f2c9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 020f55905f8884bb5268f14d82a027a9c06aca7aaa71d652cf869f6d752779093183c489d0161565f575b79b08a496d44fd5a28f6142df4d8ac82ab541637714
|
7
|
+
data.tar.gz: fb0b1c821179e7f0a1588288d3be1be9ca1056c42ea65fdeacfb8e1e7617693a3a9c1183e8cfec2683fd17d90c3c0aca11408f2b8dfd7e7cc4b8022e976a36df
|
data/README.md
CHANGED
@@ -0,0 +1,664 @@
|
|
1
|
+
/*global
|
2
|
+
console, document, MutationObserver
|
3
|
+
*/
|
4
|
+
/*jslint white*/
|
5
|
+
|
6
|
+
//
|
7
|
+
// ATV.js: Actions, Targets, Values
|
8
|
+
//
|
9
|
+
// Super vanilla JS / Lightweight alternative to stimulus without "class"
|
10
|
+
// overhead and binding nonsense, just the actions, targets, and values
|
11
|
+
// Also more forgiving with hyphens and underscores.
|
12
|
+
|
13
|
+
// The MIT License (MIT)
|
14
|
+
|
15
|
+
// Copyright (c) 2024 Timothy Breitkreutz
|
16
|
+
|
17
|
+
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
18
|
+
// of this software and associated documentation files (the "Software"), to deal
|
19
|
+
// in the Software without restriction, including without limitation the rights
|
20
|
+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
21
|
+
// copies of the Software, and to permit persons to whom the Software is
|
22
|
+
// furnished to do so, subject to the following conditions:
|
23
|
+
|
24
|
+
// The above copyright notice and this permission notice shall be included in
|
25
|
+
// all copies or substantial portions of the Software.
|
26
|
+
|
27
|
+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
28
|
+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
29
|
+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
30
|
+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
31
|
+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
32
|
+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
33
|
+
// THE SOFTWARE.
|
34
|
+
|
35
|
+
const version = "0.1.9";
|
36
|
+
|
37
|
+
// To dynamically load up the ATV javascripts if needed
|
38
|
+
function importMap() {
|
39
|
+
return JSON.parse(
|
40
|
+
document.querySelector("script[type='importmap']").innerText
|
41
|
+
).imports;
|
42
|
+
}
|
43
|
+
|
44
|
+
/* ----------------- HELPER FUNCTIONS ------------------ */
|
45
|
+
|
46
|
+
/* Variant in the context of ATV means either dash-case or snake_case */
|
47
|
+
const variantPattern = /[\-_]/;
|
48
|
+
|
49
|
+
function pascalize(string) {
|
50
|
+
return string
|
51
|
+
.split(variantPattern)
|
52
|
+
.map((str) => str.charAt(0).toUpperCase() + str.slice(1))
|
53
|
+
.join("");
|
54
|
+
}
|
55
|
+
|
56
|
+
function dasherize(string) {
|
57
|
+
return string.replace(/_/g, "-");
|
58
|
+
}
|
59
|
+
|
60
|
+
const deCommaPattern = /,[\s+]/;
|
61
|
+
|
62
|
+
/* The following methods returns combinations of the given list
|
63
|
+
* connected with dashes and underscores. For many examples see
|
64
|
+
* test/system/unit_test.rb
|
65
|
+
*/
|
66
|
+
function allVariants(...words) {
|
67
|
+
const parts = words.filter((arg) => Boolean(arg));
|
68
|
+
if (parts.length === 0) {
|
69
|
+
return [];
|
70
|
+
}
|
71
|
+
return dashVariants(...parts).flatMap((string) => [string, `${string}s`]);
|
72
|
+
}
|
73
|
+
|
74
|
+
function dashVariants(firstWord, ...words) {
|
75
|
+
function dashOrUnderscore(input) {
|
76
|
+
const string = input || "";
|
77
|
+
if (variantPattern.test(string)) {
|
78
|
+
return [dasherize(string), string.replace(/-/g, "_")];
|
79
|
+
}
|
80
|
+
return [string];
|
81
|
+
}
|
82
|
+
const first = dashOrUnderscore(firstWord);
|
83
|
+
if (words.length < 1) {
|
84
|
+
return first;
|
85
|
+
}
|
86
|
+
return dashVariants(...words).flatMap((str2) =>
|
87
|
+
first.flatMap((str1) => [`${str1}-${str2}`, `${str1}_${str2}`])
|
88
|
+
);
|
89
|
+
}
|
90
|
+
|
91
|
+
/* Returns a list of selectors to use for given name list */
|
92
|
+
function variantSelectors(container, ...words) {
|
93
|
+
return allVariants("data", ...words).flatMap((variant) =>
|
94
|
+
Array.from(container.querySelectorAll(`[${variant}]`)).map((element) => [
|
95
|
+
element,
|
96
|
+
variant
|
97
|
+
])
|
98
|
+
);
|
99
|
+
}
|
100
|
+
|
101
|
+
/* JSON is parsed aggressively and relies on "try" */
|
102
|
+
function errorReport(ex) {
|
103
|
+
if (ex?.message?.includes("JSON")) {
|
104
|
+
return;
|
105
|
+
}
|
106
|
+
console.error(`ATV: ${ex}`);
|
107
|
+
}
|
108
|
+
|
109
|
+
/*
|
110
|
+
* Gets all action declarations for an element, returns an array of structures.
|
111
|
+
*/
|
112
|
+
function actionsFor(prefix, element, onlyEvent = null) {
|
113
|
+
let result = [];
|
114
|
+
const paramSeparator = /[()]/;
|
115
|
+
|
116
|
+
// Parse a single action part, controller is passed in
|
117
|
+
// if derived from the attribute key
|
118
|
+
function parseAction(action, controller) {
|
119
|
+
let method;
|
120
|
+
let parameters = [];
|
121
|
+
let [event, rightSide] = action.split(/[\s]*[\-=]>[\s]*/);
|
122
|
+
|
123
|
+
// Figure out what to do with the part on the right of the "=> blah#blah"
|
124
|
+
if (rightSide) {
|
125
|
+
let [innerController, methodCall] = rightSide.split("#");
|
126
|
+
if (methodCall) {
|
127
|
+
controller = innerController;
|
128
|
+
} else {
|
129
|
+
methodCall = innerController;
|
130
|
+
}
|
131
|
+
method = methodCall;
|
132
|
+
} else {
|
133
|
+
method = event;
|
134
|
+
}
|
135
|
+
const [methodName, params] = method.split(paramSeparator);
|
136
|
+
if (params) {
|
137
|
+
method = methodName;
|
138
|
+
parameters = params.split(deCommaPattern);
|
139
|
+
try {
|
140
|
+
parameters = JSON.parse(`[${params}]`);
|
141
|
+
} catch (ex) {
|
142
|
+
errorReport(ex);
|
143
|
+
}
|
144
|
+
event = event.split("(")[0];
|
145
|
+
}
|
146
|
+
// Sometimes we only care about a given event name passed in.
|
147
|
+
if (onlyEvent && event !== onlyEvent) {
|
148
|
+
return;
|
149
|
+
}
|
150
|
+
if (!controller) {
|
151
|
+
controller = method;
|
152
|
+
method = event;
|
153
|
+
}
|
154
|
+
result.push({
|
155
|
+
controller: dasherize(controller),
|
156
|
+
event,
|
157
|
+
method,
|
158
|
+
parameters
|
159
|
+
});
|
160
|
+
}
|
161
|
+
|
162
|
+
// Split on commas as long as they are not inside parameter lists.
|
163
|
+
function actionSplit(string, callback) {
|
164
|
+
let paramDepth = 0;
|
165
|
+
let accumulator = [];
|
166
|
+
Array.from(string).forEach(function (char) {
|
167
|
+
if (char === "," && paramDepth === 0) {
|
168
|
+
callback(accumulator.join("").trim());
|
169
|
+
accumulator = [];
|
170
|
+
return;
|
171
|
+
}
|
172
|
+
accumulator.push(char);
|
173
|
+
if (char === "(") {
|
174
|
+
paramDepth += 1;
|
175
|
+
} else if (char === ")") {
|
176
|
+
paramDepth -= 1;
|
177
|
+
}
|
178
|
+
});
|
179
|
+
const last = accumulator.join("").trim();
|
180
|
+
if (paramDepth !== 0) {
|
181
|
+
console.error(`badly formed parameters: ${string}`);
|
182
|
+
}
|
183
|
+
if (last) {
|
184
|
+
callback(last);
|
185
|
+
}
|
186
|
+
}
|
187
|
+
|
188
|
+
element.getAttributeNames().forEach(function (name) {
|
189
|
+
const unqualified = new RegExp(`^data[_-]${prefix}[_-]?action[s]?`);
|
190
|
+
const qualified = new RegExp(
|
191
|
+
`^data[_-]${prefix}[_-]?(.*[^-_])[_-]?action[s]?`
|
192
|
+
);
|
193
|
+
|
194
|
+
let controller;
|
195
|
+
let matched = name.match(qualified);
|
196
|
+
if (matched) {
|
197
|
+
controller = matched[1];
|
198
|
+
} else {
|
199
|
+
controller = null;
|
200
|
+
matched = name.match(unqualified);
|
201
|
+
}
|
202
|
+
if (!matched) {
|
203
|
+
return;
|
204
|
+
}
|
205
|
+
let list = element.getAttribute(name);
|
206
|
+
if (list) {
|
207
|
+
try {
|
208
|
+
list = JSON.parse(list).join(",");
|
209
|
+
} catch (ex) {
|
210
|
+
errorReport(ex);
|
211
|
+
}
|
212
|
+
actionSplit(list, (action) => parseAction(action, controller));
|
213
|
+
}
|
214
|
+
});
|
215
|
+
return result;
|
216
|
+
}
|
217
|
+
|
218
|
+
function attributeKeysFor(element, type) {
|
219
|
+
if (!element || !element.getAttributeNames) {
|
220
|
+
return [];
|
221
|
+
}
|
222
|
+
const regex = new RegExp(`${type}s?$`, "i");
|
223
|
+
return element.getAttributeNames().filter((name) => regex.test(name));
|
224
|
+
}
|
225
|
+
|
226
|
+
// Returns a list of attributes for the type (controller, target, action, etc.)
|
227
|
+
function attributesFor(element, type) {
|
228
|
+
return attributeKeysFor(element, type).map((name) =>
|
229
|
+
element.getAttribute(name)
|
230
|
+
);
|
231
|
+
}
|
232
|
+
|
233
|
+
const allControllerNames = new Set();
|
234
|
+
const allTargets = new Map();
|
235
|
+
let allControllers = new Map();
|
236
|
+
|
237
|
+
// The three maps above all have the same structure: prefix
|
238
|
+
// first (atv, etc.), then element, then a Map of those things.
|
239
|
+
function findOrInitalize(map, prefix, element, initial = null) {
|
240
|
+
if (!map.has(prefix)) {
|
241
|
+
map.set(prefix, new Map());
|
242
|
+
}
|
243
|
+
if (!map.get(prefix).has(element)) {
|
244
|
+
map.get(prefix).set(element, initial ?? new Map());
|
245
|
+
}
|
246
|
+
return map.get(prefix).get(element);
|
247
|
+
}
|
248
|
+
|
249
|
+
/* Look for the controllers in the importmap or just try to load them */
|
250
|
+
function withModule(name, callback) {
|
251
|
+
let importmapName = `${name}_atv`;
|
252
|
+
const map = importMap();
|
253
|
+
Object.keys(map).forEach(function (source) {
|
254
|
+
if (dasherize(source).includes(`/${name}-atv`)) {
|
255
|
+
importmapName = map[source]; // There should only be one
|
256
|
+
}
|
257
|
+
});
|
258
|
+
import(importmapName)
|
259
|
+
.then(function (module) {
|
260
|
+
callback(module);
|
261
|
+
})
|
262
|
+
.catch(function (ex) {
|
263
|
+
console.error(`Loading ${importmapName} failed:`, ex);
|
264
|
+
});
|
265
|
+
}
|
266
|
+
|
267
|
+
/* ----------------- Main Activation Function ------------------ */
|
268
|
+
|
269
|
+
function activate(prefix = "atv") {
|
270
|
+
if (allControllers.has(prefix)) {
|
271
|
+
return;
|
272
|
+
}
|
273
|
+
const root = document.body;
|
274
|
+
|
275
|
+
// Provide selector for any controllers given a prefix
|
276
|
+
|
277
|
+
const controllersSelector = allVariants("data", prefix, "controller")
|
278
|
+
.map((selector) => `[${selector}]`)
|
279
|
+
.join(",");
|
280
|
+
|
281
|
+
// To allow for nesting controllers:
|
282
|
+
// skip if it's not the nearest enclosing controller
|
283
|
+
function outOfScope(element, root, name) {
|
284
|
+
if (!element || element.nodeType === "BODY") {
|
285
|
+
return true;
|
286
|
+
}
|
287
|
+
const closestRoot = element.closest(controllersSelector);
|
288
|
+
if (!closestRoot) {
|
289
|
+
return true;
|
290
|
+
}
|
291
|
+
let out = false;
|
292
|
+
attributesFor(closestRoot, "controller").forEach(function (attr) {
|
293
|
+
const list = attr.split(deCommaPattern).map((str) => dasherize(str));
|
294
|
+
if (list.includes(name)) {
|
295
|
+
out = !(closestRoot === root);
|
296
|
+
} else {
|
297
|
+
out = outOfScope(closestRoot.parentNode, root, name);
|
298
|
+
}
|
299
|
+
});
|
300
|
+
return out;
|
301
|
+
}
|
302
|
+
|
303
|
+
// Optional console output mainly for development
|
304
|
+
const quiet = !document.querySelector(`[data-${prefix}-report="true"]`);
|
305
|
+
function report(count, type, action) {
|
306
|
+
if (quiet || count < 1) {
|
307
|
+
return;
|
308
|
+
}
|
309
|
+
console.log(
|
310
|
+
[
|
311
|
+
[
|
312
|
+
"ATV:",
|
313
|
+
`(${prefix})`,
|
314
|
+
type,
|
315
|
+
`[${Number(allControllers.get(prefix)?.size)}]`
|
316
|
+
].join(" "),
|
317
|
+
`${action}: ${count}`,
|
318
|
+
`v${version}`
|
319
|
+
].join(" / ")
|
320
|
+
);
|
321
|
+
}
|
322
|
+
|
323
|
+
/* ----------------- Controller Factory ------------------ */
|
324
|
+
|
325
|
+
function createController(root, name) {
|
326
|
+
let actions;
|
327
|
+
let targets = {};
|
328
|
+
let values = {};
|
329
|
+
allControllerNames.add(name);
|
330
|
+
|
331
|
+
function getActions() {
|
332
|
+
return actions;
|
333
|
+
}
|
334
|
+
|
335
|
+
function registerActions(root) {
|
336
|
+
const controllers = findOrInitalize(allControllers, prefix, root);
|
337
|
+
|
338
|
+
let elements = new Set();
|
339
|
+
function collectElements(item) {
|
340
|
+
const [element] = item;
|
341
|
+
elements.add(element);
|
342
|
+
}
|
343
|
+
variantSelectors(root, prefix, name, "action").forEach(collectElements);
|
344
|
+
variantSelectors(root, prefix, "action").forEach(collectElements);
|
345
|
+
|
346
|
+
// Find each element that has this type of controller
|
347
|
+
Array.from(elements).forEach(function (element) {
|
348
|
+
// Get action definitions
|
349
|
+
const list = actionsFor(prefix, element);
|
350
|
+
// Collect the events
|
351
|
+
const eventNames = new Set(list.map((action) => action.event));
|
352
|
+
// Make one handler for each event type
|
353
|
+
eventNames.forEach(function (eventName) {
|
354
|
+
const firstForEvent = list.find(
|
355
|
+
(action) => action.event === eventName
|
356
|
+
);
|
357
|
+
if (firstForEvent?.controller !== name) {
|
358
|
+
return;
|
359
|
+
}
|
360
|
+
|
361
|
+
function invokeNext(event, actions) {
|
362
|
+
if (actions.length < 1) {
|
363
|
+
return;
|
364
|
+
}
|
365
|
+
const action = actions[0];
|
366
|
+
if (
|
367
|
+
action.event === eventName &&
|
368
|
+
!outOfScope(element, root, action.controller)
|
369
|
+
) {
|
370
|
+
const callbacks = controllers.get(action.controller).getActions();
|
371
|
+
const callback = callbacks[action.method];
|
372
|
+
let result;
|
373
|
+
if (callback) {
|
374
|
+
try {
|
375
|
+
result = callback(event.target, event, action.parameters);
|
376
|
+
} catch (error) {
|
377
|
+
console.error(`ATV ${prefix}: ${eventName}->${name}`, error);
|
378
|
+
}
|
379
|
+
if (result === false) {
|
380
|
+
event.stopPropagation();
|
381
|
+
return;
|
382
|
+
}
|
383
|
+
}
|
384
|
+
}
|
385
|
+
if (actions.length > 1) {
|
386
|
+
return invokeNext(event, actions.slice(1));
|
387
|
+
}
|
388
|
+
}
|
389
|
+
element.addEventListener(eventName, (event) =>
|
390
|
+
invokeNext(event, list)
|
391
|
+
);
|
392
|
+
});
|
393
|
+
});
|
394
|
+
}
|
395
|
+
|
396
|
+
// Update the in-memory controller from the DOM
|
397
|
+
function refresh() {
|
398
|
+
function refreshTargets(root, middle) {
|
399
|
+
const addedTargets = {};
|
400
|
+
function collectionKeys(key) {
|
401
|
+
return [`all${pascalize(key)}`, `${key}s`];
|
402
|
+
}
|
403
|
+
|
404
|
+
variantSelectors(root.parentNode, prefix, name, "target").forEach(
|
405
|
+
function (item) {
|
406
|
+
const [element, variant] = item;
|
407
|
+
element
|
408
|
+
.getAttribute(variant)
|
409
|
+
.split(deCommaPattern)
|
410
|
+
.forEach(function (key) {
|
411
|
+
const [allKey, pluralKey] = collectionKeys(key);
|
412
|
+
if (
|
413
|
+
targets[key] === element ||
|
414
|
+
(targets[allKey] && targets[allKey].includes(element)) ||
|
415
|
+
outOfScope(element, root, name)
|
416
|
+
) {
|
417
|
+
return;
|
418
|
+
}
|
419
|
+
if (targets[allKey]) {
|
420
|
+
targets[allKey].push(element);
|
421
|
+
targets[pluralKey].push(element);
|
422
|
+
} else if (targets[key]) {
|
423
|
+
targets[allKey] = [targets[key], element];
|
424
|
+
targets[pluralKey] = [targets[key], element];
|
425
|
+
// delete targets[key];
|
426
|
+
} else {
|
427
|
+
targets[key] = element;
|
428
|
+
}
|
429
|
+
if (!addedTargets[key]) {
|
430
|
+
addedTargets[key] = [];
|
431
|
+
}
|
432
|
+
addedTargets[key].push(element);
|
433
|
+
});
|
434
|
+
}
|
435
|
+
);
|
436
|
+
middle();
|
437
|
+
// This part needs to happen after the controller "activate".
|
438
|
+
Object.keys(addedTargets).forEach(function (key) {
|
439
|
+
const connectedCallback = actions[`${key}TargetConnected`];
|
440
|
+
if (connectedCallback) {
|
441
|
+
addedTargets[key].forEach(connectedCallback);
|
442
|
+
}
|
443
|
+
const disconnectedCallback = actions[`${key}TargetDisconnected`];
|
444
|
+
if (disconnectedCallback) {
|
445
|
+
addedTargets[key].forEach(function (element) {
|
446
|
+
findOrInitalize(allTargets, prefix, element, []).push(
|
447
|
+
function () {
|
448
|
+
const [allKey, pluralKey] = collectionKeys(key);
|
449
|
+
let index = targets[allKey]?.indexOf(element);
|
450
|
+
if (index) {
|
451
|
+
targets[allKey].splice(index, 1);
|
452
|
+
}
|
453
|
+
index = targets[pluralKey]?.indexOf(element);
|
454
|
+
if (index) {
|
455
|
+
targets[pluralKey].splice(index, 1);
|
456
|
+
}
|
457
|
+
if (targets[key] === element) {
|
458
|
+
if (targets[allKey]) {
|
459
|
+
targets[key] = targets[allKey][0];
|
460
|
+
} else {
|
461
|
+
delete targets[allKey];
|
462
|
+
}
|
463
|
+
}
|
464
|
+
disconnectedCallback(element);
|
465
|
+
}
|
466
|
+
);
|
467
|
+
});
|
468
|
+
}
|
469
|
+
});
|
470
|
+
}
|
471
|
+
|
472
|
+
function refreshValues(element) {
|
473
|
+
allVariants("data", prefix, name, "value").forEach(function (variant) {
|
474
|
+
const data = element.getAttribute(variant);
|
475
|
+
if (!data) {
|
476
|
+
return;
|
477
|
+
}
|
478
|
+
[data, `{${data}}`].forEach(function (json) {
|
479
|
+
try {
|
480
|
+
Object.assign(values, JSON.parse(json));
|
481
|
+
} catch (ex) {
|
482
|
+
errorReport(ex);
|
483
|
+
}
|
484
|
+
});
|
485
|
+
});
|
486
|
+
}
|
487
|
+
|
488
|
+
// Note that with module includes a promise return so this part finishes
|
489
|
+
// asynchronously.
|
490
|
+
withModule(name, function (module) {
|
491
|
+
refreshTargets(root, function () {
|
492
|
+
refreshValues(root);
|
493
|
+
const invoked = module.connect(
|
494
|
+
targets,
|
495
|
+
values,
|
496
|
+
root,
|
497
|
+
controllerBySelectorAndName
|
498
|
+
);
|
499
|
+
// Allow for returning an collection of actions or
|
500
|
+
// a function returning a collection of actions
|
501
|
+
if (typeof invoked === "function") {
|
502
|
+
actions = invoked();
|
503
|
+
} else {
|
504
|
+
actions = invoked;
|
505
|
+
}
|
506
|
+
registerActions(root);
|
507
|
+
});
|
508
|
+
});
|
509
|
+
}
|
510
|
+
|
511
|
+
/** The public controller object */
|
512
|
+
const controller = Object.freeze({
|
513
|
+
getActions,
|
514
|
+
refresh
|
515
|
+
});
|
516
|
+
return controller;
|
517
|
+
}
|
518
|
+
|
519
|
+
function registerControllers(root) {
|
520
|
+
findOrInitalize(allControllers, prefix, root);
|
521
|
+
|
522
|
+
attributesFor(root, "controller").forEach(function (attribute) {
|
523
|
+
attribute.split(deCommaPattern).forEach(function (controllerName) {
|
524
|
+
const name = dasherize(controllerName);
|
525
|
+
const controller =
|
526
|
+
allControllers.get(prefix).get(root).get(name) ||
|
527
|
+
createController(root, name);
|
528
|
+
controller.refresh();
|
529
|
+
allControllers.get(prefix).get(root).set(name, controller);
|
530
|
+
});
|
531
|
+
});
|
532
|
+
}
|
533
|
+
|
534
|
+
function updateControllers(root) {
|
535
|
+
let initialCount = 0;
|
536
|
+
if (allControllers?.has(prefix)) {
|
537
|
+
initialCount = Number(allControllers.get(prefix).size);
|
538
|
+
}
|
539
|
+
const elements = new Set();
|
540
|
+
if (root.matches(controllersSelector)) {
|
541
|
+
elements.add(root);
|
542
|
+
}
|
543
|
+
root
|
544
|
+
.querySelectorAll(controllersSelector)
|
545
|
+
.forEach((element) => elements.add(element));
|
546
|
+
elements.forEach(registerControllers);
|
547
|
+
|
548
|
+
if (allControllers.has(prefix)) {
|
549
|
+
report(
|
550
|
+
allControllers.get(prefix).size - initialCount,
|
551
|
+
"controllers",
|
552
|
+
"found"
|
553
|
+
);
|
554
|
+
}
|
555
|
+
}
|
556
|
+
|
557
|
+
updateControllers(root);
|
558
|
+
|
559
|
+
const observer = new MutationObserver(domWatcher);
|
560
|
+
|
561
|
+
/* --- Provided to client code to talk to other controllers --- */
|
562
|
+
function controllerBySelectorAndName(selector, name, callback) {
|
563
|
+
document.querySelectorAll(selector).forEach(function (element) {
|
564
|
+
let controller = findOrInitalize(allControllers, prefix, element)?.get(
|
565
|
+
name
|
566
|
+
);
|
567
|
+
if (controller) {
|
568
|
+
callback({
|
569
|
+
actions: controller.getActions()
|
570
|
+
});
|
571
|
+
}
|
572
|
+
});
|
573
|
+
}
|
574
|
+
|
575
|
+
/* ------------ React to DOM changes for this prefix --------------- */
|
576
|
+
|
577
|
+
function domWatcher(records, observer) {
|
578
|
+
function cleanup(node) {
|
579
|
+
// Hard reset
|
580
|
+
if (
|
581
|
+
node.nodeName === "BODY" ||
|
582
|
+
node.nodeName === "HTML" ||
|
583
|
+
node.nodeName === "#document"
|
584
|
+
) {
|
585
|
+
observer.disconnect();
|
586
|
+
allControllers = new Map();
|
587
|
+
return;
|
588
|
+
}
|
589
|
+
// Inner DOM reset
|
590
|
+
function cleanTargets(element) {
|
591
|
+
if (element && element.children.length > 0) {
|
592
|
+
Array.from(element.children).forEach(cleanTargets);
|
593
|
+
}
|
594
|
+
const disconnectors = allTargets.get(prefix)?.get(element);
|
595
|
+
if (disconnectors) {
|
596
|
+
disconnectors.forEach((callback) => callback());
|
597
|
+
disconnectors.splice(0, disconnectors.length);
|
598
|
+
}
|
599
|
+
}
|
600
|
+
function cleanActions(element) {
|
601
|
+
const controllers = findOrInitalize(allControllers, prefix, element);
|
602
|
+
if (controllers) {
|
603
|
+
controllers.forEach(function (controller) {
|
604
|
+
const disconnect = controller.getActions().disconnect;
|
605
|
+
if (disconnect) {
|
606
|
+
disconnect();
|
607
|
+
}
|
608
|
+
});
|
609
|
+
allControllers.get(prefix).delete(element);
|
610
|
+
}
|
611
|
+
}
|
612
|
+
cleanTargets(node);
|
613
|
+
node.querySelectorAll(controllersSelector).forEach(cleanActions);
|
614
|
+
cleanActions(node);
|
615
|
+
}
|
616
|
+
|
617
|
+
function controllerFor(element, name) {
|
618
|
+
if (!element || element === document.body) {
|
619
|
+
return;
|
620
|
+
}
|
621
|
+
return (
|
622
|
+
findOrInitalize(allControllers, prefix, element)?.get(name) ||
|
623
|
+
controllerFor(element.parentNode, name)
|
624
|
+
);
|
625
|
+
}
|
626
|
+
|
627
|
+
function updateTargets(element) {
|
628
|
+
Array.from(allControllerNames).forEach(function (name) {
|
629
|
+
controllerFor(element, name)?.refresh();
|
630
|
+
variantSelectors(element, prefix, name, "target").forEach(
|
631
|
+
function (item) {
|
632
|
+
controllerFor(item[0], name)?.refresh();
|
633
|
+
}
|
634
|
+
);
|
635
|
+
});
|
636
|
+
}
|
637
|
+
|
638
|
+
function HTMLElements(node) {
|
639
|
+
return Boolean(node.classList);
|
640
|
+
}
|
641
|
+
records.forEach(function (mutation) {
|
642
|
+
if (mutation.type === "childList") {
|
643
|
+
Array.from(mutation.removedNodes)
|
644
|
+
.filter(HTMLElements)
|
645
|
+
.forEach((node) => cleanup(node));
|
646
|
+
}
|
647
|
+
});
|
648
|
+
records.forEach(function (mutation) {
|
649
|
+
if (mutation.type === "childList") {
|
650
|
+
Array.from(mutation.addedNodes)
|
651
|
+
.filter(HTMLElements)
|
652
|
+
.forEach(function (node) {
|
653
|
+
updateTargets(node);
|
654
|
+
updateControllers(node);
|
655
|
+
});
|
656
|
+
}
|
657
|
+
});
|
658
|
+
}
|
659
|
+
|
660
|
+
const config = { childList: true, subtree: true };
|
661
|
+
observer.observe(document, config);
|
662
|
+
}
|
663
|
+
|
664
|
+
export { activate, version };
|
@@ -0,0 +1,4 @@
|
|
1
|
+
// @sbrew.com/atv@0.1.9 downloaded from https://ga.jspm.io/npm:@sbrew.com/atv@0.1.9/atv.js
|
2
|
+
|
3
|
+
const t="0.1.9";function importMap(){return JSON.parse(document.querySelector("script[type='importmap']").innerText).imports}const e=/[\-_]/;function pascalize(t){return t.split(e).map((t=>t.charAt(0).toUpperCase()+t.slice(1))).join("")}function dasherize(t){return t.replace(/_/g,"-")}const n=/,[\s+]/;function allVariants(...t){const e=t.filter((t=>Boolean(t)));return e.length===0?[]:dashVariants(...e).flatMap((t=>[t,`${t}s`]))}function dashVariants(t,...n){function dashOrUnderscore(t){const n=t||"";return e.test(n)?[dasherize(n),n.replace(/-/g,"_")]:[n]}const r=dashOrUnderscore(t);return n.length<1?r:dashVariants(...n).flatMap((t=>r.flatMap((e=>[`${e}-${t}`,`${e}_${t}`]))))}function variantSelectors(t,...e){return allVariants("data",...e).flatMap((e=>Array.from(t.querySelectorAll(`[${e}]`)).map((t=>[t,e]))))}function errorReport(t){t?.message?.includes("JSON")||console.error(`ATV: ${t}`)}function actionsFor(t,e,r=null){let o=[];const c=/[()]/;function parseAction(t,e){let i;let a=[];let[s,l]=t.split(/[\s]*[\-=]>[\s]*/);if(l){let[t,n]=l.split("#");n?e=t:n=t;i=n}else i=s;const[f,u]=i.split(c);if(u){i=f;a=u.split(n);try{a=JSON.parse(`[${u}]`)}catch(t){errorReport(t)}s=s.split("(")[0]}if(!r||s===r){if(!e){e=i;i=s}o.push({controller:dasherize(e),event:s,method:i,parameters:a})}}function actionSplit(t,e){let n=0;let r=[];Array.from(t).forEach((function(t){if(t!==","||n!==0){r.push(t);t==="("?n+=1:t===")"&&(n-=1)}else{e(r.join("").trim());r=[]}}));const o=r.join("").trim();n!==0&&console.error(`badly formed parameters: ${t}`);o&&e(o)}e.getAttributeNames().forEach((function(n){const r=new RegExp(`^data[_-]${t}[_-]?action[s]?`);const o=new RegExp(`^data[_-]${t}[_-]?(.*[^-_])[_-]?action[s]?`);let c;let i=n.match(o);if(i)c=i[1];else{c=null;i=n.match(r)}if(!i)return;let a=e.getAttribute(n);if(a){try{a=JSON.parse(a).join(",")}catch(t){errorReport(t)}actionSplit(a,(t=>parseAction(t,c)))}}));return o}function attributeKeysFor(t,e){if(!t||!t.getAttributeNames)return[];const n=new RegExp(`${e}s?$`,"i");return t.getAttributeNames().filter((t=>n.test(t)))}function attributesFor(t,e){return attributeKeysFor(t,e).map((e=>t.getAttribute(e)))}const r=new Set;const o=new Map;let c=new Map;function findOrInitalize(t,e,n,r=null){t.has(e)||t.set(e,new Map);t.get(e).has(n)||t.get(e).set(n,r??new Map);return t.get(e).get(n)}function withModule(t,e){let n=`${t}_atv`;const r=importMap();Object.keys(r).forEach((function(e){dasherize(e).includes(`/${t}-atv`)&&(n=r[e])}));import(n).then((function(t){e(t)})).catch((function(t){console.error(`Loading ${n} failed:`,t)}))}function activate(e="atv"){if(c.has(e))return;const i=document.body;const a=allVariants("data",e,"controller").map((t=>`[${t}]`)).join(",");function outOfScope(t,e,r){if(!t||t.nodeType==="BODY")return true;const o=t.closest(a);if(!o)return true;let c=false;attributesFor(o,"controller").forEach((function(t){const i=t.split(n).map((t=>dasherize(t)));c=i.includes(r)?!(o===e):outOfScope(o.parentNode,e,r)}));return c}const s=!document.querySelector(`[data-${e}-report="true"]`);function report(n,r,o){s||n<1||console.log([["ATV:",`(${e})`,r,`[${Number(c.get(e)?.size)}]`].join(" "),`${o}: ${n}`,`v${t}`].join(" / "))}function createController(t,i){let a;let s={};let l={};r.add(i);function getActions(){return a}function registerActions(t){const n=findOrInitalize(c,e,t);let r=new Set;function collectElements(t){const[e]=t;r.add(e)}variantSelectors(t,e,i,"action").forEach(collectElements);variantSelectors(t,e,"action").forEach(collectElements);Array.from(r).forEach((function(r){const o=actionsFor(e,r);const c=new Set(o.map((t=>t.event)));c.forEach((function(c){const a=o.find((t=>t.event===c));a?.controller===i&&r.addEventListener(c,(t=>invokeNext(t,o)));function invokeNext(o,a){if(a.length<1)return;const s=a[0];if(s.event===c&&!outOfScope(r,t,s.controller)){const t=n.get(s.controller).getActions();const r=t[s.method];let a;if(r){try{a=r(o.target,o,s.parameters)}catch(t){console.error(`ATV ${e}: ${c}->${i}`,t)}if(a===false){o.stopPropagation();return}}}return a.length>1?invokeNext(o,a.slice(1)):void 0}}))}))}function refresh(){function refreshTargets(t,r){const c={};function collectionKeys(t){return[`all${pascalize(t)}`,`${t}s`]}variantSelectors(t.parentNode,e,i,"target").forEach((function(e){const[r,o]=e;r.getAttribute(o).split(n).forEach((function(e){const[n,o]=collectionKeys(e);if(!(s[e]===r||s[n]&&s[n].includes(r)||outOfScope(r,t,i))){if(s[n]){s[n].push(r);s[o].push(r)}else if(s[e]){s[n]=[s[e],r];s[o]=[s[e],r]}else s[e]=r;c[e]||(c[e]=[]);c[e].push(r)}}))}));r();Object.keys(c).forEach((function(t){const n=a[`${t}TargetConnected`];n&&c[t].forEach(n);const r=a[`${t}TargetDisconnected`];r&&c[t].forEach((function(n){findOrInitalize(o,e,n,[]).push((function(){const[e,o]=collectionKeys(t);let c=s[e]?.indexOf(n);c&&s[e].splice(c,1);c=s[o]?.indexOf(n);c&&s[o].splice(c,1);s[t]===n&&(s[e]?s[t]=s[e][0]:delete s[e]);r(n)}))}))}))}function refreshValues(t){allVariants("data",e,i,"value").forEach((function(e){const n=t.getAttribute(e);n&&[n,`{${n}}`].forEach((function(t){try{Object.assign(l,JSON.parse(t))}catch(t){errorReport(t)}}))}))}withModule(i,(function(e){refreshTargets(t,(function(){refreshValues(t);const n=e.connect(s,l,t,controllerBySelectorAndName);a=typeof n==="function"?n():n;registerActions(t)}))}))}const f=Object.freeze({getActions:getActions,refresh:refresh});return f}function registerControllers(t){findOrInitalize(c,e,t);attributesFor(t,"controller").forEach((function(r){r.split(n).forEach((function(n){const r=dasherize(n);const o=c.get(e).get(t).get(r)||createController(t,r);o.refresh();c.get(e).get(t).set(r,o)}))}))}function updateControllers(t){let n=0;c?.has(e)&&(n=Number(c.get(e).size));const r=new Set;t.matches(a)&&r.add(t);t.querySelectorAll(a).forEach((t=>r.add(t)));r.forEach(registerControllers);c.has(e)&&report(c.get(e).size-n,"controllers","found")}updateControllers(i);const l=new MutationObserver(domWatcher);function controllerBySelectorAndName(t,n,r){document.querySelectorAll(t).forEach((function(t){let o=findOrInitalize(c,e,t)?.get(n);o&&r({actions:o.getActions()})}))}function domWatcher(t,n){function cleanup(t){if(t.nodeName!=="BODY"&&t.nodeName!=="HTML"&&t.nodeName!=="#document"){cleanTargets(t);t.querySelectorAll(a).forEach(cleanActions);cleanActions(t)}else{n.disconnect();c=new Map}function cleanTargets(t){t&&t.children.length>0&&Array.from(t.children).forEach(cleanTargets);const n=o.get(e)?.get(t);if(n){n.forEach((t=>t()));n.splice(0,n.length)}}function cleanActions(t){const n=findOrInitalize(c,e,t);if(n){n.forEach((function(t){const e=t.getActions().disconnect;e&&e()}));c.get(e).delete(t)}}}function controllerFor(t,n){if(t&&t!==document.body)return findOrInitalize(c,e,t)?.get(n)||controllerFor(t.parentNode,n)}function updateTargets(t){Array.from(r).forEach((function(n){controllerFor(t,n)?.refresh();variantSelectors(t,e,n,"target").forEach((function(t){controllerFor(t[0],n)?.refresh()}))}))}function HTMLElements(t){return Boolean(t.classList)}t.forEach((function(t){t.type==="childList"&&Array.from(t.removedNodes).filter(HTMLElements).forEach((t=>cleanup(t)))}));t.forEach((function(t){t.type==="childList"&&Array.from(t.addedNodes).filter(HTMLElements).forEach((function(t){updateTargets(t);updateControllers(t)}))}))}const f={childList:true,subtree:true};l.observe(document,f)}export{activate,t as version};
|
4
|
+
|
data/lib/atv/engine.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ATV
|
4
|
+
class Engine < ::Rails::Engine
|
5
|
+
JAVASCRIPTS = %w[atv.js atv.min.js].freeze
|
6
|
+
|
7
|
+
initializer "ATV.configure_rails_initialization" do
|
8
|
+
if Rails.application.config.respond_to?(:assets)
|
9
|
+
Rails.application.config.assets.precompile += JAVASCRIPTS
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
data/lib/atv/version.rb
ADDED
data/lib/atv-rails.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
Description:
|
2
|
+
============
|
3
|
+
Generates a new ATV controller at the passed path.
|
4
|
+
|
5
|
+
Examples:
|
6
|
+
=========
|
7
|
+
bin/rails generate atv chat
|
8
|
+
|
9
|
+
creates: app/javascript/controllers/chat_atv.js
|
10
|
+
|
11
|
+
|
12
|
+
bin/rails generate stimulus nested/chat
|
13
|
+
|
14
|
+
creates: app/javascript/controllers/nested/chat_atv.js
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require "rails/generators/named_base"
|
2
|
+
|
3
|
+
class AtvGenerator < Rails::Generators::NamedBase # :nodoc:
|
4
|
+
source_root File.expand_path("templates", __dir__)
|
5
|
+
|
6
|
+
def copy_view_files
|
7
|
+
@attribute = attribute_value(controller_name)
|
8
|
+
template "controller.js", "app/javascript/controllers/#{controller_name}_atv.js"
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
def controller_name
|
13
|
+
name.underscore.gsub(/_atv$/, "")
|
14
|
+
end
|
15
|
+
|
16
|
+
def attribute_value(controller_name)
|
17
|
+
controller_name.gsub(/\//, "--").gsub("_", "-")
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
function connect(targets, values, element, controllers) {
|
2
|
+
return (function() {
|
3
|
+
// Object contains named methods plus disconnect, and target backs.
|
4
|
+
return {
|
5
|
+
click: function(actor, event, params) {
|
6
|
+
},
|
7
|
+
disconnect: function() {
|
8
|
+
}
|
9
|
+
}
|
10
|
+
});
|
11
|
+
};
|
12
|
+
|
13
|
+
export { connect };
|
data/lib/tasks/atv.rake
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
namespace :atv do
|
2
|
+
desc "Install ATV into this rails app"
|
3
|
+
task :install do
|
4
|
+
importmap_file = "config/importmap.rb"
|
5
|
+
pin_command = %(pin "@sbrew.com/atv", to: "atv.min.js")
|
6
|
+
installed = false
|
7
|
+
|
8
|
+
# Add importmap
|
9
|
+
if File.exist?(importmap_file)
|
10
|
+
if File.read(importmap_file).include?(pin_command)
|
11
|
+
puts "Importmap already pinned, no change"
|
12
|
+
installed = true
|
13
|
+
end
|
14
|
+
end
|
15
|
+
unless installed
|
16
|
+
File.open(importmap_file, "a") do |file|
|
17
|
+
file.puts(pin_command)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Add to application.js
|
22
|
+
application_js_file = "app/javascript/application.js"
|
23
|
+
included = false
|
24
|
+
|
25
|
+
if File.exist?(application_js_file)
|
26
|
+
if File.read(application_js_file).match?(/import.*activate.*sbrew.*atv/)
|
27
|
+
puts "Javascript already included, no change"
|
28
|
+
included = true
|
29
|
+
end
|
30
|
+
end
|
31
|
+
unless included
|
32
|
+
File.open(application_js_file, "a") do |file|
|
33
|
+
file.puts %[import { activate } from "@sbrew.com/atv"; activate("atv");]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
metadata
CHANGED
@@ -1,30 +1,58 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: atv-rails
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Timothy Breitkreutz
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2025-01-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
14
|
+
name: railties
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version:
|
19
|
+
version: 7.0.0
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version:
|
27
|
-
|
26
|
+
version: 7.0.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: stimulus-rails
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 1.3.0
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 1.3.0
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: importmap-rails
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 2.0.0
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 2.0.0
|
55
|
+
description:
|
28
56
|
email:
|
29
57
|
- tim@sbrew.com
|
30
58
|
executables: []
|
@@ -34,10 +62,15 @@ files:
|
|
34
62
|
- MIT-LICENSE
|
35
63
|
- README.md
|
36
64
|
- Rakefile
|
37
|
-
-
|
38
|
-
-
|
39
|
-
- lib/atv
|
40
|
-
- lib/
|
65
|
+
- app/assets/javascripts/atv.js
|
66
|
+
- app/assets/javascripts/atv.min.js
|
67
|
+
- lib/atv-rails.rb
|
68
|
+
- lib/atv/engine.rb
|
69
|
+
- lib/atv/version.rb
|
70
|
+
- lib/generators/atv/USAGE
|
71
|
+
- lib/generators/atv/atv_generator.rb
|
72
|
+
- lib/generators/atv/templates/controller.js.tt
|
73
|
+
- lib/tasks/atv.rake
|
41
74
|
homepage: https://www.sbrew.com/atv
|
42
75
|
licenses:
|
43
76
|
- MIT
|
@@ -63,5 +96,5 @@ requirements: []
|
|
63
96
|
rubygems_version: 3.5.22
|
64
97
|
signing_key:
|
65
98
|
specification_version: 4
|
66
|
-
summary: Functional JavaScript for Rails
|
99
|
+
summary: Functional JavaScript for Rails.
|
67
100
|
test_files: []
|
data/lib/atv/rails/railtie.rb
DELETED
data/lib/atv/rails/version.rb
DELETED
data/lib/atv/rails.rb
DELETED