Initial commit
This commit is contained in:
commit
536ab439aa
|
@ -0,0 +1,9 @@
|
||||||
|
image: scratch
|
||||||
|
|
||||||
|
pages:
|
||||||
|
stage: deploy
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- .
|
||||||
|
only:
|
||||||
|
- master
|
|
@ -0,0 +1,19 @@
|
||||||
|
Copyright (c) 2023 blankie
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
|
@ -0,0 +1,126 @@
|
||||||
|
/*
|
||||||
|
* line: string, must be trimmed of whitespace beforehand
|
||||||
|
* tableItems: int | null, result output will be padded or trimmed to it if specified
|
||||||
|
*
|
||||||
|
* Returns an array of strings
|
||||||
|
*/
|
||||||
|
function splitByPipe(line, tableItems = null) {
|
||||||
|
let res = [];
|
||||||
|
let item = "";
|
||||||
|
let consecutiveBackslashes = 0;
|
||||||
|
|
||||||
|
if (line.startsWith("|")) {
|
||||||
|
line = line.substring(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let char of line) {
|
||||||
|
if (char === "\\") {
|
||||||
|
item += "\\";
|
||||||
|
consecutiveBackslashes++;
|
||||||
|
} else if (char === "|" && consecutiveBackslashes % 2 === 1) {
|
||||||
|
item = item.substring(0, item.length - 1) + "|";
|
||||||
|
consecutiveBackslashes = 0;
|
||||||
|
} else if (char === "|" && consecutiveBackslashes % 2 === 0) {
|
||||||
|
item = item.trim();
|
||||||
|
res.push(item);
|
||||||
|
|
||||||
|
item = "";
|
||||||
|
consecutiveBackslashes = 0;
|
||||||
|
} else {
|
||||||
|
item += char;
|
||||||
|
consecutiveBackslashes = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
item = item.trim();
|
||||||
|
if (item.length !== 0) {
|
||||||
|
res.push(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (tableItems !== null && res.length < tableItems) {
|
||||||
|
res.push("");
|
||||||
|
}
|
||||||
|
while (tableItems !== null && res.length > tableItems) {
|
||||||
|
res.pop();
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* line: string, must be trimmed of whitespace beforehand
|
||||||
|
* headerRow: array of strings
|
||||||
|
*
|
||||||
|
* Returns an array of objects, which has the following keys:
|
||||||
|
* - text: string
|
||||||
|
* - leftAligned: boolean
|
||||||
|
* - rightAligned: boolean
|
||||||
|
*/
|
||||||
|
function parseDelimiterRow(line, headerRow) {
|
||||||
|
let res = [];
|
||||||
|
let delimiterRow = splitByPipe(line);
|
||||||
|
|
||||||
|
if (delimiterRow.length !== headerRow.length) {
|
||||||
|
throw new Error(`delimiter row is not as long as the header row (${delimiterRow.length} !== ${headerRow.length})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < headerRow.length; i++) {
|
||||||
|
let header = headerRow[i];
|
||||||
|
let delimiter = delimiterRow[i];
|
||||||
|
let match = delimiter.match(/^(:)?-+(:)?$/);
|
||||||
|
|
||||||
|
if (match === null) {
|
||||||
|
throw new Error(`invalid delimiter row (index: ${i}): ${delimiter}`);
|
||||||
|
}
|
||||||
|
res.push({
|
||||||
|
text: header,
|
||||||
|
leftAligned: match[1] === ":",
|
||||||
|
rightAligned: match[2] === ":"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* lines: array of strings, each item must be trimmed of whitespace beforehand
|
||||||
|
*
|
||||||
|
* See parseDelimiterRow() for the return type
|
||||||
|
*/
|
||||||
|
function parseHeader(lines) {
|
||||||
|
if (lines.length < 2) {
|
||||||
|
throw new Error(`input must be at least 2 lines long (${lines.length} < 2)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let headerRow = splitByPipe(lines[0]);
|
||||||
|
if (headerRow.length === 0) {
|
||||||
|
throw new Error("header row must have at least one item");
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseDelimiterRow(lines[1], headerRow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* input: string
|
||||||
|
*
|
||||||
|
* Returns an object with the following keys:
|
||||||
|
* - columns: See parseHeader() for the first item
|
||||||
|
* - rows: array of an array of strings
|
||||||
|
*/
|
||||||
|
function deserializeTable(input) {
|
||||||
|
let lines = [];
|
||||||
|
let rows = [];
|
||||||
|
|
||||||
|
for (let line of input.trim().split("\n")) {
|
||||||
|
lines.push(line.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
let columns = parseHeader(lines);
|
||||||
|
for (let i = 2; i < lines.length; i++) {
|
||||||
|
rows.push(splitByPipe(lines[i], columns.length));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {columns, rows};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export {deserializeTable};
|
|
@ -0,0 +1,73 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>Markdown Table Editor</title>
|
||||||
|
<link rel="stylesheet" href="style.css" />
|
||||||
|
<script asyhc type="module" src="main.mjs"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<noscript><p class="left right">Javascript is required because the closest non-JS alternative is doing everything server-side, and I do not want to bother with that</p></noscript>
|
||||||
|
|
||||||
|
<dialog id="load-dialog">
|
||||||
|
<h3>Load table</h3>
|
||||||
|
<span id="error"></span>
|
||||||
|
<textarea autofocus></textarea>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<div class="buttons">
|
||||||
|
<button id="load-button">Load table</button>
|
||||||
|
<form method="dialog">
|
||||||
|
<button>Close</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
<button id="open-load-button">Load table</button>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<dialog id="item-context-menu">
|
||||||
|
<form id="alignment-radios">
|
||||||
|
<fieldset>
|
||||||
|
<legend>Column alignment:</legend>
|
||||||
|
<label>
|
||||||
|
<input type="radio" name="alignment" id="none" />
|
||||||
|
None
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input type="radio" name="alignment" id="left" />
|
||||||
|
Left
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input type="radio" name="alignment" id="center" />
|
||||||
|
Center
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input type="radio" name="alignment" id="right" />
|
||||||
|
Right
|
||||||
|
</label>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
<button id="delete-row">Delete row</button>
|
||||||
|
<button id="delete-column">Delete column</button>
|
||||||
|
<hr>
|
||||||
|
<button id="insert-row-above">Insert row above</button>
|
||||||
|
<button id="insert-row-below">Insert row below</button>
|
||||||
|
<button id="insert-column-left">Insert column to the left</button>
|
||||||
|
<button id="insert-column-right">Insert column to the right</button>
|
||||||
|
</dialog>
|
||||||
|
|
||||||
|
Hold Shift to suppress table controls
|
||||||
|
|
||||||
|
<div class="table-container">
|
||||||
|
<table></table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<button id="copy-to-clipboard">Copy to Clipboard</button>
|
||||||
|
<textarea id="output" readonly rows="15"></textarea>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<footer class="left right"><a href="https://gitlab.com/blankX/mdtabeditor">Source code</a> (<a href="https://git.nixnet.services/blankie/mdtabeditor">Mirror</a>)</footer>
|
||||||
|
</body>
|
|
@ -0,0 +1,362 @@
|
||||||
|
import {deserializeTable, serializeTable, calculateColumnLength} from "./tables.mjs";
|
||||||
|
|
||||||
|
let loadDialog = document.querySelector("#load-dialog");
|
||||||
|
|
||||||
|
let itemContextMenu = document.querySelector("#item-context-menu");
|
||||||
|
let deleteRow = itemContextMenu.querySelector("#delete-row");
|
||||||
|
let deleteColumn = itemContextMenu.querySelector("#delete-column");
|
||||||
|
let insertRowAbove = itemContextMenu.querySelector("#insert-row-above");
|
||||||
|
let insertRowBelow = itemContextMenu.querySelector("#insert-row-below");
|
||||||
|
let insertColumnLeft = itemContextMenu.querySelector("#insert-column-left");
|
||||||
|
let insertColumnRight = itemContextMenu.querySelector("#insert-column-right");
|
||||||
|
|
||||||
|
let output = document.querySelector("#output");
|
||||||
|
let tableElement = document.querySelector("table");
|
||||||
|
let table = {
|
||||||
|
columns: [
|
||||||
|
{text: "ABC", leftAligned: false, rightAligned: false},
|
||||||
|
{text: "DEF", leftAligned: false, rightAligned: false},
|
||||||
|
{text: "GHI", leftAligned: false, rightAligned: false}
|
||||||
|
],
|
||||||
|
rows: [
|
||||||
|
["1", "2", "3"],
|
||||||
|
["4", "5", "6"],
|
||||||
|
["7", "8", "9"]
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Array.prototype.insert = function(index, item) {
|
||||||
|
this.splice(index, 0, item);
|
||||||
|
};
|
||||||
|
|
||||||
|
Array.prototype.remove = function(index) {
|
||||||
|
this.splice(index, 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
Node.prototype.insertAfter = function(newNode, referenceNode) {
|
||||||
|
this.insertBefore(newNode, referenceNode?.nextSibling);
|
||||||
|
};
|
||||||
|
|
||||||
|
NodeList.prototype.indexOf = function(searchElement) {
|
||||||
|
for (let [index, element] of this.entries()) {
|
||||||
|
if (element === searchElement) {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
};
|
||||||
|
|
||||||
|
function findTableIndices(textarea) {
|
||||||
|
let index = tableElement.querySelectorAll("textarea").indexOf(textarea);
|
||||||
|
let columnIndex = index % table.columns.length;
|
||||||
|
let rowIndex = -1;
|
||||||
|
|
||||||
|
if (index >= table.columns.length) {
|
||||||
|
rowIndex = Math.floor(index / table.columns.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {columnIndex, rowIndex};
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateItemAlignment(textarea, column) {
|
||||||
|
textarea.classList.remove("left");
|
||||||
|
textarea.classList.remove("right");
|
||||||
|
if (column.leftAligned) {
|
||||||
|
textarea.classList.add("left");
|
||||||
|
}
|
||||||
|
if (column.rightAligned) {
|
||||||
|
textarea.classList.add("right");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function createItemElement(item, column) {
|
||||||
|
let textarea = document.createElement("textarea");
|
||||||
|
textarea.rows = 1;
|
||||||
|
textarea.cols = column.maxLength;
|
||||||
|
textarea.value = item;
|
||||||
|
if (column.leftAligned) {
|
||||||
|
textarea.classList.add("left");
|
||||||
|
}
|
||||||
|
if (column.rightAligned) {
|
||||||
|
textarea.classList.add("right");
|
||||||
|
}
|
||||||
|
return textarea;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createRowElement(row, isHeader) {
|
||||||
|
let tr = document.createElement(isHeader ? "th" : "tr");
|
||||||
|
|
||||||
|
for (let i = 0; i < row.length; i++) {
|
||||||
|
let td = document.createElement("td");
|
||||||
|
td.appendChild(createItemElement(row[i], table.columns[i]));
|
||||||
|
tr.appendChild(td);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tr;
|
||||||
|
}
|
||||||
|
|
||||||
|
function reloadTable() {
|
||||||
|
for (let i = 0; i < table.columns.length; i++) {
|
||||||
|
table.columns[i].maxLength = calculateColumnLength(table.columns[i], table.rows, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (tableElement.lastChild) {
|
||||||
|
tableElement.removeChild(tableElement.lastChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
let columnRow = [];
|
||||||
|
for (let column of table.columns) {
|
||||||
|
columnRow.push(column.text);
|
||||||
|
}
|
||||||
|
tableElement.appendChild(createRowElement(columnRow, true));
|
||||||
|
|
||||||
|
for (let row of table.rows) {
|
||||||
|
tableElement.appendChild(createRowElement(row, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
output.value = serializeTable(table);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
loadDialog.querySelector("#load-button").addEventListener("click", function() {
|
||||||
|
try {
|
||||||
|
table = deserializeTable(loadDialog.querySelector("textarea").value);
|
||||||
|
reloadTable();
|
||||||
|
loadDialog.close();
|
||||||
|
} catch (e) {
|
||||||
|
loadDialog.querySelector("#error").innerText = e.message;
|
||||||
|
}
|
||||||
|
}, {passive: true});
|
||||||
|
|
||||||
|
document.querySelector("#open-load-button").addEventListener("click", function() {
|
||||||
|
loadDialog.querySelector("textarea").value = "";
|
||||||
|
loadDialog.querySelector("#error").innerText = "";
|
||||||
|
loadDialog.showModal();
|
||||||
|
}, {passive: true});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
itemContextMenu.querySelector("form").addEventListener("change", function(event) {
|
||||||
|
let leftAligned = ["left", "center"].includes(event.target.id);
|
||||||
|
let rightAligned = ["center", "right"].includes(event.target.id);
|
||||||
|
let columnIndex = parseInt(itemContextMenu.dataset.columnIndex, 10);
|
||||||
|
let column = table.columns[columnIndex];
|
||||||
|
|
||||||
|
column.leftAligned = leftAligned;
|
||||||
|
column.rightAligned = rightAligned;
|
||||||
|
|
||||||
|
updateItemAlignment(tableElement.querySelectorAll("th textarea")[columnIndex], column);
|
||||||
|
for (let tr of tableElement.querySelectorAll("tr")) {
|
||||||
|
updateItemAlignment(tr.querySelectorAll("textarea")[columnIndex], column);
|
||||||
|
}
|
||||||
|
|
||||||
|
output.value = serializeTable(table);
|
||||||
|
});
|
||||||
|
|
||||||
|
deleteRow.addEventListener("click", function() {
|
||||||
|
let rowIndex = parseInt(itemContextMenu.dataset.rowIndex, 10);
|
||||||
|
|
||||||
|
table.rows.remove(rowIndex);
|
||||||
|
tableElement.querySelectorAll("tr")[rowIndex].remove();
|
||||||
|
|
||||||
|
output.value = serializeTable(table);
|
||||||
|
itemContextMenu.close();
|
||||||
|
}, {passive: true});
|
||||||
|
|
||||||
|
deleteColumn.addEventListener("click", function() {
|
||||||
|
let columnIndex = parseInt(itemContextMenu.dataset.columnIndex, 10);
|
||||||
|
|
||||||
|
table.columns.remove(columnIndex);
|
||||||
|
for (let row of table.rows) {
|
||||||
|
row.remove(columnIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let tr of tableElement.querySelectorAll("th, tr")) {
|
||||||
|
tr.querySelectorAll("td")[columnIndex].remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
output.value = serializeTable(table);
|
||||||
|
itemContextMenu.close();
|
||||||
|
}, {passive: true});
|
||||||
|
|
||||||
|
insertRowAbove.addEventListener("click", function() {
|
||||||
|
let rowIndex = parseInt(itemContextMenu.dataset.rowIndex, 10);
|
||||||
|
|
||||||
|
let newRow = new Array(table.columns.length);
|
||||||
|
newRow.fill("");
|
||||||
|
table.rows.insert(rowIndex, newRow);
|
||||||
|
|
||||||
|
tableElement.insertBefore(createRowElement(newRow, false), tableElement.querySelectorAll("tr")[rowIndex]);
|
||||||
|
|
||||||
|
output.value = serializeTable(table);
|
||||||
|
itemContextMenu.close();
|
||||||
|
}, {passive: true});
|
||||||
|
|
||||||
|
insertRowBelow.addEventListener("click", function() {
|
||||||
|
let rowIndex = parseInt(itemContextMenu.dataset.rowIndex, 10);
|
||||||
|
|
||||||
|
let newRow = new Array(table.columns.length);
|
||||||
|
newRow.fill("");
|
||||||
|
table.rows.insert(rowIndex + 1, newRow);
|
||||||
|
|
||||||
|
let rowElement = createRowElement(newRow, false);
|
||||||
|
let referenceNode = rowIndex >= 0
|
||||||
|
? tableElement.querySelectorAll("tr")[rowIndex]
|
||||||
|
: tableElement.querySelector("th");
|
||||||
|
tableElement.insertAfter(rowElement, referenceNode);
|
||||||
|
|
||||||
|
output.value = serializeTable(table);
|
||||||
|
itemContextMenu.close();
|
||||||
|
}, {passive: true});
|
||||||
|
|
||||||
|
insertColumnLeft.addEventListener("click", function() {
|
||||||
|
let columnIndex = parseInt(itemContextMenu.dataset.columnIndex, 10);
|
||||||
|
|
||||||
|
let column = {
|
||||||
|
text: "",
|
||||||
|
leftAligned: false,
|
||||||
|
rightAligned: false,
|
||||||
|
maxLength: 3
|
||||||
|
};
|
||||||
|
table.columns.insert(columnIndex, column);
|
||||||
|
for (let row of table.rows) {
|
||||||
|
row.insert(columnIndex, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
let td = document.createElement("td");
|
||||||
|
td.appendChild(createItemElement("", column));
|
||||||
|
|
||||||
|
let th = tableElement.querySelector("th");
|
||||||
|
for (let tr of tableElement.querySelectorAll("th, tr")) {
|
||||||
|
tr.insertBefore(td.cloneNode(true), tr.querySelectorAll("td")[columnIndex]);
|
||||||
|
}
|
||||||
|
|
||||||
|
output.value = serializeTable(table);
|
||||||
|
itemContextMenu.close();
|
||||||
|
}, {passive: true});
|
||||||
|
|
||||||
|
insertColumnRight.addEventListener("click", function() {
|
||||||
|
let columnIndex = parseInt(itemContextMenu.dataset.columnIndex, 10);
|
||||||
|
|
||||||
|
let column = {
|
||||||
|
text: "",
|
||||||
|
leftAligned: false,
|
||||||
|
rightAligned: false,
|
||||||
|
maxLength: 3
|
||||||
|
};
|
||||||
|
table.columns.insert(columnIndex + 1, column);
|
||||||
|
for (let row of table.rows) {
|
||||||
|
row.insert(columnIndex + 1, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
let td = document.createElement("td");
|
||||||
|
td.appendChild(createItemElement("", column));
|
||||||
|
|
||||||
|
let th = tableElement.querySelector("th");
|
||||||
|
for (let tr of tableElement.querySelectorAll("th, tr")) {
|
||||||
|
tr.insertAfter(td.cloneNode(true), tr.querySelectorAll("td")[columnIndex]);
|
||||||
|
}
|
||||||
|
|
||||||
|
output.value = serializeTable(table);
|
||||||
|
itemContextMenu.close();
|
||||||
|
}, {passive: true});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
tableElement.addEventListener("input", function(event) {
|
||||||
|
let {columnIndex, rowIndex} = findTableIndices(event.target);
|
||||||
|
if (rowIndex < 0) {
|
||||||
|
// is modifying a header
|
||||||
|
table.columns[columnIndex].text = event.target.value;
|
||||||
|
} else {
|
||||||
|
// is modifying a row
|
||||||
|
table.rows[rowIndex][columnIndex] = event.target.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
output.value = serializeTable(table);
|
||||||
|
|
||||||
|
table.columns[columnIndex].maxLength = calculateColumnLength(table.columns[columnIndex], table.rows, columnIndex);
|
||||||
|
for (let rowElement of tableElement.querySelectorAll("th, tr")) {
|
||||||
|
rowElement.children[columnIndex].querySelector("textarea").cols = table.columns[columnIndex].maxLength;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tableElement.addEventListener("contextmenu", function(event) {
|
||||||
|
if (event.shiftKey || event.target.localName !== "textarea") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let {columnIndex, rowIndex} = findTableIndices(event.target);
|
||||||
|
itemContextMenu.dataset.columnIndex = columnIndex;
|
||||||
|
itemContextMenu.dataset.rowIndex = rowIndex;
|
||||||
|
itemContextMenu.style.margin = 0;
|
||||||
|
itemContextMenu.style.top = itemContextMenu.style.left = "0px";
|
||||||
|
itemContextMenu.showModal();
|
||||||
|
|
||||||
|
let rect = itemContextMenu.getBoundingClientRect();
|
||||||
|
let top = event.clientY;
|
||||||
|
let left = event.clientX;
|
||||||
|
|
||||||
|
if (top + rect.height > window.innerHeight) {
|
||||||
|
top = window.innerHeight - rect.height;
|
||||||
|
}
|
||||||
|
if (left + rect.width > window.innerWidth) {
|
||||||
|
left = window.innerWidth - rect.width;
|
||||||
|
}
|
||||||
|
itemContextMenu.style.margin = 0;
|
||||||
|
itemContextMenu.style.top = `${top}px`;
|
||||||
|
itemContextMenu.style.left = `${left}px`;
|
||||||
|
|
||||||
|
let column = table.columns[columnIndex];
|
||||||
|
if (column.leftAligned && column.rightAligned) {
|
||||||
|
itemContextMenu.querySelector("#center").checked = true;
|
||||||
|
} else if (column.leftAligned) {
|
||||||
|
itemContextMenu.querySelector("#left").checked = true;
|
||||||
|
} else if (column.rightAligned) {
|
||||||
|
itemContextMenu.querySelector("#right").checked = true;
|
||||||
|
} else {
|
||||||
|
itemContextMenu.querySelector("#none").checked = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteRow.disabled = event.target.closest("th") !== null;
|
||||||
|
deleteColumn.disabled = table.columns.length < 2;
|
||||||
|
insertRowAbove.disabled = event.target.closest("th") !== null;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
document.querySelector("#copy-to-clipboard").addEventListener("click", function() {
|
||||||
|
output.select();
|
||||||
|
navigator.clipboard.writeText(output.value).then(
|
||||||
|
function() {
|
||||||
|
// success
|
||||||
|
},
|
||||||
|
function(reason) {
|
||||||
|
// TODO: make visible to user
|
||||||
|
console.error(`Failed to copy to clipboard: ${reason}`);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}, {passive: true});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
window.addEventListener("click", function(event) {
|
||||||
|
if (event.target.localName !== "dialog") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let rect = event.target.getBoundingClientRect();
|
||||||
|
if (rect.top <= event.clientY && rect.left <= event.clientX && rect.bottom >= event.clientY && rect.right >= event.clientX) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.target.close();
|
||||||
|
}, {passive: true});
|
||||||
|
|
||||||
|
reloadTable();
|
|
@ -0,0 +1,119 @@
|
||||||
|
/*
|
||||||
|
* str: string
|
||||||
|
*
|
||||||
|
* Returns a string
|
||||||
|
*/
|
||||||
|
function escapeItem(str) {
|
||||||
|
return str.replaceAll(/(\\*)\|/g, function(match, backslashes) {
|
||||||
|
if (backslashes.length % 2 === 0) {
|
||||||
|
backslashes += "\\";
|
||||||
|
}
|
||||||
|
return backslashes + "|";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* escapedColumns: array of objects
|
||||||
|
* escapedRow: array of string
|
||||||
|
*
|
||||||
|
* Returns a string
|
||||||
|
*/
|
||||||
|
function writeRow(escapedColumns, escapedRow) {
|
||||||
|
let res = "";
|
||||||
|
for (let i = 0; i < escapedColumns.length; i++) {
|
||||||
|
let escapedColumn = escapedColumns[i];
|
||||||
|
let escapedItem = escapedRow[i];
|
||||||
|
res += `| ${escapedItem}${" ".repeat(escapedColumn.maxLength - escapedItem.length)} `;
|
||||||
|
}
|
||||||
|
res += "|";
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* escapedColumns: array of objects
|
||||||
|
*
|
||||||
|
* Returns a string
|
||||||
|
*/
|
||||||
|
function writeHeader(escapedColumns) {
|
||||||
|
let headerRow = [];
|
||||||
|
let delimiterRow = [];
|
||||||
|
|
||||||
|
for (let escapedColumn of escapedColumns) {
|
||||||
|
headerRow.push(escapedColumn.text);
|
||||||
|
|
||||||
|
let delimiter = "";
|
||||||
|
if (escapedColumn.leftAligned) {
|
||||||
|
delimiter += ":";
|
||||||
|
}
|
||||||
|
delimiter += "-".repeat(escapedColumn.maxLength - escapedColumn.leftAligned - escapedColumn.rightAligned);
|
||||||
|
if (escapedColumn.rightAligned) {
|
||||||
|
delimiter += ":";
|
||||||
|
}
|
||||||
|
delimiterRow.push(delimiter);
|
||||||
|
}
|
||||||
|
|
||||||
|
return writeRow(escapedColumns, headerRow) + "\n" + writeRow(escapedColumns, delimiterRow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* column: object
|
||||||
|
* rows: array of array of strings
|
||||||
|
* headerIndex: integer
|
||||||
|
*
|
||||||
|
* Returns an integer
|
||||||
|
*/
|
||||||
|
function calculateColumnLength(column, rows, columnIndex) {
|
||||||
|
let length = Math.max(column.text.length, 3);
|
||||||
|
|
||||||
|
for (let row of rows) {
|
||||||
|
let item = row[columnIndex];
|
||||||
|
if (item.length > length) {
|
||||||
|
length = item.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* table: object
|
||||||
|
*
|
||||||
|
* Returns a string
|
||||||
|
*/
|
||||||
|
function serializeTable(table) {
|
||||||
|
let escapedColumns = [];
|
||||||
|
let escapedRows = [];
|
||||||
|
|
||||||
|
for (let column of table.columns) {
|
||||||
|
escapedColumns.push({
|
||||||
|
text: escapeItem(column.text),
|
||||||
|
leftAligned: column.leftAligned,
|
||||||
|
rightAligned: column.rightAligned
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let row of table.rows) {
|
||||||
|
let escapedRow = [];
|
||||||
|
for (let item of row) {
|
||||||
|
escapedRow.push(escapeItem(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
escapedRows.push(escapedRow);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < escapedColumns.length; i++) {
|
||||||
|
escapedColumns[i].maxLength = calculateColumnLength(escapedColumns[i], escapedRows, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = writeHeader(escapedColumns);
|
||||||
|
for (let escapedRow of escapedRows) {
|
||||||
|
res += "\n" + writeRow(escapedColumns, escapedRow);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// XXX: perhaps calculateColumnLength could be placed somewhere else?
|
||||||
|
export {serializeTable, calculateColumnLength};
|
|
@ -0,0 +1,68 @@
|
||||||
|
html:has(dialog[open]) {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
/* prevent table from wrapping when contents are too long */
|
||||||
|
table {
|
||||||
|
min-width: max-content;
|
||||||
|
}
|
||||||
|
/* prevent textboxes from soft-wrapping */
|
||||||
|
textarea {
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
td {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
textarea {
|
||||||
|
resize: none;
|
||||||
|
}
|
||||||
|
.left {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.left.right {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#load-dialog h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
#load-dialog #error {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
#load-dialog textarea {
|
||||||
|
width: 80vw;
|
||||||
|
height: 75vh;
|
||||||
|
}
|
||||||
|
#load-dialog .buttons {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
#load-dialog form {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
#item-context-menu br {
|
||||||
|
display: block;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
#item-context-menu button {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-container {
|
||||||
|
overflow-x: scroll;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
}
|
||||||
|
th textarea {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
#output {
|
||||||
|
width: 100%;
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
import {deserializeTable} from "./deserialize_table.mjs";
|
||||||
|
import {serializeTable, calculateColumnLength} from "./serialize_table.mjs";
|
||||||
|
|
||||||
|
export {deserializeTable, serializeTable, calculateColumnLength};
|
Loading…
Reference in New Issue