UNPKG

higlass

Version:

HiGlass Hi-C / genomic / large data viewer

478 lines (424 loc) 15.6 kB
// @ts-nocheck import clsx from 'clsx'; import React from 'react'; import ContextMenuContainer from './ContextMenuContainer'; import ContextMenuItem from './ContextMenuItem'; import NestedContextMenu from './NestedContextMenu'; // Configs import { THEME_DARK, TRACKS_INFO_BY_TYPE } from './configs'; import OPTIONS_INFO from './options-info'; // Styles import classes from '../styles/ContextMenu.module.scss'; /** * We're going to get the track object to see if it has a * context menu handler that will give use context menu items * to display * * @param (Dict) track The config for the track we're getting context menu * items for * * @param (TrackRenderer) trackRenderer The track renderer for the view * containing this track. We'll use it to get the track's object * * @param (Dict) position The position of the track. Relevant are the canvasLeft, * canvasRight positions which mark where the track starts relative to the * canvas. This is important because all coordinates within a track are relative * to left and top coordinates. */ function findTrackContextMenuItems(track, trackRenderer, position) { let trackObj = trackRenderer.getTrackObject(track.uid); // The track may be a LeftTrackModifier track trackObj = trackObj.originalTrack || trackObj; // See if the track will provide us with context menu items if (trackObj.contextMenuItems) { let trackLeft = position.canvasLeft - trackObj.position[0]; let trackTop = position.canvasTop - trackObj.position[1]; if (trackObj.flipText) { // This is a left track modifier track so we need to swap the // left and right values const temp = trackLeft; trackLeft = trackTop; trackTop = temp; } const items = trackObj.contextMenuItems(trackLeft, trackTop); return items || []; } // The track doesn't have a contextMenuItems function so we it's // obviously not providing any items. return []; } export default class SeriesListMenu extends ContextMenuContainer { getConfigureSeriesMenu(position, bbox, track) { const menuItems = {}; // plugin tracks can offer their own options // if they clash with the default higlass options // they will override them const pluginOptionsInfo = window.higlassTracksByType?.[track.type] && window.higlassTracksByType[track.type].config && window.higlassTracksByType[track.type].config.optionsInfo; if (pluginOptionsInfo) { for (const key of Object.keys(pluginOptionsInfo)) { OPTIONS_INFO[key] = pluginOptionsInfo[key]; } } if ( !TRACKS_INFO_BY_TYPE[track.type] || !TRACKS_INFO_BY_TYPE[track.type].availableOptions ) { return null; } for (const optionType of TRACKS_INFO_BY_TYPE[track.type].availableOptions) { if (optionType in OPTIONS_INFO) { menuItems[optionType] = { name: OPTIONS_INFO[optionType].name }; // can we dynamically generate some options? // should be used if the options depend on tileset info or other current state if (OPTIONS_INFO[optionType].generateOptions) { const generatedOptions = OPTIONS_INFO[optionType].generateOptions(track); if (!menuItems[optionType].children) { menuItems[optionType].children = {}; } for (const generatedOption of generatedOptions) { const optionSelectorSettings = { name: generatedOption.name, value: generatedOption.value, handler: () => { track.options[optionType] = generatedOption.value; this.props.onTrackOptionsChanged(track.uid, track.options); this.props.closeMenu(); }, }; menuItems[optionType].children[generatedOption.value] = optionSelectorSettings; } } if (OPTIONS_INFO[optionType].inlineOptions) { // we can simply select this option from the menu for (const inlineOptionKey in OPTIONS_INFO[optionType] .inlineOptions) { const inlineOption = OPTIONS_INFO[optionType].inlineOptions[inlineOptionKey]; // check if there's already available options (e.g. // "Top right") for this option type (e.g. "Label // position") if (!menuItems[optionType].children) { menuItems[optionType].children = {}; } const optionSelectorSettings = { name: inlineOption.name, value: inlineOption.value, // missing handler to be filled in below }; // is there a custom component available for picking this // option type value (e.g. 'custom' color scale) if (inlineOption.componentPickers?.[track.type]) { optionSelectorSettings.handler = () => { this.props.onConfigureTrack( track, inlineOption.componentPickers[track.type], ); this.props.closeMenu(); }; } else { // the menu option defines a potential value for this option // type (e.g. "top right") optionSelectorSettings.handler = () => { track.options[optionType] = inlineOption.value; this.props.onTrackOptionsChanged(track.uid, track.options); this.props.closeMenu(); }; } menuItems[optionType].children[inlineOptionKey] = optionSelectorSettings; } } else if (OPTIONS_INFO[optionType].componentPickers?.[track.type]) { // there's an option picker registered menuItems[optionType].handler = () => { this.props.onConfigureTrack( track, OPTIONS_INFO[optionType].componentPickers[track.type], ); this.props.closeMenu(); }; } } } return ( <NestedContextMenu key="config-series-menu" closeMenu={this.props.closeMenu} menuItems={menuItems} orientation={this.state.orientation} parentBbox={bbox} position={position} theme={this.props.theme} /> ); } /** * Return a list of track types that can be used * with the data for this track * * @param {Object} position The position where to draw ths menu (e.g. {left: 42, top: 88}) * * @param (Object) bbox * The bounding box of the parent menu, used to determine whether * to draw the child menu on the left or the right * * @param (Object) track The track definition for this series (as in the viewconf) */ getTrackTypeItems(position, bbox, track) { // get the datatype of the current track // // if we've loaded external track types, list them here if (window.higlassTracksByType) { // Extend `TRACKS_INFO_BY_TYPE` with the configs of plugin tracks. Object.keys(window.higlassTracksByType).forEach((pluginTrackType) => { TRACKS_INFO_BY_TYPE[pluginTrackType] = window.higlassTracksByType[pluginTrackType].config; }); } let { datatype } = track; let orientation = null; // make sure that this is a valid track type before trying to // look up other tracks that can substitute for it if (track.type in TRACKS_INFO_BY_TYPE) { if (!datatype) { datatype = TRACKS_INFO_BY_TYPE[track.type].datatype[0]; } ({ orientation } = TRACKS_INFO_BY_TYPE[track.type]); } // see which other tracks can display a similar datatype const availableTrackTypes = Object.values(TRACKS_INFO_BY_TYPE) .filter((x) => x.datatype) .filter((x) => x.orientation) .filter((x) => x.datatype.includes(datatype)) .filter((x) => x.orientation === orientation) .map((x) => x.type); const menuItems = {}; for (let i = 0; i < availableTrackTypes.length; i++) { menuItems[availableTrackTypes[i]] = { value: availableTrackTypes[i], name: availableTrackTypes[i], handler: () => { this.props.onChangeTrackType(track.uid, availableTrackTypes[i]); }, }; } return ( <NestedContextMenu key="track-type-items" closeMenu={this.props.closeMenu} menuItems={menuItems} orientation={this.state.orientation} parentBbox={bbox} position={position} theme={this.props.theme} /> ); } getSubmenu() { if (this.state.submenuShown) { // the bounding box of the element which initiated the subMenu // necessary so that we can position the submenu next to the initiating // element const bbox = this.state.submenuSourceBbox; const position = this.state.orientation === 'left' ? { left: this.state.left, top: bbox.top, } : { left: this.state.left + bbox.width + 7, top: bbox.top, }; // When a submenu is requested, the onMouseEnter handler of the // item that requested it provides a structure containing the option // picked as well as some data associated with it // e.g. {"option": "configure-series", data: track} const subMenuData = this.state.submenuShown; const track = subMenuData.value; if (subMenuData.option === 'track-type') { return this.getTrackTypeItems(position, bbox, track); } return this.getConfigureSeriesMenu(position, bbox, track); } return <div />; } getDivideByMenuItem() { if (this.props.series.data && this.props.series.data.type === 'divided') { const newData = { tilesetUid: this.props.series.data.children[0].tilesetUid, server: this.props.series.data.children[0].server, }; // this track is already being divided return ( <ContextMenuItem className={classes['context-menu-item']} onClick={() => this.props.onChangeTrackData(this.props.series.uid, newData) } onMouseEnter={(e) => this.handleOtherMouseEnter(e)} > <span className={classes['context-menu-span']}>Remove divisor</span> </ContextMenuItem> ); } return ( <ContextMenuItem className={classes['context-menu-item']} onClick={() => this.props.onAddDivisor(this.props.series)} onMouseEnter={(e) => this.handleOtherMouseEnter(e)} > <span className={classes['context-menu-span']}>Divide by</span> </ContextMenuItem> ); } componentWillUnmount() {} render() { let exportDataMenuItem = null; const trackContextMenuItems = findTrackContextMenuItems( this.props.track, this.props.trackRenderer, this.props.position, ); if ( TRACKS_INFO_BY_TYPE[this.props.series.type] && TRACKS_INFO_BY_TYPE[this.props.series.type].exportable ) { exportDataMenuItem = ( <ContextMenuItem className={classes['context-menu-item']} onClick={() => this.props.onExportData( this.props.hostTrack.uid, this.props.track.uid, ) } onMouseEnter={(e) => this.handleOtherMouseEnter(e)} > <span className={classes['context-menu-span']}>Export Data</span> </ContextMenuItem> ); } // if a track can't be replaced, this.props.onAddSeries // will be null so we don't need to display the menu item const replaceSeriesItem = this.props.onAddSeries ? ( <ContextMenuItem className={classes['context-menu-item']} onClick={() => { this.props.onCloseTrack(this.props.series.uid); this.props.onAddSeries(this.props.hostTrack.uid); }} onMouseEnter={(e) => this.handleOtherMouseEnter(e)} > <span className={classes['context-menu-span']}>Replace Series</span> </ContextMenuItem> ) : null; return ( <div ref={(c) => { this.div = c; }} className={clsx(classes['context-menu'], { [classes['context-menu-dark']]: this.props.theme === THEME_DARK, })} data-menu-type="SeriesListMenu" onMouseLeave={this.props.handleMouseLeave} style={{ left: this.state.left, top: this.state.top, }} > {trackContextMenuItems.map((x) => ( <ContextMenuItem key={x.label} onClick={(evt) => { x.onClick(evt, (newOptions) => { // We're going to pass in a handler to that the track // can use to change its options this.props.onTrackOptionsChanged(this.props.track.uid, { ...this.props.track.options, ...newOptions, }); }); this.props.closeMenu(); }} onMouseEnter={(e) => this.handleOtherMouseEnter(e)} className={classes['context-menu-item']} > <span className={classes['context-menu-span']}>{x.label}</span> </ContextMenuItem> ))} {trackContextMenuItems.length > 0 && ( <hr className={classes['context-menu-hr']} /> )} <ContextMenuItem onClick={() => {}} onMouseEnter={(e) => this.handleItemMouseEnter(e, { option: 'configure-series', value: this.props.track, }) } onMouseLeave={(e) => this.handleMouseLeave(e)} > Configure Series <svg className={classes['play-icon']}> <title>Play</title> <use xlinkHref="#play" /> </svg> </ContextMenuItem> <ContextMenuItem className={classes['context-menu-item']} onClick={() => {}} onMouseEnter={(e) => this.handleItemMouseEnter(e, { option: 'track-type', value: this.props.track, }) } onMouseLeave={(e) => this.handleMouseLeave(e)} > <span className={classes['context-menu-span']}> Track Type <svg className={classes['play-icon']}> <title>Play</title> <use xlinkHref="#play" /> </svg> </span> </ContextMenuItem> {exportDataMenuItem} {this.getDivideByMenuItem()} <ContextMenuItem className={classes['context-menu-item']} onClick={this.props.onCloseTrack} onMouseEnter={(e) => this.handleOtherMouseEnter(e)} > <span className={classes['context-menu-span']}>Close Series</span> </ContextMenuItem> {replaceSeriesItem} {/* this.props.series.type === 'heatmap' ? <ContextMenuItem onClick={() => { this.props.onDivideSeries(this.props.series.uid); // this.props.onCloseTrack(this.props.series.uid); // this.props.onAddSeries(this.props.hostTrack.uid); }} onMouseEnter={e => this.handleOtherMouseEnter(e)} className={classes["context-menu-item"]} > <span className={classes["context-menu-span"]}> {'Divide Series By'} </span> </ContextMenuItem> : null */} {this.getSubmenu()} </div> ); } }