bearings 0.5.2 → 0.5.4
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.
- package/README.md +40 -22
- package/dist/chunk-MOZJECCW.js +1823 -0
- package/dist/cli.js +189 -158
- package/dist/{update-4DZKUPFP.js → update-YY5SLPDF.js} +84 -221
- package/package.json +1 -1
- package/templates/AGENTS.md +1 -5
- package/templates/agents/commands/refresh-repo-map.md +36 -13
- package/templates/agents/commands/update-bearings-setup.md +144 -0
- package/templates/agents/skills/checklist/previewer/index.html +44 -10
- package/templates/agents/skills/checklist/previewer/viewer.css +321 -77
- package/templates/agents/skills/checklist/previewer/viewer.js +797 -97
- package/templates/initial-setup.md +112 -0
- package/dist/chunk-WYXKSCUW.js +0 -818
- package/templates/agents/commands/setup-repo.md +0 -182
|
@@ -2,22 +2,30 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
var STATES = ['not-started', 'complete', 'failed', 'blocked', 'not-applicable'];
|
|
5
|
-
var
|
|
6
|
-
'not-started': 'Not started',
|
|
7
|
-
complete: 'Complete',
|
|
8
|
-
failed: 'Failed',
|
|
9
|
-
blocked: 'Blocked',
|
|
10
|
-
'not-applicable': 'Not applicable',
|
|
5
|
+
var STATE_META = {
|
|
6
|
+
'not-started': { label: 'Not started', symbol: '○' },
|
|
7
|
+
complete: { label: 'Complete', symbol: '✓' },
|
|
8
|
+
failed: { label: 'Failed', symbol: '×' },
|
|
9
|
+
blocked: { label: 'Blocked', symbol: 'Ⅱ' },
|
|
10
|
+
'not-applicable': { label: 'Not applicable', symbol: '—' },
|
|
11
11
|
};
|
|
12
12
|
var payload = window.BEARINGS_CHECKLISTS;
|
|
13
13
|
var currentEntry = null;
|
|
14
|
-
var currentStates =
|
|
14
|
+
var currentStates = Object.create(null);
|
|
15
15
|
var storageAvailable = true;
|
|
16
16
|
var refreshPending = false;
|
|
17
|
+
var taskViews = Object.create(null);
|
|
18
|
+
var taskSequence = [];
|
|
19
|
+
var groupViews = [];
|
|
20
|
+
var sectionViews = [];
|
|
21
|
+
var keyboardCursor = null;
|
|
22
|
+
var nextDomId = 0;
|
|
17
23
|
|
|
18
24
|
var picker = document.getElementById('checklist-picker');
|
|
19
25
|
var content = document.getElementById('content');
|
|
20
26
|
var status = document.getElementById('status');
|
|
27
|
+
var actionStatus = document.getElementById('action-status');
|
|
28
|
+
var actionMenu = document.getElementById('action-menu');
|
|
21
29
|
var warning = document.getElementById('storage-warning');
|
|
22
30
|
|
|
23
31
|
function element(tag, className, text) {
|
|
@@ -27,34 +35,61 @@
|
|
|
27
35
|
return node;
|
|
28
36
|
}
|
|
29
37
|
|
|
30
|
-
function setStatus(message) {
|
|
38
|
+
function setStatus(message, isError) {
|
|
31
39
|
status.textContent = message;
|
|
40
|
+
status.dataset.tone = isError ? 'error' : 'info';
|
|
41
|
+
status.setAttribute('role', isError ? 'alert' : 'status');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function setActionStatus(message, isError) {
|
|
45
|
+
actionStatus.hidden = false;
|
|
46
|
+
actionStatus.textContent = message;
|
|
47
|
+
actionStatus.dataset.tone = isError ? 'error' : 'info';
|
|
48
|
+
actionStatus.setAttribute('role', isError ? 'alert' : 'status');
|
|
49
|
+
if (isError) actionMenu.open = true;
|
|
32
50
|
}
|
|
33
51
|
|
|
34
52
|
function reportStorageFailure() {
|
|
35
53
|
storageAvailable = false;
|
|
36
54
|
warning.hidden = false;
|
|
37
|
-
warning.textContent = 'Browser storage is unavailable. Progress works in this tab only; use Export to keep a copy.';
|
|
55
|
+
warning.textContent = 'Browser storage is unavailable. Progress works in this tab only; use Export progress to keep a copy.';
|
|
38
56
|
}
|
|
39
57
|
|
|
40
58
|
function storageKey(entry) {
|
|
41
59
|
return 'bearings-checklists:' + payload.repositoryId + ':' + entry.identity;
|
|
42
60
|
}
|
|
43
61
|
|
|
44
|
-
function
|
|
45
|
-
|
|
46
|
-
|
|
62
|
+
function hasOwnTaskContent(item) {
|
|
63
|
+
return Boolean(
|
|
64
|
+
item.instruction || item.warning || item.expectedResult || item.failureGuidance ||
|
|
65
|
+
(item.codeBlocks && item.codeBlocks.length) || (item.links && item.links.length)
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isGroupOnly(item) {
|
|
70
|
+
return Boolean(item.subItems && item.subItems.length && !hasOwnTaskContent(item));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function executableItems(entry) {
|
|
74
|
+
var items = [];
|
|
75
|
+
entry.checklist.sections.forEach(function (section, sectionIndex) {
|
|
47
76
|
if (section.kind === 'info') return;
|
|
48
77
|
section.items.forEach(function (item) {
|
|
49
|
-
|
|
50
|
-
(item.subItems || []).forEach(function (subItem) {
|
|
78
|
+
if (!isGroupOnly(item)) items.push({ id: item.id, title: item.title, sectionIndex: sectionIndex });
|
|
79
|
+
(item.subItems || []).forEach(function (subItem) {
|
|
80
|
+
items.push({ id: subItem.id, title: subItem.title, sectionIndex: sectionIndex });
|
|
81
|
+
});
|
|
51
82
|
});
|
|
52
83
|
});
|
|
53
|
-
return
|
|
84
|
+
return items;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function executableIds(entry) {
|
|
88
|
+
return executableItems(entry).map(function (item) { return item.id; });
|
|
54
89
|
}
|
|
55
90
|
|
|
56
91
|
function normalizedStates(entry, candidate) {
|
|
57
|
-
var states =
|
|
92
|
+
var states = Object.create(null);
|
|
58
93
|
executableIds(entry).forEach(function (id) {
|
|
59
94
|
if (candidate && STATES.indexOf(candidate[id]) !== -1 && candidate[id] !== 'not-started') {
|
|
60
95
|
states[id] = candidate[id];
|
|
@@ -64,14 +99,14 @@
|
|
|
64
99
|
}
|
|
65
100
|
|
|
66
101
|
function loadProgress(entry) {
|
|
67
|
-
if (!storageAvailable) return
|
|
102
|
+
if (!storageAvailable) return Object.create(null);
|
|
68
103
|
try {
|
|
69
104
|
var saved = JSON.parse(localStorage.getItem(storageKey(entry)) || 'null');
|
|
70
|
-
if (!saved || saved.contentHash !== entry.contentHash) return
|
|
105
|
+
if (!saved || saved.contentHash !== entry.contentHash) return Object.create(null);
|
|
71
106
|
return normalizedStates(entry, saved.states);
|
|
72
107
|
} catch (error) {
|
|
73
108
|
reportStorageFailure();
|
|
74
|
-
return
|
|
109
|
+
return Object.create(null);
|
|
75
110
|
}
|
|
76
111
|
}
|
|
77
112
|
|
|
@@ -92,21 +127,210 @@
|
|
|
92
127
|
setStatus('Progress saved in this browser.');
|
|
93
128
|
} catch (error) {
|
|
94
129
|
reportStorageFailure();
|
|
95
|
-
setStatus('Progress changed, but browser storage could not save it.');
|
|
130
|
+
setStatus('Progress changed, but browser storage could not save it.', true);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function itemState(id) {
|
|
135
|
+
return currentStates[id] || 'not-started';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function tally(ids) {
|
|
139
|
+
var counts = { complete: 0, failed: 0, blocked: 0, 'not-applicable': 0, 'not-started': 0 };
|
|
140
|
+
ids.forEach(function (id) { counts[itemState(id)] += 1; });
|
|
141
|
+
return counts;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function setBadge(badge, stateName, label) {
|
|
145
|
+
var meta = STATE_META[stateName] || STATE_META['not-started'];
|
|
146
|
+
badge.dataset.state = stateName;
|
|
147
|
+
badge.replaceChildren(
|
|
148
|
+
element('span', 'status-symbol', meta.symbol),
|
|
149
|
+
document.createTextNode(label || meta.label)
|
|
150
|
+
);
|
|
151
|
+
badge.firstChild.setAttribute('aria-hidden', 'true');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function setExpanded(view, expanded) {
|
|
155
|
+
view.body.hidden = !expanded;
|
|
156
|
+
view.toggle.setAttribute('aria-expanded', String(expanded));
|
|
157
|
+
view.toggle.textContent = expanded ? 'Hide details' : 'Show details';
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function isUnresolved(id) {
|
|
161
|
+
var stateName = itemState(id);
|
|
162
|
+
return stateName !== 'complete' && stateName !== 'not-applicable';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function tasksInSection(sectionIndex) {
|
|
166
|
+
return taskSequence.filter(function (task) { return task.sectionIndex === sectionIndex; });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function preferredTaskInSection(sectionIndex) {
|
|
170
|
+
var tasks = tasksInSection(sectionIndex);
|
|
171
|
+
return tasks.find(function (task) { return isUnresolved(task.id); }) || tasks[0] || null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function firstUnresolvedTask() {
|
|
175
|
+
return taskSequence.find(function (task) { return isUnresolved(task.id); }) || null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function nextUnresolvedTaskAfter(id) {
|
|
179
|
+
var index = taskSequence.findIndex(function (task) { return task.id === id; });
|
|
180
|
+
return taskSequence.slice(index + 1).find(function (task) { return isUnresolved(task.id); }) || null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function hasTextSelection() {
|
|
184
|
+
var selection = window.getSelection && window.getSelection();
|
|
185
|
+
return Boolean(selection && !selection.isCollapsed);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function isNativeShortcutTarget(target) {
|
|
189
|
+
if (!target || target.nodeType !== 1) return false;
|
|
190
|
+
return target.isContentEditable || Boolean(target.closest('input, select, textarea, button, a, summary, [contenteditable]:not([contenteditable="false"])'));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function setKeyboardCursor(sectionIndex, actionId, options) {
|
|
194
|
+
options = options || {};
|
|
195
|
+
if (!currentEntry || !sectionViews.length) {
|
|
196
|
+
keyboardCursor = null;
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
var section = sectionViews.find(function (view) { return view.index === sectionIndex; }) || sectionViews[0];
|
|
201
|
+
var action = actionId && taskSequence.find(function (task) {
|
|
202
|
+
return task.id === actionId && task.sectionIndex === section.index;
|
|
203
|
+
});
|
|
204
|
+
var previous = keyboardCursor;
|
|
205
|
+
var changed = !previous || previous.sectionIndex !== section.index || previous.actionId !== (action ? action.id : null);
|
|
206
|
+
keyboardCursor = { sectionIndex: section.index, actionId: action ? action.id : null };
|
|
207
|
+
if (changed) {
|
|
208
|
+
var chooser = content.querySelector('.copy-chooser');
|
|
209
|
+
if (chooser) {
|
|
210
|
+
if (chooser.open) chooser.close();
|
|
211
|
+
chooser.remove();
|
|
212
|
+
}
|
|
96
213
|
}
|
|
214
|
+
|
|
215
|
+
sectionViews.forEach(function (view) {
|
|
216
|
+
var current = view.index === keyboardCursor.sectionIndex;
|
|
217
|
+
view.element.dataset.keyboardCurrent = String(current);
|
|
218
|
+
if (current) view.link.setAttribute('aria-current', 'location');
|
|
219
|
+
else view.link.removeAttribute('aria-current');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
Object.keys(taskViews).forEach(function (id) {
|
|
223
|
+
var view = taskViews[id];
|
|
224
|
+
var current = id === keyboardCursor.actionId;
|
|
225
|
+
view.article.dataset.keyboardCurrent = String(current);
|
|
226
|
+
view.focusMarker.hidden = !current;
|
|
227
|
+
if (current) view.article.setAttribute('aria-current', 'step');
|
|
228
|
+
else view.article.removeAttribute('aria-current');
|
|
229
|
+
if (!changed) return;
|
|
230
|
+
if (current || view.childIds.indexOf(keyboardCursor.actionId) !== -1) setExpanded(view, true);
|
|
231
|
+
else setExpanded(view, view.manualExpanded === undefined ? false : view.manualExpanded);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
var target = action ? taskViews[action.id].article : section.element;
|
|
235
|
+
if (options.focus) {
|
|
236
|
+
try { target.focus({ preventScroll: true }); } catch (error) { target.focus(); }
|
|
237
|
+
}
|
|
238
|
+
if (options.scroll) target.scrollIntoView({ block: action ? 'center' : 'start' });
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function setTaskVisual(view, stateName) {
|
|
242
|
+
view.article.dataset.state = stateName;
|
|
243
|
+
setBadge(view.badge, stateName);
|
|
244
|
+
view.buttons.forEach(function (button) {
|
|
245
|
+
var selected = button.dataset.stateValue === stateName;
|
|
246
|
+
button.dataset.selected = String(selected);
|
|
247
|
+
button.setAttribute('aria-pressed', String(selected));
|
|
248
|
+
});
|
|
249
|
+
if (view.failure) view.failure.hidden = stateName !== 'failed' && stateName !== 'blocked';
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function updateGroupViews(nextUnresolvedId) {
|
|
253
|
+
groupViews.forEach(function (view) {
|
|
254
|
+
var counts = tally(view.ids);
|
|
255
|
+
var stateName = 'not-started';
|
|
256
|
+
if (counts.failed) stateName = 'failed';
|
|
257
|
+
else if (counts.blocked) stateName = 'blocked';
|
|
258
|
+
else if (counts.complete + counts['not-applicable'] === view.ids.length) {
|
|
259
|
+
stateName = counts.complete ? 'complete' : 'not-applicable';
|
|
260
|
+
}
|
|
261
|
+
var label = counts.complete + ' / ' + view.ids.length + ' complete';
|
|
262
|
+
if (counts.failed) label += ' · ' + counts.failed + ' failed';
|
|
263
|
+
if (counts.blocked) label += ' · ' + counts.blocked + ' blocked';
|
|
264
|
+
setBadge(view.badge, stateName, label);
|
|
265
|
+
view.article.dataset.state = stateName;
|
|
266
|
+
view.article.dataset.containsNext = String(view.ids.indexOf(nextUnresolvedId) !== -1);
|
|
267
|
+
if (view.counter) view.counter.textContent = label;
|
|
268
|
+
});
|
|
97
269
|
}
|
|
98
270
|
|
|
99
|
-
function
|
|
100
|
-
var
|
|
101
|
-
var
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
271
|
+
function updateDashboard() {
|
|
272
|
+
var entries = currentEntry ? executableItems(currentEntry) : [];
|
|
273
|
+
var ids = entries.map(function (entry) { return entry.id; });
|
|
274
|
+
var counts = tally(ids);
|
|
275
|
+
var total = ids.length;
|
|
276
|
+
var nextUnresolved = entries.find(function (entry) { return isUnresolved(entry.id); });
|
|
277
|
+
var nextUnresolvedIndex = nextUnresolved ? entries.indexOf(nextUnresolved) : -1;
|
|
278
|
+
var followingUnresolved = nextUnresolvedIndex === -1 ? null : entries.slice(nextUnresolvedIndex + 1).find(function (entry) { return isUnresolved(entry.id); });
|
|
279
|
+
|
|
280
|
+
document.getElementById('progress-copy').textContent = counts.complete + ' / ' + total + ' complete';
|
|
281
|
+
document.getElementById('failed-count').textContent = counts.failed;
|
|
282
|
+
document.getElementById('blocked-count').textContent = counts.blocked;
|
|
283
|
+
document.getElementById('na-count').textContent = counts['not-applicable'];
|
|
284
|
+
var track = document.getElementById('progress-track');
|
|
285
|
+
track.max = total || 1;
|
|
286
|
+
track.value = counts.complete;
|
|
287
|
+
track.setAttribute('aria-valuetext', counts.complete + ' of ' + total + ' complete; ' + counts.failed + ' failed; ' + counts.blocked + ' blocked; ' + counts['not-applicable'] + ' not applicable');
|
|
288
|
+
|
|
289
|
+
var currentLink = document.getElementById('current-task-link');
|
|
290
|
+
var nextCopy = document.getElementById('next-task-copy');
|
|
291
|
+
if (nextUnresolved && taskViews[nextUnresolved.id]) {
|
|
292
|
+
currentLink.textContent = nextUnresolved.id + ' ' + nextUnresolved.title;
|
|
293
|
+
currentLink.title = currentLink.textContent;
|
|
294
|
+
currentLink.href = '#' + taskViews[nextUnresolved.id].domId;
|
|
295
|
+
nextCopy.textContent = followingUnresolved ? 'Next: ' + followingUnresolved.id + ' ' + followingUnresolved.title : 'Next: finish this task';
|
|
296
|
+
} else if (currentEntry && total) {
|
|
297
|
+
currentLink.textContent = 'Checklist complete';
|
|
298
|
+
currentLink.removeAttribute('title');
|
|
299
|
+
currentLink.removeAttribute('href');
|
|
300
|
+
nextCopy.textContent = 'No open tasks';
|
|
301
|
+
} else {
|
|
302
|
+
currentLink.textContent = 'Choose a checklist';
|
|
303
|
+
currentLink.removeAttribute('title');
|
|
304
|
+
currentLink.href = '#content';
|
|
305
|
+
nextCopy.textContent = 'Next: —';
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
Object.keys(taskViews).forEach(function (id) {
|
|
309
|
+
var view = taskViews[id];
|
|
310
|
+
var isNextUnresolved = Boolean(nextUnresolved && id === nextUnresolved.id);
|
|
311
|
+
var containsNext = Boolean(nextUnresolved && view.childIds.indexOf(nextUnresolved.id) !== -1);
|
|
312
|
+
view.article.dataset.nextUnresolved = String(isNextUnresolved);
|
|
313
|
+
view.article.dataset.containsNext = String(containsNext);
|
|
314
|
+
view.marker.hidden = !isNextUnresolved;
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
updateGroupViews(nextUnresolved && nextUnresolved.id);
|
|
318
|
+
updateSubstepCounters();
|
|
319
|
+
sectionViews.forEach(function (view) {
|
|
320
|
+
if (view.kind === 'info') {
|
|
321
|
+
view.count.textContent = 'Info';
|
|
322
|
+
} else {
|
|
323
|
+
var sectionCounts = tally(view.ids);
|
|
324
|
+
var sectionLabel = sectionCounts.complete + '/' + view.ids.length;
|
|
325
|
+
if (sectionCounts.failed) sectionLabel += ' · ' + sectionCounts.failed + ' failed';
|
|
326
|
+
else if (sectionCounts.blocked) sectionLabel += ' · ' + sectionCounts.blocked + ' blocked';
|
|
327
|
+
view.count.textContent = sectionLabel;
|
|
328
|
+
}
|
|
329
|
+
});
|
|
106
330
|
}
|
|
107
331
|
|
|
108
332
|
function textBlock(className, label, value) {
|
|
109
|
-
var block = element('div', className);
|
|
333
|
+
var block = element('div', 'step-block ' + className);
|
|
110
334
|
block.append(element('span', 'field-label', label), document.createTextNode(value));
|
|
111
335
|
return block;
|
|
112
336
|
}
|
|
@@ -127,104 +351,574 @@
|
|
|
127
351
|
return item;
|
|
128
352
|
}
|
|
129
353
|
|
|
130
|
-
function
|
|
131
|
-
var
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
354
|
+
function fallbackCopy(value) {
|
|
355
|
+
var input = element('textarea');
|
|
356
|
+
input.value = value;
|
|
357
|
+
input.className = 'copy-source';
|
|
358
|
+
input.setAttribute('readonly', '');
|
|
359
|
+
document.body.append(input);
|
|
360
|
+
input.select();
|
|
361
|
+
var copied = false;
|
|
362
|
+
try { copied = document.execCommand('copy'); } catch (error) { copied = false; }
|
|
363
|
+
input.remove();
|
|
364
|
+
return copied;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function copyCode(value, button) {
|
|
368
|
+
function copied() {
|
|
369
|
+
button.textContent = 'Copied';
|
|
370
|
+
setStatus('Command copied.');
|
|
371
|
+
}
|
|
372
|
+
function fallback() {
|
|
373
|
+
if (fallbackCopy(value)) copied();
|
|
374
|
+
else setStatus('Copy failed. Select the command and copy it manually.', true);
|
|
375
|
+
}
|
|
376
|
+
button.textContent = 'Copying…';
|
|
377
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
378
|
+
navigator.clipboard.writeText(value).then(copied, fallback);
|
|
379
|
+
} else fallback();
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function codeBlock(block) {
|
|
383
|
+
var wrapper = element('div', 'code-block');
|
|
384
|
+
var heading = element('div', 'code-heading');
|
|
385
|
+
var copy = element('button', 'copy-button', 'Copy');
|
|
386
|
+
copy.type = 'button';
|
|
387
|
+
copy.setAttribute('aria-label', 'Copy ' + block.language + ' command');
|
|
388
|
+
copy.addEventListener('click', function () { copyCode(block.content, copy); });
|
|
389
|
+
heading.append(element('span', 'language', block.language), copy);
|
|
390
|
+
var pre = element('pre');
|
|
391
|
+
pre.append(element('code', '', block.content));
|
|
392
|
+
wrapper.append(heading, pre);
|
|
393
|
+
return wrapper;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function appendItemContent(item, body, kind) {
|
|
397
|
+
var failure = null;
|
|
398
|
+
if (item.warning) body.append(textBlock('warning', 'Warning', item.warning));
|
|
399
|
+
if (item.instruction) body.append(textBlock('instruction', kind === 'info' ? 'Information' : 'Do', item.instruction));
|
|
400
|
+
(item.codeBlocks || []).forEach(function (block) { body.append(codeBlock(block)); });
|
|
401
|
+
if (item.expectedResult) body.append(textBlock('expected', 'Verify', item.expectedResult));
|
|
402
|
+
if (item.failureGuidance) {
|
|
403
|
+
failure = textBlock('failure', 'Recovery', item.failureGuidance);
|
|
404
|
+
body.append(failure);
|
|
405
|
+
}
|
|
406
|
+
if (item.links && item.links.length) {
|
|
407
|
+
var resources = element('div', 'resources');
|
|
408
|
+
var links = element('ul', 'links');
|
|
409
|
+
item.links.forEach(function (link) { links.append(safeLink(link)); });
|
|
410
|
+
resources.append(element('span', 'field-label', 'Resources'), links);
|
|
411
|
+
body.append(resources);
|
|
412
|
+
}
|
|
413
|
+
return failure;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function stateButton(item, stateName, label, className, menu) {
|
|
417
|
+
var button = element('button', 'state-action' + (className ? ' ' + className : ''), label);
|
|
418
|
+
button.type = 'button';
|
|
419
|
+
button.dataset.stateValue = stateName;
|
|
420
|
+
button.setAttribute('aria-label', label + ' for ' + item.id + ': ' + item.title);
|
|
421
|
+
button.addEventListener('click', function () {
|
|
422
|
+
if (menu) menu.open = false;
|
|
423
|
+
changeItemState(item.id, stateName);
|
|
138
424
|
});
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
425
|
+
return button;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function stateActions(item) {
|
|
429
|
+
var actions = element('div', 'state-actions');
|
|
430
|
+
var buttons = [];
|
|
431
|
+
var complete = stateButton(item, 'complete', 'Mark complete', 'primary-action');
|
|
432
|
+
var blocked = stateButton(item, 'blocked', 'Blocked');
|
|
433
|
+
var failed = stateButton(item, 'failed', 'Failed');
|
|
434
|
+
var more = element('details', 'state-more');
|
|
435
|
+
var moreSummary = element('summary', '', 'More');
|
|
436
|
+
var menu = element('div', 'state-more-menu');
|
|
437
|
+
var notApplicable = stateButton(item, 'not-applicable', 'Not applicable', '', more);
|
|
438
|
+
var notStarted = stateButton(item, 'not-started', 'Mark not started', '', more);
|
|
439
|
+
buttons.push(complete, blocked, failed, notApplicable, notStarted);
|
|
440
|
+
menu.append(notApplicable, notStarted);
|
|
441
|
+
more.append(moreSummary, menu);
|
|
442
|
+
actions.append(complete, blocked, failed, more);
|
|
443
|
+
return { element: actions, buttons: buttons };
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function scrollToNextTask(id) {
|
|
447
|
+
var index = taskSequence.findIndex(function (task) { return task.id === id; });
|
|
448
|
+
var next = taskSequence.slice(index + 1).find(function (task) {
|
|
449
|
+
var stateName = itemState(task.id);
|
|
450
|
+
return stateName !== 'complete' && stateName !== 'not-applicable';
|
|
147
451
|
});
|
|
148
|
-
|
|
149
|
-
return label;
|
|
452
|
+
if (next && taskViews[next.id]) taskViews[next.id].article.scrollIntoView({ block: 'center' });
|
|
150
453
|
}
|
|
151
454
|
|
|
152
|
-
function
|
|
153
|
-
var
|
|
154
|
-
|
|
155
|
-
|
|
455
|
+
function changeItemState(id, stateName) {
|
|
456
|
+
var wasComplete = itemState(id) === 'complete';
|
|
457
|
+
if (stateName === 'not-started') delete currentStates[id];
|
|
458
|
+
else currentStates[id] = stateName;
|
|
459
|
+
var view = taskViews[id];
|
|
460
|
+
view.manualExpanded = stateName === 'complete' || stateName === 'not-applicable' ? false : true;
|
|
461
|
+
var focusWasInBody = view.body.contains(document.activeElement);
|
|
462
|
+
setExpanded(view, view.manualExpanded);
|
|
463
|
+
setTaskVisual(view, stateName);
|
|
464
|
+
saveProgress();
|
|
465
|
+
updateDashboard();
|
|
466
|
+
if (stateName === 'complete' && !wasComplete) scrollToNextTask(id);
|
|
467
|
+
if (focusWasInBody && view.body.hidden) {
|
|
468
|
+
try { view.article.focus({ preventScroll: true }); } catch (error) { view.article.focus(); }
|
|
469
|
+
}
|
|
470
|
+
setStatus(id + ': ' + STATE_META[stateName].label + (storageAvailable ? '.' : '. Progress is only in this tab.'), !storageAvailable);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function itemHeading(item, isSubItem, label) {
|
|
474
|
+
var wrap = element('div', 'item-heading');
|
|
475
|
+
wrap.append(element('span', 'hierarchy-label', label));
|
|
156
476
|
var heading = element(isSubItem ? 'h4' : 'h3');
|
|
157
477
|
heading.append(element('span', 'item-id', item.id), document.createTextNode(item.title));
|
|
158
478
|
if (item.optional) heading.append(element('span', 'optional', 'Optional'));
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
if (item.
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
479
|
+
wrap.append(heading);
|
|
480
|
+
return wrap;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function renderSubItems(item, target, kind, sectionIndex) {
|
|
484
|
+
if (!item.subItems || !item.subItems.length) return { ids: [], counter: null };
|
|
485
|
+
var wrapper = element('div', 'substeps');
|
|
486
|
+
var heading = element('div', 'substeps-heading');
|
|
487
|
+
var counter = element('span', '', '0 / ' + item.subItems.length + ' complete');
|
|
488
|
+
heading.append(element('span', '', 'Substeps'), counter);
|
|
489
|
+
var items = element('div', 'sub-items');
|
|
490
|
+
item.subItems.forEach(function (subItem) { items.append(renderItem(subItem, kind, true, sectionIndex)); });
|
|
491
|
+
wrapper.append(heading, items);
|
|
492
|
+
target.append(wrapper);
|
|
493
|
+
return { ids: item.subItems.map(function (subItem) { return subItem.id; }), counter: counter };
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function renderItem(item, kind, isSubItem, sectionIndex) {
|
|
497
|
+
var groupOnly = kind === 'checklist' && isGroupOnly(item);
|
|
498
|
+
var article = element('article', 'item' + (kind === 'info' ? ' info' : '') + (groupOnly ? ' task-group' : ''));
|
|
499
|
+
article.id = 'task-' + (++nextDomId);
|
|
500
|
+
var head = element('div', 'item-head');
|
|
501
|
+
|
|
502
|
+
if (kind === 'info') {
|
|
503
|
+
head.append(itemHeading(item, isSubItem, 'Reference'));
|
|
504
|
+
var infoBadge = element('span', 'status-badge');
|
|
505
|
+
setBadge(infoBadge, 'not-applicable', 'Reference');
|
|
506
|
+
head.append(infoBadge);
|
|
507
|
+
var infoBody = element('div', 'item-body');
|
|
508
|
+
appendItemContent(item, infoBody, kind);
|
|
509
|
+
article.append(head, infoBody);
|
|
510
|
+
renderSubItems(item, infoBody, kind, sectionIndex);
|
|
511
|
+
article.addEventListener('click', function (event) {
|
|
512
|
+
setKeyboardCursor(sectionIndex, null, { focus: !isNativeShortcutTarget(event.target) && !hasTextSelection() });
|
|
513
|
+
});
|
|
514
|
+
article.addEventListener('focusin', function () { setKeyboardCursor(sectionIndex, null); });
|
|
515
|
+
return article;
|
|
177
516
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
517
|
+
|
|
518
|
+
if (groupOnly) {
|
|
519
|
+
head.append(itemHeading(item, false, 'Task group'));
|
|
520
|
+
var groupBadge = element('span', 'status-badge');
|
|
521
|
+
head.append(groupBadge);
|
|
522
|
+
article.append(head);
|
|
523
|
+
var groupChildren = renderSubItems(item, article, kind, sectionIndex);
|
|
524
|
+
groupViews.push({ article: article, badge: groupBadge, ids: groupChildren.ids, counter: groupChildren.counter });
|
|
525
|
+
article.addEventListener('click', function (event) {
|
|
526
|
+
if (!event.target.closest('[data-keyboard-task]')) {
|
|
527
|
+
setKeyboardCursor(sectionIndex, null, { focus: !isNativeShortcutTarget(event.target) && !hasTextSelection() });
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
article.addEventListener('focusin', function (event) {
|
|
531
|
+
if (!event.target.closest('[data-keyboard-task]')) setKeyboardCursor(sectionIndex, null);
|
|
532
|
+
});
|
|
533
|
+
return article;
|
|
182
534
|
}
|
|
535
|
+
|
|
536
|
+
var hierarchy = isSubItem ? 'Substep' : (item.subItems && item.subItems.length ? 'Parent task' : 'Task');
|
|
537
|
+
var headingWrap = itemHeading(item, isSubItem, hierarchy);
|
|
538
|
+
var marker = element('span', 'next-unresolved-marker', 'Next unresolved');
|
|
539
|
+
marker.hidden = true;
|
|
540
|
+
var focusMarker = element('span', 'focused-task-marker', 'Focused action');
|
|
541
|
+
focusMarker.hidden = true;
|
|
542
|
+
headingWrap.append(marker, focusMarker);
|
|
543
|
+
var badge = element('span', 'status-badge');
|
|
544
|
+
var toggle = element('button', 'item-toggle', 'Show details');
|
|
545
|
+
toggle.type = 'button';
|
|
546
|
+
var body = element('div', 'item-body');
|
|
547
|
+
var view = {
|
|
548
|
+
article: article,
|
|
549
|
+
badge: badge,
|
|
550
|
+
body: body,
|
|
551
|
+
toggle: toggle,
|
|
552
|
+
marker: marker,
|
|
553
|
+
focusMarker: focusMarker,
|
|
554
|
+
buttons: [],
|
|
555
|
+
copyButtons: [],
|
|
556
|
+
failure: null,
|
|
557
|
+
childIds: [],
|
|
558
|
+
substepCounter: null,
|
|
559
|
+
manualExpanded: undefined,
|
|
560
|
+
domId: article.id,
|
|
561
|
+
};
|
|
562
|
+
taskViews[item.id] = view;
|
|
563
|
+
taskSequence.push({ id: item.id, title: item.title, sectionIndex: sectionIndex });
|
|
564
|
+
article.dataset.keyboardTask = item.id;
|
|
565
|
+
article.tabIndex = -1;
|
|
566
|
+
toggle.setAttribute('aria-controls', article.id + '-body');
|
|
567
|
+
body.id = article.id + '-body';
|
|
568
|
+
toggle.addEventListener('click', function () {
|
|
569
|
+
view.manualExpanded = body.hidden;
|
|
570
|
+
setExpanded(view, view.manualExpanded);
|
|
571
|
+
});
|
|
572
|
+
article.addEventListener('focusin', function (event) {
|
|
573
|
+
if (event.target.closest('[data-keyboard-task]') !== article) return;
|
|
574
|
+
setKeyboardCursor(sectionIndex, item.id);
|
|
575
|
+
});
|
|
576
|
+
article.addEventListener('click', function (event) {
|
|
577
|
+
if (!event.isTrusted || event.target.closest('[data-keyboard-task]') !== article) return;
|
|
578
|
+
var focusTask = !isNativeShortcutTarget(event.target) && !hasTextSelection();
|
|
579
|
+
setKeyboardCursor(sectionIndex, item.id, { focus: focusTask });
|
|
580
|
+
});
|
|
581
|
+
head.append(headingWrap, badge, toggle);
|
|
582
|
+
article.append(head, body);
|
|
583
|
+
view.failure = appendItemContent(item, body, kind);
|
|
584
|
+
view.copyButtons = Array.prototype.slice.call(body.querySelectorAll('.copy-button'));
|
|
585
|
+
var children = renderSubItems(item, body, kind, sectionIndex);
|
|
586
|
+
view.childIds = children.ids;
|
|
587
|
+
view.substepCounter = children.counter;
|
|
588
|
+
var actions = stateActions(item);
|
|
589
|
+
view.buttons = actions.buttons;
|
|
590
|
+
body.append(actions.element);
|
|
591
|
+
setTaskVisual(view, itemState(item.id));
|
|
183
592
|
return article;
|
|
184
593
|
}
|
|
185
594
|
|
|
595
|
+
function updateSubstepCounters() {
|
|
596
|
+
Object.keys(taskViews).forEach(function (id) {
|
|
597
|
+
var view = taskViews[id];
|
|
598
|
+
if (!view.substepCounter) return;
|
|
599
|
+
var counts = tally(view.childIds);
|
|
600
|
+
var label = counts.complete + ' / ' + view.childIds.length + ' complete';
|
|
601
|
+
if (counts.failed) label += ' · ' + counts.failed + ' failed';
|
|
602
|
+
else if (counts.blocked) label += ' · ' + counts.blocked + ' blocked';
|
|
603
|
+
view.substepCounter.textContent = label;
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function moveActionCursor(delta) {
|
|
608
|
+
if (!keyboardCursor) return false;
|
|
609
|
+
var tasks = tasksInSection(keyboardCursor.sectionIndex);
|
|
610
|
+
if (!tasks.length) return false;
|
|
611
|
+
var index = tasks.findIndex(function (task) { return task.id === keyboardCursor.actionId; });
|
|
612
|
+
var target = index === -1 ? tasks[delta > 0 ? 0 : tasks.length - 1] : tasks[index + delta];
|
|
613
|
+
if (!target) return false;
|
|
614
|
+
setKeyboardCursor(target.sectionIndex, target.id, { focus: true, scroll: true });
|
|
615
|
+
return true;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function moveSectionCursor(delta) {
|
|
619
|
+
if (!keyboardCursor) return false;
|
|
620
|
+
var position = sectionViews.findIndex(function (view) { return view.index === keyboardCursor.sectionIndex; });
|
|
621
|
+
var section = sectionViews[position + delta];
|
|
622
|
+
if (!section) return false;
|
|
623
|
+
var target = section.kind === 'checklist' ? preferredTaskInSection(section.index) : null;
|
|
624
|
+
setKeyboardCursor(section.index, target && target.id, { focus: true, scroll: true });
|
|
625
|
+
return true;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function changeCursorState(stateName, advance) {
|
|
629
|
+
if (!keyboardCursor || !keyboardCursor.actionId) return false;
|
|
630
|
+
var changedTask = taskSequence.find(function (task) { return task.id === keyboardCursor.actionId; });
|
|
631
|
+
if (!changedTask) return false;
|
|
632
|
+
changeItemState(changedTask.id, stateName);
|
|
633
|
+
var target = advance && nextUnresolvedTaskAfter(changedTask.id);
|
|
634
|
+
target = target || changedTask;
|
|
635
|
+
setKeyboardCursor(target.sectionIndex, target.id, { focus: true, scroll: Boolean(advance && target !== changedTask) });
|
|
636
|
+
return true;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function toggleCursorDetails() {
|
|
640
|
+
if (!keyboardCursor || !keyboardCursor.actionId) return false;
|
|
641
|
+
var view = taskViews[keyboardCursor.actionId];
|
|
642
|
+
if (!view) return false;
|
|
643
|
+
view.manualExpanded = view.body.hidden;
|
|
644
|
+
setExpanded(view, view.manualExpanded);
|
|
645
|
+
return true;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function dismissCopyChooser(message, restoreFocus) {
|
|
649
|
+
var chooser = content.querySelector('.copy-chooser');
|
|
650
|
+
if (!chooser) return false;
|
|
651
|
+
if (chooser.open) chooser.close();
|
|
652
|
+
chooser.remove();
|
|
653
|
+
if (restoreFocus && keyboardCursor) {
|
|
654
|
+
setKeyboardCursor(keyboardCursor.sectionIndex, keyboardCursor.actionId, { focus: true });
|
|
655
|
+
}
|
|
656
|
+
if (message) setStatus(message);
|
|
657
|
+
return true;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function showCopyChooser(buttons, action, section) {
|
|
661
|
+
dismissCopyChooser();
|
|
662
|
+
var chooser = element('dialog', 'copy-chooser');
|
|
663
|
+
var title = element('h2', 'copy-chooser-title', 'Copy which command?');
|
|
664
|
+
var hintText = element('p', 'copy-chooser-hint');
|
|
665
|
+
var choices = element('div', 'copy-choices');
|
|
666
|
+
var actions = element('div', 'copy-chooser-actions');
|
|
667
|
+
var cancel = element('button', '', 'Cancel');
|
|
668
|
+
var titleId = (action ? action.article.id : section.element.id) + '-copy-chooser-title';
|
|
669
|
+
var hintId = titleId + '-hint';
|
|
670
|
+
title.id = titleId;
|
|
671
|
+
hintText.id = hintId;
|
|
672
|
+
chooser.setAttribute('aria-labelledby', titleId);
|
|
673
|
+
chooser.setAttribute('aria-describedby', hintId);
|
|
674
|
+
cancel.type = 'button';
|
|
675
|
+
cancel.addEventListener('click', function () { dismissCopyChooser('Copy choice canceled.', true); });
|
|
676
|
+
actions.append(cancel);
|
|
677
|
+
|
|
678
|
+
buttons.forEach(function (button, index) {
|
|
679
|
+
var code = button.closest('.code-block').querySelector('code');
|
|
680
|
+
var command = code ? code.textContent.trim() : 'Command ' + (index + 1);
|
|
681
|
+
var preview = command.split('\n')[0];
|
|
682
|
+
var choice = element('button', 'copy-choice');
|
|
683
|
+
choice.type = 'button';
|
|
684
|
+
choice.dataset.copyChoice = String(index);
|
|
685
|
+
choice.setAttribute('aria-label', 'Copy command ' + (index + 1) + ': ' + command);
|
|
686
|
+
if (index < 9) choice.setAttribute('aria-keyshortcuts', String(index + 1));
|
|
687
|
+
choice.append(
|
|
688
|
+
element('span', 'copy-choice-key', String(index + 1)),
|
|
689
|
+
element('code', 'copy-choice-command', preview),
|
|
690
|
+
);
|
|
691
|
+
choice.addEventListener('click', function () {
|
|
692
|
+
dismissCopyChooser('', true);
|
|
693
|
+
button.click();
|
|
694
|
+
});
|
|
695
|
+
choices.append(choice);
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
var directCount = Math.min(buttons.length, 9);
|
|
699
|
+
var directKeys = directCount === 2 ? '1 or 2' : '1 through ' + directCount;
|
|
700
|
+
var hint = 'Press ' + directKeys + ' to copy; use j/k or Up/Down and Enter, or Escape to cancel.';
|
|
701
|
+
hintText.textContent = hint;
|
|
702
|
+
chooser.append(title, hintText, choices, actions);
|
|
703
|
+
chooser.addEventListener('cancel', function (event) {
|
|
704
|
+
event.preventDefault();
|
|
705
|
+
dismissCopyChooser('Copy choice canceled.', true);
|
|
706
|
+
});
|
|
707
|
+
content.append(chooser);
|
|
708
|
+
chooser.showModal();
|
|
709
|
+
setStatus(buttons.length + ' commands available. ' + hint);
|
|
710
|
+
try { choices.firstChild.focus({ preventScroll: true }); } catch (error) { choices.firstChild.focus(); }
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function copyKeyboardTarget() {
|
|
714
|
+
if (!keyboardCursor) return false;
|
|
715
|
+
var section = sectionViews.find(function (view) { return view.index === keyboardCursor.sectionIndex; });
|
|
716
|
+
var action = keyboardCursor.actionId && taskViews[keyboardCursor.actionId];
|
|
717
|
+
var buttons = action
|
|
718
|
+
? action.copyButtons
|
|
719
|
+
: (section ? Array.prototype.slice.call(section.element.querySelectorAll('.copy-button')) : []);
|
|
720
|
+
if (buttons.length === 1) {
|
|
721
|
+
buttons[0].click();
|
|
722
|
+
return true;
|
|
723
|
+
}
|
|
724
|
+
if (buttons.length > 1) showCopyChooser(buttons, action, section);
|
|
725
|
+
else setStatus('No copyable item in the focused ' + (action ? 'action.' : 'section.'));
|
|
726
|
+
return true;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function handleCopyChooserShortcut(event, target) {
|
|
730
|
+
var chooser = content.querySelector('.copy-chooser');
|
|
731
|
+
if (!chooser) return false;
|
|
732
|
+
var choices = Array.prototype.slice.call(chooser.querySelectorAll('.copy-choice'));
|
|
733
|
+
if (event.key === 'Escape') return dismissCopyChooser('Copy choice canceled.', true);
|
|
734
|
+
if (event.key >= '1' && event.key <= '9') {
|
|
735
|
+
if (!event.repeat && choices[Number(event.key) - 1]) choices[Number(event.key) - 1].click();
|
|
736
|
+
return true;
|
|
737
|
+
}
|
|
738
|
+
if (event.key === 'j' || event.key === 'k' || event.key === 'ArrowUp' || event.key === 'ArrowDown') {
|
|
739
|
+
var current = choices.indexOf(target.closest('.copy-choice'));
|
|
740
|
+
var delta = event.key === 'j' || event.key === 'ArrowDown' ? 1 : -1;
|
|
741
|
+
var next = current === -1
|
|
742
|
+
? (delta > 0 ? 0 : choices.length - 1)
|
|
743
|
+
: (current + delta + choices.length) % choices.length;
|
|
744
|
+
choices[next].focus();
|
|
745
|
+
setStatus('Command ' + (next + 1) + ' of ' + choices.length + ' selected. Press Enter to copy or Escape to cancel.');
|
|
746
|
+
return true;
|
|
747
|
+
}
|
|
748
|
+
if (event.key === 'Enter' && target.closest('.copy-choice')) {
|
|
749
|
+
if (!event.repeat) target.closest('.copy-choice').click();
|
|
750
|
+
return true;
|
|
751
|
+
}
|
|
752
|
+
return false;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function resetProgress() {
|
|
756
|
+
if (!currentEntry || !window.confirm('Clear progress for this checklist?')) return false;
|
|
757
|
+
currentStates = Object.create(null);
|
|
758
|
+
saveProgress();
|
|
759
|
+
renderChecklist(currentEntry, currentStates);
|
|
760
|
+
setActionStatus('Progress reset.');
|
|
761
|
+
actionMenu.open = false;
|
|
762
|
+
return true;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function handleEscape(target) {
|
|
766
|
+
var taskArticle = target && target.closest && target.closest('[data-keyboard-task]');
|
|
767
|
+
var details = target && target.closest && target.closest('details[open]');
|
|
768
|
+
if (!details && taskArticle) details = taskArticle.querySelector('details[open]');
|
|
769
|
+
if (!details && actionMenu.open) details = actionMenu;
|
|
770
|
+
if (details) details.open = false;
|
|
771
|
+
|
|
772
|
+
if (keyboardCursor && (details || (content.contains(target) && isNativeShortcutTarget(target)))) {
|
|
773
|
+
setKeyboardCursor(keyboardCursor.sectionIndex, keyboardCursor.actionId, { focus: true });
|
|
774
|
+
return true;
|
|
775
|
+
}
|
|
776
|
+
if (details) {
|
|
777
|
+
var summary = details.querySelector('summary');
|
|
778
|
+
if (summary) summary.focus();
|
|
779
|
+
return true;
|
|
780
|
+
}
|
|
781
|
+
var nextUnresolved = keyboardCursor && content.contains(target) && firstUnresolvedTask();
|
|
782
|
+
if (nextUnresolved) {
|
|
783
|
+
setKeyboardCursor(nextUnresolved.sectionIndex, nextUnresolved.id, { focus: true, scroll: true });
|
|
784
|
+
return true;
|
|
785
|
+
}
|
|
786
|
+
return false;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function handleKeyboardShortcut(event) {
|
|
790
|
+
if (event.defaultPrevented || event.isComposing || event.keyCode === 229) return;
|
|
791
|
+
if (event.ctrlKey || event.metaKey || event.altKey || hasTextSelection()) return;
|
|
792
|
+
var target = event.target && event.target.nodeType === 1 ? event.target : document.activeElement;
|
|
793
|
+
if (content.querySelector('.copy-chooser')) {
|
|
794
|
+
if (handleCopyChooserShortcut(event, target)) event.preventDefault();
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
if (event.key === 'Escape') {
|
|
798
|
+
if (handleEscape(target)) event.preventDefault();
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
if (!currentEntry || !keyboardCursor || !content.contains(document.activeElement) || isNativeShortcutTarget(target)) return;
|
|
802
|
+
|
|
803
|
+
var stateByKey = {
|
|
804
|
+
'x': 'complete',
|
|
805
|
+
'f': 'failed',
|
|
806
|
+
'b': 'blocked',
|
|
807
|
+
'-': 'not-applicable',
|
|
808
|
+
'u': 'not-started',
|
|
809
|
+
};
|
|
810
|
+
if ((stateByKey[event.key] || event.key === 'R' || event.key === 'y') && event.repeat) return;
|
|
811
|
+
|
|
812
|
+
var handled = false;
|
|
813
|
+
if (event.key === 'j') handled = moveActionCursor(1);
|
|
814
|
+
else if (event.key === 'k') handled = moveActionCursor(-1);
|
|
815
|
+
else if (event.key === ']') handled = moveSectionCursor(1);
|
|
816
|
+
else if (event.key === '[') handled = moveSectionCursor(-1);
|
|
817
|
+
else if (event.key === 'y') handled = copyKeyboardTarget();
|
|
818
|
+
else if (event.key === 'Enter') handled = toggleCursorDetails();
|
|
819
|
+
else if (event.key === 'R') handled = resetProgress();
|
|
820
|
+
else if (stateByKey[event.key]) handled = changeCursorState(stateByKey[event.key], event.key === 'x' || event.key === '-');
|
|
821
|
+
if (handled) event.preventDefault();
|
|
822
|
+
}
|
|
823
|
+
|
|
186
824
|
function renderChecklist(entry, inMemoryStates) {
|
|
187
825
|
currentEntry = entry;
|
|
188
826
|
currentStates = inMemoryStates === undefined ? loadProgress(entry) : normalizedStates(entry, inMemoryStates);
|
|
827
|
+
taskViews = Object.create(null);
|
|
828
|
+
taskSequence = [];
|
|
829
|
+
groupViews = [];
|
|
830
|
+
sectionViews = [];
|
|
831
|
+
keyboardCursor = null;
|
|
832
|
+
nextDomId = 0;
|
|
189
833
|
var checklist = entry.checklist;
|
|
190
834
|
document.title = checklist.title + ' - Checklist';
|
|
191
835
|
document.getElementById('title').textContent = checklist.title;
|
|
192
836
|
document.getElementById('summary').textContent = checklist.summary;
|
|
193
837
|
content.className = '';
|
|
194
838
|
content.replaceChildren();
|
|
195
|
-
if (checklist.intro) content.append(element('p', 'intro', checklist.intro));
|
|
196
|
-
if (checklist.callout) content.append(element('div', 'callout', checklist.callout));
|
|
197
|
-
if (checklist.conventions) content.append(element('div', 'conventions', checklist.conventions));
|
|
198
839
|
|
|
199
|
-
|
|
200
|
-
|
|
840
|
+
var shell = element('div', 'checklist-shell');
|
|
841
|
+
var navigation = element('nav', 'section-nav');
|
|
842
|
+
navigation.setAttribute('aria-label', 'Checklist sections');
|
|
843
|
+
navigation.append(element('p', 'section-nav-title field-label', 'Sections'));
|
|
844
|
+
var navigationList = element('ol', 'section-nav-list');
|
|
845
|
+
navigation.append(navigationList);
|
|
846
|
+
var checklistBody = element('div', 'checklist-body');
|
|
847
|
+
if (checklist.intro) checklistBody.append(element('p', 'intro', checklist.intro));
|
|
848
|
+
if (checklist.callout) checklistBody.append(textBlock('callout', 'Important', checklist.callout));
|
|
849
|
+
if (checklist.conventions) checklistBody.append(textBlock('conventions', 'Conventions', checklist.conventions));
|
|
850
|
+
|
|
851
|
+
checklist.sections.forEach(function (section, sectionIndex) {
|
|
852
|
+
var sequenceStart = taskSequence.length;
|
|
853
|
+
var sectionElement = element('section', 'checklist-section');
|
|
854
|
+
sectionElement.id = 'section-' + (sectionIndex + 1);
|
|
855
|
+
sectionElement.tabIndex = -1;
|
|
201
856
|
var heading = element('div', 'section-heading');
|
|
202
857
|
heading.append(element('span', 'section-number', section.number), element('h2', '', section.title));
|
|
203
858
|
sectionElement.append(heading);
|
|
204
859
|
if (section.intro) sectionElement.append(element('p', 'section-intro', section.intro));
|
|
205
860
|
var items = element('div', 'items');
|
|
206
|
-
section.items.forEach(function (item) { items.append(renderItem(item, section.kind, false)); });
|
|
861
|
+
section.items.forEach(function (item) { items.append(renderItem(item, section.kind, false, sectionIndex)); });
|
|
207
862
|
sectionElement.append(items);
|
|
208
|
-
|
|
863
|
+
checklistBody.append(sectionElement);
|
|
864
|
+
|
|
865
|
+
var navItem = element('li');
|
|
866
|
+
var navLink = element('a', 'section-nav-link');
|
|
867
|
+
navLink.href = '#' + sectionElement.id;
|
|
868
|
+
var navCount = element('span', 'nav-count');
|
|
869
|
+
navLink.append(
|
|
870
|
+
element('span', 'nav-number', section.number),
|
|
871
|
+
element('span', 'nav-title', section.title),
|
|
872
|
+
navCount
|
|
873
|
+
);
|
|
874
|
+
navItem.append(navLink);
|
|
875
|
+
navigationList.append(navItem);
|
|
876
|
+
var sectionView = {
|
|
877
|
+
index: sectionIndex,
|
|
878
|
+
kind: section.kind,
|
|
879
|
+
ids: taskSequence.slice(sequenceStart).map(function (task) { return task.id; }),
|
|
880
|
+
element: sectionElement,
|
|
881
|
+
link: navLink,
|
|
882
|
+
count: navCount,
|
|
883
|
+
};
|
|
884
|
+
sectionViews.push(sectionView);
|
|
885
|
+
navLink.addEventListener('focus', function () {
|
|
886
|
+
var task = section.kind === 'checklist' ? preferredTaskInSection(sectionIndex) : null;
|
|
887
|
+
setKeyboardCursor(sectionIndex, task && task.id);
|
|
888
|
+
});
|
|
889
|
+
navLink.addEventListener('click', function () {
|
|
890
|
+
var task = section.kind === 'checklist' ? preferredTaskInSection(sectionIndex) : null;
|
|
891
|
+
setKeyboardCursor(sectionIndex, task && task.id);
|
|
892
|
+
});
|
|
209
893
|
});
|
|
210
|
-
|
|
894
|
+
shell.append(navigation, checklistBody);
|
|
895
|
+
content.append(shell);
|
|
896
|
+
updateDashboard();
|
|
897
|
+
var initialTask = firstUnresolvedTask();
|
|
898
|
+
setKeyboardCursor(initialTask ? initialTask.sectionIndex : 0, initialTask && initialTask.id, { focus: true, scroll: true });
|
|
211
899
|
setStatus('Opened ' + entry.identity + '.');
|
|
212
900
|
}
|
|
213
901
|
|
|
214
902
|
function showMessage(message, isError) {
|
|
215
903
|
currentEntry = null;
|
|
216
|
-
currentStates =
|
|
904
|
+
currentStates = Object.create(null);
|
|
905
|
+
taskViews = Object.create(null);
|
|
906
|
+
taskSequence = [];
|
|
907
|
+
groupViews = [];
|
|
908
|
+
sectionViews = [];
|
|
909
|
+
keyboardCursor = null;
|
|
217
910
|
document.title = 'Checklist library';
|
|
218
911
|
document.getElementById('title').textContent = 'Choose a checklist';
|
|
219
912
|
document.getElementById('summary').textContent = 'Browse the checklists generated from this repository.';
|
|
220
913
|
content.className = isError ? 'error' : 'empty';
|
|
221
914
|
content.textContent = message;
|
|
222
|
-
|
|
915
|
+
updateDashboard();
|
|
223
916
|
}
|
|
224
917
|
|
|
225
918
|
function renderLibrary(selectedIdentity) {
|
|
226
919
|
document.documentElement.dataset.viewerReady = 'false';
|
|
227
|
-
|
|
920
|
+
var hasChecklists = payload && Array.isArray(payload.checklists) && payload.checklists.length;
|
|
921
|
+
picker.replaceChildren(element('option', '', hasChecklists ? 'Choose a checklist...' : 'No checklists found'));
|
|
228
922
|
picker.firstChild.value = '';
|
|
229
923
|
if (!payload || !Array.isArray(payload.checklists)) {
|
|
230
924
|
showMessage('No generated payload was loaded. Run the checklist build script, then reload this page.', true);
|
|
@@ -232,7 +926,7 @@
|
|
|
232
926
|
return;
|
|
233
927
|
}
|
|
234
928
|
|
|
235
|
-
payload.categories.forEach(function (category) {
|
|
929
|
+
(payload.categories || []).forEach(function (category) {
|
|
236
930
|
var group = element('optgroup');
|
|
237
931
|
group.label = category;
|
|
238
932
|
payload.checklists.filter(function (entry) { return entry.category === category; }).forEach(function (entry) {
|
|
@@ -256,14 +950,15 @@
|
|
|
256
950
|
}
|
|
257
951
|
|
|
258
952
|
function exportProgress() {
|
|
259
|
-
if (!currentEntry) return
|
|
953
|
+
if (!currentEntry) return setActionStatus('Choose a checklist before exporting progress.', true);
|
|
260
954
|
var blob = new Blob([JSON.stringify(progressRecord(), null, 2) + '\n'], { type: 'application/json' });
|
|
261
955
|
var anchor = document.createElement('a');
|
|
262
956
|
anchor.href = URL.createObjectURL(blob);
|
|
263
957
|
anchor.download = currentEntry.identity.replaceAll('/', '-') + '.progress.json';
|
|
264
958
|
anchor.click();
|
|
265
959
|
URL.revokeObjectURL(anchor.href);
|
|
266
|
-
|
|
960
|
+
setActionStatus('Progress export created.');
|
|
961
|
+
actionMenu.open = false;
|
|
267
962
|
}
|
|
268
963
|
|
|
269
964
|
function importProgress(file) {
|
|
@@ -279,12 +974,13 @@
|
|
|
279
974
|
currentStates = normalizedStates(currentEntry, imported.states);
|
|
280
975
|
saveProgress();
|
|
281
976
|
renderChecklist(currentEntry, currentStates);
|
|
282
|
-
|
|
977
|
+
setActionStatus('Progress imported.');
|
|
978
|
+
actionMenu.open = false;
|
|
283
979
|
} catch (error) {
|
|
284
|
-
|
|
980
|
+
setActionStatus('Progress import failed: ' + error.message, true);
|
|
285
981
|
}
|
|
286
982
|
});
|
|
287
|
-
reader.addEventListener('error', function () {
|
|
983
|
+
reader.addEventListener('error', function () { setActionStatus('Progress import failed: the file could not be read.', true); });
|
|
288
984
|
reader.readAsText(file);
|
|
289
985
|
}
|
|
290
986
|
|
|
@@ -308,7 +1004,7 @@
|
|
|
308
1004
|
script.addEventListener('error', function () {
|
|
309
1005
|
refreshPending = false;
|
|
310
1006
|
script.remove();
|
|
311
|
-
setStatus('Could not refresh the generated checklist payload.');
|
|
1007
|
+
setStatus('Could not refresh the generated checklist payload.', true);
|
|
312
1008
|
});
|
|
313
1009
|
document.head.append(script);
|
|
314
1010
|
}
|
|
@@ -320,19 +1016,23 @@
|
|
|
320
1016
|
});
|
|
321
1017
|
document.getElementById('export-progress').addEventListener('click', exportProgress);
|
|
322
1018
|
document.getElementById('import-progress').addEventListener('click', function () {
|
|
323
|
-
if (!currentEntry) return
|
|
1019
|
+
if (!currentEntry) return setActionStatus('Choose a checklist before importing progress.', true);
|
|
324
1020
|
document.getElementById('import-file').click();
|
|
325
1021
|
});
|
|
326
1022
|
document.getElementById('import-file').addEventListener('change', function (event) {
|
|
327
1023
|
importProgress(event.target.files[0]);
|
|
328
1024
|
event.target.value = '';
|
|
329
1025
|
});
|
|
330
|
-
document.getElementById('reset-progress').addEventListener('click',
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
1026
|
+
document.getElementById('reset-progress').addEventListener('click', resetProgress);
|
|
1027
|
+
document.getElementById('current-task-link').addEventListener('click', function (event) {
|
|
1028
|
+
var task = currentEntry && firstUnresolvedTask();
|
|
1029
|
+
if (!task) return;
|
|
1030
|
+
event.preventDefault();
|
|
1031
|
+
setKeyboardCursor(task.sectionIndex, task.id, { focus: true, scroll: true });
|
|
1032
|
+
});
|
|
1033
|
+
document.addEventListener('keydown', handleKeyboardShortcut);
|
|
1034
|
+
document.addEventListener('click', function (event) {
|
|
1035
|
+
if (actionMenu.open && !actionMenu.contains(event.target)) actionMenu.open = false;
|
|
336
1036
|
});
|
|
337
1037
|
window.addEventListener('focus', refreshPayload);
|
|
338
1038
|
|