@cplieger/web-terminal-ui 8.2.1 → 8.3.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 +65 -612
- package/css/01-scope.css +3 -9
- package/package.json +6 -6
- package/src/features/connection-banner.ts +2 -2
- package/src/features/tabs/index.ts +12 -48
- package/src/features/tabs/model.ts +2 -3
- package/src/index.ts +1 -0
- package/src/kernel/keyboard-presence.ts +112 -0
- package/src/kernel/loading-status.ts +4 -4
- package/src/kernel/pane.ts +92 -30
- package/src/kernel/shell.ts +45 -2
- package/src/kernel/types.ts +9 -0
- package/src/viewport.ts +18 -7
package/src/kernel/shell.ts
CHANGED
|
@@ -6,11 +6,11 @@ import {
|
|
|
6
6
|
import {
|
|
7
7
|
applyTheme,
|
|
8
8
|
buildPane,
|
|
9
|
-
hasFinePointer,
|
|
10
9
|
type PaneKernel,
|
|
11
10
|
type PaneServices,
|
|
12
11
|
type StoreRegistry,
|
|
13
12
|
} from "./pane.js";
|
|
13
|
+
import { createKeyboardPresence } from "./keyboard-presence.js";
|
|
14
14
|
import { createStatusShare } from "./status-share.js";
|
|
15
15
|
import { browserNotifierEnv, createNotifier } from "./notify.js";
|
|
16
16
|
import { attachLoadingStatus, DEFAULT_LOADING_MESSAGES } from "./loading-status.js";
|
|
@@ -269,17 +269,54 @@ function createShellInto(
|
|
|
269
269
|
});
|
|
270
270
|
}
|
|
271
271
|
const paneOpts: CreateTerminalOptions = splitEnabled ? { ...opts, layout: "container" } : opts;
|
|
272
|
+
// Called only from a keydown, so keyboardInsets below exists by then.
|
|
273
|
+
const keyboardPresence = createKeyboardPresence({
|
|
274
|
+
win,
|
|
275
|
+
softKeyboardHeight: () => keyboardInsets.softKeyboardHeight(),
|
|
276
|
+
});
|
|
277
|
+
const paneInputFocused = (): boolean => {
|
|
278
|
+
const active = doc.activeElement;
|
|
279
|
+
return (
|
|
280
|
+
active instanceof win.HTMLTextAreaElement &&
|
|
281
|
+
active.classList.contains("term-input") &&
|
|
282
|
+
shellRoot.contains(active)
|
|
283
|
+
);
|
|
284
|
+
};
|
|
272
285
|
// The visual viewport is the document's, so its geometry is read once and
|
|
273
286
|
// published on the outermost root, which the shell's chrome and every pane
|
|
274
287
|
// inherit; published on a pane root it would never reach the chrome beside
|
|
275
288
|
// the panes.
|
|
276
289
|
const keyboardInsets = createKeyboardInsets({
|
|
277
290
|
root: shellRoot,
|
|
278
|
-
suppressKeyboardInset: () =>
|
|
291
|
+
suppressKeyboardInset: () => keyboardPresence.insetSuppressed(),
|
|
292
|
+
onSoftKeyboard(heightPx) {
|
|
293
|
+
keyboardPresence.noteSoftKeyboard(heightPx, paneInputFocused());
|
|
294
|
+
},
|
|
279
295
|
});
|
|
280
296
|
held.push(() => {
|
|
281
297
|
keyboardInsets.teardown();
|
|
282
298
|
});
|
|
299
|
+
const docListeners = new AbortController();
|
|
300
|
+
held.push(() => {
|
|
301
|
+
docListeners.abort();
|
|
302
|
+
});
|
|
303
|
+
doc.addEventListener(
|
|
304
|
+
"keydown",
|
|
305
|
+
(ev) => {
|
|
306
|
+
keyboardPresence.noteKeydown(ev);
|
|
307
|
+
},
|
|
308
|
+
{ capture: true, signal: docListeners.signal },
|
|
309
|
+
);
|
|
310
|
+
// A press in the host page leaves the keys with the host, so the panes' body
|
|
311
|
+
// keydown path arms only after a press inside the terminal.
|
|
312
|
+
let pressedInsideShell = false;
|
|
313
|
+
doc.addEventListener(
|
|
314
|
+
"pointerdown",
|
|
315
|
+
(ev) => {
|
|
316
|
+
pressedInsideShell = ev.target instanceof win.Node && shellRoot.contains(ev.target);
|
|
317
|
+
},
|
|
318
|
+
{ capture: true, passive: true, signal: docListeners.signal },
|
|
319
|
+
);
|
|
283
320
|
|
|
284
321
|
const loadingStatus = attachLoadingStatus(opts.loading, {
|
|
285
322
|
...DEFAULT_LOADING_MESSAGES,
|
|
@@ -785,6 +822,10 @@ function createShellInto(
|
|
|
785
822
|
: disabledSplit();
|
|
786
823
|
const shellContext: ShellContext = {
|
|
787
824
|
root: shellRoot,
|
|
825
|
+
keyboard: {
|
|
826
|
+
hardwareSeen: () => keyboardPresence.hardwareSeen(),
|
|
827
|
+
likely: () => keyboardPresence.likely(),
|
|
828
|
+
},
|
|
788
829
|
pane: (side) => paneAt(side) ?? null,
|
|
789
830
|
panes: builtPanes,
|
|
790
831
|
selected: () => selectedSide,
|
|
@@ -841,6 +882,8 @@ function createShellInto(
|
|
|
841
882
|
scrollbackLines,
|
|
842
883
|
narrowProbe: () => isNarrow(shellRoot.clientWidth, slot.root.clientHeight),
|
|
843
884
|
keyboardInsets,
|
|
885
|
+
keyboard: shellContext.keyboard,
|
|
886
|
+
pressedInsideShell: () => pressedInsideShell,
|
|
844
887
|
titleBase(text) {
|
|
845
888
|
slot.baseTitle = text;
|
|
846
889
|
paintTitle();
|
package/src/kernel/types.ts
CHANGED
|
@@ -280,11 +280,20 @@ export interface Notifier {
|
|
|
280
280
|
forget(id: string): void;
|
|
281
281
|
}
|
|
282
282
|
|
|
283
|
+
/** Whether the page has a hardware keyboard, as far as its key events tell. */
|
|
284
|
+
export interface KeyboardPresence {
|
|
285
|
+
/** A key no on-screen keyboard produces is the latest keyboard evidence seen. */
|
|
286
|
+
hardwareSeen(): boolean;
|
|
287
|
+
/** `hardwareSeen()`, or the primary pointer is fine. */
|
|
288
|
+
likely(): boolean;
|
|
289
|
+
}
|
|
290
|
+
|
|
283
291
|
/** The shell a feature reaches through `ctx.shell`: the pane kernels and the
|
|
284
292
|
* facts shared between them. */
|
|
285
293
|
export interface ShellContext {
|
|
286
294
|
/** The shell root; the pane root in the direct-root topology. */
|
|
287
295
|
readonly root: HTMLElement;
|
|
296
|
+
readonly keyboard: KeyboardPresence;
|
|
288
297
|
/** Null while that side has no built kernel. */
|
|
289
298
|
pane(side: PaneSide): PaneHandle | null;
|
|
290
299
|
/** Built panes, left first. */
|
package/src/viewport.ts
CHANGED
|
@@ -27,12 +27,17 @@ export interface KeyboardInsetsOptions {
|
|
|
27
27
|
/** Ignore the visualViewport keyboard geometry (a hardware-keyboard device
|
|
28
28
|
* has no soft keyboard to accommodate). */
|
|
29
29
|
suppressKeyboardInset?: () => boolean;
|
|
30
|
+
/** Called with `softKeyboardHeight()` at every reading, before
|
|
31
|
+
* `suppressKeyboardInset` is consulted for it. */
|
|
32
|
+
onSoftKeyboard?: (heightPx: number) => void;
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
/** The document's one reading of its visual viewport. */
|
|
33
36
|
export interface KeyboardInsets {
|
|
34
37
|
/** The latest geometry; zero where the window has no visual viewport. */
|
|
35
38
|
current(): KeyboardGeometry;
|
|
39
|
+
/** The visual-viewport keyboard inset now in CSS px, never suppressed. */
|
|
40
|
+
softKeyboardHeight(): number;
|
|
36
41
|
/** Call `fn` with the geometry on every visual-viewport resize and scroll, on
|
|
37
42
|
* every window focus and bfcache restore, and on every `refresh()`; returns
|
|
38
43
|
* the release. */
|
|
@@ -110,16 +115,21 @@ export function createKeyboardInsets(opts: KeyboardInsetsOptions): KeyboardInset
|
|
|
110
115
|
root.style.removeProperty("--vv-top");
|
|
111
116
|
}
|
|
112
117
|
|
|
118
|
+
function readVisual(): KeyboardGeometry {
|
|
119
|
+
return vv
|
|
120
|
+
? {
|
|
121
|
+
top: Math.max(0, Math.round(vv.offsetTop)),
|
|
122
|
+
bottom: Math.max(0, Math.round(win.innerHeight - vv.offsetTop - vv.height)),
|
|
123
|
+
}
|
|
124
|
+
: { top: 0, bottom: 0 };
|
|
125
|
+
}
|
|
126
|
+
|
|
113
127
|
function update(): void {
|
|
128
|
+
const raw = readVisual();
|
|
129
|
+
opts.onSoftKeyboard?.(raw.bottom);
|
|
114
130
|
// iPadOS has been seen to report a keyboard-sized shrink with no keyboard
|
|
115
131
|
// shown and pin it, which is what suppressKeyboardInset defends against.
|
|
116
|
-
geometry =
|
|
117
|
-
!vv || suppressKeyboardInset()
|
|
118
|
-
? { top: 0, bottom: 0 }
|
|
119
|
-
: {
|
|
120
|
-
top: Math.max(0, Math.round(vv.offsetTop)),
|
|
121
|
-
bottom: Math.max(0, Math.round(win.innerHeight - vv.offsetTop - vv.height)),
|
|
122
|
-
};
|
|
132
|
+
geometry = suppressKeyboardInset() ? { top: 0, bottom: 0 } : raw;
|
|
123
133
|
root.style.setProperty("--kb-inset", `${geometry.bottom}px`);
|
|
124
134
|
root.style.setProperty("--vv-top", `${geometry.top}px`);
|
|
125
135
|
for (const fn of [...subscribers]) {
|
|
@@ -144,6 +154,7 @@ export function createKeyboardInsets(opts: KeyboardInsetsOptions): KeyboardInset
|
|
|
144
154
|
|
|
145
155
|
return {
|
|
146
156
|
current: () => geometry,
|
|
157
|
+
softKeyboardHeight: () => readVisual().bottom,
|
|
147
158
|
onChange(fn) {
|
|
148
159
|
subscribers.add(fn);
|
|
149
160
|
return () => {
|