@cplieger/web-terminal-ui 3.4.4 → 3.4.5
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 +1 -1
- package/src/features/context-menu.ts +47 -14
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cplieger/web-terminal-ui",
|
|
3
|
-
"version": "3.4.
|
|
3
|
+
"version": "3.4.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Reference touch-first browser UI for @cplieger/web-terminal-engine: a composable kernel + feature modules (tabs, activity monitor, mobile toolbar, context menu, IME, predictive echo, viewport handling) and CSS. One createTerminal() call.",
|
|
6
6
|
"license": "GPL-3.0-or-later",
|
|
@@ -21,13 +21,20 @@
|
|
|
21
21
|
// getSelection() read at a fixed delay races the native selection and caused the
|
|
22
22
|
// reported flakiness (sometimes native, sometimes custom, sometimes both). So we
|
|
23
23
|
// TRACK whether a selection is made during THIS press via a selectionchange
|
|
24
|
-
// listener armed only while a touch is down
|
|
25
|
-
// the pending menu and, if it already opened, close it
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
24
|
+
// listener armed only while a touch is down (the moment one appears we cancel
|
|
25
|
+
// the pending menu and, if it already opened, close it), AND we gate the menu on
|
|
26
|
+
// PRESS POSITION: a long-press that landed on a glyph run belongs to the
|
|
27
|
+
// platform's word-select even when no selection has registered yet (iOS 26's
|
|
28
|
+
// native selection can land well past our 550ms timer, so timing alone misfires
|
|
29
|
+
// over text). The platforms deliver the long-press differently: iPhone fires NO
|
|
30
|
+
// contextmenu (a hold timer detects it); iPadOS DOES fire contextmenu mid-press
|
|
31
|
+
// and WebKit treats preventDefault on it as "cancel every remaining default of
|
|
32
|
+
// this gesture" — including a not-yet-registered selection — so on Apple touch
|
|
33
|
+
// devices the contextmenu handler never preventDefaults (that asymmetry is what
|
|
34
|
+
// once made iPad unable to select at all while iPhone worked); Android also
|
|
35
|
+
// fires contextmenu, and there we DO preventDefault when showing our own menu so
|
|
36
|
+
// Android's native menu doesn't also appear (the "both menus" case). All paths
|
|
37
|
+
// share the same selection + position gates, so the outcome is consistent.
|
|
31
38
|
|
|
32
39
|
import type { TerminalFeature } from "../kernel/types.js";
|
|
33
40
|
import type { ClipboardApi } from "./clipboard.js";
|
|
@@ -116,6 +123,22 @@ export function contextMenu(opts: ContextMenuOptions = {}): TerminalFeature {
|
|
|
116
123
|
}
|
|
117
124
|
};
|
|
118
125
|
|
|
126
|
+
// True when the press landed on rendered text (a glyph run inside the
|
|
127
|
+
// output). The empty-space menu must not open there: a long-press on text
|
|
128
|
+
// belongs to the platform's word-selection, and opening ours first races
|
|
129
|
+
// it (on iOS 26 the native selection can register well after 550ms, so
|
|
130
|
+
// the old no-selection-yet check misfired over text). Hitting the row or
|
|
131
|
+
// container (the blank area beside/below content) still counts as empty
|
|
132
|
+
// space so Paste stays reachable.
|
|
133
|
+
const pressOverText = (x: number, y: number): boolean => {
|
|
134
|
+
const el = document.elementFromPoint(x, y);
|
|
135
|
+
if (!(el instanceof HTMLElement) || !el.closest(".term-output")) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
const run = el.closest("span, a");
|
|
139
|
+
return run !== null && run.textContent.trim().length > 0;
|
|
140
|
+
};
|
|
141
|
+
|
|
119
142
|
// True when text is (or just got) selected, so the OS callout owns Copy and
|
|
120
143
|
// our menu stays out of the way. pressSelectedText catches a native
|
|
121
144
|
// selection the instant it happens (selectionchange), avoiding a race with
|
|
@@ -238,6 +261,20 @@ export function contextMenu(opts: ContextMenuOptions = {}): TerminalFeature {
|
|
|
238
261
|
show(e.clientX, e.clientY, false);
|
|
239
262
|
return;
|
|
240
263
|
}
|
|
264
|
+
if (appleTouch) {
|
|
265
|
+
// NEVER preventDefault a touch contextmenu on iOS/iPadOS: WebKit then
|
|
266
|
+
// cancels every remaining default action of the gesture, including a
|
|
267
|
+
// native word-selection that registers AFTER this event (iPadOS fires
|
|
268
|
+
// contextmenu mid-long-press; on iOS 26 the selection can land well
|
|
269
|
+
// after it). iPhone fires no contextmenu at all, so suppressing here
|
|
270
|
+
// is exactly what made iPad behave differently from iPhone. If our
|
|
271
|
+
// empty-space menu opened first (the hold timer beat a slow native
|
|
272
|
+
// selection), stand down so the platform's selection wins.
|
|
273
|
+
if (menu.classList.contains("visible") && !hasNativeSelection()) {
|
|
274
|
+
hide(false);
|
|
275
|
+
}
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
241
278
|
if (menu.classList.contains("visible")) {
|
|
242
279
|
e.preventDefault(); // the hold timer already opened it; just suppress native
|
|
243
280
|
return;
|
|
@@ -245,12 +282,8 @@ export function contextMenu(opts: ContextMenuOptions = {}): TerminalFeature {
|
|
|
245
282
|
if (hasNativeSelection()) {
|
|
246
283
|
return; // native selection toolbar owns Copy over text
|
|
247
284
|
}
|
|
248
|
-
if (
|
|
249
|
-
//
|
|
250
|
-
// registers, so hasNativeSelection() can still be false here. Defer
|
|
251
|
-
// anyway so the platform's own selection wins instead of our
|
|
252
|
-
// empty-space menu (which would preventDefault and suppress it).
|
|
253
|
-
return;
|
|
285
|
+
if (pressOverText(e.clientX, e.clientY)) {
|
|
286
|
+
return; // a long-press on text belongs to the platform's word-select
|
|
254
287
|
}
|
|
255
288
|
e.preventDefault();
|
|
256
289
|
clearLongPress();
|
|
@@ -322,7 +355,7 @@ export function contextMenu(opts: ContextMenuOptions = {}): TerminalFeature {
|
|
|
322
355
|
longPressOrigin = { x: t.clientX, y: t.clientY };
|
|
323
356
|
longPressTimer = window.setTimeout(() => {
|
|
324
357
|
longPressTimer = 0;
|
|
325
|
-
if (!hasNativeSelection()) {
|
|
358
|
+
if (!hasNativeSelection() && !pressOverText(longPressOrigin.x, longPressOrigin.y)) {
|
|
326
359
|
show(longPressOrigin.x, longPressOrigin.y, true);
|
|
327
360
|
}
|
|
328
361
|
}, LONG_PRESS_MS);
|