@gmod/jbrowse
Version:
JBrowse - client-side genome browser
271 lines (239 loc) • 10.3 kB
JavaScript
/**
* Mixin that provides generic functions for displaying nested data.
*/
define([
'dojo/_base/declare',
'dojo/_base/lang',
'dojo/_base/array',
'dojo/query',
'dojo/dom-construct',
'dojo/dom-class',
'dstore/Memory',
'dgrid/OnDemandGrid',
'dgrid/extensions/DijitRegistry',
'JBrowse/Util'
],
function(
declare,
lang,
array,
query,
domConstruct,
domClass,
MemoryStore,
DGrid,
DGridDijitRegistry,
Util
) {
// make a DGrid that registers itself as a dijit widget
var Grid = declare([DGrid,DGridDijitRegistry]);
return declare( null, {
renderDetailField: function( parentElement, title, val, f, class_, externalFieldMeta = {} ) {
if( val === null || val === undefined )
return '';
// if this object has a 'fmtDetailFooField' function, delegate to that
var fieldSpecificFormatter;
if(( fieldSpecificFormatter = this['fmtDetail'+Util.ucFirst(title)+'Field'] ))
return fieldSpecificFormatter.apply( this, arguments );
// otherwise, use default formatting
class_ = class_ || title.replace(/\W/g,'_').toLowerCase();
var formatted_title=title;
// if this object has a config value 'fmtDetailField_Foo' function, apply it to field title
if(( fieldSpecificFormatter = this.config['fmtDetailField_'+title] ) && f) {
formatted_title= fieldSpecificFormatter(title,f);
if(!formatted_title) return ''; // if the callback returns null, remove field from dialog
}
else if(( fieldSpecificFormatter = this.config['fmtMetaField_'+title] ) && !f) {
formatted_title= fieldSpecificFormatter(title);
if(!formatted_title) return ''; // if the callback returns null, remove field from dialog
}
// special case for values that include metadata about their
// meaning, which are formed like { values: [], meta:
// {description: }. break it out, putting the meta description in a `title`
// attr on the field name so that it shows on mouseover, and
// using the values as the new field value.
var fieldMeta;
if( typeof val == 'object' && !Array.isArray(val) && ('values' in val) ) {
fieldMeta = (val.meta||{}).description;
// join the description if it is an array
if( lang.isArray( fieldMeta ) )
fieldMeta = fieldMeta.join(', ');
val = val.values;
}
else {
fieldMeta = externalFieldMeta.description
}
if(( fieldSpecificFormatter = this.config['fmtDetailDescription_'+title] ) && f) {
fieldMeta = fieldSpecificFormatter(fieldMeta);
}
else if(( fieldSpecificFormatter = this.config['fmtMetaDescription_'+title] ) && !f) {
fieldMeta = fieldSpecificFormatter(fieldMeta);
}
var titleAttr = fieldMeta ? ' title="'+fieldMeta+'"' : '';
var fieldContainer = domConstruct.create(
'div',
{ className: 'field_container',
innerHTML: '<h2 class="field '+class_+'"'+titleAttr+'>'+formatted_title+'</h2>'
}, parentElement );
var valueContainer = domConstruct.create(
'div',
{ className: 'value_container '
+ class_
}, fieldContainer );
var count = this.renderDetailValue( valueContainer, title, val, f, class_);
if( typeof count == 'number' && count > 4 ) {
query( 'h2', fieldContainer )[0].innerHTML = formatted_title + ' ('+count+')';
}
return fieldContainer;
},
renderDetailValue: function( parent, title, val, f, class_ ) {
var thisB = this;
if( !lang.isArray(val) && val.values )
val = val.values;
// if this object has a 'fmtDetailFooValue' function, delegate to that
var fieldSpecificFormatter;
if(( fieldSpecificFormatter = this['fmtDetail'+Util.ucFirst(title)+'Value'] ))
return fieldSpecificFormatter.apply( this, arguments );
// otherwise, use default formatting
// if this object has a config value 'fmtDetailValue_Foo' function, apply it to val
if(( fieldSpecificFormatter = this.config['fmtDetailValue_'+title] ) && f) {
val= fieldSpecificFormatter( val,f );
if(!val) val='';
if(val.length==1) val=val[0]; // avoid recursion when an array of length 1 is returned
}
else if(( fieldSpecificFormatter = this.config['fmtMetaValue_'+title] ) && !f) {
val=fieldSpecificFormatter( val );
if(val.length==1) val=val[0];
}
var valType = typeof val;
if( typeof val.toHTML == 'function' )
val = val.toHTML();
if( valType == 'boolean' )
val = val ? 'yes' : 'no';
else if( valType == 'undefined' || val === null )
return 0;
else if( lang.isArray( val ) ) {
var vals;
if( val.length>0 && lang.isObject(val[0])) {
parent.style.width = '90%'
vals = val.map( v => {
const itemContainer = domConstruct.create('div', {
className: 'value_container '+class_,
style: { width: '100%' },
}, parent );
this.renderDetailValue( itemContainer, title, v, f, class_ );
return itemContainer
})
} else {
vals = array.map( val, function(v) {
return this.renderDetailValue( parent, title, v, f, class_ );
}, this );
}
if( vals.length > 1 )
domClass.add( parent, 'multi_value' );
if( vals.length > 10 )
domClass.add( parent, 'big' );
return vals.length;
} else if( valType == 'object' ) {
var keys = Util.dojof.keys( val ).sort();
var count = keys.length;
if( count > 5 ) {
this.renderDetailValueGrid(
parent,
title,
f,
// iterator
function() {
if( ! keys.length )
return null;
var k = keys.shift();
var value = val[k];
var item = { id: k };
if( typeof value == 'object' ) {
for( var field in value ) {
item[field] = thisB._valToString( value[field] );
}
}
else {
item.value = value;
}
return item;
},
{ descriptions: (function() {
if( ! keys.length )
return {};
var subValue = val[keys[0]];
var descriptions = {};
for( var k in subValue ) {
descriptions[k] =
subValue[k].meta && subValue[k].meta.description
|| null;
}
return descriptions;
})()
}
);
return count;
}
else {
array.forEach( keys, function( k ) {
return this.renderDetailField( parent, k, val[k], f, class_ );
}, this );
return keys.length;
}
}
domConstruct.create('div', { className: 'value '+class_, innerHTML: val }, parent );
return 1;
},
renderDetailValueGrid: function( parent, title, f, iterator, attrs ) {
var thisB = this;
var rows = [];
var item;
var descriptions = attrs.descriptions || {};
var cellRenderers = attrs.renderCell || {};
while(( item = iterator() ))
rows.push( item );
if( ! rows.length )
return document.createElement('span');
function defaultRenderCell( field, value, node, options ) {
thisB.renderDetailValue( node, '', value, f, '' );
}
var columns = [];
for( var field in rows[0] ) {
(function(field) {
var column = {
label: { id: 'Name'}[field] || Util.ucFirst( field ),
field: field,
renderCell: cellRenderers[field] || defaultRenderCell,
renderHeaderCell: function( contentNode ) {
if( descriptions[field] )
contentNode.title = descriptions[field];
contentNode.appendChild( document.createTextNode( column.label || column.field));
}
};
columns.push( column );
})(field);
}
// create the grid
parent.style.overflow = 'hidden';
parent.style.width = '90%';
var grid = new Grid({
columns: columns,
collection: new MemoryStore({ data: rows })
}, parent );
return parent;
},
_valToString: function( val ) {
if( lang.isArray( val ) ) {
return array.map( val, lang.hitch( this,'_valToString') ).join(' ');
}
else if( typeof val == 'object' ) {
if( 'values' in val )
return this._valToString( val.values );
else
return JSON.stringify( val );
}
return ''+val;
}
});
});