@cplieger/web-terminal-ui 2.1.0 → 2.1.2
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/package.json +3 -3
- package/src/composition.ts +19 -4
- package/src/mount.ts +20 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cplieger/web-terminal-ui",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Reference touch-first browser UI for @cplieger/web-terminal-engine: textarea input model, mobile key toolbar, context menu, IME, predictive echo, viewport/keyboard-inset handling, and CSS. One mount() call.",
|
|
6
6
|
"license": "GPL-3.0-or-later",
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
"fast-check": "4.8.0",
|
|
32
32
|
"happy-dom": "20.10.6",
|
|
33
33
|
"html-validate": "11.5.3",
|
|
34
|
-
"knip": "6.
|
|
35
|
-
"prettier": "3.9.
|
|
34
|
+
"knip": "6.23.0",
|
|
35
|
+
"prettier": "3.9.1",
|
|
36
36
|
"stylelint": "17.14.0",
|
|
37
37
|
"stylelint-config-standard": "40.0.0",
|
|
38
38
|
"typescript": "6.0.3",
|
package/src/composition.ts
CHANGED
|
@@ -32,6 +32,7 @@ const { bracketTextForPaste, prepareTextForTerminal } = keyboard;
|
|
|
32
32
|
|
|
33
33
|
let textarea: HTMLTextAreaElement;
|
|
34
34
|
let compositionView: HTMLElement;
|
|
35
|
+
let scrollEl: HTMLElement;
|
|
35
36
|
let getCursorPx: () => { left: number; top: number; cellH: number };
|
|
36
37
|
let send: (bytes: string) => void;
|
|
37
38
|
|
|
@@ -43,11 +44,13 @@ let compositionSuffix = "";
|
|
|
43
44
|
export function init(opts: {
|
|
44
45
|
textarea: HTMLTextAreaElement;
|
|
45
46
|
compositionView: HTMLElement;
|
|
47
|
+
scrollEl: HTMLElement;
|
|
46
48
|
getCursorPx: () => { left: number; top: number; cellH: number };
|
|
47
49
|
send: (bytes: string) => void;
|
|
48
50
|
}): void {
|
|
49
51
|
textarea = opts.textarea;
|
|
50
52
|
compositionView = opts.compositionView;
|
|
53
|
+
scrollEl = opts.scrollEl;
|
|
51
54
|
getCursorPx = opts.getCursorPx;
|
|
52
55
|
send = opts.send;
|
|
53
56
|
|
|
@@ -137,13 +140,25 @@ function onPaste(ev: ClipboardEvent): void {
|
|
|
137
140
|
* the focused textarea is on screen at a sane location. */
|
|
138
141
|
export function positionCompositionView(): void {
|
|
139
142
|
const { left, top, cellH } = getCursorPx();
|
|
143
|
+
// getCursorPx() returns the cursor position in the scroll container's CONTENT
|
|
144
|
+
// coordinate space (measured from the top of all rows, scrollback included).
|
|
145
|
+
// The composition view is position:absolute inside that scroll container, so
|
|
146
|
+
// it scrolls with the content — content coordinates place it at the visible
|
|
147
|
+
// cursor directly.
|
|
140
148
|
compositionView.style.left = `${left}px`;
|
|
141
149
|
compositionView.style.top = `${top}px`;
|
|
142
150
|
compositionView.style.height = `${cellH}px`;
|
|
143
151
|
compositionView.style.lineHeight = `${cellH}px`;
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
|
|
147
|
-
|
|
152
|
+
// The textarea is position:fixed (viewport space) so iOS reliably opens the
|
|
153
|
+
// soft keyboard for it. A fixed element does NOT scroll with the content, so
|
|
154
|
+
// the content-space cursor offset must be converted to viewport space by
|
|
155
|
+
// subtracting the scroll offset. Without this, once a screenful of output has
|
|
156
|
+
// scrolled the cursor's content offset well below the viewport, the fixed
|
|
157
|
+
// textarea lands far off-screen; focusing it makes iOS scroll the page up to
|
|
158
|
+
// pull the focused input into view, pushing the terminal output and prompt
|
|
159
|
+
// behind the keyboard until the user scrolls back. Anchoring the textarea at
|
|
160
|
+
// the VISIBLE cursor keeps it on-screen so iOS has no reason to scroll.
|
|
161
|
+
textarea.style.left = `${left - scrollEl.scrollLeft}px`;
|
|
162
|
+
textarea.style.top = `${top - scrollEl.scrollTop}px`;
|
|
148
163
|
textarea.style.height = `${cellH}px`;
|
|
149
164
|
}
|
package/src/mount.ts
CHANGED
|
@@ -437,6 +437,7 @@ export function mount(root: HTMLElement, opts: MountOptions = {}): TerminalUI {
|
|
|
437
437
|
composition.init({
|
|
438
438
|
textarea: input,
|
|
439
439
|
compositionView: compositionViewEl,
|
|
440
|
+
scrollEl: termWrap,
|
|
440
441
|
getCursorPx: render.getCursorPx,
|
|
441
442
|
send,
|
|
442
443
|
});
|
|
@@ -645,6 +646,25 @@ export function mount(root: HTMLElement, opts: MountOptions = {}): TerminalUI {
|
|
|
645
646
|
},
|
|
646
647
|
{ passive: true },
|
|
647
648
|
);
|
|
649
|
+
// After a touch tap, iOS/WebKit dispatches a synthetic `mousedown` (following
|
|
650
|
+
// the `pointerup` above). Its default action moves focus to the nearest
|
|
651
|
+
// focusable ancestor of the tap point — but the input-model contract keeps
|
|
652
|
+
// `.term-output` and `.term` deliberately non-focusable (display-only) and the
|
|
653
|
+
// hidden textarea is `pointer-events: none`, so the tap never lands on a
|
|
654
|
+
// focusable node. With nothing focusable under the tap, that default blurs the
|
|
655
|
+
// textarea we just focused in `pointerup`, so iOS immediately dismisses the
|
|
656
|
+
// soft keyboard the tap had only just opened — the "keyboard flashes open then
|
|
657
|
+
// instantly closes" symptom on touch. Cancelling the synthetic mousedown's
|
|
658
|
+
// default keeps focus on the textarea, so the keyboard stays up. This is the
|
|
659
|
+
// xterm.js focus-preservation pattern, scoped to touch: a real mouse mousedown
|
|
660
|
+
// must keep its default so desktop drag-to-select text still works, and iOS
|
|
661
|
+
// text selection is long-press-driven (not this tap mousedown), so it is
|
|
662
|
+
// unaffected. Not passive — preventDefault must take effect.
|
|
663
|
+
termWrap.addEventListener("mousedown", (e) => {
|
|
664
|
+
if (lastPointerType === "touch") {
|
|
665
|
+
e.preventDefault();
|
|
666
|
+
}
|
|
667
|
+
});
|
|
648
668
|
termWrap.addEventListener("click", (e) => {
|
|
649
669
|
// Terminal links: URLs rendered by the engine are wrapped in
|
|
650
670
|
// <a class="term-link" target="_blank" rel="noopener noreferrer">. The
|