@apollo-annotation/jbrowse-plugin-apollo 0.3.6 → 0.3.8

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 (84) hide show
  1. package/dist/index.esm.js +4603 -2045
  2. package/dist/index.esm.js.map +1 -1
  3. package/dist/jbrowse-plugin-apollo.cjs.development.js +4611 -2039
  4. package/dist/jbrowse-plugin-apollo.cjs.development.js.map +1 -1
  5. package/dist/jbrowse-plugin-apollo.cjs.production.min.js +1 -1
  6. package/dist/jbrowse-plugin-apollo.cjs.production.min.js.map +1 -1
  7. package/dist/jbrowse-plugin-apollo.umd.development.js +9387 -4016
  8. package/dist/jbrowse-plugin-apollo.umd.development.js.map +1 -1
  9. package/dist/jbrowse-plugin-apollo.umd.production.min.js +1 -1
  10. package/dist/jbrowse-plugin-apollo.umd.production.min.js.map +1 -1
  11. package/package.json +15 -15
  12. package/src/ApolloInternetAccount/model.ts +48 -13
  13. package/src/BackendDrivers/CollaborationServerDriver.ts +23 -2
  14. package/src/ChangeManager.ts +42 -18
  15. package/src/FeatureDetailsWidget/ApolloTranscriptDetailsWidget.tsx +64 -5
  16. package/src/FeatureDetailsWidget/Attributes.tsx +8 -3
  17. package/src/FeatureDetailsWidget/TranscriptSequence.tsx +70 -81
  18. package/src/FeatureDetailsWidget/TranscriptWidgetEditLocation.tsx +946 -190
  19. package/src/FeatureDetailsWidget/TranscriptWidgetSummary.tsx +4 -0
  20. package/src/LinearApolloDisplay/components/LinearApolloDisplay.tsx +61 -73
  21. package/src/LinearApolloDisplay/glyphs/BoxGlyph.ts +55 -211
  22. package/src/LinearApolloDisplay/glyphs/GeneGlyph.ts +562 -108
  23. package/src/LinearApolloDisplay/glyphs/GenericChildGlyph.ts +78 -14
  24. package/src/LinearApolloDisplay/glyphs/Glyph.ts +15 -9
  25. package/src/LinearApolloDisplay/stateModel/base.ts +63 -43
  26. package/src/LinearApolloDisplay/stateModel/layouts.ts +3 -2
  27. package/src/LinearApolloDisplay/stateModel/mouseEvents.ts +79 -292
  28. package/src/LinearApolloDisplay/stateModel/rendering.ts +45 -344
  29. package/src/LinearApolloReferenceSequenceDisplay/components/LinearApolloReferenceSequenceDisplay.tsx +87 -0
  30. package/src/LinearApolloReferenceSequenceDisplay/components/index.ts +1 -0
  31. package/src/LinearApolloReferenceSequenceDisplay/configSchema.ts +7 -0
  32. package/src/LinearApolloReferenceSequenceDisplay/index.ts +3 -0
  33. package/src/LinearApolloReferenceSequenceDisplay/stateModel/base.ts +227 -0
  34. package/src/LinearApolloReferenceSequenceDisplay/stateModel/index.ts +25 -0
  35. package/src/LinearApolloReferenceSequenceDisplay/stateModel/rendering.ts +481 -0
  36. package/src/LinearApolloSixFrameDisplay/components/LinearApolloSixFrameDisplay.tsx +102 -40
  37. package/src/LinearApolloSixFrameDisplay/components/TrackLines.tsx +12 -20
  38. package/src/LinearApolloSixFrameDisplay/glyphs/GeneGlyph.ts +382 -243
  39. package/src/LinearApolloSixFrameDisplay/glyphs/Glyph.ts +12 -8
  40. package/src/LinearApolloSixFrameDisplay/stateModel/base.ts +83 -4
  41. package/src/LinearApolloSixFrameDisplay/stateModel/layouts.ts +23 -11
  42. package/src/LinearApolloSixFrameDisplay/stateModel/mouseEvents.ts +118 -123
  43. package/src/LinearApolloSixFrameDisplay/stateModel/rendering.ts +53 -63
  44. package/src/OntologyManager/index.ts +4 -1
  45. package/src/TabularEditor/HybridGrid/Feature.tsx +20 -14
  46. package/src/TabularEditor/HybridGrid/HybridGrid.tsx +7 -5
  47. package/src/TabularEditor/HybridGrid/featureContextMenuItems.ts +108 -16
  48. package/src/components/AddAssembly.tsx +1 -1
  49. package/src/components/AddAssemblyAliases.tsx +114 -0
  50. package/src/components/AddChildFeature.tsx +7 -7
  51. package/src/components/AddFeature.tsx +20 -15
  52. package/src/components/AddRefSeqAliases.tsx +9 -9
  53. package/src/components/CopyFeature.tsx +4 -4
  54. package/src/components/CreateApolloAnnotation.tsx +335 -151
  55. package/src/components/DeleteAssembly.tsx +1 -1
  56. package/src/components/DeleteFeature.tsx +358 -11
  57. package/src/components/DownloadGFF3.tsx +20 -1
  58. package/src/components/EditZoomThresholdDialog.tsx +69 -0
  59. package/src/components/FilterFeatures.tsx +7 -7
  60. package/src/components/FilterTranscripts.tsx +86 -0
  61. package/src/components/ImportFeatures.tsx +1 -1
  62. package/src/components/ManageChecks.tsx +1 -1
  63. package/src/components/MergeExons.tsx +193 -0
  64. package/src/components/MergeTranscripts.tsx +182 -0
  65. package/src/components/OntologyTermMultiSelect.tsx +11 -11
  66. package/src/components/OpenLocalFile.tsx +11 -7
  67. package/src/components/SplitExon.tsx +134 -0
  68. package/src/components/ViewCheckResults.tsx +1 -1
  69. package/src/components/index.ts +4 -0
  70. package/src/config.ts +11 -0
  71. package/src/extensions/annotationFromJBrowseFeature.ts +2 -0
  72. package/src/extensions/annotationFromPileup.ts +99 -89
  73. package/src/index.ts +42 -105
  74. package/src/makeDisplayComponent.tsx +0 -1
  75. package/src/menus/index.ts +1 -0
  76. package/src/{ApolloInternetAccount/addMenuItems.ts → menus/topLevelMenu.ts} +60 -33
  77. package/src/menus/topLevelMenuAdmin.ts +154 -0
  78. package/src/session/session.ts +163 -104
  79. package/src/util/annotationFeatureUtils.ts +59 -0
  80. package/src/util/copyToClipboard.ts +21 -0
  81. package/src/util/displayUtils.ts +149 -0
  82. package/src/util/glyphUtils.ts +201 -0
  83. package/src/util/index.ts +2 -0
  84. package/src/util/mouseEventsUtils.ts +145 -0
