qube_rails 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +161 -0
- data/LICENSE.txt +21 -0
- data/README.md +967 -0
- data/app/assets/javascripts/qube_rails/question.js +457 -0
- data/app/assets/stylesheets/qube_rails.css +1115 -0
- data/app/controllers/concerns/qube_rails/run_authorization.rb +37 -0
- data/app/controllers/qube_rails/connections_controller.rb +141 -0
- data/app/controllers/qube_rails/questions_controller.rb +132 -0
- data/app/controllers/qube_rails/runs_controller.rb +54 -0
- data/app/controllers/qube_rails/storyboard_controller.rb +50 -0
- data/app/controllers/qube_rails/webhooks_controller.rb +99 -0
- data/app/jobs/qube_rails/prune_runs_job.rb +18 -0
- data/app/jobs/qube_rails/refresh_job.rb +18 -0
- data/app/jobs/qube_rails/webhook_job.rb +43 -0
- data/app/models/qube_rails/application_record.rb +17 -0
- data/app/models/qube_rails/connection.rb +303 -0
- data/app/models/qube_rails/notification.rb +12 -0
- data/app/models/qube_rails/queued_request.rb +66 -0
- data/app/models/qube_rails/quickbooks_record.rb +36 -0
- data/app/models/qube_rails/request_delivery.rb +12 -0
- data/app/models/qube_rails/run.rb +187 -0
- data/app/models/qube_rails/sync_mark.rb +21 -0
- data/app/views/qube_rails/_error.html.erb +13 -0
- data/app/views/qube_rails/_question.html.erb +21 -0
- data/app/views/qube_rails/_run_status.html.erb +22 -0
- data/app/views/qube_rails/question/_choice.html.erb +20 -0
- data/app/views/qube_rails/question/_details.html.erb +6 -0
- data/app/views/qube_rails/question/_field.html.erb +15 -0
- data/app/views/qube_rails/question/_footer.html.erb +7 -0
- data/app/views/qube_rails/question/_message.html.erb +2 -0
- data/app/views/qube_rails/question/_pick.html.erb +19 -0
- data/app/views/qube_rails/question/_picks.html.erb +6 -0
- data/app/views/qube_rails/question/_question.html.erb +24 -0
- data/app/views/qube_rails/question/fields/_boolean.html.erb +7 -0
- data/app/views/qube_rails/question/fields/_number.html.erb +7 -0
- data/app/views/qube_rails/question/fields/_rows.html.erb +23 -0
- data/app/views/qube_rails/question/fields/_search.html.erb +23 -0
- data/app/views/qube_rails/question/fields/_select.html.erb +8 -0
- data/app/views/qube_rails/question/fields/_text.html.erb +7 -0
- data/app/views/qube_rails/question/rows/_cell.html.erb +24 -0
- data/app/views/qube_rails/question/rows/_row.html.erb +22 -0
- data/app/views/qube_rails/question/rows/_segmented.html.erb +14 -0
- data/app/views/qube_rails/run_status/_detail.html.erb +7 -0
- data/app/views/qube_rails/run_status/_redeliver.html.erb +5 -0
- data/app/views/qube_rails/run_status/_retry.html.erb +4 -0
- data/app/views/qube_rails/run_status/_stall.html.erb +3 -0
- data/app/views/qube_rails/run_status/_status.html.erb +21 -0
- data/app/views/qube_rails/run_status/_summary.html.erb +2 -0
- data/app/views/qube_rails/storyboard/show.html.erb +86 -0
- data/config/locales/en.yml +46 -0
- data/config/routes.rb +35 -0
- data/db/migrate/20261025000000_create_qube_tables.rb +83 -0
- data/db/migrate/20261026230000_add_qube_sync_status_indexes.rb +13 -0
- data/db/migrate/20261027000000_create_qube_sync_marks.rb +17 -0
- data/db/migrate/20261028000000_create_qube_requests.rb +38 -0
- data/lib/generators/qube_rails/install_generator.rb +47 -0
- data/lib/generators/qube_rails/templates/connection_events.rb.tt +38 -0
- data/lib/generators/qube_rails/templates/initializer.rb.tt +82 -0
- data/lib/generators/qube_rails/workflows_generator.rb +77 -0
- data/lib/qube_rails/backend.rb +70 -0
- data/lib/qube_rails/cache_dedupe.rb +34 -0
- data/lib/qube_rails/configuration.rb +319 -0
- data/lib/qube_rails/connection_events.rb +167 -0
- data/lib/qube_rails/database_dedupe.rb +28 -0
- data/lib/qube_rails/engine.rb +33 -0
- data/lib/qube_rails/json_coder.rb +43 -0
- data/lib/qube_rails/operation.rb +464 -0
- data/lib/qube_rails/question_view.rb +219 -0
- data/lib/qube_rails/request.rb +289 -0
- data/lib/qube_rails/rows_view.rb +146 -0
- data/lib/qube_rails/run_status_view.rb +71 -0
- data/lib/qube_rails/storyboard/ask_about_add.json +60 -0
- data/lib/qube_rails/storyboard/choose.json +51 -0
- data/lib/qube_rails/storyboard/enum_select.json +56 -0
- data/lib/qube_rails/storyboard/missing_record.json +141 -0
- data/lib/qube_rails/storyboard/name_clash.json +59 -0
- data/lib/qube_rails/storyboard/near_match.json +77 -0
- data/lib/qube_rails/storyboard/number_boolean.json +54 -0
- data/lib/qube_rails/storyboard/record_search.json +90 -0
- data/lib/qube_rails/storyboard/refusal.json +53 -0
- data/lib/qube_rails/storyboard/rows.json +359 -0
- data/lib/qube_rails/storyboard.rb +165 -0
- data/lib/qube_rails/sync_status.rb +124 -0
- data/lib/qube_rails/version.rb +5 -0
- data/lib/qube_rails/view_helpers.rb +29 -0
- data/lib/qube_rails.rb +201 -0
- metadata +180 -0
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
// qube_rails: the behaviour of a QuickBooks question (search boxes, a rows field) and the
|
|
2
|
+
// storyboard's copy buttons. Listeners sit on document and key only on data-qube-* attributes, so
|
|
3
|
+
// any markup carrying them works, after Turbo navigation too. Plain JavaScript, no build. README,
|
|
4
|
+
// "Styling", has the attribute contract.
|
|
5
|
+
(function () {
|
|
6
|
+
if (window.qubeRailsQuestion) return; // loaded once per page, however often it's included
|
|
7
|
+
window.qubeRailsQuestion = true;
|
|
8
|
+
|
|
9
|
+
var each = function (list, fn) { Array.prototype.forEach.call(list, fn); };
|
|
10
|
+
var timers = new WeakMap();
|
|
11
|
+
var slow = new WeakMap();
|
|
12
|
+
var asked = new WeakMap();
|
|
13
|
+
var uid = 0;
|
|
14
|
+
|
|
15
|
+
// A word from the element's data-qube-text-<name> (the app's I18n), else English.
|
|
16
|
+
var words = function (el, name, fallback) {
|
|
17
|
+
return (el && el.getAttribute("data-qube-text-" + name)) || fallback;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// ---- Search: [data-qube-search] holds [data-qube-search-input], [data-qube-search-value] and
|
|
21
|
+
// [data-qube-search-results]. It's a combobox: arrows move through the matches, Enter picks,
|
|
22
|
+
// Escape or leaving the box closes them.
|
|
23
|
+
|
|
24
|
+
var parts = function (box) {
|
|
25
|
+
return {
|
|
26
|
+
input: box.querySelector("[data-qube-search-input]"),
|
|
27
|
+
value: box.querySelector("[data-qube-search-value]"),
|
|
28
|
+
results: box.querySelector("[data-qube-search-results]")
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Until a match is picked, the form won't send what was typed.
|
|
33
|
+
var check = function (box, p) {
|
|
34
|
+
var unpicked = p.input.value && !p.value.value;
|
|
35
|
+
p.input.setCustomValidity(unpicked ? words(box, "pick-a-match", "Pick one of the matches.") : "");
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
var searchUrl = function (box, q) {
|
|
39
|
+
var url = box.getAttribute("data-qube-search");
|
|
40
|
+
url += (url.indexOf("?") === -1 ? "?" : "&") +
|
|
41
|
+
"field=" + encodeURIComponent(box.getAttribute("data-qube-field") || "") +
|
|
42
|
+
"&q=" + encodeURIComponent(q);
|
|
43
|
+
var row = box.getAttribute("data-qube-row");
|
|
44
|
+
if (row) url += "&row=" + encodeURIComponent(row);
|
|
45
|
+
return url;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
var options = function (p) {
|
|
49
|
+
return p.results.querySelectorAll("[data-qube-search-result]");
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
var highlighted = function (p) {
|
|
53
|
+
return p.results.querySelector('[data-qube-search-result][aria-selected="true"]');
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// Highlights one match (or none): instant, no animation, scrolled into view from the keyboard.
|
|
57
|
+
var highlight = function (p, option, scroll) {
|
|
58
|
+
each(options(p), function (o) { o.setAttribute("aria-selected", o === option ? "true" : "false"); });
|
|
59
|
+
if (option) {
|
|
60
|
+
p.input.setAttribute("aria-activedescendant", option.id);
|
|
61
|
+
if (scroll && option.scrollIntoView) option.scrollIntoView({ block: "nearest" });
|
|
62
|
+
} else {
|
|
63
|
+
p.input.removeAttribute("aria-activedescendant");
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
var setOpen = function (box, p, open, listbox) {
|
|
68
|
+
if (open) box.setAttribute("data-qube-open", "");
|
|
69
|
+
else box.removeAttribute("data-qube-open");
|
|
70
|
+
p.input.setAttribute("aria-expanded", listbox ? "true" : "false");
|
|
71
|
+
if (listbox) p.results.setAttribute("role", "listbox");
|
|
72
|
+
else p.results.removeAttribute("role");
|
|
73
|
+
if (!listbox) p.input.removeAttribute("aria-activedescendant");
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
var stopTimers = function (box) {
|
|
77
|
+
clearTimeout(timers.get(box));
|
|
78
|
+
clearTimeout(slow.get(box));
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// Closes the matches at once; an answer still on its way is dropped.
|
|
82
|
+
var close = function (box) {
|
|
83
|
+
var p = parts(box);
|
|
84
|
+
if (!p.input || !p.results) return;
|
|
85
|
+
stopTimers(box);
|
|
86
|
+
asked.set(box, (asked.get(box) || 0) + 1);
|
|
87
|
+
p.results.textContent = "";
|
|
88
|
+
setOpen(box, p, false, false);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// A line in place of the matches: "No matches", "Searching…", or that the search failed.
|
|
92
|
+
var note = function (box, p, text, kind) {
|
|
93
|
+
var line = document.createElement("div");
|
|
94
|
+
line.setAttribute("data-qube-search-note", kind);
|
|
95
|
+
line.textContent = text;
|
|
96
|
+
p.results.textContent = "";
|
|
97
|
+
p.results.appendChild(line);
|
|
98
|
+
setOpen(box, p, true, false);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
var showMatches = function (box, p, matches) {
|
|
102
|
+
if (!matches.length) return note(box, p, words(box, "no-matches", "No matches"), "empty");
|
|
103
|
+
if (!p.results.id) p.results.id = "qube-search-results-" + (++uid);
|
|
104
|
+
p.input.setAttribute("aria-controls", p.results.id);
|
|
105
|
+
var className = box.getAttribute("data-qube-result-class") || "qube-search-result";
|
|
106
|
+
p.results.textContent = "";
|
|
107
|
+
matches.forEach(function (match, n) {
|
|
108
|
+
var pick = document.createElement("button");
|
|
109
|
+
pick.type = "button";
|
|
110
|
+
pick.tabIndex = -1;
|
|
111
|
+
pick.id = p.results.id + "-" + n;
|
|
112
|
+
pick.className = className;
|
|
113
|
+
pick.setAttribute("role", "option");
|
|
114
|
+
pick.setAttribute("aria-selected", "false");
|
|
115
|
+
pick.setAttribute("data-qube-search-result", "");
|
|
116
|
+
pick.setAttribute("data-value", match.value);
|
|
117
|
+
pick.setAttribute("data-label", match.label);
|
|
118
|
+
var label = document.createElement("span");
|
|
119
|
+
label.textContent = match.label;
|
|
120
|
+
pick.appendChild(label);
|
|
121
|
+
if (match.detail) {
|
|
122
|
+
var detail = document.createElement("span");
|
|
123
|
+
detail.setAttribute("data-qube-search-detail", "");
|
|
124
|
+
detail.textContent = match.detail;
|
|
125
|
+
pick.appendChild(detail);
|
|
126
|
+
}
|
|
127
|
+
p.results.appendChild(pick);
|
|
128
|
+
});
|
|
129
|
+
setOpen(box, p, true, true);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
var search = function (box) {
|
|
133
|
+
var p = parts(box);
|
|
134
|
+
if (!p.input || !p.value || !p.results) return;
|
|
135
|
+
p.value.value = "";
|
|
136
|
+
check(box, p);
|
|
137
|
+
stopTimers(box);
|
|
138
|
+
var n = (asked.get(box) || 0) + 1;
|
|
139
|
+
asked.set(box, n);
|
|
140
|
+
var q = p.input.value.trim();
|
|
141
|
+
if (q.length < 2) {
|
|
142
|
+
p.results.textContent = "";
|
|
143
|
+
setOpen(box, p, false, false);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
// The matches shown stay until the new ones come; "Searching…" only for a slow answer.
|
|
147
|
+
timers.set(box, setTimeout(function () {
|
|
148
|
+
slow.set(box, setTimeout(function () {
|
|
149
|
+
if (asked.get(box) === n) note(box, p, words(box, "searching", "Searching…"), "loading");
|
|
150
|
+
}, 150));
|
|
151
|
+
fetch(searchUrl(box, q), { headers: { Accept: "application/json" }, credentials: "same-origin" })
|
|
152
|
+
.then(function (res) { return res.ok ? res.json() : []; })
|
|
153
|
+
.then(function (matches) {
|
|
154
|
+
if (asked.get(box) !== n) return; // a later search, or a close, since
|
|
155
|
+
clearTimeout(slow.get(box));
|
|
156
|
+
showMatches(box, p, matches);
|
|
157
|
+
})
|
|
158
|
+
.catch(function () {
|
|
159
|
+
if (asked.get(box) !== n) return;
|
|
160
|
+
clearTimeout(slow.get(box));
|
|
161
|
+
note(box, p, words(box, "search-failed", "Search isn't available right now."), "failed");
|
|
162
|
+
});
|
|
163
|
+
}, 250));
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
var pick = function (button) {
|
|
167
|
+
var box = button.closest("[data-qube-search]");
|
|
168
|
+
var p = parts(box);
|
|
169
|
+
p.value.value = button.getAttribute("data-value");
|
|
170
|
+
p.input.value = button.getAttribute("data-label");
|
|
171
|
+
close(box);
|
|
172
|
+
check(box, p);
|
|
173
|
+
p.value.dispatchEvent(new Event("change", { bubbles: true }));
|
|
174
|
+
box.dispatchEvent(new CustomEvent("qube:picked", { bubbles: true }));
|
|
175
|
+
gate(box.closest("form"));
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
var keys = function (event, box) {
|
|
179
|
+
var p = parts(box);
|
|
180
|
+
if (!p.results) return;
|
|
181
|
+
var list = options(p);
|
|
182
|
+
var current = highlighted(p);
|
|
183
|
+
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
184
|
+
if (!list.length) return;
|
|
185
|
+
event.preventDefault();
|
|
186
|
+
var at = Array.prototype.indexOf.call(list, current);
|
|
187
|
+
var step = event.key === "ArrowDown" ? 1 : -1;
|
|
188
|
+
var next = at === -1 ? (step === 1 ? 0 : list.length - 1) : (at + step + list.length) % list.length;
|
|
189
|
+
highlight(p, list[next], true);
|
|
190
|
+
} else if (event.key === "Enter") {
|
|
191
|
+
if (current) {
|
|
192
|
+
event.preventDefault(); // picks, rather than sending the form
|
|
193
|
+
pick(current);
|
|
194
|
+
}
|
|
195
|
+
} else if (event.key === "Escape") {
|
|
196
|
+
if (box.hasAttribute("data-qube-open")) {
|
|
197
|
+
event.preventDefault();
|
|
198
|
+
close(box);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
// ---- A form choice whose required fields are all searches: its submit waits for a pick
|
|
204
|
+
// (disabled here, never in the markup, so it still posts without the script).
|
|
205
|
+
|
|
206
|
+
var gated = function (form) {
|
|
207
|
+
var required = form.querySelectorAll("input[required], select[required], textarea[required]");
|
|
208
|
+
if (!required.length) return false;
|
|
209
|
+
return Array.prototype.every.call(required, function (input) { return input.matches("[data-qube-search-input]"); });
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
var gate = function (form) {
|
|
213
|
+
if (!form || !form.matches('[data-qube-choice-kind="form"]') || !gated(form)) return;
|
|
214
|
+
var picked = Array.prototype.every.call(form.querySelectorAll("[data-qube-search]"), function (box) {
|
|
215
|
+
var p = parts(box);
|
|
216
|
+
return !p.input || !p.input.required || (p.value && p.value.value);
|
|
217
|
+
});
|
|
218
|
+
each(form.querySelectorAll('button[type="submit"], input[type="submit"]'), function (button) {
|
|
219
|
+
button.disabled = !picked;
|
|
220
|
+
});
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
// ---- Rows: [data-qube-rows] holds records ([data-qube-row][data-qube-values]), their fields
|
|
224
|
+
// ([data-qube-col], with data-qube-when, data-qube-required) and inputs ([data-qube-rows-input]),
|
|
225
|
+
// the "Set all to:" line ([data-qube-rows-set-all="<column>"] with [data-qube-set-all="<value>"]
|
|
226
|
+
// buttons) and each field's "Use for all" ([data-qube-use-for-all]).
|
|
227
|
+
|
|
228
|
+
var rowsOf = function (list) {
|
|
229
|
+
return list.querySelectorAll("[data-qube-row][data-qube-values]");
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
// A row's answers so far: its preset values, then its shown inputs (a radio when it's checked).
|
|
233
|
+
var answers = function (row) {
|
|
234
|
+
var values = JSON.parse(row.getAttribute("data-qube-values") || "{}");
|
|
235
|
+
each(row.querySelectorAll("[data-qube-col] [data-qube-rows-input]"), function (input) {
|
|
236
|
+
if (input.disabled || (input.type === "radio" && !input.checked)) return;
|
|
237
|
+
if (input.value) values[input.closest("[data-qube-col]").getAttribute("data-qube-col")] = input.value;
|
|
238
|
+
});
|
|
239
|
+
return values;
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
var holds = function (when, values) {
|
|
243
|
+
return Object.keys(when).every(function (k) { return when[k].indexOf(values[k]) !== -1; });
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
// Shows or hides a field: hidden, its inputs disabled and not required, so it's neither sent nor
|
|
247
|
+
// asked for. The stylesheet fades it in.
|
|
248
|
+
var showCell = function (cell, shown) {
|
|
249
|
+
if (shown === !cell.hidden) return false;
|
|
250
|
+
cell.hidden = !shown;
|
|
251
|
+
if (shown) cell.removeAttribute("data-qube-hidden");
|
|
252
|
+
else cell.setAttribute("data-qube-hidden", "");
|
|
253
|
+
var required = cell.hasAttribute("data-qube-required");
|
|
254
|
+
each(cell.querySelectorAll("input, select, textarea"), function (input) {
|
|
255
|
+
input.disabled = !shown;
|
|
256
|
+
if (required && input.type !== "hidden") input.required = shown;
|
|
257
|
+
});
|
|
258
|
+
return true;
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
// Shows each field while its "when" holds, until nothing changes (a field shown or hidden can
|
|
262
|
+
// change what the others see).
|
|
263
|
+
var refresh = function (row) {
|
|
264
|
+
for (var round = 0; round < 5; round++) {
|
|
265
|
+
var values = answers(row);
|
|
266
|
+
var changed = false;
|
|
267
|
+
each(row.querySelectorAll("[data-qube-col]"), function (cell) {
|
|
268
|
+
var shown = holds(JSON.parse(cell.getAttribute("data-qube-when") || "{}"), values);
|
|
269
|
+
if (showCell(cell, shown)) changed = true;
|
|
270
|
+
});
|
|
271
|
+
if (!changed) return;
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
// A tint on what was just filled for the person, fading out (the stylesheet's animation).
|
|
276
|
+
var filled = function (cell) {
|
|
277
|
+
cell.removeAttribute("data-qube-filled");
|
|
278
|
+
void cell.offsetWidth; // restarts it
|
|
279
|
+
cell.setAttribute("data-qube-filled", "");
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
// A field's value and, for a search, the text it shows.
|
|
283
|
+
var valueOf = function (cell) {
|
|
284
|
+
var hidden = cell.querySelector("[data-qube-search-value]");
|
|
285
|
+
if (hidden) {
|
|
286
|
+
var text = cell.querySelector("[data-qube-search-input]");
|
|
287
|
+
return { value: hidden.value, label: text ? text.value : hidden.value };
|
|
288
|
+
}
|
|
289
|
+
var select = cell.querySelector("select[data-qube-rows-input]");
|
|
290
|
+
return { value: select ? select.value : "", label: null };
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
var setValue = function (cell, value) {
|
|
294
|
+
var hidden = cell.querySelector("[data-qube-search-value]");
|
|
295
|
+
if (hidden) {
|
|
296
|
+
hidden.value = value.value;
|
|
297
|
+
var text = cell.querySelector("[data-qube-search-input]");
|
|
298
|
+
if (text) {
|
|
299
|
+
text.value = value.label || value.value;
|
|
300
|
+
text.setCustomValidity("");
|
|
301
|
+
}
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
var select = cell.querySelector("select[data-qube-rows-input]");
|
|
305
|
+
if (select) select.value = value.value;
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
// The other rows' shown, empty fields of the same column.
|
|
309
|
+
var targets = function (list, cell) {
|
|
310
|
+
var col = cell.getAttribute("data-qube-col");
|
|
311
|
+
var row = cell.closest("[data-qube-row]");
|
|
312
|
+
return Array.prototype.filter.call(list.querySelectorAll("[data-qube-row] [data-qube-col]"), function (other) {
|
|
313
|
+
return other.getAttribute("data-qube-col") === col && other !== cell && !other.hidden &&
|
|
314
|
+
other.closest("[data-qube-row]") !== row && other.querySelector("[data-qube-use-for-all]") &&
|
|
315
|
+
!valueOf(other).value;
|
|
316
|
+
});
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
// "Use for all N" under each filled field that other rows could take.
|
|
320
|
+
var offers = function (list) {
|
|
321
|
+
var template = list.getAttribute("data-qube-text-use-for-all") || "Use for all %{count}";
|
|
322
|
+
each(list.querySelectorAll("[data-qube-use-for-all]"), function (button) {
|
|
323
|
+
var cell = button.closest("[data-qube-col]");
|
|
324
|
+
var n = !cell || cell.hidden || !valueOf(cell).value ? 0 : targets(list, cell).length;
|
|
325
|
+
button.hidden = n === 0;
|
|
326
|
+
if (n) button.textContent = template.replace("%{count}", n);
|
|
327
|
+
});
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
var useForAll = function (button) {
|
|
331
|
+
var cell = button.closest("[data-qube-col]");
|
|
332
|
+
var list = cell.closest("[data-qube-rows]");
|
|
333
|
+
var value = valueOf(cell);
|
|
334
|
+
targets(list, cell).forEach(function (other) {
|
|
335
|
+
setValue(other, value);
|
|
336
|
+
refresh(other.closest("[data-qube-row]"));
|
|
337
|
+
filled(other);
|
|
338
|
+
});
|
|
339
|
+
offers(list);
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
// Sets the column on every row that offers that value.
|
|
343
|
+
var setAll = function (button) {
|
|
344
|
+
var list = button.closest("[data-qube-rows]");
|
|
345
|
+
var col = button.closest("[data-qube-rows-set-all]").getAttribute("data-qube-rows-set-all");
|
|
346
|
+
var value = button.getAttribute("data-qube-set-all");
|
|
347
|
+
each(rowsOf(list), function (row) {
|
|
348
|
+
var cell = row.querySelector('[data-qube-col="' + col + '"]');
|
|
349
|
+
if (!cell || cell.hidden) return;
|
|
350
|
+
var radio = Array.prototype.find.call(cell.querySelectorAll('input[type="radio"]'), function (r) {
|
|
351
|
+
return r.value === value;
|
|
352
|
+
});
|
|
353
|
+
var select = cell.querySelector("select[data-qube-rows-input]");
|
|
354
|
+
if (radio && !radio.disabled) {
|
|
355
|
+
if (radio.checked) return;
|
|
356
|
+
radio.checked = true;
|
|
357
|
+
} else if (select && !select.disabled && Array.prototype.some.call(select.options, function (o) { return o.value === value; })) {
|
|
358
|
+
if (select.value === value) return;
|
|
359
|
+
select.value = value;
|
|
360
|
+
} else {
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
refresh(row);
|
|
364
|
+
filled(cell);
|
|
365
|
+
});
|
|
366
|
+
offers(list);
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
// Each rows field as the page has it.
|
|
370
|
+
var sweep = function () {
|
|
371
|
+
each(document.querySelectorAll("[data-qube-rows]"), function (list) {
|
|
372
|
+
each(rowsOf(list), refresh);
|
|
373
|
+
offers(list);
|
|
374
|
+
});
|
|
375
|
+
each(document.querySelectorAll('[data-qube-choice-kind="form"]'), gate);
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
// ---- The storyboard: [data-qube-copy] copies its value.
|
|
379
|
+
|
|
380
|
+
var copy = function (button) {
|
|
381
|
+
var text = button.getAttribute("data-qube-copy");
|
|
382
|
+
if (!navigator.clipboard) return;
|
|
383
|
+
navigator.clipboard.writeText(text).then(function () {
|
|
384
|
+
var label = button.textContent;
|
|
385
|
+
button.textContent = button.getAttribute("data-qube-copied") || "Copied";
|
|
386
|
+
setTimeout(function () { button.textContent = label; }, 1200);
|
|
387
|
+
});
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
// ---- Listeners
|
|
391
|
+
|
|
392
|
+
document.addEventListener("input", function (event) {
|
|
393
|
+
var input = event.target.closest && event.target.closest("[data-qube-search-input]");
|
|
394
|
+
var box = input && input.closest("[data-qube-search]");
|
|
395
|
+
if (box) {
|
|
396
|
+
search(box);
|
|
397
|
+
gate(box.closest("form"));
|
|
398
|
+
var list = box.closest("[data-qube-rows]");
|
|
399
|
+
if (list) offers(list);
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
document.addEventListener("keydown", function (event) {
|
|
404
|
+
var input = event.target.closest && event.target.closest("[data-qube-search-input]");
|
|
405
|
+
var box = input && input.closest("[data-qube-search]");
|
|
406
|
+
if (box) keys(event, box);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
// A press on the matches keeps the focus in the input, so picking doesn't close them first.
|
|
410
|
+
document.addEventListener("mousedown", function (event) {
|
|
411
|
+
if (event.target.closest && event.target.closest("[data-qube-search-results]")) event.preventDefault();
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
// The pointer highlights what it's over (the same highlight the arrows move).
|
|
415
|
+
document.addEventListener("mousemove", function (event) {
|
|
416
|
+
var option = event.target.closest && event.target.closest("[data-qube-search-result]");
|
|
417
|
+
if (!option || option.getAttribute("aria-selected") === "true") return;
|
|
418
|
+
var box = option.closest("[data-qube-search]");
|
|
419
|
+
if (box) highlight(parts(box), option, false);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
document.addEventListener("focusout", function (event) {
|
|
423
|
+
var box = event.target.closest && event.target.closest("[data-qube-search]");
|
|
424
|
+
if (box && box.hasAttribute("data-qube-open") && !(event.relatedTarget && box.contains(event.relatedTarget))) {
|
|
425
|
+
close(box);
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
document.addEventListener("click", function (event) {
|
|
430
|
+
if (!event.target.closest) return;
|
|
431
|
+
each(document.querySelectorAll("[data-qube-search][data-qube-open]"), function (box) {
|
|
432
|
+
if (!box.contains(event.target)) close(box);
|
|
433
|
+
});
|
|
434
|
+
var result = event.target.closest("[data-qube-search-result]");
|
|
435
|
+
if (result) return pick(result);
|
|
436
|
+
var setAllButton = event.target.closest("[data-qube-set-all]");
|
|
437
|
+
if (setAllButton) return setAll(setAllButton);
|
|
438
|
+
var forAll = event.target.closest("[data-qube-use-for-all]");
|
|
439
|
+
if (forAll) return useForAll(forAll);
|
|
440
|
+
var copyButton = event.target.closest("[data-qube-copy]");
|
|
441
|
+
if (copyButton) copy(copyButton);
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
document.addEventListener("change", function (event) {
|
|
445
|
+
var target = event.target;
|
|
446
|
+
if (!target.closest) return;
|
|
447
|
+
var row = target.closest("[data-qube-rows] [data-qube-row][data-qube-values]");
|
|
448
|
+
if (!row) return;
|
|
449
|
+
refresh(row);
|
|
450
|
+
offers(row.closest("[data-qube-rows]"));
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
["DOMContentLoaded", "turbo:load", "turbo:render", "turbo:frame-load"].forEach(function (name) {
|
|
454
|
+
document.addEventListener(name, sweep);
|
|
455
|
+
});
|
|
456
|
+
if (document.readyState !== "loading") sweep();
|
|
457
|
+
})();
|