Browse Source

Prepares UI, Help and Configuration

Reynold Tan 3 years ago
parent
commit
5c5839b77e

+ 65 - 0
config/install/tripal_blast.settings.yml

@@ -0,0 +1,65 @@
+# @file
+# Tripal Blast module variables/configuration.
+
+# GENERAL CONFIGURATIONS:
+tripal_blast_config_general:
+  path:
+      threads: 1
+  eval: 0.001
+  qrange: 0
+
+# UPLOAD CONFIGURATIONS:
+tripal_blast_config_upload:
+  allow_query: TRUE
+  allow_target: FALSE
+
+# SEQUENCE CONFIGURATIONS:
+tripal_blast_config_sequence:
+  nucleotide: > 
+    >partial lipoxygenase Glyma15g03040
+    TTTCGTATGA GATTAAAATG TGTGAAATTT TGTTTGATAG GACATGGGAA
+    AGGAAAAGTT GGAAAGGCTA CAAATTTAAG AGGACAAGTG TCGTTACCAA
+    CCTTGGGAGC TGGCGAAGAT GCATACGATG TTCATTTTGA ATGGGACAGT
+    GACTTCGGAA TTCCCGGTGC ATTTTACATT AAGAACTTCA TGCAAGTTGA
+    GTTCTATCTC AAGTCTCTAA CTCTCGAAGA CATTCCAAAC CACGGAACCA
+    TTCACTTCGT ATGCAACTCC TGGGTTTACA ACTCAAAATC CTACCATTCT
+    GATCGCATTT TCTTTGCCAA CAATGTAAGC TACTTAAATA CTGTTATACA
+    TTGTCTAACA TCTTGTTAGA GTCTTGCATG ATGTGTACCG TTTATTGTTG
+    TTGTTGAACT TTACCACATG GCATGGATGC AAAAGTTGTT ATACACATAA
+    ATTATAATGC AGACATATCT TCCAAGCGAG ACACCGGCTC CACTTGTCAA
+    GTACAGAGAA GAAGAATTGA AGAATGTAAG AGGGGATGGA ACTGGTGAGC
+    GCAAGGAATG GGATAGGATC TATGATTATG ATGTCTACAA TGACTTGGGC
+    GATCCAGATA AGGGTGAAAA GTATGCACGC CCCGTTCTTG GAGGTTCTGC
+    CTTACCTTAC CCTCGCAGAG GAAGAACCGG AAGAGGAAAA ACTAGAAAAG
+    GTTTCTCACT AGTCACTAAT TTATTACTTT TTAATGTTTG TTTTTAGGCA
+    TCTTTTCTGA TGAAATGTAT ACTTTTGATG TTTTTTTGTT TTAGCATAAC
+    TGAATTAGTA AAGTGTGTTG TGTTCCTTAG AAGTTAGAAA AGTACTAAGT
+    ATAAGGTCTT TGAGTTGTCG TCTTTATCTT AACAGATCCC AACAGTGAGA
+    AGCCCAGTGA TTTTGTTTAC CTTCCGAGAG ATGAAGCATT TGGTCACTTG
+    AAGTCATCAG ATTTTCTCGT TTATGGAATC AAATCAGTGG CTCAAGACGT
+    CTTGCCCGTG TTGACTGATG CGTTTGATGG CAATCTTTTG AGCCTTGAGT
+    TTGATAACTT TGCTGAAGTG CGCAAACTCT ATGAAGGTGG AGTTACACTA
+    CCTACAAACT TTCTTAGCAA GATCGCCCCT ATACCAGTGG TCAAGGAAAT
+    TTTTCGAACT GATGGCGAAC AGTTCCTCAA GTATCCACCA CCTAAAGTGA
+    TGCAGGGTAT GCTACATATT TTGAATATGT AGAATATTAT CAATATACTC
+    CTGTTTTTAT TCAACATATT TAATCACATG GATGAATTTT TGAACTGTTA
+  protein: > 
+    >gi|166477|gb|AAA96434.1| resveratrol synthase [Arachis hypogaea]
+    MVSVSGIRKVQRAEGPATVLAIGTANPPNCIDQSTYADYYFRVTNSEHMTDLKKKFQRICERTQIKNRHM
+    YLTEEILKENPNMCAYKAPSLDAREDMMIREVPRVGKEAATKAIKEWGQPMSKITHLIFCTTSGVALPGV
+    DYELIVLLGLDPCVKRYMMYHQGCFAGGTVLRLAKDLAENNKDARVLIVCSENTAVTFRGPSETDMDSLV
+    GQALFADGAAAIIIGSDPVPEVEKPIFELVSTDQKLVPGSHGAIGGLLREVGLTFYLNKSVPDIISQNIN
+    DALNKAFDPLGISDYNSIFWIAHPGGRAILDQVEQKVNLKPEKMKATRDVLSNYGNMSSACVFFIMDLMR
+    KRSLEEGLKTTGEGLDWGVLFGFGPGLTIETVVLRSVAI
+
+# LARGE JOBS CONFIGURATIONS:
+tripal_blast_config_jobs:
+  max_result: 500
+
+# VISUALIZATION CONFIGURATIONS:
+tripal_blast_config_visualization:
+  cvitjs: FALSE
+
+# NOTIFICATION CONFIGURATIONS
+tripal_blast_config_notification:
+  warning_text:

+ 27 - 0
css/tripal-blast-tabs.css

@@ -0,0 +1,27 @@
+/*
+ * @file
+ * Style Tripal BLAST Admin page:
+ */
+
+#tripal-blast-tabs div ol,
+#tripal-blast-tabs div ul {
+  margin: 20px;
+  padding: 0;
+}
+
+/*! jQuery UI - v1.12.1 - 2021-06-03
+* http://jqueryui.com
+* Includes: core.css, tabs.css
+* Copyright jQuery Foundation and other contributors; Licensed MIT */
+
+.ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:before,.ui-helper-clearfix:after{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0;filter:Alpha(Opacity=0)}.ui-front{z-index:100}.ui-state-disabled{cursor:default!important;pointer-events:none}.ui-icon{display:inline-block;vertical-align:middle;margin-top:-.25em;position:relative;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}.ui-widget-icon-block{left:50%;margin-left:-8px;display:block}.ui-widget-overlay{position:fixed;top:0;left:0;width:100%;height:100%}.ui-tabs{position:relative;padding:.2em}.ui-tabs .ui-tabs-nav{margin:0;padding:.2em .2em 0}.ui-tabs .ui-tabs-nav li{list-style:none;float:left;position:relative;top:0;margin:1px .2em 0 0;border-bottom-width:0;padding:0;white-space:nowrap}.ui-tabs .ui-tabs-nav .ui-tabs-anchor{float:left;padding:.5em 1em;text-decoration:none}.ui-tabs .ui-tabs-nav li.ui-tabs-active{margin-bottom:-1px;padding-bottom:1px}.ui-tabs .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor,.ui-tabs .ui-tabs-nav li.ui-state-disabled .ui-tabs-anchor,.ui-tabs .ui-tabs-nav li.ui-tabs-loading .ui-tabs-anchor{cursor:text}.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor{cursor:pointer}.ui-tabs .ui-tabs-panel{display:block;border-width:0;padding:1em 1.4em;background:none}
+
+#tripal-blast-tabs .ui-tabs-nav {
+  background-color: #F7F7F7;
+  height: 30px;
+}
+
+#tripal-blast-tabs .ui-tabs-active a {
+  background-color: #FFFFFF;
+  border-radius: unset;
+}

+ 1 - 1
css/tripal-blast-ui.css