@@ -0,0 +1,154 @@
1
+ import {
2
+ type AbstractMenuManager,
3
+ type AbstractSessionModel,
4
+ } from '@jbrowse/core/util'
5
+ import AddIcon from '@mui/icons-material/Add'
6
+ import AdminPanelSettingsIcon from '@mui/icons-material/AdminPanelSettings'
7
+ import DeleteIcon from '@mui/icons-material/Delete'
8
+ import InputIcon from '@mui/icons-material/Input'
9
+ import PersonIcon from '@mui/icons-material/Person'
10
+ import RuleIcon from '@mui/icons-material/Rule'
11
+
12
+ import {
13
+ AddAssembly,
14
+ AddAssemblyAliases,
15
+ AddRefSeqAliases,
16
+ DeleteAssembly,
17
+ ImportFeatures,
18
+ ManageChecks,
19
+ ManageUsers,
20
+ } from '../components'
21
+ import { type ApolloSessionModel } from '../session'
22
+
23
+ export function addTopLevelAdminMenus(rootModel: AbstractMenuManager) {
24
+ rootModel.appendToMenu('Apollo', {
25
+ label: 'Admin',
26
+ type: 'subMenu',
27
+ icon: AdminPanelSettingsIcon,
28
+ subMenu: [
29
+ {
30
+ label: 'Add Assembly',
31
+ icon: AddIcon,
32
+ onClick: (session: ApolloSessionModel) => {
33
+ ;(session as unknown as AbstractSessionModel).queueDialog(
34
+ (doneCallback) => [
35
+ AddAssembly,
36
+ {
37
+ session,
38
+ handleClose: () => {
39
+ doneCallback()
40
+ },
41
+ changeManager: session.apolloDataStore.changeManager,
42
+ },
43
+ ],
44
+ )
45
+ },
46
+ },
47
+ {
48
+ label: 'Delete Assembly',
49
+ icon: DeleteIcon,
50
+ onClick: (session: ApolloSessionModel) => {
51
+ ;(session as unknown as AbstractSessionModel).queueDialog(
52
+ (doneCallback) => [
53
+ DeleteAssembly,
54
+ {
55
+ session,
56
+ handleClose: () => {
57
+ doneCallback()
58
+ },
59
+ changeManager: session.apolloDataStore.changeManager,
60
+ },
61
+ ],
62
+ )
63
+ },
64
+ },
65
+ {
66
+ label: 'Import Features',
67
+ icon: InputIcon,
68
+ onClick: (session: ApolloSessionModel) => {
69
+ ;(session as unknown as AbstractSessionModel).queueDialog(
70
+ (doneCallback) => [
71
+ ImportFeatures,
72
+ {
73
+ session,
74
+ handleClose: () => {
75
+ doneCallback()
76
+ },
77
+ changeManager: session.apolloDataStore.changeManager,
78
+ },
79
+ ],
80
+ )
81
+ },
82
+ },
83
+ {
84
+ label: 'Add reference sequence aliases',
85
+ onClick: (session: ApolloSessionModel) => {
86
+ ;(session as unknown as AbstractSessionModel).queueDialog(
87
+ (doneCallback) => [
88
+ AddRefSeqAliases,
89
+ {
90
+ session,
91
+ handleClose: () => {
92
+ doneCallback()
93
+ },
94
+ changeManager: session.apolloDataStore.changeManager,
95
+ },
96
+ ],
97
+ )
98
+ },
99
+ },
100
+ {
101
+ label: 'Add Assembly aliases',
102
+ onClick: (session: ApolloSessionModel) => {
103
+ ;(session as unknown as AbstractSessionModel).queueDialog(
104
+ (doneCallback) => [
105
+ AddAssemblyAliases,
106
+ {
107
+ session,
108
+ handleClose: () => {
109
+ doneCallback()
110
+ },
111
+ changeManager: session.apolloDataStore.changeManager,
112
+ },
113
+ ],
114
+ )
115
+ },
116
+ },
117
+ {
118
+ label: 'Manage Users',
119
+ icon: PersonIcon,
120
+ onClick: (session: ApolloSessionModel) => {
121
+ ;(session as unknown as AbstractSessionModel).queueDialog(
122
+ (doneCallback) => [
123
+ ManageUsers,
124
+ {
125
+ session,
126
+ handleClose: () => {
127
+ doneCallback()
128
+ },
129
+ changeManager: session.apolloDataStore.changeManager,
130
+ },
131
+ ],
132
+ )
133
+ },
134
+ },
135
+ {
136
+ label: 'Manage Checks',
137
+ icon: RuleIcon,
138
+ onClick: (session: ApolloSessionModel) => {
139
+ ;(session as unknown as AbstractSessionModel).queueDialog(
140
+ (doneCallback) => [
141
+ ManageChecks,
142
+ {
143
+ session,
144
+ handleClose: () => {
145
+ doneCallback()
146
+ },
147
+ },
148
+ ],
149
+ )
150
+ },
151
+ },
152
+ ],
153
+ })
154
+ }
@@ -26,8 +26,8 @@ import { autorun, observable } from 'mobx'
26
26
  import {
27
27
  type Instance,
28
28
  type SnapshotOut,
29
+ addDisposer,
29
30
  applySnapshot,
30
- flow,
31
31
  getRoot,
32
32
  getSnapshot,
33
33
  types,
@@ -36,6 +36,7 @@ import {
36
36
  import { type ApolloInternetAccountModel } from '../ApolloInternetAccount/model'
37
37
  import { ApolloJobModel } from '../ApolloJobModel'
38
38
  import { type ChangeManager } from '../ChangeManager'
39
+ import type ApolloPluginConfigurationSchema from '../config'
39
40
  import { type ApolloRootModel } from '../types'
40
41
  import { createFetchErrorMessage } from '../util'
41
42
 
@@ -53,12 +54,15 @@ export interface Collaborator {
53
54
  locations: UserLocation[]
54
55
  }
55
56
 
57
+ export interface HoveredFeature {
58
+ feature: AnnotationFeature
59
+ bp: number
60
+ }
61
+
56
62
  export function extendSession(
57
63
  pluginManager: PluginManager,
58
64
  sessionModel: ReturnType<typeof types.model>,
59
65
  ) {
60
- const aborter = new AbortController()
61
- const { signal } = aborter
62
66
  const AnnotationFeatureExtended = pluginManager.evaluateExtensionPoint(
63
67
  'Apollo-extendAnnotationFeature',
64
68
  AnnotationFeatureModel,
@@ -69,7 +73,12 @@ export function extendSession(
69
73
  apolloDataStore: types.optional(ClientDataStore, { typeName: 'Client' }),
70
74
  apolloSelectedFeature: types.safeReference(AnnotationFeatureExtended),
71
75
  jobsManager: types.optional(ApolloJobModel, {}),
76
+ isLocked: types.optional(types.boolean, false),
72
77
  })
78
+ .volatile(() => ({
79
+ apolloHoveredFeature: undefined as HoveredFeature | undefined,
80
+ abortController: new AbortController(),
81
+ }))
73
82
  .extend(() => {
74
83
  const collabs = observable.array<Collaborator>([])
75
84
 
@@ -94,10 +103,13 @@ export function extendSession(
94
103
  }
95
104
  })
96
105
  .actions((self) => ({
97
- apolloSetSelectedFeature(feature?: AnnotationFeature) {
106
+ apolloSetSelectedFeature(feature?: AnnotationFeature | string) {
98
107
  // @ts-expect-error Not sure why TS thinks these MST types don't match
99
108
  self.apolloSelectedFeature = feature
100
109
  },
110
+ apolloSetHoveredFeature(feature?: HoveredFeature) {
111
+ self.apolloHoveredFeature = feature
112
+ },
101
113
  addApolloTrackConfig(assembly: AssemblyModel, baseURL?: string) {
102
114
  const trackId = `apollo_track_${assembly.name}`
103
115
  const hasTrack = (self as unknown as AbstractSessionModel).tracks.some(
@@ -126,6 +138,18 @@ export function extendSession(
126
138
  })
127
139
  }
128
140
  },
141
+ toggleLocked() {
142
+ self.isLocked = !self.isLocked
143
+ },
144
+ getPluginConfiguration() {
145
+ const { jbrowse } = getRoot<ApolloRootModel>(self)
146
+ const pluginConfiguration =
147
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
148
+ jbrowse.configuration.ApolloPlugin as Instance<
149
+ typeof ApolloPluginConfigurationSchema
150
+ >
151
+ return pluginConfiguration
152
+ },
129
153
  broadcastLocations() {
130
154
  const { internetAccounts } = getRoot<ApolloRootModel>(self)
131
155
  const locations: {
@@ -183,120 +207,155 @@ export function extendSession(
183
207
  }
184
208
  },
185
209
  }))
210
+ .volatile((self) => ({
211
+ previousSnapshot: getSnapshot(self),
212
+ }))
186
213
  .actions((self) => ({
187
- afterCreate: flow(function* afterCreate() {
188
- // When the initial config.json loads, it doesn't include the Apollo
189
- // tracks, which would result in a potentially invalid session snapshot
190
- // if any tracks are open. Here we copy the session snapshot, apply an
191
- // empty session snapshot, and then restore the original session
192
- // snapshot after the updated config.json loads.
193
- const sessionSnapshot = getSnapshot(self)
194
- const { id, name } = sessionSnapshot
195
- applySnapshot(self, { name, id })
196
- const { internetAccounts, jbrowse } = getRoot<ApolloRootModel>(self)
197
- autorun(
198
- () => {
199
- // broadcastLocations() // **** This is not working and therefore we need to duplicate broadcastLocations() -method code here because autorun() does not observe changes otherwise
200
- const locations: {
201
- assemblyName: string
202
- refName: string
203
- start: number
204
- end: number
205
- }[] = []
206
- for (const view of (self as unknown as AbstractSessionModel)
207
- .views) {
208
- if (view.type !== 'LinearGenomeView') {
209
- return
210
- }
211
- const lgv = view as unknown as LinearGenomeViewModel
212
- if (lgv.initialized) {
213
- const { dynamicBlocks } = lgv
214
- // eslint-disable-next-line unicorn/no-array-for-each
215
- dynamicBlocks.forEach((block) => {
216
- if (block.regionNumber !== undefined) {
217
- const { assemblyName, end, refName, start } = block
218
- const assembly =
219
- self.apolloDataStore.assemblies.get(assemblyName)
220
- if (
221
- assembly &&
222
- assembly.backendDriverType === 'CollaborationServerDriver'
223
- ) {
224
- locations.push({ assemblyName, refName, start, end })
214
+ afterCreate() {
215
+ applySnapshot(self, { name: self.name, id: self.id })
216
+ // @ts-expect-error type is missing on ApolloRootModel
217
+ const { internetAccounts, jbrowse, reloadPluginManagerCallback } =
218
+ getRoot<ApolloRootModel>(self)
219
+ addDisposer(
220
+ self,
221
+ autorun(
222
+ () => {
223
+ // broadcastLocations() // **** This is not working and therefore we need to duplicate broadcastLocations() -method code here because autorun() does not observe changes otherwise
224
+ const locations: {
225
+ assemblyName: string
226
+ refName: string
227
+ start: number
228
+ end: number
229
+ }[] = []
230
+ for (const view of (self as unknown as AbstractSessionModel)
231
+ .views) {
232
+ if (view.type !== 'LinearGenomeView') {
233
+ return
234
+ }
235
+ const lgv = view as unknown as LinearGenomeViewModel
236
+ if (lgv.initialized) {
237
+ const { dynamicBlocks } = lgv
238
+ // eslint-disable-next-line unicorn/no-array-for-each
239
+ dynamicBlocks.forEach((block) => {
240
+ if (block.regionNumber !== undefined) {
241
+ const { assemblyName, end, refName, start } = block
242
+ const assembly =
243
+ self.apolloDataStore.assemblies.get(assemblyName)
244
+ if (
245
+ assembly &&
246
+ assembly.backendDriverType ===
247
+ 'CollaborationServerDriver'
248
+ ) {
249
+ locations.push({ assemblyName, refName, start, end })
250
+ }
225
251
  }
252
+ })
253
+ }
254
+ }
255
+ if (locations.length === 0) {
256
+ for (const internetAccount of internetAccounts) {
257
+ if ('baseURL' in internetAccount) {
258
+ internetAccount.postUserLocation([])
226
259
  }
227
- })
260
+ }
261
+ return
228
262
  }
229
- }
230
- if (locations.length === 0) {
263
+
264
+ const allLocations: UserLocation[] = []
231
265
  for (const internetAccount of internetAccounts) {
232
266
  if ('baseURL' in internetAccount) {
233
- internetAccount.postUserLocation([])
267
+ for (const location of locations) {
268
+ const tmpLoc: UserLocation = {
269
+ assemblyId: location.assemblyName,
270
+ refSeq: location.refName,
271
+ start: location.start,
272
+ end: location.end,
273
+ }
274
+ allLocations.push(tmpLoc)
275
+ }
276
+ internetAccount.postUserLocation(allLocations)
234
277
  }
235
278
  }
236
- return
237
- }
279
+ },
280
+ { name: 'ApolloSessionBroadcastLocations' },
281
+ ),
282
+ )
283
+ addDisposer(
284
+ self,
285
+ autorun(
286
+ async (reaction) => {
287
+ // When the initial config.json loads, it doesn't include the Apollo
288
+ // tracks, which would result in a potentially invalid session snapshot
289
+ // if any tracks are open. Here we copy the session snapshot, apply an
290
+ // empty session snapshot, and then restore the original session
291
+ // snapshot after the updated config.json loads.
292
+ const pluginConfiguration =
293
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
294
+ jbrowse.configuration.ApolloPlugin as Instance<
295
+ typeof ApolloPluginConfigurationSchema
296
+ >
297
+ const hasRole = readConfObject(
298
+ pluginConfiguration,
299
+ 'hasRole',
300
+ ) as boolean
301
+ if (hasRole) {
302
+ // @ts-expect-error not sure why snapshot type is wrong for snapshot
303
+ applySnapshot(self, self.previousSnapshot)
304
+ reaction.dispose()
305
+ return
306
+ }
238
307
 
239
- const allLocations: UserLocation[] = []
240
- for (const internetAccount of internetAccounts) {
241
- if ('baseURL' in internetAccount) {
242
- for (const location of locations) {
243
- const tmpLoc: UserLocation = {
244
- assemblyId: location.assemblyName,
245
- refSeq: location.refName,
246
- start: location.start,
247
- end: location.end,
308
+ const { signal } = self.abortController
309
+ // fetch and initialize assemblies for each of our Apollo internet accounts
310
+ for (const internetAccount of internetAccounts as ApolloInternetAccountModel[]) {
311
+ if (internetAccount.type !== 'ApolloInternetAccount') {
312
+ continue
313
+ }
314
+
315
+ const { baseURL } = internetAccount
316
+ const uri = new URL('jbrowse/config.json', baseURL).href
317
+ const fetch = internetAccount.getFetcher({
318
+ locationType: 'UriLocation',
319
+ uri,
320
+ })
321
+ let response: Response
322
+ try {
323
+ response = await fetch(uri, { signal })
324
+ } catch (error) {
325
+ if (!self.abortController.signal.aborted) {
326
+ console.error(error)
248
327
  }
249
- allLocations.push(tmpLoc)
328
+ continue
250
329
  }
251
- internetAccount.postUserLocation(allLocations)
330
+ if (!response.ok) {
331
+ const errorMessage = await createFetchErrorMessage(
332
+ response,
333
+ 'Failed to fetch assemblies',
334
+ )
335
+ console.error(errorMessage)
336
+ continue
337
+ }
338
+ let jbrowseConfig
339
+ try {
340
+ jbrowseConfig = await response.json()
341
+ } catch (error) {
342
+ console.error(error)
343
+ continue
344
+ }
345
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call
346
+ reloadPluginManagerCallback(
347
+ jbrowseConfig,
348
+ self.previousSnapshot,
349
+ )
350
+ reaction.dispose()
252
351
  }
253
- }
254
- },
255
- { name: 'ApolloSession' },
352
+ },
353
+ { name: 'ApolloSessionLoadConfig' },
354
+ ),
256
355
  )
257
- // END AUTORUN
258
-
259
- // fetch and initialize assemblies for each of our Apollo internet accounts
260
- for (const internetAccount of internetAccounts as ApolloInternetAccountModel[]) {
261
- if (internetAccount.type !== 'ApolloInternetAccount') {
262
- continue
263
- }
264
-
265
- const { baseURL } = internetAccount
266
- const uri = new URL('jbrowse/config.json', baseURL).href
267
- const fetch = internetAccount.getFetcher({
268
- locationType: 'UriLocation',
269
- uri,
270
- })
271
- let response: Response
272
- try {
273
- response = yield fetch(uri, { signal })
274
- } catch (error) {
275
- console.error(error)
276
- continue
277
- }
278
- if (!response.ok) {
279
- const errorMessage = yield createFetchErrorMessage(
280
- response,
281
- 'Failed to fetch assemblies',
282
- )
283
- console.error(errorMessage)
284
- continue
285
- }
286
- let jbrowseConfig
287
- try {
288
- jbrowseConfig = yield response.json()
289
- } catch (error) {
290
- console.error(error)
291
- continue
292
- }
293
- applySnapshot(jbrowse, jbrowseConfig)
294
- // @ts-expect-error snapshot seems to get wrong type?
295
- applySnapshot(self, sessionSnapshot)
296
- }
297
- }),
356
+ },
298
357
  beforeDestroy() {
299
- aborter.abort('destroying session model')
358
+ self.abortController.abort('destroying session model')
300
359
  },
301
360
  }))
302
361
 
@@ -51,3 +51,62 @@ export function getStrand(strand: number | undefined) {
51
51
  }
52
52
  return ''
53
53
  }
54
+
55
+ function getChildren(feature: AnnotationFeature): AnnotationFeature[] {
56
+ const children: AnnotationFeature[] = []
57
+ //
58
+ if (feature.children) {
59
+ for (const [, ff] of feature.children) {
60
+ children.push(ff)
61
+ }
62
+ }
63
+ return children
64
+ }
65
+
66
+ function getParents(feature: AnnotationFeature): AnnotationFeature[] {
67
+ const parents: AnnotationFeature[] = []
68
+ let { parent } = feature
69
+ while (parent) {
70
+ parents.push(parent)
71
+ ;({ parent } = parent)
72
+ }
73
+ return parents
74
+ }
75
+
76
+ export function getRelatedFeatures(
77
+ feature: AnnotationFeature,
78
+ bp: number,
79
+ includeSiblings = false,
80
+ ): AnnotationFeature[] {
81
+ const relatedFeatures: AnnotationFeature[] = []
82
+ relatedFeatures.push(feature)
83
+ for (const x of getParents(feature)) {
84
+ relatedFeatures.push(x)
85
+ }
86
+ const children = getChildren(feature)
87
+ for (const child of children) {
88
+ if (child.min < bp && child.max >= bp) {
89
+ relatedFeatures.push(child)
90
+ }
91
+ }
92
+ if (!includeSiblings) {
93
+ return relatedFeatures
94
+ }
95
+
96
+ // Also add siblings , i.e. features having the same parent as the clicked
97
+ // one and intersecting the click position
98
+ if (feature.parent) {
99
+ const siblings = feature.parent.children
100
+ if (siblings) {
101
+ for (const [, sib] of siblings) {
102
+ if (sib._id == feature._id) {
103
+ continue
104
+ }
105
+ if (sib.min < bp && sib.max >= bp) {
106
+ relatedFeatures.push(sib)
107
+ }
108
+ }
109
+ }
110
+ }
111
+ return relatedFeatures
112
+ }
@@ -0,0 +1,21 @@
1
+ export async function copyToClipboard(element: HTMLElement) {
2
+ if (isSecureContext) {
3
+ const textBlob = new Blob([element.outerText], { type: 'text/plain' })
4
+ const htmlBlob = new Blob([element.outerHTML], { type: 'text/html' })
5
+ const clipboardItem = new ClipboardItem({
6
+ [textBlob.type]: textBlob,
7
+ [htmlBlob.type]: htmlBlob,
8
+ })
9
+ return navigator.clipboard.write([clipboardItem])
10
+ }
11
+ const copyCallback = (event: ClipboardEvent) => {
12
+ event.clipboardData?.setData('text/plain', element.outerText)
13
+ event.clipboardData?.setData('text/html', element.outerHTML)
14
+ event.preventDefault()
15
+ }
16
+ document.addEventListener('copy', copyCallback)
17
+ // fall back to deprecated only in non-secure contexts
18
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
19
+ document.execCommand('copy')
20
+ document.removeEventListener('copy', copyCallback)
21
+ }