@nanobpm/urban 0.40.1 → 0.40.2
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.
|
@@ -1084,6 +1084,19 @@ function renderDataGrid(node) {
|
|
|
1084
1084
|
try { selStart = active.selectionStart; selEnd = active.selectionEnd; } catch (e) { /* non-text field */ }
|
|
1085
1085
|
}
|
|
1086
1086
|
}
|
|
1087
|
+
// Capture scroll offsets of scrolled descendants before the swap. An open detail node is
|
|
1088
|
+
// reused (same element instance), but replaceChildren() detaches it, which resets the
|
|
1089
|
+
// scrollTop/scrollLeft of any inner scroll container (e.g. a scrolled-into .pc-transcript,
|
|
1090
|
+
// max-height + overflow:auto) back to 0. Only built (open) detail subtrees are re-attached
|
|
1091
|
+
// by the rebuild, so walk just those — not the whole grid — to keep this O(open panels)
|
|
1092
|
+
// rather than O(grid size). Restoration below skips any node that isn't re-attached.
|
|
1093
|
+
const scrollSaved = [];
|
|
1094
|
+
for (const entry of detailNodes.values()) {
|
|
1095
|
+
if (!entry.built) continue;
|
|
1096
|
+
for (const sc of entry.dtr.querySelectorAll("*")) {
|
|
1097
|
+
if (sc.scrollTop || sc.scrollLeft) scrollSaved.push([sc, sc.scrollTop, sc.scrollLeft]);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1087
1100
|
tbody.replaceChildren();
|
|
1088
1101
|
for (const row of rows) renderRow(row);
|
|
1089
1102
|
if (!rows.length) tbody.append(el("tr", {}, el("td", { colspan: span }, "No rows")));
|
|
@@ -1095,6 +1108,10 @@ function renderDataGrid(node) {
|
|
|
1095
1108
|
try { active.setSelectionRange(selStart, selEnd); } catch (e) { /* non-text field */ }
|
|
1096
1109
|
}
|
|
1097
1110
|
}
|
|
1111
|
+
// Restore scroll depth on the reused nodes that survived the swap (still connected).
|
|
1112
|
+
for (const [sc, top, left] of scrollSaved) {
|
|
1113
|
+
if (sc.isConnected) { sc.scrollTop = top; sc.scrollLeft = left; }
|
|
1114
|
+
}
|
|
1098
1115
|
} catch (e) {
|
|
1099
1116
|
tbody.replaceChildren(el("tr", {}, el("td", { colspan: span }, String(e.message || e))));
|
|
1100
1117
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/urban",
|
|
3
|
-
"version": "0.40.
|
|
3
|
+
"version": "0.40.2",
|
|
4
4
|
"description": "Urban: build and run code-first apps on Nano — runtime, derivation toolkit, and CLI in one. Author durable processes with @nanobpm/workflow (defineFlow), a typed datasource, forms, triggers and surfaces from a nano.app.json manifest, on Node or Deno.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -234,6 +234,21 @@ test("renderer keeps the detail input focused across the refresh poll", async ()
|
|
|
234
234
|
assert.match(js, /s\.addRange\(savedRange\)/);
|
|
235
235
|
});
|
|
236
236
|
|
|
237
|
+
test("renderer preserves inner scroll depth across the refresh poll", async () => {
|
|
238
|
+
// Regression: an open detail row is reused across the 5s poll, but tbody.replaceChildren()
|
|
239
|
+
// detaches it, which resets the scrollTop of any inner overflow:auto container — so a
|
|
240
|
+
// scrolled-into transcript (.pc-transcript, max-height + overflow:auto) jumped back to the
|
|
241
|
+
// top every refreshMs. The client now captures scroll offsets of scrolled descendants
|
|
242
|
+
// before the swap and restores them on the nodes that stay connected afterwards.
|
|
243
|
+
const res = await dispatch("GET", "/app/runtime.js");
|
|
244
|
+
const js = res.body ?? "";
|
|
245
|
+
// Offsets are captured from scrolled descendants before replaceChildren()…
|
|
246
|
+
assert.match(js, /const scrollSaved = \[\]/);
|
|
247
|
+
assert.match(js, /if\s*\(\s*sc\.scrollTop\s*\|\|\s*sc\.scrollLeft\s*\)\s*scrollSaved\.push/);
|
|
248
|
+
// …and restored only on nodes re-attached by the rebuild (still connected).
|
|
249
|
+
assert.match(js, /if\s*\(\s*sc\.isConnected\s*\)\s*\{\s*sc\.scrollTop\s*=\s*top\s*;\s*sc\.scrollLeft\s*=\s*left\s*;\s*\}/);
|
|
250
|
+
});
|
|
251
|
+
|
|
237
252
|
test("renderer wires a column's linkField to a new-tab anchor", async () => {
|
|
238
253
|
const res = await dispatch("GET", "/app/runtime.js");
|
|
239
254
|
const js = res.body ?? "";
|
|
@@ -1132,6 +1132,19 @@ function renderDataGrid(node) {
|
|
|
1132
1132
|
try { selStart = active.selectionStart; selEnd = active.selectionEnd; } catch (e) { /* non-text field */ }
|
|
1133
1133
|
}
|
|
1134
1134
|
}
|
|
1135
|
+
// Capture scroll offsets of scrolled descendants before the swap. An open detail node is
|
|
1136
|
+
// reused (same element instance), but replaceChildren() detaches it, which resets the
|
|
1137
|
+
// scrollTop/scrollLeft of any inner scroll container (e.g. a scrolled-into .pc-transcript,
|
|
1138
|
+
// max-height + overflow:auto) back to 0. Only built (open) detail subtrees are re-attached
|
|
1139
|
+
// by the rebuild, so walk just those — not the whole grid — to keep this O(open panels)
|
|
1140
|
+
// rather than O(grid size). Restoration below skips any node that isn't re-attached.
|
|
1141
|
+
const scrollSaved = [];
|
|
1142
|
+
for (const entry of detailNodes.values()) {
|
|
1143
|
+
if (!entry.built) continue;
|
|
1144
|
+
for (const sc of entry.dtr.querySelectorAll("*")) {
|
|
1145
|
+
if (sc.scrollTop || sc.scrollLeft) scrollSaved.push([sc, sc.scrollTop, sc.scrollLeft]);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1135
1148
|
tbody.replaceChildren();
|
|
1136
1149
|
for (const row of rows) renderRow(row);
|
|
1137
1150
|
if (!rows.length) tbody.append(el("tr", {}, el("td", { colspan: span }, "No rows")));
|
|
@@ -1143,6 +1156,10 @@ function renderDataGrid(node) {
|
|
|
1143
1156
|
try { active.setSelectionRange(selStart, selEnd); } catch (e) { /* non-text field */ }
|
|
1144
1157
|
}
|
|
1145
1158
|
}
|
|
1159
|
+
// Restore scroll depth on the reused nodes that survived the swap (still connected).
|
|
1160
|
+
for (const [sc, top, left] of scrollSaved) {
|
|
1161
|
+
if (sc.isConnected) { sc.scrollTop = top; sc.scrollLeft = left; }
|
|
1162
|
+
}
|
|
1146
1163
|
} catch (e) {
|
|
1147
1164
|
tbody.replaceChildren(el("tr", {}, el("td", { colspan: span }, String(e.message || e))));
|
|
1148
1165
|
}
|