@@ -1,6 +1,6 @@
 /*
 /*
  * @file
  * @file
- * Theme Tripal BLAST UI page:
+ * Style Tripal BLAST UI page:
  */
  */
  
  
 /** 
 /** 

+ 1769 - 0
js/jquery-tabs.js

@@ -0,0 +1,1769 @@
+/*! jQuery UI - v1.12.1 - 2021-06-03
+* http://jqueryui.com
+* Includes: widget.js, keycode.js, unique-id.js, widgets/tabs.js
+* Copyright jQuery Foundation and other contributors; Licensed MIT */
+
+(function( factory ) {
+	if ( typeof define === "function" && define.amd ) {
+
+		// AMD. Register as an anonymous module.
+		define([ "jquery" ], factory );
+	} else {
+
+		// Browser globals
+		factory( jQuery );
+	}
+}(function( $ ) {
+
+$.ui = $.ui || {};
+
+var version = $.ui.version = "1.12.1";
+
+
+/*!
+ * jQuery UI Widget 1.12.1
+ * http://jqueryui.com
+ *
+ * Copyright jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ */
+
+//>>label: Widget
+//>>group: Core
+//>>description: Provides a factory for creating stateful widgets with a common API.
+//>>docs: http://api.jqueryui.com/jQuery.widget/
+//>>demos: http://jqueryui.com/widget/
+
+
+
+var widgetUuid = 0;
+var widgetSlice = Array.prototype.slice;
+
+$.cleanData = ( function( orig ) {
+	return function( elems ) {
+		var events, elem, i;
+		for ( i = 0; ( elem = elems[ i ] ) != null; i++ ) {
+			try {
+
+				// Only trigger remove when necessary to save time
+				events = $._data( elem, "events" );
+				if ( events && events.remove ) {
+					$( elem ).triggerHandler( "remove" );
+				}
+
+			// Http://bugs.jquery.com/ticket/8235
+			} catch ( e ) {}
+		}
+		orig( elems );
+	};
+} )( $.cleanData );
+
+$.widget = function( name, base, prototype ) {
+	var existingConstructor, constructor, basePrototype;
+
+	// ProxiedPrototype allows the provided prototype to remain unmodified
+	// so that it can be used as a mixin for multiple widgets (#8876)
+	var proxiedPrototype = {};
+
+	var namespace = name.split( "." )[ 0 ];
+	name = name.split( "." )[ 1 ];
+	var fullName = namespace + "-" + name;
+
+	if ( !prototype ) {
+		prototype = base;
+		base = $.Widget;
+	}
+
+	if ( $.isArray( prototype ) ) {
+		prototype = $.extend.apply( null, [ {} ].concat( prototype ) );
+	}
+
+	// Create selector for plugin
+	$.expr[ ":" ][ fullName.toLowerCase() ] = function( elem ) {
+		return !!$.data( elem, fullName );
+	};
+
+	$[ namespace ] = $[ namespace ] || {};
+	existingConstructor = $[ namespace ][ name ];
+	constructor = $[ namespace ][ name ] = function( options, element ) {
+
+		// Allow instantiation without "new" keyword
+		if ( !this._createWidget ) {
+			return new constructor( options, element );
+		}
+
+		// Allow instantiation without initializing for simple inheritance
+		// must use "new" keyword (the code above always passes args)
+		if ( arguments.length ) {
+			this._createWidget( options, element );
+		}
+	};
+
+	// Extend with the existing constructor to carry over any static properties
+	$.extend( constructor, existingConstructor, {
+		version: prototype.version,
+
+		// Copy the object used to create the prototype in case we need to
+		// redefine the widget later
+		_proto: $.extend( {}, prototype ),
+
+		// Track widgets that inherit from this widget in case this widget is
+		// redefined after a widget inherits from it
+		_childConstructors: []
+	} );
+
+	basePrototype = new base();
+
+	// We need to make the options hash a property directly on the new instance
+	// otherwise we'll modify the options hash on the prototype that we're
+	// inheriting from
+	basePrototype.options = $.widget.extend( {}, basePrototype.options );
+	$.each( prototype, function( prop, value ) {
+		if ( !$.isFunction( value ) ) {
+			proxiedPrototype[ prop ] = value;
+			return;
+		}
+		proxiedPrototype[ prop ] = ( function() {
+			function _super() {
+				return base.prototype[ prop ].apply( this, arguments );
+			}
+
+			function _superApply( args ) {
+				return base.prototype[ prop ].apply( this, args );
+			}
+
+			return function() {
+				var __super = this._super;
+				var __superApply = this._superApply;
+				var returnValue;
+
+				this._super = _super;
+				this._superApply = _superApply;
+
+				returnValue = value.apply( this, arguments );
+
+				this._super = __super;
+				this._superApply = __superApply;
+
+				return returnValue;
+			};
+		} )();
+	} );
+	constructor.prototype = $.widget.extend( basePrototype, {
+
+		// TODO: remove support for widgetEventPrefix
+		// always use the name + a colon as the prefix, e.g., draggable:start
+		// don't prefix for widgets that aren't DOM-based
+		widgetEventPrefix: existingConstructor ? ( basePrototype.widgetEventPrefix || name ) : name
+	}, proxiedPrototype, {
+		constructor: constructor,
+		namespace: namespace,
+		widgetName: name,
+		widgetFullName: fullName
+	} );
+
+	// If this widget is being redefined then we need to find all widgets that
+	// are inheriting from it and redefine all of them so that they inherit from
+	// the new version of this widget. We're essentially trying to replace one
+	// level in the prototype chain.
+	if ( existingConstructor ) {
+		$.each( existingConstructor._childConstructors, function( i, child ) {
+			var childPrototype = child.prototype;
+
+			// Redefine the child widget using the same prototype that was
+			// originally used, but inherit from the new version of the base
+			$.widget( childPrototype.namespace + "." + childPrototype.widgetName, constructor,
+				child._proto );
+		} );
+
+		// Remove the list of existing child constructors from the old constructor
+		// so the old child constructors can be garbage collected
+		delete existingConstructor._childConstructors;
+	} else {
+		base._childConstructors.push( constructor );
+	}
+
+	$.widget.bridge( name, constructor );
+
+	return constructor;
+};
+
+$.widget.extend = function( target ) {
+	var input = widgetSlice.call( arguments, 1 );
+	var inputIndex = 0;
+	var inputLength = input.length;
+	var key;
+	var value;
+
+	for ( ; inputIndex < inputLength; inputIndex++ ) {
+		for ( key in input[ inputIndex ] ) {
+			value = input[ inputIndex ][ key ];
+			if ( input[ inputIndex ].hasOwnProperty( key ) && value !== undefined ) {
+
+				// Clone objects
+				if ( $.isPlainObject( value ) ) {
+					target[ key ] = $.isPlainObject( target[ key ] ) ?
+						$.widget.extend( {}, target[ key ], value ) :
+
+						// Don't extend strings, arrays, etc. with objects
+						$.widget.extend( {}, value );
+
+				// Copy everything else by reference
+				} else {
+					target[ key ] = value;
+				}
+			}
+		}
+	}
+	return target;
+};
+
+$.widget.bridge = function( name, object ) {
+	var fullName = object.prototype.widgetFullName || name;
+	$.fn[ name ] = function( options ) {
+		var isMethodCall = typeof options === "string";
+		var args = widgetSlice.call( arguments, 1 );
+		var returnValue = this;
+
+		if ( isMethodCall ) {
+
+			// If this is an empty collection, we need to have the instance method
+			// return undefined instead of the jQuery instance
+			if ( !this.length && options === "instance" ) {
+				returnValue = undefined;
+			} else {
+				this.each( function() {
+					var methodValue;
+					var instance = $.data( this, fullName );
+
+					if ( options === "instance" ) {
+						returnValue = instance;
+						return false;
+					}
+
+					if ( !instance ) {
+						return $.error( "cannot call methods on " + name +
+							" prior to initialization; " +
+							"attempted to call method '" + options + "'" );
+					}
+
+					if ( !$.isFunction( instance[ options ] ) || options.charAt( 0 ) === "_" ) {
+						return $.error( "no such method '" + options + "' for " + name +
+							" widget instance" );
+					}
+
+					methodValue = instance[ options ].apply( instance, args );
+
+					if ( methodValue !== instance && methodValue !== undefined ) {
+						returnValue = methodValue && methodValue.jquery ?
+							returnValue.pushStack( methodValue.get() ) :
+							methodValue;
+						return false;
+					}
+				} );
+			}
+		} else {
+
+			// Allow multiple hashes to be passed on init
+			if ( args.length ) {
+				options = $.widget.extend.apply( null, [ options ].concat( args ) );
+			}
+
+			this.each( function() {
+				var instance = $.data( this, fullName );
+				if ( instance ) {
+					instance.option( options || {} );
+					if ( instance._init ) {
+						instance._init();
+					}
+				} else {
+					$.data( this, fullName, new object( options, this ) );
+				}
+			} );
+		}
+
+		return returnValue;
+	};
+};
+
+$.Widget = function( /* options, element */ ) {};
+$.Widget._childConstructors = [];
+
+$.Widget.prototype = {
+	widgetName: "widget",
+	widgetEventPrefix: "",
+	defaultElement: "<div>",
+
+	options: {
+		classes: {},
+		disabled: false,
+
+		// Callbacks
+		create: null
+	},
+
+	_createWidget: function( options, element ) {
+		element = $( element || this.defaultElement || this )[ 0 ];
+		this.element = $( element );
+		this.uuid = widgetUuid++;
+		this.eventNamespace = "." + this.widgetName + this.uuid;
+
+		this.bindings = $();
+		this.hoverable = $();
+		this.focusable = $();
+		this.classesElementLookup = {};
+
+		if ( element !== this ) {
+			$.data( element, this.widgetFullName, this );
+			this._on( true, this.element, {
+				remove: function( event ) {
+					if ( event.target === element ) {
+						this.destroy();
+					}
+				}
+			} );
+			this.document = $( element.style ?
+
+				// Element within the document
+				element.ownerDocument :
+
+				// Element is window or document
+				element.document || element );
+			this.window = $( this.document[ 0 ].defaultView || this.document[ 0 ].parentWindow );
+		}
+
+		this.options = $.widget.extend( {},
+			this.options,
+			this._getCreateOptions(),
+			options );
+
+		this._create();
+
+		if ( this.options.disabled ) {
+			this._setOptionDisabled( this.options.disabled );
+		}
+
+		this._trigger( "create", null, this._getCreateEventData() );
+		this._init();
+	},
+
+	_getCreateOptions: function() {
+		return {};
+	},
+
+	_getCreateEventData: $.noop,
+
+	_create: $.noop,
+
+	_init: $.noop,
+
+	destroy: function() {
+		var that = this;
+
+		this._destroy();
+		$.each( this.classesElementLookup, function( key, value ) {
+			that._removeClass( value, key );
+		} );
+
+		// We can probably remove the unbind calls in 2.0
+		// all event bindings should go through this._on()
+		this.element
+			.off( this.eventNamespace )
+			.removeData( this.widgetFullName );
+		this.widget()
+			.off( this.eventNamespace )
+			.removeAttr( "aria-disabled" );
+
+		// Clean up events and states
+		this.bindings.off( this.eventNamespace );
+	},
+
+	_destroy: $.noop,
+
+	widget: function() {
+		return this.element;
+	},
+
+	option: function( key, value ) {
+		var options = key;
+		var parts;
+		var curOption;
+		var i;
+
+		if ( arguments.length === 0 ) {
+
+			// Don't return a reference to the internal hash
+			return $.widget.extend( {}, this.options );
+		}
+
+		if ( typeof key === "string" ) {
+
+			// Handle nested keys, e.g., "foo.bar" => { foo: { bar: ___ } }
+			options = {};
+			parts = key.split( "." );
+			key = parts.shift();
+			if ( parts.length ) {
+				curOption = options[ key ] = $.widget.extend( {}, this.options[ key ] );
+				for ( i = 0; i < parts.length - 1; i++ ) {
+					curOption[ parts[ i ] ] = curOption[ parts[ i ] ] || {};
+					curOption = curOption[ parts[ i ] ];
+				}
+				key = parts.pop();
+				if ( arguments.length === 1 ) {
+					return curOption[ key ] === undefined ? null : curOption[ key ];
+				}
+				curOption[ key ] = value;
+			} else {
+				if ( arguments.length === 1 ) {
+					return this.options[ key ] === undefined ? null : this.options[ key ];
+				}
+				options[ key ] = value;
+			}
+		}
+
+		this._setOptions( options );
+
+		return this;
+	},
+
+	_setOptions: function( options ) {
+		var key;
+
+		for ( key in options ) {
+			this._setOption( key, options[ key ] );
+		}
+
+		return this;
+	},
+
+	_setOption: function( key, value ) {
+		if ( key === "classes" ) {
+			this._setOptionClasses( value );
+		}
+
+		this.options[ key ] = value;
+
+		if ( key === "disabled" ) {
+			this._setOptionDisabled( value );
+		}
+
+		return this;
+	},
+
+	_setOptionClasses: function( value ) {
+		var classKey, elements, currentElements;
+
+		for ( classKey in value ) {
+			currentElements = this.classesElementLookup[ classKey ];
+			if ( value[ classKey ] === this.options.classes[ classKey ] ||
+					!currentElements ||
+					!currentElements.length ) {
+				continue;
+			}
+
+			// We are doing this to create a new jQuery object because the _removeClass() call
+			// on the next line is going to destroy the reference to the current elements being
+			// tracked. We need to save a copy of this collection so that we can add the new classes
+			// below.
+			elements = $( currentElements.get() );
+			this._removeClass( currentElements, classKey );
+
+			// We don't use _addClass() here, because that uses this.options.classes
+			// for generating the string of classes. We want to use the value passed in from
+			// _setOption(), this is the new value of the classes option which was passed to
+			// _setOption(). We pass this value directly to _classes().
+			elements.addClass( this._classes( {
+				element: elements,
+				keys: classKey,
+				classes: value,
+				add: true
+			} ) );
+		}
+	},
+
+	_setOptionDisabled: function( value ) {
+		this._toggleClass( this.widget(), this.widgetFullName + "-disabled", null, !!value );
+
+		// If the widget is becoming disabled, then nothing is interactive
+		if ( value ) {
+			this._removeClass( this.hoverable, null, "ui-state-hover" );
+			this._removeClass( this.focusable, null, "ui-state-focus" );
+		}
+	},
+
+	enable: function() {
+		return this._setOptions( { disabled: false } );
+	},
+
+	disable: function() {
+		return this._setOptions( { disabled: true } );
+	},
+
+	_classes: function( options ) {
+		var full = [];
+		var that = this;
+
+		options = $.extend( {
+			element: this.element,
+			classes: this.options.classes || {}
+		}, options );
+
+		function processClassString( classes, checkOption ) {
+			var current, i;
+			for ( i = 0; i < classes.length; i++ ) {
+				current = that.classesElementLookup[ classes[ i ] ] || $();
+				if ( options.add ) {
+					current = $( $.unique( current.get().concat( options.element.get() ) ) );
+				} else {
+					current = $( current.not( options.element ).get() );
+				}
+				that.classesElementLookup[ classes[ i ] ] = current;
+				full.push( classes[ i ] );
+				if ( checkOption && options.classes[ classes[ i ] ] ) {
+					full.push( options.classes[ classes[ i ] ] );
+				}
+			}
+		}
+
+		this._on( options.element, {
+			"remove": "_untrackClassesElement"
+		} );
+
+		if ( options.keys ) {
+			processClassString( options.keys.match( /\S+/g ) || [], true );
+		}
+		if ( options.extra ) {
+			processClassString( options.extra.match( /\S+/g ) || [] );
+		}
+
+		return full.join( " " );
+	},
+
+	_untrackClassesElement: function( event ) {
+		var that = this;
+		$.each( that.classesElementLookup, function( key, value ) {
+			if ( $.inArray( event.target, value ) !== -1 ) {
+				that.classesElementLookup[ key ] = $( value.not( event.target ).get() );
+			}
+		} );
+	},
+
+	_removeClass: function( element, keys, extra ) {
+		return this._toggleClass( element, keys, extra, false );
+	},
+
+	_addClass: function( element, keys, extra ) {
+		return this._toggleClass( element, keys, extra, true );
+	},
+
+	_toggleClass: function( element, keys, extra, add ) {
+		add = ( typeof add === "boolean" ) ? add : extra;
+		var shift = ( typeof element === "string" || element === null ),
+			options = {
+				extra: shift ? keys : extra,
+				keys: shift ? element : keys,
+				element: shift ? this.element : element,
+				add: add
+			};
+		options.element.toggleClass( this._classes( options ), add );
+		return this;
+	},
+
+	_on: function( suppressDisabledCheck, element, handlers ) {
+		var delegateElement;
+		var instance = this;
+
+		// No suppressDisabledCheck flag, shuffle arguments
+		if ( typeof suppressDisabledCheck !== "boolean" ) {
+			handlers = element;
+			element = suppressDisabledCheck;
+			suppressDisabledCheck = false;
+		}
+
+		// No element argument, shuffle and use this.element
+		if ( !handlers ) {
+			handlers = element;
+			element = this.element;
+			delegateElement = this.widget();
+		} else {
+			element = delegateElement = $( element );
+			this.bindings = this.bindings.add( element );
+		}
+
+		$.each( handlers, function( event, handler ) {
+			function handlerProxy() {
+
+				// Allow widgets to customize the disabled handling
+				// - disabled as an array instead of boolean
+				// - disabled class as method for disabling individual parts
+				if ( !suppressDisabledCheck &&
+						( instance.options.disabled === true ||
+						$( this ).hasClass( "ui-state-disabled" ) ) ) {
+					return;
+				}
+				return ( typeof handler === "string" ? instance[ handler ] : handler )
+					.apply( instance, arguments );
+			}
+
+			// Copy the guid so direct unbinding works
+			if ( typeof handler !== "string" ) {
+				handlerProxy.guid = handler.guid =
+					handler.guid || handlerProxy.guid || $.guid++;
+			}
+
+			var match = event.match( /^([\w:-]*)\s*(.*)$/ );
+			var eventName = match[ 1 ] + instance.eventNamespace;
+			var selector = match[ 2 ];
+
+			if ( selector ) {
+				delegateElement.on( eventName, selector, handlerProxy );
+			} else {
+				element.on( eventName, handlerProxy );
+			}
+		} );
+	},
+
+	_off: function( element, eventName ) {
+		eventName = ( eventName || "" ).split( " " ).join( this.eventNamespace + " " ) +
+			this.eventNamespace;
+		element.off( eventName ).off( eventName );
+
+		// Clear the stack to avoid memory leaks (#10056)
+		this.bindings = $( this.bindings.not( element ).get() );
+		this.focusable = $( this.focusable.not( element ).get() );
+		this.hoverable = $( this.hoverable.not( element ).get() );
+	},
+
+	_delay: function( handler, delay ) {
+		function handlerProxy() {
+			return ( typeof handler === "string" ? instance[ handler ] : handler )
+				.apply( instance, arguments );
+		}
+		var instance = this;
+		return setTimeout( handlerProxy, delay || 0 );
+	},
+
+	_hoverable: function( element ) {
+		this.hoverable = this.hoverable.add( element );
+		this._on( element, {
+			mouseenter: function( event ) {
+				this._addClass( $( event.currentTarget ), null, "ui-state-hover" );
+			},
+			mouseleave: function( event ) {
+				this._removeClass( $( event.currentTarget ), null, "ui-state-hover" );
+			}
+		} );
+	},
+
+	_focusable: function( element ) {
+		this.focusable = this.focusable.add( element );
+		this._on( element, {
+			focusin: function( event ) {
+				this._addClass( $( event.currentTarget ), null, "ui-state-focus" );
+			},
+			focusout: function( event ) {
+				this._removeClass( $( event.currentTarget ), null, "ui-state-focus" );
+			}
+		} );
+	},
+
+	_trigger: function( type, event, data ) {
+		var prop, orig;
+		var callback = this.options[ type ];
+
+		data = data || {};
+		event = $.Event( event );
+		event.type = ( type === this.widgetEventPrefix ?
+			type :
+			this.widgetEventPrefix + type ).toLowerCase();
+
+		// The original event may come from any element
+		// so we need to reset the target on the new event
+		event.target = this.element[ 0 ];
+
+		// Copy original event properties over to the new event
+		orig = event.originalEvent;
+		if ( orig ) {
+			for ( prop in orig ) {
+				if ( !( prop in event ) ) {
+					event[ prop ] = orig[ prop ];
+				}
+			}
+		}
+
+		this.element.trigger( event, data );
+		return !( $.isFunction( callback ) &&
+			callback.apply( this.element[ 0 ], [ event ].concat( data ) ) === false ||
+			event.isDefaultPrevented() );
+	}
+};
+
+$.each( { show: "fadeIn", hide: "fadeOut" }, function( method, defaultEffect ) {
+	$.Widget.prototype[ "_" + method ] = function( element, options, callback ) {
+		if ( typeof options === "string" ) {
+			options = { effect: options };
+		}
+
+		var hasOptions;
+		var effectName = !options ?
+			method :
+			options === true || typeof options === "number" ?
+				defaultEffect :
+				options.effect || defaultEffect;
+
+		options = options || {};
+		if ( typeof options === "number" ) {
+			options = { duration: options };
+		}
+
+		hasOptions = !$.isEmptyObject( options );
+		options.complete = callback;
+
+		if ( options.delay ) {
+			element.delay( options.delay );
+		}
+
+		if ( hasOptions && $.effects && $.effects.effect[ effectName ] ) {
+			element[ method ]( options );
+		} else if ( effectName !== method && element[ effectName ] ) {
+			element[ effectName ]( options.duration, options.easing, callback );
+		} else {
+			element.queue( function( next ) {
+				$( this )[ method ]();
+				if ( callback ) {
+					callback.call( element[ 0 ] );
+				}
+				next();
+			} );
+		}
+	};
+} );
+
+var widget = $.widget;
+
+
+/*!
+ * jQuery UI Keycode 1.12.1
+ * http://jqueryui.com
+ *
+ * Copyright jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ */
+
+//>>label: Keycode
+//>>group: Core
+//>>description: Provide keycodes as keynames
+//>>docs: http://api.jqueryui.com/jQuery.ui.keyCode/
+
+
+var keycode = $.ui.keyCode = {
+	BACKSPACE: 8,
+	COMMA: 188,
+	DELETE: 46,
+	DOWN: 40,
+	END: 35,
+	ENTER: 13,
+	ESCAPE: 27,
+	HOME: 36,
+	LEFT: 37,
+	PAGE_DOWN: 34,
+	PAGE_UP: 33,
+	PERIOD: 190,
+	RIGHT: 39,
+	SPACE: 32,
+	TAB: 9,
+	UP: 38
+};
+
+
+/*!
+ * jQuery UI Unique ID 1.12.1
+ * http://jqueryui.com
+ *
+ * Copyright jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ */
+
+//>>label: uniqueId
+//>>group: Core
+//>>description: Functions to generate and remove uniqueId's
+//>>docs: http://api.jqueryui.com/uniqueId/
+
+
+
+var uniqueId = $.fn.extend( {
+	uniqueId: ( function() {
+		var uuid = 0;
+
+		return function() {
+			return this.each( function() {
+				if ( !this.id ) {
+					this.id = "ui-id-" + ( ++uuid );
+				}
+			} );
+		};
+	} )(),
+
+	removeUniqueId: function() {
+		return this.each( function() {
+			if ( /^ui-id-\d+$/.test( this.id ) ) {
+				$( this ).removeAttr( "id" );
+			}
+		} );
+	}
+} );
+
+
+
+
+// Internal use only
+var escapeSelector = $.ui.escapeSelector = ( function() {
+	var selectorEscape = /([!"#$%&'()*+,./:;<=>?@[\]^`{|}~])/g;
+	return function( selector ) {
+		return selector.replace( selectorEscape, "\\$1" );
+	};
+} )();
+
+
+
+var safeActiveElement = $.ui.safeActiveElement = function( document ) {
+	var activeElement;
+
+	// Support: IE 9 only
+	// IE9 throws an "Unspecified error" accessing document.activeElement from an <iframe>
+	try {
+		activeElement = document.activeElement;
+	} catch ( error ) {
+		activeElement = document.body;
+	}
+
+	// Support: IE 9 - 11 only
+	// IE may return null instead of an element
+	// Interestingly, this only seems to occur when NOT in an iframe
+	if ( !activeElement ) {
+		activeElement = document.body;
+	}
+
+	// Support: IE 11 only
+	// IE11 returns a seemingly empty object in some cases when accessing
+	// document.activeElement from an <iframe>
+	if ( !activeElement.nodeName ) {
+		activeElement = document.body;
+	}
+
+	return activeElement;
+};
+
+
+/*!
+ * jQuery UI Tabs 1.12.1
+ * http://jqueryui.com
+ *
+ * Copyright jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ */
+
+//>>label: Tabs
+//>>group: Widgets
+//>>description: Transforms a set of container elements into a tab structure.
+//>>docs: http://api.jqueryui.com/tabs/
+//>>demos: http://jqueryui.com/tabs/
+//>>css.structure: ../../themes/base/core.css
+//>>css.structure: ../../themes/base/tabs.css
+//>>css.theme: ../../themes/base/theme.css
+
+
+
+$.widget( "ui.tabs", {
+	version: "1.12.1",
+	delay: 300,
+	options: {
+		active: null,
+		classes: {
+			"ui-tabs": "ui-corner-all",
+			"ui-tabs-nav": "ui-corner-all",
+			"ui-tabs-panel": "ui-corner-bottom",
+			"ui-tabs-tab": "ui-corner-top"
+		},
+		collapsible: false,
+		event: "click",
+		heightStyle: "content",
+		hide: null,
+		show: null,
+
+		// Callbacks
+		activate: null,
+		beforeActivate: null,
+		beforeLoad: null,
+		load: null
+	},
+
+	_isLocal: ( function() {
+		var rhash = /#.*$/;
+
+		return function( anchor ) {
+			var anchorUrl, locationUrl;
+
+			anchorUrl = anchor.href.replace( rhash, "" );
+			locationUrl = location.href.replace( rhash, "" );
+
+			// Decoding may throw an error if the URL isn't UTF-8 (#9518)
+			try {
+				anchorUrl = decodeURIComponent( anchorUrl );
+			} catch ( error ) {}
+			try {
+				locationUrl = decodeURIComponent( locationUrl );
+			} catch ( error ) {}
+
+			return anchor.hash.length > 1 && anchorUrl === locationUrl;
+		};
+	} )(),
+
+	_create: function() {
+		var that = this,
+			options = this.options;
+
+		this.running = false;
+
+		this._addClass( "ui-tabs", "ui-widget ui-widget-content" );
+		this._toggleClass( "ui-tabs-collapsible", null, options.collapsible );
+
+		this._processTabs();
+		options.active = this._initialActive();
+
+		// Take disabling tabs via class attribute from HTML
+		// into account and update option properly.
+		if ( $.isArray( options.disabled ) ) {
+			options.disabled = $.unique( options.disabled.concat(
+				$.map( this.tabs.filter( ".ui-state-disabled" ), function( li ) {
+					return that.tabs.index( li );
+				} )
+			) ).sort();
+		}
+
+		// Check for length avoids error when initializing empty list
+		if ( this.options.active !== false && this.anchors.length ) {
+			this.active = this._findActive( options.active );
+		} else {
+			this.active = $();
+		}
+
+		this._refresh();
+
+		if ( this.active.length ) {
+			this.load( options.active );
+		}
+	},
+
+	_initialActive: function() {
+		var active = this.options.active,
+			collapsible = this.options.collapsible,
+			locationHash = location.hash.substring( 1 );
+
+		if ( active === null ) {
+
+			// check the fragment identifier in the URL
+			if ( locationHash ) {
+				this.tabs.each( function( i, tab ) {
+					if ( $( tab ).attr( "aria-controls" ) === locationHash ) {
+						active = i;
+						return false;
+					}
+				} );
+			}
+
+			// Check for a tab marked active via a class
+			if ( active === null ) {
+				active = this.tabs.index( this.tabs.filter( ".ui-tabs-active" ) );
+			}
+
+			// No active tab, set to false
+			if ( active === null || active === -1 ) {
+				active = this.tabs.length ? 0 : false;
+			}
+		}
+
+		// Handle numbers: negative, out of range
+		if ( active !== false ) {
+			active = this.tabs.index( this.tabs.eq( active ) );
+			if ( active === -1 ) {
+				active = collapsible ? false : 0;
+			}
+		}
+
+		// Don't allow collapsible: false and active: false
+		if ( !collapsible && active === false && this.anchors.length ) {
+			active = 0;
+		}
+
+		return active;
+	},
+
+	_getCreateEventData: function() {
+		return {
+			tab: this.active,
+			panel: !this.active.length ? $() : this._getPanelForTab( this.active )
+		};
+	},
+
+	_tabKeydown: function( event ) {
+		var focusedTab = $( $.ui.safeActiveElement( this.document[ 0 ] ) ).closest( "li" ),
+			selectedIndex = this.tabs.index( focusedTab ),
+			goingForward = true;
+
+		if ( this._handlePageNav( event ) ) {
+			return;
+		}
+
+		switch ( event.keyCode ) {
+		case $.ui.keyCode.RIGHT:
+		case $.ui.keyCode.DOWN:
+			selectedIndex++;
+			break;
+		case $.ui.keyCode.UP:
+		case $.ui.keyCode.LEFT:
+			goingForward = false;
+			selectedIndex--;
+			break;
+		case $.ui.keyCode.END:
+			selectedIndex = this.anchors.length - 1;
+			break;
+		case $.ui.keyCode.HOME:
+			selectedIndex = 0;
+			break;
+		case $.ui.keyCode.SPACE:
+
+			// Activate only, no collapsing
+			event.preventDefault();
+			clearTimeout( this.activating );
+			this._activate( selectedIndex );
+			return;
+		case $.ui.keyCode.ENTER:
+
+			// Toggle (cancel delayed activation, allow collapsing)
+			event.preventDefault();
+			clearTimeout( this.activating );
+
+			// Determine if we should collapse or activate
+			this._activate( selectedIndex === this.options.active ? false : selectedIndex );
+			return;
+		default:
+			return;
+		}
+
+		// Focus the appropriate tab, based on which key was pressed
+		event.preventDefault();
+		clearTimeout( this.activating );
+		selectedIndex = this._focusNextTab( selectedIndex, goingForward );
+
+		// Navigating with control/command key will prevent automatic activation
+		if ( !event.ctrlKey && !event.metaKey ) {
+
+			// Update aria-selected immediately so that AT think the tab is already selected.
+			// Otherwise AT may confuse the user by stating that they need to activate the tab,
+			// but the tab will already be activated by the time the announcement finishes.
+			focusedTab.attr( "aria-selected", "false" );
+			this.tabs.eq( selectedIndex ).attr( "aria-selected", "true" );
+
+			this.activating = this._delay( function() {
+				this.option( "active", selectedIndex );
+			}, this.delay );
+		}
+	},
+
+	_panelKeydown: function( event ) {
+		if ( this._handlePageNav( event ) ) {
+			return;
+		}
+
+		// Ctrl+up moves focus to the current tab
+		if ( event.ctrlKey && event.keyCode === $.ui.keyCode.UP ) {
+			event.preventDefault();
+			this.active.trigger( "focus" );
+		}
+	},
+
+	// Alt+page up/down moves focus to the previous/next tab (and activates)
+	_handlePageNav: function( event ) {
+		if ( event.altKey && event.keyCode === $.ui.keyCode.PAGE_UP ) {
+			this._activate( this._focusNextTab( this.options.active - 1, false ) );
+			return true;
+		}
+		if ( event.altKey && event.keyCode === $.ui.keyCode.PAGE_DOWN ) {
+			this._activate( this._focusNextTab( this.options.active + 1, true ) );
+			return true;
+		}
+	},
+
+	_findNextTab: function( index, goingForward ) {
+		var lastTabIndex = this.tabs.length - 1;
+
+		function constrain() {
+			if ( index > lastTabIndex ) {
+				index = 0;
+			}
+			if ( index < 0 ) {
+				index = lastTabIndex;
+			}
+			return index;
+		}
+
+		while ( $.inArray( constrain(), this.options.disabled ) !== -1 ) {
+			index = goingForward ? index + 1 : index - 1;
+		}
+
+		return index;
+	},
+
+	_focusNextTab: function( index, goingForward ) {
+		index = this._findNextTab( index, goingForward );
+		this.tabs.eq( index ).trigger( "focus" );
+		return index;
+	},
+
+	_setOption: function( key, value ) {
+		if ( key === "active" ) {
+
+			// _activate() will handle invalid values and update this.options
+			this._activate( value );
+			return;
+		}
+
+		this._super( key, value );
+
+		if ( key === "collapsible" ) {
+			this._toggleClass( "ui-tabs-collapsible", null, value );
+
+			// Setting collapsible: false while collapsed; open first panel
+			if ( !value && this.options.active === false ) {
+				this._activate( 0 );
+			}
+		}
+
+		if ( key === "event" ) {
+			this._setupEvents( value );
+		}
+
+		if ( key === "heightStyle" ) {
+			this._setupHeightStyle( value );
+		}
+	},
+
+	_sanitizeSelector: function( hash ) {
+		return hash ? hash.replace( /[!"$%&'()*+,.\/:;<=>?@\[\]\^`{|}~]/g, "\\$&" ) : "";
+	},
+
+	refresh: function() {
+		var options = this.options,
+			lis = this.tablist.children( ":has(a[href])" );
+
+		// Get disabled tabs from class attribute from HTML
+		// this will get converted to a boolean if needed in _refresh()
+		options.disabled = $.map( lis.filter( ".ui-state-disabled" ), function( tab ) {
+			return lis.index( tab );
+		} );
+
+		this._processTabs();
+
+		// Was collapsed or no tabs
+		if ( options.active === false || !this.anchors.length ) {
+			options.active = false;
+			this.active = $();
+
+		// was active, but active tab is gone
+		} else if ( this.active.length && !$.contains( this.tablist[ 0 ], this.active[ 0 ] ) ) {
+
+			// all remaining tabs are disabled
+			if ( this.tabs.length === options.disabled.length ) {
+				options.active = false;
+				this.active = $();
+
+			// activate previous tab
+			} else {
+				this._activate( this._findNextTab( Math.max( 0, options.active - 1 ), false ) );
+			}
+
+		// was active, active tab still exists
+		} else {
+
+			// make sure active index is correct
+			options.active = this.tabs.index( this.active );
+		}
+
+		this._refresh();
+	},
+
+	_refresh: function() {
+		this._setOptionDisabled( this.options.disabled );
+		this._setupEvents( this.options.event );
+		this._setupHeightStyle( this.options.heightStyle );
+
+		this.tabs.not( this.active ).attr( {
+			"aria-selected": "false",
+			"aria-expanded": "false",
+			tabIndex: -1
+		} );
+		this.panels.not( this._getPanelForTab( this.active ) )
+			.hide()
+			.attr( {
+				"aria-hidden": "true"
+			} );
+
+		// Make sure one tab is in the tab order
+		if ( !this.active.length ) {
+			this.tabs.eq( 0 ).attr( "tabIndex", 0 );
+		} else {
+			this.active
+				.attr( {
+					"aria-selected": "true",
+					"aria-expanded": "true",
+					tabIndex: 0
+				} );
+			this._addClass( this.active, "ui-tabs-active", "ui-state-active" );
+			this._getPanelForTab( this.active )
+				.show()
+				.attr( {
+					"aria-hidden": "false"
+				} );
+		}
+	},
+
+	_processTabs: function() {
+		var that = this,
+			prevTabs = this.tabs,
+			prevAnchors = this.anchors,
+			prevPanels = this.panels;
+
+		this.tablist = this._getList().attr( "role", "tablist" );
+		this._addClass( this.tablist, "ui-tabs-nav",
+			"ui-helper-reset ui-helper-clearfix ui-widget-header" );
+
+		// Prevent users from focusing disabled tabs via click
+		this.tablist
+			.on( "mousedown" + this.eventNamespace, "> li", function( event ) {
+				if ( $( this ).is( ".ui-state-disabled" ) ) {
+					event.preventDefault();
+				}
+			} )
+
+			// Support: IE <9
+			// Preventing the default action in mousedown doesn't prevent IE
+			// from focusing the element, so if the anchor gets focused, blur.
+			// We don't have to worry about focusing the previously focused
+			// element since clicking on a non-focusable element should focus
+			// the body anyway.
+			.on( "focus" + this.eventNamespace, ".ui-tabs-anchor", function() {
+				if ( $( this ).closest( "li" ).is( ".ui-state-disabled" ) ) {
+					this.blur();
+				}
+			} );
+
+		this.tabs = this.tablist.find( "> li:has(a[href])" )
+			.attr( {
+				role: "tab",
+				tabIndex: -1
+			} );
+		this._addClass( this.tabs, "ui-tabs-tab", "ui-state-default" );
+
+		this.anchors = this.tabs.map( function() {
+			return $( "a", this )[ 0 ];
+		} )
+			.attr( {
+				role: "presentation",
+				tabIndex: -1
+			} );
+		this._addClass( this.anchors, "ui-tabs-anchor" );
+
+		this.panels = $();
+
+		this.anchors.each( function( i, anchor ) {
+			var selector, panel, panelId,
+				anchorId = $( anchor ).uniqueId().attr( "id" ),
+				tab = $( anchor ).closest( "li" ),
+				originalAriaControls = tab.attr( "aria-controls" );
+
+			// Inline tab
+			if ( that._isLocal( anchor ) ) {
+				selector = anchor.hash;
+				panelId = selector.substring( 1 );
+				panel = that.element.find( that._sanitizeSelector( selector ) );
+
+			// remote tab
+			} else {
+
+				// If the tab doesn't already have aria-controls,
+				// generate an id by using a throw-away element
+				panelId = tab.attr( "aria-controls" ) || $( {} ).uniqueId()[ 0 ].id;
+				selector = "#" + panelId;
+				panel = that.element.find( selector );
+				if ( !panel.length ) {
+					panel = that._createPanel( panelId );
+					panel.insertAfter( that.panels[ i - 1 ] || that.tablist );
+				}
+				panel.attr( "aria-live", "polite" );
+			}
+
+			if ( panel.length ) {
+				that.panels = that.panels.add( panel );
+			}
+			if ( originalAriaControls ) {
+				tab.data( "ui-tabs-aria-controls", originalAriaControls );
+			}
+			tab.attr( {
+				"aria-controls": panelId,
+				"aria-labelledby": anchorId
+			} );
+			panel.attr( "aria-labelledby", anchorId );
+		} );
+
+		this.panels.attr( "role", "tabpanel" );
+		this._addClass( this.panels, "ui-tabs-panel", "ui-widget-content" );
+
+		// Avoid memory leaks (#10056)
+		if ( prevTabs ) {
+			this._off( prevTabs.not( this.tabs ) );
+			this._off( prevAnchors.not( this.anchors ) );
+			this._off( prevPanels.not( this.panels ) );
+		}
+	},
+
+	// Allow overriding how to find the list for rare usage scenarios (#7715)
+	_getList: function() {
+		return this.tablist || this.element.find( "ol, ul" ).eq( 0 );
+	},
+
+	_createPanel: function( id ) {
+		return $( "<div>" )
+			.attr( "id", id )
+			.data( "ui-tabs-destroy", true );
+	},
+
+	_setOptionDisabled: function( disabled ) {
+		var currentItem, li, i;
+
+		if ( $.isArray( disabled ) ) {
+			if ( !disabled.length ) {
+				disabled = false;
+			} else if ( disabled.length === this.anchors.length ) {
+				disabled = true;
+			}
+		}
+
+		// Disable tabs
+		for ( i = 0; ( li = this.tabs[ i ] ); i++ ) {
+			currentItem = $( li );
+			if ( disabled === true || $.inArray( i, disabled ) !== -1 ) {
+				currentItem.attr( "aria-disabled", "true" );
+				this._addClass( currentItem, null, "ui-state-disabled" );
+			} else {
+				currentItem.removeAttr( "aria-disabled" );
+				this._removeClass( currentItem, null, "ui-state-disabled" );
+			}
+		}
+
+		this.options.disabled = disabled;
+
+		this._toggleClass( this.widget(), this.widgetFullName + "-disabled", null,
+			disabled === true );
+	},
+
+	_setupEvents: function( event ) {
+		var events = {};
+		if ( event ) {
+			$.each( event.split( " " ), function( index, eventName ) {
+				events[ eventName ] = "_eventHandler";
+			} );
+		}
+
+		this._off( this.anchors.add( this.tabs ).add( this.panels ) );
+
+		// Always prevent the default action, even when disabled
+		this._on( true, this.anchors, {
+			click: function( event ) {
+				event.preventDefault();
+			}
+		} );
+		this._on( this.anchors, events );
+		this._on( this.tabs, { keydown: "_tabKeydown" } );
+		this._on( this.panels, { keydown: "_panelKeydown" } );
+
+		this._focusable( this.tabs );
+		this._hoverable( this.tabs );
+	},
+
+	_setupHeightStyle: function( heightStyle ) {
+		var maxHeight,
+			parent = this.element.parent();
+
+		if ( heightStyle === "fill" ) {
+			maxHeight = parent.height();
+			maxHeight -= this.element.outerHeight() - this.element.height();
+
+			this.element.siblings( ":visible" ).each( function() {
+				var elem = $( this ),
+					position = elem.css( "position" );
+
+				if ( position === "absolute" || position === "fixed" ) {
+					return;
+				}
+				maxHeight -= elem.outerHeight( true );
+			} );
+
+			this.element.children().not( this.panels ).each( function() {
+				maxHeight -= $( this ).outerHeight( true );
+			} );
+
+			this.panels.each( function() {
+				$( this ).height( Math.max( 0, maxHeight -
+					$( this ).innerHeight() + $( this ).height() ) );
+			} )
+				.css( "overflow", "auto" );
+		} else if ( heightStyle === "auto" ) {
+			maxHeight = 0;
+			this.panels.each( function() {
+				maxHeight = Math.max( maxHeight, $( this ).height( "" ).height() );
+			} ).height( maxHeight );
+		}
+	},
+
+	_eventHandler: function( event ) {
+		var options = this.options,
+			active = this.active,
+			anchor = $( event.currentTarget ),
+			tab = anchor.closest( "li" ),
+			clickedIsActive = tab[ 0 ] === active[ 0 ],
+			collapsing = clickedIsActive && options.collapsible,
+			toShow = collapsing ? $() : this._getPanelForTab( tab ),
+			toHide = !active.length ? $() : this._getPanelForTab( active ),
+			eventData = {
+				oldTab: active,
+				oldPanel: toHide,
+				newTab: collapsing ? $() : tab,
+				newPanel: toShow
+			};
+
+		event.preventDefault();
+
+		if ( tab.hasClass( "ui-state-disabled" ) ||
+
+				// tab is already loading
+				tab.hasClass( "ui-tabs-loading" ) ||
+
+				// can't switch durning an animation
+				this.running ||
+
+				// click on active header, but not collapsible
+				( clickedIsActive && !options.collapsible ) ||
+
+				// allow canceling activation
+				( this._trigger( "beforeActivate", event, eventData ) === false ) ) {
+			return;
+		}
+
+		options.active = collapsing ? false : this.tabs.index( tab );
+
+		this.active = clickedIsActive ? $() : tab;
+		if ( this.xhr ) {
+			this.xhr.abort();
+		}
+
+		if ( !toHide.length && !toShow.length ) {
+			$.error( "jQuery UI Tabs: Mismatching fragment identifier." );
+		}
+
+		if ( toShow.length ) {
+			this.load( this.tabs.index( tab ), event );
+		}
+		this._toggle( event, eventData );
+	},
+
+	// Handles show/hide for selecting tabs
+	_toggle: function( event, eventData ) {
+		var that = this,
+			toShow = eventData.newPanel,
+			toHide = eventData.oldPanel;
+
+		this.running = true;
+
+		function complete() {
+			that.running = false;
+			that._trigger( "activate", event, eventData );
+		}
+
+		function show() {
+			that._addClass( eventData.newTab.closest( "li" ), "ui-tabs-active", "ui-state-active" );
+
+			if ( toShow.length && that.options.show ) {
+				that._show( toShow, that.options.show, complete );
+			} else {
+				toShow.show();
+				complete();
+			}
+		}
+
+		// Start out by hiding, then showing, then completing
+		if ( toHide.length && this.options.hide ) {
+			this._hide( toHide, this.options.hide, function() {
+				that._removeClass( eventData.oldTab.closest( "li" ),
+					"ui-tabs-active", "ui-state-active" );
+				show();
+			} );
+		} else {
+			this._removeClass( eventData.oldTab.closest( "li" ),
+				"ui-tabs-active", "ui-state-active" );
+			toHide.hide();
+			show();
+		}
+
+		toHide.attr( "aria-hidden", "true" );
+		eventData.oldTab.attr( {
+			"aria-selected": "false",
+			"aria-expanded": "false"
+		} );
+
+		// If we're switching tabs, remove the old tab from the tab order.
+		// If we're opening from collapsed state, remove the previous tab from the tab order.
+		// If we're collapsing, then keep the collapsing tab in the tab order.
+		if ( toShow.length && toHide.length ) {
+			eventData.oldTab.attr( "tabIndex", -1 );
+		} else if ( toShow.length ) {
+			this.tabs.filter( function() {
+				return $( this ).attr( "tabIndex" ) === 0;
+			} )
+				.attr( "tabIndex", -1 );
+		}
+
+		toShow.attr( "aria-hidden", "false" );
+		eventData.newTab.attr( {
+			"aria-selected": "true",
+			"aria-expanded": "true",
+			tabIndex: 0
+		} );
+	},
+
+	_activate: function( index ) {
+		var anchor,
+			active = this._findActive( index );
+
+		// Trying to activate the already active panel
+		if ( active[ 0 ] === this.active[ 0 ] ) {
+			return;
+		}
+
+		// Trying to collapse, simulate a click on the current active header
+		if ( !active.length ) {
+			active = this.active;
+		}
+
+		anchor = active.find( ".ui-tabs-anchor" )[ 0 ];
+		this._eventHandler( {
+			target: anchor,
+			currentTarget: anchor,
+			preventDefault: $.noop
+		} );
+	},
+
+	_findActive: function( index ) {
+		return index === false ? $() : this.tabs.eq( index );
+	},
+
+	_getIndex: function( index ) {
+
+		// meta-function to give users option to provide a href string instead of a numerical index.
+		if ( typeof index === "string" ) {
+			index = this.anchors.index( this.anchors.filter( "[href$='" +
+				$.ui.escapeSelector( index ) + "']" ) );
+		}
+
+		return index;
+	},
+
+	_destroy: function() {
+		if ( this.xhr ) {
+			this.xhr.abort();
+		}
+
+		this.tablist
+			.removeAttr( "role" )
+			.off( this.eventNamespace );
+
+		this.anchors
+			.removeAttr( "role tabIndex" )
+			.removeUniqueId();
+
+		this.tabs.add( this.panels ).each( function() {
+			if ( $.data( this, "ui-tabs-destroy" ) ) {
+				$( this ).remove();
+			} else {
+				$( this ).removeAttr( "role tabIndex " +
+					"aria-live aria-busy aria-selected aria-labelledby aria-hidden aria-expanded" );
+			}
+		} );
+
+		this.tabs.each( function() {
+			var li = $( this ),
+				prev = li.data( "ui-tabs-aria-controls" );
+			if ( prev ) {
+				li
+					.attr( "aria-controls", prev )
+					.removeData( "ui-tabs-aria-controls" );
+			} else {
+				li.removeAttr( "aria-controls" );
+			}
+		} );
+
+		this.panels.show();
+
+		if ( this.options.heightStyle !== "content" ) {
+			this.panels.css( "height", "" );
+		}
+	},
+
+	enable: function( index ) {
+		var disabled = this.options.disabled;
+		if ( disabled === false ) {
+			return;
+		}
+
+		if ( index === undefined ) {
+			disabled = false;
+		} else {
+			index = this._getIndex( index );
+			if ( $.isArray( disabled ) ) {
+				disabled = $.map( disabled, function( num ) {
+					return num !== index ? num : null;
+				} );
+			} else {
+				disabled = $.map( this.tabs, function( li, num ) {
+					return num !== index ? num : null;
+				} );
+			}
+		}
+		this._setOptionDisabled( disabled );
+	},
+
+	disable: function( index ) {
+		var disabled = this.options.disabled;
+		if ( disabled === true ) {
+			return;
+		}
+
+		if ( index === undefined ) {
+			disabled = true;
+		} else {
+			index = this._getIndex( index );
+			if ( $.inArray( index, disabled ) !== -1 ) {
+				return;
+			}
+			if ( $.isArray( disabled ) ) {
+				disabled = $.merge( [ index ], disabled ).sort();
+			} else {
+				disabled = [ index ];
+			}
+		}
+		this._setOptionDisabled( disabled );
+	},
+
+	load: function( index, event ) {
+		index = this._getIndex( index );
+		var that = this,
+			tab = this.tabs.eq( index ),
+			anchor = tab.find( ".ui-tabs-anchor" ),
+			panel = this._getPanelForTab( tab ),
+			eventData = {
+				tab: tab,
+				panel: panel
+			},
+			complete = function( jqXHR, status ) {
+				if ( status === "abort" ) {
+					that.panels.stop( false, true );
+				}
+
+				that._removeClass( tab, "ui-tabs-loading" );
+				panel.removeAttr( "aria-busy" );
+
+				if ( jqXHR === that.xhr ) {
+					delete that.xhr;
+				}
+			};
+
+		// Not remote
+		if ( this._isLocal( anchor[ 0 ] ) ) {
+			return;
+		}
+
+		this.xhr = $.ajax( this._ajaxSettings( anchor, event, eventData ) );
+
+		// Support: jQuery <1.8
+		// jQuery <1.8 returns false if the request is canceled in beforeSend,
+		// but as of 1.8, $.ajax() always returns a jqXHR object.
+		if ( this.xhr && this.xhr.statusText !== "canceled" ) {
+			this._addClass( tab, "ui-tabs-loading" );
+			panel.attr( "aria-busy", "true" );
+
+			this.xhr
+				.done( function( response, status, jqXHR ) {
+
+					// support: jQuery <1.8
+					// http://bugs.jquery.com/ticket/11778
+					setTimeout( function() {
+						panel.html( response );
+						that._trigger( "load", event, eventData );
+
+						complete( jqXHR, status );
+					}, 1 );
+				} )
+				.fail( function( jqXHR, status ) {
+
+					// support: jQuery <1.8
+					// http://bugs.jquery.com/ticket/11778
+					setTimeout( function() {
+						complete( jqXHR, status );
+					}, 1 );
+				} );
+		}
+	},
+
+	_ajaxSettings: function( anchor, event, eventData ) {
+		var that = this;
+		return {
+
+			// Support: IE <11 only
+			// Strip any hash that exists to prevent errors with the Ajax request
+			url: anchor.attr( "href" ).replace( /#.*$/, "" ),
+			beforeSend: function( jqXHR, settings ) {
+				return that._trigger( "beforeLoad", event,
+					$.extend( { jqXHR: jqXHR, ajaxSettings: settings }, eventData ) );
+			}
+		};
+	},
+
+	_getPanelForTab: function( tab ) {
+		var id = $( tab ).attr( "aria-controls" );
+		return this.element.find( this._sanitizeSelector( "#" + id ) );
+	}
+} );
+
+// DEPRECATED
+// TODO: Switch return back to widget declaration at top of file when this is removed
+if ( $.uiBackCompat !== false ) {
+
+	// Backcompat for ui-tab class (now ui-tabs-tab)
+	$.widget( "ui.tabs", $.ui.tabs, {
+		_processTabs: function() {
+			this._superApply( arguments );
+			this._addClass( this.tabs, "ui-tab" );
+		}
+	} );
+}
+
+var widgetsTabs = $.ui.tabs;
+
+
+
+
+}));

+ 12 - 0
js/tripal-blast-admin.js

@@ -0,0 +1,12 @@
+/**
+ * @file
+ * Initialize Tripal Blast Tabs page element.
+ */
+
+// Attach behavior.
+(function($, Drupal){
+  Drupal.behaviors.TripalBlastAdmin = {
+    attach: function (context, settings) {
+      $('#tripal-blast-tabs').tabs();
+
+}}})(jQuery, Drupal);

+ 6 - 17
src/Controller/TripalBlastAdmin.php → src/Controller/TripalBlastHelpController.php

@@ -1,8 +1,7 @@
 <?php
 <?php
 /**
 /**
  * @file 
  * @file 
- * This is the controller for Tripal BLAST configuration and help.
- * Both pages are admin pages.
+ * This is the controller for Tripal BLAST help page.
  */
  */
 
 
 namespace Drupal\tripal_blast\Controller;
 namespace Drupal\tripal_blast\Controller;
@@ -10,22 +9,10 @@ namespace Drupal\tripal_blast\Controller;
 use Drupal\Core\Controller\ControllerBase;
 use Drupal\Core\Controller\ControllerBase;
 
 
 /**
 /**
- * Defines TripalBlastUI class.
+ * Defines TripalBlastHelpController class.
  * 
  * 
  */
  */
-class TripalBlastAdmin extends ControllerBase {
-  /**
-   * Returns a render-able array to create Tripal BLAST configuration page.
-   * @see hook_theme in tripal_blast.module.
-   */
-  public function configuration() {
-    return [
-      // Tripal BLAST Configuration page theme.
-      '#theme' => 'theme-tripal-blast-help',
-      '#attached' => []
-    ];  
-  }
-  
+class TripalBlastHelpController extends ControllerBase {  
   /**
   /**
    * Returns a render-able array to create Tripal BLAST help page.
    * Returns a render-able array to create Tripal BLAST help page.
    * A list of variables (context links presented in the interface) is used
    * A list of variables (context links presented in the interface) is used
@@ -36,7 +23,9 @@ class TripalBlastAdmin extends ControllerBase {
     return [
     return [
       // Tripal BLAST Help page theme.
       // Tripal BLAST Help page theme.
       '#theme' => 'theme-tripal-blast-help',
       '#theme' => 'theme-tripal-blast-help',
-      '#attached' => []
+      '#attached' => [
+        'library' => ['tripal_blast/tripal-blast-admin']
+      ]
     ];  
     ];  
   }
   }
 }
 }

+ 2 - 2
src/Controller/TripalBlastUI.php → src/Controller/TripalBlastUIController.php

@@ -12,14 +12,14 @@ use Drupal\Core\Controller\ControllerBase;
  * Defines TripalBlastUI class.
  * Defines TripalBlastUI class.
  * 
  * 
  */
  */
-class TripalBlastUI extends ControllerBase {
+class TripalBlastUIController extends ControllerBase {
   /**
   /**
    * Returns a render-able array to create Tripal BLAST UI elements.
    * Returns a render-able array to create Tripal BLAST UI elements.
    * A list of variables (context links presented in the interface) is used
    * A list of variables (context links presented in the interface) is used
    * and is defined in the hook_theme implementation of this module.
    * and is defined in the hook_theme implementation of this module.
    * @see hook_theme in tripal_blast.module.
    * @see hook_theme in tripal_blast.module.
    */
    */
-  public function content() {
+  public function ui() {
     return [
     return [
       // Tripal BLAST UI page theme.
       // Tripal BLAST UI page theme.
       '#theme' => 'theme-tripal-blast-ui',
       '#theme' => 'theme-tripal-blast-ui',

+ 243 - 0
src/Form/TripalBlastConfigurationForm.php

@@ -0,0 +1,243 @@
+<?php
+/**
+ * @file 
+ * This is the controller for Tripal BLAST Configuration form. 
+ */
+
+namespace Drupal\tripal_blast\Form;
+
+use Drupal\Core\Form\ConfigFormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Messenger\Messenger;
+
+/**
+ * Defines TripalBlastConfigurationForm class.
+ * Constructs admin pages configuration page.
+ * Page is laid out in tabs/task. 
+ * @see tripal_blast.links.tasks.yml
+ */
+class TripalBlastConfigurationForm extends ConfigFormBase {
+  const SETTINGS = 'tripal_blast.settings';
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'blast_ui_configuration';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getEditableConfigNames() {
+    return [
+      static::SETTINGS,
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   * Build form.
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    // Configuration/module variables.
+    $config = $this->config(static::SETTINGS);
+   
+    //
+    // # GENERAL CONFIGURATIONS:
+    $form['general'] = [
+      '#type' => 'details',
+      '#title' => t('General'),
+      '#open' => TRUE,
+    ];
+  
+      $form['general']['blast_path'] = [  
+        '#type' => 'textfield',
+        '#title' => t('Enter the path of the BLAST program'),
+        '#description' => t('You can ignore if your $PATH variable is set. 
+          Otherwise, enter the absoulte path to bin folder. For example, /opt/blast/2.2.29+/bin/'),
+        '#default_value' => $config->get('tripal_blast_config_general.path')
+      ];
+
+      $form['general']['blast_threads'] = [
+        '#type' => 'textfield',
+        '#title' => t('Enter the number of CPU threads to use in blast search.'),
+        '#description' => t('You can increase the number to reduce the search time. 
+          Before you increase, please check your hardware configurations. 
+          A value of one(1) can result in a slower search for some programs eg. tblastn.'),
+        '#default_value' => $config->get('tripal_blast_config_general.threads')
+      ];
+    
+      $form['general']['eVal'] = [
+        '#type' => 'textfield',
+        '#title' => t('Default e-value (Expected Threshold)'),
+        '#description' => t('Expected number of chance matches in a random model. 
+          This number should be give in a decimal format.'),
+        '#default_value' => $config->get('tripal_blast_config_general.eval')
+      ];
+    
+      $form['general']['qRange'] = [
+        '#type' => 'textfield',
+        '#title' => t('Default max matches in a query range'),
+        '#description' => t('Limit the number of matches to a query range. 
+          This option is useful if many strong matches to one part of a query may prevent 
+          BLAST from presenting weaker matches to another part of the query.'),
+        '#default_value' => $config->get('tripal_blast_config_general.qrange')
+      ];
+
+    
+    //
+    // # FILE UPLOAD CONFIGURATIONS:  
+    $form['file_upload'] = [
+      '#type' => 'details',
+      '#open' => FALSE,
+      '#title' => t('Allow File Upload'),
+      '#description' => t('The following options allow you to control whether your users can
+        upload files for the query or target respectively. The ability to upload files allows
+        them to more conviently BLAST large sets of sequences. However, the size of the
+        files could be problematic, storage-wise, on your server.<br />')
+    ];
+
+      $form['file_upload']['query_upload'] = [
+        '#type' => 'checkbox',
+        '#title' => t('Enable Query Sequence Upload'),
+        '#description' => t('When checked, a query file upload field will be available on BLAST request forms.'),
+        '#default_value' => $config->get('tripal_blast_config_upload.allow_query')
+      ];
+    
+      $form['file_upload']['target_upload'] = [
+        '#type' => 'checkbox',
+        '#title' => 'Enable Target Sequence Upload',
+        '#description' => 'When checked, a target file upload field will be available on BLAST request forms.',
+        '#default_value' => $config->get('tripal_blast_config_upload.allow_target')
+      ];
+
+    
+    //
+    // # SEQUENCE CONFIGURATIONS:  
+    $form['example_sequence'] = [
+      '#type' => 'details',
+      '#open' => FALSE,
+      '#title' => t('Set Example Sequences'),
+      '#description' => t('There is the ability to show example sequences built-in to the various 
+        BLAST forms. Use the following fields to set these example sequences. 
+        This allows you to provide more relevant examples to your users.
+        More information: <a href="@fasta-format-url" target="_blank">FASTA format</a>.',
+        ['@fasta-format-url' => 'https://www.ncbi.nlm.nih.gov/BLAST/blastcgihelp.shtml'])
+      ];
+
+      $form['example_sequence']['nucleotide_example'] = [
+        '#type' => 'textarea',
+        '#title' => t('Nucleotide Example'),
+        '#description' => t('Enter a complete nucleotide FASTA record including the header.'),
+        '#default_value' => $config->get('tripal_blast_config_sequence.nucleotide')
+      ];
+
+      $form['example_sequence']['protein_example'] = [
+        '#type' => 'textarea',
+        '#title' => 'Protein Example',
+        '#description' => t('Enter a complete protein FASTA record including the header.'),
+        '#default_value' => $config->get('tripal_blast_config_sequence.protein')
+      ];
+
+    
+    //
+    // # JOBS CONFIGURATIONS:  
+    $form['protection'] = [
+      '#type' => 'details',
+      '#open' => FALSE,
+      '#title' => t('Protect against large jobs'),
+      '#description' => t('Depending on the size and nature of your target databases, 
+        you may wish to constrain use of this module.'),
+    ];
+
+      $form['protection']['max_results_displayed'] = [
+        '#type' => 'textfield',
+        '#title' => t('Maximum number of results to show on report page'),
+        '#description' => 'If there are more hits that this, the user is 
+          able to download but not visualize the results.',
+        '#default_value' => $config->get('tripal_blast_config_jobs.max_result')
+      ];
+
+
+    //
+    // # VISUALIZATION CONFIGURATIONS:
+    // If this is set to TRUE (allow civit visualization), set this fieldset/details
+    // to open = TRUE (expanded).
+    $config_cvitjs = $config->get('tripal_blast_config_visualization.civitjs');
+
+    $form['visualization'] = [
+      '#type' => 'details',
+      '#open' => $config_cvitjs,
+      '#title' => t('Enable and configure genome visualization'),
+      '#description' => t('The JavaScript program CViTjs enables users to see BLAST hits on an
+        entire genome assembly. See the help tab for information on how to download and set up CViTjs')
+    ];
+      
+      $absolute_cvitjs_data_path = DRUPAL_ROOT . '/sites/all/libraries/cvitjs/data';
+      $form['visualization']['explanation'] = [
+        '#type' => 'inline_template',
+        '#template' => '
+          <div role="contentinfo" aria-label="Warning message" class="messages messages--warning">
+            <div role="alert">
+              <h2 class="visually-hidden">Warning message</h2>
+              ViTjs is only applicable for genome BLAST targets. After it is enabled here, 
+              CViTjs will need to be enabled for each applicable BLAST target node.
+            </div>
+          </div>
+          <div role="contentinfo" aria-label="Status message" class="messages messages--status">
+            <div role="alert">
+              <h2 class="visually-hidden">Warning message</h2>              
+              <strong>CViTjs Data Location: ' . $absolute_cvitjs_data_path . '</strong>
+              <br />The GFF3 and Genome Target-specific CViTjs configuration files should be located
+              at the above system path. Feel free to organize this directory further.
+              See the "Help" tab for more information.
+            </div>
+          </div>'
+      ];
+
+      $form['visualization']['cvitjs_enabled'] = [
+        '#type' => 'checkbox',
+        '#title' => t('Enable CViTjs'),
+        '#description' => t('When checked, CViTjs will be enabled.'),
+        '#default_value' => $config_cvitjs,
+      ];
+    
+      // Get CViTjs confuration text, if possible.
+      // @TODO blast_ui_get_cvit_conf_text() goes in '' the condition below.
+      if (!$default_value = '') {
+        $default_value = 'Unable to get CViTjs configuration information. You will need to enable CViTjs and set and save the path to CViTjs before you can edit the CViTjs configuration text.';
+
+        $disabled = true;
+      }
+      else {
+        $disabled = false;
+      }
+
+      $description = t('This is the contents of the file that defines data directories and 
+        backbone GFF files for each genome assembly target. It is named
+        cvit.conf and is in the root directory for the CViTjs javascript code.
+        This is NOT the config file that is used to build the display for each
+        individual genome. See the help tab for more information about
+        configuration files.');
+
+      $form['visualization']['cvitjs_config'] = [
+        '#type' => 'textarea',
+        '#title' => t('CViTjs configuration'),
+        '#description' => $description,
+        '#default_value' => $default_value,
+        '#rows' => 10,
+        '#disabled' => $disabled,
+      ];
+
+    return parent::buildForm($form, $form_state);
+  }
+  /**
+   * {@inheritdoc}
+   * Save configuration.
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+
+    return parent::submitForm($form, $form_state);
+  }
+}

+ 169 - 157
templates/template-tripal-blast-help.html.twig

@@ -5,169 +5,181 @@
  */
  */
 #}
 #}
 
 
+{{ attach_library('tripal_blast/tripal-blast-admin') }}
+
 <h3>Tripal BLAST Module Description</h3>
 <h3>Tripal BLAST Module Description</h3>
 <p>This module provides a basic interface to allow your users to utilize your server's NCBI BLAST+.</p>
 <p>This module provides a basic interface to allow your users to utilize your server's NCBI BLAST+.</p>
 
 
-<p>
-  <a href="#setup">Setup</a> | 
-  <a href="#function">Functionality</a> | 
-  <a href="#protection">Large jobs | 
-  <a href="#genomeview">Genome visualization</a>
-</p>
-
-<a name="setup"></a>
-<h3><b>Setup Instructions</b></h3>
-<ol>
-  <li>
-    Install NCBI BLAST+ on your server (Tested with 2.2.26+). There is a
-    <a href="https://launchpad.net/ubuntu/+source/ncbi-blast+">package available
-    for Ubuntu</a> to ease installation. Optionally you can set the path to your
-    BLAST executable: {{ context_links['link_config'] }} Page.
-  </li>
-  <li>
-    Optionally, create Tripal External Database References to allow you to link
-    the records in your BLAST database to further information. To do this simply
-    go to {{ context_links['link_dbadd'] }} and make sure to fill in the Database
-    prefix which will be concatenated with the record IDs in your BLAST database
-    to determine the link-out to additional information. Note that a regular
-    expression can be used when creating the BLAST database to indicate what the
-    ID is.
-  </li>
-  <li>
-    Create "BLAST Database" {{ context_links['link_nodeadd'] }} nodes for each dataset you want to make available for your users to BLAST
-    against. BLAST databases should first be created using the command-line
-    <code>makeblastdb</code> program with the <code>-parse_seqids</code> flag.
-  </li>
-  <li>
-    It's recommended that you also install the <a href="http://drupal.org/project/tripal_daemon">Tripal Job Daemon</a>
-    to manage BLAST jobs and ensure they are run soon after being submitted by the
-    user. Without this additional module, administrators will have to execute the
-    tripal jobs either manually or through use of cron jobs.
-  </li>
-</ol>
+<div id="tripal-blast-tabs">
+  <ul>
+    <li><a href="#tab-setup">Setup</a></li>
+    <li><a href="#tab-function">Functionality</a></li> 
+    <li><a href="#tab-jobs">Large jobs</a></li> 
+    <li><a href="#tab-visual">Genome visualization</a></li>
+  </ul>
+  
+  <div id="tab-setup">
+    <h3><b>Setup Instructions</b></h3>
+    <ol>
+      <li>
+        Install NCBI BLAST+ on your server (Tested with 2.2.26+). There is a
+        <a href="https://launchpad.net/ubuntu/+source/ncbi-blast+">package available
+        for Ubuntu</a> to ease installation. Optionally you can set the path to your
+        BLAST executable: {{ context_links['link_config'] }} Page.
+      </li>
+      <li>
+        Optionally, create Tripal External Database References to allow you to link
+        the records in your BLAST database to further information. To do this simply
+        go to {{ context_links['link_dbadd'] }} and make sure to fill in the Database
+        prefix which will be concatenated with the record IDs in your BLAST database
+        to determine the link-out to additional information. Note that a regular
+        expression can be used when creating the BLAST database to indicate what the
+        ID is.
+      </li>
+      <li>
+        Create "BLAST Database" {{ context_links['link_nodeadd'] }} nodes for each dataset you want to make available for your users to BLAST
+        against. BLAST databases should first be created using the command-line
+        <code>makeblastdb</code> program with the <code>-parse_seqids</code> flag.
+      </li>
+      <li>
+        It's recommended that you also install the <a href="http://drupal.org/project/tripal_daemon">Tripal Job Daemon</a>
+        to manage BLAST jobs and ensure they are run soon after being submitted by the
+        user. Without this additional module, administrators will have to execute the
+        tripal jobs either manually or through use of cron jobs.
+      </li>
+    </ol>
+  </div>
 
 
-<a name="function"></a>
-&mdash;
-<h3><b>Highlighted Functionality</b></h3>
-<ul>
-  <li>Supports {{ context_links['link_blastn'] }},
-    {{ context_links['link_blastx'] }},
-    {{ context_links['blastp'] }} and
-    {{ context_links['tblastx'] }} with separate forms depending upon the database/query type.
-  </li>
-  <li>
-    Simple interface allowing users to paste or upload a query sequence and then
-    select from available databases. Additionally, a FASTA file can be uploaded
-    for use as a database to BLAST against (this functionality can be disabled).
-  </li>
-  <li>
-    Tabular Results listing with alignment information and multiple download
-    formats (HTML, TSV, XML) available.
-  </li>
-  <li>
-    Completely integrated with Tripal Jobs: {{ context_links['link_jobs'] }}
-    providing administrators with a way to track BLAST jobs and ensuring long
-    running BLASTs will not cause page time-outs
-  </li>
-  <li>
-    BLAST databases are made available to the module by creating Drupal Pages: {{ context_link['link_nodeadd'] }}
-    describing them. This allows administrators to use the Drupal Field API to add any information they want to these pages:
-    {{ context_links['link_dbfields'] }}
-  </li>
-  <li>
-    BLAST database records can be linked to an external source with more
-    information (ie: NCBI) per BLAST database.
-  </li>
-</ul>
+  <div id="tab-function">
+    <h3><b>Highlighted Functionality</b></h3>
+    <ul>
+      <li>Supports {{ context_links['link_blastn'] }},
+        {{ context_links['link_blastx'] }},
+        {{ context_links['blastp'] }} and
+        {{ context_links['tblastx'] }} with separate forms depending upon the database/query type.
+      </li>
+      <li>
+        Simple interface allowing users to paste or upload a query sequence and then
+        select from available databases. Additionally, a FASTA file can be uploaded
+        for use as a database to BLAST against (this functionality can be disabled).
+      </li>
+      <li>
+        Tabular Results listing with alignment information and multiple download
+        formats (HTML, TSV, XML) available.
+      </li>
+      <li>
+        Completely integrated with Tripal Jobs: {{ context_links['link_jobs'] }}
+        providing administrators with a way to track BLAST jobs and ensuring long
+        running BLASTs will not cause page time-outs
+      </li>
+      <li>
+        BLAST databases are made available to the module by creating Drupal Pages: {{ context_link['link_nodeadd'] }}
+        describing them. This allows administrators to use the Drupal Field API to add any information they want to these pages:
+        {{ context_links['link_dbfields'] }}
+      </li>
+      <li>
+        BLAST database records can be linked to an external source with more
+        information (ie: NCBI) per BLAST database.
+      </li>
+    </ul>
+  </div>
 
 
-<a name="protection"</a></a>
-&mdash;
-<h3><b>Protection Against Large Jobs</b></h3>
-Depending on the size and nature of your target databases, you may wish to constrain use
-of this module.
-<ol>
-  <li>Limit the number of results displayed via admin page. The recommended number is 500.</li>
-  <li>
-    Limit the maximum upload file size in php settings. This is less useful because some
-    very large queries may be manageable, and others not.
-  </li>
-  <li>
-    Repeat-mask your targets, or provide repeat-masked versions. Note that some
-    researchers may be looking for repeats, so this may limit the usefulness of the BLAST
-    service.
-  </li>
-</ol>
+  <div id="tab-jobs">
+    <h3><b>Protection Against Large Jobs</b></h3>
+    Depending on the size and nature of your target databases, you may wish to constrain use
+    of this module.
+    <ol>
+      <li>Limit the number of results displayed via admin page. The recommended number is 500.</li>
+      <li>
+        Limit the maximum upload file size in php settings. This is less useful because some
+        very large queries may be manageable, and others not.
+      </li>
+      <li>
+        Repeat-mask your targets, or provide repeat-masked versions. Note that some
+        researchers may be looking for repeats, so this may limit the usefulness of the BLAST
+        service.
+      </li>
+    </ol>
+  </div>
+  
+  <div id="tab-visual">
+    <h3><b>Whole Genome Visualization</b></h3>
+    This module can be configured to use
+    <a href="https://github.com/LegumeFederation/cvitjs">CViTjs</a> to display BLAST hits on
+    a genome image.
 
 
-<a name="genomeview"></a>
-&mdash;
-<h3><b>Whole Genome Visualization</b></h3>
-This module can be configured to use
-<a href="https://github.com/LegumeFederation/cvitjs">CViTjs</a> to display BLAST hits on
-a genome image.
+    <h4>CViTjs Setup</h4>
+    <ol>
+      <li>
+        <a href="https://github.com/LegumeFederation/cvitjs">Download CViTjs</a> and copy
+        the code to your webserver. It needs to be placed in <code>[your drupal root]/sites/all/libraries</code>. To download, execute
+        the git command inside the <code>libraries/</code> directory:<br>
+        <code>git clone https://github.com/LegumeFederation/cvitjs.git</code>
+      </li>
+      <li>
+        CViTjs will have a config file in its root directory named cvit.conf. This file
+        provides information for whole genome visualization for each genome BLAST target.
+        <b>Make sure the config file can be edited by your web server.</b>
+      </li>
+      <li>
+        Enable CViTjs from the BLAST module administration page.
+      </li>
+      <li>
+        Edit the configuration file to define each genome target. These will look like:
+        <pre>
+          [data.Cajanus cajan - genome]
+          conf = data/cajca/cajca.conf
+          defaultData = data/cajca/cajca.gff
+        </pre>
+        
+        Where:<br>
+        <ul>
+          <li>the section name, "data.Cajanus cajan - genome", consists of "data." followed
+            by the name of the BLAST target node,</li>
+          <li>the file "cajca.conf" is a cvit configuration file which describes how to draw the
+            chromosomes and BLAST hits on the <i>Cajanus cajan</i> genome,</li>
+          <li>and the file "cajca.gff" is a GFF3 file that describes the <i>Cajanus cajan</i>
+            chromosomes.</li>
+        </ul>
+        
+        At the top of the configuration file there must be a [general] section that defines
+        the default data set. For example:
+        <pre>
+          [general]
+          data_default = data.Cajanus cajan - genome
+        </pre>
+      </li>
+      <li>
+        Edit the nodes for each genome target (nodes of type "BLAST Database") and enable whole
+        genome visualization. Remember that the names listed in the CViTjs config file must
+        match the BLAST node name. In the example above, the BLAST database node for the
+        <i>Cajanus cajan</i> genome assembly is named "Cajanus cajan - genome"
+      </li>
+    </ol>
 
 
-<h4>CViTjs Setup</h4>
-<ol>
-  <li>
-    <a href="https://github.com/LegumeFederation/cvitjs">Download CViTjs</a> and copy
-    the code to your webserver. It needs to be placed in <code>[your drupal root]/sites/all/libraries</code>. To download, execute
-    the git command inside the <code>libraries/</code> directory:<br>
-    <code>git clone https://github.com/LegumeFederation/cvitjs.git</code>
-  </li>
-  <li>
-    CViTjs will have a config file in its root directory named cvit.conf. This file
-    provides information for whole genome visualization for each genome BLAST target.
-    <b>Make sure the config file can be edited by your web server.</b>
-  </li>
-  <li>
-    Enable CViTjs from the BLAST module administration page.
-  </li>
-  <li>
-    Edit the configuration file to define each genome target. These will look like:
-    <pre>
-[data.Cajanus cajan - genome]
-conf = data/cajca/cajca.conf
-defaultData = data/cajca/cajca.gff</pre>
-    Where:<br>
+    <h4>Notes</h4>
     <ul>
     <ul>
-      <li>the section name, "data.Cajanus cajan - genome", consists of "data." followed
-          by the name of the BLAST target node,</li>
-      <li>the file "cajca.conf" is a cvit configuration file which describes how to draw the
-          chromosomes and BLAST hits on the <i>Cajanus cajan</i> genome,</li>
-      <li>and the file "cajca.gff" is a GFF3 file that describes the <i>Cajanus cajan</i>
-          chromosomes.</li>
+    <li>The .conf file for each genome can be modified to suit your needs and tastes. See the
+      sample configuration file, <code>data/test1/test1.conf</code>, and the CViTjs
+      <a href="https://github.com/LegumeFederation/cvitjs#using-cvitjs">documentation</a>.</li>
+    <li>Each blast target CViTjs configuration file must define how to visualize blast hits or you will not see them.
+      <pre>
+        [blast]
+        feature = BLASTRESULT:match_part
+        glyph   = position
+        shape = rect
+        color   = #FF00FF
+        width = 5
+      </pre>
+    </li>
+    <li>You will have to put the target-specific conf and gff files (e.g. cajca.conf and
+      cjca.gff) on your web server, in the directory, <code>sites/all/libraries/cvitjs/data</code>. You may
+      choose to group files for each genome into subdirectories, for example,
+      <code>sites/all/libraries/cvitjs/data/cajca</code>.</li>
+    <li>It is important to make sure that cvit.conf points to the correct data directory and the
+      correct .gff and .conf files for the genome in question. For more information about how to
+      create the .gff file, see the
+      <a href="https://github.com/LegumeFederation/cvitjs#how-to">documentation</a>.</li>
     </ul>
     </ul>
-    At the top of the configuration file there must be a [general] section that defines
-    the default data set. For example:
-    <pre>
-[general]
-data_default = data.Cajanus cajan - genome</pre>
-  </li>
-  <li>
-    Edit the nodes for each genome target (nodes of type "BLAST Database") and enable whole
-    genome visualization. Remember that the names listed in the CViTjs config file must
-    match the BLAST node name. In the example above, the BLAST database node for the
-    <i>Cajanus cajan</i> genome assembly is named "Cajanus cajan - genome"
-  </li>
-</ol>
-
-<h4>Notes</h4>
-<ul>
-<li>The .conf file for each genome can be modified to suit your needs and tastes. See the
-  sample configuration file, <code>data/test1/test1.conf</code>, and the CViTjs
-  <a href="https://github.com/LegumeFederation/cvitjs#using-cvitjs">documentation</a>.</li>
-<li>Each blast target CViTjs configuration file must define how to visualize blast hits or you will not see them.
-  <pre>[blast]
-feature = BLASTRESULT:match_part
-glyph   = position
-shape = rect
-color   = #FF00FF
-width = 5</pre></li>
-<li>You will have to put the target-specific conf and gff files (e.g. cajca.conf and
-  cjca.gff) on your web server, in the directory, <code>sites/all/libraries/cvitjs/data</code>. You may
-  choose to group files for each genome into subdirectories, for example,
-  <code>sites/all/libraries/cvitjs/data/cajca</code>.</li>
-<li>It is important to make sure that cvit.conf points to the correct data directory and the
-  correct .gff and .conf files for the genome in question. For more information about how to
-  create the .gff file, see the
-  <a href="https://github.com/LegumeFederation/cvitjs#how-to">documentation</a>.</li>
-</ul>
+  </div>
+</div>

+ 18 - 2
tripal_blast.libraries.yml

@@ -4,7 +4,7 @@
 # Library used in UI page to generate accordion or collapsible
 # Library used in UI page to generate accordion or collapsible
 # container element showing BLAST query type options.
 # container element showing BLAST query type options.
 tripal-blast-ui:
 tripal-blast-ui:
-  version: 1.12.1
+  version: 1.x
   js:
   js:
     js/jquery-accordion.js: {}
     js/jquery-accordion.js: {}
     js/tripal-blast-ui.js: {}
     js/tripal-blast-ui.js: {}
@@ -15,4 +15,20 @@ tripal-blast-ui:
     - jquery_ui/core
     - jquery_ui/core
     - jquery_ui/widget
     - jquery_ui/widget
     - core/jquery
     - core/jquery
-    - core/jquery.once
+    - core/jquery.once
+
+# Library used in Admin page to generate content tabs
+# element showing BLAST configuration help topics.
+tripal-blast-admin:
+  version: 1.x
+  js:
+    js/jquery-tabs.js: {}
+    js/tripal-blast-admin.js: {}
+  css:
+    theme:
+      css/tripal-blast-tabs.css: {}
+  dependencies:
+    - jquery_ui/core
+    - jquery_ui/widget
+    - core/jquery
+    - core/jquery.once    

+ 4 - 4
tripal_blast.links.task.yml

@@ -8,17 +8,17 @@
 # General Configuration Tab.
 # General Configuration Tab.
 # Routes to configuration page of this module.
 # Routes to configuration page of this module.
 # /extension/tripal_blast/configuration (is the default tab/task).
 # /extension/tripal_blast/configuration (is the default tab/task).
-tripal_ui.tab1:
+tripal_blast.tab1:
   title: 'Tripal BLAST: Configuration'
   title: 'Tripal BLAST: Configuration'
   route_name: tripal_blast.configuration
   route_name: tripal_blast.configuration
-  base_route: tripal_blast.configuratinon
+  base_route: tripal_blast.configuration
   weight: 0
   weight: 0
 
 
 # Tripal BLAST Help Tab.
 # Tripal BLAST Help Tab.
 # Routes to help page of this module.
 # Routes to help page of this module.
 # /extension/tripal_blast/help
 # /extension/tripal_blast/help
-tripal_ui.tab2:
+tripal_blast.tab2:
   title: 'Tripal BLAST: Help'
   title: 'Tripal BLAST: Help'
-  route_name: tripal_blast.configuration
+  route_name: tripal_blast.help
   base_route: tripal_blast.configuration
   base_route: tripal_blast.configuration
   weight: 1 
   weight: 1 

+ 4 - 4
tripal_blast.routing.yml

@@ -8,7 +8,7 @@ tripal_blast.blast_ui:
   path: 'blast'
   path: 'blast'
   defaults:
   defaults:
     _title: 'Tripal BLAST'
     _title: 'Tripal BLAST'
-    _controller: '\Drupal\tripal_blast\Controller\TripalBlastUI::content'
+    _controller: '\Drupal\tripal_blast\Controller\TripalBlastUIController::ui'
   requirements:
   requirements:
     _permission: 'administer tripal'
     _permission: 'administer tripal'
 
 
@@ -78,14 +78,14 @@ tripal_blast.configuration:
   path: '/admin/tripal/extension/tripal_blast/configuration'
   path: '/admin/tripal/extension/tripal_blast/configuration'
   defaults:
   defaults:
     _title: 'Tripal Blast: Configuration'
     _title: 'Tripal Blast: Configuration'
-    #_controller: '\Drupal\tripal_blast\Controller\TripalBlastAdmin::configuration'
+    _form: '\Drupal\tripal_blast\Form\TripalBlastConfigurationForm'
   requirements:
   requirements:
     _permission: 'access content'
     _permission: 'access content'
 
 
 tripal_blast.help:
 tripal_blast.help:
-  path: '/admin/tripal/extension/tripal_blast/help'
+  path: '/admin/tripal/extension/tripal_blast/configuration/help'
   defaults:
   defaults:
     _title: 'Tripal Blast: Help'
     _title: 'Tripal Blast: Help'
-    _controller: '\Drupal\tripal_blast\Controller\TripalBlastAdmin::help'
+    _controller: '\Drupal\tripal_blast\Controller\TripalBlastHelpController::help'
   requirements:
   requirements:
     _permission: 'access content'
     _permission: 'access content'