408 lines
11 KiB
Vue
408 lines
11 KiB
Vue
<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>
|