Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 0 additions & 32 deletions js/widgets/genericSelectModal.js

This file was deleted.

104 changes: 104 additions & 0 deletions js/widgets/trackSelectionListModal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
export default function createTrackSelectionListModal({id, label = '', sections, description = '', okHandler}) {

// Flatten sections to get all tracks.
const tracks = []
const trackMap = new Map()
const flattenSections = (sections) => {
sections.forEach(section => {
if (section.tracks) {
section.tracks.forEach(track => {
tracks.push(track)
trackMap.set(track._id, track)
})
}
if (section.children) {
flattenSections(section.children)
}
})
}
flattenSections(sections)

// Generate the HTML for the modal
const renderTracks = (tracks) => {
// Generate options for the select element
// Disable options for tracks that are already loaded
const options = tracks.map(track => {
const disabled = track._loaded ? 'disabled' : ''
return `<option value="${track._id}" ${disabled}>${track.name}</option>`
}).join('')

// Return the Bootstrap-styled select widget
return `
<div class="mb-3 h-100 d-flex flex-column">
<select id="track-select" class="form-select flex-grow-1" multiple>
${options}
</select>
</div>
`
}

const html = `
<div id="${id}" class="modal fade igv-app-track-select-modal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header flex-column position-sticky top-0 bg-white z-index-1">
<h5 class="modal-title text-center w-100">${label}</h5>
<div class="additional-html w-100">${description}</div>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body overflow-auto" style="max-height: 70vh;">
${renderTracks(tracks)}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-sm btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">OK</button>
</div>
</div>
</div>
</div>
`

const tempDiv = document.createElement('div')
tempDiv.innerHTML = html.trim()
const modalElement = tempDiv.firstChild
document.body.appendChild(modalElement)

const modal = new bootstrap.Modal(modalElement, {
backdrop: 'static',
keyboard: false
})

let modalAction = null // Variable to track the action

// Add event listeners to buttons
document.querySelector(`#${id} .btn-secondary`).addEventListener('click', () => {
modalAction = 'ok'
})
document.querySelector(`#${id} .btn-outline-secondary`).addEventListener('click', () => {
modalAction = 'cancel'
})


// Listen for the modal close event
modalElement.addEventListener('hidden.bs.modal', () => {
if (modalAction === 'ok') {
console.log('Modal closed with OK button')
const selectedOptions = Array.from(modalElement.querySelectorAll('#track-select option:checked'))
const checkedTracks = selectedOptions.map(option => trackMap.get(option.value))
const uncheckedTracks = [] // There is no way to unselect a track with this widget
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The uncheckedTracks array is always empty, but the okHandler in trackWidgets.js (lines 247-252) expects to process unchecked tracks to remove them from the browser. This means users cannot unload tracks using this modal, which differs from trackSelectionModal where checkboxes can be unchecked. This is a functional limitation that should be addressed. Consider collecting all non-selected tracks (tracks not in the selected options) as uncheckedTracks if track removal is intended to be supported.

Suggested change
const uncheckedTracks = [] // There is no way to unselect a track with this widget
const selectedIds = new Set(selectedOptions.map(option => option.value))
const uncheckedTracks = tracks.filter(track => !selectedIds.has(track._id))

Copilot uses AI. Check for mistakes.

if (okHandler) {
okHandler(checkedTracks, uncheckedTracks)
} else {
console.log('URLs of checked tracks:', checkedTracks)
}
}

// Clean up resources
modal.dispose() // Dispose of the Bootstrap modal instance
modalElement.remove() // Remove the modal element from the DOM
modalAction = null // Reset the action
})

return modal
}
2 changes: 1 addition & 1 deletion js/widgets/trackSelectionModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export default function createTrackSelectionModal({id, label = '', sections, des
${section.tracks ? section.tracks.map((track) => `
<div class="col-6 col-md-4">
<div class="form-check d-flex align-items-center">
<input class="form-check-input" type="checkbox" id="${track._id}" ${track._checked ? 'checked' : ''} ${track.disabled ? 'disabled' : ''}>
<input class="form-check-input" type="checkbox" id="${track._id}" ${track._loaded ? 'checked' : ''} ${track.disabled ? 'disabled' : ''}>
<label class="form-check-label ms-2 me-2" for="${track._id}">
${track.name}
</label>
Expand Down
10 changes: 8 additions & 2 deletions js/widgets/trackWidgets.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import createTrackSelectionModal from './trackSelectionModal.js'
import * as Utils from './utils.js'
import {initializeDropbox} from "./dropbox.js"
import igv from '../../node_modules/igv/dist/igv.esm.js'
import createTrackSelectionListModal from "./trackSelectionListModal.js"

const id_prefix = 'genome_specific_'

Expand Down Expand Up @@ -232,7 +233,7 @@ async function trackMenuGenomeChange(browser, genome) {
if (section.tracks) {
for (const track of section.tracks) {
track._id = trackId(track)
track._checked = loadedIDs.has(track._id)
track._loaded = loadedIDs.has(track._id)
}
}
if (section.children) for (const child of section.children) {
Expand Down Expand Up @@ -264,7 +265,9 @@ async function trackMenuGenomeChange(browser, genome) {
modal.hide()
}

const modal = createTrackSelectionModal(config)
const modal = 'hub' === config.type ?
createTrackSelectionModal(config) :
createTrackSelectionListModal(config)
modal.show()
})
}
Expand All @@ -276,6 +279,7 @@ function prepRegistryConfig(registry) {

if ('custom-data-modal' === registry.type) {
const customModalTable = new ModalTable({
type: registry.type,
id: `igv-custom-modal-${Math.random().toString(36).substring(2, 9)}`,
title: registry.label,
okHandler: trackLoadHandler,
Expand All @@ -289,6 +293,7 @@ function prepRegistryConfig(registry) {
return registry
} else {
return {
type: registry.type || 'track-selection-modal',
id: `_${Math.random().toString(36).substring(2, 9)}`,
label: registry.label,
description: registry.description,
Expand All @@ -306,6 +311,7 @@ async function prepHubConfig(hubURL, genomeID) {
let descriptionUrl = hub.getDescriptionUrl()
const groups = await hub.getGroupedTrackConfigurations(genomeID)
return {
type: 'hub',
id: `_${Math.random().toString(36).substring(2, 9)}`,
label: `${hub.getShortLabel()}`,
description: descriptionUrl ? `<a target=_blank href="${descriptionUrl}">${hub.getLongLabel()}</a>` : '',
Expand Down