markdownr 0.8.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/bin/markdownr +64 -0
- data/bin/start-claude +2 -0
- data/lib/markdown_server/app.rb +253 -40
- data/lib/markdown_server/assets/editor-loader.js +362 -0
- data/lib/markdown_server/helpers/admin_helpers.rb +10 -0
- data/lib/markdown_server/helpers/formatting_helpers.rb +3 -1
- data/lib/markdown_server/helpers/markdown_helpers.rb +132 -5
- data/lib/markdown_server/helpers/path_helpers.rb +56 -7
- data/lib/markdown_server/helpers/search_helpers.rb +31 -3
- data/lib/markdown_server/permitted_bases.rb +13 -0
- data/lib/markdown_server/plugins/bible_citations/citations.rb +4 -4
- data/lib/markdown_server/unhide.rb +114 -0
- data/lib/markdown_server/version.rb +1 -1
- data/views/browser.erb +1436 -50
- data/views/layout.erb +122 -5
- data/views/popup_assets.erb +52 -26
- metadata +6 -2
data/views/browser.erb
CHANGED
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
6
|
<title><%= h(@title) %></title>
|
|
7
|
+
<% if @allow_editor %>
|
|
8
|
+
<script type="importmap">
|
|
9
|
+
<%= editor_import_map_json %>
|
|
10
|
+
</script>
|
|
11
|
+
<% end %>
|
|
7
12
|
<style>
|
|
8
13
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
9
14
|
body {
|
|
@@ -89,6 +94,8 @@
|
|
|
89
94
|
overflow: auto;
|
|
90
95
|
-webkit-overflow-scrolling: touch;
|
|
91
96
|
font-size: 0.85rem;
|
|
97
|
+
scrollbar-width: thin;
|
|
98
|
+
scrollbar-color: rgba(0,0,0,0.35) transparent;
|
|
92
99
|
}
|
|
93
100
|
.br-popup-resize {
|
|
94
101
|
position: absolute;
|
|
@@ -101,6 +108,16 @@
|
|
|
101
108
|
z-index: 2;
|
|
102
109
|
}
|
|
103
110
|
|
|
111
|
+
/* ── Collapsed popup (double-click header to toggle) ── */
|
|
112
|
+
.br-popup.collapsed {
|
|
113
|
+
height: auto !important;
|
|
114
|
+
min-height: 0 !important;
|
|
115
|
+
max-height: none !important;
|
|
116
|
+
}
|
|
117
|
+
.br-popup.collapsed .br-popup-body { display: none; }
|
|
118
|
+
.br-popup.collapsed .br-popup-resize { display: none; }
|
|
119
|
+
.br-popup.collapsed .br-popup-header { border-bottom: none; }
|
|
120
|
+
|
|
104
121
|
/* ── View menu popup ── */
|
|
105
122
|
.br-popup.view-menu {
|
|
106
123
|
width: auto;
|
|
@@ -225,17 +242,48 @@
|
|
|
225
242
|
font-family: Georgia, "Times New Roman", serif;
|
|
226
243
|
}
|
|
227
244
|
.br-md-content > :first-child { margin-top: 0; }
|
|
228
|
-
.br-md-content h1 { font-size: 1.
|
|
245
|
+
.br-md-content h1 { font-size: 1.65rem; margin: 1.4rem 0 0.7rem; color: #2a2a2a; }
|
|
229
246
|
.br-md-content h2 {
|
|
230
|
-
font-size: 1.
|
|
231
|
-
border-bottom: 1px solid #
|
|
247
|
+
font-size: 1.2rem; margin: 1.4rem 0 0.5rem; color: #3a3a3a;
|
|
248
|
+
border-bottom: 1px solid #b5a78f; padding-bottom: 0.05rem;
|
|
232
249
|
}
|
|
233
|
-
.br-md-content h3 { font-size: 1rem; margin: 1.1rem 0 0.4rem; color: #
|
|
250
|
+
.br-md-content h3 { font-size: 1rem; margin: 1.1rem 0 0.4rem 0.75rem; color: #3a3a3a; }
|
|
234
251
|
.br-md-content blockquote {
|
|
235
252
|
border-left: 4px solid #d4b96a; margin: 0.8rem 0; padding: 0.4rem 1rem;
|
|
236
253
|
background: #fdfcf6; color: #4a4a4a; font-style: italic;
|
|
237
254
|
}
|
|
238
255
|
.br-md-content blockquote p { white-space: pre-wrap; margin: 0; }
|
|
256
|
+
.br-md-content .callout {
|
|
257
|
+
border-left: 4px solid var(--callout-color, #888);
|
|
258
|
+
background: color-mix(in srgb, var(--callout-color, #888) 10%, transparent);
|
|
259
|
+
border-radius: 4px;
|
|
260
|
+
margin: 0.8rem 0;
|
|
261
|
+
padding: 0.5rem 0.8rem;
|
|
262
|
+
font-style: normal;
|
|
263
|
+
color: #2a2a2a;
|
|
264
|
+
}
|
|
265
|
+
.br-md-content .callout .callout-title {
|
|
266
|
+
display: flex;
|
|
267
|
+
align-items: center;
|
|
268
|
+
gap: 0.35rem;
|
|
269
|
+
font-weight: 600;
|
|
270
|
+
color: var(--callout-color, #444);
|
|
271
|
+
cursor: default;
|
|
272
|
+
}
|
|
273
|
+
.br-md-content .callout details > summary.callout-title { cursor: pointer; list-style: none; }
|
|
274
|
+
.br-md-content .callout details > summary.callout-title::-webkit-details-marker { display: none; }
|
|
275
|
+
.br-md-content .callout details > summary.callout-title::after {
|
|
276
|
+
content: "▸";
|
|
277
|
+
margin-left: auto;
|
|
278
|
+
font-size: 0.7rem;
|
|
279
|
+
transition: transform 0.15s;
|
|
280
|
+
}
|
|
281
|
+
.br-md-content .callout details[open] > summary.callout-title::after { transform: rotate(90deg); }
|
|
282
|
+
.br-md-content .callout .callout-icon { flex-shrink: 0; }
|
|
283
|
+
.br-md-content .callout .callout-content { margin-top: 0.35rem; }
|
|
284
|
+
.br-md-content .callout .callout-content > :first-child { margin-top: 0; }
|
|
285
|
+
.br-md-content .callout .callout-content > :last-child { margin-bottom: 0; }
|
|
286
|
+
.br-md-content .callout .callout-content p { white-space: normal; margin: 0.4rem 0; }
|
|
239
287
|
.br-md-content a { color: #8b6914; text-decoration: none; border-bottom: 1px solid #d4b96a; }
|
|
240
288
|
.br-md-content a:hover { border-bottom-color: #8b6914; }
|
|
241
289
|
.br-md-content code {
|
|
@@ -245,8 +293,34 @@
|
|
|
245
293
|
.br-md-content pre {
|
|
246
294
|
background: #2d2d2d; color: #f0f0f0; padding: 0.8rem 1rem;
|
|
247
295
|
border-radius: 6px; overflow-x: auto; font-size: 0.82rem; line-height: 1.5;
|
|
296
|
+
position: relative;
|
|
248
297
|
}
|
|
249
298
|
.br-md-content pre code { background: none; padding: 0; color: inherit; }
|
|
299
|
+
|
|
300
|
+
/* Copy-to-clipboard button on <pre> blocks */
|
|
301
|
+
.copy-btn {
|
|
302
|
+
position: absolute;
|
|
303
|
+
top: 6px;
|
|
304
|
+
right: 6px;
|
|
305
|
+
background: rgba(255, 255, 255, 0.08);
|
|
306
|
+
color: #ddd;
|
|
307
|
+
border: 1px solid rgba(255, 255, 255, 0.18);
|
|
308
|
+
border-radius: 4px;
|
|
309
|
+
padding: 3px 6px;
|
|
310
|
+
cursor: pointer;
|
|
311
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
|
312
|
+
line-height: 0;
|
|
313
|
+
opacity: 0;
|
|
314
|
+
transition: opacity 0.15s ease, background 0.15s ease, color 0.15s ease;
|
|
315
|
+
z-index: 1;
|
|
316
|
+
}
|
|
317
|
+
.copy-btn svg { display: block; }
|
|
318
|
+
pre:hover > .copy-btn,
|
|
319
|
+
.copy-btn:focus { opacity: 1; }
|
|
320
|
+
.copy-btn:hover { background: rgba(255, 255, 255, 0.18); color: #fff; }
|
|
321
|
+
.copy-btn.copied {
|
|
322
|
+
background: #2e7d32; color: #fff; border-color: #2e7d32; opacity: 1;
|
|
323
|
+
}
|
|
250
324
|
.br-md-content hr { border: none; border-top: 1px solid #e0d8c8; margin: 1.5rem 0; }
|
|
251
325
|
.br-md-content ul, .br-md-content ol { padding-left: 1.5rem; }
|
|
252
326
|
.br-md-content li { margin-bottom: 0.2rem; }
|
|
@@ -297,12 +371,116 @@
|
|
|
297
371
|
background: #2d2d2d; color: #f0f0f0; padding: 1rem 1.2rem;
|
|
298
372
|
overflow: auto; font-family: "SF Mono", Menlo, Consolas, monospace;
|
|
299
373
|
font-size: 0.8rem; line-height: 1.5; flex: 1; min-height: 0;
|
|
374
|
+
position: relative;
|
|
300
375
|
}
|
|
301
376
|
|
|
302
377
|
/* Rouge Monokai syntax highlighting (popup + full-screen) */
|
|
303
378
|
<%= Rouge::Themes::Monokai.render(scope: '.br-code-view').gsub(/^\.br-code-view \{\n.*?background-color:.*?\n\}$/m, '') %>
|
|
304
379
|
<%= Rouge::Themes::Monokai.render(scope: '.highlight').gsub(/^\.highlight \{\n.*?background-color:.*?\n\}$/m, '') %>
|
|
305
380
|
|
|
381
|
+
/* ── In-popup editor (CodeMirror) ── */
|
|
382
|
+
.br-popup-edit-btn {
|
|
383
|
+
background: none; border: none;
|
|
384
|
+
font-size: 0.75rem; line-height: 1;
|
|
385
|
+
color: #8b6914; cursor: pointer;
|
|
386
|
+
padding: 0.15rem 0.45rem;
|
|
387
|
+
border: 1px solid #d4b96a;
|
|
388
|
+
border-radius: 4px;
|
|
389
|
+
flex-shrink: 0;
|
|
390
|
+
font-family: inherit;
|
|
391
|
+
}
|
|
392
|
+
.br-popup-edit-btn:hover { background: #f5f0e0; color: #5b4710; }
|
|
393
|
+
.br-popup.is-editing .br-popup-edit-btn { display: none; }
|
|
394
|
+
.br-editor-toolbar {
|
|
395
|
+
display: flex; gap: 0.4rem; align-items: center;
|
|
396
|
+
padding: 0.35rem 0.6rem;
|
|
397
|
+
background: #f5f1ea; border-bottom: 1px solid #e8e4dc;
|
|
398
|
+
font-size: 0.75rem; color: #555; flex-shrink: 0;
|
|
399
|
+
}
|
|
400
|
+
.br-editor-toolbar .br-editor-lang {
|
|
401
|
+
color: #888; margin-right: auto; font-family: "SF Mono", Menlo, Consolas, monospace;
|
|
402
|
+
}
|
|
403
|
+
.br-editor-toolbar button {
|
|
404
|
+
padding: 0.25rem 0.7rem; font-size: 0.75rem;
|
|
405
|
+
border: 1px solid #d4b96a; border-radius: 4px;
|
|
406
|
+
background: #faf8f4; color: #555; cursor: pointer;
|
|
407
|
+
font-family: inherit;
|
|
408
|
+
}
|
|
409
|
+
.br-editor-toolbar button:hover:not(:disabled) { background: #f0ece4; color: #2c2c2c; }
|
|
410
|
+
.br-editor-toolbar button:disabled { opacity: 0.45; cursor: default; }
|
|
411
|
+
.br-editor-toolbar .br-editor-save { background: #27ae60; color: #fff; border-color: #219a52; }
|
|
412
|
+
.br-editor-toolbar .br-editor-save:hover:not(:disabled) { background: #219a52; color: #fff; }
|
|
413
|
+
.br-editor-toolbar .br-editor-save.dirty {
|
|
414
|
+
background: #e6c029; color: #2c2c2c; border-color: #c5a51d;
|
|
415
|
+
}
|
|
416
|
+
.br-editor-toolbar .br-editor-save.dirty:hover { background: #c5a51d; color: #2c2c2c; }
|
|
417
|
+
.br-editor-toolbar .br-editor-vim.active {
|
|
418
|
+
background: #2a6496; color: #fff; border-color: #1d4970;
|
|
419
|
+
}
|
|
420
|
+
.br-editor-saved-flash {
|
|
421
|
+
color: #27ae60; font-size: 0.75rem; margin-left: 0.4rem;
|
|
422
|
+
opacity: 0; transition: opacity 0.3s;
|
|
423
|
+
}
|
|
424
|
+
.br-editor-saved-flash.show { opacity: 1; }
|
|
425
|
+
.br-editor-pane {
|
|
426
|
+
flex: 1; min-height: 0; min-width: 0;
|
|
427
|
+
overflow: hidden; display: flex; flex-direction: column;
|
|
428
|
+
}
|
|
429
|
+
.br-editor-pane .cm-editor {
|
|
430
|
+
height: 100%; width: 100%; flex: 1; min-height: 0;
|
|
431
|
+
font-size: 0.82rem;
|
|
432
|
+
}
|
|
433
|
+
.br-editor-split {
|
|
434
|
+
flex: 1; min-height: 0; display: flex; flex-direction: row;
|
|
435
|
+
overflow: hidden;
|
|
436
|
+
}
|
|
437
|
+
.br-editor-divider {
|
|
438
|
+
width: 5px; flex-shrink: 0; cursor: col-resize;
|
|
439
|
+
background: #e0d8c8; border-left: 1px solid #c8c0b0; border-right: 1px solid #c8c0b0;
|
|
440
|
+
}
|
|
441
|
+
.br-editor-divider:hover { background: #d4b96a; }
|
|
442
|
+
.br-editor-preview {
|
|
443
|
+
flex: 1 1 50%; min-width: 0; min-height: 0;
|
|
444
|
+
overflow: auto; background: #faf8f4; padding: 0.6rem 0.8rem;
|
|
445
|
+
font-size: 0.85rem;
|
|
446
|
+
}
|
|
447
|
+
/* Live-preview decorations inside the markdown editor */
|
|
448
|
+
.br-editor-pane .cm-editor .cm-md-line-h1 { font-size: 1.55em; font-weight: 700; line-height: 1.3; }
|
|
449
|
+
.br-editor-pane .cm-editor .cm-md-line-h2 { font-size: 1.35em; font-weight: 700; line-height: 1.3; }
|
|
450
|
+
.br-editor-pane .cm-editor .cm-md-line-h3 { font-size: 1.2em; font-weight: 700; }
|
|
451
|
+
.br-editor-pane .cm-editor .cm-md-line-h4 { font-size: 1.08em; font-weight: 700; }
|
|
452
|
+
.br-editor-pane .cm-editor .cm-md-line-h5 { font-size: 1em; font-weight: 700; }
|
|
453
|
+
.br-editor-pane .cm-editor .cm-md-line-h6 { font-size: 0.95em; font-weight: 700; opacity: 0.85; }
|
|
454
|
+
.br-editor-pane .cm-editor .cm-md-strong { font-weight: 700; }
|
|
455
|
+
.br-editor-pane .cm-editor .cm-md-em { font-style: italic; }
|
|
456
|
+
.br-editor-pane .cm-editor .cm-md-code {
|
|
457
|
+
background: rgba(150, 150, 150, 0.18);
|
|
458
|
+
padding: 0 0.25em;
|
|
459
|
+
border-radius: 3px;
|
|
460
|
+
font-family: "SF Mono", Menlo, Consolas, monospace;
|
|
461
|
+
}
|
|
462
|
+
.br-editor-pane .cm-editor .cm-md-link {
|
|
463
|
+
color: #6db1ff;
|
|
464
|
+
text-decoration: underline;
|
|
465
|
+
}
|
|
466
|
+
.br-editor-pane .cm-editor .cm-md-bullet {
|
|
467
|
+
color: #888;
|
|
468
|
+
font-weight: 700;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
.br-editor-conflict {
|
|
472
|
+
background: #fde8e8; border-bottom: 1px solid #e6b3b3;
|
|
473
|
+
padding: 0.5rem 0.7rem; font-size: 0.78rem; color: #7a1f1f;
|
|
474
|
+
display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap;
|
|
475
|
+
flex-shrink: 0;
|
|
476
|
+
}
|
|
477
|
+
.br-editor-conflict button {
|
|
478
|
+
padding: 0.2rem 0.65rem; font-size: 0.75rem; cursor: pointer;
|
|
479
|
+
border: 1px solid #c0392b; border-radius: 3px;
|
|
480
|
+
background: #fff; color: #7a1f1f; font-family: inherit;
|
|
481
|
+
}
|
|
482
|
+
.br-editor-conflict button:hover { background: #f7d4d4; }
|
|
483
|
+
|
|
306
484
|
/* ── Download / external link in popup ── */
|
|
307
485
|
.br-link-body {
|
|
308
486
|
display: flex; flex-direction: column; align-items: center;
|
|
@@ -347,7 +525,26 @@
|
|
|
347
525
|
padding: 0.25rem 0.4rem; background: #f8f5ef; border-bottom: 1px solid #d4b96a;
|
|
348
526
|
position: sticky; top: 1.65rem; z-index: 1;
|
|
349
527
|
}
|
|
350
|
-
.br-table-wrap {
|
|
528
|
+
.br-table-wrap {
|
|
529
|
+
overflow: auto; flex: 1; min-height: 0;
|
|
530
|
+
scrollbar-width: thin;
|
|
531
|
+
scrollbar-color: rgba(0,0,0,0.35) transparent;
|
|
532
|
+
}
|
|
533
|
+
.br-popup-body::-webkit-scrollbar,
|
|
534
|
+
.br-table-wrap::-webkit-scrollbar { width: 10px; height: 10px; }
|
|
535
|
+
.br-popup-body::-webkit-scrollbar-track,
|
|
536
|
+
.br-table-wrap::-webkit-scrollbar-track { background: transparent; }
|
|
537
|
+
.br-popup-body::-webkit-scrollbar-thumb,
|
|
538
|
+
.br-table-wrap::-webkit-scrollbar-thumb {
|
|
539
|
+
background: rgba(0,0,0,0.28);
|
|
540
|
+
border-radius: 5px;
|
|
541
|
+
border: 2px solid transparent;
|
|
542
|
+
background-clip: padding-box;
|
|
543
|
+
}
|
|
544
|
+
.br-popup-body::-webkit-scrollbar-thumb:hover,
|
|
545
|
+
.br-table-wrap::-webkit-scrollbar-thumb:hover { background: rgba(0,0,0,0.5); background-clip: padding-box; }
|
|
546
|
+
.br-popup-body::-webkit-scrollbar-corner,
|
|
547
|
+
.br-table-wrap::-webkit-scrollbar-corner { background: transparent; }
|
|
351
548
|
.br-data-table { width: 100%; border-collapse: collapse; font-size: 0.8rem; }
|
|
352
549
|
.br-data-table th {
|
|
353
550
|
background: #f0ece3; font-weight: 600; text-align: left;
|
|
@@ -356,6 +553,8 @@
|
|
|
356
553
|
user-select: none; z-index: 1; overflow: hidden; text-overflow: ellipsis;
|
|
357
554
|
}
|
|
358
555
|
.br-data-table th:hover { background: #e8e0d0; }
|
|
556
|
+
.br-data-table th.br-corner-th { background: #2d2d2d; cursor: context-menu; }
|
|
557
|
+
.br-data-table th.br-corner-th:hover { background: #4a4a4a; }
|
|
359
558
|
.br-col-resize {
|
|
360
559
|
position: absolute; right: -3px; top: 0; bottom: 0; width: 6px;
|
|
361
560
|
cursor: col-resize; z-index: 2; background: transparent;
|
|
@@ -391,6 +590,8 @@
|
|
|
391
590
|
.br-data-table tr.br-row-dirty:hover td { background: #fdf3d0; }
|
|
392
591
|
.br-data-table tr.br-row-invalid td { background: #fde8e8; }
|
|
393
592
|
.br-data-table tr.br-row-invalid:hover td { background: #fad4d4; }
|
|
593
|
+
.br-data-table tr.br-row-dirty-bypass td { background: #fde8e8; }
|
|
594
|
+
.br-data-table tr.br-row-dirty-bypass:hover td { background: #fad4d4; }
|
|
394
595
|
.br-data-table td.br-cell-match { background: #e8f5e9 !important; }
|
|
395
596
|
.br-data-table tr.br-row-match td { background: #e8f5e9 !important; }
|
|
396
597
|
.br-data-table tr.br-row-match:hover td { background: #c8e6c9 !important; }
|
|
@@ -642,6 +843,11 @@
|
|
|
642
843
|
var startMode = '<%= @start_mode %>';
|
|
643
844
|
var rootTitle = '<%= h(@root_title) %>';
|
|
644
845
|
var csvDatabases = <%= @csv_databases %>;
|
|
846
|
+
var initialPath = <%= (@initial_path || "").to_json %>;
|
|
847
|
+
var isAdmin = <%= @is_admin ? 'true' : 'false' %>;
|
|
848
|
+
var defaultVim = <%= @default_vim ? 'true' : 'false' %>;
|
|
849
|
+
var allowEditor = <%= @allow_editor ? 'true' : 'false' %>;
|
|
850
|
+
var allowCsvEditor = <%= @allow_csv_editor ? 'true' : 'false' %>;
|
|
645
851
|
|
|
646
852
|
// ── Tab management ──
|
|
647
853
|
|
|
@@ -854,7 +1060,13 @@
|
|
|
854
1060
|
closeBtn.className = 'br-popup-btn';
|
|
855
1061
|
closeBtn.innerHTML = '×';
|
|
856
1062
|
if (opts.color) closeBtn.style.color = 'rgba(255,255,255,0.8)';
|
|
857
|
-
closeBtn.onclick = function() {
|
|
1063
|
+
closeBtn.onclick = function() {
|
|
1064
|
+
if (el._editorIsDirty && el._editorIsDirty()) {
|
|
1065
|
+
if (!confirm('This popup has unsaved edits. Close anyway?')) return;
|
|
1066
|
+
}
|
|
1067
|
+
if (el._editorTeardown) { try { el._editorTeardown(); } catch (e) {} }
|
|
1068
|
+
el.remove();
|
|
1069
|
+
};
|
|
858
1070
|
header.appendChild(closeBtn);
|
|
859
1071
|
}
|
|
860
1072
|
|
|
@@ -874,6 +1086,31 @@
|
|
|
874
1086
|
|
|
875
1087
|
el.style.zIndex = ++zCounter;
|
|
876
1088
|
el.addEventListener('mousedown', function() { el.style.zIndex = ++zCounter; });
|
|
1089
|
+
el.addEventListener('touchstart', function() { el.style.zIndex = ++zCounter; }, { passive: true });
|
|
1090
|
+
|
|
1091
|
+
// Double-click / double-tap on header toggles collapsed state.
|
|
1092
|
+
header.addEventListener('dblclick', function(e) {
|
|
1093
|
+
if (e.target.closest('.br-popup-btn')) return;
|
|
1094
|
+
if (e.target.closest('.br-popup-open-tab')) return;
|
|
1095
|
+
toggleCollapsed(el);
|
|
1096
|
+
});
|
|
1097
|
+
var lastTapTime = 0;
|
|
1098
|
+
var tapMoved = false;
|
|
1099
|
+
header.addEventListener('touchstart', function() { tapMoved = false; }, { passive: true });
|
|
1100
|
+
header.addEventListener('touchmove', function() { tapMoved = true; }, { passive: true });
|
|
1101
|
+
header.addEventListener('touchend', function(e) {
|
|
1102
|
+
if (tapMoved) { lastTapTime = 0; return; }
|
|
1103
|
+
if (e.target.closest('.br-popup-btn')) return;
|
|
1104
|
+
if (e.target.closest('.br-popup-open-tab')) return;
|
|
1105
|
+
var now = Date.now();
|
|
1106
|
+
if (now - lastTapTime < 400) {
|
|
1107
|
+
toggleCollapsed(el);
|
|
1108
|
+
e.preventDefault();
|
|
1109
|
+
lastTapTime = 0;
|
|
1110
|
+
} else {
|
|
1111
|
+
lastTapTime = now;
|
|
1112
|
+
}
|
|
1113
|
+
});
|
|
877
1114
|
|
|
878
1115
|
if (opts.x !== undefined && opts.y !== undefined) {
|
|
879
1116
|
el.style.left = opts.x + 'px';
|
|
@@ -1002,6 +1239,18 @@
|
|
|
1002
1239
|
return null;
|
|
1003
1240
|
}
|
|
1004
1241
|
function bringToFront(el) { el.style.zIndex = ++zCounter; }
|
|
1242
|
+
|
|
1243
|
+
function toggleCollapsed(popup) {
|
|
1244
|
+
if (popup.classList.contains('collapsed')) {
|
|
1245
|
+
popup.classList.remove('collapsed');
|
|
1246
|
+
var saved = popup.getAttribute('data-saved-height');
|
|
1247
|
+
popup.style.height = saved || '';
|
|
1248
|
+
popup.removeAttribute('data-saved-height');
|
|
1249
|
+
} else {
|
|
1250
|
+
popup.setAttribute('data-saved-height', popup.style.height || '');
|
|
1251
|
+
popup.classList.add('collapsed');
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1005
1254
|
function escHtml(s) {
|
|
1006
1255
|
var d = document.createElement('div');
|
|
1007
1256
|
d.textContent = s;
|
|
@@ -1139,7 +1388,23 @@
|
|
|
1139
1388
|
catch(e) { return {}; }
|
|
1140
1389
|
}
|
|
1141
1390
|
|
|
1391
|
+
function countDirtyEditors() {
|
|
1392
|
+
var selector = '.br-popup.is-editing';
|
|
1393
|
+
if (activeTabId) selector += '[data-tab-id="' + activeTabId + '"]';
|
|
1394
|
+
var n = 0;
|
|
1395
|
+
document.querySelectorAll(selector).forEach(function(el) {
|
|
1396
|
+
if (el._editorIsDirty && el._editorIsDirty()) n++;
|
|
1397
|
+
});
|
|
1398
|
+
return n;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1142
1401
|
function saveLayout(name) {
|
|
1402
|
+
var dirty = countDirtyEditors();
|
|
1403
|
+
if (dirty > 0) {
|
|
1404
|
+
var msg = dirty + ' popup' + (dirty === 1 ? ' has' : 's have') +
|
|
1405
|
+
' unsaved edits. Save layout anyway? (Edits will be discarded from the snapshot but remain in the live popups.)';
|
|
1406
|
+
if (!confirm(msg)) return null;
|
|
1407
|
+
}
|
|
1143
1408
|
var layouts = getLayouts();
|
|
1144
1409
|
layouts[name] = {
|
|
1145
1410
|
savedAt: new Date().toISOString(),
|
|
@@ -1265,6 +1530,9 @@
|
|
|
1265
1530
|
var url = '/browser/api/render/' + encodeURI(path);
|
|
1266
1531
|
if (opts.queryParams) url += '?' + opts.queryParams;
|
|
1267
1532
|
|
|
1533
|
+
popup.setAttribute('data-file-path', path);
|
|
1534
|
+
if (opts.queryParams) popup.setAttribute('data-file-query', opts.queryParams);
|
|
1535
|
+
|
|
1268
1536
|
fetch(url)
|
|
1269
1537
|
.then(function(r) { return r.json(); })
|
|
1270
1538
|
.then(function(data) {
|
|
@@ -1277,6 +1545,7 @@
|
|
|
1277
1545
|
} else if (data.type === 'external' && data.href) {
|
|
1278
1546
|
popup.setAttribute('data-browse-href', data.href);
|
|
1279
1547
|
}
|
|
1548
|
+
popup._editorPriorData = data;
|
|
1280
1549
|
dispatchRenderer(popup, data);
|
|
1281
1550
|
})
|
|
1282
1551
|
.catch(function() {
|
|
@@ -1326,8 +1595,13 @@
|
|
|
1326
1595
|
}
|
|
1327
1596
|
|
|
1328
1597
|
var items = data.items;
|
|
1329
|
-
var
|
|
1330
|
-
var
|
|
1598
|
+
var DIR_SORT_KEY = 'markdownr:dirSort';
|
|
1599
|
+
var dirSortPath = data.path || '';
|
|
1600
|
+
var savedSort = {};
|
|
1601
|
+
try { savedSort = JSON.parse(localStorage.getItem(DIR_SORT_KEY)) || {}; } catch (e) {}
|
|
1602
|
+
var entry = savedSort[dirSortPath] || {};
|
|
1603
|
+
var sortKey = entry.key || 'mtime';
|
|
1604
|
+
var sortDir = entry.dir || 'desc';
|
|
1331
1605
|
|
|
1332
1606
|
var sortBar = document.createElement('div');
|
|
1333
1607
|
sortBar.className = 'br-sort-bar';
|
|
@@ -1406,6 +1680,12 @@
|
|
|
1406
1680
|
var defaultDir = btn.getAttribute('data-default-dir');
|
|
1407
1681
|
if (key === sortKey) { sortDir = sortDir === 'asc' ? 'desc' : 'asc'; }
|
|
1408
1682
|
else { sortKey = key; sortDir = defaultDir; }
|
|
1683
|
+
try {
|
|
1684
|
+
var current = {};
|
|
1685
|
+
try { current = JSON.parse(localStorage.getItem(DIR_SORT_KEY)) || {}; } catch (e2) {}
|
|
1686
|
+
current[dirSortPath] = { key: sortKey, dir: sortDir };
|
|
1687
|
+
localStorage.setItem(DIR_SORT_KEY, JSON.stringify(current));
|
|
1688
|
+
} catch (e3) {}
|
|
1409
1689
|
render();
|
|
1410
1690
|
});
|
|
1411
1691
|
|
|
@@ -1425,12 +1705,480 @@
|
|
|
1425
1705
|
render();
|
|
1426
1706
|
}
|
|
1427
1707
|
|
|
1708
|
+
// ── In-popup editor ──
|
|
1709
|
+
|
|
1710
|
+
var editorModulePromise = null;
|
|
1711
|
+
function ensureEditor() {
|
|
1712
|
+
if (!editorModulePromise) {
|
|
1713
|
+
editorModulePromise = import('/__markdownr/editor-loader.js?v=<%= editor_loader_version %>')
|
|
1714
|
+
.catch(function(e) { editorModulePromise = null; throw e; });
|
|
1715
|
+
}
|
|
1716
|
+
return editorModulePromise;
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
function guessLanguageForPath(path) {
|
|
1720
|
+
var lower = (path || '').toLowerCase();
|
|
1721
|
+
var dot = lower.lastIndexOf('.');
|
|
1722
|
+
if (dot < 0) return 'text';
|
|
1723
|
+
var ext = lower.substring(dot + 1);
|
|
1724
|
+
switch (ext) {
|
|
1725
|
+
case 'md': case 'markdown': return 'markdown';
|
|
1726
|
+
case 'js': case 'mjs': return 'javascript';
|
|
1727
|
+
case 'json': return 'json';
|
|
1728
|
+
case 'yaml': case 'yml': return 'yaml';
|
|
1729
|
+
case 'html': case 'erb': case 'htm': return 'html';
|
|
1730
|
+
case 'css': return 'css';
|
|
1731
|
+
case 'py': return 'python';
|
|
1732
|
+
case 'rb': case 'ruby': return 'ruby';
|
|
1733
|
+
case 'sh': case 'bash': case 'zsh': return 'bash';
|
|
1734
|
+
default: return 'text';
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
// Heuristic for "does this markdown contain anything the live-preview
|
|
1739
|
+
// subset doesn't render?" Used to auto-open the side preview pane on
|
|
1740
|
+
// entering edit mode. We err on the side of opening — false positives
|
|
1741
|
+
// (e.g. a literal `<` in text) are better than false negatives (a table
|
|
1742
|
+
// not rendering and the user not realizing why).
|
|
1743
|
+
function markdownHasUnsupportedElements(text) {
|
|
1744
|
+
if (!text) return false;
|
|
1745
|
+
// Strip frontmatter so its `---` delimiters don't trigger the HR check.
|
|
1746
|
+
var body = text.replace(/^---\s*\n[\s\S]*?\n---\s*(\n|$)/, '');
|
|
1747
|
+
// Strip fenced and inline code so things like `<package>` inside
|
|
1748
|
+
// `https://esm.sh/<package>` don't trigger the HTML-tag check.
|
|
1749
|
+
var stripped = body
|
|
1750
|
+
.replace(/```[\s\S]*?```/g, '')
|
|
1751
|
+
.replace(/`[^`\n]*`/g, '');
|
|
1752
|
+
return /(^|\n)[ \t]*\|.*\|/.test(body) // table row
|
|
1753
|
+
|| /!\[[^\]]*\]\([^)]+\)/.test(body) // image
|
|
1754
|
+
|| /(^|\n)[ \t]*>[ \t]/.test(body) // blockquote
|
|
1755
|
+
|| /(^|\n)(---|\*\*\*|___)\s*(\n|$)/.test(body) // horizontal rule
|
|
1756
|
+
|| /<\/?[a-zA-Z][^>]*>/.test(stripped) // raw HTML tag (outside code)
|
|
1757
|
+
|| /\[\[[^\]]+\]\]/.test(stripped) // wiki link (outside code)
|
|
1758
|
+
|| /\[\^[^\]]+\]/.test(stripped); // footnote (outside code)
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
function getVimPref() {
|
|
1762
|
+
var v = null;
|
|
1763
|
+
try { v = localStorage.getItem('markdownr:vim'); } catch (e) {}
|
|
1764
|
+
if (v === 'true') return true;
|
|
1765
|
+
if (v === 'false') return false;
|
|
1766
|
+
return !!defaultVim;
|
|
1767
|
+
}
|
|
1768
|
+
function setVimPref(on) {
|
|
1769
|
+
try { localStorage.setItem('markdownr:vim', on ? 'true' : 'false'); } catch (e) {}
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
function addEditButton(popup) {
|
|
1773
|
+
if (!isAdmin || !allowEditor) return;
|
|
1774
|
+
var path = popup.getAttribute('data-file-path');
|
|
1775
|
+
if (!path) return;
|
|
1776
|
+
var queryParams = popup.getAttribute('data-file-query');
|
|
1777
|
+
if (queryParams && queryParams.indexOf('raw=1') < 0) {
|
|
1778
|
+
// Other query parameters we don't support yet — skip.
|
|
1779
|
+
return;
|
|
1780
|
+
}
|
|
1781
|
+
var header = popup.querySelector('.br-popup-header');
|
|
1782
|
+
if (!header || header.querySelector('.br-popup-edit-btn')) return;
|
|
1783
|
+
|
|
1784
|
+
var btn = document.createElement('button');
|
|
1785
|
+
btn.type = 'button';
|
|
1786
|
+
btn.className = 'br-popup-edit-btn';
|
|
1787
|
+
btn.textContent = 'Edit';
|
|
1788
|
+
btn.title = 'Edit file (admin)';
|
|
1789
|
+
btn.onclick = function(e) {
|
|
1790
|
+
e.stopPropagation();
|
|
1791
|
+
enterEditMode(popup);
|
|
1792
|
+
};
|
|
1793
|
+
var openTabBtn = header.querySelector('.br-popup-open-tab');
|
|
1794
|
+
if (openTabBtn) header.insertBefore(btn, openTabBtn);
|
|
1795
|
+
else header.appendChild(btn);
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
function flashEditorSaved(toolbar) {
|
|
1799
|
+
var flash = toolbar.querySelector('.br-editor-saved-flash');
|
|
1800
|
+
if (!flash) {
|
|
1801
|
+
flash = document.createElement('span');
|
|
1802
|
+
flash.className = 'br-editor-saved-flash';
|
|
1803
|
+
flash.textContent = 'saved';
|
|
1804
|
+
toolbar.appendChild(flash);
|
|
1805
|
+
}
|
|
1806
|
+
flash.classList.add('show');
|
|
1807
|
+
clearTimeout(flash._t);
|
|
1808
|
+
flash._t = setTimeout(function() { flash.classList.remove('show'); }, 1200);
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
function makeEditorDivider(splitWrap, divider) {
|
|
1812
|
+
var startX, leftStart, totalStart;
|
|
1813
|
+
function onDown(e) {
|
|
1814
|
+
e.preventDefault();
|
|
1815
|
+
var t = e.touches ? e.touches[0] : e;
|
|
1816
|
+
startX = t.clientX;
|
|
1817
|
+
var leftRect = splitWrap.firstElementChild.getBoundingClientRect();
|
|
1818
|
+
leftStart = leftRect.width;
|
|
1819
|
+
totalStart = splitWrap.getBoundingClientRect().width;
|
|
1820
|
+
document.addEventListener('mousemove', onMove);
|
|
1821
|
+
document.addEventListener('mouseup', onUp);
|
|
1822
|
+
document.addEventListener('touchmove', onMove, { passive: false });
|
|
1823
|
+
document.addEventListener('touchend', onUp);
|
|
1824
|
+
}
|
|
1825
|
+
function onMove(e) {
|
|
1826
|
+
e.preventDefault();
|
|
1827
|
+
var t = e.touches ? e.touches[0] : e;
|
|
1828
|
+
var dx = t.clientX - startX;
|
|
1829
|
+
var newLeft = Math.max(120, Math.min(totalStart - 120, leftStart + dx));
|
|
1830
|
+
var pct = (newLeft / totalStart) * 100;
|
|
1831
|
+
splitWrap.firstElementChild.style.flex = '0 0 ' + pct + '%';
|
|
1832
|
+
try { localStorage.setItem('markdownr:editor:split', String(pct)); } catch (e) {}
|
|
1833
|
+
}
|
|
1834
|
+
function onUp() {
|
|
1835
|
+
document.removeEventListener('mousemove', onMove);
|
|
1836
|
+
document.removeEventListener('mouseup', onUp);
|
|
1837
|
+
document.removeEventListener('touchmove', onMove);
|
|
1838
|
+
document.removeEventListener('touchend', onUp);
|
|
1839
|
+
}
|
|
1840
|
+
divider.addEventListener('mousedown', onDown);
|
|
1841
|
+
divider.addEventListener('touchstart', onDown, { passive: false });
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
function enterEditMode(popup) {
|
|
1845
|
+
var path = popup.getAttribute('data-file-path');
|
|
1846
|
+
if (!path) return;
|
|
1847
|
+
if (popup._editorTeardown) return; // already editing
|
|
1848
|
+
|
|
1849
|
+
var priorData = popup._editorPriorData || null;
|
|
1850
|
+
var body = popup.querySelector('.br-popup-body');
|
|
1851
|
+
var origBodyHtml = body.innerHTML;
|
|
1852
|
+
var origBodyDisplay = body.style.display;
|
|
1853
|
+
var origBodyFlexDir = body.style.flexDirection;
|
|
1854
|
+
var origBodyPadding = body.style.padding;
|
|
1855
|
+
body.innerHTML = '<div class="br-empty" style="padding:1rem">Loading editor…</div>';
|
|
1856
|
+
popup.classList.add('is-editing');
|
|
1857
|
+
|
|
1858
|
+
Promise.all([
|
|
1859
|
+
fetch('/__markdownr/api/file/source/' + encodeURI(path), { credentials: 'same-origin' })
|
|
1860
|
+
.then(function(r) {
|
|
1861
|
+
if (r.status === 413) throw new Error('file too large to edit');
|
|
1862
|
+
if (!r.ok) throw new Error('source fetch failed: ' + r.status);
|
|
1863
|
+
var etag = (r.headers.get('ETag') || '').replace(/^"|"$/g, '');
|
|
1864
|
+
return r.text().then(function(text) { return { text: text, etag: etag }; });
|
|
1865
|
+
}),
|
|
1866
|
+
ensureEditor()
|
|
1867
|
+
]).then(function(arr) {
|
|
1868
|
+
var src = arr[0];
|
|
1869
|
+
var mod = arr[1];
|
|
1870
|
+
buildEditorUI(popup, body, path, src.text, src.etag, priorData, mod, {
|
|
1871
|
+
origBodyHtml: origBodyHtml,
|
|
1872
|
+
origBodyDisplay: origBodyDisplay,
|
|
1873
|
+
origBodyFlexDir: origBodyFlexDir,
|
|
1874
|
+
origBodyPadding: origBodyPadding
|
|
1875
|
+
});
|
|
1876
|
+
}).catch(function(err) {
|
|
1877
|
+
body.innerHTML = '<div class="br-empty" style="padding:1rem;color:#c44">Failed to load editor: ' + escHtml(err && err.message || 'unknown') + '</div>';
|
|
1878
|
+
popup.classList.remove('is-editing');
|
|
1879
|
+
setTimeout(function() {
|
|
1880
|
+
body.style.display = origBodyDisplay;
|
|
1881
|
+
body.style.flexDirection = origBodyFlexDir;
|
|
1882
|
+
body.style.padding = origBodyPadding;
|
|
1883
|
+
if (priorData) dispatchRenderer(popup, priorData);
|
|
1884
|
+
else body.innerHTML = origBodyHtml;
|
|
1885
|
+
}, 1500);
|
|
1886
|
+
});
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
function buildEditorUI(popup, body, path, text, initialEtag, priorData, mod, orig) {
|
|
1890
|
+
body.innerHTML = '';
|
|
1891
|
+
body.style.display = 'flex';
|
|
1892
|
+
body.style.flexDirection = 'column';
|
|
1893
|
+
body.style.padding = '0';
|
|
1894
|
+
popup.classList.add('wide');
|
|
1895
|
+
|
|
1896
|
+
var lang = guessLanguageForPath(path);
|
|
1897
|
+
var isMd = lang === 'markdown';
|
|
1898
|
+
|
|
1899
|
+
var toolbar = document.createElement('div');
|
|
1900
|
+
toolbar.className = 'br-editor-toolbar';
|
|
1901
|
+
body.appendChild(toolbar);
|
|
1902
|
+
|
|
1903
|
+
var langLabel = document.createElement('span');
|
|
1904
|
+
langLabel.className = 'br-editor-lang';
|
|
1905
|
+
langLabel.textContent = lang;
|
|
1906
|
+
toolbar.appendChild(langLabel);
|
|
1907
|
+
|
|
1908
|
+
var saveBtn = document.createElement('button');
|
|
1909
|
+
saveBtn.type = 'button';
|
|
1910
|
+
saveBtn.className = 'br-editor-save';
|
|
1911
|
+
saveBtn.textContent = 'Save';
|
|
1912
|
+
saveBtn.disabled = true;
|
|
1913
|
+
toolbar.appendChild(saveBtn);
|
|
1914
|
+
|
|
1915
|
+
var cancelBtn = document.createElement('button');
|
|
1916
|
+
cancelBtn.type = 'button';
|
|
1917
|
+
cancelBtn.className = 'br-editor-cancel';
|
|
1918
|
+
cancelBtn.textContent = 'Cancel';
|
|
1919
|
+
toolbar.appendChild(cancelBtn);
|
|
1920
|
+
|
|
1921
|
+
var vimBtn = document.createElement('button');
|
|
1922
|
+
vimBtn.type = 'button';
|
|
1923
|
+
vimBtn.className = 'br-editor-vim';
|
|
1924
|
+
var vimOn = getVimPref();
|
|
1925
|
+
function syncVimBtn() {
|
|
1926
|
+
vimBtn.textContent = vimOn ? 'vim: on' : 'vim: off';
|
|
1927
|
+
vimBtn.classList.toggle('active', vimOn);
|
|
1928
|
+
}
|
|
1929
|
+
syncVimBtn();
|
|
1930
|
+
toolbar.appendChild(vimBtn);
|
|
1931
|
+
|
|
1932
|
+
var conflictBar = null;
|
|
1933
|
+
var splitWrap, editorWrap, previewWrap, previewHidden, hidePreviewBtn;
|
|
1934
|
+
|
|
1935
|
+
if (isMd) {
|
|
1936
|
+
hidePreviewBtn = document.createElement('button');
|
|
1937
|
+
hidePreviewBtn.type = 'button';
|
|
1938
|
+
hidePreviewBtn.className = 'br-editor-preview-toggle';
|
|
1939
|
+
// Decision is made once on open: walk the source for elements the
|
|
1940
|
+
// live-preview subset doesn't render. If the doc has any of them, the
|
|
1941
|
+
// side preview opens; otherwise it stays closed. Manual toggle still
|
|
1942
|
+
// works for ad-hoc viewing while typing an unsupported element. We do
|
|
1943
|
+
// NOT persist user preference — re-check happens on each open.
|
|
1944
|
+
previewHidden = !markdownHasUnsupportedElements(text);
|
|
1945
|
+
toolbar.appendChild(hidePreviewBtn);
|
|
1946
|
+
|
|
1947
|
+
splitWrap = document.createElement('div');
|
|
1948
|
+
splitWrap.className = 'br-editor-split';
|
|
1949
|
+
body.appendChild(splitWrap);
|
|
1950
|
+
|
|
1951
|
+
editorWrap = document.createElement('div');
|
|
1952
|
+
editorWrap.className = 'br-editor-pane';
|
|
1953
|
+
splitWrap.appendChild(editorWrap);
|
|
1954
|
+
|
|
1955
|
+
var divider = document.createElement('div');
|
|
1956
|
+
divider.className = 'br-editor-divider';
|
|
1957
|
+
splitWrap.appendChild(divider);
|
|
1958
|
+
|
|
1959
|
+
previewWrap = document.createElement('div');
|
|
1960
|
+
previewWrap.className = 'br-editor-preview';
|
|
1961
|
+
if (priorData && priorData.html) {
|
|
1962
|
+
previewWrap.innerHTML = (priorData.frontmatter_html || '') + '<div class="br-md-content">' + priorData.html + '</div>';
|
|
1963
|
+
addCopyButtons(previewWrap);
|
|
1964
|
+
}
|
|
1965
|
+
splitWrap.appendChild(previewWrap);
|
|
1966
|
+
|
|
1967
|
+
var savedPct = null;
|
|
1968
|
+
try { savedPct = parseFloat(localStorage.getItem('markdownr:editor:split')); } catch (e) {}
|
|
1969
|
+
if (savedPct && savedPct > 10 && savedPct < 90) {
|
|
1970
|
+
editorWrap.style.flex = '0 0 ' + savedPct + '%';
|
|
1971
|
+
} else {
|
|
1972
|
+
editorWrap.style.flex = '0 0 50%';
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
function applyPreviewHide() {
|
|
1976
|
+
previewWrap.style.display = previewHidden ? 'none' : '';
|
|
1977
|
+
divider.style.display = previewHidden ? 'none' : '';
|
|
1978
|
+
editorWrap.style.flex = previewHidden ? '1 1 100%' : (editorWrap.style.flex || '0 0 50%');
|
|
1979
|
+
hidePreviewBtn.textContent = previewHidden ? 'Show preview' : 'Hide preview';
|
|
1980
|
+
}
|
|
1981
|
+
applyPreviewHide();
|
|
1982
|
+
hidePreviewBtn.onclick = function() {
|
|
1983
|
+
previewHidden = !previewHidden;
|
|
1984
|
+
applyPreviewHide();
|
|
1985
|
+
};
|
|
1986
|
+
|
|
1987
|
+
makeEditorDivider(splitWrap, divider);
|
|
1988
|
+
} else {
|
|
1989
|
+
editorWrap = document.createElement('div');
|
|
1990
|
+
editorWrap.className = 'br-editor-pane';
|
|
1991
|
+
body.appendChild(editorWrap);
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
var currentEtag = initialEtag;
|
|
1995
|
+
var dirty = false;
|
|
1996
|
+
var saving = false;
|
|
1997
|
+
var instance = null;
|
|
1998
|
+
|
|
1999
|
+
function setDirty(d) {
|
|
2000
|
+
dirty = d;
|
|
2001
|
+
saveBtn.classList.toggle('dirty', d);
|
|
2002
|
+
saveBtn.disabled = !d || saving;
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
function clearConflict() {
|
|
2006
|
+
if (conflictBar) { conflictBar.remove(); conflictBar = null; }
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
function showConflict(info) {
|
|
2010
|
+
clearConflict();
|
|
2011
|
+
conflictBar = document.createElement('div');
|
|
2012
|
+
conflictBar.className = 'br-editor-conflict';
|
|
2013
|
+
conflictBar.innerHTML = '<span>The file changed on disk since you opened it.</span>';
|
|
2014
|
+
var reload = document.createElement('button');
|
|
2015
|
+
reload.type = 'button';
|
|
2016
|
+
reload.textContent = 'Reload';
|
|
2017
|
+
reload.title = 'Discard your edits and load the new content';
|
|
2018
|
+
var overwrite = document.createElement('button');
|
|
2019
|
+
overwrite.type = 'button';
|
|
2020
|
+
overwrite.textContent = 'Overwrite';
|
|
2021
|
+
overwrite.title = 'Force-save your edits, replacing the new content';
|
|
2022
|
+
conflictBar.appendChild(reload);
|
|
2023
|
+
conflictBar.appendChild(overwrite);
|
|
2024
|
+
body.insertBefore(conflictBar, body.children[1] || null);
|
|
2025
|
+
|
|
2026
|
+
reload.onclick = function() {
|
|
2027
|
+
fetch('/__markdownr/api/file/source/' + encodeURI(path), { credentials: 'same-origin' })
|
|
2028
|
+
.then(function(r) {
|
|
2029
|
+
var etag = (r.headers.get('ETag') || '').replace(/^"|"$/g, '');
|
|
2030
|
+
return r.text().then(function(t) { return { text: t, etag: etag }; });
|
|
2031
|
+
})
|
|
2032
|
+
.then(function(s) {
|
|
2033
|
+
instance.setValue(s.text);
|
|
2034
|
+
currentEtag = s.etag;
|
|
2035
|
+
setDirty(false);
|
|
2036
|
+
clearConflict();
|
|
2037
|
+
});
|
|
2038
|
+
};
|
|
2039
|
+
overwrite.onclick = function() {
|
|
2040
|
+
clearConflict();
|
|
2041
|
+
doSave({ force: true });
|
|
2042
|
+
};
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
function doSave(opts) {
|
|
2046
|
+
opts = opts || {};
|
|
2047
|
+
if (saving) return Promise.resolve(false);
|
|
2048
|
+
var content = instance.getValue();
|
|
2049
|
+
var hdrs = { 'Content-Type': 'text/plain; charset=utf-8' };
|
|
2050
|
+
if (currentEtag && !opts.force) hdrs['If-Match'] = '"' + currentEtag + '"';
|
|
2051
|
+
saving = true;
|
|
2052
|
+
saveBtn.disabled = true;
|
|
2053
|
+
var ok = false;
|
|
2054
|
+
return fetch('/__markdownr/api/file/' + encodeURI(path), {
|
|
2055
|
+
method: 'PUT', credentials: 'same-origin', headers: hdrs, body: content
|
|
2056
|
+
}).then(function(r) {
|
|
2057
|
+
if (r.status === 403) {
|
|
2058
|
+
return r.json().then(function(j) { showAdminRequired(j); throw new Error('forbidden'); });
|
|
2059
|
+
}
|
|
2060
|
+
if (r.status === 409) {
|
|
2061
|
+
return r.json().then(function(j) { showConflict(j); throw new Error('conflict'); });
|
|
2062
|
+
}
|
|
2063
|
+
if (!r.ok) {
|
|
2064
|
+
return r.text().then(function(t) { throw new Error('save failed: ' + r.status + ' ' + t); });
|
|
2065
|
+
}
|
|
2066
|
+
return r.json();
|
|
2067
|
+
}).then(function(j) {
|
|
2068
|
+
currentEtag = j.etag;
|
|
2069
|
+
setDirty(false);
|
|
2070
|
+
flashEditorSaved(toolbar);
|
|
2071
|
+
ok = true;
|
|
2072
|
+
}).catch(function(e) {
|
|
2073
|
+
if (e && e.message !== 'conflict' && e.message !== 'forbidden') {
|
|
2074
|
+
alert('Save failed: ' + (e && e.message || 'unknown'));
|
|
2075
|
+
}
|
|
2076
|
+
}).finally(function() {
|
|
2077
|
+
saving = false;
|
|
2078
|
+
setDirty(dirty);
|
|
2079
|
+
}).then(function() { return ok; });
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
function exitEditing(reload) {
|
|
2083
|
+
if (instance) { try { instance.destroy(); } catch (e) {} }
|
|
2084
|
+
popup._editorTeardown = null;
|
|
2085
|
+
popup.classList.remove('is-editing');
|
|
2086
|
+
body.style.display = orig.origBodyDisplay;
|
|
2087
|
+
body.style.flexDirection = orig.origBodyFlexDir;
|
|
2088
|
+
body.style.padding = orig.origBodyPadding;
|
|
2089
|
+
if (reload) {
|
|
2090
|
+
var url = '/browser/api/render/' + encodeURI(path);
|
|
2091
|
+
var qp = popup.getAttribute('data-file-query');
|
|
2092
|
+
if (qp) url += '?' + qp;
|
|
2093
|
+
fetch(url).then(function(r) { return r.json(); }).then(function(d) {
|
|
2094
|
+
popup._editorPriorData = d;
|
|
2095
|
+
dispatchRenderer(popup, d);
|
|
2096
|
+
}).catch(function() {
|
|
2097
|
+
if (priorData) dispatchRenderer(popup, priorData);
|
|
2098
|
+
});
|
|
2099
|
+
} else if (priorData) {
|
|
2100
|
+
dispatchRenderer(popup, priorData);
|
|
2101
|
+
} else {
|
|
2102
|
+
body.innerHTML = orig.origBodyHtml;
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
cancelBtn.onclick = function() {
|
|
2107
|
+
if (dirty && !confirm('Discard unsaved changes?')) return;
|
|
2108
|
+
exitEditing(false);
|
|
2109
|
+
};
|
|
2110
|
+
saveBtn.onclick = function() { if (dirty) doSave(); };
|
|
2111
|
+
vimBtn.onclick = function() {
|
|
2112
|
+
vimOn = !vimOn;
|
|
2113
|
+
setVimPref(vimOn);
|
|
2114
|
+
syncVimBtn();
|
|
2115
|
+
if (instance) instance.setVim(vimOn);
|
|
2116
|
+
};
|
|
2117
|
+
|
|
2118
|
+
var debounceTimer = null;
|
|
2119
|
+
var lastPreviewedText = null;
|
|
2120
|
+
function schedulePreview() {
|
|
2121
|
+
if (!isMd) return;
|
|
2122
|
+
clearTimeout(debounceTimer);
|
|
2123
|
+
debounceTimer = setTimeout(function() {
|
|
2124
|
+
var content = instance.getValue();
|
|
2125
|
+
if (content === lastPreviewedText) return;
|
|
2126
|
+
lastPreviewedText = content;
|
|
2127
|
+
var wikiDir = path.indexOf('/') >= 0 ? path.substring(0, path.lastIndexOf('/')) : '';
|
|
2128
|
+
fetch('/__markdownr/api/render/preview', {
|
|
2129
|
+
method: 'POST', credentials: 'same-origin',
|
|
2130
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2131
|
+
body: JSON.stringify({ content: content, current_wiki_dir: wikiDir })
|
|
2132
|
+
}).then(function(r) { return r.ok ? r.json() : null; }).then(function(j) {
|
|
2133
|
+
if (!j || !previewWrap) return;
|
|
2134
|
+
if (lastPreviewedText !== content) return;
|
|
2135
|
+
previewWrap.innerHTML = (j.frontmatter_html || '') + '<div class="br-md-content">' + j.html + '</div>';
|
|
2136
|
+
addCopyButtons(previewWrap);
|
|
2137
|
+
}).catch(function() {});
|
|
2138
|
+
}, 250);
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
instance = mod.createEditor({
|
|
2142
|
+
parent: editorWrap,
|
|
2143
|
+
doc: text,
|
|
2144
|
+
language: lang,
|
|
2145
|
+
vim: vimOn,
|
|
2146
|
+
onSave: function() { if (dirty) doSave(); },
|
|
2147
|
+
onChange: function(newText) {
|
|
2148
|
+
setDirty(newText !== text);
|
|
2149
|
+
if (isMd) schedulePreview();
|
|
2150
|
+
}
|
|
2151
|
+
});
|
|
2152
|
+
if (instance.setSaveHandler) {
|
|
2153
|
+
instance.setSaveHandler(function() {
|
|
2154
|
+
return dirty ? doSave() : Promise.resolve(true);
|
|
2155
|
+
});
|
|
2156
|
+
}
|
|
2157
|
+
if (instance.setQuitHandler) {
|
|
2158
|
+
instance.setQuitHandler(
|
|
2159
|
+
function(opts) { exitEditing(!!(opts && opts.saved)); },
|
|
2160
|
+
function() { return dirty; }
|
|
2161
|
+
);
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
popup._editorTeardown = function() {
|
|
2165
|
+
if (instance) { try { instance.destroy(); } catch (e) {} }
|
|
2166
|
+
popup._editorTeardown = null;
|
|
2167
|
+
};
|
|
2168
|
+
popup._editorIsDirty = function() { return dirty; };
|
|
2169
|
+
|
|
2170
|
+
setTimeout(function() { try { instance.focus(); } catch (e) {} }, 30);
|
|
2171
|
+
}
|
|
2172
|
+
|
|
1428
2173
|
// ── Markdown renderer ──
|
|
1429
2174
|
|
|
1430
2175
|
function renderMarkdown(popup, data) {
|
|
1431
2176
|
var body = popup.querySelector('.br-popup-body');
|
|
1432
2177
|
body.innerHTML = '';
|
|
1433
2178
|
body.style.padding = '0';
|
|
2179
|
+
body.style.display = '';
|
|
2180
|
+
body.style.flexDirection = '';
|
|
2181
|
+
popup.classList.remove('is-editing');
|
|
1434
2182
|
popup.classList.add('wide');
|
|
1435
2183
|
|
|
1436
2184
|
if (data.frontmatter_html) {
|
|
@@ -1444,6 +2192,9 @@
|
|
|
1444
2192
|
content.className = 'br-md-content';
|
|
1445
2193
|
content.innerHTML = data.html;
|
|
1446
2194
|
body.appendChild(content);
|
|
2195
|
+
addCopyButtons(content);
|
|
2196
|
+
|
|
2197
|
+
addEditButton(popup);
|
|
1447
2198
|
}
|
|
1448
2199
|
|
|
1449
2200
|
// ── Code renderer ──
|
|
@@ -1454,6 +2205,7 @@
|
|
|
1454
2205
|
body.style.display = 'flex';
|
|
1455
2206
|
body.style.flexDirection = 'column';
|
|
1456
2207
|
body.style.padding = '0';
|
|
2208
|
+
popup.classList.remove('is-editing');
|
|
1457
2209
|
popup.classList.add('wide');
|
|
1458
2210
|
|
|
1459
2211
|
var toolbar = document.createElement('div');
|
|
@@ -1465,6 +2217,62 @@
|
|
|
1465
2217
|
code.className = 'br-code-view';
|
|
1466
2218
|
code.innerHTML = data.html;
|
|
1467
2219
|
body.appendChild(code);
|
|
2220
|
+
addCopyButtons(body);
|
|
2221
|
+
|
|
2222
|
+
addEditButton(popup);
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2225
|
+
// ── Copy-to-clipboard buttons on <pre> blocks ──
|
|
2226
|
+
|
|
2227
|
+
var COPY_ICON_SVG = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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>';
|
|
2228
|
+
var CHECK_ICON_SVG = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>';
|
|
2229
|
+
|
|
2230
|
+
function legacyCopy(text) {
|
|
2231
|
+
var ta = document.createElement('textarea');
|
|
2232
|
+
ta.value = text;
|
|
2233
|
+
ta.style.position = 'fixed';
|
|
2234
|
+
ta.style.left = '-9999px';
|
|
2235
|
+
document.body.appendChild(ta);
|
|
2236
|
+
ta.select();
|
|
2237
|
+
try { document.execCommand('copy'); } catch (e) {}
|
|
2238
|
+
document.body.removeChild(ta);
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
function addCopyButtons(root) {
|
|
2242
|
+
var pres = root.querySelectorAll('.br-md-content pre, pre.br-code-view');
|
|
2243
|
+
pres.forEach(function(pre) {
|
|
2244
|
+
if (pre.querySelector(':scope > .copy-btn')) return;
|
|
2245
|
+
var btn = document.createElement('button');
|
|
2246
|
+
btn.type = 'button';
|
|
2247
|
+
btn.className = 'copy-btn';
|
|
2248
|
+
btn.title = 'Copy';
|
|
2249
|
+
btn.setAttribute('aria-label', 'Copy code');
|
|
2250
|
+
btn.innerHTML = COPY_ICON_SVG;
|
|
2251
|
+
btn.addEventListener('click', function(e) {
|
|
2252
|
+
e.stopPropagation();
|
|
2253
|
+
e.preventDefault();
|
|
2254
|
+
var clone = pre.cloneNode(true);
|
|
2255
|
+
var existing = clone.querySelector('.copy-btn');
|
|
2256
|
+
if (existing) existing.remove();
|
|
2257
|
+
var codeEl = clone.querySelector('code');
|
|
2258
|
+
var text = (codeEl || clone).textContent;
|
|
2259
|
+
var done = function() {
|
|
2260
|
+
btn.classList.add('copied');
|
|
2261
|
+
btn.innerHTML = CHECK_ICON_SVG;
|
|
2262
|
+
setTimeout(function() {
|
|
2263
|
+
btn.classList.remove('copied');
|
|
2264
|
+
btn.innerHTML = COPY_ICON_SVG;
|
|
2265
|
+
}, 1500);
|
|
2266
|
+
};
|
|
2267
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
2268
|
+
navigator.clipboard.writeText(text).then(done, function() { legacyCopy(text); done(); });
|
|
2269
|
+
} else {
|
|
2270
|
+
legacyCopy(text);
|
|
2271
|
+
done();
|
|
2272
|
+
}
|
|
2273
|
+
});
|
|
2274
|
+
pre.appendChild(btn);
|
|
2275
|
+
});
|
|
1468
2276
|
}
|
|
1469
2277
|
|
|
1470
2278
|
// ── Download renderer ──
|
|
@@ -1634,6 +2442,7 @@
|
|
|
1634
2442
|
if (openOpts.colFiltersVisible) initOpts.colFiltersVisible = openOpts.colFiltersVisible;
|
|
1635
2443
|
else if (savedMatches && savedObj.colFiltersVisible) initOpts.colFiltersVisible = savedObj.colFiltersVisible;
|
|
1636
2444
|
if (openOpts.showRowNumbers) initOpts.showRowNumbers = openOpts.showRowNumbers;
|
|
2445
|
+
if (openOpts.hiddenValues) initOpts.hiddenValues = openOpts.hiddenValues;
|
|
1637
2446
|
if (openOpts.colWidths) {
|
|
1638
2447
|
initOpts.colWidths = openOpts.colWidths;
|
|
1639
2448
|
} else if (savedMatches && savedObj.colWidths) {
|
|
@@ -1766,7 +2575,9 @@
|
|
|
1766
2575
|
var searchTerms = [];
|
|
1767
2576
|
var highlightMode = null; // null, 'cells', or 'rows'
|
|
1768
2577
|
var highlightTerms = [];
|
|
2578
|
+
var hiddenValues = (initOpts.hiddenValues || []).slice();
|
|
1769
2579
|
var dirtyRows = {};
|
|
2580
|
+
var dirtyBypassIds = {}; // rowIdx → true for rows kept visible only because they're dirty
|
|
1770
2581
|
var selectedRowIdx = null;
|
|
1771
2582
|
var editingCell = null;
|
|
1772
2583
|
var validationErrors = {}; // { rowIndex: [{message, fields}] }
|
|
@@ -1780,7 +2591,7 @@
|
|
|
1780
2591
|
var showRowNumbers = !!initOpts.showRowNumbers;
|
|
1781
2592
|
var colWidths = initOpts.colWidths || {};
|
|
1782
2593
|
var editMode = false;
|
|
1783
|
-
var isReadonly = !!data.readonly;
|
|
2594
|
+
var isReadonly = !!data.readonly || !allowCsvEditor;
|
|
1784
2595
|
|
|
1785
2596
|
var searchWrap = document.createElement('div');
|
|
1786
2597
|
searchWrap.className = 'br-search-wrap';
|
|
@@ -1864,8 +2675,12 @@
|
|
|
1864
2675
|
});
|
|
1865
2676
|
}
|
|
1866
2677
|
|
|
1867
|
-
// In highlight mode, show all rows (only apply col filters); otherwise filter normally
|
|
2678
|
+
// In highlight mode, show all rows (only apply col filters); otherwise filter normally.
|
|
2679
|
+
// Dirty rows always stay visible so an in-progress edit doesn't vanish when its value
|
|
2680
|
+
// no longer matches; those bypass rows get flagged so we can render them in red.
|
|
2681
|
+
dirtyBypassIds = {};
|
|
1868
2682
|
displayRows = originalRows.filter(function(row) {
|
|
2683
|
+
var matches = true;
|
|
1869
2684
|
if (!highlightMode && searchTerms.length > 0) {
|
|
1870
2685
|
var globalMatch = searchTerms.every(function(term) {
|
|
1871
2686
|
for (var i = 0; i < columns.length; i++) {
|
|
@@ -1873,17 +2688,30 @@
|
|
|
1873
2688
|
}
|
|
1874
2689
|
return false;
|
|
1875
2690
|
});
|
|
1876
|
-
if (!globalMatch)
|
|
2691
|
+
if (!globalMatch) matches = false;
|
|
1877
2692
|
}
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
2693
|
+
if (matches) {
|
|
2694
|
+
for (var f = 0; f < activeColFilters.length; f++) {
|
|
2695
|
+
var af = activeColFilters[f];
|
|
2696
|
+
var cellVal = getCellValue(row, af.ci);
|
|
2697
|
+
for (var t = 0; t < af.terms.length; t++) {
|
|
2698
|
+
if (!termMatchesCell(af.terms[t], cellVal)) { matches = false; break; }
|
|
2699
|
+
}
|
|
2700
|
+
if (!matches) break;
|
|
1884
2701
|
}
|
|
1885
2702
|
}
|
|
1886
|
-
|
|
2703
|
+
if (matches && hiddenValues.length > 0) {
|
|
2704
|
+
for (var h = 0; h < hiddenValues.length; h++) {
|
|
2705
|
+
var hv = hiddenValues[h];
|
|
2706
|
+
for (var hi = 0; hi < columns.length; hi++) {
|
|
2707
|
+
if (getCellValue(row, hi) === hv) { matches = false; break; }
|
|
2708
|
+
}
|
|
2709
|
+
if (!matches) break;
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
if (matches) return true;
|
|
2713
|
+
if (dirtyRows[row[0]]) { dirtyBypassIds[row[0]] = true; return true; }
|
|
2714
|
+
return false;
|
|
1887
2715
|
});
|
|
1888
2716
|
|
|
1889
2717
|
if (sortCol >= 0 && sortDirection !== 0) {
|
|
@@ -1937,6 +2765,333 @@
|
|
|
1937
2765
|
|
|
1938
2766
|
var hasValidation = Object.keys(validationErrors).length > 0;
|
|
1939
2767
|
|
|
2768
|
+
function showHeaderContextMenu(colIdx, x, y) {
|
|
2769
|
+
var col = columns[colIdx];
|
|
2770
|
+
var menu = document.createElement('div');
|
|
2771
|
+
menu.className = 'br-context-menu';
|
|
2772
|
+
menu.style.left = x + 'px';
|
|
2773
|
+
menu.style.top = y + 'px';
|
|
2774
|
+
|
|
2775
|
+
function addItem(label, onClick) {
|
|
2776
|
+
var btn = document.createElement('button');
|
|
2777
|
+
btn.className = 'br-context-menu-item default';
|
|
2778
|
+
btn.textContent = label;
|
|
2779
|
+
btn.addEventListener('click', function() { dismissContextMenu(); onClick(); });
|
|
2780
|
+
menu.appendChild(btn);
|
|
2781
|
+
}
|
|
2782
|
+
function addSep() {
|
|
2783
|
+
var sep = document.createElement('div');
|
|
2784
|
+
sep.className = 'br-context-menu-sep';
|
|
2785
|
+
menu.appendChild(sep);
|
|
2786
|
+
}
|
|
2787
|
+
|
|
2788
|
+
// Group 1: summaries
|
|
2789
|
+
addItem('Stats', function() { showColumnStats(colIdx, x, y); });
|
|
2790
|
+
addItem('Distinct values', function() { showColumnDistinct(colIdx, x, y); });
|
|
2791
|
+
addSep();
|
|
2792
|
+
|
|
2793
|
+
// Group 2: quick filters
|
|
2794
|
+
addItem('Filter: is blank', function() { setColumnFilter(col.key, '^$'); });
|
|
2795
|
+
addItem('Filter: is not blank', function() { setColumnFilter(col.key, '.'); });
|
|
2796
|
+
addSep();
|
|
2797
|
+
|
|
2798
|
+
// Group 3: export
|
|
2799
|
+
addItem('Copy column', function() { copyColumnValues(colIdx); });
|
|
2800
|
+
addSep();
|
|
2801
|
+
|
|
2802
|
+
// Group 4: width
|
|
2803
|
+
addItem('Auto-width', function() { autoFitColumn(colIdx); });
|
|
2804
|
+
addItem('Reset width', function() { resetColumnWidth(col.key); });
|
|
2805
|
+
|
|
2806
|
+
document.body.appendChild(menu);
|
|
2807
|
+
}
|
|
2808
|
+
|
|
2809
|
+
// Collect raw (non-FK-resolved) numeric values for visible rows.
|
|
2810
|
+
function collectNumericValues(colIdx) {
|
|
2811
|
+
var col = columns[colIdx];
|
|
2812
|
+
var vals = [];
|
|
2813
|
+
displayRows.forEach(function(row) {
|
|
2814
|
+
var v = row[colIdx + 1];
|
|
2815
|
+
if (dirtyRows[row[0]] && dirtyRows[row[0]][col.key] !== undefined) v = dirtyRows[row[0]][col.key];
|
|
2816
|
+
if (v === null || v === undefined || v === '') return;
|
|
2817
|
+
var n = parseFloat(String(v).replace(/[$,]/g, ''));
|
|
2818
|
+
if (!isNaN(n) && isFinite(n)) vals.push(n);
|
|
2819
|
+
});
|
|
2820
|
+
return vals;
|
|
2821
|
+
}
|
|
2822
|
+
|
|
2823
|
+
function formatStatValue(col, n) {
|
|
2824
|
+
if (isCurrencyCol(col)) return formatCurrency(n);
|
|
2825
|
+
if (col.type === 'integer' && Number.isInteger(n)) return n.toLocaleString();
|
|
2826
|
+
return n.toLocaleString(undefined, { maximumFractionDigits: 6 });
|
|
2827
|
+
}
|
|
2828
|
+
|
|
2829
|
+
function showColumnStats(colIdx, x, y) {
|
|
2830
|
+
var col = columns[colIdx];
|
|
2831
|
+
var vals = collectNumericValues(colIdx);
|
|
2832
|
+
var body;
|
|
2833
|
+
if (vals.length === 0) {
|
|
2834
|
+
body = '<div style="padding:0.75rem 1rem;color:#888;">No numeric values in visible rows.</div>';
|
|
2835
|
+
} else {
|
|
2836
|
+
var positives = vals.filter(function(v) { return v > 0; });
|
|
2837
|
+
var negatives = vals.filter(function(v) { return v < 0; });
|
|
2838
|
+
var sections = [{ label: 'All', vals: vals, countSuffix: ' of ' + displayRows.length }];
|
|
2839
|
+
if (positives.length > 0 && negatives.length > 0) {
|
|
2840
|
+
sections.push({ label: 'Positive only', vals: positives, countSuffix: ' of ' + vals.length });
|
|
2841
|
+
sections.push({ label: 'Negative only', vals: negatives, countSuffix: ' of ' + vals.length });
|
|
2842
|
+
}
|
|
2843
|
+
body = '<div style="padding:0.75rem 1rem;">';
|
|
2844
|
+
sections.forEach(function(sec, si) {
|
|
2845
|
+
if (si > 0) body += '<div style="height:0.75rem;"></div>';
|
|
2846
|
+
body += '<div style="font-weight:600;color:#8b6914;margin-bottom:0.25rem;font-size:0.85rem;">' + escHtml(sec.label) + '</div>';
|
|
2847
|
+
body += buildStatsTable(col, sec.vals, sec.countSuffix);
|
|
2848
|
+
});
|
|
2849
|
+
body += '</div>';
|
|
2850
|
+
}
|
|
2851
|
+
createPopup({ title: 'Stats — ' + col.title, html: body, x: x, y: y });
|
|
2852
|
+
}
|
|
2853
|
+
|
|
2854
|
+
function buildStatsTable(col, vals, countSuffix) {
|
|
2855
|
+
var sum = vals.reduce(function(a, b) { return a + b; }, 0);
|
|
2856
|
+
var mean = sum / vals.length;
|
|
2857
|
+
var sorted = vals.slice().sort(function(a, b) { return a - b; });
|
|
2858
|
+
var median = sorted.length % 2 === 1
|
|
2859
|
+
? sorted[(sorted.length - 1) / 2]
|
|
2860
|
+
: (sorted[sorted.length / 2 - 1] + sorted[sorted.length / 2]) / 2;
|
|
2861
|
+
var rows = [
|
|
2862
|
+
['Sum', formatStatValue(col, sum)],
|
|
2863
|
+
['Average', formatStatValue(col, mean)],
|
|
2864
|
+
['Median', formatStatValue(col, median)],
|
|
2865
|
+
['Min', formatStatValue(col, sorted[0])],
|
|
2866
|
+
['Max', formatStatValue(col, sorted[sorted.length - 1])],
|
|
2867
|
+
['Count', vals.length + (countSuffix || '')]
|
|
2868
|
+
];
|
|
2869
|
+
var html = '<table style="width:100%;border-collapse:collapse;border:1px solid #e8e4dc;">';
|
|
2870
|
+
rows.forEach(function(r, i) {
|
|
2871
|
+
var bg = i % 2 === 0 ? '#faf8f4' : '#ffffff';
|
|
2872
|
+
html += '<tr style="background:' + bg + ';">' +
|
|
2873
|
+
'<td style="padding:0.3rem 0.75rem;color:#666;">' + escHtml(r[0]) + '</td>' +
|
|
2874
|
+
'<td style="padding:0.3rem 0.75rem;text-align:right;font-weight:600;color:#3a3a3a;">' + escHtml(r[1]) + '</td>' +
|
|
2875
|
+
'</tr>';
|
|
2876
|
+
});
|
|
2877
|
+
html += '</table>';
|
|
2878
|
+
return html;
|
|
2879
|
+
}
|
|
2880
|
+
|
|
2881
|
+
function showColumnDistinct(colIdx, x, y) {
|
|
2882
|
+
var col = columns[colIdx];
|
|
2883
|
+
var counts = {};
|
|
2884
|
+
var order = [];
|
|
2885
|
+
displayRows.forEach(function(row) {
|
|
2886
|
+
var v = getCellValue(row, colIdx);
|
|
2887
|
+
if (!(v in counts)) { counts[v] = 0; order.push(v); }
|
|
2888
|
+
counts[v]++;
|
|
2889
|
+
});
|
|
2890
|
+
order.sort(function(a, b) {
|
|
2891
|
+
if (counts[b] !== counts[a]) return counts[b] - counts[a];
|
|
2892
|
+
return String(a).localeCompare(String(b));
|
|
2893
|
+
});
|
|
2894
|
+
var body;
|
|
2895
|
+
if (order.length === 0) {
|
|
2896
|
+
body = '<div style="padding:0.75rem 1rem;color:#888;">No visible rows.</div>';
|
|
2897
|
+
} else {
|
|
2898
|
+
var maxCount = counts[order[0]];
|
|
2899
|
+
body = '<div style="padding:0.5rem 0.75rem;">' +
|
|
2900
|
+
'<div style="color:#888;margin-bottom:0.5rem;">' + order.length + ' distinct value' +
|
|
2901
|
+
(order.length === 1 ? '' : 's') + ' in ' + displayRows.length + ' visible row' +
|
|
2902
|
+
(displayRows.length === 1 ? '' : 's') + '</div>' +
|
|
2903
|
+
'<table style="width:100%;border-collapse:collapse;">';
|
|
2904
|
+
order.forEach(function(v) {
|
|
2905
|
+
var label = v === '' ? '<span style="color:#bbb;font-style:italic;">(blank)</span>' : escHtml(v);
|
|
2906
|
+
var pct = Math.max(2, Math.round(counts[v] / maxCount * 100));
|
|
2907
|
+
var bar = '<div style="position:relative;height:1.3em;min-width:60px;">' +
|
|
2908
|
+
'<div style="position:absolute;left:0;top:0;bottom:0;width:' + pct + '%;background:rgba(212,185,106,0.45);border-radius:2px;"></div>' +
|
|
2909
|
+
'<span style="position:relative;padding:0 0.4rem;font-weight:600;color:#5a4410;">' + counts[v] + '</span>' +
|
|
2910
|
+
'</div>';
|
|
2911
|
+
body += '<tr>' +
|
|
2912
|
+
'<td style="padding:0.2rem 0.75rem 0.2rem 0;">' + label + '</td>' +
|
|
2913
|
+
'<td style="padding:0.2rem 0;text-align:right;width:40%;">' + bar + '</td>' +
|
|
2914
|
+
'</tr>';
|
|
2915
|
+
});
|
|
2916
|
+
body += '</table></div>';
|
|
2917
|
+
}
|
|
2918
|
+
createPopup({ title: 'Distinct — ' + col.title, html: body, x: x, y: y });
|
|
2919
|
+
}
|
|
2920
|
+
|
|
2921
|
+
function setColumnFilter(colKey, expr) {
|
|
2922
|
+
colFilterValues[colKey] = expr;
|
|
2923
|
+
if (!colFiltersVisible) {
|
|
2924
|
+
colFiltersVisible = true;
|
|
2925
|
+
colFilterToggle.textContent = '▴';
|
|
2926
|
+
}
|
|
2927
|
+
render();
|
|
2928
|
+
var input = tableWrap.querySelector('[data-col-filter="' + colKey + '"]');
|
|
2929
|
+
if (input) { input.focus(); input.selectionStart = input.selectionEnd = input.value.length; }
|
|
2930
|
+
}
|
|
2931
|
+
|
|
2932
|
+
function copyColumnValues(colIdx) {
|
|
2933
|
+
var col = columns[colIdx];
|
|
2934
|
+
var lines = [col.title].concat(displayRows.map(function(row) { return getCellValue(row, colIdx); }));
|
|
2935
|
+
copyToClipboard(lines.join('\n'));
|
|
2936
|
+
}
|
|
2937
|
+
|
|
2938
|
+
function copyToClipboard(text) {
|
|
2939
|
+
function fallback() {
|
|
2940
|
+
var ta = document.createElement('textarea');
|
|
2941
|
+
ta.value = text;
|
|
2942
|
+
ta.style.position = 'fixed';
|
|
2943
|
+
ta.style.top = '0'; ta.style.left = '0';
|
|
2944
|
+
ta.style.opacity = '0';
|
|
2945
|
+
document.body.appendChild(ta);
|
|
2946
|
+
ta.focus(); ta.select();
|
|
2947
|
+
try { document.execCommand('copy'); } catch (e) {}
|
|
2948
|
+
ta.remove();
|
|
2949
|
+
}
|
|
2950
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
2951
|
+
navigator.clipboard.writeText(text).catch(fallback);
|
|
2952
|
+
} else {
|
|
2953
|
+
fallback();
|
|
2954
|
+
}
|
|
2955
|
+
}
|
|
2956
|
+
|
|
2957
|
+
function autoFitColumn(colIdx) {
|
|
2958
|
+
var col = columns[colIdx];
|
|
2959
|
+
var sampleCell = tableWrap.querySelector('td[data-col]');
|
|
2960
|
+
var sampleTh = tableWrap.querySelector('th[data-sort-col]');
|
|
2961
|
+
if (!sampleCell || !sampleTh) return;
|
|
2962
|
+
var cellFont = getComputedStyle(sampleCell).font;
|
|
2963
|
+
var headerFont = getComputedStyle(sampleTh).font;
|
|
2964
|
+
var ctx = document.createElement('canvas').getContext('2d');
|
|
2965
|
+
ctx.font = headerFont;
|
|
2966
|
+
var maxW = ctx.measureText(col.title).width + 28; // header padding + sort arrow
|
|
2967
|
+
ctx.font = cellFont;
|
|
2968
|
+
displayRows.forEach(function(row) {
|
|
2969
|
+
var w = ctx.measureText(getCellValue(row, colIdx)).width;
|
|
2970
|
+
if (w > maxW) maxW = w;
|
|
2971
|
+
});
|
|
2972
|
+
colWidths[col.key] = Math.min(Math.ceil(maxW) + 20, 500);
|
|
2973
|
+
render();
|
|
2974
|
+
}
|
|
2975
|
+
|
|
2976
|
+
function resetColumnWidth(colKey) {
|
|
2977
|
+
delete colWidths[colKey];
|
|
2978
|
+
render();
|
|
2979
|
+
}
|
|
2980
|
+
|
|
2981
|
+
function showTableCornerContextMenu(x, y) {
|
|
2982
|
+
var menu = document.createElement('div');
|
|
2983
|
+
menu.className = 'br-context-menu';
|
|
2984
|
+
menu.style.left = x + 'px';
|
|
2985
|
+
menu.style.top = y + 'px';
|
|
2986
|
+
|
|
2987
|
+
function addItem(icon, label, onClick) {
|
|
2988
|
+
var btn = document.createElement('button');
|
|
2989
|
+
btn.className = 'br-context-menu-item default';
|
|
2990
|
+
btn.innerHTML = '<span style="display:inline-block;width:1.6em;text-align:center;">' + icon + '</span>' + escHtml(label);
|
|
2991
|
+
btn.addEventListener('click', function() { dismissContextMenu(); onClick(); });
|
|
2992
|
+
menu.appendChild(btn);
|
|
2993
|
+
}
|
|
2994
|
+
function addSep() {
|
|
2995
|
+
var sep = document.createElement('div');
|
|
2996
|
+
sep.className = 'br-context-menu-sep';
|
|
2997
|
+
menu.appendChild(sep);
|
|
2998
|
+
}
|
|
2999
|
+
|
|
3000
|
+
addItem('📋', 'Copy as CSV', function() { copyTableAsCsv(); });
|
|
3001
|
+
addItem('📋', 'Copy as markdown', function() { copyTableAsMarkdown(); });
|
|
3002
|
+
addItem('🖨', 'Print', function() { printTable(); });
|
|
3003
|
+
addSep();
|
|
3004
|
+
|
|
3005
|
+
var canValidate = dbKey && dbKey !== '_unmapped' && !isReadonly;
|
|
3006
|
+
if (canValidate) {
|
|
3007
|
+
addItem('🔥', 'Validate all rows', function() { runValidateAll(); });
|
|
3008
|
+
}
|
|
3009
|
+
addItem('#', (showRowNumbers ? 'Hide' : 'Show') + ' row numbers', function() { toggleRowNumbers(); });
|
|
3010
|
+
addSep();
|
|
3011
|
+
|
|
3012
|
+
if (dbKey && dbKey !== '_unmapped') {
|
|
3013
|
+
addItem('💾', 'Save as default view', function() { saveCurrentAsDefaultView(); });
|
|
3014
|
+
addItem('💾', 'Show saved default view', function() { showSavedDefaultViewPopup(); });
|
|
3015
|
+
addSep();
|
|
3016
|
+
}
|
|
3017
|
+
addItem('💾', 'Show all markdownr localStorage', function() { showAllLocalStoragePopup(); });
|
|
3018
|
+
|
|
3019
|
+
document.body.appendChild(menu);
|
|
3020
|
+
}
|
|
3021
|
+
|
|
3022
|
+
function csvEscape(s) {
|
|
3023
|
+
s = (s === null || s === undefined) ? '' : String(s);
|
|
3024
|
+
if (s.indexOf(',') !== -1 || s.indexOf('"') !== -1 || s.indexOf('\n') !== -1 || s.indexOf('\r') !== -1) {
|
|
3025
|
+
return '"' + s.replace(/"/g, '""') + '"';
|
|
3026
|
+
}
|
|
3027
|
+
return s;
|
|
3028
|
+
}
|
|
3029
|
+
|
|
3030
|
+
function mdEscape(s) {
|
|
3031
|
+
return ((s === null || s === undefined) ? '' : String(s)).replace(/\|/g, '\\|').replace(/\r?\n/g, '<br>');
|
|
3032
|
+
}
|
|
3033
|
+
|
|
3034
|
+
function copyTableAsCsv() {
|
|
3035
|
+
var lines = [columns.map(function(c) { return csvEscape(c.title); }).join(',')];
|
|
3036
|
+
displayRows.forEach(function(row) {
|
|
3037
|
+
lines.push(columns.map(function(_, ci) { return csvEscape(getCellValue(row, ci)); }).join(','));
|
|
3038
|
+
});
|
|
3039
|
+
copyToClipboard(lines.join('\n'));
|
|
3040
|
+
}
|
|
3041
|
+
|
|
3042
|
+
function copyTableAsMarkdown() {
|
|
3043
|
+
var header = '| ' + columns.map(function(c) { return mdEscape(c.title); }).join(' | ') + ' |';
|
|
3044
|
+
var sep = '| ' + columns.map(function(c) { return isRightAligned(c) ? '---:' : '---'; }).join(' | ') + ' |';
|
|
3045
|
+
var body = displayRows.map(function(row) {
|
|
3046
|
+
return '| ' + columns.map(function(_, ci) { return mdEscape(getCellValue(row, ci)); }).join(' | ') + ' |';
|
|
3047
|
+
});
|
|
3048
|
+
copyToClipboard([header, sep].concat(body).join('\n'));
|
|
3049
|
+
}
|
|
3050
|
+
|
|
3051
|
+
function getDisplayCellValue(row, ci) {
|
|
3052
|
+
var val = getCellValue(row, ci);
|
|
3053
|
+
if (val !== '' && isCurrencyCol(columns[ci])) return formatCurrency(val);
|
|
3054
|
+
return val;
|
|
3055
|
+
}
|
|
3056
|
+
|
|
3057
|
+
function printTable() {
|
|
3058
|
+
var titleEl = popup.querySelector('.br-popup-title');
|
|
3059
|
+
var titleText = titleEl ? titleEl.textContent : 'Table';
|
|
3060
|
+
var w = window.open('', '_blank');
|
|
3061
|
+
if (!w) { alert('Pop-up blocked. Allow pop-ups to print.'); return; }
|
|
3062
|
+
var alignAttr = function(col) { return isRightAligned(col) ? ' style="text-align:right"' : ''; };
|
|
3063
|
+
var headerHtml = columns.map(function(c) {
|
|
3064
|
+
return '<th' + alignAttr(c) + '>' + escHtml(c.title) + '</th>';
|
|
3065
|
+
}).join('');
|
|
3066
|
+
var bodyHtml = displayRows.map(function(row) {
|
|
3067
|
+
return '<tr>' + columns.map(function(_, ci) {
|
|
3068
|
+
var v = getDisplayCellValue(row, ci);
|
|
3069
|
+
return '<td' + alignAttr(columns[ci]) + '>' + (v === '' ? '' : escHtml(v)) + '</td>';
|
|
3070
|
+
}).join('') + '</tr>';
|
|
3071
|
+
}).join('');
|
|
3072
|
+
var doc = '<!doctype html><html><head><meta charset="utf-8"><title>' + escHtml(titleText) + '</title>' +
|
|
3073
|
+
'<style>' +
|
|
3074
|
+
'body{font-family:-apple-system,BlinkMacSystemFont,Helvetica,Arial,sans-serif;color:#000;margin:1rem;}' +
|
|
3075
|
+
'h1{font-size:1.1rem;margin:0 0 0.6rem 0;}' +
|
|
3076
|
+
'.meta{font-size:0.75rem;color:#666;margin-bottom:0.6rem;}' +
|
|
3077
|
+
'table{border-collapse:collapse;width:100%;font-size:11px;}' +
|
|
3078
|
+
'th,td{border:1px solid #999;padding:4px 6px;vertical-align:top;}' +
|
|
3079
|
+
'th{background:#eee;text-align:left;}' +
|
|
3080
|
+
'tbody tr:nth-child(even) td{background:#f6f6f6;}' +
|
|
3081
|
+
'@media print{body{margin:0.4in;} h1{font-size:1rem;} thead{display:table-header-group;} tr{page-break-inside:avoid;}}' +
|
|
3082
|
+
'</style></head><body>' +
|
|
3083
|
+
'<h1>' + escHtml(titleText) + '</h1>' +
|
|
3084
|
+
'<div class="meta">' + displayRows.length + ' row' + (displayRows.length === 1 ? '' : 's') +
|
|
3085
|
+
' · ' + new Date().toLocaleString() + '</div>' +
|
|
3086
|
+
'<table><thead><tr>' + headerHtml + '</tr></thead><tbody>' + bodyHtml + '</tbody></table>' +
|
|
3087
|
+
'<script>window.onload=function(){setTimeout(function(){window.print();},150);};<\/script>' +
|
|
3088
|
+
'</body></html>';
|
|
3089
|
+
w.document.open();
|
|
3090
|
+
w.document.write(doc);
|
|
3091
|
+
w.document.close();
|
|
3092
|
+
}
|
|
3093
|
+
popup._print = printTable;
|
|
3094
|
+
|
|
1940
3095
|
function isRightAligned(col) {
|
|
1941
3096
|
if (col.type === 'integer' || col.type === 'number') return true;
|
|
1942
3097
|
var c = col.constraints || {};
|
|
@@ -1962,15 +3117,20 @@
|
|
|
1962
3117
|
}
|
|
1963
3118
|
|
|
1964
3119
|
function renderTableHTML() {
|
|
3120
|
+
var DEFAULT_MAX_COL_WIDTH = 500;
|
|
1965
3121
|
var cellStyles = columns.map(function(col) {
|
|
1966
3122
|
var parts = [];
|
|
1967
|
-
if (colWidths[col.key])
|
|
3123
|
+
if (colWidths[col.key]) {
|
|
3124
|
+
parts.push('white-space:normal', 'overflow-wrap:anywhere');
|
|
3125
|
+
} else {
|
|
3126
|
+
parts.push('max-width:' + DEFAULT_MAX_COL_WIDTH + 'px', 'overflow:hidden', 'text-overflow:ellipsis');
|
|
3127
|
+
}
|
|
1968
3128
|
if (isRightAligned(col)) parts.push('text-align:right');
|
|
1969
3129
|
return parts.length > 0 ? ' style="' + parts.join(';') + '"' : '';
|
|
1970
3130
|
});
|
|
1971
3131
|
var html = '<table class="br-data-table' + (editMode ? ' edit-mode' : '') + '"><thead><tr>';
|
|
3132
|
+
html += '<th class="br-corner-th" data-table-corner="1" title="Right-click for table actions" style="width:1px;padding:0.4rem 0.3rem;"></th>';
|
|
1972
3133
|
if (hasValidation) html += '<th style="width:1px;padding:0.4rem 0.2rem;"></th>';
|
|
1973
|
-
html += '<th style="width:1px;padding:0.4rem 0.3rem;"></th>';
|
|
1974
3134
|
if (showRowNumbers) html += '<th style="width:1px;padding:0.4rem 0.3rem;color:#999;font-size:0.75rem;">#</th>';
|
|
1975
3135
|
columns.forEach(function(col, ci) {
|
|
1976
3136
|
var cls = '';
|
|
@@ -1979,7 +3139,9 @@
|
|
|
1979
3139
|
else if (ci === sortCol && sortDirection === -1) { cls = ' class="sort-desc"'; arrow += '\u25bc'; }
|
|
1980
3140
|
else { arrow += '\u25b8'; }
|
|
1981
3141
|
arrow += '</span>';
|
|
1982
|
-
var wStyle = colWidths[col.key]
|
|
3142
|
+
var wStyle = colWidths[col.key]
|
|
3143
|
+
? ' style="width:' + colWidths[col.key] + 'px;min-width:' + colWidths[col.key] + 'px;max-width:' + colWidths[col.key] + 'px"'
|
|
3144
|
+
: ' style="max-width:' + DEFAULT_MAX_COL_WIDTH + 'px"';
|
|
1983
3145
|
html += '<th' + cls + ' data-sort-col="' + ci + '"' + wStyle + '>' + escHtml(col.title) + arrow + '<span class="br-col-resize" data-resize-col="' + ci + '"></span></th>';
|
|
1984
3146
|
});
|
|
1985
3147
|
if (reverseRefs.length > 0) {
|
|
@@ -1988,8 +3150,8 @@
|
|
|
1988
3150
|
html += '</tr>';
|
|
1989
3151
|
if (colFiltersVisible) {
|
|
1990
3152
|
html += '<tr class="br-col-filter-row">';
|
|
1991
|
-
if (hasValidation) html += '<td></td>';
|
|
1992
3153
|
html += '<td></td>';
|
|
3154
|
+
if (hasValidation) html += '<td></td>';
|
|
1993
3155
|
if (showRowNumbers) html += '<td></td>';
|
|
1994
3156
|
columns.forEach(function(col) {
|
|
1995
3157
|
var val = colFilterValues[col.key] || '';
|
|
@@ -2003,17 +3165,11 @@
|
|
|
2003
3165
|
displayRows.forEach(function(row, displayIndex) {
|
|
2004
3166
|
var rowIdx = row[0];
|
|
2005
3167
|
var isDirty = !!dirtyRows[rowIdx];
|
|
3168
|
+
var isBypass = !!dirtyBypassIds[rowIdx];
|
|
2006
3169
|
var rowErrors = validationErrors[rowIdx];
|
|
2007
|
-
var rowCls = isDirty ? 'br-row-dirty' : (rowErrors ? 'br-row-invalid' : '');
|
|
3170
|
+
var rowCls = isBypass ? 'br-row-dirty-bypass' : (isDirty ? 'br-row-dirty' : (rowErrors ? 'br-row-invalid' : ''));
|
|
2008
3171
|
if (selectedRowIdx === rowIdx) rowCls = (rowCls ? rowCls + ' ' : '') + 'br-row-selected';
|
|
2009
3172
|
html += '<tr data-row-idx="' + rowIdx + '"' + (rowCls ? ' class="' + rowCls + '"' : '') + '>';
|
|
2010
|
-
if (hasValidation) {
|
|
2011
|
-
if (rowErrors) {
|
|
2012
|
-
html += '<td class="br-val-cell"><span class="br-val-icon" data-val-row="' + rowIdx + '" title="Validation errors">\ud83d\udd25</span></td>';
|
|
2013
|
-
} else {
|
|
2014
|
-
html += '<td class="br-val-cell"></td>';
|
|
2015
|
-
}
|
|
2016
|
-
}
|
|
2017
3173
|
var rowCellInvalid = rowHasCellErrors(rowIdx);
|
|
2018
3174
|
if (isDirty) {
|
|
2019
3175
|
var saveCls = 'br-row-save' + (rowCellInvalid ? ' has-errors' : '');
|
|
@@ -2025,6 +3181,13 @@
|
|
|
2025
3181
|
} else {
|
|
2026
3182
|
html += '<td class="br-row-actions"></td>';
|
|
2027
3183
|
}
|
|
3184
|
+
if (hasValidation) {
|
|
3185
|
+
if (rowErrors) {
|
|
3186
|
+
html += '<td class="br-val-cell"><span class="br-val-icon" data-val-row="' + rowIdx + '" title="Validation errors">\ud83d\udd25</span></td>';
|
|
3187
|
+
} else {
|
|
3188
|
+
html += '<td class="br-val-cell"></td>';
|
|
3189
|
+
}
|
|
3190
|
+
}
|
|
2028
3191
|
if (showRowNumbers) html += '<td style="color:#999;font-size:0.75rem;text-align:right;white-space:nowrap;">' + (rowIdx + 1) + '</td>';
|
|
2029
3192
|
for (var i = 1; i < row.length; i++) {
|
|
2030
3193
|
var val = row[i];
|
|
@@ -2047,7 +3210,11 @@
|
|
|
2047
3210
|
var fkClass = (!editMode && colRef) ? ' fk-link' : '';
|
|
2048
3211
|
var fkParts = [];
|
|
2049
3212
|
if (colRef && colRef.color) fkParts.push('background:' + escHtml(colRef.color) + '18');
|
|
2050
|
-
if (colWidths[colKey])
|
|
3213
|
+
if (colWidths[colKey]) {
|
|
3214
|
+
fkParts.push('white-space:normal', 'overflow-wrap:anywhere');
|
|
3215
|
+
} else {
|
|
3216
|
+
fkParts.push('max-width:' + DEFAULT_MAX_COL_WIDTH + 'px', 'overflow:hidden', 'text-overflow:ellipsis');
|
|
3217
|
+
}
|
|
2051
3218
|
if (isRightAligned(columns[i - 1])) fkParts.push('text-align:right');
|
|
2052
3219
|
var fkStyleAttr = fkParts.length > 0 ? ' style="' + fkParts.join(';') + '"' : '';
|
|
2053
3220
|
var fkTitle = cellHasError ? escHtml(cellValidationErrors[cellErrKey]) : 'ID: ' + escHtml(String(val));
|
|
@@ -3066,8 +4233,18 @@
|
|
|
3066
4233
|
render();
|
|
3067
4234
|
});
|
|
3068
4235
|
|
|
3069
|
-
|
|
4236
|
+
function runValidateAll() {
|
|
3070
4237
|
if (!dbKey || dbKey === '_unmapped') return;
|
|
4238
|
+
// Toggle off if validation is currently shown
|
|
4239
|
+
if (Object.keys(validationErrors).length > 0) {
|
|
4240
|
+
validationErrors = {};
|
|
4241
|
+
serverCellErrorKeys.forEach(function(k) { delete cellValidationErrors[k]; });
|
|
4242
|
+
serverCellErrorKeys = [];
|
|
4243
|
+
hasValidation = false;
|
|
4244
|
+
updateValidateBtn();
|
|
4245
|
+
render();
|
|
4246
|
+
return;
|
|
4247
|
+
}
|
|
3071
4248
|
validateBtn.disabled = true;
|
|
3072
4249
|
validateBtn.title = 'Validating\u2026';
|
|
3073
4250
|
var url = '/browser/api/csv/databases/' + encodeURIComponent(dbKey) +
|
|
@@ -3103,7 +4280,8 @@
|
|
|
3103
4280
|
validateBtn.disabled = false;
|
|
3104
4281
|
validateBtn.title = 'Validation failed';
|
|
3105
4282
|
});
|
|
3106
|
-
}
|
|
4283
|
+
}
|
|
4284
|
+
validateBtn.addEventListener('click', runValidateAll);
|
|
3107
4285
|
|
|
3108
4286
|
tableWrap.addEventListener('click', function(e) {
|
|
3109
4287
|
var valIcon = e.target.closest('[data-val-row]');
|
|
@@ -3118,13 +4296,14 @@
|
|
|
3118
4296
|
createPopup({ title: 'Row ' + (rowIdx + 1) + ' \u2014 validation errors', html: errHtml, x: pos.x, y: pos.y });
|
|
3119
4297
|
});
|
|
3120
4298
|
|
|
3121
|
-
|
|
4299
|
+
function toggleRowNumbers() {
|
|
3122
4300
|
showRowNumbers = !showRowNumbers;
|
|
3123
4301
|
rowNumToggle.classList.toggle('active', showRowNumbers);
|
|
3124
4302
|
renderTableHTML();
|
|
3125
|
-
}
|
|
4303
|
+
}
|
|
4304
|
+
rowNumToggle.addEventListener('click', toggleRowNumbers);
|
|
3126
4305
|
|
|
3127
|
-
|
|
4306
|
+
function saveCurrentAsDefaultView() {
|
|
3128
4307
|
var tableKey = data.table || data.table_key;
|
|
3129
4308
|
var viewKey = data.view || data.view_key || 'all';
|
|
3130
4309
|
var extra = {};
|
|
@@ -3139,11 +4318,10 @@
|
|
|
3139
4318
|
saveDefaultViewKey(dbKey, tableKey, viewKey, extra);
|
|
3140
4319
|
saveDefaultBtn.classList.add('active');
|
|
3141
4320
|
saveDefaultBtn.title = 'Saved as default view';
|
|
3142
|
-
}
|
|
4321
|
+
}
|
|
4322
|
+
saveDefaultBtn.addEventListener('click', saveCurrentAsDefaultView);
|
|
3143
4323
|
|
|
3144
|
-
|
|
3145
|
-
e.preventDefault();
|
|
3146
|
-
e.stopPropagation();
|
|
4324
|
+
function showSavedDefaultViewPopup() {
|
|
3147
4325
|
var tableKey = data.table || data.table_key;
|
|
3148
4326
|
var lsKey = 'markdownr:defaultView:' + dbKey + ':' + tableKey;
|
|
3149
4327
|
var saved = getSavedDefaultView(dbKey, tableKey);
|
|
@@ -3166,11 +4344,14 @@
|
|
|
3166
4344
|
} else {
|
|
3167
4345
|
body.innerHTML = '<div style="padding:0.75rem;font-size:0.85rem;color:#888;">No saved default view for this table.</div>';
|
|
3168
4346
|
}
|
|
3169
|
-
}
|
|
3170
|
-
|
|
3171
|
-
rowNumToggle.addEventListener('contextmenu', function(e) {
|
|
4347
|
+
}
|
|
4348
|
+
saveDefaultBtn.addEventListener('contextmenu', function(e) {
|
|
3172
4349
|
e.preventDefault();
|
|
3173
4350
|
e.stopPropagation();
|
|
4351
|
+
showSavedDefaultViewPopup();
|
|
4352
|
+
});
|
|
4353
|
+
|
|
4354
|
+
function showAllLocalStoragePopup() {
|
|
3174
4355
|
var all = {};
|
|
3175
4356
|
for (var i = 0; i < localStorage.length; i++) {
|
|
3176
4357
|
var k = localStorage.key(i);
|
|
@@ -3182,6 +4363,11 @@
|
|
|
3182
4363
|
var pos = nextPosition();
|
|
3183
4364
|
var p = createPopup({ title: 'All markdownr localStorage', x: pos.x, y: pos.y, wide: true });
|
|
3184
4365
|
p.querySelector('.br-popup-body').innerHTML = '<pre style="margin:0;padding:0.75rem;font-size:0.8rem;white-space:pre-wrap;font-family:monospace;">' + escHtml(yaml) + '</pre>';
|
|
4366
|
+
}
|
|
4367
|
+
rowNumToggle.addEventListener('contextmenu', function(e) {
|
|
4368
|
+
e.preventDefault();
|
|
4369
|
+
e.stopPropagation();
|
|
4370
|
+
showAllLocalStoragePopup();
|
|
3185
4371
|
});
|
|
3186
4372
|
|
|
3187
4373
|
colFilterToggle.addEventListener('click', function() {
|
|
@@ -3314,6 +4500,29 @@
|
|
|
3314
4500
|
}
|
|
3315
4501
|
|
|
3316
4502
|
tableWrap.addEventListener('contextmenu', function(e) {
|
|
4503
|
+
// Shift+right-click bypasses our menu and shows the browser's native menu (for DevTools Inspect).
|
|
4504
|
+
if (e.shiftKey) { dismissContextMenu(); return; }
|
|
4505
|
+
if (window.__brInspectNext) {
|
|
4506
|
+
window.__brInspectNext = false;
|
|
4507
|
+
return; // let the native browser context menu show (for DevTools "Inspect")
|
|
4508
|
+
}
|
|
4509
|
+
|
|
4510
|
+
var cornerTh = e.target.closest('th[data-table-corner]');
|
|
4511
|
+
if (cornerTh) {
|
|
4512
|
+
e.preventDefault();
|
|
4513
|
+
dismissContextMenu();
|
|
4514
|
+
showTableCornerContextMenu(e.clientX, e.clientY);
|
|
4515
|
+
return;
|
|
4516
|
+
}
|
|
4517
|
+
|
|
4518
|
+
var th = e.target.closest('th[data-sort-col]');
|
|
4519
|
+
if (th) {
|
|
4520
|
+
e.preventDefault();
|
|
4521
|
+
dismissContextMenu();
|
|
4522
|
+
showHeaderContextMenu(parseInt(th.getAttribute('data-sort-col')), e.clientX, e.clientY);
|
|
4523
|
+
return;
|
|
4524
|
+
}
|
|
4525
|
+
|
|
3317
4526
|
var tr = e.target.closest('tr[data-row-idx]');
|
|
3318
4527
|
if (!tr) return;
|
|
3319
4528
|
var td = e.target.closest('td[data-col]');
|
|
@@ -3445,6 +4654,29 @@
|
|
|
3445
4654
|
});
|
|
3446
4655
|
menu.appendChild(matchRowsBtn);
|
|
3447
4656
|
|
|
4657
|
+
// Hide matched rows
|
|
4658
|
+
var hideBtn = document.createElement('button');
|
|
4659
|
+
hideBtn.className = 'br-context-menu-item default';
|
|
4660
|
+
hideBtn.textContent = 'Hide matched rows';
|
|
4661
|
+
hideBtn.addEventListener('click', function() {
|
|
4662
|
+
dismissContextMenu();
|
|
4663
|
+
if (hiddenValues.indexOf(cellVal) === -1) hiddenValues.push(cellVal);
|
|
4664
|
+
render();
|
|
4665
|
+
});
|
|
4666
|
+
menu.appendChild(hideBtn);
|
|
4667
|
+
|
|
4668
|
+
if (hiddenValues.length > 0) {
|
|
4669
|
+
var unhideBtn = document.createElement('button');
|
|
4670
|
+
unhideBtn.className = 'br-context-menu-item default';
|
|
4671
|
+
unhideBtn.textContent = 'Unhide all';
|
|
4672
|
+
unhideBtn.addEventListener('click', function() {
|
|
4673
|
+
dismissContextMenu();
|
|
4674
|
+
hiddenValues = [];
|
|
4675
|
+
render();
|
|
4676
|
+
});
|
|
4677
|
+
menu.appendChild(unhideBtn);
|
|
4678
|
+
}
|
|
4679
|
+
|
|
3448
4680
|
// Clear matches
|
|
3449
4681
|
var sepClear = document.createElement('div');
|
|
3450
4682
|
sepClear.className = 'br-context-menu-sep';
|
|
@@ -3483,6 +4715,20 @@
|
|
|
3483
4715
|
deleteRow(dbKey, data.table, rowIdx);
|
|
3484
4716
|
});
|
|
3485
4717
|
menu.appendChild(deleteBtn);
|
|
4718
|
+
|
|
4719
|
+
var sepInspect = document.createElement('div');
|
|
4720
|
+
sepInspect.className = 'br-context-menu-sep';
|
|
4721
|
+
menu.appendChild(sepInspect);
|
|
4722
|
+
var inspectBtn = document.createElement('button');
|
|
4723
|
+
inspectBtn.className = 'br-context-menu-item default';
|
|
4724
|
+
inspectBtn.textContent = 'Inspect (or ⇧ + right-click)';
|
|
4725
|
+
inspectBtn.title = 'Click, then right-click the same spot again to open browser Inspect. Or use Shift + right-click directly.';
|
|
4726
|
+
inspectBtn.addEventListener('click', function() {
|
|
4727
|
+
dismissContextMenu();
|
|
4728
|
+
window.__brInspectNext = true;
|
|
4729
|
+
});
|
|
4730
|
+
menu.appendChild(inspectBtn);
|
|
4731
|
+
|
|
3486
4732
|
document.body.appendChild(menu);
|
|
3487
4733
|
|
|
3488
4734
|
// Async: fetch add-on actions for this row and append them to the menu.
|
|
@@ -3779,6 +5025,7 @@
|
|
|
3779
5025
|
showRowNumbers: showRowNumbers
|
|
3780
5026
|
};
|
|
3781
5027
|
if (Object.keys(colWidths).length > 0) state.colWidths = Object.assign({}, colWidths);
|
|
5028
|
+
if (hiddenValues.length > 0) state.hiddenValues = hiddenValues.slice();
|
|
3782
5029
|
return state;
|
|
3783
5030
|
};
|
|
3784
5031
|
|
|
@@ -3794,7 +5041,7 @@
|
|
|
3794
5041
|
csvDatabases = databases;
|
|
3795
5042
|
hasCsvDatabases = Array.isArray(csvDatabases) && csvDatabases.length > 0;
|
|
3796
5043
|
// Refresh any open database popups in the active tab
|
|
3797
|
-
var tabId = activeTabId
|
|
5044
|
+
var tabId = activeTabId;
|
|
3798
5045
|
var popups = document.querySelectorAll('.br-popup[data-tab-id="' + tabId + '"]');
|
|
3799
5046
|
popups.forEach(function(popup) {
|
|
3800
5047
|
if (!popup._getLayoutState) return;
|
|
@@ -3818,8 +5065,9 @@
|
|
|
3818
5065
|
}
|
|
3819
5066
|
|
|
3820
5067
|
function showDatabaseList(databases, opts) {
|
|
5068
|
+
var dbListTitle = document.title + ' Databases';
|
|
3821
5069
|
if (databases.length === 0) {
|
|
3822
|
-
createPopup({ title:
|
|
5070
|
+
createPopup({ title: dbListTitle, html: '<div class="br-empty">No databases configured</div>', pinned: true, noClose: true, x: 8, y: TAB_BAR_HEIGHT + 8 });
|
|
3823
5071
|
return;
|
|
3824
5072
|
}
|
|
3825
5073
|
// Skip straight to single database only when there are no virtual databases
|
|
@@ -3836,7 +5084,7 @@
|
|
|
3836
5084
|
var dupIcon = '<span class="br-dup-btn" data-dup-db="' + escHtml(db.key) + '">+</span>';
|
|
3837
5085
|
html += '<button class="' + cls + '" data-db="' + db.key + '">' + dupIcon + label + '</button>';
|
|
3838
5086
|
});
|
|
3839
|
-
var popup = createPopup({ id: 'db-list', title:
|
|
5087
|
+
var popup = createPopup({ id: 'db-list', title: dbListTitle, pinned: true, noClose: true, x: 8, y: TAB_BAR_HEIGHT + 8 });
|
|
3840
5088
|
popup._getLayoutState = function() { return { type: 'db-list' }; };
|
|
3841
5089
|
popup.querySelector('.br-popup-body').innerHTML = html;
|
|
3842
5090
|
popup.querySelector('.br-popup-body').style.padding = '0.4rem 0.5rem';
|
|
@@ -4352,10 +5600,10 @@
|
|
|
4352
5600
|
{ label: 'Restore layout', action: function() { showRestoreLayoutList(menu); } },
|
|
4353
5601
|
{ label: 'Manage layouts', action: function() { menu.remove(); showManageLayouts(); } },
|
|
4354
5602
|
'sep',
|
|
4355
|
-
{ label: 'Open databases', action: function() { showDatabaseList(csvDatabases, {}); } },
|
|
4356
|
-
{ label: 'Open root directory', action: function() { loadContent('', { title: rootTitle }); } },
|
|
5603
|
+
{ label: 'Open databases', action: function() { menu.remove(); showDatabaseList(csvDatabases, {}); } },
|
|
5604
|
+
{ label: 'Open root directory', action: function() { menu.remove(); loadContent('', { title: rootTitle }); } },
|
|
4357
5605
|
'sep',
|
|
4358
|
-
{ label: 'Reload databases', action: function() { reloadDatabases(); } }
|
|
5606
|
+
{ label: 'Reload databases', action: function() { menu.remove(); reloadDatabases(); } }
|
|
4359
5607
|
];
|
|
4360
5608
|
|
|
4361
5609
|
items.forEach(function(item) {
|
|
@@ -4368,7 +5616,8 @@
|
|
|
4368
5616
|
var btn = document.createElement('button');
|
|
4369
5617
|
btn.className = 'br-context-menu-item default';
|
|
4370
5618
|
btn.textContent = item.label;
|
|
4371
|
-
btn.addEventListener('click', function() {
|
|
5619
|
+
btn.addEventListener('click', function(ev) {
|
|
5620
|
+
ev.stopPropagation();
|
|
4372
5621
|
item.action();
|
|
4373
5622
|
});
|
|
4374
5623
|
menu.appendChild(btn);
|
|
@@ -4385,6 +5634,140 @@
|
|
|
4385
5634
|
}, 0);
|
|
4386
5635
|
});
|
|
4387
5636
|
|
|
5637
|
+
// ── Print topmost popup (Cmd+P / Ctrl+P) ──
|
|
5638
|
+
|
|
5639
|
+
var IS_MAC_PLATFORM = /Mac|iPhone|iPad|iPod/i.test(navigator.platform || '');
|
|
5640
|
+
|
|
5641
|
+
function findTopmostPopup() {
|
|
5642
|
+
var selector = '.br-popup:not(.view-menu)';
|
|
5643
|
+
if (activeTabId) selector += '[data-tab-id="' + activeTabId + '"]';
|
|
5644
|
+
var popups = Array.from(document.querySelectorAll(selector)).filter(function(el) {
|
|
5645
|
+
return el.style.display !== 'none';
|
|
5646
|
+
});
|
|
5647
|
+
if (popups.length === 0) return null;
|
|
5648
|
+
return popups.reduce(function(top, p) {
|
|
5649
|
+
var z = parseInt(p.style.zIndex) || 0;
|
|
5650
|
+
var topZ = parseInt(top.style.zIndex) || 0;
|
|
5651
|
+
return z > topZ ? p : top;
|
|
5652
|
+
});
|
|
5653
|
+
}
|
|
5654
|
+
|
|
5655
|
+
function printPopupGeneric(popup) {
|
|
5656
|
+
var titleEl = popup.querySelector('.br-popup-title');
|
|
5657
|
+
var titleText = titleEl ? titleEl.textContent : 'Popup';
|
|
5658
|
+
var bodyEl = popup.querySelector('.br-popup-body');
|
|
5659
|
+
if (!bodyEl) return;
|
|
5660
|
+
var w = window.open('', '_blank');
|
|
5661
|
+
if (!w) { alert('Pop-up blocked. Allow pop-ups to print.'); return; }
|
|
5662
|
+
var stylesHtml = '';
|
|
5663
|
+
document.querySelectorAll('style').forEach(function(s) {
|
|
5664
|
+
stylesHtml += '<style>' + s.textContent + '</style>';
|
|
5665
|
+
});
|
|
5666
|
+
var doc = '<!doctype html><html><head><meta charset="utf-8"><title>' + escHtml(titleText) + '</title>' +
|
|
5667
|
+
stylesHtml +
|
|
5668
|
+
'<style>' +
|
|
5669
|
+
'body{font-family:-apple-system,BlinkMacSystemFont,Helvetica,Arial,sans-serif;color:#000;margin:1rem;background:#fff;}' +
|
|
5670
|
+
'h1.br-print-title{font-size:1.1rem;margin:0 0 0.6rem 0;}' +
|
|
5671
|
+
'.br-print-body{}' +
|
|
5672
|
+
'@media print{body{margin:0.4in;}}' +
|
|
5673
|
+
'</style></head><body>' +
|
|
5674
|
+
'<h1 class="br-print-title">' + escHtml(titleText) + '</h1>' +
|
|
5675
|
+
'<div class="br-print-body">' + bodyEl.innerHTML + '</div>' +
|
|
5676
|
+
'<script>window.onload=function(){setTimeout(function(){window.print();},150);};<\/script>' +
|
|
5677
|
+
'</body></html>';
|
|
5678
|
+
w.document.open();
|
|
5679
|
+
w.document.write(doc);
|
|
5680
|
+
w.document.close();
|
|
5681
|
+
}
|
|
5682
|
+
|
|
5683
|
+
function printPopup(popup) {
|
|
5684
|
+
if (!popup) return false;
|
|
5685
|
+
if (typeof popup._print === 'function') {
|
|
5686
|
+
popup._print();
|
|
5687
|
+
} else {
|
|
5688
|
+
printPopupGeneric(popup);
|
|
5689
|
+
}
|
|
5690
|
+
return true;
|
|
5691
|
+
}
|
|
5692
|
+
|
|
5693
|
+
document.addEventListener('keydown', function(e) {
|
|
5694
|
+
var key = e.key && e.key.toLowerCase();
|
|
5695
|
+
if (key !== 'p') return;
|
|
5696
|
+
if (e.altKey || e.shiftKey) return;
|
|
5697
|
+
var modifier = IS_MAC_PLATFORM ? e.metaKey : e.ctrlKey;
|
|
5698
|
+
if (!modifier) return;
|
|
5699
|
+
var popup = findTopmostPopup();
|
|
5700
|
+
if (!popup) return;
|
|
5701
|
+
e.preventDefault();
|
|
5702
|
+
e.stopPropagation();
|
|
5703
|
+
printPopup(popup);
|
|
5704
|
+
});
|
|
5705
|
+
|
|
5706
|
+
// ── Refresh topmost popup (Cmd+R / Ctrl+R) ──
|
|
5707
|
+
|
|
5708
|
+
function refreshPopup(popup) {
|
|
5709
|
+
if (!popup || !popup._getLayoutState) return false;
|
|
5710
|
+
var s = popup._getLayoutState();
|
|
5711
|
+
|
|
5712
|
+
if (s.type === 'csv-table') {
|
|
5713
|
+
var hasDirty = popup.querySelector('.br-row-dirty, .br-row-dirty-bypass, .br-row-invalid');
|
|
5714
|
+
if (hasDirty && !confirm('Unsaved edits in this table will be discarded. Refresh anyway?')) return false;
|
|
5715
|
+
}
|
|
5716
|
+
|
|
5717
|
+
if (s.type === 'db-list' || s.type === 'db') {
|
|
5718
|
+
reloadDatabases();
|
|
5719
|
+
return true;
|
|
5720
|
+
}
|
|
5721
|
+
|
|
5722
|
+
var rect = popup.getBoundingClientRect();
|
|
5723
|
+
var geo = {
|
|
5724
|
+
x: parseInt(popup.style.left) || Math.round(rect.left),
|
|
5725
|
+
y: parseInt(popup.style.top) || Math.round(rect.top),
|
|
5726
|
+
width: Math.round(rect.width),
|
|
5727
|
+
height: Math.round(rect.height)
|
|
5728
|
+
};
|
|
5729
|
+
popup.remove();
|
|
5730
|
+
|
|
5731
|
+
switch (s.type) {
|
|
5732
|
+
case 'csv-table':
|
|
5733
|
+
var tableLabel = s.tableLabel || s.tableKey;
|
|
5734
|
+
openCsvTablePopup(s.dbKey, s.tableKey, tableLabel, s.viewKey, s.tableColor, {
|
|
5735
|
+
searchQuery: s.searchQuery,
|
|
5736
|
+
colFilterValues: s.colFilterValues,
|
|
5737
|
+
colFiltersVisible: s.colFiltersVisible,
|
|
5738
|
+
showRowNumbers: s.showRowNumbers,
|
|
5739
|
+
hiddenValues: s.hiddenValues,
|
|
5740
|
+
colWidths: s.colWidths,
|
|
5741
|
+
x: geo.x, y: geo.y, width: geo.width, height: geo.height
|
|
5742
|
+
});
|
|
5743
|
+
return true;
|
|
5744
|
+
case 'db-search':
|
|
5745
|
+
var searchDb = csvDatabases.find(function(d) { return d.key === s.dbKey; });
|
|
5746
|
+
if (searchDb) showDatabaseSearch(searchDb, s.query, { x: geo.x, y: geo.y, width: geo.width, height: geo.height });
|
|
5747
|
+
return true;
|
|
5748
|
+
case 'schema':
|
|
5749
|
+
showTableSchema(s.dbKey, s.tableKey, s.tableColor, geo.x, geo.y, geo.width, geo.height);
|
|
5750
|
+
return true;
|
|
5751
|
+
case 'content':
|
|
5752
|
+
loadContent(s.path, { pos: { x: geo.x, y: geo.y }, width: geo.width, height: geo.height });
|
|
5753
|
+
return true;
|
|
5754
|
+
}
|
|
5755
|
+
return false;
|
|
5756
|
+
}
|
|
5757
|
+
|
|
5758
|
+
document.addEventListener('keydown', function(e) {
|
|
5759
|
+
var key = e.key && e.key.toLowerCase();
|
|
5760
|
+
if (key !== 'r') return;
|
|
5761
|
+
if (e.altKey || e.shiftKey) return;
|
|
5762
|
+
var modifier = IS_MAC_PLATFORM ? e.metaKey : e.ctrlKey;
|
|
5763
|
+
if (!modifier) return;
|
|
5764
|
+
var popup = findTopmostPopup();
|
|
5765
|
+
if (!popup) return;
|
|
5766
|
+
e.preventDefault();
|
|
5767
|
+
e.stopPropagation();
|
|
5768
|
+
refreshPopup(popup);
|
|
5769
|
+
});
|
|
5770
|
+
|
|
4388
5771
|
// ── Start ──
|
|
4389
5772
|
|
|
4390
5773
|
function init() {
|
|
@@ -4395,6 +5778,9 @@
|
|
|
4395
5778
|
activeTabId = firstTab.id;
|
|
4396
5779
|
renderTabBar();
|
|
4397
5780
|
initNewTab(firstTab);
|
|
5781
|
+
if (initialPath) {
|
|
5782
|
+
loadContent(initialPath, { title: initialPath.split('/').pop() });
|
|
5783
|
+
}
|
|
4398
5784
|
}
|
|
4399
5785
|
|
|
4400
5786
|
if (document.readyState === 'loading') {
|