- Added HTTP server with CORS support and various endpoints - Introduced http_tx/http_rx channels for HTTP server control - Cleaned up build.rs by removing commented code - Updated .gitignore to use *.rdb pattern instead of .rdb - Simplified capabilities.json to empty object - Improved UI initialization with better error handling - Reorganized module imports in main.rs - Added worker count configuration for HTTP server The changes introduce a new HTTP server capability while cleaning up and improving existing code structure. The HTTP server includes authentication, session management, and websocket support.
427 lines
13 KiB
HTML
427 lines
13 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Tables - Excel Clone</title>
|
|
<link rel="stylesheet" href="./styles/theme.css">
|
|
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
|
<script src="./store.js"></script>
|
|
<style>
|
|
.app-container {
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100vh;
|
|
background: var(--background);
|
|
}
|
|
|
|
.content-container {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.header {
|
|
background: linear-gradient(135deg, var(--primary) 0%, var(--accent) 100%);
|
|
color: white;
|
|
padding: 15px 20px;
|
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.header h1 {
|
|
font-size: 24px;
|
|
margin-bottom: 5px;
|
|
}
|
|
|
|
.header .subtitle {
|
|
font-size: 12px;
|
|
opacity: 0.9;
|
|
}
|
|
|
|
.spreadsheet-content {
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100%;
|
|
}
|
|
|
|
/* Existing spreadsheet styles remain unchanged */
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="app-container" x-data>
|
|
<div class="navbar-container">
|
|
<div x-html="(await fetch('./components/navbar.html')).text()"></div>
|
|
</div>
|
|
|
|
<div class="content-container">
|
|
<div class="header">
|
|
<h1>📊 Tables</h1>
|
|
<div class="subtitle">Excel Clone - Celebrating Lotus 1-2-3 Legacy 🎉</div>
|
|
</div>
|
|
|
|
<div class="resizable-container">
|
|
<div class="resizable-panel left" style="width: 30%">
|
|
<!-- Left panel content -->
|
|
</div>
|
|
<div class="resizable-handle"></div>
|
|
<div class="resizable-panel right" style="width: 70%">
|
|
<div class="spreadsheet-content">
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
class TablesApp {
|
|
constructor() {
|
|
this.data = this.generateMockData(100, 26);
|
|
this.selectedCell = null;
|
|
this.cols = 26;
|
|
this.rows = 100;
|
|
this.visibleRows = 30;
|
|
this.rowOffset = 0;
|
|
|
|
this.init();
|
|
}
|
|
|
|
generateMockData(rows, cols) {
|
|
const data = [];
|
|
const products = ['Laptop', 'Mouse', 'Keyboard', 'Monitor', 'Headphones', 'Webcam', 'Desk', 'Chair'];
|
|
const regions = ['North', 'South', 'East', 'West'];
|
|
|
|
for (let i = 0; i < rows; i++) {
|
|
const row = {};
|
|
for (let j = 0; j < cols; j++) {
|
|
const col = this.getColumnName(j);
|
|
if (i === 0) {
|
|
if (j === 0) row[col] = 'Product';
|
|
else if (j === 1) row[col] = 'Region';
|
|
else if (j === 2) row[col] = 'Q1';
|
|
else if (j === 3) row[col] = 'Q2';
|
|
else if (j === 4) row[col] = 'Q3';
|
|
else if (j === 5) row[col] = 'Q4';
|
|
else if (j === 6) row[col] = 'Total';
|
|
else row[col] = `Col ${col}`;
|
|
} else {
|
|
if (j === 0) row[col] = products[i % products.length];
|
|
else if (j === 1) row[col] = regions[i % regions.length];
|
|
else if (j >= 2 && j <= 5) row[col] = Math.floor(Math.random() * 10000) + 1000;
|
|
else if (j === 6) {
|
|
row[col] = `=C${i+1}+D${i+1}+E${i+1}+F${i+1}`;
|
|
}
|
|
else row[col] = Math.random() > 0.5 ? Math.floor(Math.random() * 1000) : '';
|
|
}
|
|
}
|
|
data.push(row);
|
|
}
|
|
return data;
|
|
}
|
|
|
|
getColumnName(index) {
|
|
let name = '';
|
|
while (index >= 0) {
|
|
name = String.fromCharCode(65 + (index % 26)) + name;
|
|
index = Math.floor(index / 26) - 1;
|
|
}
|
|
return name;
|
|
}
|
|
|
|
init() {
|
|
this.renderTable();
|
|
this.setupEventListeners();
|
|
this.updateStats();
|
|
}
|
|
|
|
renderTable() {
|
|
const thead = document.getElementById('tableHead');
|
|
const tbody = document.getElementById('tableBody');
|
|
|
|
let headerHTML = '<tr><th></th>';
|
|
for (let i = 0; i < this.cols; i++) {
|
|
headerHTML += `<th>${this.getColumnName(i)}</th>`;
|
|
}
|
|
headerHTML += '</tr>';
|
|
thead.innerHTML = headerHTML;
|
|
|
|
let bodyHTML = '';
|
|
const endRow = Math.min(this.rowOffset + this.visibleRows, this.rows);
|
|
|
|
for (let i = this.rowOffset; i < endRow; i++) {
|
|
bodyHTML += `<tr><th>${i + 1}</th>`;
|
|
for (let j = 0; j < this.cols; j++) {
|
|
const col = this.getColumnName(j);
|
|
const value = this.data[i][col] || '';
|
|
const displayValue = this.calculateCell(value, i, j);
|
|
bodyHTML += `<td data-row="${i}" data-col="${j}" data-cell="${col}${i+1}">${displayValue}</td>`;
|
|
}
|
|
bodyHTML += '</tr>';
|
|
}
|
|
tbody.innerHTML = bodyHTML;
|
|
}
|
|
|
|
calculateCell(value, row, col) {
|
|
if (typeof value === 'string' && value.startsWith('=')) {
|
|
try {
|
|
const formula = value.substring(1).toUpperCase();
|
|
|
|
if (formula.includes('SUM')) {
|
|
const match = formula.match(/SUM\(([A-Z]+\d+):([A-Z]+\d+)\)/);
|
|
if (match) {
|
|
const sum = this.calculateRange(match[1], match[2], 'sum');
|
|
return sum.toFixed(2);
|
|
}
|
|
}
|
|
|
|
if (formula.includes('AVERAGE')) {
|
|
const match = formula.match(/AVERAGE\(([A-Z]+\d+):([A-Z]+\d+)\)/);
|
|
if (match) {
|
|
const avg = this.calculateRange(match[1], match[2], 'avg');
|
|
return avg.toFixed(2);
|
|
}
|
|
}
|
|
|
|
let expression = formula;
|
|
const cellRefs = expression.match(/[A-Z]+\d+/g);
|
|
if (cellRefs) {
|
|
cellRefs.forEach(ref => {
|
|
const val = this.getCellValue(ref);
|
|
expression = expression.replace(ref, val);
|
|
});
|
|
return eval(expression).toFixed(2);
|
|
}
|
|
} catch (e) {
|
|
return '#ERROR';
|
|
}
|
|
}
|
|
return value;
|
|
}
|
|
|
|
getCellValue(cellRef) {
|
|
const col = cellRef.match(/[A-Z]+/)[0];
|
|
const row = parseInt(cellRef.match(/\d+/)[0]) - 1;
|
|
const value = this.data[row][col];
|
|
|
|
if (typeof value === 'string' && value.startsWith('=')) {
|
|
return this.calculateCell(value, row, this.getColIndex(col));
|
|
}
|
|
return parseFloat(value) || 0;
|
|
}
|
|
|
|
getColIndex(colName) {
|
|
let index = 0;
|
|
for (let i = 0; i < colName.length; i++) {
|
|
index = index * 26 + (colName.charCodeAt(i) - 64);
|
|
}
|
|
return index - 1;
|
|
}
|
|
|
|
calculateRange(start, end, operation) {
|
|
const startCol = start.match(/[A-Z]+/)[0];
|
|
const startRow = parseInt(start.match(/\d+/)[0]) - 1;
|
|
const endCol = end.match(/[A-Z]+/)[0];
|
|
const endRow = parseInt(end.match(/\d+/)[0]) - 1;
|
|
|
|
let values = [];
|
|
for (let r = startRow; r <= endRow; r++) {
|
|
for (let c = this.getColIndex(startCol); c <= this.getColIndex(endCol); c++) {
|
|
const col = this.getColumnName(c);
|
|
const val = parseFloat(this.data[r][col]) || 0;
|
|
values.push(val);
|
|
}
|
|
}
|
|
|
|
if (operation === 'sum') {
|
|
return values.reduce((a, b) => a + b, 0);
|
|
} else if (operation === 'avg') {
|
|
return values.reduce((a, b) => a + b, 0) / values.length;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
setupEventListeners() {
|
|
const container = document.getElementById('spreadsheetContainer');
|
|
const formulaInput = document.getElementById('formulaInput');
|
|
|
|
container.addEventListener('scroll', () => {
|
|
const scrollPercentage = (container.scrollTop + container.clientHeight) / container.scrollHeight;
|
|
if (scrollPercentage > 0.8 && this.rowOffset + this.visibleRows < this.rows) {
|
|
this.rowOffset += 10;
|
|
this.renderTable();
|
|
}
|
|
});
|
|
|
|
document.getElementById('tableBody').addEventListener('click', (e) => {
|
|
if (e.target.tagName === 'TD') {
|
|
this.selectCell(e.target);
|
|
}
|
|
});
|
|
|
|
document.getElementById('tableBody').addEventListener('dblclick', (e) => {
|
|
if (e.target.tagName === 'TD') {
|
|
this.editCell(e.target);
|
|
}
|
|
});
|
|
|
|
formulaInput.addEventListener('keypress', (e) => {
|
|
if (e.key === 'Enter' && this.selectedCell) {
|
|
this.updateCellValue(formulaInput.value);
|
|
}
|
|
});
|
|
}
|
|
|
|
selectCell(cell) {
|
|
if (this.selectedCell) {
|
|
this.selectedCell.classList.remove('selected');
|
|
}
|
|
this.selectedCell = cell;
|
|
cell.classList.add('selected');
|
|
|
|
const cellRef = cell.dataset.cell;
|
|
document.getElementById('cellRef').textContent = cellRef;
|
|
document.getElementById('selectedCell').textContent = cellRef;
|
|
|
|
const row = parseInt(cell.dataset.row);
|
|
const col = this.getColumnName(parseInt(cell.dataset.col));
|
|
const value = this.data[row][col] || '';
|
|
document.getElementById('formulaInput').value = value;
|
|
}
|
|
|
|
editCell(cell) {
|
|
const row = parseInt(cell.dataset.row);
|
|
const col = this.getColumnName(parseInt(cell.dataset.col));
|
|
const value = this.data[row][col] || '';
|
|
|
|
cell.classList.add('editing');
|
|
cell.innerHTML = `<input type="text" class="cell-editor" value="${value}" />`;
|
|
const input = cell.querySelector('.cell-editor');
|
|
input.focus();
|
|
input.select();
|
|
|
|
input.addEventListener('blur', () => {
|
|
this.updateCellValue(input.value);
|
|
});
|
|
|
|
input.addEventListener('keypress', (e) => {
|
|
if (e.key === 'Enter') {
|
|
this.updateCellValue(input.value);
|
|
}
|
|
});
|
|
}
|
|
|
|
updateCellValue(value) {
|
|
if (!this.selectedCell) return;
|
|
|
|
const row = parseInt(this.selectedCell.dataset.row);
|
|
const col = this.getColumnName(parseInt(this.selectedCell.dataset.col));
|
|
|
|
this.data[row][col] = value;
|
|
this.renderTable();
|
|
|
|
const newCell = document.querySelector(`td[data-row="${row}"][data-col="${this.selectedCell.dataset.col}"]`);
|
|
if (newCell) this.selectCell(newCell);
|
|
}
|
|
|
|
bold() {
|
|
if (this.selectedCell) {
|
|
this.selectedCell.style.fontWeight = this.selectedCell.style.fontWeight === 'bold' ? 'normal' : 'bold';
|
|
}
|
|
}
|
|
|
|
addRow() {
|
|
const newRow = {};
|
|
for (let i = 0; i < this.cols; i++) {
|
|
newRow[this.getColumnName(i)] = '';
|
|
}
|
|
this.data.push(newRow);
|
|
this.rows++;
|
|
this.renderTable();
|
|
this.updateStats();
|
|
}
|
|
|
|
addColumn() {
|
|
const newCol = this.getColumnName(this.cols);
|
|
this.data.forEach(row => row[newCol] = '');
|
|
this.cols++;
|
|
this.renderTable();
|
|
this.updateStats();
|
|
}
|
|
|
|
deleteRow() {
|
|
if (this.selectedCell && this.rows > 1) {
|
|
const row = parseInt(this.selectedCell.dataset.row);
|
|
this.data.splice(row, 1);
|
|
this.rows--;
|
|
this.renderTable();
|
|
this.updateStats();
|
|
}
|
|
}
|
|
|
|
deleteColumn() {
|
|
if (this.selectedCell && this.cols > 1) {
|
|
const col = this.getColumnName(parseInt(this.selectedCell.dataset.col));
|
|
this.data.forEach(row => delete row[col]);
|
|
this.cols--;
|
|
this.renderTable();
|
|
this.updateStats();
|
|
}
|
|
}
|
|
|
|
sort() {
|
|
if (this.selectedCell) {
|
|
const col = this.getColumnName(parseInt(this.selectedCell.dataset.col));
|
|
const header = this.data[0];
|
|
const dataRows = this.data.slice(1);
|
|
|
|
dataRows.sort((a, b) => {
|
|
const aVal = a[col] || '';
|
|
const bVal = b[col] || '';
|
|
return aVal.toString().localeCompare(bVal.toString());
|
|
});
|
|
|
|
this.data = [header, ...dataRows];
|
|
this.renderTable();
|
|
}
|
|
}
|
|
|
|
sum() {
|
|
if (this.selectedCell) {
|
|
const col = this.getColumnName(parseInt(this.selectedCell.dataset.col));
|
|
document.getElementById('formulaInput').value = `=SUM(${col}2:${col}${this.rows})`;
|
|
}
|
|
}
|
|
|
|
average() {
|
|
if (this.selectedCell) {
|
|
const col = this.getColumnName(parseInt(this.selectedCell.dataset.col));
|
|
document.getElementById('formulaInput').value = `=AVERAGE(${col}2:${col}${this.rows})`;
|
|
}
|
|
}
|
|
|
|
clearCell() {
|
|
if (this.selectedCell) {
|
|
this.updateCellValue('');
|
|
}
|
|
}
|
|
|
|
exportData() {
|
|
const csv = this.data.map(row => {
|
|
return Object.values(row).join(',');
|
|
}).join('\n');
|
|
|
|
const blob = new Blob([csv], { type: 'text/csv' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = 'tables_export.csv';
|
|
a.click();
|
|
}
|
|
|
|
updateStats() {
|
|
document.getElementById('rowCount').textContent = this.rows;
|
|
document.getElementById('colCount').textContent = this.cols;
|
|
}
|
|
}
|
|
|
|
const app = new TablesApp();
|
|
</script>
|
|
</body>
|
|
</html>
|