simple-csv-editor
Advanced tools
Comparing version
{ | ||
"env": { | ||
"browser": true, | ||
"es2023": true | ||
"es2022": true | ||
}, | ||
@@ -10,3 +10,3 @@ "extends": [ | ||
"parserOptions": { | ||
"ecmaVersion": 2023 | ||
"ecmaVersion": 2022 | ||
}, | ||
@@ -19,3 +19,13 @@ "rules": { | ||
"no-new": 0, | ||
"no-console": ["error", { "allow": ["error"] }] | ||
"no-console": [ | ||
"error", | ||
{ | ||
"allow": [ | ||
"error" | ||
] | ||
} | ||
], | ||
"no-continue": 0, | ||
"no-alert": 0, | ||
"max-len": 0 | ||
}, | ||
@@ -22,0 +32,0 @@ "globals": { |
{ | ||
"name": "simple-csv-editor", | ||
"version": "1.2.0", | ||
"version": "1.3.0", | ||
"description": "A table editor for easily editing and retrieving CSV data.", | ||
@@ -15,6 +15,15 @@ "main": "src/simple-csv-editor.js", | ||
"keywords": [ | ||
"simple", | ||
"csv", | ||
"editor", | ||
"js", | ||
"es-modules" | ||
"json", | ||
"module", | ||
"synchronized", | ||
"synced", | ||
"html", | ||
"table", | ||
"papaparse", | ||
"parser", | ||
"unstyled", | ||
"api" | ||
], | ||
@@ -21,0 +30,0 @@ "author": "Daniel Geymayer", |
@@ -9,2 +9,8 @@ # Simple CSV Editor | ||
## Installation | ||
```bash | ||
npm install --save simple-csv-editor | ||
``` | ||
## Usage | ||
@@ -18,3 +24,5 @@ | ||
<!-- PapaParse dependency, very important for the editor to work! --> | ||
<!-- PapaParse CSV parser dependency - very important for the editor to work! | ||
Of course you can also use the library via "npm i papaparse" or download it yourself. | ||
At least the vendored version here guarantees compatibility with the editor. --> | ||
<script src="papaparse.min.js"></script> | ||
@@ -45,4 +53,12 @@ | ||
## Versioning | ||
This project uses semantic versioning. | ||
- Major version change: Expect breaking changes | ||
- Minor version change: New features | ||
- Patch version change: Minor bugfixes, Refactoring, etc. | ||
## Dependencies | ||
- [PapaParse](https://www.papaparse.com) |
@@ -6,2 +6,16 @@ class SimpleCsvEditor { | ||
onChange = null, | ||
warnOnDelete = true, | ||
showControls = true, | ||
controlLabels = { | ||
addRowBefore: '+ ↑', | ||
addRowAfter: '+ ↓', | ||
addColumnBefore: '+ ←', | ||
addColumnAfter: '+ →', | ||
deleteRow: '✖', | ||
deleteColumn: '✖', | ||
deleteAll: '✖', | ||
deleteRowWarning: 'DELETE THIS ROW?', | ||
deleteColumnWarning: 'DELETE THIS COLUMN?', | ||
deleteAllWarning: 'DELETE ALL DATA?', | ||
}, | ||
delimiter = null, | ||
@@ -24,2 +38,5 @@ quoteChar = '"', | ||
this.onChange = onChange; | ||
this.warnOnDelete = warnOnDelete; | ||
this.showControls = showControls; | ||
this.controlLabels = controlLabels; | ||
@@ -46,2 +63,70 @@ this.papaParseConfig = { | ||
static #buildBasicButton(label) { | ||
const button = document.createElement('button'); | ||
button.type = 'button'; | ||
button.innerText = label; | ||
button.tabIndex = -1; | ||
return button; | ||
} | ||
#buildAddRowButton(offsetIdx, label) { | ||
const button = SimpleCsvEditor.#buildBasicButton(label); | ||
button.addEventListener('click', (event) => { | ||
this.addRow(event.target.parentElement.parentElement.rowIndex + offsetIdx); | ||
}); | ||
return button; | ||
} | ||
#buildAddColumnButton(offsetIdx, label) { | ||
const button = SimpleCsvEditor.#buildBasicButton(label); | ||
button.addEventListener('click', (event) => { | ||
this.addColumn(event.target.parentElement.cellIndex + offsetIdx); | ||
}); | ||
return button; | ||
} | ||
#buildDeleteRowButton(label) { | ||
const button = SimpleCsvEditor.#buildBasicButton(label); | ||
button.addEventListener('click', (event) => { | ||
if (!this.warnOnDelete || window.confirm(this.controlLabels.deleteRowWarning)) { | ||
this.deleteRow(event.target.parentElement.parentElement.rowIndex); | ||
} | ||
}); | ||
return button; | ||
} | ||
#buildDeleteColumnButton(label) { | ||
const button = SimpleCsvEditor.#buildBasicButton(label); | ||
button.addEventListener('click', (event) => { | ||
if (!this.warnOnDelete || window.confirm(this.controlLabels.deleteColumnWarning)) { | ||
this.deleteColumn(event.target.parentElement.cellIndex); | ||
} | ||
}); | ||
return button; | ||
} | ||
#buildDeleteAllButton(label) { | ||
const button = SimpleCsvEditor.#buildBasicButton(label); | ||
button.addEventListener('click', () => { | ||
if (!this.warnOnDelete || window.confirm(this.controlLabels.deleteAllWarning)) { | ||
this.deleteAll(); | ||
} | ||
}); | ||
return button; | ||
} | ||
#addColumnControlCell(row, cellIdx) { | ||
const cell = row.insertCell(cellIdx); | ||
cell.appendChild(this.#buildAddColumnButton(0, this.controlLabels.addColumnBefore)); | ||
cell.appendChild(this.#buildDeleteColumnButton(this.controlLabels.deleteColumn)); | ||
cell.appendChild(this.#buildAddColumnButton(1, this.controlLabels.addColumnAfter)); | ||
} | ||
#addRowControlCell(row, cellIdx) { | ||
const cell = row.insertCell(cellIdx); | ||
cell.appendChild(this.#buildAddRowButton(0, this.controlLabels.addRowBefore)); | ||
cell.appendChild(this.#buildDeleteRowButton(this.controlLabels.deleteRow)); | ||
cell.appendChild(this.#buildAddRowButton(1, this.controlLabels.addRowAfter)); | ||
} | ||
static #checkCursorPosition(cell) { | ||
@@ -92,3 +177,3 @@ const selection = window.getSelection(); | ||
#addCellToRow(row, cellIdx = -1) { | ||
#addDataCellToRow(row, cellIdx) { | ||
const newCell = row.insertCell(cellIdx); | ||
@@ -100,3 +185,2 @@ newCell.contentEditable = true; | ||
newCell.addEventListener('keydown', (event) => { | ||
const rowIdx = event.target.parentElement.rowIndex; | ||
const { rows } = row.parentElement; | ||
@@ -106,6 +190,5 @@ switch (event.key) { | ||
event.preventDefault(); | ||
const newRowIdx = (event.shiftKey) ? rowIdx : rowIdx + 1; | ||
const newRowIdx = event.shiftKey ? row.rowIndex : row.rowIndex + 1; | ||
this.addRow(newRowIdx); | ||
rows[newRowIdx].cells[newCell.cellIndex].focus(); | ||
this.#triggerOnChange(); | ||
break; | ||
@@ -115,7 +198,7 @@ } | ||
event.preventDefault(); | ||
SimpleCsvEditor.#jumpToEndOfCell(rows[rowIdx - 1]?.cells[newCell.cellIndex]); | ||
SimpleCsvEditor.#jumpToEndOfCell(rows[row.rowIndex - 1]?.cells[newCell.cellIndex]); | ||
break; | ||
case 'ArrowDown': | ||
event.preventDefault(); | ||
SimpleCsvEditor.#jumpToEndOfCell(rows[rowIdx + 1]?.cells[newCell.cellIndex]); | ||
SimpleCsvEditor.#jumpToEndOfCell(rows[row.rowIndex + 1]?.cells[newCell.cellIndex]); | ||
break; | ||
@@ -139,9 +222,17 @@ case 'ArrowLeft': | ||
getCsv() { | ||
return Array.from(this.table.rows).slice(this.showControls ? 1 : 0) | ||
.map((row) => Array.from(row.cells).slice(0, this.showControls ? -1 : undefined) | ||
.map((cell) => cell.textContent) | ||
.join(this.papaParseConfig.delimiter)) | ||
.join(this.detectedLineBreak) + (this.lastLineEmpty ? this.detectedLineBreak : ''); | ||
} | ||
setCsv(data) { | ||
const result = Papa.parse(data, this.papaParseConfig); | ||
if (result.errors.length > 0) { | ||
for (const error of result.errors) { | ||
console.error(error); | ||
for (const error of result.errors) { | ||
if (error.type === 'Delimiter' && error.code === 'UndetectableDelimiter') { | ||
continue; | ||
} | ||
return; | ||
console.error(error); | ||
} | ||
@@ -157,6 +248,12 @@ | ||
if (this.table.rows[lineIdx] == null) { | ||
this.addRow(); | ||
const numCells = (lineIdx <= 0) ? lineTokens.length : this.table.rows[lineIdx - 1].cells.length; | ||
const newRow = this.table.insertRow(-1); | ||
for (let cellIdx = 0; cellIdx < numCells; cellIdx += 1) { | ||
this.#addDataCellToRow(newRow, -1); | ||
} | ||
} | ||
if (this.table.rows[lineIdx].cells[tokenIdx] == null) { | ||
this.addColumn(); | ||
for (const row of this.table.rows) { | ||
this.#addDataCellToRow(row, -1); | ||
} | ||
} | ||
@@ -166,43 +263,70 @@ this.table.rows[lineIdx].cells[tokenIdx].textContent = token; | ||
} | ||
if (this.table.rows.length <= 0) { | ||
this.#addDataCellToRow(this.table.insertRow(0), 0); | ||
} | ||
if (this.showControls) { | ||
const columnControlsRow = this.table.insertRow(0); | ||
for (let cellIdx = 0; cellIdx < this.table.rows[1].cells.length; cellIdx += 1) { | ||
this.#addColumnControlCell(columnControlsRow, -1); | ||
} | ||
for (const row of this.table.rows) { | ||
if (row.rowIndex === 0) { | ||
row.insertCell(-1).appendChild(this.#buildDeleteAllButton(this.controlLabels.deleteAll)); | ||
} else { | ||
this.#addRowControlCell(row, -1); | ||
} | ||
} | ||
} | ||
} | ||
getCsv() { | ||
return Array.from(this.table.rows) | ||
.map((row) => Array.from(row.cells) | ||
.map((cell) => cell.textContent) | ||
.join(this.papaParseConfig.delimiter)) | ||
.join(this.detectedLineBreak) + (this.lastLineEmpty ? this.detectedLineBreak : ''); | ||
} | ||
addRow(rowIdx = -1) { | ||
const firstRow = (this.table.rows.length > 0) ? this.table.rows[0] : null; | ||
addRow(rowIdx) { | ||
const firstDataRowIdx = this.showControls ? 1 : 0; | ||
const firstDataRow = (this.table.rows.length > firstDataRowIdx) ? this.table.rows[firstDataRowIdx] : null; | ||
const newRow = this.table.insertRow(rowIdx); | ||
for (let cellIdx = 0; cellIdx < (firstRow ?? newRow).cells.length; cellIdx += 1) { | ||
this.#addCellToRow(newRow); | ||
const numCells = (firstDataRow ?? newRow).cells.length; | ||
for (let cellIdx = 0; cellIdx < numCells; cellIdx += 1) { | ||
if (this.showControls && cellIdx === numCells - 1) { | ||
this.#addRowControlCell(newRow, -1); | ||
} else { | ||
this.#addDataCellToRow(newRow, -1); | ||
} | ||
} | ||
this.#triggerOnChange(); | ||
} | ||
addColumn(cellIdx = -1) { | ||
addColumn(cellIdx) { | ||
for (const row of this.table.rows) { | ||
this.#addCellToRow(row, cellIdx); | ||
if (this.showControls && row.rowIndex === 0) { | ||
this.#addColumnControlCell(row, cellIdx); | ||
} else { | ||
this.#addDataCellToRow(row, cellIdx); | ||
} | ||
} | ||
this.#triggerOnChange(); | ||
} | ||
deleteRow(rowIdx = -1) { | ||
if (this.table.rows.length <= 1) { | ||
deleteRow(rowIdx) { | ||
if (this.table.rows.length <= (this.showControls ? 2 : 1)) { | ||
return; | ||
} | ||
this.table.deleteRow(rowIdx); | ||
this.#triggerOnChange(); | ||
} | ||
deleteColumn(cellIdx = -1) { | ||
if (this.table.rows[0].cells.length <= 1) { | ||
deleteColumn(columnIdx) { | ||
if (this.table.rows[0].cells.length <= (this.showControls ? 2 : 1)) { | ||
return; | ||
} | ||
for (const row of this.table.rows) { | ||
row.deleteCell(cellIdx); | ||
row.deleteCell(columnIdx); | ||
} | ||
this.#triggerOnChange(); | ||
} | ||
deleteAll() { | ||
this.setCsv(''); | ||
this.#triggerOnChange(); | ||
} | ||
} | ||
export default SimpleCsvEditor; |
Sorry, the diff of this file is not supported yet
35615
17.03%357
53.88%62
34.78%