import './jquery_world';
import React from 'react';
import _ from 'underscore';
/**
* Load necessary polyfills.
*/
$.webshims.polyfill('forms');
var Page = React.createClass({
render: function () {
return (
);
},
componentDidMount: function () {
this.refs.dnd.setState({
query: this.refs.form.refs.query
});
}
});
/** Drag n drop widget.
*/
var DnD = React.createClass({
getInitialState: function () {
return {
query: null
};
},
render: function () {
return (
Drop query sequence file here
Overwrite query sequence file
One file at a time please.
Too big a file. Can only do less than 10 MB. >_<
Only FASTA files please.
);
},
componentDidMount: function () {
var self = this;
var FASTA_FORMAT = /^>/;
$(document).ready(function(){
var tgtMarker = $('.dnd-overlay');
var dndError = function (id) {
$('.dnd-error').hide();
$('#' + id + '-notification').show();
tgtMarker.effect('fade', 2500);
};
$(document)
.on('dragenter', function (evt) {
// Do not activate DnD if a modal is active.
if ($.modalActive()) return;
// Based on http://stackoverflow.com/a/8494918/1205465.
// Contrary to what the above link says, the snippet below can't
// distinguish directories from files. We handle that on drop.
var dt = evt.originalEvent.dataTransfer;
var isFile = dt.types && ((dt.types.indexOf && // Chrome and Safari
dt.types.indexOf('Files') != -1) ||
(dt.types.contains && // Firefox
dt.types.contains('application/x-moz-file')));
if (!isFile) { return; }
$('.dnd-error').hide();
tgtMarker.stop(true, true);
tgtMarker.show();
dt.effectAllowed = 'copy';
if (self.state.query.isEmpty()) {
$('.dnd-overlay-overwrite').hide();
$('.dnd-overlay-drop').show('drop', {direction: 'down'}, 'fast');
}
else {
$('.dnd-overlay-drop').hide();
$('.dnd-overlay-overwrite').show('drop', {direction: 'down'}, 'fast');
}
})
.on('dragleave', '.dnd-overlay', function (evt) {
tgtMarker.hide();
$('.dnd-overlay-drop').hide();
$('.dnd-overlay-overwrite').hide();
})
.on('dragover', '.dnd-overlay', function (evt) {
evt.originalEvent.dataTransfer.dropEffect = 'copy';
evt.preventDefault();
})
.on('drop', '.dnd-overlay', function (evt) {
evt.preventDefault();
evt.stopPropagation();
var indicator = $('#sequence-file');
self.state.query.focus();
var files = evt.originalEvent.dataTransfer.files;
if (files.length > 1) {
dndError('dnd-multi');
return;
}
var file = files[0];
if (file.size > 10 * 1048576) {
dndError('dnd-large-file');
return;
}
var reader = new FileReader();
reader.onload = function (e) {
var content = e.target.result;
if (FASTA_FORMAT.test(content)) {
indicator.text(file.name + ' ');
self.state.query.value(content);
tgtMarker.hide();
} else {
// apparently not FASTA
dndError('dnd-format');
}
};
reader.onerror = function (e) {
// Couldn't read. Means dropped stuff wasn't FASTA file.
dndError('dnd-format');
};
reader.readAsText(file);
});
});
}
});
/**
* Search form.
*
* Top level component that initialises and holds all other components, and
* facilitates communication between them.
*/
var Form = React.createClass({
getInitialState: function () {
return { databases: {}, preDefinedOpts: {} };
},
componentDidMount: function () {
/* Fetch data to initialise the search interface from the server. These
* include list of databases to search against, advanced options to
* apply when an algorithm is selected, and a query sequence that
* the user may want to search in the databases.
*/
var search = location.search.split(/\?|&/).filter(Boolean);
var job_id = sessionStorage.getItem('job_id');
if (job_id) {
search.unshift(`job_id=${job_id}`);
}
$.getJSON(`searchdata.json?${search.join('&')}`, function(data) {
/* Update form state (i.e., list of databases and predefined
* advanced options.
*/
this.setState({
databases: data['database'],
preSelectedDbs: data['preSelectedDbs'],
preDefinedOpts: data['options']
});
/* Pre-populate the form with server sent query sequences
* (if any).
*/
if (data['query']) {
this.refs.query.value(data['query']);
}
}.bind(this));
/* Enable submitting form on Cmd+Enter */
$(document).bind('keydown', _.bind(function (e) {
if (e.ctrlKey && e.keyCode === 13 &&
!$('#method').is(':disabled')) {
$(this.getDOMNode()).trigger('submit');
}
}, this));
},
determineBlastMethod: function () {
var database_type = this.databaseType;
var sequence_type = this.sequenceType;
if (this.refs.query.isEmpty()) {
return [];
}
//database type is always known
switch (database_type) {
case 'protein':
switch (sequence_type) {
case undefined:
return ['blastp', 'blastx'];
case 'protein':
return ['blastp'];
case 'nucleotide':
return ['blastx'];
}
break;
case 'nucleotide':
switch (sequence_type) {
case undefined:
return ['tblastn', 'blastn', 'tblastx'];
case 'protein':
return ['tblastn'];
case 'nucleotide':
return ['blastn', 'tblastx'];
}
break;
}
return [];
},
handleSequenceTypeChanged: function (type) {
this.sequenceType = type;
this.refs.button.setState({
hasQuery: !this.refs.query.isEmpty(),
hasDatabases: !!this.databaseType,
methods: this.determineBlastMethod()
});
},
handleDatabaseTypeChanaged: function (type) {
this.databaseType = type;
this.refs.button.setState({
hasQuery: !this.refs.query.isEmpty(),
hasDatabases: !!this.databaseType,
methods: this.determineBlastMethod()
});
},
handleAlgoChanged: function (algo) {
if (this.state.preDefinedOpts.hasOwnProperty(algo)) {
this.refs.opts.setState({
preOpts: this.state.preDefinedOpts[algo].join(' ')
});
}
else {
this.refs.opts.setState({preOpts: ''});
}
},
render: function () {
return (
);
}
});
/**
* Query widget.
*/
var Query = React.createClass({
// Kind of public API. //
/**
* Returns query sequence if no argument is provided (or null or undefined
* is provided as argument). Otherwise, sets query sequenced to the given
* value and returns `this`.
*
* Default/initial state of query sequence is an empty string. Caller must
* explicitly provide empty string as argument to "reset" query sequence.
*/
value: function (val) {
if (val == null) {
// i.e., val is null or undefined
return this.state.value;
}
else {
this.setState({
value: val
});
return this;
}
},
/**
* Clears textarea. Returns `this`.
*
* Clearing textarea also causes it to be focussed.
*/
clear: function () {
return this.value('').focus();
},
/**
* Focuses textarea. Returns `this`.
*/
focus: function () {
this.textarea().focus();
return this;
},
/**
* Returns true if query is absent ('', undefined, null), false otherwise.
*/
isEmpty: function () {
return !this.value();
},
// Internal helpers. //
textarea: function () {
return $(this.refs.textarea.getDOMNode());
},
controls: function () {
return $(this.refs.controls.getDOMNode());
},
handleInput: function (evt) {
this.value(evt.target.value);
},
/**
* Hides or shows 'clear sequence' button.
*
* Rendering the 'clear sequence' button takes into account presence or
* absence of a scrollbar.
*
* Called by `componentDidUpdate`.
*/
hideShowButton: function () {
if (!this.isEmpty()) {
// Calculation below is based on -
// http://chris-spittles.co.uk/jquery-calculate-scrollbar-width/
// FIXME: can reflow be avoided here?
var textareaNode = this.textarea()[0];
var sequenceControlsRight = textareaNode.offsetWidth - textareaNode.clientWidth;
this.controls().css('right', sequenceControlsRight + 17);
this.controls().removeClass('hidden');
}
else {
// FIXME: what are lines 1, 2, & 3 doing here?
this.textarea().parent().removeClass('has-error');
this.$sequenceFile = $('#sequence-file');
this.$sequenceFile.empty();
this.controls().addClass('hidden');
}
},
/**
* Put red border around textarea.
*/
indicateError: function () {
this.textarea().parent().addClass('has-error');
},
/**
* Put normal blue border around textarea.
*/
indicateNormal: function () {
this.textarea().parent().removeClass('has-error');
},
/**
* Returns type of the query sequence (nucleotide, protein, mixed).
*
* Query widget supports executing a callback when the query type changes.
* Components interested in query type should register a callback instead
* of directly calling this method.
*/
type: function () {
var sequences = this.value().split(/>.*/);
var type, tmp;
for (var i = 0; i < sequences.length; i++) {
tmp = this.guessSequenceType(sequences[i]);
// could not guess the sequence type; try the next sequence
if (!tmp) { continue; }
if (!type) {
// successfully guessed the type of atleast one sequence
type = tmp;
}
else if (tmp !== type) {
// user has mixed different type of sequences
return 'mixed';
}
}
return type;
},
/**
* Guesses and returns the type of the given sequence (nucleotide,
* protein).
*/
guessSequenceType: function (sequence) {
// remove 'noisy' characters
sequence = sequence.replace(/[^A-Z]/gi, ''); // non-letter characters
sequence = sequence.replace(/[NX]/gi, ''); // ambiguous characters
// can't determine the type of ultrashort queries
if (sequence.length < 10) {
return undefined;
}
// count the number of putative NA
var putative_NA_count = 0;
for (var i = 0; i < sequence.length; i++) {
if (sequence[i].match(/[ACGTU]/i)) {
putative_NA_count += 1;
}
}
var threshold = 0.9 * sequence.length;
return putative_NA_count > threshold ? 'nucleotide' : 'protein';
},
notify: function (type) {
clearTimeout(this.notification_timeout);
this.indicateNormal();
$('.notifications .active').hide().removeClass('active');
if (type) {
$('#' + type + '-sequence-notification').show('drop', {direction: 'up'}).addClass('active');
this.notification_timeout = setTimeout(function () {
$('.notifications .active').hide('drop', {direction: 'up'}).removeClass('active');
}, 5000);
if (type === 'mixed') {
this.indicateError();
}
}
},
// Lifecycle methods. //
getInitialState: function () {
var input_sequence = $('input#input_sequence').val() || '';
return {
value: input_sequence
};
},
render: function ()
{
return (
);
},
componentDidMount: function () {
$('body').click(function () {
$('.notifications .active').hide('drop', {direction: 'up'}).removeClass('active');
});
},
componentDidUpdate: function () {
this.hideShowButton();
var type = this.type();
if (!type || type !== this._type) {
this._type = type;
this.notify(type);
this.props.onSequenceTypeChanged(type);
}
}
});
var ProteinNotification = React.createClass({
render: function () {
return (
Detected: amino-acid sequence(s).
);
}
});
var NucleotideNotification = React.createClass({
render: function () {
return (
Detected: nucleotide sequence(s).
);
}
});
var MixedNotification = React.createClass({
render: function () {
return (
Detected: mixed nucleotide and amino-acid sequences. We
can't handle that. Please try one sequence at a time.
);
}
});
var Databases = React.createClass({
getInitialState: function () {
return { type: '' };
},
databases: function (category) {
if (!category) {
return this.props.databases.slice();
}
return _.select(this.props.databases,
function (database) {
return database.type === category;
});
},
nselected: function () {
return $('input[name="databases[]"]:checked').length;
},
categories: function () {
return _.uniq(_.map(this.props.databases,
_.iteratee('type'))).sort();
},
handleClick: function (database) {
var type = this.nselected() ? database.type : '';
this.setState({type: type});
},
handleToggle: function (toggleState, type) {
switch (toggleState) {
case '[Select all]':
$(`.${type} .database input:not(:checked)`).click();
break;
case '[Deselect all]':
$(`.${type} .database input:checked`).click();
break;
}
},
render: function () {
return (
);
},
renderDatabase: function (database) {
var disabled = this.state.type && this.state.type !== database.type;
return (
);
},
//shouldComponentUpdate: function (props, state) {
//return !(state.type && state.type === this.state.type);
//},
componentDidUpdate: function () {
if (this.databases() && this.databases().length === 1) {
$('.databases').find('input').prop('checked',true);
this.handleClick(this.databases()[0]);
}
if (this.props.preSelectedDbs) {
var selectors = this.props.preSelectedDbs.map(db => `input[value=${db.id}]`);
$(...selectors).prop('checked',true);
setTimeout(() => this.handleClick(this.props.preSelectedDbs[0]));
}
this.props.onDatabaseTypeChanged(this.state.type);
}
});
var Options = React.createClass({
updateBox: function (evt) {
this.setState({
preOpts: evt.target.value
});
},
getInitialState: function () {
return {
preOpts: ''
};
},
render: function () {
return (
);
}
});
/**
* SearchButton widget.
*/
var SearchButton = React.createClass({
// Internal helpers. //
/**
* Returns jquery wrapped input group.
*/
inputGroup: function () {
return $(React.findDOMNode(this.refs.inputGroup));
},
/**
* Returns jquery wrapped submit button.
*/
submitButton: function () {
return $(React.findDOMNode(this.refs.submitButton));
},
/**
* Initialise tooltip on input group and submit button.
*/
initTooltip: function () {
this.inputGroup().tooltip({
trigger: 'manual',
title: _.bind(function () {
if (!this.state.hasQuery && !this.state.hasDatabases) {
return 'You must enter a query sequence and select one or more databases above before you can run a search!';
}
else if (this.state.hasQuery && !this.state.hasDatabases) {
return 'You must select one or more databases above before you can run a search!';
}
else if (!this.state.hasQuery && this.state.hasDatabases) {
return 'You must enter a query sequence above before you can run a search!';
}
}, this)
});
this.submitButton().tooltip({
title: _.bind(function () {
var title = 'Click to BLAST or press Ctrl+Enter.';
if (this.state.methods.length > 1) {
title += ' Click dropdown button on the right for other' +
' BLAST algorithms that can be used.';
}
return title;
}, this)
});
},
/**
* Show tooltip on input group.
*/
showTooltip: function () {
this.inputGroup()._tooltip('show');
},
/**
* Hide tooltip on input group.
*/
hideTooltip: function () {
this.inputGroup()._tooltip('hide');
},
/**
* Change selected algorithm.
*
* NOTE: Called on click on dropdown menu items.
*/
changeAlgorithm: function (method) {
var methods = this.state.methods.slice();
methods.splice(methods.indexOf(method), 1);
methods.unshift(method);
this.setState({
methods: methods
});
},
/**
* Given, for example 'blastp', returns blastp.
*/
decorate: function(name) {
return name.match(/(.?)(blast)(.?)/).slice(1).map(function (token, _) {
if (token) {
if (token !== 'blast'){
return ({token});
}
else {
return token;
}
}
});
},
// Lifecycle methods. //
getInitialState: function () {
return {
methods: [],
hasQuery: false,
hasDatabases: false
};
},
render: function () {
var methods = this.state.methods;
var method = methods[0];
var multi = methods.length > 1;
return (