commit/galaxy-central: carlfeberhard: Client build: begin modularizing ui.js by extracting peekControl/peek-column-selector plugin, use UMD pattern, rename to peek-column-selector, extract less rules to their own file; Plugins, scatterplot: remove require and rework to use the new module
1 new commit in galaxy-central: https://bitbucket.org/galaxy/galaxy-central/commits/ee995df4a7e5/ Changeset: ee995df4a7e5 User: carlfeberhard Date: 2014-12-08 19:47:48+00:00 Summary: Client build: begin modularizing ui.js by extracting peekControl/peek-column-selector plugin, use UMD pattern, rename to peek-column-selector, extract less rules to their own file; Plugins, scatterplot: remove require and rework to use the new module Affected #: 12 files diff -r bfc3b5e52781683efc8024f18c4aa57c65af1e15 -r ee995df4a7e548befdfa09196b9665f05cfd55f6 client/galaxy/scripts/jq-plugins/ui/peek-column-selector.js --- /dev/null +++ b/client/galaxy/scripts/jq-plugins/ui/peek-column-selector.js @@ -0,0 +1,317 @@ +// from: https://raw.githubusercontent.com/umdjs/umd/master/jqueryPlugin.js +// Uses AMD or browser globals to create a jQuery plugin. +(function (factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(['jquery'], factory); + } else { + // Browser globals + factory(jQuery); + } + +}(function ($) { + //============================================================================== + /** Column selection using the peek display as the control. + * Adds rows to the bottom of the peek with clickable areas in each cell + * to allow the user to select columns. + * Column selection can be limited to a single column or multiple. + * (Optionally) adds a left hand column of column selection prompts. + * (Optionally) allows the column headers to be clicked/renamed + * and set to some initial value. + * (Optionally) hides comment rows. + * (Optionally) allows pre-selecting and disabling certain columns for + * each row control. + * + * Construct by selecting a peek table to be used with jQuery and + * calling 'peekColumnSelector' with options. + * Options must include a 'controls' array and can include other options + * listed below. + * @example: + * $( 'pre.peek' ).peekColumnSelector({ + * columnNames : ["Chromosome", "Start", "Base", "", "", "Qual" ], + * controls : [ + * { label: 'X Column', id: 'xColumn' }, + * { label: 'Y Column', id: 'yColumn', selected: 2 }, + * { label: 'ID Column', id: 'idColumn', selected: 4, disabled: [ 1, 5 ] }, + * { label: 'Heatmap', id: 'heatmap', selected: [ 2, 4 ], disabled: [ 0, 1 ], multiselect: true, + * selectedText: 'Included', unselectedText: 'Excluded' } + * ], + * renameColumns : true, + * hideCommentRows : true, + * includePrompts : true, + * topLeftContent : 'Data sample:' + * }).on( 'peek-column-selector.change', function( ev, selection ){ + * console.info( 'new selection:', selection ); + * //{ yColumn: 2 } + * }).on( 'peek-column-selector.rename', function( ev, names ){ + * console.info( 'column names', names ); + * //[ 'Bler', 'Start', 'Base', '', '', 'Qual' ] + * }); + * + * An event is fired when column selection is changed and the event + * is passed an object in the form: { the row id : the new selection value }. + * An event is also fired when the table headers are re-named and + * is passed the new array of column names. + */ + + /** option defaults */ + var defaults = { + /** does this control allow renaming headers? */ + renameColumns : false, + /** does this control allow renaming headers? */ + columnNames : [], + /** the comment character used by the peek's datatype */ + commentChar : '#', + /** should comment rows be shown or hidden in the peek */ + hideCommentRows : false, + /** should a column of row control prompts be used */ + includePrompts : true, + /** what is the content of the top left cell (often a title) */ + topLeftContent : 'Columns:' + }, + /** class added to the pre.peek element (to allow css on just the control) */ + PEEKCONTROL_CLASS = 'peek-column-selector', + /** the string of the event fired when a control row changes */ + CHANGE_EVENT = 'peek-column-selector.change', + /** the string of the event fired when a column is renamed */ + RENAME_EVENT = 'peek-column-selector.rename', + /** class added to the control rows */ + ROW_CLASS = 'control', + /** class added to the left-hand cells that serve as row prompts */ + PROMPT_CLASS = 'control-prompt', + /** class added to selected _cells_/tds */ + SELECTED_CLASS = 'selected', + /** class added to disabled/un-clickable cells/tds */ + DISABLED_CLASS = 'disabled', + /** class added to the clickable surface within a cell to select it */ + BUTTON_CLASS = 'button', + /** class added to peek table header (th) cells to indicate they can be clicked and are renamable */ + RENAMABLE_HEADER_CLASS = 'renamable-header', + /** the data key used for each cell to store the column index ('data-...') */ + COLUMN_INDEX_DATA_KEY = 'column-index', + /** renamable header data key used to store the column name (w/o the number and dot: '1.Bler') */ + COLUMN_NAME_DATA_KEY = 'column-name'; + + //TODO: not happy with pure functional here - rows should polymorph (multi, single, etc.) + //TODO: needs clean up, move handlers to outer scope + + // ........................................................................ + /** validate the control data sent in for each row */ + function validateControl( control ){ + if( control.disabled && jQuery.type( control.disabled ) !== 'array' ){ + throw new Error( '"disabled" must be defined as an array of indeces: ' + JSON.stringify( control ) ); + } + if( control.multiselect && control.selected && jQuery.type( control.selected ) !== 'array' ){ + throw new Error( 'Mulitselect rows need an array for "selected": ' + JSON.stringify( control ) ); + } + if( !control.label || !control.id ){ + throw new Error( 'Peek controls need a label and id for each control row: ' + JSON.stringify( control ) ); + } + if( control.disabled && control.disabled.indexOf( control.selected ) !== -1 ){ + throw new Error( 'Selected column is in the list of disabled columns: ' + JSON.stringify( control ) ); + } + return control; + } + + /** build the inner control surface (i.e. button-like) */ + function buildButton( control, columnIndex ){ + return $( '<div/>' ).addClass( BUTTON_CLASS ).text( control.label ); + } + + /** build the basic (shared) cell structure */ + function buildControlCell( control, columnIndex ){ + var $td = $( '<td/>' ) + .html( buildButton( control, columnIndex ) ) + .attr( 'data-' + COLUMN_INDEX_DATA_KEY, columnIndex ); + + // disable if index in disabled array + if( control.disabled && control.disabled.indexOf( columnIndex ) !== -1 ){ + $td.addClass( DISABLED_CLASS ); + } + return $td; + } + + /** set the text of the control based on selected/un */ + function setSelectedText( $cell, control, columnIndex ){ + var $button = $cell.children( '.' + BUTTON_CLASS ); + if( $cell.hasClass( SELECTED_CLASS ) ){ + $button.html( ( control.selectedText !== undefined )?( control.selectedText ):( control.label ) ); + } else { + $button.html( ( control.unselectedText !== undefined )?( control.unselectedText ):( control.label ) ); + } + } + + /** build a cell for a row that only allows one selection */ + function buildSingleSelectCell( control, columnIndex ){ + // only one selection - selected is single index + var $cell = buildControlCell( control, columnIndex ); + if( control.selected === columnIndex ){ + $cell.addClass( SELECTED_CLASS ); + } + setSelectedText( $cell, control, columnIndex ); + + // only add the handler to non-disabled controls + if( !$cell.hasClass( DISABLED_CLASS ) ){ + $cell.click( function selectClick( ev ){ + var $cell = $( this ); + // don't re-select or fire event if already selected + if( !$cell.hasClass( SELECTED_CLASS ) ){ + // only one can be selected - remove selected on all others, add it here + var $otherSelected = $cell.parent().children( '.' + SELECTED_CLASS ).removeClass( SELECTED_CLASS ); + $otherSelected.each( function(){ + setSelectedText( $( this ), control, columnIndex ); + }); + + $cell.addClass( SELECTED_CLASS ); + setSelectedText( $cell, control, columnIndex ); + + // fire the event from the table itself, passing the id and index of selected + var eventData = {}, + key = $cell.parent().attr( 'id' ), + val = $cell.data( COLUMN_INDEX_DATA_KEY ); + eventData[ key ] = val; + $cell.parents( '.peek' ).trigger( CHANGE_EVENT, eventData ); + } + }); + } + return $cell; + } + + /** build a cell for a row that allows multiple selections */ + function buildMultiSelectCell( control, columnIndex ){ + var $cell = buildControlCell( control, columnIndex ); + // multiple selection - selected is an array + if( control.selected && control.selected.indexOf( columnIndex ) !== -1 ){ + $cell.addClass( SELECTED_CLASS ); + } + setSelectedText( $cell, control, columnIndex ); + + // only add the handler to non-disabled controls + if( !$cell.hasClass( DISABLED_CLASS ) ){ + $cell.click( function multiselectClick( ev ){ + var $cell = $( this ); + // can be more than one selected - toggle selected on this cell + $cell.toggleClass( SELECTED_CLASS ); + setSelectedText( $cell, control, columnIndex ); + var selectedColumnIndeces = $cell.parent().find( '.' + SELECTED_CLASS ).map( function( i, e ){ + return $( e ).data( COLUMN_INDEX_DATA_KEY ); + }); + // fire the event from the table itself, passing the id and index of selected + var eventData = {}, + key = $cell.parent().attr( 'id' ), + val = jQuery.makeArray( selectedColumnIndeces ); + eventData[ key ] = val; + $cell.parents( '.peek' ).trigger( CHANGE_EVENT, eventData ); + }); + } + return $cell; + } + + /** iterate over columns in peek and create a control for each */ + function buildControlCells( count, control ){ + var $cells = []; + // build a control for each column - using a build fn based on control + for( var columnIndex=0; columnIndex<count; columnIndex+=1 ){ + $cells.push( control.multiselect? buildMultiSelectCell( control, columnIndex ) + : buildSingleSelectCell( control, columnIndex ) ); + } + return $cells; + } + + /** build a row of controls for the peek */ + function buildControlRow( cellCount, control, includePrompts ){ + var $controlRow = $( '<tr/>' ).attr( 'id', control.id ).addClass( ROW_CLASS ); + if( includePrompts ){ + var $promptCell = $( '<td/>' ).addClass( PROMPT_CLASS ).text( control.label + ':' ); + $controlRow.append( $promptCell ); + } + $controlRow.append( buildControlCells( cellCount, control ) ); + return $controlRow; + } + + // ........................................................................ + /** add to the peek, using options for configuration, return the peek */ + function peekColumnSelector( options ){ + options = jQuery.extend( true, {}, defaults, options ); + + var $peek = $( this ).addClass( PEEKCONTROL_CLASS ), + $peektable = $peek.find( 'table' ), + // get the size of the tables - width and height, number of comment rows + columnCount = $peektable.find( 'th' ).size(), + rowCount = $peektable.find( 'tr' ).size(), + // get the rows containing text starting with the comment char (also make them grey) + $commentRows = $peektable.find( 'td[colspan]' ).map( function( e, i ){ + var $this = $( this ); + if( $this.text() && $this.text().match( new RegExp( '^' + options.commentChar ) ) ){ + return $( this ).css( 'color', 'grey' ).parent().get(0); + } + return null; + }); + + // should comment rows in the peek be hidden? + if( options.hideCommentRows ){ + $commentRows.hide(); + rowCount -= $commentRows.size(); + } + //console.debug( 'rowCount:', rowCount, 'columnCount:', columnCount, '$commentRows:', $commentRows ); + + // should a first column of control prompts be added? + if( options.includePrompts ){ + var $topLeft = $( '<th/>' ).addClass( 'top-left' ).text( options.topLeftContent ) + .attr( 'rowspan', rowCount ); + $peektable.find( 'tr' ).first().prepend( $topLeft ); + } + + // save either the options column name or the parsed text of each column header in html5 data attr and text + var $headers = $peektable.find( 'th:not(.top-left)' ).each( function( i, e ){ + var $this = $( this ), + // can be '1.name' or '1' + text = $this.text().replace( /^\d+\.*/, '' ), + name = options.columnNames[ i ] || text; + $this.attr( 'data-' + COLUMN_NAME_DATA_KEY, name ) + .text( ( i + 1 ) + (( name )?( '.' + name ):( '' )) ); + }); + + // allow renaming of columns when the header is clicked + if( options.renameColumns ){ + $headers.addClass( RENAMABLE_HEADER_CLASS ) + .click( function renameColumn(){ + // prompt for new name + var $this = $( this ), + index = $this.index() + ( options.includePrompts? 0: 1 ), + prevName = $this.data( COLUMN_NAME_DATA_KEY ), + newColumnName = prompt( 'New column name:', prevName ); + if( newColumnName !== null && newColumnName !== prevName ){ + // set the new text and data + $this.text( index + ( newColumnName?( '.' + newColumnName ):'' ) ) + .data( COLUMN_NAME_DATA_KEY, newColumnName ) + .attr( 'data-', COLUMN_NAME_DATA_KEY, newColumnName ); + // fire event for new column names + var columnNames = jQuery.makeArray( + $this.parent().children( 'th:not(.top-left)' ).map( function(){ + return $( this ).data( COLUMN_NAME_DATA_KEY ); + })); + $this.parents( '.peek' ).trigger( RENAME_EVENT, columnNames ); + } + }); + } + + // build a row for each control + options.controls.forEach( function( control, i ){ + validateControl( control ); + var $controlRow = buildControlRow( columnCount, control, options.includePrompts ); + $peektable.find( 'tbody' ).append( $controlRow ); + }); + return this; + } + + // ........................................................................ + // as jq plugin + jQuery.fn.extend({ + peekColumnSelector : function $peekColumnSelector( options ){ + return this.map( function(){ + return peekColumnSelector.call( this, options ); + }); + } + }); +})); diff -r bfc3b5e52781683efc8024f18c4aa57c65af1e15 -r ee995df4a7e548befdfa09196b9665f05cfd55f6 client/galaxy/scripts/mvc/ui.js --- a/client/galaxy/scripts/mvc/ui.js +++ b/client/galaxy/scripts/mvc/ui.js @@ -1344,311 +1344,3 @@ } }); }()); - - -//============================================================================== -/** Column selection using the peek display as the control. - * Adds rows to the bottom of the peek with clickable areas in each cell - * to allow the user to select columns. - * Column selection can be limited to a single column or multiple. - * (Optionally) adds a left hand column of column selection prompts. - * (Optionally) allows the column headers to be clicked/renamed - * and set to some initial value. - * (Optionally) hides comment rows. - * (Optionally) allows pre-selecting and disabling certain columns for - * each row control. - * - * Construct by selecting a peek table to be used with jQuery and - * calling 'peekControl' with options. - * Options must include a 'controls' array and can include other options - * listed below. - * @example: - * $( 'pre.peek' ).peekControl({ - * columnNames : ["Chromosome", "Start", "Base", "", "", "Qual" ], - * controls : [ - * { label: 'X Column', id: 'xColumn' }, - * { label: 'Y Column', id: 'yColumn', selected: 2 }, - * { label: 'ID Column', id: 'idColumn', selected: 4, disabled: [ 1, 5 ] }, - * { label: 'Heatmap', id: 'heatmap', selected: [ 2, 4 ], disabled: [ 0, 1 ], multiselect: true, - * selectedText: 'Included', unselectedText: 'Excluded' } - * ], - * renameColumns : true, - * hideCommentRows : true, - * includePrompts : true, - * topLeftContent : 'Data sample:' - * }).on( 'peek-control.change', function( ev, selection ){ - * console.info( 'new selection:', selection ); - * //{ yColumn: 2 } - * }).on( 'peek-control.rename', function( ev, names ){ - * console.info( 'column names', names ); - * //[ 'Bler', 'Start', 'Base', '', '', 'Qual' ] - * }); - * - * An event is fired when column selection is changed and the event - * is passed an object in the form: { the row id : the new selection value }. - * An event is also fired when the table headers are re-named and - * is passed the new array of column names. - */ -(function(){ - - /** option defaults */ - var defaults = { - /** does this control allow renaming headers? */ - renameColumns : false, - /** does this control allow renaming headers? */ - columnNames : [], - /** the comment character used by the peek's datatype */ - commentChar : '#', - /** should comment rows be shown or hidden in the peek */ - hideCommentRows : false, - /** should a column of row control prompts be used */ - includePrompts : true, - /** what is the content of the top left cell (often a title) */ - topLeftContent : 'Columns:' - }, - /** the string of the event fired when a control row changes */ - CHANGE_EVENT = 'peek-control.change', - /** the string of the event fired when a column is renamed */ - RENAME_EVENT = 'peek-control.rename', - /** class added to the pre.peek element (to allow css on just the control) */ - PEEKCONTROL_CLASS = 'peek-control', - /** class added to the control rows */ - ROW_CLASS = 'control', - /** class added to the left-hand cells that serve as row prompts */ - PROMPT_CLASS = 'control-prompt', - /** class added to selected _cells_/tds */ - SELECTED_CLASS = 'selected', - /** class added to disabled/un-clickable cells/tds */ - DISABLED_CLASS = 'disabled', - /** class added to the clickable surface within a cell to select it */ - BUTTON_CLASS = 'button', - /** class added to peek table header (th) cells to indicate they can be clicked and are renamable */ - RENAMABLE_HEADER_CLASS = 'renamable-header', - /** the data key used for each cell to store the column index ('data-...') */ - COLUMN_INDEX_DATA_KEY = 'column-index', - /** renamable header data key used to store the column name (w/o the number and dot: '1.Bler') */ - COLUMN_NAME_DATA_KEY = 'column-name'; - - //TODO: not happy with pure functional here - rows should polymorph (multi, single, etc.) - //TODO: needs clean up, move handlers to outer scope - - // ........................................................................ - /** validate the control data sent in for each row */ - function validateControl( control ){ - if( control.disabled && jQuery.type( control.disabled ) !== 'array' ){ - throw new Error( '"disabled" must be defined as an array of indeces: ' + JSON.stringify( control ) ); - } - if( control.multiselect && control.selected && jQuery.type( control.selected ) !== 'array' ){ - throw new Error( 'Mulitselect rows need an array for "selected": ' + JSON.stringify( control ) ); - } - if( !control.label || !control.id ){ - throw new Error( 'Peek controls need a label and id for each control row: ' + JSON.stringify( control ) ); - } - if( control.disabled && control.disabled.indexOf( control.selected ) !== -1 ){ - throw new Error( 'Selected column is in the list of disabled columns: ' + JSON.stringify( control ) ); - } - return control; - } - - /** build the inner control surface (i.e. button-like) */ - function buildButton( control, columnIndex ){ - return $( '<div/>' ).addClass( BUTTON_CLASS ).text( control.label ); - } - - /** build the basic (shared) cell structure */ - function buildControlCell( control, columnIndex ){ - var $td = $( '<td/>' ) - .html( buildButton( control, columnIndex ) ) - .attr( 'data-' + COLUMN_INDEX_DATA_KEY, columnIndex ); - - // disable if index in disabled array - if( control.disabled && control.disabled.indexOf( columnIndex ) !== -1 ){ - $td.addClass( DISABLED_CLASS ); - } - return $td; - } - - /** set the text of the control based on selected/un */ - function setSelectedText( $cell, control, columnIndex ){ - var $button = $cell.children( '.' + BUTTON_CLASS ); - if( $cell.hasClass( SELECTED_CLASS ) ){ - $button.html( ( control.selectedText !== undefined )?( control.selectedText ):( control.label ) ); - } else { - $button.html( ( control.unselectedText !== undefined )?( control.unselectedText ):( control.label ) ); - } - } - - /** build a cell for a row that only allows one selection */ - function buildSingleSelectCell( control, columnIndex ){ - // only one selection - selected is single index - var $cell = buildControlCell( control, columnIndex ); - if( control.selected === columnIndex ){ - $cell.addClass( SELECTED_CLASS ); - } - setSelectedText( $cell, control, columnIndex ); - - // only add the handler to non-disabled controls - if( !$cell.hasClass( DISABLED_CLASS ) ){ - $cell.click( function selectClick( ev ){ - var $cell = $( this ); - // don't re-select or fire event if already selected - if( !$cell.hasClass( SELECTED_CLASS ) ){ - // only one can be selected - remove selected on all others, add it here - var $otherSelected = $cell.parent().children( '.' + SELECTED_CLASS ).removeClass( SELECTED_CLASS ); - $otherSelected.each( function(){ - setSelectedText( $( this ), control, columnIndex ); - }); - - $cell.addClass( SELECTED_CLASS ); - setSelectedText( $cell, control, columnIndex ); - - // fire the event from the table itself, passing the id and index of selected - var eventData = {}, - key = $cell.parent().attr( 'id' ), - val = $cell.data( COLUMN_INDEX_DATA_KEY ); - eventData[ key ] = val; - $cell.parents( '.peek' ).trigger( CHANGE_EVENT, eventData ); - } - }); - } - return $cell; - } - - /** build a cell for a row that allows multiple selections */ - function buildMultiSelectCell( control, columnIndex ){ - var $cell = buildControlCell( control, columnIndex ); - // multiple selection - selected is an array - if( control.selected && control.selected.indexOf( columnIndex ) !== -1 ){ - $cell.addClass( SELECTED_CLASS ); - } - setSelectedText( $cell, control, columnIndex ); - - // only add the handler to non-disabled controls - if( !$cell.hasClass( DISABLED_CLASS ) ){ - $cell.click( function multiselectClick( ev ){ - var $cell = $( this ); - // can be more than one selected - toggle selected on this cell - $cell.toggleClass( SELECTED_CLASS ); - setSelectedText( $cell, control, columnIndex ); - var selectedColumnIndeces = $cell.parent().find( '.' + SELECTED_CLASS ).map( function( i, e ){ - return $( e ).data( COLUMN_INDEX_DATA_KEY ); - }); - // fire the event from the table itself, passing the id and index of selected - var eventData = {}, - key = $cell.parent().attr( 'id' ), - val = jQuery.makeArray( selectedColumnIndeces ); - eventData[ key ] = val; - $cell.parents( '.peek' ).trigger( CHANGE_EVENT, eventData ); - }); - } - return $cell; - } - - /** iterate over columns in peek and create a control for each */ - function buildControlCells( count, control ){ - var $cells = []; - // build a control for each column - using a build fn based on control - for( var columnIndex=0; columnIndex<count; columnIndex+=1 ){ - $cells.push( control.multiselect? buildMultiSelectCell( control, columnIndex ) - : buildSingleSelectCell( control, columnIndex ) ); - } - return $cells; - } - - /** build a row of controls for the peek */ - function buildControlRow( cellCount, control, includePrompts ){ - var $controlRow = $( '<tr/>' ).attr( 'id', control.id ).addClass( ROW_CLASS ); - if( includePrompts ){ - var $promptCell = $( '<td/>' ).addClass( PROMPT_CLASS ).text( control.label + ':' ); - $controlRow.append( $promptCell ); - } - $controlRow.append( buildControlCells( cellCount, control ) ); - return $controlRow; - } - - // ........................................................................ - /** add to the peek, using options for configuration, return the peek */ - function peekControl( options ){ - options = jQuery.extend( true, {}, defaults, options ); - - var $peek = $( this ).addClass( PEEKCONTROL_CLASS ), - $peektable = $peek.find( 'table' ), - // get the size of the tables - width and height, number of comment rows - columnCount = $peektable.find( 'th' ).size(), - rowCount = $peektable.find( 'tr' ).size(), - // get the rows containing text starting with the comment char (also make them grey) - $commentRows = $peektable.find( 'td[colspan]' ).map( function( e, i ){ - var $this = $( this ); - if( $this.text() && $this.text().match( new RegExp( '^' + options.commentChar ) ) ){ - return $( this ).css( 'color', 'grey' ).parent().get(0); - } - return null; - }); - - // should comment rows in the peek be hidden? - if( options.hideCommentRows ){ - $commentRows.hide(); - rowCount -= $commentRows.size(); - } - //console.debug( 'rowCount:', rowCount, 'columnCount:', columnCount, '$commentRows:', $commentRows ); - - // should a first column of control prompts be added? - if( options.includePrompts ){ - var $topLeft = $( '<th/>' ).addClass( 'top-left' ).text( options.topLeftContent ) - .attr( 'rowspan', rowCount ); - $peektable.find( 'tr' ).first().prepend( $topLeft ); - } - - // save either the options column name or the parsed text of each column header in html5 data attr and text - var $headers = $peektable.find( 'th:not(.top-left)' ).each( function( i, e ){ - var $this = $( this ), - // can be '1.name' or '1' - text = $this.text().replace( /^\d+\.*/, '' ), - name = options.columnNames[ i ] || text; - $this.attr( 'data-' + COLUMN_NAME_DATA_KEY, name ) - .text( ( i + 1 ) + (( name )?( '.' + name ):( '' )) ); - }); - - // allow renaming of columns when the header is clicked - if( options.renameColumns ){ - $headers.addClass( RENAMABLE_HEADER_CLASS ) - .click( function renameColumn(){ - // prompt for new name - var $this = $( this ), - index = $this.index() + ( options.includePrompts? 0: 1 ), - prevName = $this.data( COLUMN_NAME_DATA_KEY ), - newColumnName = prompt( 'New column name:', prevName ); - if( newColumnName !== null && newColumnName !== prevName ){ - // set the new text and data - $this.text( index + ( newColumnName?( '.' + newColumnName ):'' ) ) - .data( COLUMN_NAME_DATA_KEY, newColumnName ) - .attr( 'data-', COLUMN_NAME_DATA_KEY, newColumnName ); - // fire event for new column names - var columnNames = jQuery.makeArray( - $this.parent().children( 'th:not(.top-left)' ).map( function(){ - return $( this ).data( COLUMN_NAME_DATA_KEY ); - })); - $this.parents( '.peek' ).trigger( RENAME_EVENT, columnNames ); - } - }); - } - - // build a row for each control - options.controls.forEach( function( control, i ){ - validateControl( control ); - var $controlRow = buildControlRow( columnCount, control, options.includePrompts ); - $peektable.find( 'tbody' ).append( $controlRow ); - }); - return this; - } - - // ........................................................................ - // as jq plugin - jQuery.fn.extend({ - peekControl : function $peekControl( options ){ - return this.map( function(){ - return peekControl.call( this, options ); - }); - } - }); -}()); diff -r bfc3b5e52781683efc8024f18c4aa57c65af1e15 -r ee995df4a7e548befdfa09196b9665f05cfd55f6 config/plugins/visualizations/scatterplot/src/scatterplot-config-editor.js --- a/config/plugins/visualizations/scatterplot/src/scatterplot-config-editor.js +++ b/config/plugins/visualizations/scatterplot/src/scatterplot-config-editor.js @@ -120,7 +120,7 @@ peek : this.dataset.peek })); - $dataControl.find( '.peek' ).peekControl({ + $dataControl.find( '.peek' ).peekColumnSelector({ controls : [ { label: 'X Column', id: 'xColumn', selected: config.xColumn, disabled: columnTypes.text }, { label: 'Y Column', id: 'yColumn', selected: config.yColumn, disabled: columnTypes.text }, @@ -128,11 +128,11 @@ ] //renameColumns : true - }).on( 'peek-control.change', function( ev, data ){ + }).on( 'peek-column-selector.change', function( ev, data ){ //console.info( 'new selection:', data ); editor.model.set( 'config', data ); - }).on( 'peek-control.rename', function( ev, data ){ + }).on( 'peek-column-selector.rename', function( ev, data ){ //console.info( 'new column names', data ); }); diff -r bfc3b5e52781683efc8024f18c4aa57c65af1e15 -r ee995df4a7e548befdfa09196b9665f05cfd55f6 config/plugins/visualizations/scatterplot/static/scatterplot-edit.js --- a/config/plugins/visualizations/scatterplot/static/scatterplot-edit.js +++ b/config/plugins/visualizations/scatterplot/static/scatterplot-edit.js @@ -1,1 +1,1 @@ -function scatterplot(a,b,c){function d(){var a={v:{},h:{}};return a.v.lines=p.selectAll("line.v-grid-line").data(m.x.ticks(q.x.fn.ticks()[0])),a.v.lines.enter().append("svg:line").classed("grid-line v-grid-line",!0),a.v.lines.attr("x1",m.x).attr("x2",m.x).attr("y1",0).attr("y2",b.height),a.v.lines.exit().remove(),a.h.lines=p.selectAll("line.h-grid-line").data(m.y.ticks(q.y.fn.ticks()[0])),a.h.lines.enter().append("svg:line").classed("grid-line h-grid-line",!0),a.h.lines.attr("x1",0).attr("x2",b.width).attr("y1",m.y).attr("y2",m.y),a.h.lines.exit().remove(),a}function e(){return t.attr("cx",function(a,b){return m.x(j(a,b))}).attr("cy",function(a,b){return m.y(k(a,b))}).style("display","block").filter(function(){var a=d3.select(this).attr("cx"),c=d3.select(this).attr("cy");return 0>a||a>b.width?!0:0>c||c>b.height?!0:!1}).style("display","none")}function f(){$(".chart-info-box").remove(),q.redraw(),e(),s=d(),$(o.node()).trigger("zoom.scatterplot",{scale:n.scale(),translate:n.translate()})}function g(a,c,d){return c+=8,$(['<div class="chart-info-box" style="position: absolute">',void 0!==b.idColumn?"<div>"+d[b.idColumn]+"</div>":"","<div>",j(d),"</div>","<div>",k(d),"</div>","</div>"].join("")).css({top:a,left:c,"z-index":2})}var h=function(a,b){return"translate("+a+","+b+")"},i=function(a,b,c){return"rotate("+a+","+b+","+c+")"},j=function(a){return a[b.xColumn]},k=function(a){return a[b.yColumn]},l={x:{extent:d3.extent(c,j)},y:{extent:d3.extent(c,k)}},m={x:d3.scale.linear().domain(l.x.extent).range([0,b.width]),y:d3.scale.linear().domain(l.y.extent).range([b.height,0])},n=d3.behavior.zoom().x(m.x).y(m.y).scaleExtent([1,30]).scale(b.scale||1).translate(b.translate||[0,0]),o=d3.select(a).attr("class","scatterplot").attr("width","100%").attr("height",b.height+(b.margin.top+b.margin.bottom)),p=o.append("g").attr("class","content").attr("transform",h(b.margin.left,b.margin.top)).call(n);p.append("rect").attr("class","zoom-rect").attr("width",b.width).attr("height",b.height).style("fill","transparent");var q={x:{},y:{}};q.x.fn=d3.svg.axis().orient("bottom").scale(m.x).ticks(b.xTicks).tickFormat(d3.format("s")),q.y.fn=d3.svg.axis().orient("left").scale(m.y).ticks(b.yTicks).tickFormat(d3.format("s")),q.x.g=p.append("g").attr("class","x axis").attr("transform",h(0,b.height)).call(q.x.fn),q.y.g=p.append("g").attr("class","y axis").call(q.y.fn);var r=6;q.x.label=o.append("text").attr("id","x-axis-label").attr("class","axis-label").text(b.xLabel).attr("text-anchor","middle").attr("dominant-baseline","text-after-edge").attr("x",b.width/2+b.margin.left).attr("y",b.height+b.margin.bottom+b.margin.top-r),q.y.label=o.append("text").attr("id","y-axis-label").attr("class","axis-label").text(b.yLabel).attr("text-anchor","middle").attr("dominant-baseline","text-before-edge").attr("x",r).attr("y",b.height/2).attr("transform",i(-90,r,b.height/2)),q.redraw=function(){o.select(".x.axis").call(q.x.fn),o.select(".y.axis").call(q.y.fn)};var s=d(),t=p.selectAll(".glyph").data(c).enter().append("svg:circle").classed("glyph",!0).attr("cx",function(a,b){return m.x(j(a,b))}).attr("cy",function(a,b){return m.y(k(a,b))}).attr("r",0);t.transition().duration(b.animDuration).attr("r",b.datapointSize),e(),n.on("zoom",f),t.on("mouseover",function(a,c){var d=d3.select(this);d.classed("highlight",!0).style("fill","red").style("fill-opacity",1),p.append("line").attr("stroke","red").attr("stroke-width",1).attr("x1",d.attr("cx")-b.datapointSize).attr("y1",d.attr("cy")).attr("x2",0).attr("y2",d.attr("cy")).classed("hoverline",!0),d.attr("cy")<b.height&&p.append("line").attr("stroke","red").attr("stroke-width",1).attr("x1",d.attr("cx")).attr("y1",+d.attr("cy")+b.datapointSize).attr("x2",d.attr("cx")).attr("y2",b.height).classed("hoverline",!0);var e=this.getBoundingClientRect();$("body").append(g(e.top,e.right,a)),$(o.node()).trigger("mouseover-datapoint.scatterplot",[this,a,c])}),t.on("mouseout",function(){d3.select(this).classed("highlight",!1).style("fill","black").style("fill-opacity",.2),p.selectAll(".hoverline").remove(),$(".chart-info-box").remove()})}this.scatterplot=this.scatterplot||{},this.scatterplot.chartcontrol=Handlebars.template(function(a,b,c,d,e){this.compilerInfo=[4,">= 1.0.0"],c=this.merge(c,a.helpers),e=e||{};var f,g="",h="function",i=this.escapeExpression;return g+='<p class="help-text">\n Use the following controls to how the chart is displayed.\n The slide controls can be moved by the mouse or, if the \'handle\' is in focus, your keyboard\'s arrow keys.\n Move the focus between controls by using the tab or shift+tab keys on your keyboard.\n Use the \'Draw\' button to render (or re-render) the chart with the current settings.\n</p>\n\n<div data-config-key="datapointSize" class="form-input numeric-slider-input">\n <label for="datapointSize">Size of data point: </label>\n <div class="slider-output">',(f=c.datapointSize)?f=f.call(b,{hash:{},data:e}):(f=b.datapointSize,f=typeof f===h?f.apply(b):f),g+=i(f)+'</div>\n <div class="slider"></div>\n <p class="form-help help-text-small">\n Size of the graphic representation of each data point\n </p>\n</div>\n\n<div data-config-key="width" class="form-input numeric-slider-input">\n <label for="width">Chart width: </label>\n <div class="slider-output">',(f=c.width)?f=f.call(b,{hash:{},data:e}):(f=b.width,f=typeof f===h?f.apply(b):f),g+=i(f)+'</div>\n <div class="slider"></div>\n <p class="form-help help-text-small">\n (not including chart margins and axes)\n </p>\n</div>\n\n<div data-config-key="height" class="form-input numeric-slider-input">\n <label for="height">Chart height: </label>\n <div class="slider-output">',(f=c.height)?f=f.call(b,{hash:{},data:e}):(f=b.height,f=typeof f===h?f.apply(b):f),g+=i(f)+'</div>\n <div class="slider"></div>\n <p class="form-help help-text-small">\n (not including chart margins and axes)\n </p>\n</div>\n\n<div data-config-key="X-axis-label"class="text-input form-input">\n <label for="X-axis-label">Re-label the X axis: </label>\n <input type="text" name="X-axis-label" id="X-axis-label" value="',(f=c.xLabel)?f=f.call(b,{hash:{},data:e}):(f=b.xLabel,f=typeof f===h?f.apply(b):f),g+=i(f)+'" />\n <p class="form-help help-text-small"></p>\n</div>\n\n<div data-config-key="Y-axis-label" class="text-input form-input">\n <label for="Y-axis-label">Re-label the Y axis: </label>\n <input type="text" name="Y-axis-label" id="Y-axis-label" value="',(f=c.yLabel)?f=f.call(b,{hash:{},data:e}):(f=b.yLabel,f=typeof f===h?f.apply(b):f),g+=i(f)+'" />\n <p class="form-help help-text-small"></p>\n</div>\n\n<button class="render-button btn btn-primary active">Draw</button>\n'}),this.scatterplot.datacontrol=Handlebars.template(function(a,b,c,d,e){this.compilerInfo=[4,">= 1.0.0"],c=this.merge(c,a.helpers),e=e||{};var f,g="",h="function";return g+='<p class="help-text">\n Use the following control to change which columns are used by the chart. Click any cell\n from the last three rows of the table to select the column for the appropriate data.\n Use the \'Draw\' button to render (or re-render) the chart with the current settings.\n</p>\n\n<ul class="help-text" style="margin-left: 8px">\n <li><b>X Column</b>: which column values will be used for the x axis of the chart.</li>\n <li><b>Y Column</b>: which column values will be used for the y axis of the chart.</li>\n <li><b>ID Column</b>: an additional column value displayed when the user hovers over a data point.\n It may be useful to select unique or categorical identifiers here (such as gene ids).\n </li>\n</ul>\n\n<div class="column-selection">\n <pre class="peek">',(f=c.peek)?f=f.call(b,{hash:{},data:e}):(f=b.peek,f=typeof f===h?f.apply(b):f),(f||0===f)&&(g+=f),g+='</pre>\n</div>\n\n<p class="help-text help-text-small">\n <b>Note</b>: If it can be determined from the dataset\'s filetype that a column is not numeric,\n that column choice may be disabled for either the x or y axis.\n</p>\n\n<button class="render-button btn btn-primary active">Draw</button>\n'}),this.scatterplot.editor=Handlebars.template(function(a,b,c,d,e){this.compilerInfo=[4,">= 1.0.0"],c=this.merge(c,a.helpers),e=e||{};var f="";return f+='<div class="scatterplot-editor tabbable tabs-left">\n \n <ul class="nav nav-tabs">\n \n <li class="active">\n <a title="Use this tab to change which data are used"\n href="#data-control" data-toggle="tab">Data Controls</a>\n </li>\n <li>\n <a title="Use this tab to change how the chart is drawn"\n href="#chart-control" data-toggle="tab" >Chart Controls</a>\n </li>\n \n <li class="disabled">\n <a title="This tab will display the chart"\n href="#chart-display" data-toggle="tab">Chart</a>\n </li>\n \n <li class="file-controls">\n<!-- <button class="copy-btn btn btn-default"\n title="Save this as a new visualization">Save to new</button>-->\n <button class="save-btn btn btn-default">Save</button>\n </li>\n </ul>\n\n \n <div class="tab-content">\n \n <div id="data-control" class="scatterplot-config-control tab-pane active">\n \n </div>\n \n \n <div id="chart-control" class="scatterplot-config-control tab-pane">\n \n </div>\n\n \n <div id="chart-display" class="scatterplot-display tab-pane"></div>\n\n </div>\n</div>\n'});var ScatterplotConfigEditor=Backbone.View.extend({className:"scatterplot-control-form",initialize:function(a){if(this.model||(this.model=new Visualization({type:"scatterplot"})),!a||!a.dataset)throw new Error("ScatterplotConfigEditor requires a dataset");this.dataset=a.dataset,this.display=new ScatterplotDisplay({dataset:a.dataset,model:this.model})},render:function(){this.$el.empty().append(ScatterplotConfigEditor.templates.mainLayout({})),this.model.id&&(this.$el.find(".copy-btn").show(),this.$el.find(".save-btn").text("Update saved")),this.$el.find("[title]").tooltip(),this._render_dataControl(),this._render_chartControls(),this._render_chartDisplay();var a=this.model.get("config");return this.model.id&&_.isFinite(a.xColumn)&&_.isFinite(a.yColumn)&&this.renderChart(),this},_getColumnIndecesByType:function(){var a={numeric:[],text:[],all:[]};return _.each(this.dataset.metadata_column_types||[],function(b,c){"int"===b||"float"===b?a.numeric.push(c):("str"===b||"list"===b)&&a.text.push(c),a.all.push(c)}),a.numeric.length<2&&(a.numeric=[]),a},_render_dataControl:function(a){a=a||this.$el;var b=this,c=this.model.get("config"),d=this._getColumnIndecesByType(),e=a.find(".tab-pane#data-control");return e.html(ScatterplotConfigEditor.templates.dataControl({peek:this.dataset.peek})),e.find(".peek").peekControl({controls:[{label:"X Column",id:"xColumn",selected:c.xColumn,disabled:d.text},{label:"Y Column",id:"yColumn",selected:c.yColumn,disabled:d.text},{label:"ID Column",id:"idColumn",selected:c.idColumn}]}).on("peek-control.change",function(a,c){b.model.set("config",c)}).on("peek-control.rename",function(){}),e.find("[title]").tooltip(),e},_render_chartControls:function(a){function b(){var a=$(this),b=a.slider("value");c.model.set("config",_.object([[a.parent().data("config-key"),b]])),a.siblings(".slider-output").text(b)}a=a||this.$el;var c=this,d=this.model.get("config"),e=a.find("#chart-control");e.html(ScatterplotConfigEditor.templates.chartControl(d));var f={datapointSize:{min:2,max:10,step:1},width:{min:200,max:800,step:20},height:{min:200,max:800,step:20}};e.find(".numeric-slider-input").each(function(){var a=$(this),c=a.attr("data-config-key"),e=_.extend(f[c],{value:d[c],change:b,slide:b});a.find(".slider").slider(e),a.children(".slider-output").text(d[c])});var g=this.dataset.metadata_column_names||[],h=d.xLabel||g[d.xColumn]||"X",i=d.yLabel||g[d.yColumn]||"Y";return e.find('input[name="X-axis-label"]').val(h).on("change",function(){c.model.set("config",{xLabel:$(this).val()})}),e.find('input[name="Y-axis-label"]').val(i).on("change",function(){c.model.set("config",{yLabel:$(this).val()})}),e.find("[title]").tooltip(),e},_render_chartDisplay:function(a){a=a||this.$el;var b=a.find(".tab-pane#chart-display");return this.display.setElement(b),this.display.render(),b.find("[title]").tooltip(),b},events:{"change #include-id-checkbox":"toggleThirdColumnSelector","click #data-control .render-button":"renderChart","click #chart-control .render-button":"renderChart","click .save-btn":"saveVisualization"},saveVisualization:function(){var a=this;this.model.save().fail(function(b,c,d){console.error(b,c,d),a.trigger("save:error",view),alert("Error loading data:\n"+b.responseText)}).then(function(){a.display.render()})},toggleThirdColumnSelector:function(){this.$el.find('select[name="idColumn"]').parent().toggle()},renderChart:function(){this.$el.find(".nav li.disabled").removeClass("disabled"),this.$el.find("ul.nav").find('a[href="#chart-display"]').tab("show"),this.display.fetchData()},toString:function(){return"ScatterplotConfigEditor("+(this.dataset?this.dataset.id:"")+")"}});ScatterplotConfigEditor.templates={mainLayout:scatterplot.editor,dataControl:scatterplot.datacontrol,chartControl:scatterplot.chartcontrol};var ScatterplotDisplay=Backbone.View.extend({initialize:function(a){this.data=null,this.dataset=a.dataset,this.lineCount=this.dataset.metadata_data_lines||null},fetchData:function(){this.showLoadingIndicator();var a=this,b=this.model.get("config"),c=jQuery.getJSON("/api/datasets/"+this.dataset.id,{data_type:"raw_data",provider:"dataset-column",limit:b.pagination.perPage,offset:b.pagination.currPage*b.pagination.perPage});return c.done(function(b){a.data=b.data,a.trigger("data:fetched",a),a.renderData()}),c.fail(function(b,c,d){console.error(b,c,d),a.trigger("data:error",a),alert("Error loading data:\n"+b.responseText)}),c},showLoadingIndicator:function(){this.$el.find(".scatterplot-data-info").html(['<div class="loading-indicator">','<span class="fa fa-spinner fa-spin"></span>','<span class="loading-indicator-message">loading...</span>',"</div>"].join(""))},template:function(){var a=['<div class="controls clear">','<div class="right">','<p class="scatterplot-data-info"></p>','<button class="stats-toggle-btn">Stats</button>','<button class="rerender-btn">Redraw</button>',"</div>",'<div class="left">','<div class="page-control"></div>',"</div>","</div>","<svg/>",'<div class="stats-display"></div>'].join("");return a},render:function(){return this.$el.addClass("scatterplot-display").html(this.template()),this.data&&this.renderData(),this},renderData:function(){this.renderLeftControls(),this.renderRightControls(),this.renderPlot(this.data),this.getStats()},renderLeftControls:function(){var a=this,b=this.model.get("config");return this.$el.find(".controls .left .page-control").pagination({startingPage:b.pagination.currPage,perPage:b.pagination.perPage,totalDataSize:this.lineCount,currDataSize:this.data.length}).off().on("pagination.page-change",function(c,d){b.pagination.currPage=d,a.model.set("config",{pagination:b.pagination}),a.resetZoom(),a.fetchData()}),this},renderRightControls:function(){var a=this;this.setLineInfo(this.data),this.$el.find(".stats-toggle-btn").off().click(function(){a.toggleStats()}),this.$el.find(".rerender-btn").off().click(function(){a.resetZoom(),a.renderPlot(this.data)})},renderPlot:function(){var a=this,b=this.$el.find("svg");this.toggleStats(!1),b.off().empty().show().on("zoom.scatterplot",function(b,c){a.model.set("config",c)}),scatterplot(b.get(0),this.model.get("config"),this.data)},setLineInfo:function(a,b){if(a){var c=this.model.get("config"),d=this.lineCount||"an unknown total",e=c.pagination.currPage*c.pagination.perPage,f=e+a.length;this.$el.find(".controls p.scatterplot-data-info").text([e+1,"to",f,"of",d].join(" "))}else this.$el.find(".controls p.scatterplot-data-info").html(b||"");return this},resetZoom:function(a,b){return a=void 0!==a?a:1,b=void 0!==b?b:[0,0],this.model.set("config",{scale:a,translate:b}),this},getStats:function(){if(this.data){var a=this,b=this.model.get("config"),c=new Worker("/plugins/visualizations/scatterplot/static/worker-stats.js");c.postMessage({data:this.data,keys:[b.xColumn,b.yColumn]}),c.onerror=function(){c.terminate()},c.onmessage=function(b){a.renderStats(b.data)}}},renderStats:function(a){var b=this.model.get("config"),c=this.$el.find(".stats-display"),d=b.xLabel,e=b.yLabel,f=$("<table/>").addClass("table").append(["<thead><th></th><th>",d,"</th><th>",e,"</th></thead>"].join("")).append(_.map(a,function(a,b){return $(["<tr><td>",b,"</td><td>",a[0],"</td><td>",a[1],"</td></tr>"].join(""))}));c.empty().append(f)},toggleStats:function(a){var b=this.$el.find(".stats-display");a=void 0===a?b.is(":hidden"):a,a?(this.$el.find("svg").hide(),b.show(),this.$el.find(".controls .stats-toggle-btn").text("Plot")):(b.hide(),this.$el.find("svg").show(),this.$el.find(".controls .stats-toggle-btn").text("Stats"))},toString:function(){return"ScatterplotView()"}}),ScatterplotModel=Visualization.extend({defaults:{type:"scatterplot",config:{pagination:{currPage:0,perPage:3e3},width:400,height:400,margin:{top:16,right:16,bottom:40,left:54},xTicks:10,xLabel:"X",yTicks:10,yLabel:"Y",datapointSize:4,animDuration:500,scale:1,translate:[0,0]}}}); \ No newline at end of file +function scatterplot(a,b,c){function d(){var a={v:{},h:{}};return a.v.lines=p.selectAll("line.v-grid-line").data(m.x.ticks(q.x.fn.ticks()[0])),a.v.lines.enter().append("svg:line").classed("grid-line v-grid-line",!0),a.v.lines.attr("x1",m.x).attr("x2",m.x).attr("y1",0).attr("y2",b.height),a.v.lines.exit().remove(),a.h.lines=p.selectAll("line.h-grid-line").data(m.y.ticks(q.y.fn.ticks()[0])),a.h.lines.enter().append("svg:line").classed("grid-line h-grid-line",!0),a.h.lines.attr("x1",0).attr("x2",b.width).attr("y1",m.y).attr("y2",m.y),a.h.lines.exit().remove(),a}function e(){return t.attr("cx",function(a,b){return m.x(j(a,b))}).attr("cy",function(a,b){return m.y(k(a,b))}).style("display","block").filter(function(){var a=d3.select(this).attr("cx"),c=d3.select(this).attr("cy");return 0>a||a>b.width?!0:0>c||c>b.height?!0:!1}).style("display","none")}function f(){$(".chart-info-box").remove(),q.redraw(),e(),s=d(),$(o.node()).trigger("zoom.scatterplot",{scale:n.scale(),translate:n.translate()})}function g(a,c,d){return c+=8,$(['<div class="chart-info-box" style="position: absolute">',void 0!==b.idColumn?"<div>"+d[b.idColumn]+"</div>":"","<div>",j(d),"</div>","<div>",k(d),"</div>","</div>"].join("")).css({top:a,left:c,"z-index":2})}var h=function(a,b){return"translate("+a+","+b+")"},i=function(a,b,c){return"rotate("+a+","+b+","+c+")"},j=function(a){return a[b.xColumn]},k=function(a){return a[b.yColumn]},l={x:{extent:d3.extent(c,j)},y:{extent:d3.extent(c,k)}},m={x:d3.scale.linear().domain(l.x.extent).range([0,b.width]),y:d3.scale.linear().domain(l.y.extent).range([b.height,0])},n=d3.behavior.zoom().x(m.x).y(m.y).scaleExtent([1,30]).scale(b.scale||1).translate(b.translate||[0,0]),o=d3.select(a).attr("class","scatterplot").attr("width","100%").attr("height",b.height+(b.margin.top+b.margin.bottom)),p=o.append("g").attr("class","content").attr("transform",h(b.margin.left,b.margin.top)).call(n);p.append("rect").attr("class","zoom-rect").attr("width",b.width).attr("height",b.height).style("fill","transparent");var q={x:{},y:{}};q.x.fn=d3.svg.axis().orient("bottom").scale(m.x).ticks(b.xTicks).tickFormat(d3.format("s")),q.y.fn=d3.svg.axis().orient("left").scale(m.y).ticks(b.yTicks).tickFormat(d3.format("s")),q.x.g=p.append("g").attr("class","x axis").attr("transform",h(0,b.height)).call(q.x.fn),q.y.g=p.append("g").attr("class","y axis").call(q.y.fn);var r=6;q.x.label=o.append("text").attr("id","x-axis-label").attr("class","axis-label").text(b.xLabel).attr("text-anchor","middle").attr("dominant-baseline","text-after-edge").attr("x",b.width/2+b.margin.left).attr("y",b.height+b.margin.bottom+b.margin.top-r),q.y.label=o.append("text").attr("id","y-axis-label").attr("class","axis-label").text(b.yLabel).attr("text-anchor","middle").attr("dominant-baseline","text-before-edge").attr("x",r).attr("y",b.height/2).attr("transform",i(-90,r,b.height/2)),q.redraw=function(){o.select(".x.axis").call(q.x.fn),o.select(".y.axis").call(q.y.fn)};var s=d(),t=p.selectAll(".glyph").data(c).enter().append("svg:circle").classed("glyph",!0).attr("cx",function(a,b){return m.x(j(a,b))}).attr("cy",function(a,b){return m.y(k(a,b))}).attr("r",0);t.transition().duration(b.animDuration).attr("r",b.datapointSize),e(),n.on("zoom",f),t.on("mouseover",function(a,c){var d=d3.select(this);d.classed("highlight",!0).style("fill","red").style("fill-opacity",1),p.append("line").attr("stroke","red").attr("stroke-width",1).attr("x1",d.attr("cx")-b.datapointSize).attr("y1",d.attr("cy")).attr("x2",0).attr("y2",d.attr("cy")).classed("hoverline",!0),d.attr("cy")<b.height&&p.append("line").attr("stroke","red").attr("stroke-width",1).attr("x1",d.attr("cx")).attr("y1",+d.attr("cy")+b.datapointSize).attr("x2",d.attr("cx")).attr("y2",b.height).classed("hoverline",!0);var e=this.getBoundingClientRect();$("body").append(g(e.top,e.right,a)),$(o.node()).trigger("mouseover-datapoint.scatterplot",[this,a,c])}),t.on("mouseout",function(){d3.select(this).classed("highlight",!1).style("fill","black").style("fill-opacity",.2),p.selectAll(".hoverline").remove(),$(".chart-info-box").remove()})}this.scatterplot=this.scatterplot||{},this.scatterplot.chartcontrol=Handlebars.template(function(a,b,c,d,e){this.compilerInfo=[4,">= 1.0.0"],c=this.merge(c,a.helpers),e=e||{};var f,g="",h="function",i=this.escapeExpression;return g+='<p class="help-text">\n Use the following controls to how the chart is displayed.\n The slide controls can be moved by the mouse or, if the \'handle\' is in focus, your keyboard\'s arrow keys.\n Move the focus between controls by using the tab or shift+tab keys on your keyboard.\n Use the \'Draw\' button to render (or re-render) the chart with the current settings.\n</p>\n\n<div data-config-key="datapointSize" class="form-input numeric-slider-input">\n <label for="datapointSize">Size of data point: </label>\n <div class="slider-output">',(f=c.datapointSize)?f=f.call(b,{hash:{},data:e}):(f=b.datapointSize,f=typeof f===h?f.apply(b):f),g+=i(f)+'</div>\n <div class="slider"></div>\n <p class="form-help help-text-small">\n Size of the graphic representation of each data point\n </p>\n</div>\n\n<div data-config-key="width" class="form-input numeric-slider-input">\n <label for="width">Chart width: </label>\n <div class="slider-output">',(f=c.width)?f=f.call(b,{hash:{},data:e}):(f=b.width,f=typeof f===h?f.apply(b):f),g+=i(f)+'</div>\n <div class="slider"></div>\n <p class="form-help help-text-small">\n (not including chart margins and axes)\n </p>\n</div>\n\n<div data-config-key="height" class="form-input numeric-slider-input">\n <label for="height">Chart height: </label>\n <div class="slider-output">',(f=c.height)?f=f.call(b,{hash:{},data:e}):(f=b.height,f=typeof f===h?f.apply(b):f),g+=i(f)+'</div>\n <div class="slider"></div>\n <p class="form-help help-text-small">\n (not including chart margins and axes)\n </p>\n</div>\n\n<div data-config-key="X-axis-label"class="text-input form-input">\n <label for="X-axis-label">Re-label the X axis: </label>\n <input type="text" name="X-axis-label" id="X-axis-label" value="',(f=c.xLabel)?f=f.call(b,{hash:{},data:e}):(f=b.xLabel,f=typeof f===h?f.apply(b):f),g+=i(f)+'" />\n <p class="form-help help-text-small"></p>\n</div>\n\n<div data-config-key="Y-axis-label" class="text-input form-input">\n <label for="Y-axis-label">Re-label the Y axis: </label>\n <input type="text" name="Y-axis-label" id="Y-axis-label" value="',(f=c.yLabel)?f=f.call(b,{hash:{},data:e}):(f=b.yLabel,f=typeof f===h?f.apply(b):f),g+=i(f)+'" />\n <p class="form-help help-text-small"></p>\n</div>\n\n<button class="render-button btn btn-primary active">Draw</button>\n'}),this.scatterplot.datacontrol=Handlebars.template(function(a,b,c,d,e){this.compilerInfo=[4,">= 1.0.0"],c=this.merge(c,a.helpers),e=e||{};var f,g="",h="function";return g+='<p class="help-text">\n Use the following control to change which columns are used by the chart. Click any cell\n from the last three rows of the table to select the column for the appropriate data.\n Use the \'Draw\' button to render (or re-render) the chart with the current settings.\n</p>\n\n<ul class="help-text" style="margin-left: 8px">\n <li><b>X Column</b>: which column values will be used for the x axis of the chart.</li>\n <li><b>Y Column</b>: which column values will be used for the y axis of the chart.</li>\n <li><b>ID Column</b>: an additional column value displayed when the user hovers over a data point.\n It may be useful to select unique or categorical identifiers here (such as gene ids).\n </li>\n</ul>\n\n<div class="column-selection">\n <pre class="peek">',(f=c.peek)?f=f.call(b,{hash:{},data:e}):(f=b.peek,f=typeof f===h?f.apply(b):f),(f||0===f)&&(g+=f),g+='</pre>\n</div>\n\n<p class="help-text help-text-small">\n <b>Note</b>: If it can be determined from the dataset\'s filetype that a column is not numeric,\n that column choice may be disabled for either the x or y axis.\n</p>\n\n<button class="render-button btn btn-primary active">Draw</button>\n'}),this.scatterplot.editor=Handlebars.template(function(a,b,c,d,e){this.compilerInfo=[4,">= 1.0.0"],c=this.merge(c,a.helpers),e=e||{};var f="";return f+='<div class="scatterplot-editor tabbable tabs-left">\n \n <ul class="nav nav-tabs">\n \n <li class="active">\n <a title="Use this tab to change which data are used"\n href="#data-control" data-toggle="tab">Data Controls</a>\n </li>\n <li>\n <a title="Use this tab to change how the chart is drawn"\n href="#chart-control" data-toggle="tab" >Chart Controls</a>\n </li>\n \n <li class="disabled">\n <a title="This tab will display the chart"\n href="#chart-display" data-toggle="tab">Chart</a>\n </li>\n \n <li class="file-controls">\n<!-- <button class="copy-btn btn btn-default"\n title="Save this as a new visualization">Save to new</button>-->\n <button class="save-btn btn btn-default">Save</button>\n </li>\n </ul>\n\n \n <div class="tab-content">\n \n <div id="data-control" class="scatterplot-config-control tab-pane active">\n \n </div>\n \n \n <div id="chart-control" class="scatterplot-config-control tab-pane">\n \n </div>\n\n \n <div id="chart-display" class="scatterplot-display tab-pane"></div>\n\n </div>\n</div>\n'});var ScatterplotConfigEditor=Backbone.View.extend({className:"scatterplot-control-form",initialize:function(a){if(this.model||(this.model=new Visualization({type:"scatterplot"})),!a||!a.dataset)throw new Error("ScatterplotConfigEditor requires a dataset");this.dataset=a.dataset,this.display=new ScatterplotDisplay({dataset:a.dataset,model:this.model})},render:function(){this.$el.empty().append(ScatterplotConfigEditor.templates.mainLayout({})),this.model.id&&(this.$el.find(".copy-btn").show(),this.$el.find(".save-btn").text("Update saved")),this.$el.find("[title]").tooltip(),this._render_dataControl(),this._render_chartControls(),this._render_chartDisplay();var a=this.model.get("config");return this.model.id&&_.isFinite(a.xColumn)&&_.isFinite(a.yColumn)&&this.renderChart(),this},_getColumnIndecesByType:function(){var a={numeric:[],text:[],all:[]};return _.each(this.dataset.metadata_column_types||[],function(b,c){"int"===b||"float"===b?a.numeric.push(c):("str"===b||"list"===b)&&a.text.push(c),a.all.push(c)}),a.numeric.length<2&&(a.numeric=[]),a},_render_dataControl:function(a){a=a||this.$el;var b=this,c=this.model.get("config"),d=this._getColumnIndecesByType(),e=a.find(".tab-pane#data-control");return e.html(ScatterplotConfigEditor.templates.dataControl({peek:this.dataset.peek})),e.find(".peek").peekColumnSelector({controls:[{label:"X Column",id:"xColumn",selected:c.xColumn,disabled:d.text},{label:"Y Column",id:"yColumn",selected:c.yColumn,disabled:d.text},{label:"ID Column",id:"idColumn",selected:c.idColumn}]}).on("peek-column-selector.change",function(a,c){b.model.set("config",c)}).on("peek-column-selector.rename",function(){}),e.find("[title]").tooltip(),e},_render_chartControls:function(a){function b(){var a=$(this),b=a.slider("value");c.model.set("config",_.object([[a.parent().data("config-key"),b]])),a.siblings(".slider-output").text(b)}a=a||this.$el;var c=this,d=this.model.get("config"),e=a.find("#chart-control");e.html(ScatterplotConfigEditor.templates.chartControl(d));var f={datapointSize:{min:2,max:10,step:1},width:{min:200,max:800,step:20},height:{min:200,max:800,step:20}};e.find(".numeric-slider-input").each(function(){var a=$(this),c=a.attr("data-config-key"),e=_.extend(f[c],{value:d[c],change:b,slide:b});a.find(".slider").slider(e),a.children(".slider-output").text(d[c])});var g=this.dataset.metadata_column_names||[],h=d.xLabel||g[d.xColumn]||"X",i=d.yLabel||g[d.yColumn]||"Y";return e.find('input[name="X-axis-label"]').val(h).on("change",function(){c.model.set("config",{xLabel:$(this).val()})}),e.find('input[name="Y-axis-label"]').val(i).on("change",function(){c.model.set("config",{yLabel:$(this).val()})}),e.find("[title]").tooltip(),e},_render_chartDisplay:function(a){a=a||this.$el;var b=a.find(".tab-pane#chart-display");return this.display.setElement(b),this.display.render(),b.find("[title]").tooltip(),b},events:{"change #include-id-checkbox":"toggleThirdColumnSelector","click #data-control .render-button":"renderChart","click #chart-control .render-button":"renderChart","click .save-btn":"saveVisualization"},saveVisualization:function(){var a=this;this.model.save().fail(function(b,c,d){console.error(b,c,d),a.trigger("save:error",view),alert("Error loading data:\n"+b.responseText)}).then(function(){a.display.render()})},toggleThirdColumnSelector:function(){this.$el.find('select[name="idColumn"]').parent().toggle()},renderChart:function(){this.$el.find(".nav li.disabled").removeClass("disabled"),this.$el.find("ul.nav").find('a[href="#chart-display"]').tab("show"),this.display.fetchData()},toString:function(){return"ScatterplotConfigEditor("+(this.dataset?this.dataset.id:"")+")"}});ScatterplotConfigEditor.templates={mainLayout:scatterplot.editor,dataControl:scatterplot.datacontrol,chartControl:scatterplot.chartcontrol};var ScatterplotDisplay=Backbone.View.extend({initialize:function(a){this.data=null,this.dataset=a.dataset,this.lineCount=this.dataset.metadata_data_lines||null},fetchData:function(){this.showLoadingIndicator();var a=this,b=this.model.get("config"),c=jQuery.getJSON("/api/datasets/"+this.dataset.id,{data_type:"raw_data",provider:"dataset-column",limit:b.pagination.perPage,offset:b.pagination.currPage*b.pagination.perPage});return c.done(function(b){a.data=b.data,a.trigger("data:fetched",a),a.renderData()}),c.fail(function(b,c,d){console.error(b,c,d),a.trigger("data:error",a),alert("Error loading data:\n"+b.responseText)}),c},showLoadingIndicator:function(){this.$el.find(".scatterplot-data-info").html(['<div class="loading-indicator">','<span class="fa fa-spinner fa-spin"></span>','<span class="loading-indicator-message">loading...</span>',"</div>"].join(""))},template:function(){var a=['<div class="controls clear">','<div class="right">','<p class="scatterplot-data-info"></p>','<button class="stats-toggle-btn">Stats</button>','<button class="rerender-btn">Redraw</button>',"</div>",'<div class="left">','<div class="page-control"></div>',"</div>","</div>","<svg/>",'<div class="stats-display"></div>'].join("");return a},render:function(){return this.$el.addClass("scatterplot-display").html(this.template()),this.data&&this.renderData(),this},renderData:function(){this.renderLeftControls(),this.renderRightControls(),this.renderPlot(this.data),this.getStats()},renderLeftControls:function(){var a=this,b=this.model.get("config");return this.$el.find(".controls .left .page-control").pagination({startingPage:b.pagination.currPage,perPage:b.pagination.perPage,totalDataSize:this.lineCount,currDataSize:this.data.length}).off().on("pagination.page-change",function(c,d){b.pagination.currPage=d,a.model.set("config",{pagination:b.pagination}),a.resetZoom(),a.fetchData()}),this},renderRightControls:function(){var a=this;this.setLineInfo(this.data),this.$el.find(".stats-toggle-btn").off().click(function(){a.toggleStats()}),this.$el.find(".rerender-btn").off().click(function(){a.resetZoom(),a.renderPlot(this.data)})},renderPlot:function(){var a=this,b=this.$el.find("svg");this.toggleStats(!1),b.off().empty().show().on("zoom.scatterplot",function(b,c){a.model.set("config",c)}),scatterplot(b.get(0),this.model.get("config"),this.data)},setLineInfo:function(a,b){if(a){var c=this.model.get("config"),d=this.lineCount||"an unknown total",e=c.pagination.currPage*c.pagination.perPage,f=e+a.length;this.$el.find(".controls p.scatterplot-data-info").text([e+1,"to",f,"of",d].join(" "))}else this.$el.find(".controls p.scatterplot-data-info").html(b||"");return this},resetZoom:function(a,b){return a=void 0!==a?a:1,b=void 0!==b?b:[0,0],this.model.set("config",{scale:a,translate:b}),this},getStats:function(){if(this.data){var a=this,b=this.model.get("config"),c=new Worker("/plugins/visualizations/scatterplot/static/worker-stats.js");c.postMessage({data:this.data,keys:[b.xColumn,b.yColumn]}),c.onerror=function(){c.terminate()},c.onmessage=function(b){a.renderStats(b.data)}}},renderStats:function(a){var b=this.model.get("config"),c=this.$el.find(".stats-display"),d=b.xLabel,e=b.yLabel,f=$("<table/>").addClass("table").append(["<thead><th></th><th>",d,"</th><th>",e,"</th></thead>"].join("")).append(_.map(a,function(a,b){return $(["<tr><td>",b,"</td><td>",a[0],"</td><td>",a[1],"</td></tr>"].join(""))}));c.empty().append(f)},toggleStats:function(a){var b=this.$el.find(".stats-display");a=void 0===a?b.is(":hidden"):a,a?(this.$el.find("svg").hide(),b.show(),this.$el.find(".controls .stats-toggle-btn").text("Plot")):(b.hide(),this.$el.find("svg").show(),this.$el.find(".controls .stats-toggle-btn").text("Stats"))},toString:function(){return"ScatterplotView()"}}),ScatterplotModel=Visualization.extend({defaults:{type:"scatterplot",config:{pagination:{currPage:0,perPage:3e3},width:400,height:400,margin:{top:16,right:16,bottom:40,left:54},xTicks:10,xLabel:"X",yTicks:10,yLabel:"Y",datapointSize:4,animDuration:500,scale:1,translate:[0,0]}}}); \ No newline at end of file diff -r bfc3b5e52781683efc8024f18c4aa57c65af1e15 -r ee995df4a7e548befdfa09196b9665f05cfd55f6 config/plugins/visualizations/scatterplot/templates/scatterplot.mako --- a/config/plugins/visualizations/scatterplot/templates/scatterplot.mako +++ b/config/plugins/visualizations/scatterplot/templates/scatterplot.mako @@ -27,12 +27,12 @@ 'libs/jquery/jquery.migrate', 'libs/jquery/jquery-ui', 'libs/bootstrap', - 'libs/require', 'libs/underscore', 'libs/backbone/backbone', 'libs/d3', 'libs/handlebars.runtime', 'mvc/ui', + 'jq-plugins/ui/peek-column-selector', 'mvc/visualization/visualization-model' )} ${h.javascript_link( root + 'plugins/visualizations/scatterplot/static/scatterplot-edit.js' )} diff -r bfc3b5e52781683efc8024f18c4aa57c65af1e15 -r ee995df4a7e548befdfa09196b9665f05cfd55f6 static/scripts/jq-plugins/ui/peek-column-selector.js --- /dev/null +++ b/static/scripts/jq-plugins/ui/peek-column-selector.js @@ -0,0 +1,317 @@ +// from: https://raw.githubusercontent.com/umdjs/umd/master/jqueryPlugin.js +// Uses AMD or browser globals to create a jQuery plugin. +(function (factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(['jquery'], factory); + } else { + // Browser globals + factory(jQuery); + } + +}(function ($) { + //============================================================================== + /** Column selection using the peek display as the control. + * Adds rows to the bottom of the peek with clickable areas in each cell + * to allow the user to select columns. + * Column selection can be limited to a single column or multiple. + * (Optionally) adds a left hand column of column selection prompts. + * (Optionally) allows the column headers to be clicked/renamed + * and set to some initial value. + * (Optionally) hides comment rows. + * (Optionally) allows pre-selecting and disabling certain columns for + * each row control. + * + * Construct by selecting a peek table to be used with jQuery and + * calling 'peekColumnSelector' with options. + * Options must include a 'controls' array and can include other options + * listed below. + * @example: + * $( 'pre.peek' ).peekColumnSelector({ + * columnNames : ["Chromosome", "Start", "Base", "", "", "Qual" ], + * controls : [ + * { label: 'X Column', id: 'xColumn' }, + * { label: 'Y Column', id: 'yColumn', selected: 2 }, + * { label: 'ID Column', id: 'idColumn', selected: 4, disabled: [ 1, 5 ] }, + * { label: 'Heatmap', id: 'heatmap', selected: [ 2, 4 ], disabled: [ 0, 1 ], multiselect: true, + * selectedText: 'Included', unselectedText: 'Excluded' } + * ], + * renameColumns : true, + * hideCommentRows : true, + * includePrompts : true, + * topLeftContent : 'Data sample:' + * }).on( 'peek-column-selector.change', function( ev, selection ){ + * console.info( 'new selection:', selection ); + * //{ yColumn: 2 } + * }).on( 'peek-column-selector.rename', function( ev, names ){ + * console.info( 'column names', names ); + * //[ 'Bler', 'Start', 'Base', '', '', 'Qual' ] + * }); + * + * An event is fired when column selection is changed and the event + * is passed an object in the form: { the row id : the new selection value }. + * An event is also fired when the table headers are re-named and + * is passed the new array of column names. + */ + + /** option defaults */ + var defaults = { + /** does this control allow renaming headers? */ + renameColumns : false, + /** does this control allow renaming headers? */ + columnNames : [], + /** the comment character used by the peek's datatype */ + commentChar : '#', + /** should comment rows be shown or hidden in the peek */ + hideCommentRows : false, + /** should a column of row control prompts be used */ + includePrompts : true, + /** what is the content of the top left cell (often a title) */ + topLeftContent : 'Columns:' + }, + /** class added to the pre.peek element (to allow css on just the control) */ + PEEKCONTROL_CLASS = 'peek-column-selector', + /** the string of the event fired when a control row changes */ + CHANGE_EVENT = 'peek-column-selector.change', + /** the string of the event fired when a column is renamed */ + RENAME_EVENT = 'peek-column-selector.rename', + /** class added to the control rows */ + ROW_CLASS = 'control', + /** class added to the left-hand cells that serve as row prompts */ + PROMPT_CLASS = 'control-prompt', + /** class added to selected _cells_/tds */ + SELECTED_CLASS = 'selected', + /** class added to disabled/un-clickable cells/tds */ + DISABLED_CLASS = 'disabled', + /** class added to the clickable surface within a cell to select it */ + BUTTON_CLASS = 'button', + /** class added to peek table header (th) cells to indicate they can be clicked and are renamable */ + RENAMABLE_HEADER_CLASS = 'renamable-header', + /** the data key used for each cell to store the column index ('data-...') */ + COLUMN_INDEX_DATA_KEY = 'column-index', + /** renamable header data key used to store the column name (w/o the number and dot: '1.Bler') */ + COLUMN_NAME_DATA_KEY = 'column-name'; + + //TODO: not happy with pure functional here - rows should polymorph (multi, single, etc.) + //TODO: needs clean up, move handlers to outer scope + + // ........................................................................ + /** validate the control data sent in for each row */ + function validateControl( control ){ + if( control.disabled && jQuery.type( control.disabled ) !== 'array' ){ + throw new Error( '"disabled" must be defined as an array of indeces: ' + JSON.stringify( control ) ); + } + if( control.multiselect && control.selected && jQuery.type( control.selected ) !== 'array' ){ + throw new Error( 'Mulitselect rows need an array for "selected": ' + JSON.stringify( control ) ); + } + if( !control.label || !control.id ){ + throw new Error( 'Peek controls need a label and id for each control row: ' + JSON.stringify( control ) ); + } + if( control.disabled && control.disabled.indexOf( control.selected ) !== -1 ){ + throw new Error( 'Selected column is in the list of disabled columns: ' + JSON.stringify( control ) ); + } + return control; + } + + /** build the inner control surface (i.e. button-like) */ + function buildButton( control, columnIndex ){ + return $( '<div/>' ).addClass( BUTTON_CLASS ).text( control.label ); + } + + /** build the basic (shared) cell structure */ + function buildControlCell( control, columnIndex ){ + var $td = $( '<td/>' ) + .html( buildButton( control, columnIndex ) ) + .attr( 'data-' + COLUMN_INDEX_DATA_KEY, columnIndex ); + + // disable if index in disabled array + if( control.disabled && control.disabled.indexOf( columnIndex ) !== -1 ){ + $td.addClass( DISABLED_CLASS ); + } + return $td; + } + + /** set the text of the control based on selected/un */ + function setSelectedText( $cell, control, columnIndex ){ + var $button = $cell.children( '.' + BUTTON_CLASS ); + if( $cell.hasClass( SELECTED_CLASS ) ){ + $button.html( ( control.selectedText !== undefined )?( control.selectedText ):( control.label ) ); + } else { + $button.html( ( control.unselectedText !== undefined )?( control.unselectedText ):( control.label ) ); + } + } + + /** build a cell for a row that only allows one selection */ + function buildSingleSelectCell( control, columnIndex ){ + // only one selection - selected is single index + var $cell = buildControlCell( control, columnIndex ); + if( control.selected === columnIndex ){ + $cell.addClass( SELECTED_CLASS ); + } + setSelectedText( $cell, control, columnIndex ); + + // only add the handler to non-disabled controls + if( !$cell.hasClass( DISABLED_CLASS ) ){ + $cell.click( function selectClick( ev ){ + var $cell = $( this ); + // don't re-select or fire event if already selected + if( !$cell.hasClass( SELECTED_CLASS ) ){ + // only one can be selected - remove selected on all others, add it here + var $otherSelected = $cell.parent().children( '.' + SELECTED_CLASS ).removeClass( SELECTED_CLASS ); + $otherSelected.each( function(){ + setSelectedText( $( this ), control, columnIndex ); + }); + + $cell.addClass( SELECTED_CLASS ); + setSelectedText( $cell, control, columnIndex ); + + // fire the event from the table itself, passing the id and index of selected + var eventData = {}, + key = $cell.parent().attr( 'id' ), + val = $cell.data( COLUMN_INDEX_DATA_KEY ); + eventData[ key ] = val; + $cell.parents( '.peek' ).trigger( CHANGE_EVENT, eventData ); + } + }); + } + return $cell; + } + + /** build a cell for a row that allows multiple selections */ + function buildMultiSelectCell( control, columnIndex ){ + var $cell = buildControlCell( control, columnIndex ); + // multiple selection - selected is an array + if( control.selected && control.selected.indexOf( columnIndex ) !== -1 ){ + $cell.addClass( SELECTED_CLASS ); + } + setSelectedText( $cell, control, columnIndex ); + + // only add the handler to non-disabled controls + if( !$cell.hasClass( DISABLED_CLASS ) ){ + $cell.click( function multiselectClick( ev ){ + var $cell = $( this ); + // can be more than one selected - toggle selected on this cell + $cell.toggleClass( SELECTED_CLASS ); + setSelectedText( $cell, control, columnIndex ); + var selectedColumnIndeces = $cell.parent().find( '.' + SELECTED_CLASS ).map( function( i, e ){ + return $( e ).data( COLUMN_INDEX_DATA_KEY ); + }); + // fire the event from the table itself, passing the id and index of selected + var eventData = {}, + key = $cell.parent().attr( 'id' ), + val = jQuery.makeArray( selectedColumnIndeces ); + eventData[ key ] = val; + $cell.parents( '.peek' ).trigger( CHANGE_EVENT, eventData ); + }); + } + return $cell; + } + + /** iterate over columns in peek and create a control for each */ + function buildControlCells( count, control ){ + var $cells = []; + // build a control for each column - using a build fn based on control + for( var columnIndex=0; columnIndex<count; columnIndex+=1 ){ + $cells.push( control.multiselect? buildMultiSelectCell( control, columnIndex ) + : buildSingleSelectCell( control, columnIndex ) ); + } + return $cells; + } + + /** build a row of controls for the peek */ + function buildControlRow( cellCount, control, includePrompts ){ + var $controlRow = $( '<tr/>' ).attr( 'id', control.id ).addClass( ROW_CLASS ); + if( includePrompts ){ + var $promptCell = $( '<td/>' ).addClass( PROMPT_CLASS ).text( control.label + ':' ); + $controlRow.append( $promptCell ); + } + $controlRow.append( buildControlCells( cellCount, control ) ); + return $controlRow; + } + + // ........................................................................ + /** add to the peek, using options for configuration, return the peek */ + function peekColumnSelector( options ){ + options = jQuery.extend( true, {}, defaults, options ); + + var $peek = $( this ).addClass( PEEKCONTROL_CLASS ), + $peektable = $peek.find( 'table' ), + // get the size of the tables - width and height, number of comment rows + columnCount = $peektable.find( 'th' ).size(), + rowCount = $peektable.find( 'tr' ).size(), + // get the rows containing text starting with the comment char (also make them grey) + $commentRows = $peektable.find( 'td[colspan]' ).map( function( e, i ){ + var $this = $( this ); + if( $this.text() && $this.text().match( new RegExp( '^' + options.commentChar ) ) ){ + return $( this ).css( 'color', 'grey' ).parent().get(0); + } + return null; + }); + + // should comment rows in the peek be hidden? + if( options.hideCommentRows ){ + $commentRows.hide(); + rowCount -= $commentRows.size(); + } + //console.debug( 'rowCount:', rowCount, 'columnCount:', columnCount, '$commentRows:', $commentRows ); + + // should a first column of control prompts be added? + if( options.includePrompts ){ + var $topLeft = $( '<th/>' ).addClass( 'top-left' ).text( options.topLeftContent ) + .attr( 'rowspan', rowCount ); + $peektable.find( 'tr' ).first().prepend( $topLeft ); + } + + // save either the options column name or the parsed text of each column header in html5 data attr and text + var $headers = $peektable.find( 'th:not(.top-left)' ).each( function( i, e ){ + var $this = $( this ), + // can be '1.name' or '1' + text = $this.text().replace( /^\d+\.*/, '' ), + name = options.columnNames[ i ] || text; + $this.attr( 'data-' + COLUMN_NAME_DATA_KEY, name ) + .text( ( i + 1 ) + (( name )?( '.' + name ):( '' )) ); + }); + + // allow renaming of columns when the header is clicked + if( options.renameColumns ){ + $headers.addClass( RENAMABLE_HEADER_CLASS ) + .click( function renameColumn(){ + // prompt for new name + var $this = $( this ), + index = $this.index() + ( options.includePrompts? 0: 1 ), + prevName = $this.data( COLUMN_NAME_DATA_KEY ), + newColumnName = prompt( 'New column name:', prevName ); + if( newColumnName !== null && newColumnName !== prevName ){ + // set the new text and data + $this.text( index + ( newColumnName?( '.' + newColumnName ):'' ) ) + .data( COLUMN_NAME_DATA_KEY, newColumnName ) + .attr( 'data-', COLUMN_NAME_DATA_KEY, newColumnName ); + // fire event for new column names + var columnNames = jQuery.makeArray( + $this.parent().children( 'th:not(.top-left)' ).map( function(){ + return $( this ).data( COLUMN_NAME_DATA_KEY ); + })); + $this.parents( '.peek' ).trigger( RENAME_EVENT, columnNames ); + } + }); + } + + // build a row for each control + options.controls.forEach( function( control, i ){ + validateControl( control ); + var $controlRow = buildControlRow( columnCount, control, options.includePrompts ); + $peektable.find( 'tbody' ).append( $controlRow ); + }); + return this; + } + + // ........................................................................ + // as jq plugin + jQuery.fn.extend({ + peekColumnSelector : function $peekColumnSelector( options ){ + return this.map( function(){ + return peekColumnSelector.call( this, options ); + }); + } + }); +})); diff -r bfc3b5e52781683efc8024f18c4aa57c65af1e15 -r ee995df4a7e548befdfa09196b9665f05cfd55f6 static/scripts/mvc/ui.js --- a/static/scripts/mvc/ui.js +++ b/static/scripts/mvc/ui.js @@ -1344,311 +1344,3 @@ } }); }()); - - -//============================================================================== -/** Column selection using the peek display as the control. - * Adds rows to the bottom of the peek with clickable areas in each cell - * to allow the user to select columns. - * Column selection can be limited to a single column or multiple. - * (Optionally) adds a left hand column of column selection prompts. - * (Optionally) allows the column headers to be clicked/renamed - * and set to some initial value. - * (Optionally) hides comment rows. - * (Optionally) allows pre-selecting and disabling certain columns for - * each row control. - * - * Construct by selecting a peek table to be used with jQuery and - * calling 'peekControl' with options. - * Options must include a 'controls' array and can include other options - * listed below. - * @example: - * $( 'pre.peek' ).peekControl({ - * columnNames : ["Chromosome", "Start", "Base", "", "", "Qual" ], - * controls : [ - * { label: 'X Column', id: 'xColumn' }, - * { label: 'Y Column', id: 'yColumn', selected: 2 }, - * { label: 'ID Column', id: 'idColumn', selected: 4, disabled: [ 1, 5 ] }, - * { label: 'Heatmap', id: 'heatmap', selected: [ 2, 4 ], disabled: [ 0, 1 ], multiselect: true, - * selectedText: 'Included', unselectedText: 'Excluded' } - * ], - * renameColumns : true, - * hideCommentRows : true, - * includePrompts : true, - * topLeftContent : 'Data sample:' - * }).on( 'peek-control.change', function( ev, selection ){ - * console.info( 'new selection:', selection ); - * //{ yColumn: 2 } - * }).on( 'peek-control.rename', function( ev, names ){ - * console.info( 'column names', names ); - * //[ 'Bler', 'Start', 'Base', '', '', 'Qual' ] - * }); - * - * An event is fired when column selection is changed and the event - * is passed an object in the form: { the row id : the new selection value }. - * An event is also fired when the table headers are re-named and - * is passed the new array of column names. - */ -(function(){ - - /** option defaults */ - var defaults = { - /** does this control allow renaming headers? */ - renameColumns : false, - /** does this control allow renaming headers? */ - columnNames : [], - /** the comment character used by the peek's datatype */ - commentChar : '#', - /** should comment rows be shown or hidden in the peek */ - hideCommentRows : false, - /** should a column of row control prompts be used */ - includePrompts : true, - /** what is the content of the top left cell (often a title) */ - topLeftContent : 'Columns:' - }, - /** the string of the event fired when a control row changes */ - CHANGE_EVENT = 'peek-control.change', - /** the string of the event fired when a column is renamed */ - RENAME_EVENT = 'peek-control.rename', - /** class added to the pre.peek element (to allow css on just the control) */ - PEEKCONTROL_CLASS = 'peek-control', - /** class added to the control rows */ - ROW_CLASS = 'control', - /** class added to the left-hand cells that serve as row prompts */ - PROMPT_CLASS = 'control-prompt', - /** class added to selected _cells_/tds */ - SELECTED_CLASS = 'selected', - /** class added to disabled/un-clickable cells/tds */ - DISABLED_CLASS = 'disabled', - /** class added to the clickable surface within a cell to select it */ - BUTTON_CLASS = 'button', - /** class added to peek table header (th) cells to indicate they can be clicked and are renamable */ - RENAMABLE_HEADER_CLASS = 'renamable-header', - /** the data key used for each cell to store the column index ('data-...') */ - COLUMN_INDEX_DATA_KEY = 'column-index', - /** renamable header data key used to store the column name (w/o the number and dot: '1.Bler') */ - COLUMN_NAME_DATA_KEY = 'column-name'; - - //TODO: not happy with pure functional here - rows should polymorph (multi, single, etc.) - //TODO: needs clean up, move handlers to outer scope - - // ........................................................................ - /** validate the control data sent in for each row */ - function validateControl( control ){ - if( control.disabled && jQuery.type( control.disabled ) !== 'array' ){ - throw new Error( '"disabled" must be defined as an array of indeces: ' + JSON.stringify( control ) ); - } - if( control.multiselect && control.selected && jQuery.type( control.selected ) !== 'array' ){ - throw new Error( 'Mulitselect rows need an array for "selected": ' + JSON.stringify( control ) ); - } - if( !control.label || !control.id ){ - throw new Error( 'Peek controls need a label and id for each control row: ' + JSON.stringify( control ) ); - } - if( control.disabled && control.disabled.indexOf( control.selected ) !== -1 ){ - throw new Error( 'Selected column is in the list of disabled columns: ' + JSON.stringify( control ) ); - } - return control; - } - - /** build the inner control surface (i.e. button-like) */ - function buildButton( control, columnIndex ){ - return $( '<div/>' ).addClass( BUTTON_CLASS ).text( control.label ); - } - - /** build the basic (shared) cell structure */ - function buildControlCell( control, columnIndex ){ - var $td = $( '<td/>' ) - .html( buildButton( control, columnIndex ) ) - .attr( 'data-' + COLUMN_INDEX_DATA_KEY, columnIndex ); - - // disable if index in disabled array - if( control.disabled && control.disabled.indexOf( columnIndex ) !== -1 ){ - $td.addClass( DISABLED_CLASS ); - } - return $td; - } - - /** set the text of the control based on selected/un */ - function setSelectedText( $cell, control, columnIndex ){ - var $button = $cell.children( '.' + BUTTON_CLASS ); - if( $cell.hasClass( SELECTED_CLASS ) ){ - $button.html( ( control.selectedText !== undefined )?( control.selectedText ):( control.label ) ); - } else { - $button.html( ( control.unselectedText !== undefined )?( control.unselectedText ):( control.label ) ); - } - } - - /** build a cell for a row that only allows one selection */ - function buildSingleSelectCell( control, columnIndex ){ - // only one selection - selected is single index - var $cell = buildControlCell( control, columnIndex ); - if( control.selected === columnIndex ){ - $cell.addClass( SELECTED_CLASS ); - } - setSelectedText( $cell, control, columnIndex ); - - // only add the handler to non-disabled controls - if( !$cell.hasClass( DISABLED_CLASS ) ){ - $cell.click( function selectClick( ev ){ - var $cell = $( this ); - // don't re-select or fire event if already selected - if( !$cell.hasClass( SELECTED_CLASS ) ){ - // only one can be selected - remove selected on all others, add it here - var $otherSelected = $cell.parent().children( '.' + SELECTED_CLASS ).removeClass( SELECTED_CLASS ); - $otherSelected.each( function(){ - setSelectedText( $( this ), control, columnIndex ); - }); - - $cell.addClass( SELECTED_CLASS ); - setSelectedText( $cell, control, columnIndex ); - - // fire the event from the table itself, passing the id and index of selected - var eventData = {}, - key = $cell.parent().attr( 'id' ), - val = $cell.data( COLUMN_INDEX_DATA_KEY ); - eventData[ key ] = val; - $cell.parents( '.peek' ).trigger( CHANGE_EVENT, eventData ); - } - }); - } - return $cell; - } - - /** build a cell for a row that allows multiple selections */ - function buildMultiSelectCell( control, columnIndex ){ - var $cell = buildControlCell( control, columnIndex ); - // multiple selection - selected is an array - if( control.selected && control.selected.indexOf( columnIndex ) !== -1 ){ - $cell.addClass( SELECTED_CLASS ); - } - setSelectedText( $cell, control, columnIndex ); - - // only add the handler to non-disabled controls - if( !$cell.hasClass( DISABLED_CLASS ) ){ - $cell.click( function multiselectClick( ev ){ - var $cell = $( this ); - // can be more than one selected - toggle selected on this cell - $cell.toggleClass( SELECTED_CLASS ); - setSelectedText( $cell, control, columnIndex ); - var selectedColumnIndeces = $cell.parent().find( '.' + SELECTED_CLASS ).map( function( i, e ){ - return $( e ).data( COLUMN_INDEX_DATA_KEY ); - }); - // fire the event from the table itself, passing the id and index of selected - var eventData = {}, - key = $cell.parent().attr( 'id' ), - val = jQuery.makeArray( selectedColumnIndeces ); - eventData[ key ] = val; - $cell.parents( '.peek' ).trigger( CHANGE_EVENT, eventData ); - }); - } - return $cell; - } - - /** iterate over columns in peek and create a control for each */ - function buildControlCells( count, control ){ - var $cells = []; - // build a control for each column - using a build fn based on control - for( var columnIndex=0; columnIndex<count; columnIndex+=1 ){ - $cells.push( control.multiselect? buildMultiSelectCell( control, columnIndex ) - : buildSingleSelectCell( control, columnIndex ) ); - } - return $cells; - } - - /** build a row of controls for the peek */ - function buildControlRow( cellCount, control, includePrompts ){ - var $controlRow = $( '<tr/>' ).attr( 'id', control.id ).addClass( ROW_CLASS ); - if( includePrompts ){ - var $promptCell = $( '<td/>' ).addClass( PROMPT_CLASS ).text( control.label + ':' ); - $controlRow.append( $promptCell ); - } - $controlRow.append( buildControlCells( cellCount, control ) ); - return $controlRow; - } - - // ........................................................................ - /** add to the peek, using options for configuration, return the peek */ - function peekControl( options ){ - options = jQuery.extend( true, {}, defaults, options ); - - var $peek = $( this ).addClass( PEEKCONTROL_CLASS ), - $peektable = $peek.find( 'table' ), - // get the size of the tables - width and height, number of comment rows - columnCount = $peektable.find( 'th' ).size(), - rowCount = $peektable.find( 'tr' ).size(), - // get the rows containing text starting with the comment char (also make them grey) - $commentRows = $peektable.find( 'td[colspan]' ).map( function( e, i ){ - var $this = $( this ); - if( $this.text() && $this.text().match( new RegExp( '^' + options.commentChar ) ) ){ - return $( this ).css( 'color', 'grey' ).parent().get(0); - } - return null; - }); - - // should comment rows in the peek be hidden? - if( options.hideCommentRows ){ - $commentRows.hide(); - rowCount -= $commentRows.size(); - } - //console.debug( 'rowCount:', rowCount, 'columnCount:', columnCount, '$commentRows:', $commentRows ); - - // should a first column of control prompts be added? - if( options.includePrompts ){ - var $topLeft = $( '<th/>' ).addClass( 'top-left' ).text( options.topLeftContent ) - .attr( 'rowspan', rowCount ); - $peektable.find( 'tr' ).first().prepend( $topLeft ); - } - - // save either the options column name or the parsed text of each column header in html5 data attr and text - var $headers = $peektable.find( 'th:not(.top-left)' ).each( function( i, e ){ - var $this = $( this ), - // can be '1.name' or '1' - text = $this.text().replace( /^\d+\.*/, '' ), - name = options.columnNames[ i ] || text; - $this.attr( 'data-' + COLUMN_NAME_DATA_KEY, name ) - .text( ( i + 1 ) + (( name )?( '.' + name ):( '' )) ); - }); - - // allow renaming of columns when the header is clicked - if( options.renameColumns ){ - $headers.addClass( RENAMABLE_HEADER_CLASS ) - .click( function renameColumn(){ - // prompt for new name - var $this = $( this ), - index = $this.index() + ( options.includePrompts? 0: 1 ), - prevName = $this.data( COLUMN_NAME_DATA_KEY ), - newColumnName = prompt( 'New column name:', prevName ); - if( newColumnName !== null && newColumnName !== prevName ){ - // set the new text and data - $this.text( index + ( newColumnName?( '.' + newColumnName ):'' ) ) - .data( COLUMN_NAME_DATA_KEY, newColumnName ) - .attr( 'data-', COLUMN_NAME_DATA_KEY, newColumnName ); - // fire event for new column names - var columnNames = jQuery.makeArray( - $this.parent().children( 'th:not(.top-left)' ).map( function(){ - return $( this ).data( COLUMN_NAME_DATA_KEY ); - })); - $this.parents( '.peek' ).trigger( RENAME_EVENT, columnNames ); - } - }); - } - - // build a row for each control - options.controls.forEach( function( control, i ){ - validateControl( control ); - var $controlRow = buildControlRow( columnCount, control, options.includePrompts ); - $peektable.find( 'tbody' ).append( $controlRow ); - }); - return this; - } - - // ........................................................................ - // as jq plugin - jQuery.fn.extend({ - peekControl : function $peekControl( options ){ - return this.map( function(){ - return peekControl.call( this, options ); - }); - } - }); -}()); diff -r bfc3b5e52781683efc8024f18c4aa57c65af1e15 -r ee995df4a7e548befdfa09196b9665f05cfd55f6 static/scripts/packed/jq-plugins/ui/peek-column-selector.js --- /dev/null +++ b/static/scripts/packed/jq-plugins/ui/peek-column-selector.js @@ -0,0 +1,1 @@ +(function(a){if(typeof define==="function"&&define.amd){define(["jquery"],a)}else{a(jQuery)}}(function(b){var f={renameColumns:false,columnNames:[],commentChar:"#",hideCommentRows:false,includePrompts:true,topLeftContent:"Columns:"},m="peek-column-selector",t="peek-column-selector.change",u="peek-column-selector.rename",w="control",e="control-prompt",c="selected",o="disabled",v="button",r="renamable-header",q="column-index",a="column-name";function i(x){if(x.disabled&&jQuery.type(x.disabled)!=="array"){throw new Error('"disabled" must be defined as an array of indeces: '+JSON.stringify(x))}if(x.multiselect&&x.selected&&jQuery.type(x.selected)!=="array"){throw new Error('Mulitselect rows need an array for "selected": '+JSON.stringify(x))}if(!x.label||!x.id){throw new Error("Peek controls need a label and id for each control row: "+JSON.stringify(x))}if(x.disabled&&x.disabled.indexOf(x.selected)!==-1){throw new Error("Selected column is in the list of disabled columns: "+JSON.stringify(x))}return x}function p(y,x){return b("<div/>").addClass(v).text(y.label)}function l(y,x){var z=b("<td/>").html(p(y,x)).attr("data-"+q,x);if(y.disabled&&y.disabled.indexOf(x)!==-1){z.addClass(o)}return z}function d(z,A,x){var y=z.children("."+v);if(z.hasClass(c)){y.html((A.selectedText!==undefined)?(A.selectedText):(A.label))}else{y.html((A.unselectedText!==undefined)?(A.unselectedText):(A.label))}}function h(A,y){var z=l(A,y);if(A.selected===y){z.addClass(c)}d(z,A,y);if(!z.hasClass(o)){z.click(function x(E){var F=b(this);if(!F.hasClass(c)){var B=F.parent().children("."+c).removeClass(c);B.each(function(){d(b(this),A,y)});F.addClass(c);d(F,A,y);var D={},C=F.parent().attr("id"),G=F.data(q);D[C]=G;F.parents(".peek").trigger(t,D)}})}return z}function n(A,y){var z=l(A,y);if(A.selected&&A.selected.indexOf(y)!==-1){z.addClass(c)}d(z,A,y);if(!z.hasClass(o)){z.click(function x(E){var F=b(this);F.toggleClass(c);d(F,A,y);var D=F.parent().find("."+c).map(function(H,I){return b(I).data(q)});var C={},B=F.parent().attr("id"),G=jQuery.makeArray(D);C[B]=G;F.parents(".peek").trigger(t,C)})}return z}function k(z,A){var x=[];for(var y=0;y<z;y+=1){x.push(A.multiselect?n(A,y):h(A,y))}return x}function s(z,A,y){var B=b("<tr/>").attr("id",A.id).addClass(w);if(y){var x=b("<td/>").addClass(e).text(A.label+":");B.append(x)}B.append(k(z,A));return B}function j(F){F=jQuery.extend(true,{},f,F);var E=b(this).addClass(m),B=E.find("table"),A=B.find("th").size(),D=B.find("tr").size(),x=B.find("td[colspan]").map(function(I,G){var H=b(this);if(H.text()&&H.text().match(new RegExp("^"+F.commentChar))){return b(this).css("color","grey").parent().get(0)}return null});if(F.hideCommentRows){x.hide();D-=x.size()}if(F.includePrompts){var z=b("<th/>").addClass("top-left").text(F.topLeftContent).attr("rowspan",D);B.find("tr").first().prepend(z)}var C=B.find("th:not(.top-left)").each(function(H,J){var I=b(this),K=I.text().replace(/^\d+\.*/,""),G=F.columnNames[H]||K;I.attr("data-"+a,G).text((H+1)+((G)?("."+G):("")))});if(F.renameColumns){C.addClass(r).click(function y(){var H=b(this),G=H.index()+(F.includePrompts?0:1),J=H.data(a),I=prompt("New column name:",J);if(I!==null&&I!==J){H.text(G+(I?("."+I):"")).data(a,I).attr("data-",a,I);var K=jQuery.makeArray(H.parent().children("th:not(.top-left)").map(function(){return b(this).data(a)}));H.parents(".peek").trigger(u,K)}})}F.controls.forEach(function(H,G){i(H);var I=s(A,H,F.includePrompts);B.find("tbody").append(I)});return this}jQuery.fn.extend({peekColumnSelector:function g(x){return this.map(function(){return j.call(this,x)})}})})); \ No newline at end of file diff -r bfc3b5e52781683efc8024f18c4aa57c65af1e15 -r ee995df4a7e548befdfa09196b9665f05cfd55f6 static/scripts/packed/mvc/ui.js --- a/static/scripts/packed/mvc/ui.js +++ b/static/scripts/packed/mvc/ui.js @@ -1,1 +1,1 @@ -var IconButton=Backbone.Model.extend({defaults:{title:"",icon_class:"",on_click:null,menu_options:null,is_menu_button:true,id:null,href:null,target:null,enabled:true,visible:true,tooltip_config:{}}});var IconButtonView=Backbone.View.extend({initialize:function(){this.model.attributes.tooltip_config={placement:"bottom"};this.model.bind("change",this.render,this)},render:function(){this.$el.tooltip("hide");var a=this.template(this.model.toJSON());a.tooltip(this.model.get("tooltip_config"));this.$el.replaceWith(a);this.setElement(a);return this},events:{click:"click"},click:function(a){if(_.isFunction(this.model.get("on_click"))){this.model.get("on_click")(a);return false}return true},template:function(b){var a='title="'+b.title+'" class="icon-button';if(b.is_menu_button){a+=" menu-button"}a+=" "+b.icon_class;if(!b.enabled){a+="_disabled"}a+='"';if(b.id){a+=' id="'+b.id+'"'}a+=' href="'+b.href+'"';if(b.target){a+=' target="'+b.target+'"'}if(!b.visible){a+=' style="display: none;"'}if(b.enabled){a="<a "+a+"/>"}else{a="<span "+a+"/>"}return $(a)}});var IconButtonCollection=Backbone.Collection.extend({model:IconButton});var IconButtonMenuView=Backbone.View.extend({tagName:"div",initialize:function(){this.render()},render:function(){var a=this;this.collection.each(function(d){var b=$("<a/>").attr("href","javascript:void(0)").attr("title",d.attributes.title).addClass("icon-button menu-button").addClass(d.attributes.icon_class).appendTo(a.$el).click(d.attributes.on_click);if(d.attributes.tooltip_config){b.tooltip(d.attributes.tooltip_config)}var c=d.get("options");if(c){make_popupmenu(b,c)}});return this}});var create_icon_buttons_menu=function(b,a){if(!a){a={}}var c=new IconButtonCollection(_.map(b,function(d){return new IconButton(_.extend(d,a))}));return new IconButtonMenuView({collection:c})};var Grid=Backbone.Collection.extend({});var GridView=Backbone.View.extend({});var PopupMenu=Backbone.View.extend({initialize:function(b,a){this.$button=b;if(!this.$button.size()){this.$button=$("<div/>")}this.options=a||[];var c=this;this.$button.click(function(d){$(".popmenu-wrapper").remove();c._renderAndShow(d);return false})},_renderAndShow:function(a){this.render();this.$el.appendTo("body").css(this._getShownPosition(a)).show();this._setUpCloseBehavior()},render:function(){this.$el.addClass("popmenu-wrapper").hide().css({position:"absolute"}).html(this.template(this.$button.attr("id"),this.options));if(this.options.length){var a=this;this.$el.find("li").each(function(c,b){var d=a.options[c];if(d.func){$(this).children("a.popupmenu-option").click(function(e){d.func.call(a,e,d)})}})}return this},template:function(b,a){return['<ul id="',b,'-menu" class="dropdown-menu">',this._templateOptions(a),"</ul>"].join("")},_templateOptions:function(a){if(!a.length){return"<li>(no options)</li>"}return _.map(a,function(d){if(d.divider){return'<li class="divider"></li>'}else{if(d.header){return['<li class="head"><a href="javascript:void(0);">',d.html,"</a></li>"].join("")}}var c=d.href||"javascript:void(0);",e=(d.target)?(' target="'+d.target+'"'):(""),b=(d.checked)?('<span class="fa fa-check"></span>'):("");return['<li><a class="popupmenu-option" href="',c,'"',e,">",b,d.html,"</a></li>"].join("")}).join("")},_getShownPosition:function(b){var c=this.$el.width();var a=b.pageX-c/2;a=Math.min(a,$(document).scrollLeft()+$(window).width()-c-5);a=Math.max(a,$(document).scrollLeft()+5);return{top:b.pageY,left:a}},_setUpCloseBehavior:function(){var c=this;function a(e){$(document).off("click.close_popup");if(window.parent!==window){try{$(window.parent.document).off("click.close_popup")}catch(d){}}else{try{$("iframe#galaxy_main").contents().off("click.close_popup")}catch(d){}}c.remove()}$("html").one("click.close_popup",a);if(window.parent!==window){try{$(window.parent.document).find("html").one("click.close_popup",a)}catch(b){}}else{try{$("iframe#galaxy_main").contents().one("click.close_popup",a)}catch(b){}}},addItem:function(b,a){a=(a>=0)?a:this.options.length;this.options.splice(a,0,b);return this},removeItem:function(a){if(a>=0){this.options.splice(a,1)}return this},findIndexByHtml:function(b){for(var a=0;a<this.options.length;a++){if(_.has(this.options[a],"html")&&(this.options[a].html===b)){return a}}return null},findItemByHtml:function(a){return this.options[(this.findIndexByHtml(a))]},toString:function(){return"PopupMenu"}});PopupMenu.create=function _create(b,a){return new PopupMenu(b,a)};PopupMenu.make_popupmenu=function(b,c){var a=[];_.each(c,function(f,d){var e={html:d};if(f===null){e.header=true}else{if(jQuery.type(f)==="function"){e.func=f}}a.push(e)});return new PopupMenu($(b),a)};PopupMenu.convertLinksToOptions=function(c,a){c=$(c);a=a||"a";var b=[];c.find(a).each(function(g,e){var f={},d=$(g);f.html=d.text();if(d.attr("href")){var j=d.attr("href"),k=d.attr("target"),h=d.attr("confirm");f.func=function(){if((h)&&(!confirm(h))){return}switch(k){case"_parent":window.parent.location=j;break;case"_top":window.top.location=j;break;default:window.location=j}}}b.push(f)});return b};PopupMenu.fromExistingDom=function(d,c,a){d=$(d);c=$(c);var b=PopupMenu.convertLinksToOptions(c,a);c.remove();return new PopupMenu(d,b)};PopupMenu.make_popup_menus=function(c,b,d){c=c||document;b=b||"div[popupmenu]";d=d||function(e,f){return"#"+e.attr("popupmenu")};var a=[];$(c).find(b).each(function(){var e=$(this),f=$(c).find(d(e,c));a.push(PopupMenu.fromDom(f,e));f.addClass("popup")});return a};var faIconButton=function(a){a=a||{};a.tooltipConfig=a.tooltipConfig||{placement:"bottom"};a.classes=["icon-btn"].concat(a.classes||[]);if(a.disabled){a.classes.push("disabled")}var b=['<a class="',a.classes.join(" "),'"',((a.title)?(' title="'+a.title+'"'):("")),((!a.disabled&&a.target)?(' target="'+a.target+'"'):("")),' href="',((!a.disabled&&a.href)?(a.href):("javascript:void(0);")),'">','<span class="fa ',a.faIcon,'"></span>',"</a>"].join("");var c=$(b).tooltip(a.tooltipConfig);if(_.isFunction(a.onclick)){c.click(a.onclick)}return c};function LoadingIndicator(a,c){var b=this;c=jQuery.extend({cover:false},c||{});function d(){var e=['<div class="loading-indicator">','<div class="loading-indicator-text">','<span class="fa fa-spinner fa-spin fa-lg"></span>','<span class="loading-indicator-message">loading...</span>',"</div>","</div>"].join("\n");var g=$(e).hide().css(c.css||{position:"fixed"}),f=g.children(".loading-indicator-text");if(c.cover){g.css({"z-index":2,top:a.css("top"),bottom:a.css("bottom"),left:a.css("left"),right:a.css("right"),opacity:0.5,"background-color":"white","text-align":"center"});f=g.children(".loading-indicator-text").css({"margin-top":"20px"})}else{f=g.children(".loading-indicator-text").css({margin:"12px 0px 0px 10px",opacity:"0.85",color:"grey"});f.children(".loading-indicator-message").css({margin:"0px 8px 0px 0px","font-style":"italic"})}return g}b.show=function(f,e,g){f=f||"loading...";e=e||"fast";a.parent().find(".loading-indicator").remove();b.$indicator=d().insertBefore(a);b.message(f);b.$indicator.fadeIn(e,g);return b};b.message=function(e){b.$indicator.find("i").text(e)};b.hide=function(e,f){e=e||"fast";if(b.$indicator&&b.$indicator.size()){b.$indicator.fadeOut(e,function(){b.$indicator.remove();if(f){f()}})}else{if(f){f()}}return b};return b}(function(){var b=window._l||function(d){return d};function a(k,q){var e=27,n=13,d=$(k),f=true,h={initialVal:"",name:"search",placeholder:"search",classes:"",onclear:function(){},onfirstsearch:null,onsearch:function(r){},minSearchLen:0,escWillClear:true,oninit:function(){}};function j(r){var s=$(this).parent().children("input");s.focus().val("").trigger("clear:searchInput");q.onclear()}function p(s,r){$(this).trigger("search:searchInput",r);if(typeof q.onfirstsearch==="function"&&f){f=false;q.onfirstsearch(r)}else{q.onsearch(r)}}function g(){return['<input type="text" name="',q.name,'" placeholder="',q.placeholder,'" ','class="search-query ',q.classes,'" ',"/>"].join("")}function m(){return $(g()).focus(function(r){$(this).select()}).keyup(function(s){s.preventDefault();s.stopPropagation();if(!$(this).val()){$(this).blur()}if(s.which===e&&q.escWillClear){j.call(this,s)}else{var r=$(this).val();if((s.which===n)||(q.minSearchLen&&r.length>=q.minSearchLen)){p.call(this,s,r)}else{if(!r.length){j.call(this,s)}}}}).on("change",function(r){p.call(this,r,$(this).val())}).val(q.initialVal)}function l(){return $(['<span class="search-clear fa fa-times-circle" ','title="',b("clear search (esc)"),'"></span>'].join("")).tooltip({placement:"bottom"}).click(function(r){j.call(this,r)})}function o(){return $(['<span class="search-loading fa fa-spinner fa-spin" ','title="',b("loading..."),'"></span>'].join("")).hide().tooltip({placement:"bottom"})}function i(){d.find(".search-loading").toggle();d.find(".search-clear").toggle()}if(jQuery.type(q)==="string"){if(q==="toggle-loading"){i()}return d}if(jQuery.type(q)==="object"){q=jQuery.extend(true,{},h,q)}return d.addClass("search-input").prepend([m(),l(),o()])}jQuery.fn.extend({searchInput:function c(d){return this.each(function(){return a(this,d)})}})}());(function(){function c(o,n){this.currModeIndex=0;return this._init(o,n)}c.prototype.DATA_KEY="mode-button";c.prototype.defaults={switchModesOnClick:true};c.prototype._init=function j(o,n){n=n||{};this.$element=$(o);this.options=jQuery.extend(true,{},this.defaults,n);if(!n.modes){throw new Error('ModeButton requires a "modes" array')}var q=this;this.$element.click(function p(r){q.callModeFn();if(q.options.switchModesOnClick){q._incModeIndex()}$(this).html(q.options.modes[q.currModeIndex].html)});return this.reset()};c.prototype._incModeIndex=function l(){this.currModeIndex+=1;if(this.currModeIndex>=this.options.modes.length){this.currModeIndex=0}return this};c.prototype._getModeIndex=function f(n){for(var o=0;o<this.options.modes.length;o+=1){if(this.options.modes[o].mode===n){return o}}throw new Error("mode not found: "+n)};c.prototype._setModeByIndex=function m(n){var o=this.options.modes[n];if(!o){throw new Error("mode index not found: "+n)}this.currModeIndex=n;if(o.html){this.$element.html(o.html)}return this};c.prototype.currentMode=function d(){return this.options.modes[this.currModeIndex]};c.prototype.current=function i(){return this.currentMode().mode};c.prototype.getMode=function g(n){if(!n){return this.currentMode()}return this.options.modes[(this._getModeIndex(n))]};c.prototype.hasMode=function b(n){try{return !!this.getMode(n)}catch(o){}return false};c.prototype.setMode=function a(n){return this._setModeByIndex(this._getModeIndex(n))};c.prototype.reset=function h(){this.currModeIndex=0;if(this.options.initialMode){this.currModeIndex=this._getModeIndex(this.options.initialMode)}return this._setModeByIndex(this.currModeIndex)};c.prototype.callModeFn=function e(n){var o=this.getMode(n).onclick;if(o&&jQuery.type(o==="function")){return o.call(this.$element.get(0))}return undefined};jQuery.fn.extend({modeButton:function k(n){if(!this.size()){return this}if(jQuery.type(n)==="object"){return this.map(function(){var r=$(this);r.data("mode-button",new c(r,n));return this})}var p=$(this[0]),o=p.data("mode-button");if(!o){throw new Error("modeButton needs an options object or string name of a function")}if(o&&jQuery.type(n)==="string"){var q=n;if(o&&jQuery.type(o[q])==="function"){return o[q].apply(o,jQuery.makeArray(arguments).slice(1))}}return o}})}());function dropDownSelect(b,c){c=c||((!_.isEmpty(b))?(b[0]):(""));var a=$(['<div class="dropdown-select btn-group">','<button type="button" class="btn btn-default">','<span class="dropdown-select-selected">'+c+"</span>","</button>","</div>"].join("\n"));if(b&&b.length>1){a.find("button").addClass("dropdown-toggle").attr("data-toggle","dropdown").append(' <span class="caret"></span>');a.append(['<ul class="dropdown-menu" role="menu">',_.map(b,function(e){return['<li><a href="javascript:void(0)">',e,"</a></li>"].join("")}).join("\n"),"</ul>"].join("\n"))}function d(g){var h=$(this),f=h.parents(".dropdown-select"),e=h.text();f.find(".dropdown-select-selected").text(e);f.trigger("change.dropdown-select",e)}a.find("a").click(d);return a}(function(){function e(k,j){return this.init(k,j)}e.prototype.DATA_KEY="filter-control";e.prototype.init=function g(k,j){j=j||{filters:[]};this.$element=$(k).addClass("filter-control btn-group");this.options=jQuery.extend(true,{},this.defaults,j);this.currFilter=this.options.filters[0];return this.render()};e.prototype.render=function d(){this.$element.empty().append([this._renderKeySelect(),this._renderOpSelect(),this._renderValueInput()]);return this};e.prototype._renderKeySelect=function a(){var j=this;var k=this.options.filters.map(function(l){return l.key});this.$keySelect=dropDownSelect(k,this.currFilter.key).addClass("filter-control-key").on("change.dropdown-select",function(m,l){j.currFilter=_.findWhere(j.options.filters,{key:l});j.render()._triggerChange()});return this.$keySelect};e.prototype._renderOpSelect=function i(){var j=this,k=this.currFilter.ops;this.$opSelect=dropDownSelect(k,k[0]).addClass("filter-control-op").on("change.dropdown-select",function(m,l){j._triggerChange()});return this.$opSelect};e.prototype._renderValueInput=function c(){var j=this;if(this.currFilter.values){this.$valueSelect=dropDownSelect(this.currFilter.values,this.currFilter.values[0]).on("change.dropdown-select",function(l,k){j._triggerChange()})}else{this.$valueSelect=$("<input/>").addClass("form-control").on("change",function(k,l){j._triggerChange()})}this.$valueSelect.addClass("filter-control-value");return this.$valueSelect};e.prototype.val=function b(){var k=this.$element.find(".filter-control-key .dropdown-select-selected").text(),m=this.$element.find(".filter-control-op .dropdown-select-selected").text(),j=this.$element.find(".filter-control-value"),l=(j.hasClass("dropdown-select"))?(j.find(".dropdown-select-selected").text()):(j.val());return{key:k,op:m,value:l}};e.prototype._triggerChange=function h(){this.$element.trigger("change.filter-control",this.val())};jQuery.fn.extend({filterControl:function f(k){var j=jQuery.makeArray(arguments).slice(1);return this.map(function(){var n=$(this),m=n.data(e.prototype.DATA_KEY);if(jQuery.type(k)==="object"){m=new e(n,k);n.data(e.prototype.DATA_KEY,m)}if(m&&jQuery.type(k)==="string"){var l=m[k];if(jQuery.type(l)==="function"){return l.apply(m,j)}}return this})}})}());(function(){function i(o,n){this.numPages=null;this.currPage=0;return this.init(o,n)}i.prototype.DATA_KEY="pagination";i.prototype.defaults={startingPage:0,perPage:20,totalDataSize:null,currDataSize:null};i.prototype.init=function g(n,o){o=o||{};this.$element=n;this.options=jQuery.extend(true,{},this.defaults,o);this.currPage=this.options.startingPage;if(this.options.totalDataSize!==null){this.numPages=Math.ceil(this.options.totalDataSize/this.options.perPage);if(this.currPage>=this.numPages){this.currPage=this.numPages-1}}this.$element.data(i.prototype.DATA_KEY,this);this._render();return this};function m(n){return $(['<li><a href="javascript:void(0);">',n,"</a></li>"].join(""))}i.prototype._render=function e(){if(this.options.totalDataSize===0){return this}if(this.numPages===1){return this}if(this.numPages>0){this._renderPages();this._scrollToActivePage()}else{this._renderPrevNext()}return this};i.prototype._renderPrevNext=function b(){var o=this,p=m("Prev"),n=m("Next"),q=$("<ul/>").addClass("pagination pagination-prev-next");if(this.currPage===0){p.addClass("disabled")}else{p.click(function(){o.prevPage()})}if((this.numPages&&this.currPage===(this.numPages-1))||(this.options.currDataSize&&this.options.currDataSize<this.options.perPage)){n.addClass("disabled")}else{n.click(function(){o.nextPage()})}this.$element.html(q.append([p,n]));return this.$element};i.prototype._renderPages=function a(){var n=this,q=$("<div>").addClass("pagination-scroll-container"),s=$("<ul/>").addClass("pagination pagination-page-list"),r=function(t){n.goToPage($(this).data("page"))};for(var o=0;o<this.numPages;o+=1){var p=m(o+1).attr("data-page",o).click(r);if(o===this.currPage){p.addClass("active")}s.append(p)}return this.$element.html(q.html(s))};i.prototype._scrollToActivePage=function l(){var p=this.$element.find(".pagination-scroll-container");if(!p.size()){return this}var o=this.$element.find("li.active"),n=p.width()/2;p.scrollLeft(p.scrollLeft()+o.position().left-n);return this};i.prototype.goToPage=function j(n){if(n<=0){n=0}if(this.numPages&&n>=this.numPages){n=this.numPages-1}if(n===this.currPage){return this}this.currPage=n;this.$element.trigger("pagination.page-change",this.currPage);this._render();return this};i.prototype.prevPage=function c(){return this.goToPage(this.currPage-1)};i.prototype.nextPage=function h(){return this.goToPage(this.currPage+1)};i.prototype.page=function f(){return this.currPage};i.create=function k(n,o){return new i(n,o)};jQuery.fn.extend({pagination:function d(o){var n=jQuery.makeArray(arguments).slice(1);if(jQuery.type(o)==="object"){return this.map(function(){i.create($(this),o);return this})}var q=$(this[0]),r=q.data(i.prototype.DATA_KEY);if(r){if(jQuery.type(o)==="string"){var p=r[o];if(jQuery.type(p)==="function"){return p.apply(r,n)}}else{return r}}return undefined}})}());(function(){var g={renameColumns:false,columnNames:[],commentChar:"#",hideCommentRows:false,includePrompts:true,topLeftContent:"Columns:"},s="peek-control.change",t="peek-control.rename",l="peek-control",v="control",f="control-prompt",c="selected",n="disabled",u="button",q="renamable-header",p="column-index",a="column-name";function i(w){if(w.disabled&&jQuery.type(w.disabled)!=="array"){throw new Error('"disabled" must be defined as an array of indeces: '+JSON.stringify(w))}if(w.multiselect&&w.selected&&jQuery.type(w.selected)!=="array"){throw new Error('Mulitselect rows need an array for "selected": '+JSON.stringify(w))}if(!w.label||!w.id){throw new Error("Peek controls need a label and id for each control row: "+JSON.stringify(w))}if(w.disabled&&w.disabled.indexOf(w.selected)!==-1){throw new Error("Selected column is in the list of disabled columns: "+JSON.stringify(w))}return w}function o(x,w){return $("<div/>").addClass(u).text(x.label)}function k(x,w){var y=$("<td/>").html(o(x,w)).attr("data-"+p,w);if(x.disabled&&x.disabled.indexOf(w)!==-1){y.addClass(n)}return y}function e(y,z,w){var x=y.children("."+u);if(y.hasClass(c)){x.html((z.selectedText!==undefined)?(z.selectedText):(z.label))}else{x.html((z.unselectedText!==undefined)?(z.unselectedText):(z.label))}}function h(z,x){var y=k(z,x);if(z.selected===x){y.addClass(c)}e(y,z,x);if(!y.hasClass(n)){y.click(function w(D){var E=$(this);if(!E.hasClass(c)){var A=E.parent().children("."+c).removeClass(c);A.each(function(){e($(this),z,x)});E.addClass(c);e(E,z,x);var C={},B=E.parent().attr("id"),F=E.data(p);C[B]=F;E.parents(".peek").trigger(s,C)}})}return y}function m(z,x){var y=k(z,x);if(z.selected&&z.selected.indexOf(x)!==-1){y.addClass(c)}e(y,z,x);if(!y.hasClass(n)){y.click(function w(D){var E=$(this);E.toggleClass(c);e(E,z,x);var C=E.parent().find("."+c).map(function(G,H){return $(H).data(p)});var B={},A=E.parent().attr("id"),F=jQuery.makeArray(C);B[A]=F;E.parents(".peek").trigger(s,B)})}return y}function j(y,z){var w=[];for(var x=0;x<y;x+=1){w.push(z.multiselect?m(z,x):h(z,x))}return w}function r(y,z,x){var A=$("<tr/>").attr("id",z.id).addClass(v);if(x){var w=$("<td/>").addClass(f).text(z.label+":");A.append(w)}A.append(j(y,z));return A}function b(E){E=jQuery.extend(true,{},g,E);var D=$(this).addClass(l),A=D.find("table"),z=A.find("th").size(),C=A.find("tr").size(),w=A.find("td[colspan]").map(function(H,F){var G=$(this);if(G.text()&&G.text().match(new RegExp("^"+E.commentChar))){return $(this).css("color","grey").parent().get(0)}return null});if(E.hideCommentRows){w.hide();C-=w.size()}if(E.includePrompts){var y=$("<th/>").addClass("top-left").text(E.topLeftContent).attr("rowspan",C);A.find("tr").first().prepend(y)}var B=A.find("th:not(.top-left)").each(function(G,I){var H=$(this),J=H.text().replace(/^\d+\.*/,""),F=E.columnNames[G]||J;H.attr("data-"+a,F).text((G+1)+((F)?("."+F):("")))});if(E.renameColumns){B.addClass(q).click(function x(){var G=$(this),F=G.index()+(E.includePrompts?0:1),I=G.data(a),H=prompt("New column name:",I);if(H!==null&&H!==I){G.text(F+(H?("."+H):"")).data(a,H).attr("data-",a,H);var J=jQuery.makeArray(G.parent().children("th:not(.top-left)").map(function(){return $(this).data(a)}));G.parents(".peek").trigger(t,J)}})}E.controls.forEach(function(G,F){i(G);var H=r(z,G,E.includePrompts);A.find("tbody").append(H)});return this}jQuery.fn.extend({peekControl:function d(w){return this.map(function(){return b.call(this,w)})}})}()); \ No newline at end of file +var IconButton=Backbone.Model.extend({defaults:{title:"",icon_class:"",on_click:null,menu_options:null,is_menu_button:true,id:null,href:null,target:null,enabled:true,visible:true,tooltip_config:{}}});var IconButtonView=Backbone.View.extend({initialize:function(){this.model.attributes.tooltip_config={placement:"bottom"};this.model.bind("change",this.render,this)},render:function(){this.$el.tooltip("hide");var a=this.template(this.model.toJSON());a.tooltip(this.model.get("tooltip_config"));this.$el.replaceWith(a);this.setElement(a);return this},events:{click:"click"},click:function(a){if(_.isFunction(this.model.get("on_click"))){this.model.get("on_click")(a);return false}return true},template:function(b){var a='title="'+b.title+'" class="icon-button';if(b.is_menu_button){a+=" menu-button"}a+=" "+b.icon_class;if(!b.enabled){a+="_disabled"}a+='"';if(b.id){a+=' id="'+b.id+'"'}a+=' href="'+b.href+'"';if(b.target){a+=' target="'+b.target+'"'}if(!b.visible){a+=' style="display: none;"'}if(b.enabled){a="<a "+a+"/>"}else{a="<span "+a+"/>"}return $(a)}});var IconButtonCollection=Backbone.Collection.extend({model:IconButton});var IconButtonMenuView=Backbone.View.extend({tagName:"div",initialize:function(){this.render()},render:function(){var a=this;this.collection.each(function(d){var b=$("<a/>").attr("href","javascript:void(0)").attr("title",d.attributes.title).addClass("icon-button menu-button").addClass(d.attributes.icon_class).appendTo(a.$el).click(d.attributes.on_click);if(d.attributes.tooltip_config){b.tooltip(d.attributes.tooltip_config)}var c=d.get("options");if(c){make_popupmenu(b,c)}});return this}});var create_icon_buttons_menu=function(b,a){if(!a){a={}}var c=new IconButtonCollection(_.map(b,function(d){return new IconButton(_.extend(d,a))}));return new IconButtonMenuView({collection:c})};var Grid=Backbone.Collection.extend({});var GridView=Backbone.View.extend({});var PopupMenu=Backbone.View.extend({initialize:function(b,a){this.$button=b;if(!this.$button.size()){this.$button=$("<div/>")}this.options=a||[];var c=this;this.$button.click(function(d){$(".popmenu-wrapper").remove();c._renderAndShow(d);return false})},_renderAndShow:function(a){this.render();this.$el.appendTo("body").css(this._getShownPosition(a)).show();this._setUpCloseBehavior()},render:function(){this.$el.addClass("popmenu-wrapper").hide().css({position:"absolute"}).html(this.template(this.$button.attr("id"),this.options));if(this.options.length){var a=this;this.$el.find("li").each(function(c,b){var d=a.options[c];if(d.func){$(this).children("a.popupmenu-option").click(function(e){d.func.call(a,e,d)})}})}return this},template:function(b,a){return['<ul id="',b,'-menu" class="dropdown-menu">',this._templateOptions(a),"</ul>"].join("")},_templateOptions:function(a){if(!a.length){return"<li>(no options)</li>"}return _.map(a,function(d){if(d.divider){return'<li class="divider"></li>'}else{if(d.header){return['<li class="head"><a href="javascript:void(0);">',d.html,"</a></li>"].join("")}}var c=d.href||"javascript:void(0);",e=(d.target)?(' target="'+d.target+'"'):(""),b=(d.checked)?('<span class="fa fa-check"></span>'):("");return['<li><a class="popupmenu-option" href="',c,'"',e,">",b,d.html,"</a></li>"].join("")}).join("")},_getShownPosition:function(b){var c=this.$el.width();var a=b.pageX-c/2;a=Math.min(a,$(document).scrollLeft()+$(window).width()-c-5);a=Math.max(a,$(document).scrollLeft()+5);return{top:b.pageY,left:a}},_setUpCloseBehavior:function(){var c=this;function a(e){$(document).off("click.close_popup");if(window.parent!==window){try{$(window.parent.document).off("click.close_popup")}catch(d){}}else{try{$("iframe#galaxy_main").contents().off("click.close_popup")}catch(d){}}c.remove()}$("html").one("click.close_popup",a);if(window.parent!==window){try{$(window.parent.document).find("html").one("click.close_popup",a)}catch(b){}}else{try{$("iframe#galaxy_main").contents().one("click.close_popup",a)}catch(b){}}},addItem:function(b,a){a=(a>=0)?a:this.options.length;this.options.splice(a,0,b);return this},removeItem:function(a){if(a>=0){this.options.splice(a,1)}return this},findIndexByHtml:function(b){for(var a=0;a<this.options.length;a++){if(_.has(this.options[a],"html")&&(this.options[a].html===b)){return a}}return null},findItemByHtml:function(a){return this.options[(this.findIndexByHtml(a))]},toString:function(){return"PopupMenu"}});PopupMenu.create=function _create(b,a){return new PopupMenu(b,a)};PopupMenu.make_popupmenu=function(b,c){var a=[];_.each(c,function(f,d){var e={html:d};if(f===null){e.header=true}else{if(jQuery.type(f)==="function"){e.func=f}}a.push(e)});return new PopupMenu($(b),a)};PopupMenu.convertLinksToOptions=function(c,a){c=$(c);a=a||"a";var b=[];c.find(a).each(function(g,e){var f={},d=$(g);f.html=d.text();if(d.attr("href")){var j=d.attr("href"),k=d.attr("target"),h=d.attr("confirm");f.func=function(){if((h)&&(!confirm(h))){return}switch(k){case"_parent":window.parent.location=j;break;case"_top":window.top.location=j;break;default:window.location=j}}}b.push(f)});return b};PopupMenu.fromExistingDom=function(d,c,a){d=$(d);c=$(c);var b=PopupMenu.convertLinksToOptions(c,a);c.remove();return new PopupMenu(d,b)};PopupMenu.make_popup_menus=function(c,b,d){c=c||document;b=b||"div[popupmenu]";d=d||function(e,f){return"#"+e.attr("popupmenu")};var a=[];$(c).find(b).each(function(){var e=$(this),f=$(c).find(d(e,c));a.push(PopupMenu.fromDom(f,e));f.addClass("popup")});return a};var faIconButton=function(a){a=a||{};a.tooltipConfig=a.tooltipConfig||{placement:"bottom"};a.classes=["icon-btn"].concat(a.classes||[]);if(a.disabled){a.classes.push("disabled")}var b=['<a class="',a.classes.join(" "),'"',((a.title)?(' title="'+a.title+'"'):("")),((!a.disabled&&a.target)?(' target="'+a.target+'"'):("")),' href="',((!a.disabled&&a.href)?(a.href):("javascript:void(0);")),'">','<span class="fa ',a.faIcon,'"></span>',"</a>"].join("");var c=$(b).tooltip(a.tooltipConfig);if(_.isFunction(a.onclick)){c.click(a.onclick)}return c};function LoadingIndicator(a,c){var b=this;c=jQuery.extend({cover:false},c||{});function d(){var e=['<div class="loading-indicator">','<div class="loading-indicator-text">','<span class="fa fa-spinner fa-spin fa-lg"></span>','<span class="loading-indicator-message">loading...</span>',"</div>","</div>"].join("\n");var g=$(e).hide().css(c.css||{position:"fixed"}),f=g.children(".loading-indicator-text");if(c.cover){g.css({"z-index":2,top:a.css("top"),bottom:a.css("bottom"),left:a.css("left"),right:a.css("right"),opacity:0.5,"background-color":"white","text-align":"center"});f=g.children(".loading-indicator-text").css({"margin-top":"20px"})}else{f=g.children(".loading-indicator-text").css({margin:"12px 0px 0px 10px",opacity:"0.85",color:"grey"});f.children(".loading-indicator-message").css({margin:"0px 8px 0px 0px","font-style":"italic"})}return g}b.show=function(f,e,g){f=f||"loading...";e=e||"fast";a.parent().find(".loading-indicator").remove();b.$indicator=d().insertBefore(a);b.message(f);b.$indicator.fadeIn(e,g);return b};b.message=function(e){b.$indicator.find("i").text(e)};b.hide=function(e,f){e=e||"fast";if(b.$indicator&&b.$indicator.size()){b.$indicator.fadeOut(e,function(){b.$indicator.remove();if(f){f()}})}else{if(f){f()}}return b};return b}(function(){var b=window._l||function(d){return d};function a(k,q){var e=27,n=13,d=$(k),f=true,h={initialVal:"",name:"search",placeholder:"search",classes:"",onclear:function(){},onfirstsearch:null,onsearch:function(r){},minSearchLen:0,escWillClear:true,oninit:function(){}};function j(r){var s=$(this).parent().children("input");s.focus().val("").trigger("clear:searchInput");q.onclear()}function p(s,r){$(this).trigger("search:searchInput",r);if(typeof q.onfirstsearch==="function"&&f){f=false;q.onfirstsearch(r)}else{q.onsearch(r)}}function g(){return['<input type="text" name="',q.name,'" placeholder="',q.placeholder,'" ','class="search-query ',q.classes,'" ',"/>"].join("")}function m(){return $(g()).focus(function(r){$(this).select()}).keyup(function(s){s.preventDefault();s.stopPropagation();if(!$(this).val()){$(this).blur()}if(s.which===e&&q.escWillClear){j.call(this,s)}else{var r=$(this).val();if((s.which===n)||(q.minSearchLen&&r.length>=q.minSearchLen)){p.call(this,s,r)}else{if(!r.length){j.call(this,s)}}}}).on("change",function(r){p.call(this,r,$(this).val())}).val(q.initialVal)}function l(){return $(['<span class="search-clear fa fa-times-circle" ','title="',b("clear search (esc)"),'"></span>'].join("")).tooltip({placement:"bottom"}).click(function(r){j.call(this,r)})}function o(){return $(['<span class="search-loading fa fa-spinner fa-spin" ','title="',b("loading..."),'"></span>'].join("")).hide().tooltip({placement:"bottom"})}function i(){d.find(".search-loading").toggle();d.find(".search-clear").toggle()}if(jQuery.type(q)==="string"){if(q==="toggle-loading"){i()}return d}if(jQuery.type(q)==="object"){q=jQuery.extend(true,{},h,q)}return d.addClass("search-input").prepend([m(),l(),o()])}jQuery.fn.extend({searchInput:function c(d){return this.each(function(){return a(this,d)})}})}());(function(){function c(o,n){this.currModeIndex=0;return this._init(o,n)}c.prototype.DATA_KEY="mode-button";c.prototype.defaults={switchModesOnClick:true};c.prototype._init=function j(o,n){n=n||{};this.$element=$(o);this.options=jQuery.extend(true,{},this.defaults,n);if(!n.modes){throw new Error('ModeButton requires a "modes" array')}var q=this;this.$element.click(function p(r){q.callModeFn();if(q.options.switchModesOnClick){q._incModeIndex()}$(this).html(q.options.modes[q.currModeIndex].html)});return this.reset()};c.prototype._incModeIndex=function l(){this.currModeIndex+=1;if(this.currModeIndex>=this.options.modes.length){this.currModeIndex=0}return this};c.prototype._getModeIndex=function f(n){for(var o=0;o<this.options.modes.length;o+=1){if(this.options.modes[o].mode===n){return o}}throw new Error("mode not found: "+n)};c.prototype._setModeByIndex=function m(n){var o=this.options.modes[n];if(!o){throw new Error("mode index not found: "+n)}this.currModeIndex=n;if(o.html){this.$element.html(o.html)}return this};c.prototype.currentMode=function d(){return this.options.modes[this.currModeIndex]};c.prototype.current=function i(){return this.currentMode().mode};c.prototype.getMode=function g(n){if(!n){return this.currentMode()}return this.options.modes[(this._getModeIndex(n))]};c.prototype.hasMode=function b(n){try{return !!this.getMode(n)}catch(o){}return false};c.prototype.setMode=function a(n){return this._setModeByIndex(this._getModeIndex(n))};c.prototype.reset=function h(){this.currModeIndex=0;if(this.options.initialMode){this.currModeIndex=this._getModeIndex(this.options.initialMode)}return this._setModeByIndex(this.currModeIndex)};c.prototype.callModeFn=function e(n){var o=this.getMode(n).onclick;if(o&&jQuery.type(o==="function")){return o.call(this.$element.get(0))}return undefined};jQuery.fn.extend({modeButton:function k(n){if(!this.size()){return this}if(jQuery.type(n)==="object"){return this.map(function(){var r=$(this);r.data("mode-button",new c(r,n));return this})}var p=$(this[0]),o=p.data("mode-button");if(!o){throw new Error("modeButton needs an options object or string name of a function")}if(o&&jQuery.type(n)==="string"){var q=n;if(o&&jQuery.type(o[q])==="function"){return o[q].apply(o,jQuery.makeArray(arguments).slice(1))}}return o}})}());function dropDownSelect(b,c){c=c||((!_.isEmpty(b))?(b[0]):(""));var a=$(['<div class="dropdown-select btn-group">','<button type="button" class="btn btn-default">','<span class="dropdown-select-selected">'+c+"</span>","</button>","</div>"].join("\n"));if(b&&b.length>1){a.find("button").addClass("dropdown-toggle").attr("data-toggle","dropdown").append(' <span class="caret"></span>');a.append(['<ul class="dropdown-menu" role="menu">',_.map(b,function(e){return['<li><a href="javascript:void(0)">',e,"</a></li>"].join("")}).join("\n"),"</ul>"].join("\n"))}function d(g){var h=$(this),f=h.parents(".dropdown-select"),e=h.text();f.find(".dropdown-select-selected").text(e);f.trigger("change.dropdown-select",e)}a.find("a").click(d);return a}(function(){function e(k,j){return this.init(k,j)}e.prototype.DATA_KEY="filter-control";e.prototype.init=function g(k,j){j=j||{filters:[]};this.$element=$(k).addClass("filter-control btn-group");this.options=jQuery.extend(true,{},this.defaults,j);this.currFilter=this.options.filters[0];return this.render()};e.prototype.render=function d(){this.$element.empty().append([this._renderKeySelect(),this._renderOpSelect(),this._renderValueInput()]);return this};e.prototype._renderKeySelect=function a(){var j=this;var k=this.options.filters.map(function(l){return l.key});this.$keySelect=dropDownSelect(k,this.currFilter.key).addClass("filter-control-key").on("change.dropdown-select",function(m,l){j.currFilter=_.findWhere(j.options.filters,{key:l});j.render()._triggerChange()});return this.$keySelect};e.prototype._renderOpSelect=function i(){var j=this,k=this.currFilter.ops;this.$opSelect=dropDownSelect(k,k[0]).addClass("filter-control-op").on("change.dropdown-select",function(m,l){j._triggerChange()});return this.$opSelect};e.prototype._renderValueInput=function c(){var j=this;if(this.currFilter.values){this.$valueSelect=dropDownSelect(this.currFilter.values,this.currFilter.values[0]).on("change.dropdown-select",function(l,k){j._triggerChange()})}else{this.$valueSelect=$("<input/>").addClass("form-control").on("change",function(k,l){j._triggerChange()})}this.$valueSelect.addClass("filter-control-value");return this.$valueSelect};e.prototype.val=function b(){var k=this.$element.find(".filter-control-key .dropdown-select-selected").text(),m=this.$element.find(".filter-control-op .dropdown-select-selected").text(),j=this.$element.find(".filter-control-value"),l=(j.hasClass("dropdown-select"))?(j.find(".dropdown-select-selected").text()):(j.val());return{key:k,op:m,value:l}};e.prototype._triggerChange=function h(){this.$element.trigger("change.filter-control",this.val())};jQuery.fn.extend({filterControl:function f(k){var j=jQuery.makeArray(arguments).slice(1);return this.map(function(){var n=$(this),m=n.data(e.prototype.DATA_KEY);if(jQuery.type(k)==="object"){m=new e(n,k);n.data(e.prototype.DATA_KEY,m)}if(m&&jQuery.type(k)==="string"){var l=m[k];if(jQuery.type(l)==="function"){return l.apply(m,j)}}return this})}})}());(function(){function i(o,n){this.numPages=null;this.currPage=0;return this.init(o,n)}i.prototype.DATA_KEY="pagination";i.prototype.defaults={startingPage:0,perPage:20,totalDataSize:null,currDataSize:null};i.prototype.init=function g(n,o){o=o||{};this.$element=n;this.options=jQuery.extend(true,{},this.defaults,o);this.currPage=this.options.startingPage;if(this.options.totalDataSize!==null){this.numPages=Math.ceil(this.options.totalDataSize/this.options.perPage);if(this.currPage>=this.numPages){this.currPage=this.numPages-1}}this.$element.data(i.prototype.DATA_KEY,this);this._render();return this};function m(n){return $(['<li><a href="javascript:void(0);">',n,"</a></li>"].join(""))}i.prototype._render=function e(){if(this.options.totalDataSize===0){return this}if(this.numPages===1){return this}if(this.numPages>0){this._renderPages();this._scrollToActivePage()}else{this._renderPrevNext()}return this};i.prototype._renderPrevNext=function b(){var o=this,p=m("Prev"),n=m("Next"),q=$("<ul/>").addClass("pagination pagination-prev-next");if(this.currPage===0){p.addClass("disabled")}else{p.click(function(){o.prevPage()})}if((this.numPages&&this.currPage===(this.numPages-1))||(this.options.currDataSize&&this.options.currDataSize<this.options.perPage)){n.addClass("disabled")}else{n.click(function(){o.nextPage()})}this.$element.html(q.append([p,n]));return this.$element};i.prototype._renderPages=function a(){var n=this,q=$("<div>").addClass("pagination-scroll-container"),s=$("<ul/>").addClass("pagination pagination-page-list"),r=function(t){n.goToPage($(this).data("page"))};for(var o=0;o<this.numPages;o+=1){var p=m(o+1).attr("data-page",o).click(r);if(o===this.currPage){p.addClass("active")}s.append(p)}return this.$element.html(q.html(s))};i.prototype._scrollToActivePage=function l(){var p=this.$element.find(".pagination-scroll-container");if(!p.size()){return this}var o=this.$element.find("li.active"),n=p.width()/2;p.scrollLeft(p.scrollLeft()+o.position().left-n);return this};i.prototype.goToPage=function j(n){if(n<=0){n=0}if(this.numPages&&n>=this.numPages){n=this.numPages-1}if(n===this.currPage){return this}this.currPage=n;this.$element.trigger("pagination.page-change",this.currPage);this._render();return this};i.prototype.prevPage=function c(){return this.goToPage(this.currPage-1)};i.prototype.nextPage=function h(){return this.goToPage(this.currPage+1)};i.prototype.page=function f(){return this.currPage};i.create=function k(n,o){return new i(n,o)};jQuery.fn.extend({pagination:function d(o){var n=jQuery.makeArray(arguments).slice(1);if(jQuery.type(o)==="object"){return this.map(function(){i.create($(this),o);return this})}var q=$(this[0]),r=q.data(i.prototype.DATA_KEY);if(r){if(jQuery.type(o)==="string"){var p=r[o];if(jQuery.type(p)==="function"){return p.apply(r,n)}}else{return r}}return undefined}})}()); \ No newline at end of file diff -r bfc3b5e52781683efc8024f18c4aa57c65af1e15 -r ee995df4a7e548befdfa09196b9665f05cfd55f6 static/style/blue/base.css --- a/static/style/blue/base.css +++ b/static/style/blue/base.css @@ -1648,17 +1648,6 @@ .pagination-scroll-container .pagination-page-list>li:first-child>a,.pagination-scroll-container .pagination-page-list>li:first-child>span{border-radius:0px;border-left:0px} .pagination-scroll-container .pagination-page-list>li:last-child>a,.pagination-scroll-container .pagination-page-list>li:last-child>span{border-radius:0px} .pagination-scroll-container .pagination-page-list>li>a{float:none;position:static;border:1px solid #BFBFBF;border-width:0px 0px 0px 1px} -.peek-control{border-radius:3px;border:1px solid #5f6990} -.peek-control td,.peek-control th{padding:4px 10px 4px 4px} -.peek-control th:last-child{width:100%} -.peek-control .top-left{width:10%;white-space:normal;vertical-align:top;text-align:right;font-family:"Lucida Grande",verdana,arial,helvetica,sans-serif;font-weight:normal} -.peek-control .renamable-header:hover{background-color:black} -.peek-control .control td.control-prompt{background-color:#5f6990;padding:0px 4px 0px 8px;text-align:right;color:white} -.peek-control .control td{padding:1px;font-family:"Lucida Grande",verdana,arial,helvetica,sans-serif;color:grey} -.peek-control .control td .button{min-width:28px;border:1px solid grey;border-radius:3px;padding:4px;color:grey} -.peek-control .control td:hover .button{background-color:#EEE;border:1px solid black;cursor:pointer;color:black} -.peek-control .control td.disabled .button,.peek-control .control td.disabled:hover .button{background-color:transparent;border:1px solid #CCC;cursor:not-allowed;color:#CCC} -.peek-control .control td.selected .button{background-color:black;border:1px solid black;color:white} div.metadataForm{border:solid #aaaaaa 1px} div.metadataFormTitle{font-weight:bold;padding:5px;padding-left:10px;padding-right:10px;background:#cccccc;background-repeat:repeat-x;background-position:top;border-bottom:solid #aaaaaa 1px} div.metadataFormBody{background:#FFFFFF;padding:5px 0} @@ -2112,6 +2101,17 @@ .dataset-choice.multi table th:not(:last-child),.dataset-choice.multi table td:not(:last-child){padding-right:8px} .dataset-choice.multi table td.cell-name{font-weight:bold} .dataset-choice-modal .list-panel .controls .title .name{font-size:120%} +.peek-column-selector{border-radius:3px;border:1px solid #5f6990} +.peek-column-selector td,.peek-column-selector th{padding:4px 10px 4px 4px} +.peek-column-selector th:last-child{width:100%} +.peek-column-selector .top-left{width:10%;white-space:normal;vertical-align:top;text-align:right;font-family:"Lucida Grande",verdana,arial,helvetica,sans-serif;font-weight:normal} +.peek-column-selector .renamable-header:hover{background-color:black} +.peek-column-selector .control td.control-prompt{background-color:#5f6990;padding:0px 4px 0px 8px;text-align:right;color:white} +.peek-column-selector .control td{padding:1px;font-family:"Lucida Grande",verdana,arial,helvetica,sans-serif;color:grey} +.peek-column-selector .control td .button{min-width:28px;border:1px solid grey;border-radius:3px;padding:4px;color:grey} +.peek-column-selector .control td:hover .button{background-color:#EEE;border:1px solid black;cursor:pointer;color:black} +.peek-column-selector .control td.disabled .button,.peek-column-selector .control td.disabled:hover .button{background-color:transparent;border:1px solid #CCC;cursor:not-allowed;color:#CCC} +.peek-column-selector .control td.selected .button{background-color:black;border:1px solid black;color:white} .toolMenuContainer{color:#000;background:#dfe5f9;min-height:100%;padding:5px 10px} div.toolSectionPad{margin:0;padding:0;height:5px;font-size:0px} div.toolSectionWrapper{margin-bottom:5px} diff -r bfc3b5e52781683efc8024f18c4aa57c65af1e15 -r ee995df4a7e548befdfa09196b9665f05cfd55f6 static/style/src/less/base.less --- a/static/style/src/less/base.less +++ b/static/style/src/less/base.less @@ -545,77 +545,6 @@ } -// ---------------------------------------------------------------------------- peek-based column chooser -.peek-control { - border-radius: 3px; - border: 1px solid rgb(95, 105, 144); -} - -.peek-control td, -.peek-control th { - padding: 4px 10px 4px 4px; -} - -.peek-control th:last-child { - width: 100%; -} - -.peek-control .top-left { - width: 10%; - white-space: normal; - vertical-align: top; - text-align: right; - font-family: "Lucida Grande", verdana, arial, helvetica, sans-serif; - font-weight: normal; -} - -.peek-control .renamable-header:hover { - background-color: black; -} - -.peek-control .control td.control-prompt { - background-color: rgb(95, 105, 144); - padding: 0px 4px 0px 8px; - text-align: right; - color: white; -} - -.peek-control .control td { - padding: 1px; - font-family: "Lucida Grande", verdana, arial, helvetica, sans-serif; - color: grey; -} - -.peek-control .control td .button { - min-width: 28px; - border: 1px solid grey; - border-radius: 3px; - padding: 4px; - color: grey; -} - -.peek-control .control td:hover .button { - background-color: #EEE; - border: 1px solid black; - cursor: pointer; - color: black; -} - -.peek-control .control td.disabled .button, -.peek-control .control td.disabled:hover .button { - background-color: transparent; - border: 1px solid #CCC; - cursor: not-allowed; - color: #CCC; -} - -.peek-control .control td.selected .button { - background-color: black; - border: 1px solid black; - color: white; -} - - // ==== Tool form styles ==== div.metadataForm { @@ -1565,6 +1494,7 @@ @import "ui/paired-collection-creator.less"; @import "ui/dataset-choice.less"; +@import "ui/peek-column-selector.less"; // ==== Tool menu styles diff -r bfc3b5e52781683efc8024f18c4aa57c65af1e15 -r ee995df4a7e548befdfa09196b9665f05cfd55f6 static/style/src/less/ui/peek-column-selector.less --- /dev/null +++ b/static/style/src/less/ui/peek-column-selector.less @@ -0,0 +1,69 @@ +// peek-based column chooser, see: scripts/ui/peek-column-selector.js +.peek-column-selector { + border-radius: 3px; + border: 1px solid rgb(95, 105, 144); +} + +.peek-column-selector td, +.peek-column-selector th { + padding: 4px 10px 4px 4px; +} + +.peek-column-selector th:last-child { + width: 100%; +} + +.peek-column-selector .top-left { + width: 10%; + white-space: normal; + vertical-align: top; + text-align: right; + font-family: "Lucida Grande", verdana, arial, helvetica, sans-serif; + font-weight: normal; +} + +.peek-column-selector .renamable-header:hover { + background-color: black; +} + +.peek-column-selector .control td.control-prompt { + background-color: rgb(95, 105, 144); + padding: 0px 4px 0px 8px; + text-align: right; + color: white; +} + +.peek-column-selector .control td { + padding: 1px; + font-family: "Lucida Grande", verdana, arial, helvetica, sans-serif; + color: grey; +} + +.peek-column-selector .control td .button { + min-width: 28px; + border: 1px solid grey; + border-radius: 3px; + padding: 4px; + color: grey; +} + +.peek-column-selector .control td:hover .button { + background-color: #EEE; + border: 1px solid black; + cursor: pointer; + color: black; +} + +.peek-column-selector .control td.disabled .button, +.peek-column-selector .control td.disabled:hover .button { + background-color: transparent; + border: 1px solid #CCC; + cursor: not-allowed; + color: #CCC; +} + +.peek-column-selector .control td.selected .button { + background-color: black; + border: 1px solid black; + color: white; +} Repository URL: https://bitbucket.org/galaxy/galaxy-central/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email.
participants (1)
-
commits-noreply@bitbucket.org