atv-rails 0.0.2 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 56a16b60a9e77c2d47522fde79c3014f0ebcab6e6e72f418f362d22cdb0e8a83
4
- data.tar.gz: f51f3976b0484469e6c63eb4e13da56d9ff95e6ca5317177ebd64eee50c1ad99
3
+ metadata.gz: 90eed2550de006c017a7e04a41aa89ffe9d3d0e34740c9cb6d9fc5c17c418537
4
+ data.tar.gz: d4c9e9bb798ee12cd2eda572d1300deb897fad1eee53bfb949d28cd9f341f2c9
5
5
  SHA512:
6
- metadata.gz: b744f9784980219fb4b2094b7b79c53509224cc10e4c4e4c5a244a4a6621d6ef3a597f8f784a10e775c7c43798761f9fce4d4dd0bc781d4e9fd359a87f8ce441
7
- data.tar.gz: dad98bc4ce0092bcc113eb6f0a0fed8d6405fd3b6a98af76222f4013aaf6c8a61a06339d3fa546371db811f9052d1002d56cf6a25a7001cafcb5b0396dd98d8c
6
+ metadata.gz: 020f55905f8884bb5268f14d82a027a9c06aca7aaa71d652cf869f6d752779093183c489d0161565f575b79b08a496d44fd5a28f6142df4d8ac82ab541637714
7
+ data.tar.gz: fb0b1c821179e7f0a1588288d3be1be9ca1056c42ea65fdeacfb8e1e7617693a3a9c1183e8cfec2683fd17d90c3c0aca11408f2b8dfd7e7cc4b8022e976a36df
data/README.md CHANGED
@@ -1,8 +1,8 @@
1
- # Atv::Rails
2
- Short description and motivation.
1
+ # ATV
2
+ JavaScript for Rails Curmudgeons.
3
3
 
4
4
  ## Usage
5
- How to use my plugin.
5
+ TBD
6
6
 
7
7
  ## Installation
8
8
  Add this line to your application's Gemfile:
@@ -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
@@ -0,0 +1,3 @@
1
+ module ATV
2
+ VERSION = "0.0.4"
3
+ end
data/lib/atv-rails.rb ADDED
@@ -0,0 +1,5 @@
1
+ module ATV
2
+ end
3
+
4
+ require "atv/version"
5
+ require "atv/engine"
@@ -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 };
@@ -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.2
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: 2024-12-23 00:00:00.000000000 Z
11
+ date: 2025-01-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: rails
14
+ name: railties
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 8.0.1
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: 8.0.1
27
- description: Functional alternative to Stimulus.js for Rails
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
- - lib/atv/rails.rb
38
- - lib/atv/rails/railtie.rb
39
- - lib/atv/rails/version.rb
40
- - lib/tasks/atv/rails_tasks.rake
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: []
@@ -1,6 +0,0 @@
1
- module Atv
2
- module Rails
3
- class Railtie < ::Rails::Railtie
4
- end
5
- end
6
- end
@@ -1,5 +0,0 @@
1
- module Atv
2
- module Rails
3
- VERSION = "0.0.2"
4
- end
5
- end
data/lib/atv/rails.rb DELETED
@@ -1,8 +0,0 @@
1
- require "atv/rails/version"
2
- require "atv/rails/railtie"
3
-
4
- module Atv
5
- module Rails
6
- # Your code goes here...
7
- end
8
- end
@@ -1,4 +0,0 @@
1
- # desc "Explaining what the task does"
2
- # task :atv_rails do
3
- # # Task goes here
4
- # end