cable_ready 5.0.0.pre6 → 5.0.0.pre9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +2 -488
- data/Gemfile.lock +38 -102
- data/IMPLEMENTATION.md +93 -0
- data/README.md +44 -11
- data/Rakefile +0 -6
- data/app/assets/javascripts/cable_ready.js +959 -0
- data/app/assets/javascripts/cable_ready.min.js +2 -0
- data/app/assets/javascripts/cable_ready.min.js.map +1 -0
- data/app/assets/javascripts/cable_ready.umd.js +910 -0
- data/app/assets/javascripts/cable_ready.umd.min.js +2 -0
- data/app/assets/javascripts/cable_ready.umd.min.js.map +1 -0
- data/app/helpers/cable_ready_helper.rb +3 -1
- data/app/models/concerns/cable_ready/updatable/collections_registry.rb +1 -1
- data/app/models/concerns/cable_ready/updatable/model_updatable_callbacks.rb +3 -3
- data/app/models/concerns/cable_ready/updatable.rb +56 -11
- data/cable_ready.gemspec +42 -0
- data/lib/cable_ready/channel.rb +12 -2
- data/lib/cable_ready/engine.rb +40 -0
- data/lib/cable_ready/importmap.rb +2 -0
- data/lib/cable_ready/sanity_checker.rb +0 -58
- data/lib/cable_ready/version.rb +1 -1
- data/lib/cable_ready.rb +1 -18
- data/package.json +51 -0
- data/rollup.config.js +75 -0
- data/test/dummy/app/models/dugong.rb +4 -0
- data/test/dummy/db/migrate/20220329222959_create_dugongs.rb +8 -0
- data/test/dummy/db/migrate/20220329230221_create_active_storage_tables.active_storage.rb +36 -0
- data/test/dummy/db/schema.rb +36 -1
- data/test/dummy/test/models/dugong_test.rb +7 -0
- data/test/lib/cable_ready/updatable_test.rb +31 -8
- data/yarn.lock +2817 -0
- metadata +145 -54
@@ -0,0 +1,910 @@
|
|
1
|
+
(function(global, factory) {
|
2
|
+
typeof exports === "object" && typeof module !== "undefined" ? factory(exports, require("morphdom")) : typeof define === "function" && define.amd ? define([ "exports", "morphdom" ], factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self,
|
3
|
+
factory(global.CableReady = {}, global.morphdom));
|
4
|
+
})(this, (function(exports, morphdom) {
|
5
|
+
"use strict";
|
6
|
+
function _interopDefaultLegacy(e) {
|
7
|
+
return e && typeof e === "object" && "default" in e ? e : {
|
8
|
+
default: e
|
9
|
+
};
|
10
|
+
}
|
11
|
+
var morphdom__default = _interopDefaultLegacy(morphdom);
|
12
|
+
var name = "cable_ready";
|
13
|
+
var version = "5.0.0-pre9";
|
14
|
+
var description = "CableReady helps you create great real-time user experiences by making it simple to trigger client-side DOM changes from server-side Ruby.";
|
15
|
+
var keywords = [ "ruby", "rails", "websockets", "actioncable", "cable", "ssr", "stimulus_reflex", "client-side", "dom" ];
|
16
|
+
var homepage = "https://cableready.stimulusreflex.com/";
|
17
|
+
var bugs = {
|
18
|
+
url: "https://github.com/stimulusreflex/cable_ready/issues"
|
19
|
+
};
|
20
|
+
var repository = {
|
21
|
+
type: "git",
|
22
|
+
url: "git+https://github.com:stimulusreflex/cable_ready.git"
|
23
|
+
};
|
24
|
+
var license = "MIT";
|
25
|
+
var author = "Nathan Hopkins <natehop@gmail.com>";
|
26
|
+
var main = "./dist/cable_ready.umd.min.js";
|
27
|
+
var module = "./dist/cable_ready.min.js";
|
28
|
+
var files = [ "dist/*", "javascript/*" ];
|
29
|
+
var scripts = {
|
30
|
+
lint: "yarn run prettier-standard:check",
|
31
|
+
format: "yarn run prettier-standard:format",
|
32
|
+
"prettier-standard:check": "yarn run prettier-standard --check ./javascript/**/*.js rollup.config.js",
|
33
|
+
"prettier-standard:format": "yarn run prettier-standard ./javascript/**/*.js rollup.config.js",
|
34
|
+
build: "yarn rollup -c",
|
35
|
+
watch: "yarn rollup -wc"
|
36
|
+
};
|
37
|
+
var dependencies = {
|
38
|
+
morphdom: "^2.6.1"
|
39
|
+
};
|
40
|
+
var devDependencies = {
|
41
|
+
"@rollup/plugin-commonjs": "^21.0.3",
|
42
|
+
"@rollup/plugin-json": "^4.1.0",
|
43
|
+
"@rollup/plugin-node-resolve": "^13.1.3",
|
44
|
+
"prettier-standard": "^16.4.1",
|
45
|
+
rollup: "^2.70.1",
|
46
|
+
"rollup-plugin-terser": "^7.0.2"
|
47
|
+
};
|
48
|
+
var packageInfo = {
|
49
|
+
name: name,
|
50
|
+
version: version,
|
51
|
+
description: description,
|
52
|
+
keywords: keywords,
|
53
|
+
homepage: homepage,
|
54
|
+
bugs: bugs,
|
55
|
+
repository: repository,
|
56
|
+
license: license,
|
57
|
+
author: author,
|
58
|
+
main: main,
|
59
|
+
module: module,
|
60
|
+
files: files,
|
61
|
+
scripts: scripts,
|
62
|
+
dependencies: dependencies,
|
63
|
+
devDependencies: devDependencies
|
64
|
+
};
|
65
|
+
const inputTags = {
|
66
|
+
INPUT: true,
|
67
|
+
TEXTAREA: true,
|
68
|
+
SELECT: true
|
69
|
+
};
|
70
|
+
const mutableTags = {
|
71
|
+
INPUT: true,
|
72
|
+
TEXTAREA: true,
|
73
|
+
OPTION: true
|
74
|
+
};
|
75
|
+
const textInputTypes = {
|
76
|
+
"datetime-local": true,
|
77
|
+
"select-multiple": true,
|
78
|
+
"select-one": true,
|
79
|
+
color: true,
|
80
|
+
date: true,
|
81
|
+
datetime: true,
|
82
|
+
email: true,
|
83
|
+
month: true,
|
84
|
+
number: true,
|
85
|
+
password: true,
|
86
|
+
range: true,
|
87
|
+
search: true,
|
88
|
+
tel: true,
|
89
|
+
text: true,
|
90
|
+
textarea: true,
|
91
|
+
time: true,
|
92
|
+
url: true,
|
93
|
+
week: true
|
94
|
+
};
|
95
|
+
let activeElement;
|
96
|
+
var ActiveElement = {
|
97
|
+
get element() {
|
98
|
+
return activeElement;
|
99
|
+
},
|
100
|
+
set(element) {
|
101
|
+
activeElement = element;
|
102
|
+
}
|
103
|
+
};
|
104
|
+
const isTextInput = element => inputTags[element.tagName] && textInputTypes[element.type];
|
105
|
+
const assignFocus = selector => {
|
106
|
+
const element = selector && selector.nodeType === Node.ELEMENT_NODE ? selector : document.querySelector(selector);
|
107
|
+
const focusElement = element || ActiveElement.element;
|
108
|
+
if (focusElement && focusElement.focus) focusElement.focus();
|
109
|
+
};
|
110
|
+
const dispatch = (element, name, detail = {}) => {
|
111
|
+
const init = {
|
112
|
+
bubbles: true,
|
113
|
+
cancelable: true,
|
114
|
+
detail: detail
|
115
|
+
};
|
116
|
+
const evt = new CustomEvent(name, init);
|
117
|
+
element.dispatchEvent(evt);
|
118
|
+
if (window.jQuery) window.jQuery(element).trigger(name, detail);
|
119
|
+
};
|
120
|
+
const xpathToElement = xpath => document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
|
121
|
+
const getClassNames = names => Array(names).flat();
|
122
|
+
const processElements = (operation, callback) => {
|
123
|
+
Array.from(operation.selectAll ? operation.element : [ operation.element ]).forEach(callback);
|
124
|
+
};
|
125
|
+
const kebabize = str => str.split("").map(((letter, idx) => letter.toUpperCase() === letter ? `${idx !== 0 ? "-" : ""}${letter.toLowerCase()}` : letter)).join("");
|
126
|
+
const operate = (operation, callback) => {
|
127
|
+
if (!operation.cancel) {
|
128
|
+
operation.delay ? setTimeout(callback, operation.delay) : callback();
|
129
|
+
return true;
|
130
|
+
}
|
131
|
+
return false;
|
132
|
+
};
|
133
|
+
const before = (target, operation) => dispatch(target, `cable-ready:before-${kebabize(operation.operation)}`, operation);
|
134
|
+
const after = (target, operation) => dispatch(target, `cable-ready:after-${kebabize(operation.operation)}`, operation);
|
135
|
+
function debounce(func, timeout) {
|
136
|
+
let timer;
|
137
|
+
return (...args) => {
|
138
|
+
clearTimeout(timer);
|
139
|
+
timer = setTimeout((() => func.apply(this, args)), timeout);
|
140
|
+
};
|
141
|
+
}
|
142
|
+
function handleErrors(response) {
|
143
|
+
if (!response.ok) throw Error(response.statusText);
|
144
|
+
return response;
|
145
|
+
}
|
146
|
+
async function graciouslyFetch(url, additionalHeaders) {
|
147
|
+
try {
|
148
|
+
const response = await fetch(url, {
|
149
|
+
headers: {
|
150
|
+
"X-REQUESTED-WITH": "XmlHttpRequest",
|
151
|
+
...additionalHeaders
|
152
|
+
}
|
153
|
+
});
|
154
|
+
if (response == undefined) return;
|
155
|
+
handleErrors(response);
|
156
|
+
return response;
|
157
|
+
} catch (e) {
|
158
|
+
console.error(`Could not fetch ${url}`);
|
159
|
+
}
|
160
|
+
}
|
161
|
+
var utils = Object.freeze({
|
162
|
+
__proto__: null,
|
163
|
+
isTextInput: isTextInput,
|
164
|
+
assignFocus: assignFocus,
|
165
|
+
dispatch: dispatch,
|
166
|
+
xpathToElement: xpathToElement,
|
167
|
+
getClassNames: getClassNames,
|
168
|
+
processElements: processElements,
|
169
|
+
operate: operate,
|
170
|
+
before: before,
|
171
|
+
after: after,
|
172
|
+
debounce: debounce,
|
173
|
+
handleErrors: handleErrors,
|
174
|
+
graciouslyFetch: graciouslyFetch,
|
175
|
+
kebabize: kebabize
|
176
|
+
});
|
177
|
+
const shouldMorph = operation => (fromEl, toEl) => !shouldMorphCallbacks.map((callback => typeof callback === "function" ? callback(operation, fromEl, toEl) : true)).includes(false);
|
178
|
+
const didMorph = operation => el => {
|
179
|
+
didMorphCallbacks.forEach((callback => {
|
180
|
+
if (typeof callback === "function") callback(operation, el);
|
181
|
+
}));
|
182
|
+
};
|
183
|
+
const verifyNotMutable = (detail, fromEl, toEl) => {
|
184
|
+
if (!mutableTags[fromEl.tagName] && fromEl.isEqualNode(toEl)) return false;
|
185
|
+
return true;
|
186
|
+
};
|
187
|
+
const verifyNotContentEditable = (detail, fromEl, toEl) => {
|
188
|
+
if (fromEl === ActiveElement.element && fromEl.isContentEditable) return false;
|
189
|
+
return true;
|
190
|
+
};
|
191
|
+
const verifyNotPermanent = (detail, fromEl, toEl) => {
|
192
|
+
const {permanentAttributeName: permanentAttributeName} = detail;
|
193
|
+
if (!permanentAttributeName) return true;
|
194
|
+
const permanent = fromEl.closest(`[${permanentAttributeName}]`);
|
195
|
+
if (!permanent && fromEl === ActiveElement.element && isTextInput(fromEl)) {
|
196
|
+
const ignore = {
|
197
|
+
value: true
|
198
|
+
};
|
199
|
+
Array.from(toEl.attributes).forEach((attribute => {
|
200
|
+
if (!ignore[attribute.name]) fromEl.setAttribute(attribute.name, attribute.value);
|
201
|
+
}));
|
202
|
+
return false;
|
203
|
+
}
|
204
|
+
return !permanent;
|
205
|
+
};
|
206
|
+
const shouldMorphCallbacks = [ verifyNotMutable, verifyNotPermanent, verifyNotContentEditable ];
|
207
|
+
const didMorphCallbacks = [];
|
208
|
+
var morph_callbacks = Object.freeze({
|
209
|
+
__proto__: null,
|
210
|
+
shouldMorphCallbacks: shouldMorphCallbacks,
|
211
|
+
didMorphCallbacks: didMorphCallbacks,
|
212
|
+
shouldMorph: shouldMorph,
|
213
|
+
didMorph: didMorph,
|
214
|
+
verifyNotMutable: verifyNotMutable,
|
215
|
+
verifyNotContentEditable: verifyNotContentEditable,
|
216
|
+
verifyNotPermanent: verifyNotPermanent
|
217
|
+
});
|
218
|
+
var Operations = {
|
219
|
+
append: operation => {
|
220
|
+
processElements(operation, (element => {
|
221
|
+
before(element, operation);
|
222
|
+
operate(operation, (() => {
|
223
|
+
const {html: html, focusSelector: focusSelector} = operation;
|
224
|
+
element.insertAdjacentHTML("beforeend", html || "");
|
225
|
+
assignFocus(focusSelector);
|
226
|
+
}));
|
227
|
+
after(element, operation);
|
228
|
+
}));
|
229
|
+
},
|
230
|
+
graft: operation => {
|
231
|
+
processElements(operation, (element => {
|
232
|
+
before(element, operation);
|
233
|
+
operate(operation, (() => {
|
234
|
+
const {parent: parent, focusSelector: focusSelector} = operation;
|
235
|
+
const parentElement = document.querySelector(parent);
|
236
|
+
if (parentElement) {
|
237
|
+
parentElement.appendChild(element);
|
238
|
+
assignFocus(focusSelector);
|
239
|
+
}
|
240
|
+
}));
|
241
|
+
after(element, operation);
|
242
|
+
}));
|
243
|
+
},
|
244
|
+
innerHtml: operation => {
|
245
|
+
processElements(operation, (element => {
|
246
|
+
before(element, operation);
|
247
|
+
operate(operation, (() => {
|
248
|
+
const {html: html, focusSelector: focusSelector} = operation;
|
249
|
+
element.innerHTML = html || "";
|
250
|
+
assignFocus(focusSelector);
|
251
|
+
}));
|
252
|
+
after(element, operation);
|
253
|
+
}));
|
254
|
+
},
|
255
|
+
insertAdjacentHtml: operation => {
|
256
|
+
processElements(operation, (element => {
|
257
|
+
before(element, operation);
|
258
|
+
operate(operation, (() => {
|
259
|
+
const {html: html, position: position, focusSelector: focusSelector} = operation;
|
260
|
+
element.insertAdjacentHTML(position || "beforeend", html || "");
|
261
|
+
assignFocus(focusSelector);
|
262
|
+
}));
|
263
|
+
after(element, operation);
|
264
|
+
}));
|
265
|
+
},
|
266
|
+
insertAdjacentText: operation => {
|
267
|
+
processElements(operation, (element => {
|
268
|
+
before(element, operation);
|
269
|
+
operate(operation, (() => {
|
270
|
+
const {text: text, position: position, focusSelector: focusSelector} = operation;
|
271
|
+
element.insertAdjacentText(position || "beforeend", text || "");
|
272
|
+
assignFocus(focusSelector);
|
273
|
+
}));
|
274
|
+
after(element, operation);
|
275
|
+
}));
|
276
|
+
},
|
277
|
+
morph: operation => {
|
278
|
+
processElements(operation, (element => {
|
279
|
+
const {html: html} = operation;
|
280
|
+
const template = document.createElement("template");
|
281
|
+
template.innerHTML = String(html).trim();
|
282
|
+
operation.content = template.content;
|
283
|
+
const parent = element.parentElement;
|
284
|
+
const ordinal = Array.from(parent.children).indexOf(element);
|
285
|
+
before(element, operation);
|
286
|
+
operate(operation, (() => {
|
287
|
+
const {childrenOnly: childrenOnly, focusSelector: focusSelector} = operation;
|
288
|
+
morphdom__default["default"](element, childrenOnly ? template.content : template.innerHTML, {
|
289
|
+
childrenOnly: !!childrenOnly,
|
290
|
+
onBeforeElUpdated: shouldMorph(operation),
|
291
|
+
onElUpdated: didMorph(operation)
|
292
|
+
});
|
293
|
+
assignFocus(focusSelector);
|
294
|
+
}));
|
295
|
+
after(parent.children[ordinal], operation);
|
296
|
+
}));
|
297
|
+
},
|
298
|
+
outerHtml: operation => {
|
299
|
+
processElements(operation, (element => {
|
300
|
+
const parent = element.parentElement;
|
301
|
+
const ordinal = Array.from(parent.children).indexOf(element);
|
302
|
+
before(element, operation);
|
303
|
+
operate(operation, (() => {
|
304
|
+
const {html: html, focusSelector: focusSelector} = operation;
|
305
|
+
element.outerHTML = html || "";
|
306
|
+
assignFocus(focusSelector);
|
307
|
+
}));
|
308
|
+
after(parent.children[ordinal], operation);
|
309
|
+
}));
|
310
|
+
},
|
311
|
+
prepend: operation => {
|
312
|
+
processElements(operation, (element => {
|
313
|
+
before(element, operation);
|
314
|
+
operate(operation, (() => {
|
315
|
+
const {html: html, focusSelector: focusSelector} = operation;
|
316
|
+
element.insertAdjacentHTML("afterbegin", html || "");
|
317
|
+
assignFocus(focusSelector);
|
318
|
+
}));
|
319
|
+
after(element, operation);
|
320
|
+
}));
|
321
|
+
},
|
322
|
+
remove: operation => {
|
323
|
+
processElements(operation, (element => {
|
324
|
+
before(element, operation);
|
325
|
+
operate(operation, (() => {
|
326
|
+
const {focusSelector: focusSelector} = operation;
|
327
|
+
element.remove();
|
328
|
+
assignFocus(focusSelector);
|
329
|
+
}));
|
330
|
+
after(document, operation);
|
331
|
+
}));
|
332
|
+
},
|
333
|
+
replace: operation => {
|
334
|
+
processElements(operation, (element => {
|
335
|
+
const parent = element.parentElement;
|
336
|
+
const ordinal = Array.from(parent.children).indexOf(element);
|
337
|
+
before(element, operation);
|
338
|
+
operate(operation, (() => {
|
339
|
+
const {html: html, focusSelector: focusSelector} = operation;
|
340
|
+
element.outerHTML = html || "";
|
341
|
+
assignFocus(focusSelector);
|
342
|
+
}));
|
343
|
+
after(parent.children[ordinal], operation);
|
344
|
+
}));
|
345
|
+
},
|
346
|
+
textContent: operation => {
|
347
|
+
processElements(operation, (element => {
|
348
|
+
before(element, operation);
|
349
|
+
operate(operation, (() => {
|
350
|
+
const {text: text, focusSelector: focusSelector} = operation;
|
351
|
+
element.textContent = text != null ? text : "";
|
352
|
+
assignFocus(focusSelector);
|
353
|
+
}));
|
354
|
+
after(element, operation);
|
355
|
+
}));
|
356
|
+
},
|
357
|
+
addCssClass: operation => {
|
358
|
+
processElements(operation, (element => {
|
359
|
+
before(element, operation);
|
360
|
+
operate(operation, (() => {
|
361
|
+
const {name: name} = operation;
|
362
|
+
element.classList.add(...getClassNames(name || ""));
|
363
|
+
}));
|
364
|
+
after(element, operation);
|
365
|
+
}));
|
366
|
+
},
|
367
|
+
removeAttribute: operation => {
|
368
|
+
processElements(operation, (element => {
|
369
|
+
before(element, operation);
|
370
|
+
operate(operation, (() => {
|
371
|
+
const {name: name} = operation;
|
372
|
+
element.removeAttribute(name);
|
373
|
+
}));
|
374
|
+
after(element, operation);
|
375
|
+
}));
|
376
|
+
},
|
377
|
+
removeCssClass: operation => {
|
378
|
+
processElements(operation, (element => {
|
379
|
+
before(element, operation);
|
380
|
+
operate(operation, (() => {
|
381
|
+
const {name: name} = operation;
|
382
|
+
element.classList.remove(...getClassNames(name));
|
383
|
+
}));
|
384
|
+
after(element, operation);
|
385
|
+
}));
|
386
|
+
},
|
387
|
+
setAttribute: operation => {
|
388
|
+
processElements(operation, (element => {
|
389
|
+
before(element, operation);
|
390
|
+
operate(operation, (() => {
|
391
|
+
const {name: name, value: value} = operation;
|
392
|
+
element.setAttribute(name, value || "");
|
393
|
+
}));
|
394
|
+
after(element, operation);
|
395
|
+
}));
|
396
|
+
},
|
397
|
+
setDatasetProperty: operation => {
|
398
|
+
processElements(operation, (element => {
|
399
|
+
before(element, operation);
|
400
|
+
operate(operation, (() => {
|
401
|
+
const {name: name, value: value} = operation;
|
402
|
+
element.dataset[name] = value || "";
|
403
|
+
}));
|
404
|
+
after(element, operation);
|
405
|
+
}));
|
406
|
+
},
|
407
|
+
setProperty: operation => {
|
408
|
+
processElements(operation, (element => {
|
409
|
+
before(element, operation);
|
410
|
+
operate(operation, (() => {
|
411
|
+
const {name: name, value: value} = operation;
|
412
|
+
if (name in element) element[name] = value || "";
|
413
|
+
}));
|
414
|
+
after(element, operation);
|
415
|
+
}));
|
416
|
+
},
|
417
|
+
setStyle: operation => {
|
418
|
+
processElements(operation, (element => {
|
419
|
+
before(element, operation);
|
420
|
+
operate(operation, (() => {
|
421
|
+
const {name: name, value: value} = operation;
|
422
|
+
element.style[name] = value || "";
|
423
|
+
}));
|
424
|
+
after(element, operation);
|
425
|
+
}));
|
426
|
+
},
|
427
|
+
setStyles: operation => {
|
428
|
+
processElements(operation, (element => {
|
429
|
+
before(element, operation);
|
430
|
+
operate(operation, (() => {
|
431
|
+
const {styles: styles} = operation;
|
432
|
+
for (let [name, value] of Object.entries(styles)) element.style[name] = value || "";
|
433
|
+
}));
|
434
|
+
after(element, operation);
|
435
|
+
}));
|
436
|
+
},
|
437
|
+
setValue: operation => {
|
438
|
+
processElements(operation, (element => {
|
439
|
+
before(element, operation);
|
440
|
+
operate(operation, (() => {
|
441
|
+
const {value: value} = operation;
|
442
|
+
element.value = value || "";
|
443
|
+
}));
|
444
|
+
after(element, operation);
|
445
|
+
}));
|
446
|
+
},
|
447
|
+
dispatchEvent: operation => {
|
448
|
+
processElements(operation, (element => {
|
449
|
+
before(element, operation);
|
450
|
+
operate(operation, (() => {
|
451
|
+
const {name: name, detail: detail} = operation;
|
452
|
+
dispatch(element, name, detail);
|
453
|
+
}));
|
454
|
+
after(element, operation);
|
455
|
+
}));
|
456
|
+
},
|
457
|
+
setMeta: operation => {
|
458
|
+
before(document, operation);
|
459
|
+
operate(operation, (() => {
|
460
|
+
const {name: name, content: content} = operation;
|
461
|
+
let meta = document.head.querySelector(`meta[name='${name}']`);
|
462
|
+
if (!meta) {
|
463
|
+
meta = document.createElement("meta");
|
464
|
+
meta.name = name;
|
465
|
+
document.head.appendChild(meta);
|
466
|
+
}
|
467
|
+
meta.content = content;
|
468
|
+
}));
|
469
|
+
after(document, operation);
|
470
|
+
},
|
471
|
+
clearStorage: operation => {
|
472
|
+
before(document, operation);
|
473
|
+
operate(operation, (() => {
|
474
|
+
const {type: type} = operation;
|
475
|
+
const storage = type === "session" ? sessionStorage : localStorage;
|
476
|
+
storage.clear();
|
477
|
+
}));
|
478
|
+
after(document, operation);
|
479
|
+
},
|
480
|
+
go: operation => {
|
481
|
+
before(window, operation);
|
482
|
+
operate(operation, (() => {
|
483
|
+
const {delta: delta} = operation;
|
484
|
+
history.go(delta);
|
485
|
+
}));
|
486
|
+
after(window, operation);
|
487
|
+
},
|
488
|
+
pushState: operation => {
|
489
|
+
before(window, operation);
|
490
|
+
operate(operation, (() => {
|
491
|
+
const {state: state, title: title, url: url} = operation;
|
492
|
+
history.pushState(state || {}, title || "", url);
|
493
|
+
}));
|
494
|
+
after(window, operation);
|
495
|
+
},
|
496
|
+
redirectTo: operation => {
|
497
|
+
before(window, operation);
|
498
|
+
operate(operation, (() => {
|
499
|
+
let {url: url, action: action} = operation;
|
500
|
+
action = action || "advance";
|
501
|
+
if (window.Turbo) window.Turbo.visit(url, {
|
502
|
+
action: action
|
503
|
+
});
|
504
|
+
if (window.Turbolinks) window.Turbolinks.visit(url, {
|
505
|
+
action: action
|
506
|
+
});
|
507
|
+
if (!window.Turbo && !window.Turbolinks) window.location.href = url;
|
508
|
+
}));
|
509
|
+
after(window, operation);
|
510
|
+
},
|
511
|
+
reload: operation => {
|
512
|
+
before(window, operation);
|
513
|
+
operate(operation, (() => {
|
514
|
+
window.location.reload();
|
515
|
+
}));
|
516
|
+
after(window, operation);
|
517
|
+
},
|
518
|
+
removeStorageItem: operation => {
|
519
|
+
before(document, operation);
|
520
|
+
operate(operation, (() => {
|
521
|
+
const {key: key, type: type} = operation;
|
522
|
+
const storage = type === "session" ? sessionStorage : localStorage;
|
523
|
+
storage.removeItem(key);
|
524
|
+
}));
|
525
|
+
after(document, operation);
|
526
|
+
},
|
527
|
+
replaceState: operation => {
|
528
|
+
before(window, operation);
|
529
|
+
operate(operation, (() => {
|
530
|
+
const {state: state, title: title, url: url} = operation;
|
531
|
+
history.replaceState(state || {}, title || "", url);
|
532
|
+
}));
|
533
|
+
after(window, operation);
|
534
|
+
},
|
535
|
+
scrollIntoView: operation => {
|
536
|
+
const {element: element} = operation;
|
537
|
+
before(element, operation);
|
538
|
+
operate(operation, (() => {
|
539
|
+
element.scrollIntoView(operation);
|
540
|
+
}));
|
541
|
+
after(element, operation);
|
542
|
+
},
|
543
|
+
setCookie: operation => {
|
544
|
+
before(document, operation);
|
545
|
+
operate(operation, (() => {
|
546
|
+
const {cookie: cookie} = operation;
|
547
|
+
document.cookie = cookie || "";
|
548
|
+
}));
|
549
|
+
after(document, operation);
|
550
|
+
},
|
551
|
+
setFocus: operation => {
|
552
|
+
const {element: element} = operation;
|
553
|
+
before(element, operation);
|
554
|
+
operate(operation, (() => {
|
555
|
+
assignFocus(element);
|
556
|
+
}));
|
557
|
+
after(element, operation);
|
558
|
+
},
|
559
|
+
setStorageItem: operation => {
|
560
|
+
before(document, operation);
|
561
|
+
operate(operation, (() => {
|
562
|
+
const {key: key, value: value, type: type} = operation;
|
563
|
+
const storage = type === "session" ? sessionStorage : localStorage;
|
564
|
+
storage.setItem(key, value || "");
|
565
|
+
}));
|
566
|
+
after(document, operation);
|
567
|
+
},
|
568
|
+
consoleLog: operation => {
|
569
|
+
before(document, operation);
|
570
|
+
operate(operation, (() => {
|
571
|
+
const {message: message, level: level} = operation;
|
572
|
+
level && [ "warn", "info", "error" ].includes(level) ? console[level](message || "") : console.log(message || "");
|
573
|
+
}));
|
574
|
+
after(document, operation);
|
575
|
+
},
|
576
|
+
consoleTable: operation => {
|
577
|
+
before(document, operation);
|
578
|
+
operate(operation, (() => {
|
579
|
+
const {data: data, columns: columns} = operation;
|
580
|
+
console.table(data, columns || []);
|
581
|
+
}));
|
582
|
+
after(document, operation);
|
583
|
+
},
|
584
|
+
notification: operation => {
|
585
|
+
before(document, operation);
|
586
|
+
operate(operation, (() => {
|
587
|
+
const {title: title, options: options} = operation;
|
588
|
+
Notification.requestPermission().then((result => {
|
589
|
+
operation.permission = result;
|
590
|
+
if (result === "granted") new Notification(title || "", options);
|
591
|
+
}));
|
592
|
+
}));
|
593
|
+
after(document, operation);
|
594
|
+
}
|
595
|
+
};
|
596
|
+
let operations = Operations;
|
597
|
+
const add = newOperations => {
|
598
|
+
operations = {
|
599
|
+
...operations,
|
600
|
+
...newOperations
|
601
|
+
};
|
602
|
+
};
|
603
|
+
const addOperations = operations => {
|
604
|
+
add(operations);
|
605
|
+
};
|
606
|
+
const addOperation = (name, operation) => {
|
607
|
+
const operations = {};
|
608
|
+
operations[name] = operation;
|
609
|
+
add(operations);
|
610
|
+
};
|
611
|
+
var OperationStore = {
|
612
|
+
get all() {
|
613
|
+
return operations;
|
614
|
+
}
|
615
|
+
};
|
616
|
+
const perform = (operations, options = {
|
617
|
+
emitMissingElementWarnings: true
|
618
|
+
}) => {
|
619
|
+
const batches = {};
|
620
|
+
operations.forEach((operation => {
|
621
|
+
if (!!operation.batch) batches[operation.batch] = batches[operation.batch] ? ++batches[operation.batch] : 1;
|
622
|
+
}));
|
623
|
+
operations.forEach((operation => {
|
624
|
+
const name = operation.operation;
|
625
|
+
try {
|
626
|
+
if (operation.selector) {
|
627
|
+
operation.element = operation.xpath ? xpathToElement(operation.selector) : document[operation.selectAll ? "querySelectorAll" : "querySelector"](operation.selector);
|
628
|
+
} else {
|
629
|
+
operation.element = document;
|
630
|
+
}
|
631
|
+
if (operation.element || options.emitMissingElementWarnings) {
|
632
|
+
ActiveElement.set(document.activeElement);
|
633
|
+
const cableReadyOperation = OperationStore.all[name];
|
634
|
+
if (cableReadyOperation) {
|
635
|
+
cableReadyOperation(operation);
|
636
|
+
if (!!operation.batch && --batches[operation.batch] === 0) dispatch(document, "cable-ready:batch-complete", {
|
637
|
+
batch: operation.batch
|
638
|
+
});
|
639
|
+
} else {
|
640
|
+
console.error(`CableReady couldn't find the "${name}" operation. Make sure you use the camelized form when calling an operation method.`);
|
641
|
+
}
|
642
|
+
}
|
643
|
+
} catch (e) {
|
644
|
+
if (operation.element) {
|
645
|
+
console.error(`CableReady detected an error in ${name || "operation"}: ${e.message}. If you need to support older browsers make sure you've included the corresponding polyfills. https://docs.stimulusreflex.com/setup#polyfills-for-ie11.`);
|
646
|
+
console.error(e);
|
647
|
+
} else {
|
648
|
+
console.warn(`CableReady ${name || "operation"} failed due to missing DOM element for selector: '${operation.selector}'`);
|
649
|
+
}
|
650
|
+
}
|
651
|
+
}));
|
652
|
+
};
|
653
|
+
const performAsync = (operations, options = {
|
654
|
+
emitMissingElementWarnings: true
|
655
|
+
}) => new Promise(((resolve, reject) => {
|
656
|
+
try {
|
657
|
+
resolve(perform(operations, options));
|
658
|
+
} catch (err) {
|
659
|
+
reject(err);
|
660
|
+
}
|
661
|
+
}));
|
662
|
+
let consumer;
|
663
|
+
const BACKOFF = [ 25, 50, 75, 100, 200, 250, 500, 800, 1e3, 2e3 ];
|
664
|
+
const wait = ms => new Promise((resolve => setTimeout(resolve, ms)));
|
665
|
+
const getConsumerWithRetry = async (retry = 0) => {
|
666
|
+
if (consumer) return consumer;
|
667
|
+
if (retry >= BACKOFF.length) {
|
668
|
+
throw new Error("Couldn't obtain a Action Cable consumer within 5s");
|
669
|
+
}
|
670
|
+
await wait(BACKOFF[retry]);
|
671
|
+
return await getConsumerWithRetry(retry + 1);
|
672
|
+
};
|
673
|
+
var CableConsumer = {
|
674
|
+
setConsumer(value) {
|
675
|
+
consumer = value;
|
676
|
+
},
|
677
|
+
get consumer() {
|
678
|
+
return consumer;
|
679
|
+
},
|
680
|
+
async getConsumer() {
|
681
|
+
return await getConsumerWithRetry();
|
682
|
+
}
|
683
|
+
};
|
684
|
+
class SubscribingElement extends HTMLElement {
|
685
|
+
disconnectedCallback() {
|
686
|
+
if (this.channel) this.channel.unsubscribe();
|
687
|
+
}
|
688
|
+
createSubscription(consumer, channel, receivedCallback) {
|
689
|
+
this.channel = consumer.subscriptions.create({
|
690
|
+
channel: channel,
|
691
|
+
identifier: this.identifier
|
692
|
+
}, {
|
693
|
+
received: receivedCallback
|
694
|
+
});
|
695
|
+
}
|
696
|
+
get preview() {
|
697
|
+
return document.documentElement.hasAttribute("data-turbolinks-preview") || document.documentElement.hasAttribute("data-turbo-preview");
|
698
|
+
}
|
699
|
+
get identifier() {
|
700
|
+
return this.getAttribute("identifier");
|
701
|
+
}
|
702
|
+
}
|
703
|
+
class StreamFromElement extends SubscribingElement {
|
704
|
+
async connectedCallback() {
|
705
|
+
if (this.preview) return;
|
706
|
+
const consumer = await CableConsumer.getConsumer();
|
707
|
+
if (consumer) {
|
708
|
+
this.createSubscription(consumer, "CableReady::Stream", this.performOperations);
|
709
|
+
} else {
|
710
|
+
console.error("The `stream_from` helper cannot connect without an ActionCable consumer.\nPlease run `rails generate cable_ready:helpers` to fix this.");
|
711
|
+
}
|
712
|
+
}
|
713
|
+
performOperations(data) {
|
714
|
+
if (data.cableReady) perform(data.operations);
|
715
|
+
}
|
716
|
+
}
|
717
|
+
const template = `\n<style>\n :host {\n display: block;\n }\n</style>\n<slot></slot>\n`;
|
718
|
+
class UpdatesForElement extends SubscribingElement {
|
719
|
+
constructor() {
|
720
|
+
super();
|
721
|
+
const shadowRoot = this.attachShadow({
|
722
|
+
mode: "open"
|
723
|
+
});
|
724
|
+
shadowRoot.innerHTML = template;
|
725
|
+
}
|
726
|
+
async connectedCallback() {
|
727
|
+
if (this.preview) return;
|
728
|
+
this.update = debounce(this.update.bind(this), this.debounce);
|
729
|
+
const consumer = await CableConsumer.getConsumer();
|
730
|
+
if (consumer) {
|
731
|
+
this.createSubscription(consumer, "CableReady::Stream", this.update);
|
732
|
+
} else {
|
733
|
+
console.error("The `updates-for` helper cannot connect without an ActionCable consumer.\nPlease run `rails generate cable_ready:helpers` to fix this.");
|
734
|
+
}
|
735
|
+
}
|
736
|
+
async update(data) {
|
737
|
+
const blocks = Array.from(document.querySelectorAll(this.query), (element => new Block(element)));
|
738
|
+
if (blocks[0].element !== this) return;
|
739
|
+
ActiveElement.set(document.activeElement);
|
740
|
+
this.html = {};
|
741
|
+
const uniqueUrls = [ ...new Set(blocks.map((block => block.url))) ];
|
742
|
+
await Promise.all(uniqueUrls.map((async url => {
|
743
|
+
if (!this.html.hasOwnProperty(url)) {
|
744
|
+
const response = await graciouslyFetch(url, {
|
745
|
+
"X-Cable-Ready": "update"
|
746
|
+
});
|
747
|
+
this.html[url] = await response.text();
|
748
|
+
}
|
749
|
+
})));
|
750
|
+
this.index = {};
|
751
|
+
blocks.forEach((block => {
|
752
|
+
this.index.hasOwnProperty(block.url) ? this.index[block.url]++ : this.index[block.url] = 0;
|
753
|
+
block.process(data, this.html, this.index);
|
754
|
+
}));
|
755
|
+
}
|
756
|
+
get query() {
|
757
|
+
return `updates-for[identifier="${this.identifier}"]`;
|
758
|
+
}
|
759
|
+
get identifier() {
|
760
|
+
return this.getAttribute("identifier");
|
761
|
+
}
|
762
|
+
get debounce() {
|
763
|
+
return this.hasAttribute("debounce") ? parseInt(this.getAttribute("debounce")) : 20;
|
764
|
+
}
|
765
|
+
}
|
766
|
+
class Block {
|
767
|
+
constructor(element) {
|
768
|
+
this.element = element;
|
769
|
+
}
|
770
|
+
async process(data, html, index) {
|
771
|
+
if (!this.shouldUpdate(data)) return;
|
772
|
+
const blockIndex = index[this.url];
|
773
|
+
const template = document.createElement("template");
|
774
|
+
this.element.setAttribute("updating", "updating");
|
775
|
+
template.innerHTML = String(html[this.url]).trim();
|
776
|
+
await this.resolveTurboFrames(template.content);
|
777
|
+
const fragments = template.content.querySelectorAll(this.query);
|
778
|
+
if (fragments.length <= blockIndex) {
|
779
|
+
console.warn(`Update aborted due to insufficient number of elements. The offending url is ${this.url}.`);
|
780
|
+
return;
|
781
|
+
}
|
782
|
+
const operation = {
|
783
|
+
element: this.element,
|
784
|
+
html: fragments[blockIndex],
|
785
|
+
permanentAttributeName: "data-ignore-updates"
|
786
|
+
};
|
787
|
+
dispatch(this.element, "cable-ready:before-update", operation);
|
788
|
+
morphdom__default["default"](this.element, fragments[blockIndex], {
|
789
|
+
childrenOnly: true,
|
790
|
+
onBeforeElUpdated: shouldMorph(operation),
|
791
|
+
onElUpdated: _ => {
|
792
|
+
this.element.removeAttribute("updating");
|
793
|
+
dispatch(this.element, "cable-ready:after-update", operation);
|
794
|
+
assignFocus(operation.focusSelector);
|
795
|
+
}
|
796
|
+
});
|
797
|
+
}
|
798
|
+
async resolveTurboFrames(documentFragment) {
|
799
|
+
const reloadingTurboFrames = [ ...documentFragment.querySelectorAll('turbo-frame[src]:not([loading="lazy"])') ];
|
800
|
+
return Promise.all(reloadingTurboFrames.map((frame => new Promise((async resolve => {
|
801
|
+
const frameResponse = await graciouslyFetch(frame.getAttribute("src"), {
|
802
|
+
"Turbo-Frame": frame.id,
|
803
|
+
"X-Cable-Ready": "update"
|
804
|
+
});
|
805
|
+
const frameTemplate = document.createElement("template");
|
806
|
+
frameTemplate.innerHTML = await frameResponse.text();
|
807
|
+
await this.resolveTurboFrames(frameTemplate.content);
|
808
|
+
documentFragment.querySelector(`turbo-frame#${frame.id}`).innerHTML = String(frameTemplate.content.querySelector(`turbo-frame#${frame.id}`).innerHTML).trim();
|
809
|
+
resolve();
|
810
|
+
})))));
|
811
|
+
}
|
812
|
+
shouldUpdate(data) {
|
813
|
+
return !this.ignoresInnerUpdates && this.hasChangesSelectedForUpdate(data);
|
814
|
+
}
|
815
|
+
hasChangesSelectedForUpdate(data) {
|
816
|
+
const only = this.element.getAttribute("only");
|
817
|
+
return !(only && data.changed && !only.split(" ").some((attribute => data.changed.includes(attribute))));
|
818
|
+
}
|
819
|
+
get ignoresInnerUpdates() {
|
820
|
+
return this.element.hasAttribute("ignore-inner-updates") && this.element.hasAttribute("performing-inner-update");
|
821
|
+
}
|
822
|
+
get url() {
|
823
|
+
return this.element.hasAttribute("url") ? this.element.getAttribute("url") : location.href;
|
824
|
+
}
|
825
|
+
get identifier() {
|
826
|
+
return this.element.identifier;
|
827
|
+
}
|
828
|
+
get query() {
|
829
|
+
return this.element.query;
|
830
|
+
}
|
831
|
+
}
|
832
|
+
const registerInnerUpdates = () => {
|
833
|
+
document.addEventListener("stimulus-reflex:before", (event => {
|
834
|
+
recursiveMarkUpdatesForElements(event.detail.element);
|
835
|
+
}));
|
836
|
+
document.addEventListener("stimulus-reflex:after", (event => {
|
837
|
+
setTimeout((() => {
|
838
|
+
recursiveUnmarkUpdatesForElements(event.detail.element);
|
839
|
+
}));
|
840
|
+
}));
|
841
|
+
document.addEventListener("turbo:submit-start", (event => {
|
842
|
+
recursiveMarkUpdatesForElements(event.target);
|
843
|
+
}));
|
844
|
+
document.addEventListener("turbo:submit-end", (event => {
|
845
|
+
setTimeout((() => {
|
846
|
+
recursiveUnmarkUpdatesForElements(event.target);
|
847
|
+
}));
|
848
|
+
}));
|
849
|
+
};
|
850
|
+
const recursiveMarkUpdatesForElements = leaf => {
|
851
|
+
const closestUpdatesFor = leaf && leaf.parentElement.closest("updates-for");
|
852
|
+
if (closestUpdatesFor) {
|
853
|
+
closestUpdatesFor.setAttribute("performing-inner-update", "");
|
854
|
+
recursiveMarkUpdatesForElements(closestUpdatesFor);
|
855
|
+
}
|
856
|
+
};
|
857
|
+
const recursiveUnmarkUpdatesForElements = leaf => {
|
858
|
+
const closestUpdatesFor = leaf && leaf.parentElement.closest("updates-for");
|
859
|
+
if (closestUpdatesFor) {
|
860
|
+
closestUpdatesFor.removeAttribute("performing-inner-update");
|
861
|
+
recursiveUnmarkUpdatesForElements(closestUpdatesFor);
|
862
|
+
}
|
863
|
+
};
|
864
|
+
const initialize = (initializeOptions = {}) => {
|
865
|
+
const {consumer: consumer} = initializeOptions;
|
866
|
+
registerInnerUpdates();
|
867
|
+
if (consumer) {
|
868
|
+
CableConsumer.setConsumer(consumer);
|
869
|
+
} else {
|
870
|
+
console.error("CableReady requires a reference to your Action Cable `consumer` for its helpers to function.\nEnsure that you have imported the `CableReady` package as well as `consumer` from your `channels` folder, then call `CableReady.initialize({ consumer })`.");
|
871
|
+
}
|
872
|
+
if (!customElements.get("stream-from")) {
|
873
|
+
customElements.define("stream-from", StreamFromElement);
|
874
|
+
}
|
875
|
+
if (!customElements.get("updates-for")) {
|
876
|
+
customElements.define("updates-for", UpdatesForElement);
|
877
|
+
}
|
878
|
+
};
|
879
|
+
const global = {
|
880
|
+
perform: perform,
|
881
|
+
performAsync: performAsync,
|
882
|
+
shouldMorphCallbacks: shouldMorphCallbacks,
|
883
|
+
didMorphCallbacks: didMorphCallbacks,
|
884
|
+
initialize: initialize,
|
885
|
+
addOperation: addOperation,
|
886
|
+
addOperations: addOperations,
|
887
|
+
version: packageInfo.version,
|
888
|
+
cable: CableConsumer,
|
889
|
+
get DOMOperations() {
|
890
|
+
console.warn("DEPRECATED: Please use `CableReady.operations` instead of `CableReady.DOMOperations`");
|
891
|
+
return OperationStore.all;
|
892
|
+
},
|
893
|
+
get operations() {
|
894
|
+
return OperationStore.all;
|
895
|
+
},
|
896
|
+
get consumer() {
|
897
|
+
return CableConsumer.consumer;
|
898
|
+
}
|
899
|
+
};
|
900
|
+
window.CableReady = global;
|
901
|
+
exports.MorphCallbacks = morph_callbacks;
|
902
|
+
exports.StreamFromElement = StreamFromElement;
|
903
|
+
exports.SubscribingElement = SubscribingElement;
|
904
|
+
exports.UpdatesForElement = UpdatesForElement;
|
905
|
+
exports.Utils = utils;
|
906
|
+
exports["default"] = global;
|
907
|
+
Object.defineProperty(exports, "__esModule", {
|
908
|
+
value: true
|
909
|
+
});
|
910
|
+
}));
|