atv-rails 0.0.2 → 0.0.3

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: 62607ef7dc7cbea06ec251592fe5fe9f0c76921573059a61c4f1bf5f1d40dac1
4
+ data.tar.gz: ae1c07fdebafe198fc55a6154b9d3c23124142371490acceec49c15ad495fdb8
5
5
  SHA512:
6
- metadata.gz: b744f9784980219fb4b2094b7b79c53509224cc10e4c4e4c5a244a4a6621d6ef3a597f8f784a10e775c7c43798761f9fce4d4dd0bc781d4e9fd359a87f8ce441
7
- data.tar.gz: dad98bc4ce0092bcc113eb6f0a0fed8d6405fd3b6a98af76222f4013aaf6c8a61a06339d3fa546371db811f9052d1002d56cf6a25a7001cafcb5b0396dd98d8c
6
+ metadata.gz: 0a80e9a1de4c5bbd1d9e01af0b1778adadedc7b8ddc51b6d8b8c4e900c6e4a443ef9f62edf25b03ba9ba557b6fedd4017dfbe102623d997ab3c1aad73b2d451f
7
+ data.tar.gz: 159a2ad890ce7bf69260b053a5d671c24a0395a9d576109f9c744984077bdcfb2726619f633ffc987420b028d60015cd0b87c50973884aa86a2ddf3e976ab0b9
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Atv::Rails
1
+ # ATV::Rails
2
2
  Short description and motivation.
3
3
 
4
4
  ## Usage
