mdtabeditor/main.mjs

363 lines
11 KiB
JavaScript

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();