diff --git a/src/componentes/EliEntrada/EliEntradaDataHora/useDataHora.ts b/src/componentes/EliEntrada/EliEntradaDataHora/useDataHora.ts index 92caa38..4298346 100644 --- a/src/componentes/EliEntrada/EliEntradaDataHora/useDataHora.ts +++ b/src/componentes/EliEntrada/EliEntradaDataHora/useDataHora.ts @@ -1,14 +1,14 @@ import dayjs from "dayjs" import "dayjs/locale/pt-br" -import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue" +import { nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue" import type { CampoDensidade, CampoVariante } from "../../../tipos" import type { PadroesEntradas } from "../tiposEntradas" -import { - aplicarMascaraDataHora, - converterTextoParaIso, - formatarValor, - gerarCelulas, -} from "./calendarioHelpers" +import { formatarValor } from "./calendarioHelpers" +import { useDataHoraInput } from "./useDataHoraInput" +import { useDataHoraPosicao } from "./useDataHoraPosicao" +import { diasSemana, MESES, useDataHoraProps } from "./useDataHoraProps" + +export { diasSemana, MESES } dayjs.locale("pt-br") @@ -23,13 +23,6 @@ export interface CelulaCalendario { desabilitado: boolean } -export const MESES = Array.from({ length: 12 }, (_, i) => { - const s = dayjs().month(i).format("MMMM") - return s.charAt(0).toUpperCase() + s.slice(1) -}) - -export const diasSemana = ["Dom", "Seg", "Ter", "Qua", "Qui", "Sex", "Sáb"] - /** Função de emit genérica do Vue (compatível com defineComponent) */ type EmitFn = (event: string, payload?: unknown) => void @@ -58,73 +51,44 @@ let contadorUid = 0 export function useDataHora(props: Props, emit: EmitFn) { const uid = ++contadorUid const aberto = ref(false) - const focado = ref(false) - const textoDigitado = ref("") - const estiloPopover = ref>({}) const referencia = ref(dayjs()) const diaSelecionado = ref(null) const horaTemp = ref("00") const minutoTemp = ref("00") - const opcoesEfetivas = computed(() => { - const o = props.opcoes - return { - rotulo: o?.rotulo ?? props.rotulo ?? "Data", - placeholder: o?.placeholder ?? props.placeholder ?? "", - modo: o?.modo ?? props.modo ?? "dataHora", - limpavel: o?.limpavel ?? props.limpavel, - erro: o?.erro ?? props.erro, - mensagensErro: o?.mensagensErro ?? props.mensagensErro, - dica: o?.dica ?? props.dica, - dicaPersistente: o?.dicaPersistente ?? props.dicaPersistente, - densidade: o?.densidade ?? props.densidade, - variante: o?.variante ?? props.variante, - min: o?.min ?? props.min, - max: o?.max ?? props.max, - } + const { + opcoesEfetivas, + modoEfetivo, + desabilitadoEfetivo, + effectiveModelValue, + valor, + mensagensVisiveis, + valorFormatado, + nomeMesAtual, + anoAtual, + mesAtual, + listaAnos, + celulas, + } = useDataHoraProps({ props, referencia, diaSelecionado }) + + const { estiloPopover, calcularPosicao, adicionarListenersPosicao, removerListenersPosicao } = + useDataHoraPosicao({ uid, modoEfetivo }) + + function emitirValor(iso: string | null) { + ;["update:value", "input", "change", "update:modelValue", "alterar"].forEach((e) => { + emit(e, iso) + }) + } + + const { textoDigitado, focado, onTextInput, onInputFocus, onInputBlur } = useDataHoraInput({ + modoEfetivo, + valor, + horaTemp, + minutoTemp, + emitirValor, + emit, }) - const modoEfetivo = computed<"data" | "dataHora">(() => opcoesEfetivas.value.modo ?? "dataHora") - const desabilitadoEfetivo = computed(() => Boolean(props.desabilitado)) - - const effectiveModelValue = computed(() => - props.value !== undefined ? (props.value ?? null) : (props.modelValue ?? null), - ) - - const valor = computed(() => effectiveModelValue.value) - - const mensagensVisiveis = computed(() => { - const m = opcoesEfetivas.value.erro ? opcoesEfetivas.value.mensagensErro : null - return m ? (Array.isArray(m) ? m : [m]) : [] - }) - - const valorFormatado = computed(() => formatarValor(valor.value, modoEfetivo.value)) - - const nomeMesAtual = computed(() => MESES[referencia.value.month()]) - const anoAtual = computed(() => referencia.value.year()) - const mesAtual = computed(() => referencia.value.month()) - const listaAnos = computed(() => { - const y = referencia.value.year() - const min = opcoesEfetivas.value.min - ? dayjs(opcoesEfetivas.value.min).year() - : dayjs().year() - 80 - const max = opcoesEfetivas.value.max - ? dayjs(opcoesEfetivas.value.max).year() - : dayjs().year() + 20 - const anos: number[] = [] - for (let i = Math.min(min, y); i <= Math.max(max, y); i++) anos.push(i) - return anos - }) - - const celulas = computed(() => - gerarCelulas( - referencia.value, - diaSelecionado.value, - opcoesEfetivas.value.min, - opcoesEfetivas.value.max, - ), - ) - watch( effectiveModelValue, (v) => { @@ -147,35 +111,6 @@ export function useDataHora(props: Props, emit: EmitFn) { { immediate: true }, ) - function calcularPosicao() { - const el = document.getElementById(`eli-dh-campo-${uid}`) - if (!el) return - const rect = el.getBoundingClientRect() - const popoverAltura = modoEfetivo.value === "dataHora" ? 380 : 320 - - let top = rect.bottom + 6 - if (rect.bottom + popoverAltura > window.innerHeight && rect.top - popoverAltura > 0) { - top = rect.top - popoverAltura - 6 - } - - let left = rect.left - if (left + 316 > window.innerWidth - 8) { - left = Math.max(8, rect.right - 316) - } - - estiloPopover.value = { position: "fixed", top: `${top}px`, left: `${left}px` } - } - - function adicionarListenersPosicao() { - window.addEventListener("scroll", calcularPosicao, true) - window.addEventListener("resize", calcularPosicao) - } - - function removerListenersPosicao() { - window.removeEventListener("scroll", calcularPosicao, true) - window.removeEventListener("resize", calcularPosicao) - } - function abrirSeHabilitado() { if (desabilitadoEfetivo.value) return if (!aberto.value) { @@ -223,12 +158,6 @@ export function useDataHora(props: Props, emit: EmitFn) { confirmar() } - function emitirValor(iso: string | null) { - ;["update:value", "input", "change", "update:modelValue", "alterar"].forEach((e) => { - emit(e, iso) - }) - } - function atualizarHoraTemp(v: string) { horaTemp.value = v if (diaSelecionado.value) { @@ -266,43 +195,6 @@ export function useDataHora(props: Props, emit: EmitFn) { emitirValor(null) } - function onTextInput(e: Event) { - const rawVal = (e.target as HTMLInputElement).value - const comMascara = aplicarMascaraDataHora(rawVal, modoEfetivo.value) - textoDigitado.value = comMascara - ;(e.target as HTMLInputElement).value = comMascara - - if (!comMascara) { - emitirValor(null) - return - } - - const iso = converterTextoParaIso(comMascara, modoEfetivo.value, { - hora: horaTemp.value, - minuto: minutoTemp.value, - }) - if (iso) { - emitirValor(iso) - } - } - - function onInputFocus() { - focado.value = true - emit("foco") - emit("focus") - } - - function onInputBlur() { - focado.value = false - if (!valor.value) { - textoDigitado.value = "" - } else { - textoDigitado.value = formatarValor(valor.value, modoEfetivo.value) - } - emit("desfoco") - emit("blur") - } - function onDocClick(e: MouseEvent) { if (!aberto.value) return const alvo = e.target as Node diff --git a/src/componentes/EliEntrada/EliEntradaDataHora/useDataHoraInput.ts b/src/componentes/EliEntrada/EliEntradaDataHora/useDataHoraInput.ts new file mode 100644 index 0000000..1e1ea08 --- /dev/null +++ b/src/componentes/EliEntrada/EliEntradaDataHora/useDataHoraInput.ts @@ -0,0 +1,71 @@ +import { type Ref, ref } from "vue" +import { aplicarMascaraDataHora, converterTextoParaIso, formatarValor } from "./calendarioHelpers" + +interface UseDataHoraInputOptions { + modoEfetivo: Ref<"data" | "dataHora"> + valor: Ref + horaTemp: Ref + minutoTemp: Ref + emitirValor: (iso: string | null) => void + emit: (event: string, payload?: unknown) => void +} + +/** + * Composable responsável pelo controle do campo de texto editável com máscara e eventos de foco/blur. + */ +export function useDataHoraInput({ + modoEfetivo, + valor, + horaTemp, + minutoTemp, + emitirValor, + emit, +}: UseDataHoraInputOptions) { + const textoDigitado = ref("") + const focado = ref(false) + + function onTextInput(e: Event) { + const rawVal = (e.target as HTMLInputElement).value + const comMascara = aplicarMascaraDataHora(rawVal, modoEfetivo.value) + textoDigitado.value = comMascara + ;(e.target as HTMLInputElement).value = comMascara + + if (!comMascara) { + emitirValor(null) + return + } + + const iso = converterTextoParaIso(comMascara, modoEfetivo.value, { + hora: horaTemp.value, + minuto: minutoTemp.value, + }) + if (iso) { + emitirValor(iso) + } + } + + function onInputFocus() { + focado.value = true + emit("foco") + emit("focus") + } + + function onInputBlur() { + focado.value = false + if (!valor.value) { + textoDigitado.value = "" + } else { + textoDigitado.value = formatarValor(valor.value, modoEfetivo.value) + } + emit("desfoco") + emit("blur") + } + + return { + textoDigitado, + focado, + onTextInput, + onInputFocus, + onInputBlur, + } +} diff --git a/src/componentes/EliEntrada/EliEntradaDataHora/useDataHoraPosicao.ts b/src/componentes/EliEntrada/EliEntradaDataHora/useDataHoraPosicao.ts new file mode 100644 index 0000000..0f1980f --- /dev/null +++ b/src/componentes/EliEntrada/EliEntradaDataHora/useDataHoraPosicao.ts @@ -0,0 +1,49 @@ +import { type Ref, ref } from "vue" + +interface UseDataHoraPosicaoOptions { + uid: number + modoEfetivo: Ref<"data" | "dataHora"> +} + +/** + * Composable responsável por calcular a posição fixed do popover do calendário. + */ +export function useDataHoraPosicao({ uid, modoEfetivo }: UseDataHoraPosicaoOptions) { + const estiloPopover = ref>({}) + + function calcularPosicao() { + const el = document.getElementById(`eli-dh-campo-${uid}`) + if (!el) return + const rect = el.getBoundingClientRect() + const popoverAltura = modoEfetivo.value === "dataHora" ? 380 : 320 + + let top = rect.bottom + 6 + if (rect.bottom + popoverAltura > window.innerHeight && rect.top - popoverAltura > 0) { + top = rect.top - popoverAltura - 6 + } + + let left = rect.left + if (left + 316 > window.innerWidth - 8) { + left = Math.max(8, rect.right - 316) + } + + estiloPopover.value = { position: "fixed", top: `${top}px`, left: `${left}px` } + } + + function adicionarListenersPosicao() { + window.addEventListener("scroll", calcularPosicao, true) + window.addEventListener("resize", calcularPosicao) + } + + function removerListenersPosicao() { + window.removeEventListener("scroll", calcularPosicao, true) + window.removeEventListener("resize", calcularPosicao) + } + + return { + estiloPopover, + calcularPosicao, + adicionarListenersPosicao, + removerListenersPosicao, + } +} diff --git a/src/componentes/EliEntrada/EliEntradaDataHora/useDataHoraProps.ts b/src/componentes/EliEntrada/EliEntradaDataHora/useDataHoraProps.ts new file mode 100644 index 0000000..8ff18ad --- /dev/null +++ b/src/componentes/EliEntrada/EliEntradaDataHora/useDataHoraProps.ts @@ -0,0 +1,113 @@ +import dayjs from "dayjs" +import { computed, type Ref } from "vue" +import { formatarValor, gerarCelulas } from "./calendarioHelpers" +import type { EntradaDataHora } from "./useDataHora" + +export const MESES = Array.from({ length: 12 }, (_, i) => { + const s = dayjs().month(i).format("MMMM") + return s.charAt(0).toUpperCase() + s.slice(1) +}) + +export const diasSemana = ["Dom", "Seg", "Ter", "Qua", "Qui", "Sex", "Sáb"] + +export interface UseDataHoraPropsOptions { + props: { + value?: string | null + opcoes?: EntradaDataHora["opcoes"] + modelValue?: string | null + modo?: "data" | "dataHora" + rotulo?: string + placeholder?: string + desabilitado?: boolean + limpavel?: boolean + erro?: boolean + mensagensErro?: string | string[] + dica?: string + dicaPersistente?: boolean + densidade?: EntradaDataHora["opcoes"]["densidade"] + variante?: EntradaDataHora["opcoes"]["variante"] + min?: string + max?: string + } + referencia: Ref + diaSelecionado: Ref +} + +/** + * Composable responsável pelo cálculo de propriedades derivadas e opções do EliEntradaDataHora. + */ +export function useDataHoraProps({ props, referencia, diaSelecionado }: UseDataHoraPropsOptions) { + const opcoesEfetivas = computed(() => { + const o = props.opcoes + return { + rotulo: o?.rotulo ?? props.rotulo ?? "Data", + placeholder: o?.placeholder ?? props.placeholder ?? "", + modo: o?.modo ?? props.modo ?? "dataHora", + limpavel: o?.limpavel ?? props.limpavel, + erro: o?.erro ?? props.erro, + mensagensErro: o?.mensagensErro ?? props.mensagensErro, + dica: o?.dica ?? props.dica, + dicaPersistente: o?.dicaPersistente ?? props.dicaPersistente, + densidade: o?.densidade ?? props.densidade, + variante: o?.variante ?? props.variante, + min: o?.min ?? props.min, + max: o?.max ?? props.max, + } + }) + + const modoEfetivo = computed<"data" | "dataHora">(() => opcoesEfetivas.value.modo ?? "dataHora") + const desabilitadoEfetivo = computed(() => Boolean(props.desabilitado)) + + const effectiveModelValue = computed(() => + props.value !== undefined ? (props.value ?? null) : (props.modelValue ?? null), + ) + + const valor = computed(() => effectiveModelValue.value) + + const mensagensVisiveis = computed(() => { + const m = opcoesEfetivas.value.erro ? opcoesEfetivas.value.mensagensErro : null + return m ? (Array.isArray(m) ? m : [m]) : [] + }) + + const valorFormatado = computed(() => formatarValor(valor.value, modoEfetivo.value)) + + const nomeMesAtual = computed(() => MESES[referencia.value.month()]) + const anoAtual = computed(() => referencia.value.year()) + const mesAtual = computed(() => referencia.value.month()) + const listaAnos = computed(() => { + const y = referencia.value.year() + const min = opcoesEfetivas.value.min + ? dayjs(opcoesEfetivas.value.min).year() + : dayjs().year() - 80 + const max = opcoesEfetivas.value.max + ? dayjs(opcoesEfetivas.value.max).year() + : dayjs().year() + 20 + const anos: number[] = [] + for (let i = Math.min(min, y); i <= Math.max(max, y); i++) anos.push(i) + return anos + }) + + const celulas = computed(() => + gerarCelulas( + referencia.value, + diaSelecionado.value, + opcoesEfetivas.value.min, + opcoesEfetivas.value.max, + ), + ) + + return { + opcoesEfetivas, + modoEfetivo, + desabilitadoEfetivo, + effectiveModelValue, + valor, + mensagensVisiveis, + valorFormatado, + nomeMesAtual, + anoAtual, + mesAtual, + listaAnos, + celulas, + } +}