table = async function*(dataPromise, options = {}) {
yield loader;
const dataO = await Promise.resolve(dataPromise);
let filter = () => true;
const data = Array.isArray(dataO)
? dataO
: Object.entries(dataO).map(([key, v]) => Object.assign({ key }, v));
function mdTable(data) {
console.log(data);
let lines = [];
for (let [key, value] of Object.entries(data)) {
if (typeof value === 'object') {
lines.push(`${key}|${mdTable(value).outerHTML}`);
} else {
lines.push(`${key}|${value}`);
}
}
return md`
[]() |
---|---
${lines.join('\n')}
`;
}
let {
humanize,
nully,
limit,
columns,
enableFilter,
enableCSVDownload
} = Object.assign(
{
humanize: defaultHumanize,
nully: () => '<span style="opacity: 0.5">-</span>',
limit: 250,
columns: Object.keys(data[0]),
enableFilter: data.length > 50,
enableCSVDownload: true
},
options || {}
);
columns = columns.map(column => {
let key;
if (typeof column == 'string') {
key = column;
column = { key };
} else {
key = column.key;
}
const decimalNumber = data.reduce((memo, d) => {
if (typeof d[key] === 'number') {
let frac = (d[key] || '').toString().split('.')[1];
if (frac) return Math.max(memo, frac.length);
}
return memo;
}, 0);
return Object.assign(
{
name: humanize(key),
type: typeof data[0][key],
render: val => {
if (decimalNumber && typeof val === 'number') {
return val.toFixed(decimalNumber);
} else if (val == null || val == undefined) {
return nully(val);
} else if (typeof val === 'object') {
return mdTable(val).outerHTML;
// return JSON.stringify(val);
}
return val;
}
},
column
);
});
const renderTable = () => {
return `${data
.filter(filter)
.slice(0, limit)
.map(
(datum, idx) =>
`<tr bgcolor="${idx % 2 ? '#f8f8ff' : ''}">
${columns
.map(c => {
let val = datum[c.key];
return `<td class=${c.type}>${c.render(val)}</td>`;
})
.join('')}
</tr>`
)
.join('')}`;
};
const output = html`<div class="fancy-table">
<style>
.fancy-table table {
min-width: 100%;
}
.fancy-table td { vertical-align: top; }
.fancy-table th:not(:first-child):not(:last-child), .fancy-table td:not(:first-child):not(:last-child) { padding: 0 10px; }
.fancy-table th { vertical-align: bottom; }
.fancy-table th.number, .fancy-table td.number {
text-align: right;
font-feature-settings: 'tnum';
}
.fancy-table tbody tr td.with-decimal {
padding-left:10px !important;
padding-right:0px !important;
}
.fancy-table tbody tr td.decimal {
padding-left:0 !important;
padding-right:10px !important;
}
.fancy-table details {
position: absolute;
bottom: 0;
border-radius: 15px;
background: transparent;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
width: 30px;
}
.fancy-table details[open] {
width: 360px;
}
.fancy-table summary {
list-style-type: none;
display: inline-block;
line-height: 28px;
width: 30px;
height: 30px;
text-align: center;
}
.fancy-table details summary::-webkit-details-marker {
display: none;
}
.fancy-table details input {
position: absolute;
top: 3px;
left: 25px;
}
.fancy-table a[download] {
position: absolute;
bottom: 0;
right: 5px;
border-radius: 15px;
background: transparent;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
width: 30px;
height: 30px;
text-align: center;
line-height: 28px;
}
.fancy-table a[download] button {
border: none;
background: transparent;
}
</style>
<div>
<div style="max-height: 400px; overflow: auto; position: relative;">
<table>
<thead>
<tr>
${columns
.map(
c => `<th class="${c.type}" data-key="${c.key}">${c.name}</th>`
)
.join('')}
</tr>
</thead>
<tbody>
${renderTable(data)}
</tbody>
</table>${
data.filter(filter).length > limit
? `<p><em>This table has been truncated from ${data.length} rows to ${limit}</em></p>`
: ''
}
</div>
${
enableFilter
? '<details><summary>🔎</summary><input type="search" placeholder="Type in a filter query" /></details>'
: ''
}
</div>
`;
output.value = dataO;
yield output;
output.querySelectorAll('th').forEach(th =>
th.addEventListener('click', () => {
const key = th.dataset.key;
const order = th.dataset.order === 'ascending' ? -1 : 1;
data.sort((a, b) =>
a[key] > b[key] ? order : b[key] > a[key] ? -order : 0
);
output.querySelector('tbody').innerHTML = renderTable(data);
th.dataset.order = order === -1 ? 'descending' : 'ascending';
})
);
if (enableFilter) {
const search = output.querySelector('input[type=search]');
search.addEventListener('input', e => {
filter = parseQuery(search.value, columns);
output.querySelector('tbody').innerHTML = renderTable(data);
});
}
if (enableCSVDownload) {
output.appendChild(
DOM.download(
new Blob(
[
CSV(data, {
fields: columns.map(c => ({ label: c.name, value: c.key }))
})
],
{ type: "application/json" }
),
'data.csv',
'📥'
)
);
}
}