loom-browser 0.0.4 → 0.0.5
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 +681 -4
- package/dist/loom-react.esm.min.js +1 -1
- package/dist/loom-react.esm.min.js.map +1 -1
- package/dist/loom.esm.js +9 -1
- package/dist/loom.esm.min.js +1 -1
- package/dist/loom.esm.min.js.map +1 -1
- package/dist/loom.js +9 -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/react/index.d.ts +2 -0
- 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 +7 -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/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
|
*
|
|
@@ -11210,6 +11239,13 @@ class GenomeBrowser extends HeadlessGenomeBrowser {
|
|
|
11210
11239
|
this.events.on(BrowserEvent.DataLoaded, ({ track }) => {
|
|
11211
11240
|
this.updateAxisContent(track);
|
|
11212
11241
|
});
|
|
11242
|
+
// Repaint axis canvases when theme changes (track canvases update via
|
|
11243
|
+
// track.setTheme(), but axis sidebar is managed here in GenomeBrowser).
|
|
11244
|
+
this.events.on(BrowserEvent.ThemeChanged, () => {
|
|
11245
|
+
for (const mt of this.managedTracks) {
|
|
11246
|
+
this.updateAxisContent(mt.track);
|
|
11247
|
+
}
|
|
11248
|
+
});
|
|
11213
11249
|
// Re-render ROI overlays when ROIs change or viewport moves
|
|
11214
11250
|
this.events.on(BrowserEvent.ROIAdded, () => this.renderROIOverlays());
|
|
11215
11251
|
this.events.on(BrowserEvent.ROIRemoved, () => this.renderROIOverlays());
|
|
@@ -11412,9 +11448,10 @@ class GenomeBrowser extends HeadlessGenomeBrowser {
|
|
|
11412
11448
|
const { axisDiv } = entry;
|
|
11413
11449
|
const rawInfo = (_a = track.getAxisInfo) === null || _a === void 0 ? void 0 : _a.call(track);
|
|
11414
11450
|
if (!rawInfo) {
|
|
11415
|
-
// No axis info —
|
|
11451
|
+
// No axis info — keep the column for alignment but clear content
|
|
11416
11452
|
axisDiv.innerHTML = "";
|
|
11417
11453
|
axisDiv.style.borderRight = "none";
|
|
11454
|
+
axisDiv.style.backgroundColor = this.theme.palette.background;
|
|
11418
11455
|
entry.axisCanvas = null;
|
|
11419
11456
|
return;
|
|
11420
11457
|
}
|
|
@@ -12578,7 +12615,7 @@ const LoomBrowser = forwardRef(function LoomBrowser(props, ref) {
|
|
|
12578
12615
|
detachRemote() { var _a; (_a = browserRef.current) === null || _a === void 0 ? void 0 : _a.detachRemote(); },
|
|
12579
12616
|
}), [browser]);
|
|
12580
12617
|
const ctxValue = useMemo(() => ({ browser }), [browser]);
|
|
12581
|
-
return (
|
|
12618
|
+
return (jsx(GenomeBrowserContext.Provider, { value: ctxValue, children: jsxs("div", { className: className, style: style, children: [browser && children, jsx("div", { ref: containerRef })] }) }));
|
|
12582
12619
|
});
|
|
12583
12620
|
|
|
12584
12621
|
/**
|
|
@@ -12722,6 +12759,646 @@ function GtxTrack({ url, experimentId, config, height, background, windowFunctio
|
|
|
12722
12759
|
return null;
|
|
12723
12760
|
}
|
|
12724
12761
|
|
|
12762
|
+
/**
|
|
12763
|
+
* <loom-chromosome-select> — Chromosome dropdown for quick navigation.
|
|
12764
|
+
*
|
|
12765
|
+
* Lists main chromosomes (chr1-22, X, Y, M) plus "All" for whole genome view.
|
|
12766
|
+
* Updates automatically when the browser locus changes.
|
|
12767
|
+
*/
|
|
12768
|
+
const template$5 = document.createElement('template');
|
|
12769
|
+
template$5.innerHTML = /* html */ `
|
|
12770
|
+
<style>
|
|
12771
|
+
:host {
|
|
12772
|
+
display: inline-flex;
|
|
12773
|
+
align-items: center;
|
|
12774
|
+
}
|
|
12775
|
+
select {
|
|
12776
|
+
font: var(--loom-font, 12px Arial, sans-serif);
|
|
12777
|
+
color: var(--loom-text-color, #333);
|
|
12778
|
+
background: var(--loom-input-bg, white);
|
|
12779
|
+
border: var(--loom-input-border, 1px solid #b0b0b0);
|
|
12780
|
+
border-radius: var(--loom-border-radius, 4px);
|
|
12781
|
+
height: var(--loom-input-height, 22px);
|
|
12782
|
+
padding: 0 4px;
|
|
12783
|
+
box-sizing: border-box;
|
|
12784
|
+
outline: none;
|
|
12785
|
+
cursor: pointer;
|
|
12786
|
+
transition: border-color 0.15s;
|
|
12787
|
+
}
|
|
12788
|
+
select:focus {
|
|
12789
|
+
border: var(--loom-input-focus-border, 1px solid #4A90D9);
|
|
12790
|
+
}
|
|
12791
|
+
</style>
|
|
12792
|
+
<select></select>
|
|
12793
|
+
`;
|
|
12794
|
+
class LoomChromosomeSelect extends HTMLElement {
|
|
12795
|
+
constructor() {
|
|
12796
|
+
super();
|
|
12797
|
+
this.unsubscribe = null;
|
|
12798
|
+
this._browser = null;
|
|
12799
|
+
this.attachShadow({ mode: 'open' });
|
|
12800
|
+
this.shadowRoot.appendChild(template$5.content.cloneNode(true));
|
|
12801
|
+
this.select = this.shadowRoot.querySelector('select');
|
|
12802
|
+
this.select.addEventListener('change', () => {
|
|
12803
|
+
if (!this._browser)
|
|
12804
|
+
return;
|
|
12805
|
+
const value = this.select.value;
|
|
12806
|
+
if (value === 'all') {
|
|
12807
|
+
this._browser.search('all');
|
|
12808
|
+
}
|
|
12809
|
+
else {
|
|
12810
|
+
const chromSizes = this._browser.chromSizes;
|
|
12811
|
+
if (chromSizes && chromSizes[value] != null) {
|
|
12812
|
+
this._browser.setLocus({ chr: value, start: 0, end: chromSizes[value] });
|
|
12813
|
+
}
|
|
12814
|
+
}
|
|
12815
|
+
});
|
|
12816
|
+
}
|
|
12817
|
+
set browser(b) {
|
|
12818
|
+
if (this.unsubscribe) {
|
|
12819
|
+
this.unsubscribe();
|
|
12820
|
+
this.unsubscribe = null;
|
|
12821
|
+
}
|
|
12822
|
+
this._browser = b;
|
|
12823
|
+
if (b) {
|
|
12824
|
+
this.populate(b);
|
|
12825
|
+
this.updateSelection(b.locus.chr);
|
|
12826
|
+
this.unsubscribe = b.on(BrowserEvent.LocusChange, ({ locus }) => {
|
|
12827
|
+
this.updateSelection(locus.chr);
|
|
12828
|
+
});
|
|
12829
|
+
}
|
|
12830
|
+
}
|
|
12831
|
+
get browser() {
|
|
12832
|
+
return this._browser;
|
|
12833
|
+
}
|
|
12834
|
+
populate(browser) {
|
|
12835
|
+
this.select.innerHTML = '';
|
|
12836
|
+
const chromSizes = browser.chromSizes;
|
|
12837
|
+
if (!chromSizes)
|
|
12838
|
+
return;
|
|
12839
|
+
// "All" option for whole genome view
|
|
12840
|
+
if (browser.cumulativeOffsets) {
|
|
12841
|
+
const opt = document.createElement('option');
|
|
12842
|
+
opt.value = 'all';
|
|
12843
|
+
opt.textContent = 'All';
|
|
12844
|
+
this.select.appendChild(opt);
|
|
12845
|
+
}
|
|
12846
|
+
// Main chromosomes in order
|
|
12847
|
+
const names = mainChromosomeNames(chromSizes);
|
|
12848
|
+
for (const name of names) {
|
|
12849
|
+
const opt = document.createElement('option');
|
|
12850
|
+
opt.value = name;
|
|
12851
|
+
opt.textContent = name;
|
|
12852
|
+
this.select.appendChild(opt);
|
|
12853
|
+
}
|
|
12854
|
+
}
|
|
12855
|
+
updateSelection(chr) {
|
|
12856
|
+
if (isWholeGenomeView({ chr, start: 0, end: 0 })) {
|
|
12857
|
+
this.select.value = 'all';
|
|
12858
|
+
}
|
|
12859
|
+
else {
|
|
12860
|
+
this.select.value = chr;
|
|
12861
|
+
}
|
|
12862
|
+
}
|
|
12863
|
+
disconnectedCallback() {
|
|
12864
|
+
if (this.unsubscribe) {
|
|
12865
|
+
this.unsubscribe();
|
|
12866
|
+
this.unsubscribe = null;
|
|
12867
|
+
}
|
|
12868
|
+
}
|
|
12869
|
+
}
|
|
12870
|
+
|
|
12871
|
+
/**
|
|
12872
|
+
* <loom-locus-input> — Locus search/display input.
|
|
12873
|
+
*
|
|
12874
|
+
* Shows the current locus as a formatted string. User can type a new locus
|
|
12875
|
+
* and press Enter to navigate. Updates automatically on locuschange events.
|
|
12876
|
+
*/
|
|
12877
|
+
const template$4 = document.createElement('template');
|
|
12878
|
+
template$4.innerHTML = /* html */ `
|
|
12879
|
+
<style>
|
|
12880
|
+
:host {
|
|
12881
|
+
display: inline-flex;
|
|
12882
|
+
align-items: center;
|
|
12883
|
+
}
|
|
12884
|
+
.container {
|
|
12885
|
+
display: flex;
|
|
12886
|
+
align-items: center;
|
|
12887
|
+
gap: 4px;
|
|
12888
|
+
}
|
|
12889
|
+
input {
|
|
12890
|
+
font: var(--loom-font, 12px Arial, sans-serif);
|
|
12891
|
+
color: var(--loom-text-color, #333);
|
|
12892
|
+
background: var(--loom-input-bg, white);
|
|
12893
|
+
border: var(--loom-input-border, 1px solid #b0b0b0);
|
|
12894
|
+
border-radius: var(--loom-border-radius, 4px);
|
|
12895
|
+
width: var(--loom-input-width, 220px);
|
|
12896
|
+
height: var(--loom-input-height, 22px);
|
|
12897
|
+
padding: 0 8px;
|
|
12898
|
+
box-sizing: border-box;
|
|
12899
|
+
outline: none;
|
|
12900
|
+
transition: border-color 0.15s;
|
|
12901
|
+
}
|
|
12902
|
+
input:focus {
|
|
12903
|
+
border: var(--loom-input-focus-border, 1px solid #4A90D9);
|
|
12904
|
+
}
|
|
12905
|
+
input::placeholder {
|
|
12906
|
+
color: var(--loom-text-muted, #737373);
|
|
12907
|
+
}
|
|
12908
|
+
.search-btn {
|
|
12909
|
+
display: flex;
|
|
12910
|
+
align-items: center;
|
|
12911
|
+
justify-content: center;
|
|
12912
|
+
width: var(--loom-button-size, 24px);
|
|
12913
|
+
height: var(--loom-button-size, 24px);
|
|
12914
|
+
background: var(--loom-button-bg, white);
|
|
12915
|
+
border: var(--loom-button-border, 1px solid #b0b0b0);
|
|
12916
|
+
border-radius: var(--loom-border-radius, 4px);
|
|
12917
|
+
cursor: pointer;
|
|
12918
|
+
color: var(--loom-icon-color, #555);
|
|
12919
|
+
transition: background 0.15s;
|
|
12920
|
+
padding: 0;
|
|
12921
|
+
line-height: 1;
|
|
12922
|
+
}
|
|
12923
|
+
.search-btn:hover {
|
|
12924
|
+
background: var(--loom-button-hover, #e8e8e8);
|
|
12925
|
+
}
|
|
12926
|
+
.search-btn svg {
|
|
12927
|
+
width: var(--loom-icon-size, 14px);
|
|
12928
|
+
height: var(--loom-icon-size, 14px);
|
|
12929
|
+
fill: none;
|
|
12930
|
+
stroke: currentColor;
|
|
12931
|
+
stroke-width: 2;
|
|
12932
|
+
stroke-linecap: round;
|
|
12933
|
+
stroke-linejoin: round;
|
|
12934
|
+
}
|
|
12935
|
+
</style>
|
|
12936
|
+
<div class="container">
|
|
12937
|
+
<input type="text" placeholder="Locus Search" spellcheck="false" />
|
|
12938
|
+
<button class="search-btn" title="Go to locus">
|
|
12939
|
+
<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>
|
|
12940
|
+
</button>
|
|
12941
|
+
</div>
|
|
12942
|
+
`;
|
|
12943
|
+
class LoomLocusInput extends HTMLElement {
|
|
12944
|
+
constructor() {
|
|
12945
|
+
super();
|
|
12946
|
+
this.unsubscribe = null;
|
|
12947
|
+
this._browser = null;
|
|
12948
|
+
this.attachShadow({ mode: 'open' });
|
|
12949
|
+
this.shadowRoot.appendChild(template$4.content.cloneNode(true));
|
|
12950
|
+
this.input = this.shadowRoot.querySelector('input');
|
|
12951
|
+
this.input.addEventListener('keydown', (e) => {
|
|
12952
|
+
if (e.key === 'Enter') {
|
|
12953
|
+
this.doSearch();
|
|
12954
|
+
}
|
|
12955
|
+
});
|
|
12956
|
+
this.shadowRoot.querySelector('.search-btn').addEventListener('click', () => {
|
|
12957
|
+
this.doSearch();
|
|
12958
|
+
});
|
|
12959
|
+
}
|
|
12960
|
+
set browser(b) {
|
|
12961
|
+
if (this.unsubscribe) {
|
|
12962
|
+
this.unsubscribe();
|
|
12963
|
+
this.unsubscribe = null;
|
|
12964
|
+
}
|
|
12965
|
+
this._browser = b;
|
|
12966
|
+
if (b) {
|
|
12967
|
+
this.updateDisplay(b.locus);
|
|
12968
|
+
this.unsubscribe = b.on(BrowserEvent.LocusChange, ({ locus }) => {
|
|
12969
|
+
this.updateDisplay(locus);
|
|
12970
|
+
});
|
|
12971
|
+
}
|
|
12972
|
+
}
|
|
12973
|
+
get browser() {
|
|
12974
|
+
return this._browser;
|
|
12975
|
+
}
|
|
12976
|
+
updateDisplay(locus) {
|
|
12977
|
+
if (document.activeElement !== this.input && this.shadowRoot.activeElement !== this.input) {
|
|
12978
|
+
this.input.value = formatLocus(locus);
|
|
12979
|
+
}
|
|
12980
|
+
}
|
|
12981
|
+
doSearch() {
|
|
12982
|
+
if (!this._browser)
|
|
12983
|
+
return;
|
|
12984
|
+
const value = this.input.value.trim();
|
|
12985
|
+
if (value) {
|
|
12986
|
+
this._browser.search(value);
|
|
12987
|
+
}
|
|
12988
|
+
this.input.blur();
|
|
12989
|
+
}
|
|
12990
|
+
disconnectedCallback() {
|
|
12991
|
+
if (this.unsubscribe) {
|
|
12992
|
+
this.unsubscribe();
|
|
12993
|
+
this.unsubscribe = null;
|
|
12994
|
+
}
|
|
12995
|
+
}
|
|
12996
|
+
}
|
|
12997
|
+
|
|
12998
|
+
/**
|
|
12999
|
+
* <loom-zoom-controls> — Zoom in/out buttons.
|
|
13000
|
+
*/
|
|
13001
|
+
const template$3 = document.createElement('template');
|
|
13002
|
+
template$3.innerHTML = /* html */ `
|
|
13003
|
+
<style>
|
|
13004
|
+
:host {
|
|
13005
|
+
display: inline-flex;
|
|
13006
|
+
align-items: center;
|
|
13007
|
+
}
|
|
13008
|
+
.container {
|
|
13009
|
+
display: flex;
|
|
13010
|
+
align-items: center;
|
|
13011
|
+
gap: 2px;
|
|
13012
|
+
}
|
|
13013
|
+
button {
|
|
13014
|
+
display: flex;
|
|
13015
|
+
align-items: center;
|
|
13016
|
+
justify-content: center;
|
|
13017
|
+
width: var(--loom-button-size, 24px);
|
|
13018
|
+
height: var(--loom-button-size, 24px);
|
|
13019
|
+
background: var(--loom-button-bg, white);
|
|
13020
|
+
border: var(--loom-button-border, 1px solid #b0b0b0);
|
|
13021
|
+
border-radius: var(--loom-border-radius, 4px);
|
|
13022
|
+
cursor: pointer;
|
|
13023
|
+
color: var(--loom-icon-color, #555);
|
|
13024
|
+
transition: background 0.15s;
|
|
13025
|
+
padding: 0;
|
|
13026
|
+
line-height: 1;
|
|
13027
|
+
}
|
|
13028
|
+
button:hover {
|
|
13029
|
+
background: var(--loom-button-hover, #e8e8e8);
|
|
13030
|
+
}
|
|
13031
|
+
button:active {
|
|
13032
|
+
transform: scale(0.95);
|
|
13033
|
+
}
|
|
13034
|
+
button svg {
|
|
13035
|
+
width: var(--loom-icon-size, 14px);
|
|
13036
|
+
height: var(--loom-icon-size, 14px);
|
|
13037
|
+
fill: none;
|
|
13038
|
+
stroke: currentColor;
|
|
13039
|
+
stroke-width: 2;
|
|
13040
|
+
stroke-linecap: round;
|
|
13041
|
+
}
|
|
13042
|
+
</style>
|
|
13043
|
+
<div class="container">
|
|
13044
|
+
<button class="zoom-out" title="Zoom out">
|
|
13045
|
+
<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><line x1="8" y1="12" x2="16" y2="12"/></svg>
|
|
13046
|
+
</button>
|
|
13047
|
+
<button class="zoom-in" title="Zoom in">
|
|
13048
|
+
<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>
|
|
13049
|
+
</button>
|
|
13050
|
+
</div>
|
|
13051
|
+
`;
|
|
13052
|
+
class LoomZoomControls extends HTMLElement {
|
|
13053
|
+
constructor() {
|
|
13054
|
+
super();
|
|
13055
|
+
this._browser = null;
|
|
13056
|
+
this.attachShadow({ mode: 'open' });
|
|
13057
|
+
this.shadowRoot.appendChild(template$3.content.cloneNode(true));
|
|
13058
|
+
this.shadowRoot.querySelector('.zoom-out').addEventListener('click', () => {
|
|
13059
|
+
var _a;
|
|
13060
|
+
(_a = this._browser) === null || _a === void 0 ? void 0 : _a.zoomOut();
|
|
13061
|
+
});
|
|
13062
|
+
this.shadowRoot.querySelector('.zoom-in').addEventListener('click', () => {
|
|
13063
|
+
var _a;
|
|
13064
|
+
(_a = this._browser) === null || _a === void 0 ? void 0 : _a.zoomIn();
|
|
13065
|
+
});
|
|
13066
|
+
}
|
|
13067
|
+
set browser(b) {
|
|
13068
|
+
this._browser = b;
|
|
13069
|
+
}
|
|
13070
|
+
get browser() {
|
|
13071
|
+
return this._browser;
|
|
13072
|
+
}
|
|
13073
|
+
}
|
|
13074
|
+
|
|
13075
|
+
/**
|
|
13076
|
+
* <loom-window-size> — Read-only display of current viewport bp range.
|
|
13077
|
+
*/
|
|
13078
|
+
const template$2 = document.createElement('template');
|
|
13079
|
+
template$2.innerHTML = /* html */ `
|
|
13080
|
+
<style>
|
|
13081
|
+
:host {
|
|
13082
|
+
display: inline-flex;
|
|
13083
|
+
align-items: center;
|
|
13084
|
+
}
|
|
13085
|
+
.label {
|
|
13086
|
+
font: var(--loom-font-small, 11px Arial, sans-serif);
|
|
13087
|
+
color: var(--loom-text-muted, #737373);
|
|
13088
|
+
white-space: nowrap;
|
|
13089
|
+
user-select: none;
|
|
13090
|
+
}
|
|
13091
|
+
</style>
|
|
13092
|
+
<span class="label"></span>
|
|
13093
|
+
`;
|
|
13094
|
+
class LoomWindowSize extends HTMLElement {
|
|
13095
|
+
constructor() {
|
|
13096
|
+
super();
|
|
13097
|
+
this.unsubscribe = null;
|
|
13098
|
+
this._browser = null;
|
|
13099
|
+
this.attachShadow({ mode: 'open' });
|
|
13100
|
+
this.shadowRoot.appendChild(template$2.content.cloneNode(true));
|
|
13101
|
+
this.label = this.shadowRoot.querySelector('.label');
|
|
13102
|
+
}
|
|
13103
|
+
set browser(b) {
|
|
13104
|
+
if (this.unsubscribe) {
|
|
13105
|
+
this.unsubscribe();
|
|
13106
|
+
this.unsubscribe = null;
|
|
13107
|
+
}
|
|
13108
|
+
this._browser = b;
|
|
13109
|
+
if (b) {
|
|
13110
|
+
this.update(b.locus);
|
|
13111
|
+
this.unsubscribe = b.on(BrowserEvent.LocusChange, ({ locus }) => {
|
|
13112
|
+
this.update(locus);
|
|
13113
|
+
});
|
|
13114
|
+
}
|
|
13115
|
+
}
|
|
13116
|
+
get browser() {
|
|
13117
|
+
return this._browser;
|
|
13118
|
+
}
|
|
13119
|
+
update(locus) {
|
|
13120
|
+
this.label.textContent = formatBpLength(locus.end - locus.start);
|
|
13121
|
+
}
|
|
13122
|
+
disconnectedCallback() {
|
|
13123
|
+
if (this.unsubscribe) {
|
|
13124
|
+
this.unsubscribe();
|
|
13125
|
+
this.unsubscribe = null;
|
|
13126
|
+
}
|
|
13127
|
+
}
|
|
13128
|
+
}
|
|
13129
|
+
|
|
13130
|
+
/**
|
|
13131
|
+
* <loom-export-controls> — SVG/PNG export buttons.
|
|
13132
|
+
*
|
|
13133
|
+
* Adds "Save SVG" and "Save PNG" buttons to the shell navbar.
|
|
13134
|
+
* Calls GenomeBrowser.saveSVGtoFile() and savePNGtoFile() respectively.
|
|
13135
|
+
*/
|
|
13136
|
+
const template$1 = document.createElement('template');
|
|
13137
|
+
template$1.innerHTML = /* html */ `
|
|
13138
|
+
<style>
|
|
13139
|
+
:host {
|
|
13140
|
+
display: inline-flex;
|
|
13141
|
+
align-items: center;
|
|
13142
|
+
}
|
|
13143
|
+
.container {
|
|
13144
|
+
display: flex;
|
|
13145
|
+
align-items: center;
|
|
13146
|
+
gap: 2px;
|
|
13147
|
+
}
|
|
13148
|
+
button {
|
|
13149
|
+
display: flex;
|
|
13150
|
+
align-items: center;
|
|
13151
|
+
justify-content: center;
|
|
13152
|
+
width: var(--loom-button-size, 24px);
|
|
13153
|
+
height: var(--loom-button-size, 24px);
|
|
13154
|
+
background: var(--loom-button-bg, white);
|
|
13155
|
+
border: var(--loom-button-border, 1px solid #b0b0b0);
|
|
13156
|
+
border-radius: var(--loom-border-radius, 4px);
|
|
13157
|
+
cursor: pointer;
|
|
13158
|
+
color: var(--loom-icon-color, #555);
|
|
13159
|
+
transition: background 0.15s;
|
|
13160
|
+
padding: 0;
|
|
13161
|
+
line-height: 1;
|
|
13162
|
+
}
|
|
13163
|
+
button:hover {
|
|
13164
|
+
background: var(--loom-button-hover, #e8e8e8);
|
|
13165
|
+
}
|
|
13166
|
+
button:active {
|
|
13167
|
+
transform: scale(0.95);
|
|
13168
|
+
}
|
|
13169
|
+
button svg {
|
|
13170
|
+
width: var(--loom-icon-size, 14px);
|
|
13171
|
+
height: var(--loom-icon-size, 14px);
|
|
13172
|
+
fill: none;
|
|
13173
|
+
stroke: currentColor;
|
|
13174
|
+
stroke-width: 2;
|
|
13175
|
+
stroke-linecap: round;
|
|
13176
|
+
stroke-linejoin: round;
|
|
13177
|
+
}
|
|
13178
|
+
</style>
|
|
13179
|
+
<div class="container">
|
|
13180
|
+
<button class="save-svg" title="Save as SVG">
|
|
13181
|
+
<svg viewBox="0 0 24 24">
|
|
13182
|
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
|
13183
|
+
<polyline points="7 10 12 15 17 10"/>
|
|
13184
|
+
<line x1="12" y1="15" x2="12" y2="3"/>
|
|
13185
|
+
</svg>
|
|
13186
|
+
</button>
|
|
13187
|
+
<button class="save-png" title="Save as PNG">
|
|
13188
|
+
<svg viewBox="0 0 24 24">
|
|
13189
|
+
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
|
|
13190
|
+
<circle cx="8.5" cy="8.5" r="1.5"/>
|
|
13191
|
+
<polyline points="21 15 16 10 5 21"/>
|
|
13192
|
+
</svg>
|
|
13193
|
+
</button>
|
|
13194
|
+
</div>
|
|
13195
|
+
`;
|
|
13196
|
+
class LoomExportControls extends HTMLElement {
|
|
13197
|
+
constructor() {
|
|
13198
|
+
super();
|
|
13199
|
+
this._browser = null;
|
|
13200
|
+
this.attachShadow({ mode: 'open' });
|
|
13201
|
+
this.shadowRoot.appendChild(template$1.content.cloneNode(true));
|
|
13202
|
+
this.shadowRoot.querySelector('.save-svg').addEventListener('click', () => {
|
|
13203
|
+
var _a;
|
|
13204
|
+
(_a = this._browser) === null || _a === void 0 ? void 0 : _a.saveSVGtoFile();
|
|
13205
|
+
});
|
|
13206
|
+
this.shadowRoot.querySelector('.save-png').addEventListener('click', () => {
|
|
13207
|
+
var _a;
|
|
13208
|
+
void ((_a = this._browser) === null || _a === void 0 ? void 0 : _a.savePNGtoFile());
|
|
13209
|
+
});
|
|
13210
|
+
}
|
|
13211
|
+
set browser(b) {
|
|
13212
|
+
this._browser = b;
|
|
13213
|
+
}
|
|
13214
|
+
get browser() {
|
|
13215
|
+
return this._browser;
|
|
13216
|
+
}
|
|
13217
|
+
}
|
|
13218
|
+
|
|
13219
|
+
/**
|
|
13220
|
+
* <loom-navbar> — Horizontal navigation bar composing locus input, window size, and zoom controls.
|
|
13221
|
+
*/
|
|
13222
|
+
// Ensure child custom elements are registered
|
|
13223
|
+
if (!customElements.get('loom-chromosome-select')) {
|
|
13224
|
+
customElements.define('loom-chromosome-select', LoomChromosomeSelect);
|
|
13225
|
+
}
|
|
13226
|
+
if (!customElements.get('loom-locus-input')) {
|
|
13227
|
+
customElements.define('loom-locus-input', LoomLocusInput);
|
|
13228
|
+
}
|
|
13229
|
+
if (!customElements.get('loom-zoom-controls')) {
|
|
13230
|
+
customElements.define('loom-zoom-controls', LoomZoomControls);
|
|
13231
|
+
}
|
|
13232
|
+
if (!customElements.get('loom-window-size')) {
|
|
13233
|
+
customElements.define('loom-window-size', LoomWindowSize);
|
|
13234
|
+
}
|
|
13235
|
+
if (!customElements.get('loom-export-controls')) {
|
|
13236
|
+
customElements.define('loom-export-controls', LoomExportControls);
|
|
13237
|
+
}
|
|
13238
|
+
const template = document.createElement('template');
|
|
13239
|
+
template.innerHTML = /* html */ `
|
|
13240
|
+
<style>
|
|
13241
|
+
:host {
|
|
13242
|
+
display: block;
|
|
13243
|
+
}
|
|
13244
|
+
.navbar {
|
|
13245
|
+
display: flex;
|
|
13246
|
+
align-items: center;
|
|
13247
|
+
justify-content: space-between;
|
|
13248
|
+
height: var(--loom-navbar-height, 32px);
|
|
13249
|
+
padding: var(--loom-navbar-padding, 0 8px);
|
|
13250
|
+
background: var(--loom-navbar-bg, #f3f3f3);
|
|
13251
|
+
border-bottom: var(--loom-border, 1px solid #ccc);
|
|
13252
|
+
box-sizing: border-box;
|
|
13253
|
+
gap: var(--loom-gap, 8px);
|
|
13254
|
+
}
|
|
13255
|
+
.left {
|
|
13256
|
+
display: flex;
|
|
13257
|
+
align-items: center;
|
|
13258
|
+
gap: var(--loom-gap, 8px);
|
|
13259
|
+
flex: 1;
|
|
13260
|
+
min-width: 0;
|
|
13261
|
+
}
|
|
13262
|
+
.right {
|
|
13263
|
+
display: flex;
|
|
13264
|
+
align-items: center;
|
|
13265
|
+
gap: var(--loom-gap, 8px);
|
|
13266
|
+
flex-shrink: 0;
|
|
13267
|
+
}
|
|
13268
|
+
</style>
|
|
13269
|
+
<div class="navbar">
|
|
13270
|
+
<div class="left">
|
|
13271
|
+
<loom-chromosome-select></loom-chromosome-select>
|
|
13272
|
+
<loom-locus-input></loom-locus-input>
|
|
13273
|
+
<loom-window-size></loom-window-size>
|
|
13274
|
+
</div>
|
|
13275
|
+
<div class="right">
|
|
13276
|
+
<loom-export-controls></loom-export-controls>
|
|
13277
|
+
<loom-zoom-controls></loom-zoom-controls>
|
|
13278
|
+
</div>
|
|
13279
|
+
</div>
|
|
13280
|
+
`;
|
|
13281
|
+
class LoomNavbar extends HTMLElement {
|
|
13282
|
+
constructor() {
|
|
13283
|
+
super();
|
|
13284
|
+
this._browser = null;
|
|
13285
|
+
this.attachShadow({ mode: 'open' });
|
|
13286
|
+
this.shadowRoot.appendChild(template.content.cloneNode(true));
|
|
13287
|
+
this.chromSelect = this.shadowRoot.querySelector('loom-chromosome-select');
|
|
13288
|
+
this.locusInput = this.shadowRoot.querySelector('loom-locus-input');
|
|
13289
|
+
this.zoomControls = this.shadowRoot.querySelector('loom-zoom-controls');
|
|
13290
|
+
this.windowSize = this.shadowRoot.querySelector('loom-window-size');
|
|
13291
|
+
this.exportControls = this.shadowRoot.querySelector('loom-export-controls');
|
|
13292
|
+
}
|
|
13293
|
+
set browser(b) {
|
|
13294
|
+
this._browser = b;
|
|
13295
|
+
this.chromSelect.browser = b;
|
|
13296
|
+
this.locusInput.browser = b;
|
|
13297
|
+
this.zoomControls.browser = b;
|
|
13298
|
+
this.windowSize.browser = b;
|
|
13299
|
+
this.exportControls.browser = b;
|
|
13300
|
+
}
|
|
13301
|
+
get browser() {
|
|
13302
|
+
return this._browser;
|
|
13303
|
+
}
|
|
13304
|
+
}
|
|
13305
|
+
|
|
13306
|
+
/**
|
|
13307
|
+
* Ensures all Loom UI web components are registered as custom elements.
|
|
13308
|
+
* Safe to call multiple times — each element is only registered once.
|
|
13309
|
+
*/
|
|
13310
|
+
let registered = false;
|
|
13311
|
+
function ensureRegistered() {
|
|
13312
|
+
if (registered || typeof customElements === 'undefined')
|
|
13313
|
+
return;
|
|
13314
|
+
registered = true;
|
|
13315
|
+
const elements = [
|
|
13316
|
+
['loom-chromosome-select', LoomChromosomeSelect],
|
|
13317
|
+
['loom-locus-input', LoomLocusInput],
|
|
13318
|
+
['loom-zoom-controls', LoomZoomControls],
|
|
13319
|
+
['loom-window-size', LoomWindowSize],
|
|
13320
|
+
['loom-export-controls', LoomExportControls],
|
|
13321
|
+
['loom-navbar', LoomNavbar],
|
|
13322
|
+
];
|
|
13323
|
+
for (const [name, ctor] of elements) {
|
|
13324
|
+
if (!customElements.get(name)) {
|
|
13325
|
+
customElements.define(name, ctor);
|
|
13326
|
+
}
|
|
13327
|
+
}
|
|
13328
|
+
}
|
|
13329
|
+
|
|
13330
|
+
function ChromosomeSelect({ browser: browserProp }) {
|
|
13331
|
+
const contextBrowser = useGenomeBrowser();
|
|
13332
|
+
const browser = browserProp !== null && browserProp !== void 0 ? browserProp : contextBrowser;
|
|
13333
|
+
const ref = useRef(null);
|
|
13334
|
+
ensureRegistered();
|
|
13335
|
+
useEffect(() => {
|
|
13336
|
+
if (ref.current)
|
|
13337
|
+
ref.current.browser = browser;
|
|
13338
|
+
}, [browser]);
|
|
13339
|
+
return jsx("loom-chromosome-select", { ref: ref });
|
|
13340
|
+
}
|
|
13341
|
+
|
|
13342
|
+
function LocusInput({ browser: browserProp }) {
|
|
13343
|
+
const contextBrowser = useGenomeBrowser();
|
|
13344
|
+
const browser = browserProp !== null && browserProp !== void 0 ? browserProp : contextBrowser;
|
|
13345
|
+
const ref = useRef(null);
|
|
13346
|
+
ensureRegistered();
|
|
13347
|
+
useEffect(() => {
|
|
13348
|
+
if (ref.current)
|
|
13349
|
+
ref.current.browser = browser;
|
|
13350
|
+
}, [browser]);
|
|
13351
|
+
return jsx("loom-locus-input", { ref: ref });
|
|
13352
|
+
}
|
|
13353
|
+
|
|
13354
|
+
function ZoomControls({ browser: browserProp }) {
|
|
13355
|
+
const contextBrowser = useGenomeBrowser();
|
|
13356
|
+
const browser = browserProp !== null && browserProp !== void 0 ? browserProp : contextBrowser;
|
|
13357
|
+
const ref = useRef(null);
|
|
13358
|
+
ensureRegistered();
|
|
13359
|
+
useEffect(() => {
|
|
13360
|
+
if (ref.current)
|
|
13361
|
+
ref.current.browser = browser;
|
|
13362
|
+
}, [browser]);
|
|
13363
|
+
return jsx("loom-zoom-controls", { ref: ref });
|
|
13364
|
+
}
|
|
13365
|
+
|
|
13366
|
+
function WindowSize({ browser: browserProp }) {
|
|
13367
|
+
const contextBrowser = useGenomeBrowser();
|
|
13368
|
+
const browser = browserProp !== null && browserProp !== void 0 ? browserProp : contextBrowser;
|
|
13369
|
+
const ref = useRef(null);
|
|
13370
|
+
ensureRegistered();
|
|
13371
|
+
useEffect(() => {
|
|
13372
|
+
if (ref.current)
|
|
13373
|
+
ref.current.browser = browser;
|
|
13374
|
+
}, [browser]);
|
|
13375
|
+
return jsx("loom-window-size", { ref: ref });
|
|
13376
|
+
}
|
|
13377
|
+
|
|
13378
|
+
function ExportControls({ browser: browserProp }) {
|
|
13379
|
+
const contextBrowser = useGenomeBrowser();
|
|
13380
|
+
const browser = browserProp !== null && browserProp !== void 0 ? browserProp : contextBrowser;
|
|
13381
|
+
const ref = useRef(null);
|
|
13382
|
+
ensureRegistered();
|
|
13383
|
+
useEffect(() => {
|
|
13384
|
+
if (ref.current)
|
|
13385
|
+
ref.current.browser = browser;
|
|
13386
|
+
}, [browser]);
|
|
13387
|
+
return jsx("loom-export-controls", { ref: ref });
|
|
13388
|
+
}
|
|
13389
|
+
|
|
13390
|
+
function Navbar({ browser: browserProp }) {
|
|
13391
|
+
const contextBrowser = useGenomeBrowser();
|
|
13392
|
+
const browser = browserProp !== null && browserProp !== void 0 ? browserProp : contextBrowser;
|
|
13393
|
+
const ref = useRef(null);
|
|
13394
|
+
ensureRegistered();
|
|
13395
|
+
useEffect(() => {
|
|
13396
|
+
if (ref.current)
|
|
13397
|
+
ref.current.browser = browser;
|
|
13398
|
+
}, [browser]);
|
|
13399
|
+
return jsx("loom-navbar", { ref: ref });
|
|
13400
|
+
}
|
|
13401
|
+
|
|
12725
13402
|
/**
|
|
12726
13403
|
* <loom-context-menu> — Right-click context menu Web Component.
|
|
12727
13404
|
*
|
|
@@ -13287,4 +13964,4 @@ var LoomInputDialog$1 = /*#__PURE__*/Object.freeze({
|
|
|
13287
13964
|
LoomInputDialog: LoomInputDialog
|
|
13288
13965
|
});
|
|
13289
13966
|
|
|
13290
|
-
export { BedTrack, GeneTrack, GenomeBrowserContext, GtxTrack, InteractionTrack, LoomBrowser, RulerTrack, SequenceTrack, WigTrack, useBrowserEvent, useGenomeBrowser, useLocus };
|
|
13967
|
+
export { BedTrack, ChromosomeSelect, ExportControls, GeneTrack, GenomeBrowserContext, GtxTrack, InteractionTrack, LocusInput, LoomBrowser, Navbar, RulerTrack, SequenceTrack, WigTrack, WindowSize, ZoomControls, useBrowserEvent, useGenomeBrowser, useLocus };
|