@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.
Files changed (71) hide show
  1. package/dist/GridBookmarkWidget/components/AssemblySelector.d.ts +3 -4
  2. package/dist/GridBookmarkWidget/components/AssemblySelector.js +27 -37
  3. package/dist/GridBookmarkWidget/components/BookmarkGrid.d.ts +6 -0
  4. package/dist/GridBookmarkWidget/components/BookmarkGrid.js +120 -0
  5. package/dist/GridBookmarkWidget/components/{ClearBookmarks.d.ts → DeleteBookmarks.d.ts} +2 -3
  6. package/dist/GridBookmarkWidget/components/DeleteBookmarks.js +41 -0
  7. package/dist/GridBookmarkWidget/components/DeleteBookmarksDialog.d.ts +7 -0
  8. package/dist/GridBookmarkWidget/components/DeleteBookmarksDialog.js +29 -0
  9. package/dist/GridBookmarkWidget/components/EditBookmarkLabelDialog.d.ts +8 -0
  10. package/dist/GridBookmarkWidget/components/EditBookmarkLabelDialog.js +50 -0
  11. package/dist/GridBookmarkWidget/components/ExportBookmarks.d.ts +6 -0
  12. package/dist/GridBookmarkWidget/components/{ClearBookmarks.js → ExportBookmarks.js} +9 -19
  13. package/dist/GridBookmarkWidget/components/ExportBookmarksDialog.d.ts +7 -0
  14. package/dist/GridBookmarkWidget/components/{DownloadBookmarks.js → ExportBookmarksDialog.js} +22 -20
  15. package/dist/GridBookmarkWidget/components/GridBookmarkWidget.d.ts +3 -4
  16. package/dist/GridBookmarkWidget/components/GridBookmarkWidget.js +28 -97
  17. package/dist/GridBookmarkWidget/components/ImportBookmarks.d.ts +3 -5
  18. package/dist/GridBookmarkWidget/components/ImportBookmarks.js +8 -64
  19. package/dist/GridBookmarkWidget/components/ImportBookmarksDialog.d.ts +7 -0
  20. package/dist/GridBookmarkWidget/components/ImportBookmarksDialog.js +129 -0
  21. package/{esm/GridBookmarkWidget/components/ClearBookmarks.d.ts → dist/GridBookmarkWidget/components/ShareBookmarks.d.ts} +2 -3
  22. package/dist/GridBookmarkWidget/components/ShareBookmarks.js +37 -0
  23. package/dist/GridBookmarkWidget/components/ShareBookmarksDialog.d.ts +7 -0
  24. package/dist/GridBookmarkWidget/components/ShareBookmarksDialog.js +106 -0
  25. package/dist/GridBookmarkWidget/model.d.ts +178 -11
  26. package/dist/GridBookmarkWidget/model.js +105 -27
  27. package/dist/GridBookmarkWidget/sessionSharing.d.ts +6 -0
  28. package/dist/GridBookmarkWidget/sessionSharing.js +96 -0
  29. package/dist/GridBookmarkWidget/utils.d.ts +19 -3
  30. package/dist/GridBookmarkWidget/utils.js +132 -40
  31. package/dist/index.d.ts +1 -1
  32. package/dist/index.js +100 -75
  33. package/esm/GridBookmarkWidget/components/AssemblySelector.d.ts +3 -4
  34. package/esm/GridBookmarkWidget/components/AssemblySelector.js +28 -38
  35. package/esm/GridBookmarkWidget/components/BookmarkGrid.d.ts +6 -0
  36. package/esm/GridBookmarkWidget/components/BookmarkGrid.js +92 -0
  37. package/{dist/GridBookmarkWidget/components/DownloadBookmarks.d.ts → esm/GridBookmarkWidget/components/DeleteBookmarks.d.ts} +2 -3
  38. package/esm/GridBookmarkWidget/components/DeleteBookmarks.js +13 -0
  39. package/esm/GridBookmarkWidget/components/DeleteBookmarksDialog.d.ts +7 -0
  40. package/esm/GridBookmarkWidget/components/DeleteBookmarksDialog.js +24 -0
  41. package/esm/GridBookmarkWidget/components/EditBookmarkLabelDialog.d.ts +8 -0
  42. package/esm/GridBookmarkWidget/components/EditBookmarkLabelDialog.js +25 -0
  43. package/esm/GridBookmarkWidget/components/ExportBookmarks.d.ts +6 -0
  44. package/esm/GridBookmarkWidget/components/ExportBookmarks.js +13 -0
  45. package/esm/GridBookmarkWidget/components/ExportBookmarksDialog.d.ts +7 -0
  46. package/esm/GridBookmarkWidget/components/ExportBookmarksDialog.js +35 -0
  47. package/esm/GridBookmarkWidget/components/GridBookmarkWidget.d.ts +3 -4
  48. package/esm/GridBookmarkWidget/components/GridBookmarkWidget.js +28 -74
  49. package/esm/GridBookmarkWidget/components/ImportBookmarks.d.ts +3 -5
  50. package/esm/GridBookmarkWidget/components/ImportBookmarks.js +10 -66
  51. package/esm/GridBookmarkWidget/components/ImportBookmarksDialog.d.ts +7 -0
  52. package/esm/GridBookmarkWidget/components/ImportBookmarksDialog.js +101 -0
  53. package/esm/GridBookmarkWidget/components/{DownloadBookmarks.d.ts → ShareBookmarks.d.ts} +2 -3
  54. package/esm/GridBookmarkWidget/components/ShareBookmarks.js +12 -0
  55. package/esm/GridBookmarkWidget/components/ShareBookmarksDialog.d.ts +7 -0
  56. package/esm/GridBookmarkWidget/components/ShareBookmarksDialog.js +78 -0
  57. package/esm/GridBookmarkWidget/model.d.ts +178 -11
  58. package/esm/GridBookmarkWidget/model.js +106 -28
  59. package/esm/GridBookmarkWidget/sessionSharing.d.ts +6 -0
  60. package/esm/GridBookmarkWidget/sessionSharing.js +68 -0
  61. package/esm/GridBookmarkWidget/utils.d.ts +19 -3
  62. package/esm/GridBookmarkWidget/utils.js +105 -39
  63. package/esm/index.d.ts +1 -1
  64. package/esm/index.js +101 -76
  65. package/package.json +4 -3
  66. package/dist/GridBookmarkWidget/components/DeleteBookmark.d.ts +0 -9
  67. package/dist/GridBookmarkWidget/components/DeleteBookmark.js +0 -31
  68. package/esm/GridBookmarkWidget/components/ClearBookmarks.js +0 -23
  69. package/esm/GridBookmarkWidget/components/DeleteBookmark.d.ts +0 -9
  70. package/esm/GridBookmarkWidget/components/DeleteBookmark.js +0 -26
  71. 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', { label: types.optional(types.string, '') }))
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
- export default function f(pluginManager) {
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
- view: types.safeReference(pluginManager.pluggableMstType('view', 'stateModel')),
16
- bookmarkedRegions: types.array(LabeledRegionModel),
17
- modelSelectedAssembly: '',
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.bookmarkedRegions = cast([...self.bookmarkedRegions, ...regions]);
86
+ self.bookmarks = cast([...self.bookmarks, ...regions]);
22
87
  },
