Files
malio-layer-ui/app/components/malio/InputTextArea.vue

187 lines
5.1 KiB
Vue

<template>
<div
class="relative mt-4 w-full"
>
<textarea
:id="inputId"
:name="name"
:autocomplete="autocomplete"
class="floating-input peer w-full border bg-white pl-3 pr-3 py-1 outline-none placeholder:text-transparent focus:border-2 overflow-auto"
:class="[
isFilled ? 'border-black' : 'border-m-muted',
disabled ? 'cursor-not-allowed text-black/60 border-m-muted' : 'cursor-text',
hasError
? 'border-m-error focus:border-m-error focus:pl-[11px]'
: hasSuccess
? 'border-m-success focus:border-m-success focus:pl-[11px]'
: 'focus:border-m-primary focus:pl-[11px]',
textInput,
showCounterComputed ? 'pb-6' : '',
rounded,
]"
:required="required"
:maxlength="maxLength"
:rows="rowsCount"
:disabled="disabled"
:value="currentValue"
:readonly="readonly"
:aria-invalid="hasError"
:aria-describedby="describedBy"
:style="textareaStyle"
v-bind="attrs"
placeholder="_"
@input="onInput"
@focus="isFocused = true"
@blur="isFocused = false"
/>
<label
v-if="label"
:for="inputId"
class="floating-label absolute left-3 top-2 mt-1 inline-block origin-left transition-transform duration-150 font-medium"
:class="[
shouldFloatLabel ? '-translate-y-[1.30rem] scale-90' : '',
disabled ? 'text-black/60' : '',
hasError
? 'text-m-error'
: hasSuccess
? 'text-m-success'
: isFocused ? 'text-m-primary' : shouldFloatLabel ? 'text-black' : 'text-m-muted',
textLabel,
]"
>
{{ label }}
</label>
<span
v-if="showCounterComputed"
class="pointer-events-none absolute bottom-2 left-3 text-xs text-m-muted"
>
{{ currentLength }}/{{ maxLength }}
</span>
</div>
<div
v-if="hasError || hasSuccess || hint"
class="mt-1 flex items-center justify-between gap-2 text-xs"
>
<p
:id="`${inputId}-describedby`"
:class="[
hasError
? 'text-m-error'
: hasSuccess
? 'text-m-success'
: 'text-m-muted',
'ml-[2px]',
]"
>
{{ error || success || hint }}
</p>
</div>
</template>
<script setup lang="ts">
import {computed, ref, useAttrs, useId} from 'vue'
defineOptions({name: 'MalioInputTextArea', inheritAttrs: false})
const props = withDefaults(
defineProps<{
id?: string
label?: string
name?: string
autocomplete?: string
modelValue?: string | null | undefined
size?: number | string
textInput?: string
textLabel?: string
resize?: 'none' | 'both' | 'horizontal' | 'vertical'
minResizeWidth?: number
maxResizeWidth?: number
minResizeHeight?: number
maxResizeHeight?: number
required?: boolean
maxLength?: number
showCounter?: boolean
disabled?: boolean
readonly?: boolean
hint?: string
error?: string
success?: string
rounded?: string
}>(),
{
id: '',
name: '',
autocomplete: 'off',
modelValue: undefined,
label: '',
size: 2,
textInput: 'text-lg',
required: false,
maxLength: 800,
showCounter: false,
readonly: false,
textLabel: 'text-sm',
disabled: false,
rounded: 'rounded-md',
hint: '',
error: '',
success: '',
resize: 'both',
minResizeWidth: 280,
maxResizeWidth: 640,
minResizeHeight: 40,
maxResizeHeight: 320,
},
)
const attrs = useAttrs()
const generatedId = useId()
const localValue = ref('')
const isFocused = ref(false)
const inputId = computed(() => props.id?.toString() || `malio-input-textarea-${generatedId}`)
const isControlled = computed(() => props.modelValue !== undefined)
const currentValue = computed(() => (isControlled.value ? (props.modelValue ?? '') : localValue.value))
const shouldFloatLabel = computed(() => isFocused.value || currentValue.value.length > 0)
const hasError = computed(() => !!props.error)
const hasSuccess = computed(() => !!props.success && !hasError.value)
const rowsCount = computed(() => Math.max(1, Number(props.size || 3)))
const currentLength = computed(() => (currentValue.value ?? '').length)
const showCounterComputed = computed(() =>
props.showCounter && Number(props.maxLength) > 0
)
const toCssSize = (value: number | string) => (typeof value === 'number' ? `${value}px` : value)
const textareaStyle = computed(() => ({
resize: props.resize,
minWidth: toCssSize(props.minResizeWidth),
maxWidth: toCssSize(props.maxResizeWidth),
minHeight: toCssSize(props.minResizeHeight),
maxHeight: toCssSize(props.maxResizeHeight),
}))
const isFilled = computed(() => currentValue.value.trim().length > 0)
const describedBy = computed(() =>
(hasError.value || hasSuccess.value || !!props.hint) ? `${inputId.value}-describedby` : undefined,
)
const emit = defineEmits<{
(event: 'update:modelValue', value: string): void
}>()
const onInput = (event: Event) => {
const target = event.target as HTMLTextAreaElement
if (!isControlled.value) {
localValue.value = target.value
}
emit('update:modelValue', target.value)
}
</script>
<style scoped>
.floating-label {
background: white;
padding: 0 0.25rem;
}
</style>