2023-05-07 23:31:10 +02:00
|
|
|
<script lang="ts" setup>
|
|
|
|
import { useAppTheme } from '../theme/themes';
|
2023-05-28 23:13:24 +02:00
|
|
|
import { useTheme } from './c-input-text.theme';
|
|
|
|
import { generateRandomId } from '@/utils/random';
|
|
|
|
import { type UseValidationRule, useValidation } from '@/composable/validation';
|
2023-05-07 23:31:10 +02:00
|
|
|
|
|
|
|
const props = withDefaults(
|
|
|
|
defineProps<{
|
2023-05-28 23:13:24 +02:00
|
|
|
value?: string
|
|
|
|
id?: string
|
|
|
|
placeholder?: string
|
|
|
|
label?: string
|
|
|
|
readonly?: boolean
|
|
|
|
disabled?: boolean
|
|
|
|
validationRules?: UseValidationRule<string>[]
|
|
|
|
validationWatch?: Ref<unknown>[]
|
|
|
|
validation?: ReturnType<typeof useValidation>
|
|
|
|
labelPosition?: 'top' | 'left'
|
|
|
|
labelWidth?: string
|
|
|
|
labelAlign?: 'left' | 'right'
|
|
|
|
clearable?: boolean
|
|
|
|
testId?: string
|
|
|
|
autocapitalize?: 'none' | 'sentences' | 'words' | 'characters' | 'on' | 'off' | string
|
|
|
|
autocomplete?: 'on' | 'off' | string
|
|
|
|
autocorrect?: 'on' | 'off' | string
|
|
|
|
spellcheck?: 'true' | 'false' | boolean
|
|
|
|
rawText?: boolean
|
|
|
|
type?: 'text' | 'password'
|
|
|
|
multiline?: boolean
|
|
|
|
rows?: number | string
|
|
|
|
autosize?: boolean
|
2023-06-19 00:21:36 +02:00
|
|
|
autofocus?: boolean
|
2023-06-23 16:51:52 +02:00
|
|
|
monospace?: boolean
|
2024-12-15 19:33:46 +01:00
|
|
|
pasteHtml?: boolean
|
2023-05-07 23:31:10 +02:00
|
|
|
}>(),
|
|
|
|
{
|
|
|
|
value: '',
|
|
|
|
id: generateRandomId,
|
|
|
|
placeholder: 'Input text',
|
|
|
|
label: undefined,
|
|
|
|
readonly: false,
|
|
|
|
disabled: false,
|
|
|
|
validationRules: () => [],
|
2023-05-15 14:35:44 +02:00
|
|
|
validationWatch: undefined,
|
2023-05-14 21:26:18 +02:00
|
|
|
validation: undefined,
|
2023-05-07 23:31:10 +02:00
|
|
|
labelPosition: 'top',
|
|
|
|
labelWidth: 'auto',
|
|
|
|
labelAlign: 'left',
|
|
|
|
clearable: false,
|
|
|
|
testId: undefined,
|
|
|
|
autocapitalize: undefined,
|
|
|
|
autocomplete: undefined,
|
|
|
|
autocorrect: undefined,
|
|
|
|
spellcheck: undefined,
|
|
|
|
rawText: false,
|
2023-05-14 21:26:18 +02:00
|
|
|
type: 'text',
|
|
|
|
multiline: false,
|
|
|
|
rows: 3,
|
|
|
|
autosize: false,
|
2023-06-19 00:21:36 +02:00
|
|
|
autofocus: false,
|
2023-06-23 16:51:52 +02:00
|
|
|
monospace: false,
|
2024-12-15 19:33:46 +01:00
|
|
|
pasteHtml: false,
|
2023-05-07 23:31:10 +02:00
|
|
|
},
|
|
|
|
);
|
|
|
|
const emit = defineEmits(['update:value']);
|
|
|
|
const value = useVModel(props, 'value', emit);
|
2023-05-14 21:26:18 +02:00
|
|
|
const showPassword = ref(false);
|
2023-05-07 23:31:10 +02:00
|
|
|
|
2024-12-15 19:33:46 +01:00
|
|
|
const { id, placeholder, label, validationRules, labelPosition, labelWidth, labelAlign, autosize, readonly, disabled, clearable, type, multiline, rows, rawText, autofocus, monospace, pasteHtml } = toRefs(props);
|
2023-05-07 23:31:10 +02:00
|
|
|
|
2023-05-28 23:13:24 +02:00
|
|
|
const validation
|
|
|
|
= props.validation
|
|
|
|
?? useValidation({
|
2023-05-14 21:26:18 +02:00
|
|
|
rules: validationRules,
|
|
|
|
source: value,
|
2023-05-15 14:35:44 +02:00
|
|
|
watch: props.validationWatch,
|
2023-05-14 21:26:18 +02:00
|
|
|
});
|
2023-05-07 23:31:10 +02:00
|
|
|
|
|
|
|
const theme = useTheme();
|
|
|
|
const appTheme = useAppTheme();
|
2023-05-14 21:26:18 +02:00
|
|
|
|
|
|
|
const textareaRef = ref<HTMLTextAreaElement>();
|
2023-06-19 00:21:36 +02:00
|
|
|
const inputRef = ref<HTMLInputElement>();
|
2023-05-14 21:26:18 +02:00
|
|
|
const inputWrapperRef = ref<HTMLElement>();
|
|
|
|
|
2024-12-15 19:33:46 +01:00
|
|
|
interface HTMLElementWithValue {
|
|
|
|
value?: string
|
|
|
|
}
|
|
|
|
|
|
|
|
function onPasteInputHtml(evt: ClipboardEvent) {
|
|
|
|
if (!pasteHtml.value) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
const target = (evt.target as HTMLElementWithValue);
|
|
|
|
if (!target) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
const textHtmlData = evt.clipboardData?.getData('text/html');
|
|
|
|
if (textHtmlData && textHtmlData !== '') {
|
|
|
|
evt.preventDefault();
|
|
|
|
value.value = textHtmlData;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2023-05-14 21:26:18 +02:00
|
|
|
watch(
|
2023-06-25 15:00:50 +02:00
|
|
|
[value, autosize, multiline, inputWrapperRef, textareaRef],
|
|
|
|
() => nextTick(() => {
|
2023-05-14 21:26:18 +02:00
|
|
|
if (props.multiline && autosize.value) {
|
|
|
|
resizeTextarea();
|
|
|
|
}
|
2023-06-25 15:00:50 +02:00
|
|
|
}),
|
2023-05-14 21:26:18 +02:00
|
|
|
{ immediate: true },
|
|
|
|
);
|
|
|
|
|
|
|
|
function resizeTextarea() {
|
|
|
|
if (!textareaRef.value || !inputWrapperRef.value) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-06-25 15:00:50 +02:00
|
|
|
const scrollHeight = textareaRef.value.scrollHeight + 2;
|
2023-05-14 21:26:18 +02:00
|
|
|
|
2023-06-25 15:00:50 +02:00
|
|
|
inputWrapperRef.value.style.height = `${scrollHeight}px`;
|
2023-05-14 21:26:18 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
const htmlInputType = computed(() => {
|
|
|
|
if (props.type === 'password' && !showPassword.value) {
|
|
|
|
return 'password';
|
|
|
|
}
|
|
|
|
|
|
|
|
return 'text';
|
|
|
|
});
|
2023-06-19 00:21:36 +02:00
|
|
|
|
|
|
|
function focus() {
|
|
|
|
if (textareaRef.value) {
|
|
|
|
textareaRef.value.focus();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (inputRef.value) {
|
|
|
|
inputRef.value.focus();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function blur() {
|
|
|
|
if (textareaRef.value) {
|
|
|
|
textareaRef.value.blur?.();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (inputRef.value) {
|
|
|
|
inputRef.value.blur?.();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
if (autofocus.value) {
|
|
|
|
focus();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
defineExpose({
|
|
|
|
inputWrapperRef,
|
|
|
|
focus,
|
|
|
|
blur,
|
|
|
|
});
|
2023-05-07 23:31:10 +02:00
|
|
|
</script>
|
|
|
|
|
2023-05-28 23:13:24 +02:00
|
|
|
<template>
|
|
|
|
<div
|
|
|
|
class="c-input-text"
|
|
|
|
:class="{ disabled, 'error': !validation.isValid, 'label-left': labelPosition === 'left', multiline }"
|
|
|
|
>
|
|
|
|
<label v-if="label" :for="id" class="label"> {{ label }} </label>
|
|
|
|
|
|
|
|
<div class="feedback-wrapper">
|
|
|
|
<div ref="inputWrapperRef" class="input-wrapper">
|
|
|
|
<slot name="prefix" />
|
|
|
|
|
|
|
|
<textarea
|
|
|
|
v-if="multiline"
|
|
|
|
:id="id"
|
|
|
|
ref="textareaRef"
|
|
|
|
v-model="value"
|
|
|
|
class="input"
|
2023-06-23 16:51:52 +02:00
|
|
|
:class="{
|
|
|
|
'leading-5 !font-mono': monospace,
|
|
|
|
}"
|
2023-05-28 23:13:24 +02:00
|
|
|
:placeholder="placeholder"
|
|
|
|
:readonly="readonly"
|
|
|
|
:disabled="disabled"
|
|
|
|
:data-test-id="testId"
|
|
|
|
:autocapitalize="autocapitalize ?? (rawText ? 'off' : undefined)"
|
|
|
|
:autocomplete="autocomplete ?? (rawText ? 'off' : undefined)"
|
|
|
|
:autocorrect="autocorrect ?? (rawText ? 'off' : undefined)"
|
|
|
|
:spellcheck="spellcheck ?? (rawText ? false : undefined)"
|
|
|
|
:rows="rows"
|
2024-12-15 19:33:46 +01:00
|
|
|
@paste="onPasteInputHtml"
|
2023-05-28 23:13:24 +02:00
|
|
|
/>
|
|
|
|
|
|
|
|
<input
|
|
|
|
v-else
|
|
|
|
:id="id"
|
2023-06-19 00:21:36 +02:00
|
|
|
ref="inputRef"
|
2023-05-28 23:13:24 +02:00
|
|
|
v-model="value"
|
|
|
|
:type="htmlInputType"
|
|
|
|
class="input"
|
2023-06-23 16:51:52 +02:00
|
|
|
:class="{
|
|
|
|
'leading-5 !font-mono': monospace,
|
|
|
|
}"
|
2023-05-28 23:13:24 +02:00
|
|
|
size="1"
|
|
|
|
:placeholder="placeholder"
|
|
|
|
:readonly="readonly"
|
|
|
|
:disabled="disabled"
|
|
|
|
:data-test-id="testId"
|
|
|
|
:autocapitalize="autocapitalize ?? (rawText ? 'off' : undefined)"
|
|
|
|
:autocomplete="autocomplete ?? (rawText ? 'off' : undefined)"
|
|
|
|
:autocorrect="autocorrect ?? (rawText ? 'off' : undefined)"
|
|
|
|
:spellcheck="spellcheck ?? (rawText ? false : undefined)"
|
2024-12-15 19:33:46 +01:00
|
|
|
@paste="onPasteInputHtml"
|
2023-05-28 23:13:24 +02:00
|
|
|
>
|
|
|
|
|
|
|
|
<c-button v-if="clearable && value" variant="text" circle size="small" @click="value = ''">
|
|
|
|
<icon-mdi-close />
|
|
|
|
</c-button>
|
|
|
|
|
|
|
|
<c-button v-if="type === 'password'" variant="text" circle size="small" @click="showPassword = !showPassword">
|
|
|
|
<icon-mdi-eye v-if="!showPassword" />
|
|
|
|
<icon-mdi-eye-off v-if="showPassword" />
|
|
|
|
</c-button>
|
|
|
|
<slot name="suffix" />
|
|
|
|
</div>
|
|
|
|
<span v-if="!validation.isValid" class="feedback"> {{ validation.message }} </span>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</template>
|
|
|
|
|
2023-05-07 23:31:10 +02:00
|
|
|
<style lang="less" scoped>
|
|
|
|
.c-input-text {
|
|
|
|
display: inline-flex;
|
|
|
|
flex-direction: column;
|
|
|
|
width: 100%;
|
|
|
|
|
|
|
|
&.label-left {
|
|
|
|
flex-direction: row;
|
|
|
|
align-items: baseline;
|
|
|
|
}
|
|
|
|
|
|
|
|
&.error {
|
|
|
|
& > .input {
|
|
|
|
border-color: v-bind('appTheme.error.color');
|
|
|
|
&:hover,
|
|
|
|
&:focus {
|
|
|
|
border-color: v-bind('appTheme.error.color');
|
|
|
|
}
|
|
|
|
|
|
|
|
&:focus {
|
|
|
|
background-color: v-bind('appTheme.error.color + 22');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-14 21:26:18 +02:00
|
|
|
& .feedback {
|
2023-05-07 23:31:10 +02:00
|
|
|
color: v-bind('appTheme.error.color');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
& > .label {
|
|
|
|
margin-bottom: 5px;
|
|
|
|
flex: 0 0 v-bind('labelWidth');
|
|
|
|
text-align: v-bind('labelAlign');
|
2023-05-14 21:26:18 +02:00
|
|
|
padding-right: 12px;
|
2023-05-07 23:31:10 +02:00
|
|
|
}
|
|
|
|
|
2023-05-14 21:26:18 +02:00
|
|
|
.feedback-wrapper {
|
2023-05-07 23:31:10 +02:00
|
|
|
flex: 1 1 0;
|
|
|
|
min-width: 0;
|
2023-05-14 21:26:18 +02:00
|
|
|
}
|
|
|
|
.input-wrapper {
|
2023-05-07 23:31:10 +02:00
|
|
|
display: flex;
|
|
|
|
flex-direction: row;
|
|
|
|
align-items: center;
|
|
|
|
background-color: v-bind('theme.backgroundColor');
|
2023-05-14 21:26:18 +02:00
|
|
|
color: transparent;
|
2023-05-07 23:31:10 +02:00
|
|
|
border: 1px solid v-bind('theme.borderColor');
|
|
|
|
border-radius: 4px;
|
|
|
|
padding: 0 4px 0 12px;
|
2023-05-14 21:26:18 +02:00
|
|
|
transition: border-color 0.2s ease-in-out;
|
|
|
|
|
|
|
|
.multiline& {
|
|
|
|
resize: vertical;
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
|
|
& > textarea {
|
|
|
|
height: 100%;
|
|
|
|
resize: none;
|
|
|
|
word-break: break-word;
|
|
|
|
white-space: pre-wrap;
|
|
|
|
overflow-wrap: break-word;
|
|
|
|
border: none;
|
|
|
|
outline: none;
|
|
|
|
font-family: inherit;
|
|
|
|
font-size: inherit;
|
|
|
|
color: v-bind('appTheme.text.baseColor');
|
|
|
|
|
|
|
|
&::placeholder {
|
|
|
|
color: v-bind('appTheme.text.mutedColor');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-05-07 23:31:10 +02:00
|
|
|
|
|
|
|
& > .input {
|
|
|
|
flex: 1 1 0;
|
|
|
|
min-width: 0;
|
|
|
|
|
|
|
|
padding: 8px 0;
|
|
|
|
outline: none;
|
|
|
|
background-color: transparent;
|
|
|
|
background-image: none;
|
|
|
|
-webkit-box-shadow: none;
|
|
|
|
-moz-box-shadow: none;
|
|
|
|
box-shadow: none;
|
|
|
|
border: none;
|
|
|
|
color: v-bind('appTheme.text.baseColor');
|
|
|
|
|
|
|
|
&::placeholder {
|
|
|
|
color: v-bind('appTheme.text.mutedColor');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-14 21:26:18 +02:00
|
|
|
&:hover {
|
2023-05-07 23:31:10 +02:00
|
|
|
border-color: v-bind('appTheme.primary.color');
|
|
|
|
}
|
|
|
|
|
2023-05-14 21:26:18 +02:00
|
|
|
&:focus-within {
|
|
|
|
border-color: v-bind('appTheme.primary.color');
|
|
|
|
|
2023-05-07 23:31:10 +02:00
|
|
|
background-color: v-bind('theme.focus.backgroundColor');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
&.error .input-wrapper {
|
|
|
|
border-color: v-bind('appTheme.error.color');
|
|
|
|
|
|
|
|
&:hover,
|
2023-05-14 21:26:18 +02:00
|
|
|
&:focus-within {
|
2023-05-07 23:31:10 +02:00
|
|
|
border-color: v-bind('appTheme.error.color');
|
|
|
|
}
|
|
|
|
|
2023-05-14 21:26:18 +02:00
|
|
|
&:focus-within {
|
2023-05-07 23:31:10 +02:00
|
|
|
background-color: v-bind('appTheme.error.color + 22');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
&.disabled .input-wrapper {
|
|
|
|
opacity: 0.5;
|
|
|
|
|
|
|
|
&:hover,
|
2023-05-14 21:26:18 +02:00
|
|
|
&:focus-within {
|
2023-05-07 23:31:10 +02:00
|
|
|
border-color: v-bind('theme.borderColor');
|
|
|
|
}
|
|
|
|
|
|
|
|
& > .input {
|
|
|
|
cursor: not-allowed;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2024-12-15 19:33:46 +01:00
|
|
|
</style>
|