1293
codex-cli/src/components/vendor/cli-spinners/index.js
vendored
Normal file
1293
codex-cli/src/components/vendor/cli-spinners/index.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
codex-cli/src/components/vendor/ink-select/index.js
vendored
Normal file
1
codex-cli/src/components/vendor/ink-select/index.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./select.js";
|
||||
26
codex-cli/src/components/vendor/ink-select/option-map.js
vendored
Normal file
26
codex-cli/src/components/vendor/ink-select/option-map.js
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
export default class OptionMap extends Map {
|
||||
first;
|
||||
constructor(options) {
|
||||
const items = [];
|
||||
let firstItem;
|
||||
let previous;
|
||||
let index = 0;
|
||||
for (const option of options) {
|
||||
const item = {
|
||||
...option,
|
||||
previous,
|
||||
next: undefined,
|
||||
index,
|
||||
};
|
||||
if (previous) {
|
||||
previous.next = item;
|
||||
}
|
||||
firstItem ||= item;
|
||||
items.push([option.value, item]);
|
||||
index++;
|
||||
previous = item;
|
||||
}
|
||||
super(items);
|
||||
this.first = firstItem;
|
||||
}
|
||||
}
|
||||
27
codex-cli/src/components/vendor/ink-select/select-option.js
vendored
Normal file
27
codex-cli/src/components/vendor/ink-select/select-option.js
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
import React from "react";
|
||||
import { Box, Text } from "ink";
|
||||
import figures from "figures";
|
||||
import { styles } from "./theme";
|
||||
export function SelectOption({ isFocused, isSelected, children }) {
|
||||
return React.createElement(
|
||||
Box,
|
||||
{ ...styles.option({ isFocused }) },
|
||||
isFocused &&
|
||||
React.createElement(
|
||||
Text,
|
||||
{ ...styles.focusIndicator() },
|
||||
figures.pointer,
|
||||
),
|
||||
React.createElement(
|
||||
Text,
|
||||
{ ...styles.label({ isFocused, isSelected }) },
|
||||
children,
|
||||
),
|
||||
isSelected &&
|
||||
React.createElement(
|
||||
Text,
|
||||
{ ...styles.selectedIndicator() },
|
||||
figures.tick,
|
||||
),
|
||||
);
|
||||
}
|
||||
53
codex-cli/src/components/vendor/ink-select/select.js
vendored
Normal file
53
codex-cli/src/components/vendor/ink-select/select.js
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
import React from "react";
|
||||
import { Box, Text } from "ink";
|
||||
import { styles } from "./theme";
|
||||
import { SelectOption } from "./select-option";
|
||||
import { useSelectState } from "./use-select-state";
|
||||
import { useSelect } from "./use-select";
|
||||
export function Select({
|
||||
isDisabled = false,
|
||||
visibleOptionCount = 5,
|
||||
highlightText,
|
||||
options,
|
||||
defaultValue,
|
||||
onChange,
|
||||
}) {
|
||||
const state = useSelectState({
|
||||
visibleOptionCount,
|
||||
options,
|
||||
defaultValue,
|
||||
onChange,
|
||||
});
|
||||
useSelect({ isDisabled, state });
|
||||
return React.createElement(
|
||||
Box,
|
||||
{ ...styles.container() },
|
||||
state.visibleOptions.map((option) => {
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
let label = option.label;
|
||||
if (highlightText && option.label.includes(highlightText)) {
|
||||
const index = option.label.indexOf(highlightText);
|
||||
label = React.createElement(
|
||||
React.Fragment,
|
||||
null,
|
||||
option.label.slice(0, index),
|
||||
React.createElement(
|
||||
Text,
|
||||
{ ...styles.highlightedText() },
|
||||
highlightText,
|
||||
),
|
||||
option.label.slice(index + highlightText.length),
|
||||
);
|
||||
}
|
||||
return React.createElement(
|
||||
SelectOption,
|
||||
{
|
||||
key: option.value,
|
||||
isFocused: !isDisabled && state.focusedValue === option.value,
|
||||
isSelected: state.value === option.value,
|
||||
},
|
||||
label,
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
32
codex-cli/src/components/vendor/ink-select/theme.js
vendored
Normal file
32
codex-cli/src/components/vendor/ink-select/theme.js
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
const theme = {
|
||||
styles: {
|
||||
container: () => ({
|
||||
flexDirection: "column",
|
||||
}),
|
||||
option: ({ isFocused }) => ({
|
||||
gap: 1,
|
||||
paddingLeft: isFocused ? 0 : 2,
|
||||
}),
|
||||
selectedIndicator: () => ({
|
||||
color: "green",
|
||||
}),
|
||||
focusIndicator: () => ({
|
||||
color: "blue",
|
||||
}),
|
||||
label({ isFocused, isSelected }) {
|
||||
let color;
|
||||
if (isSelected) {
|
||||
color = "green";
|
||||
}
|
||||
if (isFocused) {
|
||||
color = "blue";
|
||||
}
|
||||
return { color };
|
||||
},
|
||||
highlightedText: () => ({
|
||||
bold: true,
|
||||
}),
|
||||
},
|
||||
};
|
||||
export const styles = theme.styles;
|
||||
export default theme;
|
||||
158
codex-cli/src/components/vendor/ink-select/use-select-state.js
vendored
Normal file
158
codex-cli/src/components/vendor/ink-select/use-select-state.js
vendored
Normal file
@@ -0,0 +1,158 @@
|
||||
import { isDeepStrictEqual } from "node:util";
|
||||
import { useReducer, useCallback, useMemo, useState, useEffect } from "react";
|
||||
import OptionMap from "./option-map";
|
||||
const reducer = (state, action) => {
|
||||
switch (action.type) {
|
||||
case "focus-next-option": {
|
||||
if (!state.focusedValue) {
|
||||
return state;
|
||||
}
|
||||
const item = state.optionMap.get(state.focusedValue);
|
||||
if (!item) {
|
||||
return state;
|
||||
}
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
const next = item.next;
|
||||
if (!next) {
|
||||
return state;
|
||||
}
|
||||
const needsToScroll = next.index >= state.visibleToIndex;
|
||||
if (!needsToScroll) {
|
||||
return {
|
||||
...state,
|
||||
focusedValue: next.value,
|
||||
};
|
||||
}
|
||||
const nextVisibleToIndex = Math.min(
|
||||
state.optionMap.size,
|
||||
state.visibleToIndex + 1,
|
||||
);
|
||||
const nextVisibleFromIndex =
|
||||
nextVisibleToIndex - state.visibleOptionCount;
|
||||
return {
|
||||
...state,
|
||||
focusedValue: next.value,
|
||||
visibleFromIndex: nextVisibleFromIndex,
|
||||
visibleToIndex: nextVisibleToIndex,
|
||||
};
|
||||
}
|
||||
case "focus-previous-option": {
|
||||
if (!state.focusedValue) {
|
||||
return state;
|
||||
}
|
||||
const item = state.optionMap.get(state.focusedValue);
|
||||
if (!item) {
|
||||
return state;
|
||||
}
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
const previous = item.previous;
|
||||
if (!previous) {
|
||||
return state;
|
||||
}
|
||||
const needsToScroll = previous.index <= state.visibleFromIndex;
|
||||
if (!needsToScroll) {
|
||||
return {
|
||||
...state,
|
||||
focusedValue: previous.value,
|
||||
};
|
||||
}
|
||||
const nextVisibleFromIndex = Math.max(0, state.visibleFromIndex - 1);
|
||||
const nextVisibleToIndex =
|
||||
nextVisibleFromIndex + state.visibleOptionCount;
|
||||
return {
|
||||
...state,
|
||||
focusedValue: previous.value,
|
||||
visibleFromIndex: nextVisibleFromIndex,
|
||||
visibleToIndex: nextVisibleToIndex,
|
||||
};
|
||||
}
|
||||
case "select-focused-option": {
|
||||
return {
|
||||
...state,
|
||||
previousValue: state.value,
|
||||
value: state.focusedValue,
|
||||
};
|
||||
}
|
||||
case "reset": {
|
||||
return action.state;
|
||||
}
|
||||
}
|
||||
};
|
||||
const createDefaultState = ({
|
||||
visibleOptionCount: customVisibleOptionCount,
|
||||
defaultValue,
|
||||
options,
|
||||
}) => {
|
||||
const visibleOptionCount =
|
||||
typeof customVisibleOptionCount === "number"
|
||||
? Math.min(customVisibleOptionCount, options.length)
|
||||
: options.length;
|
||||
const optionMap = new OptionMap(options);
|
||||
return {
|
||||
optionMap,
|
||||
visibleOptionCount,
|
||||
focusedValue: optionMap.first?.value,
|
||||
visibleFromIndex: 0,
|
||||
visibleToIndex: visibleOptionCount,
|
||||
previousValue: defaultValue,
|
||||
value: defaultValue,
|
||||
};
|
||||
};
|
||||
export const useSelectState = ({
|
||||
visibleOptionCount = 5,
|
||||
options,
|
||||
defaultValue,
|
||||
onChange,
|
||||
}) => {
|
||||
const [state, dispatch] = useReducer(
|
||||
reducer,
|
||||
{ visibleOptionCount, defaultValue, options },
|
||||
createDefaultState,
|
||||
);
|
||||
const [lastOptions, setLastOptions] = useState(options);
|
||||
if (options !== lastOptions && !isDeepStrictEqual(options, lastOptions)) {
|
||||
dispatch({
|
||||
type: "reset",
|
||||
state: createDefaultState({ visibleOptionCount, defaultValue, options }),
|
||||
});
|
||||
setLastOptions(options);
|
||||
}
|
||||
const focusNextOption = useCallback(() => {
|
||||
dispatch({
|
||||
type: "focus-next-option",
|
||||
});
|
||||
}, []);
|
||||
const focusPreviousOption = useCallback(() => {
|
||||
dispatch({
|
||||
type: "focus-previous-option",
|
||||
});
|
||||
}, []);
|
||||
const selectFocusedOption = useCallback(() => {
|
||||
dispatch({
|
||||
type: "select-focused-option",
|
||||
});
|
||||
}, []);
|
||||
const visibleOptions = useMemo(() => {
|
||||
return options
|
||||
.map((option, index) => ({
|
||||
...option,
|
||||
index,
|
||||
}))
|
||||
.slice(state.visibleFromIndex, state.visibleToIndex);
|
||||
}, [options, state.visibleFromIndex, state.visibleToIndex]);
|
||||
useEffect(() => {
|
||||
if (state.value && state.previousValue !== state.value) {
|
||||
onChange?.(state.value);
|
||||
}
|
||||
}, [state.previousValue, state.value, options, onChange]);
|
||||
return {
|
||||
focusedValue: state.focusedValue,
|
||||
visibleFromIndex: state.visibleFromIndex,
|
||||
visibleToIndex: state.visibleToIndex,
|
||||
value: state.value,
|
||||
visibleOptions,
|
||||
focusNextOption,
|
||||
focusPreviousOption,
|
||||
selectFocusedOption,
|
||||
};
|
||||
};
|
||||
17
codex-cli/src/components/vendor/ink-select/use-select.js
vendored
Normal file
17
codex-cli/src/components/vendor/ink-select/use-select.js
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
import { useInput } from "ink";
|
||||
export const useSelect = ({ isDisabled = false, state }) => {
|
||||
useInput(
|
||||
(_input, key) => {
|
||||
if (key.downArrow) {
|
||||
state.focusNextOption();
|
||||
}
|
||||
if (key.upArrow) {
|
||||
state.focusPreviousOption();
|
||||
}
|
||||
if (key.return) {
|
||||
state.selectFocusedOption();
|
||||
}
|
||||
},
|
||||
{ isActive: !isDisabled },
|
||||
);
|
||||
};
|
||||
36
codex-cli/src/components/vendor/ink-spinner.tsx
vendored
Normal file
36
codex-cli/src/components/vendor/ink-spinner.tsx
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Text } from "ink";
|
||||
import React, { useState } from "react";
|
||||
import { useInterval } from "use-interval";
|
||||
|
||||
const spinnerTypes: Record<string, string[]> = {
|
||||
dots: ["⢎ ", "⠎⠁", "⠊⠑", "⠈⠱", " ⡱", "⢀⡰", "⢄⡠", "⢆⡀"],
|
||||
ball: [
|
||||
"( ● )",
|
||||
"( ● )",
|
||||
"( ● )",
|
||||
"( ● )",
|
||||
"( ●)",
|
||||
"( ● )",
|
||||
"( ● )",
|
||||
"( ● )",
|
||||
"( ● )",
|
||||
"(● )",
|
||||
],
|
||||
};
|
||||
|
||||
export default function Spinner({
|
||||
type = "dots",
|
||||
}: {
|
||||
type?: string;
|
||||
}): JSX.Element {
|
||||
const frames = spinnerTypes[type || "dots"] || [];
|
||||
const interval = 80;
|
||||
const [frame, setFrame] = useState(0);
|
||||
useInterval(() => {
|
||||
setFrame((previousFrame) => {
|
||||
const isLastFrame = previousFrame === frames.length - 1;
|
||||
return isLastFrame ? 0 : previousFrame + 1;
|
||||
});
|
||||
}, interval);
|
||||
return <Text>{frames[frame]}</Text>;
|
||||
}
|
||||
338
codex-cli/src/components/vendor/ink-text-input.tsx
vendored
Normal file
338
codex-cli/src/components/vendor/ink-text-input.tsx
vendored
Normal file
@@ -0,0 +1,338 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Text, useInput } from "ink";
|
||||
import chalk from "chalk";
|
||||
import type { Except } from "type-fest";
|
||||
|
||||
export type TextInputProps = {
|
||||
/**
|
||||
* Text to display when `value` is empty.
|
||||
*/
|
||||
readonly placeholder?: string;
|
||||
|
||||
/**
|
||||
* Listen to user's input. Useful in case there are multiple input components
|
||||
* at the same time and input must be "routed" to a specific component.
|
||||
*/
|
||||
readonly focus?: boolean; // eslint-disable-line react/boolean-prop-naming
|
||||
|
||||
/**
|
||||
* Replace all chars and mask the value. Useful for password inputs.
|
||||
*/
|
||||
readonly mask?: string;
|
||||
|
||||
/**
|
||||
* Whether to show cursor and allow navigation inside text input with arrow keys.
|
||||
*/
|
||||
readonly showCursor?: boolean; // eslint-disable-line react/boolean-prop-naming
|
||||
|
||||
/**
|
||||
* Highlight pasted text
|
||||
*/
|
||||
readonly highlightPastedText?: boolean; // eslint-disable-line react/boolean-prop-naming
|
||||
|
||||
/**
|
||||
* Value to display in a text input.
|
||||
*/
|
||||
readonly value: string;
|
||||
|
||||
/**
|
||||
* Function to call when value updates.
|
||||
*/
|
||||
readonly onChange: (value: string) => void;
|
||||
|
||||
/**
|
||||
* Function to call when `Enter` is pressed, where first argument is a value of the input.
|
||||
*/
|
||||
readonly onSubmit?: (value: string) => void;
|
||||
};
|
||||
|
||||
function findPrevWordJump(prompt: string, cursorOffset: number) {
|
||||
const regex = /[\s,.;!?]+/g;
|
||||
let lastMatch = 0;
|
||||
let currentMatch: RegExpExecArray | null;
|
||||
|
||||
const stringToCursorOffset = prompt
|
||||
.slice(0, cursorOffset)
|
||||
.replace(/[\s,.;!?]+$/, "");
|
||||
|
||||
// Loop through all matches
|
||||
while ((currentMatch = regex.exec(stringToCursorOffset)) !== null) {
|
||||
lastMatch = currentMatch.index;
|
||||
}
|
||||
|
||||
// Include the last match unless it is the first character
|
||||
if (lastMatch != 0) {
|
||||
lastMatch += 1;
|
||||
}
|
||||
return lastMatch;
|
||||
}
|
||||
|
||||
function findNextWordJump(prompt: string, cursorOffset: number) {
|
||||
const regex = /[\s,.;!?]+/g;
|
||||
let currentMatch: RegExpExecArray | null;
|
||||
|
||||
// Loop through all matches
|
||||
while ((currentMatch = regex.exec(prompt)) !== null) {
|
||||
if (currentMatch.index > cursorOffset) {
|
||||
return currentMatch.index + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return prompt.length;
|
||||
}
|
||||
|
||||
function TextInput({
|
||||
value: originalValue,
|
||||
placeholder = "",
|
||||
focus = true,
|
||||
mask,
|
||||
highlightPastedText = false,
|
||||
showCursor = true,
|
||||
onChange,
|
||||
onSubmit,
|
||||
}: TextInputProps) {
|
||||
const [state, setState] = useState({
|
||||
cursorOffset: (originalValue || "").length,
|
||||
cursorWidth: 0,
|
||||
});
|
||||
|
||||
const { cursorOffset, cursorWidth } = state;
|
||||
|
||||
useEffect(() => {
|
||||
setState((previousState) => {
|
||||
if (!focus || !showCursor) {
|
||||
return previousState;
|
||||
}
|
||||
|
||||
const newValue = originalValue || "";
|
||||
|
||||
if (previousState.cursorOffset > newValue.length - 1) {
|
||||
return {
|
||||
cursorOffset: newValue.length,
|
||||
cursorWidth: 0,
|
||||
};
|
||||
}
|
||||
|
||||
return previousState;
|
||||
});
|
||||
}, [originalValue, focus, showCursor]);
|
||||
|
||||
const cursorActualWidth = highlightPastedText ? cursorWidth : 0;
|
||||
|
||||
const value = mask ? mask.repeat(originalValue.length) : originalValue;
|
||||
let renderedValue = value;
|
||||
let renderedPlaceholder = placeholder ? chalk.grey(placeholder) : undefined;
|
||||
|
||||
// Fake mouse cursor, because it's too inconvenient to deal with actual cursor and ansi escapes.
|
||||
if (showCursor && focus) {
|
||||
renderedPlaceholder =
|
||||
placeholder.length > 0
|
||||
? chalk.inverse(placeholder[0]) + chalk.grey(placeholder.slice(1))
|
||||
: chalk.inverse(" ");
|
||||
|
||||
renderedValue = value.length > 0 ? "" : chalk.inverse(" ");
|
||||
|
||||
let i = 0;
|
||||
|
||||
for (const char of value) {
|
||||
renderedValue +=
|
||||
i >= cursorOffset - cursorActualWidth && i <= cursorOffset
|
||||
? chalk.inverse(char)
|
||||
: char;
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
if (value.length > 0 && cursorOffset === value.length) {
|
||||
renderedValue += chalk.inverse(" ");
|
||||
}
|
||||
}
|
||||
|
||||
useInput(
|
||||
(input, key) => {
|
||||
if (
|
||||
key.upArrow ||
|
||||
key.downArrow ||
|
||||
(key.ctrl && input === "c") ||
|
||||
key.tab ||
|
||||
(key.shift && key.tab)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
let nextCursorOffset = cursorOffset;
|
||||
let nextValue = originalValue;
|
||||
let nextCursorWidth = 0;
|
||||
|
||||
// TODO: continue improving the cursor management to feel native
|
||||
if (key.return) {
|
||||
if (key.meta) {
|
||||
// This does not work yet. We would like to have this behavior:
|
||||
// Mac terminal: Settings → Profiles → Keyboard → Use Option as Meta key
|
||||
// iTerm2: Open Settings → Profiles → Keys → General → Set Left/Right Option as Esc+
|
||||
// And then when Option+ENTER is pressed, we want to insert a newline.
|
||||
// However, even with the settings, the input="\n" and only key.shift is True.
|
||||
// This is likely an artifact of how ink works.
|
||||
nextValue =
|
||||
originalValue.slice(0, cursorOffset) +
|
||||
"\n" +
|
||||
originalValue.slice(cursorOffset, originalValue.length);
|
||||
nextCursorOffset++;
|
||||
} else {
|
||||
// Handle Enter key: support bash-style line continuation with backslash
|
||||
// -- count consecutive backslashes immediately before cursor
|
||||
// -- only a single trailing backslash at end indicates line continuation
|
||||
const isAtEnd = cursorOffset === originalValue.length;
|
||||
const trailingMatch = originalValue.match(/\\+$/);
|
||||
const trailingCount = trailingMatch ? trailingMatch[0].length : 0;
|
||||
if (isAtEnd && trailingCount === 1) {
|
||||
nextValue += "\n";
|
||||
nextCursorOffset = nextValue.length;
|
||||
nextCursorWidth = 0;
|
||||
} else if (onSubmit) {
|
||||
onSubmit(originalValue);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else if ((key.ctrl && input === "a") || (key.meta && key.leftArrow)) {
|
||||
nextCursorOffset = 0;
|
||||
} else if ((key.ctrl && input === "e") || (key.meta && key.rightArrow)) {
|
||||
// Move cursor to end of line
|
||||
nextCursorOffset = originalValue.length;
|
||||
// Emacs/readline-style navigation and editing shortcuts
|
||||
} else if (key.ctrl && input === "b") {
|
||||
// Move cursor backward by one
|
||||
if (showCursor) {
|
||||
nextCursorOffset = Math.max(cursorOffset - 1, 0);
|
||||
}
|
||||
} else if (key.ctrl && input === "f") {
|
||||
// Move cursor forward by one
|
||||
if (showCursor) {
|
||||
nextCursorOffset = Math.min(cursorOffset + 1, originalValue.length);
|
||||
}
|
||||
} else if (key.ctrl && input === "d") {
|
||||
// Delete character at cursor (forward delete)
|
||||
if (cursorOffset < originalValue.length) {
|
||||
nextValue =
|
||||
originalValue.slice(0, cursorOffset) +
|
||||
originalValue.slice(cursorOffset + 1);
|
||||
}
|
||||
} else if (key.ctrl && input === "k") {
|
||||
// Kill text from cursor to end of line
|
||||
nextValue = originalValue.slice(0, cursorOffset);
|
||||
} else if (key.ctrl && input === "u") {
|
||||
// Kill text from start to cursor
|
||||
nextValue = originalValue.slice(cursorOffset);
|
||||
nextCursorOffset = 0;
|
||||
} else if (key.ctrl && input === "w") {
|
||||
// Delete the word before cursor
|
||||
{
|
||||
const left = originalValue.slice(0, cursorOffset);
|
||||
const match = left.match(/\s*\S+$/);
|
||||
const cut = match ? match[0].length : cursorOffset;
|
||||
nextValue =
|
||||
originalValue.slice(0, cursorOffset - cut) +
|
||||
originalValue.slice(cursorOffset);
|
||||
nextCursorOffset = cursorOffset - cut;
|
||||
}
|
||||
} else if (key.meta && (key.backspace || key.delete)) {
|
||||
const regex = /[\s,.;!?]+/g;
|
||||
let lastMatch = 0;
|
||||
let currentMatch: RegExpExecArray | null;
|
||||
|
||||
const stringToCursorOffset = originalValue
|
||||
.slice(0, cursorOffset)
|
||||
.replace(/[\s,.;!?]+$/, "");
|
||||
|
||||
// Loop through all matches
|
||||
while ((currentMatch = regex.exec(stringToCursorOffset)) !== null) {
|
||||
lastMatch = currentMatch.index;
|
||||
}
|
||||
|
||||
// Include the last match unless it is the first character
|
||||
if (lastMatch != 0) {
|
||||
lastMatch += 1;
|
||||
}
|
||||
|
||||
nextValue =
|
||||
stringToCursorOffset.slice(0, lastMatch) +
|
||||
originalValue.slice(cursorOffset, originalValue.length);
|
||||
nextCursorOffset = lastMatch;
|
||||
} else if (key.meta && (input === "b" || key.leftArrow)) {
|
||||
nextCursorOffset = findPrevWordJump(originalValue, cursorOffset);
|
||||
} else if (key.meta && (input === "f" || key.rightArrow)) {
|
||||
nextCursorOffset = findNextWordJump(originalValue, cursorOffset);
|
||||
} else if (key.leftArrow) {
|
||||
if (showCursor) {
|
||||
nextCursorOffset--;
|
||||
}
|
||||
} else if (key.rightArrow) {
|
||||
if (showCursor) {
|
||||
nextCursorOffset++;
|
||||
}
|
||||
} else if (key.backspace || key.delete) {
|
||||
if (cursorOffset > 0) {
|
||||
nextValue =
|
||||
originalValue.slice(0, cursorOffset - 1) +
|
||||
originalValue.slice(cursorOffset, originalValue.length);
|
||||
|
||||
nextCursorOffset--;
|
||||
}
|
||||
} else {
|
||||
nextValue =
|
||||
originalValue.slice(0, cursorOffset) +
|
||||
input +
|
||||
originalValue.slice(cursorOffset, originalValue.length);
|
||||
|
||||
nextCursorOffset += input.length;
|
||||
|
||||
if (input.length > 1) {
|
||||
nextCursorWidth = input.length;
|
||||
}
|
||||
}
|
||||
|
||||
if (cursorOffset < 0) {
|
||||
nextCursorOffset = 0;
|
||||
}
|
||||
|
||||
if (cursorOffset > originalValue.length) {
|
||||
nextCursorOffset = originalValue.length;
|
||||
}
|
||||
|
||||
setState({
|
||||
cursorOffset: nextCursorOffset,
|
||||
cursorWidth: nextCursorWidth,
|
||||
});
|
||||
|
||||
if (nextValue !== originalValue) {
|
||||
onChange(nextValue);
|
||||
}
|
||||
},
|
||||
{ isActive: focus },
|
||||
);
|
||||
|
||||
return (
|
||||
<Text>
|
||||
{placeholder
|
||||
? value.length > 0
|
||||
? renderedValue
|
||||
: renderedPlaceholder
|
||||
: renderedValue}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
export default TextInput;
|
||||
|
||||
type UncontrolledProps = {
|
||||
readonly initialValue?: string;
|
||||
} & Except<TextInputProps, "value" | "onChange">;
|
||||
|
||||
export function UncontrolledTextInput({
|
||||
initialValue = "",
|
||||
...props
|
||||
}: UncontrolledProps) {
|
||||
const [value, setValue] = useState(initialValue);
|
||||
|
||||
return <TextInput {...props} value={value} onChange={setValue} />;
|
||||
}
|
||||
Reference in New Issue
Block a user