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