@sheetpane/grid 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +294 -13
- package/custom-elements.json +292 -27
- package/dist/check.d.ts +75 -0
- package/dist/check.d.ts.map +1 -0
- package/dist/check.js +151 -0
- package/dist/check.js.map +1 -0
- package/dist/errors/element.d.ts +90 -12
- package/dist/errors/element.d.ts.map +1 -1
- package/dist/errors/element.js +572 -50
- package/dist/errors/element.js.map +1 -1
- package/dist/excel/element.d.ts +168 -15
- package/dist/excel/element.d.ts.map +1 -1
- package/dist/excel/element.js +752 -90
- package/dist/excel/element.js.map +1 -1
- package/dist/excel/link.d.ts +40 -0
- package/dist/excel/link.d.ts.map +1 -0
- package/dist/excel/link.js +90 -0
- package/dist/excel/link.js.map +1 -0
- package/dist/excel/styles.d.ts.map +1 -1
- package/dist/excel/styles.js +37 -5
- package/dist/excel/styles.js.map +1 -1
- package/dist/frameworks/elements.d.ts +57 -17
- package/dist/frameworks/jsx.d.ts +15 -9
- package/dist/frameworks/solid.d.ts +51 -11
- package/dist/frameworks/svelte.d.ts +28 -16
- package/dist/frameworks/vue.d.ts +15 -9
- package/dist/index.d.ts +9 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/dist/llms-full.txt +3027 -239
- package/dist/llms.txt +92 -86
- package/dist/pane/protection.d.ts +61 -0
- package/dist/pane/protection.d.ts.map +1 -0
- package/dist/pane/protection.js +89 -0
- package/dist/pane/protection.js.map +1 -0
- package/dist/pane/rules.d.ts +1 -1
- package/dist/pane/rules.d.ts.map +1 -1
- package/dist/pane/store.d.ts +336 -15
- package/dist/pane/store.d.ts.map +1 -1
- package/dist/pane/store.js +891 -44
- package/dist/pane/store.js.map +1 -1
- package/dist/pane/structure.d.ts +58 -0
- package/dist/pane/structure.d.ts.map +1 -0
- package/dist/pane/structure.js +238 -0
- package/dist/pane/structure.js.map +1 -0
- package/dist/pane/types.d.ts +195 -6
- package/dist/pane/types.d.ts.map +1 -1
- package/dist/pane/types.js +57 -0
- package/dist/pane/types.js.map +1 -1
- package/dist/plugins/types.d.ts +34 -6
- package/dist/plugins/types.d.ts.map +1 -1
- package/dist/plugins/types.js.map +1 -1
- package/dist/shared/detail.d.ts +24 -0
- package/dist/shared/detail.d.ts.map +1 -0
- package/dist/shared/detail.js +75 -0
- package/dist/shared/detail.js.map +1 -0
- package/dist/shared/dom.d.ts +3 -0
- package/dist/shared/dom.d.ts.map +1 -1
- package/dist/shared/dom.js +3 -0
- package/dist/shared/dom.js.map +1 -1
- package/dist/static.d.ts +81 -0
- package/dist/static.d.ts.map +1 -0
- package/dist/static.js +352 -0
- package/dist/static.js.map +1 -0
- package/dist/style.css +3 -3
- package/dist/tokens.d.ts +1 -0
- package/dist/tokens.d.ts.map +1 -1
- package/dist/tokens.js +2 -2
- package/dist/tokens.js.map +1 -1
- package/dist/vscode.css-custom-data.json +11 -1
- package/dist/vscode.html-custom-data.json +89 -2
- package/dist/web-types.json +204 -21
- package/package.json +12 -4
package/dist/errors/element.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
// <sheetpane-errors> — the error component (W-02): the list and the New error
|
|
2
2
|
// form, on the same store as <sheetpane-excel> (SR-LAW-7). It keeps no private
|
|
3
3
|
// copy of errors or workbook data; no "linked" chip, no export footer.
|
|
4
|
-
import { cellRef, colName, parseRange, parseRef } from '@sheetpane/export';
|
|
4
|
+
import { cellRef, colName, parseRange, parseRef, rangeRef } from '@sheetpane/export';
|
|
5
5
|
import { pluginsFromIds } from '../plugins/host.js';
|
|
6
6
|
import { acquirePane, attach, detach, isPrimary, releasePane } from '../pane/registry.js';
|
|
7
|
-
import { createWorkbook } from '../pane/store.js';
|
|
8
|
-
import { BUILTIN_TYPE_NAMES, DEFAULT_ERROR_TYPES, SEVERITY, machineAuthored } from '../pane/types.js';
|
|
7
|
+
import { createWorkbook, editRef, splitKey } from '../pane/store.js';
|
|
8
|
+
import { BUILTIN_TYPE_NAMES, DEFAULT_ERROR_TYPES, DEFAULT_LAYER, SEVERITY, SEVERITY_ORDER, errorScope, isResult, layerOf, machineAuthored, resultsLevelOf } from '../pane/types.js';
|
|
9
9
|
import { BASE_CSS } from '../excel/styles.js';
|
|
10
|
-
import {
|
|
10
|
+
import { DETAIL_CSS, DetailRegion, acceptedHtml, inDetail } from '../shared/detail.js';
|
|
11
|
+
import { ElementBase, ICON, esc, fire, layersAttr } from '../shared/dom.js';
|
|
11
12
|
const CSS = `.grp{padding:8px 12px 2px;font-size:11px;font-weight:600;color:var(--sp-ink-3);text-transform:uppercase;letter-spacing:.04em}
|
|
12
13
|
${BASE_CSS}
|
|
13
14
|
:host{display:block}
|
|
@@ -25,7 +26,7 @@ ${BASE_CSS}
|
|
|
25
26
|
.issue:hover{background:var(--sp-ground)}
|
|
26
27
|
.item.on .issue{background:var(--sp-accent-soft)}
|
|
27
28
|
.issue .n{font-size:11px;font-weight:700;min-width:22px;height:20px;border-radius:10px;color:var(--sp-on-solid);display:inline-flex;align-items:center;justify-content:center;font-variant-numeric:tabular-nums;flex:none}
|
|
28
|
-
.n.error{background:var(--sp-fail)}.n.warning{background:var(--sp-warn)}.n.note{background:var(--sp-accent)}.n.resolved{background:var(--sp-pass)}
|
|
29
|
+
.n.error{background:var(--sp-fail)}.n.warning{background:var(--sp-warn)}.n.note{background:var(--sp-accent)}.n.resolved{background:var(--sp-pass)}.n.accepted{background:var(--sp-surface);color:var(--sp-ink-2);box-shadow:inset 0 0 0 1.5px var(--sp-ink-3)}
|
|
29
30
|
.issue .bd{flex:1;min-width:0;display:flex;flex-direction:column;gap:3px}
|
|
30
31
|
.issue .ty{font-weight:600;font-size:13px}
|
|
31
32
|
.item.done .ty{color:var(--sp-ink-3);text-decoration:line-through}
|
|
@@ -36,6 +37,8 @@ ${BASE_CSS}
|
|
|
36
37
|
.detail{padding:4px 12px 12px 44px;display:flex;flex-direction:column;gap:8px;background:var(--sp-accent-soft)}
|
|
37
38
|
.detail .why{font-size:13px;margin:0}
|
|
38
39
|
.detail .acts{display:flex;gap:6px;flex-wrap:wrap}
|
|
40
|
+
.accept{display:flex;flex-direction:column;gap:6px;padding:8px;border:1px solid var(--sp-line);border-radius:var(--sp-r);background:var(--sp-surface)}
|
|
41
|
+
.accept .acts{justify-content:flex-end}
|
|
39
42
|
.form{border-bottom:1px solid var(--sp-line);display:flex;flex-direction:column;background:var(--sp-surface)}
|
|
40
43
|
.form .fhd{display:flex;align-items:center;gap:8px;padding:10px 12px;border-bottom:1px solid var(--sp-line)}
|
|
41
44
|
.form .fbd{padding:12px;display:flex;flex-direction:column;gap:10px}
|
|
@@ -48,9 +51,38 @@ ${BASE_CSS}
|
|
|
48
51
|
.field{display:flex;flex-direction:column;gap:4px}
|
|
49
52
|
.field>span{font-size:12px;font-weight:500;color:var(--sp-ink-2)}
|
|
50
53
|
.ex{padding:10px;border-radius:var(--sp-r);background:var(--sp-ground);border:1px dashed var(--sp-line-strong);font-size:12px;color:var(--sp-ink-2);display:flex;flex-direction:column;gap:4px}
|
|
54
|
+
.hd .seg>button{height:24px;padding:0 8px;font-size:12px}
|
|
55
|
+
.hd .seg .lc{font-variant-numeric:tabular-nums;color:var(--sp-ink-3)}
|
|
51
56
|
.notice{margin:8px 12px 0;padding:8px 10px;border-radius:var(--sp-r);background:var(--sp-warn-soft);border:1px solid var(--sp-warn);font-size:12px}
|
|
57
|
+
${DETAIL_CSS}
|
|
58
|
+
.summary{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:8px 12px;border-bottom:1px solid var(--sp-line);font-size:12px;color:var(--sp-ink-2)}
|
|
59
|
+
.summary b{color:var(--sp-ink);font-weight:600}.summary .p{color:var(--sp-pass)}.summary .sk{color:var(--sp-ink-3)}
|
|
60
|
+
.sumtext{display:inline-flex;gap:8px;flex-wrap:wrap}
|
|
61
|
+
.sw{margin-left:auto;display:inline-flex;align-items:center;gap:6px;border:0;background:none;padding:2px 0;color:var(--sp-ink-2);font:inherit;font-size:12px;cursor:pointer}
|
|
62
|
+
.sw .tg{width:26px;height:15px;border-radius:8px;background:var(--sp-line-strong);position:relative;flex:none}
|
|
63
|
+
.sw .tg::after{content:"";position:absolute;left:2px;top:2px;width:11px;height:11px;border-radius:50%;background:var(--sp-surface)}
|
|
64
|
+
.sw[aria-checked=true] .tg{background:var(--sp-ink)}.sw[aria-checked=true] .tg::after{left:13px}
|
|
65
|
+
.rg{font-weight:700;min-width:22px;line-height:20px;text-align:center;flex:none}.rg.pass{color:var(--sp-pass)}.rg.skipped{color:var(--sp-skip)}
|
|
66
|
+
.rgrp .grow{display:flex;align-items:center}.rgrp .grow .issue{flex:1;min-width:0}
|
|
67
|
+
.lnk{flex:none;margin-right:8px;color:var(--sp-accent-ink)}
|
|
68
|
+
.cells{overflow:auto;border-top:1px solid var(--sp-line);background:var(--sp-ground)}
|
|
69
|
+
.cells .vsp{position:relative}
|
|
70
|
+
.cellrow{position:absolute;left:0;right:0;height:28px;display:flex;align-items:center;gap:8px;padding:0 12px 0 44px;border:0;background:none;color:var(--sp-ink);font:inherit;font-size:12px;text-align:left;cursor:pointer;white-space:nowrap;overflow:hidden}
|
|
71
|
+
.cellrow:hover{background:var(--sp-surface)}.cellrow .m{color:var(--sp-ink-3);overflow:hidden;text-overflow:ellipsis}
|
|
72
|
+
.edbar{display:flex;align-items:center;gap:8px;padding:6px 12px;border-bottom:1px solid var(--sp-line);font-size:12px;color:var(--sp-ink-2)}
|
|
73
|
+
.edbar .spacer{flex:1}
|
|
74
|
+
.ek{min-width:22px;line-height:20px;text-align:center;flex:none;color:var(--sp-ink-3)}
|
|
75
|
+
.fx{display:flex;flex-wrap:wrap;gap:4px 10px;padding:0 12px 8px 44px;font-size:12px}
|
|
76
|
+
.fxl{border:0;background:none;padding:0;font:inherit;font-size:12px;color:var(--sp-ink-2);cursor:pointer;text-align:left}
|
|
77
|
+
button.fxl{text-decoration:underline;text-underline-offset:2px}button.fxl:hover{color:var(--sp-ink)}
|
|
78
|
+
.fxl .g-cleared{color:var(--sp-pass)}.fxl .g-raised{color:var(--sp-fail)}
|
|
79
|
+
.more{margin:8px 12px}
|
|
52
80
|
`;
|
|
53
|
-
|
|
81
|
+
/** The Edits view (W-64) draws this many rows, then *Show more* another page. */
|
|
82
|
+
const EDITS_PAGE = 100;
|
|
83
|
+
/** A result group's cell rows (W-50): 28 px each, drawn only where its box scrolls to. */
|
|
84
|
+
const CELL_ROW = 28, CELLS_MAX = 280;
|
|
85
|
+
const sevOrder = SEVERITY_ORDER;
|
|
54
86
|
const errNum = (e) => Number(/(\d+)$/.exec(e.id)?.[1] ?? 0);
|
|
55
87
|
/**
|
|
56
88
|
* `<sheetpane-errors>` — the error list and the Flag issue form, on the same in-memory
|
|
@@ -59,25 +91,33 @@ const errNum = (e) => Number(/(\d+)$/.exec(e.id)?.[1] ?? 0);
|
|
|
59
91
|
*
|
|
60
92
|
* Attributes:
|
|
61
93
|
* - `pane` — the pane id shared with the sheet; none gives the element its own store.
|
|
62
|
-
* - `readonly` — hides the form, Resolve, Delete, Undo and Redo (a sign-off screen).
|
|
63
|
-
* - `author` — written into the errors this element's user adds.
|
|
94
|
+
* - `readonly` — hides the form, Resolve, Accept…, Delete, Undo and Redo (a sign-off screen).
|
|
95
|
+
* - `author` — written into the errors this element's user adds, and as who accepted one (W-69).
|
|
64
96
|
* - `plugins` — space-separated registered plugin ids; fixed per pane.
|
|
65
97
|
* - `theme` — `light` (the default), `dark` or `auto`.
|
|
98
|
+
* - `results` — `summary` (the default), `marks` or `off`: how `pass` and `skipped` results show, pane-wide (W-50).
|
|
99
|
+
* - `layers` — space-separated review layers to show (W-51); none shows every layer. Shared by the pane's elements; with two or more layers the header shows a switch.
|
|
66
100
|
*
|
|
67
|
-
* Properties: `workbook`, `errors`, `errorTypes`, `readOnly`, `theme`.
|
|
101
|
+
* Properties: `workbook`, `errors`, `errorTypes`, `readOnly`, `theme`, `results`, `renderDetail`, `layers`, `layerNames`.
|
|
68
102
|
*
|
|
69
|
-
* Methods: `getErrors()`, `setErrors(list, { plugins? })`, `renderNow()`.
|
|
103
|
+
* Methods: `getErrors({ layer? })`, `setErrors(list | envelope, { plugins?, layer? })`, `renderNow()`.
|
|
70
104
|
*
|
|
71
105
|
* Every event bubbles and is composed; an action's event fires once, on the element where it
|
|
72
106
|
* happened.
|
|
73
107
|
*
|
|
74
|
-
* @fires sheetpane-errors `{ errors, plugins }` when the pane's errors change — the envelope to save a review.
|
|
108
|
+
* @fires sheetpane-errors `{ v, errors, plugins, layers, changed }` when the pane's errors change — the envelope to save a review (`v: 2`, W-51); `changed` names the layers whose errors changed.
|
|
109
|
+
* @fires sheetpane-layers `{ visible, layers }` when the visible review layers change — the switch or the `layers` attribute (W-51).
|
|
75
110
|
* @fires sheetpane-change `{ sheet, ref, kind, was, now }` when a cell changes from here ("Also change the cell", undo, redo).
|
|
76
111
|
* @fires sheetpane-select `{ sheet, ref }` when picking a row selects its cell.
|
|
77
112
|
* @fires sheetpane-history `{ reason, canUndo, canRedo }` on undo, redo, or the stack cleared by `setErrors()`.
|
|
78
|
-
* @fires sheetpane-reveal `{ error }` on Show in sheet, so the host can switch tab or open the pop-up.
|
|
113
|
+
* @fires sheetpane-reveal `{ error }` on Show in sheet, so the host can switch tab or open the pop-up — for a whole-sheet error too (its tab is opened); never for a workbook-level one (W-55); for a result group's Show ▸, its first entry (W-50).
|
|
114
|
+
*
|
|
115
|
+
* Views (W-50, W-64): *Problems* (the default); *✓ Passed* and *— Not checked* while the pane holds results; *Edits n* —
|
|
116
|
+
* every change in the pane, newest first, each with its place, kind, was → now, time, the errors it cleared or raised,
|
|
117
|
+
* and its own Revert — in Edit mode, and in View mode while there are edits.
|
|
79
118
|
*
|
|
80
119
|
* @csspart root - The panel's frame: the header, the form, the filters and the list.
|
|
120
|
+
* @csspart detail - The host's region in an open row, after the message: what `renderDetail` returns (W-68).
|
|
81
121
|
*
|
|
82
122
|
* @example
|
|
83
123
|
* ```ts
|
|
@@ -88,14 +128,16 @@ const errNum = (e) => Number(/(\d+)$/.exec(e.id)?.[1] ?? 0);
|
|
|
88
128
|
* document.body.append(list);
|
|
89
129
|
* list.addEventListener('sheetpane-errors', (e) => localStorage.setItem('review', JSON.stringify((e as CustomEvent).detail)));
|
|
90
130
|
* const saved = JSON.parse(localStorage.getItem('review') ?? 'null');
|
|
91
|
-
* if (saved) list.setErrors(saved
|
|
131
|
+
* if (saved) list.setErrors(saved); // the envelope: every layer, and the plugins notice
|
|
132
|
+
* list.setErrors(pdfFindings, { layer: 'pdf' }); // one layer replaced; the others kept
|
|
133
|
+
* list.layerNames = { pdf: 'PDF check' };
|
|
92
134
|
* ```
|
|
93
135
|
* @see https://github.com/arpitarya/sheetpane/blob/main/records/0106_ERRORS.md
|
|
94
136
|
* @see https://github.com/arpitarya/sheetpane/blob/main/packages/grid/README.md#sheetpane-errors
|
|
95
137
|
*/
|
|
96
138
|
export class SheetpaneErrors extends ElementBase {
|
|
97
139
|
/** The attributes the element reacts to. */
|
|
98
|
-
static observedAttributes = ['pane', 'readonly', 'author', 'plugins'];
|
|
140
|
+
static observedAttributes = ['pane', 'readonly', 'author', 'plugins', 'results', 'layers'];
|
|
99
141
|
_store;
|
|
100
142
|
_explicitStore;
|
|
101
143
|
_paneId;
|
|
@@ -109,12 +151,30 @@ export class SheetpaneErrors extends ElementBase {
|
|
|
109
151
|
draft = { type: DEFAULT_ERROR_TYPES[0].id, message: '', expected: '', severity: DEFAULT_ERROR_TYPES[0].severity, change: false };
|
|
110
152
|
formKey = '';
|
|
111
153
|
savedPlugins;
|
|
154
|
+
detailRegion = new DetailRegion();
|
|
155
|
+
/** W-69: the Accepted chip is on (only accepted errors listed); the error whose Accept form is open, and its reason so far. */
|
|
156
|
+
showAccepted = false;
|
|
157
|
+
/** W-51: the switch's button to focus after a re-draw (`''` is All). */
|
|
158
|
+
focusLayer = '';
|
|
159
|
+
accepting;
|
|
160
|
+
reasonDraft = '';
|
|
161
|
+
focusNext;
|
|
162
|
+
_layerNames = {};
|
|
163
|
+
view = 'problems';
|
|
164
|
+
openGroups = new Set();
|
|
165
|
+
groupEntries = new Map();
|
|
166
|
+
/** A control to focus again once the list is redrawn (a group toggled). */
|
|
167
|
+
refocus;
|
|
168
|
+
/** W-64: how many edits the Edits view draws; the Edits chip to focus once the last row is reverted. */
|
|
169
|
+
editsLimit = EDITS_PAGE;
|
|
170
|
+
focusEditsChip = false;
|
|
112
171
|
// ── properties ───────────────────────────────────────────────────────────
|
|
113
172
|
/** The pane's store; assigning one replaces the `pane` lookup. */
|
|
114
173
|
get workbook() { return this._store; }
|
|
115
174
|
set workbook(s) { this._explicitStore = s; if (this.isConnected) {
|
|
116
175
|
this.unbind();
|
|
117
176
|
this.bind();
|
|
177
|
+
this.applyLayers();
|
|
118
178
|
this.schedule();
|
|
119
179
|
} }
|
|
120
180
|
/** A copy of the pane's errors; assigning restores a saved review, like `setErrors()`. */
|
|
@@ -129,24 +189,60 @@ export class SheetpaneErrors extends ElementBase {
|
|
|
129
189
|
/** `light` (the default), `dark` or `auto` — the same tokens as the sheet (W-29). */
|
|
130
190
|
get theme() { const t = this.getAttribute('theme'); return t === 'dark' || t === 'auto' ? t : 'light'; }
|
|
131
191
|
set theme(t) { this.setAttribute('theme', t); }
|
|
132
|
-
/**
|
|
133
|
-
|
|
134
|
-
|
|
192
|
+
/**
|
|
193
|
+
* The host's extra detail for an error (W-68): called with a copy of the error when its row is opened, and drawn after
|
|
194
|
+
* the message in a `part="detail"` region. A string is text, never HTML; a Node is the host's own. Called again only when
|
|
195
|
+
* the error changes (its `meta` included) or this is replaced; a throw is logged as SP-3017. Set it on each element
|
|
196
|
+
* that should show it — the sheet's error card has its own.
|
|
197
|
+
*/
|
|
198
|
+
get renderDetail() { return this.detailRegion.render; }
|
|
199
|
+
set renderDetail(f) { this.detailRegion.render = f; this.schedule(); }
|
|
200
|
+
/** The pane's results level (W-50): `summary` (the default), `marks` or `off`; setting it sets the `results` attribute. */
|
|
201
|
+
get results() { return this._store?.results ?? resultsLevelOf(this.getAttribute('results')); }
|
|
202
|
+
set results(v) { this.setAttribute('results', v); }
|
|
203
|
+
/**
|
|
204
|
+
* The review layers shown (W-51) — the pane's, shared with `<sheetpane-excel>`: every layer until the `layers`
|
|
205
|
+
* attribute or the switch names some. Setting it sets the attribute; `null` or `[]` shows every layer.
|
|
206
|
+
*/
|
|
207
|
+
get layers() { return [...(this._store?.getSnapshot().visibleLayers ?? layersAttr(this.getAttribute('layers')) ?? [])]; }
|
|
208
|
+
set layers(v) { if (v && v.length)
|
|
209
|
+
this.setAttribute('layers', v.join(' '));
|
|
210
|
+
else
|
|
211
|
+
this.removeAttribute('layers'); this._store?.setVisibleLayers(v, this); }
|
|
212
|
+
/** What the switch and the rows call each layer (W-51), e.g. `{ pdf: 'PDF check' }`; a layer not named here shows its name (`review` as *Review*). */
|
|
213
|
+
get layerNames() { return this._layerNames; }
|
|
214
|
+
set layerNames(m) { this._layerNames = { ...(m ?? {}) }; this.schedule(); }
|
|
215
|
+
/** A copy of the pane's errors, to save a review; `{ layer }` gives one layer's (W-51). */
|
|
216
|
+
getErrors(o = {}) { return this._store?.getErrors(o) ?? []; }
|
|
217
|
+
/**
|
|
218
|
+
* Restores a saved review — a list, or the `sheetpane-errors` envelope with its layers — or, with `layer`, replaces that
|
|
219
|
+
* layer only (W-51). `plugins` (from the saved envelope; read from it when it is passed whole) shows a notice when it
|
|
220
|
+
* differs from this pane's set.
|
|
221
|
+
*/
|
|
135
222
|
setErrors(list, o = {}) {
|
|
136
|
-
|
|
137
|
-
|
|
223
|
+
if (o.layer === undefined)
|
|
224
|
+
this.savedPlugins = o.plugins ?? (Array.isArray(list) ? undefined : list.plugins);
|
|
225
|
+
this.ensureStore().setErrors(list, o.layer === undefined ? {} : { layer: o.layer }, this);
|
|
138
226
|
}
|
|
227
|
+
layerName(l) { return this._layerNames[l] ?? (l === DEFAULT_LAYER ? 'Review' : l); }
|
|
228
|
+
/** The `layers` attribute, given to the store (connect, a new store). */
|
|
229
|
+
applyLayers() { if (this.hasAttribute('layers'))
|
|
230
|
+
this._store?.setVisibleLayers(layersAttr(this.getAttribute('layers')), this); }
|
|
139
231
|
/** Builds the shadow root once and binds to the pane's store. */
|
|
140
232
|
connectedCallback() {
|
|
141
233
|
if (!this.shadow) {
|
|
142
234
|
this.shadow = this.attachShadow({ mode: 'open' });
|
|
143
|
-
this.shadow.innerHTML = `<style>${CSS}</style><div class="spe" part="root"><div class="hd"><span class="t">Errors</span><span class="chip opencount"></span><span class="spacer" style="flex:1"></span><button type="button" class="btn btn-g btn-i" data-act="undo" aria-label="Undo (Ctrl+Z)" title="Undo">${ICON.undo}</button><button type="button" class="btn btn-g btn-i" data-act="redo" aria-label="Redo (Ctrl+Shift+Z)" title="Redo">${ICON.redo}</button></div><div class="scrollarea"><div class="noticebox"></div><div class="formbox"></div><div class="filters" role="toolbar" aria-label="Filter errors"></div><div class="list" role="list"></div></div></div>`;
|
|
235
|
+
this.shadow.innerHTML = `<style>${CSS}</style><div class="spe" part="root"><div class="hd"><span class="t">Errors</span><span class="chip opencount"></span><span class="layerbox"></span><span class="spacer" style="flex:1"></span><button type="button" class="btn btn-g btn-i" data-act="undo" aria-label="Undo (Ctrl+Z)" title="Undo">${ICON.undo}</button><button type="button" class="btn btn-g btn-i" data-act="redo" aria-label="Redo (Ctrl+Shift+Z)" title="Redo">${ICON.redo}</button></div><div class="summary" hidden><span class="sumtext"></span><button type="button" class="sw" role="switch" aria-checked="false" data-act="onsheet"><span class="tg" aria-hidden="true"></span>On sheet</button></div><div class="scrollarea"><div class="noticebox"></div><div class="formbox"></div><div class="filters" role="toolbar" aria-label="Filter errors"></div><div class="edbar" hidden></div><div class="list" role="list"></div></div></div>`;
|
|
144
236
|
this.shadow.addEventListener('click', (e) => this.onClick(e));
|
|
145
237
|
this.shadow.addEventListener('input', (e) => this.onInput(e));
|
|
146
238
|
this.shadow.addEventListener('change', (e) => this.onInput(e));
|
|
147
239
|
this.shadow.addEventListener('keydown', (e) => this.onKey(e));
|
|
240
|
+
// a result group's cells scroll in their own box: draw the rows it shows (scroll does not bubble — capture)
|
|
241
|
+
this.shadow.addEventListener('scroll', (e) => { const t = e.target; if (t.classList?.contains('cells'))
|
|
242
|
+
this.fillCells(t); }, true);
|
|
148
243
|
}
|
|
149
244
|
this.bind();
|
|
245
|
+
this.applyLayers();
|
|
150
246
|
this.schedule();
|
|
151
247
|
}
|
|
152
248
|
/** Releases the pane's store. */
|
|
@@ -158,7 +254,12 @@ export class SheetpaneErrors extends ElementBase {
|
|
|
158
254
|
if (name === 'pane' || name === 'plugins') {
|
|
159
255
|
this.unbind();
|
|
160
256
|
this.bind();
|
|
257
|
+
this.applyLayers();
|
|
161
258
|
}
|
|
259
|
+
else if (name === 'results' && val !== null)
|
|
260
|
+
this._store?.setResults(resultsLevelOf(val));
|
|
261
|
+
else if (name === 'layers')
|
|
262
|
+
this._store?.setVisibleLayers(layersAttr(val), this);
|
|
162
263
|
this.schedule();
|
|
163
264
|
}
|
|
164
265
|
ensureStore() { if (!this._store)
|
|
@@ -178,6 +279,8 @@ export class SheetpaneErrors extends ElementBase {
|
|
|
178
279
|
this._store = createWorkbook({ plugins: pluginsFromIds(plugins) });
|
|
179
280
|
const s = this._store;
|
|
180
281
|
attach(s, this);
|
|
282
|
+
if (this.hasAttribute('results'))
|
|
283
|
+
s.setResults(resultsLevelOf(this.getAttribute('results')));
|
|
181
284
|
const detachView = s.attachErrorsView();
|
|
182
285
|
this.unsub.push(s.subscribe(() => this.schedule()), s.on((e) => this.onStoreEvent(e)), () => detach(s, this), detachView);
|
|
183
286
|
}
|
|
@@ -196,7 +299,9 @@ export class SheetpaneErrors extends ElementBase {
|
|
|
196
299
|
return;
|
|
197
300
|
const s = this._store;
|
|
198
301
|
if (e.type === 'errors')
|
|
199
|
-
fire(this, 'sheetpane-errors',
|
|
302
|
+
fire(this, 'sheetpane-errors', s.envelope(e.layers));
|
|
303
|
+
else if (e.type === 'layers')
|
|
304
|
+
fire(this, 'sheetpane-layers', { visible: [...s.getSnapshot().visibleLayers], layers: [...s.getSnapshot().layers] });
|
|
200
305
|
else if (e.type === 'change')
|
|
201
306
|
fire(this, 'sheetpane-change', { sheet: e.sheet, ref: e.ref, kind: e.kind, was: e.was, now: e.now });
|
|
202
307
|
else if (e.type === 'select')
|
|
@@ -213,6 +318,8 @@ export class SheetpaneErrors extends ElementBase {
|
|
|
213
318
|
renderNow() { cancelAnimationFrame(this.raf); this.raf = 0; this.render(); }
|
|
214
319
|
// ── interactions ─────────────────────────────────────────────────────────
|
|
215
320
|
onClick(e) {
|
|
321
|
+
if (inDetail(e.target))
|
|
322
|
+
return; // the host's region: its clicks are the host's (W-68)
|
|
216
323
|
const t = e.target, s = this._store;
|
|
217
324
|
const el = t.closest('[data-act]');
|
|
218
325
|
if (!el)
|
|
@@ -228,28 +335,96 @@ export class SheetpaneErrors extends ElementBase {
|
|
|
228
335
|
this.authorFilter = el.dataset.v;
|
|
229
336
|
this.schedule();
|
|
230
337
|
break;
|
|
338
|
+
case 'status':
|
|
339
|
+
this.showAccepted = !this.showAccepted;
|
|
340
|
+
this.schedule();
|
|
341
|
+
break;
|
|
342
|
+
case 'view':
|
|
343
|
+
this.view = el.dataset.v;
|
|
344
|
+
this.schedule();
|
|
345
|
+
break;
|
|
346
|
+
case 'onsheet':
|
|
347
|
+
s.setResults(s.results === 'marks' ? 'summary' : 'marks');
|
|
348
|
+
break;
|
|
349
|
+
case 'group': {
|
|
350
|
+
const g = el.closest('[data-g]').dataset.g;
|
|
351
|
+
if (!this.openGroups.delete(g))
|
|
352
|
+
this.openGroups.add(g);
|
|
353
|
+
this.refocus = `[data-g="${globalThis.CSS.escape(g)}"] [data-act=group]`;
|
|
354
|
+
this.schedule();
|
|
355
|
+
break;
|
|
356
|
+
}
|
|
357
|
+
case 'show-group':
|
|
358
|
+
this.showGroup(el.closest('[data-g]').dataset.g);
|
|
359
|
+
break;
|
|
360
|
+
case 'layer': { // W-51: one layer, or every layer
|
|
361
|
+
const v = el.dataset.v;
|
|
362
|
+
if (s.setVisibleLayers(v ? [v] : null, this)) {
|
|
363
|
+
this.expanded = undefined;
|
|
364
|
+
this.focusNext = 'layer';
|
|
365
|
+
this.focusLayer = v ?? '';
|
|
366
|
+
}
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
369
|
+
case 'edit-pick':
|
|
370
|
+
this.showEdit(el.closest('[data-k]').dataset.k);
|
|
371
|
+
break;
|
|
372
|
+
case 'revert-edit':
|
|
373
|
+
this.revertEdit(el.closest('[data-k]').dataset.k);
|
|
374
|
+
break;
|
|
375
|
+
case 'revert-all':
|
|
376
|
+
if (!this.readOnly && s.revertEdits(undefined, this))
|
|
377
|
+
this.focusEditsChip = true;
|
|
378
|
+
break;
|
|
379
|
+
case 'edits-more':
|
|
380
|
+
this.editsLimit += EDITS_PAGE;
|
|
381
|
+
this.refocus = `.item.edit:nth-child(${this.editsLimit - EDITS_PAGE + 1}) [data-act=edit-pick]`;
|
|
382
|
+
this.schedule();
|
|
383
|
+
break;
|
|
384
|
+
case 'goto-error': // an edit's cleared or raised error: shown in the Problems view, opened (W-64)
|
|
385
|
+
if (!err)
|
|
386
|
+
return;
|
|
387
|
+
this.view = 'problems';
|
|
388
|
+
this.filter = 'all';
|
|
389
|
+
this.authorFilter = 'all';
|
|
390
|
+
this.showAccepted = err.status === 'accepted';
|
|
391
|
+
this.expanded = err.id;
|
|
392
|
+
this.showPlace(err);
|
|
393
|
+
this.refocus = `[data-id="${globalThis.CSS.escape(err.id)}"] .issue`;
|
|
394
|
+
this.schedule();
|
|
395
|
+
break;
|
|
231
396
|
case 'pick':
|
|
232
397
|
if (!err)
|
|
233
398
|
return;
|
|
234
399
|
this.expanded = this.expanded === err.id ? undefined : err.id;
|
|
235
|
-
|
|
236
|
-
const r = parseRange(err.range);
|
|
237
|
-
s.select({ sheet: err.sheet, ref: cellRef(r.top, r.left) }, this);
|
|
238
|
-
}
|
|
400
|
+
this.showPlace(err);
|
|
239
401
|
this.schedule();
|
|
240
402
|
break;
|
|
241
403
|
case 'reveal':
|
|
242
|
-
if (!err)
|
|
243
|
-
return;
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
s.
|
|
247
|
-
}
|
|
404
|
+
if (!err || errorScope(err) === 'workbook')
|
|
405
|
+
return; // a workbook-level error has no sheet to show (W-55)
|
|
406
|
+
this.showPlace(err);
|
|
407
|
+
if (isResult(err))
|
|
408
|
+
s.focusResults(err.sheet, err.range); // a result shown from its group is marked, even at `summary` (W-50)
|
|
248
409
|
fire(this, 'sheetpane-reveal', { error: { ...err } });
|
|
249
410
|
break;
|
|
250
411
|
case 'resolve':
|
|
251
412
|
if (err && !this.readOnly)
|
|
252
413
|
s.updateError(err.id, { status: err.status === 'open' ? 'resolved' : 'open' }, this);
|
|
414
|
+
break; // Reopen also takes back an acceptance (W-69)
|
|
415
|
+
case 'accept': // W-69: the reason form, in the open row
|
|
416
|
+
if (!err || err.status !== 'open' || this.readOnly || machineAuthored(err.author))
|
|
417
|
+
return;
|
|
418
|
+
this.accepting = this.accepting === err.id ? undefined : err.id;
|
|
419
|
+
this.reasonDraft = '';
|
|
420
|
+
this.focusNext = this.accepting ? 'reason' : undefined;
|
|
421
|
+
this.schedule();
|
|
422
|
+
break;
|
|
423
|
+
case 'accept-cancel':
|
|
424
|
+
this.closeAccept();
|
|
425
|
+
break;
|
|
426
|
+
case 'accept-ok':
|
|
427
|
+
this.acceptNow();
|
|
253
428
|
break;
|
|
254
429
|
case 'delete':
|
|
255
430
|
if (err && !this.readOnly) {
|
|
@@ -288,6 +463,8 @@ export class SheetpaneErrors extends ElementBase {
|
|
|
288
463
|
}
|
|
289
464
|
}
|
|
290
465
|
onInput(e) {
|
|
466
|
+
if (inDetail(e.target))
|
|
467
|
+
return;
|
|
291
468
|
const t = e.target, f = t.dataset.field;
|
|
292
469
|
if (f === 'message')
|
|
293
470
|
this.draft.message = t.value;
|
|
@@ -295,9 +472,39 @@ export class SheetpaneErrors extends ElementBase {
|
|
|
295
472
|
this.draft.expected = t.value;
|
|
296
473
|
else if (f === 'change')
|
|
297
474
|
this.draft.change = t.checked;
|
|
475
|
+
else if (f === 'reason') {
|
|
476
|
+
this.reasonDraft = t.value;
|
|
477
|
+
const ok = this.shadow.querySelector('[data-act=accept-ok]');
|
|
478
|
+
if (ok)
|
|
479
|
+
ok.disabled = !t.value.trim();
|
|
480
|
+
}
|
|
298
481
|
}
|
|
299
482
|
onKey(e) {
|
|
483
|
+
if (inDetail(e.composedPath()[0] ?? null))
|
|
484
|
+
return; // the host's keys in its region (W-68)
|
|
300
485
|
const typing = e.composedPath()[0]?.matches?.('input, textarea');
|
|
486
|
+
if (this.accepting && e.composedPath()[0]?.dataset?.field === 'reason') { // W-69: the reason form
|
|
487
|
+
if (e.key === 'Escape') {
|
|
488
|
+
e.preventDefault();
|
|
489
|
+
this.closeAccept();
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
|
|
493
|
+
e.preventDefault();
|
|
494
|
+
this.acceptNow();
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
const at = e.composedPath()[0];
|
|
499
|
+
if (at?.dataset?.act === 'edit-pick' && ['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(e.key)) { // W-64: the Edits list moves by row
|
|
500
|
+
const rows = Array.from(this.shadow.querySelectorAll('.list [data-act=edit-pick]')), i = rows.indexOf(at);
|
|
501
|
+
const to = e.key === 'Home' ? 0 : e.key === 'End' ? rows.length - 1 : i + (e.key === 'ArrowDown' ? 1 : -1);
|
|
502
|
+
if (rows[to]) {
|
|
503
|
+
e.preventDefault();
|
|
504
|
+
rows[to].focus();
|
|
505
|
+
}
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
301
508
|
if (!typing && (e.metaKey || e.ctrlKey) && (e.key === 'z' || e.key === 'Z')) {
|
|
302
509
|
e.preventDefault();
|
|
303
510
|
if (e.shiftKey)
|
|
@@ -320,6 +527,54 @@ export class SheetpaneErrors extends ElementBase {
|
|
|
320
527
|
this._store.compose(null);
|
|
321
528
|
}
|
|
322
529
|
}
|
|
530
|
+
/** Selects an error's cell; a sheet-level error opens its tab (W-55), keeping the selection when it is already there; a workbook-level one has no place. */
|
|
531
|
+
showPlace(err) {
|
|
532
|
+
const s = this._store, scope = errorScope(err);
|
|
533
|
+
if (scope === 'cell') {
|
|
534
|
+
const r = parseRange(err.range);
|
|
535
|
+
if (r)
|
|
536
|
+
s.select({ sheet: err.sheet, ref: cellRef(r.top, r.left) }, this);
|
|
537
|
+
}
|
|
538
|
+
else if (scope === 'sheet' && s.getSnapshot().selection.sheet !== err.sheet && s.sheet(err.sheet))
|
|
539
|
+
s.select({ sheet: err.sheet, ref: 'A1' }, this); // as a tab click does
|
|
540
|
+
}
|
|
541
|
+
/** Selects an edit's cell or range (W-64); an edit about a whole sheet opens its tab, keeping the selection when it is already there. */
|
|
542
|
+
showEdit(k) {
|
|
543
|
+
const s = this._store, { sheet, ref } = splitKey(k), r = parseRange(editRef(ref));
|
|
544
|
+
if (r) {
|
|
545
|
+
const range = rangeRef(r), at = cellRef(r.top, r.left);
|
|
546
|
+
s.select(range === at ? { sheet, ref: at } : { sheet, ref: at, range }, this);
|
|
547
|
+
}
|
|
548
|
+
else if (s.getSnapshot().selection.sheet !== sheet && s.sheet(sheet))
|
|
549
|
+
s.select({ sheet, ref: 'A1' }, this);
|
|
550
|
+
}
|
|
551
|
+
/** Reverts one edit (W-64) — itself one undo step. Focus moves to the next row's Revert, else the previous one's, else the Edits chip. */
|
|
552
|
+
revertEdit(k) {
|
|
553
|
+
if (this.readOnly)
|
|
554
|
+
return;
|
|
555
|
+
const keys = Array.from(this.shadow.querySelectorAll('.list [data-k]')).map((x) => x.dataset.k), i = keys.indexOf(k);
|
|
556
|
+
if (!this._store.revertEdits([k], this))
|
|
557
|
+
return;
|
|
558
|
+
const next = keys[i + 1] ?? keys[i - 1];
|
|
559
|
+
if (next)
|
|
560
|
+
this.refocus = `[data-k="${globalThis.CSS.escape(next)}"] [data-act=revert-edit]`;
|
|
561
|
+
else
|
|
562
|
+
this.focusEditsChip = true;
|
|
563
|
+
}
|
|
564
|
+
/** Closes the reason form; focus goes back to Accept…. */
|
|
565
|
+
closeAccept() { this.accepting = undefined; this.reasonDraft = ''; this.focusNext = 'accept'; this.schedule(); }
|
|
566
|
+
/** Accepts the error whose form is open (W-69): only with a reason; who is this element's `author`. The row moves under the Accepted chip, which takes focus. */
|
|
567
|
+
acceptNow() {
|
|
568
|
+
const id = this.accepting;
|
|
569
|
+
if (!id || this.readOnly || !this.reasonDraft.trim())
|
|
570
|
+
return;
|
|
571
|
+
if (!this._store.acceptError(id, this.reasonDraft, { by: this.getAttribute('author') ?? '' }, this))
|
|
572
|
+
return;
|
|
573
|
+
this.accepting = undefined;
|
|
574
|
+
this.reasonDraft = '';
|
|
575
|
+
this.focusNext = 'chip';
|
|
576
|
+
this.schedule();
|
|
577
|
+
}
|
|
323
578
|
add() {
|
|
324
579
|
const s = this._store, c = s.getSnapshot().composing;
|
|
325
580
|
if (!c || this.readOnly)
|
|
@@ -335,6 +590,19 @@ export class SheetpaneErrors extends ElementBase {
|
|
|
335
590
|
this.draft = { type: t.id, message: '', expected: '', severity: t.severity, change: false };
|
|
336
591
|
this.formKey = '';
|
|
337
592
|
}
|
|
593
|
+
/** Show ▸ (W-50): selects the group's range on its first sheet and marks that range's results, even at `summary`. */
|
|
594
|
+
showGroup(key) {
|
|
595
|
+
const s = this._store, entries = this.groupEntries.get(key);
|
|
596
|
+
if (!entries?.length)
|
|
597
|
+
return;
|
|
598
|
+
const sheet = entries[0].sheet, box = boxOf(entries.filter((e) => e.sheet === sheet));
|
|
599
|
+
if (!box)
|
|
600
|
+
return;
|
|
601
|
+
const range = rangeRef(box), ref = cellRef(box.top, box.left);
|
|
602
|
+
s.select(range === ref ? { sheet, ref } : { sheet, ref, range }, this);
|
|
603
|
+
s.focusResults(sheet, range);
|
|
604
|
+
fire(this, 'sheetpane-reveal', { error: { ...entries[0] } });
|
|
605
|
+
}
|
|
338
606
|
// ── rendering ────────────────────────────────────────────────────────────
|
|
339
607
|
authorKind(e) { return e.author === 'workbook' ? 'workbook' : e.author === 'host' ? 'host' : e.author === 'file' ? 'file' : 'person'; }
|
|
340
608
|
render() {
|
|
@@ -343,9 +611,40 @@ export class SheetpaneErrors extends ElementBase {
|
|
|
343
611
|
return;
|
|
344
612
|
const snap = s.getSnapshot();
|
|
345
613
|
const q = (sel) => this.shadow.querySelector(sel);
|
|
346
|
-
|
|
614
|
+
// counts that ask for action count problems only; results are counted apart (W-50)
|
|
615
|
+
// W-51: what the visible layers hold — the sheet draws the same
|
|
616
|
+
const errors = snap.visibleErrors.filter((e) => !isResult(e));
|
|
347
617
|
const open = errors.filter((e) => e.status === 'open');
|
|
348
618
|
q('.opencount').textContent = `${open.length} open`;
|
|
619
|
+
const rc = s.resultCounts(), hasResults = s.results !== 'off' && rc.pass + rc.skipped > 0;
|
|
620
|
+
// W-64: the Edits view — in Edit mode, and in View mode while the pane holds edits
|
|
621
|
+
const editCount = snap.changes.size, hasEdits = editCount > 0 || snap.mode === 'edit';
|
|
622
|
+
if ((this.view === 'pass' && !(hasResults && rc.pass)) || (this.view === 'skipped' && !(hasResults && rc.skipped)) || (this.view === 'edits' && !hasEdits))
|
|
623
|
+
this.view = 'problems';
|
|
624
|
+
q('.summary').hidden = !hasResults;
|
|
625
|
+
if (hasResults) {
|
|
626
|
+
const sum = `<span><b>${rc.open}</b> problem${rc.open === 1 ? '' : 's'}</span><span class="p">✓ ${rc.pass} passed</span><span class="sk">— ${rc.skipped} not checked</span>`;
|
|
627
|
+
const st = q('.sumtext');
|
|
628
|
+
if (st.dataset.html !== sum) {
|
|
629
|
+
st.innerHTML = sum;
|
|
630
|
+
st.dataset.html = sum;
|
|
631
|
+
}
|
|
632
|
+
q('.sw').setAttribute('aria-checked', String(s.results === 'marks'));
|
|
633
|
+
}
|
|
634
|
+
// W-51: the layers switch, with two or more layers — All, then each layer with its open count
|
|
635
|
+
const multi = snap.layers.length > 1;
|
|
636
|
+
const allOn = snap.visibleLayers.length === snap.layers.length;
|
|
637
|
+
const lb = q('.layerbox');
|
|
638
|
+
const layersHtml = multi ? `<div class="seg" role="group" aria-label="Review layers"><button type="button" data-act="layer" data-v="" aria-pressed="${allOn}">All</button>${snap.layers.map((l) => `<button type="button" data-act="layer" data-v="${esc(l)}" data-sp-layer="${esc(l)}" aria-pressed="${snap.visibleLayers.includes(l)}">${esc(this.layerName(l))} <span class="lc">${snap.errors.filter((e) => e.status === 'open' && !isResult(e) && layerOf(e) === l).length}</span></button>`).join('')}</div>` : '';
|
|
639
|
+
if (lb.dataset.html !== layersHtml) {
|
|
640
|
+
lb.innerHTML = layersHtml;
|
|
641
|
+
lb.dataset.html = layersHtml;
|
|
642
|
+
}
|
|
643
|
+
if (this.focusNext === 'layer') {
|
|
644
|
+
[...lb.querySelectorAll('[data-act=layer]')].find((b) => b.dataset.v === this.focusLayer)?.focus();
|
|
645
|
+
this.focusNext = undefined;
|
|
646
|
+
}
|
|
647
|
+
const tagLayer = multi && snap.visibleLayers.length > 1;
|
|
349
648
|
q('[data-act=undo]').disabled = !snap.canUndo || this.readOnly;
|
|
350
649
|
q('[data-act=redo]').disabled = !snap.canRedo || this.readOnly;
|
|
351
650
|
q('[data-act=undo]').hidden = q('[data-act=redo]').hidden = this.readOnly || snap.mode !== 'edit';
|
|
@@ -353,14 +652,32 @@ export class SheetpaneErrors extends ElementBase {
|
|
|
353
652
|
const differ = this.savedPlugins !== undefined && this.savedPlugins.join(' ') !== s.pluginIds.join(' ');
|
|
354
653
|
q('.noticebox').innerHTML = differ ? `<div class="notice" role="status">This review was saved with ${this.savedPlugins.length ? esc(this.savedPlugins.join(', ')) : 'no plugins'}; this pane runs ${s.pluginIds.length ? esc(s.pluginIds.join(', ')) : 'none'} — cells may be formatted differently.</div>` : '';
|
|
355
654
|
// filters: severity, and author once more than people write errors (W-18 DoD 5)
|
|
356
|
-
|
|
655
|
+
// W-69: accepted errors are not counted; the Accepted chip lists them instead of the rest
|
|
656
|
+
const accepted = errors.filter((e) => e.status === 'accepted');
|
|
657
|
+
if (!accepted.length)
|
|
658
|
+
this.showAccepted = false;
|
|
659
|
+
const counted = errors.filter((e) => e.status !== 'accepted');
|
|
660
|
+
const counts = { all: counted.length, error: counted.filter((e) => e.severity === 'error').length, warning: counted.filter((e) => e.severity === 'warning').length, note: counted.filter((e) => e.severity === 'note').length };
|
|
661
|
+
// Problems · Passed · Not checked — the result chips only when results exist; Problems is the default (W-50); then Edits (W-64)
|
|
662
|
+
const views = [['problems', `Problems ${counts.all}`, `Problems: ${counts.all}`], ['pass', `✓ Passed ${rc.pass}`, `Passed: ${rc.pass} cells`], ['skipped', `— Not checked ${rc.skipped}`, `Not checked: ${rc.skipped} cells`], ['edits', `Edits ${editCount}`, `Edits: ${editCount}`]]
|
|
663
|
+
.filter(([v]) => v === 'problems' || (v === 'edits' ? hasEdits : hasResults && rc[v] > 0));
|
|
664
|
+
const viewBtns = views.length > 1
|
|
665
|
+
? `${views.map(([v, label, name]) => `<button type="button" data-act="view" data-v="${v}" aria-pressed="${this.view === v}" aria-label="${name}">${label}</button>`).join('')}${this.view === 'problems' ? '<span class="sep" aria-hidden="true"></span>' : ''}`
|
|
666
|
+
: '';
|
|
357
667
|
const authors = new Set(errors.map((e) => this.authorKind(e)));
|
|
358
668
|
const sevBtns = [['all', `All ${counts.all}`, 'All severities'], ['error', `✕ ${counts.error}`, 'Errors'], ['warning', `! ${counts.warning}`, 'Warnings'], ['note', `i ${counts.note}`, 'Notes']]
|
|
359
669
|
.map(([v, label, name]) => `<button type="button" data-act="filter" data-v="${v}" aria-pressed="${this.filter === v}" aria-label="${name}: ${counts[v]}">${label}</button>`).join('');
|
|
360
670
|
const authorBtns = authors.size > 1 || (authors.size === 1 && !authors.has('person'))
|
|
361
671
|
? `<span class="sep" aria-hidden="true"></span>${['all', 'person', 'workbook', 'host', 'file'].filter((a) => a === 'all' || authors.has(a)).map((a) => `<button type="button" data-act="author" data-v="${a}" aria-pressed="${this.authorFilter === a}">${a === 'all' ? 'Anyone' : a === 'person' ? 'People' : a === 'workbook' ? 'Workbook rules' : a === 'host' ? (s.hasRulesDocument ? 'Rules file' : 'Column rules') : 'File comments'}</button>`).join('')}`
|
|
362
672
|
: '';
|
|
363
|
-
|
|
673
|
+
const acceptedBtn = accepted.length ? `<span class="sep" aria-hidden="true"></span><button type="button" data-act="status" data-v="accepted" aria-pressed="${this.showAccepted}">Accepted ${accepted.length}</button>` : '';
|
|
674
|
+
// the severity, Accepted and author filters are the Problems view's (W-50)
|
|
675
|
+
const filtersHtml = viewBtns + (this.view === 'problems' ? sevBtns + acceptedBtn + authorBtns : '');
|
|
676
|
+
const fl = q('.filters');
|
|
677
|
+
if (fl.dataset.html !== filtersHtml) {
|
|
678
|
+
fl.innerHTML = filtersHtml;
|
|
679
|
+
fl.dataset.html = filtersHtml;
|
|
680
|
+
}
|
|
364
681
|
// the form (contract decision 4: this element shows it whenever it is connected to the pane)
|
|
365
682
|
const formbox = q('.formbox');
|
|
366
683
|
const c = snap.composing;
|
|
@@ -378,33 +695,147 @@ export class SheetpaneErrors extends ElementBase {
|
|
|
378
695
|
}
|
|
379
696
|
// the list
|
|
380
697
|
const list = errors
|
|
381
|
-
.filter((e) => (this.filter === 'all' || e.severity === this.filter) && (this.authorFilter === 'all' || this.authorKind(e) === this.authorFilter))
|
|
382
|
-
.sort((a, b) => (a.status ===
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
const order = (snap.book?.sheets ?? []).map((x) => x.name);
|
|
387
|
-
list.sort((a, b) => order.indexOf(a.sheet) - order.indexOf(b.sheet));
|
|
698
|
+
.filter((e) => (this.showAccepted ? e.status === 'accepted' : e.status !== 'accepted') && (this.filter === 'all' || e.severity === this.filter) && (this.authorFilter === 'all' || this.authorKind(e) === this.authorFilter))
|
|
699
|
+
.sort((a, b) => (a.status === 'open' ? 0 : 1) - (b.status === 'open' ? 0 : 1) || errNum(a) - errNum(b));
|
|
700
|
+
if (this.accepting && !list.some((e) => e.id === this.accepting && e.id === this.expanded && e.status === 'open')) {
|
|
701
|
+
this.accepting = undefined;
|
|
702
|
+
this.reasonDraft = '';
|
|
388
703
|
}
|
|
389
|
-
|
|
704
|
+
// W-55: errors with no cell come first — the Workbook group, then each sheet's whole-sheet errors in sheet order
|
|
705
|
+
const order = (snap.book?.sheets ?? []).map((x) => x.name);
|
|
706
|
+
const rank = (e) => { const sc = errorScope(e); return sc === 'workbook' ? 0 : sc === 'sheet' ? 1 : 2; };
|
|
707
|
+
const sheetAt = (n) => { const i = order.indexOf(n); return i < 0 ? order.length : i; };
|
|
708
|
+
const scoped = list.some((e) => rank(e) < 2);
|
|
709
|
+
// rule rows are read by sheet (W-33): under the host filter, grouped by the workbook's sheet order
|
|
710
|
+
const grouped = this.authorFilter === 'host' && new Set(list.filter((e) => rank(e) === 2).map((e) => e.sheet)).size > 1;
|
|
711
|
+
list.sort((a, b) => rank(a) - rank(b) || (rank(a) === 1 || grouped ? sheetAt(a.sheet) - sheetAt(b.sheet) : 0)); // stable: open first, by number, within each
|
|
712
|
+
let lastHead = '';
|
|
390
713
|
const types = new Map([...Object.entries(BUILTIN_TYPE_NAMES), ...this._errorTypes.map((t) => [t.id, t.name])]);
|
|
391
714
|
const sel = snap.selection;
|
|
392
715
|
const html = list.length ? list.map((e) => {
|
|
393
|
-
const
|
|
394
|
-
|
|
716
|
+
const scope = errorScope(e);
|
|
717
|
+
const title = scope === 'workbook' ? 'Workbook' : scope === 'sheet' ? `${e.sheet} · whole sheet` : grouped ? e.sheet : scoped ? 'Cells' : '';
|
|
718
|
+
const head = title && title !== lastHead ? `<div class="grp" role="presentation">${esc(title)}</div>` : '';
|
|
719
|
+
lastHead = title;
|
|
720
|
+
const place = scope === 'workbook' ? 'workbook' : scope === 'sheet' ? 'whole sheet' : e.range;
|
|
395
721
|
const r = parseRange(e.range);
|
|
396
722
|
const on = this.expanded === e.id || (!!r && e.sheet === sel.sheet && parseRef(sel.ref) && r.top === parseRef(sel.ref).row && r.left <= parseRef(sel.ref).col && r.right >= parseRef(sel.ref).col);
|
|
397
|
-
const tags = [e.cellChanged ? 'cell changed' : '', e.status === 'resolved' ? 'resolved' : '', e.matchesExpected ? 'matches suggestion' : '', s.isStale(e) ? 'stale' : '', ...hiddenTags(s, e)].filter(Boolean);
|
|
723
|
+
const tags = [tagLayer ? this.layerName(layerOf(e)) : '', e.cellChanged ? 'cell changed' : '', e.status === 'resolved' ? 'resolved' : e.status === 'accepted' ? 'accepted' : '', e.matchesExpected ? 'matches suggestion' : '', s.isStale(e) ? 'stale' : '', ...hiddenTags(s, e)].filter(Boolean);
|
|
398
724
|
const ro = this.readOnly || machineAuthored(e.author);
|
|
399
725
|
const sv = SEVERITY[e.severity];
|
|
400
726
|
const time = e.createdAt ? new Date(e.createdAt).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' }) : '';
|
|
401
|
-
const detail = this.expanded === e.id ? `<div class="detail">${e.message ? `<p class="why">${esc(e.message)}</p>` : ''}<span class="xs faint">${esc([e.author === 'workbook' ? 'Workbook rule' : e.author === 'file' ? 'File comment' : e.author === 'host' ? (s.hasRulesDocument ? 'Rules file' : 'Column rule') : e.author, e.code, time].filter(Boolean).join(' · '))}</span><div class="acts"><button type="button" class="btn" data-act="reveal">Show in sheet</button
|
|
402
|
-
return `${head}<div class="item${e.status === 'resolved' ? ' done' : ''}${on ? ' on' : ''}" role="listitem" data-id="${esc(e.id)}" data-sp-error="${esc(e.id)}" data-sp-ref="${esc(e.sheet)}!${esc(e.range)}"><button type="button" class="issue" data-act="pick" aria-expanded="${this.expanded === e.id}" aria-label="Error ${errNum(e)}: ${esc(types.get(e.type) ?? e.type)}, ${esc(e.sheet)} ${esc(
|
|
727
|
+
const detail = this.expanded === e.id ? `<div class="detail">${e.message ? `<p class="why">${esc(e.message)}</p>` : ''}${this.detailRegion.html()}${acceptedHtml(e)}<span class="xs faint">${esc([e.author === 'workbook' ? 'Workbook rule' : e.author === 'file' ? 'File comment' : e.author === 'host' ? (s.hasRulesDocument ? 'Rules file' : 'Column rule') : e.author, e.code, time].filter(Boolean).join(' · '))}</span><div class="acts">${scope === 'workbook' ? '<button type="button" class="btn" data-act="reveal" disabled aria-describedby="nowhere">Show in sheet</button><span class="xs faint" id="nowhere">About the whole workbook — no sheet to show</span>' : `<button type="button" class="btn" data-act="reveal">Show in sheet</button>`}${ro ? '' : `<button type="button" class="btn" data-act="resolve" data-sp="resolve">${e.status === 'open' ? '✓ Resolve' : 'Reopen'}</button>${e.status === 'open' ? `<button type="button" class="btn" data-act="accept" data-sp="accept-open" aria-expanded="${this.accepting === e.id}">Accept…</button>` : ''}<span class="spacer" style="flex:1"></span><button type="button" class="btn btn-g btn-d" data-act="delete">Delete</button>`}</div>${this.accepting === e.id && !ro ? ACCEPT_FORM : ''}</div>` : '';
|
|
728
|
+
return `${head}<div class="item${e.status === 'resolved' ? ' done' : ''}${on ? ' on' : ''}" role="listitem" data-id="${esc(e.id)}" data-sp-error="${esc(e.id)}" data-sp-ref="${esc(e.sheet)}!${esc(e.range)}"><button type="button" class="issue" data-act="pick" aria-expanded="${this.expanded === e.id}" aria-label="Error ${errNum(e)}: ${esc(types.get(e.type) ?? e.type)}, ${scope === 'workbook' ? 'whole workbook' : `${esc(e.sheet)} ${esc(place)}`}, ${sv.name}${e.status === 'open' ? '' : `, ${e.status}`}"><span class="n ${e.status === 'open' ? e.severity : e.status}" aria-hidden="true">${e.status === 'resolved' ? '✓' : e.status === 'accepted' ? 'A' : errNum(e)}</span><span class="bd"><span class="ty">${esc(types.get(e.type) ?? e.type)}</span>${scope !== 'cell' && !e.found && !e.expected ? '' : `<span class="ba"><span class="found">${esc(scope === 'cell' ? e.found : e.found || '—')}</span> → <span class="want">${esc(e.expected || '—')}</span></span>`}<span class="m">${esc(place)} · <span class="g g-${e.severity}" aria-hidden="true">${sv.glyph}</span> ${sv.name}${tags.length ? ` · ${esc(tags.join(' · '))}` : ''}</span></span></button>${detail}</div>`;
|
|
403
729
|
}).join('') : `<p class="empty">${errors.length ? 'Nothing matches this filter.' : 'Nothing flagged yet. Select a cell in the sheet, then Flag issue.'}</p>`;
|
|
404
730
|
const listEl = q('.list');
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
731
|
+
// the Problems view lists the errors (W-55's groups first); Passed and Not checked list grouped results (W-50); Edits the changes (W-64)
|
|
732
|
+
const edits = this.view === 'edits' ? s.edits() : [];
|
|
733
|
+
const canRevert = !this.readOnly && snap.mode === 'edit';
|
|
734
|
+
const edbar = q('.edbar');
|
|
735
|
+
edbar.hidden = this.view !== 'edits' || !edits.length;
|
|
736
|
+
const edbarHtml = edbar.hidden ? '' : `<span>${edits.length} edit${edits.length === 1 ? '' : 's'} · newest first</span><span class="spacer"></span>${canRevert ? '<button type="button" class="btn btn-g" data-act="revert-all" data-sp="revert-all">Revert all</button>' : ''}`;
|
|
737
|
+
if (edbar.dataset.html !== edbarHtml) {
|
|
738
|
+
edbar.innerHTML = edbarHtml;
|
|
739
|
+
edbar.dataset.html = edbarHtml;
|
|
740
|
+
}
|
|
741
|
+
const shown = this.view === 'problems' ? html : this.view === 'edits' ? this.editsHtml(edits, canRevert, types) : this.groupsHtml(snap.visibleErrors, this.view, types);
|
|
742
|
+
const hadReason = this.shadow.activeElement?.matches('[data-field=reason]') ?? false;
|
|
743
|
+
const rebuilt = listEl.dataset.html !== shown;
|
|
744
|
+
if (rebuilt) {
|
|
745
|
+
listEl.innerHTML = shown;
|
|
746
|
+
listEl.dataset.html = shown;
|
|
747
|
+
if (this.refocus)
|
|
748
|
+
listEl.querySelector(this.refocus)?.focus();
|
|
749
|
+
}
|
|
750
|
+
this.refocus = undefined;
|
|
751
|
+
this.detailRegion.sync(listEl, this.view === 'problems' ? list.find((e) => e.id === this.expanded) : undefined); // the host's region, after the message (W-68)
|
|
752
|
+
// W-69: the reason lives outside the markup, so typing never re-draws the list; focus follows the form
|
|
753
|
+
const reason = listEl.querySelector('[data-field=reason]');
|
|
754
|
+
if (reason) {
|
|
755
|
+
if (reason.value !== this.reasonDraft)
|
|
756
|
+
reason.value = this.reasonDraft;
|
|
757
|
+
listEl.querySelector('[data-act=accept-ok]').disabled = !this.reasonDraft.trim();
|
|
758
|
+
if (this.focusNext === 'reason' || (rebuilt && hadReason))
|
|
759
|
+
reason.focus();
|
|
760
|
+
}
|
|
761
|
+
if (this.focusEditsChip) {
|
|
762
|
+
this.focusEditsChip = false;
|
|
763
|
+
q('.filters').querySelector('[data-act=view][data-v=edits]')?.focus();
|
|
764
|
+
}
|
|
765
|
+
if (this.focusNext === 'accept')
|
|
766
|
+
listEl.querySelector('[data-act=accept]')?.focus();
|
|
767
|
+
else if (this.focusNext === 'chip')
|
|
768
|
+
q('.filters').querySelector('[data-act=status]')?.focus();
|
|
769
|
+
this.focusNext = undefined;
|
|
770
|
+
for (const box of Array.from(listEl.querySelectorAll('.cells')))
|
|
771
|
+
this.fillCells(box);
|
|
772
|
+
}
|
|
773
|
+
/** The Passed or Not checked view (W-50): one row per check (passes, by type + message) or reason (not checked, by message). */
|
|
774
|
+
groupsHtml(all, sev, types) {
|
|
775
|
+
const groups = new Map();
|
|
776
|
+
for (const e of all) {
|
|
777
|
+
if (e.severity !== sev)
|
|
778
|
+
continue;
|
|
779
|
+
const key = sev === 'pass' ? `${e.type}|${e.message}` : e.message ? `|${e.message}` : `${e.type}|`;
|
|
780
|
+
let g = groups.get(key);
|
|
781
|
+
if (!g)
|
|
782
|
+
groups.set(key, (g = { key, label: e.message || (types.get(e.type) ?? e.type), sev, entries: [], cells: 0, where: '' }));
|
|
783
|
+
g.entries.push(e);
|
|
784
|
+
const r = parseRange(e.range);
|
|
785
|
+
g.cells += r ? (r.bottom - r.top + 1) * (r.right - r.left + 1) : 1;
|
|
786
|
+
}
|
|
787
|
+
this.groupEntries.clear();
|
|
788
|
+
const list = [...groups.values()].sort((a, b) => b.cells - a.cells || a.label.localeCompare(b.label));
|
|
789
|
+
return list.map((g) => {
|
|
790
|
+
this.groupEntries.set(g.key, g.entries);
|
|
791
|
+
const open = this.openGroups.has(g.key), sv = SEVERITY[sev];
|
|
792
|
+
const n = g.entries.length;
|
|
793
|
+
const cells = open ? `<div class="cells" role="group" aria-label="${esc(g.label)}: ${g.cells} cell${g.cells === 1 ? '' : 's'}" style="height:${Math.min(n * CELL_ROW, CELLS_MAX)}px"><div class="vsp" style="height:${n * CELL_ROW}px"></div></div>` : '';
|
|
794
|
+
return `<div class="item rgrp" role="listitem" data-g="${esc(g.key)}"><div class="grow"><button type="button" class="issue" data-act="group" aria-expanded="${open}"><span class="rg ${sev}" aria-hidden="true">${sv.glyph}</span><span class="bd"><span class="ty"><span class="vh">${sv.name}: </span>${esc(g.label)}</span><span class="m">${g.cells} cell${g.cells === 1 ? '' : 's'} · ${esc(whereOf(g.entries))}</span></span></button><button type="button" class="btn btn-g lnk" data-act="show-group" aria-label="Show ${esc(g.label)} in sheet">Show ▸</button></div>${cells}</div>`;
|
|
795
|
+
}).join('');
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* The Edits view (W-64): one row per change-map entry, newest first — its place and kind, was → now, the time, and the
|
|
799
|
+
* errors it cleared or raised (each a link to the error in Problems); Revert per row in Edit mode. A page at a time.
|
|
800
|
+
*/
|
|
801
|
+
editsHtml(edits, canRevert, types) {
|
|
802
|
+
if (!edits.length)
|
|
803
|
+
return '<p class="empty">No edits yet. Each change to the workbook is listed here — what it was, what it is now — with its own Revert.</p>';
|
|
804
|
+
const s = this._store, snap = s.getSnapshot(), byId = new Map(snap.errors.map((e) => [e.id, e]));
|
|
805
|
+
const shown = new Set(snap.visibleErrors.map((e) => e.id)); // W-51: an error in a hidden layer is named, not linked — Problems would not show it
|
|
806
|
+
const link = (id, how) => {
|
|
807
|
+
const e = byId.get(id), word = how === 'cleared' ? 'Cleared' : how === 'raised' ? 'Raised' : 'Matches the suggestion of';
|
|
808
|
+
const glyph = how === 'cleared' ? '✓' : how === 'raised' ? '✕' : '=';
|
|
809
|
+
const what = e ? `error ${errNum(e)}: ${types.get(e.type) ?? e.type}` : id;
|
|
810
|
+
return e && shown.has(id)
|
|
811
|
+
? `<button type="button" class="fxl" data-act="goto-error" data-id="${esc(id)}"><span class="g-${how}" aria-hidden="true">${glyph}</span> ${word} ${esc(what)}</button>`
|
|
812
|
+
: `<span class="fxl"><span class="g-${how}" aria-hidden="true">${glyph}</span> ${word} ${esc(what)}</span>`;
|
|
813
|
+
};
|
|
814
|
+
const rows = edits.slice(0, this.editsLimit).map((ed) => {
|
|
815
|
+
const place = ed.ref ? `${ed.sheet}!${ed.ref}` : `${ed.sheet} · whole sheet`, kind = kindName(ed);
|
|
816
|
+
const fx = s.editEffects(ed.key);
|
|
817
|
+
const links = [...fx.cleared.map((id) => link(id, 'cleared')), ...fx.raised.map((id) => link(id, 'raised')), ...fx.matched.map((id) => link(id, 'matched'))];
|
|
818
|
+
const time = Date.parse(ed.at) ? new Date(ed.at).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' }) : '';
|
|
819
|
+
const summary = [fx.cleared.length ? `${fx.cleared.length} cleared` : '', fx.raised.length ? `${fx.raised.length} raised` : ''].filter(Boolean).join(' · ');
|
|
820
|
+
return `<div class="item edit" role="listitem" data-k="${esc(ed.key)}" data-sp-edit="${esc(ed.key)}" data-sp-ref="${esc(ed.sheet)}!${esc(ed.ref)}"><div class="grow"><button type="button" class="issue" data-act="edit-pick"><span class="ek" aria-hidden="true">✎</span><span class="bd"><span class="ty">${esc(place)} · ${esc(kind)}</span><span class="ba"><span class="vh">was </span><span class="found">${esc(editText(s, ed, ed.was))}</span> → <span class="vh">now </span><span class="want">${esc(editText(s, ed, ed.now))}</span></span><span class="m">${time ? `<time datetime="${esc(ed.at)}">${esc(time)}</time>` : ''}${time && summary ? ' · ' : ''}${esc(summary)}</span></span></button>${canRevert ? `<button type="button" class="btn btn-g lnk" data-act="revert-edit" data-sp="revert-edit" aria-label="Revert ${esc(place)}${ed.kind === 'value' ? '' : ` ${esc(kind.toLowerCase())}`}">Revert</button>` : ''}</div>${links.length ? `<div class="fx">${links.join('')}</div>` : ''}</div>`;
|
|
821
|
+
}).join('');
|
|
822
|
+
const rest = edits.length - Math.min(edits.length, this.editsLimit);
|
|
823
|
+
return rows + (rest ? `<div role="listitem"><button type="button" class="btn more" data-act="edits-more">Show ${Math.min(EDITS_PAGE, rest)} more of ${rest}</button></div>` : '');
|
|
824
|
+
}
|
|
825
|
+
/** Draws the cell rows an opened group's box shows, and a few either side — never all of them (W-50, virtualised). */
|
|
826
|
+
fillCells(box) {
|
|
827
|
+
const entries = this.groupEntries.get(box.closest('[data-g]')?.dataset.g ?? '') ?? [];
|
|
828
|
+
const top = box.scrollTop, h = box.clientHeight || CELLS_MAX;
|
|
829
|
+
const a = Math.max(0, Math.floor(top / CELL_ROW) - 4), b = Math.min(entries.length, Math.ceil((top + h) / CELL_ROW) + 4);
|
|
830
|
+
let html = '';
|
|
831
|
+
for (let i = a; i < b; i++) {
|
|
832
|
+
const e = entries[i], sv = SEVERITY[e.severity];
|
|
833
|
+
html += `<button type="button" class="cellrow" data-act="reveal" data-id="${esc(e.id)}" data-sp-error="${esc(e.id)}" data-sp-ref="${esc(e.sheet)}!${esc(e.range)}" style="top:${i * CELL_ROW}px" title="Show in sheet"><span class="rg ${e.severity}" aria-hidden="true">${sv.glyph}</span><span class="vh">${sv.name}: </span>${esc(e.range)} <span class="m">${esc(e.sheet)}${e.found ? ` · ${esc(e.found)}` : ''}</span></button>`;
|
|
834
|
+
}
|
|
835
|
+
const sp = box.firstElementChild;
|
|
836
|
+
if (sp.dataset.html !== html) {
|
|
837
|
+
sp.innerHTML = html;
|
|
838
|
+
sp.dataset.html = html;
|
|
408
839
|
}
|
|
409
840
|
}
|
|
410
841
|
formHtml() {
|
|
@@ -429,6 +860,8 @@ export class SheetpaneErrors extends ElementBase {
|
|
|
429
860
|
<div class="fft"><span class="xs faint">⌘ ↵</span><span class="spacer" style="flex:1"></span><button type="button" class="btn" data-act="cancel">Cancel</button><button type="button" class="btn btn-p" data-act="add" data-sp="add-error">Add error</button></div></div>`;
|
|
430
861
|
}
|
|
431
862
|
}
|
|
863
|
+
/** The reason form in an open row (W-69): a reason is required — Accept stays disabled until one is typed. */
|
|
864
|
+
const ACCEPT_FORM = `<div class="accept" role="group" aria-label="Accept this deviation"><label class="field"><span>Why is this accepted?</span><textarea class="input" rows="2" data-field="reason" required aria-required="true" aria-describedby="sp-accept-help"></textarea></label><span class="xs faint" id="sp-accept-help">Required. Saved with your name and the time; the error stays in the review and the download, and no longer counts as open.</span><div class="acts"><button type="button" class="btn" data-act="accept-cancel">Cancel</button><button type="button" class="btn btn-p" data-act="accept-ok" data-sp="accept" disabled>Accept</button></div></div>`;
|
|
432
865
|
/** The column's header text: the nearest row above with two or more text cells. */
|
|
433
866
|
export function headerFor(text, row, col) {
|
|
434
867
|
for (let r = 0; r < Math.min(row, 10); r++) {
|
|
@@ -439,6 +872,33 @@ export function headerFor(text, row, col) {
|
|
|
439
872
|
return '';
|
|
440
873
|
}
|
|
441
874
|
export { sevOrder };
|
|
875
|
+
/** The bounding range of entries on one sheet, or undefined when none parses. */
|
|
876
|
+
function boxOf(entries) {
|
|
877
|
+
let box;
|
|
878
|
+
for (const e of entries) {
|
|
879
|
+
const r = parseRange(e.range);
|
|
880
|
+
if (!r)
|
|
881
|
+
continue;
|
|
882
|
+
box = box ? { top: Math.min(box.top, r.top), left: Math.min(box.left, r.left), bottom: Math.max(box.bottom, r.bottom), right: Math.max(box.right, r.right) } : { ...r };
|
|
883
|
+
}
|
|
884
|
+
return box;
|
|
885
|
+
}
|
|
886
|
+
/** Where a result group sits (W-50): one range when its cells fill it ("H3:H214"), else the first three ("H6, H31, H44 …"). */
|
|
887
|
+
function whereOf(entries) {
|
|
888
|
+
const sheets = new Set(entries.map((e) => e.sheet));
|
|
889
|
+
if (sheets.size > 1)
|
|
890
|
+
return `${sheets.size} sheets`;
|
|
891
|
+
const box = boxOf(entries);
|
|
892
|
+
let cells = 0;
|
|
893
|
+
for (const e of entries) {
|
|
894
|
+
const r = parseRange(e.range);
|
|
895
|
+
if (r)
|
|
896
|
+
cells += (r.bottom - r.top + 1) * (r.right - r.left + 1);
|
|
897
|
+
}
|
|
898
|
+
if (box && cells === (box.bottom - box.top + 1) * (box.right - box.left + 1))
|
|
899
|
+
return rangeRef(box);
|
|
900
|
+
return entries.slice(0, 3).map((e) => e.range).join(', ') + (entries.length > 3 ? ' …' : '');
|
|
901
|
+
}
|
|
442
902
|
/** Where an error sits out of sight: a hidden sheet, row or column (W-49). Selecting it shows it in the sheet. */
|
|
443
903
|
function hiddenTags(s, e) {
|
|
444
904
|
const h = s.hidden(e.sheet), r = parseRange(e.range);
|
|
@@ -453,4 +913,66 @@ function hiddenTags(s, e) {
|
|
|
453
913
|
out.push('hidden column');
|
|
454
914
|
return out;
|
|
455
915
|
}
|
|
916
|
+
/** An edit's kind in words (W-64); a kind this list does not know is named as it is. */
|
|
917
|
+
function kindName(e) {
|
|
918
|
+
switch (e.kind) {
|
|
919
|
+
case 'value': return 'Value';
|
|
920
|
+
case 'formula': return 'Formula';
|
|
921
|
+
case 'style': return 'Style';
|
|
922
|
+
case 'numFmt': return 'Number format';
|
|
923
|
+
case 'merge': return 'Merge';
|
|
924
|
+
case 'freeze': return 'Freeze panes';
|
|
925
|
+
case 'hidden': return e.key.endsWith('!#hiddenCols') ? 'Hidden columns' : 'Hidden rows';
|
|
926
|
+
case 'sheetState': return 'Sheet visibility';
|
|
927
|
+
case 'outline': return e.key.endsWith('!#outlineCols') ? 'Column groups' : 'Row groups';
|
|
928
|
+
case 'structure': return 'Rows and columns'; // W-52: every insert and delete on the sheet, one row
|
|
929
|
+
default: {
|
|
930
|
+
const k = String(e.kind);
|
|
931
|
+
return k ? k[0].toUpperCase() + k.slice(1) : 'Change';
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
/** One side of an edit as text (W-64): a cell value as the cell shows it, `(blank)` for none; other kinds in a few words. */
|
|
936
|
+
function editText(s, e, v) {
|
|
937
|
+
if (v === undefined || v === null || v === '')
|
|
938
|
+
return e.kind === 'value' || e.kind === 'formula' ? '(blank)' : '—';
|
|
939
|
+
if (e.kind === 'structure' && Array.isArray(v))
|
|
940
|
+
return v.length ? clip(v.map(opText).join(', ')) : 'as read';
|
|
941
|
+
const r = parseRange(e.ref);
|
|
942
|
+
if ((e.kind === 'value' || e.kind === 'formula') && r && (typeof v === 'number' || typeof v === 'boolean' || typeof v === 'string'))
|
|
943
|
+
return s.format(e.sheet, cellRef(r.top, r.left), v);
|
|
944
|
+
if (e.kind === 'merge')
|
|
945
|
+
return v ? 'merged' : 'not merged';
|
|
946
|
+
if (e.kind === 'freeze' && typeof v === 'object') {
|
|
947
|
+
const f = v;
|
|
948
|
+
return f.rows || f.cols ? `${f.rows ?? 0} row${f.rows === 1 ? '' : 's'}, ${f.cols ?? 0} column${f.cols === 1 ? '' : 's'}` : 'none';
|
|
949
|
+
}
|
|
950
|
+
if (e.kind === 'hidden' && Array.isArray(v)) {
|
|
951
|
+
if (!v.length)
|
|
952
|
+
return 'none';
|
|
953
|
+
const cols = e.key.endsWith('!#hiddenCols'), names = v.slice(0, 6).map((i) => (cols ? colName(i) : String(i + 1)));
|
|
954
|
+
return `${cols ? 'columns' : 'rows'} ${names.join(', ')}${v.length > 6 ? ' …' : ''}`;
|
|
955
|
+
}
|
|
956
|
+
if (e.kind === 'outline' && typeof v === 'object') {
|
|
957
|
+
const n = Object.keys(v).length;
|
|
958
|
+
return n ? `${n} grouped` : 'none';
|
|
959
|
+
}
|
|
960
|
+
if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean')
|
|
961
|
+
return String(v);
|
|
962
|
+
let json;
|
|
963
|
+
try {
|
|
964
|
+
json = JSON.stringify(v);
|
|
965
|
+
}
|
|
966
|
+
catch {
|
|
967
|
+
json = String(v);
|
|
968
|
+
}
|
|
969
|
+
return clip(json);
|
|
970
|
+
}
|
|
971
|
+
const clip = (t) => (t.length > 60 ? `${t.slice(0, 59)}…` : t);
|
|
972
|
+
/** One insert or delete in a few words (W-52): `2 rows inserted at 3`, `column C deleted`. */
|
|
973
|
+
function opText(o) {
|
|
974
|
+
const rows = o.op.endsWith('Rows'), n = o.count, end = o.at + n - 1;
|
|
975
|
+
const where = rows ? (n === 1 ? `row ${o.at + 1}` : `rows ${o.at + 1}–${end + 1}`) : n === 1 ? `column ${colName(o.at)}` : `columns ${colName(o.at)}–${colName(end)}`;
|
|
976
|
+
return o.op.startsWith('insert') ? `${n} ${rows ? 'row' : 'column'}${n === 1 ? '' : 's'} inserted at ${rows ? o.at + 1 : colName(o.at)}` : `${where} deleted`;
|
|
977
|
+
}
|
|
456
978
|
//# sourceMappingURL=element.js.map
|