Skip to content

Excel

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta author="MiduDev" />
<meta url="midudev" />
<meta author="ShunTrDev" />
<title>Excel.js</title>
<style>
body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell,
'Open Sans', 'Helvetica Neue', sans-serif;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
img {
max-width: 20px;
height: auto;
}
table {
border-collapse: collapse;
}
thead,
tr td:first-child {
background: #eee;
}
th,
td {
border: 1px solid #ccc;
font-weight: normal;
font-size: 12px;
text-align: center;
width: 64px;
height: 20px;
vertical-align: middle;
position: relative;
}
/* td:active {
border-radius: 2px;
outline: 2px solid #09f;
} */
span,
input {
position: absolute;
inset: 0;
vertical-align: middle;
display: inline-flex;
justify-content: center;
align-items: center;
}
input {
border: 0;
opacity: 0;
pointer-events: none;
width: 100%;
border-radius: 2px;
&:focus {
opacity: 1;
outline: 2px solid #09f;
}
}
.selected {
background: rgb(174, 223, 255);
}
th.selected {
background: rgb(146, 211, 255);
}
</style>
<script type="module">
const $ = (el) => document.querySelector(el)
const $$ = (el) => document.querySelectorAll(el)
const $table = $('table')
const $head = $('thead')
const $body = $('tbody')
const ROWS = 10
const COLUMNS = 5
const FIRST_CHAR_CODE = 65
const times = (length) => Array.from({ length }, (_, i) => i)
const getColumn = (i) => String.fromCharCode(FIRST_CHAR_CODE + i)
let selectedColumn = null
let STATE = times(COLUMNS).map((i) => times(ROWS).map((j) => ({ computedValue: 0, value: 0 })))
console.log(STATE)
function updateCell({ x, y, value }) {
const newState = structuredClone(STATE)
const constants = generateCellsConstants(newState)
const cell = newState[x][y]
cell.computedValue = computeValue(value, constants) // -> span
cell.value = value // -> input
newState[x][y] = cell
computeAllCells(newState, generateCellsConstants(newState))
STATE = newState
renderSpreadSheet()
}
function generateCellsConstants(cells) {
return cells
.map((rows, x) => {
return rows
.map((cell, y) => {
const letter = getColumn(x) // -> A
const cellId = `${letter}${y + 1}` // -> A1
return `const ${cellId} = ${cell.computedValue};`
})
.join('\n')
})
.join('\n')
}
function computeAllCells(cells, constants) {
console.log('computeAllCells')
cells.forEach((rows, x) => {
rows.forEach((cell, y) => {
const computedValue = computeValue(cell.value, constants)
cell.computedValue = computedValue
})
})
}
function computeValue(value, constants) {
if (typeof value === 'number') return value
if (!value.startsWith('=')) return value
const formula = value.slice(1)
let computedValue
try {
computedValue = eval(`(() => {
${constants}
return ${formula};
})()`)
} catch (e) {
computedValue = `!ERROR: ${e.message}`
}
console.log({ value, computedValue })
return computedValue
}
const renderSpreadSheet = () => {
const headerHTML = `<tr>
<th></th>
${times(COLUMNS)
.map((i) => `<th>${getColumn(i)}</th>`)
.join('')}
</tr>`
$head.innerHTML = headerHTML
const bodyHTML = times(ROWS)
.map((row) => {
return `<tr>
<td>${row + 1}</td>
${times(COLUMNS)
.map(
(column) => `
<td data-x="${column}" data-y="${row}">
<span>${STATE[column][row].computedValue}</span>
<input type="text" value="${STATE[column][row].value}" />
</td>
`
)
.join('')}
</tr>`
})
.join('')
$body.innerHTML = bodyHTML
}
$body.addEventListener('click', (event) => {
const td = event.target.closest('td')
if (!td) return
const { x, y } = td.dataset
const input = td.querySelector('input')
const span = td.querySelector('span')
const end = input.value.length
input.setSelectionRange(end, end)
input.focus()
$$('.selected').forEach((el) => el.classList.remove('selected'))
selectedColumn = null
input.addEventListener('keydown', (event) => {
if (event.key === 'Enter') input.blur()
})
input.addEventListener(
'blur',
() => {
console.log({ value: input.value, state: STATE[x][y].value })
if (input.value === STATE[x][y].value) return
updateCell({ x, y, value: input.value })
},
{ once: true }
)
})
$head.addEventListener('click', (event) => {
const th = event.target.closest('th')
if (!th) return
const x = [...th.parentNode.children].indexOf(th)
if (x <= 0) return
selectedColumn = x - 1
$$('.selected').forEach((el) => el.classList.remove('selected'))
th.classList.add('selected')
$$(`tr td:nth-child(${x + 1})`).forEach((el) => el.classList.add('selected'))
})
document.addEventListener('keydown', (event) => {
if (event.key === 'Backspace' && selectedColumn !== null) {
times(ROWS).forEach((row) => {
updateCell({ x: selectedColumn, y: row, value: '' })
})
renderSpreadSheet()
}
})
document.addEventListener('copy', (event) => {
if (selectedColumn !== null) {
const columnValues = times(ROWS).map((row) => {
return STATE[selectedColumn][row].computedValue
})
event.clipboardData.setData('text/plain', columnValues.join('\n'))
event.preventDefault()
}
})
document.addEventListener('click', (event) => {
const { target } = event
const isThClicked = target.closest('th')
const isTdClicked = target.closest('td')
if (!isThClicked && !isTdClicked) {
$$('.selected').forEach((el) => el.classList.remove('selected'))
selectedColumn = null
}
})
renderSpreadSheet()
</script>
</head>
<body>
<img
src="https://upload.wikimedia.org/wikipedia/commons/thumb/7/73/Microsoft_Excel_2013-2019_logo.svg/1200px-Microsoft_Excel_2013-2019_logo.svg.png" />
<table>
<thead></thead>
<tbody></tbody>
</table>
</body>
</html>
<!--
- Añadir la funconalidad de filas
- Haz la suma por rangos=A1:A20
- Seleccionar por celdas
-->