@youtyan/code-viewer 0.13.1 → 0.14.0
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.
- package/README.md +10 -2
- package/package.json +1 -1
- package/web/app.js +293 -14
- package/web/style.css +79 -3
package/README.md
CHANGED
|
@@ -454,9 +454,17 @@ Open Datastores in the global navigation to access:
|
|
|
454
454
|
row-count changes. Double-click a cell to edit inline when Edit mode is on;
|
|
455
455
|
the whole pending edit batch commits atomically.
|
|
456
456
|
- **Detail footer and related panel** — click any cell to open a resizable
|
|
457
|
-
detail footer
|
|
457
|
+
detail footer, where JSON values are pretty-printed and syntax-highlighted;
|
|
458
|
+
foreign-key cells open a related panel showing the
|
|
458
459
|
referenced or referencing rows, with multi-step drill-down breadcrumbs.
|
|
459
|
-
Cells that match the focused row are highlighted in both panels.
|
|
460
|
+
Cells that match the focused row are highlighted in both panels. Once the
|
|
461
|
+
grid has focus, arrow keys move the active cell from data cell to data cell
|
|
462
|
+
and the detail footer follows it, `Enter` follows a foreign key (arrow keys
|
|
463
|
+
alone never fire a related-table query), `Escape` closes whichever panel is
|
|
464
|
+
open, and `Tab` / `Shift+Tab` move between the main grid and the related
|
|
465
|
+
grid. The related panel's reference list keeps each entry on one line with
|
|
466
|
+
the full table name and condition in a tooltip, and its width can be dragged
|
|
467
|
+
and is remembered.
|
|
460
468
|
- **Footer dock with Query History & Session log** — JetBrains-style bottom
|
|
461
469
|
dock that hosts two tabs: the per-database **Query History** (master/detail
|
|
462
470
|
list of saved queries, SSE-synced across tabs) and a **Session log** that
|
package/package.json
CHANGED
package/web/app.js
CHANGED
|
@@ -12478,6 +12478,7 @@ ${frontmatter.yaml}
|
|
|
12478
12478
|
exportAction: "Export",
|
|
12479
12479
|
foreignKeyHint: "Foreign key — click to view related rows",
|
|
12480
12480
|
relatedEmpty: "No matching row in the referenced table",
|
|
12481
|
+
relatedListResize: "Resize the related-reference list",
|
|
12481
12482
|
filteredEmptyTitle: (count) => `No rows match ${count} active filter${count === 1 ? "" : "s"}`,
|
|
12482
12483
|
filteredEmptyHint: "The table was loaded, but the current search or column filters hide every row.",
|
|
12483
12484
|
filteredEmptyAction: "Clear filters",
|
|
@@ -12835,6 +12836,7 @@ ${frontmatter.yaml}
|
|
|
12835
12836
|
exportAction: "エクスポート",
|
|
12836
12837
|
foreignKeyHint: "外部キー: クリックして関連データを表示",
|
|
12837
12838
|
relatedEmpty: "参照先に該当する行がありません",
|
|
12839
|
+
relatedListResize: "関連参照リストの幅を変える",
|
|
12838
12840
|
filteredEmptyTitle: (count) => `フィルタ ${count} 件に一致する行がありません`,
|
|
12839
12841
|
filteredEmptyHint: "表は読み込めていますが、現在の検索/列フィルタですべての行が隠れています。",
|
|
12840
12842
|
filteredEmptyAction: "フィルタ解除",
|
|
@@ -20087,17 +20089,29 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
|
|
|
20087
20089
|
|
|
20088
20090
|
// web-src/views/database/table-grid.ts
|
|
20089
20091
|
var ROW_HEIGHT = 28;
|
|
20092
|
+
var ROWNUM_WIDTH = 50;
|
|
20090
20093
|
var OVERSCAN = 20;
|
|
20091
20094
|
var PAGE_SIZE = 200;
|
|
20092
20095
|
var MAX_PAGE_CACHE_PAGES = 32;
|
|
20093
20096
|
var FILTER_DEBOUNCE_MS = 300;
|
|
20094
20097
|
var DEFAULT_COL_WIDTH = 180;
|
|
20095
20098
|
var CELL_PREVIEW_MAX_CHARS = 4e3;
|
|
20099
|
+
var DETAIL_JSON_HIGHLIGHT_MAX_CHARS = 1e5;
|
|
20096
20100
|
var RELATED_PANEL_DEFAULT_HEIGHT = 320;
|
|
20097
20101
|
var RELATED_PANEL_MIN_HEIGHT = 60;
|
|
20098
20102
|
var DETAIL_PANEL_DEFAULT_HEIGHT = 200;
|
|
20099
20103
|
var DETAIL_PANEL_MIN_HEIGHT = 40;
|
|
20100
20104
|
var PANEL_MAX_RESERVE = 20;
|
|
20105
|
+
var RELATED_LIST_DEFAULT_WIDTH = 200;
|
|
20106
|
+
var RELATED_LIST_MIN_WIDTH = 120;
|
|
20107
|
+
var RELATED_LIST_MAX_WIDTH = 480;
|
|
20108
|
+
var RELATED_LIST_WIDTH_KEY = "code-viewer:db-related-list-width";
|
|
20109
|
+
var ARROW_STEP = {
|
|
20110
|
+
ArrowUp: { row: -1, col: 0 },
|
|
20111
|
+
ArrowDown: { row: 1, col: 0 },
|
|
20112
|
+
ArrowLeft: { row: 0, col: -1 },
|
|
20113
|
+
ArrowRight: { row: 0, col: 1 }
|
|
20114
|
+
};
|
|
20101
20115
|
function createTableGrid(callbacks, options = {}) {
|
|
20102
20116
|
const embedded = options.embedded === true;
|
|
20103
20117
|
const text3 = () => callbacks.getText?.() ?? dbText("en");
|
|
@@ -20222,6 +20236,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
|
|
|
20222
20236
|
filterRowWrap.appendChild(filterRow);
|
|
20223
20237
|
const viewport = document.createElement("div");
|
|
20224
20238
|
viewport.className = "db-grid-viewport";
|
|
20239
|
+
viewport.tabIndex = 0;
|
|
20225
20240
|
const spacer = document.createElement("div");
|
|
20226
20241
|
spacer.className = "db-grid-spacer";
|
|
20227
20242
|
const body = document.createElement("div");
|
|
@@ -20325,6 +20340,156 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
|
|
|
20325
20340
|
function clearActiveCell() {
|
|
20326
20341
|
setActiveCell(-1, -1);
|
|
20327
20342
|
}
|
|
20343
|
+
function syncHorizontalScroll() {
|
|
20344
|
+
headerWrap.scrollLeft = viewport.scrollLeft;
|
|
20345
|
+
filterRowWrap.scrollLeft = viewport.scrollLeft;
|
|
20346
|
+
}
|
|
20347
|
+
function focusCell(rowIndex, row, colIndex) {
|
|
20348
|
+
setSelectedRow(rowIndex, row);
|
|
20349
|
+
setActiveCell(rowIndex, colIndex);
|
|
20350
|
+
viewport.focus({ preventScroll: true });
|
|
20351
|
+
}
|
|
20352
|
+
function moveActiveCell(rowIndex, colIndex) {
|
|
20353
|
+
const rendered = body.children[rowIndex - renderStartRow];
|
|
20354
|
+
focusCell(rowIndex, rendered ?? null, colIndex);
|
|
20355
|
+
scrollCellIntoView(rowIndex, colIndex);
|
|
20356
|
+
showDetailForActiveCell();
|
|
20357
|
+
}
|
|
20358
|
+
function scrollCellIntoView(rowIndex, colIndex) {
|
|
20359
|
+
const viewHeight = viewport.clientHeight;
|
|
20360
|
+
if (viewHeight > 0) {
|
|
20361
|
+
const top = rowIndex * ROW_HEIGHT;
|
|
20362
|
+
const bottom = top + ROW_HEIGHT;
|
|
20363
|
+
if (top < viewport.scrollTop) viewport.scrollTop = top;
|
|
20364
|
+
else if (bottom > viewport.scrollTop + viewHeight) {
|
|
20365
|
+
viewport.scrollTop = bottom - viewHeight;
|
|
20366
|
+
}
|
|
20367
|
+
}
|
|
20368
|
+
const viewWidth = viewport.clientWidth;
|
|
20369
|
+
if (viewWidth <= 0) return;
|
|
20370
|
+
let left = ROWNUM_WIDTH;
|
|
20371
|
+
for (let c2 = 0; c2 < colIndex; c2++) left += getColWidth(columnNames[c2]);
|
|
20372
|
+
const right = left + getColWidth(columnNames[colIndex]);
|
|
20373
|
+
if (left < viewport.scrollLeft) {
|
|
20374
|
+
viewport.scrollLeft = colIndex === 0 ? 0 : left;
|
|
20375
|
+
} else if (right > viewport.scrollLeft + viewWidth) {
|
|
20376
|
+
viewport.scrollLeft = right - viewWidth;
|
|
20377
|
+
}
|
|
20378
|
+
}
|
|
20379
|
+
function cachedRow(rowIndex) {
|
|
20380
|
+
const pageStart = Math.floor(rowIndex / PAGE_SIZE) * PAGE_SIZE;
|
|
20381
|
+
return getCachedPage(pageStart)?.[rowIndex - pageStart];
|
|
20382
|
+
}
|
|
20383
|
+
function cachedCellValue(rowIndex, colIndex) {
|
|
20384
|
+
const row = cachedRow(rowIndex);
|
|
20385
|
+
return row ? row[colIndex] : void 0;
|
|
20386
|
+
}
|
|
20387
|
+
function showDetailForActiveCell() {
|
|
20388
|
+
const rowIndex = activeCellRowIndex;
|
|
20389
|
+
const colIndex = activeCellColIndex;
|
|
20390
|
+
if (rowIndex < 0 || colIndex < 0) return;
|
|
20391
|
+
const value = cachedCellValue(rowIndex, colIndex);
|
|
20392
|
+
if (value !== void 0) {
|
|
20393
|
+
showCellDetail(colIndex, value);
|
|
20394
|
+
return;
|
|
20395
|
+
}
|
|
20396
|
+
const pageStart = Math.floor(rowIndex / PAGE_SIZE) * PAGE_SIZE;
|
|
20397
|
+
void ensurePage(pageStart).then(() => {
|
|
20398
|
+
if (activeCellRowIndex !== rowIndex || activeCellColIndex !== colIndex) {
|
|
20399
|
+
return;
|
|
20400
|
+
}
|
|
20401
|
+
const loaded = cachedCellValue(rowIndex, colIndex);
|
|
20402
|
+
if (loaded !== void 0) showCellDetail(colIndex, loaded);
|
|
20403
|
+
});
|
|
20404
|
+
}
|
|
20405
|
+
function moveGridFocus(back) {
|
|
20406
|
+
if (back) {
|
|
20407
|
+
if (!embedded || !callbacks.onFocusParentGrid) return false;
|
|
20408
|
+
callbacks.onFocusParentGrid();
|
|
20409
|
+
return true;
|
|
20410
|
+
}
|
|
20411
|
+
if (embedded || !relatedPanel || relatedPanel.hidden || !embeddedGrid) {
|
|
20412
|
+
return false;
|
|
20413
|
+
}
|
|
20414
|
+
embeddedGrid.focusGrid();
|
|
20415
|
+
return true;
|
|
20416
|
+
}
|
|
20417
|
+
function closeOpenPanel() {
|
|
20418
|
+
if (relatedPanel && !relatedPanel.hidden) {
|
|
20419
|
+
hideRelatedPanel();
|
|
20420
|
+
clearActiveCell();
|
|
20421
|
+
return true;
|
|
20422
|
+
}
|
|
20423
|
+
if (!detailPanel.hidden) {
|
|
20424
|
+
detailPanel.hidden = true;
|
|
20425
|
+
clearDetailContent();
|
|
20426
|
+
clearActiveCell();
|
|
20427
|
+
return true;
|
|
20428
|
+
}
|
|
20429
|
+
return false;
|
|
20430
|
+
}
|
|
20431
|
+
function activateActiveCell() {
|
|
20432
|
+
const rowIndex = activeCellRowIndex;
|
|
20433
|
+
const colIndex = activeCellColIndex;
|
|
20434
|
+
if (rowIndex < 0 || colIndex < 0) return false;
|
|
20435
|
+
const rowValues = cachedRow(rowIndex);
|
|
20436
|
+
if (!rowValues) return false;
|
|
20437
|
+
const colName = columnNames[colIndex];
|
|
20438
|
+
const fkClickable = fkColumns.has(colName) && (!embedded || !!callbacks.onForeignKeyCellClick);
|
|
20439
|
+
if (!fkClickable) {
|
|
20440
|
+
showDetailForActiveCell();
|
|
20441
|
+
return true;
|
|
20442
|
+
}
|
|
20443
|
+
if (embedded) {
|
|
20444
|
+
callbacks.onForeignKeyCellClick?.(
|
|
20445
|
+
currentTable,
|
|
20446
|
+
columnNames,
|
|
20447
|
+
rowValues,
|
|
20448
|
+
colName
|
|
20449
|
+
);
|
|
20450
|
+
} else {
|
|
20451
|
+
openRelatedForRow(currentTable, columnNames, rowValues, colName);
|
|
20452
|
+
}
|
|
20453
|
+
return true;
|
|
20454
|
+
}
|
|
20455
|
+
function onViewportKeydown(e2) {
|
|
20456
|
+
if (isImeComposing(e2)) return;
|
|
20457
|
+
if (isEditableKeyTarget(e2.target)) return;
|
|
20458
|
+
if (e2.key === "Tab" && !e2.ctrlKey && !e2.metaKey && !e2.altKey) {
|
|
20459
|
+
if (moveGridFocus(e2.shiftKey)) e2.preventDefault();
|
|
20460
|
+
return;
|
|
20461
|
+
}
|
|
20462
|
+
if (e2.ctrlKey || e2.metaKey || e2.altKey || e2.shiftKey) return;
|
|
20463
|
+
if (e2.key === "Escape") {
|
|
20464
|
+
if (closeOpenPanel()) e2.preventDefault();
|
|
20465
|
+
return;
|
|
20466
|
+
}
|
|
20467
|
+
if (e2.key === "Enter") {
|
|
20468
|
+
if (activateActiveCell()) e2.preventDefault();
|
|
20469
|
+
return;
|
|
20470
|
+
}
|
|
20471
|
+
const step = ARROW_STEP[e2.key];
|
|
20472
|
+
if (!step) return;
|
|
20473
|
+
const lastRow = totalRows - 1;
|
|
20474
|
+
const lastCol = columnNames.length - 1;
|
|
20475
|
+
if (lastRow < 0 || lastCol < 0) return;
|
|
20476
|
+
e2.preventDefault();
|
|
20477
|
+
if (activeCellRowIndex < 0 || activeCellColIndex < 0) {
|
|
20478
|
+
moveActiveCell(Math.min(Math.max(selectedRowIndex, 0), lastRow), 0);
|
|
20479
|
+
return;
|
|
20480
|
+
}
|
|
20481
|
+
const nextRow = Math.min(
|
|
20482
|
+
Math.max(activeCellRowIndex + step.row, 0),
|
|
20483
|
+
lastRow
|
|
20484
|
+
);
|
|
20485
|
+
const nextCol = Math.min(
|
|
20486
|
+
Math.max(activeCellColIndex + step.col, 0),
|
|
20487
|
+
lastCol
|
|
20488
|
+
);
|
|
20489
|
+
if (nextRow === activeCellRowIndex && nextCol === activeCellColIndex)
|
|
20490
|
+
return;
|
|
20491
|
+
moveActiveCell(nextRow, nextCol);
|
|
20492
|
+
}
|
|
20328
20493
|
function clearDetailContent() {
|
|
20329
20494
|
for (const child of Array.from(detailPanel.children)) {
|
|
20330
20495
|
if (child !== detailResize) child.remove();
|
|
@@ -20399,6 +20564,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
|
|
|
20399
20564
|
let editingCellCol = -1;
|
|
20400
20565
|
let relatedPanel = null;
|
|
20401
20566
|
let relatedListEl = null;
|
|
20567
|
+
let relatedListResizeEl = null;
|
|
20402
20568
|
let relatedGridHost = null;
|
|
20403
20569
|
let relatedEmptyEl = null;
|
|
20404
20570
|
let relatedCrumbEl = null;
|
|
@@ -20412,6 +20578,20 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
|
|
|
20412
20578
|
RELATED_PANEL_MIN_HEIGHT,
|
|
20413
20579
|
Math.min(panelMaxHeight(), savedRelatedHeight)
|
|
20414
20580
|
) : RELATED_PANEL_DEFAULT_HEIGHT;
|
|
20581
|
+
let relatedListWidth = clampRelatedListWidth(
|
|
20582
|
+
readStoredSize(RELATED_LIST_WIDTH_KEY, RELATED_LIST_DEFAULT_WIDTH)
|
|
20583
|
+
);
|
|
20584
|
+
let relatedListResizeDetach = null;
|
|
20585
|
+
function clampRelatedListWidth(width) {
|
|
20586
|
+
return Math.max(
|
|
20587
|
+
RELATED_LIST_MIN_WIDTH,
|
|
20588
|
+
Math.min(RELATED_LIST_MAX_WIDTH, Math.round(width))
|
|
20589
|
+
);
|
|
20590
|
+
}
|
|
20591
|
+
function applyRelatedListWidth(width) {
|
|
20592
|
+
relatedListWidth = clampRelatedListWidth(width);
|
|
20593
|
+
el2.style.setProperty("--db-related-list-w", `${relatedListWidth}px`);
|
|
20594
|
+
}
|
|
20415
20595
|
if (!embedded) {
|
|
20416
20596
|
relatedPanel = document.createElement("div");
|
|
20417
20597
|
relatedPanel.className = "db-related-panel";
|
|
@@ -20591,6 +20771,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
|
|
|
20591
20771
|
function resetSelectionAndDetail() {
|
|
20592
20772
|
selectedRowIndex = -1;
|
|
20593
20773
|
selectedRowElement = null;
|
|
20774
|
+
clearActiveCell();
|
|
20594
20775
|
detailPanel.hidden = true;
|
|
20595
20776
|
clearDetailContent();
|
|
20596
20777
|
}
|
|
@@ -20797,6 +20978,24 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
|
|
|
20797
20978
|
bodyRow.className = "db-related-body";
|
|
20798
20979
|
relatedListEl = document.createElement("div");
|
|
20799
20980
|
relatedListEl.className = "db-related-list";
|
|
20981
|
+
const listResize = document.createElement("div");
|
|
20982
|
+
listResize.className = "db-related-list-resize";
|
|
20983
|
+
listResize.tabIndex = 0;
|
|
20984
|
+
listResize.setAttribute("role", "separator");
|
|
20985
|
+
listResize.setAttribute("aria-orientation", "vertical");
|
|
20986
|
+
listResize.setAttribute("aria-label", text3().grid.relatedListResize);
|
|
20987
|
+
relatedListResizeEl = listResize;
|
|
20988
|
+
applyRelatedListWidth(relatedListWidth);
|
|
20989
|
+
relatedListResizeDetach = attachDragResizer({
|
|
20990
|
+
handle: listResize,
|
|
20991
|
+
getSize: () => relatedListWidth,
|
|
20992
|
+
applySize: applyRelatedListWidth,
|
|
20993
|
+
direction: 1,
|
|
20994
|
+
axis: "x",
|
|
20995
|
+
onEnd: () => writeStoredSize(RELATED_LIST_WIDTH_KEY, relatedListWidth),
|
|
20996
|
+
activeClassTarget: relatedPanel,
|
|
20997
|
+
activeClassName: "db-related-list-resizing"
|
|
20998
|
+
});
|
|
20800
20999
|
relatedGridHost = document.createElement("div");
|
|
20801
21000
|
relatedGridHost.className = "db-related-grid-host";
|
|
20802
21001
|
embeddedGrid = createTableGrid(
|
|
@@ -20817,7 +21016,9 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
|
|
|
20817
21016
|
getBaseEq: () => relatedEq,
|
|
20818
21017
|
getText: callbacks.getText,
|
|
20819
21018
|
// 埋め込みグリッドの FK クリックで 1 段潜る。
|
|
20820
|
-
onForeignKeyCellClick: (sourceTable, colNames, rowData, clicked) => drillIntoRelated(sourceTable, colNames, rowData, clicked)
|
|
21019
|
+
onForeignKeyCellClick: (sourceTable, colNames, rowData, clicked) => drillIntoRelated(sourceTable, colNames, rowData, clicked),
|
|
21020
|
+
// 埋め込み側で Shift+Tab を押したらメイングリッドへ戻す。
|
|
21021
|
+
onFocusParentGrid: () => viewport.focus({ preventScroll: true })
|
|
20821
21022
|
},
|
|
20822
21023
|
{ embedded: true }
|
|
20823
21024
|
);
|
|
@@ -20827,7 +21028,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
|
|
|
20827
21028
|
relatedEmptyEl.textContent = text3().grid.relatedEmpty;
|
|
20828
21029
|
relatedEmptyEl.hidden = true;
|
|
20829
21030
|
relatedGridHost.appendChild(relatedEmptyEl);
|
|
20830
|
-
bodyRow.append(relatedListEl, relatedGridHost);
|
|
21031
|
+
bodyRow.append(relatedListEl, listResize, relatedGridHost);
|
|
20831
21032
|
relatedPanel.append(resizer, header, bodyRow);
|
|
20832
21033
|
}
|
|
20833
21034
|
function startRelatedResize(e2) {
|
|
@@ -20945,7 +21146,11 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
|
|
|
20945
21146
|
if (i2 === level.selectedIndex) item.classList.add("active");
|
|
20946
21147
|
const name = document.createElement("span");
|
|
20947
21148
|
name.className = "db-related-list-name";
|
|
20948
|
-
|
|
21149
|
+
const tableName = document.createElement("span");
|
|
21150
|
+
tableName.className = "db-related-list-table";
|
|
21151
|
+
tableName.textContent = relatedDrillTable(target);
|
|
21152
|
+
tableName.title = relatedDrillTable(target);
|
|
21153
|
+
name.appendChild(tableName);
|
|
20949
21154
|
if (target.fk.inferred) {
|
|
20950
21155
|
const badge = document.createElement("span");
|
|
20951
21156
|
badge.className = "db-related-list-inferred-badge";
|
|
@@ -20955,7 +21160,9 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
|
|
|
20955
21160
|
}
|
|
20956
21161
|
const via = document.createElement("span");
|
|
20957
21162
|
via.className = "db-related-list-via";
|
|
20958
|
-
|
|
21163
|
+
const condition = target.direction === "outgoing" ? `${target.fk.fromColumn} = ${target.value}` : `${target.fk.fromTable}.${target.fk.fromColumn} = ${target.value}`;
|
|
21164
|
+
via.textContent = condition;
|
|
21165
|
+
via.title = condition;
|
|
20959
21166
|
item.append(name, via);
|
|
20960
21167
|
item.addEventListener("click", () => selectRelatedTarget(i2));
|
|
20961
21168
|
relatedListEl?.appendChild(item);
|
|
@@ -20992,9 +21199,42 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
|
|
|
20992
21199
|
grid.showError(err instanceof Error ? err.message : String(err));
|
|
20993
21200
|
}
|
|
20994
21201
|
}
|
|
21202
|
+
let jsonHighlighter = null;
|
|
21203
|
+
let jsonHighlighterRequested = false;
|
|
21204
|
+
let detailJsonPre = null;
|
|
21205
|
+
let detailJsonText = "";
|
|
21206
|
+
function paintJsonHighlight(pre, json) {
|
|
21207
|
+
if (json.length > DETAIL_JSON_HIGHLIGHT_MAX_CHARS) return false;
|
|
21208
|
+
const inner = highlightToInnerHtml(json, "json", jsonHighlighter);
|
|
21209
|
+
if (!inner) return false;
|
|
21210
|
+
pre.innerHTML = inner;
|
|
21211
|
+
return true;
|
|
21212
|
+
}
|
|
21213
|
+
function showJsonDetail(pre, json) {
|
|
21214
|
+
detailJsonPre = pre;
|
|
21215
|
+
detailJsonText = json;
|
|
21216
|
+
if (paintJsonHighlight(pre, json)) return;
|
|
21217
|
+
pre.textContent = json;
|
|
21218
|
+
if (json.length > DETAIL_JSON_HIGHLIGHT_MAX_CHARS) return;
|
|
21219
|
+
ensureJsonHighlighter();
|
|
21220
|
+
}
|
|
21221
|
+
function ensureJsonHighlighter() {
|
|
21222
|
+
if (jsonHighlighterRequested) return;
|
|
21223
|
+
jsonHighlighterRequested = true;
|
|
21224
|
+
void loadShikiHighlighter({
|
|
21225
|
+
themes: ["github-light", "github-dark"],
|
|
21226
|
+
langs: ["json"]
|
|
21227
|
+
}).then((highlighter) => {
|
|
21228
|
+
jsonHighlighter = highlighter;
|
|
21229
|
+
if (!highlighter || !detailJsonPre?.isConnected) return;
|
|
21230
|
+
paintJsonHighlight(detailJsonPre, detailJsonText);
|
|
21231
|
+
});
|
|
21232
|
+
}
|
|
20995
21233
|
function showCellDetail(colIndex, value) {
|
|
20996
21234
|
const colName = columnNames[colIndex];
|
|
20997
21235
|
const colType = columns[colIndex]?.type || "";
|
|
21236
|
+
detailJsonPre = null;
|
|
21237
|
+
detailJsonText = "";
|
|
20998
21238
|
hideRelatedPanel();
|
|
20999
21239
|
detailPanel.hidden = false;
|
|
21000
21240
|
clearDetailContent();
|
|
@@ -21046,7 +21286,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
|
|
|
21046
21286
|
const parsed = JSON.parse(str);
|
|
21047
21287
|
const pre = document.createElement("pre");
|
|
21048
21288
|
pre.className = "db-grid-detail-json";
|
|
21049
|
-
pre
|
|
21289
|
+
showJsonDetail(pre, JSON.stringify(parsed, null, 2));
|
|
21050
21290
|
content.appendChild(pre);
|
|
21051
21291
|
} catch {
|
|
21052
21292
|
content.textContent = str;
|
|
@@ -21111,6 +21351,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
|
|
|
21111
21351
|
}
|
|
21112
21352
|
renderFilterRow();
|
|
21113
21353
|
syncContentWidth();
|
|
21354
|
+
syncHorizontalScroll();
|
|
21114
21355
|
}
|
|
21115
21356
|
function startResize(colIndex, startEvent) {
|
|
21116
21357
|
cleanupResize();
|
|
@@ -21191,9 +21432,27 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
|
|
|
21191
21432
|
invalidateData();
|
|
21192
21433
|
}, FILTER_DEBOUNCE_MS);
|
|
21193
21434
|
}
|
|
21435
|
+
let scrollbarGutterPx = -1;
|
|
21436
|
+
function syncScrollbarGutter() {
|
|
21437
|
+
const outer = viewport.offsetWidth;
|
|
21438
|
+
const inner = viewport.clientWidth;
|
|
21439
|
+
if (!(outer > 0) || !(inner > 0)) return;
|
|
21440
|
+
const gutter = outer - inner;
|
|
21441
|
+
if (gutter === scrollbarGutterPx) return;
|
|
21442
|
+
scrollbarGutterPx = gutter;
|
|
21443
|
+
el2.style.setProperty("--db-grid-scrollbar-w", `${gutter}px`);
|
|
21444
|
+
}
|
|
21445
|
+
function measureContentWidth() {
|
|
21446
|
+
let total = 0;
|
|
21447
|
+
for (const cell of headerRow.children) {
|
|
21448
|
+
total += cell.getBoundingClientRect().width;
|
|
21449
|
+
}
|
|
21450
|
+
return total;
|
|
21451
|
+
}
|
|
21194
21452
|
function syncContentWidth() {
|
|
21195
21453
|
requestAnimationFrame(() => {
|
|
21196
|
-
|
|
21454
|
+
syncScrollbarGutter();
|
|
21455
|
+
const w = measureContentWidth();
|
|
21197
21456
|
if (w > 0) {
|
|
21198
21457
|
spacer.style.minWidth = `${w}px`;
|
|
21199
21458
|
body.style.minWidth = `${w}px`;
|
|
@@ -21393,8 +21652,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
|
|
|
21393
21652
|
const fkClickable = fkColumns.has(cellColName) && (!embedded || !!callbacks.onForeignKeyCellClick);
|
|
21394
21653
|
const isEditingThisCell = !ro && i2 === editingCellRow && c2 === editingCellCol;
|
|
21395
21654
|
const activate = () => {
|
|
21396
|
-
|
|
21397
|
-
setActiveCell(rowIndex, cellColIndex);
|
|
21655
|
+
focusCell(rowIndex, row, cellColIndex);
|
|
21398
21656
|
if (fkClickable) {
|
|
21399
21657
|
if (embedded) {
|
|
21400
21658
|
callbacks.onForeignKeyCellClick?.(
|
|
@@ -21486,8 +21744,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
|
|
|
21486
21744
|
}
|
|
21487
21745
|
cell.addEventListener("click", (e2) => {
|
|
21488
21746
|
e2.stopPropagation();
|
|
21489
|
-
|
|
21490
|
-
setActiveCell(rowIndex, cellColIndex);
|
|
21747
|
+
focusCell(rowIndex, row, cellColIndex);
|
|
21491
21748
|
if (fkClickable) {
|
|
21492
21749
|
if (embedded) {
|
|
21493
21750
|
callbacks.onForeignKeyCellClick?.(
|
|
@@ -21597,6 +21854,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
|
|
|
21597
21854
|
);
|
|
21598
21855
|
}
|
|
21599
21856
|
syncFilteredEmptyState();
|
|
21857
|
+
syncHorizontalScroll();
|
|
21600
21858
|
if (focusRestore) {
|
|
21601
21859
|
const next = body.querySelector(
|
|
21602
21860
|
`.db-grid-cell-input[data-edit-row="${focusRestore.row}"][data-edit-col="${focusRestore.col}"]`
|
|
@@ -21762,11 +22020,22 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
|
|
|
21762
22020
|
void refreshCurrentTable();
|
|
21763
22021
|
});
|
|
21764
22022
|
const onViewportScroll = () => {
|
|
21765
|
-
|
|
21766
|
-
filterRowWrap.scrollLeft = viewport.scrollLeft;
|
|
22023
|
+
syncHorizontalScroll();
|
|
21767
22024
|
renderViewport();
|
|
21768
22025
|
};
|
|
22026
|
+
const onWrapScroll = (wrap) => () => {
|
|
22027
|
+
if (wrap.scrollLeft === viewport.scrollLeft) return;
|
|
22028
|
+
viewport.scrollLeft = wrap.scrollLeft;
|
|
22029
|
+
syncHorizontalScroll();
|
|
22030
|
+
};
|
|
22031
|
+
const onHeaderWrapScroll = onWrapScroll(headerWrap);
|
|
22032
|
+
const onFilterWrapScroll = onWrapScroll(filterRowWrap);
|
|
21769
22033
|
viewport.addEventListener("scroll", onViewportScroll, { passive: true });
|
|
22034
|
+
viewport.addEventListener("keydown", onViewportKeydown);
|
|
22035
|
+
headerWrap.addEventListener("scroll", onHeaderWrapScroll, { passive: true });
|
|
22036
|
+
filterRowWrap.addEventListener("scroll", onFilterWrapScroll, {
|
|
22037
|
+
passive: true
|
|
22038
|
+
});
|
|
21770
22039
|
const onCompositionStart = () => {
|
|
21771
22040
|
isComposing = true;
|
|
21772
22041
|
};
|
|
@@ -21780,7 +22049,12 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
|
|
|
21780
22049
|
clear();
|
|
21781
22050
|
embeddedGrid?.destroy();
|
|
21782
22051
|
embeddedGrid = null;
|
|
22052
|
+
relatedListResizeDetach?.();
|
|
22053
|
+
relatedListResizeDetach = null;
|
|
21783
22054
|
viewport.removeEventListener("scroll", onViewportScroll);
|
|
22055
|
+
viewport.removeEventListener("keydown", onViewportKeydown);
|
|
22056
|
+
headerWrap.removeEventListener("scroll", onHeaderWrapScroll);
|
|
22057
|
+
filterRowWrap.removeEventListener("scroll", onFilterWrapScroll);
|
|
21784
22058
|
body.removeEventListener("compositionstart", onCompositionStart);
|
|
21785
22059
|
body.removeEventListener("compositionend", onCompositionEnd);
|
|
21786
22060
|
detailResizeCleanup?.();
|
|
@@ -21804,6 +22078,10 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
|
|
|
21804
22078
|
updateStatus();
|
|
21805
22079
|
}
|
|
21806
22080
|
renderRelatedCrumbs();
|
|
22081
|
+
renderRelatedList();
|
|
22082
|
+
if (relatedListResizeEl) {
|
|
22083
|
+
relatedListResizeEl.setAttribute("aria-label", t2.grid.relatedListResize);
|
|
22084
|
+
}
|
|
21807
22085
|
embeddedGrid?.localize();
|
|
21808
22086
|
}
|
|
21809
22087
|
function rebuildFkColumnsForCurrentTable() {
|
|
@@ -22053,6 +22331,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
|
|
|
22053
22331
|
}
|
|
22054
22332
|
return {
|
|
22055
22333
|
el: el2,
|
|
22334
|
+
focusGrid: () => viewport.focus({ preventScroll: true }),
|
|
22056
22335
|
load,
|
|
22057
22336
|
refresh: refreshCurrentTable,
|
|
22058
22337
|
showError,
|
|
@@ -29342,7 +29621,7 @@ code-viewer annotate add-db --db app.db --tab query \\
|
|
|
29342
29621
|
],
|
|
29343
29622
|
[
|
|
29344
29623
|
"Detail footer & related panel",
|
|
29345
|
-
"Click any cell to open a resizable detail footer. Foreign-key cells open a related-rows panel with multi-step drill-down breadcrumbs, supporting both outgoing (FK → PK) and incoming (PK ← FK) navigation."
|
|
29624
|
+
"Click any cell to open a resizable detail footer; JSON values are pretty-printed and syntax-highlighted there. Once the grid has focus, arrow keys move the active cell from data cell to data cell and the footer follows the value under it, scrolling only as far as needed; Enter follows a foreign key (arrow keys alone never fire a related-table query), Escape closes whichever panel is open, and Tab / Shift+Tab move between the main grid and the related grid. Foreign-key cells open a related-rows panel with multi-step drill-down breadcrumbs, supporting both outgoing (FK → PK) and incoming (PK ← FK) navigation. Its reference list keeps each entry on one line with the full table name and condition in a tooltip, and the list width can be dragged and is remembered."
|
|
29346
29625
|
],
|
|
29347
29626
|
[
|
|
29348
29627
|
"Schema tab",
|
|
@@ -30109,7 +30388,7 @@ code-viewer annotate add-db --db app.db --tab query \\
|
|
|
30109
30388
|
],
|
|
30110
30389
|
[
|
|
30111
30390
|
"詳細フッタ・関連パネル",
|
|
30112
|
-
"
|
|
30391
|
+
"セルをクリックするとリサイズ可能な詳細フッタが開きます。JSON 値は整形してシンタックスハイライト付きで表示されます。グリッドにフォーカスがある間は矢印キーでデータセル間を移動でき、詳細フッタもその値に追従します(スクロールは見える位置まで必要なぶんだけ)。Enter で外部キーを辿り(矢印キーだけでは関連テーブルへのクエリは飛びません)、Escape で開いているパネルを閉じ、Tab / Shift+Tab でメイングリッドと関連グリッドを行き来できます。外部キー値からは関連行パネルが開き、ブレッドクラム付きで多段ドリルダウン可能。outgoing (FK→PK) と incoming (PK←FK) の両方向に対応。左の参照リストは各項目を1行に保ち、テーブル名と条件の全文は tooltip で確認できます。リスト幅はドラッグで変更でき、次回も保持されます。"
|
|
30113
30392
|
],
|
|
30114
30393
|
[
|
|
30115
30394
|
"Schema タブ",
|
package/web/style.css
CHANGED
|
@@ -10464,6 +10464,14 @@ body.journal-task-editor-resizing * {
|
|
|
10464
10464
|
.db-grid-header-wrap {
|
|
10465
10465
|
flex-shrink: 0;
|
|
10466
10466
|
overflow: hidden;
|
|
10467
|
+
/* 本文 (.db-grid-viewport) の縦スクロールバーが食う幅を、右端に透明な
|
|
10468
|
+
border として空ける。空けないと「見える幅」が本文だけ狭くなり、本文が
|
|
10469
|
+
右へ余分にスクロールできてしまう。ヘッダは scrollLeft を同期しても
|
|
10470
|
+
そこまで追随できず、右端で列がスクロールバー幅ぶんずれる。
|
|
10471
|
+
scrollbar-gutter は overflow:hidden だと見た目の幅しか縮まず、
|
|
10472
|
+
スクロール範囲は縮まないので使えない (実測済み)。
|
|
10473
|
+
幅は table-grid.ts が実測して --db-grid-scrollbar-w に入れる。 */
|
|
10474
|
+
border-right: var(--db-grid-scrollbar-w, 0px) solid transparent;
|
|
10467
10475
|
border-bottom: 2px solid var(--border);
|
|
10468
10476
|
background: var(--bg-soft);
|
|
10469
10477
|
}
|
|
@@ -10508,6 +10516,20 @@ body.journal-task-editor-resizing * {
|
|
|
10508
10516
|
flex: 1;
|
|
10509
10517
|
overflow: auto;
|
|
10510
10518
|
position: relative;
|
|
10519
|
+
/* 行が少なくて縦スクロールバーが出ないときも幅を変えない。ここを可変に
|
|
10520
|
+
すると、ヘッダ / フィルタ行に渡す --db-grid-scrollbar-w が行数やフィルタで
|
|
10521
|
+
変わり、列の位置が動く。 */
|
|
10522
|
+
scrollbar-gutter: stable;
|
|
10523
|
+
}
|
|
10524
|
+
/* 矢印キーでセルを移動するため viewport 自体がフォーカスを受け取る
|
|
10525
|
+
(tabIndex=0)。セルをクリックするたびに枠が出ると常時ノイズになるので、
|
|
10526
|
+
キーボードで到達したときだけリングを出す。親の .db-grid が
|
|
10527
|
+
overflow:hidden なので外側に描くと切れる → inset で内側に描く。 */
|
|
10528
|
+
.db-grid-viewport:focus {
|
|
10529
|
+
outline: none;
|
|
10530
|
+
}
|
|
10531
|
+
.db-grid-viewport:focus-visible {
|
|
10532
|
+
box-shadow: inset var(--db-focus-ring);
|
|
10511
10533
|
}
|
|
10512
10534
|
|
|
10513
10535
|
.db-grid-spacer {
|
|
@@ -11097,6 +11119,8 @@ html[data-theme="dark"] .db-schema-table tr.pk-row {
|
|
|
11097
11119
|
.db-grid-filter-row-wrap {
|
|
11098
11120
|
flex-shrink: 0;
|
|
11099
11121
|
overflow: hidden;
|
|
11122
|
+
/* ヘッダと同じ理由でスクロールバー幅を空ける (.db-grid-header-wrap 参照)。 */
|
|
11123
|
+
border-right: var(--db-grid-scrollbar-w, 0px) solid transparent;
|
|
11100
11124
|
border-bottom: 1px solid var(--border);
|
|
11101
11125
|
background: var(--bg-soft);
|
|
11102
11126
|
}
|
|
@@ -11276,6 +11300,23 @@ html[data-theme="dark"] .db-schema-table tr.pk-row {
|
|
|
11276
11300
|
white-space: pre-wrap;
|
|
11277
11301
|
word-break: break-word;
|
|
11278
11302
|
}
|
|
11303
|
+
/* shiki は `<span style="--shiki-light:#xxx;--shiki-dark:#yyy">` の inline
|
|
11304
|
+
CSS 変数で token 色を出すので、light / dark でそれぞれ参照する。
|
|
11305
|
+
明示テーマ (html[data-theme]) を OS 設定より後に置いて優先させる。 */
|
|
11306
|
+
.db-grid-detail-json span[style] {
|
|
11307
|
+
color: var(--shiki-light);
|
|
11308
|
+
}
|
|
11309
|
+
@media (prefers-color-scheme: dark) {
|
|
11310
|
+
.db-grid-detail-json span[style] {
|
|
11311
|
+
color: var(--shiki-dark);
|
|
11312
|
+
}
|
|
11313
|
+
}
|
|
11314
|
+
html[data-theme="light"] .db-grid-detail-json span[style] {
|
|
11315
|
+
color: var(--shiki-light);
|
|
11316
|
+
}
|
|
11317
|
+
html[data-theme="dark"] .db-grid-detail-json span[style] {
|
|
11318
|
+
color: var(--shiki-dark);
|
|
11319
|
+
}
|
|
11279
11320
|
|
|
11280
11321
|
/* ---------- Foreign-key related data ---------- */
|
|
11281
11322
|
.db-grid-header-fk-icon {
|
|
@@ -11373,13 +11414,32 @@ html[data-theme="dark"] .db-schema-table tr.pk-row {
|
|
|
11373
11414
|
}
|
|
11374
11415
|
|
|
11375
11416
|
.db-related-list {
|
|
11376
|
-
|
|
11417
|
+
/* 既定 / 下限 / 上限は table-grid.ts が持つ。ここは JS 実行前の初回描画用の
|
|
11418
|
+
fallback だけ (ui-layout: CSS と TS に同じ数値を書かない)。 */
|
|
11419
|
+
width: var(--db-related-list-w, 200px);
|
|
11377
11420
|
flex-shrink: 0;
|
|
11378
11421
|
overflow: auto;
|
|
11379
|
-
border-right: 1px solid var(--border);
|
|
11380
11422
|
padding: 4px;
|
|
11381
11423
|
}
|
|
11382
11424
|
|
|
11425
|
+
/* リストとグリッドの間の掴みしろ。境界線の役目も兼ねるので、リスト側の
|
|
11426
|
+
border-right はこちらへ移してある (二重線にしない)。 */
|
|
11427
|
+
.db-related-list-resize {
|
|
11428
|
+
flex: 0 0 auto;
|
|
11429
|
+
width: 5px;
|
|
11430
|
+
cursor: col-resize;
|
|
11431
|
+
background: var(--border);
|
|
11432
|
+
background-clip: content-box;
|
|
11433
|
+
border-left: 2px solid transparent;
|
|
11434
|
+
border-right: 2px solid transparent;
|
|
11435
|
+
}
|
|
11436
|
+
.db-related-list-resize:hover,
|
|
11437
|
+
.db-related-list-resize:focus-visible,
|
|
11438
|
+
.db-related-panel.db-related-list-resizing .db-related-list-resize {
|
|
11439
|
+
background: var(--accent);
|
|
11440
|
+
outline: none;
|
|
11441
|
+
}
|
|
11442
|
+
|
|
11383
11443
|
.db-related-list-item {
|
|
11384
11444
|
display: flex;
|
|
11385
11445
|
flex-direction: column;
|
|
@@ -11410,6 +11470,17 @@ html[data-theme="dark"] .db-schema-table tr.pk-row {
|
|
|
11410
11470
|
display: inline-flex;
|
|
11411
11471
|
align-items: baseline;
|
|
11412
11472
|
gap: 6px;
|
|
11473
|
+
/* 項目の幅を超えたら省略記号にする。min-width:0 が無いと flex 子は
|
|
11474
|
+
内容幅より縮まず、はみ出したまま折り返す。 */
|
|
11475
|
+
max-width: 100%;
|
|
11476
|
+
min-width: 0;
|
|
11477
|
+
}
|
|
11478
|
+
|
|
11479
|
+
.db-related-list-table {
|
|
11480
|
+
min-width: 0;
|
|
11481
|
+
overflow: hidden;
|
|
11482
|
+
text-overflow: ellipsis;
|
|
11483
|
+
white-space: nowrap;
|
|
11413
11484
|
}
|
|
11414
11485
|
/* 方向アイコンを name の前に出す: outgoing は → (自分→他テーブル)、
|
|
11415
11486
|
incoming は ← (他テーブル→自分の PK)。テキスト 1 文字なのでフォントに
|
|
@@ -11430,7 +11501,12 @@ html[data-theme="dark"] .db-schema-table tr.pk-row {
|
|
|
11430
11501
|
.db-related-list-via {
|
|
11431
11502
|
font-size: 11px;
|
|
11432
11503
|
color: var(--fg-muted);
|
|
11433
|
-
|
|
11504
|
+
/* break-all だと "sample_association_table.sa / mple_column_id = 12" の
|
|
11505
|
+
ように語の途中で折り返って読めなくなる。1 行で省略し、全文は title で出す。 */
|
|
11506
|
+
max-width: 100%;
|
|
11507
|
+
overflow: hidden;
|
|
11508
|
+
text-overflow: ellipsis;
|
|
11509
|
+
white-space: nowrap;
|
|
11434
11510
|
}
|
|
11435
11511
|
|
|
11436
11512
|
.db-related-grid-host {
|