Compare commits

..

3 commits

Author SHA1 Message Date
dbf08a312d build 2026-08-08 14:57:31 -03:00
8c1d26f968 refatoração 2026-08-08 14:57:24 -03:00
2763da4108 ajuste nos componentes de calendarios 2026-08-08 14:55:23 -03:00
20 changed files with 4856 additions and 4320 deletions

8310
dist/eli-vue.es.js vendored

File diff suppressed because one or more lines are too long

118
dist/eli-vue.umd.js vendored

File diff suppressed because one or more lines are too long

View file

@ -11,3 +11,14 @@ export declare function gerarCelulas(referencia: dayjs.Dayjs, diaSelecionado: da
* @param modo - "data" exibe apenas a data, "dataHora" inclui hora * @param modo - "data" exibe apenas a data, "dataHora" inclui hora
*/ */
export declare function formatarValor(valor: string | null, modo: EntradaDataHora["opcoes"]["modo"]): string; export declare function formatarValor(valor: string | null, modo: EntradaDataHora["opcoes"]["modo"]): string;
/**
* Aplica máscara visual de data/hora (DD/MM/YYYY ou DD/MM/YYYY HH:mm) em texto digitado.
*/
export declare function aplicarMascaraDataHora(valor: string, modo: "data" | "dataHora"): string;
/**
* Converte um texto mascarado (ex: "15/08/2026 14:30") em string ISO 8601 com fuso local.
*/
export declare function converterTextoParaIso(texto: string, modo: "data" | "dataHora", horaMinutoPadrao?: {
hora: string;
minuto: string;
}): string | null;

View file

@ -2,6 +2,8 @@ import dayjs from "dayjs";
import "dayjs/locale/pt-br"; import "dayjs/locale/pt-br";
import type { CampoDensidade, CampoVariante } from "../../../tipos"; import type { CampoDensidade, CampoVariante } from "../../../tipos";
import type { PadroesEntradas } from "../tiposEntradas"; import type { PadroesEntradas } from "../tiposEntradas";
import { diasSemana, MESES } from "./useDataHoraProps";
export { diasSemana, MESES };
export type EntradaDataHora = PadroesEntradas["dataHora"]; export type EntradaDataHora = PadroesEntradas["dataHora"];
export interface CelulaCalendario { export interface CelulaCalendario {
dia: number; dia: number;
@ -11,8 +13,6 @@ export interface CelulaCalendario {
selecionado: boolean; selecionado: boolean;
desabilitado: boolean; desabilitado: boolean;
} }
export declare const MESES: string[];
export declare const diasSemana: string[];
/** Função de emit genérica do Vue (compatível com defineComponent) */ /** Função de emit genérica do Vue (compatível com defineComponent) */
type EmitFn = (event: string, payload?: unknown) => void; type EmitFn = (event: string, payload?: unknown) => void;
type Props = { type Props = {
@ -37,6 +37,8 @@ type Props = {
export declare function useDataHora(props: Props, emit: EmitFn): { export declare function useDataHora(props: Props, emit: EmitFn): {
uid: number; uid: number;
aberto: import("vue").Ref<boolean, boolean>; aberto: import("vue").Ref<boolean, boolean>;
focado: import("vue").Ref<boolean, boolean>;
textoDigitado: import("vue").Ref<string, string>;
estiloPopover: import("vue").Ref<Record<string, string>, Record<string, string>>; estiloPopover: import("vue").Ref<Record<string, string>, Record<string, string>>;
referencia: import("vue").Ref<{ referencia: import("vue").Ref<{
clone: () => dayjs.Dayjs; clone: () => dayjs.Dayjs;
@ -399,10 +401,15 @@ export declare function useDataHora(props: Props, emit: EmitFn): {
mudarAno: (ano: number) => void; mudarAno: (ano: number) => void;
celulas: import("vue").ComputedRef<CelulaCalendario[]>; celulas: import("vue").ComputedRef<CelulaCalendario[]>;
abrirSeHabilitado: () => void; abrirSeHabilitado: () => void;
alternarPopover: () => void;
fechar: () => void; fechar: () => void;
navegarMes: (delta: number) => void; navegarMes: (delta: number) => void;
selecionarDia: (cell: CelulaCalendario) => void; selecionarDia: (cell: CelulaCalendario) => void;
confirmar: () => void; confirmar: () => void;
limpar: () => void; limpar: () => void;
onTextInput: (e: Event) => void;
onInputFocus: () => void;
onInputBlur: () => void;
atualizarHoraTemp: (v: string) => void;
atualizarMinutoTemp: (v: string) => void;
}; };
export {};

View file

@ -0,0 +1,20 @@
import { type Ref } from "vue";
interface UseDataHoraInputOptions {
modoEfetivo: Ref<"data" | "dataHora">;
valor: Ref<string | null>;
horaTemp: Ref<string>;
minutoTemp: Ref<string>;
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 declare function useDataHoraInput({ modoEfetivo, valor, horaTemp, minutoTemp, emitirValor, emit, }: UseDataHoraInputOptions): {
textoDigitado: Ref<string, string>;
focado: Ref<boolean, boolean>;
onTextInput: (e: Event) => void;
onInputFocus: () => void;
onInputBlur: () => void;
};
export {};

View file

@ -0,0 +1,15 @@
import { type 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 declare function useDataHoraPosicao({ uid, modoEfetivo }: UseDataHoraPosicaoOptions): {
estiloPopover: Ref<Record<string, string>, Record<string, string>>;
calcularPosicao: () => void;
adicionarListenersPosicao: () => void;
removerListenersPosicao: () => void;
};
export {};

View file

@ -0,0 +1,59 @@
import dayjs from "dayjs";
import { type Ref } from "vue";
import type { EntradaDataHora } from "./useDataHora";
export declare const MESES: string[];
export declare const diasSemana: string[];
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<dayjs.Dayjs>;
diaSelecionado: Ref<dayjs.Dayjs | null>;
}
/**
* Composable responsável pelo cálculo de propriedades derivadas e opções do EliEntradaDataHora.
*/
export declare function useDataHoraProps({ props, referencia, diaSelecionado }: UseDataHoraPropsOptions): {
opcoesEfetivas: import("vue").ComputedRef<{
rotulo: string;
placeholder?: string;
apenasLeitura?: boolean;
} & {
modo?: "data" | "dataHora";
limpavel?: boolean;
erro?: boolean;
mensagensErro?: string | string[];
dica?: string;
dicaPersistente?: boolean;
min?: string;
max?: string;
densidade?: import("../../..").CampoDensidade;
variante?: import("../../..").CampoVariante;
}>;
modoEfetivo: import("vue").ComputedRef<"data" | "dataHora">;
desabilitadoEfetivo: import("vue").ComputedRef<boolean>;
effectiveModelValue: import("vue").ComputedRef<string | null>;
valor: import("vue").ComputedRef<string | null>;
mensagensVisiveis: import("vue").ComputedRef<string[]>;
valorFormatado: import("vue").ComputedRef<string>;
nomeMesAtual: import("vue").ComputedRef<string>;
anoAtual: import("vue").ComputedRef<number>;
mesAtual: import("vue").ComputedRef<number>;
listaAnos: import("vue").ComputedRef<number[]>;
celulas: import("vue").ComputedRef<import("./useDataHora").CelulaCalendario[]>;
};

View file

@ -1,6 +1,6 @@
{ {
"name": "eli-vue", "name": "eli-vue",
"version": "0.1.227", "version": "0.1.230",
"private": false, "private": false,
"main": "./dist/eli-vue.umd.js", "main": "./dist/eli-vue.umd.js",
"module": "./dist/eli-vue.es.js", "module": "./dist/eli-vue.es.js",
@ -34,7 +34,7 @@
"@vitejs/plugin-vue": "^5.0.0", "@vitejs/plugin-vue": "^5.0.0",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"sass": "^1.94.2", "sass": "^1.94.2",
"typescript": "^7.0.2", "typescript": "^7",
"vite": "^6.0.0", "vite": "^6.0.0",
"vite-plugin-css-injected-by-js": "^3.5.2", "vite-plugin-css-injected-by-js": "^3.5.2",
"vite-plugin-vuetify": "^2.1.2", "vite-plugin-vuetify": "^2.1.2",

2
pnpm-lock.yaml generated
View file

@ -52,7 +52,7 @@ importers:
specifier: ^1.94.2 specifier: ^1.94.2
version: 1.102.0 version: 1.102.0
typescript: typescript:
specifier: ^7.0.2 specifier: ^7
version: 7.0.2 version: 7.0.2
vite: vite:
specifier: ^6.0.0 specifier: ^6.0.0

View file

@ -3,10 +3,8 @@
───────────────────────────────────────────── */ ───────────────────────────────────────────── */
.eli-dh__popover { .eli-dh__popover {
position: absolute; position: fixed;
top: calc(100% + 6px); z-index: 99999;
left: 0;
z-index: 2000;
background: var(--cor-eli-superficie, #fff); background: var(--cor-eli-superficie, #fff);
border: 1.5px solid var(--cor-eli-borda, #D5D8CC); border: 1.5px solid var(--cor-eli-borda, #D5D8CC);
border-radius: 14px; border-radius: 14px;

View file

@ -67,6 +67,7 @@
:aria-selected="cell.selecionado" :aria-selected="cell.selecionado"
:aria-label="`${cell.dia} de ${nomeMesAtual} de ${anoAtual}`" :aria-label="`${cell.dia} de ${nomeMesAtual} de ${anoAtual}`"
@click="$emit('selecionarDia', cell)" @click="$emit('selecionarDia', cell)"
@dblclick="$emit('confirmar')"
> >
{{ cell.dia }} {{ cell.dia }}
</button> </button>
@ -91,6 +92,8 @@
@input="onHoraInput" @input="onHoraInput"
@blur="onHoraBlur" @blur="onHoraBlur"
@keydown="onHoraKeydown" @keydown="onHoraKeydown"
@focus="onInputFocusSelect"
@click.stop
/> />
<span class="eli-dh__hora-sep" aria-hidden="true">:</span> <span class="eli-dh__hora-sep" aria-hidden="true">:</span>
<input <input
@ -105,6 +108,8 @@
@input="onMinutoInput" @input="onMinutoInput"
@blur="onMinutoBlur" @blur="onMinutoBlur"
@keydown="onMinutoKeydown" @keydown="onMinutoKeydown"
@focus="onInputFocusSelect"
@click.stop
/> />
</div> </div>
</div> </div>
@ -214,6 +219,10 @@ export default defineComponent({
) )
} }
} }
function onInputFocusSelect(e: Event) {
;(e.target as HTMLInputElement).select()
}
return { return {
diasSemana, diasSemana,
MESES, MESES,
@ -225,6 +234,7 @@ export default defineComponent({
onMinutoInput, onMinutoInput,
onMinutoBlur, onMinutoBlur,
onMinutoKeydown, onMinutoKeydown,
onInputFocusSelect,
} }
}, },
}) })

View file

@ -44,18 +44,29 @@
opacity: 0.6; opacity: 0.6;
} }
.eli-dh__icone { .eli-dh__icone-btn {
flex-shrink: 0; display: flex;
width: 20px; align-items: center;
height: 20px; justify-content: center;
padding: 0;
border: none;
background: none;
cursor: pointer;
color: var(--cor-eli-neutro, #5E6553); color: var(--cor-eli-neutro, #5E6553);
transition: color 0.18s; transition: color 0.18s;
border-radius: 4px;
flex-shrink: 0;
} }
.eli-dh__campo--focado .eli-dh__icone { .eli-dh__icone-btn:hover:not(:disabled) {
color: var(--cor-eli-primaria, #688C00); color: var(--cor-eli-primaria, #688C00);
} }
.eli-dh__icone {
width: 20px;
height: 20px;
}
.eli-dh__conteudo { .eli-dh__conteudo {
flex: 1; flex: 1;
display: flex; display: flex;
@ -72,23 +83,35 @@
letter-spacing: 0.02em; letter-spacing: 0.02em;
text-transform: uppercase; text-transform: uppercase;
transition: color 0.18s; transition: color 0.18s;
cursor: pointer;
} }
.eli-dh__campo--focado .eli-dh__rotulo { color: var(--cor-eli-primaria, #688C00); } .eli-dh__campo--focado .eli-dh__rotulo { color: var(--cor-eli-primaria, #688C00); }
.eli-dh__campo--erro .eli-dh__rotulo { color: var(--cor-eli-erro, #C62828); } .eli-dh__campo--erro .eli-dh__rotulo { color: var(--cor-eli-erro, #C62828); }
.eli-dh__valor { .eli-dh__input {
width: 100%;
border: none;
background: transparent;
outline: none;
font-size: 15px; font-size: 15px;
font-weight: 500; font-weight: 500;
font-family: inherit;
color: var(--cor-eli-neutro-escuro, #3E4436); color: var(--cor-eli-neutro-escuro, #3E4436);
white-space: nowrap; padding: 0;
overflow: hidden; margin: 0;
text-overflow: ellipsis; line-height: 1.2;
} }
.eli-dh__valor--vazio { .eli-dh__input::placeholder {
color: var(--cor-eli-borda-forte, #B5BAA8); color: var(--cor-eli-borda-forte, #B5BAA8);
font-weight: 400; font-weight: 400;
opacity: 1;
}
.eli-dh__input:disabled {
cursor: not-allowed;
color: var(--cor-eli-neutro, #5E6553);
} }
.eli-dh__limpar { .eli-dh__limpar {

View file

@ -67,7 +67,71 @@ export function formatarValor(
modo: EntradaDataHora["opcoes"]["modo"], modo: EntradaDataHora["opcoes"]["modo"],
): string { ): string {
if (!valor) return "" if (!valor) return ""
return modo === "data" const d = dayjs(valor)
? dayjs(valor).format("DD/MM/YYYY") if (!d.isValid()) return ""
: dayjs(valor).format("DD/MM/YYYY HH:mm") return modo === "data" ? d.format("DD/MM/YYYY") : d.format("DD/MM/YYYY HH:mm")
}
/**
* Aplica máscara visual de data/hora (DD/MM/YYYY ou DD/MM/YYYY HH:mm) em texto digitado.
*/
export function aplicarMascaraDataHora(valor: string, modo: "data" | "dataHora"): string {
const digitos = valor.replace(/\D/g, "")
if (!digitos) return ""
const dia = digitos.slice(0, 2)
const mes = digitos.slice(2, 4)
const ano = digitos.slice(4, 8)
const hora = digitos.slice(8, 10)
const minuto = digitos.slice(10, 12)
let res = dia
if (mes) res += `/${mes}`
if (ano) res += `/${ano}`
if (modo === "dataHora") {
if (hora) res += ` ${hora}`
if (minuto) res += `:${minuto}`
}
return res
}
/**
* Converte um texto mascarado (ex: "15/08/2026 14:30") em string ISO 8601 com fuso local.
*/
export function converterTextoParaIso(
texto: string,
modo: "data" | "dataHora",
horaMinutoPadrao?: { hora: string; minuto: string },
): string | null {
const digitos = texto.replace(/\D/g, "")
if (digitos.length < 8) return null
const d = Number.parseInt(digitos.slice(0, 2), 10)
const m = Number.parseInt(digitos.slice(2, 4), 10)
const y = Number.parseInt(digitos.slice(4, 8), 10)
if (m < 1 || m > 12 || d < 1 || d > 31 || y < 1900 || y > 2100) return null
let h = 0
let min = 0
if (modo === "dataHora") {
if (digitos.length >= 10) {
h = Math.min(23, Number.parseInt(digitos.slice(8, 10), 10) || 0)
} else if (horaMinutoPadrao) {
h = Number.parseInt(horaMinutoPadrao.hora, 10) || 0
}
if (digitos.length >= 12) {
min = Math.min(59, Number.parseInt(digitos.slice(10, 12), 10) || 0)
} else if (horaMinutoPadrao) {
min = Number.parseInt(horaMinutoPadrao.minuto, 10) || 0
}
}
const strIso = `${y}-${String(m).padStart(2, "0")}-${String(d).padStart(2, "0")}T${String(h).padStart(2, "0")}:${String(min).padStart(2, "0")}:00`
const dt = dayjs(strIso)
return dt.isValid() ? dt.format() : null
} }

View file

@ -5,30 +5,49 @@
:id="`eli-dh-campo-${uid}`" :id="`eli-dh-campo-${uid}`"
class="eli-dh__campo" class="eli-dh__campo"
:class="{ :class="{
'eli-dh__campo--focado': aberto, 'eli-dh__campo--focado': aberto || focado,
'eli-dh__campo--erro': Boolean(opcoesEfetivas.erro), 'eli-dh__campo--erro': Boolean(opcoesEfetivas.erro),
'eli-dh__campo--desabilitado': desabilitadoEfetivo, 'eli-dh__campo--desabilitado': desabilitadoEfetivo,
}" }"
role="button"
:tabindex="desabilitadoEfetivo ? -1 : 0"
:aria-haspopup="true"
:aria-expanded="aberto"
:aria-label="opcoesEfetivas.rotulo"
@click="abrirSeHabilitado" @click="abrirSeHabilitado"
@keydown.enter.prevent="abrirSeHabilitado" >
@keydown.space.prevent="abrirSeHabilitado" <button
@keydown.escape="fechar" class="eli-dh__icone-btn"
type="button"
tabindex="-1"
:title="`Abrir calendário (${opcoesEfetivas.rotulo})`"
:disabled="desabilitadoEfetivo"
@click.stop="alternarPopover"
> >
<svg class="eli-dh__icone" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"> <svg class="eli-dh__icone" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<rect x="3" y="4" width="18" height="18" rx="3" ry="3" /> <rect x="3" y="4" width="18" height="18" rx="3" ry="3" />
<line x1="16" y1="2" x2="16" y2="6" /><line x1="8" y1="2" x2="8" y2="6" /><line x1="3" y1="10" x2="21" y2="10" /> <line x1="16" y1="2" x2="16" y2="6" /><line x1="8" y1="2" x2="8" y2="6" /><line x1="3" y1="10" x2="21" y2="10" />
</svg> </svg>
</button>
<div class="eli-dh__conteudo"> <div class="eli-dh__conteudo">
<span class="eli-dh__rotulo">{{ opcoesEfetivas.rotulo }}</span> <label :for="`eli-dh-input-${uid}`" class="eli-dh__rotulo">{{ opcoesEfetivas.rotulo }}</label>
<span class="eli-dh__valor" :class="{ 'eli-dh__valor--vazio': !valorFormatado }"> <input
{{ valorFormatado || opcoesEfetivas.placeholder || 'Selecione...' }} :id="`eli-dh-input-${uid}`"
</span> class="eli-dh__input"
type="text"
inputmode="numeric"
:value="textoDigitado"
:placeholder="opcoesEfetivas.placeholder || (modoEfetivo === 'data' ? 'DD/MM/AAAA' : 'DD/MM/AAAA HH:mm')"
:disabled="desabilitadoEfetivo"
:readonly="Boolean(opcoesEfetivas.apenasLeitura)"
:aria-expanded="aberto"
:aria-haspopup="true"
:aria-label="opcoesEfetivas.rotulo"
@click="abrirSeHabilitado"
@input="onTextInput"
@blur="onInputBlur"
@focus="onInputFocus"
@keydown.down.prevent="abrirSeHabilitado"
@keydown.escape="fechar"
/>
</div> </div>
<button <button
v-if="Boolean(opcoesEfetivas.limpavel) && valor && !desabilitadoEfetivo" v-if="Boolean(opcoesEfetivas.limpavel) && valor && !desabilitadoEfetivo"
class="eli-dh__limpar" class="eli-dh__limpar"
@ -47,7 +66,7 @@
<div v-if="mensagensVisiveis.length" class="eli-dh__mensagem eli-dh__mensagem--erro" role="alert"> <div v-if="mensagensVisiveis.length" class="eli-dh__mensagem eli-dh__mensagem--erro" role="alert">
{{ mensagensVisiveis[0] }} {{ mensagensVisiveis[0] }}
</div> </div>
<div v-else-if="opcoesEfetivas.dica && (opcoesEfetivas.dicaPersistente || aberto)" class="eli-dh__mensagem eli-dh__mensagem--dica"> <div v-else-if="opcoesEfetivas.dica && (opcoesEfetivas.dicaPersistente || aberto || focado)" class="eli-dh__mensagem eli-dh__mensagem--dica">
{{ opcoesEfetivas.dica }} {{ opcoesEfetivas.dica }}
</div> </div>
@ -72,8 +91,8 @@
@selecionarDia="selecionarDia" @selecionarDia="selecionarDia"
@mudarMes="mudarMes" @mudarMes="mudarMes"
@mudarAno="mudarAno" @mudarAno="mudarAno"
@update:horaTemp="horaTemp = $event" @update:horaTemp="atualizarHoraTemp"
@update:minutoTemp="minutoTemp = $event" @update:minutoTemp="atualizarMinutoTemp"
/> />
</div> </div>
</template> </template>

View file

@ -1,9 +1,14 @@
import dayjs from "dayjs" import dayjs from "dayjs"
import "dayjs/locale/pt-br" 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 { CampoDensidade, CampoVariante } from "../../../tipos"
import type { PadroesEntradas } from "../tiposEntradas" import type { PadroesEntradas } from "../tiposEntradas"
import { 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") dayjs.locale("pt-br")
@ -18,13 +23,6 @@ export interface CelulaCalendario {
desabilitado: boolean 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) */ /** Função de emit genérica do Vue (compatível com defineComponent) */
type EmitFn = (event: string, payload?: unknown) => void type EmitFn = (event: string, payload?: unknown) => void
@ -53,113 +51,91 @@ let contadorUid = 0
export function useDataHora(props: Props, emit: EmitFn) { export function useDataHora(props: Props, emit: EmitFn) {
const uid = ++contadorUid const uid = ++contadorUid
const aberto = ref(false) const aberto = ref(false)
const estiloPopover = ref<Record<string, string>>({})
const referencia = ref(dayjs()) const referencia = ref(dayjs())
const diaSelecionado = ref<dayjs.Dayjs | null>(null) const diaSelecionado = ref<dayjs.Dayjs | null>(null)
const horaTemp = ref("00") const horaTemp = ref("00")
const minutoTemp = ref("00") const minutoTemp = ref("00")
const opcoesEfetivas = computed<EntradaDataHora["opcoes"]>( const {
() => opcoesEfetivas,
props.opcoes ?? { modoEfetivo,
rotulo: props.rotulo ?? "Data", desabilitadoEfetivo,
placeholder: props.placeholder ?? "", effectiveModelValue,
modo: props.modo ?? "dataHora", valor,
limpavel: props.limpavel, mensagensVisiveis,
erro: props.erro, valorFormatado,
mensagensErro: props.mensagensErro, nomeMesAtual,
dica: props.dica, anoAtual,
dicaPersistente: props.dicaPersistente, mesAtual,
densidade: props.densidade, listaAnos,
variante: props.variante, celulas,
min: props.min, } = useDataHoraProps({ props, referencia, diaSelecionado })
max: props.max,
},
)
const modoEfetivo = computed<"data" | "dataHora">(() => opcoesEfetivas.value.modo ?? "dataHora") const { estiloPopover, calcularPosicao, adicionarListenersPosicao, removerListenersPosicao } =
const desabilitadoEfetivo = computed<boolean>(() => Boolean(props.desabilitado)) useDataHoraPosicao({ uid, modoEfetivo })
const effectiveModelValue = computed<string | null>(() => function emitirValor(iso: string | null) {
props.value !== undefined ? (props.value ?? null) : (props.modelValue ?? null), ;["update:value", "input", "change", "update:modelValue", "alterar"].forEach((e) => {
) emit(e, iso)
const valor = computed(() => effectiveModelValue.value)
const mensagensVisiveis = computed<string[]>(() => {
const m = opcoesEfetivas.value.erro ? opcoesEfetivas.value.mensagensErro : null
return m ? (Array.isArray(m) ? m : [m]) : []
}) })
}
const valorFormatado = computed<string>(() => formatarValor(valor.value, modoEfetivo.value)) const { textoDigitado, focado, onTextInput, onInputFocus, onInputBlur } = useDataHoraInput({
modoEfetivo,
const nomeMesAtual = computed(() => MESES[referencia.value.month()]) valor,
const anoAtual = computed(() => referencia.value.year()) horaTemp,
const mesAtual = computed(() => referencia.value.month()) minutoTemp,
const listaAnos = computed(() => { emitirValor,
const y = referencia.value.year() emit,
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<CelulaCalendario[]>(() =>
gerarCelulas(
referencia.value,
diaSelecionado.value,
opcoesEfetivas.value.min,
opcoesEfetivas.value.max,
),
)
watch( watch(
effectiveModelValue, effectiveModelValue,
(v) => { (v) => {
if (v) { if (v) {
const d = dayjs(v) const d = dayjs(v)
if (d.isValid()) {
diaSelecionado.value = d.startOf("day") diaSelecionado.value = d.startOf("day")
referencia.value = d.startOf("month") referencia.value = d.startOf("month")
horaTemp.value = d.format("HH") horaTemp.value = d.format("HH")
minutoTemp.value = d.format("mm") minutoTemp.value = d.format("mm")
} else { textoDigitado.value = formatarValor(v, modoEfetivo.value)
return
}
}
diaSelecionado.value = null diaSelecionado.value = null
horaTemp.value = "00" horaTemp.value = "00"
minutoTemp.value = "00" minutoTemp.value = "00"
} textoDigitado.value = ""
}, },
{ immediate: true }, { immediate: true },
) )
function calcularPosicao() {
const el = document.getElementById(`eli-dh-campo-${uid}`)
if (!el) return
const rect = el.getBoundingClientRect()
const top = rect.bottom + window.scrollY + 6
let left = rect.left + window.scrollX
if (left + 316 > window.innerWidth + window.scrollX - 8)
left = rect.right + window.scrollX - 316
estiloPopover.value = { position: "absolute", top: `${top}px`, left: `${left}px` }
}
function abrirSeHabilitado() { function abrirSeHabilitado() {
if (desabilitadoEfetivo.value) return if (desabilitadoEfetivo.value) return
aberto.value = !aberto.value if (!aberto.value) {
if (aberto.value) { aberto.value = true
calcularPosicao()
emit("foco") emit("foco")
emit("focus") emit("focus")
nextTick(calcularPosicao) nextTick(calcularPosicao)
adicionarListenersPosicao()
}
}
function alternarPopover() {
if (desabilitadoEfetivo.value) return
if (aberto.value) {
fechar()
} else {
abrirSeHabilitado()
} }
} }
function fechar() { function fechar() {
if (!aberto.value) return if (!aberto.value) return
aberto.value = false aberto.value = false
removerListenersPosicao()
emit("desfoco") emit("desfoco")
emit("blur") emit("blur")
} }
@ -179,13 +155,27 @@ export function useDataHora(props: Props, emit: EmitFn) {
function selecionarDia(cell: CelulaCalendario) { function selecionarDia(cell: CelulaCalendario) {
if (cell.desabilitado) return if (cell.desabilitado) return
diaSelecionado.value = cell.data.startOf("day") diaSelecionado.value = cell.data.startOf("day")
if (modoEfetivo.value === "data") confirmar() confirmar()
} }
function emitirValor(iso: string | null) { function atualizarHoraTemp(v: string) {
;["update:value", "input", "change", "update:modelValue", "alterar"].forEach((e) => { horaTemp.value = v
emit(e, iso) if (diaSelecionado.value) {
}) const h = Number.parseInt(v, 10) || 0
const m = Number.parseInt(minutoTemp.value, 10) || 0
const r = diaSelecionado.value.hour(h).minute(m).second(0)
emitirValor(r.format())
}
}
function atualizarMinutoTemp(v: string) {
minutoTemp.value = v
if (diaSelecionado.value) {
const h = Number.parseInt(horaTemp.value, 10) || 0
const m = Number.parseInt(v, 10) || 0
const r = diaSelecionado.value.hour(h).minute(m).second(0)
emitirValor(r.format())
}
} }
function confirmar() { function confirmar() {
@ -201,6 +191,7 @@ export function useDataHora(props: Props, emit: EmitFn) {
} }
function limpar() { function limpar() {
textoDigitado.value = ""
emitirValor(null) emitirValor(null)
} }
@ -215,11 +206,16 @@ export function useDataHora(props: Props, emit: EmitFn) {
} }
onMounted(() => document.addEventListener("click", onDocClick, true)) onMounted(() => document.addEventListener("click", onDocClick, true))
onBeforeUnmount(() => document.removeEventListener("click", onDocClick, true)) onBeforeUnmount(() => {
document.removeEventListener("click", onDocClick, true)
removerListenersPosicao()
})
return { return {
uid, uid,
aberto, aberto,
focado,
textoDigitado,
estiloPopover, estiloPopover,
referencia, referencia,
diaSelecionado, diaSelecionado,
@ -239,10 +235,16 @@ export function useDataHora(props: Props, emit: EmitFn) {
mudarAno, mudarAno,
celulas, celulas,
abrirSeHabilitado, abrirSeHabilitado,
alternarPopover,
fechar, fechar,
navegarMes, navegarMes,
selecionarDia, selecionarDia,
confirmar, confirmar,
limpar, limpar,
onTextInput,
onInputFocus,
onInputBlur,
atualizarHoraTemp,
atualizarMinutoTemp,
} }
} }

View file

@ -0,0 +1,71 @@
import { type Ref, ref } from "vue"
import { aplicarMascaraDataHora, converterTextoParaIso, formatarValor } from "./calendarioHelpers"
interface UseDataHoraInputOptions {
modoEfetivo: Ref<"data" | "dataHora">
valor: Ref<string | null>
horaTemp: Ref<string>
minutoTemp: Ref<string>
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,
}
}

View file

@ -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<Record<string, string>>({})
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,
}
}

View file

@ -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<dayjs.Dayjs>
diaSelecionado: Ref<dayjs.Dayjs | null>
}
/**
* Composable responsável pelo cálculo de propriedades derivadas e opções do EliEntradaDataHora.
*/
export function useDataHoraProps({ props, referencia, diaSelecionado }: UseDataHoraPropsOptions) {
const opcoesEfetivas = computed<EntradaDataHora["opcoes"]>(() => {
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>(() => Boolean(props.desabilitado))
const effectiveModelValue = computed<string | null>(() =>
props.value !== undefined ? (props.value ?? null) : (props.modelValue ?? null),
)
const valor = computed(() => effectiveModelValue.value)
const mensagensVisiveis = computed<string[]>(() => {
const m = opcoesEfetivas.value.erro ? opcoesEfetivas.value.mensagensErro : null
return m ? (Array.isArray(m) ? m : [m]) : []
})
const valorFormatado = computed<string>(() => 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,
}
}

View file

@ -1,12 +1,5 @@
<template> <template>
<div class="eli-tabela"> <div class="eli-tabela">
<EliTabelaDebug :isDev="isDev" :menuAberto="menuAberto" :menuPopupPos="menuPopupPos">
<div>paginaAtual: {{ paginaAtual }}</div>
<div>limit: {{ registrosPorConsulta }}</div>
<div>texto_busca: {{ (valorBusca || '').trim() }}</div>
<div>filtrosAvancadosAtivos: {{ JSON.stringify(filtrosAvancadosAtivos) }}</div>
<div>quantidadeTotal: {{ quantidade }}</div>
</EliTabelaDebug>
<EliTabelaCabecalho <EliTabelaCabecalho
v-if="exibirBusca || temAcoesSuperiores" v-if="exibirBusca || temAcoesSuperiores"
@ -123,7 +116,6 @@ import { defineComponent, type PropType } from "vue"
import "./EliTabela.css" import "./EliTabela.css"
import EliTabelaBody from "./EliTabelaBody.vue" import EliTabelaBody from "./EliTabelaBody.vue"
import EliTabelaCabecalho from "./EliTabelaCabecalho.vue" import EliTabelaCabecalho from "./EliTabelaCabecalho.vue"
import EliTabelaDebug from "./EliTabelaDebug.vue"
import EliTabelaEstados from "./EliTabelaEstados.vue" import EliTabelaEstados from "./EliTabelaEstados.vue"
import EliTabelaHead from "./EliTabelaHead.vue" import EliTabelaHead from "./EliTabelaHead.vue"
import EliTabelaMenuAcoes from "./EliTabelaMenuAcoes.vue" import EliTabelaMenuAcoes from "./EliTabelaMenuAcoes.vue"
@ -143,7 +135,6 @@ export default defineComponent({
components: { components: {
EliTabelaCabecalho, EliTabelaCabecalho,
EliTabelaEstados, EliTabelaEstados,
EliTabelaDebug,
EliTabelaHead, EliTabelaHead,
EliTabelaBody, EliTabelaBody,
EliTabelaMenuAcoes, EliTabelaMenuAcoes,

View file

@ -72,6 +72,8 @@
.playground-console-scroll::-webkit-scrollbar-thumb:hover { .playground-console-scroll::-webkit-scrollbar-thumb:hover {
background: #52525b; background: #52525b;
} }
</style>
<script lang="ts"> <script lang="ts">
import { defineComponent, provide, ref } from "vue" import { defineComponent, provide, ref } from "vue"
import { EliFeedbackWidget } from "../componentes/FeedbackWidget" import { EliFeedbackWidget } from "../componentes/FeedbackWidget"