23
88
  addBookmark(region) {
24
- self.bookmarkedRegions.push(region);
89
+ self.bookmarks.push(region);
25
90
  },
26
91
  removeBookmark(index) {
27
- self.bookmarkedRegions.splice(index, 1);
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.bookmarkedRegions.clear();
106
+ for (const bookmark of self.bookmarks) {
107
+ if (self.validAssemblies.has(bookmark.assemblyName)) {
108
+ self.bookmarks.remove(bookmark);
109
+ }
110
+ }
31
111
  },
32
- updateBookmarkLabel(index, label) {
33
- var _a;
34
- (_a = self.bookmarkedRegions[index]) === null || _a === void 0 ? void 0 : _a.setLabel(label);
112
+ clearSelectedBookmarks() {
113
+ for (const bookmark of self.selectedBookmarks) {
114
+ self.bookmarks.remove(bookmark.correspondingObj);
115
+ }
116
+ self.selectedBookmarks = [];
35
117
  },
36
- setSelectedAssembly(assembly) {
37
- self.modelSelectedAssembly = assembly;
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
- .views(self => ({
41
- get selectedAssembly() {
42
- return (self.modelSelectedAssembly ||
43
- (self.bookmarkedRegions.length
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
- import { LabeledRegion } from './types';
4
- export declare function navToBookmark(locString: string, views: AbstractViewModel[], model: GridBookmarkModel): Promise<void>;
5
- export declare function downloadBookmarkFile(bookmarkedRegions: LabeledRegion[], fileFormat: string, model: GridBookmarkModel): void;
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
- // search for exact match to an lgv that this bookmark widget launched, or
7
- // any lgv that looks like it is relevant to what we are browsing
8
- const { selectedAssembly } = model;
9
- const newViewId = `${model.id}_${selectedAssembly}`;
10
- let view = (views.find(v => v.type === 'LinearGenomeView' && v.id === newViewId) ||
11
- views.find(v => v.type === 'LinearGenomeView' &&
12
- // @ts-expect-error
13
- v.assemblyNames[0] === selectedAssembly));
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, selectedAssembly);
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(bookmarkedRegions, fileFormat, model) {
27
- const { selectedAssembly } = model;
28
- const fileHeader = fileFormat === 'TSV'
29
- ? 'chrom\tstart\tend\tlabel\tassembly_name\tcoord_range\n'
30
- : '';
31
- const fileContents = bookmarkedRegions
32
- .map(b => {
33
- const { label } = b;
34
- const labelVal = label === '' ? '.' : label;
35
- const locString = assembleLocString(b);
36
- if (fileFormat === 'BED') {
37
- if (b.assemblyName === selectedAssembly || selectedAssembly === 'all') {
38
- return `${b.refName}\t${b.start}\t${b.end}\t${labelVal}\n`;
39
- }
40
- return '';
41
- }
42
- else {
43
- return `${b.refName}\t${b.start + 1}\t${b.end}\t${labelVal}\t${b.assemblyName}\t${locString}\n`;
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
- .reduce((a, b) => a + b, fileHeader);
47
- const blob = new Blob([fileContents || ''], {
48
- type: fileFormat === 'BED'
49
- ? 'text/x-bed;charset=utf-8'
50
- : 'text/tab-separated-values;charset=utf-8',
51
- });
52
- const fileName = fileFormat === 'BED'
53
- ? `jbrowse_bookmarks_${selectedAssembly}.bed`
54
- : 'jbrowse_bookmarks.tsv';
55
- saveAs(blob, fileName);
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
@@ -3,5 +3,5 @@ import PluginManager from '@jbrowse/core/PluginManager';
3
3
  export default class extends Plugin {
4
4
  name: string;
5
5
  install(pluginManager: PluginManager): void;
6
- configure(_pluginManager: PluginManager): void;
6
+ configure(pluginManager: PluginManager): void;
7
7
  }
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 newStateModel = stateModel.extend((self) => {
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
- actions: {
22
- activateBookmarkWidget() {
23
- const session = getSession(self);
24
- if (isSessionModelWithWidgets(session)) {
25
- let bookmarkWidget = session.widgets.get('GridBookmark');
26
- if (!bookmarkWidget) {
27
- bookmarkWidget = session.addWidget('GridBookmarkWidget', 'GridBookmark', { view: self });
28
- }
29
- session.showWidget(bookmarkWidget);
30
- return bookmarkWidget;
31
- }
32
- throw new Error('Could not open bookmark widget');
33
- },
34
- bookmarkCurrentRegion() {
35
- const selectedRegions = self.getSelectedRegions(self.leftOffset, self.rightOffset);
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
- views: {
51
- menuItems() {
52
- return [
53
- ...superMenuItems(),
54
- { type: 'divider' },
55
- {
56
- label: 'Open bookmark widget',
57
- icon: BookmarksIcon,
58
- // @ts-expect-error
59
- onClick: self.activateBookmarkWidget,
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
- label: 'Bookmark current region',
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(_pluginManager) { }
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
  }