/*
* mdtable.js - OSS tool for HTML5 Tables
* This Class creates a HTML Tag (md-table) which lets you define a Markdown Table INSIDE
* of your exsisting HTML, you do NOT need any external libraries as this is a vanilla javascript class.
* Example HTML at EOF!
*
* written by rattatwinko@26/02/26 (license: MIT)
*/
class MdTable extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: "open" });
const style = document.createElement("style");
style.textContent = MdTable.styles;
this._wrapper = document.createElement("div");
shadow.append(style, this._wrapper);
}
static get observedAttributes() {
return ["caption"];
}
connectedCallback() {
this._render();
}
attributeChangedCallback() {
this._render();
}
_render() {
const raw = this.textContent.trim();
if (!raw) return;
const parsed = this._parseMarkdown(raw);
if (!parsed) return;
const { headers, rows, aligns } = parsed;
const table = document.createElement("table");
const captionText = this.getAttribute("caption");
if (captionText) {
const caption = document.createElement("caption");
caption.textContent = captionText;
table.appendChild(caption);
}
const thead = document.createElement("thead");
const tbody = document.createElement("tbody");
const trHead = document.createElement("tr");
headers.forEach((text, i) => {
const th = document.createElement("th");
th.textContent = text;
th.dataset.align = aligns[i];
if (this.hasAttribute("sortable")) {
th.classList.add("sortable");
th.addEventListener("click", () => {
this._sortTable(tbody, i);
});
}
trHead.appendChild(th);
});
thead.appendChild(trHead);
// create table elements based on how many rows there are
rows.forEach(row => {
const tr = document.createElement("tr");
row.forEach((text, i) => {
const td = document.createElement("td");
td.textContent = text;
td.dataset.align = aligns[i];
tr.appendChild(td);
});
tbody.appendChild(tr);
});
table.append(thead, tbody);
this._wrapper.replaceChildren(table);
}
// parse table
_parseMarkdown(md) {
const lines = md
.split("\n")
.map(l => l.trim())
.filter(Boolean);
if (lines.length < 2) return null;
const headers = lines[0]
.split("|")
.map(c => c.trim())
.filter(Boolean);
const alignRow = lines[1]
.split("|")
.map(c => c.trim())
.filter(Boolean);
const aligns = alignRow.map(a => {
if (a.startsWith(":") && a.endsWith(":")) return "center";
if (a.endsWith(":")) return "right";
if (a.startsWith(":")) return "left";
return "left";
});
const rows = lines.slice(2).map(line =>
line.split("|")
.map(c => c.trim())
.filter(Boolean)
);
return { headers, rows, aligns };
}
_sortTable(tbody, columnIndex) {
const rows = Array.from(tbody.querySelectorAll("tr"));
const isNumeric = rows.every(row =>
!isNaN(parseFloat(row.children[columnIndex].textContent))
);
const asc = this._lastSortCol !== columnIndex || !this._lastAsc;
this._lastSortCol = columnIndex;
this._lastAsc = asc;
rows.sort((a, b) => {
let A = a.children[columnIndex].textContent;
let B = b.children[columnIndex].textContent;
if (isNumeric) {
A = parseFloat(A);
B = parseFloat(B);
}
return asc ? (A > B ? 1 : -1) : (A < B ? 1 : -1);
});
rows.forEach(r => tbody.appendChild(r));
}
static styles = `
:host {
display: block;
font-family: sans-serif;
--border-color: #ccc;
--header-bg: #f5f5f5;
}
table {
width: 100%;
border-collapse: collapse;
}
caption {
caption-side: top;
padding: 6px 0;
font-weight: bold;
text-align: left;
}
th, td {
border: 1px solid var(--border-color);
padding: 6px 10px;
}
th {
background: var(--header-bg);
font-weight: 600;
}
th[data-align="right"],
td[data-align="right"] {
text-align: right;
}
th[data-align="center"],
td[data-align="center"] {
text-align: center;
}
th.sortable {
cursor: pointer;
}
`;
}
// define