@jbrowse/plugin-grid-bookmark 2.6.2 → 2.7.0
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/dist/GridBookmarkWidget/components/AssemblySelector.d.ts +3 -4
- package/dist/GridBookmarkWidget/components/AssemblySelector.js +27 -37
- package/dist/GridBookmarkWidget/components/BookmarkGrid.d.ts +6 -0
- package/dist/GridBookmarkWidget/components/BookmarkGrid.js +120 -0
- package/dist/GridBookmarkWidget/components/{ClearBookmarks.d.ts → DeleteBookmarks.d.ts} +2 -3
- package/dist/GridBookmarkWidget/components/DeleteBookmarks.js +41 -0
- package/dist/GridBookmarkWidget/components/DeleteBookmarksDialog.d.ts +7 -0
- package/dist/GridBookmarkWidget/components/DeleteBookmarksDialog.js +29 -0
- package/dist/GridBookmarkWidget/components/EditBookmarkLabelDialog.d.ts +8 -0
- package/dist/GridBookmarkWidget/components/EditBookmarkLabelDialog.js +50 -0
- package/dist/GridBookmarkWidget/components/ExportBookmarks.d.ts +6 -0
- package/dist/GridBookmarkWidget/components/{ClearBookmarks.js → ExportBookmarks.js} +9 -19
- package/dist/GridBookmarkWidget/components/ExportBookmarksDialog.d.ts +7 -0
- package/dist/GridBookmarkWidget/components/{DownloadBookmarks.js → ExportBookmarksDialog.js} +22 -20
- package/dist/GridBookmarkWidget/components/GridBookmarkWidget.d.ts +3 -4
- package/dist/GridBookmarkWidget/components/GridBookmarkWidget.js +28 -97
- package/dist/GridBookmarkWidget/components/ImportBookmarks.d.ts +3 -5
- package/dist/GridBookmarkWidget/components/ImportBookmarks.js +8 -64
- package/dist/GridBookmarkWidget/components/ImportBookmarksDialog.d.ts +7 -0
- package/dist/GridBookmarkWidget/components/ImportBookmarksDialog.js +129 -0
- package/{esm/GridBookmarkWidget/components/ClearBookmarks.d.ts → dist/GridBookmarkWidget/components/ShareBookmarks.d.ts} +2 -3
- package/dist/GridBookmarkWidget/components/ShareBookmarks.js +37 -0
- package/dist/GridBookmarkWidget/components/ShareBookmarksDialog.d.ts +7 -0
- package/dist/GridBookmarkWidget/components/ShareBookmarksDialog.js +106 -0
- package/dist/GridBookmarkWidget/model.d.ts +178 -11
- package/dist/GridBookmarkWidget/model.js +105 -27
- package/dist/GridBookmarkWidget/sessionSharing.d.ts +6 -0
- package/dist/GridBookmarkWidget/sessionSharing.js +96 -0
- package/dist/GridBookmarkWidget/utils.d.ts +19 -3
- package/dist/GridBookmarkWidget/utils.js +132 -40
- package/dist/index.d.ts +1 -1
- package/dist/index.js +100 -75
- package/esm/GridBookmarkWidget/components/AssemblySelector.d.ts +3 -4
- package/esm/GridBookmarkWidget/components/AssemblySelector.js +28 -38
- package/esm/GridBookmarkWidget/components/BookmarkGrid.d.ts +6 -0
- package/esm/GridBookmarkWidget/components/BookmarkGrid.js +92 -0
- package/{dist/GridBookmarkWidget/components/DownloadBookmarks.d.ts → esm/GridBookmarkWidget/components/DeleteBookmarks.d.ts} +2 -3
- package/esm/GridBookmarkWidget/components/DeleteBookmarks.js +13 -0
- package/esm/GridBookmarkWidget/components/DeleteBookmarksDialog.d.ts +7 -0
- package/esm/GridBookmarkWidget/components/DeleteBookmarksDialog.js +24 -0
- package/esm/GridBookmarkWidget/components/EditBookmarkLabelDialog.d.ts +8 -0
- package/esm/GridBookmarkWidget/components/EditBookmarkLabelDialog.js +25 -0
- package/esm/GridBookmarkWidget/components/ExportBookmarks.d.ts +6 -0
- package/esm/GridBookmarkWidget/components/ExportBookmarks.js +13 -0
- package/esm/GridBookmarkWidget/components/ExportBookmarksDialog.d.ts +7 -0
- package/esm/GridBookmarkWidget/components/ExportBookmarksDialog.js +35 -0
- package/esm/GridBookmarkWidget/components/GridBookmarkWidget.d.ts +3 -4
- package/esm/GridBookmarkWidget/components/GridBookmarkWidget.js +28 -74
- package/esm/GridBookmarkWidget/components/ImportBookmarks.d.ts +3 -5
- package/esm/GridBookmarkWidget/components/ImportBookmarks.js +10 -66
- package/esm/GridBookmarkWidget/components/ImportBookmarksDialog.d.ts +7 -0
- package/esm/GridBookmarkWidget/components/ImportBookmarksDialog.js +101 -0
- package/esm/GridBookmarkWidget/components/{DownloadBookmarks.d.ts → ShareBookmarks.d.ts} +2 -3
- package/esm/GridBookmarkWidget/components/ShareBookmarks.js +12 -0
- package/esm/GridBookmarkWidget/components/ShareBookmarksDialog.d.ts +7 -0
- package/esm/GridBookmarkWidget/components/ShareBookmarksDialog.js +78 -0
- package/esm/GridBookmarkWidget/model.d.ts +178 -11
- package/esm/GridBookmarkWidget/model.js +106 -28
- package/esm/GridBookmarkWidget/sessionSharing.d.ts +6 -0
- package/esm/GridBookmarkWidget/sessionSharing.js +68 -0
- package/esm/GridBookmarkWidget/utils.d.ts +19 -3
- package/esm/GridBookmarkWidget/utils.js +105 -39
- package/esm/index.d.ts +1 -1
- package/esm/index.js +101 -76
- package/package.json +4 -3
- package/dist/GridBookmarkWidget/components/DeleteBookmark.d.ts +0 -9
- package/dist/GridBookmarkWidget/components/DeleteBookmark.js +0 -31
- package/esm/GridBookmarkWidget/components/ClearBookmarks.js +0 -23
- package/esm/GridBookmarkWidget/components/DeleteBookmark.d.ts +0 -9
- package/esm/GridBookmarkWidget/components/DeleteBookmark.js +0 -26
- package/esm/GridBookmarkWidget/components/DownloadBookmarks.js +0 -33
|
@@ -1,53 +1,131 @@
|
|
|
1
|
-
import { types, cast } from 'mobx-state-tree';
|
|
1
|
+
import { types, cast, addDisposer, } from 'mobx-state-tree';
|
|
2
2
|
import { Region as RegionModel, ElementId } from '@jbrowse/core/util/types/mst';
|
|
3
|
+
import { getSession, localStorageGetItem, localStorageSetItem, } from '@jbrowse/core/util';
|
|
4
|
+
import { autorun } from 'mobx';
|
|
3
5
|
const LabeledRegionModel = types
|
|
4
|
-
.compose(RegionModel, types.model('Label', {
|
|
6
|
+
.compose(RegionModel, types.model('Label', {
|
|
7
|
+
label: types.optional(types.string, ''),
|
|
8
|
+
}))
|
|
5
9
|
.actions(self => ({
|
|
6
10
|
setLabel(label) {
|
|
7
11
|
self.label = label;
|
|
8
12
|
},
|
|
9
13
|
}));
|
|
10
|
-
|
|
14
|
+
const SharedBookmarksModel = types.model('SharedBookmarksModel', {
|
|
15
|
+
sharedBookmarks: types.maybe(types.array(LabeledRegionModel)),
|
|
16
|
+
});
|
|
17
|
+
const localStorageKeyF = () => typeof window !== undefined
|
|
18
|
+
? `bookmarks-${[window.location.host + window.location.pathname].join('-')}`
|
|
19
|
+
: 'empty';
|
|
20
|
+
export default function f(_pluginManager) {
|
|
11
21
|
return types
|
|
12
22
|
.model('GridBookmarkModel', {
|
|
23
|
+
/**
|
|
24
|
+
* #property
|
|
25
|
+
*/
|
|
13
26
|
id: ElementId,
|
|
27
|
+
/**
|
|
28
|
+
* #property
|
|
29
|
+
*/
|
|
14
30
|
type: types.literal('GridBookmarkWidget'),
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
31
|
+
/**
|
|
32
|
+
* #property
|
|
33
|
+
* removed by postProcessSnapshot, only loaded from localStorage
|
|
34
|
+
*/
|
|
35
|
+
bookmarks: types.optional(types.array(LabeledRegionModel), () => JSON.parse(localStorageGetItem(localStorageKeyF()) || '[]')),
|
|
18
36
|
})
|
|
37
|
+
.volatile(() => ({
|
|
38
|
+
selectedBookmarks: [],
|
|
39
|
+
selectedAssembliesPre: undefined,
|
|
40
|
+
}))
|
|
41
|
+
.views(self => ({
|
|
42
|
+
get bookmarkAssemblies() {
|
|
43
|
+
return [...new Set(self.bookmarks.map(r => r.assemblyName))];
|
|
44
|
+
},
|
|
45
|
+
get validAssemblies() {
|
|
46
|
+
const { assemblyManager } = getSession(self);
|
|
47
|
+
return new Set(this.bookmarkAssemblies.filter(a => assemblyManager.get(a)));
|
|
48
|
+
},
|
|
49
|
+
}))
|
|
50
|
+
.views(self => ({
|
|
51
|
+
get bookmarksWithValidAssemblies() {
|
|
52
|
+
return self.bookmarks.filter(e => self.validAssemblies.has(e.assemblyName));
|
|
53
|
+
},
|
|
54
|
+
}))
|
|
55
|
+
.views(self => ({
|
|
56
|
+
get sharedBookmarksModel() {
|
|
57
|
+
// requires cloning bookmarks with JSON.stringify/parse to avoid duplicate
|
|
58
|
+
// reference to same object in the same state tree, will otherwise error
|
|
59
|
+
// when performing share
|
|
60
|
+
return SharedBookmarksModel.create({
|
|
61
|
+
sharedBookmarks: JSON.parse(JSON.stringify(self.selectedBookmarks)),
|
|
62
|
+
});
|
|
63
|
+
},
|
|
64
|
+
get allBookmarksModel() {
|
|
65
|
+
// requires cloning bookmarks with JSON.stringify/parse to avoid duplicate
|
|
66
|
+
// reference to same object in the same state tree, will otherwise error
|
|
67
|
+
// when performing share
|
|
68
|
+
return SharedBookmarksModel.create({
|
|
69
|
+
sharedBookmarks: JSON.parse(JSON.stringify(self.bookmarksWithValidAssemblies)),
|
|
70
|
+
});
|
|
71
|
+
},
|
|
72
|
+
}))
|
|
73
|
+
.actions(self => ({
|
|
74
|
+
setSelectedAssemblies(assemblies) {
|
|
75
|
+
self.selectedAssembliesPre = assemblies;
|
|
76
|
+
},
|
|
77
|
+
}))
|
|
78
|
+
.views(self => ({
|
|
79
|
+
get selectedAssemblies() {
|
|
80
|
+
var _a, _b;
|
|
81
|
+
return ((_b = (_a = self.selectedAssembliesPre) === null || _a === void 0 ? void 0 : _a.filter(f => self.validAssemblies.has(f))) !== null && _b !== void 0 ? _b : [...self.validAssemblies]);
|
|
82
|
+
},
|
|
83
|
+
}))
|
|
19
84
|
.actions(self => ({
|
|
20
85
|
importBookmarks(regions) {
|
|
21
|
-
self.
|
|
86
|
+
self.bookmarks = cast([...self.bookmarks, ...regions]);
|
|
22
87
|
},
|
|
23
88
|
addBookmark(region) {
|
|
24
|
-
self.
|
|
89
|
+
self.bookmarks.push(region);
|
|
25
90
|
},
|
|
26
91
|
removeBookmark(index) {
|
|
27
|
-
self.
|
|
92
|
+
self.bookmarks.splice(index, 1);
|
|
93
|
+
},
|
|
94
|
+
updateBookmarkLabel(bookmark, label) {
|
|
95
|
+
bookmark.correspondingObj.setLabel(label);
|
|
96
|
+
},
|
|
97
|
+
setSelectedBookmarks(bookmarks) {
|
|
98
|
+
self.selectedBookmarks = bookmarks;
|
|
28
99
|
},
|
|
100
|
+
setBookmarkedRegions(regions) {
|
|
101
|
+
self.bookmarks = cast(regions);
|
|
102
|
+
},
|
|
103
|
+
}))
|
|
104
|
+
.actions(self => ({
|
|
29
105
|
clearAllBookmarks() {
|
|
30
|
-
self.
|
|
106
|
+
for (const bookmark of self.bookmarks) {
|
|
107
|
+
if (self.validAssemblies.has(bookmark.assemblyName)) {
|
|
108
|
+
self.bookmarks.remove(bookmark);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
31
111
|
},
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
112
|
+
clearSelectedBookmarks() {
|
|
113
|
+
for (const bookmark of self.selectedBookmarks) {
|
|
114
|
+
self.bookmarks.remove(bookmark.correspondingObj);
|
|
115
|
+
}
|
|
116
|
+
self.selectedBookmarks = [];
|
|
35
117
|
},
|
|
36
|
-
|
|
37
|
-
|
|
118
|
+
}))
|
|
119
|
+
.actions(self => ({
|
|
120
|
+
afterAttach() {
|
|
121
|
+
const key = localStorageKeyF();
|
|
122
|
+
addDisposer(self, autorun(() => {
|
|
123
|
+
localStorageSetItem(key, JSON.stringify(self.bookmarks));
|
|
124
|
+
}));
|
|
38
125
|
},
|
|
39
126
|
}))
|
|
40
|
-
.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
? self.bookmarkedRegions[0].assemblyName
|
|
45
|
-
: ''));
|
|
46
|
-
},
|
|
47
|
-
get assemblies() {
|
|
48
|
-
return [
|
|
49
|
-
...new Set(self.bookmarkedRegions.map(region => region.assemblyName)),
|
|
50
|
-
];
|
|
51
|
-
},
|
|
52
|
-
}));
|
|
127
|
+
.postProcessSnapshot(snap => {
|
|
128
|
+
const { bookmarks: _, ...rest } = snap;
|
|
129
|
+
return rest;
|
|
130
|
+
});
|
|
53
131
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare function shareSessionToDynamo(session: unknown, url: string, referer: string): Promise<{
|
|
2
|
+
json: any;
|
|
3
|
+
encryptedSession: string;
|
|
4
|
+
password: string;
|
|
5
|
+
}>;
|
|
6
|
+
export declare function readSessionFromDynamo(baseUrl: string, sessionQueryParam: string, password: string, signal?: AbortSignal): Promise<string>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// duplicated from products/jbrowse-web/src/sessionSharing.ts ; could possibly be moved into a higher directory and shared between the two
|
|
2
|
+
import { toUrlSafeB64 } from './utils';
|
|
3
|
+
// from https://stackoverflow.com/questions/1349404/
|
|
4
|
+
function generateUID(length) {
|
|
5
|
+
return window
|
|
6
|
+
.btoa([...window.crypto.getRandomValues(new Uint8Array(length * 2))]
|
|
7
|
+
.map(b => String.fromCharCode(b))
|
|
8
|
+
.join(''))
|
|
9
|
+
.replaceAll(/[+/]/g, '')
|
|
10
|
+
.slice(0, length);
|
|
11
|
+
}
|
|
12
|
+
const encrypt = async (text, password) => {
|
|
13
|
+
const AES = await import('crypto-js/aes');
|
|
14
|
+
return AES.encrypt(text, password).toString();
|
|
15
|
+
};
|
|
16
|
+
const decrypt = async (text, password) => {
|
|
17
|
+
const AES = await import('crypto-js/aes');
|
|
18
|
+
const Utf8 = await import('crypto-js/enc-utf8');
|
|
19
|
+
const bytes = AES.decrypt(text, password);
|
|
20
|
+
return bytes.toString(Utf8);
|
|
21
|
+
};
|
|
22
|
+
function getErrorMsg(err) {
|
|
23
|
+
try {
|
|
24
|
+
const obj = JSON.parse(err);
|
|
25
|
+
return obj.message;
|
|
26
|
+
}
|
|
27
|
+
catch (e) {
|
|
28
|
+
return err;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// writes the encrypted session, current datetime, and referer to DynamoDB
|
|
32
|
+
export async function shareSessionToDynamo(session, url, referer) {
|
|
33
|
+
const sess = await toUrlSafeB64(JSON.stringify(session));
|
|
34
|
+
const password = generateUID(5);
|
|
35
|
+
const encryptedSession = await encrypt(sess, password);
|
|
36
|
+
const data = new FormData();
|
|
37
|
+
data.append('session', encryptedSession);
|
|
38
|
+
data.append('dateShared', `${Date.now()}`);
|
|
39
|
+
data.append('referer', referer);
|
|
40
|
+
const response = await fetch(`${url}share`, {
|
|
41
|
+
method: 'POST',
|
|
42
|
+
mode: 'cors',
|
|
43
|
+
body: data,
|
|
44
|
+
});
|
|
45
|
+
if (!response.ok) {
|
|
46
|
+
const err = await response.text();
|
|
47
|
+
throw new Error(getErrorMsg(err));
|
|
48
|
+
}
|
|
49
|
+
const json = await response.json();
|
|
50
|
+
return {
|
|
51
|
+
json,
|
|
52
|
+
encryptedSession,
|
|
53
|
+
password,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export async function readSessionFromDynamo(baseUrl, sessionQueryParam, password, signal) {
|
|
57
|
+
const sessionId = sessionQueryParam.split('share-')[1];
|
|
58
|
+
const url = `${baseUrl}?sessionId=${encodeURIComponent(sessionId)}`;
|
|
59
|
+
const response = await fetch(url, {
|
|
60
|
+
signal,
|
|
61
|
+
});
|
|
62
|
+
if (!response.ok) {
|
|
63
|
+
const err = await response.text();
|
|
64
|
+
throw new Error(getErrorMsg(err));
|
|
65
|
+
}
|
|
66
|
+
const json = await response.json();
|
|
67
|
+
return decrypt(json.session, password);
|
|
68
|
+
}
|
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
import { AbstractViewModel } from '@jbrowse/core/util/types';
|
|
2
2
|
import { GridBookmarkModel } from './model';
|
|
3
|
-
|
|
4
|
-
export declare function
|
|
5
|
-
|
|
3
|
+
export declare function navToBookmark(locString: string, assembly: string, views: AbstractViewModel[], model: GridBookmarkModel): Promise<void>;
|
|
4
|
+
export declare function downloadBookmarkFile(fileFormat: string, model: GridBookmarkModel): void;
|
|
5
|
+
/**
|
|
6
|
+
* Pad the end of a base64 string with "=" to make it valid
|
|
7
|
+
* @param b64 - unpadded b64 string
|
|
8
|
+
*/
|
|
9
|
+
export declare function b64PadSuffix(b64: string): string;
|
|
10
|
+
/**
|
|
11
|
+
* Decode and inflate a url-safe base64 to a string
|
|
12
|
+
* See {@link https://en.wikipedia.org/wiki/Base64#URL_applications}
|
|
13
|
+
* @param b64 - a base64 string to decode and inflate
|
|
14
|
+
*/
|
|
15
|
+
export declare function fromUrlSafeB64(b64: string): Promise<string>;
|
|
16
|
+
/**
|
|
17
|
+
* Compress and encode a string as url-safe base64
|
|
18
|
+
* See {@link https://en.wikipedia.org/wiki/Base64#URL_applications}
|
|
19
|
+
* @param str- a string to compress and encode
|
|
20
|
+
*/
|
|
21
|
+
export declare function toUrlSafeB64(str: string): Promise<string>;
|
|
@@ -1,56 +1,122 @@
|
|
|
1
1
|
import { saveAs } from 'file-saver';
|
|
2
2
|
import { getSession, assembleLocString } from '@jbrowse/core/util';
|
|
3
|
-
export async function navToBookmark(locString, views, model) {
|
|
3
|
+
export async function navToBookmark(locString, assembly, views, model) {
|
|
4
4
|
const session = getSession(model);
|
|
5
5
|
try {
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
6
|
+
// get the focused view
|
|
7
|
+
let view = views.find(view => view.id === session.focusedViewId);
|
|
8
|
+
// check if the focused view is the appropriate assembly, if not proceed
|
|
9
|
+
if ((view === null || view === void 0 ? void 0 : view.assemblyNames[0]) !== assembly) {
|
|
10
|
+
view = views.find(elt =>
|
|
11
|
+
// @ts-expect-error
|
|
12
|
+
elt.type === 'LinearGenomeView' && elt.assemblyNames[0] === assembly);
|
|
13
|
+
}
|
|
14
|
+
// if no view is opened of the selectedAssembly, open a new
|
|
15
|
+
// view with that assembly
|
|
14
16
|
if (!view) {
|
|
17
|
+
const newViewId = `${model.id}_${assembly}`;
|
|
15
18
|
view = session.addView('LinearGenomeView', {
|
|
16
19
|
id: newViewId,
|
|
17
20
|
});
|
|
18
21
|
}
|
|
19
|
-
await view.navToLocString(locString,
|
|
22
|
+
await view.navToLocString(locString, assembly);
|
|
20
23
|
}
|
|
21
24
|
catch (e) {
|
|
22
25
|
console.error(e);
|
|
23
26
|
session.notify(`${e}`, 'error');
|
|
24
27
|
}
|
|
25
28
|
}
|
|
26
|
-
export function downloadBookmarkFile(
|
|
27
|
-
const {
|
|
28
|
-
const
|
|
29
|
-
?
|
|
30
|
-
:
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
|
|
29
|
+
export function downloadBookmarkFile(fileFormat, model) {
|
|
30
|
+
const { selectedBookmarks, bookmarksWithValidAssemblies } = model;
|
|
31
|
+
const bookmarksToDownload = selectedBookmarks.length === 0
|
|
32
|
+
? bookmarksWithValidAssemblies
|
|
33
|
+
: selectedBookmarks;
|
|
34
|
+
if (fileFormat === 'BED') {
|
|
35
|
+
const fileHeader = '';
|
|
36
|
+
const fileContents = {};
|
|
37
|
+
bookmarksToDownload.forEach(bookmark => {
|
|
38
|
+
const { label } = bookmark;
|
|
39
|
+
const labelVal = label === '' ? '.' : label;
|
|
40
|
+
const line = `${bookmark.refName}\t${bookmark.start}\t${bookmark.end}\t${labelVal}\n`;
|
|
41
|
+
fileContents[bookmark.assemblyName]
|
|
42
|
+
? fileContents[bookmark.assemblyName].push(line)
|
|
43
|
+
: (fileContents[bookmark.assemblyName] = [line]);
|
|
44
|
+
});
|
|
45
|
+
for (const assembly in fileContents) {
|
|
46
|
+
const fileContent = fileContents[assembly].reduce((a, b) => a + b, fileHeader);
|
|
47
|
+
const blob = new Blob([fileContent || ''], {
|
|
48
|
+
type: 'text/x-bed;charset=utf-8',
|
|
49
|
+
});
|
|
50
|
+
const fileName = `jbrowse_bookmarks_${assembly}.bed`;
|
|
51
|
+
saveAs(blob, fileName);
|
|
44
52
|
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
// TSV
|
|
56
|
+
const fileHeader = 'chrom\tstart\tend\tlabel\tassembly_name\tcoord_range\n';
|
|
57
|
+
const fileContents = bookmarksToDownload
|
|
58
|
+
.map(bookmark => {
|
|
59
|
+
const { label } = bookmark;
|
|
60
|
+
const labelVal = label === '' ? '.' : label;
|
|
61
|
+
const locString = assembleLocString(bookmark);
|
|
62
|
+
return `${bookmark.refName}\t${bookmark.start + 1}\t${bookmark.end}\t${labelVal}\t${bookmark.assemblyName}\t${locString}\n`;
|
|
63
|
+
})
|
|
64
|
+
.reduce((a, b) => a + b, fileHeader);
|
|
65
|
+
const blob = new Blob([fileContents || ''], {
|
|
66
|
+
type: 'text/tab-separated-values;charset=utf-8',
|
|
67
|
+
});
|
|
68
|
+
const fileName = 'jbrowse_bookmarks.tsv';
|
|
69
|
+
saveAs(blob, fileName);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Pad the end of a base64 string with "=" to make it valid
|
|
74
|
+
* @param b64 - unpadded b64 string
|
|
75
|
+
*/
|
|
76
|
+
export function b64PadSuffix(b64) {
|
|
77
|
+
let num = 0;
|
|
78
|
+
const mo = b64.length % 4;
|
|
79
|
+
switch (mo) {
|
|
80
|
+
case 3:
|
|
81
|
+
num = 1;
|
|
82
|
+
break;
|
|
83
|
+
case 2:
|
|
84
|
+
num = 2;
|
|
85
|
+
break;
|
|
86
|
+
case 0:
|
|
87
|
+
num = 0;
|
|
88
|
+
break;
|
|
89
|
+
default:
|
|
90
|
+
throw new Error('base64 not a valid length');
|
|
91
|
+
}
|
|
92
|
+
return b64 + '='.repeat(num);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Decode and inflate a url-safe base64 to a string
|
|
96
|
+
* See {@link https://en.wikipedia.org/wiki/Base64#URL_applications}
|
|
97
|
+
* @param b64 - a base64 string to decode and inflate
|
|
98
|
+
*/
|
|
99
|
+
export async function fromUrlSafeB64(b64) {
|
|
100
|
+
const originalB64 = b64PadSuffix(b64.replaceAll('-', '+').replaceAll('_', '/'));
|
|
101
|
+
const { toByteArray } = await import('base64-js');
|
|
102
|
+
const { inflate } = await import('pako');
|
|
103
|
+
const bytes = toByteArray(originalB64);
|
|
104
|
+
const inflated = inflate(bytes);
|
|
105
|
+
return new TextDecoder().decode(inflated);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Compress and encode a string as url-safe base64
|
|
109
|
+
* See {@link https://en.wikipedia.org/wiki/Base64#URL_applications}
|
|
110
|
+
* @param str- a string to compress and encode
|
|
111
|
+
*/
|
|
112
|
+
export async function toUrlSafeB64(str) {
|
|
113
|
+
const bytes = new TextEncoder().encode(str);
|
|
114
|
+
const { deflate } = await import('pako');
|
|
115
|
+
const { fromByteArray } = await import('base64-js');
|
|
116
|
+
const deflated = deflate(bytes);
|
|
117
|
+
const encoded = fromByteArray(deflated);
|
|
118
|
+
const pos = encoded.indexOf('=');
|
|
119
|
+
return pos > 0
|
|
120
|
+
? encoded.slice(0, pos).replaceAll('+', '-').replaceAll('/', '_')
|
|
121
|
+
: encoded.replaceAll('+', '-').replaceAll('/', '_');
|
|
56
122
|
}
|
package/esm/index.d.ts
CHANGED
package/esm/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import Plugin from '@jbrowse/core/Plugin';
|
|
2
|
-
import { getSession, isSessionModelWithWidgets } from '@jbrowse/core/util';
|
|
2
|
+
import { getSession, isAbstractMenuManager, isSessionModelWithWidgets, } from '@jbrowse/core/util';
|
|
3
3
|
// icons
|
|
4
4
|
import BookmarkIcon from '@mui/icons-material/Bookmark';
|
|
5
5
|
import BookmarksIcon from '@mui/icons-material/Bookmarks';
|
|
@@ -14,91 +14,116 @@ export default class extends Plugin {
|
|
|
14
14
|
pluginManager.addToExtensionPoint('Core-extendPluggableElement', (pluggableElement) => {
|
|
15
15
|
if (pluggableElement.name === 'LinearGenomeView') {
|
|
16
16
|
const { stateModel } = pluggableElement;
|
|
17
|
-
const
|
|
17
|
+
const lgv = stateModel;
|
|
18
|
+
const newStateModel = lgv
|
|
19
|
+
.actions(self => ({
|
|
20
|
+
activateBookmarkWidget() {
|
|
21
|
+
const session = getSession(self);
|
|
22
|
+
if (isSessionModelWithWidgets(session)) {
|
|
23
|
+
let bookmarkWidget = session.widgets.get('GridBookmark');
|
|
24
|
+
if (!bookmarkWidget) {
|
|
25
|
+
bookmarkWidget = session.addWidget('GridBookmarkWidget', 'GridBookmark');
|
|
26
|
+
}
|
|
27
|
+
session.showWidget(bookmarkWidget);
|
|
28
|
+
return session.widgets.get('GridBookmark');
|
|
29
|
+
}
|
|
30
|
+
throw new Error('Could not open bookmark widget');
|
|
31
|
+
},
|
|
32
|
+
}))
|
|
33
|
+
.actions(self => ({
|
|
34
|
+
navigateNewestBookmark() {
|
|
35
|
+
const session = getSession(self);
|
|
36
|
+
const bookmarkWidget = self.activateBookmarkWidget();
|
|
37
|
+
const regions = bookmarkWidget.bookmarks;
|
|
38
|
+
if (regions === null || regions === void 0 ? void 0 : regions.length) {
|
|
39
|
+
self.navTo(regions.at(-1));
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
session.notify('There are no recent bookmarks to navigate to.', 'info');
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
bookmarkCurrentRegion() {
|
|
46
|
+
if (self.id === getSession(self).focusedViewId) {
|
|
47
|
+
const selectedRegions = self.getSelectedRegions(undefined, undefined);
|
|
48
|
+
const bookmarkWidget = self.activateBookmarkWidget();
|
|
49
|
+
bookmarkWidget.addBookmark(selectedRegions[0]);
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
}))
|
|
53
|
+
.views(self => {
|
|
18
54
|
const superMenuItems = self.menuItems;
|
|
19
55
|
const superRubberBandMenuItems = self.rubberBandMenuItems;
|
|
20
56
|
return {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const firstRegion = selectedRegions[0];
|
|
37
|
-
const session = getSession(self);
|
|
38
|
-
if (isSessionModelWithWidgets(session)) {
|
|
39
|
-
const { widgets } = session;
|
|
40
|
-
let bookmarkWidget = widgets.get('GridBookmark');
|
|
41
|
-
if (!bookmarkWidget) {
|
|
42
|
-
this.activateBookmarkWidget();
|
|
43
|
-
bookmarkWidget = widgets.get('GridBookmark');
|
|
44
|
-
}
|
|
45
|
-
// @ts-expect-error
|
|
46
|
-
bookmarkWidget.addBookmark(firstRegion);
|
|
47
|
-
}
|
|
48
|
-
},
|
|
57
|
+
menuItems() {
|
|
58
|
+
return [
|
|
59
|
+
...superMenuItems(),
|
|
60
|
+
{ type: 'divider' },
|
|
61
|
+
{
|
|
62
|
+
label: 'Open bookmark widget',
|
|
63
|
+
icon: BookmarksIcon,
|
|
64
|
+
onClick: () => self.activateBookmarkWidget(),
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
label: 'Bookmark current region',
|
|
68
|
+
icon: BookmarkIcon,
|
|
69
|
+
onClick: () => self.bookmarkCurrentRegion(),
|
|
70
|
+
},
|
|
71
|
+
];
|
|
49
72
|
},
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
73
|
+
rubberBandMenuItems() {
|
|
74
|
+
return [
|
|
75
|
+
...superRubberBandMenuItems(),
|
|
76
|
+
{
|
|
77
|
+
label: 'Bookmark region',
|
|
78
|
+
icon: BookmarkIcon,
|
|
79
|
+
onClick: () => {
|
|
80
|
+
const { leftOffset, rightOffset } = self;
|
|
81
|
+
const selectedRegions = self.getSelectedRegions(leftOffset, rightOffset);
|
|
82
|
+
const bookmarkWidget = self.activateBookmarkWidget();
|
|
83
|
+
bookmarkWidget.addBookmark(selectedRegions[0]);
|
|
60
84
|
},
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
icon: BookmarkIcon,
|
|
64
|
-
// @ts-expect-error
|
|
65
|
-
onClick: self.bookmarkCurrentRegion,
|
|
66
|
-
},
|
|
67
|
-
];
|
|
68
|
-
},
|
|
69
|
-
rubberBandMenuItems() {
|
|
70
|
-
return [
|
|
71
|
-
...superRubberBandMenuItems(),
|
|
72
|
-
{
|
|
73
|
-
label: 'Bookmark region',
|
|
74
|
-
icon: BookmarkIcon,
|
|
75
|
-
onClick: () => {
|
|
76
|
-
const { leftOffset, rightOffset } = self;
|
|
77
|
-
const selectedRegions = self.getSelectedRegions(leftOffset, rightOffset);
|
|
78
|
-
const firstRegion = selectedRegions[0];
|
|
79
|
-
const session = getSession(self);
|
|
80
|
-
if (isSessionModelWithWidgets(session)) {
|
|
81
|
-
const { widgets } = session;
|
|
82
|
-
let bookmarkWidget = widgets.get('GridBookmark');
|
|
83
|
-
if (!bookmarkWidget) {
|
|
84
|
-
// @ts-expect-error
|
|
85
|
-
self.activateBookmarkWidget();
|
|
86
|
-
bookmarkWidget = widgets.get('GridBookmark');
|
|
87
|
-
}
|
|
88
|
-
// @ts-expect-error
|
|
89
|
-
bookmarkWidget.addBookmark(firstRegion);
|
|
90
|
-
}
|
|
91
|
-
},
|
|
92
|
-
},
|
|
93
|
-
];
|
|
94
|
-
},
|
|
85
|
+
},
|
|
86
|
+
];
|
|
95
87
|
},
|
|
96
88
|
};
|
|
97
|
-
})
|
|
89
|
+
})
|
|
90
|
+
.actions(self => ({
|
|
91
|
+
afterCreate() {
|
|
92
|
+
document.addEventListener('keydown', e => {
|
|
93
|
+
const activationSequence = (e.ctrlKey || e.metaKey) && e.shiftKey;
|
|
94
|
+
// ctrl+shift+d or cmd+shift+d
|
|
95
|
+
if (activationSequence && e.code === 'KeyD') {
|
|
96
|
+
e.preventDefault();
|
|
97
|
+
self.activateBookmarkWidget();
|
|
98
|
+
self.bookmarkCurrentRegion();
|
|
99
|
+
getSession(self).notify('Bookmark created.', 'success');
|
|
100
|
+
}
|
|
101
|
+
// ctrl+shift+m or cmd+shift+m
|
|
102
|
+
if (activationSequence && e.code === 'KeyM') {
|
|
103
|
+
e.preventDefault();
|
|
104
|
+
self.navigateNewestBookmark();
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
},
|
|
108
|
+
}));
|
|
98
109
|
pluggableElement.stateModel = newStateModel;
|
|
99
110
|
}
|
|
100
111
|
return pluggableElement;
|
|
101
112
|
});
|
|
102
113
|
}
|
|
103
|
-
configure(
|
|
114
|
+
configure(pluginManager) {
|
|
115
|
+
if (isAbstractMenuManager(pluginManager.rootModel)) {
|
|
116
|
+
pluginManager.rootModel.appendToMenu('Tools', {
|
|
117
|
+
label: 'Bookmarks',
|
|
118
|
+
icon: BookmarksIcon,
|
|
119
|
+
onClick: (session) => {
|
|
120
|
+
let bookmarkWidget = session.widgets.get('GridBookmark');
|
|
121
|
+
if (!bookmarkWidget) {
|
|
122
|
+
bookmarkWidget = session.addWidget('GridBookmarkWidget', 'GridBookmark');
|
|
123
|
+
}
|
|
124
|
+
session.showWidget(bookmarkWidget);
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
104
129
|
}
|