flexdesk 0.2.0 → 0.4.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 +26 -0
- package/css/base.css +2243 -872
- package/css/flexdesk.css +1375 -14
- package/css/overrides.css +44 -0
- package/css/tokens.css +45 -0
- package/dist/charts.js +5 -3
- package/dist/charts.js.map +1 -1
- package/dist/{chunk-DVU44T77.js → chunk-ELXVW542.js} +196 -75
- package/dist/chunk-ELXVW542.js.map +7 -0
- package/dist/chunk-LH5TSOZW.js +1237 -0
- package/dist/chunk-LH5TSOZW.js.map +7 -0
- package/dist/{chunk-TLZUUFOE.js → chunk-O5OHMWBB.js} +10 -2
- package/dist/chunk-O5OHMWBB.js.map +7 -0
- package/dist/{chunk-CT4YXXLP.js → chunk-QIU5S2RU.js} +371 -73
- package/dist/chunk-QIU5S2RU.js.map +7 -0
- package/dist/chunk-QNQHQ24V.js +408 -0
- package/dist/chunk-QNQHQ24V.js.map +7 -0
- package/dist/{chunk-DRYCDMEG.js → chunk-XKDTIT4Q.js} +168 -12
- package/dist/chunk-XKDTIT4Q.js.map +7 -0
- package/dist/editor.js +3 -380
- package/dist/editor.js.map +3 -3
- package/dist/flexdesk.css +1375 -14
- package/dist/tiles.js +168 -41
- package/dist/tiles.js.map +2 -2
- package/dist/tokens.css +45 -0
- package/dist/widgets.js +44 -14
- package/dist/widgets.js.map +2 -2
- package/dist/wm.js +3140 -157
- package/dist/wm.js.map +4 -4
- package/package.json +3 -2
- package/src/charts/chart_types.js +167 -0
- package/src/charts/plotly_wrapper.js +178 -10
- package/src/editor/notebook_tab_bar.js +39 -3
- package/src/tiles/tile_base.js +143 -35
- package/src/tiles/tile_grid.js +52 -1
- package/src/tiling/command_palette.js +71 -18
- package/src/tiling/desktops.js +36 -12
- package/src/tiling/keymap.js +24 -4
- package/src/tiling/shell.js +156 -25
- package/src/tiling/tab_strip.js +184 -0
- package/src/tiling/tile_breadcrumb.js +34 -2
- package/src/tiling/tile_renderer.js +1386 -21
- package/src/tiling/tile_tab_menu.js +101 -0
- package/src/tiling/tile_tree.js +115 -11
- package/src/tiling/wm.js +2375 -84
- package/src/tiling/zoom.js +248 -0
- package/src/ui/components/action_dropdown.js +34 -3
- package/src/ui/components/autocomplete_field.js +65 -13
- package/src/ui/components/context_menu.js +79 -8
- package/src/ui/components/data_table.js +508 -84
- package/src/ui/components/managed_window.js +928 -36
- package/src/ui/components/modal.js +214 -8
- package/dist/chunk-CT4YXXLP.js.map +0 -7
- package/dist/chunk-DRYCDMEG.js.map +0 -7
- package/dist/chunk-DVU44T77.js.map +0 -7
- package/dist/chunk-TLZUUFOE.js.map +0 -7
- package/dist/chunk-UCJ2WD4D.js +0 -625
- package/dist/chunk-UCJ2WD4D.js.map +0 -7
|
@@ -15,6 +15,7 @@ function createRafResizeObserver(callback) {
|
|
|
15
15
|
|
|
16
16
|
// src/ui/components/data_table.js
|
|
17
17
|
var DEFAULT_PAGE_SIZE = 100;
|
|
18
|
+
var AUTOSIZE_MAX_PX = 520;
|
|
18
19
|
var DataTable = class {
|
|
19
20
|
/**
|
|
20
21
|
* Create a DataTable instance
|
|
@@ -64,6 +65,12 @@ var DataTable = class {
|
|
|
64
65
|
// ephemeral. WHERE the store persists is the embedder's business.
|
|
65
66
|
persistKey: null,
|
|
66
67
|
stateStore: null,
|
|
68
|
+
// How the automatic fit pass sizes an undragged column — see the
|
|
69
|
+
// typedef. 'container' is exactly what every consumer got before
|
|
70
|
+
// this key existed, so the default is not a preference: it is the
|
|
71
|
+
// promise that adding the key changed no existing layout by a
|
|
72
|
+
// pixel. Only a consumer that asks for 'content' sees anything new.
|
|
73
|
+
columnFit: "container",
|
|
67
74
|
...config
|
|
68
75
|
};
|
|
69
76
|
if (this.config.persistKey && !this.config.stateStore) {
|
|
@@ -1189,38 +1196,7 @@ var DataTable = class {
|
|
|
1189
1196
|
if (selected.has(globalIdx)) {
|
|
1190
1197
|
tr.classList.add("selected");
|
|
1191
1198
|
}
|
|
1192
|
-
|
|
1193
|
-
const td = document.createElement("td");
|
|
1194
|
-
td.className = "num";
|
|
1195
|
-
td.textContent = String(globalIdx + 1);
|
|
1196
|
-
tr.appendChild(td);
|
|
1197
|
-
}
|
|
1198
|
-
for (let colIdx = 0; colIdx < row.length; colIdx++) {
|
|
1199
|
-
const td = document.createElement("td");
|
|
1200
|
-
td.className = this._columnTypes[colIdx] || "text";
|
|
1201
|
-
const value = row[colIdx];
|
|
1202
|
-
if (this.config.renderCell) {
|
|
1203
|
-
const handled = this.config.renderCell(td, value, colIdx, globalIdx, row);
|
|
1204
|
-
if (!handled) {
|
|
1205
|
-
td.textContent = this._formatValue(value, colIdx);
|
|
1206
|
-
}
|
|
1207
|
-
} else {
|
|
1208
|
-
td.textContent = this._formatValue(value, colIdx);
|
|
1209
|
-
}
|
|
1210
|
-
if (this.config.onCellContextMenu) {
|
|
1211
|
-
const cellColIdx = colIdx;
|
|
1212
|
-
td.addEventListener("contextmenu", (ev) => {
|
|
1213
|
-
this.config.onCellContextMenu(
|
|
1214
|
-
cellColIdx,
|
|
1215
|
-
globalIdx,
|
|
1216
|
-
value,
|
|
1217
|
-
td,
|
|
1218
|
-
ev
|
|
1219
|
-
);
|
|
1220
|
-
});
|
|
1221
|
-
}
|
|
1222
|
-
tr.appendChild(td);
|
|
1223
|
-
}
|
|
1199
|
+
this._fillRowCells(tr, row, globalIdx, showRowNumbers);
|
|
1224
1200
|
if (this.config.onRowClick) {
|
|
1225
1201
|
tr.addEventListener("click", (ev) => {
|
|
1226
1202
|
this.config.onRowClick(globalIdx, row, ev);
|
|
@@ -1237,21 +1213,148 @@ var DataTable = class {
|
|
|
1237
1213
|
this._tbodyEl = tbody;
|
|
1238
1214
|
return table;
|
|
1239
1215
|
}
|
|
1216
|
+
/** Build (or rebuild) one row's cells in place.
|
|
1217
|
+
*
|
|
1218
|
+
* Extracted from the body loop so that `updateRow` and the initial
|
|
1219
|
+
* render share ONE cell-building path. Two paths would drift, and the
|
|
1220
|
+
* drift would show as a cell that renders differently after a live
|
|
1221
|
+
* update than it did on load. */
|
|
1222
|
+
_fillRowCells(tr, row, globalIdx, showRowNumbers) {
|
|
1223
|
+
tr.replaceChildren();
|
|
1224
|
+
if (showRowNumbers) {
|
|
1225
|
+
const td = document.createElement("td");
|
|
1226
|
+
td.className = "num";
|
|
1227
|
+
td.textContent = String(globalIdx + 1);
|
|
1228
|
+
tr.appendChild(td);
|
|
1229
|
+
}
|
|
1230
|
+
for (let colIdx = 0; colIdx < row.length; colIdx++) {
|
|
1231
|
+
const td = document.createElement("td");
|
|
1232
|
+
td.className = this._columnTypes[colIdx] || "text";
|
|
1233
|
+
const value = row[colIdx];
|
|
1234
|
+
if (this.config.renderCell) {
|
|
1235
|
+
const handled = this.config.renderCell(td, value, colIdx, globalIdx, row);
|
|
1236
|
+
if (!handled) {
|
|
1237
|
+
td.textContent = this._formatValue(value, colIdx);
|
|
1238
|
+
}
|
|
1239
|
+
} else {
|
|
1240
|
+
td.textContent = this._formatValue(value, colIdx);
|
|
1241
|
+
}
|
|
1242
|
+
if (this.config.onCellContextMenu) {
|
|
1243
|
+
const cellColIdx = colIdx;
|
|
1244
|
+
td.addEventListener("contextmenu", (ev) => {
|
|
1245
|
+
this.config.onCellContextMenu(
|
|
1246
|
+
cellColIdx,
|
|
1247
|
+
globalIdx,
|
|
1248
|
+
value,
|
|
1249
|
+
td,
|
|
1250
|
+
ev
|
|
1251
|
+
);
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
tr.appendChild(td);
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
/** Re-render ONE row in place, preserving everything around it.
|
|
1258
|
+
*
|
|
1259
|
+
* `render()` rebuilds the entire `<tbody>`, which takes the scroll
|
|
1260
|
+
* position, any open editor, the keyboard focus and the measured column
|
|
1261
|
+
* widths with it. That is fine for a sort or a page change and wrong for
|
|
1262
|
+
* a single-cell commit or a live update arriving over a socket — the
|
|
1263
|
+
* common case in an editable grid, where a full rebuild once per keystroke
|
|
1264
|
+
* is both visible and destructive.
|
|
1265
|
+
*
|
|
1266
|
+
* What survives, by construction:
|
|
1267
|
+
* - scroll position, because the tbody is not replaced;
|
|
1268
|
+
* - the separately-rendered thead/tbody column widths, because the
|
|
1269
|
+
* explicit widths live on the header cells and on the FIRST body row,
|
|
1270
|
+
* and all three of the properties `_setCellWidth` writes are re-applied
|
|
1271
|
+
* here when that first row is the one being replaced;
|
|
1272
|
+
* - selection, because the `selected` class is recomputed from the
|
|
1273
|
+
* selection set rather than carried on the old element;
|
|
1274
|
+
* - keyboard focus, because the focused element's position is recorded
|
|
1275
|
+
* before the replace and restored after.
|
|
1276
|
+
*
|
|
1277
|
+
* @param {number} index row index as tracked by `tr.__rowIndex`
|
|
1278
|
+
* @param {any[]} row the new cell values
|
|
1279
|
+
* @returns {boolean} false when the row is not currently rendered
|
|
1280
|
+
* (it is on another page, or outside the render
|
|
1281
|
+
* window) — which is NOT an error: the caller has
|
|
1282
|
+
* nothing to update on screen.
|
|
1283
|
+
*/
|
|
1284
|
+
updateRow(index, row) {
|
|
1285
|
+
const tbody = this._tbodyEl;
|
|
1286
|
+
if (!tbody) return false;
|
|
1287
|
+
let tr = null;
|
|
1288
|
+
for (const candidate of tbody.children) {
|
|
1289
|
+
if (candidate.__rowIndex === index) {
|
|
1290
|
+
tr = candidate;
|
|
1291
|
+
break;
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
if (!tr) return false;
|
|
1295
|
+
if (Array.isArray(this.config.rows) && this.config.rows[index]) {
|
|
1296
|
+
this.config.rows[index] = row;
|
|
1297
|
+
}
|
|
1298
|
+
const active = document.activeElement;
|
|
1299
|
+
let focusedCol = -1;
|
|
1300
|
+
if (active && tr.contains(active)) {
|
|
1301
|
+
focusedCol = Array.prototype.indexOf.call(tr.children, active.closest("td"));
|
|
1302
|
+
}
|
|
1303
|
+
const isFirstRow = tr === tbody.firstElementChild;
|
|
1304
|
+
const widths = isFirstRow ? Array.prototype.map.call(tr.children, (td) => ({
|
|
1305
|
+
width: td.style.width,
|
|
1306
|
+
minWidth: td.style.minWidth,
|
|
1307
|
+
maxWidth: td.style.maxWidth
|
|
1308
|
+
})) : null;
|
|
1309
|
+
const showRowNumbers = this.config.showRowNumbers !== false && tr.firstElementChild?.classList.contains("num");
|
|
1310
|
+
this._fillRowCells(tr, row, index, showRowNumbers);
|
|
1311
|
+
if (widths) {
|
|
1312
|
+
widths.forEach((saved, i) => {
|
|
1313
|
+
const cell = tr.children[i];
|
|
1314
|
+
if (!saved.width || !cell) return;
|
|
1315
|
+
cell.style.width = saved.width;
|
|
1316
|
+
cell.style.minWidth = saved.minWidth;
|
|
1317
|
+
cell.style.maxWidth = saved.maxWidth;
|
|
1318
|
+
});
|
|
1319
|
+
}
|
|
1320
|
+
tr.classList.toggle("selected", this._state?.selected?.has(index) === true);
|
|
1321
|
+
if (focusedCol >= 0 && tr.children[focusedCol]) {
|
|
1322
|
+
tr.children[focusedCol].focus?.();
|
|
1323
|
+
}
|
|
1324
|
+
return true;
|
|
1325
|
+
}
|
|
1240
1326
|
/** Sync the (separate) header table's column widths to the body
|
|
1241
1327
|
* table's measured widths. Without this, the two tables compute
|
|
1242
1328
|
* widths independently and the columns drift apart. Locks both
|
|
1243
1329
|
* tables to `table-layout: fixed` and writes explicit width onto
|
|
1244
1330
|
* each header cell of every header row (header + filter row). */
|
|
1245
|
-
|
|
1331
|
+
/**
|
|
1332
|
+
* Measure every column's NATURAL content width, in one transient reflow.
|
|
1333
|
+
*
|
|
1334
|
+
* Extracted from `_syncHeaderWidths` so that auto-size can ask the same
|
|
1335
|
+
* question the fit pass asks, and get the same answer. Two measurement
|
|
1336
|
+
* passes would drift, and the drift would show as a double-click that
|
|
1337
|
+
* sized a column differently from the render that follows it.
|
|
1338
|
+
*
|
|
1339
|
+
* The pass ignores the CSS caps (the 150px-pinned first column, the 80px
|
|
1340
|
+
* min on the rest) and the filter-row inputs: `twm-dt-measuring` flips both
|
|
1341
|
+
* tables to `table-layout:auto; width:max-content` with those caps off (via
|
|
1342
|
+
* `!important`) for a single reflow, then reverts before paint — it is
|
|
1343
|
+
* never visible. Clearing inline widths first stops the last sync's forced
|
|
1344
|
+
* widths from constraining the measure.
|
|
1345
|
+
*
|
|
1346
|
+
* @returns {number[]|null} width per DOM column index, or null when there
|
|
1347
|
+
* is nothing laid out to measure.
|
|
1348
|
+
*/
|
|
1349
|
+
_measureNaturalWidths() {
|
|
1246
1350
|
const headerTable = this._headerTableEl;
|
|
1247
1351
|
const bodyTable = this._tableEl;
|
|
1248
|
-
if (!headerTable || !bodyTable) return;
|
|
1352
|
+
if (!headerTable || !bodyTable) return null;
|
|
1249
1353
|
const firstRow = bodyTable.querySelector("tbody > tr");
|
|
1250
|
-
if (!firstRow) return;
|
|
1354
|
+
if (!firstRow) return null;
|
|
1251
1355
|
const bodyCells = firstRow.children;
|
|
1252
|
-
if (!bodyCells.length) return;
|
|
1253
|
-
|
|
1254
|
-
headerRows.forEach((tr) => {
|
|
1356
|
+
if (!bodyCells.length) return null;
|
|
1357
|
+
headerTable.querySelectorAll("thead > tr").forEach((tr) => {
|
|
1255
1358
|
for (const th of tr.children) this._clearCellWidth(th);
|
|
1256
1359
|
});
|
|
1257
1360
|
for (const td of bodyCells) this._clearCellWidth(td);
|
|
@@ -1263,20 +1366,120 @@ var DataTable = class {
|
|
|
1263
1366
|
const labelRow = headerTable.querySelector("thead > tr");
|
|
1264
1367
|
const cols = bodyCells.length;
|
|
1265
1368
|
const natural = new Array(cols);
|
|
1369
|
+
const zoom = bodyTable.currentCSSZoom || 1;
|
|
1266
1370
|
for (let i = 0; i < cols; i++) {
|
|
1267
|
-
const body = bodyCells[i].getBoundingClientRect().width;
|
|
1268
|
-
const head = labelRow && labelRow.children[i] ? labelRow.children[i].getBoundingClientRect().width : 0;
|
|
1371
|
+
const body = bodyCells[i].getBoundingClientRect().width / zoom;
|
|
1372
|
+
const head = labelRow && labelRow.children[i] ? labelRow.children[i].getBoundingClientRect().width / zoom : 0;
|
|
1269
1373
|
natural[i] = Math.max(body, head);
|
|
1270
1374
|
}
|
|
1271
1375
|
headerTable.classList.remove("twm-dt-measuring");
|
|
1272
1376
|
bodyTable.classList.remove("twm-dt-measuring");
|
|
1377
|
+
return natural;
|
|
1378
|
+
}
|
|
1379
|
+
/**
|
|
1380
|
+
* Fit one column to its widest visible value and pin it there.
|
|
1381
|
+
*
|
|
1382
|
+
* The gesture is a double-click on the column's resize grip, which is where
|
|
1383
|
+
* every spreadsheet has put it — and the grip is this component's, which is
|
|
1384
|
+
* why the behaviour is too. A consumer that wanted this had no hook to hang
|
|
1385
|
+
* it on: `_installColumnResizers` bound `mousedown` and a `click` that only
|
|
1386
|
+
* suppressed the sort, and the measurement, the pin, the table-width
|
|
1387
|
+
* invariant and the write-through to `stateStore` are all private here.
|
|
1388
|
+
*
|
|
1389
|
+
* @param {number} domIdx column index INCLUDING the row-number column when
|
|
1390
|
+
* `showRowNumbers` is on — the same index space as `_colWidths`.
|
|
1391
|
+
* @param {{maxWidth?: number}} [opts] ceiling; defaults to AUTOSIZE_MAX_PX.
|
|
1392
|
+
* @returns {boolean} whether it had a layout to measure.
|
|
1393
|
+
*/
|
|
1394
|
+
autoSizeColumn(domIdx, opts = {}) {
|
|
1395
|
+
const natural = this._measureNaturalWidths();
|
|
1396
|
+
if (!natural || domIdx < 0 || domIdx >= natural.length) return false;
|
|
1397
|
+
this._colWidths[domIdx] = this._autoWidth(natural[domIdx], opts);
|
|
1398
|
+
this._colWidthsSig = this._colSig();
|
|
1399
|
+
this._savePersisted();
|
|
1400
|
+
this._syncHeaderWidths();
|
|
1401
|
+
return true;
|
|
1402
|
+
}
|
|
1403
|
+
/**
|
|
1404
|
+
* The same for every column at once.
|
|
1405
|
+
*
|
|
1406
|
+
* It REPLACES existing drag overrides rather than sizing around them: "fit
|
|
1407
|
+
* every column to its content" that quietly excepted the three columns you
|
|
1408
|
+
* had dragged would be a button whose result depends on history nobody can
|
|
1409
|
+
* see.
|
|
1410
|
+
*
|
|
1411
|
+
* The result may well be wider than the container — forty columns of real
|
|
1412
|
+
* content usually are — and that is the intended answer, not a failure:
|
|
1413
|
+
* `_syncHeaderWidths` honours overrides verbatim, sizes both tables to their
|
|
1414
|
+
* total and the body wrap scrolls sideways with the header following it.
|
|
1415
|
+
* Squeezing them to fit is what the automatic fit pass does on every render
|
|
1416
|
+
* already, so a button that did that would be a button that does nothing.
|
|
1417
|
+
*/
|
|
1418
|
+
autoSizeColumns(opts = {}) {
|
|
1419
|
+
const natural = this._measureNaturalWidths();
|
|
1420
|
+
if (!natural) return false;
|
|
1421
|
+
this._colWidths = {};
|
|
1422
|
+
for (let i = 0; i < natural.length; i++) {
|
|
1423
|
+
this._colWidths[i] = this._autoWidth(natural[i], opts);
|
|
1424
|
+
}
|
|
1425
|
+
this._colWidthsSig = this._colSig();
|
|
1426
|
+
this._savePersisted();
|
|
1427
|
+
this._syncHeaderWidths();
|
|
1428
|
+
return true;
|
|
1429
|
+
}
|
|
1430
|
+
/** One measured width, floored at the drag-resize minimum and capped, with
|
|
1431
|
+
* the same sub-pixel pad `_fitColumnWidths` adds against ellipsis. */
|
|
1432
|
+
_autoWidth(natural, opts = {}) {
|
|
1433
|
+
const max = opts.maxWidth ?? AUTOSIZE_MAX_PX;
|
|
1434
|
+
return Math.min(max, Math.max(40, Math.ceil((natural || 0) + 2)));
|
|
1435
|
+
}
|
|
1436
|
+
/**
|
|
1437
|
+
* Does every one of the `n` DOM columns already carry an override taken
|
|
1438
|
+
* against the column set that is on screen right now?
|
|
1439
|
+
*
|
|
1440
|
+
* The signature half is not belt-and-braces. `render()` drops `_colWidths`
|
|
1441
|
+
* when `_colSig` changes, but `_syncHeaderWidths` is also reached straight
|
|
1442
|
+
* from the ResizeObserver, which never goes through `render()` — so a set
|
|
1443
|
+
* of overrides measured against the PREVIOUS headers can still be sitting
|
|
1444
|
+
* in `_colWidths` when this is asked. Answering "pinned" then would skip
|
|
1445
|
+
* the measure and hand `_fitColumnWidths` widths keyed to columns that are
|
|
1446
|
+
* no longer there, each one landing on its neighbour.
|
|
1447
|
+
*
|
|
1448
|
+
* `== null` rather than a falsy test, to match `_fitColumnWidths`' own
|
|
1449
|
+
* `overrides[i] != null`: a legitimately-stored 0 must be read the same way
|
|
1450
|
+
* in both places or the two disagree about which columns are flexible.
|
|
1451
|
+
*/
|
|
1452
|
+
_allColumnsPinned(n) {
|
|
1453
|
+
if (!n || this._colWidthsSig !== this._colSig()) return false;
|
|
1454
|
+
for (let i = 0; i < n; i++) {
|
|
1455
|
+
if (this._colWidths[i] == null) return false;
|
|
1456
|
+
}
|
|
1457
|
+
return true;
|
|
1458
|
+
}
|
|
1459
|
+
/** Sync the (separate) header table's column widths to the body
|
|
1460
|
+
* table's measured widths. Without this, the two tables compute
|
|
1461
|
+
* widths independently and the columns drift apart. Locks both
|
|
1462
|
+
* tables to `table-layout: fixed` and writes explicit width onto
|
|
1463
|
+
* each header cell of every header row (header + filter row). */
|
|
1464
|
+
_syncHeaderWidths() {
|
|
1465
|
+
const headerTable = this._headerTableEl;
|
|
1466
|
+
const bodyTable = this._tableEl;
|
|
1467
|
+
if (!headerTable || !bodyTable) return;
|
|
1468
|
+
const firstRow = bodyTable.querySelector("tbody > tr");
|
|
1469
|
+
if (!firstRow) return;
|
|
1470
|
+
const bodyCells = firstRow.children;
|
|
1471
|
+
if (!bodyCells.length) return;
|
|
1472
|
+
const headerRows = headerTable.querySelectorAll("thead > tr");
|
|
1473
|
+
const natural = this._allColumnsPinned(bodyCells.length) ? new Array(bodyCells.length).fill(0) : this._measureNaturalWidths();
|
|
1474
|
+
if (!natural) return;
|
|
1273
1475
|
const wrap = this._tableWrapEl;
|
|
1274
1476
|
const headerWrap = this._headerWrapEl;
|
|
1275
1477
|
const avail = wrap ? wrap.clientWidth : 0;
|
|
1276
|
-
const firstIdx = this.config.showRowNumbers &&
|
|
1478
|
+
const firstIdx = this.config.showRowNumbers && bodyCells.length > 1 ? 1 : 0;
|
|
1277
1479
|
const widths = this._fitColumnWidths(natural, avail, {
|
|
1278
1480
|
firstIdx,
|
|
1279
|
-
overrides: this._colWidths
|
|
1481
|
+
overrides: this._colWidths,
|
|
1482
|
+
fit: this.config.columnFit
|
|
1280
1483
|
});
|
|
1281
1484
|
const total = widths.reduce((a, b) => a + b, 0);
|
|
1282
1485
|
headerRows.forEach((tr) => {
|
|
@@ -1300,6 +1503,7 @@ var DataTable = class {
|
|
|
1300
1503
|
if (wrap && headerWrap) {
|
|
1301
1504
|
const sbw = Math.max(0, wrap.offsetWidth - wrap.clientWidth);
|
|
1302
1505
|
headerWrap.style.paddingRight = sbw ? `${sbw}px` : "";
|
|
1506
|
+
this._wrapperEl?.style?.setProperty("--twm-dt-gutter", `${sbw}px`);
|
|
1303
1507
|
}
|
|
1304
1508
|
this._syncHeaderScroll();
|
|
1305
1509
|
this._updateCellTooltips();
|
|
@@ -1318,9 +1522,17 @@ var DataTable = class {
|
|
|
1318
1522
|
* the rest (trim the widest first) until it fits; only if even the
|
|
1319
1523
|
* floors overflow do we give up and let the body scroll sideways.
|
|
1320
1524
|
*
|
|
1525
|
+
* Under `fit: 'content'` the first and last of those goals invert: each
|
|
1526
|
+
* unpinned column asks for its own content width through the same
|
|
1527
|
+
* `_autoWidth` the grip double-click uses, and an overflow is the answer
|
|
1528
|
+
* rather than a problem — the caller wanted a table that scrolls sideways,
|
|
1529
|
+
* not one squeezed toward the floor. The grow-to-fill branch is shared by
|
|
1530
|
+
* both, because a table narrower than its container leaves a dead gap on
|
|
1531
|
+
* the right under either reading.
|
|
1532
|
+
*
|
|
1321
1533
|
* @param {number[]} natural measured content width per column (px)
|
|
1322
1534
|
* @param {number} avail usable width of the body wrap (px)
|
|
1323
|
-
* @param {{firstIdx?:number, overrides?:Object}} [opts]
|
|
1535
|
+
* @param {{firstIdx?:number, overrides?:Object, fit?:'container'|'content'}} [opts]
|
|
1324
1536
|
* @returns {number[]} final width per column (px)
|
|
1325
1537
|
*/
|
|
1326
1538
|
_fitColumnWidths(natural, avail, opts = {}) {
|
|
@@ -1331,6 +1543,7 @@ var DataTable = class {
|
|
|
1331
1543
|
const n = natural.length;
|
|
1332
1544
|
const firstIdx = opts.firstIdx ?? 0;
|
|
1333
1545
|
const overrides = opts.overrides || {};
|
|
1546
|
+
const toContent = opts.fit === "content";
|
|
1334
1547
|
const desired = new Array(n);
|
|
1335
1548
|
const pinned = new Array(n).fill(false);
|
|
1336
1549
|
for (let i = 0; i < n; i++) {
|
|
@@ -1339,6 +1552,10 @@ var DataTable = class {
|
|
|
1339
1552
|
pinned[i] = true;
|
|
1340
1553
|
continue;
|
|
1341
1554
|
}
|
|
1555
|
+
if (toContent) {
|
|
1556
|
+
desired[i] = this._autoWidth(natural[i]);
|
|
1557
|
+
continue;
|
|
1558
|
+
}
|
|
1342
1559
|
const cap = i === firstIdx ? FIRST_CAP : CAP;
|
|
1343
1560
|
desired[i] = Math.min(cap, Math.max(FLOOR, Math.ceil(natural[i] + PAD)));
|
|
1344
1561
|
}
|
|
@@ -1349,13 +1566,15 @@ var DataTable = class {
|
|
|
1349
1566
|
const flex = [];
|
|
1350
1567
|
for (let i = 0; i < n; i++) if (!pinned[i]) flex.push(i);
|
|
1351
1568
|
const slack = avail - sum;
|
|
1352
|
-
if (slack > 0
|
|
1353
|
-
const
|
|
1354
|
-
|
|
1355
|
-
widths[
|
|
1569
|
+
if (slack > 0) {
|
|
1570
|
+
const targets = flex.length ? flex : [n - 1];
|
|
1571
|
+
const per = Math.floor(slack / targets.length);
|
|
1572
|
+
for (const i of targets) widths[i] += per;
|
|
1573
|
+
widths[targets[targets.length - 1]] += slack - per * targets.length;
|
|
1356
1574
|
}
|
|
1357
1575
|
return widths;
|
|
1358
1576
|
}
|
|
1577
|
+
if (toContent) return widths;
|
|
1359
1578
|
let protectedSum = 0;
|
|
1360
1579
|
const shrinkable = [];
|
|
1361
1580
|
for (let i = 0; i < n; i++) {
|
|
@@ -1407,8 +1626,9 @@ var DataTable = class {
|
|
|
1407
1626
|
}
|
|
1408
1627
|
/** Wire the body wrap's horizontal scroll to the header. The header
|
|
1409
1628
|
* sits in an overflow:hidden wrap, so it can't scroll sideways on
|
|
1410
|
-
* its own —
|
|
1411
|
-
* render against the freshly-built
|
|
1629
|
+
* its own — `_syncHeaderScroll` scrolls it programmatically to the
|
|
1630
|
+
* body's `scrollLeft`. Re-installed each render against the freshly-built
|
|
1631
|
+
* wrap. */
|
|
1412
1632
|
_installHeaderScrollSync() {
|
|
1413
1633
|
const wrap = this._tableWrapEl;
|
|
1414
1634
|
if (!wrap) return;
|
|
@@ -1420,14 +1640,56 @@ var DataTable = class {
|
|
|
1420
1640
|
wrap.addEventListener("scroll", this._onBodyScroll, { passive: true });
|
|
1421
1641
|
this._syncHeaderScroll();
|
|
1422
1642
|
}
|
|
1423
|
-
/**
|
|
1424
|
-
*
|
|
1643
|
+
/**
|
|
1644
|
+
* Scroll the header wrap to match the body's horizontal scroll so the
|
|
1645
|
+
* columns stay aligned when the table overflows sideways.
|
|
1646
|
+
*
|
|
1647
|
+
* ══ C29. A TRANSFORM IS WHAT MAKES A STICKY COLUMN IMPOSSIBLE ═══════
|
|
1648
|
+
*
|
|
1649
|
+
* This wrote `headerTable.style.transform = translateX(-scrollLeft)`. It
|
|
1650
|
+
* aligns the two tables perfectly and it forecloses `position: sticky`
|
|
1651
|
+
* entirely, because the two halves of the table then pin against different
|
|
1652
|
+
* things: a sticky cell in the BODY pins to the body wrap's scrollport,
|
|
1653
|
+
* while its header counterpart is moved by a transform inside a box that
|
|
1654
|
+
* never scrolls at all. Scroll sideways and the pinned body column stands
|
|
1655
|
+
* still while its header slides away with everything else — so a table
|
|
1656
|
+
* with a trailing verb column (the row-actions column Tables needs pinned
|
|
1657
|
+
* to the right edge) can have a sticky body or an aligned header, never
|
|
1658
|
+
* both. A transform also establishes a containing block for fixed/sticky
|
|
1659
|
+
* descendants, which breaks the header cell independently of the offset.
|
|
1660
|
+
*
|
|
1661
|
+
* AN `overflow: hidden` BOX IS STILL A SCROLL CONTAINER. It has no
|
|
1662
|
+
* scrollbar and no user affordance, and `scrollLeft` moves it exactly like
|
|
1663
|
+
* any other. Scrolling the wrap rather than transforming its child gives
|
|
1664
|
+
* the header a real scrollport at the same offset as the body's, which is
|
|
1665
|
+
* the one arrangement in which a sticky cell on each side pins to the same
|
|
1666
|
+
* place. Identical for ordinary content: same pixels, no transform, no new
|
|
1667
|
+
* containing block.
|
|
1668
|
+
*
|
|
1669
|
+
* THE CLAMP IS ALREADY PAID FOR. A scroll container clamps `scrollLeft` to
|
|
1670
|
+
* `scrollWidth - clientWidth`, and the body wrap carries a vertical
|
|
1671
|
+
* scrollbar the header wrap does not — so the header would clamp short by
|
|
1672
|
+
* the scrollbar width and desync at the far right. `_syncHeaderWidths`
|
|
1673
|
+
* already pads the header wrap by exactly that gutter (`paddingRight`,
|
|
1674
|
+
* :1905), and end padding counts toward `scrollWidth`, so the two maxima
|
|
1675
|
+
* coincide. The residual below is the honest belt for the day some engine
|
|
1676
|
+
* disagrees about that: it is zero in the ordinary case, so the transform
|
|
1677
|
+
* is not set and sticky keeps working, and it is the difference rather
|
|
1678
|
+
* than the whole offset if it is ever not.
|
|
1679
|
+
*/
|
|
1425
1680
|
_syncHeaderScroll() {
|
|
1426
1681
|
const wrap = this._tableWrapEl;
|
|
1427
1682
|
const headerTable = this._headerTableEl;
|
|
1428
1683
|
if (!wrap || !headerTable) return;
|
|
1429
1684
|
const x = wrap.scrollLeft;
|
|
1430
|
-
|
|
1685
|
+
const headerWrap = this._headerWrapEl;
|
|
1686
|
+
if (!headerWrap) {
|
|
1687
|
+
headerTable.style.transform = x ? `translateX(${-x}px)` : "";
|
|
1688
|
+
return;
|
|
1689
|
+
}
|
|
1690
|
+
headerWrap.scrollLeft = x;
|
|
1691
|
+
const residual = x - headerWrap.scrollLeft;
|
|
1692
|
+
headerTable.style.transform = residual ? `translateX(${-residual}px)` : "";
|
|
1431
1693
|
}
|
|
1432
1694
|
/** Drag-to-resize: hang a thin grab handle off the right edge of
|
|
1433
1695
|
* every header cell. Dragging it writes a per-column width override
|
|
@@ -1447,6 +1709,11 @@ var DataTable = class {
|
|
|
1447
1709
|
(ev) => this._beginColResize(ev, domIdx)
|
|
1448
1710
|
);
|
|
1449
1711
|
grip.addEventListener("click", (ev) => ev.stopPropagation());
|
|
1712
|
+
grip.addEventListener("dblclick", (ev) => {
|
|
1713
|
+
ev.preventDefault();
|
|
1714
|
+
ev.stopPropagation();
|
|
1715
|
+
this.autoSizeColumn(domIdx);
|
|
1716
|
+
});
|
|
1450
1717
|
th.appendChild(grip);
|
|
1451
1718
|
});
|
|
1452
1719
|
}
|
|
@@ -1456,23 +1723,13 @@ var DataTable = class {
|
|
|
1456
1723
|
const headerTable = this._headerTableEl;
|
|
1457
1724
|
const bodyTable = this._tableEl;
|
|
1458
1725
|
const headRow = headerTable && headerTable.querySelector("thead > tr");
|
|
1459
|
-
const bodyRow = bodyTable && bodyTable.querySelector("tbody > tr");
|
|
1460
1726
|
if (!headRow) return;
|
|
1461
1727
|
const startWidths = [...headRow.children].map(
|
|
1462
1728
|
(c) => c.getBoundingClientRect().width
|
|
1463
1729
|
);
|
|
1464
1730
|
headerTable.style.tableLayout = "fixed";
|
|
1465
1731
|
if (bodyTable) bodyTable.style.tableLayout = "fixed";
|
|
1466
|
-
|
|
1467
|
-
this._colWidths[i] = w;
|
|
1468
|
-
headerTable.querySelectorAll("thead > tr").forEach((tr) => {
|
|
1469
|
-
if (tr.children[i]) this._setCellWidth(tr.children[i], w);
|
|
1470
|
-
});
|
|
1471
|
-
if (bodyRow && bodyRow.children[i]) {
|
|
1472
|
-
this._setCellWidth(bodyRow.children[i], w);
|
|
1473
|
-
}
|
|
1474
|
-
};
|
|
1475
|
-
startWidths.forEach((w, i) => applyCol(i, w));
|
|
1732
|
+
startWidths.forEach((w, i) => this._pinColumnWidth(i, w));
|
|
1476
1733
|
this._applyTableWidth();
|
|
1477
1734
|
const startX = ev.clientX;
|
|
1478
1735
|
const MIN = 40;
|
|
@@ -1482,8 +1739,8 @@ var DataTable = class {
|
|
|
1482
1739
|
MIN,
|
|
1483
1740
|
Math.round(startWidths[domIdx] + (mv.clientX - startX))
|
|
1484
1741
|
);
|
|
1485
|
-
|
|
1486
|
-
this._applyTableWidth();
|
|
1742
|
+
this._pinColumnWidth(domIdx, w);
|
|
1743
|
+
this._applyTableWidth(domIdx);
|
|
1487
1744
|
this._syncHeaderScroll();
|
|
1488
1745
|
};
|
|
1489
1746
|
const onUp = () => {
|
|
@@ -1496,19 +1753,60 @@ var DataTable = class {
|
|
|
1496
1753
|
document.addEventListener("mousemove", onMove);
|
|
1497
1754
|
document.addEventListener("mouseup", onUp);
|
|
1498
1755
|
}
|
|
1499
|
-
/**
|
|
1500
|
-
*
|
|
1501
|
-
*
|
|
1502
|
-
|
|
1756
|
+
/** Pin one column to an explicit width across BOTH tables (every
|
|
1757
|
+
* header row + the body's first row) and record it as a user
|
|
1758
|
+
* override so `_syncHeaderWidths` honors it on later renders. The
|
|
1759
|
+
* single place that writes a column width during a drag / fill. */
|
|
1760
|
+
_pinColumnWidth(i, w) {
|
|
1761
|
+
const headerTable = this._headerTableEl;
|
|
1762
|
+
const bodyTable = this._tableEl;
|
|
1763
|
+
if (!headerTable) return;
|
|
1764
|
+
this._colWidths[i] = w;
|
|
1765
|
+
headerTable.querySelectorAll("thead > tr").forEach((tr) => {
|
|
1766
|
+
if (tr.children[i]) this._setCellWidth(tr.children[i], w);
|
|
1767
|
+
});
|
|
1768
|
+
const bodyRow = bodyTable && bodyTable.querySelector("tbody > tr");
|
|
1769
|
+
if (bodyRow && bodyRow.children[i]) this._setCellWidth(bodyRow.children[i], w);
|
|
1770
|
+
}
|
|
1771
|
+
/** Size both tables so a widened column grows the table (→ horizontal
|
|
1772
|
+
* scroll) rather than stealing from its neighbours — while enforcing
|
|
1773
|
+
* the TABLE WIDTH INVARIANT: the table is never narrower than its
|
|
1774
|
+
* container. Any width freed by dragging a column in stretches a
|
|
1775
|
+
* flexible column (the last one, stepping off the column being
|
|
1776
|
+
* dragged) so the table always spans ≥100% of the wrap.
|
|
1777
|
+
*
|
|
1778
|
+
* @param {number|null} dragIdx column the user is actively dragging,
|
|
1779
|
+
* so the fill lands on a DIFFERENT column and doesn't fight the
|
|
1780
|
+
* drag. Null (non-drag callers) lets the last column flex. */
|
|
1781
|
+
_applyTableWidth(dragIdx = null) {
|
|
1503
1782
|
const headerTable = this._headerTableEl;
|
|
1504
1783
|
const bodyTable = this._tableEl;
|
|
1505
1784
|
const headRow = headerTable && headerTable.querySelector("thead > tr");
|
|
1506
1785
|
if (!headRow) return;
|
|
1507
|
-
|
|
1508
|
-
|
|
1786
|
+
const cells = [...headRow.children];
|
|
1787
|
+
const cols = cells.length;
|
|
1788
|
+
if (cols === 0) return;
|
|
1789
|
+
const FLOOR = 40;
|
|
1790
|
+
const widthOf = (th) => {
|
|
1509
1791
|
const px = parseFloat(th.style.width);
|
|
1510
|
-
|
|
1792
|
+
return Number.isFinite(px) ? px : th.getBoundingClientRect().width;
|
|
1793
|
+
};
|
|
1794
|
+
const cur = cells.map(widthOf);
|
|
1795
|
+
const wrap = this._tableWrapEl;
|
|
1796
|
+
const avail = wrap ? wrap.clientWidth : 0;
|
|
1797
|
+
let flexIdx = cols - 1;
|
|
1798
|
+
if (dragIdx != null && flexIdx === dragIdx) flexIdx -= 1;
|
|
1799
|
+
if (avail > 1 && flexIdx >= 0 && flexIdx !== dragIdx) {
|
|
1800
|
+
let rest = 0;
|
|
1801
|
+
for (let i = 0; i < cols; i++) if (i !== flexIdx) rest += cur[i];
|
|
1802
|
+
const fill = Math.max(FLOOR, Math.round(avail - rest));
|
|
1803
|
+
if (Math.round(fill) !== Math.round(cur[flexIdx])) {
|
|
1804
|
+
this._pinColumnWidth(flexIdx, fill);
|
|
1805
|
+
cur[flexIdx] = fill;
|
|
1806
|
+
}
|
|
1511
1807
|
}
|
|
1808
|
+
let total = 0;
|
|
1809
|
+
for (const w of cur) total += w;
|
|
1512
1810
|
total = Math.ceil(total);
|
|
1513
1811
|
headerTable.style.width = `${total}px`;
|
|
1514
1812
|
if (bodyTable) bodyTable.style.width = `${total}px`;
|
|
@@ -1767,4 +2065,4 @@ export {
|
|
|
1767
2065
|
createRafResizeObserver,
|
|
1768
2066
|
DataTable
|
|
1769
2067
|
};
|
|
1770
|
-
//# sourceMappingURL=chunk-
|
|
2068
|
+
//# sourceMappingURL=chunk-QIU5S2RU.js.map
|