@neuroplastio/xterm-addon-hotty 0.1.0-next.1 → 0.1.0-next.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/README.md CHANGED
@@ -171,11 +171,23 @@ runs programs for whoever connects.
171
171
  lose the capture. The browser clicks the root on a captured release, so
172
172
  the drag reports the click itself when it ends where it began. The host
173
173
  stylesheet makes the elements that opt in unselectable, important in its
174
- layer, whatever the document's CSS. A touch never drags.
174
+ layer, whatever the document's CSS.
175
+
176
+ A touch drags such an element only where its `touch-action` allows no
177
+ pan along the touch's first move past 8 px (the larger delta; a tie is a
178
+ pan). The value is the element's own, met with its ancestors' up to the
179
+ nearest one that scrolls, both included, as Pointer Events have it; the
180
+ host's own `touch-action` on the root (below) is not the document's. The
181
+ touch then reports `press` and `dragstart` at the cell where it began,
182
+ and drags from there as a mouse does; the lift reports `dragend`, and the
183
+ click where it began. A second finger ends it with `dragend` and no
184
+ target, and nothing more is heard until every finger lifts. A long press,
185
+ or Alt held at the touch, never drags. Any other touch pans as below.
175
186
  - **Presses** (SPEC §5.2, §9: `p=1` on `a=place`). Every primary press of a
176
187
  mouse or a pen in the window reports `press` on its pointerdown, before
177
188
  the drag and the focus it causes; a tap reports it on the mousedown the
178
- browser makes for it. A press on a hyperlink is the terminal's.
189
+ browser makes for it, and a touch that drags when it becomes a drag. A
190
+ touch that pans presses nothing. A press on a hyperlink is the terminal's.
179
191
  - **Fit** (SPEC §5.2, §9: `f=1` on `a=place`). The program hears `fit`
180
192
  with the rows the document needs at the placement's width whenever they
181
193
  differ from the rows it heard last, starting from the placement's own;
@@ -232,8 +244,8 @@ runs programs for whoever connects.
232
244
  - It shows no scrollbars, and pans nothing on a touch
233
245
  (`touch-action: none`). Any scroll offset the browser sets goes back to
234
246
  zero, except a text field's own text.
235
- - A wheel over a surface goes to the terminal, and so does a touch drag,
236
- as wheel events at the finger. After the finger lifts, the drag keeps
247
+ - A wheel over a surface goes to the terminal, and so does a touch that
248
+ pans, as wheel events at the finger. After the finger lifts, the drag keeps
237
249
  going and slows down.
238
250
  - Taps and long presses stay the surface's.
239
251
  - On the cells, the addon handles touch too (the `touch` option, on by
@@ -280,9 +292,12 @@ runs programs for whoever connects.
280
292
  the pointer that can move that way can (the browser scrolls it), else
281
293
  on to the terminal, as over the cells, unless `overscroll-behavior`
282
294
  stops it. A gesture begun over the cells stays the terminal's when a
283
- surface comes under the pointer. A touch drag decides the same way once
284
- it has a direction: the browser pans the document, or the drag goes to
285
- the terminal as before.
295
+ surface comes under the pointer. A touch that pans decides the same way
296
+ once it has a direction: the browser pans the document, or the touch
297
+ goes to the terminal as before. Where the document's `touch-action`
298
+ would stop the browser's pan (a `none` on a box that doesn't drag), the
299
+ addon pans the box itself, so that only a drag element takes a touch
300
+ from scrolling (SPEC §9.1).
286
301
  - **Keys.** While the surface has the keyboard, the keys a browser
287
302
  scrolls with that the focused element neither uses nor gives the
288
303
  program (SPEC §10.2) scroll the innermost box, from the focused
