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.
@@ -2,22 +2,30 @@
2
2
  'use strict';
3
3
 
4
4
  var STATES = ['not-started', 'complete', 'failed', 'blocked', 'not-applicable'];
5
- var STATE_LABELS = {
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 setStatus(message) {
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 executableIds(entry) {
45
- var ids = [];
46
- entry.checklist.sections.forEach(function (section) {
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
- ids.push(item.id);
50
- (item.subItems || []).forEach(function (subItem) { ids.push(subItem.id); });
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 ids;
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 updateProgress() {
100
- var ids = currentEntry ? executableIds(currentEntry) : [];
101
- var resolved = ids.filter(function (id) {
102
- return currentStates[id] && currentStates[id] !== 'not-started';
103
- }).length;
104
- document.getElementById('progress-copy').textContent = resolved + ' / ' + ids.length + ' resolved';
105
- document.getElementById('progress-fill').style.width = (ids.length ? resolved / ids.length * 100 : 0) + '%';
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), document.createTextNode(value));
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 stateControl(item, article) {
131
- var label = element('label', 'state-label', 'State');
132
- var select = element('select', 'state-select');
133
- select.setAttribute('aria-label', 'State for ' + item.id + ': ' + item.title);
134
- STATES.forEach(function (stateName) {
135
- var option = element('option', '', STATE_LABELS[stateName]);
136
- option.value = stateName;
137
- select.append(option);
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
- select.value = currentStates[item.id] || 'not-started';
140
- article.dataset.state = select.value;
141
- select.addEventListener('change', function () {
142
- if (select.value === 'not-started') delete currentStates[item.id];
143
- else currentStates[item.id] = select.value;
144
- article.dataset.state = select.value;
145
- saveProgress();
146
- updateProgress();
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
- label.append(select);
149
- return label;
458
+ if (next && taskViews[next.id]) taskViews[next.id].article.scrollIntoView({ block: 'center' });
150
459
  }
151
460
 
152
- function renderItem(item, kind, isSubItem) {
153
- var article = element('article', 'item' + (kind === 'info' ? ' info' : ''));
154
- var head = element('div', 'item-head');
155
- var headingWrap = element('div', 'item-heading');
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
- headingWrap.append(heading);
160
- head.append(headingWrap);
161
- if (kind === 'checklist') head.append(stateControl(item, article));
162
- article.append(head);
163
-
164
- if (item.warning) article.append(textBlock('warning', 'Warning', item.warning));
165
- if (item.instruction) article.append(element('p', 'instruction', item.instruction));
166
- (item.codeBlocks || []).forEach(function (block) {
167
- var pre = element('pre');
168
- pre.append(element('span', 'language', block.language), element('code', '', block.content));
169
- article.append(pre);
170
- });
171
- if (item.expectedResult) article.append(textBlock('expected', 'Expected result', item.expectedResult));
172
- if (item.failureGuidance) article.append(textBlock('failure', 'If this fails', item.failureGuidance));
173
- if (item.links && item.links.length) {
174
- var links = element('ul', 'links');
175
- item.links.forEach(function (link) { links.append(safeLink(link)); });
176
- article.append(links);
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
- if (item.subItems && item.subItems.length) {
179
- var subItems = element('div', 'sub-items');
180
- item.subItems.forEach(function (subItem) { subItems.append(renderItem(subItem, kind, true)); });
181
- article.append(subItems);
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
- checklist.sections.forEach(function (section) {
200
- var sectionElement = element('section');
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(element('p', 'section-intro', section.intro));
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
- content.append(sectionElement);
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
- updateProgress();
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
- updateProgress();
921
+ updateDashboard();
223
922
  }
224
923
 
225
924
  function renderLibrary(selectedIdentity) {
226
925
  document.documentElement.dataset.viewerReady = 'false';
227
- picker.replaceChildren(element('option', '', payload && payload.checklists.length ? 'Choose a checklist...' : 'No checklists found'));
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 setStatus('Choose a checklist before exporting progress.');
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
- setStatus('Progress export created.');
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
- setStatus('Progress imported.');
983
+ setActionStatus('Progress imported.');
984
+ actionMenu.open = false;
283
985
  } catch (error) {
284
- setStatus('Progress import failed: ' + error.message);
986
+ setActionStatus('Progress import failed: ' + error.message, true);
285
987
  }
286
988
  });
287
- reader.addEventListener('error', function () { setStatus('Progress import failed: the file could not be read.'); });
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 setStatus('Choose a checklist before importing progress.');
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', function () {
331
- if (!currentEntry || !window.confirm('Clear progress for this checklist?')) return;
332
- currentStates = {};
333
- saveProgress();
334
- renderChecklist(currentEntry);
335
- setStatus('Progress reset.');
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