feat: suporte para selecionar campos na tabela

This commit is contained in:
marcio 2026-05-28 10:55:04 -03:00
parent 7dc071373a
commit 0886fd5c3e
16 changed files with 5867 additions and 4684 deletions

9715
dist/eli-vue.es.js vendored

File diff suppressed because one or more lines are too long

99
dist/eli-vue.umd.js vendored

File diff suppressed because one or more lines are too long

View file

@ -11,6 +11,11 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
type: PropType<tipoEliTabelaConsulta<any>>;
required: true;
};
/** Array de itens selecionados (v-model:selecionados) */
selecionados: {
type: PropType<unknown[]>;
default: () => never[];
};
}>, {
isDev: boolean;
tabela: import("vue").ComputedRef<tipoEliTabelaConsulta<any>>;
@ -77,6 +82,12 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
temColunasInvisiveis: import("vue").ComputedRef<boolean>;
colunasInvisiveisEfetivas: import("vue").ComputedRef<tipoEliColuna<any>[]>;
linhasExpandidas: import("vue").Ref<Record<number, boolean>, Record<number, boolean>>;
mostrarSelecao: import("vue").ComputedRef<boolean>;
isSelecionado: (linha: unknown) => boolean;
alternarSelecao: (linha: unknown) => void;
todosSelecionados: import("vue").ComputedRef<boolean>;
algunsSelecionados: import("vue").ComputedRef<boolean>;
alternarSelecaoTodos: (forcarMarcar?: boolean) => void;
abrirModalColunas: () => void;
abrirModalFiltro: () => void;
fecharModalColunas: () => void;
@ -129,13 +140,22 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
top: number;
left: number;
}>;
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, "update:selecionados"[], "update:selecionados", import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
/** Configuração principal da tabela (colunas, consulta e ações) */
tabela: {
type: PropType<tipoEliTabelaConsulta<any>>;
required: true;
};
}>> & Readonly<{}>, {}, {}, {
/** Array de itens selecionados (v-model:selecionados) */
selecionados: {
type: PropType<unknown[]>;
default: () => never[];
};
}>> & Readonly<{
"onUpdate:selecionados"?: ((...args: any[]) => any) | undefined;
}>, {
selecionados: unknown[];
}, {}, {
EliTabelaCabecalho: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
exibirBusca: {
type: BooleanConstructor;
@ -366,6 +386,18 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
type: PropType<"asc" | "desc">;
required: true;
};
mostrarSelecao: {
type: BooleanConstructor;
default: boolean;
};
todosSelecionados: {
type: BooleanConstructor;
default: boolean;
};
algunsSelecionados: {
type: BooleanConstructor;
default: boolean;
};
}>, {
ArrowUp: import("@lucide/vue").LucideIcon;
ArrowDown: import("@lucide/vue").LucideIcon;
@ -373,6 +405,7 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
emitAlternarOrdenacao: (chave: string) => void;
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
alternarOrdenacao(chave: string): boolean;
"alternar-selecao-todos": () => true;
}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
colunas: {
type: PropType<Array<tipoEliColuna<any>>>;
@ -394,10 +427,26 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
type: PropType<"asc" | "desc">;
required: true;
};
mostrarSelecao: {
type: BooleanConstructor;
default: boolean;
};
todosSelecionados: {
type: BooleanConstructor;
default: boolean;
};
algunsSelecionados: {
type: BooleanConstructor;
default: boolean;
};
}>> & Readonly<{
"onAlternar-selecao-todos"?: (() => any) | undefined;
onAlternarOrdenacao?: ((chave: string) => any) | undefined;
}>, {
mostrarSelecao: boolean;
colunaOrdenacao: string | null;
todosSelecionados: boolean;
algunsSelecionados: boolean;
}, {}, {
ArrowUp: import("@lucide/vue").LucideIcon;
ArrowDown: import("@lucide/vue").LucideIcon;
@ -443,6 +492,18 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
type: PropType<(indice: number) => void>;
required: true;
};
mostrarSelecao: {
type: BooleanConstructor;
default: boolean;
};
isSelecionado: {
type: PropType<(linha: unknown) => boolean>;
required: true;
};
alternarSelecao: {
type: PropType<(linha: unknown) => void>;
required: true;
};
}>, {
ChevronRight: import("@lucide/vue").LucideIcon;
ChevronDown: import("@lucide/vue").LucideIcon;
@ -487,7 +548,21 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
type: PropType<(indice: number) => void>;
required: true;
};
}>> & Readonly<{}>, {}, {}, {
mostrarSelecao: {
type: BooleanConstructor;
default: boolean;
};
isSelecionado: {
type: PropType<(linha: unknown) => boolean>;
required: true;
};
alternarSelecao: {
type: PropType<(linha: unknown) => void>;
required: true;
};
}>> & Readonly<{}>, {
mostrarSelecao: boolean;
}, {}, {
EliTabelaCelula: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
celula: {
type: PropType<import("./types-eli-tabela").tipoComponenteCelula>;
@ -828,11 +903,13 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
}>, {
visiveisLocal: import("vue").Ref<string[], string[]>;
invisiveisLocal: import("vue").Ref<string[], string[]>;
itemSendoArrastado: import("vue").Ref<string | null, string | null>;
emitFechar: () => void;
emitSalvar: () => void;
onDragStart: (e: DragEvent, rotulo: string, origem: "visiveis" | "invisiveis", index: number) => void;
onDropItem: (e: DragEvent, destino: "visiveis" | "invisiveis", index: number) => void;
onDropLista: (e: DragEvent, destino: "visiveis" | "invisiveis", _index: number | null) => void;
onDragStart: (e: DragEvent, rotulo: string) => void;
onDragEnd: () => void;
onDragOverItem: (alvoRotulo: string, destino: "visiveis" | "invisiveis") => void;
onDragOverLista: (destino: "visiveis" | "invisiveis") => void;
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
fechar(): true;
salvar(_config: EliTabelaColunasConfig): true;

View file

@ -43,6 +43,18 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
type: PropType<(indice: number) => void>;
required: true;
};
mostrarSelecao: {
type: BooleanConstructor;
default: boolean;
};
isSelecionado: {
type: PropType<(linha: unknown) => boolean>;
required: true;
};
alternarSelecao: {
type: PropType<(linha: unknown) => void>;
required: true;
};
}>, {
ChevronRight: import("@lucide/vue").LucideIcon;
ChevronDown: import("@lucide/vue").LucideIcon;
@ -87,7 +99,21 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
type: PropType<(indice: number) => void>;
required: true;
};
}>> & Readonly<{}>, {}, {}, {
mostrarSelecao: {
type: BooleanConstructor;
default: boolean;
};
isSelecionado: {
type: PropType<(linha: unknown) => boolean>;
required: true;
};
alternarSelecao: {
type: PropType<(linha: unknown) => void>;
required: true;
};
}>> & Readonly<{}>, {
mostrarSelecao: boolean;
}, {}, {
EliTabelaCelula: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
celula: {
type: PropType<import("./types-eli-tabela").tipoComponenteCelula>;

View file

@ -23,6 +23,18 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
type: PropType<"asc" | "desc">;
required: true;
};
mostrarSelecao: {
type: BooleanConstructor;
default: boolean;
};
todosSelecionados: {
type: BooleanConstructor;
default: boolean;
};
algunsSelecionados: {
type: BooleanConstructor;
default: boolean;
};
}>, {
ArrowUp: import("@lucide/vue").LucideIcon;
ArrowDown: import("@lucide/vue").LucideIcon;
@ -30,6 +42,7 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
emitAlternarOrdenacao: (chave: string) => void;
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
alternarOrdenacao(chave: string): boolean;
"alternar-selecao-todos": () => true;
}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
colunas: {
type: PropType<Array<tipoEliColuna<any>>>;
@ -51,10 +64,26 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
type: PropType<"asc" | "desc">;
required: true;
};
mostrarSelecao: {
type: BooleanConstructor;
default: boolean;
};
todosSelecionados: {
type: BooleanConstructor;
default: boolean;
};
algunsSelecionados: {
type: BooleanConstructor;
default: boolean;
};
}>> & Readonly<{
"onAlternar-selecao-todos"?: (() => any) | undefined;
onAlternarOrdenacao?: ((chave: string) => any) | undefined;
}>, {
mostrarSelecao: boolean;
colunaOrdenacao: string | null;
todosSelecionados: boolean;
algunsSelecionados: boolean;
}, {}, {
ArrowUp: import("@lucide/vue").LucideIcon;
ArrowDown: import("@lucide/vue").LucideIcon;

View file

@ -24,11 +24,13 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
}>, {
visiveisLocal: import("vue").Ref<string[], string[]>;
invisiveisLocal: import("vue").Ref<string[], string[]>;
itemSendoArrastado: import("vue").Ref<string | null, string | null>;
emitFechar: () => void;
emitSalvar: () => void;
onDragStart: (e: DragEvent, rotulo: string, origem: OrigemLista, index: number) => void;
onDropItem: (e: DragEvent, destino: OrigemLista, index: number) => void;
onDropLista: (e: DragEvent, destino: OrigemLista, _index: number | null) => void;
onDragStart: (e: DragEvent, rotulo: string) => void;
onDragEnd: () => void;
onDragOverItem: (alvoRotulo: string, destino: OrigemLista) => void;
onDragOverLista: (destino: OrigemLista) => void;
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
fechar(): true;
salvar(_config: EliTabelaColunasConfig): true;

View file

@ -72,6 +72,10 @@ export type tipoEliTabelaConsulta<T> = {
nome: string;
/** Indica se a caixa de busca deve ser exibida acima da tabela. */
mostrarCaixaDeBusca?: boolean;
/** Habilita seleção múltipla (checkboxes) nas linhas da tabela. */
mostrar_selecao?: boolean;
/** Chave (ex: "id") para identificar unicamente a linha nas seleções entre páginas. */
chave_selecao?: keyof T;
/** Lista de colunas da tabela. */
colunas: tipoEliColuna<T>[];
/** Quantidade de registros solicitados por consulta (padrão `10`). */

View file

@ -1,6 +1,6 @@
{
"name": "eli-vue",
"version": "0.1.128",
"version": "0.1.144",
"private": false,
"main": "./dist/eli-vue.umd.js",
"module": "./dist/eli-vue.es.js",

16
pnpm-lock.yaml generated
View file

@ -19,7 +19,7 @@ importers:
version: 1.11.19
p-comuns:
specifier: git+https://git2.idz.one/publico/_comuns.git
version: git+https://git2.idz.one/publico/_comuns.git#64ddd6ad8af5e09a81d247fb1b23c2b05731d321(cross-fetch@4.1.0)(dayjs@1.11.19)(uuid@13.0.0)(zod@4.3.6)
version: git+https://git2.idz.one/publico/_comuns.git#64ddd6ad8af5e09a81d247fb1b23c2b05731d321(cross-fetch@4.1.0)(dayjs@1.11.19)(uuid@13.0.0)(zod@4.4.3)
p-respostas:
specifier: git+https://git2.idz.one/publico/_respostas.git
version: git+https://git2.idz.one/publico/_respostas.git#8c24d790ace7255404745dcbdf12c5396e8b9843(cross-fetch@4.1.0)(dayjs@1.11.19)(uuid@13.0.0)
@ -27,8 +27,8 @@ importers:
specifier: ^13.0.0
version: 13.0.0
zod:
specifier: 4.3.6
version: 4.3.6
specifier: 4.4.3
version: 4.4.3
devDependencies:
'@biomejs/biome':
specifier: 2.4.12
@ -837,8 +837,8 @@ packages:
zod@4.1.4:
resolution: {integrity: sha512-2YqJuWkU6IIK9qcE4k1lLLhyZ6zFw7XVRdQGpV97jEIZwTrscUw+DY31Xczd8nwaoksyJUIxCojZXwckJovWxA==}
zod@4.3.6:
resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==}
zod@4.4.3:
resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==}
snapshots:
@ -1315,12 +1315,12 @@ snapshots:
uuid: 13.0.0
zod: 4.1.4
p-comuns@git+https://git2.idz.one/publico/_comuns.git#64ddd6ad8af5e09a81d247fb1b23c2b05731d321(cross-fetch@4.1.0)(dayjs@1.11.19)(uuid@13.0.0)(zod@4.3.6):
p-comuns@git+https://git2.idz.one/publico/_comuns.git#64ddd6ad8af5e09a81d247fb1b23c2b05731d321(cross-fetch@4.1.0)(dayjs@1.11.19)(uuid@13.0.0)(zod@4.4.3):
dependencies:
cross-fetch: 4.1.0
dayjs: 1.11.19
uuid: 13.0.0
zod: 4.3.6
zod: 4.4.3
p-respostas@git+https://git2.idz.one/publico/_respostas.git#8c24d790ace7255404745dcbdf12c5396e8b9843(cross-fetch@4.1.0)(dayjs@1.11.19)(uuid@13.0.0):
dependencies:
@ -1466,4 +1466,4 @@ snapshots:
zod@4.1.4: {}
zod@4.3.6: {}
zod@4.4.3: {}

View file

@ -11,6 +11,20 @@
overflow: visible;
}
/* Corrige o vazamento (pontas quadradas) do fundo dos th/td nas bordas arredondadas da tabela */
.eli-tabela__table thead tr:first-child th:first-child {
border-top-left-radius: 11px;
}
.eli-tabela__table thead tr:first-child th:last-child {
border-top-right-radius: 11px;
}
.eli-tabela__table tbody tr:last-child td:first-child {
border-bottom-left-radius: 11px;
}
.eli-tabela__table tbody tr:last-child td:last-child {
border-bottom-right-radius: 11px;
}
.eli-tabela__tbody {
overflow: visible;
}
@ -66,7 +80,7 @@
}
.eli-tabela__th-botao--ativo {
color: rgba(37, 99, 235, 0.95);
color: rgba(15, 23, 42, 0.85);
}
.eli-tabela__th-texto {
@ -395,3 +409,70 @@
grid-template-columns: 1fr;
}
}
/* =========================================
* Seleção (Checkboxes)
* ========================================= */
.eli-tabela__th--selecao,
.eli-tabela__td--selecao {
width: 48px;
min-width: 48px;
max-width: 48px;
text-align: center;
padding: 0 8px;
vertical-align: middle;
}
.eli-tabela__checkbox {
appearance: none;
-webkit-appearance: none;
width: 15px;
height: 15px;
cursor: pointer;
border: 2px solid rgba(0, 0, 0, 0.54);
border-radius: 2px;
margin: 0;
vertical-align: middle;
position: relative;
transition: all 0.2s ease-in-out;
background-color: transparent;
outline: none;
}
.eli-tabela__checkbox:hover {
border-color: rgba(0, 0, 0, 0.87);
}
.eli-tabela__checkbox:checked,
.eli-tabela__checkbox:indeterminate {
background-color: #1976d2;
border-color: #1976d2;
}
.eli-tabela__checkbox:checked::after {
content: '';
position: absolute;
top: 0px;
left: 4px;
width: 4px;
height: 9px;
border: solid white;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
.eli-tabela__checkbox:indeterminate::after {
content: '';
position: absolute;
top: 5px;
left: 2px;
width: 8px;
height: 2px;
background-color: white;
border-radius: 1px;
}
.eli-tabela__checkbox:focus-visible {
box-shadow: 0 0 0 2px rgba(25, 118, 210, 0.3);
}

View file

@ -45,7 +45,11 @@
:temColunasInvisiveis="temColunasInvisiveis"
:colunaOrdenacao="colunaOrdenacao"
:direcaoOrdenacao="direcaoOrdenacao"
:mostrarSelecao="mostrarSelecao"
:todosSelecionados="todosSelecionados"
:algunsSelecionados="algunsSelecionados"
@alternar-ordenacao="alternarOrdenacao"
@alternar-selecao-todos="alternarSelecaoTodos"
/>
<tbody v-if="carregando || Boolean(erro) || !linhas.length">
@ -73,6 +77,9 @@
:possuiAcoes="possuiAcoes"
:toggleMenu="toggleMenu"
:alternarLinhaExpandida="alternarLinhaExpandida"
:mostrarSelecao="mostrarSelecao"
:isSelecionado="isSelecionado"
:alternarSelecao="alternarSelecao"
/>
</table>
@ -165,8 +172,14 @@ export default defineComponent({
type: Object as PropType<tipoEliTabelaConsulta<any>>,
required: true,
},
/** Array de itens selecionados (v-model:selecionados) */
selecionados: {
type: Array as PropType<unknown[]>,
default: () => [],
},
},
setup(props) {
emits: ["update:selecionados"],
setup(props, { emit }) {
/** Flag para habilitar elementos de debug */
const isDev = import.meta.env.DEV
/** Estados de carregamento/erro e dados retornados */
@ -258,6 +271,84 @@ export default defineComponent({
/** Alias reativo da prop tabela */
const tabela = computed(() => props.tabela)
/** Seleção Múltipla */
const mostrarSelecao = computed(() => Boolean(props.tabela.mostrar_selecao))
const chaveSelecao = computed(
() => props.tabela.chave_selecao as string | undefined,
)
function obterChaveLinha(linha: unknown): unknown {
if (chaveSelecao.value && linha && typeof linha === "object") {
return (linha as Record<string, unknown>)[chaveSelecao.value]
}
return linha
}
function isSelecionado(linha: unknown): boolean {
const chave = obterChaveLinha(linha)
if (chaveSelecao.value) {
return props.selecionados.some((s) => obterChaveLinha(s) === chave)
}
return props.selecionados.includes(linha)
}
function alternarSelecao(linha: unknown) {
const atual = [...props.selecionados]
const chave = obterChaveLinha(linha)
let index = -1
if (chaveSelecao.value) {
index = atual.findIndex((s) => obterChaveLinha(s) === chave)
} else {
index = atual.indexOf(linha)
}
if (index === -1) {
atual.push(linha)
} else {
atual.splice(index, 1)
}
emit("update:selecionados", atual)
}
const todosSelecionados = computed(() => {
if (linhasPaginadas.value.length === 0) return false
return linhasPaginadas.value.every((linha) => isSelecionado(linha))
})
const algunsSelecionados = computed(() => {
if (linhasPaginadas.value.length === 0) return false
return (
linhasPaginadas.value.some((linha) => isSelecionado(linha)) &&
!todosSelecionados.value
)
})
function alternarSelecaoTodos(forcarMarcar?: boolean) {
const marcarTodos = forcarMarcar ?? !todosSelecionados.value
let atual = [...props.selecionados]
if (marcarTodos) {
// Adiciona os que faltam na página atual
for (const linha of linhasPaginadas.value) {
if (!isSelecionado(linha)) {
atual.push(linha)
}
}
} else {
// Remove os da página atual
const chavesPagina = linhasPaginadas.value.map(obterChaveLinha)
if (chaveSelecao.value) {
atual = atual.filter(
(s) => !chavesPagina.includes(obterChaveLinha(s)),
)
} else {
atual = atual.filter((s) => !chavesPagina.includes(s))
}
}
emit("update:selecionados", atual)
}
/** Exibição da busca e ações do cabeçalho */
const exibirBusca = computed(() =>
Boolean(props.tabela.mostrarCaixaDeBusca),
@ -859,6 +950,14 @@ export default defineComponent({
colunasInvisiveisEfetivas,
linhasExpandidas,
// selecao
mostrarSelecao,
isSelecionado,
alternarSelecao,
todosSelecionados,
algunsSelecionados,
alternarSelecaoTodos,
abrirModalColunas,
abrirModalFiltro,
fecharModalColunas,

View file

@ -25,6 +25,17 @@
</button>
</td>
<td v-if="mostrarSelecao" class="eli-tabela__td eli-tabela__td--selecao" :key="`td-${i}-selecao`">
<input
type="checkbox"
class="eli-tabela__checkbox"
:checked="isSelecionado(linha)"
@change="alternarSelecao(linha)"
@click.stop
aria-label="Selecionar linha"
/>
</td>
<td
v-for="(coluna, j) in colunas"
:key="`td-${i}-${j}`"
@ -63,7 +74,7 @@
>
<td
class="eli-tabela__td eli-tabela__td--detalhes"
:colspan="(temColunasInvisiveis ? 1 : 0) + colunas.length + (temAcoes ? 1 : 0)"
:colspan="(temColunasInvisiveis ? 1 : 0) + (mostrarSelecao ? 1 : 0) + colunas.length + (temAcoes ? 1 : 0)"
>
<EliTabelaDetalhesLinha :linha="linha" :colunasInvisiveis="colunasInvisiveis" />
</td>
@ -131,6 +142,18 @@ export default defineComponent({
type: Function as PropType<(indice: number) => void>,
required: true,
},
mostrarSelecao: {
type: Boolean,
default: false,
},
isSelecionado: {
type: Function as PropType<(linha: unknown) => boolean>,
required: true,
},
alternarSelecao: {
type: Function as PropType<(linha: unknown) => void>,
required: true,
},
},
setup() {
return {

View file

@ -3,6 +3,17 @@
<tr class="eli-tabela__tr eli-tabela__tr--header">
<th v-if="temColunasInvisiveis" class="eli-tabela__th eli-tabela__th--expander" scope="col"></th>
<th v-if="mostrarSelecao" class="eli-tabela__th eli-tabela__th--selecao" scope="col">
<input
type="checkbox"
class="eli-tabela__checkbox"
:checked="todosSelecionados"
:indeterminate="algunsSelecionados"
@change="$emit('alternar-selecao-todos')"
aria-label="Selecionar todos os itens da página"
/>
</th>
<th
v-for="(coluna, idx) in colunas"
:key="`th-${idx}`"
@ -78,11 +89,24 @@ export default defineComponent({
type: String as PropType<"asc" | "desc">,
required: true,
},
mostrarSelecao: {
type: Boolean,
default: false,
},
todosSelecionados: {
type: Boolean,
default: false,
},
algunsSelecionados: {
type: Boolean,
default: false,
},
},
emits: {
alternarOrdenacao(chave: string) {
return typeof chave === "string" && chave.length > 0
},
"alternar-selecao-todos": () => true,
},
setup(_props, { emit }) {
// biome-ignore lint/suspicious/noExplicitAny: dynamic column

View file

@ -1,74 +1,88 @@
<template>
<div v-if="aberto" class="eli-tabela-modal-colunas__overlay" role="presentation" @click.self="emitFechar">
<div
class="eli-tabela-modal-colunas__modal"
role="dialog"
aria-modal="true"
aria-label="Configurar colunas"
>
<header class="eli-tabela-modal-colunas__header">
<h3 class="eli-tabela-modal-colunas__titulo">Colunas</h3>
<button type="button" class="eli-tabela-modal-colunas__fechar" aria-label="Fechar" @click="emitFechar">
×
</button>
</header>
<Teleport to="body">
<div v-if="aberto" class="eli-tabela-modal-colunas__overlay" role="presentation" @click.self="emitFechar">
<div
class="eli-tabela-modal-colunas__modal"
role="dialog"
aria-modal="true"
aria-label="Configurar colunas"
>
<header class="eli-tabela-modal-colunas__header">
<h3 class="eli-tabela-modal-colunas__titulo">Colunas</h3>
<button type="button" class="eli-tabela-modal-colunas__fechar" aria-label="Fechar" @click="emitFechar">
×
</button>
</header>
<div class="eli-tabela-modal-colunas__conteudo">
<div class="eli-tabela-modal-colunas__coluna">
<div class="eli-tabela-modal-colunas__coluna-titulo">Visíveis</div>
<div
class="eli-tabela-modal-colunas__lista"
@dragover.prevent
@drop="(e) => onDropLista(e, 'visiveis', null)"
>
<div
v-for="(rotulo, idx) in visiveisLocal"
:key="`vis-${rotulo}`"
class="eli-tabela-modal-colunas__item"
draggable="true"
@dragstart="(e) => onDragStart(e, rotulo, 'visiveis', idx)"
@dragover.prevent
@drop="(e) => onDropItem(e, 'visiveis', idx)"
<div class="eli-tabela-modal-colunas__conteudo">
<div class="eli-tabela-modal-colunas__coluna">
<div class="eli-tabela-modal-colunas__coluna-titulo">Visíveis</div>
<TransitionGroup
name="list"
tag="div"
class="eli-tabela-modal-colunas__lista"
@dragover.prevent="onDragOverLista('visiveis')"
@drop="onDragEnd"
>
<span class="eli-tabela-modal-colunas__item-handle" aria-hidden="true">⋮⋮</span>
<span class="eli-tabela-modal-colunas__item-texto">{{ rotulo }}</span>
</div>
<div
v-for="rotulo in visiveisLocal"
:key="`vis-${rotulo}`"
class="eli-tabela-modal-colunas__item"
:class="{
'eli-tabela-modal-colunas__item--arrastando': itemSendoArrastado === rotulo
}"
draggable="true"
@dragstart="onDragStart($event, rotulo)"
@dragend="onDragEnd"
@dragover.stop.prevent="onDragOverItem(rotulo, 'visiveis')"
@drop.stop.prevent="onDragEnd"
>
<span class="eli-tabela-modal-colunas__item-handle" aria-hidden="true">⋮⋮</span>
<span class="eli-tabela-modal-colunas__item-texto">{{ rotulo }}</span>
</div>
</TransitionGroup>
</div>
<div class="eli-tabela-modal-colunas__coluna">
<div class="eli-tabela-modal-colunas__coluna-titulo">Invisíveis</div>
<TransitionGroup
name="list"
tag="div"
class="eli-tabela-modal-colunas__lista"
@dragover.prevent="onDragOverLista('invisiveis')"
@drop="onDragEnd"
>
<div
v-for="rotulo in invisiveisLocal"
:key="`inv-${rotulo}`"
class="eli-tabela-modal-colunas__item"
:class="{
'eli-tabela-modal-colunas__item--arrastando': itemSendoArrastado === rotulo
}"
draggable="true"
@dragstart="onDragStart($event, rotulo)"
@dragend="onDragEnd"
@dragover.stop.prevent="onDragOverItem(rotulo, 'invisiveis')"
@drop.stop.prevent="onDragEnd"
>
<span class="eli-tabela-modal-colunas__item-handle" aria-hidden="true">⋮⋮</span>
<span class="eli-tabela-modal-colunas__item-texto">{{ rotulo }}</span>
</div>
</TransitionGroup>
</div>
</div>
<div class="eli-tabela-modal-colunas__coluna">
<div class="eli-tabela-modal-colunas__coluna-titulo">Invisíveis</div>
<div
class="eli-tabela-modal-colunas__lista"
@dragover.prevent
@drop="(e) => onDropLista(e, 'invisiveis', null)"
>
<div
v-for="(rotulo, idx) in invisiveisLocal"
:key="`inv-${rotulo}`"
class="eli-tabela-modal-colunas__item"
draggable="true"
@dragstart="(e) => onDragStart(e, rotulo, 'invisiveis', idx)"
@dragover.prevent
@drop="(e) => onDropItem(e, 'invisiveis', idx)"
>
<span class="eli-tabela-modal-colunas__item-handle" aria-hidden="true">⋮⋮</span>
<span class="eli-tabela-modal-colunas__item-texto">{{ rotulo }}</span>
</div>
</div>
</div>
<footer class="eli-tabela-modal-colunas__footer">
<button type="button" class="eli-tabela-modal-colunas__botao eli-tabela-modal-colunas__botao--sec" @click="emitFechar">
Cancelar
</button>
<button type="button" class="eli-tabela-modal-colunas__botao eli-tabela-modal-colunas__botao--prim" @click="emitSalvar">
Salvar
</button>
</footer>
</div>
<footer class="eli-tabela-modal-colunas__footer">
<button type="button" class="eli-tabela-modal-colunas__botao eli-tabela-modal-colunas__botao--sec" @click="emitFechar">
Cancelar
</button>
<button type="button" class="eli-tabela-modal-colunas__botao eli-tabela-modal-colunas__botao--prim" @click="emitSalvar">
Salvar
</button>
</footer>
</div>
</div>
</Teleport>
</template>
<script lang="ts">
@ -78,14 +92,6 @@ import type { tipoEliColuna } from "./types-eli-tabela"
type OrigemLista = "visiveis" | "invisiveis"
type DragPayload = {
rotulo: string
origem: OrigemLista
index: number
}
const DRAG_MIME = "application/x-eli-tabela-coluna"
export default defineComponent({
name: "EliTabelaModalColunas",
props: {
@ -119,6 +125,8 @@ export default defineComponent({
const visiveisLocal = ref<string[]>([])
const invisiveisLocal = ref<string[]>([])
const itemSendoArrastado = ref<string | null>(null)
function sincronizarEstado() {
const todos = props.rotulosColunas
const configTemDados =
@ -178,118 +186,66 @@ export default defineComponent({
})
}
function writeDragData(e: DragEvent, payload: DragPayload) {
try {
e.dataTransfer?.setData(DRAG_MIME, JSON.stringify(payload))
e.dataTransfer?.setData("text/plain", payload.rotulo)
if (e.dataTransfer) {
e.dataTransfer.effectAllowed = "move"
}
} catch {
// ignore
function onDragStart(e: DragEvent, rotulo: string) {
itemSendoArrastado.value = rotulo
if (e.dataTransfer) {
e.dataTransfer.setData("text/plain", rotulo)
e.dataTransfer.effectAllowed = "move"
}
}
function readDragData(e: DragEvent): DragPayload | null {
try {
const raw = e.dataTransfer?.getData(DRAG_MIME)
if (!raw) return null
const parsed = JSON.parse(raw)
if (
!parsed ||
typeof parsed.rotulo !== "string" ||
(parsed.origem !== "visiveis" && parsed.origem !== "invisiveis")
) {
return null
}
return parsed as DragPayload
} catch {
return null
}
function onDragEnd() {
itemSendoArrastado.value = null
}
function removerDaOrigem(payload: DragPayload) {
function onDragOverItem(alvoRotulo: string, destino: OrigemLista) {
const draggedRotulo = itemSendoArrastado.value
if (!draggedRotulo || draggedRotulo === alvoRotulo) return
const origemVisIdx = visiveisLocal.value.indexOf(draggedRotulo)
const origemInvIdx = invisiveisLocal.value.indexOf(draggedRotulo)
const origem =
origemVisIdx >= 0 ? "visiveis" : origemInvIdx >= 0 ? "invisiveis" : null
if (!origem) return
const origemArr =
payload.origem === "visiveis"
? visiveisLocal.value
: invisiveisLocal.value
const idx = origemArr.indexOf(payload.rotulo)
if (idx >= 0) origemArr.splice(idx, 1)
}
function inserirNoDestino(
destino: OrigemLista,
rotulo: string,
index: number | null,
) {
const arr =
origem === "visiveis" ? visiveisLocal.value : invisiveisLocal.value
const destinoArr =
destino === "visiveis" ? visiveisLocal.value : invisiveisLocal.value
// remove duplicatas
const existing = arr.indexOf(rotulo)
if (existing >= 0) arr.splice(existing, 1)
if (index === null || index < 0 || index > arr.length) {
arr.push(rotulo)
} else {
arr.splice(index, 0, rotulo)
}
const dragIdx = origemArr.indexOf(draggedRotulo)
const alvoIdx = destinoArr.indexOf(alvoRotulo)
origemArr.splice(dragIdx, 1)
destinoArr.splice(alvoIdx, 0, draggedRotulo)
}
function onDragStart(
e: DragEvent,
rotulo: string,
origem: OrigemLista,
index: number,
) {
writeDragData(e, { rotulo, origem, index })
}
function onDragOverLista(destino: OrigemLista) {
const draggedRotulo = itemSendoArrastado.value
if (!draggedRotulo) return
function onDropItem(e: DragEvent, destino: OrigemLista, index: number) {
const payload = readDragData(e)
if (!payload) return
const destinoArr =
destino === "visiveis" ? visiveisLocal.value : invisiveisLocal.value
if (destinoArr[destinoArr.length - 1] === draggedRotulo) return
// remove da origem e insere no destino na posição do item
removerDaOrigem(payload)
inserirNoDestino(destino, payload.rotulo, index)
const origemVisIdx = visiveisLocal.value.indexOf(draggedRotulo)
const origemInvIdx = invisiveisLocal.value.indexOf(draggedRotulo)
if (origemVisIdx >= 0) visiveisLocal.value.splice(origemVisIdx, 1)
if (origemInvIdx >= 0) invisiveisLocal.value.splice(origemInvIdx, 1)
// garante que uma coluna não fique nos 2 lados
if (destino === "visiveis") {
const idxInv = invisiveisLocal.value.indexOf(payload.rotulo)
if (idxInv >= 0) invisiveisLocal.value.splice(idxInv, 1)
} else {
const idxVis = visiveisLocal.value.indexOf(payload.rotulo)
if (idxVis >= 0) visiveisLocal.value.splice(idxVis, 1)
}
}
function onDropLista(
e: DragEvent,
destino: OrigemLista,
_index: number | null,
) {
const payload = readDragData(e)
if (!payload) return
removerDaOrigem(payload)
inserirNoDestino(destino, payload.rotulo, null)
if (destino === "visiveis") {
const idxInv = invisiveisLocal.value.indexOf(payload.rotulo)
if (idxInv >= 0) invisiveisLocal.value.splice(idxInv, 1)
} else {
const idxVis = visiveisLocal.value.indexOf(payload.rotulo)
if (idxVis >= 0) visiveisLocal.value.splice(idxVis, 1)
}
destinoArr.push(draggedRotulo)
}
return {
visiveisLocal,
invisiveisLocal,
itemSendoArrastado,
emitFechar,
emitSalvar,
onDragStart,
onDropItem,
onDropLista,
onDragEnd,
onDragOverItem,
onDragOverLista,
}
},
})
@ -364,6 +320,8 @@ export default defineComponent({
border-radius: 12px;
padding: 10px;
background: rgba(15, 23, 42, 0.01);
position: relative;
overflow: hidden;
}
.eli-tabela-modal-colunas__item {
@ -376,6 +334,7 @@ export default defineComponent({
background: #fff;
cursor: grab;
user-select: none;
transition: box-shadow 0.15s, opacity 0.15s, transform 0.15s;
}
.eli-tabela-modal-colunas__item + .eli-tabela-modal-colunas__item {
@ -386,6 +345,31 @@ export default defineComponent({
cursor: grabbing;
}
.eli-tabela-modal-colunas__item--arrastando {
opacity: 0.1;
background: #f8fafc;
border-style: dashed;
}
/* Animações de reposicionamento e listas do Vue TransitionGroup */
.list-move,
.list-enter-active,
.list-leave-active {
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}
.list-enter-from,
.list-leave-to {
opacity: 0;
transform: scale(0.95);
}
.list-leave-active {
/* position absolute para suavizar a animação de saída de outras listas */
position: absolute;
width: calc(100% - 20px);
}
.eli-tabela-modal-colunas__item-handle {
color: rgba(15, 23, 42, 0.55);
font-size: 14px;

View file

@ -93,6 +93,10 @@ export type tipoEliTabelaConsulta<T> = {
nome: string
/** Indica se a caixa de busca deve ser exibida acima da tabela. */
mostrarCaixaDeBusca?: boolean
/** Habilita seleção múltipla (checkboxes) nas linhas da tabela. */
mostrar_selecao?: boolean
/** Chave (ex: "id") para identificar unicamente a linha nas seleções entre páginas. */
chave_selecao?: keyof T
/** Lista de colunas da tabela. */
colunas: tipoEliColuna<T>[]
/** Quantidade de registros solicitados por consulta (padrão `10`). */

View file

@ -3,7 +3,10 @@
<h2>EliTabela</h2>
<!-- Exemplo: botão de ação "Novo" no cabeçalho que adiciona uma linha -->
<EliTabela :tabela="tabelaOk" :key="versaoTabelaOk" />
<div style="margin-bottom: 12px">
<strong>Selecionados:</strong> {{ selecionados.map(s => s.empreendedor).join(', ') || 'Nenhum' }}
</div>
<EliTabela :tabela="tabelaOk" :key="versaoTabelaOk" v-model:selecionados="selecionados" />
<EliTabela :tabela="tabelaVazia" />
@ -97,6 +100,7 @@ export default defineComponent({
]
const versaoTabelaOk = ref(0)
const selecionados = ref<Linha[]>([])
const mostrarModalCadastro = ref(false)
const salvando = ref(false)
const novoCadastro = ref({
@ -175,6 +179,8 @@ export default defineComponent({
nome: "ExemploPlaygroundData",
registros_por_consulta: 10,
mostrarCaixaDeBusca: true,
mostrar_selecao: true,
chave_selecao: "empreendedor",
acoesTabela: [
{
posicao: "superior",
@ -300,6 +306,7 @@ export default defineComponent({
{
chave: "empreendedor",
rotulo: "Empreendedor",
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
filtro: (valor: unknown) =>
criarFiltro26({
// biome-ignore lint/suspicious/noExplicitAny: playground
@ -310,7 +317,8 @@ export default defineComponent({
{
chave: "documento",
rotulo: "Documento",
filtro: (valor: unknown) =>
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
filtro: (valor: string) =>
criarFiltro26({
// biome-ignore lint/suspicious/noExplicitAny: playground
documento: { like: valor as string } as any,
@ -323,7 +331,8 @@ export default defineComponent({
{
chave: "email",
rotulo: "E-mail",
filtro: (valor: unknown) =>
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
filtro: (valor: string) =>
criarFiltro26({
// biome-ignore lint/suspicious/noExplicitAny: playground
email: { like: valor as string } as any,
@ -407,6 +416,7 @@ export default defineComponent({
novoCadastro,
salvarNovoCadastro,
erroCadastro,
selecionados,
}
},
})