@@ -0,0 +1 @@
1
+ const t="0.1.8";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;const c=new Map;let i=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(i.has(e))return;const a=document.body;const s=allVariants("data",e,"controller").map((t=>`[${t}]`)).join(",");function outOfScope(t,e,r){if(!t||t.nodeType==="BODY")return true;const o=t.closest(s);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 l=!document.querySelector(`[data-${e}-report="true"]`);function report(n,r,o){l||n<1||console.log([["ATV:",`(${e})`,r,`[${Number(i.get(e)?.size)}]`].join(" "),`${o}: ${n}`,`v${t}`].join(" / "))}function createController(t,a){let s;let l={};let f={};r.add(a);function getActions(){return s}function registerActions(t){const n=findOrInitalize(i,e,t);let r=new Set;function collectElements(t){const[e]=t;r.add(e)}variantSelectors(t,e,a,"action").forEach(collectElements);variantSelectors(t,e,"action").forEach(collectElements);Array.from(r).forEach((function(r){const c=actionsFor(e,r);const i=new Set(c.map((t=>t.event)));i.forEach((function(i){const s=c.find((t=>t.event===i));if(s?.controller!==a)return;function invokeNext(e,o){if(o.length<1)return;const c=o[0];if(c.event===i&&!outOfScope(r,t,c.controller)){const t=n.get(c.controller).getActions();const r=t[c.method];if(r){const t=r(e.target,e,c.parameters);if(t===false)return}}return o.length>1?invokeNext(e,o.slice(1)):void 0}const handler=t=>invokeNext(t,c);const l=findOrInitalize(o,e,r);if(!l.get(i)){r.addEventListener(i,handler);l.set(i,handler)}}))}))}function refresh(){function refreshTargets(t,r){const o={};function collectionKeys(t){return[`all${pascalize(t)}`,`${t}s`]}variantSelectors(t.parentNode,e,a,"target").forEach((function(e){const[r,c]=e;r.getAttribute(c).split(n).forEach((function(e){const[n,c]=collectionKeys(e);if(!(l[e]===r||l[n]&&l[n].includes(r)||outOfScope(r,t,a))){if(l[n]){l[n].push(r);l[c].push(r)}else if(l[e]){l[n]=[l[e],r];l[c]=[l[e],r]}else l[e]=r;o[e]||(o[e]=[]);o[e].push(r)}}))}));r();Object.keys(o).forEach((function(t){const n=s[`${t}TargetConnected`];n&&o[t].forEach(n);const r=s[`${t}TargetDisconnected`];r&&o[t].forEach((function(n){findOrInitalize(c,e,n,[]).push((function(){const[e,o]=collectionKeys(t);let c=l[e]?.indexOf(n);c&&l[e].splice(c,1);c=l[o]?.indexOf(n);c&&l[o].splice(c,1);l[t]===n&&(l[e]?l[t]=l[e][0]:delete l[e]);r(n)}))}))}))}function refreshValues(t){allVariants("data",e,a,"value").forEach((function(e){const n=t.getAttribute(e);n&&[n,`{${n}}`].forEach((function(t){try{Object.assign(f,JSON.parse(t))}catch(t){errorReport(t)}}))}))}withModule(a,(function(e){refreshTargets(t,(function(){refreshValues(t);const n=e.connect(l,f,t,controllerBySelectorAndName);s=typeof n==="function"?n():n;registerActions(t)}))}))}const u=Object.freeze({getActions:getActions,refresh:refresh});return u}function registerControllers(t){findOrInitalize(i,e,t);attributesFor(t,"controller").forEach((function(r){r.split(n).forEach((function(n){const r=dasherize(n);const o=i.get(e).get(t).get(r)||createController(t,r);o.refresh();i.get(e).get(t).set(r,o)}))}))}function updateControllers(t){let n=Number(i.get(e)?.size);const r=new Set;t.matches(s)&&r.add(t);t.querySelectorAll(s).forEach((t=>r.add(t)));r.forEach(registerControllers);report(i.get(e).size-n,"controllers","found")}updateControllers(a);const f=new MutationObserver(domWatcher);function controllerBySelectorAndName(t,n,r){document.querySelectorAll(t).forEach((function(t){let o=findOrInitalize(i,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(s).forEach(cleanActions);cleanActions(t)}else{n.disconnect();i=new Map}function cleanTargets(t){t&&t.children.length>0&&Array.from(t.children).forEach(cleanTargets);const n=c.get(e)?.get(t);if(n){n.forEach((t=>t()));n.splice(0,n.length)}}function cleanActions(t){const n=findOrInitalize(i,e,t);if(n){n.forEach((function(t){const e=t.getActions().disconnect;e&&e()}));i.get(e).delete(t)}}}function controllerFor(t,n){if(t&&t!==document.body)return findOrInitalize(i,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 u={childList:true,subtree:true};f.observe(document,u)}export{activate,t as version};
@@ -0,0 +1,659 @@
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.8";
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 allEventListeners = new Map();
235
+ const allTargets = new Map();
236
+ let allControllers = new Map();
237
+
238
+ // The three maps above all have the same structure: prefix
239
+ // first (atv, etc.), then element, then a Map of those things.
240
+ function findOrInitalize(map, prefix, element, initial = null) {
241
+ if (!map.has(prefix)) {
242
+ map.set(prefix, new Map());
243
+ }
244
+ if (!map.get(prefix).has(element)) {
245
+ map.get(prefix).set(element, initial ?? new Map());
246
+ }
247
+ return map.get(prefix).get(element);
248
+ }
249
+
250
+ /* Look for the controllers in the importmap or just try to load them */
251
+ function withModule(name, callback) {
252
+ let importmapName = `${name}_atv`;
253
+ const map = importMap();
254
+ Object.keys(map).forEach(function (source) {
255
+ if (dasherize(source).includes(`/${name}-atv`)) {
256
+ importmapName = map[source]; // There should only be one
257
+ }
258
+ });
259
+ import(importmapName)
260
+ .then(function (module) {
261
+ callback(module);
262
+ })
263
+ .catch(function (ex) {
264
+ console.error(`Loading ${importmapName} failed:`, ex);
265
+ });
266
+ }
267
+
268
+ /* ----------------- Main Activation Function ------------------ */
269
+
270
+ function activate(prefix = "atv") {
271
+ if (allControllers.has(prefix)) {
272
+ return;
273
+ }
274
+ const root = document.body;
275
+
276
+ // Provide selector for any controllers given a prefix
277
+
278
+ const controllersSelector = allVariants("data", prefix, "controller")
279
+ .map((selector) => `[${selector}]`)
280
+ .join(",");
281
+
282
+ // To allow for nesting controllers:
283
+ // skip if it's not the nearest enclosing controller
284
+ function outOfScope(element, root, name) {
285
+ if (!element || element.nodeType === "BODY") {
286
+ return true;
287
+ }
288
+ const closestRoot = element.closest(controllersSelector);
289
+ if (!closestRoot) {
290
+ return true;
291
+ }
292
+ let out = false;
293
+ attributesFor(closestRoot, "controller").forEach(function (attr) {
294
+ const list = attr.split(deCommaPattern).map((str) => dasherize(str));
295
+ if (list.includes(name)) {
296
+ out = !(closestRoot === root);
297
+ } else {
298
+ out = outOfScope(closestRoot.parentNode, root, name);
299
+ }
300
+ });
301
+ return out;
302
+ }
303
+
304
+ // Optional console output mainly for development
305
+ const quiet = !document.querySelector(`[data-${prefix}-report="true"]`);
306
+ function report(count, type, action) {
307
+ if (quiet || count < 1) {
308
+ return;
309
+ }
310
+ console.log(
311
+ [
312
+ [
313
+ "ATV:",
314
+ `(${prefix})`,
315
+ type,
316
+ `[${Number(allControllers.get(prefix)?.size)}]`
317
+ ].join(" "),
318
+ `${action}: ${count}`,
319
+ `v${version}`
320
+ ].join(" / ")
321
+ );
322
+ }
323
+
324
+ /* ----------------- Controller Factory ------------------ */
325
+
326
+ function createController(root, name) {
327
+ let actions;
328
+ let targets = {};
329
+ let values = {};
330
+ allControllerNames.add(name);
331
+
332
+ function getActions() {
333
+ return actions;
334
+ }
335
+
336
+ function registerActions(root) {
337
+ const controllers = findOrInitalize(allControllers, prefix, root);
338
+
339
+ let elements = new Set();
340
+ function collectElements(item) {
341
+ const [element] = item;
342
+ elements.add(element);
343
+ }
344
+ variantSelectors(root, prefix, name, "action").forEach(collectElements);
345
+ variantSelectors(root, prefix, "action").forEach(collectElements);
346
+
347
+ // Find each element that has this type of controller
348
+ Array.from(elements).forEach(function (element) {
349
+ // Get action definitions
350
+ const list = actionsFor(prefix, element);
351
+ // Collect the events
352
+ const eventNames = new Set(list.map((action) => action.event));
353
+ // Make one handler for each event type
354
+ eventNames.forEach(function (eventName) {
355
+ const firstForEvent = list.find(
356
+ (action) => action.event === eventName
357
+ );
358
+ if (firstForEvent?.controller !== name) {
359
+ return;
360
+ }
361
+
362
+ function invokeNext(event, actions) {
363
+ if (actions.length < 1) {
364
+ return;
365
+ }
366
+ const action = actions[0];
367
+ if (
368
+ action.event === eventName &&
369
+ !outOfScope(element, root, action.controller)
370
+ ) {
371
+ const callbacks = controllers.get(action.controller).getActions();
372
+ const callback = callbacks[action.method];
373
+ if (callback) {
374
+ const result = callback(event.target, event, action.parameters);
375
+ if (result === false) {
376
+ return;
377
+ }
378
+ }
379
+ }
380
+ if (actions.length > 1) {
381
+ return invokeNext(event, actions.slice(1));
382
+ }
383
+ }
384
+
385
+ const handler = (event) => invokeNext(event, list);
386
+ const events = findOrInitalize(allEventListeners, prefix, element);
387
+ if (events.get(eventName)) {
388
+ return;
389
+ }
390
+ element.addEventListener(eventName, handler);
391
+ events.set(eventName, handler);
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 = Number(allControllers.get(prefix)?.size);
536
+ const elements = new Set();
537
+ if (root.matches(controllersSelector)) {
538
+ elements.add(root);
539
+ }
540
+ root
541
+ .querySelectorAll(controllersSelector)
542
+ .forEach((element) => elements.add(element));
543
+ elements.forEach(registerControllers);
544
+
545
+ report(
546
+ allControllers.get(prefix).size - initialCount,
547
+ "controllers",
548
+ "found"
549
+ );
550
+ }
551
+
552
+ updateControllers(root);
553
+
554
+ const observer = new MutationObserver(domWatcher);
555
+
556
+ /* --- Provided to client code to talk to other controllers --- */
557
+ function controllerBySelectorAndName(selector, name, callback) {
558
+ document.querySelectorAll(selector).forEach(function (element) {
559
+ let controller = findOrInitalize(allControllers, prefix, element)?.get(
560
+ name
561
+ );
562
+ if (controller) {
563
+ callback({
564
+ actions: controller.getActions()
565
+ });
566
+ }
567
+ });
568
+ }
569
+
570
+ /* ------------ React to DOM changes for this prefix --------------- */
571
+
572
+ function domWatcher(records, observer) {
573
+ function cleanup(node) {
574
+ // Hard reset
575
+ if (
576
+ node.nodeName === "BODY" ||
577
+ node.nodeName === "HTML" ||
578
+ node.nodeName === "#document"
579
+ ) {
580
+ observer.disconnect();
581
+ allControllers = new Map();
582
+ return;
583
+ }
584
+ // Inner DOM reset
585
+ function cleanTargets(element) {
586
+ if (element && element.children.length > 0) {
587
+ Array.from(element.children).forEach(cleanTargets);
588
+ }
589
+ const disconnectors = allTargets.get(prefix)?.get(element);
590
+ if (disconnectors) {
591
+ disconnectors.forEach((callback) => callback());
592
+ disconnectors.splice(0, disconnectors.length);
593
+ }
594
+ }
595
+ function cleanActions(element) {
596
+ const controllers = findOrInitalize(allControllers, prefix, element);
597
+ if (controllers) {
598
+ controllers.forEach(function (controller) {
599
+ const disconnect = controller.getActions().disconnect;
600
+ if (disconnect) {
601
+ disconnect();
602
+ }
603
+ });
604
+ allControllers.get(prefix).delete(element);
605
+ }
606
+ }
607
+ cleanTargets(node);
608
+ node.querySelectorAll(controllersSelector).forEach(cleanActions);
609
+ cleanActions(node);
610
+ }
611
+
612
+ function controllerFor(element, name) {
613
+ if (!element || element === document.body) {
614
+ return;
615
+ }
616
+ return (
617
+ findOrInitalize(allControllers, prefix, element)?.get(name) ||
618
+ controllerFor(element.parentNode, name)
619
+ );
620
+ }
621
+
622
+ function updateTargets(element) {
623
+ Array.from(allControllerNames).forEach(function (name) {
624
+ controllerFor(element, name)?.refresh();
625
+ variantSelectors(element, prefix, name, "target").forEach(
626
+ function (item) {
627
+ controllerFor(item[0], name)?.refresh();
628
+ }
629
+ );
630
+ });
631
+ }
632
+
633
+ function HTMLElements(node) {
634
+ return Boolean(node.classList);
635
+ }
636
+ records.forEach(function (mutation) {
637
+ if (mutation.type === "childList") {
638
+ Array.from(mutation.removedNodes)
639
+ .filter(HTMLElements)
640
+ .forEach((node) => cleanup(node));
641
+ }
642
+ });
643
+ records.forEach(function (mutation) {
644
+ if (mutation.type === "childList") {
645
+ Array.from(mutation.addedNodes)
646
+ .filter(HTMLElements)
647
+ .forEach(function (node) {
648
+ updateTargets(node);
649
+ updateControllers(node);
650
+ });
651
+ }
652
+ });
653
+ }
654
+
655
+ const config = { childList: true, subtree: true };
656
+ observer.observe(document, config);
657
+ }
658
+
659
+ export { activate, version };
@@ -1,4 +1,4 @@
1
- module Atv
1
+ module ATV
2
2
  module Rails
3
3
  class Railtie < ::Rails::Railtie
4
4
  end
@@ -1,5 +1,5 @@
1
- module Atv
1
+ module ATV
2
2
  module Rails
3
- VERSION = "0.0.2"
3
+ VERSION = "0.0.3"
4
4
  end
5
5
  end
data/lib/atv/rails.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  require "atv/rails/version"
2
2
  require "atv/rails/railtie"
3
3
 
4
- module Atv
4
+ module ATV
5
5
  module Rails
6
6
  # Your code goes here...
7
7
  end
@@ -0,0 +1,8 @@
1
+ # Assumes stimulus is already installed
2
+
3
+ say "Copying ATV Rails JavaScript"
4
+ copy_file "#{__dir__}/app/javascript/controllers/atv-rails.js", "app/javascript/controllers/atv-rails.js"
5
+
6
+ say "Pin ATV"
7
+ say %(Appending: pin "@sbrew.com/atv")
8
+ append_to_file "config/importmap.rb", %(pin "atv", to: "@sbrew.com--atv.js"\n)
metadata CHANGED
@@ -1,7 +1,7 @@
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.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Timothy Breitkreutz
@@ -11,20 +11,20 @@ cert_chain: []
11
11
  date: 2024-12-23 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: 6.0.1
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: 6.0.1
27
+ description:
28
28
  email:
29
29
  - tim@sbrew.com
30
30
  executables: []
@@ -34,9 +34,12 @@ files:
34
34
  - MIT-LICENSE
35
35
  - README.md
36
36
  - Rakefile
37
+ - app/assets/atv-min.js
38
+ - app/assets/javascripts/atv.js
37
39
  - lib/atv/rails.rb
38
40
  - lib/atv/rails/railtie.rb
39
41
  - lib/atv/rails/version.rb
42
+ - lib/install/importmap.rb
40
43
  - lib/tasks/atv/rails_tasks.rake
41
44
  homepage: https://www.sbrew.com/atv
42
45
  licenses:
@@ -63,5 +66,5 @@ requirements: []
63
66
  rubygems_version: 3.5.22
64
67
  signing_key:
65
68
  specification_version: 4
66
- summary: Functional JavaScript for Rails
69
+ summary: Functional JavaScript for Rails.
67
70
  test_files: []