- 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.
364 lines
11 KiB
HTML
364 lines
11 KiB
HTML
<template>
|
|
<div class="spreadsheet-container" id="spreadsheetContainer">
|
|
<table class="spreadsheet" id="spreadsheet">
|
|
<thead id="tableHead"></thead>
|
|
<tbody id="tableBody"></tbody>
|
|
</table>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
class TablesApp {
|
|
constructor() {
|
|
this.data = this.generateMockData(100, 26); // 100 rows, 26 columns (A-Z)
|
|
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) {
|
|
// Header row
|
|
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) {
|
|
// Calculate total (this will be formatted as formula)
|
|
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();
|
|
}
|
|
|
|
renderTable() {
|
|
const thead = document.getElementById('tableHead');
|
|
const tbody = document.getElementById('tableBody');
|
|
|
|
// Render header
|
|
let headerHTML = '<tr><th></th>';
|
|
for (let i = 0; i < this.cols; i++) {
|
|
headerHTML += `<th>${this.getColumnName(i)}</th>`;
|
|
}
|
|
headerHTML += '</tr>';
|
|
thead.innerHTML = headerHTML;
|
|
|
|
// Render visible rows
|
|
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();
|
|
|
|
// Handle SUM formula
|
|
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);
|
|
}
|
|
}
|
|
|
|
// Handle AVERAGE formula
|
|
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);
|
|
}
|
|
}
|
|
|
|
// Handle simple arithmetic (e.g., =C2+D2+E2+F2)
|
|
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');
|
|
|
|
// Infinite scroll
|
|
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();
|
|
}
|
|
});
|
|
|
|
// Cell selection
|
|
document.getElementById('tableBody').addEventListener('click', (e) => {
|
|
if (e.target.tagName === 'TD') {
|
|
this.selectCell(e.target);
|
|
}
|
|
});
|
|
|
|
// Cell editing
|
|
document.getElementById('tableBody').addEventListener('dblclick', (e) => {
|
|
if (e.target.tagName === 'TD') {
|
|
this.editCell(e.target);
|
|
}
|
|
});
|
|
|
|
// Formula bar
|
|
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();
|
|
|
|
// Reselect the cell
|
|
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();
|
|
}
|
|
|
|
addColumn() {
|
|
const newCol = this.getColumnName(this.cols);
|
|
this.data.forEach(row => row[newCol] = '');
|
|
this.cols++;
|
|
this.renderTable();
|
|
}
|
|
|
|
deleteRow() {
|
|
if (this.selectedCell && this.rows > 1) {
|
|
const row = parseInt(this.selectedCell.dataset.row);
|
|
this.data.splice(row, 1);
|
|
this.rows--;
|
|
this.renderTable();
|
|
}
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|
|
|
|
const app = new TablesApp();
|
|
</script>
|