Skip to content

Commit

Permalink
Finished implementing menu
Browse files Browse the repository at this point in the history
  • Loading branch information
tmtabor committed Aug 30, 2021
1 parent 705434c commit f606065
Show file tree
Hide file tree
Showing 2 changed files with 299 additions and 34 deletions.
47 changes: 42 additions & 5 deletions lib/menu.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* Navbar container */
.igv-navbar {
.igv-darkbar {
overflow: hidden;
background-color: #5f5f5f;
font-family: "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
Expand Down Expand Up @@ -36,8 +36,9 @@
color: white;
padding: 14px 16px;
background-color: inherit;
font-family: inherit; /* Important for vertical align on mobile phones */
margin: 0; /* Important for vertical align on mobile phones */
cursor: pointer;
font-family: inherit;
margin: 0;
}

.igv-navbar i {
Expand All @@ -55,8 +56,11 @@
position: absolute;
background-color: #f9f9f9;
min-width: 160px;
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
z-index: 1;
box-shadow: 0 8px 16px 0 rgba(0,0,0,0.2);
z-index: 200;
top: 48px;
max-height: 300px;
overflow-y: auto;
}

/* Links inside the dropdown */
Expand All @@ -82,4 +86,37 @@
/* Show the dropdown menu on hover */
.igv-dropdown:hover .igv-dropdown-content {
display: block;
}


/* Dialog styles */

.igv-dialog {
display: none;
position: absolute;
top: calc(50% - 100px);
width: 50%;
min-width: 400px;
max-width: 1000px;
}

.igv-dialog label {
width: 100px;
display: inline-block;
font-weight: bold;
}

.igv-dialog input {
display: inline-block;
width: calc(100% - 120px);
margin-bottom: 5px;
}

.igv-dialog-footer {
margin-top: 10px;
text-align: right;
}

.igv-dialog-footer > button {
cursor: pointer;
}
286 changes: 257 additions & 29 deletions lib/menu.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import './menu.css';
import igv from "./igv";
import igv_jupyter from "../package.json";

class Menu {
/**
Expand All @@ -16,51 +18,63 @@ class Menu {
* @param div - menu node to initialize
*/
static render(navbar) {
// const template = `
// <div class="igv-navbar">
// <a href="#home">Home</a>
// <a href="#news">News</a>
// <div class="igv-dropdown">
// <button class="igv-dropdown-button">Dropdown
// <i class="fa fa-caret-down"></i>
// </button>
// <div class="igv-dropdown-content">
// <a href="#">Link 1</a>
// <a href="#">Link 2</a>
// <a href="#">Link 3</a>
// </div>
// </div>
// </div>`;
//
// const menu = new DOMParser().parseFromString(template, "text/html").querySelector('div.navbar');
// container.appendChild(menu);

navbar.classList.add('igv-darkbar');
navbar.appendChild(Menu.create_menu_dropdown('Genome', [
Menu.create_menu_item('Local File ...', () => {}),
Menu.create_menu_item('URL ...', () => {})
Menu.create_menu_item('Local File ...', GenomeMenu.local_genome),
Menu.create_menu_item('URL ...', GenomeMenu.remote_genome)
// TODO: List genomes
]));
navbar.appendChild(Menu.create_menu_dropdown('Tracks', [
Menu.create_menu_item('Local File ...', TrackMenu.local_track),
Menu.create_menu_item('URL ...', TrackMenu.remote_track)
// TODO: List tracks
]));
navbar.appendChild(Menu.create_menu_dropdown('Session', [
Menu.create_menu_item('Local File ...', SessionMenu.local_session),
Menu.create_menu_item('URL ...', SessionMenu.remote_session),
Menu.create_menu_item('Save', SessionMenu.save_session)
]));
// navbar.appendChild(Menu.create_menu_item('Share', null)); // Doesn't make sense in JupyterLab
// navbar.appendChild(Menu.create_menu_item('Bookmark', null)); // Doesn't make sense in JupyterLab
navbar.appendChild(Menu.create_menu_item('Save SVG', SVGMenu.create_svg));
navbar.appendChild(Menu.create_menu_dropdown('Help', [
Menu.create_menu_item('GitHub Repository', HelpMenu.github),
Menu.create_menu_item('User Forum', HelpMenu.forum),
Menu.create_menu_item('About IGV-Jupyter', HelpMenu.about)
]));
navbar.appendChild(Menu.create_menu_dropdown('Tracks', []));
navbar.appendChild(Menu.create_menu_dropdown('Session', []));
navbar.appendChild(Menu.create_menu_item('Share', null));
navbar.appendChild(Menu.create_menu_item('Bookmark', null));
navbar.appendChild(Menu.create_menu_item('Save SVG', null));
navbar.appendChild(Menu.create_menu_dropdown('Help', []));

navbar.classList.add('igv-menu-rendered');
navbar.parentNode.style.overflow = 'visible';
Menu.populate_genomes(navbar);
}

static populate_genomes(navbar) {
fetch('https://s3.amazonaws.com/igv.org.genomes/genomes.json', { mode: 'cors' })
.then(response => response.json())
.then(data => {
const menu = navbar.querySelector(".igv-dropdown[data-name='Genome'] > .igv-dropdown-content");
menu.appendChild(document.createElement('hr'));
for (const genome of data) {
menu.appendChild(Menu.create_menu_item(genome.name, (igv_instance) => {
igv_instance.loadGenome(genome);
}));
}
});
}

static create_menu_item(name, callback) {
const link = document.createElement('a');
link.href = '#';
link.textContent = name;
link.addEventListener('click', () => {
console.log('CLICKED!');
link.addEventListener('click', (event) => {
callback(Menu.igv_from_event(event));
});
return link
}

static create_menu_dropdown(name, items) {
const dropdown = document.createElement('div');
dropdown.setAttribute('data-name', name);
dropdown.classList.add('igv-dropdown');
dropdown.appendChild(Menu.menu_button(name));
dropdown.appendChild(Menu.menu_content(items));
Expand All @@ -87,6 +101,220 @@ class Menu {
icon.classList.add('fa', 'fa-caret-down');
return icon;
}

static igv_from_event(event) {
const igv_node = event.target.closest('.igv-navbar').parentNode.lastChild;
return igv.browserCache[igv_node.id];
}
}

class Dialog {
static form_data(element) {
const data = {};
element.querySelectorAll('input, select, textarea').forEach((e) => {
const name = e.getAttribute('name');
data[name] = e.value;
});
return data;
}

static dialog_footer(element, resolve, reject) {
// Create the dialog footer
const footer = document.createElement('footer');
footer.classList.add('igv-dialog-footer');

// Create the Cancel button
const cancel_button = document.createElement('button');
cancel_button.classList.add('jp-Dialog-button', 'jp-mod-styled', 'jp-mod-reject');
cancel_button.innerText = 'Cancel';
cancel_button.addEventListener('click', () => {
element.remove();
reject();
});
footer.append(cancel_button);

// Create the OK button
const ok_button = document.createElement('button');
ok_button.classList.add('jp-Dialog-button', 'jp-mod-styled', 'jp-mod-accept');
ok_button.innerText = 'OK';
ok_button.addEventListener('click', () => {
const data = Dialog.form_data(element);
element.remove();
resolve(data);
});
footer.append(ok_button);

return footer;
}

static create(content) {
const element = document.createElement('dialog');
element.classList.add('igv-dialog');
element.innerHTML = content;
const button_promse = new Promise((resolve, reject) => {
element.append(Dialog.dialog_footer(element, resolve, reject));
});
document.body.append(element);
element.style.display = 'block';
return button_promse;
}

static name_from_url(url) {
const name = url.substring(url.lastIndexOf('/')+1);
if (!name.trim().length) return url;
else return name;
}

static upload() {
let input = document.querySelector('.igv-upload-input');
if (!input) { // Lazily create the upload input, if necessary
input = document.createElement('input');
input.setAttribute('type', 'file');
input.classList.add('igv-upload-input');
input.style.display = 'none';
document.body.appendChild(input);
}
return input;
}

static read_local_file(accept=null, data_uri=false) {
return new Promise((resolve, reject) => {
const upload_input = Dialog.upload();
if (accept) upload_input.setAttribute('accept', accept);
else upload_input.removeAttribute('accept');
upload_input.addEventListener('change', () => {
if (upload_input.files.length === 0) return; // Protect against empty input
const reader = new FileReader();
reader.onload = event => {
resolve({
'name': upload_input.files[0].name,
'contents': event.target.result
});
};
if (data_uri) reader.readAsDataURL(upload_input.files[0]);
else reader.readAsText(upload_input.files[0]);
}, { 'once': true });
upload_input.click();
});
}
}

class GenomeMenu {
static local_genome(igv_instance) {
Dialog.read_local_file(null, true)
.then((data) => {
igv_instance.loadGenome({
id: data.name,
fastaURL: data.contents,
indexed: false
}).catch(error => {
alert("Genome did not load - invalid data and/or index file(s). " + error);
})
});
}

static remote_genome(igv_instance) {
Dialog.create(`
<label>Genome URL</label>
<input type="text" name="genome_url" />
<label>Index URL</label>
<input type="text" name="index_url" />
`).then((data) => {
igv_instance.loadGenome({
id: Dialog.name_from_url(data.genome_url),
fastaURL: data.genome_url,
indexURL: data.index_url
});
});
}
}

class TrackMenu {
static local_track(igv_instance) {
Dialog.read_local_file(null, true)
.then((data) => {
igv_instance.loadTrack({
name: data.name,
url: data.contents,
format: TrackMenu.guess_format(data.name)
});
});
}

static remote_track(igv_instance) {
Dialog.create(`
<label>Track URL</label>
<input type="text" name="track_url" />
<label>Index URL</label>
<input type="text" name="index_url" />
`).then((data) => {
igv_instance.loadTrack({
name: Dialog.name_from_url(data.track_url),
url: data.track_url,
indexURL: data.index_url
});
});
}

static guess_format(filename) {
const type = filename.substring(filename.lastIndexOf('.')+1);
if (!type.trim().length) return filename;
else return type;
}
}

class SessionMenu {
static local_session(igv_instance) {
Dialog.read_local_file('.json')
.then(data => igv_instance.loadSession(JSON.parse(data.contents)));
}

static remote_session(igv_instance) {
Dialog.create(`
<label>Session URL</label>
<input type="text" name="session_url" />
`).then((data) => {
igv_instance.loadSession({
url: data.session_url
});
});
}

static save_session(igv_instance) {
Dialog.create(`
<label>Filename</label>
<input type="text" name="filename" value="igv-jupyter-session.json" >
`).then((data) => {
const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(igv_instance.toJSON()));
if (!data.filename.endsWith('.json')) data.filename += '.json';
const downloadAnchorNode = document.createElement('a');
downloadAnchorNode.setAttribute("href", dataStr);
downloadAnchorNode.setAttribute("download", data.filename);
document.body.appendChild(downloadAnchorNode); // required for firefox
downloadAnchorNode.click();
downloadAnchorNode.remove();
});
}
}

class SVGMenu {
static create_svg(igv_instance) {
igv_instance.saveSVGtoFile('igv.svg');
}
}

class HelpMenu {
static github() {
window.open('https://github.com/igvteam/igv-jupyter');
}

static forum() {
window.open('https://groups.google.com/g/igv-help');
}

static about() {
alert(`igv-jupyter Version ${igv_jupyter.version}\nigv.js Version ${igv.version()}`);
}
}

export default Menu;

0 comments on commit f606065

Please sign in to comment.