diff --git a/packages/core/src/components/Select.ts b/packages/core/src/components/Select.ts new file mode 100644 index 0000000..5451382 --- /dev/null +++ b/packages/core/src/components/Select.ts @@ -0,0 +1,180 @@ +import { + h, + provide, + inject, + defineComponent, + onMounted, + onUpdated, + onUnmounted, + Fragment, + computed, + Comment, +} from '@vue/runtime-core' +import type { + PropType, + InjectionKey, + VNode, + ComputedRef, +} from '@vue/runtime-core' +import { TuiBox } from './Box' +import { TuiText } from './Text' +import { scheduleUpdateSymbol } from '../injectionSymbols' +import { onKeyData } from '../composables/keyboard' +import type { ForegroundColorProp } from '../renderer/textColor' +import { KeyDataEventKey } from '../input/types' + +export interface TuiSelectIndicator { + /** + * Figure of indicator. + * + * @default ❯ + */ + figure: string + /** + * Indicator figure color. + * + * @default ❯ + */ + color: ForegroundColorProp +} + +export const tuiSelectSymbol = Symbol('vue-termui:select') as InjectionKey<{ + activeName: ComputedRef + indicator: TuiSelectIndicator +}> + +export const TuiSelect = defineComponent({ + name: 'TuiSelect', + props: { + modelValue: { + type: [String, Number], + required: true, + }, + indicator: { + type: Object as PropType, + default: { + figure: '❯', + color: 'blue', + }, + }, + submitKey: { + type: [String, Array] as PropType, + required: false, + // Space key + default: [' '], + }, + }, + emit: ['update:modelValue', 'submit'], + setup(props, { slots, emit }) { + const children = computed(() => { + const defaultSlots = slots.default?.() + const children = defaultSlots + ?.filter((child) => child.type !== Comment) + ?.reduce( + (nodeList: VNode[], node: VNode) => + node.type === Fragment + ? [...nodeList, ...(node.children as VNode[])] + : [...nodeList, node], + [] + ) + + return children ?? [] + }) + + const activeName = computed(() => props.modelValue) + const activeIndex = computed(() => + children.value?.findIndex( + (item) => item?.props?.value === activeName.value + ) + ) + + const scheduleUpdate = inject(scheduleUpdateSymbol)! + + onMounted(scheduleUpdate) + + onUpdated(scheduleUpdate) + + onUnmounted(scheduleUpdate) + + provide(tuiSelectSymbol, { + activeName: activeName, + indicator: props.indicator, + }) + + const stopDownInput = onKeyData(['ArrowDown', 'ArrowRight'], () => { + const index = + activeIndex.value! + 1 > children.value.length! - 1 + ? 0 + : activeIndex.value! + 1 + emit('update:modelValue', children.value?.[index]?.props?.value) + }) + + const stopUpInput = onKeyData(['ArrowUp', 'ArrowLeft'], () => { + const index = + activeIndex.value! - 1 < 0 + ? children.value.length! - 1 + : activeIndex.value! - 1 + emit('update:modelValue', children.value?.[index]?.props?.value) + }) + + const stopSubmitInput = onKeyData(props.submitKey, () => { + stopDownInput() + stopUpInput() + stopSubmitInput() + emit('submit', activeName.value, activeIndex.value) + }) + + return () => { + return h( + TuiBox, + { + flexDirection: 'column', + }, + () => slots.default?.() + ) + } + }, +}) + +export const TuiOption = defineComponent({ + name: 'TuiOption', + + props: { + label: { + type: [String, Number], + required: false, + }, + value: [String, Number], + }, + + setup(props, { slots }) { + const tuiSelectRoot = inject(tuiSelectSymbol)! + return () => { + const isActive = props.value === tuiSelectRoot.activeName.value + const { color, figure } = tuiSelectRoot.indicator + return h(TuiBox, {}, () => [ + // Indicator + h( + TuiText, + { + color, + }, + () => (isActive ? figure : ' ') + ' ' + ), + // Option + slots + .default?.({ + isActive, + }) + ?.filter((child) => child.type !== Comment)?.length + ? h( + Fragment, + slots.default?.({ + isActive, + }) + ) + : h(TuiText, () => props.label), + ]) + } + }, +}) diff --git a/packages/core/src/components/index.ts b/packages/core/src/components/index.ts index 392921c..dd9080b 100644 --- a/packages/core/src/components/index.ts +++ b/packages/core/src/components/index.ts @@ -3,6 +3,7 @@ export { TuiTextTransform } from './TextTransform' export { TuiNewline } from './Newline' export { TuiApp } from './App' export { TuiBox } from './Box' +export { TuiSelect, TuiOption } from './Select' export { TuiLink } from './Link' // export { default as TuiInput } from './Input.vue' diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index bc9dee5..1412fd9 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -7,6 +7,8 @@ export { TuiNewline, TuiLink, TuiTextTransform, + TuiSelect, + TuiOption, } from './components' export { render } from './renderer' diff --git a/packages/core/src/renderer/textColor.ts b/packages/core/src/renderer/textColor.ts index 6eda09e..aba6711 100644 --- a/packages/core/src/renderer/textColor.ts +++ b/packages/core/src/renderer/textColor.ts @@ -1,6 +1,9 @@ import chalk from 'chalk' - +import type { ForegroundColor } from 'chalk' type ColorType = 'foreground' | 'background' +import type { LiteralUnion } from '../utils' + +export type ForegroundColorProp = LiteralUnion const RGB_LIKE_REGEX = /^(rgb|hsl|hsv|hwb)\(\s?(\d+),\s?(\d+),\s?(\d+)\s?\)$/ const ANSI_REGEX = /^(ansi|ansi256)\(\s?(\d+)\s?\)$/ diff --git a/packages/playground/components.d.ts b/packages/playground/components.d.ts index 8737c06..8023ff6 100644 --- a/packages/playground/components.d.ts +++ b/packages/playground/components.d.ts @@ -11,6 +11,8 @@ declare module '@vue/runtime-core' { Br: typeof import('vue-termui')['TuiNewline'] Div: typeof import('vue-termui')['TuiBox'] Link: typeof import('vue-termui')['TuiLink'] + Option: typeof import('vue-termui')['TuiOption'] + Select: typeof import('vue-termui')['TuiSelect'] Span: typeof import('vue-termui')['TuiText'] Text: typeof import('vue-termui')['TuiText'] } diff --git a/packages/playground/src/SelectDemo.vue b/packages/playground/src/SelectDemo.vue new file mode 100644 index 0000000..07cf230 --- /dev/null +++ b/packages/playground/src/SelectDemo.vue @@ -0,0 +1,38 @@ + + + diff --git a/packages/playground/src/main.ts b/packages/playground/src/main.ts index bbaa6b2..b564075 100644 --- a/packages/playground/src/main.ts +++ b/packages/playground/src/main.ts @@ -1,12 +1,13 @@ // import devtools from '@vue/devtools' // import devtools from '@vue/devtools/node' import { createApp } from 'vue-termui' -import App from './Focusables.vue' +// import App from './Focusables.vue' // import App from './Fragments.vue' // import App from './CenteredDemo.vue' // import App from './App.vue' // import App from './Counter.vue' // import App from './Borders.vue' +import App from './SelectDemo.vue' createApp(App, { // swapScreens: true, diff --git a/packages/vite-plugin-vue-termui/src/index.ts b/packages/vite-plugin-vue-termui/src/index.ts index 35dce1b..da20ecc 100644 --- a/packages/vite-plugin-vue-termui/src/index.ts +++ b/packages/vite-plugin-vue-termui/src/index.ts @@ -174,6 +174,9 @@ export const VueTuiComponents = new Map([ ['transform', 'TuiTextTransform'], ['text-transform', 'TuiTextTransform'], + + ['select', 'TuiSelect'], + ['option', 'TuiOption'], ]) // copied from auto import plugin source code