atv-rails 0.0.1 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/MIT-LICENSE +20 -0
- data/README.md +28 -0
- data/Rakefile +3 -0
- data/app/assets/atv-min.js +1 -0
- data/app/assets/javascripts/atv.js +659 -0
- data/lib/atv/rails/railtie.rb +6 -0
- data/lib/atv/rails/version.rb +5 -0
- data/lib/atv/rails.rb +8 -0
- data/lib/install/importmap.rb +8 -0
- data/lib/tasks/atv/rails_tasks.rake +4 -0
- metadata +38 -11
- data/lib/atv_helpers.rb +0 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 62607ef7dc7cbea06ec251592fe5fe9f0c76921573059a61c4f1bf5f1d40dac1
|
4
|
+
data.tar.gz: ae1c07fdebafe198fc55a6154b9d3c23124142371490acceec49c15ad495fdb8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0a80e9a1de4c5bbd1d9e01af0b1778adadedc7b8ddc51b6d8b8c4e900c6e4a443ef9f62edf25b03ba9ba557b6fedd4017dfbe102623d997ab3c1aad73b2d451f
|
7
|
+
data.tar.gz: 159a2ad890ce7bf69260b053a5d671c24a0395a9d576109f9c744984077bdcfb2726619f633ffc987420b028d60015cd0b87c50973884aa86a2ddf3e976ab0b9
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright Timothy Breitkreutz
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# ATV::Rails
|
2
|
+
Short description and motivation.
|
3
|
+
|
4
|
+
## Usage
|
5
|
+
How to use my plugin.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
Add this line to your application's Gemfile:
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
gem "atv-rails"
|
12
|
+
```
|
13
|
+
|
14
|
+
And then execute:
|
15
|
+
```bash
|
16
|
+
$ bundle
|
17
|
+
```
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
```bash
|
21
|
+
$ gem install atv-rails
|
22
|
+
```
|
23
|
+
|
24
|
+
## Contributing
|
25
|
+
Contribution directions go here.
|
26
|
+
|
27
|
+
## License
|
28
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -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 };
|
data/lib/atv/rails.rb
ADDED
@@ -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,26 +1,53 @@
|
|
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.3
|
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
|
-
dependencies:
|
13
|
-
|
14
|
-
|
11
|
+
date: 2024-12-23 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: railties
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 6.0.1
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 6.0.1
|
27
|
+
description:
|
28
|
+
email:
|
29
|
+
- tim@sbrew.com
|
15
30
|
executables: []
|
16
31
|
extensions: []
|
17
32
|
extra_rdoc_files: []
|
18
33
|
files:
|
19
|
-
-
|
20
|
-
|
34
|
+
- MIT-LICENSE
|
35
|
+
- README.md
|
36
|
+
- Rakefile
|
37
|
+
- app/assets/atv-min.js
|
38
|
+
- app/assets/javascripts/atv.js
|
39
|
+
- lib/atv/rails.rb
|
40
|
+
- lib/atv/rails/railtie.rb
|
41
|
+
- lib/atv/rails/version.rb
|
42
|
+
- lib/install/importmap.rb
|
43
|
+
- lib/tasks/atv/rails_tasks.rake
|
44
|
+
homepage: https://www.sbrew.com/atv
|
21
45
|
licenses:
|
22
46
|
- MIT
|
23
|
-
metadata:
|
47
|
+
metadata:
|
48
|
+
homepage_uri: https://www.sbrew.com/atv
|
49
|
+
source_code_uri: https://github.com/timbreitkreutz/atv-rails
|
50
|
+
changelog_uri: https://github.com/timbreitkreutz/atv-rails/blob/main/CHANGELOG.md
|
24
51
|
post_install_message:
|
25
52
|
rdoc_options: []
|
26
53
|
require_paths:
|
@@ -36,8 +63,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
36
63
|
- !ruby/object:Gem::Version
|
37
64
|
version: '0'
|
38
65
|
requirements: []
|
39
|
-
rubygems_version: 3.5.
|
66
|
+
rubygems_version: 3.5.22
|
40
67
|
signing_key:
|
41
68
|
specification_version: 4
|
42
|
-
summary:
|
69
|
+
summary: Functional JavaScript for Rails.
|
43
70
|
test_files: []
|