docs.sheetjs.com/docz/static/vtl/App.vue

224 lines
5.3 KiB
Vue

<script setup lang="ts">
/*! sheetjs (C) SheetJS -- https://sheetjs.com */
import { ref, onMounted } from "vue";
import VueTableLite from "vue3-table-lite/ts";
import { read, utils, WorkSheet, writeFile } from "xlsx";
type DataSet = { [index: string]: WorkSheet; };
type Row = any[];
type RowCB = (row: Row) => string;
type Column = { field: string; label: string; display: RowCB; };
type RowCol = { rows: Row[]; cols: Column[]; };
const currFileName = ref<string>("");
const currSheet = ref<string>("");
const sheets = ref<string[]>([]);
const workBook = ref<DataSet>({} as DataSet);
const rows = ref<Row[]>([]);
const columns = ref<Column[]>([]);
const loading = ref<boolean>(true);
const paging = ref<boolean>(true);
const exportTypes: string[] = ["xlsx", "xlsb", "csv", "html"];
let cell = 0;
function resetCell() {
cell = 0;
}
const getRowsCols = ( data: DataSet, sheetName: string ): RowCol => ({
rows: utils.sheet_to_json<Row>(data[sheetName], {header:1}),
cols: Array.from({
length: utils.decode_range(data[sheetName]["!ref"]||"A1").e.c + 1
}, (_, i) => (<Column>{ field: String(i), label: utils.encode_col(i), display: makeDisplay(i) }))
});
const makeDisplay = (col: number): RowCB => (row: Row) => `<span
style="user-select: none; display: block"
onblur="endEdit(event)" ondblclick="startEdit(event)"
position="${Math.floor(cell++ / columns.value.length)}.${col}"
onkeydown="endEdit(event)">${row?.[col] ?? "&nbsp;"}</span>`;
(window as any).startEdit = function (ev: MouseEvent) {
(ev?.target as HTMLSpanElement).contentEditable = "true";
(ev?.target as HTMLSpanElement).focus();
};
(window as any).endEdit = function (ev: FocusEvent | KeyboardEvent) {
if (typeof (ev as KeyboardEvent).key == "undefined" || (ev as KeyboardEvent).key === "Enter") {
const pos = (ev.target as HTMLSpanElement)?.getAttribute("position")?.split(".");
if(!pos) return;
(ev?.target as HTMLSpanElement).contentEditable = "true";
rows.value[+pos[0]][+pos[1]] = (ev.target as HTMLSpanElement).innerText;
workBook.value[currSheet.value] = utils.json_to_sheet(rows.value, {
header: columns.value.map((col: Column) => col.field),
skipHeader: true,
});
}
};
async function importAB(ab: ArrayBuffer, name: string): Promise<void> {
loading.value = true;
const data = read(ab);
currFileName.value = name;
currSheet.value = data.SheetNames?.[0];
sheets.value = data.SheetNames;
workBook.value = data.Sheets;
loading.value = false;
selectSheet(currSheet.value);
}
async function importFile(ev: Event): Promise<void> {
const file = (ev.target as HTMLInputElement)?.files?.[0];
if(!file) return;
await importAB(await file.arrayBuffer(), file.name);
}
function exportFile(type: string): void {
const wb = utils.book_new();
sheets.value.forEach((sheet) => {
utils.book_append_sheet(wb, workBook.value[sheet], sheet);
});
writeFile(wb, `sheet.${type}`);
}
function selectSheet(sheet: string): void {
const { rows: newRows, cols: newCols } = getRowsCols(workBook.value, sheet);
resetCell();
currSheet.value = sheet;
columns.value = newCols;
rows.value = newRows;
paging.value = newRows.length > 50
}
/* Download from https://docs.sheetjs.com/pres.numbers */
onMounted(async() => {
const response = await fetch("https://docs.sheetjs.com/pres.numbers");
await importAB(await response.arrayBuffer(), "pres.numbers");
});
</script>
<template>
<header class="imp-exp">
<div class="import">
<input type="file" id="import" @change="importFile" />
<label for="import">import</label>
</div>
<span v-if="currFileName">{{ currFileName }}</span>
<div class="export" v-if="currFileName">
<span>export</span>
<ul>
<li v-for="(type, idx) in exportTypes" :key="idx" @click="exportFile(type)">
{{ `.${type}` }}
</li>
</ul>
</div>
</header>
<div class="sheets">
<span
v-for="(sheet, idx) in sheets"
:key="idx"
@click="selectSheet(sheet)"
:class="[currSheet === sheet ? 'selected' : '']"
>
{{ sheet }}
</span>
</div>
<vue-table-lite :is-loading="loading" :page-size="50" :columns="columns" :is-hide-paging="paging" :rows="rows"></vue-table-lite>
</template>
<style>
.imp-exp {
display: flex;
justify-content: space-between;
padding: 0.5rem;
font-family: mono;
color: #212529;
}
.import {
font-size: medium;
}
.import input {
position: absolute;
opacity: 0;
cursor: pointer;
}
.import label {
background-color: white;
border: 1px solid;
padding: 0.3rem;
}
.export:hover {
border-bottom: none;
}
.export:hover ul {
display: block;
}
.export span {
padding: 0.3rem;
border: 1px solid;
cursor: pointer;
}
.export ul {
display: none;
position: absolute;
z-index: 5;
background-color: white;
list-style: none;
padding: 0.3rem;
border: 1px solid;
margin-top: 0.3rem;
border-top: none;
}
.export ul li {
padding: 0.3rem;
text-align: center;
}
.export ul li:hover {
background-color: lightgray;
cursor: pointer;
}
.sheets {
display: flex;
justify-content: center;
margin: 0.3rem;
color: #212529;
}
.sheets span {
border: 1px solid;
padding: 0.5rem;
margin: 0.3rem;
}
.sheets span:hover:not(.selected) {
background-color: lightgray;
cursor: pointer;
}
.selected {
background-color: #343a40;
color: white;
}
</style>