Skip to content

Commit 4f426f8

Browse files
committed
implement better scroll handling and clipping for immortal dom elts
1 parent 6379f73 commit 4f426f8

File tree

2 files changed

+182
-107
lines changed

2 files changed

+182
-107
lines changed

src/packages/frontend/jupyter/cell-list.tsx

Lines changed: 79 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import { Cell } from "./cell";
3939
import HeadingTagComponent from "./heading-tag";
4040
interface IFrameContextType {
4141
iframeDivRef?: MutableRefObject<any>;
42+
cellListDivRef?: MutableRefObject<any>;
4243
iframeOnScrolls?: { [key: string]: () => void };
4344
}
4445
const IFrameContext = createContext<IFrameContextType>({});
@@ -597,85 +598,90 @@ export const CellList: React.FC<CellListProps> = (props: CellListProps) => {
597598
let body;
598599

599600
const iframeDivRef = useRef<HTMLDivElement>(null);
601+
const cellListDivRef = useRef<HTMLDivElement>(null);
600602
const virtuosoHeightsRef = useRef<{ [index: number]: number }>({});
601603
if (use_windowed_list) {
602604
body = (
603-
<IFrameContext.Provider value={{ iframeDivRef, iframeOnScrolls }}>
604-
<Virtuoso
605-
ref={virtuosoRef}
606-
onClick={actions != null && complete != null ? on_click : undefined}
607-
topItemCount={EXTRA_TOP_CELLS}
608-
style={{
609-
fontSize: `${font_size}px`,
610-
flex: 1,
611-
overflowX: "hidden",
612-
}}
613-
totalCount={
614-
cell_list.size +
615-
EXTRA_TOP_CELLS /* +EXTRA_TOP_CELLS due to the iframe cell and style cell at the top */ +
616-
EXTRA_BOTTOM_CELLS
617-
}
618-
itemSize={(el) => {
619-
// We capture measured heights -- see big coment above the
620-
// the DivTempHeight component below for why this is needed
621-
// for Jupyter notebooks (but not most things).
622-
const h = el.getBoundingClientRect().height;
623-
// WARNING: This uses perhaps an internal implementation detail of
624-
// virtuoso, which I hope they don't change, which is that the index of
625-
// the elements whose height we're measuring is in the data-item-index
626-
// attribute.
627-
const data = el.getAttribute("data-item-index");
628-
if (data != null) {
629-
const index = parseInt(data);
630-
virtuosoHeightsRef.current[index] = h;
605+
<IFrameContext.Provider
606+
value={{ iframeDivRef, cellListDivRef, iframeOnScrolls }}
607+
>
608+
<div ref={cellListDivRef} className="smc-vfill">
609+
<Virtuoso
610+
ref={virtuosoRef}
611+
onClick={actions != null && complete != null ? on_click : undefined}
612+
topItemCount={EXTRA_TOP_CELLS}
613+
style={{
614+
fontSize: `${font_size}px`,
615+
flex: 1,
616+
overflowX: "hidden",
617+
}}
618+
totalCount={
619+
cell_list.size +
620+
EXTRA_TOP_CELLS /* +EXTRA_TOP_CELLS due to the iframe cell and style cell at the top */ +
621+
EXTRA_BOTTOM_CELLS
631622
}
632-
return h;
633-
}}
634-
itemContent={(index) => {
635-
if (index == 0) {
636-
return (
637-
<div key="iframes" ref={iframeDivRef} style={ITEM_STYLE}>
638-
iframes here
639-
</div>
640-
);
641-
} else if (index == 1) {
623+
itemSize={(el) => {
624+
// We capture measured heights -- see big coment above the
625+
// the DivTempHeight component below for why this is needed
626+
// for Jupyter notebooks (but not most things).
627+
const h = el.getBoundingClientRect().height;
628+
// WARNING: This uses perhaps an internal implementation detail of
629+
// virtuoso, which I hope they don't change, which is that the index of
630+
// the elements whose height we're measuring is in the data-item-index
631+
// attribute.
632+
const data = el.getAttribute("data-item-index");
633+
if (data != null) {
634+
const index = parseInt(data);
635+
virtuosoHeightsRef.current[index] = h;
636+
}
637+
return h;
638+
}}
639+
itemContent={(index) => {
640+
if (index == 0) {
641+
return (
642+
<div key="iframes" ref={iframeDivRef} style={ITEM_STYLE}>
643+
iframes here
644+
</div>
645+
);
646+
} else if (index == 1) {
647+
return (
648+
<div key="styles" ref={iframeDivRef} style={ITEM_STYLE}>
649+
<style>{allStyles}</style>
650+
</div>
651+
);
652+
} else if (index == cell_list.size + EXTRA_TOP_CELLS) {
653+
return BOTTOM_PADDING_CELL;
654+
}
655+
const id = cell_list.get(index - EXTRA_TOP_CELLS);
656+
if (id == null) return null;
657+
const h = virtuosoHeightsRef.current[index];
658+
if (actions == null) {
659+
return render_cell({
660+
id,
661+
isScrolling: false,
662+
index: index - EXTRA_TOP_CELLS,
663+
});
664+
}
642665
return (
643-
<div key="styles" ref={iframeDivRef} style={ITEM_STYLE}>
644-
<style>{allStyles}</style>
645-
</div>
666+
<SortableItem id={id} key={id}>
667+
<DivTempHeight height={h ? `${h}px` : undefined}>
668+
{render_cell({
669+
id,
670+
isScrolling: false,
671+
index: index - EXTRA_TOP_CELLS,
672+
isFirst: id === cell_list.get(0),
673+
isLast: id === cell_list.get(-1),
674+
})}
675+
</DivTempHeight>
676+
</SortableItem>
646677
);
647-
} else if (index == cell_list.size + EXTRA_TOP_CELLS) {
648-
return BOTTOM_PADDING_CELL;
649-
}
650-
const id = cell_list.get(index - EXTRA_TOP_CELLS);
651-
if (id == null) return null;
652-
const h = virtuosoHeightsRef.current[index];
653-
if (actions == null) {
654-
return render_cell({
655-
id,
656-
isScrolling: false,
657-
index: index - EXTRA_TOP_CELLS,
658-
});
659-
}
660-
return (
661-
<SortableItem id={id} key={id}>
662-
<DivTempHeight height={h ? `${h}px` : undefined}>
663-
{render_cell({
664-
id,
665-
isScrolling: false,
666-
index: index - EXTRA_TOP_CELLS,
667-
isFirst: id === cell_list.get(0),
668-
isLast: id === cell_list.get(-1),
669-
})}
670-
</DivTempHeight>
671-
</SortableItem>
672-
);
673-
}}
674-
rangeChanged={(visibleRange) => {
675-
virtuosoRangeRef.current = visibleRange;
676-
}}
677-
{...virtuosoScroll}
678-
/>
678+
}}
679+
rangeChanged={(visibleRange) => {
680+
virtuosoRangeRef.current = visibleRange;
681+
}}
682+
{...virtuosoScroll}
683+
/>
684+
</div>
679685
</IFrameContext.Provider>
680686
);
681687
} else {

src/packages/frontend/jupyter/output-messages/immortal-dom-node.tsx

Lines changed: 103 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,8 @@ This supports virtualization, window splitting, etc., without loss of state.
66

77
import { useCallback, useEffect, useRef } from "react";
88
import $ from "jquery";
9-
10-
// This is just an initial default height; the actual height of the should
11-
// resize to the content.
12-
const HEIGHT = "50vh";
9+
import { useFrameContext } from "@cocalc/frontend/frame-editors/frame-tree/frame-context";
10+
import { useIFrameContext } from "@cocalc/frontend/jupyter/cell-list";
1311

1412
interface Props {
1513
globalKey: string;
@@ -21,69 +19,140 @@ const immortals: { [globalKey: string]: any } = {};
2119

2220
const Z_INDEX = 1;
2321

22+
// make it really standout:
23+
// const PADDING = 5;
24+
// const STYLE = {
25+
// border: "1px solid #ccc",
26+
// borderRadius: "5px",
27+
// padding: `${PADDING}px`,
28+
// background: "#eee",
29+
// } as const;
30+
31+
// make it blend in
32+
const PADDING = 0;
33+
const STYLE = {} as const;
34+
2435
export default function ImmortalDomNode({
2536
globalKey,
2637
html,
2738
zIndex = Z_INDEX, // todo: support changing?
2839
}: Props) {
2940
const divRef = useRef<any>(null);
30-
const eltRef = useRef<any>(null);
3141
const intervalRef = useRef<any>(null);
42+
const { isVisible } = useFrameContext();
43+
const iframeContext = useIFrameContext();
3244

3345
const position = useCallback(() => {
34-
// make it so eltRef.current is exactly positioned on top of divRef.current using CSS
35-
if (eltRef.current == null || divRef.current == null) {
46+
// make it so elt is exactly positioned on top of divRef.current using CSS
47+
if (divRef.current == null) {
3648
return;
3749
}
38-
const eltRect = eltRef.current.getBoundingClientRect();
50+
const elt = getElt()[0];
51+
const eltRect = elt.getBoundingClientRect();
3952
const divRect = divRef.current.getBoundingClientRect();
53+
54+
// position our immortal html element
4055
let deltaTop = divRect.top - eltRect.top;
4156
if (deltaTop) {
42-
if (eltRef.current.style.top) {
43-
deltaTop += parseFloat(eltRef.current.style.top.slice(0, -2));
57+
if (elt.style.top) {
58+
deltaTop += parseFloat(elt.style.top.slice(0, -2));
4459
}
45-
eltRef.current.style.top = `${deltaTop}px`;
60+
elt.style.top = `${deltaTop + PADDING}px`;
4661
}
4762
let deltaLeft = divRect.left - eltRect.left;
4863
if (deltaLeft) {
49-
if (eltRef.current.style.left) {
50-
deltaLeft += parseFloat(eltRef.current.style.left.slice(0, -2));
64+
if (elt.style.left) {
65+
deltaLeft += parseFloat(elt.style.left.slice(0, -2));
5166
}
52-
eltRef.current.style.left = `${deltaLeft}px`;
67+
elt.style.left = `${deltaLeft + PADDING}px`;
5368
}
54-
}, []);
5569

56-
useEffect(() => {
57-
if (divRef.current == null) {
58-
return;
70+
// set the size of the actual react div that is in place
71+
divRef.current.style.height = `${
72+
eltRect.bottom - eltRect.top + 2 * PADDING
73+
}px`;
74+
divRef.current.style.width = `${
75+
eltRect.right - eltRect.left + 2 * PADDING
76+
}px`;
77+
78+
// clip our immortal html so it isn't visible outside the parent
79+
const parent = $(iframeContext.cellListDivRef?.current)[0];
80+
if (parent != null) {
81+
const parentRect = parent.getBoundingClientRect();
82+
console.log({ parentRect, eltRect });
83+
// Calculate the overlap area
84+
const top = Math.max(0, parentRect.top - eltRect.top);
85+
const right = Math.min(eltRect.width, parentRect.right - eltRect.left);
86+
const bottom = Math.min(eltRect.height, parentRect.bottom - eltRect.top);
87+
const left = Math.max(0, parentRect.left - eltRect.left);
88+
89+
// Apply clip-path to elt to make it visible only inside of parentRect:
90+
elt.style.clipPath = `polygon(${left}px ${top}px, ${right}px ${top}px, ${right}px ${bottom}px, ${left}px ${bottom}px)`;
5991
}
60-
let elt;
92+
}, []);
93+
94+
const getElt = () => {
6195
if (immortals[globalKey] == null) {
62-
elt = immortals[globalKey] = $(
63-
`<div id="${globalKey}" style="border:0;overflow:hidden;width:100%;height:${HEIGHT};position:absolute;left:130px;z-index:${zIndex}"/>${html}</div>`,
64-
);
96+
const elt = (immortals[globalKey] = $(
97+
`<div id="${globalKey}" style="border:0;position:absolute;z-index:${zIndex}"/>${html}</div>`,
98+
));
6599
$("body").append(elt);
100+
return elt;
66101
} else {
67-
elt = immortals[globalKey];
68-
elt.show();
102+
return immortals[globalKey];
69103
}
70-
eltRef.current = elt[0];
71-
intervalRef.current = setInterval(position, 1000);
104+
};
105+
106+
const show = () => {
107+
if (divRef.current == null) {
108+
return;
109+
}
110+
const elt = getElt();
111+
elt.show();
72112
position();
113+
};
114+
115+
const hide = () => {
116+
// unmounting so hide
117+
const elt = getElt();
118+
elt.hide();
119+
};
120+
121+
useEffect(() => {
122+
if (isVisible) {
123+
show();
124+
return hide;
125+
}
126+
}, [isVisible]);
127+
128+
useEffect(() => {
129+
intervalRef.current = setInterval(position, 1000);
130+
131+
if (iframeContext.iframeOnScrolls != null) {
132+
let count = 0;
133+
iframeContext.iframeOnScrolls[globalKey] = async () => {
134+
// We run position a lot whenever there is a scroll
135+
// in order to make it so the iframe doesn't appear
136+
// to just get "dragged along" nearly as much, as
137+
// onScroll is throttled.
138+
count = Math.min(100, count + 100);
139+
while (count > 0) {
140+
position();
141+
await new Promise(requestAnimationFrame);
142+
count -= 1;
143+
}
144+
// throw in an update when we're done.
145+
position();
146+
};
147+
}
73148

74149
return () => {
75-
// unmounting so hide
76-
elt.hide();
150+
delete iframeContext.iframeOnScrolls?.[globalKey];
77151
if (intervalRef.current) {
78152
clearInterval(intervalRef.current);
79153
}
80154
};
81155
}, []);
82156

83-
return (
84-
<div
85-
ref={divRef}
86-
style={{ border: "1px solid black", height: HEIGHT }}
87-
></div>
88-
);
157+
return <div ref={divRef} style={STYLE}></div>;
89158
}

0 commit comments

Comments
 (0)