Skip to content

Commit fbaf83c

Browse files
authored
feat: Input component
1 parent 370f62a commit fbaf83c

File tree

4 files changed

+204
-16
lines changed

4 files changed

+204
-16
lines changed

packages/core/src/composables/utils.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,19 @@ import { onMounted, onUnmounted, ref } from '@vue/runtime-core'
22

33
export function useInterval(fn: () => void, interval?: number) {
44
let handle: ReturnType<typeof setInterval>
5-
onMounted(() => {
5+
6+
function restart() {
7+
stop()
68
handle = setInterval(fn, interval)
7-
})
8-
onUnmounted(() => {
9+
}
10+
function stop() {
911
clearInterval(handle)
10-
})
12+
}
13+
14+
onMounted(restart)
15+
onUnmounted(stop)
16+
17+
return { restart, stop }
1118
}
1219

1320
export function useTimeout(fn: () => void, delay?: number) {

packages/playground/components.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ declare module '@vue/runtime-core' {
1010
Box: typeof import('vue-termui')['TuiBox']
1111
Br: typeof import('vue-termui')['TuiNewline']
1212
Div: typeof import('vue-termui')['TuiBox']
13-
Input: typeof import('vue-termui')['TuiInput']
13+
Input: typeof import('./src/components/Input.vue')['default']
1414
Link: typeof import('vue-termui')['TuiLink']
1515
Newline: typeof import('vue-termui')['TuiNewline']
1616
Progressbar: typeof import('vue-termui')['TuiProgressBar']

packages/playground/src/InputDemo.vue

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
11
<script setup lang="ts">
2-
const value = ref('')
3-
const isFocus = ref(true)
4-
const value1 = ref('')
5-
const isFocus1 = ref(false)
2+
import { onKeyData, ref } from 'vue-termui'
3+
import MyInput from './components/Input.vue'
64
7-
watch(value, (v: string) => {
8-
if (v === 'hello') {
9-
isFocus.value = false
10-
isFocus1.value = true
11-
}
5+
const msg = ref('Hello World')
6+
7+
const disabled = ref(false)
8+
onKeyData(['d', 'D'], () => {
9+
disabled.value = !disabled.value
1210
})
1311
</script>
1412

1513
<template borderStyle="round">
16-
<Input v-model="value" :focus="isFocus" />
17-
<Input v-model="value1" :focus="isFocus1" type="password" />
14+
<Text bold>{{ msg }}</Text>
15+
<MyInput label="Enter a message: " v-model="msg" :disabled="disabled" />
16+
<MyInput type="password" />
1817
</template>
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
<script setup lang="ts">
2+
import {
3+
computed,
4+
getCurrentInstance,
5+
isKeyDataEvent,
6+
onInputData,
7+
onKeyData,
8+
onMounted,
9+
ref,
10+
toRef,
11+
toRefs,
12+
useFocus,
13+
useInterval,
14+
watch,
15+
} from 'vue-termui'
16+
17+
const emit = defineEmits<{
18+
(event: 'update:modelValue', value: string): void
19+
}>()
20+
21+
const props = withDefaults(
22+
defineProps<{
23+
modelValue?: string
24+
disabled?: boolean
25+
label?: string
26+
minWidth?: number
27+
type?: 'text' | 'password'
28+
}>(),
29+
{
30+
minWidth: 10,
31+
type: 'text',
32+
}
33+
)
34+
35+
const { active, disabled } = useFocus({
36+
// @ts-expect-error: vue bug?
37+
disabled: toRef(props, 'disabled'),
38+
})
39+
40+
watch(active, () => {
41+
if (active.value) {
42+
restartBlinking()
43+
}
44+
})
45+
46+
// used for fallback value when no v-model
47+
const localText = ref('')
48+
const text = computed({
49+
get() {
50+
return props.modelValue ?? localText.value
51+
},
52+
set(value) {
53+
if (props.modelValue == null) {
54+
localText.value = value
55+
} else {
56+
emit('update:modelValue', value)
57+
}
58+
},
59+
})
60+
const cursorPosition = ref(text.value.length)
61+
const displayedValue = computed(() => {
62+
const textValue =
63+
props.type === 'text' ? text.value : '*'.repeat(text.value.length)
64+
if (showCursorBlock.value && active.value) {
65+
return (
66+
textValue.slice(0, cursorPosition.value) +
67+
FULL_BLOCK +
68+
textValue.slice(cursorPosition.value + 1) +
69+
(cursorPosition.value >= textValue.length ? '' : ' ')
70+
)
71+
}
72+
return textValue + ' '
73+
})
74+
75+
const FULL_BLOCK = '\u{2588}' // '█'
76+
const showCursorBlock = ref(true)
77+
78+
const { restart } = useInterval(() => {
79+
showCursorBlock.value = !showCursorBlock.value
80+
}, 700)
81+
// allows to always show the cursor while moving it
82+
function restartBlinking() {
83+
restart()
84+
showCursorBlock.value = true
85+
}
86+
87+
onInputData(({ event }) => {
88+
if (active.value && !disabled.value) {
89+
if (isKeyDataEvent(event)) {
90+
switch (event.key) {
91+
// cursor moving
92+
case 'ArrowLeft':
93+
cursorPosition.value = Math.max(0, cursorPosition.value - 1)
94+
// TODO: handle alt, ctrl
95+
restartBlinking()
96+
break
97+
case 'ArrowRight':
98+
cursorPosition.value = Math.min(
99+
text.value.length,
100+
cursorPosition.value + 1
101+
)
102+
restartBlinking()
103+
break
104+
105+
case 'Backspace':
106+
if (cursorPosition.value > 0) {
107+
text.value =
108+
text.value.slice(0, cursorPosition.value - 1) +
109+
text.value.slice(cursorPosition.value)
110+
cursorPosition.value--
111+
}
112+
break
113+
case 'e':
114+
case 'E':
115+
if (event.ctrlKey) {
116+
cursorPosition.value = text.value.length
117+
restartBlinking()
118+
break
119+
}
120+
121+
case 'a':
122+
case 'A':
123+
if (event.ctrlKey) {
124+
cursorPosition.value = 0
125+
restartBlinking()
126+
break
127+
}
128+
129+
case 'u':
130+
case 'U':
131+
if (event.ctrlKey) {
132+
text.value = text.value.slice(cursorPosition.value)
133+
cursorPosition.value = 0
134+
restartBlinking()
135+
break
136+
}
137+
138+
case 'k':
139+
case 'K':
140+
if (event.ctrlKey) {
141+
text.value = text.value.slice(0, cursorPosition.value)
142+
restartBlinking()
143+
break
144+
}
145+
146+
default:
147+
if (
148+
event.key.length === 1 &&
149+
!event.altKey &&
150+
!event.ctrlKey &&
151+
!event.metaKey
152+
) {
153+
text.value =
154+
text.value.slice(0, cursorPosition.value) +
155+
event.key +
156+
text.value.slice(cursorPosition.value)
157+
cursorPosition.value++
158+
}
159+
break
160+
}
161+
}
162+
}
163+
})
164+
</script>
165+
166+
<template borderStyle="round">
167+
<Box>
168+
<Box alignItems="center">
169+
<!-- Could also be a title prop in Box -->
170+
<Text dimmed>{{ label }} </Text>
171+
</Box>
172+
<Box
173+
borderStyle="round"
174+
:borderColor="disabled ? 'gray' : active ? 'blue' : undefined"
175+
:backgroundColor="active ? 'blue' : undefined"
176+
:height="3"
177+
:minWidth="minWidth"
178+
>
179+
<Text :dimmed="disabled">{{ displayedValue }}</Text>
180+
</Box>
181+
</Box>
182+
</template>

0 commit comments

Comments
 (0)