Repository navigation
Expand file tree
/
Copy pathviewer.htm
More file actions
699 lines (664 loc) · 54.1 KB
/
Copy pathviewer.htm
File metadata and controls
699 lines (664 loc) · 54.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>csvfab</title>
<!-- The session token stays in this page, never in a script file: a hostile page could
include a script from 127.0.0.1 and read its globals, but cannot read this HTML. -->
<script>window.CSVFAB_TOKEN = '__CSVE_TOKEN__';
/* The files the launcher queued (csvfab x.csv) are asked for from here, first thing in the
page, rather than once app.js has run (~40 ms later): the long poll's first answer, and the
bytes of the first file, which the server names here (CSVFAB_QUEUED, without taking it off
the queue). drain() and addTabs() (04-, 02-…) take both. Not a workbook, JSON or database:
those are converted first (27-…). */
window.CSVFAB_QUEUED = null;
(function (h, q) {
if (!window.fetch) return;
window.CSVFAB_EARLY = fetch('/api/pending?wait=0', { headers: h }).then(function (r) { return r.ok ? r.json() : null; }).catch(function () { return null; });
if (q && q.stat && !/\.(xlsx|xlsm|json|jsonl|ndjson|sqlite|sqlite3|db|db3)$/i.test(q.path))
window.CSVFAB_FILE = { path: q.path, req: fetch('/api/file?path=' + encodeURIComponent(q.path), { headers: h }) };
})({ 'X-Csv-Token': window.CSVFAB_TOKEN }, window.CSVFAB_QUEUED);</script>
<!-- The theme is applied before the first paint, so the other one never flashes — and
its background with it, set on <html> right here: the window's first frames, before
the stylesheets arrive, are the theme's colour rather than the browser's default.
(The colours repeat each palette's --bg; THEME_SWATCH in 01-core.js holds them too.) -->
<script>try { var th = localStorage.getItem('csvfab-theme'); if (th && th !== 'dark') document.documentElement.dataset.theme = th;
if (localStorage.getItem('csvfab-type-colors') === '0') document.documentElement.classList.add('no-kcol');
if (localStorage.getItem('csvfab-sparklines') === '0') document.documentElement.classList.add('no-spk');
if (localStorage.getItem('csvfab-filter-row') === '0') document.documentElement.classList.add('no-filt');
if (localStorage.getItem('csvfab-totals') === '0') document.documentElement.classList.add('no-tot');
var mf = localStorage.getItem('csvfab-mono'); if (mf) document.documentElement.style.setProperty('--mono-pick', mf === 'monospace' ? 'monospace' : '"' + mf.replace(/["\\]/g, '') + '"'); } catch (e) {}
(function (h, t) { var bg = { dark: '#0d1117', tokyonight: '#15161e', catppuccin: '#11111b', vscode: '#1a1b26', light: '#ffffff', latte: '#eff1f5' }[t] || '#0d1117';
h.style.background = bg; h.style.colorScheme = t === 'light' || t === 'latte' ? 'light' : 'dark'; })(document.documentElement, document.documentElement.dataset.theme || 'dark');</script>
<link rel="icon" href="icons/csvfab.svg" type="image/svg+xml">
<script src="papaparse.min.js"></script>
<!-- Vendored so the desktop app works offline; the CDN is only a safety net. -->
<script>window.Papa || document.write('<scr'+'ipt src="https://cdnjs.cloudflare.com/ajax/libs/PapaParse/5.4.1/papaparse.min.js"><\/scr'+'ipt>');</script>
<link rel="stylesheet" href="ui/themes.css">
<link rel="stylesheet" href="ui/app.css">
</head>
<body>
<div id="drop-overlay">Drop your CSV files here</div>
<div id="modal-bg" onclick="closeAllModals()"></div>
<div id="modal-cols" class="modal-box">
<h4 style="margin-top:0; margin-bottom: 15px;">Visible Columns</h4>
<div id="cols-list"></div>
</div>
<div id="modal-save" class="modal-box">
<h4>Save</h4>
<label class="fld-label" for="save-name">File name</label>
<input type="text" id="save-name" class="bs-input" spellcheck="false" autocomplete="off">
<span class="fld-label">Format</span>
<div id="save-fmt"></div>
<div id="save-enc-box">
<span class="fld-label" style="margin-top: 12px;">Encoding</span>
<div id="save-enc"></div>
</div>
<div id="save-note"></div>
<div class="modal-actions">
<button class="btn btn-outline" onclick="closeAllModals()">Cancel</button>
<button class="btn" onclick="submitSaveModal()">Save</button>
</div>
</div>
<div id="modal-dedupe" class="modal-box">
<h4>Duplicates</h4>
<span class="fld-label">Rows are duplicates when <b>any</b> key matches; within a key, <b>every</b> field must match once cleaned. A key whose fields are not all filled is ignored for that row. Groups chain: rows sharing an e-mail with one and a phone with another are one group. No field at all: the whole row is compared.</span>
<div id="dedupe-keys"></div>
<div class="dk-tools">
<button class="btn btn-outline" onclick="dedupeEdit(0, -1, 'addKey')">+ Key (or)</button>
<button class="btn btn-outline" onclick="dedupeSuggest()" title="Name, e-mail, phone, from the columns found">Suggest keys</button>
</div>
<span class="fld-label" style="margin-top: 10px;">Delete keeps, in each group</span>
<label class="col-label"><input type="radio" name="dedupe-keep" value="first" checked onchange="updateDedupeNote()"> Its first row</label>
<label class="col-label"><input type="radio" name="dedupe-keep" value="last" onchange="updateDedupeNote()"> Its last row</label>
<div id="dedupe-note"></div>
<div class="modal-actions">
<button class="btn btn-outline" onclick="closeAllModals()">Cancel</button>
<button class="btn btn-outline" id="dedupe-show" onclick="markDuplicates()" title="Group the duplicates, differences in red; nothing is deleted">Mark them</button>
<button class="btn" id="dedupe-go" onclick="applyDedupe()">Delete duplicates</button>
</div>
</div>
<div id="modal-font" class="modal-box">
<h4>Monospace font</h4>
<span class="fld-label">The font of the cells, formulas and codes, kept for the next start. The fonts installed come first, then free ones to install in a click.</span>
<input type="text" id="font-q" class="bs-input" placeholder="Search a font…" spellcheck="false" autocomplete="off" oninput="openFontDialog.q = this.value; renderFontDialog()">
<div id="font-list"></div>
<div class="modal-actions" style="margin-top: 14px;">
<button class="btn btn-outline" id="font-more" style="display: none; margin-right: auto;" onclick="listLocalFonts()" title="Asks your permission">List installed fonts</button>
<button class="btn" onclick="closeAllModals()">Done</button>
</div>
</div>
<div id="modal-keys" class="modal-box">
<h4>Keyboard shortcuts</h4>
<div id="keys-list"></div>
<div class="modal-actions" style="margin-top: 14px;"><button class="btn" onclick="closeAllModals()">Close</button></div>
</div>
<div id="modal-about" class="modal-box">
<img class="ab-logo" src="icons/csvfab.svg" alt="">
<h1>csvfab</h1>
<div class="ab-ver" id="ab-ver"></div>
<p class="ab-what">A CSV editor that rewrites the file in place.<br>Open, filter, clean, save — nothing leaves your machine.</p>
<button class="btn btn-outline ab-news" onclick="openWhatsNew()">What's new</button>
<div class="ab-links">
<a href="https://github.com/fabiochelly/csvfab" target="_blank" rel="noopener noreferrer">Website & source</a>
<a href="https://github.com/fabiochelly/csvfab/releases" target="_blank" rel="noopener noreferrer">Release notes</a>
<a href="https://github.com/fabiochelly/csvfab/issues" target="_blank" rel="noopener noreferrer">Report a problem</a>
</div>
<div class="ab-env" id="ab-env"></div>
<div class="ab-legal">© 2026 · MIT license</div>
<div class="ab-author">Fabio Chelly · <a href="mailto:fabio@chelly.net">fabio@chelly.net</a></div>
<div class="modal-actions" style="margin-top: 16px; justify-content: center;"><button class="btn" onclick="closeAllModals()">Close</button></div>
</div>
<div id="modal-review" class="modal-box">
<h4>Pending changes</h4>
<div class="stat-line" id="rv-sum" style="margin-top: 0;"></div>
<div class="pv-wrap rv-wrap"><table class="pv rv" id="rv-list"></table></div>
<div class="modal-actions" style="margin-top: 16px;">
<button class="btn btn-outline" onclick="closeAllModals()">Close</button>
<button class="btn" id="rv-mark" onclick="reviewMark()" title="Tint the changed cells, show only those rows">Show changed rows</button>
</div>
</div>
<div id="modal-compare" class="modal-box">
<h4>Compare with another tab</h4>
<div class="row2 lk-keys">
<div><span class="fld-label">Key in this tab</span><select id="cmp-key" class="bs-input bs-select" onchange="cmp.userKey = true; compareRefresh()"></select></div>
<div><span class="fld-label">Compare with the tab</span><select id="cmp-src" class="bs-input bs-select" onchange="compareSource()"></select></div>
<div><span class="fld-label">…on its column</span><select id="cmp-skey" class="bs-input bs-select" onchange="compareRefresh()"></select></div>
</div>
<div class="opts">
<label class="col-label"><input type="radio" name="cmp-match" value="exact" onchange="compareRefresh()"> Exact</label>
<label class="col-label"><input type="radio" name="cmp-match" value="loose" checked onchange="compareRefresh()"> Ignoring case and surrounding spaces</label>
<label class="col-label" title="No accents, case or symbols"><input type="radio" name="cmp-match" value="slug" onchange="compareRefresh()"> Slugified</label>
</div>
<div class="stat-line" id="cmp-stats"></div>
<div class="pv-wrap"><table class="pv rv" id="cmp-pv"></table></div>
<div class="modal-actions" style="margin-top: 16px;">
<button class="btn btn-outline" onclick="cmp = null; closeAllModals()">Cancel</button>
<button class="btn btn-outline" id="cmp-add" onclick="compareAppend()" title="Add the rows only the other tab has">Append the rows only there</button>
<button class="btn btn-outline" id="cmp-col" onclick="compareStatusColumn()" title="changed / same / only here">Add a status column</button>
<button class="btn" id="cmp-go" onclick="compareMark()" title="Tint the differing cells, show only those rows">Mark the differences</button>
</div>
</div>
<div id="modal-clean" class="modal-box">
<h4>Clean up</h4>
<span class="fld-label">Every row, filtered or not, and the header line.</span>
<div id="clean-opts"></div>
<div id="clean-note"></div>
<div class="modal-actions">
<button class="btn btn-outline" onclick="closeAllModals()">Cancel</button>
<button class="btn" id="clean-go" onclick="applyClean()">Clean</button>
</div>
</div>
<div id="modal-noise" class="modal-box">
<h4>Remove noise words</h4>
<span class="fld-label">Words or phrases to remove — one per line, or separated by commas</span>
<textarea id="nz-words" class="bs-input" rows="4" spellcheck="false" placeholder="e.g. SARL, Monsieur, et Fils" oninput="noiseRefreshSoon()" autocomplete="off"></textarea>
<div class="chips" id="nz-chips"></div>
<span class="fld-label">From</span>
<select id="nz-col" class="bs-input bs-select" style="width: 100%;" onchange="noiseRefresh()"></select>
<div class="opts">
<label class="col-label"><input type="checkbox" id="nz-whole" checked onchange="noiseRefresh()"> Whole words only — "SA" leaves "SARLAT" alone</label>
<label class="col-label"><input type="checkbox" id="nz-case" checked onchange="noiseRefresh()"> Ignore case</label>
<label class="col-label"><input type="checkbox" id="nz-acc" checked onchange="noiseRefresh()"> Ignore accents — "societe" removes "Société"</label>
<label class="col-label" title="Doubled spaces, a comma or dash left dangling, empty brackets — only where a word was removed"><input type="checkbox" id="nz-tidy" checked onchange="noiseRefresh()"> Tidy the gaps left behind</label>
</div>
<div class="stat-line" id="nz-stats"></div>
<div class="pv-wrap"><table class="pv" id="nz-pv"></table></div>
<div class="modal-actions" style="margin-top: 16px;">
<button class="btn btn-outline" onclick="closeAllModals()">Cancel</button>
<button class="btn" id="nz-go" onclick="applyNoise()">Remove</button>
</div>
</div>
<div id="modal-bulk" class="modal-box">
<h4>Edit filtered rows</h4>
<label class="fld-label" for="bulk-col">Column</label>
<select id="bulk-col" class="bs-input bs-select" onchange="updateBulkNote()"></select>
<span class="fld-label" style="margin-top: 12px;">Change</span>
<label class="col-label"><input type="radio" name="bulk-op" value="set" checked onchange="updateBulkNote()"> Set to <input type="text" id="bulk-val" class="bs-input bulk-inline" placeholder="value" oninput="document.querySelector('input[name=bulk-op][value=set]').checked = true; updateBulkNote()" autocomplete="off"></label>
<label class="col-label"><input type="radio" name="bulk-op" value="clear" onchange="updateBulkNote()"> Clear</label>
<label class="col-label"><input type="radio" name="bulk-op" value="trim" onchange="updateBulkNote()"> Trim, and collapse repeated spaces</label>
<label class="col-label"><input type="radio" name="bulk-op" value="upper" onchange="updateBulkNote()"> UPPER CASE</label>
<label class="col-label"><input type="radio" name="bulk-op" value="lower" onchange="updateBulkNote()"> lower case</label>
<label class="col-label"><input type="radio" name="bulk-op" value="title" onchange="updateBulkNote()"> Capitalized Words</label>
<div id="bulk-note"></div>
<div class="modal-actions">
<button class="btn btn-outline" onclick="closeAllModals()">Cancel</button>
<button class="btn" id="bulk-go" onclick="applyBulk()">Apply</button>
</div>
</div>
<div id="modal-split" class="modal-box">
<h4>Split a column</h4>
<span class="fld-label">Column</span>
<select id="split-col" class="bs-input bs-select" style="width: 100%;" onchange="splitRefresh(true)"></select>
<span class="fld-label">Separator — a character, a string, or a regular expression</span>
<div class="row2">
<input type="text" id="split-sep" class="bs-input" spellcheck="false" placeholder="e.g. ; or - or \t for a tab" oninput="splitRefresh(true)" autocomplete="off">
<label class="col-label" style="margin: 0;"><input type="checkbox" id="split-re" onchange="splitRefresh(true)"> Regex</label>
</div>
<div class="chips" id="split-chips"></div>
<div class="opts">
<label class="col-label"><input type="checkbox" id="split-collapse" checked onchange="splitRefresh(true)"> Consecutive separators count as one</label>
<label class="col-label"><input type="checkbox" id="split-trim" checked onchange="splitRefresh(false)"> Trim the parts</label>
<label class="col-label"><input type="checkbox" id="split-keep" onchange="splitRefresh(false)"> Keep the original column</label>
</div>
<div class="stat-line" id="split-stats"></div>
<div class="row2" style="margin-top: 10px;">
<span style="font-size: 13px;">Columns to create</span>
<input type="number" id="split-n" class="bs-input" min="2" max="50" oninput="splitState.userN = true; splitRefresh(false)">
<span class="stat-line" id="split-nnote" style="margin: 0;"></span>
</div>
<div class="pv-wrap"><table class="pv" id="split-pv"></table></div>
<div class="modal-actions" style="margin-top: 16px;">
<button class="btn btn-outline" onclick="closeAllModals()">Cancel</button>
<button class="btn" id="split-go" onclick="applySplit()">Split</button>
</div>
</div>
<div id="modal-merge" class="modal-box">
<h4>Merge columns</h4>
<span class="fld-label">Columns — tick them in the order they are joined</span>
<div id="merge-cols"></div>
<span class="fld-label">Separator</span>
<input type="text" id="merge-sep" class="bs-input" style="width: 100%;" spellcheck="false" value=" " oninput="mergeRefresh()" autocomplete="off">
<div class="chips" id="merge-chips"></div>
<div class="opts">
<label class="col-label"><input type="checkbox" id="merge-skip" checked onchange="mergeRefresh()"> Skip empty values</label>
<label class="col-label"><input type="checkbox" id="merge-keep" checked onchange="mergeRefresh()"> Keep the original columns</label>
</div>
<span class="fld-label">Name of the new column</span>
<input type="text" id="merge-name" class="bs-input" style="width: 100%;" spellcheck="false" oninput="mergeState.userName = true; mergeRefresh()" autocomplete="off">
<div class="pv-wrap"><table class="pv" id="merge-pv"></table></div>
<div class="modal-actions" style="margin-top: 16px;">
<button class="btn btn-outline" onclick="closeAllModals()">Cancel</button>
<button class="btn" id="merge-go" onclick="applyMerge()">Merge</button>
</div>
</div>
<div id="modal-lookup" class="modal-box">
<h4>Look up values in another tab</h4>
<div class="row2 lk-keys">
<div><span class="fld-label">Key in this tab</span><select id="lk-key" class="bs-input bs-select" onchange="lookup.userKey = true; lookupRefresh()"></select></div>
<div><span class="fld-label">Look up in the tab</span><select id="lk-src" class="bs-input bs-select" onchange="lookupSource()"></select></div>
<div><span class="fld-label">…on its column</span><select id="lk-skey" class="bs-input bs-select" onchange="lookupRefresh()"></select></div>
</div>
<div class="opts">
<label class="col-label"><input type="radio" name="lk-match" value="exact" onchange="lookupRefresh()"> Exact keys</label>
<label class="col-label"><input type="radio" name="lk-match" value="loose" checked onchange="lookupRefresh()"> Ignoring case and surrounding spaces</label>
<label class="col-label" title="No accents, case or symbols"><input type="radio" name="lk-match" value="slug" onchange="lookupRefresh()"> Slugified</label>
</div>
<span class="fld-label">Columns to bring over — added right after the key, in the order ticked</span>
<div id="lk-cols"></div>
<div class="stat-line" id="lk-stats"></div>
<div class="pv-wrap"><table class="pv" id="lk-pv"></table></div>
<div class="modal-actions" style="margin-top: 16px;">
<button class="btn btn-outline" onclick="lookup = null; closeAllModals()">Cancel</button>
<button class="btn" id="lk-go" onclick="applyLookup()">Add the columns</button>
</div>
</div>
<div id="modal-convert" class="modal-box">
<h4>Convert formats</h4>
<span class="fld-label">Column</span>
<select id="cv-col" class="bs-input bs-select" style="width: 100%;" onchange="convertColChanged()"></select>
<span class="fld-label">Its values are</span>
<div class="opts" style="margin-top: 0;">
<label class="col-label"><input type="radio" name="cv-kind" value="date" checked onchange="convertRefresh()"> Dates</label>
<label class="col-label"><input type="radio" name="cv-kind" value="num" onchange="convertRefresh()"> Numbers</label>
<label class="col-label"><input type="radio" name="cv-kind" value="phone" onchange="convertRefresh()"> Phone numbers</label>
</div>
<div class="cv-grid" id="cv-o-date">
<span>Read as</span>
<select id="cv-dorder" class="bs-input bs-select" onchange="convertRefresh()"><option value="dmy">day first — 15/06/2024</option><option value="mdy">month first — 06/15/2024</option></select>
<span></span><span class="cv-note" id="cv-dnote"></span>
<span>Write as</span>
<select id="cv-dout" class="bs-input bs-select" onchange="convertRefresh()">
<option value="iso">2024-06-15 (ISO)</option><option value="dmy">15/06/2024</option><option value="mdy">06/15/2024</option>
<option value="dmyd">15.06.2024</option><option value="dmyh">15-06-2024</option>
</select>
</div>
<div class="cv-grid" id="cv-o-num">
<span>Decimal separator</span>
<select id="cv-ndec" class="bs-input bs-select" onchange="convertRefresh()"><option value=",">comma — 1 234,50</option><option value=".">point — 1,234.50</option></select>
<span>Write as</span>
<select id="cv-nout" class="bs-input bs-select" onchange="convertRefresh()">
<option value="point">1234.5</option><option value="comma">1234,5</option><option value="fr">1 234,5</option><option value="en">1,234.5</option>
</select>
<span>Decimals</span>
<select id="cv-nfix" class="bs-input bs-select" onchange="convertRefresh()">
<option value="">as they are</option><option value="0">0</option><option value="1">1</option><option value="2">2</option><option value="3">3</option><option value="4">4</option>
</select>
</div>
<div class="cv-grid" id="cv-o-phone">
<span>Country code</span>
<input type="text" id="cv-cc" class="bs-input" value="33" spellcheck="false" oninput="convertRefresh()" title="For numbers starting with 0" autocomplete="off">
<span>Write as</span>
<select id="cv-pout" class="bs-input bs-select" onchange="convertRefresh()">
<option value="e164">+33612345678 (E.164)</option><option value="intl">+33 6 12 34 56 78</option><option value="nat">06 12 34 56 78</option>
</select>
</div>
<div class="stat-line" id="cv-stats"></div>
<div class="pv-wrap"><table class="pv" id="cv-pv"></table></div>
<div class="modal-actions" style="margin-top: 16px;">
<button class="btn btn-outline" onclick="closeAllModals()">Cancel</button>
<button class="btn btn-outline" id="cv-show" onclick="showUnrecognised()">Show unrecognised</button>
<button class="btn" id="cv-go" onclick="applyConvert()">Convert</button>
</div>
</div>
<div id="modal-formula" class="modal-box">
<h4>Computed column</h4>
<div class="fx-lbl"><span class="fld-label">Formula — <code>{Column}</code> is the row's cell, the rest is JavaScript</span><button class="fx-btn fx-big" onclick="openFxPicker('formula')" title="Every function, with examples run on your data"><span class="fx-glyph" aria-hidden="true"></span>Functions</button></div>
<textarea id="fx-expr" class="bs-input" rows="3" spellcheck="false" placeholder='e.g. {firstName} + " " + {lastName}' oninput="fxRefresh()"></textarea>
<div class="chips" id="fx-cols"></div>
<div class="fx-note">Numbers are written with a decimal comma in a <code>;</code> file, dates as yyyy-mm-dd.</div>
<div class="row2" style="margin-top: 12px;">
<div style="flex: 1;"><span class="fld-label">Name</span><input type="text" id="fx-name" class="bs-input" style="width: 100%;" value="Computed" spellcheck="false" oninput="fxRefresh()" autocomplete="off"></div>
<div style="flex: 1;"><span class="fld-label">Write to</span><select id="fx-dest" class="bs-input bs-select" style="width: 100%;" onchange="fxRefresh()"></select></div>
</div>
<div class="stat-line" id="fx-stats"></div>
<div class="pv-wrap"><table class="pv" id="fx-pv"></table></div>
<div class="modal-actions" style="margin-top: 16px;">
<button class="btn btn-outline" onclick="closeAllModals()">Cancel</button>
<button class="btn" id="fx-go" onclick="applyFormula()">Apply</button>
</div>
</div>
<div id="modal-profile" class="modal-box">
<h4>File profile</h4>
<div class="stat-line" id="pf-sum" style="margin-top: 0;"></div>
<div class="pv-wrap pf-wrap"><table class="pv rv pf" id="pf-list"></table></div>
<div class="stat-line" id="pf-note"></div>
<div class="modal-actions" style="margin-top: 16px;">
<button class="btn btn-outline" onclick="copyProfile()">Copy as text</button>
<button class="btn" onclick="closeAllModals()">Close</button>
</div>
</div>
<div id="modal-schema" class="modal-box">
<h4 id="sc-title">Schema</h4>
<div class="stat-line" id="sc-sum" style="margin-top: 0;"></div>
<div class="pv-wrap pf-wrap"><table class="pv rv sc" id="sc-list"></table></div>
<div class="stat-line" id="sc-note"></div>
<div class="modal-actions" id="sc-actions" style="margin-top: 16px;"></div>
</div>
<div id="modal-anon" class="modal-box">
<h4>Anonymise</h4>
<span class="fld-label">A treatment per column, guessed from its title and values — change it where the guess is wrong. The same value always gives the same replacement, so duplicates stay duplicates and files still join.</span>
<div class="pv-wrap an-wrap"><table class="pv an" id="an-list"></table></div>
<div class="row2" style="margin-top: 8px;">
<button class="btn btn-outline" onclick="anonGuessAll()">Guess again</button>
<button class="btn btn-outline" onclick="anonAllKeep()" title="Every column kept as is">Keep all</button>
<label class="col-label" style="margin: 0 0 0 auto;"><input type="checkbox" id="an-shown" onchange="renderAnon()"> Only the rows shown</label>
</div>
<div class="stat-line" id="an-stats"></div>
<div class="modal-actions" style="margin-top: 16px;">
<button class="btn btn-outline" onclick="closeAllModals()">Cancel</button>
<button class="btn" id="an-go" onclick="applyAnon()">Anonymise</button>
</div>
</div>
<div id="modal-group" class="modal-box">
<h4>Group by</h4>
<span class="fld-label">Group the rows shown by — tick the columns in order</span>
<div id="gb-cols"></div>
<div class="opts">
<label class="col-label"><input type="radio" name="gb-match" value="exact" onchange="groupRefresh(true)"> Exact values</label>
<label class="col-label"><input type="radio" name="gb-match" value="loose" checked onchange="groupRefresh(true)"> Ignoring case and surrounding spaces</label>
<label class="col-label" title="No accents, case or symbols"><input type="radio" name="gb-match" value="slug" onchange="groupRefresh(true)"> Slugified</label>
</div>
<span class="fld-label">Per group: the row count, and for the columns ticked below</span>
<div class="pv-wrap gb-wrap"><table class="pv gb" id="gb-aggs"></table></div>
<div class="stat-line" id="gb-stats"></div>
<div class="pv-wrap"><table class="pv" id="gb-pv"></table></div>
<div class="modal-actions" style="margin-top: 16px;">
<button class="btn btn-outline" onclick="gb = null; closeAllModals()">Cancel</button>
<button class="btn" id="gb-go" onclick="applyGroupBy()">Open the result in a new tab</button>
</div>
</div>
<div id="row-card">
<div class="rc-head">
<b id="rc-title">Row card</b>
<button class="btn btn-outline" id="rc-prev" onclick="rowCardMove(-1)" title="Previous row" aria-label="Previous row">▲</button>
<button class="btn btn-outline" id="rc-next" onclick="rowCardMove(1)" title="Next row" aria-label="Next row">▼</button>
<button class="btn btn-outline" id="rc-rawbtn" onclick="rowCardRaw()" title="The line as in the file, invisible characters shown">Raw</button>
<button class="btn btn-outline" id="rc-diagbtn" onclick="rowCardDiag()" title="Why does this row look wrong? Its bytes, examined">Why?</button>
<span class="rc-x" onclick="toggleRowCard(false)" title="Close (Ctrl+I)">×</span>
</div>
<div class="rc-sub" id="rc-sub"></div>
<div id="rc-diag"></div>
<div id="rc-raw"></div>
<input type="text" class="bs-input rc-search" placeholder="Find a field…" spellcheck="false" oninput="rowCard.q = this.value; rowCardFields()" onkeydown="event.stopPropagation()" autocomplete="off">
<div id="rc-fields"></div>
</div>
<div id="col-panel"></div>
<div id="toasts" aria-live="polite"></div>
<div id="cmdk" onmousedown="if (event.target === this) closePalette()">
<div class="cmdk-box" role="dialog" aria-label="Command palette">
<div class="cmdk-in-wrap">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.5" y2="16.5"/></svg>
<input id="cmdk-in" type="text" spellcheck="false" autocomplete="off" placeholder="Type a command, a column, a tab, a recent file…">
</div>
<div id="cmdk-list"></div>
<div class="cmdk-foot"><span><kbd>↑</kbd><kbd>↓</kbd> navigate</span><span><kbd>Enter</kbd> run</span><span><kbd>Esc</kbd> close</span></div>
</div>
</div>
<div id="chrome">
<div id="tabbar">
<div id="tabs"></div>
<div id="tab-add" onclick="openFiles()">+</div>
<div id="ram-info">
<span id="ram-txt"></span>
<span>Keep in RAM:</span>
<select id="max-ram" title="Files kept parsed in memory; others are re-read on demand">
<option value="1">1 tab</option>
<option value="2">2 tabs</option>
<option value="3" selected>3 tabs</option>
<option value="5">5 tabs</option>
<option value="0">All</option>
</select>
</div>
</div>
<div id="toolbar">
<div class="dd">
<button class="btn btn-outline" id="menu-btn" onclick="toggleDD(event, this)" title="Menu" aria-label="Menu">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" style="width: 18px; height: 18px;"><line x1="4" y1="6" x2="20" y2="6"></line><line x1="4" y1="12" x2="20" y2="12"></line><line x1="4" y1="18" x2="20" y2="18"></line></svg>
</button>
<div class="dd-menu" id="main-menu">
<div class="dd-item" onclick="closeDDs(); openPalette()">
<span class="lbl"><svg class="ic" viewBox="0 0 24 24"><path d="M18 3a3 3 0 0 0-3 3v12a3 3 0 0 0 3 3 3 3 0 0 0 3-3 3 3 0 0 0-3-3H6a3 3 0 0 0-3 3 3 3 0 0 0 3 3 3 3 0 0 0 3-3V6a3 3 0 0 0-3-3 3 3 0 0 0-3 3 3 3 0 0 0 3 3h12a3 3 0 0 0 3-3 3 3 0 0 0-3-3z"/></svg>Command palette…</span><span class="k">Ctrl+P</span>
</div>
<div class="dd-sep"></div>
<div class="dd-item" onclick="closeDDs(); openFiles()">
<span class="lbl"><svg class="ic" viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="17 8 12 3 7 8"></polyline><line x1="12" y1="3" x2="12" y2="15"></line></svg>Open</span><span class="k">Ctrl+O</span>
</div>
<div class="dd-row">
<span class="lbl"><svg class="ic" viewBox="0 0 24 24"><path d="M3 5h18M3 19h18"></path><rect x="3" y="9" width="18" height="6" rx="1"></rect></svg>Row</span>
<span class="dd-btns">
<span class="dd-btn" onclick="closeDDs(); goToRow()" title="Go to a row by its number Ctrl+G"><svg class="ic" viewBox="0 0 24 24"><path d="M4 12h12"></path><polyline points="12 8 16 12 12 16"></polyline><path d="M20 4v16"></path></svg>Go to…</span>
<span class="dd-btn" onclick="closeDDs(); toggleRowCard()" title="The selected row as an editable form Ctrl+I"><svg class="ic" viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="16" rx="2"></rect><circle cx="9" cy="10" r="2"></circle><path d="M6 16c0-1.7 1.3-3 3-3s3 1.3 3 3"></path><path d="M14 9h4M14 13h4"></path></svg>Card</span>
</span>
</div>
<div class="dd-item" onclick="closeDDs(); openProfile()" title="Every column's stats at a glance">
<span class="lbl"><svg class="ic" viewBox="0 0 24 24"><path d="M3 3v18h18"></path><path d="M7 15v-4M12 15V7M17 15v-6"></path></svg>File profile…</span>
</div>
<div class="dd-row">
<span class="lbl"><svg class="ic" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"></circle><line x1="21" y1="21" x2="16.5" y2="16.5"></line></svg>Look</span>
<span class="dd-btns">
<span class="dd-btn" onclick="closeDDs(); openFileMap()" title="The whole file as one picture: types, gaps, misfits Ctrl+M"><svg class="ic" viewBox="0 0 24 24"><path d="M3 6l6-3 6 3 6-3v15l-6 3-6-3-6 3z"></path><path d="M9 3v15M15 6v15"></path></svg>Map</span>
<span class="dd-btn" onclick="closeDDs(); openInsights()" title="What I noticed: keys, misfits, outliers, copies, hidden rules"><svg class="ic" viewBox="0 0 24 24"><path d="M12 3l2 6 6 2-6 2-2 6-2-6-6-2 6-2z"></path></svg>Insights</span>
</span>
</div>
<div class="dd-row">
<span class="lbl"><svg class="ic" viewBox="0 0 24 24"><path d="M12 3l8 3v6c0 4.5-3.4 8.3-8 9-4.6-.7-8-4.5-8-9V6z"></path><polyline points="8.5 12 11 14.5 15.5 9.5"></polyline></svg>Schema</span>
<span class="dd-btns">
<span class="dd-btn" onclick="closeDDs(); openSchemaCreate()" title="The rules this file follows, written beside it as name.schema.json (Table Schema)"><svg class="ic" viewBox="0 0 24 24"><path d="M14 3H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="14 3 14 9 20 9"></polyline><line x1="12" y1="12" x2="12" y2="18"></line><line x1="9" y1="15" x2="15" y2="15"></line></svg>Create…</span>
<span class="dd-btn" onclick="closeDDs(); openSchemaValidate()" title="Check this file against its schema, or any other"><svg class="ic" viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"></polyline></svg>Validate…</span>
</span>
</div>
<div class="dd-sep"></div>
<!-- Undo and Redo share one line, as Chrome groups Cut / Copy / Paste: a label, then
a button each; the shortcuts are in their tooltips. -->
<div class="dd-row">
<span class="lbl"><svg class="ic" viewBox="0 0 24 24"><path d="M3 12a9 9 0 1 0 3-6.7L3 8"></path><polyline points="3 3 3 8 8 8"></polyline><polyline points="12 7 12 12 15 14"></polyline></svg>Edit</span>
<span class="dd-btns">
<span class="dd-btn disabled" id="menu-undo" onclick="closeDDs(); undo()" title="Ctrl+Z"><svg class="ic" viewBox="0 0 24 24"><polyline points="9 14 4 9 9 4"></polyline><path d="M20 20v-7a4 4 0 0 0-4-4H4"></path></svg>Undo</span>
<span class="dd-btn disabled" id="menu-redo" onclick="closeDDs(); redo()" title="Ctrl+Y"><svg class="ic" viewBox="0 0 24 24"><polyline points="15 14 20 9 15 4"></polyline><path d="M4 20v-7a4 4 0 0 1 4-4h12"></path></svg>Redo</span>
</span>
</div>
<div class="dd-item" onclick="closeDDs(); openReview()" title="What Save would change on disk">
<span class="lbl"><svg class="ic" viewBox="0 0 24 24"><path d="M14 3H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="14 3 14 9 20 9"></polyline><line x1="9" y1="13" x2="15" y2="13"></line><line x1="12" y1="10" x2="12" y2="16"></line><line x1="9" y1="19" x2="15" y2="19"></line></svg>Review changes…</span>
</div>
<div class="dd-sep"></div>
<div class="dd-item" onclick="closeDDs(); toggleSRBar()" title="F3: next match">
<span class="lbl"><svg class="ic" viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>Find & replace</span><span class="k">Ctrl+F</span>
</div>
<!-- What reworks the whole file, or makes other files of it: one submenu, the menu had grown long. -->
<div class="dd-item has-sub">
<span class="lbl"><svg class="ic" viewBox="0 0 24 24"><path d="M14 3H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="14 3 14 9 20 9"></polyline></svg>Transform the file</span><span class="chev">›</span>
<div class="dd-menu sub">
<div class="dd-item" onclick="closeDDs(); openClean()" title="Accents, invisible characters, spaces, empty rows">
<span class="lbl"><svg class="ic" viewBox="0 0 24 24"><path d="M10 3l1.9 5.1L17 10l-5.1 1.9L10 17l-1.9-5.1L3 10l5.1-1.9z"></path><path d="M18 14l.9 2.1L21 17l-2.1.9L18 20l-.9-2.1L15 17l2.1-.9z"></path></svg>Clean up…</span>
</div>
<div class="dd-item" onclick="closeDDs(); openNoise()" title="Take a list of words out of a column or of every column">
<span class="lbl"><svg class="ic" viewBox="0 0 24 24"><path d="M4 7h16M4 12h7M4 17h16"></path><path d="M14 9.5l5 5M19 9.5l-5 5"></path></svg>Remove noise words…</span>
</div>
<div class="dd-item" onclick="closeDDs(); openAnon()" title="Replace personal data with consistent fakes">
<span class="lbl"><svg class="ic" viewBox="0 0 24 24"><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7S2 12 2 12z"></path><path d="M9 10a3 3 0 1 0 6 0"></path><path d="M5 5l14 14"></path></svg>Anonymise…</span>
</div>
<div class="dd-sep"></div>
<div class="dd-item" onclick="closeDDs(); openSplitFiles()" title="One file per value of a column (per city, per month…), in a new folder">
<span class="lbl"><svg class="ic" viewBox="0 0 24 24"><path d="M4 12h6"></path><path d="M10 12l4-6h6M10 12l4 6h6"></path></svg>Split into files…</span>
</div>
<div class="dd-item" onclick="closeDDs(); openCombine()" title="Open tabs one after the other in one new file, columns matched by title">
<span class="lbl"><svg class="ic" viewBox="0 0 24 24"><path d="M4 6h6l4 6h6"></path><path d="M4 18h6l4-6"></path></svg>Combine files…</span>
</div>
</div>
</div>
<div class="dd-sep"></div>
<!-- Submenus (.has-sub > .dd-menu.sub): shown while the pointer is on the item or on them. -->
<div class="dd-item has-sub">
<span class="lbl"><svg class="ic" viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="16" rx="2"></rect><line x1="9" y1="4" x2="9" y2="20"></line><line x1="15" y1="4" x2="15" y2="20"></line></svg>Columns</span><span class="chev">›</span>
<div class="dd-menu sub">
<div class="dd-item" onclick="closeDDs(); openSplit()">
<span class="lbl"><svg class="ic" viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="16" rx="2"></rect><line x1="12" y1="4" x2="12" y2="20" stroke-dasharray="2.5 2.5"></line></svg>Split column…</span>
</div>
<div class="dd-item" onclick="closeDDs(); openMerge()" title="Join several columns into one">
<span class="lbl"><svg class="ic" viewBox="0 0 24 24"><path d="M8 4v6a4 4 0 0 0 4 4 4 4 0 0 0 4-4V4"></path><line x1="12" y1="14" x2="12" y2="20"></line></svg>Merge columns…</span>
</div>
<div class="dd-item" onclick="closeDDs(); openFormula()">
<span class="lbl"><svg class="ic" viewBox="0 0 24 24"><path d="M14 4h-2.5a2 2 0 0 0-2 1.8L8 20"></path><line x1="6" y1="10" x2="14" y2="10"></line><path d="M15 14l5 5M20 14l-5 5"></path></svg>Computed column…</span>
</div>
<div class="dd-item" onclick="closeDDs(); openLookup()" title="Like VLOOKUP, from another tab">
<span class="lbl"><svg class="ic" viewBox="0 0 24 24"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>Look up from another tab…</span>
</div>
<div class="dd-item" onclick="closeDDs(); openConvert()" title="Dates, numbers, phones into one format">
<span class="lbl"><svg class="ic" viewBox="0 0 24 24"><polyline points="17 1 21 5 17 9"></polyline><path d="M3 11V9a4 4 0 0 1 4-4h14"></path><polyline points="7 23 3 19 7 15"></polyline><path d="M21 13v2a4 4 0 0 1-4 4H3"></path></svg>Convert formats…</span>
</div>
<div class="dd-sep"></div>
<div class="dd-item" onclick="closeDDs(); toggleFreeze()" title="Keep the first column in view">
<span class="lbl"><svg class="ic" viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2"></rect><line x1="9" y1="3" x2="9" y2="21"></line><path d="M5.5 7v2M5.5 12v2"></path></svg><span class="frz-off">Freeze the first column</span><span class="frz-on">Unfreeze the columns</span></span>
</div>
<div class="dd-item" onclick="closeDDs(); toggleSparklines()" title="Each column's values drawn under its title">
<span class="lbl"><svg class="ic" viewBox="0 0 24 24"><path d="M3 20h18"></path><path d="M6 16v-3M10 16V7M14 16v-6M18 16V4"></path></svg><span class="spk-off">Show the column charts</span><span class="spk-on">Hide the column charts</span></span>
</div>
<div class="dd-item" onclick="closeDDs(); toggleTotals()" title="A sum, average, count… under each column">
<span class="lbl"><svg class="ic" viewBox="0 0 24 24"><path d="M18 5H6l6 7-6 7h12"></path></svg><span class="tot-off">Show the totals row</span><span class="tot-on">Hide the totals row</span></span>
</div>
<div class="dd-item" onclick="closeDDs(); toggleFilterRow()" title="The filter boxes under the titles">
<span class="lbl"><svg class="ic" viewBox="0 0 24 24"><path d="M3 4h18l-7 8.5V19l-4 2v-8.5z"></path></svg><span class="filt-off">Show the filter row</span><span class="filt-on">Hide the filter row</span></span>
</div>
<div class="dd-item" onclick="closeDDs(); openColManager()" title="Tick the columns to show">
<span class="lbl"><svg class="ic" viewBox="0 0 24 24"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>Show or hide columns…</span>
</div>
</div>
</div>
<div class="dd-item has-sub">
<span class="lbl"><svg class="ic" viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="16" rx="2"></rect><line x1="3" y1="10" x2="21" y2="10"></line><line x1="3" y1="15" x2="21" y2="15"></line></svg>Rows</span><span class="chev">›</span>
<div class="dd-menu sub">
<div class="dd-item" onclick="closeDDs(); openBulk()">
<span class="lbl"><svg class="ic" viewBox="0 0 24 24"><path d="M12 20h9"></path><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"></path></svg>Edit filtered rows…</span>
</div>
<div class="dd-sep"></div>
<div class="dd-item" onclick="closeDDs(); fillDown()" title="Continue the series of the top cells (numbers, dates, Lot 7…)">
<span class="lbl"><svg class="ic" viewBox="0 0 24 24"><path d="M12 4v13"></path><polyline points="6 11 12 17 18 11"></polyline><line x1="5" y1="21" x2="19" y2="21"></line></svg>Fill series down</span><span class="k">Ctrl+D</span>
</div>
<div class="dd-item" onclick="closeDDs(); fillBlanks()" title="Each empty cell takes the value above it">
<span class="lbl"><svg class="ic" viewBox="0 0 24 24"><rect x="4" y="3" width="16" height="5" rx="1"></rect><rect x="4" y="16" width="16" height="5" rx="1" stroke-dasharray="3 2"></rect><path d="M12 8v6"></path><polyline points="9 11 12 14 15 11"></polyline></svg>Fill empty cells from above</span>
</div>
<div class="dd-item" onclick="closeDDs(); fillByExample()" title="Type one or two results in a column: the rest follows">
<span class="lbl"><svg class="ic" viewBox="0 0 24 24"><path d="M12 3l2 6 6 2-6 2-2 6-2-6-6-2 6-2z"></path></svg>Fill by example</span><span class="k">Ctrl+E</span>
</div>
<div class="dd-sep"></div>
<div class="dd-item" onclick="closeDDs(); openDedupe()">
<span class="lbl"><svg class="ic" viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>Duplicates…</span>
</div>
<div class="dd-item" onclick="closeDDs(); deleteHiddenRows()" title="Keep only the rows shown">
<span class="lbl"><svg class="ic" viewBox="0 0 24 24"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path><line x1="1" y1="1" x2="23" y2="23"></line></svg>Delete hidden rows</span>
</div>
<div class="dd-sep"></div>
<div class="dd-item" onclick="closeDDs(); openGroupBy()" title="Counts, sums, averages per value, in a new tab">
<span class="lbl"><svg class="ic" viewBox="0 0 24 24"><rect x="3" y="3" width="7" height="7" rx="1"></rect><rect x="14" y="3" width="7" height="7" rx="1"></rect><rect x="3" y="14" width="7" height="7" rx="1"></rect><path d="M14 17.5h7M17.5 14v7"></path></svg>Group by…</span>
</div>
<div class="dd-item" onclick="closeDDs(); openPivot()" title="Rows × columns: counts, sums or averages, with a colour scale; click a number to see its rows">
<span class="lbl"><svg class="ic" viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2"></rect><path d="M3 9h18M9 3v18"></path><path d="M13 13h4M13 17h4"></path></svg>Pivot table…</span>
</div>
<div class="dd-item" onclick="closeDDs(); openCompare()" title="Diff with another tab on a key column">
<span class="lbl"><svg class="ic" viewBox="0 0 24 24"><rect x="3" y="4" width="7" height="16" rx="1"></rect><rect x="14" y="4" width="7" height="16" rx="1"></rect><path d="M10 9h4M10 15h4"></path></svg>Compare with another tab…</span>
</div>
</div>
</div>
<div class="dd-sep"></div>
<div class="dd-row">
<span class="lbl"><svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"></circle><path d="M9.1 9a3 3 0 0 1 5.8 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>Help</span>
<span class="dd-btns">
<span class="dd-btn" onclick="closeDDs(); openKeys()" title="Every keyboard shortcut F1"><svg class="ic" viewBox="0 0 24 24"><rect x="2" y="6" width="20" height="12" rx="2"></rect><path d="M6 10h.01M10 10h.01M14 10h.01M18 10h.01M7 14h10"></path></svg>Shortcuts</span>
<span class="dd-btn" onclick="closeDDs(); openAbout()" title="Version, licence, website"><svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="11"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>About…</span>
</span>
</div>
<div class="dd-item quit" onclick="closeDDs(); quitApp()">
<span class="lbl"><svg class="ic" viewBox="0 0 24 24"><path d="M18.36 6.64a9 9 0 1 1-12.73 0"></path><line x1="12" y1="2" x2="12" y2="12"></line></svg>Quit</span><span class="k">Ctrl+Q</span>
</div>
</div>
</div>
<input type="file" id="loader" accept=".csv,.tsv,.txt,.xlsx,.xlsm,.json,.jsonl,.ndjson,.sqlite,.sqlite3,.db,.db3" multiple>
<div class="input-group">
<button class="fx-btn" id="btn-fx" onclick="openFxPicker('search')" title="Insert a function" aria-label="Insert a function"><span class="fx-glyph" aria-hidden="true"></span></button>
<input type="text" id="global-search" class="bs-input" placeholder="Filter..." autocomplete="off" title="All words, any order; "quotes" for a phrase">
</div>
<!-- The search options as toggles that light up: the checkboxes are kept (hidden), so everything reading them is unchanged. -->
<div class="tgl-group">
<label class="tgl" title="Ignore accents: é finds e, and e finds é"><input type="checkbox" id="use-slug"><svg class="tg" viewBox="0 0 18 18"><text x="9" y="16.2" text-anchor="middle">e</text><path class="acc" d="M8 5.2l3.2-3.4"/></svg></label>
<label class="tgl" title="Regex: the search is a regular expression"><input type="checkbox" id="use-regex"><svg class="tg" viewBox="0 0 18 18"><circle cx="4.5" cy="13.5" r="1.6" class="fill"/><path d="M12 2.5v8M8.6 4.5l6.8 4M15.4 4.5l-6.8 4"/></svg></label>
<label class="tgl inv" title="Invert: the rows that do NOT match"><input type="checkbox" id="use-reverse"><svg class="tg" viewBox="0 0 18 18"><path d="M3 7h12M3 11.5h12M12 2.5L6 16"/></svg></label>
<label class="tgl" title="Expression: filter with a formula, e.g. {Amount}.num() > 1000"><input type="checkbox" id="use-expr"><span class="fx-glyph" aria-hidden="true"></span></label>
</div>
<div id="fchips"></div>
<div style="margin-left: auto; display: flex; gap: 10px;">
<button class="btn btn-outline" id="btn-discard" onclick="discardEdits()" style="display:none;" title="Discard the edits" aria-label="Discard edits">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" style="width: 16px; height: 16px;"><line x1="6" y1="6" x2="18" y2="18"></line><line x1="18" y1="6" x2="6" y2="18"></line></svg>
</button>
<div class="btn-grp">
<button class="btn btn-outline" id="btn-export" onclick="extractFiltered()" style="display:none;" title="Extract the rows shown to a new file">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width: 16px; height: 16px;"><mask id="extract-cut"><rect x="-1" y="-1" width="26" height="26" fill="#fff" stroke="none"></rect><path d="M10.5 18v-5C10.5 3 16.5 7 21.5 12" stroke="#000" stroke-width="6"></path><polyline points="20.5 8 21.5 12 17.5 13" stroke="#000" stroke-width="6"></polyline></mask><g mask="url(#extract-cut)"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V6z"></path><polyline points="14 2 14 6 18 6"></polyline></g><path d="M10.5 18v-5C10.5 3 16.5 7 21.5 12"></path><polyline points="20.5 8 21.5 12 17.5 13"></polyline></svg>
Extract
</button>
<div class="split" id="save-split">
<button class="btn btn-outline" id="btn-save" onclick="saveInPlace()" title="A .bak copy is kept">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width: 16px; height: 16px; vertical-align: text-bottom;"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path><polyline points="17 21 17 13 7 13 7 21"></polyline><polyline points="7 3 7 8 15 8"></polyline></svg>
Save
<span id="edit-badge" style="display:none;">0</span>
</button>
<button class="btn btn-outline" id="btn-save-more" onclick="openSaveModal()" title="Save as…" aria-label="Save options">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" style="width: 16px; height: 12px;"><path d="M2 5l6 6 6-6"></path></svg>
</button>
</div>
</div>
</div>
<div id="progress-container"><div id="progress-bar"></div></div>
</div>
<div id="sr-bar">
<span style="font-family: var(--ui); font-size: 13px;">Find & replace in the <b>rows shown</b>:</span>
<input type="text" id="sr-find" class="bs-input" placeholder="Find text..." autocomplete="off" onkeydown="findKey(event)">
<span class="sr-nav" title="Highlight without filtering">
<button class="sr-arrow" onclick="findStep(-1)" title="Shift+F3">‹</button><span id="sr-pos"></span><button class="sr-arrow" onclick="findStep(1)" title="Next match (Enter, F3)">›</button>
</span>
<input type="text" id="sr-replace" class="bs-input" placeholder="Replace with..." autocomplete="off">
<select id="sr-col" class="bs-input bs-select" title="Where to find and replace" onchange="findChanged()"></select>
<label class="sr-chk" title="$1, $2… in the replacement"><input type="checkbox" id="sr-regex" onchange="findChanged()"> Regex</label>
<label class="sr-chk"><input type="checkbox" id="sr-case" onchange="findChanged()"> Match case</label>
<button class="btn btn-warn" onclick="applySearchReplace()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width: 14px; height: 14px; vertical-align: text-bottom;"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"></path></svg>
Apply
</button>
<span id="sr-count" style="font-family: var(--ui); font-size:12px; color:var(--txt-mut);"></span>
</div>
</div>
<div id="view-container">
<div id="empty-state"></div>
<!-- The grid lives in a sticky, clipped layer that the script scrolls (12-…, SCROLLING);
the native scrollbars come from #scroll-space, sized like the grid. The header is a
table, sticky at the layer's top; the rows are divs placed absolutely in #tbody
(12-…, VIRTUAL RENDERING); the totals row sticks at its bottom (54-…). -->
<div id="grid-layer">
<table id="mainTable" class="grid">
<thead id="thead"></thead>
</table>
<div id="tbody"></div>
<div id="tfoot"></div>
</div>
<div id="scroll-space"></div>
</div>
<div id="statusbar">
<div id="stats">Ready.</div>
<span id="irr-chip" onclick="toggleIrregular()" style="display:none;"></span>
<span id="dup-chip" onclick="toggleDupView()" style="display:none;"></span>
<span id="mark-chip" onclick="toggleMarkView()" style="display:none;"></span>
<span id="moji-chip" onclick="openClean()" style="display:none;" title="é for é: click to repair">⚠ Garbled accents</span>
<span id="sb-pos" style="display:none;"></span>
<span id="sb-count" style="display:none;"></span>
<div id="sb-fmt"></div>
</div>
<div class="dd-menu" id="sb-menu"></div>
<div class="dd-menu" id="row-menu"></div>
<div class="dd-menu" id="cell-menu"></div>
<div class="dd-menu" id="tot-menu"></div>
<div class="dd-menu" id="sc-menu"></div>
<!-- The app: ui/js/*.js, joined in name order by server.py into one script (/app.js), so
the files behave exactly as the single script they were cut from (functions declared
in a later file are callable from an earlier one). -->
<script src="app.js"></script>
</body>
</html>