@vitessce/launcher 3.8.6
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/LICENSE +21 -0
- package/README.md +34 -0
- package/dist/OrbitControls-BF52POQp.js +18694 -0
- package/dist/ReactNeuroglancer-BzSHY22y.js +65780 -0
- package/dist/blosc-DvQQ1ST0.js +887 -0
- package/dist/chunk-INHXZS53-DHVJiuU3.js +19 -0
- package/dist/deflate-CQwOaSLw.js +10 -0
- package/dist/higlass-C7qBSekP.js +133163 -0
- package/dist/index-B00rYWyD.js +3686 -0
- package/dist/index-BIrF4FKE.js +9854 -0
- package/dist/index-CYL9MBhI.js +384168 -0
- package/dist/index.js +5 -0
- package/dist/jpeg-BFml4vEx.js +856 -0
- package/dist/lerc-BOiupD76.js +2057 -0
- package/dist/lz4-BIbM36RN.js +801 -0
- package/dist/lzw-DjHwit8p.js +125 -0
- package/dist/packbits-io9qfQZa.js +27 -0
- package/dist/pako.esm-SxljTded.js +2216 -0
- package/dist/raw-BzEs_FKC.js +9 -0
- package/dist/troika-three-text.esm-26C0zM6u.js +5685 -0
- package/dist/webimage-CwJqBfRI.js +29 -0
- package/dist/zstd-CO575QiM.js +769 -0
- package/dist-tsc/ControlledLauncher.d.ts +2 -0
- package/dist-tsc/ControlledLauncher.d.ts.map +1 -0
- package/dist-tsc/ControlledLauncher.js +129 -0
- package/dist-tsc/LauncherStart.d.ts +2 -0
- package/dist-tsc/LauncherStart.d.ts.map +1 -0
- package/dist-tsc/LauncherStart.js +142 -0
- package/dist-tsc/UncontrolledLauncher.d.ts +14 -0
- package/dist-tsc/UncontrolledLauncher.d.ts.map +1 -0
- package/dist-tsc/UncontrolledLauncher.js +52 -0
- package/dist-tsc/index.d.ts +3 -0
- package/dist-tsc/index.d.ts.map +1 -0
- package/dist-tsc/index.js +2 -0
- package/dist-tsc/launcher-styles.d.ts +10 -0
- package/dist-tsc/launcher-styles.d.ts.map +1 -0
- package/dist-tsc/launcher-styles.js +69 -0
- package/dist-tsc/use-hash-param.d.ts +3 -0
- package/dist-tsc/use-hash-param.d.ts.map +1 -0
- package/dist-tsc/use-hash-param.js +110 -0
- package/dist-tsc/use-query-or-hash-param.d.ts +2 -0
- package/dist-tsc/use-query-or-hash-param.d.ts.map +1 -0
- package/dist-tsc/use-query-or-hash-param.js +51 -0
- package/package.json +51 -0
- package/src/ControlledLauncher.js +208 -0
- package/src/LauncherStart.js +351 -0
- package/src/UncontrolledLauncher.js +110 -0
- package/src/index.js +2 -0
- package/src/launcher-styles.js +71 -0
- package/src/use-hash-param.js +124 -0
- package/src/use-query-or-hash-param.js +63 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ControlledLauncher.d.ts","sourceRoot":"","sources":["../src/ControlledLauncher.js"],"names":[],"mappings":"AA+LA,4DAgBC"}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/* eslint-disable no-unused-vars */
|
|
3
|
+
/* eslint-disable no-nested-ternary */
|
|
4
|
+
/* eslint-disable max-len */
|
|
5
|
+
/* eslint-disable no-console */
|
|
6
|
+
/* eslint-disable prefer-destructuring */
|
|
7
|
+
import React, { useMemo, useState, useEffect } from 'react';
|
|
8
|
+
import { Vitessce } from '@vitessce/all';
|
|
9
|
+
import { generateConfigAlt as generateConfig, parseUrls } from '@vitessce/config';
|
|
10
|
+
import { Alert } from '@vitessce/styles';
|
|
11
|
+
import clsx from 'clsx';
|
|
12
|
+
import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query';
|
|
13
|
+
import { useStyles } from './launcher-styles.js';
|
|
14
|
+
import { LauncherStart } from './LauncherStart.js';
|
|
15
|
+
function logConfigUpgrade(prevConfig, nextConfig) {
|
|
16
|
+
// eslint-disable-next-line no-console
|
|
17
|
+
console.log(`Upgrade view config schema from ${prevConfig.version} to ${nextConfig.version}`);
|
|
18
|
+
// eslint-disable-next-line no-console
|
|
19
|
+
console.log(prevConfig);
|
|
20
|
+
// eslint-disable-next-line no-console
|
|
21
|
+
console.log(nextConfig);
|
|
22
|
+
}
|
|
23
|
+
function ControlledLauncherInner(props) {
|
|
24
|
+
const { isEditing, setIsEditing, setIsUsingLocalFiles, configUrl, setConfigUrl, sourceUrlArr, setSourceUrlArr, marginTop = 0, launcherError, setLauncherError, } = props;
|
|
25
|
+
const { classes } = useStyles();
|
|
26
|
+
const [validConfigFromDroppedData, setValidConfigFromDroppedData] = useState(null);
|
|
27
|
+
const [storesFromDroppedData, setStoresFromDroppedData] = useState(null);
|
|
28
|
+
const configUrlQueryEnabled = !isEditing && !!configUrl;
|
|
29
|
+
const configUrlResult = useQuery({
|
|
30
|
+
enabled: configUrlQueryEnabled,
|
|
31
|
+
queryKey: ['config-from-url', configUrl],
|
|
32
|
+
queryFn: async () => {
|
|
33
|
+
const resp = await fetch(configUrl);
|
|
34
|
+
if (!resp.ok) {
|
|
35
|
+
throw new Error(`Failed to fetch config from URL: ${resp.status} ${resp.statusText}`);
|
|
36
|
+
}
|
|
37
|
+
const configJson = await resp.json();
|
|
38
|
+
return configJson;
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
const sourceUrlQueryEnabled = !isEditing && Array.isArray(sourceUrlArr) && sourceUrlArr.length > 0;
|
|
42
|
+
const sourceUrlResult = useQuery({
|
|
43
|
+
enabled: sourceUrlQueryEnabled,
|
|
44
|
+
queryKey: ['data-metadata-from-urls', sourceUrlArr],
|
|
45
|
+
queryFn: async () => {
|
|
46
|
+
const { config, stores } = await generateConfig(parseUrls(sourceUrlArr));
|
|
47
|
+
return { config, stores };
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
let isLoading = false;
|
|
51
|
+
let validConfig = null;
|
|
52
|
+
let stores = null;
|
|
53
|
+
if (configUrlQueryEnabled) {
|
|
54
|
+
isLoading = configUrlResult.isLoading;
|
|
55
|
+
if (configUrlResult.isSuccess) {
|
|
56
|
+
validConfig = configUrlResult.data;
|
|
57
|
+
setLauncherError(null);
|
|
58
|
+
}
|
|
59
|
+
else if (configUrlResult.isError) {
|
|
60
|
+
setIsEditing(true);
|
|
61
|
+
setLauncherError(`Error loading config from URL: ${configUrlResult.error.message}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
else if (sourceUrlQueryEnabled) {
|
|
65
|
+
isLoading = sourceUrlResult.isLoading;
|
|
66
|
+
if (sourceUrlResult.isSuccess) {
|
|
67
|
+
validConfig = sourceUrlResult.data.config.toJSON();
|
|
68
|
+
stores = sourceUrlResult.data.stores;
|
|
69
|
+
setLauncherError(null);
|
|
70
|
+
}
|
|
71
|
+
else if (sourceUrlResult.isError) {
|
|
72
|
+
setIsEditing(true);
|
|
73
|
+
setLauncherError(`Error loading data/metadata from source URL(s): ${sourceUrlResult.error.message}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
else if (validConfigFromDroppedData) {
|
|
77
|
+
validConfig = validConfigFromDroppedData;
|
|
78
|
+
stores = storesFromDroppedData;
|
|
79
|
+
setLauncherError(null);
|
|
80
|
+
}
|
|
81
|
+
// Possible states:
|
|
82
|
+
// - needsStart: No URL parameters; Show the launcher UI.
|
|
83
|
+
// - Awaiting URL parameter parsing.
|
|
84
|
+
// - Invalid/unrecognized example ID
|
|
85
|
+
// - Loading config or data/metadata from URL.
|
|
86
|
+
// - Loaded config but was invalid.
|
|
87
|
+
// - Could not identify a valid/supported data format from data file extension(s).
|
|
88
|
+
// - Suggest to append `$supportedFileType` to source URL(s) to specify the data format if file extension is non-standard.
|
|
89
|
+
// - Failed to load config or data/metadata from URL.
|
|
90
|
+
// - Error if BOTH config and source parameters are provided.
|
|
91
|
+
// - Error if BOTH hash and query parameters are provided.
|
|
92
|
+
const pageMode = false; // TODO
|
|
93
|
+
const PageComponent = null; // TODO
|
|
94
|
+
const isExpanded = true; // TODO
|
|
95
|
+
const onDropHandler = null; // TODO
|
|
96
|
+
const debug = false; // TODO
|
|
97
|
+
const theme = 'light2'; // TODO
|
|
98
|
+
return (isEditing ? (_jsxs(_Fragment, { children: [launcherError ? (_jsx(Alert, { severity: "error", onClose: () => setLauncherError(null), children: launcherError })) : null, _jsx(LauncherStart, { setIsEditing: setIsEditing, setIsUsingLocalFiles: setIsUsingLocalFiles, configUrl: configUrl, setConfigUrl: setConfigUrl, sourceUrlArr: sourceUrlArr, setSourceUrlArr: setSourceUrlArr, setValidConfigFromDroppedData: setValidConfigFromDroppedData, setStoresFromDroppedData: setStoresFromDroppedData, launcherError: launcherError, setLauncherError: setLauncherError })] })) : (validConfig ? (_jsxs("main", { className: clsx(classes.vitessceApp, { 'vitessce-expanded': isExpanded, 'vitessce-page': pageMode }), style: {
|
|
99
|
+
height: `calc(100vh - ${marginTop}px)`,
|
|
100
|
+
}, children: [pageMode ? (_jsx("style", { children: `
|
|
101
|
+
.vitessce-container {
|
|
102
|
+
height: max(100%,100vh);
|
|
103
|
+
width: 100%;
|
|
104
|
+
overflow: hidden;
|
|
105
|
+
position: relative;
|
|
106
|
+
left: 0;
|
|
107
|
+
}
|
|
108
|
+
` })) : (_jsx("style", { children: `
|
|
109
|
+
.vitessce-container {
|
|
110
|
+
min-height: calc(100vh - ${marginTop}px);
|
|
111
|
+
width: 100%;
|
|
112
|
+
overflow: hidden;
|
|
113
|
+
position: relative;
|
|
114
|
+
left: 0;
|
|
115
|
+
}
|
|
116
|
+
` })), _jsx(Vitessce, { theme: theme, validateOnConfigChange: debug, onConfigChange: debug ? console.log : undefined, onConfigUpgrade: debug ? logConfigUpgrade : undefined, config: validConfig, height: isExpanded ? undefined : 800, pageMode: pageMode, stores: stores, onDrop: onDropHandler, children: pageMode && PageComponent ? (_jsx(PageComponent, {})) : null })] })) : (isLoading ? (_jsx("h2", { children: "Loading..." })) : (_jsx("p", { children: "Error: undefined state" })))));
|
|
117
|
+
}
|
|
118
|
+
export function ControlledLauncher(props) {
|
|
119
|
+
const queryClient = useMemo(() => new QueryClient({
|
|
120
|
+
// Reference: https://tanstack.com/query/latest/docs/react/guides/window-focus-refetching
|
|
121
|
+
defaultOptions: {
|
|
122
|
+
queries: {
|
|
123
|
+
refetchOnWindowFocus: false,
|
|
124
|
+
retry: 2,
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
}), []);
|
|
128
|
+
return (_jsx(QueryClientProvider, { client: queryClient, children: _jsx(ControlledLauncherInner, { ...props }) }));
|
|
129
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LauncherStart.d.ts","sourceRoot":"","sources":["../src/LauncherStart.js"],"names":[],"mappings":"AAoBA,uDA0UC"}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/* eslint-disable no-unused-vars */
|
|
3
|
+
/* eslint-disable react/no-unknown-property */
|
|
4
|
+
/* eslint-disable max-len */
|
|
5
|
+
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
6
|
+
import { createOnDrop } from '@vitessce/vit-s';
|
|
7
|
+
import { TextField, Button, Typography, Card, CardContent, Accordion, AccordionSummary, AccordionDetails, ExpandMore, } from '@vitessce/styles';
|
|
8
|
+
import clsx from 'clsx';
|
|
9
|
+
import { useStyles } from './launcher-styles.js';
|
|
10
|
+
export function LauncherStart(props) {
|
|
11
|
+
const { setIsEditing, setIsUsingLocalFiles, configUrl, setConfigUrl, sourceUrlArr, setSourceUrlArr, setValidConfigFromDroppedData, setStoresFromDroppedData, setLauncherError, } = props;
|
|
12
|
+
const { classes } = useStyles();
|
|
13
|
+
const [spotlightCard, setSpotlightCard] = useState(null);
|
|
14
|
+
// Single dropzone for both data files/folders and config files.
|
|
15
|
+
const dropzoneRef = useRef(null);
|
|
16
|
+
const localDataInputFoldersRef = useRef(null);
|
|
17
|
+
const localDataInputFilesRef = useRef(null);
|
|
18
|
+
const localConfigInputRef = useRef(null);
|
|
19
|
+
// Handlers for drag-and-drop and file input changes.
|
|
20
|
+
const onDropHandler = useMemo(() => createOnDrop({ setViewConfig: setValidConfigFromDroppedData, setStores: setStoresFromDroppedData }, false, // isFileInput
|
|
21
|
+
true), [setValidConfigFromDroppedData, setStoresFromDroppedData]);
|
|
22
|
+
const onFileInputHandler = useMemo(() => createOnDrop({ setViewConfig: setValidConfigFromDroppedData, setStores: setStoresFromDroppedData }, true, // isFileInput
|
|
23
|
+
true), [setValidConfigFromDroppedData, setStoresFromDroppedData]);
|
|
24
|
+
// Effect for setting up drag-and-drop event listeners for data file/folder input.
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
// Make the entire Launcher a dropzone,
|
|
27
|
+
// and then determine whether the dropped file(s) are config or data files
|
|
28
|
+
// (e.g., if a single .json file, assume config; else data).
|
|
29
|
+
const zone = dropzoneRef.current;
|
|
30
|
+
const zoneDataInput1 = localDataInputFoldersRef.current;
|
|
31
|
+
const zoneDataInput2 = localDataInputFilesRef.current;
|
|
32
|
+
const zoneConfigInput = localConfigInputRef.current;
|
|
33
|
+
const onDragEnter = (e) => {
|
|
34
|
+
e.preventDefault();
|
|
35
|
+
setSpotlightCard('data-local');
|
|
36
|
+
};
|
|
37
|
+
const onDragLeave = () => {
|
|
38
|
+
setSpotlightCard(null);
|
|
39
|
+
};
|
|
40
|
+
const onDragOver = (e) => {
|
|
41
|
+
e.preventDefault();
|
|
42
|
+
};
|
|
43
|
+
const onDrop = async (e) => {
|
|
44
|
+
e.preventDefault();
|
|
45
|
+
setIsEditing(false);
|
|
46
|
+
// Clear the other field values.
|
|
47
|
+
setConfigUrl(undefined);
|
|
48
|
+
setSourceUrlArr(undefined);
|
|
49
|
+
// Call onDrop handler passed in from parent of <VitS/Vitessce/> via prop.
|
|
50
|
+
try {
|
|
51
|
+
await onDropHandler(e);
|
|
52
|
+
setIsUsingLocalFiles(true);
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
setLauncherError(`Error processing dropped files: ${err.message}`);
|
|
56
|
+
setIsEditing(true);
|
|
57
|
+
}
|
|
58
|
+
setSpotlightCard(null);
|
|
59
|
+
};
|
|
60
|
+
const onDataInputChange = async (e) => {
|
|
61
|
+
// Reference: https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/File_drag_and_drop
|
|
62
|
+
e.preventDefault();
|
|
63
|
+
setIsEditing(false);
|
|
64
|
+
setSpotlightCard('data-local');
|
|
65
|
+
// Clear the other field values.
|
|
66
|
+
setConfigUrl(undefined);
|
|
67
|
+
setSourceUrlArr(undefined);
|
|
68
|
+
try {
|
|
69
|
+
await onFileInputHandler(e);
|
|
70
|
+
setIsUsingLocalFiles(true);
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
setLauncherError(`Error processing selected files: ${err.message}`);
|
|
74
|
+
setIsEditing(true);
|
|
75
|
+
}
|
|
76
|
+
setSpotlightCard(null);
|
|
77
|
+
};
|
|
78
|
+
const onConfigInputChange = async (e) => {
|
|
79
|
+
// Reference: https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/File_drag_and_drop
|
|
80
|
+
e.preventDefault();
|
|
81
|
+
setIsEditing(false);
|
|
82
|
+
setSpotlightCard('config-local');
|
|
83
|
+
// Clear the other field values.
|
|
84
|
+
setConfigUrl(undefined);
|
|
85
|
+
setSourceUrlArr(undefined);
|
|
86
|
+
try {
|
|
87
|
+
await onFileInputHandler(e);
|
|
88
|
+
setIsUsingLocalFiles(true);
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
setLauncherError(`Error processing selected file: ${err.message}`);
|
|
92
|
+
setIsEditing(true);
|
|
93
|
+
}
|
|
94
|
+
setSpotlightCard(null);
|
|
95
|
+
};
|
|
96
|
+
// The dragenter event happens at the moment you drag something in to the target element,
|
|
97
|
+
// and then it stops.
|
|
98
|
+
// The dragover event happens during the time you are dragging something until you drop it.
|
|
99
|
+
zone.addEventListener('dragenter', onDragEnter);
|
|
100
|
+
zone.addEventListener('dragleave', onDragLeave);
|
|
101
|
+
zone.addEventListener('dragover', onDragOver);
|
|
102
|
+
zone.addEventListener('drop', onDrop);
|
|
103
|
+
zoneDataInput1.addEventListener('change', onDataInputChange);
|
|
104
|
+
zoneDataInput2.addEventListener('change', onDataInputChange);
|
|
105
|
+
zoneConfigInput.addEventListener('change', onConfigInputChange);
|
|
106
|
+
return () => {
|
|
107
|
+
zone.removeEventListener('dragenter', onDragEnter);
|
|
108
|
+
zone.removeEventListener('dragleave', onDragLeave);
|
|
109
|
+
zone.removeEventListener('dragover', onDragOver);
|
|
110
|
+
zone.removeEventListener('drop', onDrop);
|
|
111
|
+
zoneDataInput1.removeEventListener('change', onDataInputChange);
|
|
112
|
+
zoneDataInput2.removeEventListener('change', onDataInputChange);
|
|
113
|
+
zoneConfigInput.removeEventListener('change', onConfigInputChange);
|
|
114
|
+
};
|
|
115
|
+
}, [dropzoneRef, localDataInputFoldersRef, localDataInputFilesRef,
|
|
116
|
+
onDropHandler, setIsUsingLocalFiles, setIsEditing, setSpotlightCard,
|
|
117
|
+
onFileInputHandler, setConfigUrl, setSourceUrlArr, setLauncherError,
|
|
118
|
+
]);
|
|
119
|
+
return (_jsxs("div", { className: classes.launcher, ref: dropzoneRef, children: [_jsxs("div", { className: classes.launcherRow, children: [_jsxs("div", { className: clsx({ [classes.cardDim]: spotlightCard && (spotlightCard === 'config-local' || spotlightCard === 'config-remote') }), children: [_jsx(Typography, { variant: "h5", children: "Begin with data" }), _jsxs("p", { children: ["Let Vitessce ", _jsx("a", { href: "https://vitessce.io/docs/default-config-json/", children: "infer an initial configuration" }), " based on the contents of your ", _jsx("a", { href: "https://vitessce.io/docs/data-types-file-types/", children: "data files." })] })] }), _jsxs("div", { className: classes.launcherCardRow, children: [_jsx(Card, { className: clsx(classes.launcherCard, { [classes.cardDim]: spotlightCard && spotlightCard !== 'data-local' }), children: _jsx("span", { className: classes.cardDashed, children: _jsxs(CardContent, { className: classes.cardContent, children: [_jsxs(Typography, { variant: "h6", children: ["Local data ", _jsx("br", {}), "(Drag and drop)"] }), _jsx("p", { children: "Select or drop local files (or folders) to view them in Vitessce. Vitessce launches with a default configuration (based on file extensions and contents). Files remain local; no upload occurs." }), _jsx("div", { className: classes.buttonSpacer }), _jsxs("div", { className: classes.dataButtonGroup, children: [_jsxs(Button, { component: "label", htmlFor: "data-local-input-directory", variant: "outlined", fullWidth: true, children: [_jsx("span", { children: "Select folder(s)" }), _jsx("input", { id: "data-local-input-directory", type: "file", ref: localDataInputFoldersRef, className: classes.hiddenFileInput, multiple: true, directory: "true", webkitdirectory: "true", mozdirectory: "true" })] }), "\u00A0", _jsxs(Button, { component: "label", htmlFor: "data-local-input-files", variant: "outlined", fullWidth: true, children: [_jsx("span", { children: "Select file(s)" }), _jsx("input", { id: "data-local-input-files", type: "file", ref: localDataInputFilesRef, className: classes.hiddenFileInput, multiple: true })] })] }), _jsx("div", { className: classes.buttonSpacer })] }) }) }), _jsx(Card, { className: clsx(classes.launcherCard, { [classes.cardDim]: spotlightCard && spotlightCard !== 'data-remote' }), children: _jsxs(CardContent, { children: [_jsxs(Typography, { variant: "h6", children: ["Remote data ", _jsx("br", {}), " (Load from URL)"] }), _jsxs("p", { children: ["Enter file (or folder) URLs to view them in Vitessce. Vitessce launches with a default configuration (based on file types and contents). See our ", _jsx("a", { href: "https://vitessce.io/docs/data-hosting/", children: "data hosting" }), " documentation for assistance in making your data accessible to Vitessce.\u00A0"] }), _jsxs("div", { className: classes.textareaAndButton, children: [_jsx(TextField, { multiline: true, label: "Data URL(s)", placeholder: "One or more file URLs (semicolon-separated)", minRows: 2, className: classes.dataUrlTextarea, value: sourceUrlArr ? sourceUrlArr.join(';') : '', onChange: (e) => {
|
|
120
|
+
// Clear existing state.
|
|
121
|
+
setConfigUrl(undefined);
|
|
122
|
+
setSourceUrlArr(e.target.value);
|
|
123
|
+
}, onBlur: () => setSpotlightCard(null), onFocus: () => {
|
|
124
|
+
setSpotlightCard('data-remote');
|
|
125
|
+
// We need to set isEditing, otherwise the launcher UI will disappear
|
|
126
|
+
// as soon as there is a non-empty sourceUrlArr value.
|
|
127
|
+
setIsEditing(true);
|
|
128
|
+
} }), _jsx(Button, { variant: "outlined", onClick: () => {
|
|
129
|
+
setIsEditing(false);
|
|
130
|
+
}, children: "Visualize" })] }), _jsxs(Accordion, { disableGutters: true, style: { marginTop: '5px' }, children: [_jsx(AccordionSummary, { expandIcon: _jsx(ExpandMore, {}), "aria-controls": "panel1-content", id: "panel1-header", children: "Specifying file extensions" }), _jsx(AccordionDetails, { children: _jsxs(Typography, { variant: "body2", children: ["Vitessce automatically tries to infer the file type based on the file extension. For URLs with non-standard extensions, the file type can be specified by appending ", _jsxs("code", { children: [_jsx("b", { children: "$" }), "FILE_TYPE"] }), " to the URL. For example, to specify that a folder at ", _jsx("code", { children: "https://example.com/my_data.zarr" }), " is in SpatialData format, use the URL ", _jsxs("code", { children: ["https://example.com/my_data.zarr", _jsx("b", { children: "$" }), "spatialdata.zarr"] }), ". See our ", _jsx("a", { href: "https://vitessce.io/docs/data-types-file-types/", children: "data types and file types documentation" }), " for a list of supported file types."] }) })] })] }) })] })] }), _jsxs("div", { className: classes.launcherRow, children: [_jsxs("div", { className: clsx({ [classes.cardDim]: spotlightCard && (spotlightCard === 'data-local' || spotlightCard === 'data-remote') }), children: [_jsx(Typography, { variant: "h5", children: "Begin with configuration" }), _jsxs("p", { children: ["Use the options below to supply a Vitessce ", _jsx("a", { href: "https://vitessce.io/docs/view-config-json/", children: "configuration in JSON format." })] })] }), _jsxs("div", { className: classes.launcherCardRow, children: [_jsx(Card, { className: clsx(classes.launcherCard, { [classes.cardDim]: spotlightCard && spotlightCard !== 'config-local' }), children: _jsx("span", { className: classes.cardDashed, children: _jsxs(CardContent, { className: classes.cardContent, children: [_jsxs(Typography, { variant: "h6", children: ["Local config file ", _jsx("br", {}), " (Drag and drop)"] }), _jsx("p", { children: "View a configured Vitessce visualization by selecting or dropping a local JSON file." }), _jsx("div", { className: classes.buttonSpacer }), _jsxs(Button, { component: "label", variant: "outlined", fullWidth: true, children: [_jsx("span", { children: "Select file" }), _jsx("input", { type: "file", ref: localConfigInputRef, className: classes.hiddenFileInput, accept: ".json,application/json" })] }), _jsx("div", { className: classes.buttonSpacer })] }) }) }), _jsx(Card, { className: clsx(classes.launcherCard, { [classes.cardDim]: spotlightCard && spotlightCard !== 'config-remote' }), children: _jsxs(CardContent, { children: [_jsxs(Typography, { variant: "h6", children: ["Remote config file ", _jsx("br", {}), " (Load from URL)"] }), _jsxs("p", { children: ["View a configured Vitessce visualization by specifying a URL to a JSON config file. See our ", _jsx("a", { href: "https://vitessce.io/docs/data-hosting/", children: "data hosting" }), " documentation for assistance in making your config file accessible to Vitessce.\u00A0"] }), _jsxs("div", { className: classes.textareaAndButton, children: [_jsx(TextField, { multiline: true, label: "Config URL", placeholder: "Enter a URL", minRows: 2, className: classes.dataUrlTextarea, value: configUrl || '', onChange: (e) => {
|
|
131
|
+
// Clear existing state.
|
|
132
|
+
setSourceUrlArr(undefined);
|
|
133
|
+
setConfigUrl(e.target.value);
|
|
134
|
+
}, onBlur: () => setSpotlightCard(null), onFocus: () => {
|
|
135
|
+
setSpotlightCard('config-remote');
|
|
136
|
+
// We need to set isEditing, otherwise the launcher UI will disappear
|
|
137
|
+
// as soon as there is a non-empty configUrl value.
|
|
138
|
+
setIsEditing(true);
|
|
139
|
+
} }), _jsx(Button, { variant: "outlined", onClick: () => {
|
|
140
|
+
setIsEditing(false);
|
|
141
|
+
}, children: "Visualize" })] })] }) })] })] })] }));
|
|
142
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* @param {object} props
|
|
4
|
+
* @param {number} [props.marginTop=0] The distance in pixels from the top of the window.
|
|
5
|
+
* This is used to calculate the height of the Vitessce component (100vh minus marginTop).
|
|
6
|
+
* @param {function} [props.setIsFooterVisible] Callback to set whether the footer is visible.
|
|
7
|
+
* Used to hide the footer when Vitessce component is visible.
|
|
8
|
+
* @returns
|
|
9
|
+
*/
|
|
10
|
+
export function UncontrolledLauncher(props: {
|
|
11
|
+
marginTop?: number | undefined;
|
|
12
|
+
setIsFooterVisible?: Function | undefined;
|
|
13
|
+
}): JSX.Element;
|
|
14
|
+
//# sourceMappingURL=UncontrolledLauncher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"UncontrolledLauncher.d.ts","sourceRoot":"","sources":["../src/UncontrolledLauncher.js"],"names":[],"mappings":"AAmEA;;;;;;;;GAQG;AACH,4CANG;IAAuB,SAAS;IAEP,kBAAkB;CAE3C,eAmCF"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/* eslint-disable no-unused-vars */
|
|
3
|
+
/* eslint-disable max-len */
|
|
4
|
+
import React, { useEffect, useState } from 'react';
|
|
5
|
+
import { QueryParamProvider } from 'use-query-params';
|
|
6
|
+
import { useHashOrQueryParam } from './use-query-or-hash-param.js';
|
|
7
|
+
import { ControlledLauncher } from './ControlledLauncher.js';
|
|
8
|
+
function UncontrolledLauncherInner(props) {
|
|
9
|
+
const { marginTop = 0, setIsFooterVisible, configUrlParamName = 'config', sourceUrlParamName = 'source', exampleIdParamName = 'example', isEditingParamName = 'edit', } = props;
|
|
10
|
+
const [launcherError, setLauncherError] = useState(null);
|
|
11
|
+
// Logic for managing state and query/hash params.
|
|
12
|
+
const [exampleId, setExampleId, exampleIdParamError] = useHashOrQueryParam(exampleIdParamName, undefined, 'string');
|
|
13
|
+
// TODO: support vitessce-link code param
|
|
14
|
+
// const wsCodeValue = useHashOrQueryParam('session', undefined, 'string');
|
|
15
|
+
const [configUrl, setConfigUrl, configUrlParamError] = useHashOrQueryParam(configUrlParamName, undefined, 'string');
|
|
16
|
+
const [sourceUrlArr, setSourceUrlArr, sourceUrlArrParamError] = useHashOrQueryParam(sourceUrlParamName, undefined, 'string-array');
|
|
17
|
+
const [isEditingParam, setIsEditing, isEditingParamError] = useHashOrQueryParam(isEditingParamName, undefined, 'boolean');
|
|
18
|
+
// We need this here since the URL param will not indicate whether local data was dropped.
|
|
19
|
+
// TODO: Store in a cookie/localStorage? Store in a URL param?
|
|
20
|
+
// However if the page changes, we will lose the dropped data anyway...
|
|
21
|
+
const [isUsingLocalFiles, setIsUsingLocalFiles] = useState(false);
|
|
22
|
+
// Check for parameter errors (e.g., both hash+query for same value) and conflicting parameters.
|
|
23
|
+
const hasNoParamError = !exampleIdParamError && !configUrlParamError && !sourceUrlArrParamError && !isEditingParamError;
|
|
24
|
+
const hasNoConflictingParams = ((exampleId ? 1 : 0)
|
|
25
|
+
+ (configUrl ? 1 : 0)
|
|
26
|
+
+ (sourceUrlArr && sourceUrlArr.length > 0 ? 1 : 0)) <= 1;
|
|
27
|
+
const needsStart = !exampleId && !configUrl && (!sourceUrlArr || (Array.isArray(sourceUrlArr) && sourceUrlArr.length === 0));
|
|
28
|
+
const isEditing = !isUsingLocalFiles && (isEditingParam === true || needsStart);
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (typeof setIsFooterVisible === 'function') {
|
|
31
|
+
setIsFooterVisible(isEditing);
|
|
32
|
+
}
|
|
33
|
+
}, [isEditing, setIsFooterVisible]);
|
|
34
|
+
return (_jsx(ControlledLauncher, { isEditing: isEditing, setIsEditing: setIsEditing, isUsingLocalFiles: isUsingLocalFiles, setIsUsingLocalFiles: setIsUsingLocalFiles, configUrl: configUrl, setConfigUrl: setConfigUrl, sourceUrlArr: sourceUrlArr, setSourceUrlArr: setSourceUrlArr, marginTop: marginTop, launcherError: launcherError, setLauncherError: setLauncherError }));
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
*
|
|
38
|
+
* @param {object} props
|
|
39
|
+
* @param {number} [props.marginTop=0] The distance in pixels from the top of the window.
|
|
40
|
+
* This is used to calculate the height of the Vitessce component (100vh minus marginTop).
|
|
41
|
+
* @param {function} [props.setIsFooterVisible] Callback to set whether the footer is visible.
|
|
42
|
+
* Used to hide the footer when Vitessce component is visible.
|
|
43
|
+
* @returns
|
|
44
|
+
*/
|
|
45
|
+
export function UncontrolledLauncher(props) {
|
|
46
|
+
const { marginTop = 0, setIsFooterVisible = null, configUrlParamName = 'config', sourceUrlParamName = 'source', exampleIdParamName = 'example', isEditingParamName = 'edit',
|
|
47
|
+
// TODO: Optional mapping from example IDs to their full JSON config, and potentially more values such as PageComponent, Plugins, etc.
|
|
48
|
+
// See https://github.com/vitessce/vitessce/blob/main/sites/demo/src/api.js
|
|
49
|
+
exampleConfigs = null, } = props;
|
|
50
|
+
// TODO: wrap in MUI themeProvider?
|
|
51
|
+
return (_jsxs(QueryParamProvider, { children: [_jsxs("div", { style: { display: 'none' }, children: [_jsx("a", { href: "/", children: "Reset" }), "\u00A0", _jsx("a", { href: "/?source=https://storage.googleapis.com/vitessce-demo-data/maynard-2021/151673.sdata.zarr", children: "SpatialData Example" }), "\u00A0", _jsx("a", { href: "/#?source=https://uk1s3.embassy.ebi.ac.uk/idr/zarr/v0.4/idr0062A/6001240.zarr$image.ome-zarr", children: "OME-Zarr Example" })] }), _jsx(UncontrolledLauncherInner, { marginTop: marginTop, setIsFooterVisible: setIsFooterVisible, isEditingParamName: isEditingParamName, exampleIdParamName: exampleIdParamName, configUrlParamName: configUrlParamName, sourceUrlParamName: sourceUrlParamName })] }));
|
|
52
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.js"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const useStyles: (params: void, muiStyleOverridesParams?: {
|
|
2
|
+
props: Record<string, unknown>;
|
|
3
|
+
ownerState?: Record<string, unknown> | undefined;
|
|
4
|
+
} | undefined) => {
|
|
5
|
+
classes: Record<"launcher" | "launcherRow" | "launcherRowTitle" | "launcherCardRow" | "launcherCard" | "cardTitle" | "cardDashed" | "cardDim" | "cardContent" | "buttonSpacer" | "dataButtonGroup" | "vitessceApp" | "dataUrlTextarea" | "textareaAndButton" | "hiddenFileInput", string>;
|
|
6
|
+
theme: import("@mui/material").Theme;
|
|
7
|
+
css: import("tss-react").Css;
|
|
8
|
+
cx: import("tss-react").Cx;
|
|
9
|
+
};
|
|
10
|
+
//# sourceMappingURL=launcher-styles.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"launcher-styles.d.ts","sourceRoot":"","sources":["../src/launcher-styles.js"],"names":[],"mappings":"AAGA;;cAmCG,CAAC;;;kBAMF,eACC;gBAEG,WAAU;eACC,WACT;EAqBJ"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { makeStyles } from '@vitessce/styles';
|
|
2
|
+
export const useStyles = makeStyles()(() => ({
|
|
3
|
+
launcher: {
|
|
4
|
+
display: 'flex',
|
|
5
|
+
flexDirection: 'column',
|
|
6
|
+
margin: '0 10px',
|
|
7
|
+
},
|
|
8
|
+
launcherRow: {
|
|
9
|
+
marginTop: '15px',
|
|
10
|
+
marginBottom: '15px',
|
|
11
|
+
},
|
|
12
|
+
launcherRowTitle: {
|
|
13
|
+
marginBottom: 0,
|
|
14
|
+
},
|
|
15
|
+
launcherCardRow: {
|
|
16
|
+
display: 'grid',
|
|
17
|
+
gridTemplateColumns: '1fr 1fr',
|
|
18
|
+
gap: '10px',
|
|
19
|
+
},
|
|
20
|
+
launcherCard: {
|
|
21
|
+
transition: 'opacity 0.3s',
|
|
22
|
+
},
|
|
23
|
+
cardTitle: {
|
|
24
|
+
marginTop: 0,
|
|
25
|
+
},
|
|
26
|
+
cardDashed: {
|
|
27
|
+
display: 'inline-block',
|
|
28
|
+
boxSizing: 'border-box',
|
|
29
|
+
height: '100%',
|
|
30
|
+
width: '100%',
|
|
31
|
+
borderRadius: '4px',
|
|
32
|
+
border: '2px dashed rgba(128, 128, 128, 0.2)',
|
|
33
|
+
},
|
|
34
|
+
cardDim: {
|
|
35
|
+
opacity: 0.2,
|
|
36
|
+
},
|
|
37
|
+
cardContent: {
|
|
38
|
+
display: 'flex',
|
|
39
|
+
flexDirection: 'column',
|
|
40
|
+
height: '100%',
|
|
41
|
+
boxSizing: 'border-box',
|
|
42
|
+
},
|
|
43
|
+
buttonSpacer: {
|
|
44
|
+
flexGrow: 1,
|
|
45
|
+
},
|
|
46
|
+
dataButtonGroup: {
|
|
47
|
+
width: '100%',
|
|
48
|
+
display: 'flex',
|
|
49
|
+
flexDirection: 'row',
|
|
50
|
+
gap: '5px',
|
|
51
|
+
},
|
|
52
|
+
vitessceApp: {
|
|
53
|
+
position: 'relative',
|
|
54
|
+
display: 'flex',
|
|
55
|
+
width: '100%',
|
|
56
|
+
},
|
|
57
|
+
dataUrlTextarea: {
|
|
58
|
+
width: '100%',
|
|
59
|
+
boxSizing: 'border-box',
|
|
60
|
+
},
|
|
61
|
+
textareaAndButton: {
|
|
62
|
+
display: 'flex',
|
|
63
|
+
flexDirection: 'row',
|
|
64
|
+
gap: '5px',
|
|
65
|
+
},
|
|
66
|
+
hiddenFileInput: {
|
|
67
|
+
display: 'none',
|
|
68
|
+
},
|
|
69
|
+
}));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-hash-param.d.ts","sourceRoot":"","sources":["../src/use-hash-param.js"],"names":[],"mappings":"AAyEO,+EAmCN;AAEM,+EAaN"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/* eslint-disable no-param-reassign */
|
|
2
|
+
// Adapted from https://github.com/hejmsdz/use-hash-param/blob/3fd6818/src/index.js
|
|
3
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
4
|
+
const getHashSearchParams = (location) => {
|
|
5
|
+
const hash = location.hash.slice(1);
|
|
6
|
+
const [prefix, query] = hash.split('?');
|
|
7
|
+
return [prefix, new URLSearchParams(query)];
|
|
8
|
+
};
|
|
9
|
+
const getHashParam = (key, location = window.location) => {
|
|
10
|
+
// eslint-disable-next-line no-unused-vars
|
|
11
|
+
const [_, searchParams] = getHashSearchParams(location);
|
|
12
|
+
return searchParams.get(key);
|
|
13
|
+
};
|
|
14
|
+
const getAllHashParams = (key, location = window.location) => {
|
|
15
|
+
// eslint-disable-next-line no-unused-vars
|
|
16
|
+
const [_, searchParams] = getHashSearchParams(location);
|
|
17
|
+
return searchParams.getAll(key);
|
|
18
|
+
};
|
|
19
|
+
const getTypedHashParam = (key, varType) => {
|
|
20
|
+
if (varType === 'string-array') {
|
|
21
|
+
return getAllHashParams(key);
|
|
22
|
+
}
|
|
23
|
+
const val = getHashParam(key);
|
|
24
|
+
let typedVal = val;
|
|
25
|
+
if (varType === 'boolean') {
|
|
26
|
+
try {
|
|
27
|
+
typedVal = JSON.parse(val);
|
|
28
|
+
}
|
|
29
|
+
catch (e) {
|
|
30
|
+
typedVal = false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return typedVal;
|
|
34
|
+
};
|
|
35
|
+
const setHashParam = (key, value, location = window.location) => {
|
|
36
|
+
const [prefix, searchParams] = getHashSearchParams(location);
|
|
37
|
+
if (typeof value === 'undefined' || value === '') {
|
|
38
|
+
searchParams.delete(key);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
searchParams.set(key, value);
|
|
42
|
+
}
|
|
43
|
+
const search = searchParams.toString();
|
|
44
|
+
location.hash = search ? `${prefix}?${search}` : prefix;
|
|
45
|
+
};
|
|
46
|
+
// There is no "pushState" or "replaceState" event on the
|
|
47
|
+
// window, only a "popstate" event. We can add our own
|
|
48
|
+
// pushState and replaceState events by wrapping the
|
|
49
|
+
// methods of the window.history object and dispatching
|
|
50
|
+
// as custom events.
|
|
51
|
+
// Reference: https://stackoverflow.com/a/25673911
|
|
52
|
+
const wrapHistoryMethod = (methodName) => {
|
|
53
|
+
const orig = window.history[methodName];
|
|
54
|
+
window.history.wrapped = true;
|
|
55
|
+
// eslint-disable-next-line func-names
|
|
56
|
+
return function () {
|
|
57
|
+
// eslint-disable-next-line prefer-rest-params
|
|
58
|
+
const rv = orig.apply(this, arguments);
|
|
59
|
+
const e = new Event(methodName);
|
|
60
|
+
// eslint-disable-next-line prefer-rest-params
|
|
61
|
+
e.arguments = arguments;
|
|
62
|
+
window.dispatchEvent(e);
|
|
63
|
+
return rv;
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
export const useHashParam = (key, defaultValue, varType) => {
|
|
67
|
+
const [innerValue, setInnerValue] = useState(getTypedHashParam(key, varType));
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
let unmounted = false;
|
|
70
|
+
if (window.history && !window.history.wrapped) {
|
|
71
|
+
window.history.pushState = wrapHistoryMethod('pushState');
|
|
72
|
+
}
|
|
73
|
+
const handleHashChange = () => {
|
|
74
|
+
if (unmounted) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const nextValue = getTypedHashParam(key, varType);
|
|
78
|
+
setInnerValue(nextValue);
|
|
79
|
+
};
|
|
80
|
+
window.addEventListener('hashchange', handleHashChange);
|
|
81
|
+
window.addEventListener('pushState', handleHashChange);
|
|
82
|
+
return () => {
|
|
83
|
+
unmounted = true;
|
|
84
|
+
window.removeEventListener('hashchange', handleHashChange);
|
|
85
|
+
window.removeEventListener('pushState', handleHashChange);
|
|
86
|
+
};
|
|
87
|
+
}, [key, varType]);
|
|
88
|
+
const setValue = useCallback((value) => {
|
|
89
|
+
if (typeof value === 'function') {
|
|
90
|
+
setHashParam(key, value(getHashParam(key)));
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
setHashParam(key, value);
|
|
94
|
+
}
|
|
95
|
+
}, [key]);
|
|
96
|
+
return [innerValue === undefined ? defaultValue : innerValue, setValue];
|
|
97
|
+
};
|
|
98
|
+
export const useSetHashParams = () => (values, location = window.location) => {
|
|
99
|
+
const [prefix, searchParams] = getHashSearchParams(location);
|
|
100
|
+
Object.entries(values).forEach(([key, value]) => {
|
|
101
|
+
if (typeof value === 'undefined' || value === '') {
|
|
102
|
+
searchParams.delete(key);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
searchParams.set(key, value);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
const search = searchParams.toString();
|
|
109
|
+
location.hash = search ? `${prefix}?${search}` : prefix;
|
|
110
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-query-or-hash-param.d.ts","sourceRoot":"","sources":["../src/use-query-or-hash-param.js"],"names":[],"mappings":"AAuDA,0FAOC"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { useQueryParam, StringParam as StringQueryParam, ArrayParam as StringArrayQueryParam, } from 'use-query-params';
|
|
2
|
+
import { useHashParam } from './use-hash-param.js';
|
|
3
|
+
const DtypeToParamType = {
|
|
4
|
+
string: StringQueryParam,
|
|
5
|
+
'string-array': StringArrayQueryParam,
|
|
6
|
+
// Use string param for boolean since use-query-params only supports 0/1.
|
|
7
|
+
boolean: StringQueryParam,
|
|
8
|
+
};
|
|
9
|
+
function isTruthyString(v) {
|
|
10
|
+
return ['1', 'true'].includes(String(v).toLowerCase());
|
|
11
|
+
}
|
|
12
|
+
function useHashOrQueryParamAux(paramName, defaultValue, dtype) {
|
|
13
|
+
const [valueQ, setValueQ] = useQueryParam(paramName, DtypeToParamType[dtype]);
|
|
14
|
+
const [valueH, setValueH] = useHashParam(paramName, undefined, dtype);
|
|
15
|
+
if (dtype === 'boolean') {
|
|
16
|
+
const boolValueQ = valueQ !== undefined ? isTruthyString(valueQ) : undefined;
|
|
17
|
+
const boolValueH = valueH !== undefined ? isTruthyString(valueH) : undefined;
|
|
18
|
+
return valueQ ? [boolValueQ, setValueQ] : [boolValueH, setValueH];
|
|
19
|
+
}
|
|
20
|
+
if (dtype === 'string-array') {
|
|
21
|
+
// eslint-disable-next-line max-len
|
|
22
|
+
if (Array.isArray(valueH) && valueH.length > 0 && (!Array.isArray(valueQ) || valueQ.length === 0)) {
|
|
23
|
+
// Check for ; delimiter in individual array values, and split if necessary.
|
|
24
|
+
const splitValues = valueH.map(item => item.split(';')).flat();
|
|
25
|
+
return [splitValues, setValueH];
|
|
26
|
+
// eslint-disable-next-line max-len
|
|
27
|
+
}
|
|
28
|
+
if (Array.isArray(valueQ) && valueQ.length > 0 && (!Array.isArray(valueH) || valueH.length === 0)) {
|
|
29
|
+
// Check for ; delimiter in individual array values, and split if necessary.
|
|
30
|
+
const splitValues = valueQ.map(item => item.split(';')).flat();
|
|
31
|
+
return [splitValues, setValueQ];
|
|
32
|
+
}
|
|
33
|
+
return [null, setValueH];
|
|
34
|
+
}
|
|
35
|
+
if (Array.isArray(valueQ) || Array.isArray(valueH)) {
|
|
36
|
+
throw new Error(`Expected non-array values for "${paramName}".`);
|
|
37
|
+
}
|
|
38
|
+
if (valueQ && valueH) {
|
|
39
|
+
throw new Error(`Both query and hash parameters provided for "${paramName}". Please provide only one.`);
|
|
40
|
+
}
|
|
41
|
+
return valueQ ? [valueQ, setValueQ] : [valueH, setValueH];
|
|
42
|
+
}
|
|
43
|
+
export function useHashOrQueryParam(paramName, defaultValue, dtype) {
|
|
44
|
+
const [value, setValue] = useHashOrQueryParamAux(paramName, defaultValue, dtype);
|
|
45
|
+
try {
|
|
46
|
+
return [value, setValue, false];
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
return [value, setValue, error.message];
|
|
50
|
+
}
|
|
51
|
+
}
|