loom-browser 0.0.4 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/loom-react.esm.js +957 -9
- package/dist/loom-react.esm.min.js +1 -1
- package/dist/loom-react.esm.min.js.map +1 -1
- package/dist/loom.esm.js +138 -2
- package/dist/loom.esm.min.js +1 -1
- package/dist/loom.esm.min.js.map +1 -1
- package/dist/loom.js +138 -1
- package/dist/loom.min.js +1 -1
- package/dist/loom.min.js.map +1 -1
- package/dist/tsconfig.src.tsbuildinfo +1 -1
- package/dist/types/dataSources/memoryDataSource.d.ts +32 -0
- package/dist/types/headlessGenomeBrowser.d.ts +33 -1
- package/dist/types/index.d.ts +3 -2
- package/dist/types/react/GenomeBrowserContext.d.ts +3 -0
- package/dist/types/react/index.d.ts +2 -0
- package/dist/types/react/tracks/BedTrack.d.ts +8 -3
- package/dist/types/react/tracks/WigTrack.d.ts +6 -3
- package/dist/types/react/ui/ChromosomeSelect.d.ts +7 -0
- package/dist/types/react/ui/ExportControls.d.ts +7 -0
- package/dist/types/react/ui/LocusInput.d.ts +7 -0
- package/dist/types/react/ui/Navbar.d.ts +10 -0
- package/dist/types/react/ui/WindowSize.d.ts +7 -0
- package/dist/types/react/ui/ZoomControls.d.ts +7 -0
- package/dist/types/react/ui/ensureRegistered.d.ts +5 -0
- package/dist/types/react/ui/index.d.ts +12 -0
- package/dist/types/react/ui/types.d.ts +24 -0
- package/dist/types/types.d.ts +4 -1
- package/package.json +1 -1
package/dist/loom-react.esm.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
2
2
|
import { createContext, forwardRef, useRef, useState, useEffect, useImperativeHandle, useMemo, useContext, useCallback } from 'react';
|
|
3
3
|
import { BigWig } from '@gmod/bbi';
|
|
4
4
|
import { RemoteFile } from 'generic-filehandle2';
|
|
@@ -1337,6 +1337,35 @@ function parseLocus(str, cumOffsets) {
|
|
|
1337
1337
|
return null;
|
|
1338
1338
|
return { chr, start, end };
|
|
1339
1339
|
}
|
|
1340
|
+
/**
|
|
1341
|
+
* Format a Locus object as a human-readable string with thousands separators.
|
|
1342
|
+
* e.g., { chr: 'chr17', start: 7668000, end: 7688000 } → "chr17:7,668,000-7,688,000"
|
|
1343
|
+
*/
|
|
1344
|
+
function formatLocus(locus) {
|
|
1345
|
+
if (isWholeGenomeView(locus))
|
|
1346
|
+
return 'All Chromosomes';
|
|
1347
|
+
return `${locus.chr}:${Math.round(locus.start).toLocaleString()}-${Math.round(locus.end).toLocaleString()}`;
|
|
1348
|
+
}
|
|
1349
|
+
/**
|
|
1350
|
+
* Format a base pair length as a human-readable string.
|
|
1351
|
+
* e.g., 20000 → "20 kb", 2500000 → "2.5 Mb", 500 → "500 bp"
|
|
1352
|
+
*/
|
|
1353
|
+
function formatBpLength(bp) {
|
|
1354
|
+
bp = Math.abs(bp);
|
|
1355
|
+
if (bp >= 1e9) {
|
|
1356
|
+
const val = bp / 1e9;
|
|
1357
|
+
return `${val % 1 === 0 ? val.toFixed(0) : val.toFixed(1)} Gb`;
|
|
1358
|
+
}
|
|
1359
|
+
if (bp >= 1e6) {
|
|
1360
|
+
const val = bp / 1e6;
|
|
1361
|
+
return `${val % 1 === 0 ? val.toFixed(0) : val.toFixed(1)} Mb`;
|
|
1362
|
+
}
|
|
1363
|
+
if (bp >= 1e3) {
|
|
1364
|
+
const val = bp / 1e3;
|
|
1365
|
+
return `${val % 1 === 0 ? val.toFixed(0) : val.toFixed(1)} kb`;
|
|
1366
|
+
}
|
|
1367
|
+
return `${Math.round(bp)} bp`;
|
|
1368
|
+
}
|
|
1340
1369
|
/**
|
|
1341
1370
|
* Clamp a locus to valid chromosome bounds.
|
|
1342
1371
|
*
|
|
@@ -8202,6 +8231,72 @@ class SequenceDataSource {
|
|
|
8202
8231
|
}
|
|
8203
8232
|
}
|
|
8204
8233
|
|
|
8234
|
+
/**
|
|
8235
|
+
* DataSource backed by an in-memory feature array.
|
|
8236
|
+
*
|
|
8237
|
+
* Wraps a pre-loaded feature array in a FeatureCache for spatial queries,
|
|
8238
|
+
* enabling inline features to flow through the same data lifecycle as
|
|
8239
|
+
* URL-based data sources.
|
|
8240
|
+
*
|
|
8241
|
+
* Layer 1 (Data + Layout): no DOM, no canvas.
|
|
8242
|
+
*/
|
|
8243
|
+
/**
|
|
8244
|
+
* DataSource that serves features from an in-memory array.
|
|
8245
|
+
*
|
|
8246
|
+
* Features are indexed in a FeatureCache on construction for efficient
|
|
8247
|
+
* spatial queries. Calling `setFeatures()` replaces the cache entirely.
|
|
8248
|
+
*/
|
|
8249
|
+
class MemoryDataSource {
|
|
8250
|
+
constructor(features) {
|
|
8251
|
+
this.cache = new FeatureCache(features);
|
|
8252
|
+
}
|
|
8253
|
+
/** Replace the in-memory features and rebuild the spatial index. */
|
|
8254
|
+
setFeatures(features) {
|
|
8255
|
+
this.cache = new FeatureCache(features);
|
|
8256
|
+
}
|
|
8257
|
+
/** Set a chromosome name resolver for alias resolution. */
|
|
8258
|
+
setChromNameResolver(resolver) {
|
|
8259
|
+
this._resolveChromName = resolver;
|
|
8260
|
+
}
|
|
8261
|
+
/** Set cumulative offsets for whole genome view coordinate transformation. */
|
|
8262
|
+
setCumulativeOffsets(offsets) {
|
|
8263
|
+
this._cumulativeOffsets = offsets;
|
|
8264
|
+
}
|
|
8265
|
+
async fetch(locus, _bpPerPixel, _signal) {
|
|
8266
|
+
if (isWholeGenomeView(locus) && this._cumulativeOffsets) {
|
|
8267
|
+
return this.fetchWG();
|
|
8268
|
+
}
|
|
8269
|
+
const chr = this._resolveChromName
|
|
8270
|
+
? this._resolveChromName(locus.chr)
|
|
8271
|
+
: locus.chr;
|
|
8272
|
+
return this.cache.queryFeatures(chr, locus.start, locus.end);
|
|
8273
|
+
}
|
|
8274
|
+
fetchWG() {
|
|
8275
|
+
const offsets = this._cumulativeOffsets;
|
|
8276
|
+
const chrNames = mainChromosomeNames(Object.fromEntries(offsets.chromosomeNames.map(name => { var _a; return [name, (_a = offsets.offsets[name]) !== null && _a !== void 0 ? _a : 0]; })));
|
|
8277
|
+
const wgFeatures = [];
|
|
8278
|
+
const allByChrom = this.cache.getAllFeatures();
|
|
8279
|
+
for (const chr of chrNames) {
|
|
8280
|
+
const features = allByChrom[chr];
|
|
8281
|
+
if (!features)
|
|
8282
|
+
continue;
|
|
8283
|
+
const offset = offsets.offsets[chr];
|
|
8284
|
+
if (offset === undefined)
|
|
8285
|
+
continue;
|
|
8286
|
+
for (const f of features) {
|
|
8287
|
+
wgFeatures.push({
|
|
8288
|
+
...f,
|
|
8289
|
+
chr: 'all',
|
|
8290
|
+
start: offset + f.start,
|
|
8291
|
+
end: offset + f.end,
|
|
8292
|
+
});
|
|
8293
|
+
}
|
|
8294
|
+
}
|
|
8295
|
+
wgFeatures.sort((a, b) => a.start - b.start);
|
|
8296
|
+
return wgFeatures;
|
|
8297
|
+
}
|
|
8298
|
+
}
|
|
8299
|
+
|
|
8205
8300
|
/**
|
|
8206
8301
|
* Stateless renderer for interaction (arc/BEDPE) tracks.
|
|
8207
8302
|
*
|
|
@@ -8772,6 +8867,11 @@ function createDataSource(config, workerProvider) {
|
|
|
8772
8867
|
indexed: config.indexed,
|
|
8773
8868
|
workerProvider,
|
|
8774
8869
|
});
|
|
8870
|
+
case 'memory':
|
|
8871
|
+
// Memory data sources are created directly with features by the caller.
|
|
8872
|
+
// This path is only hit during session restore, where in-memory features
|
|
8873
|
+
// are not available — return an empty MemoryDataSource as a placeholder.
|
|
8874
|
+
return new MemoryDataSource([]);
|
|
8775
8875
|
}
|
|
8776
8876
|
}
|
|
8777
8877
|
// ─── Built-in track creators ─────────────────────────────────────────────────
|
|
@@ -8950,6 +9050,9 @@ function dataSourceCacheKey(config) {
|
|
|
8950
9050
|
return `ucsc:${(_b = config.genome) !== null && _b !== void 0 ? _b : ''}:${(_c = config.track) !== null && _c !== void 0 ? _c : ''}`;
|
|
8951
9051
|
case 'text':
|
|
8952
9052
|
return `text:${config.url}:${(_d = config.format) !== null && _d !== void 0 ? _d : ''}:${(_e = config.indexURL) !== null && _e !== void 0 ? _e : ''}`;
|
|
9053
|
+
case 'memory':
|
|
9054
|
+
// Each memory data source is unique — no deduplication.
|
|
9055
|
+
return `memory:${Math.random()}`;
|
|
8953
9056
|
}
|
|
8954
9057
|
}
|
|
8955
9058
|
|
|
@@ -10060,6 +10163,60 @@ class HeadlessGenomeBrowser {
|
|
|
10060
10163
|
this.managedTracks[this.managedTracks.length - 1].metadata = options.metadata;
|
|
10061
10164
|
return track;
|
|
10062
10165
|
}
|
|
10166
|
+
/** Add a BigWig-style signal track backed by in-memory features (no URL required). */
|
|
10167
|
+
addWigTrackWithFeatures(features, options) {
|
|
10168
|
+
const { canvas } = this.canvasProvider.createCanvas(0, 0);
|
|
10169
|
+
const track = new WigTrackCanvas(canvas, {
|
|
10170
|
+
locus: this._locus,
|
|
10171
|
+
features: [],
|
|
10172
|
+
config: options === null || options === void 0 ? void 0 : options.config,
|
|
10173
|
+
height: options === null || options === void 0 ? void 0 : options.height,
|
|
10174
|
+
background: options === null || options === void 0 ? void 0 : options.background,
|
|
10175
|
+
theme: this.theme,
|
|
10176
|
+
canvasProvider: this.canvasProvider,
|
|
10177
|
+
name: options === null || options === void 0 ? void 0 : options.name,
|
|
10178
|
+
sequenceProvider: this.sequenceProvider,
|
|
10179
|
+
});
|
|
10180
|
+
const dataSource = new MemoryDataSource(features);
|
|
10181
|
+
if (this.cumulativeOffsets) {
|
|
10182
|
+
dataSource.setCumulativeOffsets(this.cumulativeOffsets);
|
|
10183
|
+
}
|
|
10184
|
+
if (this.genome) {
|
|
10185
|
+
dataSource.setChromNameResolver(alias => this.genome.getChromosomeName(alias));
|
|
10186
|
+
}
|
|
10187
|
+
const dataSourceConfig = { type: 'memory' };
|
|
10188
|
+
this.addTrack(track, dataSource, dataSourceConfig, options === null || options === void 0 ? void 0 : options.maxTrackHeight);
|
|
10189
|
+
if (options === null || options === void 0 ? void 0 : options.metadata)
|
|
10190
|
+
this.managedTracks[this.managedTracks.length - 1].metadata = options.metadata;
|
|
10191
|
+
return track;
|
|
10192
|
+
}
|
|
10193
|
+
/** Add a BED/annotation track backed by in-memory features (no URL required). Features must include `chr`. */
|
|
10194
|
+
addBedTrackWithFeatures(features, options) {
|
|
10195
|
+
const { canvas } = this.canvasProvider.createCanvas(0, 0);
|
|
10196
|
+
const track = new AnnotationTrackCanvas(canvas, {
|
|
10197
|
+
locus: this._locus,
|
|
10198
|
+
features: [],
|
|
10199
|
+
config: options === null || options === void 0 ? void 0 : options.config,
|
|
10200
|
+
height: options === null || options === void 0 ? void 0 : options.height,
|
|
10201
|
+
background: options === null || options === void 0 ? void 0 : options.background,
|
|
10202
|
+
theme: this.theme,
|
|
10203
|
+
canvasProvider: this.canvasProvider,
|
|
10204
|
+
workerProvider: this.workerProvider,
|
|
10205
|
+
name: options === null || options === void 0 ? void 0 : options.name,
|
|
10206
|
+
});
|
|
10207
|
+
const dataSource = new MemoryDataSource(features);
|
|
10208
|
+
if (this.cumulativeOffsets) {
|
|
10209
|
+
dataSource.setCumulativeOffsets(this.cumulativeOffsets);
|
|
10210
|
+
}
|
|
10211
|
+
if (this.genome) {
|
|
10212
|
+
dataSource.setChromNameResolver(alias => this.genome.getChromosomeName(alias));
|
|
10213
|
+
}
|
|
10214
|
+
const dataSourceConfig = { type: 'memory' };
|
|
10215
|
+
this.addTrack(track, dataSource, dataSourceConfig, options === null || options === void 0 ? void 0 : options.maxTrackHeight);
|
|
10216
|
+
if (options === null || options === void 0 ? void 0 : options.metadata)
|
|
10217
|
+
this.managedTracks[this.managedTracks.length - 1].metadata = options.metadata;
|
|
10218
|
+
return track;
|
|
10219
|
+
}
|
|
10063
10220
|
/** Add a DNA/RNA sequence track. Data fetching is handled automatically via the genome's sequence provider. */
|
|
10064
10221
|
addSequenceTrack(options) {
|
|
10065
10222
|
const trackConfig = { type: 'sequence', config: options === null || options === void 0 ? void 0 : options.config };
|
|
@@ -11210,6 +11367,13 @@ class GenomeBrowser extends HeadlessGenomeBrowser {
|
|
|
11210
11367
|
this.events.on(BrowserEvent.DataLoaded, ({ track }) => {
|
|
11211
11368
|
this.updateAxisContent(track);
|
|
11212
11369
|
});
|
|
11370
|
+
// Repaint axis canvases when theme changes (track canvases update via
|
|
11371
|
+
// track.setTheme(), but axis sidebar is managed here in GenomeBrowser).
|
|
11372
|
+
this.events.on(BrowserEvent.ThemeChanged, () => {
|
|
11373
|
+
for (const mt of this.managedTracks) {
|
|
11374
|
+
this.updateAxisContent(mt.track);
|
|
11375
|
+
}
|
|
11376
|
+
});
|
|
11213
11377
|
// Re-render ROI overlays when ROIs change or viewport moves
|
|
11214
11378
|
this.events.on(BrowserEvent.ROIAdded, () => this.renderROIOverlays());
|
|
11215
11379
|
this.events.on(BrowserEvent.ROIRemoved, () => this.renderROIOverlays());
|
|
@@ -11412,9 +11576,10 @@ class GenomeBrowser extends HeadlessGenomeBrowser {
|
|
|
11412
11576
|
const { axisDiv } = entry;
|
|
11413
11577
|
const rawInfo = (_a = track.getAxisInfo) === null || _a === void 0 ? void 0 : _a.call(track);
|
|
11414
11578
|
if (!rawInfo) {
|
|
11415
|
-
// No axis info —
|
|
11579
|
+
// No axis info — keep the column for alignment but clear content
|
|
11416
11580
|
axisDiv.innerHTML = "";
|
|
11417
11581
|
axisDiv.style.borderRight = "none";
|
|
11582
|
+
axisDiv.style.backgroundColor = this.theme.palette.background;
|
|
11418
11583
|
entry.axisCanvas = null;
|
|
11419
11584
|
return;
|
|
11420
11585
|
}
|
|
@@ -12435,6 +12600,23 @@ class GenomeBrowser extends HeadlessGenomeBrowser {
|
|
|
12435
12600
|
|
|
12436
12601
|
const GenomeBrowserContext = createContext(null);
|
|
12437
12602
|
|
|
12603
|
+
/** Infer the shell theme from a RenderTheme's palette. */
|
|
12604
|
+
function inferShellTheme(theme) {
|
|
12605
|
+
var _a;
|
|
12606
|
+
const bg = (_a = theme === null || theme === void 0 ? void 0 : theme.palette) === null || _a === void 0 ? void 0 : _a.background;
|
|
12607
|
+
if (!bg)
|
|
12608
|
+
return 'modern';
|
|
12609
|
+
// Simple luminance check: dark backgrounds → dark shell theme
|
|
12610
|
+
const el = document.createElement('canvas');
|
|
12611
|
+
const ctx = el.getContext('2d');
|
|
12612
|
+
ctx.fillStyle = bg;
|
|
12613
|
+
const hex = ctx.fillStyle; // normalized to #rrggbb
|
|
12614
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
12615
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
12616
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
12617
|
+
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
|
12618
|
+
return luminance < 0.5 ? 'dark' : 'modern';
|
|
12619
|
+
}
|
|
12438
12620
|
const LoomBrowser = forwardRef(function LoomBrowser(props, ref) {
|
|
12439
12621
|
const { defaultLocus, locus, onLocusChange, theme, genome, interactive, wheelZoom, workerProvider, popupProvider, contextMenuProvider, ruler, genes, sequence, session, tracks: trackConfigs, remoteSocket, className, style, children, } = props;
|
|
12440
12622
|
const containerRef = useRef(null);
|
|
@@ -12577,8 +12759,9 @@ const LoomBrowser = forwardRef(function LoomBrowser(props, ref) {
|
|
|
12577
12759
|
attachRemote(socket) { var _a; (_a = browserRef.current) === null || _a === void 0 ? void 0 : _a.attachRemote(socket); },
|
|
12578
12760
|
detachRemote() { var _a; (_a = browserRef.current) === null || _a === void 0 ? void 0 : _a.detachRemote(); },
|
|
12579
12761
|
}), [browser]);
|
|
12580
|
-
const
|
|
12581
|
-
|
|
12762
|
+
const shellTheme = useMemo(() => inferShellTheme(theme), [theme]);
|
|
12763
|
+
const ctxValue = useMemo(() => ({ browser, shellTheme }), [browser, shellTheme]);
|
|
12764
|
+
return (jsx(GenomeBrowserContext.Provider, { value: ctxValue, children: jsxs("div", { className: className, style: style, children: [browser && children, jsx("div", { ref: containerRef })] }) }));
|
|
12582
12765
|
});
|
|
12583
12766
|
|
|
12584
12767
|
/**
|
|
@@ -12686,8 +12869,15 @@ function RulerTrack({ config, maxTrackHeight }) {
|
|
|
12686
12869
|
return null;
|
|
12687
12870
|
}
|
|
12688
12871
|
|
|
12689
|
-
function WigTrack({ url, config, height, background, windowFunction, maxTrackHeight, name, metadata }) {
|
|
12690
|
-
useTrackManager((browser) =>
|
|
12872
|
+
function WigTrack({ url, features, config, height, background, windowFunction, maxTrackHeight, name, metadata }) {
|
|
12873
|
+
useTrackManager((browser) => {
|
|
12874
|
+
if (features) {
|
|
12875
|
+
return browser.addWigTrackWithFeatures(features, { config, height, background, maxTrackHeight, name, metadata });
|
|
12876
|
+
}
|
|
12877
|
+
if (!url)
|
|
12878
|
+
throw new Error('WigTrack requires either a `url` or `features` prop');
|
|
12879
|
+
return browser.addWigTrack(url, { config, height, background, windowFunction, maxTrackHeight, name, metadata });
|
|
12880
|
+
}, [url, features, windowFunction], (track) => { if (config)
|
|
12691
12881
|
track.setConfig(config); }, [config, height, background, name]);
|
|
12692
12882
|
return null;
|
|
12693
12883
|
}
|
|
@@ -12698,8 +12888,15 @@ function GeneTrack({ config, height, background, genome, track, maxTrackHeight,
|
|
|
12698
12888
|
return null;
|
|
12699
12889
|
}
|
|
12700
12890
|
|
|
12701
|
-
function BedTrack({ url, config, height, background, format, indexURL, indexed, maxTrackHeight, name, metadata }) {
|
|
12702
|
-
useTrackManager((browser) =>
|
|
12891
|
+
function BedTrack({ url, features, config, height, background, format, indexURL, indexed, maxTrackHeight, name, metadata }) {
|
|
12892
|
+
useTrackManager((browser) => {
|
|
12893
|
+
if (features) {
|
|
12894
|
+
return browser.addBedTrackWithFeatures(features, { config, height, background, maxTrackHeight, name, metadata });
|
|
12895
|
+
}
|
|
12896
|
+
if (!url)
|
|
12897
|
+
throw new Error('BedTrack requires either a `url` or `features` prop');
|
|
12898
|
+
return browser.addBedTrack(url, { config, height, background, format, indexURL, indexed, maxTrackHeight, name, metadata });
|
|
12899
|
+
}, [url, features, format, indexURL, indexed], (track) => { if (config)
|
|
12703
12900
|
track.setConfig(config); }, [config, height, background, name]);
|
|
12704
12901
|
return null;
|
|
12705
12902
|
}
|
|
@@ -12722,6 +12919,757 @@ function GtxTrack({ url, experimentId, config, height, background, windowFunctio
|
|
|
12722
12919
|
return null;
|
|
12723
12920
|
}
|
|
12724
12921
|
|
|
12922
|
+
/**
|
|
12923
|
+
* <loom-chromosome-select> — Chromosome dropdown for quick navigation.
|
|
12924
|
+
*
|
|
12925
|
+
* Lists main chromosomes (chr1-22, X, Y, M) plus "All" for whole genome view.
|
|
12926
|
+
* Updates automatically when the browser locus changes.
|
|
12927
|
+
*/
|
|
12928
|
+
const template$5 = document.createElement('template');
|
|
12929
|
+
template$5.innerHTML = /* html */ `
|
|
12930
|
+
<style>
|
|
12931
|
+
:host {
|
|
12932
|
+
display: inline-flex;
|
|
12933
|
+
align-items: center;
|
|
12934
|
+
}
|
|
12935
|
+
select {
|
|
12936
|
+
font: var(--loom-font, 12px Arial, sans-serif);
|
|
12937
|
+
color: var(--loom-text-color, #333);
|
|
12938
|
+
background: var(--loom-input-bg, white);
|
|
12939
|
+
border: var(--loom-input-border, 1px solid #b0b0b0);
|
|
12940
|
+
border-radius: var(--loom-border-radius, 4px);
|
|
12941
|
+
height: var(--loom-input-height, 22px);
|
|
12942
|
+
padding: 0 4px;
|
|
12943
|
+
box-sizing: border-box;
|
|
12944
|
+
outline: none;
|
|
12945
|
+
cursor: pointer;
|
|
12946
|
+
transition: border-color 0.15s;
|
|
12947
|
+
}
|
|
12948
|
+
select:focus {
|
|
12949
|
+
border: var(--loom-input-focus-border, 1px solid #4A90D9);
|
|
12950
|
+
}
|
|
12951
|
+
</style>
|
|
12952
|
+
<select></select>
|
|
12953
|
+
`;
|
|
12954
|
+
class LoomChromosomeSelect extends HTMLElement {
|
|
12955
|
+
constructor() {
|
|
12956
|
+
super();
|
|
12957
|
+
this.unsubscribe = null;
|
|
12958
|
+
this._browser = null;
|
|
12959
|
+
this.attachShadow({ mode: 'open' });
|
|
12960
|
+
this.shadowRoot.appendChild(template$5.content.cloneNode(true));
|
|
12961
|
+
this.select = this.shadowRoot.querySelector('select');
|
|
12962
|
+
this.select.addEventListener('change', () => {
|
|
12963
|
+
if (!this._browser)
|
|
12964
|
+
return;
|
|
12965
|
+
const value = this.select.value;
|
|
12966
|
+
if (value === 'all') {
|
|
12967
|
+
this._browser.search('all');
|
|
12968
|
+
}
|
|
12969
|
+
else {
|
|
12970
|
+
const chromSizes = this._browser.chromSizes;
|
|
12971
|
+
if (chromSizes && chromSizes[value] != null) {
|
|
12972
|
+
this._browser.setLocus({ chr: value, start: 0, end: chromSizes[value] });
|
|
12973
|
+
}
|
|
12974
|
+
}
|
|
12975
|
+
});
|
|
12976
|
+
}
|
|
12977
|
+
set browser(b) {
|
|
12978
|
+
if (this.unsubscribe) {
|
|
12979
|
+
this.unsubscribe();
|
|
12980
|
+
this.unsubscribe = null;
|
|
12981
|
+
}
|
|
12982
|
+
this._browser = b;
|
|
12983
|
+
if (b) {
|
|
12984
|
+
this.populate(b);
|
|
12985
|
+
this.updateSelection(b.locus.chr);
|
|
12986
|
+
this.unsubscribe = b.on(BrowserEvent.LocusChange, ({ locus }) => {
|
|
12987
|
+
this.updateSelection(locus.chr);
|
|
12988
|
+
});
|
|
12989
|
+
}
|
|
12990
|
+
}
|
|
12991
|
+
get browser() {
|
|
12992
|
+
return this._browser;
|
|
12993
|
+
}
|
|
12994
|
+
populate(browser) {
|
|
12995
|
+
this.select.innerHTML = '';
|
|
12996
|
+
const chromSizes = browser.chromSizes;
|
|
12997
|
+
if (!chromSizes)
|
|
12998
|
+
return;
|
|
12999
|
+
// "All" option for whole genome view
|
|
13000
|
+
if (browser.cumulativeOffsets) {
|
|
13001
|
+
const opt = document.createElement('option');
|
|
13002
|
+
opt.value = 'all';
|
|
13003
|
+
opt.textContent = 'All';
|
|
13004
|
+
this.select.appendChild(opt);
|
|
13005
|
+
}
|
|
13006
|
+
// Main chromosomes in order
|
|
13007
|
+
const names = mainChromosomeNames(chromSizes);
|
|
13008
|
+
for (const name of names) {
|
|
13009
|
+
const opt = document.createElement('option');
|
|
13010
|
+
opt.value = name;
|
|
13011
|
+
opt.textContent = name;
|
|
13012
|
+
this.select.appendChild(opt);
|
|
13013
|
+
}
|
|
13014
|
+
}
|
|
13015
|
+
updateSelection(chr) {
|
|
13016
|
+
if (isWholeGenomeView({ chr, start: 0, end: 0 })) {
|
|
13017
|
+
this.select.value = 'all';
|
|
13018
|
+
}
|
|
13019
|
+
else {
|
|
13020
|
+
this.select.value = chr;
|
|
13021
|
+
}
|
|
13022
|
+
}
|
|
13023
|
+
disconnectedCallback() {
|
|
13024
|
+
if (this.unsubscribe) {
|
|
13025
|
+
this.unsubscribe();
|
|
13026
|
+
this.unsubscribe = null;
|
|
13027
|
+
}
|
|
13028
|
+
}
|
|
13029
|
+
}
|
|
13030
|
+
|
|
13031
|
+
/**
|
|
13032
|
+
* <loom-locus-input> — Locus search/display input.
|
|
13033
|
+
*
|
|
13034
|
+
* Shows the current locus as a formatted string. User can type a new locus
|
|
13035
|
+
* and press Enter to navigate. Updates automatically on locuschange events.
|
|
13036
|
+
*/
|
|
13037
|
+
const template$4 = document.createElement('template');
|
|
13038
|
+
template$4.innerHTML = /* html */ `
|
|
13039
|
+
<style>
|
|
13040
|
+
:host {
|
|
13041
|
+
display: inline-flex;
|
|
13042
|
+
align-items: center;
|
|
13043
|
+
}
|
|
13044
|
+
.container {
|
|
13045
|
+
display: flex;
|
|
13046
|
+
align-items: center;
|
|
13047
|
+
gap: 4px;
|
|
13048
|
+
}
|
|
13049
|
+
input {
|
|
13050
|
+
font: var(--loom-font, 12px Arial, sans-serif);
|
|
13051
|
+
color: var(--loom-text-color, #333);
|
|
13052
|
+
background: var(--loom-input-bg, white);
|
|
13053
|
+
border: var(--loom-input-border, 1px solid #b0b0b0);
|
|
13054
|
+
border-radius: var(--loom-border-radius, 4px);
|
|
13055
|
+
width: var(--loom-input-width, 220px);
|
|
13056
|
+
height: var(--loom-input-height, 22px);
|
|
13057
|
+
padding: 0 8px;
|
|
13058
|
+
box-sizing: border-box;
|
|
13059
|
+
outline: none;
|
|
13060
|
+
transition: border-color 0.15s;
|
|
13061
|
+
}
|
|
13062
|
+
input:focus {
|
|
13063
|
+
border: var(--loom-input-focus-border, 1px solid #4A90D9);
|
|
13064
|
+
}
|
|
13065
|
+
input::placeholder {
|
|
13066
|
+
color: var(--loom-text-muted, #737373);
|
|
13067
|
+
}
|
|
13068
|
+
.search-btn {
|
|
13069
|
+
display: flex;
|
|
13070
|
+
align-items: center;
|
|
13071
|
+
justify-content: center;
|
|
13072
|
+
width: var(--loom-button-size, 24px);
|
|
13073
|
+
height: var(--loom-button-size, 24px);
|
|
13074
|
+
background: var(--loom-button-bg, white);
|
|
13075
|
+
border: var(--loom-button-border, 1px solid #b0b0b0);
|
|
13076
|
+
border-radius: var(--loom-border-radius, 4px);
|
|
13077
|
+
cursor: pointer;
|
|
13078
|
+
color: var(--loom-icon-color, #555);
|
|
13079
|
+
transition: background 0.15s;
|
|
13080
|
+
padding: 0;
|
|
13081
|
+
line-height: 1;
|
|
13082
|
+
}
|
|
13083
|
+
.search-btn:hover {
|
|
13084
|
+
background: var(--loom-button-hover, #e8e8e8);
|
|
13085
|
+
}
|
|
13086
|
+
.search-btn svg {
|
|
13087
|
+
width: var(--loom-icon-size, 14px);
|
|
13088
|
+
height: var(--loom-icon-size, 14px);
|
|
13089
|
+
fill: none;
|
|
13090
|
+
stroke: currentColor;
|
|
13091
|
+
stroke-width: 2;
|
|
13092
|
+
stroke-linecap: round;
|
|
13093
|
+
stroke-linejoin: round;
|
|
13094
|
+
}
|
|
13095
|
+
</style>
|
|
13096
|
+
<div class="container">
|
|
13097
|
+
<input type="text" placeholder="Locus Search" spellcheck="false" />
|
|
13098
|
+
<button class="search-btn" title="Go to locus">
|
|
13099
|
+
<svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><line x1="16.5" y1="16.5" x2="21" y2="21"/></svg>
|
|
13100
|
+
</button>
|
|
13101
|
+
</div>
|
|
13102
|
+
`;
|
|
13103
|
+
class LoomLocusInput extends HTMLElement {
|
|
13104
|
+
constructor() {
|
|
13105
|
+
super();
|
|
13106
|
+
this.unsubscribe = null;
|
|
13107
|
+
this._browser = null;
|
|
13108
|
+
this.attachShadow({ mode: 'open' });
|
|
13109
|
+
this.shadowRoot.appendChild(template$4.content.cloneNode(true));
|
|
13110
|
+
this.input = this.shadowRoot.querySelector('input');
|
|
13111
|
+
this.input.addEventListener('keydown', (e) => {
|
|
13112
|
+
if (e.key === 'Enter') {
|
|
13113
|
+
this.doSearch();
|
|
13114
|
+
}
|
|
13115
|
+
});
|
|
13116
|
+
this.shadowRoot.querySelector('.search-btn').addEventListener('click', () => {
|
|
13117
|
+
this.doSearch();
|
|
13118
|
+
});
|
|
13119
|
+
}
|
|
13120
|
+
set browser(b) {
|
|
13121
|
+
if (this.unsubscribe) {
|
|
13122
|
+
this.unsubscribe();
|
|
13123
|
+
this.unsubscribe = null;
|
|
13124
|
+
}
|
|
13125
|
+
this._browser = b;
|
|
13126
|
+
if (b) {
|
|
13127
|
+
this.updateDisplay(b.locus);
|
|
13128
|
+
this.unsubscribe = b.on(BrowserEvent.LocusChange, ({ locus }) => {
|
|
13129
|
+
this.updateDisplay(locus);
|
|
13130
|
+
});
|
|
13131
|
+
}
|
|
13132
|
+
}
|
|
13133
|
+
get browser() {
|
|
13134
|
+
return this._browser;
|
|
13135
|
+
}
|
|
13136
|
+
updateDisplay(locus) {
|
|
13137
|
+
if (document.activeElement !== this.input && this.shadowRoot.activeElement !== this.input) {
|
|
13138
|
+
this.input.value = formatLocus(locus);
|
|
13139
|
+
}
|
|
13140
|
+
}
|
|
13141
|
+
doSearch() {
|
|
13142
|
+
if (!this._browser)
|
|
13143
|
+
return;
|
|
13144
|
+
const value = this.input.value.trim();
|
|
13145
|
+
if (value) {
|
|
13146
|
+
this._browser.search(value);
|
|
13147
|
+
}
|
|
13148
|
+
this.input.blur();
|
|
13149
|
+
}
|
|
13150
|
+
disconnectedCallback() {
|
|
13151
|
+
if (this.unsubscribe) {
|
|
13152
|
+
this.unsubscribe();
|
|
13153
|
+
this.unsubscribe = null;
|
|
13154
|
+
}
|
|
13155
|
+
}
|
|
13156
|
+
}
|
|
13157
|
+
|
|
13158
|
+
/**
|
|
13159
|
+
* <loom-zoom-controls> — Zoom in/out buttons.
|
|
13160
|
+
*/
|
|
13161
|
+
const template$3 = document.createElement('template');
|
|
13162
|
+
template$3.innerHTML = /* html */ `
|
|
13163
|
+
<style>
|
|
13164
|
+
:host {
|
|
13165
|
+
display: inline-flex;
|
|
13166
|
+
align-items: center;
|
|
13167
|
+
}
|
|
13168
|
+
.container {
|
|
13169
|
+
display: flex;
|
|
13170
|
+
align-items: center;
|
|
13171
|
+
gap: 2px;
|
|
13172
|
+
}
|
|
13173
|
+
button {
|
|
13174
|
+
display: flex;
|
|
13175
|
+
align-items: center;
|
|
13176
|
+
justify-content: center;
|
|
13177
|
+
width: var(--loom-button-size, 24px);
|
|
13178
|
+
height: var(--loom-button-size, 24px);
|
|
13179
|
+
background: var(--loom-button-bg, white);
|
|
13180
|
+
border: var(--loom-button-border, 1px solid #b0b0b0);
|
|
13181
|
+
border-radius: var(--loom-border-radius, 4px);
|
|
13182
|
+
cursor: pointer;
|
|
13183
|
+
color: var(--loom-icon-color, #555);
|
|
13184
|
+
transition: background 0.15s;
|
|
13185
|
+
padding: 0;
|
|
13186
|
+
line-height: 1;
|
|
13187
|
+
}
|
|
13188
|
+
button:hover {
|
|
13189
|
+
background: var(--loom-button-hover, #e8e8e8);
|
|
13190
|
+
}
|
|
13191
|
+
button:active {
|
|
13192
|
+
transform: scale(0.95);
|
|
13193
|
+
}
|
|
13194
|
+
button svg {
|
|
13195
|
+
width: var(--loom-icon-size, 14px);
|
|
13196
|
+
height: var(--loom-icon-size, 14px);
|
|
13197
|
+
fill: none;
|
|
13198
|
+
stroke: currentColor;
|
|
13199
|
+
stroke-width: 2;
|
|
13200
|
+
stroke-linecap: round;
|
|
13201
|
+
}
|
|
13202
|
+
</style>
|
|
13203
|
+
<div class="container">
|
|
13204
|
+
<button class="zoom-out" title="Zoom out">
|
|
13205
|
+
<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><line x1="8" y1="12" x2="16" y2="12"/></svg>
|
|
13206
|
+
</button>
|
|
13207
|
+
<button class="zoom-in" title="Zoom in">
|
|
13208
|
+
<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><line x1="12" y1="8" x2="12" y2="16"/><line x1="8" y1="12" x2="16" y2="12"/></svg>
|
|
13209
|
+
</button>
|
|
13210
|
+
</div>
|
|
13211
|
+
`;
|
|
13212
|
+
class LoomZoomControls extends HTMLElement {
|
|
13213
|
+
constructor() {
|
|
13214
|
+
super();
|
|
13215
|
+
this._browser = null;
|
|
13216
|
+
this.attachShadow({ mode: 'open' });
|
|
13217
|
+
this.shadowRoot.appendChild(template$3.content.cloneNode(true));
|
|
13218
|
+
this.shadowRoot.querySelector('.zoom-out').addEventListener('click', () => {
|
|
13219
|
+
var _a;
|
|
13220
|
+
(_a = this._browser) === null || _a === void 0 ? void 0 : _a.zoomOut();
|
|
13221
|
+
});
|
|
13222
|
+
this.shadowRoot.querySelector('.zoom-in').addEventListener('click', () => {
|
|
13223
|
+
var _a;
|
|
13224
|
+
(_a = this._browser) === null || _a === void 0 ? void 0 : _a.zoomIn();
|
|
13225
|
+
});
|
|
13226
|
+
}
|
|
13227
|
+
set browser(b) {
|
|
13228
|
+
this._browser = b;
|
|
13229
|
+
}
|
|
13230
|
+
get browser() {
|
|
13231
|
+
return this._browser;
|
|
13232
|
+
}
|
|
13233
|
+
}
|
|
13234
|
+
|
|
13235
|
+
/**
|
|
13236
|
+
* <loom-window-size> — Read-only display of current viewport bp range.
|
|
13237
|
+
*/
|
|
13238
|
+
const template$2 = document.createElement('template');
|
|
13239
|
+
template$2.innerHTML = /* html */ `
|
|
13240
|
+
<style>
|
|
13241
|
+
:host {
|
|
13242
|
+
display: inline-flex;
|
|
13243
|
+
align-items: center;
|
|
13244
|
+
}
|
|
13245
|
+
.label {
|
|
13246
|
+
font: var(--loom-font-small, 11px Arial, sans-serif);
|
|
13247
|
+
color: var(--loom-text-muted, #737373);
|
|
13248
|
+
white-space: nowrap;
|
|
13249
|
+
user-select: none;
|
|
13250
|
+
}
|
|
13251
|
+
</style>
|
|
13252
|
+
<span class="label"></span>
|
|
13253
|
+
`;
|
|
13254
|
+
class LoomWindowSize extends HTMLElement {
|
|
13255
|
+
constructor() {
|
|
13256
|
+
super();
|
|
13257
|
+
this.unsubscribe = null;
|
|
13258
|
+
this._browser = null;
|
|
13259
|
+
this.attachShadow({ mode: 'open' });
|
|
13260
|
+
this.shadowRoot.appendChild(template$2.content.cloneNode(true));
|
|
13261
|
+
this.label = this.shadowRoot.querySelector('.label');
|
|
13262
|
+
}
|
|
13263
|
+
set browser(b) {
|
|
13264
|
+
if (this.unsubscribe) {
|
|
13265
|
+
this.unsubscribe();
|
|
13266
|
+
this.unsubscribe = null;
|
|
13267
|
+
}
|
|
13268
|
+
this._browser = b;
|
|
13269
|
+
if (b) {
|
|
13270
|
+
this.update(b.locus);
|
|
13271
|
+
this.unsubscribe = b.on(BrowserEvent.LocusChange, ({ locus }) => {
|
|
13272
|
+
this.update(locus);
|
|
13273
|
+
});
|
|
13274
|
+
}
|
|
13275
|
+
}
|
|
13276
|
+
get browser() {
|
|
13277
|
+
return this._browser;
|
|
13278
|
+
}
|
|
13279
|
+
update(locus) {
|
|
13280
|
+
this.label.textContent = formatBpLength(locus.end - locus.start);
|
|
13281
|
+
}
|
|
13282
|
+
disconnectedCallback() {
|
|
13283
|
+
if (this.unsubscribe) {
|
|
13284
|
+
this.unsubscribe();
|
|
13285
|
+
this.unsubscribe = null;
|
|
13286
|
+
}
|
|
13287
|
+
}
|
|
13288
|
+
}
|
|
13289
|
+
|
|
13290
|
+
/**
|
|
13291
|
+
* <loom-export-controls> — SVG/PNG export buttons.
|
|
13292
|
+
*
|
|
13293
|
+
* Adds "Save SVG" and "Save PNG" buttons to the shell navbar.
|
|
13294
|
+
* Calls GenomeBrowser.saveSVGtoFile() and savePNGtoFile() respectively.
|
|
13295
|
+
*/
|
|
13296
|
+
const template$1 = document.createElement('template');
|
|
13297
|
+
template$1.innerHTML = /* html */ `
|
|
13298
|
+
<style>
|
|
13299
|
+
:host {
|
|
13300
|
+
display: inline-flex;
|
|
13301
|
+
align-items: center;
|
|
13302
|
+
}
|
|
13303
|
+
.container {
|
|
13304
|
+
display: flex;
|
|
13305
|
+
align-items: center;
|
|
13306
|
+
gap: 2px;
|
|
13307
|
+
}
|
|
13308
|
+
button {
|
|
13309
|
+
display: flex;
|
|
13310
|
+
align-items: center;
|
|
13311
|
+
justify-content: center;
|
|
13312
|
+
width: var(--loom-button-size, 24px);
|
|
13313
|
+
height: var(--loom-button-size, 24px);
|
|
13314
|
+
background: var(--loom-button-bg, white);
|
|
13315
|
+
border: var(--loom-button-border, 1px solid #b0b0b0);
|
|
13316
|
+
border-radius: var(--loom-border-radius, 4px);
|
|
13317
|
+
cursor: pointer;
|
|
13318
|
+
color: var(--loom-icon-color, #555);
|
|
13319
|
+
transition: background 0.15s;
|
|
13320
|
+
padding: 0;
|
|
13321
|
+
line-height: 1;
|
|
13322
|
+
}
|
|
13323
|
+
button:hover {
|
|
13324
|
+
background: var(--loom-button-hover, #e8e8e8);
|
|
13325
|
+
}
|
|
13326
|
+
button:active {
|
|
13327
|
+
transform: scale(0.95);
|
|
13328
|
+
}
|
|
13329
|
+
button svg {
|
|
13330
|
+
width: var(--loom-icon-size, 14px);
|
|
13331
|
+
height: var(--loom-icon-size, 14px);
|
|
13332
|
+
fill: none;
|
|
13333
|
+
stroke: currentColor;
|
|
13334
|
+
stroke-width: 2;
|
|
13335
|
+
stroke-linecap: round;
|
|
13336
|
+
stroke-linejoin: round;
|
|
13337
|
+
}
|
|
13338
|
+
</style>
|
|
13339
|
+
<div class="container">
|
|
13340
|
+
<button class="save-svg" title="Save as SVG">
|
|
13341
|
+
<svg viewBox="0 0 24 24">
|
|
13342
|
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
|
13343
|
+
<polyline points="7 10 12 15 17 10"/>
|
|
13344
|
+
<line x1="12" y1="15" x2="12" y2="3"/>
|
|
13345
|
+
</svg>
|
|
13346
|
+
</button>
|
|
13347
|
+
<button class="save-png" title="Save as PNG">
|
|
13348
|
+
<svg viewBox="0 0 24 24">
|
|
13349
|
+
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
|
|
13350
|
+
<circle cx="8.5" cy="8.5" r="1.5"/>
|
|
13351
|
+
<polyline points="21 15 16 10 5 21"/>
|
|
13352
|
+
</svg>
|
|
13353
|
+
</button>
|
|
13354
|
+
</div>
|
|
13355
|
+
`;
|
|
13356
|
+
class LoomExportControls extends HTMLElement {
|
|
13357
|
+
constructor() {
|
|
13358
|
+
super();
|
|
13359
|
+
this._browser = null;
|
|
13360
|
+
this.attachShadow({ mode: 'open' });
|
|
13361
|
+
this.shadowRoot.appendChild(template$1.content.cloneNode(true));
|
|
13362
|
+
this.shadowRoot.querySelector('.save-svg').addEventListener('click', () => {
|
|
13363
|
+
var _a;
|
|
13364
|
+
(_a = this._browser) === null || _a === void 0 ? void 0 : _a.saveSVGtoFile();
|
|
13365
|
+
});
|
|
13366
|
+
this.shadowRoot.querySelector('.save-png').addEventListener('click', () => {
|
|
13367
|
+
var _a;
|
|
13368
|
+
void ((_a = this._browser) === null || _a === void 0 ? void 0 : _a.savePNGtoFile());
|
|
13369
|
+
});
|
|
13370
|
+
}
|
|
13371
|
+
set browser(b) {
|
|
13372
|
+
this._browser = b;
|
|
13373
|
+
}
|
|
13374
|
+
get browser() {
|
|
13375
|
+
return this._browser;
|
|
13376
|
+
}
|
|
13377
|
+
}
|
|
13378
|
+
|
|
13379
|
+
/**
|
|
13380
|
+
* <loom-navbar> — Horizontal navigation bar composing locus input, window size, and zoom controls.
|
|
13381
|
+
*/
|
|
13382
|
+
// Ensure child custom elements are registered
|
|
13383
|
+
if (!customElements.get('loom-chromosome-select')) {
|
|
13384
|
+
customElements.define('loom-chromosome-select', LoomChromosomeSelect);
|
|
13385
|
+
}
|
|
13386
|
+
if (!customElements.get('loom-locus-input')) {
|
|
13387
|
+
customElements.define('loom-locus-input', LoomLocusInput);
|
|
13388
|
+
}
|
|
13389
|
+
if (!customElements.get('loom-zoom-controls')) {
|
|
13390
|
+
customElements.define('loom-zoom-controls', LoomZoomControls);
|
|
13391
|
+
}
|
|
13392
|
+
if (!customElements.get('loom-window-size')) {
|
|
13393
|
+
customElements.define('loom-window-size', LoomWindowSize);
|
|
13394
|
+
}
|
|
13395
|
+
if (!customElements.get('loom-export-controls')) {
|
|
13396
|
+
customElements.define('loom-export-controls', LoomExportControls);
|
|
13397
|
+
}
|
|
13398
|
+
const template = document.createElement('template');
|
|
13399
|
+
template.innerHTML = /* html */ `
|
|
13400
|
+
<style>
|
|
13401
|
+
:host {
|
|
13402
|
+
display: block;
|
|
13403
|
+
}
|
|
13404
|
+
.navbar {
|
|
13405
|
+
display: flex;
|
|
13406
|
+
align-items: center;
|
|
13407
|
+
justify-content: space-between;
|
|
13408
|
+
height: var(--loom-navbar-height, 32px);
|
|
13409
|
+
padding: var(--loom-navbar-padding, 0 8px);
|
|
13410
|
+
background: var(--loom-navbar-bg, #f3f3f3);
|
|
13411
|
+
border-bottom: var(--loom-border, 1px solid #ccc);
|
|
13412
|
+
box-sizing: border-box;
|
|
13413
|
+
gap: var(--loom-gap, 8px);
|
|
13414
|
+
}
|
|
13415
|
+
.left {
|
|
13416
|
+
display: flex;
|
|
13417
|
+
align-items: center;
|
|
13418
|
+
gap: var(--loom-gap, 8px);
|
|
13419
|
+
flex: 1;
|
|
13420
|
+
min-width: 0;
|
|
13421
|
+
}
|
|
13422
|
+
.right {
|
|
13423
|
+
display: flex;
|
|
13424
|
+
align-items: center;
|
|
13425
|
+
gap: var(--loom-gap, 8px);
|
|
13426
|
+
flex-shrink: 0;
|
|
13427
|
+
}
|
|
13428
|
+
</style>
|
|
13429
|
+
<div class="navbar">
|
|
13430
|
+
<div class="left">
|
|
13431
|
+
<loom-chromosome-select></loom-chromosome-select>
|
|
13432
|
+
<loom-locus-input></loom-locus-input>
|
|
13433
|
+
<loom-window-size></loom-window-size>
|
|
13434
|
+
</div>
|
|
13435
|
+
<div class="right">
|
|
13436
|
+
<loom-export-controls></loom-export-controls>
|
|
13437
|
+
<loom-zoom-controls></loom-zoom-controls>
|
|
13438
|
+
</div>
|
|
13439
|
+
</div>
|
|
13440
|
+
`;
|
|
13441
|
+
class LoomNavbar extends HTMLElement {
|
|
13442
|
+
constructor() {
|
|
13443
|
+
super();
|
|
13444
|
+
this._browser = null;
|
|
13445
|
+
this.attachShadow({ mode: 'open' });
|
|
13446
|
+
this.shadowRoot.appendChild(template.content.cloneNode(true));
|
|
13447
|
+
this.chromSelect = this.shadowRoot.querySelector('loom-chromosome-select');
|
|
13448
|
+
this.locusInput = this.shadowRoot.querySelector('loom-locus-input');
|
|
13449
|
+
this.zoomControls = this.shadowRoot.querySelector('loom-zoom-controls');
|
|
13450
|
+
this.windowSize = this.shadowRoot.querySelector('loom-window-size');
|
|
13451
|
+
this.exportControls = this.shadowRoot.querySelector('loom-export-controls');
|
|
13452
|
+
}
|
|
13453
|
+
set browser(b) {
|
|
13454
|
+
this._browser = b;
|
|
13455
|
+
this.chromSelect.browser = b;
|
|
13456
|
+
this.locusInput.browser = b;
|
|
13457
|
+
this.zoomControls.browser = b;
|
|
13458
|
+
this.windowSize.browser = b;
|
|
13459
|
+
this.exportControls.browser = b;
|
|
13460
|
+
}
|
|
13461
|
+
get browser() {
|
|
13462
|
+
return this._browser;
|
|
13463
|
+
}
|
|
13464
|
+
}
|
|
13465
|
+
|
|
13466
|
+
/**
|
|
13467
|
+
* Ensures all Loom UI web components are registered as custom elements.
|
|
13468
|
+
* Safe to call multiple times — each element is only registered once.
|
|
13469
|
+
*/
|
|
13470
|
+
let registered = false;
|
|
13471
|
+
function ensureRegistered() {
|
|
13472
|
+
if (registered || typeof customElements === 'undefined')
|
|
13473
|
+
return;
|
|
13474
|
+
registered = true;
|
|
13475
|
+
const elements = [
|
|
13476
|
+
['loom-chromosome-select', LoomChromosomeSelect],
|
|
13477
|
+
['loom-locus-input', LoomLocusInput],
|
|
13478
|
+
['loom-zoom-controls', LoomZoomControls],
|
|
13479
|
+
['loom-window-size', LoomWindowSize],
|
|
13480
|
+
['loom-export-controls', LoomExportControls],
|
|
13481
|
+
['loom-navbar', LoomNavbar],
|
|
13482
|
+
];
|
|
13483
|
+
for (const [name, ctor] of elements) {
|
|
13484
|
+
if (!customElements.get(name)) {
|
|
13485
|
+
customElements.define(name, ctor);
|
|
13486
|
+
}
|
|
13487
|
+
}
|
|
13488
|
+
}
|
|
13489
|
+
|
|
13490
|
+
function ChromosomeSelect({ browser: browserProp }) {
|
|
13491
|
+
const contextBrowser = useGenomeBrowser();
|
|
13492
|
+
const browser = browserProp !== null && browserProp !== void 0 ? browserProp : contextBrowser;
|
|
13493
|
+
const ref = useRef(null);
|
|
13494
|
+
ensureRegistered();
|
|
13495
|
+
useEffect(() => {
|
|
13496
|
+
if (ref.current)
|
|
13497
|
+
ref.current.browser = browser;
|
|
13498
|
+
}, [browser]);
|
|
13499
|
+
return jsx("loom-chromosome-select", { ref: ref });
|
|
13500
|
+
}
|
|
13501
|
+
|
|
13502
|
+
function LocusInput({ browser: browserProp }) {
|
|
13503
|
+
const contextBrowser = useGenomeBrowser();
|
|
13504
|
+
const browser = browserProp !== null && browserProp !== void 0 ? browserProp : contextBrowser;
|
|
13505
|
+
const ref = useRef(null);
|
|
13506
|
+
ensureRegistered();
|
|
13507
|
+
useEffect(() => {
|
|
13508
|
+
if (ref.current)
|
|
13509
|
+
ref.current.browser = browser;
|
|
13510
|
+
}, [browser]);
|
|
13511
|
+
return jsx("loom-locus-input", { ref: ref });
|
|
13512
|
+
}
|
|
13513
|
+
|
|
13514
|
+
function ZoomControls({ browser: browserProp }) {
|
|
13515
|
+
const contextBrowser = useGenomeBrowser();
|
|
13516
|
+
const browser = browserProp !== null && browserProp !== void 0 ? browserProp : contextBrowser;
|
|
13517
|
+
const ref = useRef(null);
|
|
13518
|
+
ensureRegistered();
|
|
13519
|
+
useEffect(() => {
|
|
13520
|
+
if (ref.current)
|
|
13521
|
+
ref.current.browser = browser;
|
|
13522
|
+
}, [browser]);
|
|
13523
|
+
return jsx("loom-zoom-controls", { ref: ref });
|
|
13524
|
+
}
|
|
13525
|
+
|
|
13526
|
+
function WindowSize({ browser: browserProp }) {
|
|
13527
|
+
const contextBrowser = useGenomeBrowser();
|
|
13528
|
+
const browser = browserProp !== null && browserProp !== void 0 ? browserProp : contextBrowser;
|
|
13529
|
+
const ref = useRef(null);
|
|
13530
|
+
ensureRegistered();
|
|
13531
|
+
useEffect(() => {
|
|
13532
|
+
if (ref.current)
|
|
13533
|
+
ref.current.browser = browser;
|
|
13534
|
+
}, [browser]);
|
|
13535
|
+
return jsx("loom-window-size", { ref: ref });
|
|
13536
|
+
}
|
|
13537
|
+
|
|
13538
|
+
function ExportControls({ browser: browserProp }) {
|
|
13539
|
+
const contextBrowser = useGenomeBrowser();
|
|
13540
|
+
const browser = browserProp !== null && browserProp !== void 0 ? browserProp : contextBrowser;
|
|
13541
|
+
const ref = useRef(null);
|
|
13542
|
+
ensureRegistered();
|
|
13543
|
+
useEffect(() => {
|
|
13544
|
+
if (ref.current)
|
|
13545
|
+
ref.current.browser = browser;
|
|
13546
|
+
}, [browser]);
|
|
13547
|
+
return jsx("loom-export-controls", { ref: ref });
|
|
13548
|
+
}
|
|
13549
|
+
|
|
13550
|
+
/**
|
|
13551
|
+
* CSS custom property themes for UI shell web components.
|
|
13552
|
+
*
|
|
13553
|
+
* Themes are injected as <style> blocks into Shadow DOM roots.
|
|
13554
|
+
* Components reference CSS custom properties (--loom-*) for all visual styling.
|
|
13555
|
+
*/
|
|
13556
|
+
/** Classic theme — matches igv.js navbar look. */
|
|
13557
|
+
const classicThemeCSS = /* css */ `
|
|
13558
|
+
:host {
|
|
13559
|
+
--loom-navbar-bg: #f3f3f3;
|
|
13560
|
+
--loom-navbar-height: 32px;
|
|
13561
|
+
--loom-navbar-padding: 0 8px;
|
|
13562
|
+
--loom-font: 12px Arial, sans-serif;
|
|
13563
|
+
--loom-font-small: 11px Arial, sans-serif;
|
|
13564
|
+
--loom-text-color: #333;
|
|
13565
|
+
--loom-text-muted: #737373;
|
|
13566
|
+
--loom-border: 1px solid #ccc;
|
|
13567
|
+
--loom-border-radius: 4px;
|
|
13568
|
+
--loom-button-bg: white;
|
|
13569
|
+
--loom-button-hover: #e8e8e8;
|
|
13570
|
+
--loom-button-border: 1px solid #b0b0b0;
|
|
13571
|
+
--loom-button-size: 24px;
|
|
13572
|
+
--loom-input-bg: white;
|
|
13573
|
+
--loom-input-border: 1px solid #b0b0b0;
|
|
13574
|
+
--loom-input-focus-border: 1px solid #4A90D9;
|
|
13575
|
+
--loom-input-width: 220px;
|
|
13576
|
+
--loom-input-height: 22px;
|
|
13577
|
+
--loom-accent: #4A90D9;
|
|
13578
|
+
--loom-icon-color: #555;
|
|
13579
|
+
--loom-icon-size: 14px;
|
|
13580
|
+
--loom-gap: 8px;
|
|
13581
|
+
}
|
|
13582
|
+
`;
|
|
13583
|
+
/** Modern theme — softer colors, rounded corners, taller navbar. */
|
|
13584
|
+
const modernThemeCSS = /* css */ `
|
|
13585
|
+
:host {
|
|
13586
|
+
--loom-navbar-bg: #fafbfc;
|
|
13587
|
+
--loom-navbar-height: 40px;
|
|
13588
|
+
--loom-navbar-padding: 0 12px;
|
|
13589
|
+
--loom-font: 13px Inter, system-ui, -apple-system, sans-serif;
|
|
13590
|
+
--loom-font-small: 11px Inter, system-ui, -apple-system, sans-serif;
|
|
13591
|
+
--loom-text-color: #1a1a1a;
|
|
13592
|
+
--loom-text-muted: #6b7280;
|
|
13593
|
+
--loom-border: 1px solid rgba(0, 0, 0, 0.08);
|
|
13594
|
+
--loom-border-radius: 8px;
|
|
13595
|
+
--loom-button-bg: white;
|
|
13596
|
+
--loom-button-hover: #f0f4ff;
|
|
13597
|
+
--loom-button-border: 1px solid rgba(0, 0, 0, 0.1);
|
|
13598
|
+
--loom-button-size: 28px;
|
|
13599
|
+
--loom-input-bg: white;
|
|
13600
|
+
--loom-input-border: 1px solid rgba(0, 0, 0, 0.12);
|
|
13601
|
+
--loom-input-focus-border: 1px solid #4A90D9;
|
|
13602
|
+
--loom-input-width: 260px;
|
|
13603
|
+
--loom-input-height: 28px;
|
|
13604
|
+
--loom-accent: #4A90D9;
|
|
13605
|
+
--loom-icon-color: #6b7280;
|
|
13606
|
+
--loom-icon-size: 16px;
|
|
13607
|
+
--loom-gap: 10px;
|
|
13608
|
+
}
|
|
13609
|
+
`;
|
|
13610
|
+
/** Dark theme — dark backgrounds, light text, high contrast. */
|
|
13611
|
+
const darkThemeCSS = /* css */ `
|
|
13612
|
+
:host {
|
|
13613
|
+
--loom-shell-bg: #16162a;
|
|
13614
|
+
--loom-navbar-bg: #1a1a2e;
|
|
13615
|
+
--loom-navbar-height: 36px;
|
|
13616
|
+
--loom-navbar-padding: 0 10px;
|
|
13617
|
+
--loom-font: 12px Arial, sans-serif;
|
|
13618
|
+
--loom-font-small: 11px Arial, sans-serif;
|
|
13619
|
+
--loom-text-color: #e0e0e0;
|
|
13620
|
+
--loom-text-muted: #888;
|
|
13621
|
+
--loom-border: 1px solid #333;
|
|
13622
|
+
--loom-border-radius: 4px;
|
|
13623
|
+
--loom-button-bg: #2a2a3e;
|
|
13624
|
+
--loom-button-hover: #3a3a50;
|
|
13625
|
+
--loom-button-border: 1px solid #444;
|
|
13626
|
+
--loom-button-size: 24px;
|
|
13627
|
+
--loom-input-bg: #2a2a3e;
|
|
13628
|
+
--loom-input-border: 1px solid #444;
|
|
13629
|
+
--loom-input-focus-border: 1px solid #6a9fd9;
|
|
13630
|
+
--loom-input-width: 220px;
|
|
13631
|
+
--loom-input-height: 22px;
|
|
13632
|
+
--loom-accent: #6a9fd9;
|
|
13633
|
+
--loom-icon-color: #b0b0b0;
|
|
13634
|
+
--loom-icon-size: 14px;
|
|
13635
|
+
--loom-gap: 8px;
|
|
13636
|
+
}
|
|
13637
|
+
`;
|
|
13638
|
+
function getThemeCSS(theme) {
|
|
13639
|
+
if (theme === 'modern')
|
|
13640
|
+
return modernThemeCSS;
|
|
13641
|
+
if (theme === 'dark')
|
|
13642
|
+
return darkThemeCSS;
|
|
13643
|
+
return classicThemeCSS;
|
|
13644
|
+
}
|
|
13645
|
+
|
|
13646
|
+
function Navbar({ browser: browserProp, theme: themeProp }) {
|
|
13647
|
+
var _a;
|
|
13648
|
+
const ctx = useContext(GenomeBrowserContext);
|
|
13649
|
+
const contextBrowser = useGenomeBrowser();
|
|
13650
|
+
const browser = browserProp !== null && browserProp !== void 0 ? browserProp : contextBrowser;
|
|
13651
|
+
const theme = (_a = themeProp !== null && themeProp !== void 0 ? themeProp : ctx === null || ctx === void 0 ? void 0 : ctx.shellTheme) !== null && _a !== void 0 ? _a : 'modern';
|
|
13652
|
+
const ref = useRef(null);
|
|
13653
|
+
const styleRef = useRef(null);
|
|
13654
|
+
ensureRegistered();
|
|
13655
|
+
useEffect(() => {
|
|
13656
|
+
if (ref.current)
|
|
13657
|
+
ref.current.browser = browser;
|
|
13658
|
+
}, [browser]);
|
|
13659
|
+
// Inject shell theme CSS into the navbar's shadow root
|
|
13660
|
+
useEffect(() => {
|
|
13661
|
+
const el = ref.current;
|
|
13662
|
+
if (!(el === null || el === void 0 ? void 0 : el.shadowRoot))
|
|
13663
|
+
return;
|
|
13664
|
+
if (!styleRef.current) {
|
|
13665
|
+
styleRef.current = document.createElement('style');
|
|
13666
|
+
el.shadowRoot.prepend(styleRef.current);
|
|
13667
|
+
}
|
|
13668
|
+
styleRef.current.textContent = getThemeCSS(theme);
|
|
13669
|
+
}, [theme]);
|
|
13670
|
+
return jsx("loom-navbar", { ref: ref });
|
|
13671
|
+
}
|
|
13672
|
+
|
|
12725
13673
|
/**
|
|
12726
13674
|
* <loom-context-menu> — Right-click context menu Web Component.
|
|
12727
13675
|
*
|
|
@@ -13287,4 +14235,4 @@ var LoomInputDialog$1 = /*#__PURE__*/Object.freeze({
|
|
|
13287
14235
|
LoomInputDialog: LoomInputDialog
|
|
13288
14236
|
});
|
|
13289
14237
|
|
|
13290
|
-
export { BedTrack, GeneTrack, GenomeBrowserContext, GtxTrack, InteractionTrack, LoomBrowser, RulerTrack, SequenceTrack, WigTrack, useBrowserEvent, useGenomeBrowser, useLocus };
|
|
14238
|
+
export { BedTrack, ChromosomeSelect, ExportControls, GeneTrack, GenomeBrowserContext, GtxTrack, InteractionTrack, LocusInput, LoomBrowser, Navbar, RulerTrack, SequenceTrack, WigTrack, WindowSize, ZoomControls, useBrowserEvent, useGenomeBrowser, useLocus };
|