@issuegraph/editor 0.10.0 → 0.22.1
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 +38 -7
- package/dist/a11y/baseline.d.ts.map +1 -1
- package/dist/a11y/baseline.js +4 -0
- package/dist/a11y/baseline.js.map +1 -1
- package/dist/audit/findings.d.ts +107 -1
- package/dist/audit/findings.d.ts.map +1 -1
- package/dist/audit/findings.js +113 -6
- package/dist/audit/findings.js.map +1 -1
- package/dist/audit/panel.d.ts +277 -0
- package/dist/audit/panel.d.ts.map +1 -0
- package/dist/audit/panel.js +462 -0
- package/dist/audit/panel.js.map +1 -0
- package/dist/audit/refused.d.ts +99 -0
- package/dist/audit/refused.d.ts.map +1 -0
- package/dist/audit/refused.js +276 -0
- package/dist/audit/refused.js.map +1 -0
- package/dist/audit/styles.d.ts +1 -1
- package/dist/audit/styles.d.ts.map +1 -1
- package/dist/audit/styles.js +428 -0
- package/dist/audit/styles.js.map +1 -1
- package/dist/audit/surface.d.ts +54 -12
- package/dist/audit/surface.d.ts.map +1 -1
- package/dist/audit/surface.js +53 -13
- package/dist/audit/surface.js.map +1 -1
- package/dist/create/draft.d.ts +19 -0
- package/dist/create/draft.d.ts.map +1 -1
- package/dist/create/draft.js +21 -0
- package/dist/create/draft.js.map +1 -1
- package/dist/create/keys.d.ts +68 -8
- package/dist/create/keys.d.ts.map +1 -1
- package/dist/create/keys.js +43 -5
- package/dist/create/keys.js.map +1 -1
- package/dist/index.d.ts +38 -15
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +34 -13
- package/dist/index.js.map +1 -1
- package/dist/overlay/render.d.ts +11 -3
- package/dist/overlay/render.d.ts.map +1 -1
- package/dist/overlay/render.js +22 -6
- package/dist/overlay/render.js.map +1 -1
- package/dist/overlay/request.d.ts +71 -0
- package/dist/overlay/request.d.ts.map +1 -0
- package/dist/overlay/request.js +129 -0
- package/dist/overlay/request.js.map +1 -0
- package/dist/overlay/styles.d.ts +1 -1
- package/dist/overlay/styles.d.ts.map +1 -1
- package/dist/overlay/styles.js +38 -7
- package/dist/overlay/styles.js.map +1 -1
- package/dist/reevaluate/parts.d.ts +130 -0
- package/dist/reevaluate/parts.d.ts.map +1 -0
- package/dist/reevaluate/parts.js +260 -0
- package/dist/reevaluate/parts.js.map +1 -0
- package/dist/reevaluate/render.d.ts +18 -11
- package/dist/reevaluate/render.d.ts.map +1 -1
- package/dist/reevaluate/render.js +21 -135
- package/dist/reevaluate/render.js.map +1 -1
- package/dist/reevaluate/styles.d.ts +1 -1
- package/dist/reevaluate/styles.d.ts.map +1 -1
- package/dist/reevaluate/styles.js +26 -0
- package/dist/reevaluate/styles.js.map +1 -1
- package/dist/reevaluate/view.d.ts +16 -1
- package/dist/reevaluate/view.d.ts.map +1 -1
- package/dist/reevaluate/view.js +21 -1
- package/dist/reevaluate/view.js.map +1 -1
- package/dist/reevaluate/words.d.ts +7 -0
- package/dist/reevaluate/words.d.ts.map +1 -1
- package/dist/scale/ladder.d.ts +62 -6
- package/dist/scale/ladder.d.ts.map +1 -1
- package/dist/scale/ladder.js +119 -8
- package/dist/scale/ladder.js.map +1 -1
- package/dist/scale/render.d.ts +44 -0
- package/dist/scale/render.d.ts.map +1 -1
- package/dist/scale/render.js +197 -27
- package/dist/scale/render.js.map +1 -1
- package/dist/scale/styles.d.ts +11 -5
- package/dist/scale/styles.d.ts.map +1 -1
- package/dist/scale/styles.js +133 -4
- package/dist/scale/styles.js.map +1 -1
- package/dist/workspace/host.d.ts +83 -1
- package/dist/workspace/host.d.ts.map +1 -1
- package/dist/workspace/host.js +197 -4
- package/dist/workspace/host.js.map +1 -1
- package/dist/workspace/inspector.d.ts +33 -7
- package/dist/workspace/inspector.d.ts.map +1 -1
- package/dist/workspace/inspector.js +15 -0
- package/dist/workspace/inspector.js.map +1 -1
- package/dist/workspace/mount.d.ts +23 -1
- package/dist/workspace/mount.d.ts.map +1 -1
- package/dist/workspace/mount.js +500 -17
- package/dist/workspace/mount.js.map +1 -1
- package/dist/workspace/rail.d.ts +5 -2
- package/dist/workspace/rail.d.ts.map +1 -1
- package/dist/workspace/rail.js.map +1 -1
- package/dist/workspace/render.d.ts +379 -3
- package/dist/workspace/render.d.ts.map +1 -1
- package/dist/workspace/render.js +893 -133
- package/dist/workspace/render.js.map +1 -1
- package/dist/workspace/selection.d.ts +11 -0
- package/dist/workspace/selection.d.ts.map +1 -1
- package/dist/workspace/selection.js +10 -0
- package/dist/workspace/selection.js.map +1 -1
- package/dist/workspace/styles.d.ts +1 -1
- package/dist/workspace/styles.d.ts.map +1 -1
- package/dist/workspace/styles.js +868 -18
- package/dist/workspace/styles.js.map +1 -1
- package/package.json +3 -3
package/dist/workspace/mount.js
CHANGED
|
@@ -49,7 +49,7 @@ import { edgeIdentity } from '@issuegraph/core';
|
|
|
49
49
|
import { nextDocument } from '@issuegraph/store';
|
|
50
50
|
import { navigate, renderViewer, resolveTheme, } from '@issuegraph/viewer';
|
|
51
51
|
import { CONTROL_ATTRIBUTES } from "../a11y/baseline.js";
|
|
52
|
-
import { isChoosingKind } from "../create/draft.js";
|
|
52
|
+
import { isChoosingKind, isDraftLive } from "../create/draft.js";
|
|
53
53
|
import { KIND_KEYS, keyIntent } from "../create/keys.js";
|
|
54
54
|
import { pickerPlacement } from "../create/placement.js";
|
|
55
55
|
import { firstPassIntent } from "../firstpass/keys.js";
|
|
@@ -61,7 +61,7 @@ import { renderPicker } from "../picker/render.js";
|
|
|
61
61
|
import { pickerStylesheet } from "../picker/styles.js";
|
|
62
62
|
import { scaleLadder } from "../scale/ladder.js";
|
|
63
63
|
import { mountStylesheet } from "./chrome.js";
|
|
64
|
-
import { INITIAL_HOST_STATE, editCarrier, railRowAt, railSlackFor, railWindowTarget, reconcileHost, reduceHost, targetMatches, } from "./host.js";
|
|
64
|
+
import { INITIAL_HOST_STATE, editCarrier, narrowOverlayOpen, railRowAt, railSlackFor, railWindowTarget, reconcileHost, reduceHost, targetMatches, } from "./host.js";
|
|
65
65
|
import { conflictDiff } from "./recovery.js";
|
|
66
66
|
import { renderWorkspace, } from "./render.js";
|
|
67
67
|
import { selectedEdgeId, selectedKey } from "./selection.js";
|
|
@@ -74,8 +74,31 @@ const CHOOSER_SIZE = { width: 280, height: 220 };
|
|
|
74
74
|
/** The drag threshold, in CSS pixels: a press that moves less is a click. */
|
|
75
75
|
const DRAG_THRESHOLD = 6;
|
|
76
76
|
const KEY_ATTRIBUTE = 'data-ig-key';
|
|
77
|
+
/**
|
|
78
|
+
* §17c's change region, which is the one node this surface carries ACROSS a
|
|
79
|
+
* redraw rather than replacing.
|
|
80
|
+
*
|
|
81
|
+
* Matched on the role as well as the class: the class is a styling hook, and
|
|
82
|
+
* what has to be preserved is the live region specifically.
|
|
83
|
+
*/
|
|
84
|
+
const LIVE_REGION = '.ig-change-line[role="status"]';
|
|
77
85
|
const GROUP_ATTRIBUTE = 'data-ig-group';
|
|
78
86
|
const COMMAND_ATTRIBUTE = 'data-ig-command';
|
|
87
|
+
/**
|
|
88
|
+
* How many surfaces this module has mounted, so each gets an id nothing else
|
|
89
|
+
* on the page carries.
|
|
90
|
+
*
|
|
91
|
+
* MODULE STATE, DELIBERATELY, and it is the smallest honest way to do this. The
|
|
92
|
+
* renderer is pure and must stay so — `scale/render.ts`'s `searchSpec` records
|
|
93
|
+
* what a fixed id costs when a host draws two documents side by side — and the
|
|
94
|
+
* value has to be stable across every redraw of the SAME mount, so it cannot be
|
|
95
|
+
* derived per render. A counter in the module that does the mounting satisfies
|
|
96
|
+
* both: one value per `mountWorkspace` call, fixed for that surface's life.
|
|
97
|
+
*
|
|
98
|
+
* Not a random value: a deterministic sequence keeps the markup reproducible
|
|
99
|
+
* for anything that snapshots it.
|
|
100
|
+
*/
|
|
101
|
+
let surfaces = 0;
|
|
79
102
|
/**
|
|
80
103
|
* Which subject a command acts on, when one command names several.
|
|
81
104
|
*
|
|
@@ -123,9 +146,21 @@ function isElement(target) {
|
|
|
123
146
|
function isFocusable(node) {
|
|
124
147
|
return node !== null && node !== undefined && 'focus' in node;
|
|
125
148
|
}
|
|
126
|
-
/** The
|
|
149
|
+
/** The three refusals, allocated once: they carry no per-press data. */
|
|
127
150
|
const REFUSED_INPUT = Object.freeze({ kind: 'refused', reason: 'input' });
|
|
128
151
|
const REFUSED_INERT = Object.freeze({ kind: 'refused', reason: 'inert' });
|
|
152
|
+
/** A control whose activation the BROWSER performs. See {@link isNavigating}. */
|
|
153
|
+
const REFUSED_NAVIGATION = Object.freeze({ kind: 'refused', reason: 'navigation' });
|
|
154
|
+
/**
|
|
155
|
+
* Whether this control is a link the browser will follow by itself.
|
|
156
|
+
*
|
|
157
|
+
* THE `href` IS PART OF THE TEST. A bare `<a>` with no `href` is not a link —
|
|
158
|
+
* it has no activation behaviour of its own — so one carrying a command is an
|
|
159
|
+
* ordinary control and must still be dispatched.
|
|
160
|
+
*/
|
|
161
|
+
function isNavigating(node) {
|
|
162
|
+
return node?.tagName.toLowerCase() === 'a' && node.getAttribute('href') !== null;
|
|
163
|
+
}
|
|
129
164
|
function isInput(node) {
|
|
130
165
|
return node !== null && node !== undefined && node.tagName === 'INPUT';
|
|
131
166
|
}
|
|
@@ -176,6 +211,10 @@ export function mountWorkspace(element, options) {
|
|
|
176
211
|
const doc = element.ownerDocument;
|
|
177
212
|
const { store } = options;
|
|
178
213
|
let current = options;
|
|
214
|
+
// ONE PER MOUNT, FIXED FOR ITS LIFE. See `surfaces` for why it is taken here
|
|
215
|
+
// rather than per render.
|
|
216
|
+
surfaces += 1;
|
|
217
|
+
const surfaceId = `w${String(surfaces)}`;
|
|
179
218
|
const styles = doc.createElement('style');
|
|
180
219
|
const surface = doc.createElement('div');
|
|
181
220
|
surface.className = 'ig-mount';
|
|
@@ -188,6 +227,21 @@ export function mountWorkspace(element, options) {
|
|
|
188
227
|
let drawn = null;
|
|
189
228
|
// A rail row to focus once the window has been re-cut around it.
|
|
190
229
|
let pendingFocus = null;
|
|
230
|
+
/**
|
|
231
|
+
* Set when a command opened the isolated list, cleared by the redraw that
|
|
232
|
+
* reveals it.
|
|
233
|
+
*
|
|
234
|
+
* THE LIST IS NOT WHERE ITS CONTROL IS, and that is what makes this
|
|
235
|
+
* necessary rather than tidy. §17a puts the count and the toggle at the foot
|
|
236
|
+
* of the ORDER RAIL; the list itself is drawn by the ladder in the CANVAS,
|
|
237
|
+
* because the rail is virtualized on a fixed row pitch (`railRowAt`,
|
|
238
|
+
* `railSpacer`) and content of arbitrary height inside that scroll track
|
|
239
|
+
* makes every offset beneath it name the wrong row. So pressing the toggle
|
|
240
|
+
* flips a control in one zone and grows a list in another, below whatever
|
|
241
|
+
* that zone was already scrolled to — and the reader is told the list is open
|
|
242
|
+
* while seeing nothing change.
|
|
243
|
+
*/
|
|
244
|
+
let revealIsolated = false;
|
|
191
245
|
// The keys currently holding a control they activated. See `onKeydown`'s first
|
|
192
246
|
// arm for why this is a fact about the PRESS and not a question asked of
|
|
193
247
|
// whatever holds focus by the time the repeats arrive.
|
|
@@ -508,6 +562,12 @@ export function mountWorkspace(element, options) {
|
|
|
508
562
|
const dispatch = (command) => {
|
|
509
563
|
if (destroyed)
|
|
510
564
|
return;
|
|
565
|
+
// ON THE COMMAND, NOT ON THE RESULTING STATE. `open-isolated` is what the
|
|
566
|
+
// reader just asked for; `scale.isolatedOpen` is also true on every redraw
|
|
567
|
+
// that follows for any other reason, and revealing it again there would
|
|
568
|
+
// yank the canvas back under a reader who had scrolled away.
|
|
569
|
+
if (command.kind === 'control' && command.name === 'open-isolated')
|
|
570
|
+
revealIsolated = true;
|
|
511
571
|
applyResult(reduceHost(state, command, landed()));
|
|
512
572
|
};
|
|
513
573
|
const zone = (name) => surface.querySelector(`.ig-zone[data-zone="${name}"]`);
|
|
@@ -889,6 +949,38 @@ export function mountWorkspace(element, options) {
|
|
|
889
949
|
const keyed = isElement(active) ? active.closest(`[${KEY_ATTRIBUTE}]`) : null;
|
|
890
950
|
return keyed !== null && surface.contains(keyed) ? keyed.getAttribute(KEY_ATTRIBUTE) : null;
|
|
891
951
|
};
|
|
952
|
+
/**
|
|
953
|
+
* The subject `+ add` publishes, when that control itself owns focus.
|
|
954
|
+
*
|
|
955
|
+
* THE SECOND SOURCE FOR `KeyboardContext.focused`, and it exists because the
|
|
956
|
+
* first one cannot answer here: `focusedKey` reads the rail's roving tab stop,
|
|
957
|
+
* and a focused button has no keyed ancestor — every command control this
|
|
958
|
+
* package draws sits in chrome beside the keyed rows rather than inside one.
|
|
959
|
+
* So `R` on `+ add` reached a `relate` arm whose subject was `null`, which
|
|
960
|
+
* answers `none` ("nothing focused is nothing to relate FROM"). `#173`.
|
|
961
|
+
*
|
|
962
|
+
* IT READS THE CONTROL'S OWN ATTRIBUTE, NOT THE SELECTION, for the reason
|
|
963
|
+
* `reduceHost`'s `add` arm already reads it: `inspectorView` canonicalizes a
|
|
964
|
+
* selection naming a together unit's member onto the slot's LEAD, and the
|
|
965
|
+
* panel is worded from that, so the control publishes the canonical subject
|
|
966
|
+
* and the selection is only a fallback for a control that names none. Taking
|
|
967
|
+
* the selection here would begin the keyboard's draft from a different issue
|
|
968
|
+
* than the pointer's, on one screen, from one control.
|
|
969
|
+
*
|
|
970
|
+
* NARROWED TO THE `add` COMMAND, and that is a correctness bound rather than
|
|
971
|
+
* tidiness: {@link TARGET_ATTRIBUTE} is carried by the row remove control
|
|
972
|
+
* (an edge id), by a recovery card (a mutation id) and by a hold's subject
|
|
973
|
+
* button. Reading it off whichever control happens to hold focus would hand
|
|
974
|
+
* `relate` an edge id as an issue ref.
|
|
975
|
+
*/
|
|
976
|
+
const focusedAddSubject = () => {
|
|
977
|
+
const active = doc.activeElement;
|
|
978
|
+
if (!isElement(active) || !surface.contains(active))
|
|
979
|
+
return null;
|
|
980
|
+
if (active.getAttribute(COMMAND_ATTRIBUTE) !== 'add')
|
|
981
|
+
return null;
|
|
982
|
+
return active.getAttribute(TARGET_ATTRIBUTE);
|
|
983
|
+
};
|
|
892
984
|
const render = () => {
|
|
893
985
|
if (destroyed)
|
|
894
986
|
return;
|
|
@@ -1089,11 +1181,43 @@ export function mountWorkspace(element, options) {
|
|
|
1089
1181
|
}
|
|
1090
1182
|
const result = renderWorkspace(viewer, {
|
|
1091
1183
|
words: current.words,
|
|
1184
|
+
// §17c's CAUSE AND EFFECT, STRAIGHT OFF THE SNAPSHOT. The store is the
|
|
1185
|
+
// single source of truth for both — `lastChange` persists until the next
|
|
1186
|
+
// edit or an explicit dismissal, and `dismissChange()` is already the
|
|
1187
|
+
// only thing besides the next edit that clears it — so nothing about
|
|
1188
|
+
// "has this been dismissed" is held out here. A second copy of that is
|
|
1189
|
+
// exactly the drift the command grammar exists to avoid.
|
|
1190
|
+
//
|
|
1191
|
+
// `orderStatus` is the SAME value stamped on `surface` below. They cannot
|
|
1192
|
+
// disagree because they are one read of one snapshot, and each is for a
|
|
1193
|
+
// different reader: the attribute on `surface` is the host's, on the
|
|
1194
|
+
// element the host holds; the option is the stylesheet's, on the root the
|
|
1195
|
+
// renderer owns, so an unmounted rendering greys the held rail too.
|
|
1196
|
+
change: snapshot.lastChange ?? null,
|
|
1197
|
+
orderStatus: snapshot.order.status,
|
|
1092
1198
|
selection: state.selection,
|
|
1093
1199
|
scale: state.scale,
|
|
1094
1200
|
rail: { start: state.railStart, count: railCount() },
|
|
1201
|
+
// §16f's open rows. Host state, because the viewer is re-rendered from
|
|
1202
|
+
// scratch and cannot keep any of its own.
|
|
1203
|
+
expanded: state.expanded,
|
|
1204
|
+
// UNIQUE TO THIS MOUNT, so §17a's rail footer can name the isolated list
|
|
1205
|
+
// the canvas draws without two mounted workspaces on one page naming the
|
|
1206
|
+
// same element. See `WorkspaceOptions.surfaceId`; the counter is why a
|
|
1207
|
+
// second mount gets a second value.
|
|
1208
|
+
surfaceId,
|
|
1095
1209
|
audit,
|
|
1096
1210
|
auditFiltered: state.auditFiltered,
|
|
1211
|
+
// §17k's two reader decisions. Host state, for the reason the filter
|
|
1212
|
+
// beside it is: the renderer keeps nothing across a redraw, and a strip
|
|
1213
|
+
// that reopened itself on every landed write would be unusable.
|
|
1214
|
+
canvasOpen: state.canvasOpen,
|
|
1215
|
+
inspectorDismissed: state.inspectorDismissed,
|
|
1216
|
+
// §17d's overlay. Host state, for the same reason the filter beside it
|
|
1217
|
+
// is: the renderer keeps nothing across a redraw.
|
|
1218
|
+
auditOpen: state.auditOpen,
|
|
1219
|
+
issueUrl: current.issueUrl,
|
|
1220
|
+
railDensity: current.railDensity,
|
|
1097
1221
|
theme: resolved,
|
|
1098
1222
|
themeSelector: current.themeSelector,
|
|
1099
1223
|
// THE WRITE STATES ONLY. The workspace holds the one selection, and the
|
|
@@ -1121,9 +1245,23 @@ export function mountWorkspace(element, options) {
|
|
|
1121
1245
|
if (styles.textContent !== sheet)
|
|
1122
1246
|
styles.textContent = sheet;
|
|
1123
1247
|
// What the reader was doing survives the redraw: the rail's scroll offset,
|
|
1124
|
-
// and the caret in whichever search box they were typing
|
|
1248
|
+
// the audit panel's, and the caret in whichever search box they were typing
|
|
1249
|
+
// into.
|
|
1125
1250
|
const railBefore = zone('rail');
|
|
1126
1251
|
const scrollTop = railBefore?.scrollTop ?? 0;
|
|
1252
|
+
// THE AUDIT SCROLLS TOO, SINCE IT TOOK A SHARE OF ITS COLUMN. Before it had
|
|
1253
|
+
// a bound it grew to its content and only the ZONE scrolled, so there was no
|
|
1254
|
+
// second offset to keep. Now a long audit is read inside its own scroller,
|
|
1255
|
+
// and every store update and unrelated command runs this redraw — so without
|
|
1256
|
+
// this a reader deep in the findings is returned to the first one by
|
|
1257
|
+
// something they did somewhere else entirely.
|
|
1258
|
+
//
|
|
1259
|
+
// THE REGION, NOT THE PANEL. #177 put the bound on `.ig-audit-panel`, and
|
|
1260
|
+
// §17d's second surface moved it up to the region holding both — so reading
|
|
1261
|
+
// the panel's offset now reads a number that is always zero, and restoring
|
|
1262
|
+
// it is a no-op a reader feels as the audit jumping back on every redraw.
|
|
1263
|
+
const auditBefore = surface.querySelector('.ig-audit-overlay');
|
|
1264
|
+
const auditScrollTop = auditBefore?.scrollTop ?? 0;
|
|
1127
1265
|
const active = doc.activeElement;
|
|
1128
1266
|
const activeInput = isElement(active) && isInput(active) && surface.contains(active) ? active : null;
|
|
1129
1267
|
const activeCommand = activeInput?.getAttribute(COMMAND_ATTRIBUTE) ?? null;
|
|
@@ -1146,13 +1284,60 @@ export function mountWorkspace(element, options) {
|
|
|
1146
1284
|
// redraw that destroyed the focused control and of a reader who simply is
|
|
1147
1285
|
// not here.
|
|
1148
1286
|
const heldFocus = isElement(active) && surface.contains(active);
|
|
1287
|
+
// AND WHETHER IT WAS ONE OF THE AUDIT'S OWN TAB STOPS, which are the stops
|
|
1288
|
+
// on this surface no token can name. Neither has a keyed ancestor or a
|
|
1289
|
+
// command attribute, so `focusedKey()` and `commandFocusToken()` both answer
|
|
1290
|
+
// nothing for them — and the last-resort arm below fires on exactly that,
|
|
1291
|
+
// sending a reader who was reading a long audit to the first rail row, in
|
|
1292
|
+
// another zone.
|
|
1293
|
+
//
|
|
1294
|
+
// TWO OF THEM SINCE §17d GAINED ITS SECOND SURFACE, and the refused block is
|
|
1295
|
+
// the one that needs this MOST: both of its controls are conditional, so a
|
|
1296
|
+
// block whose refusals all name unloaded issues has no focusable descendant
|
|
1297
|
+
// at all and the section itself is the only stop a reader can hold.
|
|
1298
|
+
//
|
|
1299
|
+
// A CLASS RATHER THAN THE NODE THE OFFSET CAME FROM. That identity test was
|
|
1300
|
+
// right while the scrolling element and the tab stop were one element; they
|
|
1301
|
+
// are not any more — the region scrolls and its members carry the stops —
|
|
1302
|
+
// so the answer has to name which stop, and it is restored by the same name.
|
|
1303
|
+
const AUDIT_STOPS = ['.ig-audit-refused', '.ig-audit-panel'];
|
|
1304
|
+
const auditStop = isElement(active)
|
|
1305
|
+
? (AUDIT_STOPS.find((one) => active.matches(one)) ?? null)
|
|
1306
|
+
: null;
|
|
1149
1307
|
// The ZONE too: an issue is commonly drawn in the rail and on the canvas,
|
|
1150
1308
|
// and restoring "the first element with this key" would move focus from
|
|
1151
1309
|
// a canvas node into the rail on every redraw.
|
|
1152
1310
|
const focusedZone = isElement(active) ? (active.closest('.ig-zone')?.getAttribute('data-zone') ?? null) : null;
|
|
1153
1311
|
drawn = { viewer, rail: result.view.rail };
|
|
1312
|
+
// THE LIVE REGION HAS TO SURVIVE THE REDRAW, or it announces nothing.
|
|
1313
|
+
//
|
|
1314
|
+
// `summarySpec` mounts §17c's `role="status"` region ALWAYS and leaves it
|
|
1315
|
+
// EMPTY until there is something to say, precisely because a status node
|
|
1316
|
+
// that is created already carrying its text is not reliably announced —
|
|
1317
|
+
// the region has to exist first and then have its contents change. This
|
|
1318
|
+
// function replaces the whole subtree on every redraw, so rendering that
|
|
1319
|
+
// region into the markup and stopping there would destroy it and insert a
|
|
1320
|
+
// new, already-populated one every time: the affordance present, correct
|
|
1321
|
+
// in the markup, and inert. Worse than absent, because it looks done.
|
|
1322
|
+
//
|
|
1323
|
+
// So the ELEMENT is carried across: re-attached still holding the previous
|
|
1324
|
+
// summary, and only then given the new one. The insertion says nothing and
|
|
1325
|
+
// the mutation is what announces, which is the sequence the region was
|
|
1326
|
+
// designed around.
|
|
1327
|
+
const liveBefore = surface.querySelector(LIVE_REGION);
|
|
1154
1328
|
// Package-rendered markup, escaped by the package.
|
|
1155
1329
|
surface.innerHTML = result.markup;
|
|
1330
|
+
const liveAfter = surface.querySelector(LIVE_REGION);
|
|
1331
|
+
if (liveBefore !== null && liveAfter !== null) {
|
|
1332
|
+
const next = liveAfter.innerHTML;
|
|
1333
|
+
liveAfter.replaceWith(liveBefore);
|
|
1334
|
+
// GUARDED, so a redraw that did not touch the summary is not a change to
|
|
1335
|
+
// announce. Every keystroke in the ladder's search box redraws this
|
|
1336
|
+
// surface, and re-assigning identical content would re-announce the last
|
|
1337
|
+
// edit's result on each one.
|
|
1338
|
+
if (liveBefore.innerHTML !== next)
|
|
1339
|
+
liveBefore.innerHTML = next;
|
|
1340
|
+
}
|
|
1156
1341
|
// THE ORDER'S STATUS, PUBLISHED AS DATA. A write in flight holds the order
|
|
1157
1342
|
// — it does not move until the edit lands — and a host that wants to say
|
|
1158
1343
|
// so has nowhere to read it once the snapshot is consumed here. The
|
|
@@ -1169,6 +1354,41 @@ export function mountWorkspace(element, options) {
|
|
|
1169
1354
|
// as data, the same `data-ig-state` the ladder's line carries, so a
|
|
1170
1355
|
// pending edge is not drawn as a settled one and a host styles or reads
|
|
1171
1356
|
// it the same way in both modes. The merge is the ladder's own.
|
|
1357
|
+
// §17f'S ROW SURVIVES THIS ASSIGNMENT, AND ITS CAPTION DOES NOT.
|
|
1358
|
+
// `innerHTML` replaces every child, and `renderWorkspace` puts the
|
|
1359
|
+
// toolbar in this zone as its FIRST child — so the row, the pill
|
|
1360
|
+
// included, was silently deleted the moment a reader switched to the
|
|
1361
|
+
// tree. The pill is a property of the SURFACE, and a mounted surface is
|
|
1362
|
+
// exactly where a host is told to supply it, so losing it here took the
|
|
1363
|
+
// control away in the one state it is documented to exist in.
|
|
1364
|
+
//
|
|
1365
|
+
// THE CAPTION IS DIFFERENT, and is dropped on purpose rather than by
|
|
1366
|
+
// accident. It states what the LADDER's canvas draws — one connected
|
|
1367
|
+
// component out of the whole backlog — and this branch does not draw the
|
|
1368
|
+
// ladder's canvas: it draws `viewer`, the whole document, as a tree. So
|
|
1369
|
+
// "6 out of 312 drawn here" is simply false over a tree of 312, on the
|
|
1370
|
+
// same rule that keeps the caption off the refusing tiers.
|
|
1371
|
+
const toolbar = canvas.querySelector('.ig-canvas-toolbar');
|
|
1372
|
+
toolbar?.querySelector('.ig-canvas-caption')?.remove();
|
|
1373
|
+
// THE ISOLATED BLOCK SURVIVES THIS ASSIGNMENT TOO, and for a sharper
|
|
1374
|
+
// reason than the row above. §17a moves the isolated CONTROL to the
|
|
1375
|
+
// foot of the order rail, which this branch does not touch — so the
|
|
1376
|
+
// control survives the switch to the tree whatever happens here, while
|
|
1377
|
+
// the list it opens is drawn in THIS zone and would not. The reader
|
|
1378
|
+
// presses it, the label turns to "hide" and `aria-expanded` to `true`,
|
|
1379
|
+
// and nothing appears: a control that reports a state the surface is
|
|
1380
|
+
// not in.
|
|
1381
|
+
//
|
|
1382
|
+
// WORSE THAN WHAT IT REPLACED, WHICH IS WHY IT IS FIXED HERE RATHER
|
|
1383
|
+
// THAN NOTED. Before that move the chip lived in this zone beside its
|
|
1384
|
+
// list, so the tree deleted BOTH and the affordance was merely absent —
|
|
1385
|
+
// wrong, but not lying. Keeping the list is what makes the control it
|
|
1386
|
+
// is now separated from honest.
|
|
1387
|
+
//
|
|
1388
|
+
// UNCONDITIONAL, unlike the toolbar's `childElementCount` guard. This
|
|
1389
|
+
// element is not assembled from optional parts: `isolatedSpec` returns
|
|
1390
|
+
// null rather than an empty husk, so anything found here has content.
|
|
1391
|
+
const isolated = canvas.querySelector('.ig-ladder-isolated');
|
|
1172
1392
|
canvas.innerHTML = renderViewer(withoutChrome(viewer), {
|
|
1173
1393
|
projection: 'tree',
|
|
1174
1394
|
theme: resolved,
|
|
@@ -1176,6 +1396,21 @@ export function mountWorkspace(element, options) {
|
|
|
1176
1396
|
// The rail beside this canvas draws the panel's one header.
|
|
1177
1397
|
chrome: false,
|
|
1178
1398
|
}).markup;
|
|
1399
|
+
// Re-inserted rather than re-rendered: the row is already assembled,
|
|
1400
|
+
// and rebuilding it here would be a second place that decides what it
|
|
1401
|
+
// says.
|
|
1402
|
+
//
|
|
1403
|
+
// ONLY IF IT STILL HAS CONTENT, which is `canvasToolbar`'s own rule —
|
|
1404
|
+
// "nothing to say, no row" — and removing the caption above is exactly
|
|
1405
|
+
// what can empty it. A host that words the caption and not the pill
|
|
1406
|
+
// gets a caption-only row on the direct tier, and prepending its
|
|
1407
|
+
// emptied husk here left a sticky padded band with a border and no
|
|
1408
|
+
// content over the tree. Reading the element rather than re-deriving
|
|
1409
|
+
// the condition keeps the rule in one place.
|
|
1410
|
+
if (isolated !== null)
|
|
1411
|
+
canvas.append(isolated);
|
|
1412
|
+
if (toolbar !== null && toolbar.childElementCount > 0)
|
|
1413
|
+
canvas.prepend(toolbar);
|
|
1179
1414
|
const states = new Map(overlaysFor(viewer.edges, writeStates, selectedEdgeId(state.selection)).map((edge) => [edge.id, overlayFor(edge).attribute]));
|
|
1180
1415
|
for (const badge of canvas.querySelectorAll(`[${GROUP_ATTRIBUTE}]`)) {
|
|
1181
1416
|
const attribute = states.get(badge.getAttribute(GROUP_ATTRIBUTE) ?? '');
|
|
@@ -1203,6 +1438,25 @@ export function mountWorkspace(element, options) {
|
|
|
1203
1438
|
const rail = zone('rail');
|
|
1204
1439
|
if (rail !== null)
|
|
1205
1440
|
rail.scrollTop = scrollTop;
|
|
1441
|
+
// RE-QUERIED, NEVER REUSED: `surface.innerHTML` above replaced the subtree,
|
|
1442
|
+
// so the element captured from is gone. A region that shrank clamps this on
|
|
1443
|
+
// assignment, and one the redraw removed — the audit going clean — is
|
|
1444
|
+
// simply absent, which is why nothing is restored rather than zeroed.
|
|
1445
|
+
const auditRegion = surface.querySelector('.ig-audit-overlay');
|
|
1446
|
+
if (auditRegion !== null)
|
|
1447
|
+
auditRegion.scrollTop = auditScrollTop;
|
|
1448
|
+
// THE LIST THE READER JUST OPENED IS BROUGHT INTO VIEW. See
|
|
1449
|
+
// `revealIsolated` for why the control and the list are in different zones.
|
|
1450
|
+
// `block: 'nearest'` so a list already visible is not scrolled at all, and
|
|
1451
|
+
// guarded on the method because the same guard is used for the rail's own
|
|
1452
|
+
// jumps — this module renders into whatever document a host hands it.
|
|
1453
|
+
if (revealIsolated) {
|
|
1454
|
+
revealIsolated = false;
|
|
1455
|
+
const list = surface.querySelector('.ig-isolated-list');
|
|
1456
|
+
if (list !== null && typeof list.scrollIntoView === 'function') {
|
|
1457
|
+
list.scrollIntoView({ block: 'nearest' });
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1206
1460
|
// FOCUS SURVIVES THE REDRAW, and it moves to the target search when that
|
|
1207
1461
|
// step opens. The keyboard path is R -> kind -> search -> Enter, and every
|
|
1208
1462
|
// step redraws: without this, R destroyed the focused row, the next press
|
|
@@ -1231,6 +1485,21 @@ export function mountWorkspace(element, options) {
|
|
|
1231
1485
|
// irreversible answer under the reader's next Space.
|
|
1232
1486
|
(again ?? overlay).focus({ preventScroll: true });
|
|
1233
1487
|
}
|
|
1488
|
+
else if (auditStop !== null && surface.querySelector(auditStop) !== null) {
|
|
1489
|
+
// THE READER WAS IN THE AUDIT, SO THEY STAY THERE — and on the same one of
|
|
1490
|
+
// its two surfaces, which is why the stop is restored by the name it was
|
|
1491
|
+
// captured under rather than by whichever the redraw drew first.
|
|
1492
|
+
//
|
|
1493
|
+
// Beside the offset restored above rather than instead of it: the offset
|
|
1494
|
+
// says where the region is scrolled to and this says who is reading it,
|
|
1495
|
+
// and a redraw that kept one without the other still moves the reader out
|
|
1496
|
+
// of the zone.
|
|
1497
|
+
//
|
|
1498
|
+
// `preventScroll`, because the offset was already put back — focusing
|
|
1499
|
+
// normally would scroll the column to bring the element into view and
|
|
1500
|
+
// undo it.
|
|
1501
|
+
surface.querySelector(auditStop)?.focus({ preventScroll: true });
|
|
1502
|
+
}
|
|
1234
1503
|
else if (activeCommand !== null) {
|
|
1235
1504
|
const again = surface.querySelector(`input[${COMMAND_ATTRIBUTE}="${activeCommand}"]`);
|
|
1236
1505
|
if (again !== null) {
|
|
@@ -1482,6 +1751,22 @@ export function mountWorkspace(element, options) {
|
|
|
1482
1751
|
const name = control.getAttribute(COMMAND_ATTRIBUTE) ?? '';
|
|
1483
1752
|
if (isInput(control))
|
|
1484
1753
|
return REFUSED_INPUT; // the `input` listener owns these
|
|
1754
|
+
// AND THE BROWSER OWNS A LINK, which is the same refusal one element over.
|
|
1755
|
+
// §17d's `Open in GitHub` is an ANCHOR — it leaves the document, and a
|
|
1756
|
+
// control that navigates away must not announce itself as a button — and
|
|
1757
|
+
// it carries a command name only so `a11y/baseline.ts` can see it: that
|
|
1758
|
+
// artifact records a control through the attribute channels it knows, and
|
|
1759
|
+
// an anchor bearing nothing but `href` is invisible to it.
|
|
1760
|
+
//
|
|
1761
|
+
// DISPATCHING IT WOULD BE A REDRAW UNDER THE READER'S OWN CLICK. The
|
|
1762
|
+
// reducer has no arm for the name, so the command comes back unclaimed —
|
|
1763
|
+
// but `applyResult` still schedules, and the redraw replaces the surface's
|
|
1764
|
+
// markup, detaching the anchor mid-activation. An `<a>` is exempt from the
|
|
1765
|
+
// "cannot navigate" connectedness check so the navigation still happens,
|
|
1766
|
+
// which is precisely why this would have gone unnoticed: a full re-render
|
|
1767
|
+
// per click, paid for a command nothing handles.
|
|
1768
|
+
if (isNavigating(control))
|
|
1769
|
+
return REFUSED_NAVIGATION;
|
|
1485
1770
|
// THE ENTRY IS INERT WITHOUT A BUNDLE, and it is withheld HERE rather
|
|
1486
1771
|
// than in the reducer. `renderWorkspace` draws §17a's `First pass →`
|
|
1487
1772
|
// inside this surface, so a host cannot intercept it from outside; and
|
|
@@ -1532,6 +1817,13 @@ export function mountWorkspace(element, options) {
|
|
|
1532
1817
|
if (target.closest('[data-ig-audit-filter]') !== null) {
|
|
1533
1818
|
return { kind: 'dispatch', command: { kind: 'control', name: 'audit-filter' } };
|
|
1534
1819
|
}
|
|
1820
|
+
// §17d's HEADER COUNT. Checked AFTER the filter, and the order is not
|
|
1821
|
+
// arbitrary: the filter chip lives inside the panel the count opens, so a
|
|
1822
|
+
// click on it is inside neither the other's subtree today — but if the two
|
|
1823
|
+
// ever nest, the inner control is the one the reader aimed at.
|
|
1824
|
+
if (target.closest('[data-ig-audit-panel]') !== null) {
|
|
1825
|
+
return { kind: 'dispatch', command: { kind: 'control', name: 'audit-panel' } };
|
|
1826
|
+
}
|
|
1535
1827
|
return null;
|
|
1536
1828
|
};
|
|
1537
1829
|
const onClick = (event) => {
|
|
@@ -1621,14 +1913,23 @@ export function mountWorkspace(element, options) {
|
|
|
1621
1913
|
* `elsewhere` as "everything that is not our own search box", and a button is
|
|
1622
1914
|
* that. The target search keeps its two bindings.
|
|
1623
1915
|
*
|
|
1624
|
-
* ## The
|
|
1916
|
+
* ## The TWO controls that are not `elsewhere`, and the test they pass
|
|
1625
1917
|
*
|
|
1626
|
-
* The rule above is right for every control it names and wrong for
|
|
1918
|
+
* The rule above is right for every control it names and wrong for these two,
|
|
1627
1919
|
* and the difference is namable rather than a taste: the audit filter and the
|
|
1628
|
-
* delete button share no vocabulary with the key map, while
|
|
1629
|
-
* key map rendered
|
|
1630
|
-
* resolves the press against
|
|
1631
|
-
*
|
|
1920
|
+
* delete button share no vocabulary with the key map, while these two ARE the
|
|
1921
|
+
* key map rendered. The kind chooser's digits are `KIND_KEYS`, the same table
|
|
1922
|
+
* `keyIntent` resolves the press against, and `+ add` draws `RELATE_KEY`
|
|
1923
|
+
* itself as its hint. A control whose labels tell the reader to press `2`
|
|
1924
|
+
* cannot be a control the reader's `2` is refused by, and `#173` is the same
|
|
1925
|
+
* sentence about `R`: the hint named a key the reader genuinely has, at the
|
|
1926
|
+
* one focus position where it was inert.
|
|
1927
|
+
*
|
|
1928
|
+
* THE TEST IS "DOES THIS CONTROL DRAW A KEY THIS MAP READS", and it is what
|
|
1929
|
+
* stops the list growing on taste. It admits exactly two controls today; a
|
|
1930
|
+
* host's own widget, a filter, a remove button and every control that draws
|
|
1931
|
+
* no key stay `elsewhere`. `create/keys.ts` carries the matching half — the
|
|
1932
|
+
* new state admits the ONE binding it renders and no other.
|
|
1632
1933
|
*
|
|
1633
1934
|
* ## Asked of the DRAFT, not of where focus happens to be
|
|
1634
1935
|
*
|
|
@@ -1646,6 +1947,25 @@ export function mountWorkspace(element, options) {
|
|
|
1646
1947
|
* digits would still have died while the change typechecked and its own table
|
|
1647
1948
|
* test passed.
|
|
1648
1949
|
*
|
|
1950
|
+
* ## `add-control` reads focus, and that is not a relapse
|
|
1951
|
+
*
|
|
1952
|
+
* `#173`'s arm asks where focus is, which is the shape this section rejects —
|
|
1953
|
+
* so it has to survive the argument rather than repeat what it rejected, and
|
|
1954
|
+
* the difference is WHICH FACT IS MISSING. At the kind step the fact is the
|
|
1955
|
+
* DRAFT: a source with no kind IS the reader at that step, wherever a restore
|
|
1956
|
+
* put them, so reading focus there answers a question the draft had already
|
|
1957
|
+
* answered better. At the begin step there is no draft yet — beginning one is
|
|
1958
|
+
* the act — and the fact is the SUBJECT to begin from. The control publishes
|
|
1959
|
+
* it on `data-ig-target`, so what this arm reads is a control's own claim
|
|
1960
|
+
* about what it is for, and focus is what says the reader is standing on it.
|
|
1961
|
+
*
|
|
1962
|
+
* The `#150` failure mode is therefore absent by construction: it was focus
|
|
1963
|
+
* MOVING AWAY from the control the draft belonged to, and the arm here has no
|
|
1964
|
+
* "away" — it answers about the control still holding focus, and once the
|
|
1965
|
+
* draft begins the panel replaces this control with the kind list, at which
|
|
1966
|
+
* point the draft is the fact again and `kind-chooser` answers. That ordering
|
|
1967
|
+
* is written below rather than left to be inferred.
|
|
1968
|
+
*
|
|
1649
1969
|
* ## `canvas` is asked FIRST, and that ordering is the safety property
|
|
1650
1970
|
*
|
|
1651
1971
|
* The only answers that move are ones that read `elsewhere` today — where
|
|
@@ -1657,6 +1977,52 @@ export function mountWorkspace(element, options) {
|
|
|
1657
1977
|
* list sits in an inspector that carries no key — so it answers null from
|
|
1658
1978
|
* inside either one however this is ordered.
|
|
1659
1979
|
*/
|
|
1980
|
+
/**
|
|
1981
|
+
* Whether Escape has nothing of the CREATE PATH's to cancel.
|
|
1982
|
+
*
|
|
1983
|
+
* ONE PREDICATE FOR WHAT WAS TWO. §17d's overlay and §17k's transients both
|
|
1984
|
+
* take Escape, and both must yield to a live draft — a reader mid-relationship
|
|
1985
|
+
* means "not this edge", not "close the panel behind it". Written out at each
|
|
1986
|
+
* site, that rule is one a later edit can move at one of them.
|
|
1987
|
+
*
|
|
1988
|
+
* FIVE FIELDS, NOT THREE. `isDraftLive` answers for the draft itself, and the
|
|
1989
|
+
* target search and the drop point are the create path's too: a reader who
|
|
1990
|
+
* typed into the target box with no draft open still has something for Escape
|
|
1991
|
+
* to clear, and taking the press for a panel would leave their query sitting
|
|
1992
|
+
* there. `cancel` clears all five, which is why all five are asked.
|
|
1993
|
+
*/
|
|
1994
|
+
/**
|
|
1995
|
+
* Whether §17k's lifted inspector is actually ON SCREEN.
|
|
1996
|
+
*
|
|
1997
|
+
* `narrowOverlayOpen` answers about STATE, and state is not visibility here:
|
|
1998
|
+
* the panel is lifted between `1120` and `1359` and is an ordinary column
|
|
1999
|
+
* either side of that, so at the wide layout a selection makes the reducer's
|
|
2000
|
+
* predicate true with nothing overlaid for Escape to dismiss. The press was
|
|
2001
|
+
* being spent on a change the reader could not see.
|
|
2002
|
+
*
|
|
2003
|
+
* THE STYLESHEET KNOWS AND THE DOM SHOWS ITS ANSWER, which is why this is a
|
|
2004
|
+
* measurement rather than a width. The lifted panel is placed in the CANVAS's
|
|
2005
|
+
* grid area, so it overlaps the canvas's box; as a column it sits beside it
|
|
2006
|
+
* and cannot. That is the same fact the sheet encodes, read back rather than
|
|
2007
|
+
* re-derived — and it is not the window, which §17k rules out.
|
|
2008
|
+
*
|
|
2009
|
+
* A SURFACE WITH NO LAYOUT CANNOT ANSWER, and says so by yielding to the
|
|
2010
|
+
* state. Every box is zero-sized before layout, and under `node --test` there
|
|
2011
|
+
* is no layout at all; a strict reading would disable the key there and take
|
|
2012
|
+
* a tested behaviour away from every consumer that renders without a browser.
|
|
2013
|
+
*/
|
|
2014
|
+
const inspectorIsLifted = () => {
|
|
2015
|
+
const inspector = surface.querySelector('.ig-zone[data-zone="inspector"]');
|
|
2016
|
+
const canvas = surface.querySelector('.ig-zone[data-zone="canvas"]');
|
|
2017
|
+
if (inspector === null || canvas === null)
|
|
2018
|
+
return false;
|
|
2019
|
+
const panel = inspector.getBoundingClientRect();
|
|
2020
|
+
const zone = canvas.getBoundingClientRect();
|
|
2021
|
+
if (panel.width === 0 && zone.width === 0)
|
|
2022
|
+
return true;
|
|
2023
|
+
return panel.left < zone.right && panel.right > zone.left;
|
|
2024
|
+
};
|
|
2025
|
+
const nothingToCancel = () => !isDraftLive(state.draft) && state.targetQuery === '' && state.drop === null;
|
|
1660
2026
|
const interaction = () => {
|
|
1661
2027
|
const active = doc.activeElement;
|
|
1662
2028
|
if (isElement(active) && isInput(active) && active.getAttribute(COMMAND_ATTRIBUTE) === 'target-query') {
|
|
@@ -1680,7 +2046,14 @@ export function mountWorkspace(element, options) {
|
|
|
1680
2046
|
// change's to settle.
|
|
1681
2047
|
if (isInput(active))
|
|
1682
2048
|
return 'elsewhere';
|
|
1683
|
-
|
|
2049
|
+
// THE DRAFT IS ASKED BEFORE THE CONTROL, so the section above stays true of
|
|
2050
|
+
// the state it was written for. The two cannot both be live in practice —
|
|
2051
|
+
// `createStep` withholds `+ add` for the whole of the kind step — but a rule
|
|
2052
|
+
// that depends on another module's rendering condition is one that changes
|
|
2053
|
+
// silently the day that condition does.
|
|
2054
|
+
if (isChoosingKind(state.draft))
|
|
2055
|
+
return 'kind-chooser';
|
|
2056
|
+
return focusedAddSubject() === null ? 'elsewhere' : 'add-control';
|
|
1684
2057
|
};
|
|
1685
2058
|
/**
|
|
1686
2059
|
* The rail's window edge, decided BEFORE the viewer is asked.
|
|
@@ -1776,6 +2149,26 @@ export function mountWorkspace(element, options) {
|
|
|
1776
2149
|
focusIn(zoneName, result.command.key);
|
|
1777
2150
|
return true;
|
|
1778
2151
|
}
|
|
2152
|
+
// §16f's `→ expands provenance` on a list row. The viewer cannot expand
|
|
2153
|
+
// anything itself, so it publishes `expand:<key>` and this is where the
|
|
2154
|
+
// host takes it — split into the name/target shape every other control
|
|
2155
|
+
// here already uses, rather than teaching the reducer a second spelling.
|
|
2156
|
+
//
|
|
2157
|
+
// THE COLON IS LOAD-BEARING. `expand` with no key is §16a's panel-size
|
|
2158
|
+
// control and means something else entirely; only the qualified form is a
|
|
2159
|
+
// row. A press that names no key is handed back rather than swallowed.
|
|
2160
|
+
if (result.command.kind === 'command') {
|
|
2161
|
+
const [name, ...rest] = result.command.command.split(':');
|
|
2162
|
+
// REJOINED, NOT `rest[0]`. A qualified key is `owner/repo#12`, which
|
|
2163
|
+
// carries no colon today — but the split is over the whole command and
|
|
2164
|
+
// taking only the first segment would silently truncate any key that
|
|
2165
|
+
// ever does.
|
|
2166
|
+
const row = rest.join(':');
|
|
2167
|
+
if ((name === 'expand' || name === 'collapse') && row !== '') {
|
|
2168
|
+
dispatch({ kind: 'control', name, target: row });
|
|
2169
|
+
return true;
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
1779
2172
|
return false;
|
|
1780
2173
|
};
|
|
1781
2174
|
/**
|
|
@@ -1889,11 +2282,89 @@ export function mountWorkspace(element, options) {
|
|
|
1889
2282
|
// The FOCUSED row, not the selection: on a fresh page a row can own
|
|
1890
2283
|
// focus while nothing is selected, and after focus moves on, a stale
|
|
1891
2284
|
// selection must not become the source of a keyboard-started draft.
|
|
1892
|
-
|
|
2285
|
+
// AND `+ add`'S OWN SUBJECT WHERE THERE IS NO ROW, which is still that
|
|
2286
|
+
// rule rather than an exception to it — the fallback is a control's
|
|
2287
|
+
// published claim about what it acts on, not the selection this line
|
|
2288
|
+
// refuses. It moves nothing that worked: the second term is consulted
|
|
2289
|
+
// only where the first is `null`, and where it answers, `interaction()`
|
|
2290
|
+
// answers `add-control`, which admits `relate` alone.
|
|
2291
|
+
// AND IT DOES NOT OPEN THE NAVIGATION ARM BELOW, which is the one place a
|
|
2292
|
+
// wider `focused` could have leaked. That arm is guarded on this field,
|
|
2293
|
+
// but `navigateFocus` re-derives the key from a `[data-ig-key]` ANCESTOR
|
|
2294
|
+
// and answers `false` without one — and no command control has one, since
|
|
2295
|
+
// every one this package draws sits in chrome beside the keyed rows. So
|
|
2296
|
+
// the guard now admits a press the arm still declines, rather than
|
|
2297
|
+
// stepping the rail from a focused button.
|
|
2298
|
+
focused: focusedKey() ?? focusedAddSubject(),
|
|
1893
2299
|
match,
|
|
1894
2300
|
selectedEdge: selectedEdgeId(state.selection),
|
|
1895
2301
|
interaction: interaction(),
|
|
1896
2302
|
};
|
|
2303
|
+
// §17k'S TRANSIENT SURFACES ARE DISMISSED ON ESCAPE — the ruling's own
|
|
2304
|
+
// words for the lifted inspector, and the strip takes the same key because
|
|
2305
|
+
// a canvas opened over the inspector is the same kind of thing.
|
|
2306
|
+
//
|
|
2307
|
+
// BEFORE `keyIntent`, AND THE OBVIOUS PLACEMENT IS THE WRONG ONE. Putting
|
|
2308
|
+
// it after reads as the safe order — let the create loop have the press
|
|
2309
|
+
// first — and it makes this arm DEAD. `create/keys.ts` binds `escape` to
|
|
2310
|
+
// `cancel` with the comment "ALWAYS AVAILABLE", and `reaches` admits it
|
|
2311
|
+
// whenever the interaction is `canvas`, which `interaction()` answers for
|
|
2312
|
+
// any focused `[data-ig-key]`. So with focus on a rail row — where it sits
|
|
2313
|
+
// after almost every click on this surface — `keyIntent` claims Escape for
|
|
2314
|
+
// a draft that does not exist and returns, and nothing below it is reached.
|
|
2315
|
+
// MEASURED IN A BROWSER, not reasoned about: the panel stayed open, and the
|
|
2316
|
+
// `dataInspector` attribute stayed `lifted` across the press.
|
|
2317
|
+
//
|
|
2318
|
+
// THE DRAFT STILL WINS, WHICH IS WHAT THE OTHER ORDER WAS FOR. A reader
|
|
2319
|
+
// mid-relationship pressing Escape means "not this edge", not "close the
|
|
2320
|
+
// panel behind it" — so the guard is `nothingToCancel`, which asks about
|
|
2321
|
+
// the create path rather than about the position in this function. An idle
|
|
2322
|
+
// one is the only state `keyIntent` would have spent the press on, and
|
|
2323
|
+
// cancelling an idle draft is a no-op.
|
|
2324
|
+
//
|
|
2325
|
+
// GUARDED ALSO ON `narrowOverlayOpen`, WHICH IS THE REDUCER'S OWN PREDICATE
|
|
2326
|
+
// rather than a second reading of the same two fields, so the two cannot
|
|
2327
|
+
// drift apart — `reduceHost` answers `unclaimed` for exactly the same case.
|
|
2328
|
+
//
|
|
2329
|
+
// WHAT THAT DOES NOT DO IS LET THE PRESS THROUGH. A press this arm declines
|
|
2330
|
+
// falls to `keyIntent`, which binds Escape to a cancel that is "ALWAYS
|
|
2331
|
+
// AVAILABLE" and cancels it there — so on a focused rail row Escape is
|
|
2332
|
+
// consumed by this surface whether or not anything was dismissed. That
|
|
2333
|
+
// predates this branch and is not changed here; it is written down because
|
|
2334
|
+
// the obvious reading of `unclaimed` is that the key reaches the host, and
|
|
2335
|
+
// on this surface it does not.
|
|
2336
|
+
//
|
|
2337
|
+
// THE REDUCER CANNOT ASK WHICH OF THE TWO THE READER CAN SEE, and it must
|
|
2338
|
+
// not: the width is the stylesheet's to know, per §17k's note that a
|
|
2339
|
+
// package reading the window cannot be dropped into someone else's page.
|
|
2340
|
+
// So it closes both and the sheet draws whichever was on screen.
|
|
2341
|
+
//
|
|
2342
|
+
// THE SHELL CAN ASK, THOUGH, AND HAS TO. An earlier revision left the guard
|
|
2343
|
+
// at the reducer's predicate alone and accepted "a press spent invisibly at
|
|
2344
|
+
// the wide layout" as a cost. It is not a cost worth taking: a key consumed
|
|
2345
|
+
// for a surface nobody can see is indistinguishable from a broken key. See
|
|
2346
|
+
// `inspectorIsLifted`, which reads the sheet's own answer off the DOM
|
|
2347
|
+
// rather than re-deriving it from a width.
|
|
2348
|
+
//
|
|
2349
|
+
// TWO TRANSIENTS SHARE THIS KEY NOW, AND §17d'S GOES FIRST. The audit
|
|
2350
|
+
// overlay is the one the reader pressed a control to open, and it is drawn
|
|
2351
|
+
// at EVERY width; §17k's lifted inspector is opened implicitly by a
|
|
2352
|
+
// selection and exists only between `1120` and `1359`. So a press with the
|
|
2353
|
+
// audit panel up closes the audit panel, at any width — without this order
|
|
2354
|
+
// a reader at the wide layout, where nothing of §17k's is drawn, would
|
|
2355
|
+
// press Escape and watch the panel stay put.
|
|
2356
|
+
if (event.key === 'Escape' && nothingToCancel()) {
|
|
2357
|
+
if (state.auditOpen) {
|
|
2358
|
+
event.preventDefault();
|
|
2359
|
+
dispatch({ kind: 'control', name: 'audit-close' });
|
|
2360
|
+
return;
|
|
2361
|
+
}
|
|
2362
|
+
if (narrowOverlayOpen(state) && (state.canvasOpen || inspectorIsLifted())) {
|
|
2363
|
+
event.preventDefault();
|
|
2364
|
+
dispatch({ kind: 'control', name: 'narrow-dismiss' });
|
|
2365
|
+
return;
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
1897
2368
|
const intent = keyIntent(event, context);
|
|
1898
2369
|
if (intent.kind !== 'none') {
|
|
1899
2370
|
event.preventDefault();
|
|
@@ -1913,11 +2384,13 @@ export function mountWorkspace(element, options) {
|
|
|
1913
2384
|
// Two facts make it true here rather than merely plausible.
|
|
1914
2385
|
//
|
|
1915
2386
|
// `interaction()` answers `elsewhere` for a focused command control that is
|
|
1916
|
-
// neither an input nor the kind step, so `keyIntent` returns
|
|
1917
|
-
// every key there. At the KIND STEP it answers `kind-chooser` —
|
|
1918
|
-
// predicate is asked of the DRAFT, not of where focus is — and there
|
|
2387
|
+
// neither an input, nor the kind step, nor `+ add`, so `keyIntent` returns
|
|
2388
|
+
// `none` for every key there. At the KIND STEP it answers `kind-chooser` —
|
|
2389
|
+
// the predicate is asked of the DRAFT, not of where focus is — and there
|
|
1919
2390
|
// `enter` does not survive and `' '` is unbound, so `keyIntent` is silent
|
|
1920
|
-
// again.
|
|
2391
|
+
// again. On `+ add` it answers `add-control`, which admits `RELATE_KEY`
|
|
2392
|
+
// alone (`#173`) and neither of these two, so this arm still owns them
|
|
2393
|
+
// there. So this arm does claim `Enter` and `Space` on the inspector's
|
|
1921
2394
|
// `clear`, which is where the mount's focus restore lands after `begin`.
|
|
1922
2395
|
// That is a control doing what a control does, and it is new behaviour on
|
|
1923
2396
|
// a step the create loop passes through every time — said out loud because
|
|
@@ -2001,8 +2474,18 @@ export function mountWorkspace(element, options) {
|
|
|
2001
2474
|
// A REFUSAL IS THIS PACKAGE'S OWN ANSWER, so it keeps the press: `Space`
|
|
2002
2475
|
// would otherwise scroll the page under a reader who has just pressed
|
|
2003
2476
|
// something, and there is nothing else waiting for this key.
|
|
2477
|
+
//
|
|
2478
|
+
// EXCEPT A NAVIGATION REFUSAL, WHICH MEANS THE OPPOSITE. `input` and `inert`
|
|
2479
|
+
// say "this press does nothing, and nothing else wants it"; `navigation`
|
|
2480
|
+
// says the BROWSER performs this control's activation, so there is something
|
|
2481
|
+
// else waiting for the key and cancelling is how it never arrives. An
|
|
2482
|
+
// anchor's `Enter` activation is a DEFAULT ACTION — cancel the keydown and
|
|
2483
|
+
// no click is synthesized and the link never opens, which would leave §17d's
|
|
2484
|
+
// outward link working for a pointer and dead for a keyboard. The click path
|
|
2485
|
+
// still refuses to DISPATCH it; the two answers differ because the acts do.
|
|
2004
2486
|
if (answer.kind === 'refused') {
|
|
2005
|
-
|
|
2487
|
+
if (answer.reason !== 'navigation')
|
|
2488
|
+
event.preventDefault();
|
|
2006
2489
|
return;
|
|
2007
2490
|
}
|
|
2008
2491
|
// DISPATCH FIRST, THEN DECIDE WHETHER THE PRESS WAS OURS TO KEEP — and that
|