vue-componentes/src/componentes/EliTabela/EliTabelaModalColunas.vue

408 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>
<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)"
>
<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>
<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>
</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>
</template>
<script lang="ts">
import { defineComponent, PropType, ref, watch } from "vue";
import type { EliTabelaColunasConfig } from "./colunasStorage";
import type { EliColuna } 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: {
aberto: {
type: Boolean,
required: true,
},
rotulosColunas: {
type: Array as PropType<string[]>,
required: true,
},
configInicial: {
type: Object as PropType<EliTabelaColunasConfig>,
required: true,
},
colunas: {
type: Array as PropType<Array<EliColuna<any>>>,
required: true,
},
},
emits: {
fechar() {
return true;
},
salvar(_config: EliTabelaColunasConfig) {
return true;
},
},
setup(props, { emit }) {
const visiveisLocal = ref<string[]>([]);
const invisiveisLocal = ref<string[]>([]);
function sincronizarEstado() {
const todos = props.rotulosColunas;
const configTemDados =
(props.configInicial.visiveis?.length ?? 0) > 0 ||
(props.configInicial.invisiveis?.length ?? 0) > 0;
const invisiveisPadraoSet = new Set(
props.colunas.filter((c) => c.visivel === false).map((c) => c.rotulo)
);
const invisiveisSet = configTemDados
? new Set(props.configInicial.invisiveis ?? [])
: invisiveisPadraoSet;
const baseVisiveis = todos.filter((r) => !invisiveisSet.has(r));
// ordenação: aplica ordem salva (visiveis) e adiciona novas ao final
const ordemSalva = props.configInicial.visiveis ?? [];
const setVis = new Set(baseVisiveis);
const ordenadas: string[] = [];
for (const r of ordemSalva) {
if (setVis.has(r)) ordenadas.push(r);
}
for (const r of baseVisiveis) {
if (!ordenadas.includes(r)) ordenadas.push(r);
}
visiveisLocal.value = ordenadas;
// invisíveis: somente as que existem na tabela
invisiveisLocal.value = todos.filter((r) => invisiveisSet.has(r));
}
watch(
() => [props.aberto, props.rotulosColunas, props.configInicial, props.colunas] as const,
() => {
if (props.aberto) sincronizarEstado();
},
{ deep: true, immediate: true }
);
function emitFechar() {
emit("fechar");
}
function emitSalvar() {
emit("salvar", {
visiveis: [...visiveisLocal.value],
invisiveis: [...invisiveisLocal.value],
});
}
function writeDragData(e: DragEvent, payload: DragPayload) {
try {
e.dataTransfer?.setData(DRAG_MIME, JSON.stringify(payload));
e.dataTransfer?.setData("text/plain", payload.rotulo);
e.dataTransfer!.effectAllowed = "move";
} catch {
// ignore
}
}
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 removerDaOrigem(payload: DragPayload) {
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 = 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);
}
}
function onDragStart(e: DragEvent, rotulo: string, origem: OrigemLista, index: number) {
writeDragData(e, { rotulo, origem, index });
}
function onDropItem(e: DragEvent, destino: OrigemLista, index: number) {
const payload = readDragData(e);
if (!payload) return;
// remove da origem e insere no destino na posição do item
removerDaOrigem(payload);
inserirNoDestino(destino, payload.rotulo, index);
// 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);
}
}
return {
visiveisLocal,
invisiveisLocal,
emitFechar,
emitSalvar,
onDragStart,
onDropItem,
onDropLista,
};
},
});
</script>
<style scoped>
.eli-tabela-modal-colunas__overlay {
position: fixed;
inset: 0;
background: rgba(15, 23, 42, 0.35);
z-index: 4000;
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
}
.eli-tabela-modal-colunas__modal {
width: min(860px, 100%);
background: #fff;
border-radius: 14px;
border: 1px solid rgba(15, 23, 42, 0.1);
box-shadow: 0 18px 60px rgba(15, 23, 42, 0.25);
overflow: hidden;
}
.eli-tabela-modal-colunas__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 16px;
border-bottom: 1px solid rgba(15, 23, 42, 0.08);
}
.eli-tabela-modal-colunas__titulo {
font-size: 1rem;
margin: 0;
}
.eli-tabela-modal-colunas__fechar {
width: 34px;
height: 34px;
border-radius: 10px;
border: none;
background: transparent;
cursor: pointer;
font-size: 22px;
line-height: 1;
color: rgba(15, 23, 42, 0.8);
}
.eli-tabela-modal-colunas__fechar:hover,
.eli-tabela-modal-colunas__fechar:focus-visible {
background: rgba(15, 23, 42, 0.06);
}
.eli-tabela-modal-colunas__conteudo {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
padding: 16px;
}
.eli-tabela-modal-colunas__coluna-titulo {
font-weight: 600;
margin-bottom: 8px;
}
.eli-tabela-modal-colunas__lista {
min-height: 260px;
border: 1px solid rgba(15, 23, 42, 0.12);
border-radius: 12px;
padding: 10px;
background: rgba(15, 23, 42, 0.01);
}
.eli-tabela-modal-colunas__item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 10px;
border-radius: 10px;
border: 1px solid rgba(15, 23, 42, 0.08);
background: #fff;
cursor: grab;
user-select: none;
}
.eli-tabela-modal-colunas__item + .eli-tabela-modal-colunas__item {
margin-top: 8px;
}
.eli-tabela-modal-colunas__item:active {
cursor: grabbing;
}
.eli-tabela-modal-colunas__item-handle {
color: rgba(15, 23, 42, 0.55);
font-size: 14px;
}
.eli-tabela-modal-colunas__item-texto {
flex: 1;
min-width: 0;
}
.eli-tabela-modal-colunas__footer {
display: flex;
justify-content: flex-end;
gap: 8px;
padding: 14px 16px;
border-top: 1px solid rgba(15, 23, 42, 0.08);
}
.eli-tabela-modal-colunas__botao {
height: 34px;
padding: 0 14px;
border-radius: 10px;
border: 1px solid rgba(15, 23, 42, 0.12);
background: #fff;
cursor: pointer;
font-size: 0.9rem;
}
.eli-tabela-modal-colunas__botao--sec:hover,
.eli-tabela-modal-colunas__botao--sec:focus-visible {
background: rgba(15, 23, 42, 0.06);
}
.eli-tabela-modal-colunas__botao--prim {
border: none;
background: rgba(37, 99, 235, 0.95);
color: #fff;
}
.eli-tabela-modal-colunas__botao--prim:hover,
.eli-tabela-modal-colunas__botao--prim:focus-visible {
background: rgba(37, 99, 235, 1);
}
@media (max-width: 720px) {
.eli-tabela-modal-colunas__conteudo {
grid-template-columns: 1fr;
}
}
</style>