mirror of
https://github.com/HoshinoSuzumi/rayine-ui.git
synced 2025-04-10 04:58:51 +08:00
✨ feat(textarea): new component textarea
This commit is contained in:
parent
77cc38e552
commit
00a7c05aec
182
docs/content/2.components/textarea.md
Normal file
182
docs/content/2.components/textarea.md
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
---
|
||||||
|
description: Create a textarea component
|
||||||
|
since: 1.3.5
|
||||||
|
---
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
The basic usage.
|
||||||
|
|
||||||
|
::ComponentPreview
|
||||||
|
---
|
||||||
|
privateProps:
|
||||||
|
placeholder: Description
|
||||||
|
---
|
||||||
|
::
|
||||||
|
|
||||||
|
### Sizes
|
||||||
|
|
||||||
|
::ComponentPreview
|
||||||
|
---
|
||||||
|
privateProps:
|
||||||
|
placeholder: Description
|
||||||
|
props:
|
||||||
|
size: sm
|
||||||
|
---
|
||||||
|
::
|
||||||
|
|
||||||
|
### Colors
|
||||||
|
|
||||||
|
::ComponentPreview
|
||||||
|
---
|
||||||
|
privateProps:
|
||||||
|
placeholder: Description
|
||||||
|
props:
|
||||||
|
color: primary
|
||||||
|
---
|
||||||
|
::
|
||||||
|
|
||||||
|
### Variants
|
||||||
|
|
||||||
|
::ComponentPreview
|
||||||
|
---
|
||||||
|
privateProps:
|
||||||
|
placeholder: Description
|
||||||
|
props:
|
||||||
|
variant: outline
|
||||||
|
---
|
||||||
|
::
|
||||||
|
|
||||||
|
### Placeholder
|
||||||
|
|
||||||
|
You can also set a placeholder.
|
||||||
|
|
||||||
|
::ComponentPreview
|
||||||
|
---
|
||||||
|
props:
|
||||||
|
placeholder: "Description here..."
|
||||||
|
---
|
||||||
|
::
|
||||||
|
|
||||||
|
### Padded
|
||||||
|
|
||||||
|
::ComponentPreview
|
||||||
|
---
|
||||||
|
privateProps:
|
||||||
|
placeholder: Description
|
||||||
|
variant: plain
|
||||||
|
props:
|
||||||
|
padded: false
|
||||||
|
---
|
||||||
|
::
|
||||||
|
|
||||||
|
### Rows
|
||||||
|
|
||||||
|
Set the number of rows of the textarea.
|
||||||
|
|
||||||
|
::ComponentPreview
|
||||||
|
---
|
||||||
|
privateProps:
|
||||||
|
placeholder: Description
|
||||||
|
props:
|
||||||
|
rows: 4
|
||||||
|
---
|
||||||
|
::
|
||||||
|
|
||||||
|
### Resize
|
||||||
|
|
||||||
|
Enable the resize control.
|
||||||
|
|
||||||
|
::ComponentPreview
|
||||||
|
---
|
||||||
|
privateProps:
|
||||||
|
placeholder: Description
|
||||||
|
props:
|
||||||
|
resize: true
|
||||||
|
---
|
||||||
|
::
|
||||||
|
|
||||||
|
### Auto Resize
|
||||||
|
|
||||||
|
The `autosize` prop enables the auto resizing of the textarea. The textarea will grow in height as the user types.
|
||||||
|
|
||||||
|
::ComponentPreview
|
||||||
|
---
|
||||||
|
privateProps:
|
||||||
|
placeholder: Description
|
||||||
|
props:
|
||||||
|
autosize: true
|
||||||
|
---
|
||||||
|
::
|
||||||
|
|
||||||
|
The `maxrows` prop can be used to set the maximum number of rows the textarea can grow to.
|
||||||
|
|
||||||
|
::ComponentPreview
|
||||||
|
---
|
||||||
|
privateProps:
|
||||||
|
placeholder: Description
|
||||||
|
props:
|
||||||
|
autosize: true
|
||||||
|
maxrows: 8
|
||||||
|
---
|
||||||
|
::
|
||||||
|
|
||||||
|
### Disabled
|
||||||
|
|
||||||
|
::ComponentPreview
|
||||||
|
---
|
||||||
|
privateProps:
|
||||||
|
placeholder: Description
|
||||||
|
props:
|
||||||
|
disabled: true
|
||||||
|
---
|
||||||
|
::
|
||||||
|
|
||||||
|
### Model Modifiers
|
||||||
|
|
||||||
|
#### .trim
|
||||||
|
|
||||||
|
The `.trim` modifier trims the input value.
|
||||||
|
|
||||||
|
```vue [page]
|
||||||
|
<script lang="ts" setup>
|
||||||
|
const modal = ref<string>("");
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<RayTextarea v-model.trim="modal" />
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### .number
|
||||||
|
|
||||||
|
The `.number` modifier converts the input value to a number. Non-numeric values are ignored.
|
||||||
|
|
||||||
|
```vue [page]
|
||||||
|
<script lang="ts" setup>
|
||||||
|
const modal = ref<number>(0);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<RayTextarea v-model.number="modal" />
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### .lazy
|
||||||
|
|
||||||
|
The `.lazy` modifier syncs the input value with the model only on `change` event.
|
||||||
|
|
||||||
|
```vue [page]
|
||||||
|
<script lang="ts" setup>
|
||||||
|
const modal = ref<string>("");
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<RayTextarea v-model.lazy="modal" />
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Config
|
||||||
|
|
||||||
|
::ComponentDefaults
|
||||||
|
::
|
192
src/runtime/components/forms/Textarea.vue
Normal file
192
src/runtime/components/forms/Textarea.vue
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { computed, defineComponent, onMounted, ref, toRef, watch, type PropType } from 'vue'
|
||||||
|
import { twMerge, twJoin } from 'tailwind-merge'
|
||||||
|
import defu from 'defu'
|
||||||
|
import { textarea } from '../../ui.config'
|
||||||
|
import type { DeepPartial, Strategy, TextareaColor, TextareaModelModifiers, TextareaSize, TextareaVariant } from '../../types'
|
||||||
|
import { useRayUI } from '#build/imports'
|
||||||
|
|
||||||
|
const config = textarea
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
modelValue: {
|
||||||
|
type: [String, Number] as PropType<string | number | null>,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: String as PropType<TextareaSize>,
|
||||||
|
default: config.default.size,
|
||||||
|
},
|
||||||
|
variant: {
|
||||||
|
type: String as PropType<TextareaVariant>,
|
||||||
|
default: config.default.variant,
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
type: String as PropType<TextareaColor>,
|
||||||
|
default: config.default.color,
|
||||||
|
},
|
||||||
|
autofocus: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
autofocusDelay: {
|
||||||
|
type: Number,
|
||||||
|
default: 100,
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
rows: {
|
||||||
|
type: Number,
|
||||||
|
default: 3,
|
||||||
|
},
|
||||||
|
autosize: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
maxrows: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
padded: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
resize: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
class: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
ui: {
|
||||||
|
type: Object as PropType<DeepPartial<typeof config> & { strategy?: Strategy }>,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
modelModifiers: {
|
||||||
|
type: Object as PropType<TextareaModelModifiers>,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: [
|
||||||
|
'update:modelValue',
|
||||||
|
'blur',
|
||||||
|
'change',
|
||||||
|
],
|
||||||
|
setup(props, { emit }) {
|
||||||
|
const { ui, attrs } = useRayUI('textarea', toRef(props, 'ui'), config)
|
||||||
|
const modelModifiers = ref(defu({}, props.modelModifiers, { lazy: false, number: false, trim: false }))
|
||||||
|
|
||||||
|
const textarea = ref<HTMLTextAreaElement | null>(null)
|
||||||
|
|
||||||
|
const baseClass = computed(() => {
|
||||||
|
return twMerge(twJoin(
|
||||||
|
ui.value.base,
|
||||||
|
ui.value.rounded,
|
||||||
|
ui.value.placeholder,
|
||||||
|
ui.value.size[props.size],
|
||||||
|
props.padded && ui.value.padding[props.size],
|
||||||
|
ui.value.variant[props.variant].replaceAll('{color}', props.color),
|
||||||
|
!props.resize && 'resize-none',
|
||||||
|
), props.class)
|
||||||
|
})
|
||||||
|
|
||||||
|
const autoResize = () => {
|
||||||
|
if (!props.autosize) return
|
||||||
|
if (!textarea.value) return
|
||||||
|
textarea.value.rows = props.rows
|
||||||
|
const overflowBefore = textarea.value.style.overflow
|
||||||
|
textarea.value.style.overflow = 'hidden'
|
||||||
|
|
||||||
|
const style = window.getComputedStyle(textarea.value)
|
||||||
|
const padding = Number.parseInt(style.paddingTop) + Number.parseInt(style.paddingBottom)
|
||||||
|
const lineHeight = Number.parseInt(style.lineHeight)
|
||||||
|
const { scrollHeight, clientHeight } = textarea.value
|
||||||
|
const computedRows = Math.floor((scrollHeight - padding) / lineHeight)
|
||||||
|
if (computedRows > props.rows) {
|
||||||
|
textarea.value.rows = props.maxrows ? Math.min(computedRows, props.maxrows) : computedRows
|
||||||
|
}
|
||||||
|
textarea.value.style.overflow = overflowBefore
|
||||||
|
|
||||||
|
console.log('computedRows', computedRows)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateValue = (value: string) => {
|
||||||
|
if (modelModifiers.value.trim) {
|
||||||
|
value = value.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modelModifiers.value.number) {
|
||||||
|
const n = Number.parseFloat(value)
|
||||||
|
value = (Number.isNaN(n) ? value : n) as any
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('update:modelValue', value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onInput = (e: Event) => {
|
||||||
|
autoResize()
|
||||||
|
if (modelModifiers.value.lazy) return
|
||||||
|
updateValue((e.target as HTMLInputElement).value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onChange = (e: Event) => {
|
||||||
|
const value = (e.target as HTMLInputElement).value
|
||||||
|
emit('change', value)
|
||||||
|
if (modelModifiers.value.lazy) {
|
||||||
|
updateValue(value)
|
||||||
|
}
|
||||||
|
if (modelModifiers.value.trim) {
|
||||||
|
(e.target as HTMLInputElement).value = value.trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onBlur = (e: Event) => {
|
||||||
|
emit('blur', e)
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => props.modelValue, () => {
|
||||||
|
autoResize()
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.autofocus) {
|
||||||
|
setTimeout(() => {
|
||||||
|
textarea.value?.focus()
|
||||||
|
}, props.autofocusDelay)
|
||||||
|
}
|
||||||
|
autoResize()
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
// eslint-disable-next-line vue/no-dupe-keys
|
||||||
|
ui,
|
||||||
|
attrs,
|
||||||
|
textarea,
|
||||||
|
baseClass,
|
||||||
|
onInput,
|
||||||
|
onChange,
|
||||||
|
onBlur,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div :class="ui.wrapper">
|
||||||
|
<textarea
|
||||||
|
ref="textarea"
|
||||||
|
:class="baseClass"
|
||||||
|
:rows="rows"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
v-bind="attrs"
|
||||||
|
:value="modelValue"
|
||||||
|
@input="onInput"
|
||||||
|
@change="onChange"
|
||||||
|
@blur="onBlur"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
@ -1,6 +1,7 @@
|
|||||||
export * from './button'
|
export * from './button'
|
||||||
export * from './message'
|
export * from './message'
|
||||||
export * from './input'
|
export * from './input'
|
||||||
|
export * from './textarea'
|
||||||
export * from './kbd'
|
export * from './kbd'
|
||||||
export * from './toggle'
|
export * from './toggle'
|
||||||
|
|
||||||
|
19
src/runtime/types/textarea.d.ts
vendored
Normal file
19
src/runtime/types/textarea.d.ts
vendored
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import type { AppConfig } from '@nuxt/schema'
|
||||||
|
import type { textarea } from '../ui.config'
|
||||||
|
import type { ExtractDeepKey } from './utils'
|
||||||
|
import type colors from '#ray-colors'
|
||||||
|
|
||||||
|
export type TextareaSize =
|
||||||
|
| keyof typeof textarea.size
|
||||||
|
| ExtractDeepKey<AppConfig, ['rayui', 'textarea', 'size']>
|
||||||
|
export type TextareaColor =
|
||||||
|
| ExtractDeepKey<AppConfig, ['rayui', 'textarea', 'color']>
|
||||||
|
| (typeof colors)[number]
|
||||||
|
export type TextareaVariant =
|
||||||
|
| keyof typeof textarea.variant
|
||||||
|
| ExtractDeepKey<AppConfig, ['rayui', 'textarea', 'variant']>
|
||||||
|
export type TextareaModelModifiers = {
|
||||||
|
number?: boolean
|
||||||
|
trim?: boolean
|
||||||
|
lazy?: boolean
|
||||||
|
}
|
24
src/runtime/ui.config/forms/textarea.ts
Normal file
24
src/runtime/ui.config/forms/textarea.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { standard } from '..'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
wrapper: 'relative',
|
||||||
|
base: 'relative w-full block focus:outline-none disabled:cursor-not-allowed disabled:opacity-70 transition',
|
||||||
|
placeholder: 'placeholder:text-gray-400 dark:placeholder:text-gray-500',
|
||||||
|
rounded: 'rounded-md',
|
||||||
|
size: {
|
||||||
|
...standard.size,
|
||||||
|
},
|
||||||
|
padding: {
|
||||||
|
...standard.padding,
|
||||||
|
},
|
||||||
|
variant: {
|
||||||
|
outline:
|
||||||
|
'shadow-sm bg-transparent text-gray-900 dark:text-white ring ring-1 ring-inset ring-gray-300 dark:ring-gray-700 focus:ring-2 focus:ring-{color}-500 dark:focus:ring-{color}-400',
|
||||||
|
plain: 'bg-transparent',
|
||||||
|
},
|
||||||
|
default: {
|
||||||
|
size: 'sm',
|
||||||
|
color: 'primary',
|
||||||
|
variant: 'outline',
|
||||||
|
},
|
||||||
|
}
|
@ -7,6 +7,7 @@ export { default as kbd } from './elements/kbd'
|
|||||||
|
|
||||||
// forms
|
// forms
|
||||||
export { default as input } from './forms/input'
|
export { default as input } from './forms/input'
|
||||||
|
export { default as textarea } from './forms/textarea'
|
||||||
export { default as toggle } from './forms/toggle'
|
export { default as toggle } from './forms/toggle'
|
||||||
|
|
||||||
// overlays
|
// overlays
|
||||||
|
Loading…
Reference in New Issue
Block a user