220 lines
5.1 KiB
Vue
220 lines
5.1 KiB
Vue
<script setup lang="ts">
|
|
/*! sheetjs (C) SheetJS -- http://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 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] ?? " "}</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> {
|
|
const data = read(ab);
|
|
|
|
currFileName.value = name;
|
|
currSheet.value = data.SheetNames?.[0];
|
|
sheets.value = data.SheetNames;
|
|
workBook.value = data.Sheets;
|
|
|
|
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();
|
|
|
|
rows.value = newRows;
|
|
columns.value = newCols;
|
|
currSheet.value = sheet;
|
|
}
|
|
|
|
/* Download from https://sheetjs.com/pres.numbers */
|
|
onMounted(async() => {
|
|
const response = await fetch("https://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-static-mode="true" :page-size="50" :columns="columns" :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>
|
|
|