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,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A themed listbox.
|
|
3
|
+
*
|
|
4
|
+
* A native `<select>` cannot be styled where it matters: the open menu is
|
|
5
|
+
* drawn by the operating system, in the system font, with the system
|
|
6
|
+
* highlight colour, ignoring the page entirely. On a dark theme that means a
|
|
7
|
+
* white menu with blue selection appearing out of a dark page.
|
|
8
|
+
*
|
|
9
|
+
* It also cannot show two lines per option, which is the actual requirement
|
|
10
|
+
* here — a connection is a name *and* a host, and the host is what tells two
|
|
11
|
+
* similarly named databases apart.
|
|
12
|
+
*
|
|
13
|
+
* Rebuilding a select means rebuilding what a select gives you for free, so
|
|
14
|
+
* this implements the listbox keyboard contract rather than approximating it:
|
|
15
|
+
* arrows move, Home and End jump, Enter and Space choose, Escape closes and
|
|
16
|
+
* returns focus, Tab closes, typing jumps to a matching option. Anything less
|
|
17
|
+
* is a control that looks better and works worse.
|
|
18
|
+
*/
|
|
19
|
+
import { el } from './core.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {object} options
|
|
23
|
+
* @param {Array<{value: string, label: string, detail?: string, badge?: string, disabled?: boolean, tone?: string}>} options.items
|
|
24
|
+
* @param {string} options.value currently selected value
|
|
25
|
+
* @param {(value: string) => void} options.onChange
|
|
26
|
+
* @param {string} [options.ariaLabel]
|
|
27
|
+
* @param {Array<{label: string, onSelect: () => void}>} [options.actions] pinned at the bottom
|
|
28
|
+
*/
|
|
29
|
+
export function dropdown({ items, value, onChange, ariaLabel = 'Select', actions = [] }) {
|
|
30
|
+
/* Reassigned by `choose`, so the control tracks its own state rather than
|
|
31
|
+
depending on the caller to redraw it. */
|
|
32
|
+
let selected = items.find((i) => i.value === value);
|
|
33
|
+
|
|
34
|
+
const button = el('button', {
|
|
35
|
+
type: 'button',
|
|
36
|
+
class: 'dd-button',
|
|
37
|
+
'aria-haspopup': 'listbox',
|
|
38
|
+
'aria-expanded': 'false',
|
|
39
|
+
'aria-label': ariaLabel,
|
|
40
|
+
title: selected?.detail ?? selected?.label ?? '',
|
|
41
|
+
}, [
|
|
42
|
+
el('span', { class: 'dd-value', text: selected?.label ?? '—' }),
|
|
43
|
+
el('span', { class: 'dd-caret', 'aria-hidden': 'true', text: '▾' }),
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
const list = el('div', { class: 'dd-list', role: 'listbox', 'aria-label': ariaLabel, hidden: true });
|
|
47
|
+
const root = el('div', { class: 'dd' }, [button, list]);
|
|
48
|
+
|
|
49
|
+
/** Options plus actions, in DOM order, for keyboard traversal. */
|
|
50
|
+
let focusables = [];
|
|
51
|
+
let activeIndex = -1;
|
|
52
|
+
|
|
53
|
+
function build() {
|
|
54
|
+
list.replaceChildren();
|
|
55
|
+
focusables = [];
|
|
56
|
+
|
|
57
|
+
for (const item of items) {
|
|
58
|
+
const option = el('div', {
|
|
59
|
+
class: `dd-option${item.value === value ? ' selected' : ''}${item.disabled ? ' disabled' : ''}${item.tone ? ` tone-${item.tone}` : ''}`,
|
|
60
|
+
role: 'option',
|
|
61
|
+
'aria-selected': String(item.value === value),
|
|
62
|
+
'aria-disabled': item.disabled ? 'true' : undefined,
|
|
63
|
+
tabindex: -1,
|
|
64
|
+
onclick: () => !item.disabled && choose(item.value),
|
|
65
|
+
}, [
|
|
66
|
+
el('span', { class: 'dd-check', 'aria-hidden': 'true', text: item.value === value ? '✓' : '' }),
|
|
67
|
+
el('span', { class: 'dd-text' }, [
|
|
68
|
+
el('span', { class: 'dd-label', text: item.label }),
|
|
69
|
+
item.detail ? el('span', { class: 'dd-detail', text: item.detail }) : null,
|
|
70
|
+
]),
|
|
71
|
+
item.badge ? el('span', { class: 'dd-badge', text: item.badge }) : null,
|
|
72
|
+
]);
|
|
73
|
+
list.append(option);
|
|
74
|
+
focusables.push({ node: option, run: () => !item.disabled && choose(item.value), text: item.label });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (actions.length) {
|
|
78
|
+
list.append(el('div', { class: 'dd-divider', role: 'presentation' }));
|
|
79
|
+
for (const action of actions) {
|
|
80
|
+
const node = el('div', {
|
|
81
|
+
class: 'dd-option dd-action',
|
|
82
|
+
role: 'option',
|
|
83
|
+
'aria-selected': 'false',
|
|
84
|
+
tabindex: -1,
|
|
85
|
+
onclick: () => {
|
|
86
|
+
close();
|
|
87
|
+
action.onSelect();
|
|
88
|
+
},
|
|
89
|
+
}, el('span', { class: 'dd-text' }, el('span', { class: 'dd-label', text: action.label })));
|
|
90
|
+
list.append(node);
|
|
91
|
+
focusables.push({ node, run: () => { close(); action.onSelect(); }, text: action.label });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function choose(next) {
|
|
97
|
+
close();
|
|
98
|
+
if (next === value) return;
|
|
99
|
+
|
|
100
|
+
/* The control shows what was chosen.
|
|
101
|
+
|
|
102
|
+
It used to only announce the choice and leave the button reading the
|
|
103
|
+
old label — which was invisible for years because every caller rebuilt
|
|
104
|
+
its whole surface in response, so a stale button never survived to be
|
|
105
|
+
seen. The page builder does not rebuild: it writes the value onto a
|
|
106
|
+
draft and stays put, and there the dropdown simply refused to change.
|
|
107
|
+
|
|
108
|
+
A control that does not reflect its own click is not a control. If the
|
|
109
|
+
caller disagrees — a connection that failed to open, say — it redraws
|
|
110
|
+
and says so, which is the same correction it was already making. */
|
|
111
|
+
value = next;
|
|
112
|
+
const chosen = items.find((i) => i.value === value);
|
|
113
|
+
button.querySelector('.dd-value').textContent = chosen?.label ?? '—';
|
|
114
|
+
button.title = chosen?.detail ?? chosen?.label ?? '';
|
|
115
|
+
onChange(next);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function setActive(index) {
|
|
119
|
+
if (!focusables.length) return;
|
|
120
|
+
// Wraps, because a list that stops at the end makes you reverse direction
|
|
121
|
+
// to reach the item one step past it.
|
|
122
|
+
activeIndex = (index + focusables.length) % focusables.length;
|
|
123
|
+
focusables.forEach(({ node }, i) => node.classList.toggle('active', i === activeIndex));
|
|
124
|
+
focusables[activeIndex].node.scrollIntoView({ block: 'nearest' });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function open() {
|
|
128
|
+
build();
|
|
129
|
+
list.hidden = false;
|
|
130
|
+
button.setAttribute('aria-expanded', 'true');
|
|
131
|
+
position();
|
|
132
|
+
const current = items.findIndex((i) => i.value === value);
|
|
133
|
+
setActive(current >= 0 ? current : 0);
|
|
134
|
+
// Listeners are added on open and removed on close, so a page with a
|
|
135
|
+
// dozen of these does not accumulate a dozen document handlers.
|
|
136
|
+
document.addEventListener('pointerdown', onOutside, true);
|
|
137
|
+
document.addEventListener('keydown', onKey, true);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function close() {
|
|
141
|
+
if (list.hidden) return;
|
|
142
|
+
list.hidden = true;
|
|
143
|
+
button.setAttribute('aria-expanded', 'false');
|
|
144
|
+
activeIndex = -1;
|
|
145
|
+
document.removeEventListener('pointerdown', onOutside, true);
|
|
146
|
+
document.removeEventListener('keydown', onKey, true);
|
|
147
|
+
window.removeEventListener('scroll', close, true);
|
|
148
|
+
window.removeEventListener('resize', close);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Place the open list in the viewport rather than in the flow.
|
|
153
|
+
*
|
|
154
|
+
* An absolutely positioned menu is clipped by any ancestor that scrolls,
|
|
155
|
+
* and the panels this control lives in all scroll — the composer's sidebar
|
|
156
|
+
* clips its overflow precisely so its sections can scroll independently, so
|
|
157
|
+
* a menu opening inside one was being cut in half.
|
|
158
|
+
*
|
|
159
|
+
* Fixed positioning escapes every clip, at the cost of not moving with the
|
|
160
|
+
* page, so the list closes on scroll rather than drifting away from the
|
|
161
|
+
* button that owns it.
|
|
162
|
+
*/
|
|
163
|
+
function position() {
|
|
164
|
+
const box = button.getBoundingClientRect();
|
|
165
|
+
/* Sized *before* it is measured, which is the whole subtlety here.
|
|
166
|
+
|
|
167
|
+
The stylesheet gives the list `min-width: 100%` as a sensible default,
|
|
168
|
+
and a `position: fixed` element resolves percentages against the
|
|
169
|
+
viewport rather than against the button it belongs to — so an unsized
|
|
170
|
+
list measures as nearly the width of the screen. Measuring first and
|
|
171
|
+
correcting afterwards meant `width` was that screen-wide figure, the
|
|
172
|
+
clamp below decided the menu could not fit anywhere, and it was pinned
|
|
173
|
+
to the left edge of the window — yards from the control that opened it.
|
|
174
|
+
Visible on any dropdown in a narrow container; the driver picker inside
|
|
175
|
+
the connection dialog is where it was finally obvious. */
|
|
176
|
+
list.style.position = 'fixed';
|
|
177
|
+
list.style.minWidth = `${box.width}px`;
|
|
178
|
+
const width = Math.max(list.offsetWidth, box.width);
|
|
179
|
+
const height = list.offsetHeight;
|
|
180
|
+
// Flipped above the button when there is no room below, and pulled back
|
|
181
|
+
// from the right edge when the menu is wider than the space beside it.
|
|
182
|
+
const below = window.innerHeight - box.bottom;
|
|
183
|
+
list.style.top = below < height + 12 && box.top > height + 12
|
|
184
|
+
? `${box.top - height - 4}px`
|
|
185
|
+
: `${box.bottom + 4}px`;
|
|
186
|
+
list.style.left = `${Math.max(8, Math.min(box.left, window.innerWidth - width - 12))}px`;
|
|
187
|
+
window.addEventListener('scroll', close, true);
|
|
188
|
+
window.addEventListener('resize', close);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function onOutside(e) {
|
|
192
|
+
if (!root.contains(e.target)) close();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
let typed = '';
|
|
196
|
+
let typedAt = 0;
|
|
197
|
+
|
|
198
|
+
function onKey(e) {
|
|
199
|
+
if (list.hidden) return;
|
|
200
|
+
switch (e.key) {
|
|
201
|
+
case 'Escape':
|
|
202
|
+
e.preventDefault();
|
|
203
|
+
close();
|
|
204
|
+
button.focus();
|
|
205
|
+
return;
|
|
206
|
+
case 'ArrowDown':
|
|
207
|
+
e.preventDefault();
|
|
208
|
+
setActive(activeIndex + 1);
|
|
209
|
+
return;
|
|
210
|
+
case 'ArrowUp':
|
|
211
|
+
e.preventDefault();
|
|
212
|
+
setActive(activeIndex - 1);
|
|
213
|
+
return;
|
|
214
|
+
case 'Home':
|
|
215
|
+
e.preventDefault();
|
|
216
|
+
setActive(0);
|
|
217
|
+
return;
|
|
218
|
+
case 'End':
|
|
219
|
+
e.preventDefault();
|
|
220
|
+
setActive(focusables.length - 1);
|
|
221
|
+
return;
|
|
222
|
+
case 'Enter':
|
|
223
|
+
case ' ':
|
|
224
|
+
e.preventDefault();
|
|
225
|
+
focusables[activeIndex]?.run();
|
|
226
|
+
return;
|
|
227
|
+
case 'Tab':
|
|
228
|
+
// Tab means "leave", not "choose". Closing without selecting matches
|
|
229
|
+
// what a native select does.
|
|
230
|
+
close();
|
|
231
|
+
return;
|
|
232
|
+
default:
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
if (e.key.length === 1 && !e.metaKey && !e.ctrlKey && !e.altKey) {
|
|
236
|
+
// Type-ahead. The one-second window is what makes "de" jump to "demo"
|
|
237
|
+
// rather than treating d and e as two separate jumps.
|
|
238
|
+
const now = Date.now();
|
|
239
|
+
typed = now - typedAt > 1000 ? e.key : typed + e.key;
|
|
240
|
+
typedAt = now;
|
|
241
|
+
const hit = focusables.findIndex((f) => f.text.toLowerCase().startsWith(typed.toLowerCase()));
|
|
242
|
+
if (hit >= 0) setActive(hit);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
button.addEventListener('click', () => (list.hidden ? open() : close()));
|
|
247
|
+
button.addEventListener('keydown', (e) => {
|
|
248
|
+
if (list.hidden && (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ')) {
|
|
249
|
+
e.preventDefault();
|
|
250
|
+
open();
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Set it from outside, without calling back.
|
|
256
|
+
*
|
|
257
|
+
* `connmanager` has always written `driver.value = parts.driver` when a
|
|
258
|
+
* pasted URL changes the driver, and until now that set an expando property
|
|
259
|
+
* on a `<div>` and did nothing at all. Pasting `postgres@127.0.0.1/db` —
|
|
260
|
+
* which is a *file path*, having no `://` — switched the form to SQLite and
|
|
261
|
+
* left the control still reading "postgres", so the dialog contradicted
|
|
262
|
+
* itself and the error came back as "unable to open database file".
|
|
263
|
+
*
|
|
264
|
+
* No `onChange`: the caller assigning this is the one making the decision,
|
|
265
|
+
* and calling it back would loop through whatever handler it set.
|
|
266
|
+
*/
|
|
267
|
+
Object.defineProperty(root, 'value', {
|
|
268
|
+
get: () => value,
|
|
269
|
+
set: (next) => {
|
|
270
|
+
if (next === value) return;
|
|
271
|
+
value = next;
|
|
272
|
+
const chosen = items.find((i) => i.value === value);
|
|
273
|
+
button.querySelector('.dd-value').textContent = chosen?.label ?? '—';
|
|
274
|
+
button.title = chosen?.detail ?? chosen?.label ?? '';
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
return root;
|
|
279
|
+
}
|