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