@jbrowse/core 4.0.4 → 4.1.1
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/esm/PluginLoader.js +11 -9
- package/esm/pluggableElementTypes/RpcMethodType.d.ts +1 -0
- package/esm/pluggableElementTypes/RpcMethodType.js +49 -5
- package/esm/pluggableElementTypes/RpcMethodTypeWithFiltersAndRenameRegions.js +1 -0
- package/esm/pluggableElementTypes/models/saveTrackFileTypes/bed.js +5 -2
- package/esm/pluggableElementTypes/models/saveTrackFileTypes/genbank.js +57 -26
- package/esm/pluggableElementTypes/models/saveTrackFileTypes/gff3.js +35 -16
- package/esm/pluggableElementTypes/renderers/LayoutSession.js +6 -3
- package/esm/pluggableElementTypes/renderers/ServerSideRendererType.js +4 -1
- package/esm/pluggableElementTypes/renderers/util/serializableFilterChain.d.ts +2 -1
- package/esm/pluggableElementTypes/renderers/util/serializableFilterChain.js +2 -2
- package/esm/rpc/methods/CoreRender.js +7 -2
- package/esm/ui/FileSelector/FileSelector.d.ts +0 -1
- package/esm/ui/FileSelector/FileSelector.js +19 -31
- package/esm/ui/FileSelector/LocalFileChooser.js +90 -26
- package/esm/ui/FileSelector/SourceTypeSelector.js +18 -33
- package/esm/ui/FileSelector/util.d.ts +8 -0
- package/esm/ui/FileSelector/util.js +34 -0
- package/esm/ui/SnackbarContents.js +4 -4
- package/esm/ui/SnackbarModel.d.ts +4 -4
- package/esm/ui/SnackbarModel.js +18 -7
- package/esm/ui/index.d.ts +1 -1
- package/esm/ui/index.js +1 -1
- package/esm/util/IntervalTree.d.ts +42 -0
- package/esm/util/IntervalTree.js +257 -0
- package/esm/util/colord.d.ts +22 -8
- package/esm/util/colord.js +227 -10
- package/esm/util/crypto.d.ts +7 -0
- package/esm/util/crypto.js +536 -0
- package/esm/util/fileHandleStore.d.ts +6 -0
- package/esm/util/fileHandleStore.js +68 -0
- package/esm/util/idMaker.js +9 -1
- package/esm/util/index.d.ts +3 -0
- package/esm/util/index.js +3 -0
- package/esm/util/io/index.js +11 -1
- package/esm/util/jexl.js +1 -0
- package/esm/util/tracks.d.ts +41 -7
- package/esm/util/tracks.js +141 -9
- package/esm/util/types/index.d.ts +10 -4
- package/esm/util/types/index.js +6 -0
- package/esm/util/types/mst.d.ts +17 -0
- package/esm/util/types/mst.js +10 -2
- package/package.json +350 -15
- package/esm/ui/FileSelector/index.d.ts +0 -1
- package/esm/ui/FileSelector/index.js +0 -1
|
@@ -1,39 +1,103 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Button, FormControl, Typography } from '@mui/material';
|
|
3
|
+
import { isFileSystemAccessSupported } from "../../util/fileHandleStore.js";
|
|
3
4
|
import { isElectron } from "../../util/index.js";
|
|
4
|
-
import { getBlob, storeBlobLocation } from "../../util/tracks.js";
|
|
5
|
+
import { ensureFileHandleReady, getBlob, getFileFromCache, storeBlobLocation, storeFileHandleLocation, } from "../../util/tracks.js";
|
|
5
6
|
import { makeStyles } from "../../util/tss-react/index.js";
|
|
6
|
-
|
|
7
|
-
return 'localPath' in loc;
|
|
8
|
-
}
|
|
9
|
-
function isBlobLocation(loc) {
|
|
10
|
-
return 'blobId' in loc;
|
|
11
|
-
}
|
|
7
|
+
import { isBlobLocation, isFileHandleLocation, isLocalPathLocation, } from "../../util/types/index.js";
|
|
12
8
|
const useStyles = makeStyles()(theme => ({
|
|
13
9
|
filename: {
|
|
14
10
|
marginLeft: theme.spacing(1),
|
|
15
11
|
},
|
|
16
12
|
}));
|
|
13
|
+
const supportsFileSystemAccess = isFileSystemAccessSupported() && !isElectron;
|
|
14
|
+
function getFilename(location) {
|
|
15
|
+
if (!location) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
if (isBlobLocation(location)) {
|
|
19
|
+
return location.name;
|
|
20
|
+
}
|
|
21
|
+
if (isLocalPathLocation(location)) {
|
|
22
|
+
return location.localPath;
|
|
23
|
+
}
|
|
24
|
+
if (isFileHandleLocation(location)) {
|
|
25
|
+
return location.name;
|
|
26
|
+
}
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
function needsReload(location) {
|
|
30
|
+
if (!location) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
if (isBlobLocation(location)) {
|
|
34
|
+
return !getBlob(location.blobId);
|
|
35
|
+
}
|
|
36
|
+
if (isFileHandleLocation(location)) {
|
|
37
|
+
return !getFileFromCache(location.handleId);
|
|
38
|
+
}
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
async function openFileSystemAccessPicker() {
|
|
42
|
+
const [handle] = await window.showOpenFilePicker();
|
|
43
|
+
return storeFileHandleLocation(handle);
|
|
44
|
+
}
|
|
45
|
+
function FilePickerButton({ setLocation, }) {
|
|
46
|
+
if (supportsFileSystemAccess) {
|
|
47
|
+
return (_jsx(Button, { variant: "outlined", onClick: async () => {
|
|
48
|
+
try {
|
|
49
|
+
setLocation(await openFileSystemAccessPicker());
|
|
50
|
+
}
|
|
51
|
+
catch (e) {
|
|
52
|
+
if (e.name !== 'AbortError') {
|
|
53
|
+
throw e;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}, children: "Choose File" }));
|
|
57
|
+
}
|
|
58
|
+
return (_jsxs(Button, { variant: "outlined", component: "label", children: ["Choose File", _jsx("input", { type: "file", hidden: true, onChange: ({ target }) => {
|
|
59
|
+
const file = target.files?.[0];
|
|
60
|
+
if (file) {
|
|
61
|
+
if (isElectron) {
|
|
62
|
+
const { webUtils } = window.require('electron');
|
|
63
|
+
setLocation({
|
|
64
|
+
localPath: webUtils.getPathForFile(file),
|
|
65
|
+
locationType: 'LocalPathLocation',
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
const loc = storeBlobLocation({ blob: file });
|
|
70
|
+
if ('blobId' in loc) {
|
|
71
|
+
setLocation(loc);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
} })] }));
|
|
76
|
+
}
|
|
77
|
+
function ReloadPrompt({ location, setLocation, }) {
|
|
78
|
+
const handleReopen = async () => {
|
|
79
|
+
if (isFileHandleLocation(location)) {
|
|
80
|
+
try {
|
|
81
|
+
await ensureFileHandleReady(location.handleId, true);
|
|
82
|
+
setLocation({ ...location });
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
try {
|
|
86
|
+
setLocation(await openFileSystemAccessPicker());
|
|
87
|
+
}
|
|
88
|
+
catch (e) {
|
|
89
|
+
if (e.name !== 'AbortError') {
|
|
90
|
+
throw e;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
return (_jsxs(Box, { display: "flex", alignItems: "center", gap: 1, children: [_jsx(Typography, { color: "error", children: "(need to reload)" }), isFileHandleLocation(location) && (_jsx(Button, { size: "small", variant: "text", color: "primary", onClick: handleReopen, children: "Reopen" }))] }));
|
|
97
|
+
}
|
|
17
98
|
function LocalFileChooser({ location, setLocation, }) {
|
|
18
99
|
const { classes } = useStyles();
|
|
19
|
-
const filename = location
|
|
20
|
-
|
|
21
|
-
(isLocalPathLocation(location) && location.localPath));
|
|
22
|
-
const needToReload = location && isBlobLocation(location) && !getBlob(location.blobId);
|
|
23
|
-
return (_jsxs(Box, { display: "flex", flexDirection: "row", alignItems: "center", children: [_jsx(Box, { children: _jsx(FormControl, { fullWidth: true, children: _jsxs(Button, { variant: "outlined", component: "label", children: ["Choose File", _jsx("input", { type: "file", hidden: true, onChange: ({ target }) => {
|
|
24
|
-
const file = target.files?.[0];
|
|
25
|
-
if (file) {
|
|
26
|
-
if (isElectron) {
|
|
27
|
-
const { webUtils } = window.require('electron');
|
|
28
|
-
setLocation({
|
|
29
|
-
localPath: webUtils.getPathForFile(file),
|
|
30
|
-
locationType: 'LocalPathLocation',
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
else {
|
|
34
|
-
setLocation(storeBlobLocation({ blob: file }));
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
} })] }) }) }), _jsxs(Box, { children: [_jsx(Typography, { component: "span", className: classes.filename, color: filename ? 'initial' : 'textSecondary', children: filename || 'No file chosen' }), needToReload ? (_jsx(Typography, { color: "error", children: "(need to reload)" })) : null] })] }));
|
|
100
|
+
const filename = getFilename(location);
|
|
101
|
+
return (_jsxs(Box, { display: "flex", flexDirection: "row", alignItems: "center", children: [_jsx(Box, { children: _jsx(FormControl, { fullWidth: true, children: _jsx(FilePickerButton, { setLocation: setLocation }) }) }), _jsxs(Box, { children: [_jsx(Typography, { component: "span", className: classes.filename, color: filename ? 'initial' : 'textSecondary', children: filename || 'No file chosen' }), location && needsReload(location) && (_jsx(ReloadPrompt, { location: location, setLocation: setLocation }))] })] }));
|
|
38
102
|
}
|
|
39
103
|
export default LocalFileChooser;
|
|
@@ -1,38 +1,23 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs
|
|
2
|
-
import { useState } from 'react';
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
2
|
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
import { ToggleButton, ToggleButtonGroup, Tooltip } from '@mui/material';
|
|
4
|
+
import CascadingMenuButton from "../CascadingMenuButton.js";
|
|
5
|
+
import { getAccountLabel, isAdminMode } from "./util.js";
|
|
6
|
+
function AccountToggleButton({ account, }) {
|
|
7
|
+
return (_jsx(Tooltip, { title: account.name, children: _jsx(ToggleButton, { value: account.internetAccountId, children: getAccountLabel(account) }) }));
|
|
7
8
|
}
|
|
8
|
-
function
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
12
|
-
const isAdminMode = () => new URLSearchParams(window.location.search).get('adminKey') !== null;
|
|
13
|
-
function HiddenAccountsMenu({ anchorEl, hiddenAccountIds, accountMap, onClose, onSelect, }) {
|
|
14
|
-
if (!anchorEl) {
|
|
15
|
-
return null;
|
|
16
|
-
}
|
|
17
|
-
return (_jsx(Menu, { open: true, anchorEl: anchorEl, anchorOrigin: { vertical: 'bottom', horizontal: 'center' }, transformOrigin: { vertical: 'top', horizontal: 'center' }, onClose: onClose, children: hiddenAccountIds.map(id => (_jsx(MenuItem, { value: id, onClick: () => {
|
|
18
|
-
onSelect(id);
|
|
19
|
-
}, children: accountMap[id].name }, id))) }));
|
|
9
|
+
function MoreButton({ onClick, disabled, children, }) {
|
|
10
|
+
return (_jsx(ToggleButton, { value: "more", selected: false, disabled: disabled, onClick: e => {
|
|
11
|
+
onClick(e);
|
|
12
|
+
}, children: children }));
|
|
20
13
|
}
|
|
21
14
|
export default function SourceTypeSelector({ value, shownAccountIds, hiddenAccountIds, accountMap, onChange, onHiddenAccountSelect, }) {
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}), hiddenAccountIds.length > 0 ? (_jsxs(ToggleButton, { value: "more", onClick: event => {
|
|
31
|
-
setAnchorEl(event.target);
|
|
32
|
-
}, selected: false, children: ["More", _jsx(ArrowDropDownIcon, {})] })) : null] }), _jsx(HiddenAccountsMenu, { anchorEl: anchorEl, hiddenAccountIds: hiddenAccountIds, accountMap: accountMap, onClose: () => {
|
|
33
|
-
setAnchorEl(null);
|
|
34
|
-
}, onSelect: id => {
|
|
35
|
-
onHiddenAccountSelect(id);
|
|
36
|
-
setAnchorEl(null);
|
|
37
|
-
} })] }));
|
|
15
|
+
const shownAccounts = shownAccountIds.map(id => accountMap[id]);
|
|
16
|
+
const hiddenAccounts = hiddenAccountIds.map(id => accountMap[id]);
|
|
17
|
+
return (_jsxs(ToggleButtonGroup, { value: value, exclusive: true, size: "small", onChange: onChange, "aria-label": "file, url, or account picker", children: [!isAdminMode() && (_jsx(ToggleButton, { value: "file", "aria-label": "local file", children: "File" })), _jsx(ToggleButton, { value: "url", "aria-label": "url", children: "URL" }), shownAccounts.map(account => (_jsx(AccountToggleButton, { account: account }, account.internetAccountId))), hiddenAccounts.length > 0 && (_jsxs(CascadingMenuButton, { menuItems: hiddenAccounts.map(account => ({
|
|
18
|
+
label: account.name,
|
|
19
|
+
onClick: () => {
|
|
20
|
+
onHiddenAccountSelect(account.internetAccountId);
|
|
21
|
+
},
|
|
22
|
+
})), ButtonComponent: MoreButton, anchorOrigin: { vertical: 'bottom', horizontal: 'center' }, transformOrigin: { vertical: 'top', horizontal: 'center' }, children: ["More", _jsx(ArrowDropDownIcon, {})] }))] }));
|
|
38
23
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { BaseInternetAccountModel } from '../../pluggableElementTypes/index.ts';
|
|
2
|
+
import type { FileLocation } from '../../util/index.ts';
|
|
3
|
+
export declare const MAX_LABEL_LENGTH = 5;
|
|
4
|
+
export declare function isAdminMode(): boolean;
|
|
5
|
+
export declare function shorten(str: string): string;
|
|
6
|
+
export declare function getAccountLabel(account: BaseInternetAccountModel): string | number | bigint | true | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<import("react").ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<import("react").ReactNode> | null | undefined>;
|
|
7
|
+
export declare function getInitialSourceType(location?: FileLocation): string;
|
|
8
|
+
export declare function addAccountToLocation(location: FileLocation, account?: BaseInternetAccountModel): FileLocation;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { isUriLocation } from "../../util/index.js";
|
|
2
|
+
export const MAX_LABEL_LENGTH = 5;
|
|
3
|
+
export function isAdminMode() {
|
|
4
|
+
return (typeof window !== 'undefined' &&
|
|
5
|
+
new URLSearchParams(window.location.search).get('adminKey') !== null);
|
|
6
|
+
}
|
|
7
|
+
export function shorten(str) {
|
|
8
|
+
return str.length > MAX_LABEL_LENGTH
|
|
9
|
+
? `${str.slice(0, MAX_LABEL_LENGTH)}…`
|
|
10
|
+
: str;
|
|
11
|
+
}
|
|
12
|
+
export function getAccountLabel(account) {
|
|
13
|
+
const { toggleContents, name } = account;
|
|
14
|
+
if (toggleContents) {
|
|
15
|
+
return typeof toggleContents === 'string'
|
|
16
|
+
? shorten(toggleContents)
|
|
17
|
+
: toggleContents;
|
|
18
|
+
}
|
|
19
|
+
return shorten(name);
|
|
20
|
+
}
|
|
21
|
+
export function getInitialSourceType(location) {
|
|
22
|
+
if (location &&
|
|
23
|
+
'internetAccountId' in location &&
|
|
24
|
+
location.internetAccountId) {
|
|
25
|
+
return location.internetAccountId;
|
|
26
|
+
}
|
|
27
|
+
return !location || isUriLocation(location) ? 'url' : 'file';
|
|
28
|
+
}
|
|
29
|
+
export function addAccountToLocation(location, account) {
|
|
30
|
+
if (account && isUriLocation(location)) {
|
|
31
|
+
return { ...location, internetAccountId: account.internetAccountId };
|
|
32
|
+
}
|
|
33
|
+
return location;
|
|
34
|
+
}
|
|
@@ -3,9 +3,9 @@ import CloseIcon from '@mui/icons-material/Close';
|
|
|
3
3
|
import Report from '@mui/icons-material/Report';
|
|
4
4
|
import { Alert, Button, IconButton, Snackbar as MUISnackbar, } from '@mui/material';
|
|
5
5
|
export default function SnackbarContents({ onClose, contents, }) {
|
|
6
|
-
const
|
|
7
|
-
return (_jsx(MUISnackbar, { open: true, onClose: onClose, anchorOrigin: { vertical: 'bottom', horizontal: 'center' }, children: _jsx(Alert, { onClose: onClose, action:
|
|
8
|
-
|
|
6
|
+
const { actions } = contents;
|
|
7
|
+
return (_jsx(MUISnackbar, { open: true, onClose: onClose, anchorOrigin: { vertical: 'bottom', horizontal: 'center' }, children: _jsx(Alert, { onClose: onClose, action: actions?.length ? (_jsxs(_Fragment, { children: [actions.map((action, idx) => (_jsx(Button, { color: "inherit", onClick: e => {
|
|
8
|
+
action.onClick();
|
|
9
9
|
onClose(e);
|
|
10
|
-
}, children:
|
|
10
|
+
}, children: action.name === 'report' ? _jsx(Report, {}) : action.name }, idx))), _jsx(IconButton, { color: "inherit", onClick: onClose, children: _jsx(CloseIcon, {}) })] })) : null, severity: contents.level || 'warning', children: contents.message }) }));
|
|
11
11
|
}
|
|
@@ -2,16 +2,16 @@ import type { NotificationLevel, SnackAction } from '../util/types/index.ts';
|
|
|
2
2
|
export interface SnackbarMessage {
|
|
3
3
|
message: string;
|
|
4
4
|
level?: NotificationLevel;
|
|
5
|
-
|
|
5
|
+
actions?: SnackAction[];
|
|
6
6
|
}
|
|
7
7
|
export default function SnackbarModel(): import("@jbrowse/mobx-state-tree").IModelType<{}, {
|
|
8
8
|
snackbarMessages: import("mobx").IObservableArray<SnackbarMessage>;
|
|
9
9
|
} & {
|
|
10
10
|
readonly snackbarMessageSet: Map<string, SnackbarMessage>;
|
|
11
11
|
} & {
|
|
12
|
-
notify(message: string, level?: NotificationLevel, action?: SnackAction): void;
|
|
13
|
-
notifyError(errorMessage: string, error?: unknown, extra?: unknown): void;
|
|
14
|
-
pushSnackbarMessage(message: string, level?: NotificationLevel,
|
|
12
|
+
notify(message: string, level?: NotificationLevel, action?: SnackAction | SnackAction[]): void;
|
|
13
|
+
notifyError(errorMessage: string, error?: unknown, extra?: unknown, action?: SnackAction): void;
|
|
14
|
+
pushSnackbarMessage(message: string, level?: NotificationLevel, actions?: SnackAction[]): void;
|
|
15
15
|
popSnackbarMessage(): SnackbarMessage | undefined;
|
|
16
16
|
removeSnackbarMessage(message: string): void;
|
|
17
17
|
}, import("@jbrowse/mobx-state-tree")._NotCustomized, import("@jbrowse/mobx-state-tree")._NotCustomized>;
|
package/esm/ui/SnackbarModel.js
CHANGED
|
@@ -15,15 +15,20 @@ export default function SnackbarModel() {
|
|
|
15
15
|
}))
|
|
16
16
|
.actions(self => ({
|
|
17
17
|
notify(message, level, action) {
|
|
18
|
-
|
|
18
|
+
const actions = action
|
|
19
|
+
? Array.isArray(action)
|
|
20
|
+
? action
|
|
21
|
+
: [action]
|
|
22
|
+
: undefined;
|
|
23
|
+
this.pushSnackbarMessage(message, level, actions);
|
|
19
24
|
if (level === 'info' || level === 'success') {
|
|
20
25
|
setTimeout(() => {
|
|
21
26
|
this.removeSnackbarMessage(message);
|
|
22
27
|
}, 5000);
|
|
23
28
|
}
|
|
24
29
|
},
|
|
25
|
-
notifyError(errorMessage, error, extra) {
|
|
26
|
-
|
|
30
|
+
notifyError(errorMessage, error, extra, action) {
|
|
31
|
+
const reportAction = {
|
|
27
32
|
name: 'report',
|
|
28
33
|
onClick: () => {
|
|
29
34
|
self.queueDialog((onClose) => [
|
|
@@ -35,11 +40,17 @@ export default function SnackbarModel() {
|
|
|
35
40
|
},
|
|
36
41
|
]);
|
|
37
42
|
},
|
|
38
|
-
}
|
|
43
|
+
};
|
|
44
|
+
const actions = action ? [reportAction, action] : [reportAction];
|
|
45
|
+
this.pushSnackbarMessage(errorMessage, 'error', actions);
|
|
39
46
|
},
|
|
40
|
-
pushSnackbarMessage(message, level,
|
|
41
|
-
if (
|
|
42
|
-
self.snackbarMessages.push({
|
|
47
|
+
pushSnackbarMessage(message, level, actions) {
|
|
48
|
+
if (actions?.length || !self.snackbarMessageSet.has(message)) {
|
|
49
|
+
self.snackbarMessages.push({
|
|
50
|
+
message,
|
|
51
|
+
level,
|
|
52
|
+
actions,
|
|
53
|
+
});
|
|
43
54
|
}
|
|
44
55
|
},
|
|
45
56
|
popSnackbarMessage() {
|
package/esm/ui/index.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export { default as Dialog } from './Dialog.tsx';
|
|
|
7
7
|
export { default as EditableTypography } from './EditableTypography.tsx';
|
|
8
8
|
export { default as ErrorMessage } from './ErrorMessage.tsx';
|
|
9
9
|
export { default as FatalErrorDialog } from './FatalErrorDialog.tsx';
|
|
10
|
-
export { default as FileSelector } from './FileSelector/
|
|
10
|
+
export { default as FileSelector } from './FileSelector/FileSelector.tsx';
|
|
11
11
|
export { default as LoadingEllipses } from './LoadingEllipses.tsx';
|
|
12
12
|
export { default as Menu } from './Menu.tsx';
|
|
13
13
|
export { default as PrerenderedCanvas } from './PrerenderedCanvas.tsx';
|
package/esm/ui/index.js
CHANGED
|
@@ -7,7 +7,7 @@ export { default as Dialog } from "./Dialog.js";
|
|
|
7
7
|
export { default as EditableTypography } from "./EditableTypography.js";
|
|
8
8
|
export { default as ErrorMessage } from "./ErrorMessage.js";
|
|
9
9
|
export { default as FatalErrorDialog } from "./FatalErrorDialog.js";
|
|
10
|
-
export { default as FileSelector } from "./FileSelector/
|
|
10
|
+
export { default as FileSelector } from "./FileSelector/FileSelector.js";
|
|
11
11
|
export { default as LoadingEllipses } from "./LoadingEllipses.js";
|
|
12
12
|
export { default as Menu } from "./Menu.js";
|
|
13
13
|
export { default as PrerenderedCanvas } from "./PrerenderedCanvas.js";
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
declare const RB_TREE_COLOR_RED = 1;
|
|
2
|
+
declare const RB_TREE_COLOR_BLACK = 0;
|
|
3
|
+
type NodeColor = typeof RB_TREE_COLOR_RED | typeof RB_TREE_COLOR_BLACK;
|
|
4
|
+
declare class Interval {
|
|
5
|
+
low: number;
|
|
6
|
+
high: number;
|
|
7
|
+
constructor(low: number, high: number);
|
|
8
|
+
lessThan(other: Interval): boolean;
|
|
9
|
+
equalTo(other: Interval): boolean;
|
|
10
|
+
intersects(other: Interval): boolean;
|
|
11
|
+
merge(other: Interval): Interval;
|
|
12
|
+
}
|
|
13
|
+
declare class Node<V> {
|
|
14
|
+
left: Node<V> | null;
|
|
15
|
+
right: Node<V> | null;
|
|
16
|
+
parent: Node<V> | null;
|
|
17
|
+
color: NodeColor;
|
|
18
|
+
key: Interval | undefined;
|
|
19
|
+
values: V[];
|
|
20
|
+
max: Interval | undefined;
|
|
21
|
+
constructor(key?: Interval | [number, number], value?: V, left?: Node<V> | null, right?: Node<V> | null, parent?: Node<V> | null, color?: NodeColor);
|
|
22
|
+
lessThan(other: Node<V>): boolean;
|
|
23
|
+
equalTo(other: Node<V>): boolean;
|
|
24
|
+
intersects(other: Node<V>): boolean;
|
|
25
|
+
updateMax(): void;
|
|
26
|
+
notIntersectLeftSubtree(searchNode: Node<V>): boolean;
|
|
27
|
+
notIntersectRightSubtree(searchNode: Node<V>): boolean;
|
|
28
|
+
}
|
|
29
|
+
export declare class IntervalTree<V> {
|
|
30
|
+
root: Node<V> | null;
|
|
31
|
+
private nilNode;
|
|
32
|
+
insert(key: [number, number], value: V): Node<V>;
|
|
33
|
+
search(interval: [number, number]): V[];
|
|
34
|
+
private recalcMax;
|
|
35
|
+
private treeInsert;
|
|
36
|
+
private insertFixup;
|
|
37
|
+
private treeSearch;
|
|
38
|
+
private treeSearchInterval;
|
|
39
|
+
private rotateLeft;
|
|
40
|
+
private rotateRight;
|
|
41
|
+
}
|
|
42
|
+
export {};
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
const RB_TREE_COLOR_RED = 1;
|
|
2
|
+
const RB_TREE_COLOR_BLACK = 0;
|
|
3
|
+
class Interval {
|
|
4
|
+
low;
|
|
5
|
+
high;
|
|
6
|
+
constructor(low, high) {
|
|
7
|
+
this.low = low;
|
|
8
|
+
this.high = high;
|
|
9
|
+
}
|
|
10
|
+
lessThan(other) {
|
|
11
|
+
return (this.low < other.low || (this.low === other.low && this.high < other.high));
|
|
12
|
+
}
|
|
13
|
+
equalTo(other) {
|
|
14
|
+
return this.low === other.low && this.high === other.high;
|
|
15
|
+
}
|
|
16
|
+
intersects(other) {
|
|
17
|
+
return !(this.high < other.low || other.high < this.low);
|
|
18
|
+
}
|
|
19
|
+
merge(other) {
|
|
20
|
+
return new Interval(Math.min(this.low, other.low), Math.max(this.high, other.high));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
class Node {
|
|
24
|
+
left = null;
|
|
25
|
+
right = null;
|
|
26
|
+
parent = null;
|
|
27
|
+
color = RB_TREE_COLOR_BLACK;
|
|
28
|
+
key;
|
|
29
|
+
values = [];
|
|
30
|
+
max;
|
|
31
|
+
constructor(key, value, left, right, parent, color) {
|
|
32
|
+
if (left !== undefined) {
|
|
33
|
+
this.left = left;
|
|
34
|
+
}
|
|
35
|
+
if (right !== undefined) {
|
|
36
|
+
this.right = right;
|
|
37
|
+
}
|
|
38
|
+
if (parent !== undefined) {
|
|
39
|
+
this.parent = parent;
|
|
40
|
+
}
|
|
41
|
+
if (color !== undefined) {
|
|
42
|
+
this.color = color;
|
|
43
|
+
}
|
|
44
|
+
if (value !== undefined) {
|
|
45
|
+
this.values.push(value);
|
|
46
|
+
}
|
|
47
|
+
if (key !== undefined) {
|
|
48
|
+
if (Array.isArray(key)) {
|
|
49
|
+
const [low, high] = key;
|
|
50
|
+
this.key =
|
|
51
|
+
low <= high ? new Interval(low, high) : new Interval(high, low);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
this.key = key;
|
|
55
|
+
}
|
|
56
|
+
this.max = this.key;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
lessThan(other) {
|
|
60
|
+
return this.key.lessThan(other.key);
|
|
61
|
+
}
|
|
62
|
+
equalTo(other) {
|
|
63
|
+
return this.key.equalTo(other.key);
|
|
64
|
+
}
|
|
65
|
+
intersects(other) {
|
|
66
|
+
return this.key.intersects(other.key);
|
|
67
|
+
}
|
|
68
|
+
updateMax() {
|
|
69
|
+
this.max = this.key;
|
|
70
|
+
if (this.right?.max && this.max) {
|
|
71
|
+
this.max = this.max.merge(this.right.max);
|
|
72
|
+
}
|
|
73
|
+
if (this.left?.max && this.max) {
|
|
74
|
+
this.max = this.max.merge(this.left.max);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
notIntersectLeftSubtree(searchNode) {
|
|
78
|
+
if (!this.left) {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
const high = this.left.max?.high ?? this.left.key.high;
|
|
82
|
+
return high < searchNode.key.low;
|
|
83
|
+
}
|
|
84
|
+
notIntersectRightSubtree(searchNode) {
|
|
85
|
+
if (!this.right) {
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
const low = this.right.max?.low ?? this.right.key.low;
|
|
89
|
+
return searchNode.key.high < low;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
export class IntervalTree {
|
|
93
|
+
root = null;
|
|
94
|
+
nilNode = new Node();
|
|
95
|
+
insert(key, value) {
|
|
96
|
+
const existing = this.treeSearch(this.root, new Node(key));
|
|
97
|
+
if (existing) {
|
|
98
|
+
existing.values.push(value);
|
|
99
|
+
return existing;
|
|
100
|
+
}
|
|
101
|
+
const insertNode = new Node(key, value, this.nilNode, this.nilNode, null, RB_TREE_COLOR_RED);
|
|
102
|
+
this.treeInsert(insertNode);
|
|
103
|
+
this.recalcMax(insertNode);
|
|
104
|
+
return insertNode;
|
|
105
|
+
}
|
|
106
|
+
search(interval) {
|
|
107
|
+
const searchNode = new Node(interval);
|
|
108
|
+
const resultNodes = [];
|
|
109
|
+
this.treeSearchInterval(this.root, searchNode, resultNodes);
|
|
110
|
+
const results = [];
|
|
111
|
+
for (const node of resultNodes) {
|
|
112
|
+
for (const v of node.values) {
|
|
113
|
+
results.push(v);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return results;
|
|
117
|
+
}
|
|
118
|
+
recalcMax(node) {
|
|
119
|
+
let current = node;
|
|
120
|
+
while (current.parent != null) {
|
|
121
|
+
current.parent.updateMax();
|
|
122
|
+
current = current.parent;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
treeInsert(insertNode) {
|
|
126
|
+
let current = this.root;
|
|
127
|
+
let parent = null;
|
|
128
|
+
if (this.root == null || this.root === this.nilNode) {
|
|
129
|
+
this.root = insertNode;
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
while (current !== this.nilNode) {
|
|
133
|
+
parent = current;
|
|
134
|
+
current = insertNode.lessThan(current) ? current.left : current.right;
|
|
135
|
+
}
|
|
136
|
+
insertNode.parent = parent;
|
|
137
|
+
if (insertNode.lessThan(parent)) {
|
|
138
|
+
parent.left = insertNode;
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
parent.right = insertNode;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
this.insertFixup(insertNode);
|
|
145
|
+
}
|
|
146
|
+
insertFixup(insertNode) {
|
|
147
|
+
let current = insertNode;
|
|
148
|
+
while (current !== this.root &&
|
|
149
|
+
current.parent.color === RB_TREE_COLOR_RED) {
|
|
150
|
+
if (current.parent === current.parent.parent.left) {
|
|
151
|
+
const uncle = current.parent.parent.right;
|
|
152
|
+
if (uncle.color === RB_TREE_COLOR_RED) {
|
|
153
|
+
current.parent.color = RB_TREE_COLOR_BLACK;
|
|
154
|
+
uncle.color = RB_TREE_COLOR_BLACK;
|
|
155
|
+
current.parent.parent.color = RB_TREE_COLOR_RED;
|
|
156
|
+
current = current.parent.parent;
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
if (current === current.parent.right) {
|
|
160
|
+
current = current.parent;
|
|
161
|
+
this.rotateLeft(current);
|
|
162
|
+
}
|
|
163
|
+
current.parent.color = RB_TREE_COLOR_BLACK;
|
|
164
|
+
current.parent.parent.color = RB_TREE_COLOR_RED;
|
|
165
|
+
this.rotateRight(current.parent.parent);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
const uncle = current.parent.parent.left;
|
|
170
|
+
if (uncle.color === RB_TREE_COLOR_RED) {
|
|
171
|
+
current.parent.color = RB_TREE_COLOR_BLACK;
|
|
172
|
+
uncle.color = RB_TREE_COLOR_BLACK;
|
|
173
|
+
current.parent.parent.color = RB_TREE_COLOR_RED;
|
|
174
|
+
current = current.parent.parent;
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
if (current === current.parent.left) {
|
|
178
|
+
current = current.parent;
|
|
179
|
+
this.rotateRight(current);
|
|
180
|
+
}
|
|
181
|
+
current.parent.color = RB_TREE_COLOR_BLACK;
|
|
182
|
+
current.parent.parent.color = RB_TREE_COLOR_RED;
|
|
183
|
+
this.rotateLeft(current.parent.parent);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
this.root.color = RB_TREE_COLOR_BLACK;
|
|
188
|
+
}
|
|
189
|
+
treeSearch(node, searchNode) {
|
|
190
|
+
if (node == null || node === this.nilNode) {
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
193
|
+
if (searchNode.equalTo(node)) {
|
|
194
|
+
return node;
|
|
195
|
+
}
|
|
196
|
+
return searchNode.lessThan(node)
|
|
197
|
+
? this.treeSearch(node.left, searchNode)
|
|
198
|
+
: this.treeSearch(node.right, searchNode);
|
|
199
|
+
}
|
|
200
|
+
treeSearchInterval(node, searchNode, results) {
|
|
201
|
+
if (node != null && node !== this.nilNode) {
|
|
202
|
+
if (node.left !== this.nilNode &&
|
|
203
|
+
!node.notIntersectLeftSubtree(searchNode)) {
|
|
204
|
+
this.treeSearchInterval(node.left, searchNode, results);
|
|
205
|
+
}
|
|
206
|
+
if (node.intersects(searchNode)) {
|
|
207
|
+
results.push(node);
|
|
208
|
+
}
|
|
209
|
+
if (node.right !== this.nilNode &&
|
|
210
|
+
!node.notIntersectRightSubtree(searchNode)) {
|
|
211
|
+
this.treeSearchInterval(node.right, searchNode, results);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
rotateLeft(x) {
|
|
216
|
+
const y = x.right;
|
|
217
|
+
x.right = y.left;
|
|
218
|
+
if (y.left !== this.nilNode) {
|
|
219
|
+
y.left.parent = x;
|
|
220
|
+
}
|
|
221
|
+
y.parent = x.parent;
|
|
222
|
+
if (x === this.root) {
|
|
223
|
+
this.root = y;
|
|
224
|
+
}
|
|
225
|
+
else if (x === x.parent.left) {
|
|
226
|
+
x.parent.left = y;
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
x.parent.right = y;
|
|
230
|
+
}
|
|
231
|
+
y.left = x;
|
|
232
|
+
x.parent = y;
|
|
233
|
+
x.updateMax();
|
|
234
|
+
y.updateMax();
|
|
235
|
+
}
|
|
236
|
+
rotateRight(y) {
|
|
237
|
+
const x = y.left;
|
|
238
|
+
y.left = x.right;
|
|
239
|
+
if (x.right !== this.nilNode) {
|
|
240
|
+
x.right.parent = y;
|
|
241
|
+
}
|
|
242
|
+
x.parent = y.parent;
|
|
243
|
+
if (y === this.root) {
|
|
244
|
+
this.root = x;
|
|
245
|
+
}
|
|
246
|
+
else if (y === y.parent.left) {
|
|
247
|
+
y.parent.left = x;
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
y.parent.right = x;
|
|
251
|
+
}
|
|
252
|
+
x.right = y;
|
|
253
|
+
y.parent = x;
|
|
254
|
+
y.updateMax();
|
|
255
|
+
x.updateMax();
|
|
256
|
+
}
|
|
257
|
+
}
|