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