|
|
<!DOCTYPE html> |
|
|
<html lang="pt-br"> |
|
|
|
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>XLSX File Reader & Editor</title> |
|
|
|
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5/dist/css/bootstrap.min.css" rel="stylesheet"> |
|
|
|
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1/font/bootstrap-icons.css"> |
|
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js"></script> |
|
|
|
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js"></script> |
|
|
<style> |
|
|
.dropzone { |
|
|
border: 2px dashed #0d6efd; |
|
|
transition: all 0.3s ease; |
|
|
cursor: pointer; |
|
|
} |
|
|
|
|
|
.dropzone.active { |
|
|
border-color: #198754; |
|
|
background-color: rgba(25, 135, 84, 0.05); |
|
|
} |
|
|
|
|
|
.table-container { |
|
|
|
|
|
overflow-y: auto; |
|
|
} |
|
|
|
|
|
.editable-cell { |
|
|
min-width: 120px; |
|
|
border: 1px solid #dee2e6; |
|
|
padding: 0.5rem; |
|
|
} |
|
|
|
|
|
.editable-cell:focus { |
|
|
outline: 2px solid #0d6efd; |
|
|
background-color: rgba(13, 110, 253, 0.1); |
|
|
} |
|
|
|
|
|
.sheet-tab { |
|
|
cursor: pointer; |
|
|
padding: 0.5rem 1rem; |
|
|
border-radius: 0.375rem; |
|
|
margin-right: 0.5rem; |
|
|
background-color: #f8f9fa; |
|
|
transition: all 0.2s ease; |
|
|
} |
|
|
|
|
|
.sheet-tab:hover { |
|
|
background-color: #e9ecef; |
|
|
} |
|
|
|
|
|
.sheet-tab.active { |
|
|
background-color: #0d6efd; |
|
|
color: white; |
|
|
} |
|
|
|
|
|
.file-info { |
|
|
background-color: #f8f9fa; |
|
|
border-radius: 0.375rem; |
|
|
padding: 1rem; |
|
|
} |
|
|
|
|
|
.how-to-use-card { |
|
|
height: 100%; |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
|
|
|
<body class="bg-light"> |
|
|
<div class="container py-5"> |
|
|
<div class="text-center mb-5"> |
|
|
<h1 class="display-5 fw-bold text-dark mb-3">Excel File Reader & Editor</h1> |
|
|
<p class="lead text-muted">Upload, view, edit and export Excel files directly in your browser</p> |
|
|
</div> |
|
|
|
|
|
<div class="shadow-sm mb-5"> |
|
|
<div class=""> |
|
|
<div class="row"> |
|
|
|
|
|
|
|
|
<div class="col-md"> |
|
|
<div id="dropzone" class="dropzone rounded-0 p-3 text-center mb-4"> |
|
|
<div class="d-flex flex-column align-items-center justify-content-center"> |
|
|
<i class="bi bi-file-earmark-excel text-primary fs-1 mb-3"></i> |
|
|
<p class="text-muted mb-2">Drag & drop your Excel file here</p> |
|
|
<p class="text-muted small mb-3">or</p> |
|
|
|
|
|
|
|
|
|
|
|
<input type="file" id="fileInput" class="d-none" accept=".xlsx, .xls, .csv" /> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
</div> |
|
|
<div class="col-md-3"> |
|
|
<h5 class="fw-bold mb-3">File Information</h5> |
|
|
<div class="file-info"> |
|
|
<div class="d-flex align-items-center mb-2"> |
|
|
<i class="bi bi-file-earmark text-muted me-2"></i> |
|
|
<span id="fileName" class="text-dark">No file selected</span> |
|
|
</div> |
|
|
<div class="d-flex align-items-center mb-2"> |
|
|
<i class="bi bi-collection text-muted me-2"></i> |
|
|
<span id="sheetCount" class="text-dark">0 sheets</span> |
|
|
</div> |
|
|
<div class="d-flex align-items-center"> |
|
|
<i class="bi bi-table text-muted me-2"></i> |
|
|
<span id="rowCount" class="text-dark">0 rows</span> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
|
|
|
<div class="col-ms"> |
|
|
<div id="sheetTabs" class="d-flex overflow-auto py-2 mb-3"> |
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
<div id="tableContainer" class="table-container border rounded-0 overflow-hidden"> |
|
|
<div class="text-center py-5 text-muted" id="emptyState"> |
|
|
<i class="bi bi-file-earmark-excel display-4 opacity-25 mb-3"></i> |
|
|
<p class="h5">Upload an Excel file to get started</p> |
|
|
</div> |
|
|
<table id="dataTable" class="table table-bordered m-0 d-none overflow-auto"> |
|
|
|
|
|
</table> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="d-flex flex-wrap gap-2 mt-4"> |
|
|
<button id="exportBtn" class="btn btn-success" disabled> |
|
|
<i class="bi bi-download me-2"></i> Export Excel |
|
|
</button> |
|
|
<button id="addRowBtn" class="btn btn-primary" disabled> |
|
|
<i class="bi bi-plus me-2"></i> Add Row |
|
|
</button> |
|
|
<button id="addColBtn" class="btn btn-primary" disabled> |
|
|
<i class="bi bi-plus me-2"></i> Add Column |
|
|
</button> |
|
|
<button id="resetBtn" class="btn btn-secondary"> |
|
|
<i class="bi bi-arrow-counterclockwise me-2"></i> Reset |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="card shadow-sm"> |
|
|
<div class="card-body p-4"> |
|
|
<h2 class="h4 fw-bold mb-4">How to use</h2> |
|
|
<div class="row g-4"> |
|
|
<div class="col-md-4"> |
|
|
<div class="card bg-primary bg-opacity-10 how-to-use-card"> |
|
|
<div class="card-body"> |
|
|
<div class="d-flex align-items-center mb-3"> |
|
|
<div class="bg-primary bg-opacity-25 p-2 rounded-circle me-3"> |
|
|
<i class="bi bi-upload text-primary"></i> |
|
|
</div> |
|
|
<h3 class="h6 fw-bold mb-0 text-primary">Upload</h3> |
|
|
</div> |
|
|
<p class="card-text text-muted">Drag & drop or click to upload your Excel file (.xlsx, .xls, .csv)</p> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
<div class="col-md-4"> |
|
|
<div class="card bg-success bg-opacity-10 how-to-use-card"> |
|
|
<div class="card-body"> |
|
|
<div class="d-flex align-items-center mb-3"> |
|
|
<div class="bg-success bg-opacity-25 p-2 rounded-circle me-3"> |
|
|
<i class="bi bi-pencil text-success"></i> |
|
|
</div> |
|
|
<h3 class="h6 fw-bold mb-0 text-success">Edit</h3> |
|
|
</div> |
|
|
<p class="card-text text-muted">Click on any cell to edit its content. Add/remove rows and columns as needed.</p> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
<div class="col-md-4"> |
|
|
<div class="card bg-info bg-opacity-10 how-to-use-card"> |
|
|
<div class="card-body"> |
|
|
<div class="d-flex align-items-center mb-3"> |
|
|
<div class="bg-info bg-opacity-25 p-2 rounded-circle me-3"> |
|
|
<i class="bi bi-download text-info"></i> |
|
|
</div> |
|
|
<h3 class="h6 fw-bold mb-0 text-info">Export</h3> |
|
|
</div> |
|
|
<p class="card-text text-muted">Download your edited file back to Excel format when you're done.</p> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5/dist/js/bootstrap.bundle.min.js"></script> |
|
|
|
|
|
<script> |
|
|
document.addEventListener("DOMContentLoaded", function () { |
|
|
|
|
|
const fileInput = document.getElementById("fileInput"); |
|
|
const dropzone = document.getElementById("dropzone"); |
|
|
const fileName = document.getElementById("fileName"); |
|
|
const sheetCount = document.getElementById("sheetCount"); |
|
|
const rowCount = document.getElementById("rowCount"); |
|
|
const tableContainer = document.getElementById("tableContainer"); |
|
|
const dataTable = document.getElementById("dataTable"); |
|
|
const emptyState = document.getElementById("emptyState"); |
|
|
const sheetTabs = document.getElementById("sheetTabs"); |
|
|
const exportBtn = document.getElementById("exportBtn"); |
|
|
const addRowBtn = document.getElementById("addRowBtn"); |
|
|
const addColBtn = document.getElementById("addColBtn"); |
|
|
const resetBtn = document.getElementById("resetBtn"); |
|
|
|
|
|
|
|
|
let workbook = null; |
|
|
let currentSheetName = ""; |
|
|
let sheetData = []; |
|
|
let headers = []; |
|
|
|
|
|
|
|
|
fileInput.addEventListener("change", handleFileSelect); |
|
|
dropzone.addEventListener("click", () => fileInput.click()); |
|
|
dropzone.addEventListener("dragover", handleDragOver); |
|
|
dropzone.addEventListener("dragleave", handleDragLeave); |
|
|
dropzone.addEventListener("drop", handleDrop); |
|
|
exportBtn.addEventListener("click", exportToExcel); |
|
|
addRowBtn.addEventListener("click", addNewRow); |
|
|
addColBtn.addEventListener("click", addNewColumn); |
|
|
resetBtn.addEventListener("click", resetApp); |
|
|
|
|
|
|
|
|
function handleFileSelect(e) { |
|
|
const file = e.target.files[0]; |
|
|
if (!file) return; |
|
|
|
|
|
processExcelFile(file); |
|
|
} |
|
|
|
|
|
function handleDragOver(e) { |
|
|
e.preventDefault(); |
|
|
e.stopPropagation(); |
|
|
dropzone.classList.add("active"); |
|
|
} |
|
|
|
|
|
function handleDragLeave(e) { |
|
|
e.preventDefault(); |
|
|
e.stopPropagation(); |
|
|
dropzone.classList.remove("active"); |
|
|
} |
|
|
|
|
|
function handleDrop(e) { |
|
|
e.preventDefault(); |
|
|
e.stopPropagation(); |
|
|
dropzone.classList.remove("active"); |
|
|
|
|
|
const file = e.dataTransfer.files[0]; |
|
|
if (!file) return; |
|
|
|
|
|
processExcelFile(file); |
|
|
} |
|
|
|
|
|
function processExcelFile(file) { |
|
|
fileName.textContent = file.name; |
|
|
|
|
|
const reader = new FileReader(); |
|
|
reader.onload = function (e) { |
|
|
const data = new Uint8Array(e.target.result); |
|
|
workbook = XLSX.read(data, { type: "array" }); |
|
|
|
|
|
updateSheetInfo(); |
|
|
renderSheetTabs(); |
|
|
loadFirstSheet(); |
|
|
enableButtons(); |
|
|
}; |
|
|
reader.readAsArrayBuffer(file); |
|
|
} |
|
|
|
|
|
function updateSheetInfo() { |
|
|
if (!workbook) return; |
|
|
|
|
|
sheetCount.textContent = `${workbook.SheetNames.length} sheet${ |
|
|
workbook.SheetNames.length !== 1 ? "s" : "" |
|
|
}`; |
|
|
} |
|
|
|
|
|
function renderSheetTabs() { |
|
|
if (!workbook) return; |
|
|
|
|
|
sheetTabs.innerHTML = ""; |
|
|
workbook.SheetNames.forEach((sheetName) => { |
|
|
const tab = document.createElement("div"); |
|
|
tab.className = "sheet-tab"; |
|
|
tab.textContent = sheetName; |
|
|
tab.addEventListener("click", () => switchSheet(sheetName)); |
|
|
|
|
|
if (sheetName === workbook.SheetNames[0]) { |
|
|
tab.classList.add("active"); |
|
|
} |
|
|
|
|
|
sheetTabs.appendChild(tab); |
|
|
}); |
|
|
} |
|
|
|
|
|
function switchSheet(sheetName) { |
|
|
if (!workbook || currentSheetName === sheetName) return; |
|
|
|
|
|
|
|
|
document.querySelectorAll(".sheet-tab").forEach((tab) => { |
|
|
tab.classList.remove("active"); |
|
|
if (tab.textContent === sheetName) { |
|
|
tab.classList.add("active"); |
|
|
} |
|
|
}); |
|
|
|
|
|
currentSheetName = sheetName; |
|
|
loadSheetData(); |
|
|
renderTable(); |
|
|
} |
|
|
|
|
|
function loadFirstSheet() { |
|
|
if (!workbook || workbook.SheetNames.length === 0) return; |
|
|
|
|
|
currentSheetName = workbook.SheetNames[0]; |
|
|
loadSheetData(); |
|
|
renderTable(); |
|
|
} |
|
|
|
|
|
function loadSheetData() { |
|
|
if (!workbook || !currentSheetName) return; |
|
|
|
|
|
const worksheet = workbook.Sheets[currentSheetName]; |
|
|
sheetData = XLSX.utils.sheet_to_json(worksheet, { header: 1, defval: "" }); |
|
|
|
|
|
if (sheetData.length > 0) { |
|
|
headers = sheetData[0]; |
|
|
rowCount.textContent = `${sheetData.length - 1} row${ |
|
|
sheetData.length - 1 !== 1 ? "s" : "" |
|
|
}`; |
|
|
} else { |
|
|
headers = []; |
|
|
rowCount.textContent = "0 rows"; |
|
|
} |
|
|
} |
|
|
|
|
|
function renderTable() { |
|
|
if (!sheetData || sheetData.length === 0) { |
|
|
emptyState.classList.remove("d-none"); |
|
|
dataTable.classList.add("d-none"); |
|
|
return; |
|
|
} |
|
|
|
|
|
emptyState.classList.add("d-none"); |
|
|
dataTable.classList.remove("d-none"); |
|
|
|
|
|
|
|
|
dataTable.innerHTML = ""; |
|
|
|
|
|
|
|
|
const thead = document.createElement("thead"); |
|
|
const headerRow = document.createElement("tr"); |
|
|
headerRow.className = "table-light"; |
|
|
|
|
|
|
|
|
const cornerCell = document.createElement("th"); |
|
|
cornerCell.className = "text-center"; |
|
|
cornerCell.style.padding = "0px 45px"; |
|
|
headerRow.appendChild(cornerCell); |
|
|
|
|
|
|
|
|
headers.forEach((header, colIndex) => { |
|
|
const th = document.createElement("th"); |
|
|
th.className = "text-nowrap"; |
|
|
th.textContent = header || `Column ${colIndex + 1}`; |
|
|
|
|
|
|
|
|
const deleteBtn = document.createElement("button"); |
|
|
deleteBtn.className = "btn btn-sm btn-link text-danger ms-2 p-0"; |
|
|
deleteBtn.innerHTML = '<i class="bi bi-x"></i>'; |
|
|
deleteBtn.addEventListener("click", () => deleteColumn(colIndex)); |
|
|
|
|
|
th.appendChild(deleteBtn); |
|
|
headerRow.appendChild(th); |
|
|
}); |
|
|
|
|
|
|
|
|
const newColHeader = document.createElement("th"); |
|
|
newColHeader.className = "text-center"; |
|
|
newColHeader.style.width = "50px"; |
|
|
|
|
|
const addColBtn = document.createElement("button"); |
|
|
addColBtn.className = "btn btn-sm btn-link text-primary p-0"; |
|
|
addColBtn.innerHTML = '<i class="bi bi-plus"></i>'; |
|
|
addColBtn.addEventListener("click", addNewColumn); |
|
|
|
|
|
newColHeader.appendChild(addColBtn); |
|
|
headerRow.appendChild(newColHeader); |
|
|
|
|
|
thead.appendChild(headerRow); |
|
|
dataTable.appendChild(thead); |
|
|
|
|
|
|
|
|
const tbody = document.createElement("tbody"); |
|
|
|
|
|
|
|
|
for (let rowIndex = 1; rowIndex < sheetData.length; rowIndex++) { |
|
|
const row = document.createElement("tr"); |
|
|
|
|
|
|
|
|
const rowNumCell = document.createElement("td"); |
|
|
rowNumCell.className = "text-center text-muted"; |
|
|
rowNumCell.textContent = rowIndex; |
|
|
|
|
|
|
|
|
const deleteRowBtn = document.createElement("button"); |
|
|
deleteRowBtn.className = "btn btn-sm btn-link text-danger ms-2 p-0"; |
|
|
deleteRowBtn.innerHTML = '<i class="bi bi-x"></i>'; |
|
|
deleteRowBtn.addEventListener("click", () => deleteRow(rowIndex)); |
|
|
|
|
|
rowNumCell.appendChild(deleteRowBtn); |
|
|
row.appendChild(rowNumCell); |
|
|
|
|
|
|
|
|
for (let colIndex = 0; colIndex < headers.length; colIndex++) { |
|
|
const cell = document.createElement("td"); |
|
|
cell.className = "editable-cell"; |
|
|
cell.contentEditable = true; |
|
|
|
|
|
|
|
|
const cellValue = |
|
|
sheetData[rowIndex][colIndex] !== undefined |
|
|
? sheetData[rowIndex][colIndex] |
|
|
: ""; |
|
|
cell.textContent = cellValue; |
|
|
|
|
|
|
|
|
cell.addEventListener("blur", () => { |
|
|
if (!sheetData[rowIndex]) { |
|
|
sheetData[rowIndex] = new Array(headers.length).fill(""); |
|
|
} |
|
|
sheetData[rowIndex][colIndex] = cell.textContent; |
|
|
}); |
|
|
|
|
|
row.appendChild(cell); |
|
|
} |
|
|
|
|
|
|
|
|
const newColCell = document.createElement("td"); |
|
|
newColCell.className = "text-center"; |
|
|
row.appendChild(newColCell); |
|
|
|
|
|
tbody.appendChild(row); |
|
|
} |
|
|
|
|
|
|
|
|
const newRow = document.createElement("tr"); |
|
|
const newRowCell = document.createElement("td"); |
|
|
newRowCell.colSpan = headers.length + 2; |
|
|
newRowCell.className = "text-center py-3"; |
|
|
|
|
|
const addRowBtn = document.createElement("button"); |
|
|
addRowBtn.className = "btn btn-sm btn-primary"; |
|
|
addRowBtn.innerHTML = '<i class="bi bi-plus me-2"></i>Add Row'; |
|
|
addRowBtn.addEventListener("click", addNewRow); |
|
|
|
|
|
newRowCell.appendChild(addRowBtn); |
|
|
newRow.appendChild(newRowCell); |
|
|
tbody.appendChild(newRow); |
|
|
|
|
|
dataTable.appendChild(tbody); |
|
|
} |
|
|
|
|
|
function addNewRow() { |
|
|
if (!sheetData) return; |
|
|
|
|
|
const newRow = new Array(headers.length).fill(""); |
|
|
sheetData.push(newRow); |
|
|
rowCount.textContent = `${sheetData.length - 1} row${ |
|
|
sheetData.length - 1 !== 1 ? "s" : "" |
|
|
}`; |
|
|
renderTable(); |
|
|
} |
|
|
|
|
|
function addNewColumn() { |
|
|
if (!headers) return; |
|
|
|
|
|
const newHeader = `Column ${headers.length + 1}`; |
|
|
headers.push(newHeader); |
|
|
|
|
|
|
|
|
for (let i = 0; i < sheetData.length; i++) { |
|
|
sheetData[i].push(""); |
|
|
} |
|
|
|
|
|
renderTable(); |
|
|
} |
|
|
|
|
|
function deleteRow(rowIndex) { |
|
|
if (!sheetData || rowIndex < 1 || rowIndex >= sheetData.length) return; |
|
|
|
|
|
sheetData.splice(rowIndex, 1); |
|
|
rowCount.textContent = `${sheetData.length - 1} row${ |
|
|
sheetData.length - 1 !== 1 ? "s" : "" |
|
|
}`; |
|
|
renderTable(); |
|
|
} |
|
|
|
|
|
function deleteColumn(colIndex) { |
|
|
if (!headers || colIndex < 0 || colIndex >= headers.length) return; |
|
|
|
|
|
headers.splice(colIndex, 1); |
|
|
|
|
|
|
|
|
for (let i = 0; i < sheetData.length; i++) { |
|
|
if (sheetData[i].length > colIndex) { |
|
|
sheetData[i].splice(colIndex, 1); |
|
|
} |
|
|
} |
|
|
|
|
|
renderTable(); |
|
|
} |
|
|
|
|
|
function exportToExcel() { |
|
|
if (!workbook || !currentSheetName) return; |
|
|
|
|
|
|
|
|
const worksheet = XLSX.utils.aoa_to_sheet(sheetData); |
|
|
workbook.Sheets[currentSheetName] = worksheet; |
|
|
|
|
|
|
|
|
const excelBuffer = XLSX.write(workbook, { |
|
|
bookType: "xlsx", |
|
|
type: "array" |
|
|
}); |
|
|
const blob = new Blob([excelBuffer], { |
|
|
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" |
|
|
}); |
|
|
|
|
|
|
|
|
let exportName = "edited_" + (fileName.textContent || "spreadsheet"); |
|
|
if (!exportName.endsWith(".xlsx")) { |
|
|
exportName += ".xlsx"; |
|
|
} |
|
|
|
|
|
|
|
|
saveAs(blob, exportName); |
|
|
} |
|
|
|
|
|
function resetApp() { |
|
|
|
|
|
fileName.textContent = "No file selected"; |
|
|
sheetCount.textContent = "0 sheets"; |
|
|
rowCount.textContent = "0 rows"; |
|
|
|
|
|
emptyState.classList.remove("d-none"); |
|
|
dataTable.classList.add("d-none"); |
|
|
sheetTabs.innerHTML = ""; |
|
|
|
|
|
|
|
|
workbook = null; |
|
|
currentSheetName = ""; |
|
|
sheetData = []; |
|
|
headers = []; |
|
|
|
|
|
|
|
|
exportBtn.disabled = true; |
|
|
addRowBtn.disabled = true; |
|
|
addColBtn.disabled = true; |
|
|
|
|
|
|
|
|
fileInput.value = ""; |
|
|
} |
|
|
|
|
|
function enableButtons() { |
|
|
exportBtn.disabled = false; |
|
|
addRowBtn.disabled = false; |
|
|
addColBtn.disabled = false; |
|
|
} |
|
|
}); |
|
|
|
|
|
</script> |
|
|
</body> |
|
|
|
|
|
</html> |