dsh-neotui 0.0.15 → 0.0.16
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/package.json +1 -1
- package/src/views.js +47 -20
package/package.json
CHANGED
package/src/views.js
CHANGED
|
@@ -836,8 +836,35 @@ export class ChatView extends Widget {
|
|
|
836
836
|
}
|
|
837
837
|
|
|
838
838
|
#clickLine(y, ev) {
|
|
839
|
+
// Capture the viewport geometry BEFORE any flush: the flush can grow the
|
|
840
|
+
// streaming tail (and atBottom snaps the view 1–2 rows), so anchoring on
|
|
841
|
+
// the post-flush state would reproduce exactly the shift the user saw.
|
|
842
|
+
// The pre-flush lineMap is what the user clicked on.
|
|
843
|
+
const lineKey = (m) => (m ? `${m.nodeIdx}:${m.blockIdx ?? "n"}` : null);
|
|
844
|
+
const topKey = lineKey(this.lineMap?.[this.view.scrollY]) ?? null;
|
|
845
|
+
let topFirst = -1;
|
|
846
|
+
if (topKey !== null) {
|
|
847
|
+
for (let i = 0; i < this.lineMap.length; i++) {
|
|
848
|
+
if (lineKey(this.lineMap[i]) === topKey) { topFirst = i; break; }
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
const topOffset = topFirst >= 0 ? this.view.scrollY - topFirst : 0;
|
|
852
|
+
// the clicked block's header (first non-empty line with its mark) and its
|
|
853
|
+
// pre-click viewport row — the PRIMARY anchor: keep the clicked thing put
|
|
854
|
+
const preInfo = this.lineMap?.[y];
|
|
855
|
+
const clickedMatch = preInfo?.blockIdx !== null
|
|
856
|
+
? (m) => m?.nodeIdx === preInfo?.nodeIdx && m?.blockIdx === preInfo?.blockIdx
|
|
857
|
+
: (m) => m?.nodeIdx === preInfo?.nodeIdx;
|
|
858
|
+
const firstNonEmpty = (match) => {
|
|
859
|
+
for (let i = 0; i < this.lineMap.length; i++) {
|
|
860
|
+
if (match(this.lineMap[i]) && (this.lines[i] ?? []).some((g) => g.t.trim() !== "")) return i;
|
|
861
|
+
}
|
|
862
|
+
return -1;
|
|
863
|
+
};
|
|
864
|
+
const preHeaderIdx = preInfo ? firstNonEmpty(clickedMatch) : -1;
|
|
865
|
+
const preHeaderRow = preHeaderIdx >= 0 ? preHeaderIdx - this.view.scrollY : null;
|
|
839
866
|
// A queued (deferred) rebuild would leave lineMap one frame behind the
|
|
840
|
-
// nodes array; flush
|
|
867
|
+
// nodes array; flush now so the click maps to the CURRENT node/block.
|
|
841
868
|
this.flushRebuild();
|
|
842
869
|
// map rendered line back to node via cached line→node map
|
|
843
870
|
const info = this.lineMap?.[y];
|
|
@@ -848,22 +875,20 @@ export class ChatView extends Widget {
|
|
|
848
875
|
if (ref) { this.app.openImage(ref, { all: node.images, index: info.imgIdx }); return true; }
|
|
849
876
|
}
|
|
850
877
|
const node = this.nodes[info.nodeIdx];
|
|
851
|
-
// Expand/collapse changes the line count
|
|
852
|
-
//
|
|
853
|
-
//
|
|
854
|
-
//
|
|
855
|
-
// at all when the top line survives (e.g. clicking a block header), and
|
|
856
|
-
// lands on the fold's last line when the top line itself was collapsed.
|
|
857
|
-
const lineKey = (m) => (m ? `${m.nodeIdx}:${m.blockIdx ?? "n"}` : null);
|
|
858
|
-
const topKey = lineKey(this.lineMap[this.view.scrollY]);
|
|
859
|
-
let topFirst = -1;
|
|
860
|
-
if (topKey !== null) {
|
|
861
|
-
for (let i = 0; i < this.lineMap.length; i++) {
|
|
862
|
-
if (lineKey(this.lineMap[i]) === topKey) { topFirst = i; break; }
|
|
863
|
-
}
|
|
864
|
-
}
|
|
865
|
-
const topOffset = topFirst >= 0 ? this.view.scrollY - topFirst : 0;
|
|
878
|
+
// Expand/collapse changes the line count around the click. Primary
|
|
879
|
+
// anchor: the clicked block's header stays at its pre-click viewport
|
|
880
|
+
// row (zero shift). Fallback (header was above the viewport — a deep
|
|
881
|
+
// content click): restore the pre-click viewport-top position.
|
|
866
882
|
const reanchor = () => {
|
|
883
|
+
// primary anchor applies only when the header was ON-SCREEN (a deep
|
|
884
|
+
// content click has the header above the viewport — use the fallback)
|
|
885
|
+
if (preHeaderRow !== null && preHeaderRow >= 0 && preHeaderIdx >= 0) {
|
|
886
|
+
const h2 = firstNonEmpty((m) => m?.nodeIdx === info.nodeIdx && m?.blockIdx === info.blockIdx);
|
|
887
|
+
if (h2 >= 0) {
|
|
888
|
+
this.view.scrollY = Math.max(0, Math.min(h2 - preHeaderRow, this.view.maxScroll()));
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
867
892
|
if (topKey === null || topFirst < 0) return;
|
|
868
893
|
let first = -1, last = -1;
|
|
869
894
|
for (let i = 0; i < this.lineMap.length; i++) {
|
|
@@ -874,8 +899,10 @@ export class ChatView extends Widget {
|
|
|
874
899
|
if (first < 0) return;
|
|
875
900
|
const target = Math.min(first + topOffset, last);
|
|
876
901
|
this.view.scrollY = Math.max(0, Math.min(target, this.view.maxScroll()));
|
|
877
|
-
|
|
878
|
-
|
|
902
|
+
};
|
|
903
|
+
// never park the viewport on a blank separator row — it reads as a
|
|
904
|
+
// spurious offset right after the click
|
|
905
|
+
const nudge = () => {
|
|
879
906
|
while (
|
|
880
907
|
this.view.scrollY < this.lineMap.length - 1 &&
|
|
881
908
|
!(this.lines[this.view.scrollY] ?? []).some((g) => g.t.trim() !== "")
|
|
@@ -895,7 +922,7 @@ export class ChatView extends Widget {
|
|
|
895
922
|
else this.collapsedBlocks.add(key);
|
|
896
923
|
}
|
|
897
924
|
this.#rebuild();
|
|
898
|
-
reanchor();
|
|
925
|
+
reanchor(); nudge();
|
|
899
926
|
return true;
|
|
900
927
|
}
|
|
901
928
|
}
|
|
@@ -903,7 +930,7 @@ export class ChatView extends Widget {
|
|
|
903
930
|
if (this.expanded.has(info.nodeIdx)) this.expanded.delete(info.nodeIdx);
|
|
904
931
|
else this.expanded.add(info.nodeIdx);
|
|
905
932
|
this.#rebuild();
|
|
906
|
-
reanchor();
|
|
933
|
+
reanchor(); nudge();
|
|
907
934
|
return true;
|
|
908
935
|
}
|
|
909
936
|
}
|