tablewalk 0.0.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/LICENSE +21 -0
- package/README.md +553 -0
- package/dist/adapters/adapter.js +372 -0
- package/dist/adapters/connect.js +33 -0
- package/dist/adapters/mysql.js +951 -0
- package/dist/adapters/postgres.js +1000 -0
- package/dist/adapters/sqlite.js +781 -0
- package/dist/client/agent.js +262 -0
- package/dist/client/app.js +973 -0
- package/dist/client/arrange.js +254 -0
- package/dist/client/ask.js +133 -0
- package/dist/client/breakdown.js +317 -0
- package/dist/client/clauses.js +390 -0
- package/dist/client/columns.js +98 -0
- package/dist/client/complete.js +437 -0
- package/dist/client/compose.js +166 -0
- package/dist/client/composer.css +495 -0
- package/dist/client/composer.js +1972 -0
- package/dist/client/connections.js +234 -0
- package/dist/client/connmanager.js +962 -0
- package/dist/client/connurl.js +188 -0
- package/dist/client/core.js +893 -0
- package/dist/client/deeplink.js +270 -0
- package/dist/client/delete.js +144 -0
- package/dist/client/diagram.js +885 -0
- package/dist/client/dropdown.js +279 -0
- package/dist/client/export.js +456 -0
- package/dist/client/features.css +524 -0
- package/dist/client/findvalue.js +169 -0
- package/dist/client/grid.js +205 -0
- package/dist/client/handoff.js +153 -0
- package/dist/client/help.css +145 -0
- package/dist/client/help.js +881 -0
- package/dist/client/history.js +222 -0
- package/dist/client/index.html +116 -0
- package/dist/client/insert.js +151 -0
- package/dist/client/menu.js +160 -0
- package/dist/client/nested.js +255 -0
- package/dist/client/page.css +713 -0
- package/dist/client/page.js +1345 -0
- package/dist/client/pagebuilder.js +1222 -0
- package/dist/client/pagemarks.js +95 -0
- package/dist/client/palette.js +374 -0
- package/dist/client/peek.js +254 -0
- package/dist/client/picker.js +139 -0
- package/dist/client/pins.js +140 -0
- package/dist/client/prompt.js +129 -0
- package/dist/client/record.js +707 -0
- package/dist/client/schemaexport.js +242 -0
- package/dist/client/schematext.js +125 -0
- package/dist/client/shape.js +178 -0
- package/dist/client/shapecheck.js +129 -0
- package/dist/client/skeleton.js +139 -0
- package/dist/client/sql.css +126 -0
- package/dist/client/sql.js +398 -0
- package/dist/client/sqlcomplete.js +163 -0
- package/dist/client/sqlsaved.js +107 -0
- package/dist/client/style.css +2711 -0
- package/dist/client/summary.js +259 -0
- package/dist/client/table.js +1035 -0
- package/dist/client/template.js +539 -0
- package/dist/client/theme.js +74 -0
- package/dist/client/tour.js +324 -0
- package/dist/client/undo.js +105 -0
- package/dist/client/url.js +166 -0
- package/dist/client/value.js +223 -0
- package/dist/client/views.js +215 -0
- package/dist/client/virtual.js +176 -0
- package/dist/client/welcome.js +170 -0
- package/dist/client/write.js +414 -0
- package/dist/server/changeimpact.js +195 -0
- package/dist/server/connections.js +615 -0
- package/dist/server/constraints.js +62 -0
- package/dist/server/credentials.js +230 -0
- package/dist/server/fixture.js +199 -0
- package/dist/server/graph.js +194 -0
- package/dist/server/impact.js +48 -0
- package/dist/server/index.js +2204 -0
- package/dist/server/journal.js +173 -0
- package/dist/server/layouts.js +128 -0
- package/dist/server/mcp.js +2840 -0
- package/dist/server/shapeonly.js +91 -0
- package/dist/shared/breakdown.js +231 -0
- package/dist/shared/breakdowntext.js +257 -0
- package/dist/shared/diff.js +130 -0
- package/dist/shared/like.js +29 -0
- package/dist/shared/lint.js +149 -0
- package/dist/shared/order.js +133 -0
- package/dist/shared/page.js +932 -0
- package/dist/shared/query.js +831 -0
- package/dist/shared/recordview.js +343 -0
- package/dist/shared/schema.js +377 -0
- package/dist/shared/sqlsaved.js +67 -0
- package/dist/shared/view.js +981 -0
- package/dist/shared/viewtext.js +273 -0
- package/dist/shared/vocabulary.js +164 -0
- package/package.json +57 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Arranging a group of fields: which ones, and in what order.
|
|
3
|
+
*
|
|
4
|
+
* Two builders ask this question. The page builder asks it of a page section;
|
|
5
|
+
* the record layout builder asks it of a block on a record. They asked it in
|
|
6
|
+
* two different ways for months — the page builder with a draggable list, the
|
|
7
|
+
* record builder with a grid of checkboxes — and the difference was an
|
|
8
|
+
* accident of which was written second, not a decision anybody made. The
|
|
9
|
+
* checkbox grid answers "which fields" and cannot answer "in what order",
|
|
10
|
+
* which is half the question: fields fill their columns left to right, so the
|
|
11
|
+
* order *is* the layout.
|
|
12
|
+
*
|
|
13
|
+
* So this module owns the answer and both builders ask it. It knows nothing
|
|
14
|
+
* about pages or record templates — it is handed a list of candidates, the
|
|
15
|
+
* ones currently on, and a function to call when that changes. What the
|
|
16
|
+
* caller stores, and where, stays the caller's business: the page builder
|
|
17
|
+
* writes `undefined` when the arrangement is the table's own order, and the
|
|
18
|
+
* record builder always writes the list. Those are two different storage
|
|
19
|
+
* rules over one interaction, which is exactly the split worth having.
|
|
20
|
+
*
|
|
21
|
+
* The classes stay `pb-*`. They were named for the page builder because that
|
|
22
|
+
* is where this was written, and renaming them would churn a stylesheet and a
|
|
23
|
+
* browser suite to say something the file header already says.
|
|
24
|
+
*/
|
|
25
|
+
import { el } from './core.js';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A field mid-drag, when one is: its name, the group it left, and how to take
|
|
29
|
+
* it out of that group.
|
|
30
|
+
*
|
|
31
|
+
* Module state because the drag starts inside one group's arranger and ends
|
|
32
|
+
* on another group's row — two closures that otherwise never speak. There is
|
|
33
|
+
* one pointer and one drag, so there is one of these.
|
|
34
|
+
*/
|
|
35
|
+
let carried = null;
|
|
36
|
+
|
|
37
|
+
/** The field being dragged between groups, or null. */
|
|
38
|
+
export const carriedField = () => carried;
|
|
39
|
+
|
|
40
|
+
/** Drop the carried field into a group that is not the one it came from. */
|
|
41
|
+
export function dropCarried(owner, receive, redraw) {
|
|
42
|
+
if (!carried || carried.owner === owner) return;
|
|
43
|
+
const drag = carried;
|
|
44
|
+
carried = null;
|
|
45
|
+
drag.take();
|
|
46
|
+
receive(drag.name);
|
|
47
|
+
redraw();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The arranger for one group.
|
|
52
|
+
*
|
|
53
|
+
* @param {object} spec
|
|
54
|
+
* @param {string[]} spec.all every candidate, in the source's own order
|
|
55
|
+
* @param {string[]} spec.chosen the ones on now, in the order they are laid out
|
|
56
|
+
* @param {number} [spec.columns] how many columns the group draws in
|
|
57
|
+
* @param {(name: string) => string} [spec.label] what to show on the row
|
|
58
|
+
* @param {(name: string) => string} [spec.typeOf] the type, dimmed beside it
|
|
59
|
+
* @param {(next: string[]) => void} spec.onChange called with the new list
|
|
60
|
+
* @param {object} spec.owner identity of this group, for cross-group drags
|
|
61
|
+
* @param {Array<{title: string, receive: (name: string) => void}>} [spec.others]
|
|
62
|
+
* the other groups a field can be sent to
|
|
63
|
+
* @param {() => void} [spec.onSend] redraw everything after a hand-over
|
|
64
|
+
* @param {string} [spec.noun] what a field is taken off — "page", "layout"
|
|
65
|
+
* @param {string|null} [spec.heading] the panel's own label, or null where
|
|
66
|
+
* whatever the arranger is inside already says what these are
|
|
67
|
+
*/
|
|
68
|
+
export function fieldArranger({
|
|
69
|
+
all,
|
|
70
|
+
chosen: initial,
|
|
71
|
+
columns = 1,
|
|
72
|
+
noun = 'layout',
|
|
73
|
+
heading = 'Fields',
|
|
74
|
+
label = (name) => name,
|
|
75
|
+
typeOf = () => '',
|
|
76
|
+
onChange,
|
|
77
|
+
owner,
|
|
78
|
+
others = [],
|
|
79
|
+
onSend,
|
|
80
|
+
}) {
|
|
81
|
+
let chosen = [...initial];
|
|
82
|
+
|
|
83
|
+
const list = el('div', { class: 'pb-arrange' });
|
|
84
|
+
const pool = el('div', { class: 'pb-pool' });
|
|
85
|
+
const count = el('span', { class: 'pb-field-count' });
|
|
86
|
+
|
|
87
|
+
const commit = () => {
|
|
88
|
+
onChange([...chosen]);
|
|
89
|
+
count.textContent = `${chosen.length} of ${all.length} · drag to arrange`;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const move = (from, to) => {
|
|
93
|
+
if (to < 0 || to >= chosen.length || from === to) return;
|
|
94
|
+
const [item] = chosen.splice(from, 1);
|
|
95
|
+
chosen.splice(to, 0, item);
|
|
96
|
+
commit();
|
|
97
|
+
paintList({ focus: to });
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
let dragging = null;
|
|
101
|
+
|
|
102
|
+
function paintList({ focus = -1 } = {}) {
|
|
103
|
+
list.style.gridTemplateColumns = `repeat(${columns}, minmax(0, 1fr))`;
|
|
104
|
+
list.replaceChildren(...chosen.map((name, i) => {
|
|
105
|
+
const row = el('div', {
|
|
106
|
+
class: 'pb-arrange-row',
|
|
107
|
+
draggable: 'true',
|
|
108
|
+
tabindex: '0',
|
|
109
|
+
role: 'listitem',
|
|
110
|
+
'aria-label': `${name} — position ${i + 1} of ${chosen.length}`,
|
|
111
|
+
ondragstart: (e) => {
|
|
112
|
+
dragging = i;
|
|
113
|
+
/* Also offered to every other group's row: reordering inside this
|
|
114
|
+
list and carrying to another group are the same grip. `take` is
|
|
115
|
+
how the receiving side removes the field from here without
|
|
116
|
+
reaching into this closure's state. */
|
|
117
|
+
carried = {
|
|
118
|
+
name,
|
|
119
|
+
owner,
|
|
120
|
+
take: () => {
|
|
121
|
+
const at = chosen.indexOf(name);
|
|
122
|
+
if (at >= 0) chosen.splice(at, 1);
|
|
123
|
+
commit();
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
row.classList.add('dragging');
|
|
127
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
128
|
+
/* Firefox will not start a drag without something on the transfer,
|
|
129
|
+
and the payload is never read — the index above is the state. */
|
|
130
|
+
e.dataTransfer.setData('text/plain', name);
|
|
131
|
+
},
|
|
132
|
+
ondragend: () => { dragging = null; carried = null; paintList(); },
|
|
133
|
+
ondragover: (e) => {
|
|
134
|
+
if (dragging === null || dragging === i) return;
|
|
135
|
+
e.preventDefault();
|
|
136
|
+
const at = dragging;
|
|
137
|
+
dragging = i;
|
|
138
|
+
const [item] = chosen.splice(at, 1);
|
|
139
|
+
chosen.splice(i, 0, item);
|
|
140
|
+
commit();
|
|
141
|
+
paintList();
|
|
142
|
+
/* Re-marked after the repaint, or the row under the pointer loses
|
|
143
|
+
the class that says it is the one being carried. */
|
|
144
|
+
list.children[i]?.classList.add('dragging');
|
|
145
|
+
},
|
|
146
|
+
ondrop: (e) => { e.preventDefault(); },
|
|
147
|
+
onkeydown: (e) => {
|
|
148
|
+
if (!e.altKey) return;
|
|
149
|
+
if (e.key === 'ArrowUp') { e.preventDefault(); move(i, i - 1); }
|
|
150
|
+
if (e.key === 'ArrowDown') { e.preventDefault(); move(i, i + 1); }
|
|
151
|
+
},
|
|
152
|
+
}, [
|
|
153
|
+
el('span', { class: 'pb-grip', 'aria-hidden': 'true', text: '∷' }),
|
|
154
|
+
el('span', { class: 'pb-field-name', text: label(name), title: name }),
|
|
155
|
+
el('span', { class: 'pb-field-type', text: typeOf(name) }),
|
|
156
|
+
others.length
|
|
157
|
+
? el('button', {
|
|
158
|
+
type: 'button', class: 'ghost pb-send',
|
|
159
|
+
text: '→',
|
|
160
|
+
title: 'Move this field to another group',
|
|
161
|
+
'aria-label': `Move ${name} to another group`,
|
|
162
|
+
onclick: () => sendTo(name, i),
|
|
163
|
+
})
|
|
164
|
+
: null,
|
|
165
|
+
el('button', {
|
|
166
|
+
type: 'button', class: 'ghost pb-remove', text: '×',
|
|
167
|
+
title: `Take this field off the ${noun}`,
|
|
168
|
+
'aria-label': `Remove ${name}`,
|
|
169
|
+
onclick: () => {
|
|
170
|
+
chosen.splice(i, 1);
|
|
171
|
+
commit();
|
|
172
|
+
paintList();
|
|
173
|
+
paintPool();
|
|
174
|
+
},
|
|
175
|
+
}),
|
|
176
|
+
].filter(Boolean));
|
|
177
|
+
return row;
|
|
178
|
+
}));
|
|
179
|
+
if (focus >= 0) list.children[focus]?.focus();
|
|
180
|
+
if (!chosen.length) {
|
|
181
|
+
list.replaceChildren(el('p', { class: 'note', text: 'No fields in this group yet.' }));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/* Sending a field elsewhere redraws whatever the caller draws, because two
|
|
186
|
+
groups change and the other one may be open. */
|
|
187
|
+
function sendTo(name, i) {
|
|
188
|
+
if (others.length === 1) return handOver(name, i, others[0]);
|
|
189
|
+
/* More than one candidate, so it is a choice — made in the row rather
|
|
190
|
+
than in a dialog, which would cover the thing being arranged. */
|
|
191
|
+
const menu = el('div', { class: 'pb-send-menu' }, others.map((other) => el('button', {
|
|
192
|
+
type: 'button', class: 'ghost',
|
|
193
|
+
text: other.title,
|
|
194
|
+
onclick: () => handOver(name, i, other),
|
|
195
|
+
})));
|
|
196
|
+
const row = list.children[i];
|
|
197
|
+
row?.querySelector('.pb-send-menu')?.remove();
|
|
198
|
+
row?.append(menu);
|
|
199
|
+
menu.querySelector('button')?.focus();
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function handOver(name, i, other) {
|
|
204
|
+
chosen.splice(i, 1);
|
|
205
|
+
commit();
|
|
206
|
+
other.receive(name);
|
|
207
|
+
onSend?.();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function paintPool() {
|
|
211
|
+
const rest = all.filter((n) => !chosen.includes(n));
|
|
212
|
+
pool.replaceChildren(
|
|
213
|
+
el('span', { class: 'pb-label', text: rest.length ? 'Add' : `Every field is on the ${noun}` }),
|
|
214
|
+
...rest.map((name) => el('button', {
|
|
215
|
+
type: 'button', class: 'ghost pb-add-field',
|
|
216
|
+
text: label(name),
|
|
217
|
+
title: `Add ${name} to this group`,
|
|
218
|
+
/* Named for what pressing it does. The visible text is the field's
|
|
219
|
+
own name, which reads as a label rather than as a control to
|
|
220
|
+
anyone hearing the page one element at a time. */
|
|
221
|
+
'aria-label': `Add ${name}`,
|
|
222
|
+
onclick: () => {
|
|
223
|
+
chosen.push(name);
|
|
224
|
+
commit();
|
|
225
|
+
paintList();
|
|
226
|
+
paintPool();
|
|
227
|
+
},
|
|
228
|
+
})),
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
count.textContent = `${chosen.length} of ${all.length} · drag to arrange`;
|
|
233
|
+
paintList();
|
|
234
|
+
paintPool();
|
|
235
|
+
|
|
236
|
+
return el('div', { class: 'pb-panel' }, [
|
|
237
|
+
el('div', { class: 'pb-panel-head' }, [
|
|
238
|
+
heading ? el('span', { class: 'pb-label', text: heading }) : null,
|
|
239
|
+
count,
|
|
240
|
+
el('span', { class: 'pb-spacer' }),
|
|
241
|
+
el('button', {
|
|
242
|
+
type: 'button', class: 'ghost', text: 'All',
|
|
243
|
+
title: 'Every field, in the table’s own order',
|
|
244
|
+
onclick: () => { chosen = [...all]; commit(); paintList(); paintPool(); },
|
|
245
|
+
}),
|
|
246
|
+
el('button', {
|
|
247
|
+
type: 'button', class: 'ghost', text: 'None',
|
|
248
|
+
onclick: () => { chosen = []; commit(); paintList(); paintPool(); },
|
|
249
|
+
}),
|
|
250
|
+
].filter(Boolean)),
|
|
251
|
+
list,
|
|
252
|
+
pool,
|
|
253
|
+
]);
|
|
254
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The query bar, in the middle of the screen.
|
|
3
|
+
*
|
|
4
|
+
* The bar in the header is right for refining what you are already looking at
|
|
5
|
+
* — it holds the query you ran, a click away, and it is where the answer's
|
|
6
|
+
* sort clause gets rewritten. It is the wrong surface for *writing* one. It is
|
|
7
|
+
* a single line about 90 characters wide sharing the header with a wordmark, a
|
|
8
|
+
* connection picker and four buttons, and a real question outgrows it:
|
|
9
|
+
*
|
|
10
|
+
* invoice where total > 100 count, sum total as billed by month invoice_date sort billed desc
|
|
11
|
+
*
|
|
12
|
+
* scrolls sideways in the bar while you are still deciding what to ask. So
|
|
13
|
+
* this is the same input given room — centred, wrapped, nothing else on
|
|
14
|
+
* screen — on ⌘/ , beside ⌘K. The pairing is the point: **⌘K goes to things,
|
|
15
|
+
* ⌘/ asks things.**
|
|
16
|
+
*
|
|
17
|
+
* It deliberately does not run the query itself. The decision about *which
|
|
18
|
+
* endpoint answers a line* — a `by` clause means a breakdown, everything else
|
|
19
|
+
* is a view — belongs in one place, and a modal that re-made it would be the
|
|
20
|
+
* second place for it to go stale. The caller hands in `onRun`, which is the
|
|
21
|
+
* same function the header's form submits to.
|
|
22
|
+
*
|
|
23
|
+
* A textarea rather than an input, purely so a long line wraps instead of
|
|
24
|
+
* scrolling. The language is single-line, so Enter runs and any newline that
|
|
25
|
+
* arrives by paste is flattened rather than being quietly sent to a parser
|
|
26
|
+
* that has no idea what to do with it.
|
|
27
|
+
*/
|
|
28
|
+
import { el, trapFocus } from './core.js';
|
|
29
|
+
|
|
30
|
+
let panel = null;
|
|
31
|
+
let restoreFocus = null;
|
|
32
|
+
/** Undoes the Tab trap. The focus restore below is this module's own. */
|
|
33
|
+
let release = null;
|
|
34
|
+
|
|
35
|
+
export const askIsOpen = () => panel !== null;
|
|
36
|
+
|
|
37
|
+
export function closeAsk() {
|
|
38
|
+
if (!panel) return;
|
|
39
|
+
release?.();
|
|
40
|
+
release = null;
|
|
41
|
+
panel.remove();
|
|
42
|
+
panel = null;
|
|
43
|
+
document.removeEventListener('keydown', onKey, true);
|
|
44
|
+
/* Back to whatever opened it. Without this, closing left the caret on
|
|
45
|
+
`document.body`, where the single-key shortcuts do work — so an Escape
|
|
46
|
+
followed by a stray `h` went home from a view somebody had just decided
|
|
47
|
+
not to leave. */
|
|
48
|
+
restoreFocus?.focus?.();
|
|
49
|
+
restoreFocus = null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function onKey(e) {
|
|
53
|
+
if (e.key !== 'Escape') return;
|
|
54
|
+
/* Stopped as well as prevented, the same rule the palette follows: the page
|
|
55
|
+
under a modal listens for Escape too, and one press must not close two
|
|
56
|
+
things. */
|
|
57
|
+
e.preventDefault();
|
|
58
|
+
e.stopPropagation();
|
|
59
|
+
closeAsk();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Open it.
|
|
64
|
+
*
|
|
65
|
+
* `initial` is the line to start from — the one in the bar, so this is "edit
|
|
66
|
+
* what I ran" as much as "write something new" — and it arrives selected, so
|
|
67
|
+
* typing replaces it and an arrow key keeps it.
|
|
68
|
+
*/
|
|
69
|
+
export function openAsk({ initial = '', onRun } = {}) {
|
|
70
|
+
if (panel) return;
|
|
71
|
+
restoreFocus = document.activeElement;
|
|
72
|
+
|
|
73
|
+
const input = el('textarea', {
|
|
74
|
+
class: 'ask-input',
|
|
75
|
+
rows: '3',
|
|
76
|
+
spellcheck: 'false',
|
|
77
|
+
autocomplete: 'off',
|
|
78
|
+
'aria-label': 'Query',
|
|
79
|
+
placeholder: 'customer active = true sort name limit 20',
|
|
80
|
+
});
|
|
81
|
+
input.value = initial;
|
|
82
|
+
|
|
83
|
+
const run = () => {
|
|
84
|
+
/* Flattened rather than trimmed at the ends: a pasted two-line query would
|
|
85
|
+
otherwise reach the parser with a newline in the middle of it, and the
|
|
86
|
+
message that comes back is about whatever token followed it. */
|
|
87
|
+
const text = input.value.replace(/\s+/g, ' ').trim();
|
|
88
|
+
if (!text) return;
|
|
89
|
+
closeAsk();
|
|
90
|
+
onRun?.(text);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
input.addEventListener('keydown', (e) => {
|
|
94
|
+
if (e.key !== 'Enter' || e.shiftKey) return;
|
|
95
|
+
e.preventDefault();
|
|
96
|
+
run();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const box = el('div', {
|
|
100
|
+
class: 'ask',
|
|
101
|
+
role: 'dialog',
|
|
102
|
+
'aria-modal': 'true',
|
|
103
|
+
'aria-label': 'Write a query',
|
|
104
|
+
}, [
|
|
105
|
+
input,
|
|
106
|
+
el('div', { class: 'ask-foot' }, [
|
|
107
|
+
/* What the language can do that the placeholder cannot show, in the one
|
|
108
|
+
place someone is definitely about to write one. `by` is here because
|
|
109
|
+
it is the newest clause and the least guessable — nothing else in the
|
|
110
|
+
bar hints that grouping exists at all. */
|
|
111
|
+
el('span', { class: 'ask-hint' }, [
|
|
112
|
+
el('code', { text: 'by' }),
|
|
113
|
+
document.createTextNode(' groups — '),
|
|
114
|
+
el('code', { text: 'invoice count by status' }),
|
|
115
|
+
]),
|
|
116
|
+
el('span', { class: 'ask-keys', text: 'Enter to run · Esc to cancel' }),
|
|
117
|
+
]),
|
|
118
|
+
]);
|
|
119
|
+
|
|
120
|
+
panel = el('div', {
|
|
121
|
+
class: 'ask-backdrop',
|
|
122
|
+
/* Clicking away is a cancel, but only on the backdrop itself — a click
|
|
123
|
+
that started on the box and drifted out while selecting text is not a
|
|
124
|
+
request to throw the line away. */
|
|
125
|
+
onmousedown: (e) => { if (e.target === panel) closeAsk(); },
|
|
126
|
+
}, box);
|
|
127
|
+
|
|
128
|
+
document.body.append(panel);
|
|
129
|
+
release = trapFocus(panel);
|
|
130
|
+
document.addEventListener('keydown', onKey, true);
|
|
131
|
+
input.focus();
|
|
132
|
+
input.select();
|
|
133
|
+
}
|