feat: inputs de texto e tipos cpf/cnpj, telefone e tipos numericos inteiro e decimal
This commit is contained in:
parent
454fddb061
commit
6c84508996
9 changed files with 454 additions and 23 deletions
256
src/componentes/EliInput/EliInput.vue
Normal file
256
src/componentes/EliInput/EliInput.vue
Normal file
|
|
@ -0,0 +1,256 @@
|
|||
<template>
|
||||
<div class="eli-input">
|
||||
<!-- TEXT LIKE INPUTS -->
|
||||
<v-text-field
|
||||
v-if="isTextLike"
|
||||
v-model="value"
|
||||
:type="inputHtmlType"
|
||||
:label="label"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
:clearable="clearable && type !== 'password'"
|
||||
:error="error"
|
||||
:error-messages="errorMessages"
|
||||
:hint="hint"
|
||||
:persistent-hint="persistentHint"
|
||||
:density="density"
|
||||
:variant="variant"
|
||||
:color="internalColor"
|
||||
:inputmode="inputMode"
|
||||
v-bind="attrs"
|
||||
@focus="onFocus"
|
||||
@blur="onBlur"
|
||||
@input="onInput"
|
||||
>
|
||||
<!-- PASSWORD TOGGLE -->
|
||||
<template
|
||||
v-if="type === 'password' && showPasswordToggle"
|
||||
#append-inner
|
||||
>
|
||||
<v-icon
|
||||
class="cursor-pointer"
|
||||
@click="togglePassword"
|
||||
>
|
||||
{{ showPassword ? "mdi-eye-off" : "mdi-eye" }}
|
||||
</v-icon>
|
||||
</template>
|
||||
</v-text-field>
|
||||
|
||||
<!-- TEXTAREA -->
|
||||
<v-textarea
|
||||
v-else-if="type === 'textarea'"
|
||||
v-model="value"
|
||||
:label="label"
|
||||
:rows="rows"
|
||||
:density="density"
|
||||
:variant="variant"
|
||||
v-bind="attrs"
|
||||
/>
|
||||
|
||||
<!-- RADIO -->
|
||||
<v-radio-group
|
||||
v-else-if="type === 'radio'"
|
||||
v-model="value"
|
||||
:row="row"
|
||||
>
|
||||
<v-radio
|
||||
v-for="opt in options"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</v-radio-group>
|
||||
|
||||
<!-- CHECKBOX -->
|
||||
<div v-else-if="type === 'checkbox'" class="checkbox-group">
|
||||
<v-checkbox
|
||||
v-for="opt in options"
|
||||
:key="opt.value"
|
||||
v-model="value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
:density="density"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, computed, PropType } from "vue";
|
||||
import { formatarCpfCnpj } from "./utils/cpfCnpj";
|
||||
import { formatTelefone } from "./utils/telefone";
|
||||
import { formatarDecimal, formatarMoeda, somenteNumeros } from "./utils/numerico"
|
||||
|
||||
type Option = {
|
||||
label: string;
|
||||
value: any;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
type InputVariant =
|
||||
| "outlined"
|
||||
| "filled"
|
||||
| "plain"
|
||||
| "solo"
|
||||
| "solo-filled"
|
||||
| "solo-inverted"
|
||||
| "underlined";
|
||||
|
||||
type Density = "default" | "comfortable" | "compact";
|
||||
|
||||
type TipoNumerico =
|
||||
| "numericoInteiro"
|
||||
| "numericoDecimal"
|
||||
| "numericoMoeda";
|
||||
|
||||
type InputType =
|
||||
| "text"
|
||||
| "password"
|
||||
| "email"
|
||||
| "search"
|
||||
| "url"
|
||||
| "textarea"
|
||||
| "radio"
|
||||
| "checkbox"
|
||||
| "telefone"
|
||||
| "cpfCnpj"
|
||||
| TipoNumerico;
|
||||
|
||||
export default defineComponent({
|
||||
name: "EliInput",
|
||||
inheritAttrs: false,
|
||||
|
||||
props: {
|
||||
modelValue: { type: [String, Number, Array] as any, default: "" },
|
||||
type: { type: String as PropType<InputType>, default: "text" },
|
||||
label: String,
|
||||
placeholder: String,
|
||||
disabled: Boolean,
|
||||
error: Boolean,
|
||||
errorMessages: { type: [String, Array] as any, default: () => [] },
|
||||
hint: String,
|
||||
persistentHint: Boolean,
|
||||
rows: { type: Number, default: 4 },
|
||||
options: { type: Array as PropType<Option[]>, default: () => [] },
|
||||
clearable: Boolean,
|
||||
variant: { type: String as PropType<InputVariant>, default: "outlined" },
|
||||
density: { type: String as PropType<Density>, default: "comfortable" },
|
||||
color: { type: String, default: "primary" },
|
||||
row: Boolean,
|
||||
showPasswordToggle: Boolean,
|
||||
},
|
||||
|
||||
emits: ["update:modelValue", "change", "focus", "blur"],
|
||||
|
||||
setup(props, { emit, attrs }) {
|
||||
const focused = ref(false);
|
||||
const showPassword = ref(false);
|
||||
|
||||
const value = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (v) => {
|
||||
emit("update:modelValue", v);
|
||||
emit("change", v);
|
||||
},
|
||||
});
|
||||
|
||||
const isTextLike = computed(() =>
|
||||
[
|
||||
"text",
|
||||
"password",
|
||||
"email",
|
||||
"search",
|
||||
"url",
|
||||
"telefone",
|
||||
"cpfCnpj",
|
||||
"numericoInteiro",
|
||||
"numericoDecimal",
|
||||
"numericoMoeda",
|
||||
].includes(props.type)
|
||||
);
|
||||
|
||||
const inputHtmlType = computed(() =>
|
||||
props.type === "password"
|
||||
? showPassword.value
|
||||
? "text"
|
||||
: "password"
|
||||
: "text"
|
||||
);
|
||||
|
||||
const inputMode = computed(() => {
|
||||
if (props.type === "telefone") return "tel";
|
||||
if (props.type.startsWith("numerico")) return "numeric";
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const internalColor = computed(() =>
|
||||
props.error ? "error" : focused.value ? props.color : undefined
|
||||
);
|
||||
|
||||
function onInput(e: Event) {
|
||||
const target = e.target as HTMLInputElement;
|
||||
let resultado = target.value;
|
||||
|
||||
switch (props.type) {
|
||||
case "numericoInteiro":
|
||||
resultado = somenteNumeros(resultado);
|
||||
break;
|
||||
|
||||
case "numericoDecimal":
|
||||
resultado = formatarDecimal(resultado);
|
||||
break;
|
||||
|
||||
case "numericoMoeda":
|
||||
resultado = formatarMoeda(resultado);
|
||||
break;
|
||||
|
||||
case "telefone":
|
||||
resultado = formatTelefone(resultado);
|
||||
break;
|
||||
|
||||
case "cpfCnpj":
|
||||
resultado = formatarCpfCnpj(resultado);
|
||||
break;
|
||||
}
|
||||
|
||||
target.value = resultado;
|
||||
emit("update:modelValue", resultado);
|
||||
emit("change", resultado);
|
||||
}
|
||||
|
||||
function togglePassword() {
|
||||
showPassword.value = !showPassword.value;
|
||||
}
|
||||
|
||||
return {
|
||||
attrs,
|
||||
value,
|
||||
isTextLike,
|
||||
inputHtmlType,
|
||||
inputMode,
|
||||
internalColor,
|
||||
showPassword,
|
||||
togglePassword,
|
||||
onInput,
|
||||
onFocus: () => emit("focus"),
|
||||
onBlur: () => emit("blur"),
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.eli-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
0
src/componentes/EliInput/README.md
Normal file
0
src/componentes/EliInput/README.md
Normal file
4
src/componentes/EliInput/index.ts
Normal file
4
src/componentes/EliInput/index.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import EliInput from "./EliInput.vue";
|
||||
|
||||
export { EliInput };
|
||||
export default EliInput;
|
||||
24
src/componentes/EliInput/utils/cpfCnpj.ts
Normal file
24
src/componentes/EliInput/utils/cpfCnpj.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
function somenteNumeros(v: string): string {
|
||||
return v.replace(/\D+/g, "");
|
||||
}
|
||||
|
||||
export function formatarCpfCnpj(v: string): string {
|
||||
const d = somenteNumeros(v);
|
||||
|
||||
// CPF
|
||||
if (d.length <= 11) {
|
||||
return d
|
||||
.replace(/(\d{3})(\d)/, "$1.$2")
|
||||
.replace(/(\d{3})(\d)/, "$1.$2")
|
||||
.replace(/(\d{3})(\d{1,2})$/, "$1-$2")
|
||||
.slice(0, 14);
|
||||
}
|
||||
|
||||
// CNPJ
|
||||
return d
|
||||
.replace(/^(\d{2})(\d)/, "$1.$2")
|
||||
.replace(/^(\d{2})\.(\d{3})(\d)/, "$1.$2.$3")
|
||||
.replace(/\.(\d{3})(\d)/, ".$1/$2")
|
||||
.replace(/(\d{4})(\d)/, "$1-$2")
|
||||
.slice(0, 18);
|
||||
}
|
||||
17
src/componentes/EliInput/utils/numerico.ts
Normal file
17
src/componentes/EliInput/utils/numerico.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
export function somenteNumeros(valor: string) {
|
||||
return valor.replace(/\D+/g, "");
|
||||
}
|
||||
|
||||
export function formatarDecimal(valor: string) {
|
||||
const limpo = valor.replace(/[^\d,]/g, "");
|
||||
const partes = limpo.split(",");
|
||||
return partes.length > 2 ? partes[0] + "," + partes.slice(1).join("") : limpo;
|
||||
}
|
||||
|
||||
export function formatarMoeda(valor: string) {
|
||||
const numero = somenteNumeros(valor);
|
||||
if (!numero) return "";
|
||||
|
||||
const inteiro = (parseInt(numero, 10) / 100).toFixed(2);
|
||||
return inteiro.replace(".", ",").replace(/\B(?=(\d{3})+(?!\d))/g, ".");
|
||||
}
|
||||
31
src/componentes/EliInput/utils/telefone.ts
Normal file
31
src/componentes/EliInput/utils/telefone.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
// utils/telefone.ts
|
||||
|
||||
/**
|
||||
* Remove tudo que não é número
|
||||
*/
|
||||
export function sanitizeTelefone(value: string): string {
|
||||
return value.replace(/\D+/g, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Aplica máscara dinâmica de telefone BR
|
||||
*/
|
||||
export function formatTelefone(value: string): string {
|
||||
const digits = sanitizeTelefone(value);
|
||||
|
||||
if (!digits) return "";
|
||||
|
||||
// (99) 9999-9999
|
||||
if (digits.length <= 10) {
|
||||
return digits
|
||||
.replace(/^(\d{2})(\d)/, "($1) $2")
|
||||
.replace(/(\d{4})(\d)/, "$1-$2")
|
||||
.slice(0, 14);
|
||||
}
|
||||
|
||||
// (99) 99999-9999
|
||||
return digits
|
||||
.replace(/^(\d{2})(\d)/, "($1) $2")
|
||||
.replace(/(\d{5})(\d)/, "$1-$2")
|
||||
.slice(0, 15);
|
||||
}
|
||||
|
|
@ -1,23 +1,136 @@
|
|||
<template>
|
||||
<v-container>
|
||||
<v-card class="mx-auto" max_width="400">
|
||||
<v-card-title>Olá Mundo!</v-card-title>
|
||||
<v-card-title>
|
||||
<EliBadge :badge="'Novo'" offset-x="-15" location="right center">
|
||||
Olá Mundo!
|
||||
</EliBadge>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
Este é um componente de exemplo integrado com Vuetify.
|
||||
|
||||
<div class="grid-example">
|
||||
<!-- text normal -->
|
||||
<EliInput
|
||||
v-model="nome"
|
||||
label="Nome"
|
||||
placeholder="Digite o nome"
|
||||
density="compact"
|
||||
/>
|
||||
|
||||
<EliInput v-model="idade" type="numericoInteiro" label="Idade" density="default" />
|
||||
|
||||
<EliInput v-model="altura" type="numericoDecimal" label="Altura" density="comfortable" />
|
||||
|
||||
<EliInput v-model="valor" type="numericoMoeda" label="Valor" />
|
||||
|
||||
<EliInput v-model="telefone" type="telefone" label="Telefone" />
|
||||
|
||||
<EliInput
|
||||
v-model="documento"
|
||||
type="cpfCnpj"
|
||||
label="CPF / CNPJ"
|
||||
/>
|
||||
|
||||
<EliInput
|
||||
v-model="email"
|
||||
label="Email"
|
||||
placeholder="email@exemplo.com"
|
||||
/>
|
||||
|
||||
<EliInput
|
||||
v-model="senha"
|
||||
label="Senha"
|
||||
type="password"
|
||||
:showPasswordToggle="true"
|
||||
placeholder="Digite sua senha"
|
||||
/>
|
||||
|
||||
<!-- textarea -->
|
||||
<EliInput
|
||||
type="textarea"
|
||||
v-model="mensagem"
|
||||
label="Mensagem"
|
||||
:rows="5"
|
||||
/>
|
||||
|
||||
<!-- radio -->
|
||||
<EliInput
|
||||
type="radio"
|
||||
v-model="cor"
|
||||
label="Cor favorita"
|
||||
:options="[
|
||||
{ label: 'Azul', value: 'azul' },
|
||||
{ label: 'Verde', value: 'verde' },
|
||||
]"
|
||||
/>
|
||||
|
||||
<!-- checkbox group -->
|
||||
<EliInput
|
||||
type="checkbox"
|
||||
v-model="habilidades"
|
||||
:options="[
|
||||
{ label: 'Vue', value: 'vue' },
|
||||
{ label: 'React', value: 'react' },
|
||||
]"
|
||||
/>
|
||||
|
||||
<!-- erro -->
|
||||
<EliInput
|
||||
v-model="nome"
|
||||
label="Nome"
|
||||
:error="true"
|
||||
:error-messages="['Obrigatório']"
|
||||
/>
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn color="primary" variant="elevated" block style="padding: 10px;">
|
||||
<EliBotao color="primary" variant="elevated" block>
|
||||
Botão Vuetify
|
||||
</v-btn>
|
||||
</EliBotao>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue"
|
||||
import { defineComponent, ref } from "vue";
|
||||
import EliBotao from "../EliBotao/EliBotao.vue";
|
||||
import EliBadge from "../EliBadge/EliBadge.vue";
|
||||
import EliInput from "../EliInput/EliInput.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "EliOlaMundo",
|
||||
})
|
||||
components: {
|
||||
EliBotao,
|
||||
EliBadge,
|
||||
EliInput,
|
||||
},
|
||||
setup() {
|
||||
const nome = ref("");
|
||||
const telefone = ref("");
|
||||
const idade = ref("");
|
||||
const altura = ref("");
|
||||
const valor = ref("");
|
||||
const email = ref("");
|
||||
const mensagem = ref("");
|
||||
const senha = ref("");
|
||||
const documento = ref("")
|
||||
const cor = ref(null);
|
||||
const habilidades = ref<any[]>([]);
|
||||
return {
|
||||
nome,
|
||||
email,
|
||||
documento,
|
||||
telefone,
|
||||
mensagem,
|
||||
senha,
|
||||
cor,
|
||||
habilidades,
|
||||
idade,
|
||||
altura,
|
||||
valor,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -2,15 +2,18 @@ import type { App } from "vue";
|
|||
import { EliOlaMundo } from "./componentes/EliOlaMundo";
|
||||
import { EliBotao } from "./componentes/EliBotao";
|
||||
import { EliBadge } from "./componentes/EliBadge";
|
||||
import { EliInput } from "./componentes/EliInput";
|
||||
|
||||
export { EliOlaMundo };
|
||||
export { EliBotao };
|
||||
export { EliBadge };
|
||||
export { EliInput };
|
||||
|
||||
export default {
|
||||
install(app: App) {
|
||||
app.component("EliOlaMundo", EliOlaMundo);
|
||||
app.component("EliBotao", EliBotao);
|
||||
app.component("EliBadge", EliBadge);
|
||||
app.component("EliInput", EliInput);
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,33 +1,16 @@
|
|||
<template>
|
||||
<EliOlaMundo />
|
||||
<EliBotao
|
||||
color="primary"
|
||||
@click="() => {console.log('xxx')}"
|
||||
>
|
||||
Button
|
||||
</EliBotao>
|
||||
<EliBadge
|
||||
badge="Novo"
|
||||
offset-x="-20"
|
||||
location="right center"
|
||||
radius="pill"
|
||||
>
|
||||
Vistoria
|
||||
</EliBadge>
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import { EliOlaMundo } from '@/componentes/EliOlaMundo'
|
||||
import { EliBotao } from '@/componentes/EliBotao';
|
||||
import EliBadge from '@/componentes/EliBadge/EliBadge.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'App',
|
||||
components: {
|
||||
EliOlaMundo,
|
||||
EliBotao,
|
||||
EliBadge,
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue