@textui/widgets 0.1.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/LICENSE +21 -0
- package/README.md +69 -0
- package/dist/chart/area-chart.d.ts +4 -0
- package/dist/chart/area-chart.d.ts.map +1 -0
- package/dist/chart/area-chart.js +3 -0
- package/dist/chart/bar-chart.d.ts +32 -0
- package/dist/chart/bar-chart.d.ts.map +1 -0
- package/dist/chart/bar-chart.js +32 -0
- package/dist/chart/gauge.d.ts +26 -0
- package/dist/chart/gauge.d.ts.map +1 -0
- package/dist/chart/gauge.js +26 -0
- package/dist/chart/heatmap.d.ts +13 -0
- package/dist/chart/heatmap.d.ts.map +1 -0
- package/dist/chart/heatmap.js +27 -0
- package/dist/chart/histogram.d.ts +12 -0
- package/dist/chart/histogram.d.ts.map +1 -0
- package/dist/chart/histogram.js +22 -0
- package/dist/chart/index.d.ts +20 -0
- package/dist/chart/index.d.ts.map +1 -0
- package/dist/chart/index.js +32 -0
- package/dist/chart/line-chart.d.ts +7 -0
- package/dist/chart/line-chart.d.ts.map +1 -0
- package/dist/chart/line-chart.js +75 -0
- package/dist/chart/shared.d.ts +34 -0
- package/dist/chart/shared.d.ts.map +1 -0
- package/dist/chart/shared.js +78 -0
- package/dist/chart/sparkline.d.ts +15 -0
- package/dist/chart/sparkline.d.ts.map +1 -0
- package/dist/chart/sparkline.js +22 -0
- package/dist/control/button.d.ts +27 -0
- package/dist/control/button.d.ts.map +1 -0
- package/dist/control/button.js +130 -0
- package/dist/control/checkbox.d.ts +10 -0
- package/dist/control/checkbox.d.ts.map +1 -0
- package/dist/control/checkbox.js +31 -0
- package/dist/control/index.d.ts +20 -0
- package/dist/control/index.d.ts.map +1 -0
- package/dist/control/index.js +36 -0
- package/dist/control/radio-group.d.ts +17 -0
- package/dist/control/radio-group.d.ts.map +1 -0
- package/dist/control/radio-group.js +32 -0
- package/dist/control/search-box.d.ts +7 -0
- package/dist/control/search-box.d.ts.map +1 -0
- package/dist/control/search-box.js +3 -0
- package/dist/control/select.d.ts +42 -0
- package/dist/control/select.d.ts.map +1 -0
- package/dist/control/select.js +166 -0
- package/dist/control/shared.d.ts +41 -0
- package/dist/control/shared.d.ts.map +1 -0
- package/dist/control/shared.js +1 -0
- package/dist/control/slider.d.ts +14 -0
- package/dist/control/slider.d.ts.map +1 -0
- package/dist/control/slider.js +45 -0
- package/dist/control/switch.d.ts +10 -0
- package/dist/control/switch.d.ts.map +1 -0
- package/dist/control/switch.js +32 -0
- package/dist/control/text-area.d.ts +87 -0
- package/dist/control/text-area.d.ts.map +1 -0
- package/dist/control/text-area.js +299 -0
- package/dist/control/text-input.d.ts +10 -0
- package/dist/control/text-input.d.ts.map +1 -0
- package/dist/control/text-input.js +129 -0
- package/dist/data/code-viewer.d.ts +71 -0
- package/dist/data/code-viewer.d.ts.map +1 -0
- package/dist/data/code-viewer.js +232 -0
- package/dist/data/feed.d.ts +43 -0
- package/dist/data/feed.d.ts.map +1 -0
- package/dist/data/feed.js +178 -0
- package/dist/data/index.d.ts +54 -0
- package/dist/data/index.d.ts.map +1 -0
- package/dist/data/index.js +187 -0
- package/dist/data/list.d.ts +31 -0
- package/dist/data/list.d.ts.map +1 -0
- package/dist/data/list.js +104 -0
- package/dist/data/log-viewer.d.ts +26 -0
- package/dist/data/log-viewer.d.ts.map +1 -0
- package/dist/data/log-viewer.js +69 -0
- package/dist/data/markdown-view.d.ts +31 -0
- package/dist/data/markdown-view.d.ts.map +1 -0
- package/dist/data/markdown-view.js +139 -0
- package/dist/data/pagination.d.ts +9 -0
- package/dist/data/pagination.d.ts.map +1 -0
- package/dist/data/pagination.js +18 -0
- package/dist/data/scroll-thumb.d.ts +15 -0
- package/dist/data/scroll-thumb.d.ts.map +1 -0
- package/dist/data/scroll-thumb.js +32 -0
- package/dist/data/shared.d.ts +12 -0
- package/dist/data/shared.d.ts.map +1 -0
- package/dist/data/shared.js +17 -0
- package/dist/data/tree.d.ts +35 -0
- package/dist/data/tree.d.ts.map +1 -0
- package/dist/data/tree.js +158 -0
- package/dist/decorations.d.ts +88 -0
- package/dist/decorations.d.ts.map +1 -0
- package/dist/decorations.js +135 -0
- package/dist/display/alert.d.ts +8 -0
- package/dist/display/alert.d.ts.map +1 -0
- package/dist/display/alert.js +12 -0
- package/dist/display/badge.d.ts +10 -0
- package/dist/display/badge.d.ts.map +1 -0
- package/dist/display/badge.js +21 -0
- package/dist/display/card.d.ts +8 -0
- package/dist/display/card.d.ts.map +1 -0
- package/dist/display/card.js +12 -0
- package/dist/display/empty-state.d.ts +10 -0
- package/dist/display/empty-state.d.ts.map +1 -0
- package/dist/display/empty-state.js +5 -0
- package/dist/display/error-state.d.ts +9 -0
- package/dist/display/error-state.d.ts.map +1 -0
- package/dist/display/error-state.js +7 -0
- package/dist/display/heading.d.ts +6 -0
- package/dist/display/heading.d.ts.map +1 -0
- package/dist/display/heading.js +8 -0
- package/dist/display/index.d.ts +25 -0
- package/dist/display/index.d.ts.map +1 -0
- package/dist/display/index.js +52 -0
- package/dist/display/key-value.d.ts +14 -0
- package/dist/display/key-value.d.ts.map +1 -0
- package/dist/display/key-value.js +11 -0
- package/dist/display/label.d.ts +6 -0
- package/dist/display/label.d.ts.map +1 -0
- package/dist/display/label.js +3 -0
- package/dist/display/marquee.d.ts +36 -0
- package/dist/display/marquee.d.ts.map +1 -0
- package/dist/display/marquee.js +55 -0
- package/dist/display/progress.d.ts +39 -0
- package/dist/display/progress.d.ts.map +1 -0
- package/dist/display/progress.js +46 -0
- package/dist/display/skeleton.d.ts +20 -0
- package/dist/display/skeleton.d.ts.map +1 -0
- package/dist/display/skeleton.js +20 -0
- package/dist/display/spinner.d.ts +7 -0
- package/dist/display/spinner.d.ts.map +1 -0
- package/dist/display/spinner.js +9 -0
- package/dist/display/status-dot.d.ts +8 -0
- package/dist/display/status-dot.d.ts.map +1 -0
- package/dist/display/status-dot.js +13 -0
- package/dist/display/timeline.d.ts +12 -0
- package/dist/display/timeline.d.ts.map +1 -0
- package/dist/display/timeline.js +22 -0
- package/dist/find.d.ts +74 -0
- package/dist/find.d.ts.map +1 -0
- package/dist/find.js +102 -0
- package/dist/form/danger-zone.d.ts +11 -0
- package/dist/form/danger-zone.d.ts.map +1 -0
- package/dist/form/danger-zone.js +15 -0
- package/dist/form/field.d.ts +19 -0
- package/dist/form/field.d.ts.map +1 -0
- package/dist/form/field.js +35 -0
- package/dist/form/form-actions.d.ts +11 -0
- package/dist/form/form-actions.d.ts.map +1 -0
- package/dist/form/form-actions.js +16 -0
- package/dist/form/form-section.d.ts +7 -0
- package/dist/form/form-section.d.ts.map +1 -0
- package/dist/form/form-section.js +5 -0
- package/dist/form/form.d.ts +7 -0
- package/dist/form/form.d.ts.map +1 -0
- package/dist/form/form.js +3 -0
- package/dist/form/index.d.ts +42 -0
- package/dist/form/index.d.ts.map +1 -0
- package/dist/form/index.js +140 -0
- package/dist/form/shared.d.ts +25 -0
- package/dist/form/shared.d.ts.map +1 -0
- package/dist/form/shared.js +9 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +62 -0
- package/dist/layout/center.d.ts +7 -0
- package/dist/layout/center.d.ts.map +1 -0
- package/dist/layout/center.js +8 -0
- package/dist/layout/column.d.ts +3 -0
- package/dist/layout/column.d.ts.map +1 -0
- package/dist/layout/column.js +2 -0
- package/dist/layout/divider.d.ts +11 -0
- package/dist/layout/divider.d.ts.map +1 -0
- package/dist/layout/divider.js +13 -0
- package/dist/layout/grid.d.ts +10 -0
- package/dist/layout/grid.d.ts.map +1 -0
- package/dist/layout/grid.js +22 -0
- package/dist/layout/index.d.ts +19 -0
- package/dist/layout/index.d.ts.map +1 -0
- package/dist/layout/index.js +36 -0
- package/dist/layout/panel.d.ts +37 -0
- package/dist/layout/panel.d.ts.map +1 -0
- package/dist/layout/panel.js +71 -0
- package/dist/layout/row.d.ts +7 -0
- package/dist/layout/row.d.ts.map +1 -0
- package/dist/layout/row.js +2 -0
- package/dist/layout/scroll-view.d.ts +27 -0
- package/dist/layout/scroll-view.d.ts.map +1 -0
- package/dist/layout/scroll-view.js +93 -0
- package/dist/layout/splitter.d.ts +10 -0
- package/dist/layout/splitter.d.ts.map +1 -0
- package/dist/layout/splitter.js +9 -0
- package/dist/layout/stack.d.ts +7 -0
- package/dist/layout/stack.d.ts.map +1 -0
- package/dist/layout/stack.js +5 -0
- package/dist/navigation/breadcrumb.d.ts +14 -0
- package/dist/navigation/breadcrumb.d.ts.map +1 -0
- package/dist/navigation/breadcrumb.js +24 -0
- package/dist/navigation/index.d.ts +17 -0
- package/dist/navigation/index.d.ts.map +1 -0
- package/dist/navigation/index.js +30 -0
- package/dist/navigation/key-hints.d.ts +11 -0
- package/dist/navigation/key-hints.d.ts.map +1 -0
- package/dist/navigation/key-hints.js +13 -0
- package/dist/navigation/menu.d.ts +35 -0
- package/dist/navigation/menu.d.ts.map +1 -0
- package/dist/navigation/menu.js +88 -0
- package/dist/navigation/status-bar.d.ts +18 -0
- package/dist/navigation/status-bar.d.ts.map +1 -0
- package/dist/navigation/status-bar.js +12 -0
- package/dist/navigation/tabs.d.ts +20 -0
- package/dist/navigation/tabs.d.ts.map +1 -0
- package/dist/navigation/tabs.js +41 -0
- package/dist/navigation/toolbar.d.ts +14 -0
- package/dist/navigation/toolbar.d.ts.map +1 -0
- package/dist/navigation/toolbar.js +10 -0
- package/dist/navigation/wizard.d.ts +15 -0
- package/dist/navigation/wizard.d.ts.map +1 -0
- package/dist/navigation/wizard.js +17 -0
- package/dist/overlay/command-palette.d.ts +63 -0
- package/dist/overlay/command-palette.d.ts.map +1 -0
- package/dist/overlay/command-palette.js +415 -0
- package/dist/overlay/dialog.d.ts +16 -0
- package/dist/overlay/dialog.d.ts.map +1 -0
- package/dist/overlay/dialog.js +44 -0
- package/dist/overlay/index.d.ts +84 -0
- package/dist/overlay/index.d.ts.map +1 -0
- package/dist/overlay/index.js +157 -0
- package/dist/overlay/layer-scope.d.ts +21 -0
- package/dist/overlay/layer-scope.d.ts.map +1 -0
- package/dist/overlay/layer-scope.js +5 -0
- package/dist/overlay/path-picker.d.ts +14 -0
- package/dist/overlay/path-picker.d.ts.map +1 -0
- package/dist/overlay/path-picker.js +200 -0
- package/dist/overlay/prompt-dialog.d.ts +12 -0
- package/dist/overlay/prompt-dialog.d.ts.map +1 -0
- package/dist/overlay/prompt-dialog.js +29 -0
- package/dist/overlay/shared.d.ts +17 -0
- package/dist/overlay/shared.d.ts.map +1 -0
- package/dist/overlay/shared.js +17 -0
- package/dist/overlay/toast-host.d.ts +8 -0
- package/dist/overlay/toast-host.d.ts.map +1 -0
- package/dist/overlay/toast-host.js +19 -0
- package/dist/overlay/toast.d.ts +9 -0
- package/dist/overlay/toast.d.ts.map +1 -0
- package/dist/overlay/toast.js +20 -0
- package/dist/overlay/tooltip.d.ts +6 -0
- package/dist/overlay/tooltip.d.ts.map +1 -0
- package/dist/overlay/tooltip.js +11 -0
- package/dist/panel/index.d.ts +79 -0
- package/dist/panel/index.d.ts.map +1 -0
- package/dist/panel/index.js +256 -0
- package/dist/panel/resource-panel.d.ts +93 -0
- package/dist/panel/resource-panel.d.ts.map +1 -0
- package/dist/panel/resource-panel.js +213 -0
- package/dist/panel/shared.d.ts +6 -0
- package/dist/panel/shared.d.ts.map +1 -0
- package/dist/panel/shared.js +4 -0
- package/dist/shells/console-shell.d.ts +8 -0
- package/dist/shells/console-shell.d.ts.map +1 -0
- package/dist/shells/console-shell.js +30 -0
- package/dist/shells/index.d.ts +20 -0
- package/dist/shells/index.d.ts.map +1 -0
- package/dist/shells/index.js +61 -0
- package/dist/shells/paper-shell.d.ts +9 -0
- package/dist/shells/paper-shell.d.ts.map +1 -0
- package/dist/shells/paper-shell.js +23 -0
- package/dist/shells/plain-shell.d.ts +3 -0
- package/dist/shells/plain-shell.d.ts.map +1 -0
- package/dist/shells/plain-shell.js +11 -0
- package/dist/shells/shared.d.ts +12 -0
- package/dist/shells/shared.d.ts.map +1 -0
- package/dist/shells/shared.js +21 -0
- package/dist/shells/workbench-shell.d.ts +9 -0
- package/dist/shells/workbench-shell.d.ts.map +1 -0
- package/dist/shells/workbench-shell.js +43 -0
- package/dist/surface/bar-layout.d.ts +4 -0
- package/dist/surface/bar-layout.d.ts.map +1 -0
- package/dist/surface/bar-layout.js +7 -0
- package/dist/surface/index.d.ts +58 -0
- package/dist/surface/index.d.ts.map +1 -0
- package/dist/surface/index.js +48 -0
- package/dist/surface/inline-layout.d.ts +4 -0
- package/dist/surface/inline-layout.d.ts.map +1 -0
- package/dist/surface/inline-layout.js +7 -0
- package/dist/surface/mount-view.d.ts +7 -0
- package/dist/surface/mount-view.d.ts.map +1 -0
- package/dist/surface/mount-view.js +19 -0
- package/dist/surface/rail-layout.d.ts +4 -0
- package/dist/surface/rail-layout.d.ts.map +1 -0
- package/dist/surface/rail-layout.js +19 -0
- package/dist/surface/shared.d.ts +7 -0
- package/dist/surface/shared.d.ts.map +1 -0
- package/dist/surface/shared.js +1 -0
- package/dist/surface/single-layout.d.ts +4 -0
- package/dist/surface/single-layout.d.ts.map +1 -0
- package/dist/surface/single-layout.js +18 -0
- package/dist/surface/split-layout.d.ts +4 -0
- package/dist/surface/split-layout.d.ts.map +1 -0
- package/dist/surface/split-layout.js +7 -0
- package/dist/surface/stack-layout.d.ts +4 -0
- package/dist/surface/stack-layout.d.ts.map +1 -0
- package/dist/surface/stack-layout.js +22 -0
- package/dist/surface/surface-area.d.ts +16 -0
- package/dist/surface/surface-area.d.ts.map +1 -0
- package/dist/surface/surface-area.js +32 -0
- package/dist/surface/tabs-layout.d.ts +4 -0
- package/dist/surface/tabs-layout.d.ts.map +1 -0
- package/dist/surface/tabs-layout.js +24 -0
- package/dist/tone.d.ts +16 -0
- package/dist/tone.d.ts.map +1 -0
- package/dist/tone.js +45 -0
- package/dist/viewport.d.ts +30 -0
- package/dist/viewport.d.ts.map +1 -0
- package/dist/viewport.js +17 -0
- package/package.json +54 -0
- package/src/chart/area-chart.ts +9 -0
- package/src/chart/bar-chart.ts +74 -0
- package/src/chart/gauge.ts +60 -0
- package/src/chart/heatmap.ts +49 -0
- package/src/chart/histogram.ts +36 -0
- package/src/chart/index.ts +36 -0
- package/src/chart/line-chart.ts +100 -0
- package/src/chart/shared.ts +112 -0
- package/src/chart/sparkline.ts +46 -0
- package/src/control/button.ts +184 -0
- package/src/control/checkbox.ts +48 -0
- package/src/control/index.ts +40 -0
- package/src/control/radio-group.ts +60 -0
- package/src/control/search-box.ts +15 -0
- package/src/control/select.ts +230 -0
- package/src/control/shared.ts +41 -0
- package/src/control/slider.ts +63 -0
- package/src/control/switch.ts +39 -0
- package/src/control/text-area.ts +390 -0
- package/src/control/text-input.ts +150 -0
- package/src/data/code-viewer.ts +337 -0
- package/src/data/feed.ts +204 -0
- package/src/data/index.ts +243 -0
- package/src/data/list.ts +149 -0
- package/src/data/log-viewer.ts +105 -0
- package/src/data/markdown-view.ts +182 -0
- package/src/data/pagination.ts +31 -0
- package/src/data/scroll-thumb.ts +40 -0
- package/src/data/shared.ts +23 -0
- package/src/data/tree.ts +215 -0
- package/src/decorations.ts +176 -0
- package/src/display/alert.ts +28 -0
- package/src/display/badge.ts +40 -0
- package/src/display/card.ts +23 -0
- package/src/display/empty-state.ts +21 -0
- package/src/display/error-state.ts +23 -0
- package/src/display/heading.ts +17 -0
- package/src/display/index.ts +55 -0
- package/src/display/key-value.ts +32 -0
- package/src/display/label.ts +11 -0
- package/src/display/marquee.ts +88 -0
- package/src/display/progress.ts +92 -0
- package/src/display/skeleton.ts +34 -0
- package/src/display/spinner.ts +20 -0
- package/src/display/status-dot.ts +24 -0
- package/src/display/timeline.ts +47 -0
- package/src/find.ts +138 -0
- package/src/form/danger-zone.ts +34 -0
- package/src/form/field.ts +61 -0
- package/src/form/form-actions.ts +35 -0
- package/src/form/form-section.ts +16 -0
- package/src/form/form.ts +13 -0
- package/src/form/index.ts +191 -0
- package/src/form/shared.ts +36 -0
- package/src/index.ts +63 -0
- package/src/layout/center.ts +17 -0
- package/src/layout/column.ts +6 -0
- package/src/layout/divider.ts +31 -0
- package/src/layout/grid.ts +34 -0
- package/src/layout/index.ts +39 -0
- package/src/layout/panel.ts +114 -0
- package/src/layout/row.ts +11 -0
- package/src/layout/scroll-view.ts +115 -0
- package/src/layout/splitter.ts +24 -0
- package/src/layout/stack.ts +12 -0
- package/src/navigation/breadcrumb.ts +43 -0
- package/src/navigation/index.ts +33 -0
- package/src/navigation/key-hints.ts +25 -0
- package/src/navigation/menu.ts +134 -0
- package/src/navigation/status-bar.ts +43 -0
- package/src/navigation/tabs.ts +73 -0
- package/src/navigation/toolbar.ts +25 -0
- package/src/navigation/wizard.ts +43 -0
- package/src/overlay/command-palette.ts +485 -0
- package/src/overlay/dialog.ts +65 -0
- package/src/overlay/index.ts +232 -0
- package/src/overlay/layer-scope.ts +26 -0
- package/src/overlay/path-picker.ts +215 -0
- package/src/overlay/prompt-dialog.ts +45 -0
- package/src/overlay/shared.ts +19 -0
- package/src/overlay/toast-host.ts +30 -0
- package/src/overlay/toast.ts +35 -0
- package/src/overlay/tooltip.ts +17 -0
- package/src/panel/index.ts +317 -0
- package/src/panel/resource-panel.ts +304 -0
- package/src/panel/shared.ts +8 -0
- package/src/shells/console-shell.ts +47 -0
- package/src/shells/index.ts +66 -0
- package/src/shells/paper-shell.ts +31 -0
- package/src/shells/plain-shell.ts +19 -0
- package/src/shells/shared.ts +24 -0
- package/src/shells/workbench-shell.ts +62 -0
- package/src/surface/bar-layout.ts +11 -0
- package/src/surface/index.ts +53 -0
- package/src/surface/inline-layout.ts +11 -0
- package/src/surface/mount-view.ts +30 -0
- package/src/surface/rail-layout.ts +27 -0
- package/src/surface/shared.ts +7 -0
- package/src/surface/single-layout.ts +25 -0
- package/src/surface/split-layout.ts +11 -0
- package/src/surface/stack-layout.ts +32 -0
- package/src/surface/surface-area.ts +46 -0
- package/src/surface/tabs-layout.ts +30 -0
- package/src/tone.ts +48 -0
- package/src/viewport.ts +45 -0
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import type { BoxProps, KeyEvent, SemanticVariant } from '@textui/core';
|
|
2
|
+
import {
|
|
3
|
+
defineComponent,
|
|
4
|
+
graphemes,
|
|
5
|
+
h,
|
|
6
|
+
stringWidth,
|
|
7
|
+
useEffect,
|
|
8
|
+
useFocus,
|
|
9
|
+
useInput,
|
|
10
|
+
useMeasure,
|
|
11
|
+
useState,
|
|
12
|
+
useTicker,
|
|
13
|
+
} from '@textui/core';
|
|
14
|
+
|
|
15
|
+
export interface TextAreaProps extends BoxProps {
|
|
16
|
+
value: string;
|
|
17
|
+
onChange(value: string): void;
|
|
18
|
+
/**
|
|
19
|
+
* Enter. A newline is `ctrl+enter`, which is the one people reach for, with
|
|
20
|
+
* `alt+enter` as the one that cannot fail.
|
|
21
|
+
*
|
|
22
|
+
* A terminal has three ways to say `ctrl+enter` and `@textui/terminal`
|
|
23
|
+
* decodes all three: the kitty protocol's `CSI 13;5u`, xterm's
|
|
24
|
+
* `modifyOtherKeys` `CSI 27;5;13~`, and a bare LF - which is what most
|
|
25
|
+
* terminals send, and which is *not* the Return key, because in raw mode
|
|
26
|
+
* Return sends CR.
|
|
27
|
+
*
|
|
28
|
+
* `shift+enter` is not offered. There is no encoding in which it differs
|
|
29
|
+
* from enter, so a field that claimed it would be claiming a key that
|
|
30
|
+
* cannot arrive.
|
|
31
|
+
*
|
|
32
|
+
* Left off, enter inserts a newline like every other key does.
|
|
33
|
+
*/
|
|
34
|
+
onSubmit?(value: string): void;
|
|
35
|
+
/** Escape, when there is nothing inside the field to cancel. */
|
|
36
|
+
onCancel?(): void;
|
|
37
|
+
/** Up at the top, down at the bottom: for walking a history. */
|
|
38
|
+
onOverflow?(direction: -1 | 1): void;
|
|
39
|
+
/**
|
|
40
|
+
* Left at the very start, right at the very end.
|
|
41
|
+
*
|
|
42
|
+
* The horizontal pair of `onOverflow`, and separate from it because they
|
|
43
|
+
* mean different things: up and down walk a history, and left off the front
|
|
44
|
+
* of the field is "I am done here" - which is how a composer hands the
|
|
45
|
+
* reader back to what is beside it without them reaching for escape.
|
|
46
|
+
*/
|
|
47
|
+
onEdge?(edge: 'start' | 'end'): void;
|
|
48
|
+
placeholder?: string;
|
|
49
|
+
/** Rows before it stops growing and starts scrolling. */
|
|
50
|
+
maxRows?: number;
|
|
51
|
+
maxLength?: number;
|
|
52
|
+
autoFocus?: boolean;
|
|
53
|
+
focusId?: string;
|
|
54
|
+
/**
|
|
55
|
+
* The caret's colour. `cursor` by default, which is the theme's own.
|
|
56
|
+
*
|
|
57
|
+
* A composer usually wants `accent`: the caret is the one thing on the
|
|
58
|
+
* screen saying where typing goes, and the field it sits in is the point of
|
|
59
|
+
* the screen.
|
|
60
|
+
*/
|
|
61
|
+
caretTone?: SemanticVariant;
|
|
62
|
+
/**
|
|
63
|
+
* What the caret looks like. An underline under the character it is on by
|
|
64
|
+
* default; `block` fills the cell instead.
|
|
65
|
+
*
|
|
66
|
+
* Both **mark a cell rather than occupying one**. The caret used to be a
|
|
67
|
+
* glyph pushed in between the text before it and the text after, so every
|
|
68
|
+
* character to its right sat one column off from where it would be once the
|
|
69
|
+
* caret moved on, and the row was a cell wider than its own text. On a
|
|
70
|
+
* wrapped row that extra cell is the one that does not fit.
|
|
71
|
+
*/
|
|
72
|
+
caretStyle?: 'underline' | 'block';
|
|
73
|
+
/**
|
|
74
|
+
* Blink the caret while the field has the keyboard. On by default.
|
|
75
|
+
*
|
|
76
|
+
* Driven by the animation ticker, so it stops with every other animation -
|
|
77
|
+
* a still, a test and a terminal that has animation off all draw the caret
|
|
78
|
+
* solid rather than at whatever phase the clock happened to be in.
|
|
79
|
+
*/
|
|
80
|
+
blink?: boolean;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* A field that is a paragraph.
|
|
85
|
+
*
|
|
86
|
+
* `TextInput` is one line, and a message, a note or a commit body is not. It
|
|
87
|
+
* soft-wraps, grows to what has been typed and then scrolls, keeps the caret
|
|
88
|
+
* visible, and hands back every key it does not want - which is what lets the
|
|
89
|
+
* thing behind it keep its own shortcuts while this has the keyboard.
|
|
90
|
+
*
|
|
91
|
+
* Wrapping means a line is not a row: `maxRows` is a budget of rows, up and
|
|
92
|
+
* down move by row, and the inherited `wrap` prop picks how a line breaks.
|
|
93
|
+
* Any of its `truncate-*` values asks for one row per line instead, with an
|
|
94
|
+
* ellipsis where the text was cut.
|
|
95
|
+
*
|
|
96
|
+
* It takes a printable character before any keybinding sees it, because that
|
|
97
|
+
* is what typing is. That single fact is why an application with one of these
|
|
98
|
+
* cannot have single-letter global commands, and why it does not need to.
|
|
99
|
+
*/
|
|
100
|
+
export const TextArea = defineComponent<TextAreaProps>('TextArea', (props) => {
|
|
101
|
+
const {
|
|
102
|
+
value, onChange, onSubmit, onCancel, onOverflow, onEdge, placeholder, maxRows = 6,
|
|
103
|
+
maxLength, autoFocus, focusId, disabled, caretTone, blink = true, wrap = 'word',
|
|
104
|
+
caretStyle = 'underline', ...rest
|
|
105
|
+
} = props;
|
|
106
|
+
|
|
107
|
+
const focus = useFocus({
|
|
108
|
+
...(focusId ? { id: focusId } : {}),
|
|
109
|
+
disabled,
|
|
110
|
+
...(autoFocus ? { autoFocus } : {}),
|
|
111
|
+
});
|
|
112
|
+
const [caret, setCaret] = useState(value.length);
|
|
113
|
+
const [lit, setLit] = useState(true);
|
|
114
|
+
|
|
115
|
+
// Only while it has the keyboard, and solid the moment it loses it: a caret
|
|
116
|
+
// blinking in a field nobody is typing into is two carets on one screen.
|
|
117
|
+
useTicker(() => setLit((on) => !on), { fps: 2, enabled: blink && focus.focused });
|
|
118
|
+
useEffect(() => { setLit(true); }, [focus.focused]);
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The caret, as a style laid over one cell.
|
|
122
|
+
*
|
|
123
|
+
* `inverse` for a block rather than a background colour, because it swaps
|
|
124
|
+
* whatever the cell already had - a caret over selected or coloured text
|
|
125
|
+
* stays legible instead of painting the theme's cursor colour over it.
|
|
126
|
+
*/
|
|
127
|
+
const mark = caretStyle === 'block'
|
|
128
|
+
? { inverse: true, fg: caretTone ?? 'cursor' }
|
|
129
|
+
: { underline: true, fg: caretTone ?? 'cursor' };
|
|
130
|
+
|
|
131
|
+
const chars = graphemes(value);
|
|
132
|
+
const position = Math.min(caret, chars.length);
|
|
133
|
+
const before = chars.slice(0, position).join('');
|
|
134
|
+
const lines = value === '' ? [''] : value.split('\n');
|
|
135
|
+
const caretLine = before.split('\n').length - 1;
|
|
136
|
+
const caretColumn = graphemes(before.split('\n').pop() ?? '').length;
|
|
137
|
+
|
|
138
|
+
/*
|
|
139
|
+
* Visual rows, which are not logical lines.
|
|
140
|
+
*
|
|
141
|
+
* Every row was drawn with `truncate: 'end'`, so a line wider than the field
|
|
142
|
+
* came out as its first few words and an ellipsis - text that had been typed
|
|
143
|
+
* and could not be read back. This is the component the docs call "the one
|
|
144
|
+
* that is a paragraph", and a paragraph wraps.
|
|
145
|
+
*
|
|
146
|
+
* So one line can occupy several rows, and everything below counts rows
|
|
147
|
+
* rather than lines: `maxRows`, the scroll offset, up and down. The
|
|
148
|
+
* inherited `wrap` prop chooses how, and any of the `truncate-*` values ask
|
|
149
|
+
* for the old single-row-per-line behaviour back.
|
|
150
|
+
*
|
|
151
|
+
* The width is the layout's, so it arrives a frame late. Until it does it is
|
|
152
|
+
* 0 and nothing wraps, which is precisely the behaviour above - and the pass
|
|
153
|
+
* that `useMeasure` schedules draws the wrapped version.
|
|
154
|
+
*/
|
|
155
|
+
const width = useMeasure().width;
|
|
156
|
+
const mode = width > 0 && (wrap === 'word' || wrap === 'char') ? wrap : 'none';
|
|
157
|
+
|
|
158
|
+
const visual: { line: number; start: number; cells: string[] }[] = [];
|
|
159
|
+
lines.forEach((line, index) => {
|
|
160
|
+
const cells = graphemes(line);
|
|
161
|
+
const starts = mode === 'none' ? [0] : breakColumns(cells, width, mode);
|
|
162
|
+
starts.forEach((start, i) => {
|
|
163
|
+
visual.push({ line: index, start, cells: cells.slice(start, starts[i + 1] ?? cells.length) });
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// A caret at the end of a row that is already full has no cell of its own
|
|
167
|
+
// to mark and no room for a spare one, so it belongs on a new empty row -
|
|
168
|
+
// which is where the next character was going to land anyway.
|
|
169
|
+
if (mode !== 'none' && index === caretLine && caretColumn === cells.length) {
|
|
170
|
+
const last = visual[visual.length - 1];
|
|
171
|
+
if (last && rowWidth(last.cells) >= width) {
|
|
172
|
+
visual.push({ line: index, start: cells.length, cells: [] });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// The *last* row starting at or before the caret. With the caret exactly on
|
|
178
|
+
// a break that is the row it begins, not the one it just filled - which is
|
|
179
|
+
// where the caret goes when a word has just been pushed onto a new row.
|
|
180
|
+
let caretRow = 0;
|
|
181
|
+
visual.forEach((row, i) => {
|
|
182
|
+
if (row.line === caretLine && row.start <= caretColumn) caretRow = i;
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const rows = Math.min(maxRows, Math.max(1, visual.length));
|
|
186
|
+
const first = Math.max(0, Math.min(caretRow - rows + 1, visual.length - rows));
|
|
187
|
+
|
|
188
|
+
const lineStart = (index: number): number =>
|
|
189
|
+
lines.slice(0, index).reduce((n, line) => n + graphemes(line).length + 1, 0);
|
|
190
|
+
|
|
191
|
+
/** The caret's column within its own row, carried onto another row. */
|
|
192
|
+
const rowCaret = (index: number): number => {
|
|
193
|
+
const row = visual[index];
|
|
194
|
+
if (!row) return position;
|
|
195
|
+
const column = Math.min(caretColumn - (visual[caretRow]?.start ?? 0), row.cells.length);
|
|
196
|
+
return lineStart(row.line) + row.start + column;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const replace = (next: string, at: number): void => {
|
|
200
|
+
const capped = maxLength !== undefined ? next.slice(0, maxLength) : next;
|
|
201
|
+
onChange(capped);
|
|
202
|
+
setCaret(Math.max(0, Math.min(graphemes(capped).length, at)));
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const insert = (text: string): void => {
|
|
206
|
+
const inserted = graphemes(text);
|
|
207
|
+
replace([...chars.slice(0, position), ...inserted, ...chars.slice(position)].join(''), position + inserted.length);
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
useInput(
|
|
211
|
+
(event: KeyEvent) => {
|
|
212
|
+
if (disabled) return false;
|
|
213
|
+
|
|
214
|
+
switch (event.name) {
|
|
215
|
+
case 'left':
|
|
216
|
+
// Off the front of the field is the caller's key, the same way up at
|
|
217
|
+
// the top is - so "left, left, left" walks out of the composer.
|
|
218
|
+
if (position === 0) { onEdge?.('start'); return true; }
|
|
219
|
+
setCaret(position - 1);
|
|
220
|
+
return true;
|
|
221
|
+
case 'right':
|
|
222
|
+
if (position === chars.length) { onEdge?.('end'); return true; }
|
|
223
|
+
setCaret(position + 1);
|
|
224
|
+
return true;
|
|
225
|
+
case 'home': setCaret(lineStart(caretLine)); return true;
|
|
226
|
+
case 'end': setCaret(lineStart(caretLine) + graphemes(lines[caretLine] ?? '').length); return true;
|
|
227
|
+
|
|
228
|
+
// By *row*, not by line. In a wrapped paragraph moving by line jumps
|
|
229
|
+
// the whole paragraph, and what is above the caret on the screen is
|
|
230
|
+
// the row above it.
|
|
231
|
+
case 'up': {
|
|
232
|
+
// At the top there is nowhere to go inside the field, so the caller
|
|
233
|
+
// gets the key - which is where "the last thing you sent" comes from.
|
|
234
|
+
if (caretRow === 0) { onOverflow?.(-1); return true; }
|
|
235
|
+
setCaret(rowCaret(caretRow - 1));
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
case 'down': {
|
|
239
|
+
if (caretRow === visual.length - 1) { onOverflow?.(1); return true; }
|
|
240
|
+
setCaret(rowCaret(caretRow + 1));
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
case 'backspace':
|
|
245
|
+
if (position > 0) replace([...chars.slice(0, position - 1), ...chars.slice(position)].join(''), position - 1);
|
|
246
|
+
return true;
|
|
247
|
+
case 'delete':
|
|
248
|
+
if (position < chars.length) replace([...chars.slice(0, position), ...chars.slice(position + 1)].join(''), position);
|
|
249
|
+
return true;
|
|
250
|
+
|
|
251
|
+
case 'enter':
|
|
252
|
+
// Alt or ctrl says "a line, not a message". `shift+enter` is not in
|
|
253
|
+
// this list on purpose: most terminals cannot tell it from enter, so
|
|
254
|
+
// binding it produces a key that works on one machine.
|
|
255
|
+
if (!onSubmit || event.alt || event.ctrl) { insert('\n'); return true; }
|
|
256
|
+
onSubmit(value);
|
|
257
|
+
setCaret(0);
|
|
258
|
+
return true;
|
|
259
|
+
|
|
260
|
+
case 'escape': onCancel?.(); return true;
|
|
261
|
+
case 'paste': insert(event.char ?? ''); return true;
|
|
262
|
+
default: break;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ctrl+j, for the one case where it is a key of its own: with the kitty
|
|
266
|
+
// protocol on it arrives as `CSI 106;5u` and is named `j`. Without it
|
|
267
|
+
// ctrl+j is 0x0a, the same byte as ctrl+enter, and the case above has
|
|
268
|
+
// already taken it - there is no encoding in which those two differ.
|
|
269
|
+
if (event.name === 'j' && event.ctrl) { insert('\n'); return true; }
|
|
270
|
+
|
|
271
|
+
if (event.char && !event.ctrl && !event.alt && !event.meta) {
|
|
272
|
+
insert(event.char);
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
return false;
|
|
276
|
+
},
|
|
277
|
+
{ focusId: focus.id, enabled: !disabled },
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
const shown = visual.slice(first, first + rows);
|
|
281
|
+
|
|
282
|
+
// The row count fixes the *content*, on an inner box, and the outer one
|
|
283
|
+
// sizes to it.
|
|
284
|
+
//
|
|
285
|
+
// `height: rows` on the outer box meant the rows and the border had to share
|
|
286
|
+
// one allowance: `<TextArea border="single"/>` came out as a top border and
|
|
287
|
+
// nothing else, because one row is what the field asked for and the border
|
|
288
|
+
// spent it. Nobody saw it because a field without a border looks the same
|
|
289
|
+
// either way, and `TextInput` fixes no height at all.
|
|
290
|
+
return h('box', {
|
|
291
|
+
id: focus.id,
|
|
292
|
+
role: 'textbox',
|
|
293
|
+
label: placeholder,
|
|
294
|
+
direction: 'column',
|
|
295
|
+
...rest,
|
|
296
|
+
}, h('box', { direction: 'column', height: rows },
|
|
297
|
+
value === ''
|
|
298
|
+
// The placeholder stays while the field is focused and empty: it is the
|
|
299
|
+
// only thing on screen saying what enter will do, and hiding it when the
|
|
300
|
+
// caret arrives hides it exactly when it is read.
|
|
301
|
+
? (() => {
|
|
302
|
+
// The caret sits on the placeholder's first cell rather than in front
|
|
303
|
+
// of it: a caret that pushed the placeholder along moved the one piece
|
|
304
|
+
// of text on the screen every time the field was focused.
|
|
305
|
+
const hint = graphemes(placeholder ?? '');
|
|
306
|
+
const head = hint.length > 0 ? (hint[0] as string) : ' ';
|
|
307
|
+
return h('box', { direction: 'row' },
|
|
308
|
+
h('text', {
|
|
309
|
+
content: head,
|
|
310
|
+
// The placeholder keeps its own colour and only takes the mark:
|
|
311
|
+
// recolouring it would make the first letter of the hint the one
|
|
312
|
+
// bright thing in an empty field.
|
|
313
|
+
...(focus.focused && lit ? { ...mark, fg: 'subtle' as const } : { fg: 'subtle' as const }),
|
|
314
|
+
}),
|
|
315
|
+
h('text', { content: hint.slice(1).join(''), fg: 'subtle', flex: 1, truncate: 'end' }));
|
|
316
|
+
})()
|
|
317
|
+
: shown.map((row, i) => {
|
|
318
|
+
const index = first + i;
|
|
319
|
+
const text = row.cells.join('');
|
|
320
|
+
if (index !== caretRow || !focus.focused) {
|
|
321
|
+
return h('text', { key: index, content: text === '' ? ' ' : text, wrap: 'none', truncate: 'end' });
|
|
322
|
+
}
|
|
323
|
+
// The caret's column inside this row. The row has already been broken
|
|
324
|
+
// to the width it will be drawn at, so the split lands where it looks.
|
|
325
|
+
//
|
|
326
|
+
// Three parts, and the middle one is the cell the caret is *on* - not
|
|
327
|
+
// an extra cell between them. Past the last character there is nothing
|
|
328
|
+
// to mark, so a space stands in; that is the one case where the caret
|
|
329
|
+
// adds a column, and it adds it at the end where nothing moves.
|
|
330
|
+
const at = caretColumn - row.start;
|
|
331
|
+
return h('box', { key: index, direction: 'row' },
|
|
332
|
+
h('text', { content: row.cells.slice(0, at).join('') }),
|
|
333
|
+
h('text', { content: row.cells[at] ?? ' ', ...(lit ? mark : {}) }),
|
|
334
|
+
h('text', { content: row.cells.slice(at + 1).join(''), flex: 1, truncate: 'end' }));
|
|
335
|
+
}),
|
|
336
|
+
));
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Where a line breaks, as columns into its own grapheme array.
|
|
341
|
+
*
|
|
342
|
+
* One entry per visual row and always starting at 0, which is what makes a
|
|
343
|
+
* wrapped row addressable: a caret is a character offset, so a wrap that only
|
|
344
|
+
* returned strings could not say which offset a row begins at. `wrapText` in
|
|
345
|
+
* core returns strings and trims each one, and both of those lose the mapping.
|
|
346
|
+
*
|
|
347
|
+
* Nothing is trimmed here for the same reason. A trailing space is a character
|
|
348
|
+
* somebody typed and can delete, so the row it sits on has to contain it -
|
|
349
|
+
* otherwise the caret drifts one cell from the text for the rest of the line.
|
|
350
|
+
*
|
|
351
|
+
* Cells rather than characters throughout: a wide glyph is two columns.
|
|
352
|
+
*/
|
|
353
|
+
function breakColumns(cells: string[], width: number, mode: 'word' | 'char'): number[] {
|
|
354
|
+
if (width <= 0 || cells.length === 0) return [0];
|
|
355
|
+
|
|
356
|
+
const starts = [0];
|
|
357
|
+
let start = 0;
|
|
358
|
+
let used = 0;
|
|
359
|
+
// The last space on the row so far, and -1 once a break has consumed it.
|
|
360
|
+
let space = -1;
|
|
361
|
+
|
|
362
|
+
for (let i = 0; i < cells.length; i++) {
|
|
363
|
+
const cell = cells[i] as string;
|
|
364
|
+
const cellWidth = stringWidth(cell);
|
|
365
|
+
|
|
366
|
+
if (used + cellWidth > width && i > start) {
|
|
367
|
+
// Break after the last space, or mid-word when the row has none - a word
|
|
368
|
+
// longer than the field has to break somewhere, and `space > start`
|
|
369
|
+
// rather than `>=` keeps a row that begins with a space from being one.
|
|
370
|
+
const at = mode === 'word' && space > start ? space + 1 : i;
|
|
371
|
+
starts.push(at);
|
|
372
|
+
start = at;
|
|
373
|
+
space = -1;
|
|
374
|
+
used = 0;
|
|
375
|
+
for (let j = at; j < i; j++) used += stringWidth(cells[j] as string);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (/\s/.test(cell)) space = i;
|
|
379
|
+
used += cellWidth;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return starts;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** A row's width in cells, which is not its length in characters. */
|
|
386
|
+
function rowWidth(cells: string[]): number {
|
|
387
|
+
let width = 0;
|
|
388
|
+
for (const cell of cells) width += stringWidth(cell);
|
|
389
|
+
return width;
|
|
390
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type { KeyEvent } from '@textui/core';
|
|
2
|
+
import {
|
|
3
|
+
defineComponent,
|
|
4
|
+
graphemes,
|
|
5
|
+
h,
|
|
6
|
+
sliceColumns,
|
|
7
|
+
stringWidth,
|
|
8
|
+
useFocus,
|
|
9
|
+
useInput,
|
|
10
|
+
useMeasure,
|
|
11
|
+
useState,
|
|
12
|
+
useTheme,
|
|
13
|
+
} from '@textui/core';
|
|
14
|
+
import type { TextInputProps } from './shared.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A single-line text field.
|
|
18
|
+
*
|
|
19
|
+
* The caret is a real cursor position published to the renderer when the
|
|
20
|
+
* terminal has a cursor, and a drawn glyph when it does not - so a text field
|
|
21
|
+
* still shows where typing lands over a link that swallowed cursor control.
|
|
22
|
+
*/
|
|
23
|
+
export const TextInput = defineComponent<TextInputProps>('TextInput', (props) => {
|
|
24
|
+
const theme = useTheme();
|
|
25
|
+
const {
|
|
26
|
+
value, onChange, onSubmit, onEdge, placeholder, label, hideLabel, mask, maxLength,
|
|
27
|
+
autoFocus, search, disabled, focusId, ...rest
|
|
28
|
+
} = props;
|
|
29
|
+
const inlineLabel = label && !search && !hideLabel ? label : undefined;
|
|
30
|
+
|
|
31
|
+
const focus = useFocus({ ...(focusId ? { id: focusId } : {}), disabled, autoFocus });
|
|
32
|
+
const measured = useMeasure();
|
|
33
|
+
const [caret, setCaret] = useState(value.length);
|
|
34
|
+
const [scroll, setScroll] = useState(0);
|
|
35
|
+
|
|
36
|
+
const chars = graphemes(value);
|
|
37
|
+
const position = Math.min(caret, chars.length);
|
|
38
|
+
const shown = mask ? mask.repeat(chars.length) : value;
|
|
39
|
+
const empty = shown === '';
|
|
40
|
+
|
|
41
|
+
// Everything drawn before the value pushes the caret along with it. Counting
|
|
42
|
+
// the search glyph but not the label is what leaves the caret several cells
|
|
43
|
+
// to the left of where the typing lands.
|
|
44
|
+
const lead =
|
|
45
|
+
(search ? stringWidth(theme.glyphs.search) + 1 : 0) +
|
|
46
|
+
(inlineLabel ? stringWidth(inlineLabel) + 1 : 0);
|
|
47
|
+
|
|
48
|
+
const total = stringWidth(shown);
|
|
49
|
+
const field = measured.width > 0
|
|
50
|
+
? Math.max(1, measured.width - lead)
|
|
51
|
+
: Math.max(1, total);
|
|
52
|
+
|
|
53
|
+
// Cells, not characters: a wide glyph is two columns of caret travel.
|
|
54
|
+
const columnAt = (index: number): number => stringWidth(chars.slice(0, index).join(''));
|
|
55
|
+
const caretColumn = columnAt(position);
|
|
56
|
+
const anchored = Math.max(0, Math.min(scroll, caretColumn, Math.max(0, total - field + 1)));
|
|
57
|
+
const start = caretColumn > anchored + field - 1 ? caretColumn - field + 1 : anchored;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Move the caret and scroll the field just enough to keep it in view.
|
|
61
|
+
*
|
|
62
|
+
* `limit` is the length the value is about to have: `chars` still holds the
|
|
63
|
+
* old one during the render that inserts a character, and clamping against
|
|
64
|
+
* that is what would pin the caret behind every keystroke.
|
|
65
|
+
*/
|
|
66
|
+
const move = (next: number, limit = chars.length): void => {
|
|
67
|
+
const clamped = Math.max(0, Math.min(limit, next));
|
|
68
|
+
setCaret(clamped);
|
|
69
|
+
const column = columnAt(clamped);
|
|
70
|
+
if (column < start) setScroll(column);
|
|
71
|
+
else if (column > start + field - 1) setScroll(column - field + 1);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
useInput(
|
|
75
|
+
(event: KeyEvent) => {
|
|
76
|
+
if (disabled) return false;
|
|
77
|
+
|
|
78
|
+
switch (event.name) {
|
|
79
|
+
// Off either end is the caller's key, the same way it is in a
|
|
80
|
+
// `TextArea`. Reported rather than passed on, because the field is
|
|
81
|
+
// focused and a handler beside it would never see the event.
|
|
82
|
+
case 'left':
|
|
83
|
+
if (position === 0) { onEdge?.('start'); return true; }
|
|
84
|
+
move(position - 1);
|
|
85
|
+
return true;
|
|
86
|
+
case 'right':
|
|
87
|
+
if (position === chars.length) { onEdge?.('end'); return true; }
|
|
88
|
+
move(position + 1);
|
|
89
|
+
return true;
|
|
90
|
+
case 'home': move(0); return true;
|
|
91
|
+
case 'end': move(chars.length); return true;
|
|
92
|
+
case 'backspace':
|
|
93
|
+
if (position > 0) {
|
|
94
|
+
onChange?.([...chars.slice(0, position - 1), ...chars.slice(position)].join(''));
|
|
95
|
+
move(position - 1);
|
|
96
|
+
}
|
|
97
|
+
return true;
|
|
98
|
+
case 'delete':
|
|
99
|
+
if (position < chars.length) {
|
|
100
|
+
onChange?.([...chars.slice(0, position), ...chars.slice(position + 1)].join(''));
|
|
101
|
+
}
|
|
102
|
+
return true;
|
|
103
|
+
case 'enter':
|
|
104
|
+
onSubmit?.(value);
|
|
105
|
+
return true;
|
|
106
|
+
case 'paste': {
|
|
107
|
+
const text = event.char ?? '';
|
|
108
|
+
const inserted = graphemes(text).length;
|
|
109
|
+
const next = [...chars.slice(0, position), ...graphemes(text), ...chars.slice(position)].join('');
|
|
110
|
+
onChange?.(maxLength ? next.slice(0, maxLength) : next);
|
|
111
|
+
move(position + inserted, chars.length + inserted);
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
default: break;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// A printable character. Modifiers mean it was a shortcut, not typing.
|
|
118
|
+
if (event.char && !event.ctrl && !event.alt && !event.meta) {
|
|
119
|
+
if (maxLength !== undefined && chars.length >= maxLength) return true;
|
|
120
|
+
onChange?.([...chars.slice(0, position), event.char, ...chars.slice(position)].join(''));
|
|
121
|
+
move(position + 1, chars.length + 1);
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
return false;
|
|
125
|
+
},
|
|
126
|
+
{ focusId: focus.id },
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
return h('box', {
|
|
130
|
+
id: focus.id,
|
|
131
|
+
role: search ? 'searchbox' : 'textbox',
|
|
132
|
+
label,
|
|
133
|
+
direction: 'row',
|
|
134
|
+
gap: 1,
|
|
135
|
+
border: { style: theme.border, color: focus.focused ? 'focus' : 'border' },
|
|
136
|
+
padding: [0, 1],
|
|
137
|
+
cursor: focus.focused ? lead + caretColumn - start : undefined,
|
|
138
|
+
...rest,
|
|
139
|
+
},
|
|
140
|
+
search ? h('text', { content: theme.glyphs.search, fg: 'muted' }) : null,
|
|
141
|
+
inlineLabel ? h('text', { content: inlineLabel, fg: 'muted' }) : null,
|
|
142
|
+
h('text', {
|
|
143
|
+
content: empty
|
|
144
|
+
? sliceColumns(placeholder ?? '', 0, field)
|
|
145
|
+
: sliceColumns(shown, start, field),
|
|
146
|
+
fg: empty ? 'subtle' : disabled ? 'disabled' : undefined,
|
|
147
|
+
flex: 1,
|
|
148
|
+
}),
|
|
149
|
+
);
|
|
150
|
+
});
|