cable_ready 4.5.0 → 5.0.0
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 -376
- data/Gemfile +4 -1
- data/Gemfile.lock +146 -144
- data/README.md +54 -20
- data/Rakefile +8 -8
- data/app/assets/javascripts/cable_ready.js +1269 -0
- data/app/assets/javascripts/cable_ready.umd.js +1190 -0
- data/app/channels/cable_ready/stream.rb +14 -0
- data/app/helpers/cable_ready/view_helper.rb +58 -0
- data/app/jobs/cable_ready/broadcast_job.rb +15 -0
- data/app/models/concerns/cable_ready/updatable/collection_updatable_callbacks.rb +21 -0
- data/app/models/concerns/cable_ready/updatable/collections_registry.rb +59 -0
- data/app/models/concerns/cable_ready/updatable/memory_cache_debounce_adapter.rb +24 -0
- data/app/models/concerns/cable_ready/updatable/model_updatable_callbacks.rb +33 -0
- data/app/models/concerns/cable_ready/updatable.rb +211 -0
- data/app/models/concerns/extend_has_many.rb +15 -0
- data/bin/standardize +1 -1
- data/cable_ready.gemspec +20 -6
- data/lib/cable_ready/broadcaster.rb +4 -3
- data/lib/cable_ready/cable_car.rb +19 -0
- data/lib/cable_ready/channel.rb +29 -31
- data/lib/cable_ready/channels.rb +4 -5
- data/lib/cable_ready/compoundable.rb +11 -0
- data/lib/cable_ready/config.rb +28 -1
- data/lib/cable_ready/engine.rb +59 -0
- data/lib/cable_ready/identifiable.rb +48 -0
- data/lib/cable_ready/importmap.rb +4 -0
- data/lib/cable_ready/installer.rb +224 -0
- data/lib/cable_ready/operation_builder.rb +80 -0
- data/lib/cable_ready/sanity_checker.rb +63 -0
- data/lib/cable_ready/stream_identifier.rb +13 -0
- data/lib/cable_ready/version.rb +1 -1
- data/lib/cable_ready.rb +23 -10
- data/lib/cable_ready_helper.rb +13 -0
- data/lib/generators/cable_ready/channel_generator.rb +110 -0
- data/lib/generators/cable_ready/templates/app/javascript/channels/consumer.js.tt +6 -0
- data/lib/generators/cable_ready/templates/app/javascript/channels/index.js.esbuild.tt +4 -0
- data/lib/generators/cable_ready/templates/app/javascript/channels/index.js.importmap.tt +2 -0
- data/lib/generators/cable_ready/templates/app/javascript/channels/index.js.shakapacker.tt +5 -0
- data/lib/generators/cable_ready/templates/app/javascript/channels/index.js.vite.tt +1 -0
- data/lib/generators/cable_ready/templates/app/javascript/channels/index.js.webpacker.tt +5 -0
- data/lib/generators/cable_ready/templates/app/javascript/config/cable_ready.js.tt +4 -0
- data/lib/generators/cable_ready/templates/app/javascript/config/index.js.tt +1 -0
- data/lib/generators/cable_ready/templates/app/javascript/config/mrujs.js.tt +9 -0
- data/lib/generators/cable_ready/templates/app/javascript/controllers/%file_name%_controller.js.tt +38 -0
- data/lib/generators/cable_ready/templates/app/javascript/controllers/application.js.tt +11 -0
- data/lib/generators/cable_ready/templates/app/javascript/controllers/index.js.esbuild.tt +7 -0
- data/lib/generators/cable_ready/templates/app/javascript/controllers/index.js.importmap.tt +5 -0
- data/lib/generators/cable_ready/templates/app/javascript/controllers/index.js.shakapacker.tt +5 -0
- data/lib/generators/cable_ready/templates/app/javascript/controllers/index.js.vite.tt +5 -0
- data/lib/generators/cable_ready/templates/app/javascript/controllers/index.js.webpacker.tt +5 -0
- data/lib/generators/cable_ready/templates/config/initializers/cable_ready.rb +27 -0
- data/lib/generators/cable_ready/templates/esbuild.config.mjs.tt +94 -0
- data/lib/install/action_cable.rb +144 -0
- data/lib/install/broadcaster.rb +109 -0
- data/lib/install/bundle.rb +54 -0
- data/lib/install/compression.rb +51 -0
- data/lib/install/config.rb +39 -0
- data/lib/install/development.rb +34 -0
- data/lib/install/esbuild.rb +101 -0
- data/lib/install/importmap.rb +96 -0
- data/lib/install/initializers.rb +15 -0
- data/lib/install/mrujs.rb +121 -0
- data/lib/install/npm_packages.rb +13 -0
- data/lib/install/shakapacker.rb +65 -0
- data/lib/install/spring.rb +54 -0
- data/lib/install/updatable.rb +34 -0
- data/lib/install/vite.rb +66 -0
- data/lib/install/webpacker.rb +93 -0
- data/lib/install/yarn.rb +56 -0
- data/lib/tasks/cable_ready/cable_ready.rake +247 -0
- data/package.json +42 -13
- data/rollup.config.mjs +57 -0
- data/web-test-runner.config.mjs +12 -0
- data/yarn.lock +3252 -327
- metadata +138 -9
- data/tags +0 -62
|
@@ -0,0 +1,1269 @@
|
|
|
1
|
+
import morphdom from "morphdom";
|
|
2
|
+
|
|
3
|
+
var name = "cable_ready";
|
|
4
|
+
|
|
5
|
+
var version = "5.0.0";
|
|
6
|
+
|
|
7
|
+
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.";
|
|
8
|
+
|
|
9
|
+
var keywords = [ "ruby", "rails", "websockets", "actioncable", "cable", "ssr", "stimulus_reflex", "client-side", "dom" ];
|
|
10
|
+
|
|
11
|
+
var homepage = "https://cableready.stimulusreflex.com";
|
|
12
|
+
|
|
13
|
+
var bugs = "https://github.com/stimulusreflex/cable_ready/issues";
|
|
14
|
+
|
|
15
|
+
var repository = "https://github.com/stimulusreflex/cable_ready";
|
|
16
|
+
|
|
17
|
+
var license = "MIT";
|
|
18
|
+
|
|
19
|
+
var author = "Nathan Hopkins <natehop@gmail.com>";
|
|
20
|
+
|
|
21
|
+
var contributors = [ "Andrew Mason <andrewmcodes@protonmail.com>", "Julian Rubisch <julian@julianrubisch.at>", "Marco Roth <marco.roth@intergga.ch>", "Nathan Hopkins <natehop@gmail.com>" ];
|
|
22
|
+
|
|
23
|
+
var main = "./dist/cable_ready.js";
|
|
24
|
+
|
|
25
|
+
var module = "./dist/cable_ready.js";
|
|
26
|
+
|
|
27
|
+
var browser = "./dist/cable_ready.js";
|
|
28
|
+
|
|
29
|
+
var unpkg = "./dist/cable_ready.umd.js";
|
|
30
|
+
|
|
31
|
+
var umd = "./dist/cable_ready.umd.js";
|
|
32
|
+
|
|
33
|
+
var files = [ "dist/*", "javascript/*" ];
|
|
34
|
+
|
|
35
|
+
var scripts = {
|
|
36
|
+
lint: "yarn run format --check",
|
|
37
|
+
format: "yarn run prettier-standard ./javascript/**/*.js rollup.config.mjs",
|
|
38
|
+
build: "yarn rollup -c",
|
|
39
|
+
watch: "yarn rollup -wc",
|
|
40
|
+
test: "web-test-runner javascript/test/**/*.test.js",
|
|
41
|
+
"docs:dev": "vitepress dev docs",
|
|
42
|
+
"docs:build": "vitepress build docs && cp ./docs/_redirects ./docs/.vitepress/dist",
|
|
43
|
+
"docs:preview": "vitepress preview docs"
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
var dependencies = {
|
|
47
|
+
morphdom: "2.6.1"
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
var devDependencies = {
|
|
51
|
+
"@open-wc/testing": "^3.1.7",
|
|
52
|
+
"@rollup/plugin-json": "^6.0.0",
|
|
53
|
+
"@rollup/plugin-node-resolve": "^15.0.1",
|
|
54
|
+
"@rollup/plugin-terser": "^0.4.0",
|
|
55
|
+
"@web/dev-server-esbuild": "^0.3.3",
|
|
56
|
+
"@web/dev-server-rollup": "^0.3.21",
|
|
57
|
+
"@web/test-runner": "^0.15.1",
|
|
58
|
+
"prettier-standard": "^16.4.1",
|
|
59
|
+
rollup: "^3.19.1",
|
|
60
|
+
sinon: "^15.0.2",
|
|
61
|
+
vite: "^4.1.4",
|
|
62
|
+
vitepress: "^1.0.0-alpha.56",
|
|
63
|
+
"vitepress-plugin-search": "^1.0.4-alpha.19"
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
var packageInfo = {
|
|
67
|
+
name: name,
|
|
68
|
+
version: version,
|
|
69
|
+
description: description,
|
|
70
|
+
keywords: keywords,
|
|
71
|
+
homepage: homepage,
|
|
72
|
+
bugs: bugs,
|
|
73
|
+
repository: repository,
|
|
74
|
+
license: license,
|
|
75
|
+
author: author,
|
|
76
|
+
contributors: contributors,
|
|
77
|
+
main: main,
|
|
78
|
+
module: module,
|
|
79
|
+
browser: browser,
|
|
80
|
+
import: "./dist/cable_ready.js",
|
|
81
|
+
unpkg: unpkg,
|
|
82
|
+
umd: umd,
|
|
83
|
+
files: files,
|
|
84
|
+
scripts: scripts,
|
|
85
|
+
dependencies: dependencies,
|
|
86
|
+
devDependencies: devDependencies
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const inputTags = {
|
|
90
|
+
INPUT: true,
|
|
91
|
+
TEXTAREA: true,
|
|
92
|
+
SELECT: true
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const mutableTags = {
|
|
96
|
+
INPUT: true,
|
|
97
|
+
TEXTAREA: true,
|
|
98
|
+
OPTION: true
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const textInputTypes = {
|
|
102
|
+
"datetime-local": true,
|
|
103
|
+
"select-multiple": true,
|
|
104
|
+
"select-one": true,
|
|
105
|
+
color: true,
|
|
106
|
+
date: true,
|
|
107
|
+
datetime: true,
|
|
108
|
+
email: true,
|
|
109
|
+
month: true,
|
|
110
|
+
number: true,
|
|
111
|
+
password: true,
|
|
112
|
+
range: true,
|
|
113
|
+
search: true,
|
|
114
|
+
tel: true,
|
|
115
|
+
text: true,
|
|
116
|
+
textarea: true,
|
|
117
|
+
time: true,
|
|
118
|
+
url: true,
|
|
119
|
+
week: true
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
let activeElement;
|
|
123
|
+
|
|
124
|
+
var ActiveElement = {
|
|
125
|
+
get element() {
|
|
126
|
+
return activeElement;
|
|
127
|
+
},
|
|
128
|
+
set(element) {
|
|
129
|
+
activeElement = element;
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// Indicates if the passed element is considered a text input.
|
|
134
|
+
|
|
135
|
+
const isTextInput = element => inputTags[element.tagName] && textInputTypes[element.type];
|
|
136
|
+
|
|
137
|
+
// Assigns focus to the appropriate element... preferring the explicitly passed selector
|
|
138
|
+
|
|
139
|
+
// * selector - a CSS selector for the element that should have focus
|
|
140
|
+
|
|
141
|
+
const assignFocus = selector => {
|
|
142
|
+
const element = selector && selector.nodeType === Node.ELEMENT_NODE ? selector : document.querySelector(selector);
|
|
143
|
+
const focusElement = element || ActiveElement.element;
|
|
144
|
+
if (focusElement && focusElement.focus) focusElement.focus();
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// Dispatches an event on the passed element
|
|
148
|
+
|
|
149
|
+
// * element - the element
|
|
150
|
+
// * name - the name of the event
|
|
151
|
+
// * detail - the event detail
|
|
152
|
+
|
|
153
|
+
const dispatch = (element, name, detail = {}) => {
|
|
154
|
+
const init = {
|
|
155
|
+
bubbles: true,
|
|
156
|
+
cancelable: true,
|
|
157
|
+
detail: detail
|
|
158
|
+
};
|
|
159
|
+
const event = new CustomEvent(name, init);
|
|
160
|
+
element.dispatchEvent(event);
|
|
161
|
+
if (window.jQuery) window.jQuery(element).trigger(name, detail);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// Accepts an xPath query and returns the element found at that position in the DOM
|
|
165
|
+
|
|
166
|
+
const xpathToElement = xpath => document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
|
|
167
|
+
|
|
168
|
+
// Accepts an xPath query and returns all matching elements in the DOM
|
|
169
|
+
|
|
170
|
+
const xpathToElementArray = (xpath, reverse = false) => {
|
|
171
|
+
const snapshotList = document.evaluate(xpath, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
|
|
172
|
+
const snapshots = [];
|
|
173
|
+
for (let i = 0; i < snapshotList.snapshotLength; i++) {
|
|
174
|
+
snapshots.push(snapshotList.snapshotItem(i));
|
|
175
|
+
}
|
|
176
|
+
return reverse ? snapshots.reverse() : snapshots;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// Return an array with the class names to be used
|
|
180
|
+
|
|
181
|
+
// * names - could be a string or an array of strings for multiple classes.
|
|
182
|
+
|
|
183
|
+
const getClassNames = names => Array.from(names).flat()
|
|
184
|
+
// Perform operation for either the first or all of the elements returned by CSS selector
|
|
185
|
+
|
|
186
|
+
// * operation - the instruction payload from perform
|
|
187
|
+
// * callback - the operation function to run for each element
|
|
188
|
+
|
|
189
|
+
;
|
|
190
|
+
|
|
191
|
+
const processElements = (operation, callback) => {
|
|
192
|
+
Array.from(operation.selectAll ? operation.element : [ operation.element ]).forEach(callback);
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
// convert string to kebab-case
|
|
196
|
+
// most other implementations (lodash) are focused on camelCase to kebab-case
|
|
197
|
+
// instead, this uses word token boundaries to produce readable URL slugs and keys
|
|
198
|
+
// this implementation will not support Emoji or other non-ASCII characters
|
|
199
|
+
|
|
200
|
+
const kebabize = createCompounder((function(result, word, index) {
|
|
201
|
+
return result + (index ? "-" : "") + word.toLowerCase();
|
|
202
|
+
}));
|
|
203
|
+
|
|
204
|
+
function createCompounder(callback) {
|
|
205
|
+
return function(str) {
|
|
206
|
+
return words(str).reduce(callback, "");
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const words = str => {
|
|
211
|
+
str = str == null ? "" : str;
|
|
212
|
+
return str.match(/([A-Z]{2,}|[0-9]+|[A-Z]?[a-z]+|[A-Z])/g) || [];
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
// Provide a standardized pipeline of checks and modifications to all operations based on provided options
|
|
216
|
+
// Currently skips execution if cancelled and implements an optional delay
|
|
217
|
+
|
|
218
|
+
const operate = (operation, callback) => {
|
|
219
|
+
if (!operation.cancel) {
|
|
220
|
+
operation.delay ? setTimeout(callback, operation.delay) : callback();
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
return false;
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
// Dispatch life-cycle events with standardized naming
|
|
227
|
+
const before = (target, operation) => dispatch(target, `cable-ready:before-${kebabize(operation.operation)}`, operation);
|
|
228
|
+
|
|
229
|
+
const after = (target, operation) => dispatch(target, `cable-ready:after-${kebabize(operation.operation)}`, operation);
|
|
230
|
+
|
|
231
|
+
function debounce(fn, delay = 250) {
|
|
232
|
+
let timer;
|
|
233
|
+
return (...args) => {
|
|
234
|
+
const callback = () => fn.apply(this, args);
|
|
235
|
+
if (timer) clearTimeout(timer);
|
|
236
|
+
timer = setTimeout(callback, delay);
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function handleErrors(response) {
|
|
241
|
+
if (!response.ok) throw Error(response.statusText);
|
|
242
|
+
return response;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function safeScalar(val) {
|
|
246
|
+
if (val !== undefined && ![ "string", "number", "boolean" ].includes(typeof val)) console.warn(`Operation expects a string, number or boolean, but got ${val} (${typeof val})`);
|
|
247
|
+
return val != null ? val : "";
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function safeString(str) {
|
|
251
|
+
if (str !== undefined && typeof str !== "string") console.warn(`Operation expects a string, but got ${str} (${typeof str})`);
|
|
252
|
+
return str != null ? String(str) : "";
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function safeArray(arr) {
|
|
256
|
+
if (arr !== undefined && !Array.isArray(arr)) console.warn(`Operation expects an array, but got ${arr} (${typeof arr})`);
|
|
257
|
+
return arr != null ? Array.from(arr) : [];
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function safeObject(obj) {
|
|
261
|
+
if (obj !== undefined && typeof obj !== "object") console.warn(`Operation expects an object, but got ${obj} (${typeof obj})`);
|
|
262
|
+
return obj != null ? Object(obj) : {};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function safeStringOrArray(elem) {
|
|
266
|
+
if (elem !== undefined && !Array.isArray(elem) && typeof elem !== "string") console.warn(`Operation expects an Array or a String, but got ${elem} (${typeof elem})`);
|
|
267
|
+
return elem == null ? "" : Array.isArray(elem) ? Array.from(elem) : String(elem);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function fragmentToString(fragment) {
|
|
271
|
+
return (new XMLSerializer).serializeToString(fragment);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// A proxy method to wrap a fetch call in error handling
|
|
275
|
+
|
|
276
|
+
// * url - the URL to fetch
|
|
277
|
+
// * additionalHeaders - an object of additional headers passed to fetch
|
|
278
|
+
|
|
279
|
+
async function graciouslyFetch(url, additionalHeaders) {
|
|
280
|
+
try {
|
|
281
|
+
const response = await fetch(url, {
|
|
282
|
+
headers: {
|
|
283
|
+
"X-REQUESTED-WITH": "XmlHttpRequest",
|
|
284
|
+
...additionalHeaders
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
if (response == undefined) return;
|
|
288
|
+
handleErrors(response);
|
|
289
|
+
return response;
|
|
290
|
+
} catch (e) {
|
|
291
|
+
console.error(`Could not fetch ${url}`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
var utils = Object.freeze({
|
|
296
|
+
__proto__: null,
|
|
297
|
+
after: after,
|
|
298
|
+
assignFocus: assignFocus,
|
|
299
|
+
before: before,
|
|
300
|
+
debounce: debounce,
|
|
301
|
+
dispatch: dispatch,
|
|
302
|
+
fragmentToString: fragmentToString,
|
|
303
|
+
getClassNames: getClassNames,
|
|
304
|
+
graciouslyFetch: graciouslyFetch,
|
|
305
|
+
handleErrors: handleErrors,
|
|
306
|
+
isTextInput: isTextInput,
|
|
307
|
+
kebabize: kebabize,
|
|
308
|
+
operate: operate,
|
|
309
|
+
processElements: processElements,
|
|
310
|
+
safeArray: safeArray,
|
|
311
|
+
safeObject: safeObject,
|
|
312
|
+
safeScalar: safeScalar,
|
|
313
|
+
safeString: safeString,
|
|
314
|
+
safeStringOrArray: safeStringOrArray,
|
|
315
|
+
xpathToElement: xpathToElement,
|
|
316
|
+
xpathToElementArray: xpathToElementArray
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// Indicates whether or not we should morph an element via onBeforeElUpdated callback
|
|
320
|
+
// SEE: https://github.com/patrick-steele-idem/morphdom#morphdomfromnode-tonode-options--node
|
|
321
|
+
|
|
322
|
+
const shouldMorph = operation => (fromEl, toEl) => !shouldMorphCallbacks.map((callback => typeof callback === "function" ? callback(operation, fromEl, toEl) : true)).includes(false)
|
|
323
|
+
// Execute any pluggable functions that modify elements after morphing via onElUpdated callback
|
|
324
|
+
|
|
325
|
+
;
|
|
326
|
+
|
|
327
|
+
const didMorph = operation => el => {
|
|
328
|
+
didMorphCallbacks.forEach((callback => {
|
|
329
|
+
if (typeof callback === "function") callback(operation, el);
|
|
330
|
+
}));
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
const verifyNotMutable = (detail, fromEl, toEl) => {
|
|
334
|
+
// Skip nodes that are equal:
|
|
335
|
+
// https://github.com/patrick-steele-idem/morphdom#can-i-make-morphdom-blaze-through-the-dom-tree-even-faster-yes
|
|
336
|
+
if (!mutableTags[fromEl.tagName] && fromEl.isEqualNode(toEl)) return false;
|
|
337
|
+
return true;
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
const verifyNotContentEditable = (detail, fromEl, toEl) => {
|
|
341
|
+
if (fromEl === ActiveElement.element && fromEl.isContentEditable) return false;
|
|
342
|
+
return true;
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
const verifyNotPermanent = (detail, fromEl, toEl) => {
|
|
346
|
+
const {permanentAttributeName: permanentAttributeName} = detail;
|
|
347
|
+
if (!permanentAttributeName) return true;
|
|
348
|
+
const permanent = fromEl.closest(`[${permanentAttributeName}]`);
|
|
349
|
+
// only morph attributes on the active non-permanent text input
|
|
350
|
+
if (!permanent && fromEl === ActiveElement.element && isTextInput(fromEl)) {
|
|
351
|
+
const ignore = {
|
|
352
|
+
value: true
|
|
353
|
+
};
|
|
354
|
+
Array.from(toEl.attributes).forEach((attribute => {
|
|
355
|
+
if (!ignore[attribute.name]) fromEl.setAttribute(attribute.name, attribute.value);
|
|
356
|
+
}));
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
return !permanent;
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
const shouldMorphCallbacks = [ verifyNotMutable, verifyNotPermanent, verifyNotContentEditable ];
|
|
363
|
+
|
|
364
|
+
const didMorphCallbacks = [];
|
|
365
|
+
|
|
366
|
+
var morph_callbacks = Object.freeze({
|
|
367
|
+
__proto__: null,
|
|
368
|
+
didMorph: didMorph,
|
|
369
|
+
didMorphCallbacks: didMorphCallbacks,
|
|
370
|
+
shouldMorph: shouldMorph,
|
|
371
|
+
shouldMorphCallbacks: shouldMorphCallbacks,
|
|
372
|
+
verifyNotContentEditable: verifyNotContentEditable,
|
|
373
|
+
verifyNotMutable: verifyNotMutable,
|
|
374
|
+
verifyNotPermanent: verifyNotPermanent
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
var Operations = {
|
|
378
|
+
// DOM Mutations
|
|
379
|
+
append: operation => {
|
|
380
|
+
processElements(operation, (element => {
|
|
381
|
+
before(element, operation);
|
|
382
|
+
operate(operation, (() => {
|
|
383
|
+
const {html: html, focusSelector: focusSelector} = operation;
|
|
384
|
+
element.insertAdjacentHTML("beforeend", safeScalar(html));
|
|
385
|
+
assignFocus(focusSelector);
|
|
386
|
+
}));
|
|
387
|
+
after(element, operation);
|
|
388
|
+
}));
|
|
389
|
+
},
|
|
390
|
+
graft: operation => {
|
|
391
|
+
processElements(operation, (element => {
|
|
392
|
+
before(element, operation);
|
|
393
|
+
operate(operation, (() => {
|
|
394
|
+
const {parent: parent, focusSelector: focusSelector} = operation;
|
|
395
|
+
const parentElement = document.querySelector(parent);
|
|
396
|
+
if (parentElement) {
|
|
397
|
+
parentElement.appendChild(element);
|
|
398
|
+
assignFocus(focusSelector);
|
|
399
|
+
}
|
|
400
|
+
}));
|
|
401
|
+
after(element, operation);
|
|
402
|
+
}));
|
|
403
|
+
},
|
|
404
|
+
innerHtml: operation => {
|
|
405
|
+
processElements(operation, (element => {
|
|
406
|
+
before(element, operation);
|
|
407
|
+
operate(operation, (() => {
|
|
408
|
+
const {html: html, focusSelector: focusSelector} = operation;
|
|
409
|
+
element.innerHTML = safeScalar(html);
|
|
410
|
+
assignFocus(focusSelector);
|
|
411
|
+
}));
|
|
412
|
+
after(element, operation);
|
|
413
|
+
}));
|
|
414
|
+
},
|
|
415
|
+
insertAdjacentHtml: operation => {
|
|
416
|
+
processElements(operation, (element => {
|
|
417
|
+
before(element, operation);
|
|
418
|
+
operate(operation, (() => {
|
|
419
|
+
const {html: html, position: position, focusSelector: focusSelector} = operation;
|
|
420
|
+
element.insertAdjacentHTML(position || "beforeend", safeScalar(html));
|
|
421
|
+
assignFocus(focusSelector);
|
|
422
|
+
}));
|
|
423
|
+
after(element, operation);
|
|
424
|
+
}));
|
|
425
|
+
},
|
|
426
|
+
insertAdjacentText: operation => {
|
|
427
|
+
processElements(operation, (element => {
|
|
428
|
+
before(element, operation);
|
|
429
|
+
operate(operation, (() => {
|
|
430
|
+
const {text: text, position: position, focusSelector: focusSelector} = operation;
|
|
431
|
+
element.insertAdjacentText(position || "beforeend", safeScalar(text));
|
|
432
|
+
assignFocus(focusSelector);
|
|
433
|
+
}));
|
|
434
|
+
after(element, operation);
|
|
435
|
+
}));
|
|
436
|
+
},
|
|
437
|
+
outerHtml: operation => {
|
|
438
|
+
processElements(operation, (element => {
|
|
439
|
+
const parent = element.parentElement;
|
|
440
|
+
const idx = parent && Array.from(parent.children).indexOf(element);
|
|
441
|
+
before(element, operation);
|
|
442
|
+
operate(operation, (() => {
|
|
443
|
+
const {html: html, focusSelector: focusSelector} = operation;
|
|
444
|
+
element.outerHTML = safeScalar(html);
|
|
445
|
+
assignFocus(focusSelector);
|
|
446
|
+
}));
|
|
447
|
+
after(parent ? parent.children[idx] : document.documentElement, operation);
|
|
448
|
+
}));
|
|
449
|
+
},
|
|
450
|
+
prepend: operation => {
|
|
451
|
+
processElements(operation, (element => {
|
|
452
|
+
before(element, operation);
|
|
453
|
+
operate(operation, (() => {
|
|
454
|
+
const {html: html, focusSelector: focusSelector} = operation;
|
|
455
|
+
element.insertAdjacentHTML("afterbegin", safeScalar(html));
|
|
456
|
+
assignFocus(focusSelector);
|
|
457
|
+
}));
|
|
458
|
+
after(element, operation);
|
|
459
|
+
}));
|
|
460
|
+
},
|
|
461
|
+
remove: operation => {
|
|
462
|
+
processElements(operation, (element => {
|
|
463
|
+
before(element, operation);
|
|
464
|
+
operate(operation, (() => {
|
|
465
|
+
const {focusSelector: focusSelector} = operation;
|
|
466
|
+
element.remove();
|
|
467
|
+
assignFocus(focusSelector);
|
|
468
|
+
}));
|
|
469
|
+
after(document, operation);
|
|
470
|
+
}));
|
|
471
|
+
},
|
|
472
|
+
replace: operation => {
|
|
473
|
+
processElements(operation, (element => {
|
|
474
|
+
const parent = element.parentElement;
|
|
475
|
+
const idx = parent && Array.from(parent.children).indexOf(element);
|
|
476
|
+
before(element, operation);
|
|
477
|
+
operate(operation, (() => {
|
|
478
|
+
const {html: html, focusSelector: focusSelector} = operation;
|
|
479
|
+
element.outerHTML = safeScalar(html);
|
|
480
|
+
assignFocus(focusSelector);
|
|
481
|
+
}));
|
|
482
|
+
after(parent ? parent.children[idx] : document.documentElement, operation);
|
|
483
|
+
}));
|
|
484
|
+
},
|
|
485
|
+
textContent: operation => {
|
|
486
|
+
processElements(operation, (element => {
|
|
487
|
+
before(element, operation);
|
|
488
|
+
operate(operation, (() => {
|
|
489
|
+
const {text: text, focusSelector: focusSelector} = operation;
|
|
490
|
+
element.textContent = safeScalar(text);
|
|
491
|
+
assignFocus(focusSelector);
|
|
492
|
+
}));
|
|
493
|
+
after(element, operation);
|
|
494
|
+
}));
|
|
495
|
+
},
|
|
496
|
+
// Element Property Mutations
|
|
497
|
+
addCssClass: operation => {
|
|
498
|
+
processElements(operation, (element => {
|
|
499
|
+
before(element, operation);
|
|
500
|
+
operate(operation, (() => {
|
|
501
|
+
const {name: name} = operation;
|
|
502
|
+
element.classList.add(...getClassNames([ safeStringOrArray(name) ]));
|
|
503
|
+
}));
|
|
504
|
+
after(element, operation);
|
|
505
|
+
}));
|
|
506
|
+
},
|
|
507
|
+
removeAttribute: operation => {
|
|
508
|
+
processElements(operation, (element => {
|
|
509
|
+
before(element, operation);
|
|
510
|
+
operate(operation, (() => {
|
|
511
|
+
const {name: name} = operation;
|
|
512
|
+
element.removeAttribute(safeString(name));
|
|
513
|
+
}));
|
|
514
|
+
after(element, operation);
|
|
515
|
+
}));
|
|
516
|
+
},
|
|
517
|
+
removeCssClass: operation => {
|
|
518
|
+
processElements(operation, (element => {
|
|
519
|
+
before(element, operation);
|
|
520
|
+
operate(operation, (() => {
|
|
521
|
+
const {name: name} = operation;
|
|
522
|
+
element.classList.remove(...getClassNames([ safeStringOrArray(name) ]));
|
|
523
|
+
if (element.classList.length === 0) element.removeAttribute("class");
|
|
524
|
+
}));
|
|
525
|
+
after(element, operation);
|
|
526
|
+
}));
|
|
527
|
+
},
|
|
528
|
+
setAttribute: operation => {
|
|
529
|
+
processElements(operation, (element => {
|
|
530
|
+
before(element, operation);
|
|
531
|
+
operate(operation, (() => {
|
|
532
|
+
const {name: name, value: value} = operation;
|
|
533
|
+
element.setAttribute(safeString(name), safeScalar(value));
|
|
534
|
+
}));
|
|
535
|
+
after(element, operation);
|
|
536
|
+
}));
|
|
537
|
+
},
|
|
538
|
+
setDatasetProperty: operation => {
|
|
539
|
+
processElements(operation, (element => {
|
|
540
|
+
before(element, operation);
|
|
541
|
+
operate(operation, (() => {
|
|
542
|
+
const {name: name, value: value} = operation;
|
|
543
|
+
element.dataset[safeString(name)] = safeScalar(value);
|
|
544
|
+
}));
|
|
545
|
+
after(element, operation);
|
|
546
|
+
}));
|
|
547
|
+
},
|
|
548
|
+
setProperty: operation => {
|
|
549
|
+
processElements(operation, (element => {
|
|
550
|
+
before(element, operation);
|
|
551
|
+
operate(operation, (() => {
|
|
552
|
+
const {name: name, value: value} = operation;
|
|
553
|
+
if (name in element) element[safeString(name)] = safeScalar(value);
|
|
554
|
+
}));
|
|
555
|
+
after(element, operation);
|
|
556
|
+
}));
|
|
557
|
+
},
|
|
558
|
+
setStyle: operation => {
|
|
559
|
+
processElements(operation, (element => {
|
|
560
|
+
before(element, operation);
|
|
561
|
+
operate(operation, (() => {
|
|
562
|
+
const {name: name, value: value} = operation;
|
|
563
|
+
element.style[safeString(name)] = safeScalar(value);
|
|
564
|
+
}));
|
|
565
|
+
after(element, operation);
|
|
566
|
+
}));
|
|
567
|
+
},
|
|
568
|
+
setStyles: operation => {
|
|
569
|
+
processElements(operation, (element => {
|
|
570
|
+
before(element, operation);
|
|
571
|
+
operate(operation, (() => {
|
|
572
|
+
const {styles: styles} = operation;
|
|
573
|
+
for (let [name, value] of Object.entries(styles)) element.style[safeString(name)] = safeScalar(value);
|
|
574
|
+
}));
|
|
575
|
+
after(element, operation);
|
|
576
|
+
}));
|
|
577
|
+
},
|
|
578
|
+
setValue: operation => {
|
|
579
|
+
processElements(operation, (element => {
|
|
580
|
+
before(element, operation);
|
|
581
|
+
operate(operation, (() => {
|
|
582
|
+
const {value: value} = operation;
|
|
583
|
+
element.value = safeScalar(value);
|
|
584
|
+
}));
|
|
585
|
+
after(element, operation);
|
|
586
|
+
}));
|
|
587
|
+
},
|
|
588
|
+
// DOM Events and Meta-Operations
|
|
589
|
+
dispatchEvent: operation => {
|
|
590
|
+
processElements(operation, (element => {
|
|
591
|
+
before(element, operation);
|
|
592
|
+
operate(operation, (() => {
|
|
593
|
+
const {name: name, detail: detail} = operation;
|
|
594
|
+
dispatch(element, safeString(name), safeObject(detail));
|
|
595
|
+
}));
|
|
596
|
+
after(element, operation);
|
|
597
|
+
}));
|
|
598
|
+
},
|
|
599
|
+
setMeta: operation => {
|
|
600
|
+
before(document, operation);
|
|
601
|
+
operate(operation, (() => {
|
|
602
|
+
const {name: name, content: content} = operation;
|
|
603
|
+
let meta = document.head.querySelector(`meta[name='${name}']`);
|
|
604
|
+
if (!meta) {
|
|
605
|
+
meta = document.createElement("meta");
|
|
606
|
+
meta.name = safeString(name);
|
|
607
|
+
document.head.appendChild(meta);
|
|
608
|
+
}
|
|
609
|
+
meta.content = safeScalar(content);
|
|
610
|
+
}));
|
|
611
|
+
after(document, operation);
|
|
612
|
+
},
|
|
613
|
+
setTitle: operation => {
|
|
614
|
+
before(document, operation);
|
|
615
|
+
operate(operation, (() => {
|
|
616
|
+
const {title: title} = operation;
|
|
617
|
+
document.title = safeScalar(title);
|
|
618
|
+
}));
|
|
619
|
+
after(document, operation);
|
|
620
|
+
},
|
|
621
|
+
// Browser Manipulations
|
|
622
|
+
clearStorage: operation => {
|
|
623
|
+
before(document, operation);
|
|
624
|
+
operate(operation, (() => {
|
|
625
|
+
const {type: type} = operation;
|
|
626
|
+
const storage = type === "session" ? sessionStorage : localStorage;
|
|
627
|
+
storage.clear();
|
|
628
|
+
}));
|
|
629
|
+
after(document, operation);
|
|
630
|
+
},
|
|
631
|
+
go: operation => {
|
|
632
|
+
before(window, operation);
|
|
633
|
+
operate(operation, (() => {
|
|
634
|
+
const {delta: delta} = operation;
|
|
635
|
+
history.go(delta);
|
|
636
|
+
}));
|
|
637
|
+
after(window, operation);
|
|
638
|
+
},
|
|
639
|
+
pushState: operation => {
|
|
640
|
+
before(window, operation);
|
|
641
|
+
operate(operation, (() => {
|
|
642
|
+
const {state: state, title: title, url: url} = operation;
|
|
643
|
+
history.pushState(safeObject(state), safeString(title), safeString(url));
|
|
644
|
+
}));
|
|
645
|
+
after(window, operation);
|
|
646
|
+
},
|
|
647
|
+
redirectTo: operation => {
|
|
648
|
+
before(window, operation);
|
|
649
|
+
operate(operation, (() => {
|
|
650
|
+
let {url: url, action: action, turbo: turbo} = operation;
|
|
651
|
+
action = action || "advance";
|
|
652
|
+
url = safeString(url);
|
|
653
|
+
if (turbo === undefined) turbo = true;
|
|
654
|
+
if (turbo) {
|
|
655
|
+
if (window.Turbo) window.Turbo.visit(url, {
|
|
656
|
+
action: action
|
|
657
|
+
});
|
|
658
|
+
if (window.Turbolinks) window.Turbolinks.visit(url, {
|
|
659
|
+
action: action
|
|
660
|
+
});
|
|
661
|
+
if (!window.Turbo && !window.Turbolinks) window.location.href = url;
|
|
662
|
+
} else {
|
|
663
|
+
window.location.href = url;
|
|
664
|
+
}
|
|
665
|
+
}));
|
|
666
|
+
after(window, operation);
|
|
667
|
+
},
|
|
668
|
+
reload: operation => {
|
|
669
|
+
before(window, operation);
|
|
670
|
+
operate(operation, (() => {
|
|
671
|
+
window.location.reload();
|
|
672
|
+
}));
|
|
673
|
+
after(window, operation);
|
|
674
|
+
},
|
|
675
|
+
removeStorageItem: operation => {
|
|
676
|
+
before(document, operation);
|
|
677
|
+
operate(operation, (() => {
|
|
678
|
+
const {key: key, type: type} = operation;
|
|
679
|
+
const storage = type === "session" ? sessionStorage : localStorage;
|
|
680
|
+
storage.removeItem(safeString(key));
|
|
681
|
+
}));
|
|
682
|
+
after(document, operation);
|
|
683
|
+
},
|
|
684
|
+
replaceState: operation => {
|
|
685
|
+
before(window, operation);
|
|
686
|
+
operate(operation, (() => {
|
|
687
|
+
const {state: state, title: title, url: url} = operation;
|
|
688
|
+
history.replaceState(safeObject(state), safeString(title), safeString(url));
|
|
689
|
+
}));
|
|
690
|
+
after(window, operation);
|
|
691
|
+
},
|
|
692
|
+
scrollIntoView: operation => {
|
|
693
|
+
const {element: element} = operation;
|
|
694
|
+
before(element, operation);
|
|
695
|
+
operate(operation, (() => {
|
|
696
|
+
element.scrollIntoView(operation);
|
|
697
|
+
}));
|
|
698
|
+
after(element, operation);
|
|
699
|
+
},
|
|
700
|
+
setCookie: operation => {
|
|
701
|
+
before(document, operation);
|
|
702
|
+
operate(operation, (() => {
|
|
703
|
+
const {cookie: cookie} = operation;
|
|
704
|
+
document.cookie = safeScalar(cookie);
|
|
705
|
+
}));
|
|
706
|
+
after(document, operation);
|
|
707
|
+
},
|
|
708
|
+
setFocus: operation => {
|
|
709
|
+
const {element: element} = operation;
|
|
710
|
+
before(element, operation);
|
|
711
|
+
operate(operation, (() => {
|
|
712
|
+
assignFocus(element);
|
|
713
|
+
}));
|
|
714
|
+
after(element, operation);
|
|
715
|
+
},
|
|
716
|
+
setStorageItem: operation => {
|
|
717
|
+
before(document, operation);
|
|
718
|
+
operate(operation, (() => {
|
|
719
|
+
const {key: key, value: value, type: type} = operation;
|
|
720
|
+
const storage = type === "session" ? sessionStorage : localStorage;
|
|
721
|
+
storage.setItem(safeString(key), safeScalar(value));
|
|
722
|
+
}));
|
|
723
|
+
after(document, operation);
|
|
724
|
+
},
|
|
725
|
+
// Notifications
|
|
726
|
+
consoleLog: operation => {
|
|
727
|
+
before(document, operation);
|
|
728
|
+
operate(operation, (() => {
|
|
729
|
+
const {message: message, level: level} = operation;
|
|
730
|
+
level && [ "warn", "info", "error" ].includes(level) ? console[level](message) : console.log(message);
|
|
731
|
+
}));
|
|
732
|
+
after(document, operation);
|
|
733
|
+
},
|
|
734
|
+
consoleTable: operation => {
|
|
735
|
+
before(document, operation);
|
|
736
|
+
operate(operation, (() => {
|
|
737
|
+
const {data: data, columns: columns} = operation;
|
|
738
|
+
console.table(data, safeArray(columns));
|
|
739
|
+
}));
|
|
740
|
+
after(document, operation);
|
|
741
|
+
},
|
|
742
|
+
notification: operation => {
|
|
743
|
+
before(document, operation);
|
|
744
|
+
operate(operation, (() => {
|
|
745
|
+
const {title: title, options: options} = operation;
|
|
746
|
+
Notification.requestPermission().then((result => {
|
|
747
|
+
operation.permission = result;
|
|
748
|
+
if (result === "granted") new Notification(safeString(title), safeObject(options));
|
|
749
|
+
}));
|
|
750
|
+
}));
|
|
751
|
+
after(document, operation);
|
|
752
|
+
},
|
|
753
|
+
// Morph operations
|
|
754
|
+
morph: operation => {
|
|
755
|
+
processElements(operation, (element => {
|
|
756
|
+
const {html: html} = operation;
|
|
757
|
+
const template = document.createElement("template");
|
|
758
|
+
template.innerHTML = String(safeScalar(html)).trim();
|
|
759
|
+
operation.content = template.content;
|
|
760
|
+
const parent = element.parentElement;
|
|
761
|
+
const idx = parent && Array.from(parent.children).indexOf(element);
|
|
762
|
+
before(element, operation);
|
|
763
|
+
operate(operation, (() => {
|
|
764
|
+
const {childrenOnly: childrenOnly, focusSelector: focusSelector} = operation;
|
|
765
|
+
morphdom(element, childrenOnly ? template.content : template.innerHTML, {
|
|
766
|
+
childrenOnly: !!childrenOnly,
|
|
767
|
+
onBeforeElUpdated: shouldMorph(operation),
|
|
768
|
+
onElUpdated: didMorph(operation)
|
|
769
|
+
});
|
|
770
|
+
assignFocus(focusSelector);
|
|
771
|
+
}));
|
|
772
|
+
after(parent ? parent.children[idx] : document.documentElement, operation);
|
|
773
|
+
}));
|
|
774
|
+
}
|
|
775
|
+
};
|
|
776
|
+
|
|
777
|
+
let operations = Operations;
|
|
778
|
+
|
|
779
|
+
const add = newOperations => {
|
|
780
|
+
operations = {
|
|
781
|
+
...operations,
|
|
782
|
+
...newOperations
|
|
783
|
+
};
|
|
784
|
+
};
|
|
785
|
+
|
|
786
|
+
const addOperations = operations => {
|
|
787
|
+
add(operations);
|
|
788
|
+
};
|
|
789
|
+
|
|
790
|
+
const addOperation = (name, operation) => {
|
|
791
|
+
const operations = {};
|
|
792
|
+
operations[name] = operation;
|
|
793
|
+
add(operations);
|
|
794
|
+
};
|
|
795
|
+
|
|
796
|
+
var OperationStore = {
|
|
797
|
+
get all() {
|
|
798
|
+
return operations;
|
|
799
|
+
}
|
|
800
|
+
};
|
|
801
|
+
|
|
802
|
+
let missingElement = "warn";
|
|
803
|
+
|
|
804
|
+
var MissingElement$1 = {
|
|
805
|
+
get behavior() {
|
|
806
|
+
return missingElement;
|
|
807
|
+
},
|
|
808
|
+
set(value) {
|
|
809
|
+
if ([ "warn", "ignore", "event", "exception" ].includes(value)) missingElement = value; else console.warn("Invalid 'onMissingElement' option. Defaulting to 'warn'.");
|
|
810
|
+
}
|
|
811
|
+
};
|
|
812
|
+
|
|
813
|
+
const perform = (operations, options = {
|
|
814
|
+
onMissingElement: MissingElement$1.behavior
|
|
815
|
+
}) => {
|
|
816
|
+
const batches = {};
|
|
817
|
+
operations.forEach((operation => {
|
|
818
|
+
if (!!operation.batch) batches[operation.batch] = batches[operation.batch] ? ++batches[operation.batch] : 1;
|
|
819
|
+
}));
|
|
820
|
+
operations.forEach((operation => {
|
|
821
|
+
const name = operation.operation;
|
|
822
|
+
try {
|
|
823
|
+
if (operation.selector) {
|
|
824
|
+
if (operation.xpath) {
|
|
825
|
+
operation.element = operation.selectAll ? xpathToElementArray(operation.selector) : xpathToElement(operation.selector);
|
|
826
|
+
} else {
|
|
827
|
+
operation.element = operation.selectAll ? document.querySelectorAll(operation.selector) : document.querySelector(operation.selector);
|
|
828
|
+
}
|
|
829
|
+
} else {
|
|
830
|
+
operation.element = document;
|
|
831
|
+
}
|
|
832
|
+
if (operation.element || options.onMissingElement !== "ignore") {
|
|
833
|
+
ActiveElement.set(document.activeElement);
|
|
834
|
+
const cableReadyOperation = OperationStore.all[name];
|
|
835
|
+
if (cableReadyOperation) {
|
|
836
|
+
cableReadyOperation(operation);
|
|
837
|
+
if (!!operation.batch && --batches[operation.batch] === 0) dispatch(document, "cable-ready:batch-complete", {
|
|
838
|
+
batch: operation.batch
|
|
839
|
+
});
|
|
840
|
+
} else {
|
|
841
|
+
console.error(`CableReady couldn't find the "${name}" operation. Make sure you use the camelized form when calling an operation method.`);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
} catch (e) {
|
|
845
|
+
if (operation.element) {
|
|
846
|
+
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.`);
|
|
847
|
+
console.error(e);
|
|
848
|
+
} else {
|
|
849
|
+
const warning = `CableReady ${name || ""} operation failed due to missing DOM element for selector: '${operation.selector}'`;
|
|
850
|
+
switch (options.onMissingElement) {
|
|
851
|
+
case "ignore":
|
|
852
|
+
break;
|
|
853
|
+
|
|
854
|
+
case "event":
|
|
855
|
+
dispatch(document, "cable-ready:missing-element", {
|
|
856
|
+
warning: warning,
|
|
857
|
+
operation: operation
|
|
858
|
+
});
|
|
859
|
+
break;
|
|
860
|
+
|
|
861
|
+
case "exception":
|
|
862
|
+
throw warning;
|
|
863
|
+
|
|
864
|
+
default:
|
|
865
|
+
console.warn(warning);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}));
|
|
870
|
+
};
|
|
871
|
+
|
|
872
|
+
const performAsync = (operations, options = {
|
|
873
|
+
onMissingElement: MissingElement$1.behavior
|
|
874
|
+
}) => new Promise(((resolve, reject) => {
|
|
875
|
+
try {
|
|
876
|
+
resolve(perform(operations, options));
|
|
877
|
+
} catch (err) {
|
|
878
|
+
reject(err);
|
|
879
|
+
}
|
|
880
|
+
}));
|
|
881
|
+
|
|
882
|
+
class SubscribingElement extends HTMLElement {
|
|
883
|
+
static get tagName() {
|
|
884
|
+
throw new Error("Implement the tagName() getter in the inheriting class");
|
|
885
|
+
}
|
|
886
|
+
static define() {
|
|
887
|
+
if (!customElements.get(this.tagName)) {
|
|
888
|
+
customElements.define(this.tagName, this);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
disconnectedCallback() {
|
|
892
|
+
if (this.channel) this.channel.unsubscribe();
|
|
893
|
+
}
|
|
894
|
+
createSubscription(consumer, channel, receivedCallback) {
|
|
895
|
+
this.channel = consumer.subscriptions.create({
|
|
896
|
+
channel: channel,
|
|
897
|
+
identifier: this.identifier
|
|
898
|
+
}, {
|
|
899
|
+
received: receivedCallback
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
get preview() {
|
|
903
|
+
return document.documentElement.hasAttribute("data-turbolinks-preview") || document.documentElement.hasAttribute("data-turbo-preview");
|
|
904
|
+
}
|
|
905
|
+
get identifier() {
|
|
906
|
+
return this.getAttribute("identifier");
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
let consumer;
|
|
911
|
+
|
|
912
|
+
const BACKOFF = [ 25, 50, 75, 100, 200, 250, 500, 800, 1e3, 2e3 ];
|
|
913
|
+
|
|
914
|
+
const wait = ms => new Promise((resolve => setTimeout(resolve, ms)));
|
|
915
|
+
|
|
916
|
+
const getConsumerWithRetry = async (retry = 0) => {
|
|
917
|
+
if (consumer) return consumer;
|
|
918
|
+
if (retry >= BACKOFF.length) {
|
|
919
|
+
throw new Error("Couldn't obtain a Action Cable consumer within 5s");
|
|
920
|
+
}
|
|
921
|
+
await wait(BACKOFF[retry]);
|
|
922
|
+
return await getConsumerWithRetry(retry + 1);
|
|
923
|
+
};
|
|
924
|
+
|
|
925
|
+
var CableConsumer = {
|
|
926
|
+
setConsumer(value) {
|
|
927
|
+
consumer = value;
|
|
928
|
+
},
|
|
929
|
+
get consumer() {
|
|
930
|
+
return consumer;
|
|
931
|
+
},
|
|
932
|
+
async getConsumer() {
|
|
933
|
+
return await getConsumerWithRetry();
|
|
934
|
+
}
|
|
935
|
+
};
|
|
936
|
+
|
|
937
|
+
class StreamFromElement extends SubscribingElement {
|
|
938
|
+
static get tagName() {
|
|
939
|
+
return "cable-ready-stream-from";
|
|
940
|
+
}
|
|
941
|
+
async connectedCallback() {
|
|
942
|
+
if (this.preview) return;
|
|
943
|
+
const consumer = await CableConsumer.getConsumer();
|
|
944
|
+
if (consumer) {
|
|
945
|
+
this.createSubscription(consumer, "CableReady::Stream", this.performOperations.bind(this));
|
|
946
|
+
} else {
|
|
947
|
+
console.error("The `cable_ready_stream_from` helper cannot connect. You must initialize CableReady with an Action Cable consumer.");
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
performOperations(data) {
|
|
951
|
+
if (data.cableReady) perform(data.operations, {
|
|
952
|
+
onMissingElement: this.onMissingElement
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
get onMissingElement() {
|
|
956
|
+
const value = this.getAttribute("missing") || MissingElement$1.behavior;
|
|
957
|
+
// stream_from does not support raising exceptions on missing elements because there's no way to catch them
|
|
958
|
+
if ([ "warn", "ignore", "event" ].includes(value)) return value; else {
|
|
959
|
+
console.warn("Invalid 'missing' attribute. Defaulting to 'warn'.");
|
|
960
|
+
return "warn";
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
let debugging = false;
|
|
966
|
+
|
|
967
|
+
var Debug = {
|
|
968
|
+
get enabled() {
|
|
969
|
+
return debugging;
|
|
970
|
+
},
|
|
971
|
+
get disabled() {
|
|
972
|
+
return !debugging;
|
|
973
|
+
},
|
|
974
|
+
get value() {
|
|
975
|
+
return debugging;
|
|
976
|
+
},
|
|
977
|
+
set(value) {
|
|
978
|
+
debugging = !!value;
|
|
979
|
+
},
|
|
980
|
+
set debug(value) {
|
|
981
|
+
debugging = !!value;
|
|
982
|
+
}
|
|
983
|
+
};
|
|
984
|
+
|
|
985
|
+
const request = (data, blocks) => {
|
|
986
|
+
if (Debug.disabled) return;
|
|
987
|
+
console.log(`↑ Updatable request affecting ${blocks.length} element(s): `, {
|
|
988
|
+
elements: blocks.map((b => b.element)),
|
|
989
|
+
identifiers: blocks.map((b => b.element.getAttribute("identifier"))),
|
|
990
|
+
data: data
|
|
991
|
+
});
|
|
992
|
+
};
|
|
993
|
+
|
|
994
|
+
const cancel = (timestamp, reason) => {
|
|
995
|
+
if (Debug.disabled) return;
|
|
996
|
+
const duration = new Date - timestamp;
|
|
997
|
+
console.log(`❌ Updatable request canceled after ${duration}ms: ${reason}`);
|
|
998
|
+
};
|
|
999
|
+
|
|
1000
|
+
const response = (timestamp, element, urls) => {
|
|
1001
|
+
if (Debug.disabled) return;
|
|
1002
|
+
const duration = new Date - timestamp;
|
|
1003
|
+
console.log(`↓ Updatable response: All URLs fetched in ${duration}ms`, {
|
|
1004
|
+
element: element,
|
|
1005
|
+
urls: urls
|
|
1006
|
+
});
|
|
1007
|
+
};
|
|
1008
|
+
|
|
1009
|
+
const morphStart = (timestamp, element) => {
|
|
1010
|
+
if (Debug.disabled) return;
|
|
1011
|
+
const duration = new Date - timestamp;
|
|
1012
|
+
console.log(`↻ Updatable morph: starting after ${duration}ms`, {
|
|
1013
|
+
element: element
|
|
1014
|
+
});
|
|
1015
|
+
};
|
|
1016
|
+
|
|
1017
|
+
const morphEnd = (timestamp, element) => {
|
|
1018
|
+
if (Debug.disabled) return;
|
|
1019
|
+
const duration = new Date - timestamp;
|
|
1020
|
+
console.log(`↺ Updatable morph: completed after ${duration}ms`, {
|
|
1021
|
+
element: element
|
|
1022
|
+
});
|
|
1023
|
+
};
|
|
1024
|
+
|
|
1025
|
+
var Log = {
|
|
1026
|
+
request: request,
|
|
1027
|
+
cancel: cancel,
|
|
1028
|
+
response: response,
|
|
1029
|
+
morphStart: morphStart,
|
|
1030
|
+
morphEnd: morphEnd
|
|
1031
|
+
};
|
|
1032
|
+
|
|
1033
|
+
const template = `\n<style>\n :host {\n display: block;\n }\n</style>\n<slot></slot>\n`;
|
|
1034
|
+
|
|
1035
|
+
class UpdatesForElement extends SubscribingElement {
|
|
1036
|
+
static get tagName() {
|
|
1037
|
+
return "cable-ready-updates-for";
|
|
1038
|
+
}
|
|
1039
|
+
constructor() {
|
|
1040
|
+
super();
|
|
1041
|
+
const shadowRoot = this.attachShadow({
|
|
1042
|
+
mode: "open"
|
|
1043
|
+
});
|
|
1044
|
+
shadowRoot.innerHTML = template;
|
|
1045
|
+
}
|
|
1046
|
+
async connectedCallback() {
|
|
1047
|
+
if (this.preview) return;
|
|
1048
|
+
this.update = debounce(this.update.bind(this), this.debounce);
|
|
1049
|
+
const consumer = await CableConsumer.getConsumer();
|
|
1050
|
+
if (consumer) {
|
|
1051
|
+
this.createSubscription(consumer, "CableReady::Stream", this.update);
|
|
1052
|
+
} else {
|
|
1053
|
+
console.error("The `cable_ready_updates_for` helper cannot connect. You must initialize CableReady with an Action Cable consumer.");
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
async update(data) {
|
|
1057
|
+
this.lastUpdateTimestamp = new Date;
|
|
1058
|
+
const blocks = Array.from(document.querySelectorAll(this.query), (element => new Block(element))).filter((block => block.shouldUpdate(data)));
|
|
1059
|
+
Log.request(data, blocks);
|
|
1060
|
+
if (blocks.length === 0) {
|
|
1061
|
+
Log.cancel(this.lastUpdateTimestamp, "All elements filtered out");
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
// first <cable-ready-updates-for> element in the DOM *at any given moment* updates all of the others
|
|
1065
|
+
if (blocks[0].element !== this) {
|
|
1066
|
+
Log.cancel(this.lastUpdateTimestamp, "Update already requested");
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
// hold a reference to the active element so that it can be restored after the morph
|
|
1070
|
+
ActiveElement.set(document.activeElement);
|
|
1071
|
+
// store all retrieved HTML in an object keyed by URL to minimize fetch calls
|
|
1072
|
+
this.html = {};
|
|
1073
|
+
const uniqueUrls = [ ...new Set(blocks.map((block => block.url))) ];
|
|
1074
|
+
await Promise.all(uniqueUrls.map((async url => {
|
|
1075
|
+
if (!this.html.hasOwnProperty(url)) {
|
|
1076
|
+
const response = await graciouslyFetch(url, {
|
|
1077
|
+
"X-Cable-Ready": "update"
|
|
1078
|
+
});
|
|
1079
|
+
this.html[url] = await response.text();
|
|
1080
|
+
}
|
|
1081
|
+
})));
|
|
1082
|
+
Log.response(this.lastUpdateTimestamp, this, uniqueUrls);
|
|
1083
|
+
// track current block index for each URL; referred to as fragments
|
|
1084
|
+
this.index = {};
|
|
1085
|
+
blocks.forEach((block => {
|
|
1086
|
+
// if the block's URL is not in the index, initialize it to 0; otherwise, increment it
|
|
1087
|
+
this.index.hasOwnProperty(block.url) ? this.index[block.url]++ : this.index[block.url] = 0;
|
|
1088
|
+
block.process(data, this.html, this.index, this.lastUpdateTimestamp);
|
|
1089
|
+
}));
|
|
1090
|
+
}
|
|
1091
|
+
get query() {
|
|
1092
|
+
return `${this.tagName}[identifier="${this.identifier}"]`;
|
|
1093
|
+
}
|
|
1094
|
+
get identifier() {
|
|
1095
|
+
return this.getAttribute("identifier");
|
|
1096
|
+
}
|
|
1097
|
+
get debounce() {
|
|
1098
|
+
return this.hasAttribute("debounce") ? parseInt(this.getAttribute("debounce")) : 20;
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
class Block {
|
|
1103
|
+
constructor(element) {
|
|
1104
|
+
this.element = element;
|
|
1105
|
+
}
|
|
1106
|
+
async process(data, html, index, startTimestamp) {
|
|
1107
|
+
const blockIndex = index[this.url];
|
|
1108
|
+
const template = document.createElement("template");
|
|
1109
|
+
this.element.setAttribute("updating", "updating");
|
|
1110
|
+
template.innerHTML = String(html[this.url]).trim();
|
|
1111
|
+
await this.resolveTurboFrames(template.content);
|
|
1112
|
+
const fragments = template.content.querySelectorAll(this.query);
|
|
1113
|
+
if (fragments.length <= blockIndex) {
|
|
1114
|
+
console.warn(`Update aborted due to insufficient number of elements. The offending url is ${this.url}.`);
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
const operation = {
|
|
1118
|
+
element: this.element,
|
|
1119
|
+
html: fragments[blockIndex],
|
|
1120
|
+
permanentAttributeName: "data-ignore-updates"
|
|
1121
|
+
};
|
|
1122
|
+
dispatch(this.element, "cable-ready:before-update", operation);
|
|
1123
|
+
Log.morphStart(startTimestamp, this.element);
|
|
1124
|
+
morphdom(this.element, fragments[blockIndex], {
|
|
1125
|
+
childrenOnly: true,
|
|
1126
|
+
onBeforeElUpdated: shouldMorph(operation),
|
|
1127
|
+
onElUpdated: _ => {
|
|
1128
|
+
this.element.removeAttribute("updating");
|
|
1129
|
+
dispatch(this.element, "cable-ready:after-update", operation);
|
|
1130
|
+
assignFocus(operation.focusSelector);
|
|
1131
|
+
}
|
|
1132
|
+
});
|
|
1133
|
+
Log.morphEnd(startTimestamp, this.element);
|
|
1134
|
+
}
|
|
1135
|
+
async resolveTurboFrames(documentFragment) {
|
|
1136
|
+
const reloadingTurboFrames = [ ...documentFragment.querySelectorAll('turbo-frame[src]:not([loading="lazy"])') ];
|
|
1137
|
+
return Promise.all(reloadingTurboFrames.map((frame => new Promise((async resolve => {
|
|
1138
|
+
const frameResponse = await graciouslyFetch(frame.getAttribute("src"), {
|
|
1139
|
+
"Turbo-Frame": frame.id,
|
|
1140
|
+
"X-Cable-Ready": "update"
|
|
1141
|
+
});
|
|
1142
|
+
const frameTemplate = document.createElement("template");
|
|
1143
|
+
frameTemplate.innerHTML = await frameResponse.text();
|
|
1144
|
+
// recurse here to get all nested eager loaded frames
|
|
1145
|
+
await this.resolveTurboFrames(frameTemplate.content);
|
|
1146
|
+
const selector = `turbo-frame#${frame.id}`;
|
|
1147
|
+
const frameContent = frameTemplate.content.querySelector(selector);
|
|
1148
|
+
const content = frameContent ? frameContent.innerHTML.trim() : "";
|
|
1149
|
+
documentFragment.querySelector(selector).innerHTML = content;
|
|
1150
|
+
resolve();
|
|
1151
|
+
})))));
|
|
1152
|
+
}
|
|
1153
|
+
shouldUpdate(data) {
|
|
1154
|
+
// if everything that could prevent an update is false, update this block
|
|
1155
|
+
return !this.ignoresInnerUpdates && this.hasChangesSelectedForUpdate(data);
|
|
1156
|
+
}
|
|
1157
|
+
hasChangesSelectedForUpdate(data) {
|
|
1158
|
+
// if there's an only attribute, only update if at least one of the attributes changed is in the allow list
|
|
1159
|
+
const only = this.element.getAttribute("only");
|
|
1160
|
+
return !(only && data.changed && !only.split(" ").some((attribute => data.changed.includes(attribute))));
|
|
1161
|
+
}
|
|
1162
|
+
get ignoresInnerUpdates() {
|
|
1163
|
+
// don't update during a Reflex or Turbolinks redraw
|
|
1164
|
+
return this.element.hasAttribute("ignore-inner-updates") && this.element.hasAttribute("performing-inner-update");
|
|
1165
|
+
}
|
|
1166
|
+
get url() {
|
|
1167
|
+
return this.element.hasAttribute("url") ? this.element.getAttribute("url") : location.href;
|
|
1168
|
+
}
|
|
1169
|
+
get identifier() {
|
|
1170
|
+
return this.element.identifier;
|
|
1171
|
+
}
|
|
1172
|
+
get query() {
|
|
1173
|
+
return this.element.query;
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
const registerInnerUpdates = () => {
|
|
1178
|
+
document.addEventListener("stimulus-reflex:before", (event => {
|
|
1179
|
+
recursiveMarkUpdatesForElements(event.detail.element);
|
|
1180
|
+
}));
|
|
1181
|
+
document.addEventListener("stimulus-reflex:after", (event => {
|
|
1182
|
+
setTimeout((() => {
|
|
1183
|
+
recursiveUnmarkUpdatesForElements(event.detail.element);
|
|
1184
|
+
}));
|
|
1185
|
+
}));
|
|
1186
|
+
document.addEventListener("turbo:submit-start", (event => {
|
|
1187
|
+
recursiveMarkUpdatesForElements(event.target);
|
|
1188
|
+
}));
|
|
1189
|
+
document.addEventListener("turbo:submit-end", (event => {
|
|
1190
|
+
setTimeout((() => {
|
|
1191
|
+
recursiveUnmarkUpdatesForElements(event.target);
|
|
1192
|
+
}));
|
|
1193
|
+
}));
|
|
1194
|
+
document.addEventListener("turbo-boost:command:start", (event => {
|
|
1195
|
+
recursiveMarkUpdatesForElements(event.target);
|
|
1196
|
+
}));
|
|
1197
|
+
document.addEventListener("turbo-boost:command:finish", (event => {
|
|
1198
|
+
setTimeout((() => {
|
|
1199
|
+
recursiveUnmarkUpdatesForElements(event.target);
|
|
1200
|
+
}));
|
|
1201
|
+
}));
|
|
1202
|
+
document.addEventListener("turbo-boost:command:error", (event => {
|
|
1203
|
+
setTimeout((() => {
|
|
1204
|
+
recursiveUnmarkUpdatesForElements(event.target);
|
|
1205
|
+
}));
|
|
1206
|
+
}));
|
|
1207
|
+
};
|
|
1208
|
+
|
|
1209
|
+
const recursiveMarkUpdatesForElements = leaf => {
|
|
1210
|
+
const closestUpdatesFor = leaf && leaf.parentElement && leaf.parentElement.closest("cable-ready-updates-for");
|
|
1211
|
+
if (closestUpdatesFor) {
|
|
1212
|
+
closestUpdatesFor.setAttribute("performing-inner-update", "");
|
|
1213
|
+
recursiveMarkUpdatesForElements(closestUpdatesFor);
|
|
1214
|
+
}
|
|
1215
|
+
};
|
|
1216
|
+
|
|
1217
|
+
const recursiveUnmarkUpdatesForElements = leaf => {
|
|
1218
|
+
const closestUpdatesFor = leaf && leaf.parentElement && leaf.parentElement.closest("cable-ready-updates-for");
|
|
1219
|
+
if (closestUpdatesFor) {
|
|
1220
|
+
closestUpdatesFor.removeAttribute("performing-inner-update");
|
|
1221
|
+
recursiveUnmarkUpdatesForElements(closestUpdatesFor);
|
|
1222
|
+
}
|
|
1223
|
+
};
|
|
1224
|
+
|
|
1225
|
+
const defineElements = () => {
|
|
1226
|
+
registerInnerUpdates();
|
|
1227
|
+
StreamFromElement.define();
|
|
1228
|
+
UpdatesForElement.define();
|
|
1229
|
+
};
|
|
1230
|
+
|
|
1231
|
+
const initialize = (initializeOptions = {}) => {
|
|
1232
|
+
const {consumer: consumer, onMissingElement: onMissingElement, debug: debug} = initializeOptions;
|
|
1233
|
+
Debug.set(!!debug);
|
|
1234
|
+
if (consumer) {
|
|
1235
|
+
CableConsumer.setConsumer(consumer);
|
|
1236
|
+
} else {
|
|
1237
|
+
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 })`.");
|
|
1238
|
+
}
|
|
1239
|
+
if (onMissingElement) {
|
|
1240
|
+
MissingElement.set(onMissingElement);
|
|
1241
|
+
}
|
|
1242
|
+
defineElements();
|
|
1243
|
+
};
|
|
1244
|
+
|
|
1245
|
+
const global = {
|
|
1246
|
+
perform: perform,
|
|
1247
|
+
performAsync: performAsync,
|
|
1248
|
+
shouldMorphCallbacks: shouldMorphCallbacks,
|
|
1249
|
+
didMorphCallbacks: didMorphCallbacks,
|
|
1250
|
+
initialize: initialize,
|
|
1251
|
+
addOperation: addOperation,
|
|
1252
|
+
addOperations: addOperations,
|
|
1253
|
+
version: packageInfo.version,
|
|
1254
|
+
cable: CableConsumer,
|
|
1255
|
+
get DOMOperations() {
|
|
1256
|
+
console.warn("DEPRECATED: Please use `CableReady.operations` instead of `CableReady.DOMOperations`");
|
|
1257
|
+
return OperationStore.all;
|
|
1258
|
+
},
|
|
1259
|
+
get operations() {
|
|
1260
|
+
return OperationStore.all;
|
|
1261
|
+
},
|
|
1262
|
+
get consumer() {
|
|
1263
|
+
return CableConsumer.consumer;
|
|
1264
|
+
}
|
|
1265
|
+
};
|
|
1266
|
+
|
|
1267
|
+
window.CableReady = global;
|
|
1268
|
+
|
|
1269
|
+
export { morph_callbacks as MorphCallbacks, StreamFromElement, SubscribingElement, UpdatesForElement, utils as Utils, global as default };
|