basic viewport save and restore fnuctiontality
This commit is contained in:
@@ -12,6 +12,7 @@
|
|||||||
|
|
||||||
<input type="file" id="dicomInput" multiple />
|
<input type="file" id="dicomInput" multiple />
|
||||||
<button onclick="sendToViewer()">Send to Viewer</button>
|
<button onclick="sendToViewer()">Send to Viewer</button>
|
||||||
|
<button onclick="testLoadWadouriStack()">Test Load wadouri Stack</button>
|
||||||
<script>
|
<script>
|
||||||
function sendToViewer() {
|
function sendToViewer() {
|
||||||
const files = document.getElementById('dicomInput').files;
|
const files = document.getElementById('dicomInput').files;
|
||||||
@@ -21,6 +22,43 @@ function sendToViewer() {
|
|||||||
alert("Viewer API not ready");
|
alert("Viewer API not ready");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function testLoadWadouriStack() {
|
||||||
|
const urls = [
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_757_XQQyovo.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_766_8fWI0Qc.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_774_c8IpxUu.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_758_6WKI7ws.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_767_TduVOb8.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_775_7zWneQ0.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_759_BtKzgS5.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_768_RdvtM8K.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_776_LhVY3Ty.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_760_a8CiR4J.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_769_YHyJ1l6.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_777_8IFYJmL.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_761_vyvO1EN.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_770_vQwi4LW.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_778_BwGAMOR.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_762_DhSgQsm.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_771_xifitCl.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_779_LyA1zYn.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_763_aVjff3H.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_772_cDQZtxQ.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_780_qaZqsqg.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_764_dDoeldL.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_773_93Hvedg.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_781_bEx9Owg.dcm",
|
||||||
|
"https://www.penracourses.org.uk/media/atlas/dicom/IMG_765_qmd5mYJ.dcm"
|
||||||
|
];
|
||||||
|
const wadouriList = urls.map(url => "wadouri:" + url);
|
||||||
|
if (window.loadDicomStackFromWadouriList) {
|
||||||
|
window.loadDicomStackFromWadouriList(wadouriList);
|
||||||
|
} else {
|
||||||
|
alert("Viewer API not ready");
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
+284
-4
@@ -24,6 +24,16 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add to global types:
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
exportViewerState?: () => string;
|
||||||
|
importViewerState?: (json: string) => void;
|
||||||
|
exportAnnotations?: () => string;
|
||||||
|
importAnnotations?: (json: string) => void;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type StackEntry = { imageIds: string[], studyInstanceUID?: string };
|
type StackEntry = { imageIds: string[], studyInstanceUID?: string };
|
||||||
|
|
||||||
// Add this type for clarity
|
// Add this type for clarity
|
||||||
@@ -33,6 +43,12 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
loadDicomStackFromWadouriList?: (wadouriImageIds: string[]) => void;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
window.cornerstoneTools = cornerstoneTools;
|
window.cornerstoneTools = cornerstoneTools;
|
||||||
|
|
||||||
|
|
||||||
@@ -124,6 +140,7 @@ const [openDropdownIdx, setOpenDropdownIdx] = useState<number | null>(null);
|
|||||||
const renderingEngineId = "mainRenderingEngine";
|
const renderingEngineId = "mainRenderingEngine";
|
||||||
const renderingEngineRef = useRef<any>(null);
|
const renderingEngineRef = useRef<any>(null);
|
||||||
|
|
||||||
|
const restoringStateRef = useRef(false);
|
||||||
|
|
||||||
|
|
||||||
const [availableStacks, setAvailableStacks] = useState<StackEntry[]>([]);
|
const [availableStacks, setAvailableStacks] = useState<StackEntry[]>([]);
|
||||||
@@ -135,6 +152,7 @@ const [availableStacks, setAvailableStacks] = useState<StackEntry[]>([]);
|
|||||||
() => Array(MAX_VIEWPORTS).fill(0)
|
() => Array(MAX_VIEWPORTS).fill(0)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
// Load available stacks on mount
|
// Load available stacks on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
availableImageIds().then(async stacks => {
|
availableImageIds().then(async stacks => {
|
||||||
@@ -629,6 +647,8 @@ const dataSet = dicomParser.parseDicom(new Uint8Array(arrayBuffer));
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const setup = async () => {
|
const setup = async () => {
|
||||||
|
console.log("setup")
|
||||||
|
|
||||||
// 1. Initialize Cornerstone3D and tools ONCE
|
// 1. Initialize Cornerstone3D and tools ONCE
|
||||||
if (!running.current) {
|
if (!running.current) {
|
||||||
running.current = true;
|
running.current = true;
|
||||||
@@ -719,7 +739,7 @@ const dataSet = dicomParser.parseDicom(new Uint8Array(arrayBuffer));
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (viewportModes[i] === 'stack') {
|
if (viewportModes[i] === 'stack') {
|
||||||
if (imageIds) {
|
if (imageIds && !restoringStateRef.current) {
|
||||||
viewport.setStack(imageIds);
|
viewport.setStack(imageIds);
|
||||||
}
|
}
|
||||||
// CrosshairsTool is not applicable for stack viewports
|
// CrosshairsTool is not applicable for stack viewports
|
||||||
@@ -754,6 +774,7 @@ const dataSet = dicomParser.parseDicom(new Uint8Array(arrayBuffer));
|
|||||||
}
|
}
|
||||||
viewport.render();
|
viewport.render();
|
||||||
|
|
||||||
|
if (!restoringStateRef.current) {
|
||||||
// Restore previous scroll position for existing viewports only
|
// Restore previous scroll position for existing viewports only
|
||||||
if (
|
if (
|
||||||
prevScrollIndices.current[i] !== undefined &&
|
prevScrollIndices.current[i] !== undefined &&
|
||||||
@@ -782,11 +803,13 @@ const dataSet = dicomParser.parseDicom(new Uint8Array(arrayBuffer));
|
|||||||
propsToRestore.colormap = colormap;
|
propsToRestore.colormap = colormap;
|
||||||
}
|
}
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
console.log("Restoring properties (timeout) for viewport", i, propsToRestore);
|
||||||
viewport.setProperties(propsToRestore);
|
viewport.setProperties(propsToRestore);
|
||||||
viewport.render()
|
viewport.render()
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Always (re-)attach overlay update for each viewport
|
// Always (re-)attach overlay update for each viewport
|
||||||
@@ -868,6 +891,8 @@ const dataSet = dicomParser.parseDicom(new Uint8Array(arrayBuffer));
|
|||||||
const viewport = renderingEngine.getViewport(viewportId);
|
const viewport = renderingEngine.getViewport(viewportId);
|
||||||
if (viewport && typeof viewport.resetCamera === "function") {
|
if (viewport && typeof viewport.resetCamera === "function") {
|
||||||
viewport.resetCamera();
|
viewport.resetCamera();
|
||||||
|
|
||||||
|
console.log("Restoring viewport properties for viewport", viewportIdx);
|
||||||
viewport.resetProperties(); // Resets window/level and other properties
|
viewport.resetProperties(); // Resets window/level and other properties
|
||||||
viewport.render();
|
viewport.render();
|
||||||
}
|
}
|
||||||
@@ -881,6 +906,7 @@ const dataSet = dicomParser.parseDicom(new Uint8Array(arrayBuffer));
|
|||||||
if (renderingEngine) {
|
if (renderingEngine) {
|
||||||
const viewport = renderingEngine.getViewport(viewportId);
|
const viewport = renderingEngine.getViewport(viewportId);
|
||||||
if (viewport && typeof viewport.setProperties === "function") {
|
if (viewport && typeof viewport.setProperties === "function") {
|
||||||
|
console.log(`Applying preset to viewport ${viewportIdx}: center=${center}, width=${width}`);
|
||||||
viewport.setProperties({
|
viewport.setProperties({
|
||||||
voiRange: {
|
voiRange: {
|
||||||
lower: center - width / 2,
|
lower: center - width / 2,
|
||||||
@@ -948,6 +974,50 @@ const dataSet = dicomParser.parseDicom(new Uint8Array(arrayBuffer));
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// API: Load a stack from a list of wadouri imageIds (strings)
|
||||||
|
window.loadDicomStackFromWadouriList = async (wadouriImageIds: string[]) => {
|
||||||
|
if (!wadouriImageIds || wadouriImageIds.length === 0) return;
|
||||||
|
|
||||||
|
// Try to extract StudyInstanceUID from the first image if possible
|
||||||
|
let studyInstanceUID: string | undefined = undefined;
|
||||||
|
try {
|
||||||
|
const firstId = wadouriImageIds[0];
|
||||||
|
if (firstId && firstId.startsWith("wadouri:")) {
|
||||||
|
const url = firstId.slice(8);
|
||||||
|
const response = await fetch(url);
|
||||||
|
const arrayBuffer = await response.arrayBuffer();
|
||||||
|
const dataSet = dicomParser.parseDicom(new Uint8Array(arrayBuffer));
|
||||||
|
studyInstanceUID = dataSet.string('x0020000d');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore errors, fallback to undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
setAvailableStacks(prev => {
|
||||||
|
const next = [...prev, { imageIds: wadouriImageIds, studyInstanceUID }];
|
||||||
|
setViewportImageIds(vpPrev => {
|
||||||
|
const vpNext = [...vpPrev];
|
||||||
|
vpNext[0] = wadouriImageIds;
|
||||||
|
return vpNext;
|
||||||
|
});
|
||||||
|
setSelectedStackIdx(selPrev => {
|
||||||
|
const selNext = [...selPrev];
|
||||||
|
selNext[0] = next.length - 1;
|
||||||
|
return selNext;
|
||||||
|
});
|
||||||
|
setLoadedImageIdsAndCacheMeta(wadouriImageIds);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
return () => {
|
||||||
|
window.loadDicomStackFromWadouriList = undefined;
|
||||||
|
};
|
||||||
|
}, [setAvailableStacks, setViewportImageIds, setSelectedStackIdx, setLoadedImageIdsAndCacheMeta]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!orderBySliceLocation) return;
|
if (!orderBySliceLocation) return;
|
||||||
|
|
||||||
@@ -1001,6 +1071,182 @@ const dataSet = dicomParser.parseDicom(new Uint8Array(arrayBuffer));
|
|||||||
mainToolGroup.setToolDisabled(ReferenceLinesTool.toolName)
|
mainToolGroup.setToolDisabled(ReferenceLinesTool.toolName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
useEffect(() => {
|
||||||
|
// Export the current viewer state as JSON
|
||||||
|
window.exportViewerState = () => {
|
||||||
|
const numActive = viewportGrid.rows * viewportGrid.cols;
|
||||||
|
const activeViewportModes = viewportModes.slice(0, numActive);
|
||||||
|
const activeSelectedStackIdx = selectedStackIdx.slice(0, numActive);
|
||||||
|
const activeAvailableStacks = availableStacks;
|
||||||
|
const activeViewportImageIds = viewportImageIds.slice(0, numActive);
|
||||||
|
|
||||||
|
// Collect viewport properties, stack positions, and camera for active viewports
|
||||||
|
const renderingEngine = renderingEngineRef.current;
|
||||||
|
const viewportProperties: any[] = [];
|
||||||
|
const stackPositions: number[] = [];
|
||||||
|
const cameraProperties: any[] = [];
|
||||||
|
for (let i = 0; i < numActive; i++) {
|
||||||
|
let props = null;
|
||||||
|
let stackPos = null;
|
||||||
|
let camera = null;
|
||||||
|
if (renderingEngine) {
|
||||||
|
const viewportId = `CT_${i}`;
|
||||||
|
const viewport = renderingEngine.getViewport(viewportId);
|
||||||
|
if (viewport) {
|
||||||
|
if (typeof viewport.getProperties === "function") {
|
||||||
|
props = viewport.getProperties();
|
||||||
|
}
|
||||||
|
if (typeof viewport.getCurrentImageIdIndex === "function") {
|
||||||
|
stackPos = viewport.getCurrentImageIdIndex();
|
||||||
|
}
|
||||||
|
if (typeof viewport.getCamera === "function") {
|
||||||
|
camera = viewport.getCamera();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
viewportProperties.push(props);
|
||||||
|
stackPositions.push(stackPos);
|
||||||
|
cameraProperties.push(camera);
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
viewportGrid,
|
||||||
|
viewportModes: activeViewportModes,
|
||||||
|
selectedStackIdx: activeSelectedStackIdx,
|
||||||
|
availableStacks: activeAvailableStacks,
|
||||||
|
viewportImageIds: activeViewportImageIds,
|
||||||
|
viewportProperties,
|
||||||
|
stackPositions,
|
||||||
|
cameraProperties,
|
||||||
|
};
|
||||||
|
return JSON.stringify(state, null, 2);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.importViewerState = (json: string) => {
|
||||||
|
try {
|
||||||
|
console.log("Importing viewer state:", json);
|
||||||
|
restoringStateRef.current = true;
|
||||||
|
|
||||||
|
const state = typeof json === "string" ? JSON.parse(json) : json;
|
||||||
|
if (state.viewportGrid) setViewportGrid(state.viewportGrid);
|
||||||
|
if (state.viewportModes) setViewportModes(state.viewportModes);
|
||||||
|
if (state.selectedStackIdx) setSelectedStackIdx(state.selectedStackIdx);
|
||||||
|
if (state.availableStacks) setAvailableStacks(state.availableStacks);
|
||||||
|
if (state.viewportImageIds) setViewportImageIds(state.viewportImageIds);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
const renderingEngine = renderingEngineRef.current;
|
||||||
|
if (!renderingEngine) {
|
||||||
|
restoringStateRef.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const numActive = state.viewportGrid?.rows * state.viewportGrid?.cols || 1;
|
||||||
|
for (let i = 0; i < numActive; i++) {
|
||||||
|
const viewportId = `CT_${i}`;
|
||||||
|
const viewport = renderingEngine.getViewport(viewportId);
|
||||||
|
if (!viewport) continue;
|
||||||
|
|
||||||
|
// Restore stack position (scroll)
|
||||||
|
if (
|
||||||
|
state.stackPositions &&
|
||||||
|
typeof state.stackPositions[i] === "number" &&
|
||||||
|
typeof viewport.setImageIdIndex === "function"
|
||||||
|
) {
|
||||||
|
if (csUtilities && csUtilities.jumpToSlice) {
|
||||||
|
csUtilities.jumpToSlice(viewport.element, { imageIndex: state.stackPositions[i] });
|
||||||
|
} else {
|
||||||
|
viewport.setImageIdIndex(state.stackPositions[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore viewport properties (VOI, invert, etc.)
|
||||||
|
if (
|
||||||
|
state.viewportProperties &&
|
||||||
|
state.viewportProperties[i] &&
|
||||||
|
typeof viewport.setProperties === "function"
|
||||||
|
) {
|
||||||
|
const props = state.viewportProperties[i];
|
||||||
|
const { voiRange, invert, interpolationType, VOILUTFunction, colormap } = props;
|
||||||
|
const propsToRestore: any = {};
|
||||||
|
if (voiRange && voiRange.lower !== undefined && voiRange.upper !== undefined) propsToRestore.voiRange = voiRange;
|
||||||
|
if (invert !== undefined) propsToRestore.invert = invert;
|
||||||
|
if (interpolationType !== undefined) propsToRestore.interpolationType = interpolationType;
|
||||||
|
if (VOILUTFunction) propsToRestore.VOILUTFunction = VOILUTFunction;
|
||||||
|
if (
|
||||||
|
colormap !== undefined &&
|
||||||
|
colormap !== null &&
|
||||||
|
(typeof colormap === "string" || (typeof colormap === "object" && !("actor" in colormap)))
|
||||||
|
) {
|
||||||
|
propsToRestore.colormap = colormap;
|
||||||
|
}
|
||||||
|
viewport.setProperties(propsToRestore);
|
||||||
|
viewport.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore camera properties (zoom, pan, orientation)
|
||||||
|
if (
|
||||||
|
state.cameraProperties &&
|
||||||
|
state.cameraProperties[i] &&
|
||||||
|
typeof viewport.setCamera === "function"
|
||||||
|
) {
|
||||||
|
viewport.setCamera(state.cameraProperties[i]);
|
||||||
|
viewport.render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
restoringStateRef.current = false;
|
||||||
|
console.log("Viewer state restored successfully");
|
||||||
|
}, 300);
|
||||||
|
} catch (e) {
|
||||||
|
restoringStateRef.current = false;
|
||||||
|
alert("Failed to import viewer state: " + e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Export all annotations using cornerstoneTools.annotation.state.getAllAnnotations()
|
||||||
|
window.exportAnnotations = () => {
|
||||||
|
try {
|
||||||
|
const allAnnotations = cornerstoneTools.annotation.state.getAllAnnotations();
|
||||||
|
return JSON.stringify(allAnnotations, null, 2);
|
||||||
|
} catch (e) {
|
||||||
|
alert("Failed to export annotations: " + e);
|
||||||
|
return "{}";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Import and restore all annotations using cornerstoneTools.annotation.state.restoreAnnotations()
|
||||||
|
window.importAnnotations = (json: string) => {
|
||||||
|
try {
|
||||||
|
const annotationData = typeof json === "string" ? JSON.parse(json) : json;
|
||||||
|
cornerstoneTools.annotation.state.restoreAnnotations(annotationData);
|
||||||
|
if (renderingEngineRef.current) {
|
||||||
|
renderingEngineRef.current.render();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
alert("Failed to import annotations: " + e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
return () => {
|
||||||
|
window.exportViewerState = undefined;
|
||||||
|
window.importViewerState = undefined;
|
||||||
|
window.exportAnnotations = undefined;
|
||||||
|
window.importAnnotations = undefined;
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
viewportGrid,
|
||||||
|
viewportModes,
|
||||||
|
selectedStackIdx,
|
||||||
|
availableStacks,
|
||||||
|
viewportImageIds,
|
||||||
|
setViewportGrid,
|
||||||
|
setViewportModes,
|
||||||
|
setSelectedStackIdx,
|
||||||
|
setAvailableStacks,
|
||||||
|
setViewportImageIds,
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Reference stack and UID for study comparison (use active viewport)
|
// Reference stack and UID for study comparison (use active viewport)
|
||||||
const referenceStackIdx = activeViewport !== null ? selectedStackIdx[activeViewport] : 0;
|
const referenceStackIdx = activeViewport !== null ? selectedStackIdx[activeViewport] : 0;
|
||||||
@@ -1444,7 +1690,7 @@ const referenceUID = referenceStackObj?.studyInstanceUID;
|
|||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
}}
|
}}
|
||||||
onClick={() => setOpenDropdownIdx(openDropdownIdx === i ? null : i)}
|
onClick={() => setOpenDropdownIdx(openDropdownIdx === i ? null : i)}
|
||||||
onBlur={() => setTimeout(() => setOpenDropdownIdx(null), 150)}
|
//onBlur={() => setTimeout(() => setOpenDropdownIdx(null), 150)}
|
||||||
aria-haspopup="listbox"
|
aria-haspopup="listbox"
|
||||||
aria-expanded={openDropdownIdx === i}
|
aria-expanded={openDropdownIdx === i}
|
||||||
>
|
>
|
||||||
@@ -1471,10 +1717,14 @@ const referenceUID = referenceStackObj?.studyInstanceUID;
|
|||||||
</button>
|
</button>
|
||||||
{openDropdownIdx === i && (
|
{openDropdownIdx === i && (
|
||||||
<div
|
<div
|
||||||
|
ref={el => {
|
||||||
|
if (el) el.focus();
|
||||||
|
}}
|
||||||
|
tabIndex={0}
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
left: 0,
|
left: 0,
|
||||||
bottom: "110%", // <-- changed from top: "110%" to bottom: "110%"
|
bottom: "110%",
|
||||||
background: "#222",
|
background: "#222",
|
||||||
border: "1px solid #444",
|
border: "1px solid #444",
|
||||||
borderRadius: "4px",
|
borderRadius: "4px",
|
||||||
@@ -1483,9 +1733,37 @@ const referenceUID = referenceStackObj?.studyInstanceUID;
|
|||||||
minWidth: 180,
|
minWidth: 180,
|
||||||
maxHeight: 220,
|
maxHeight: 220,
|
||||||
overflowY: "auto",
|
overflowY: "auto",
|
||||||
|
overscrollBehavior: "contain",
|
||||||
|
scrollbarWidth: "thin",
|
||||||
}}
|
}}
|
||||||
tabIndex={-1}
|
|
||||||
role="listbox"
|
role="listbox"
|
||||||
|
onMouseDown={e => e.stopPropagation()}
|
||||||
|
onWheel={e => {
|
||||||
|
// Trap scroll inside the dropdown
|
||||||
|
const target = e.currentTarget as HTMLDivElement;
|
||||||
|
const { scrollTop, scrollHeight, clientHeight } = target;
|
||||||
|
const atTop = scrollTop === 0;
|
||||||
|
const atBottom = scrollTop + clientHeight >= scrollHeight - 1;
|
||||||
|
if (
|
||||||
|
(e.deltaY < 0 && atTop) ||
|
||||||
|
(e.deltaY > 0 && atBottom)
|
||||||
|
) {
|
||||||
|
// Let the event bubble to parent (viewport)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Otherwise, prevent scrolling the viewport
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
// Manually scroll the menu
|
||||||
|
target.scrollTop += e.deltaY;
|
||||||
|
}}
|
||||||
|
onBlur={e => {
|
||||||
|
const related = e.relatedTarget as Node;
|
||||||
|
const parent = e.currentTarget.parentElement;
|
||||||
|
if (!parent?.contains(related)) {
|
||||||
|
setTimeout(() => setOpenDropdownIdx(null), 100);
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{availableStacks.map((stackObj, idx) => {
|
{availableStacks.map((stackObj, idx) => {
|
||||||
const thisUID = stackObj.studyInstanceUID;
|
const thisUID = stackObj.studyInstanceUID;
|
||||||
@@ -1505,7 +1783,9 @@ const referenceUID = referenceStackObj?.studyInstanceUID;
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
fontWeight: selectedStackIdx[i] === idx ? "bold" : "normal",
|
fontWeight: selectedStackIdx[i] === idx ? "bold" : "normal",
|
||||||
borderBottom: "1px solid #333",
|
borderBottom: "1px solid #333",
|
||||||
|
userSelect: "none",
|
||||||
}}
|
}}
|
||||||
|
tabIndex={-1}
|
||||||
onMouseDown={e => {
|
onMouseDown={e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleLoadStack(i, idx);
|
handleLoadStack(i, idx);
|
||||||
|
|||||||
Reference in New Issue
Block a user