@@ -67,14 +67,13 @@ function scrollCss(axes, pageScrolls) {
67
67
  if (!axes) {
68
68
  return [
69
69
  "@layer hotty-host {",
70
- `:root { overflow: hidden; touch-action: ${pageScrolls ? "manipulation" : "none"}; }`,
70
+ `:root { overflow: hidden; touch-action: ${rootTouchAction(axes, pageScrolls)}; }`,
71
71
  "* { scrollbar-width: none !important; }",
72
72
  "::-webkit-scrollbar { display: none !important; }",
73
73
  "}"
74
74
  ].join("\n");
75
75
  }
76
- const pan = pageScrolls ? "manipulation" : axes === 3 ? "pan-x pan-y" : axes === 1 ? "pan-y" : "pan-x";
77
- const lines = ["@layer hotty-host {", `:root { touch-action: ${pan}; }`];
76
+ const lines = ["@layer hotty-host {", `:root { touch-action: ${rootTouchAction(axes, pageScrolls)}; }`];
78
77
  if (axes !== 3) {
79
78
  const clipped = axes === 1 ? "overflow-x" : "overflow-y";
80
79
  lines.push(`:root, :root:not([${UNCLIPPED}]) [${CLIP}] { ${clipped}: hidden !important; }`);
@@ -82,6 +81,10 @@ function scrollCss(axes, pageScrolls) {
82
81
  lines.push("}");
83
82
  return lines.join("\n");
84
83
  }
84
+ function rootTouchAction(axes, pageScrolls) {
85
+ if (pageScrolls) return "manipulation";
86
+ return axes === 3 ? "pan-x pan-y" : axes === 1 ? "pan-y" : axes === 2 ? "pan-x" : "none";
87
+ }
85
88
  function fontStack(family) {
86
89
  const f = family.trim();
87
90
  if (!f) return "monospace";
@@ -515,9 +518,27 @@ var Store = class {
515
518
 
516
519
  // src/touch.ts
517
520
  var DRAG_SLOP = 8;
521
+ var LONG_PRESS_MS = 500;
518
522
  var FLING_DECAY = 0.996;
519
523
  var REST_MS = 80;
524
+ function touchPans(value) {
525
+ const v = value.trim();
526
+ if (!v || v === "auto" || v === "manipulation") return [true, true];
527
+ const words = v.split(/\s+/);
528
+ return [words.some((w) => /^pan-(x|left|right)$/.test(w)), words.some((w) => /^pan-(y|up|down)$/.test(w))];
529
+ }
530
+ function panBlocked(pans, dx, dy) {
531
+ const ax = Math.abs(dx), ay = Math.abs(dy);
532
+ return ax > ay ? !pans.x : ay > ax ? !pans.y : false;
533
+ }
520
534
  var Touch = class {
535
+ touch = null;
536
+ fling = 0;
537
+ off = [];
538
+ win;
539
+ host;
540
+ exclusive;
541
+ engage;
521
542
  /**
522
543
  * `exclusive`: every touch here is this Touch's (the cells: xterm.js's own
523
544
  * touch handling, which listens on the document, never sees it). Otherwise
@@ -537,19 +558,12 @@ var Touch = class {
537
558
  };
538
559
  on("touchstart", (e) => this.start(e), true);
539
560
  on("touchmove", (e) => this.move(e), false);
540
- on("touchend", (e) => this.end(e), !exclusive);
561
+ on("touchend", (e) => this.end(e), false);
541
562
  on("touchcancel", (e) => {
542
563
  this.keep(e);
543
- this.touch = null;
564
+ this.drop();
544
565
  }, true);
545
566
  }
546
- win;
547
- host;
548
- exclusive;
549
- engage;
550
- touch = null;
551
- fling = 0;
552
- off = [];
553
567
  dispose() {
554
568
  this.stopFling();
555
569
  for (const f of this.off) f();
@@ -557,32 +571,62 @@ var Touch = class {
557
571
  keep(e) {
558
572
  if (this.exclusive) e.stopPropagation();
559
573
  }
574
+ /** The touch under way ends without its lift: a drag the host holds
575
+ * hears it. */
576
+ drop() {
577
+ const s = this.touch;
578
+ this.touch = null;
579
+ if (s?.held) this.host.dragCancel?.();
580
+ }
560
581
  start(e) {
561
582
  this.keep(e);
562
583
  this.stopFling();
563
584
  const t = e.touches.length === 1 ? e.touches[0] : void 0;
564
585
  if (!t || this.engage && !this.engage(e)) {
565
- this.touch = null;
586
+ this.drop();
566
587
  return;
567
588
  }
568
589
  const [x, y] = this.host.toPage(t.clientX, t.clientY);
569
- this.touch = { id: t.identifier, x0: x, y0: y, x, y, t: e.timeStamp, t0: e.timeStamp, vx: 0, vy: 0, dragging: false };
590
+ const begin = {
591
+ target: e.target,
592
+ x: t.clientX,
593
+ y: t.clientY,
594
+ time: e.timeStamp,
595
+ shiftKey: e.shiftKey,
596
+ ctrlKey: e.ctrlKey,
597
+ altKey: e.altKey,
598
+ metaKey: e.metaKey
599
+ };
600
+ this.touch = { id: t.identifier, x0: x, y0: y, x, y, t: e.timeStamp, t0: e.timeStamp, vx: 0, vy: 0, dragging: false, held: false, begin };
570
601
  }
571
602
  move(e) {
572
603
  this.keep(e);
573
604
  const s = this.touch;
574
605
  if (!s) return;
575
606
  if (e.touches.length !== 1) {
576
- this.touch = null;
607
+ this.drop();
577
608
  return;
578
609
  }
579
610
  const t = [...e.changedTouches].find((c) => c.identifier === s.id);
580
611
  if (!t) return;
612
+ if (s.held) {
613
+ e.preventDefault();
614
+ this.host.dragMove?.(t.clientX, t.clientY, e);
615
+ return;
616
+ }
581
617
  const [x, y] = this.host.toPage(t.clientX, t.clientY);
582
618
  if (!s.dragging && Math.hypot(x - s.x0, y - s.y0) < DRAG_SLOP) return;
583
- if (!s.dragging && this.host.claim && !this.host.claim(s.x0 - x, s.y0 - y, e)) {
584
- this.touch = null;
585
- return;
619
+ if (!s.dragging && this.host.claim) {
620
+ const claimed = this.host.claim(s.x0 - x, s.y0 - y, e, s.begin);
621
+ if (!claimed) {
622
+ this.touch = null;
623
+ return;
624
+ }
625
+ if (claimed === "drag") {
626
+ s.held = true;
627
+ e.preventDefault();
628
+ return;
629
+ }
586
630
  }
587
631
  s.dragging = true;
588
632
  e.preventDefault();
@@ -599,13 +643,19 @@ var Touch = class {
599
643
  end(e) {
600
644
  this.keep(e);
601
645
  const s = this.touch;
602
- if (!s || ![...e.changedTouches].some((c) => c.identifier === s.id)) return;
646
+ const t = s && [...e.changedTouches].find((c) => c.identifier === s.id);
647
+ if (!s || !t) return;
603
648
  this.touch = null;
649
+ if (s.held) {
650
+ e.preventDefault();
651
+ this.host.dragEnd?.(t.clientX, t.clientY, e);
652
+ return;
653
+ }
604
654
  if (s.dragging) {
605
655
  if (e.timeStamp - s.t < REST_MS) this.startFling(-s.vx, -s.vy, s.x, s.y);
606
656
  return;
607
657
  }
608
- if (this.host.tap && e.timeStamp - s.t0 < 500) {
658
+ if (this.host.tap && e.timeStamp - s.t0 < LONG_PRESS_MS) {
609
659
  e.preventDefault();
610
660
  this.host.tap(s.x, s.y);
611
661
  }
@@ -1371,8 +1421,13 @@ var Surface = class {
1371
1421
  /** The axes the document asked to scroll along (`scroll`, SPEC §5.1): 1
1372
1422
  * vertically, 2 horizontally, 3 both; 0, nothing scrolls. */
1373
1423
  axes = 0;
1374
- /** Where the touch drag under way goes, once it has a direction. */
1424
+ /** Where the touch drag under way goes, once it has a direction: "self",
1425
+ * the document, but scrolled by the surface, since the document's
1426
+ * `touch-action` would stop the browser's own pan (SPEC §9.1: it changes
1427
+ * nothing there). */
1375
1428
  touchRoute = "terminal";
1429
+ /** The box a touch drag scrolls itself ("self"). */
1430
+ touchBox = null;
1376
1431
  /** The box the wheel gesture under way scrolls, if it went to the
1377
1432
  * document: the gesture moves no other (`scrollWheel`). */
1378
1433
  wheelBox = null;
@@ -1727,6 +1782,11 @@ var Surface = class {
1727
1782
  setTimeout(() => this.auxPress = false, 0);
1728
1783
  return;
1729
1784
  }
1785
+ this.pressAt(e);
1786
+ }
1787
+ /** What a click (SPEC §10.1) at `e`'s target does to the keyboard: a
1788
+ * press of the primary button, a tap, or a touch that becomes a drag. */
1789
+ pressAt(e) {
1730
1790
  const el = this.detachedState ? null : focusTargetAt(e, this.hyper);
1731
1791
  if (el) {
1732
1792
  this.pressed = el;
@@ -2292,13 +2352,11 @@ var Surface = class {
2292
2352
  /** The surface's cell under the pointer, from its top left (the frame's
2293
2353
  * origin), counting on past its edges. */
2294
2354
  cellOf(e) {
2295
- return [Math.floor(e.clientX / this.cellW), Math.floor(e.clientY / this.cellH)];
2355
+ return this.cellAt(e.clientX, e.clientY);
2296
2356
  }
2297
2357
  /** The element under the pointer, if it is in the window (SPEC §5.2). */
2298
2358
  elementAt(e, c, r) {
2299
- const w = this.win;
2300
- if (c < w.x || c >= w.x + w.w || r < w.y || r >= w.y + w.h) return null;
2301
- return this.doc.elementFromPoint(e.clientX, e.clientY);
2359
+ return this.elementAtPoint(e.clientX, e.clientY, c, r);
2302
2360
  }
2303
2361
  /** A drag's target: the nearest element with an id and `drag` in its
2304
2362
  * `data-on`, from `el` outward; "" where there is none. */
@@ -2309,6 +2367,139 @@ var Surface = class {
2309
2367
  }
2310
2368
  return "";
2311
2369
  }
2370
+ // --- Touch drags (SPEC §9.1) ----------------------------------------------
2371
+ /**
2372
+ * Where a touch that has just gone past the slop, scrolling by dx, dy so
2373
+ * far, goes. A drag, where the touched element opts in and the
2374
+ * touch-action that counts for it allows no pan along the touch's first
2375
+ * move (the larger delta; a tie pans). Otherwise a pan, as §5.3 and §9
2376
+ * have it, whatever touch-action says: where the browser would pan the
2377
+ * document but the document's touch-action stops it, the surface scrolls
2378
+ * the box itself.
2379
+ */
2380
+ claimTouch(dx, dy, e, begin) {
2381
+ const el = begin.target && isElement(begin.target) ? begin.target : null;
2382
+ const pans = this.allowedPans(el);
2383
+ if (panBlocked(pans, dx, dy) && this.touchDragStart(e, begin)) return "drag";
2384
+ if (!this.axes) {
2385
+ this.touchRoute = "terminal";
2386
+ return true;
2387
+ }
2388
+ const to = this.gestureTarget(el, dx, dy);
2389
+ if (typeof to === "string") {
2390
+ this.touchRoute = to;
2391
+ return true;
2392
+ }
2393
+ if (pans.x && pans.y) {
2394
+ this.touchRoute = "doc";
2395
+ return false;
2396
+ }
2397
+ this.touchRoute = "self";
2398
+ this.touchBox = to;
2399
+ return true;
2400
+ }
2401
+ /** Whether the touch-action that counts at `target` allows every pan. */
2402
+ pansFreely(target) {
2403
+ const p = this.allowedPans(target && isElement(target) ? target : null);
2404
+ return p.x && p.y;
2405
+ }
2406
+ /**
2407
+ * The pans the document's touch-action allows a touch on `el`, as Pointer
2408
+ * Events determine them: its own, with its ancestors' up to the nearest
2409
+ * element that scrolls (SPEC §5.3), both included. The root's is the
2410
+ * document's only where it differs from the host's (`rootTouchAction`),
2411
+ * which is there for the browser's own panning.
2412
+ */
2413
+ allowedPans(el) {
2414
+ let x = true;
2415
+ let y = true;
2416
+ const root = this.doc.documentElement;
2417
+ const host = rootTouchAction(this.axes, this.host.pageScrolls);
2418
+ for (let n = el; n; n = n.parentElement) {
2419
+ const s = n.ownerDocument.defaultView.getComputedStyle(n);
2420
+ const value = n === root && s.touchAction === host ? "auto" : s.touchAction;
2421
+ const [px, py] = touchPans(value);
2422
+ x &&= px;
2423
+ y &&= py;
2424
+ if (n !== root && this.scrolls(s)) break;
2425
+ }
2426
+ return { x, y };
2427
+ }
2428
+ /** Whether an element with this style scrolls (SPEC §5.3): its overflow is
2429
+ * `auto` or `scroll` along an axis the document asked to scroll. */
2430
+ scrolls(s) {
2431
+ return !!(this.axes & 1) && /auto|scroll/.test(s.overflowY) || !!(this.axes & 2) && /auto|scroll/.test(s.overflowX);
2432
+ }
2433
+ /**
2434
+ * A touch becomes a drag (SPEC §9.1), if it can: one that began without
2435
+ * Alt and was not held still past a long press, on an element that opts
2436
+ * in and has an id, in a surface not detached. It presses at this moment
2437
+ * as a mouse does: `press`, then `dragstart`, both for where it began,
2438
+ * then what the press causes; then a `drag` if the finger is by now over
2439
+ * another element.
2440
+ */
2441
+ touchDragStart(e, begin) {
2442
+ if (this.detachedState || begin.altKey || e.timeStamp - begin.time >= LONG_PRESS_MS) return false;
2443
+ const el = begin.target && isElement(begin.target) ? begin.target : null;
2444
+ let start = null;
2445
+ for (let n = el; n; n = n.parentElement) {
2446
+ if (listens(n, "drag")) {
2447
+ start = n;
2448
+ break;
2449
+ }
2450
+ }
2451
+ const id = start?.getAttribute("id");
2452
+ if (!id) return false;
2453
+ this.cancelDrag();
2454
+ const [c, r] = this.cellAt(begin.x, begin.y);
2455
+ const keys = keysOf(begin);
2456
+ this.reportPress(e);
2457
+ this.drag = { pointer: NaN, start: id, target: id, c, r, keys };
2458
+ this.emit("dragstart", id, { c, r, keys });
2459
+ this.pressAt(e);
2460
+ const t = [...e.changedTouches][0];
2461
+ if (t) this.touchDragMove(t.clientX, t.clientY, e);
2462
+ return true;
2463
+ }
2464
+ /** A touch drag's finger moved to (x, y) of the document: as a pointer's
2465
+ * move (`onDragMove`). */
2466
+ touchDragMove(x, y, e) {
2467
+ const d = this.drag;
2468
+ if (!d) return;
2469
+ const [c, r] = this.cellAt(x, y);
2470
+ const target = this.dragTarget(this.elementAtPoint(x, y, c, r));
2471
+ const keys = keysOf(e);
2472
+ if (target !== d.target || target === "" && (c !== d.c || r !== d.r)) {
2473
+ this.emit("drag", target, { c, r, keys });
2474
+ }
2475
+ Object.assign(d, { target, c, r, keys });
2476
+ }
2477
+ /** A touch drag's finger lifted at (x, y): as a pointer's release
2478
+ * (`onDragUp`), a click where it began. */
2479
+ touchDragEnd(x, y, e) {
2480
+ const d = this.drag;
2481
+ if (!d) return;
2482
+ this.drag = null;
2483
+ const [c, r] = this.cellAt(x, y);
2484
+ const at = this.elementAtPoint(x, y, c, r);
2485
+ const target = this.dragTarget(at);
2486
+ this.emit("dragend", target, { c, r, keys: keysOf(e) });
2487
+ this.dragReleased = true;
2488
+ setTimeout(() => this.dragReleased = false, 0);
2489
+ if (target === d.start && at) this.reportClick(ancestry(at));
2490
+ }
2491
+ /** The surface's cell at (x, y) of the document, counting on past its
2492
+ * edges. */
2493
+ cellAt(x, y) {
2494
+ return [Math.floor(x / this.cellW), Math.floor(y / this.cellH)];
2495
+ }
2496
+ /** The element at (x, y) of the document, if cell (c, r) is in the
2497
+ * window (SPEC §5.2). */
2498
+ elementAtPoint(x, y, c, r) {
2499
+ const w = this.win;
2500
+ if (c < w.x || c >= w.x + w.w || r < w.y || r >= w.y + w.h) return null;
2501
+ return this.doc.elementFromPoint(x, y);
2502
+ }
2312
2503
  // --- Presses with Alt (SPEC §9.2) ---------------------------------------
2313
2504
  /**
2314
2505
  * A press of a mouse's or a pen's primary button with Alt held is the
@@ -2455,26 +2646,27 @@ var Surface = class {
2455
2646
  const r = this.frame.getBoundingClientRect();
2456
2647
  return [r.left + x, r.top + y];
2457
2648
  };
2458
- if (this.host.pageScrolls) {
2459
- new Touch(
2460
- d,
2461
- this.frame.ownerDocument.defaultView,
2462
- { scroll: (dx, dy) => this.host.scrollPage(dx, dy), toPage },
2463
- false,
2464
- (e) => !this.axes && this.inScroller(e.target)
2465
- );
2466
- } else {
2467
- new Touch(d, this.frame.ownerDocument.defaultView, {
2649
+ new Touch(
2650
+ d,
2651
+ this.frame.ownerDocument.defaultView,
2652
+ {
2468
2653
  scroll: (dx, dy, x, y) => {
2469
- if (this.touchRoute === "terminal") this.host.wheel(new WheelEvent("wheel", { deltaX: dx, deltaY: dy, deltaMode: 0, clientX: x, clientY: y }), x, y);
2654
+ if (this.touchRoute === "self") this.touchBox?.scrollBy(dx, dy);
2655
+ else if (this.touchRoute !== "terminal") return;
2656
+ else if (this.host.pageScrolls) this.host.scrollPage(dx, dy);
2657
+ else this.host.wheel(new WheelEvent("wheel", { deltaX: dx, deltaY: dy, deltaMode: 0, clientX: x, clientY: y }), x, y);
2470
2658
  },
2471
2659
  toPage,
2472
- claim: (dx, dy, e) => {
2473
- this.touchRoute = this.axes ? this.gestureRoute(e.target, dx, dy) : "terminal";
2474
- return this.touchRoute !== "doc";
2475
- }
2476
- });
2477
- }
2660
+ claim: (dx, dy, e, begin) => this.claimTouch(dx, dy, e, begin),
2661
+ dragMove: (x, y, e) => this.touchDragMove(x, y, e),
2662
+ dragEnd: (x, y, e) => this.touchDragEnd(x, y, e),
2663
+ dragCancel: () => this.cancelDrag()
2664
+ },
2665
+ false,
2666
+ // Where the page scrolls, the browser pans it from a touch the
2667
+ // surface leaves alone.
2668
+ this.host.pageScrolls ? (e) => !this.axes && this.inScroller(e.target) || !this.pansFreely(e.target) : void 0
2669
+ );
2478
2670
  win.addEventListener("focus", () => {
2479
2671
  if (!this.programFocus && !this.auxPress && !this.keyboard) this.settle();
2480
2672
  });