@svelterm/core 0.1.0 → 0.23.0
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/CHANGELOG.md +465 -0
- package/README.md +42 -29
- package/dist/src/cli/build.d.ts +13 -0
- package/dist/src/cli/build.js +119 -0
- package/dist/src/cli/bundle.d.ts +25 -0
- package/dist/src/cli/bundle.js +61 -0
- package/dist/src/cli/dev.d.ts +10 -0
- package/dist/src/cli/dev.js +152 -0
- package/dist/src/cli/devtools.d.ts +9 -0
- package/dist/src/cli/devtools.js +47 -0
- package/dist/src/cli/init.d.ts +8 -0
- package/dist/src/cli/init.js +153 -0
- package/dist/src/cli/main.d.ts +9 -0
- package/dist/src/cli/main.js +52 -0
- package/dist/src/cli/svt-bin.d.ts +2 -0
- package/dist/src/cli/svt-bin.js +6 -0
- package/dist/src/cli/svt.d.ts +14 -0
- package/dist/src/cli/svt.js +76 -0
- package/dist/src/components/text-buffer.js +8 -5
- package/dist/src/css/animation-runner.d.ts +15 -6
- package/dist/src/css/animation-runner.js +80 -29
- package/dist/src/css/animation.d.ts +12 -0
- package/dist/src/css/animation.js +21 -0
- package/dist/src/css/calc.js +4 -3
- package/dist/src/css/color.d.ts +19 -0
- package/dist/src/css/color.js +371 -62
- package/dist/src/css/compute.d.ts +31 -4
- package/dist/src/css/compute.js +273 -34
- package/dist/src/css/defaults.d.ts +1 -1
- package/dist/src/css/defaults.js +9 -0
- package/dist/src/css/easing.d.ts +9 -0
- package/dist/src/css/easing.js +95 -0
- package/dist/src/css/incremental.d.ts +1 -1
- package/dist/src/css/incremental.js +2 -2
- package/dist/src/css/interpolate.d.ts +13 -0
- package/dist/src/css/interpolate.js +41 -0
- package/dist/src/css/parser.js +59 -3
- package/dist/src/css/pseudo-elements.d.ts +9 -0
- package/dist/src/css/pseudo-elements.js +97 -0
- package/dist/src/css/selector.d.ts +17 -2
- package/dist/src/css/selector.js +128 -13
- package/dist/src/css/specificity.js +17 -6
- package/dist/src/css/values.d.ts +6 -1
- package/dist/src/css/values.js +13 -6
- package/dist/src/debug/context.d.ts +13 -0
- package/dist/src/debug/context.js +11 -0
- package/dist/src/debug/css.d.ts +12 -0
- package/dist/src/debug/css.js +28 -0
- package/dist/src/debug/dom.d.ts +17 -0
- package/dist/src/debug/dom.js +92 -0
- package/dist/src/devtools/DevTools.compiled.js +327 -0
- package/dist/src/devtools/DevTools.css.js +1 -0
- package/dist/src/devtools/client.d.ts +36 -0
- package/dist/src/devtools/client.js +76 -0
- package/dist/src/framelog.d.ts +54 -0
- package/dist/src/framelog.js +99 -0
- package/dist/src/headless.js +12 -4
- package/dist/src/index.d.ts +66 -3
- package/dist/src/index.js +610 -81
- package/dist/src/input/checkable.d.ts +8 -0
- package/dist/src/input/checkable.js +66 -0
- package/dist/src/input/details.d.ts +6 -0
- package/dist/src/input/details.js +34 -0
- package/dist/src/input/focus.d.ts +6 -0
- package/dist/src/input/focus.js +27 -9
- package/dist/src/input/keyboard.d.ts +2 -2
- package/dist/src/input/keyboard.js +32 -5
- package/dist/src/input/label.d.ts +8 -0
- package/dist/src/input/label.js +53 -0
- package/dist/src/input/modal.d.ts +9 -0
- package/dist/src/input/modal.js +28 -0
- package/dist/src/input/mouse.d.ts +2 -2
- package/dist/src/input/mouse.js +15 -2
- package/dist/src/input/select.d.ts +12 -0
- package/dist/src/input/select.js +63 -0
- package/dist/src/input/selection.d.ts +48 -0
- package/dist/src/input/selection.js +150 -0
- package/dist/src/layout/engine.d.ts +2 -0
- package/dist/src/layout/engine.js +1092 -142
- package/dist/src/layout/flex.js +4 -4
- package/dist/src/layout/size.js +3 -2
- package/dist/src/layout/text.d.ts +3 -2
- package/dist/src/layout/text.js +96 -17
- package/dist/src/layout/unicode.d.ts +20 -0
- package/dist/src/layout/unicode.js +121 -0
- package/dist/src/render/animation-clock.d.ts +57 -0
- package/dist/src/render/animation-clock.js +221 -0
- package/dist/src/render/ansi-text.d.ts +26 -0
- package/dist/src/render/ansi-text.js +131 -0
- package/dist/src/render/ansi.d.ts +18 -0
- package/dist/src/render/ansi.js +64 -19
- package/dist/src/render/border.js +166 -17
- package/dist/src/render/buffer.d.ts +1 -0
- package/dist/src/render/buffer.js +5 -2
- package/dist/src/render/clock.d.ts +35 -0
- package/dist/src/render/clock.js +67 -0
- package/dist/src/render/color-depth.d.ts +8 -0
- package/dist/src/render/color-depth.js +59 -0
- package/dist/src/render/context.d.ts +1 -0
- package/dist/src/render/context.js +17 -21
- package/dist/src/render/cursor-emit.d.ts +18 -0
- package/dist/src/render/cursor-emit.js +50 -0
- package/dist/src/render/diff.d.ts +12 -0
- package/dist/src/render/diff.js +120 -0
- package/dist/src/render/generation.d.ts +9 -0
- package/dist/src/render/generation.js +14 -0
- package/dist/src/render/graphics-layer.d.ts +27 -0
- package/dist/src/render/graphics-layer.js +86 -0
- package/dist/src/render/image.d.ts +27 -0
- package/dist/src/render/image.js +113 -0
- package/dist/src/render/incremental-paint.d.ts +7 -3
- package/dist/src/render/incremental-paint.js +52 -79
- package/dist/src/render/inline.d.ts +59 -0
- package/dist/src/render/inline.js +219 -0
- package/dist/src/render/kitty-graphics.d.ts +24 -0
- package/dist/src/render/kitty-graphics.js +58 -0
- package/dist/src/render/paint-text.js +68 -22
- package/dist/src/render/paint.d.ts +8 -1
- package/dist/src/render/paint.js +358 -31
- package/dist/src/render/png.d.ts +13 -0
- package/dist/src/render/png.js +145 -0
- package/dist/src/render/scrollbar.d.ts +8 -2
- package/dist/src/render/scrollbar.js +71 -14
- package/dist/src/render/snapshot.js +3 -1
- package/dist/src/renderer/default.d.ts +7 -0
- package/dist/src/renderer/default.js +11 -0
- package/dist/src/renderer/index.d.ts +8 -2
- package/dist/src/renderer/index.js +4 -2
- package/dist/src/renderer/node.d.ts +109 -0
- package/dist/src/renderer/node.js +165 -1
- package/dist/src/terminal/capabilities.d.ts +33 -0
- package/dist/src/terminal/capabilities.js +66 -0
- package/dist/src/terminal/clipboard.d.ts +9 -0
- package/dist/src/terminal/clipboard.js +39 -0
- package/dist/src/terminal/io.d.ts +82 -0
- package/dist/src/terminal/io.js +155 -0
- package/dist/src/terminal/screen.d.ts +3 -10
- package/dist/src/terminal/screen.js +5 -28
- package/dist/src/terminal/stdin-router.d.ts +8 -5
- package/dist/src/terminal/stdin-router.js +22 -11
- package/dist/src/utils/node-map.d.ts +24 -0
- package/dist/src/utils/node-map.js +75 -0
- package/dist/src/vite/config.d.ts +62 -0
- package/dist/src/vite/config.js +191 -0
- package/docs/compatibility.md +67 -0
- package/docs/debug/devtools.md +40 -0
- package/docs/debug/svt.md +50 -0
- package/docs/distribution.md +106 -0
- package/docs/elements.md +120 -0
- package/docs/getting-started.md +177 -0
- package/docs/guide/css.md +187 -0
- package/docs/guide/input.md +143 -0
- package/docs/guide/layout.md +171 -0
- package/docs/guide/theming.md +94 -0
- package/docs/how-it-works.md +115 -0
- package/docs/inline-mode.md +77 -0
- package/docs/layout.md +112 -0
- package/docs/motion.md +91 -0
- package/docs/reference/README.md +65 -0
- package/docs/reference/css/properties/border-corner.md +82 -0
- package/docs/reference/css/properties/border-style.md +168 -0
- package/docs/reference.md +227 -0
- package/docs/selectors.md +80 -0
- package/docs/terminal-css.md +149 -0
- package/docs/terminals.md +83 -0
- package/package.json +28 -7
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { TermNode } from '../renderer/node.js';
|
|
2
|
+
export declare function isCheckableInput(node: TermNode): boolean;
|
|
3
|
+
/**
|
|
4
|
+
* Toggle a checkbox, or select a radio (unchecking its name-group
|
|
5
|
+
* siblings). Fires change/input events with the new checked state, as
|
|
6
|
+
* Svelte's bind:checked listens for them.
|
|
7
|
+
*/
|
|
8
|
+
export declare function toggleCheckable(node: TermNode): void;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { hasBooleanAttribute } from '../renderer/node.js';
|
|
2
|
+
import { dispatchEvent } from './dispatch.js';
|
|
3
|
+
export function isCheckableInput(node) {
|
|
4
|
+
if (node.tag !== 'input')
|
|
5
|
+
return false;
|
|
6
|
+
const type = node.attributes.get('type');
|
|
7
|
+
return type === 'checkbox' || type === 'radio';
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Toggle a checkbox, or select a radio (unchecking its name-group
|
|
11
|
+
* siblings). Fires change/input events with the new checked state, as
|
|
12
|
+
* Svelte's bind:checked listens for them.
|
|
13
|
+
*/
|
|
14
|
+
export function toggleCheckable(node) {
|
|
15
|
+
if (node.attributes.get('type') === 'radio') {
|
|
16
|
+
selectRadio(node);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
setChecked(node, !hasBooleanAttribute(node, 'checked'));
|
|
20
|
+
}
|
|
21
|
+
function selectRadio(node) {
|
|
22
|
+
if (hasBooleanAttribute(node, 'checked'))
|
|
23
|
+
return; // radios don't untoggle
|
|
24
|
+
const name = node.attributes.get('name');
|
|
25
|
+
if (name) {
|
|
26
|
+
uncheckGroupSiblings(treeRoot(node), node, name);
|
|
27
|
+
}
|
|
28
|
+
setChecked(node, true);
|
|
29
|
+
}
|
|
30
|
+
function uncheckGroupSiblings(scope, selected, name) {
|
|
31
|
+
if (scope !== selected && isCheckableInput(scope)
|
|
32
|
+
&& scope.attributes.get('type') === 'radio'
|
|
33
|
+
&& scope.attributes.get('name') === name
|
|
34
|
+
&& hasBooleanAttribute(scope, 'checked')) {
|
|
35
|
+
clearChecked(scope);
|
|
36
|
+
}
|
|
37
|
+
for (const child of scope.children)
|
|
38
|
+
uncheckGroupSiblings(child, selected, name);
|
|
39
|
+
}
|
|
40
|
+
function treeRoot(node) {
|
|
41
|
+
let root = node;
|
|
42
|
+
while (root.parent)
|
|
43
|
+
root = root.parent;
|
|
44
|
+
return root;
|
|
45
|
+
}
|
|
46
|
+
function setChecked(node, checked) {
|
|
47
|
+
if (checked) {
|
|
48
|
+
if (node.ctx)
|
|
49
|
+
node.ctx.onSetAttribute(node, 'checked', 'true');
|
|
50
|
+
else
|
|
51
|
+
node.attributes.set('checked', 'true');
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
clearChecked(node);
|
|
55
|
+
}
|
|
56
|
+
// Browsers expose the control's value on change (radio groups read it)
|
|
57
|
+
const detail = { checked, value: node.attributes.get('value') ?? '' };
|
|
58
|
+
dispatchEvent(node, 'change', detail);
|
|
59
|
+
dispatchEvent(node, 'input', detail);
|
|
60
|
+
}
|
|
61
|
+
function clearChecked(node) {
|
|
62
|
+
if (node.ctx)
|
|
63
|
+
node.ctx.onRemoveAttribute(node, 'checked');
|
|
64
|
+
else
|
|
65
|
+
node.attributes.delete('checked');
|
|
66
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { hasBooleanAttribute } from '../renderer/node.js';
|
|
2
|
+
import { dispatchEvent } from './dispatch.js';
|
|
3
|
+
/**
|
|
4
|
+
* Toggle the nearest <details> from itself or its <summary>, firing a
|
|
5
|
+
* toggle event with the new state, as in browsers.
|
|
6
|
+
*/
|
|
7
|
+
export function toggleDetails(node) {
|
|
8
|
+
const details = nearestDetails(node);
|
|
9
|
+
if (!details)
|
|
10
|
+
return;
|
|
11
|
+
const open = !hasBooleanAttribute(details, 'open');
|
|
12
|
+
if (open) {
|
|
13
|
+
if (details.ctx)
|
|
14
|
+
details.ctx.onSetAttribute(details, 'open', 'true');
|
|
15
|
+
else
|
|
16
|
+
details.attributes.set('open', 'true');
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
if (details.ctx)
|
|
20
|
+
details.ctx.onRemoveAttribute(details, 'open');
|
|
21
|
+
else
|
|
22
|
+
details.attributes.delete('open');
|
|
23
|
+
}
|
|
24
|
+
dispatchEvent(details, 'toggle', { open });
|
|
25
|
+
}
|
|
26
|
+
function nearestDetails(node) {
|
|
27
|
+
let current = node;
|
|
28
|
+
while (current) {
|
|
29
|
+
if (current.tag === 'details')
|
|
30
|
+
return current;
|
|
31
|
+
current = current.parent;
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
@@ -2,6 +2,8 @@ import { TermNode } from '../renderer/node.js';
|
|
|
2
2
|
export declare class FocusManager {
|
|
3
3
|
private elements;
|
|
4
4
|
private focusIndex;
|
|
5
|
+
/** When set, focus cycling is trapped inside this subtree (modals). */
|
|
6
|
+
private scope;
|
|
5
7
|
onSetAttribute?: (node: TermNode, key: string, value: string) => void;
|
|
6
8
|
onRemoveAttribute?: (node: TermNode, key: string) => void;
|
|
7
9
|
onFocusChange?: (focused: TermNode | null, previous: TermNode | null) => void;
|
|
@@ -9,10 +11,14 @@ export declare class FocusManager {
|
|
|
9
11
|
get count(): number;
|
|
10
12
|
register(node: TermNode): void;
|
|
11
13
|
unregister(node: TermNode): void;
|
|
14
|
+
/** Trap focus cycling inside `node`'s subtree; null lifts the trap. */
|
|
15
|
+
setScope(node: TermNode | null): void;
|
|
12
16
|
focusNext(): void;
|
|
13
17
|
focusPrevious(): void;
|
|
14
18
|
focusByNode(node: TermNode): void;
|
|
15
19
|
clearFocus(): void;
|
|
20
|
+
/** Step through the ring from `start` until an enabled element is found. */
|
|
21
|
+
private focusFirstEnabledFrom;
|
|
16
22
|
private setFocusIndex;
|
|
17
23
|
private clearFocusAttribute;
|
|
18
24
|
}
|
package/dist/src/input/focus.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import { hasBooleanAttribute } from '../renderer/node.js';
|
|
2
|
+
import { withinSubtree } from './modal.js';
|
|
1
3
|
export class FocusManager {
|
|
2
4
|
elements = [];
|
|
3
5
|
focusIndex = -1;
|
|
6
|
+
/** When set, focus cycling is trapped inside this subtree (modals). */
|
|
7
|
+
scope = null;
|
|
4
8
|
onSetAttribute;
|
|
5
9
|
onRemoveAttribute;
|
|
6
10
|
onFocusChange;
|
|
@@ -31,20 +35,20 @@ export class FocusManager {
|
|
|
31
35
|
this.focusIndex--;
|
|
32
36
|
}
|
|
33
37
|
}
|
|
38
|
+
/** Trap focus cycling inside `node`'s subtree; null lifts the trap. */
|
|
39
|
+
setScope(node) {
|
|
40
|
+
this.scope = node;
|
|
41
|
+
}
|
|
34
42
|
focusNext() {
|
|
35
|
-
|
|
36
|
-
return;
|
|
37
|
-
this.setFocusIndex((this.focusIndex + 1) % this.elements.length);
|
|
43
|
+
this.focusFirstEnabledFrom(this.focusIndex, +1);
|
|
38
44
|
}
|
|
39
45
|
focusPrevious() {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const next = this.focusIndex <= 0
|
|
43
|
-
? this.elements.length - 1
|
|
44
|
-
: this.focusIndex - 1;
|
|
45
|
-
this.setFocusIndex(next);
|
|
46
|
+
const start = this.focusIndex === -1 ? this.elements.length : this.focusIndex;
|
|
47
|
+
this.focusFirstEnabledFrom(start, -1);
|
|
46
48
|
}
|
|
47
49
|
focusByNode(node) {
|
|
50
|
+
if (hasBooleanAttribute(node, 'disabled'))
|
|
51
|
+
return;
|
|
48
52
|
const idx = this.elements.indexOf(node);
|
|
49
53
|
if (idx !== -1)
|
|
50
54
|
this.setFocusIndex(idx);
|
|
@@ -54,6 +58,20 @@ export class FocusManager {
|
|
|
54
58
|
this.clearFocusAttribute(this.focused);
|
|
55
59
|
this.focusIndex = -1;
|
|
56
60
|
}
|
|
61
|
+
/** Step through the ring from `start` until an enabled element is found. */
|
|
62
|
+
focusFirstEnabledFrom(start, step) {
|
|
63
|
+
const count = this.elements.length;
|
|
64
|
+
for (let offset = 1; offset <= count; offset++) {
|
|
65
|
+
const index = ((start + step * offset) % count + count) % count;
|
|
66
|
+
const element = this.elements[index];
|
|
67
|
+
if (hasBooleanAttribute(element, 'disabled'))
|
|
68
|
+
continue;
|
|
69
|
+
if (this.scope && !withinSubtree(element, this.scope))
|
|
70
|
+
continue;
|
|
71
|
+
this.setFocusIndex(index);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
57
75
|
setFocusIndex(index) {
|
|
58
76
|
const prev = this.focused;
|
|
59
77
|
if (prev)
|
|
@@ -5,5 +5,5 @@ export interface KeyEvent {
|
|
|
5
5
|
meta: boolean;
|
|
6
6
|
}
|
|
7
7
|
/** Check if data contains a bracketed paste sequence. Returns the pasted text or null. */
|
|
8
|
-
export declare function parsePaste(data: Buffer): string | null;
|
|
9
|
-
export declare function parseKeyEvent(data: Buffer): KeyEvent | null;
|
|
8
|
+
export declare function parsePaste(data: Buffer | Uint8Array): string | null;
|
|
9
|
+
export declare function parseKeyEvent(data: Buffer | Uint8Array): KeyEvent | null;
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
+
function decodeBytes(data) {
|
|
2
|
+
if (typeof Buffer !== 'undefined' && Buffer.isBuffer(data))
|
|
3
|
+
return data.toString();
|
|
4
|
+
return new TextDecoder().decode(data);
|
|
5
|
+
}
|
|
1
6
|
const PASTE_START = '\x1b[200~';
|
|
2
7
|
const PASTE_END = '\x1b[201~';
|
|
3
8
|
/** Check if data contains a bracketed paste sequence. Returns the pasted text or null. */
|
|
4
9
|
export function parsePaste(data) {
|
|
5
|
-
const str = data
|
|
10
|
+
const str = decodeBytes(data);
|
|
6
11
|
if (str.startsWith(PASTE_START)) {
|
|
7
12
|
const endIdx = str.indexOf(PASTE_END);
|
|
8
13
|
if (endIdx !== -1) {
|
|
@@ -51,10 +56,31 @@ function parseEscapeSequence(data) {
|
|
|
51
56
|
}
|
|
52
57
|
return { key: 'Escape', ctrl: false, shift: false, meta: false };
|
|
53
58
|
}
|
|
59
|
+
/** Codepoints the kitty protocol uses for functional keys. */
|
|
60
|
+
const KITTY_FUNCTIONAL = {
|
|
61
|
+
9: 'Tab', 13: 'Enter', 27: 'Escape', 127: 'Backspace',
|
|
62
|
+
57352: 'Insert', 57349: 'Delete',
|
|
63
|
+
57354: 'PageUp', 57355: 'PageDown', 57356: 'Home', 57357: 'End',
|
|
64
|
+
57358: 'CapsLock',
|
|
65
|
+
};
|
|
66
|
+
const KITTY_KEY_RE = /^\x1b\[(\d+)(?:;(\d+)(?::\d+)?)?u/;
|
|
67
|
+
/** Parse a kitty-protocol CSI u key report, or null if not one. */
|
|
68
|
+
function parseKittyKey(str) {
|
|
69
|
+
const match = KITTY_KEY_RE.exec(str);
|
|
70
|
+
if (!match)
|
|
71
|
+
return null;
|
|
72
|
+
const codepoint = parseInt(match[1], 10);
|
|
73
|
+
const mods = match[2] ? parseModifier(parseInt(match[2], 10)) : { ctrl: false, shift: false, meta: false };
|
|
74
|
+
const key = KITTY_FUNCTIONAL[codepoint] ?? String.fromCodePoint(codepoint);
|
|
75
|
+
return { key, ...mods };
|
|
76
|
+
}
|
|
54
77
|
function parseCSI(data) {
|
|
55
78
|
const base = { ctrl: false, shift: false, meta: false };
|
|
56
79
|
if (data.length < 3)
|
|
57
80
|
return { key: 'Escape', ...base };
|
|
81
|
+
const kitty = parseKittyKey(decodeBytes(data));
|
|
82
|
+
if (kitty)
|
|
83
|
+
return kitty;
|
|
58
84
|
const third = data[2];
|
|
59
85
|
// Arrow keys: ESC [ A/B/C/D
|
|
60
86
|
switch (third) {
|
|
@@ -91,10 +117,11 @@ const CSI_KEYS = {
|
|
|
91
117
|
0x48: 'Home', 0x46: 'End',
|
|
92
118
|
};
|
|
93
119
|
function parseModifier(mod) {
|
|
94
|
-
// CSI modifier values
|
|
120
|
+
// CSI modifier values are bitmask+1: 2=Shift, 3=Alt, 5=Ctrl, 6=Shift+Ctrl…
|
|
121
|
+
const mask = mod - 1;
|
|
95
122
|
return {
|
|
96
|
-
shift: (
|
|
97
|
-
meta: (
|
|
98
|
-
ctrl: (
|
|
123
|
+
shift: (mask & 1) !== 0,
|
|
124
|
+
meta: (mask & 2) !== 0,
|
|
125
|
+
ctrl: (mask & 4) !== 0,
|
|
99
126
|
};
|
|
100
127
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { TermNode } from '../renderer/node.js';
|
|
2
|
+
/**
|
|
3
|
+
* The form control a click on `node` should activate through its
|
|
4
|
+
* <label>, per browser behaviour: walk up to the nearest label, then
|
|
5
|
+
* resolve its `for="id"` reference or first labellable descendant.
|
|
6
|
+
* Returns null when no label or no control is involved.
|
|
7
|
+
*/
|
|
8
|
+
export declare function labelledControl(node: TermNode): TermNode | null;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const LABELLABLE = new Set(['input', 'select', 'textarea', 'button']);
|
|
2
|
+
/**
|
|
3
|
+
* The form control a click on `node` should activate through its
|
|
4
|
+
* <label>, per browser behaviour: walk up to the nearest label, then
|
|
5
|
+
* resolve its `for="id"` reference or first labellable descendant.
|
|
6
|
+
* Returns null when no label or no control is involved.
|
|
7
|
+
*/
|
|
8
|
+
export function labelledControl(node) {
|
|
9
|
+
const label = nearestLabel(node);
|
|
10
|
+
if (!label)
|
|
11
|
+
return null;
|
|
12
|
+
const forId = label.attributes.get('for');
|
|
13
|
+
if (forId)
|
|
14
|
+
return findById(treeRoot(label), forId);
|
|
15
|
+
return findControl(label);
|
|
16
|
+
}
|
|
17
|
+
function nearestLabel(node) {
|
|
18
|
+
let current = node;
|
|
19
|
+
while (current) {
|
|
20
|
+
if (current.tag === 'label')
|
|
21
|
+
return current;
|
|
22
|
+
current = current.parent;
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
function findControl(scope) {
|
|
27
|
+
for (const child of scope.children) {
|
|
28
|
+
if (child.nodeType !== 'element')
|
|
29
|
+
continue;
|
|
30
|
+
if (LABELLABLE.has(child.tag ?? ''))
|
|
31
|
+
return child;
|
|
32
|
+
const nested = findControl(child);
|
|
33
|
+
if (nested)
|
|
34
|
+
return nested;
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
function findById(scope, id) {
|
|
39
|
+
if (scope.nodeType === 'element' && scope.attributes.get('id') === id)
|
|
40
|
+
return scope;
|
|
41
|
+
for (const child of scope.children) {
|
|
42
|
+
const found = findById(child, id);
|
|
43
|
+
if (found)
|
|
44
|
+
return found;
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
function treeRoot(node) {
|
|
49
|
+
let root = node;
|
|
50
|
+
while (root.parent)
|
|
51
|
+
root = root.parent;
|
|
52
|
+
return root;
|
|
53
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Modal key routing: an open <dialog> captures keys — Tab cycling traps
|
|
3
|
+
* inside it, Escape closes it — matching browser modal behaviour.
|
|
4
|
+
*/
|
|
5
|
+
import { TermNode } from '../renderer/node.js';
|
|
6
|
+
/** The topmost open <dialog> (last in tree order), or null. */
|
|
7
|
+
export declare function activeModal(root: TermNode): TermNode | null;
|
|
8
|
+
/** Whether `node` is `ancestor` or inside it. */
|
|
9
|
+
export declare function withinSubtree(node: TermNode, ancestor: TermNode): boolean;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Modal key routing: an open <dialog> captures keys — Tab cycling traps
|
|
3
|
+
* inside it, Escape closes it — matching browser modal behaviour.
|
|
4
|
+
*/
|
|
5
|
+
/** The topmost open <dialog> (last in tree order), or null. */
|
|
6
|
+
export function activeModal(root) {
|
|
7
|
+
let found = null;
|
|
8
|
+
const walk = (node) => {
|
|
9
|
+
if (node.nodeType === 'element' && node.tag === 'dialog'
|
|
10
|
+
&& node.attributes.has('open')) {
|
|
11
|
+
found = node;
|
|
12
|
+
}
|
|
13
|
+
for (const child of node.children)
|
|
14
|
+
walk(child);
|
|
15
|
+
};
|
|
16
|
+
walk(root);
|
|
17
|
+
return found;
|
|
18
|
+
}
|
|
19
|
+
/** Whether `node` is `ancestor` or inside it. */
|
|
20
|
+
export function withinSubtree(node, ancestor) {
|
|
21
|
+
let current = node;
|
|
22
|
+
while (current) {
|
|
23
|
+
if (current === ancestor)
|
|
24
|
+
return true;
|
|
25
|
+
current = current.parent;
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export interface MouseEvent {
|
|
2
|
-
button: 'left' | 'right' | 'middle' | 'scrollUp' | 'scrollDown' | 'none';
|
|
2
|
+
button: 'left' | 'right' | 'middle' | 'scrollUp' | 'scrollDown' | 'scrollLeft' | 'scrollRight' | 'none';
|
|
3
3
|
type: 'press' | 'release' | 'motion' | 'scroll';
|
|
4
4
|
col: number;
|
|
5
5
|
row: number;
|
|
6
6
|
}
|
|
7
|
-
export declare function parseMouseEvent(data: Buffer): MouseEvent | null;
|
|
7
|
+
export declare function parseMouseEvent(data: Buffer | Uint8Array): MouseEvent | null;
|
package/dist/src/input/mouse.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
const SGR_MOUSE_RE = /^\x1b\[<(\d+);(\d+);(\d+)([Mm])/;
|
|
2
2
|
export function parseMouseEvent(data) {
|
|
3
|
-
const str =
|
|
3
|
+
const str = typeof Buffer !== 'undefined' && Buffer.isBuffer(data)
|
|
4
|
+
? data.toString()
|
|
5
|
+
: new TextDecoder().decode(data);
|
|
4
6
|
const match = SGR_MOUSE_RE.exec(str);
|
|
5
7
|
if (!match)
|
|
6
8
|
return null;
|
|
@@ -15,7 +17,18 @@ export function parseMouseEvent(data) {
|
|
|
15
17
|
}
|
|
16
18
|
// Scroll: codes 64-67
|
|
17
19
|
if (base >= 64 && base <= 67) {
|
|
18
|
-
|
|
20
|
+
// Shift modifier (bit 2) on vertical scroll = horizontal scroll
|
|
21
|
+
const hasShift = (code & 4) !== 0;
|
|
22
|
+
let button;
|
|
23
|
+
if (base === 64)
|
|
24
|
+
button = hasShift ? 'scrollLeft' : 'scrollUp';
|
|
25
|
+
else if (base === 65)
|
|
26
|
+
button = hasShift ? 'scrollRight' : 'scrollDown';
|
|
27
|
+
else if (base === 66)
|
|
28
|
+
button = 'scrollLeft';
|
|
29
|
+
else
|
|
30
|
+
button = 'scrollRight';
|
|
31
|
+
return { button, type: 'scroll', col, row };
|
|
19
32
|
}
|
|
20
33
|
// Motion: bit 5 (32)
|
|
21
34
|
if (base & 32) {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { TermNode } from '../renderer/node.js';
|
|
2
|
+
/** The select's options, in document order (through optgroups). */
|
|
3
|
+
export declare function selectOptions(select: TermNode): TermNode[];
|
|
4
|
+
/** Index of the selected option — the first with a selected attribute, else 0. */
|
|
5
|
+
export declare function selectedIndex(select: TermNode): number;
|
|
6
|
+
/** The selected option's value attribute, falling back to its text. */
|
|
7
|
+
export declare function selectValue(select: TermNode): string;
|
|
8
|
+
/**
|
|
9
|
+
* Move the selection by delta with wraparound — the popup-less cycling
|
|
10
|
+
* interaction — updating selected attributes and firing change/input.
|
|
11
|
+
*/
|
|
12
|
+
export declare function cycleSelect(select: TermNode, delta: 1 | -1): void;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { hasBooleanAttribute } from '../renderer/node.js';
|
|
2
|
+
import { dispatchEvent } from './dispatch.js';
|
|
3
|
+
/** The select's options, in document order (through optgroups). */
|
|
4
|
+
export function selectOptions(select) {
|
|
5
|
+
const options = [];
|
|
6
|
+
collectOptions(select, options);
|
|
7
|
+
return options;
|
|
8
|
+
}
|
|
9
|
+
/** Index of the selected option — the first with a selected attribute, else 0. */
|
|
10
|
+
export function selectedIndex(select) {
|
|
11
|
+
const options = selectOptions(select);
|
|
12
|
+
const index = options.findIndex(option => hasBooleanAttribute(option, 'selected'));
|
|
13
|
+
return index === -1 ? 0 : index;
|
|
14
|
+
}
|
|
15
|
+
/** The selected option's value attribute, falling back to its text. */
|
|
16
|
+
export function selectValue(select) {
|
|
17
|
+
const option = selectOptions(select)[selectedIndex(select)];
|
|
18
|
+
if (!option)
|
|
19
|
+
return '';
|
|
20
|
+
return option.attributes.get('value') ?? option.textContent.trim();
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Move the selection by delta with wraparound — the popup-less cycling
|
|
24
|
+
* interaction — updating selected attributes and firing change/input.
|
|
25
|
+
*/
|
|
26
|
+
export function cycleSelect(select, delta) {
|
|
27
|
+
const options = selectOptions(select);
|
|
28
|
+
if (options.length === 0)
|
|
29
|
+
return;
|
|
30
|
+
const current = selectedIndex(select);
|
|
31
|
+
const next = (current + delta + options.length) % options.length;
|
|
32
|
+
if (next === current)
|
|
33
|
+
return;
|
|
34
|
+
setSelected(options[current], false);
|
|
35
|
+
setSelected(options[next], true);
|
|
36
|
+
const value = selectValue(select);
|
|
37
|
+
dispatchEvent(select, 'change', { value });
|
|
38
|
+
dispatchEvent(select, 'input', { value });
|
|
39
|
+
}
|
|
40
|
+
function collectOptions(node, out) {
|
|
41
|
+
for (const child of node.children) {
|
|
42
|
+
if (child.nodeType !== 'element')
|
|
43
|
+
continue;
|
|
44
|
+
if (child.tag === 'option')
|
|
45
|
+
out.push(child);
|
|
46
|
+
else if (child.tag === 'optgroup')
|
|
47
|
+
collectOptions(child, out);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function setSelected(option, selected) {
|
|
51
|
+
if (selected) {
|
|
52
|
+
if (option.ctx)
|
|
53
|
+
option.ctx.onSetAttribute(option, 'selected', 'true');
|
|
54
|
+
else
|
|
55
|
+
option.attributes.set('selected', 'true');
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
if (option.ctx)
|
|
59
|
+
option.ctx.onRemoveAttribute(option, 'selected');
|
|
60
|
+
else
|
|
61
|
+
option.attributes.delete('selected');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal-style text selection over the painted cell grid. Mouse
|
|
3
|
+
* reporting means the terminal's native selection is off, so svelterm
|
|
4
|
+
* provides its own: drag selects a row-major range, double-click a word,
|
|
5
|
+
* triple-click a line. Selected cells paint inverted; releasing returns
|
|
6
|
+
* the selected text for the clipboard.
|
|
7
|
+
*/
|
|
8
|
+
import type { CellBuffer } from '../render/buffer.js';
|
|
9
|
+
export interface CellPos {
|
|
10
|
+
col: number;
|
|
11
|
+
row: number;
|
|
12
|
+
}
|
|
13
|
+
export interface SelectionRange {
|
|
14
|
+
start: CellPos;
|
|
15
|
+
end: CellPos;
|
|
16
|
+
}
|
|
17
|
+
export declare class SelectionController {
|
|
18
|
+
private getBuffer;
|
|
19
|
+
private now;
|
|
20
|
+
private anchor;
|
|
21
|
+
private point;
|
|
22
|
+
private dragging;
|
|
23
|
+
private pressed;
|
|
24
|
+
private lastClick;
|
|
25
|
+
constructor(getBuffer: () => CellBuffer | null, now?: () => number);
|
|
26
|
+
/** The selected range in row-major order, or null when nothing is selected. */
|
|
27
|
+
range(): SelectionRange | null;
|
|
28
|
+
/**
|
|
29
|
+
* Left button pressed. Counts multi-clicks: the second quick click on
|
|
30
|
+
* the same cell selects the word, the third the line.
|
|
31
|
+
*/
|
|
32
|
+
onPress(col: number, row: number): void;
|
|
33
|
+
/** Pointer moved with the left button held: extend the selection. */
|
|
34
|
+
onMotion(col: number, row: number): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Left button released. Returns the selected text (for the clipboard)
|
|
37
|
+
* when a selection exists; the highlight stays until the next press.
|
|
38
|
+
*/
|
|
39
|
+
onRelease(): string | null;
|
|
40
|
+
/** Drop the selection; returns whether anything was cleared. */
|
|
41
|
+
clear(): boolean;
|
|
42
|
+
private selectWord;
|
|
43
|
+
private selectLine;
|
|
44
|
+
private rowText;
|
|
45
|
+
private extractText;
|
|
46
|
+
}
|
|
47
|
+
/** Invert the cells covered by the selection so it reads as highlighted. */
|
|
48
|
+
export declare function applySelectionOverlay(buffer: CellBuffer, range: SelectionRange | null): void;
|