import $ from 'jquery'; import menuDotsIcon from '../img/menudots.svg'; import OncoprintModel, { CustomTrackOption, GAP_MODE_ENUM, TrackId, TrackProp, TrackSortDirection, } from './oncoprintmodel'; import { CLOSE_MENUS_EVENT as HEADER_VIEW_CLOSE_MENUS_EVENT } from './oncoprintheaderview'; import ClickEvent = JQuery.ClickEvent; const TOGGLE_BTN_CLASS = 'oncoprintjs__track_options__toggle_btn_img'; const TOGGLE_BTN_OPEN_CLASS = 'oncoprintjs__track_options__open'; const DROPDOWN_CLASS = 'oncoprintjs__track_options__dropdown'; const SEPARATOR_CLASS = 'oncoprintjs__track_options__separator'; const NTH_CLASS_PREFIX = 'nth-'; export const CLOSE_MENUS_EVENT = 'oncoprint-track-options-view.do-close-menus'; type TrackCallback = (trackId: TrackId) => void; export default class OncoprintTrackOptionsView { private $ctr: JQuery; private $buttons_ctr: JQuery; private $dropdown_ctr: JQuery; private img_size: number; private rendering_suppressed = false; private track_options_$elts: TrackProp<{ $div: JQuery; $img: JQuery; $dropdown: JQuery; }> = {}; private menu_shown: TrackProp = {}; private clickHandler: () => void; private interaction_disabled = false; constructor( private $div: JQuery, private moveUpCallback: TrackCallback, private moveDownCallback: TrackCallback, private removeCallback: TrackCallback, private sortChangeCallback: ( trackId: TrackId, sortDirection: TrackSortDirection ) => void, private unexpandCallback: TrackCallback, private showGapsCallback: ( trackId: TrackId, showGaps: GAP_MODE_ENUM ) => void ) { const position = $div.css('position'); if (position !== 'absolute' && position !== 'relative') { console.log( 'WARNING: div passed to OncoprintTrackOptionsView must be absolute or relative positioned - layout problems will occur' ); } this.$ctr = $('
') .css({ position: 'absolute', 'overflow-y': 'hidden', 'overflow-x': 'hidden', }) .appendTo(this.$div); this.$buttons_ctr = $('
') .css({ position: 'absolute' }) .appendTo(this.$ctr); this.$dropdown_ctr = $('
') .css({ position: 'absolute' }) .appendTo(this.$div); const self = this; this.clickHandler = function() { $(document).trigger(CLOSE_MENUS_EVENT); }; $(document).on('click', this.clickHandler); } private renderAllOptions(model: OncoprintModel) { if (this.rendering_suppressed) { return; } const self = this; $(document).off(CLOSE_MENUS_EVENT); $(document).on(CLOSE_MENUS_EVENT, function() { self.hideAllMenus(); }); this.$buttons_ctr.empty(); this.$dropdown_ctr.empty(); this.scroll(model.getVertScroll()); var tracks = model.getTracks(); var minimum_track_height = Number.POSITIVE_INFINITY; for (let i = 0; i < tracks.length; i++) { minimum_track_height = Math.min( minimum_track_height, model.getTrackHeight(tracks[i]) ); } this.img_size = Math.floor(minimum_track_height * 0.75); for (let i = 0; i < tracks.length; i++) { this.renderTrackOptions(model, tracks[i], i); } } private scroll(scroll_y: number) { if (this.rendering_suppressed) { return; } this.$buttons_ctr.css({ top: -scroll_y }); this.$dropdown_ctr.css({ top: -scroll_y }); this.hideAllMenus(); } private resize(model: OncoprintModel, getCellViewHeight: () => number) { if (this.rendering_suppressed) { return; } this.$div.css({ width: this.getWidth(), height: getCellViewHeight() }); this.$ctr.css({ width: this.getWidth(), height: getCellViewHeight() }); } private hideTrackMenu(track_id: TrackId) { this.menu_shown[track_id] = false; const $elts = this.track_options_$elts[track_id]; $elts.$dropdown.css({ 'z-index': 1 }); $elts.$dropdown.css({ border: '1px solid rgba(125,125,125,0)' }); $elts.$img.css({ border: '1px solid rgba(125,125,125,0)' }); $elts.$dropdown.fadeOut(100); } private showTrackMenu(track_id: TrackId) { this.menu_shown[track_id] = true; const $elts = this.track_options_$elts[track_id]; $elts.$dropdown.css({ 'z-index': 10 }); $elts.$dropdown.css({ border: '1px solid rgba(125,125,125,1)' }); $elts.$img.css({ border: '1px solid rgba(125,125,125,1)' }); $elts.$dropdown.fadeIn(100); } private hideAllMenus() { for (const track_id in this.track_options_$elts) { if (this.track_options_$elts.hasOwnProperty(track_id)) { this.hideTrackMenu(parseInt(track_id, 10)); } } } private hideMenusExcept(track_id: TrackId) { for (const _other_track_id in this.track_options_$elts) { if (this.track_options_$elts.hasOwnProperty(_other_track_id)) { const other_track_id = parseInt(_other_track_id, 10); if (other_track_id === track_id) { continue; } this.hideTrackMenu(other_track_id); } } $(document).trigger(HEADER_VIEW_CLOSE_MENUS_EVENT); } private static $makeDropdownOption( text: string, weight: string, disabled?: boolean, callback?: (evt: ClickEvent) => void ) { const li = $('
  • ') .text(text) .css({ 'font-weight': weight, 'font-size': 12, 'border-bottom': '1px solid rgba(0,0,0,0.3)', // Disables the iOS 300ms tap delay and lets click fire on first tap. 'touch-action': 'manipulation', }); if (!disabled) { if (callback) { li.addClass('clickable'); li.css({ cursor: 'pointer' }); // Also fire on touchend so taps work when the synthetic click // doesn't arrive (sticky hover on iOS). preventDefault on the // touch stops the follow-up click so the callback runs once. let fired = false; const invoke = function(evt: any) { if (fired) return; fired = true; setTimeout(() => { fired = false; }, 400); callback(evt); }; li.on('click', invoke); li.on('touchend', function(evt) { evt.preventDefault(); invoke(evt); }); li.hover( function() { $(this).css({ 'background-color': 'rgb(200,200,200)' }); }, function() { $(this).css({ 'background-color': 'rgba(255,255,255,0)', }); } ); } else { li.click(function(evt) { evt.stopPropagation(); }); } } else { li.addClass('disabled'); li.css({ color: 'rgb(200, 200, 200)', cursor: 'default' }); } return li; } private static $makeDropdownSeparator() { return $('
  • ') .css({ 'border-top': '1px solid black' }) .addClass(SEPARATOR_CLASS); } // Hover-expanding parent item that reveals a floating submenu with leaf // options. Used for nested "Sort by > " style menus without // cluttering the top-level dropdown. private static $makeDropdownSubmenuOption( text: string, children: CustomTrackOption[], track_id: TrackId, closeOuterMenu: () => void ) { const $li = $('
  • ') .text(text + ' \u25B8') .css({ 'font-weight': 'normal', 'font-size': 12, 'border-bottom': '1px solid rgba(0,0,0,0.3)', cursor: 'default', position: 'relative', }) .addClass('has-submenu'); const $submenu = $('