diff --git a/client/GruntFile.js b/client/GruntFile.js new file mode 100644 index 00000000000..149e90b89cf --- /dev/null +++ b/client/GruntFile.js @@ -0,0 +1,21 @@ +module.exports = function(grunt) { + + // Project configuration. + grunt.initConfig({ + pkg: grunt.file.readJSON('package.json'), + + copy: { + main: { + expand: true, + cwd: 'scripts/', + src: '**', + dest: '../static/scripts/' + } + } + + }); + + grunt.loadNpmTasks('grunt-contrib-copy'); + + grunt.registerTask('default', ['copy']); +}; diff --git a/client/package.json b/client/package.json new file mode 100644 index 00000000000..c74cd6c2413 --- /dev/null +++ b/client/package.json @@ -0,0 +1,11 @@ +{ + "name": "galaxy-client", + "version": "0.1.0", + "description": "Galaxy client application build system", + "keywords": [ + "galaxy" + ], + "devDependencies": { + "grunt-contrib-copy": "^0.5.0" + } +} diff --git a/client/scripts/base.js b/client/scripts/base.js new file mode 100644 index 00000000000..24a5e6eb63b --- /dev/null +++ b/client/scripts/base.js @@ -0,0 +1,15 @@ +define( ["libs/backbone/backbone"], function( Backbone ) { + + var Base = function() { + if( this.initialize ) { + this.initialize.apply(this, arguments); + } + }; + Base.extend = Backbone.Model.extend; + + return { + Base: Base, + Backbone: Backbone + }; + +}); \ No newline at end of file diff --git a/client/scripts/galaxy-app-base.js b/client/scripts/galaxy-app-base.js new file mode 100644 index 00000000000..2a7cc2076b1 --- /dev/null +++ b/client/scripts/galaxy-app-base.js @@ -0,0 +1,160 @@ +define([ + 'mvc/user/user-model', + 'utils/metrics-logger', + 'utils/add-logging', + 'utils/localization', + 'bootstrapped-data' +], function( userModel, metricsLogger, addLogging, localize, bootstrapped ){ +// ============================================================================ +/** Base galaxy client-side application. + * Iniitializes: + * logger : the logger/metrics-logger + * localize : the string localizer + * config : the current configuration (any k/v in + * universe_wsgi.ini available from the configuration API) + * user : the current user (as a mvc/user/user-model) + */ +function GalaxyApp( options ){ + var self = this; + return self._init( options || {} ); +} + +// add logging shortcuts for this object +addLogging( GalaxyApp, 'GalaxyApp' ); + +/** default options */ +GalaxyApp.prototype.defaultOptions = { + /** monkey patch attributes from existing window.Galaxy object? */ + patchExisting : true, + /** root url of this app */ + // move to self.root? + root : '/' +}; + +/** initalize options and sub-components */ +GalaxyApp.prototype._init = function init( options ){ + var self = this; + _.extend( self, Backbone.Events ); + + self._processOptions( options ); + self.debug( 'GalaxyApp.options: ', self.options ); + + self._patchGalaxy( window.Galaxy ); + + self._initLogger( options.loggerOptions || {} ); + self.debug( 'GalaxyApp.logger: ', self.logger ); + + self._initLocale(); + self.debug( 'GalaxyApp.localize: ', self.localize ); + + self.config = options.config || bootstrapped.config || {}; + self.debug( 'GalaxyApp.config: ', self.config ); + + self._initUser( options.user || bootstrapped.user || {} ); + self.debug( 'GalaxyApp.user: ', self.user ); + + //TODO: temp + self.trigger( 'ready', self ); + //if( typeof options.onload === 'function' ){ + // options.onload(); + //} + + self._setUpListeners(); + + return self; +}; + +/** add an option from options if the key matches an option in defaultOptions */ +GalaxyApp.prototype._processOptions = function _processOptions( options ){ + var self = this, + defaults = self.defaultOptions; + self.debug( '_processOptions: ', options ); + + self.options = {}; + for( var k in defaults ){ + if( defaults.hasOwnProperty( k ) ){ + self.options[ k ] = ( options.hasOwnProperty( k ) )?( options[ k ] ):( defaults[ k ] ); + } + } + return self; +}; + +/** add an option from options if the key matches an option in defaultOptions */ +GalaxyApp.prototype._patchGalaxy = function _processOptions( patchWith ){ + var self = this; + // in case req or plain script tag order has created a prev. version of the Galaxy obj... + if( self.options.patchExisting && patchWith ){ + self.debug( 'found existing Galaxy object:', patchWith ); + // ...(for now) monkey patch any added attributes that the previous Galaxy may have had + //TODO: move those attributes to more formal assignment in GalaxyApp + for( var k in patchWith ){ + if( patchWith.hasOwnProperty( k ) ){ + self.debug( '\t patching in ' + k + ' to Galaxy' ); + self[ k ] = patchWith[ k ]; + } + } + } +}; + +/** set up the metrics logger (utils/metrics-logger) and pass loggerOptions */ +GalaxyApp.prototype._initLogger = function _initLogger( loggerOptions ){ + var self = this; + self.debug( '_initLogger:', loggerOptions ); + self.logger = new metricsLogger.MetricsLogger( loggerOptions ); + return self; +}; + +/** add the localize fn to this object and the window namespace (as '_l') */ +GalaxyApp.prototype._initLocale = function _initLocale( options ){ + var self = this; + self.debug( '_initLocale:', options ); + self.localize = localize; + // add to window as global shortened alias + window._l = self.localize; + return self; +}; + +/** set up the current user as a Backbone model (mvc/user/user-model) */ +GalaxyApp.prototype._initUser = function _initUser( userJSON ){ + var self = this; + self.debug( '_initUser:', userJSON ); + self.user = new userModel.User( userJSON ); + //TODO: temp - old alias + self.currUser = self.user; + return self; +}; + +/** Set up DOM/jQuery/Backbone event listeners enabled for all pages */ +GalaxyApp.prototype._setUpListeners = function _setUpListeners(){ + var self = this; + + // hook to jq beforeSend to record the most recent ajax call and cache some data about it + /** cached info about the last ajax call made through jQuery */ + self.lastAjax = {}; + $( document ).bind( 'ajaxSend', function( ev, xhr, options ){ + var data = options.data; + try { + data = JSON.parse( data ); + } catch( err ){} + + self.lastAjax = { + url : location.href.slice( 0, -1 ) + options.url, + data : data + }; + //TODO:?? we might somehow manage to *retry* ajax using either this hook or Backbone.sync + }); + +}; + +/** string rep */ +GalaxyApp.prototype.toString = function toString(){ + var userEmail = this.user.get( 'email' ) || '(anonymous)'; + return 'GalaxyApp(' + userEmail + ')'; +}; + + +// ============================================================================ + return { + GalaxyApp : GalaxyApp + }; +}); diff --git a/client/scripts/galaxy.autocom_tagging.js b/client/scripts/galaxy.autocom_tagging.js new file mode 100644 index 00000000000..9f61f008fb2 --- /dev/null +++ b/client/scripts/galaxy.autocom_tagging.js @@ -0,0 +1,368 @@ +/** +* JQuery extension for tagging with autocomplete. +* @author: Jeremy Goecks +* @require: jquery.autocomplete plugin +*/ +// +// Initialize "tag click functions" for tags. +// +function init_tag_click_function(tag_elt, click_func) { + $(tag_elt).find('.tag-name').each( function() { + $(this).click( function() { + var tag_str = $(this).text(); + var tag_name_and_value = tag_str.split(":"); + click_func(tag_name_and_value[0], tag_name_and_value[1]); + return true; + }); + }); +} + +jQuery.fn.autocomplete_tagging = function(options) { + + var defaults = { + get_toggle_link_text_fn: function(tags) { + var text = ""; + var num_tags = obj_length(tags); + if (num_tags > 0) { + text = num_tags + (num_tags > 1 ? " Tags" : " Tag"); + } else { + text = "Add tags"; + } + return text; + }, + tag_click_fn : function (name, value) {}, + editable: true, + input_size: 20, + in_form: false, + tags : {}, + use_toggle_link: true, + item_id: "", + add_tag_img: "", + add_tag_img_rollover: "", + delete_tag_img: "", + ajax_autocomplete_tag_url: "", + ajax_retag_url: "", + ajax_delete_tag_url: "", + ajax_add_tag_url: "" + }; + + var settings = jQuery.extend(defaults, options); + + // + // Initalize object's elements. + // + + // Get elements for this object. For this_obj, assume the last element with the id is the "this"; this is somewhat of a hack to address the problem + // that there may be two tagging elements for a single item if there are both community and individual tags for an element. + var this_obj = $(this); + var tag_area = this_obj.find('.tag-area'); + var toggle_link = this_obj.find('.toggle-link'); + var tag_input_field = this_obj.find('.tag-input'); + var add_tag_button = this_obj.find('.add-tag-button'); + + // Initialize toggle link. + toggle_link.click( function() { + // Take special actions depending on whether toggle is showing or hiding link. + var after_toggle_fn; + if (tag_area.is(":hidden")) { + after_toggle_fn = function() { + // If there are no tags, go right to editing mode by generating a click on the area. + var num_tags = $(this).find('.tag-button').length; + if (num_tags === 0) { + tag_area.click(); + } + }; + } else { + after_toggle_fn = function() { + tag_area.blur(); + }; + } + tag_area.slideToggle("fast", after_toggle_fn); + return $(this); + }); + + // Initialize tag input field. + if (settings.editable) { + tag_input_field.hide(); + } + tag_input_field.keyup( function(e) { + if ( e.keyCode === 27 ) { + // Escape key + $(this).trigger( "blur" ); + } else if ( + ( e.keyCode === 13 ) || // Return Key + ( e.keyCode === 188 ) || // Comma + ( e.keyCode === 32 ) // Space + ) { + // + // Check input. + // + + var new_value = this.value; + + // Suppress space after a ":" + if ( new_value.indexOf(": ", new_value.length - 2) !== -1) { + this.value = new_value.substring(0, new_value.length-1); + return false; + } + + // Remove trigger keys from input. + if ( (e.keyCode === 188) || (e.keyCode === 32) ) { + new_value = new_value.substring( 0 , new_value.length - 1 ); + } + + // Trim whitespace. + new_value = $.trim(new_value); + + // Too short? + if (new_value.length < 2) { + return false; + } + + // + // New tag OK - apply it. + // + + this.value = ""; // Reset text field now that tag is being added + + // Add button for tag after all other tag buttons. + var new_tag_button = build_tag_button(new_value); + var tag_buttons = tag_area.children(".tag-button"); + if (tag_buttons.length !== 0) { + var last_tag_button = tag_buttons.slice(tag_buttons.length-1); + last_tag_button.after(new_tag_button); + } else { + tag_area.prepend(new_tag_button); + } + + // Add tag to internal list. + var tag_name_and_value = new_value.split(":"); + settings.tags[tag_name_and_value[0]] = tag_name_and_value[1]; + + // Update toggle link text. + var new_text = settings.get_toggle_link_text_fn(settings.tags); + toggle_link.text(new_text); + + // Commit tag to server. + var zz = $(this); + $.ajax({ + url: settings.ajax_add_tag_url, + data: { new_tag: new_value }, + error: function() { + // Failed. Roll back changes and show alert. + new_tag_button.remove(); + delete settings.tags[tag_name_and_value[0]]; + var new_text = settings.get_toggle_link_text_fn(settings.tags); + toggle_link.text(new_text); + alert( "Add tag failed" ); + }, + success: function() { + // Flush autocomplete cache because it's not out of date. + // TODO: in the future, we could remove the particular item + // that was chosen from the cache rather than flush it. + zz.data('autocompleter').cacheFlush(); + } + }); + + return false; + } + }); + + // Add autocomplete to input. + var format_item_func = function(key, row_position, num_rows, value, search_term) { + var tag_name_and_value = value.split(":"); + return (tag_name_and_value.length === 1 ? tag_name_and_value[0] : tag_name_and_value[1]); + }; + var autocomplete_options = { selectFirst: false, formatItem: format_item_func, + autoFill: false, highlight: false }; + tag_input_field.autocomplete(settings.ajax_autocomplete_tag_url, autocomplete_options); + + + // Initialize delete tag images for current tags. + this_obj.find('.delete-tag-img').each(function() { + init_delete_tag_image( $(this) ); + }); + + + // Initialize tag click function. + init_tag_click_function($(this), settings.tag_click_fn); + + // Initialize "add tag" button. + add_tag_button.click( function() { + $(this).hide(); + + // Clicking on button is the same as clicking on the tag area. + tag_area.click(); + return false; + }); + + // + // Set up tag area interactions; these are needed only if tags are editable. + // + if (settings.editable) { + // When the tag area blurs, go to "view tag" mode. + tag_area.bind("blur", function(e) { + if (obj_length(settings.tags) > 0) { + add_tag_button.show(); + tag_input_field.hide(); + tag_area.removeClass("active-tag-area"); + // tag_area.addClass("tooltip"); + } else { + // No tags, so do nothing to ensure that input is still visible. + } + }); + + // On click, enable user to add tags. + tag_area.click( function(e) { + var is_active = $(this).hasClass("active-tag-area"); + + // If a "delete image" object was pressed and area is inactive, do nothing. + if ($(e.target).hasClass("delete-tag-img") && !is_active) { + return false; + } + + // If a "tag name" object was pressed and area is inactive, do nothing. + if ($(e.target).hasClass("tag-name") && !is_active) { + return false; + } + + // Remove tooltip. + // $(this).removeClass("tooltip"); + + // Hide add tag button, show tag_input field. Change background to show + // area is active. + $(this).addClass("active-tag-area"); + add_tag_button.hide(); + tag_input_field.show(); + tag_input_field.focus(); + + // Add handler to document that will call blur when the tag area is blurred; + // a tag area is blurred when a user clicks on an element outside the area. + var handle_document_click = function(e) { + var check_click = function(tag_area, target) { + var tag_area_id = tag_area.attr("id"); + // Blur the tag area if the element clicked on is not in the tag area. + if (target !== tag_area) { + tag_area.blur(); + $(window).unbind("click.tagging_blur"); + $(this).addClass("tooltip"); + } + }; + check_click(tag_area, $(e.target)); + }; + // TODO: we should attach the click handler to all frames in order to capture + // clicks outside the frame that this element is in. + //window.parent.document.onclick = handle_document_click; + //var temp = $(window.parent.document.body).contents().find("iframe").html(); + //alert(temp); + //$(document).parent().click(handle_document_click); + $(window).bind("click.tagging_blur", handle_document_click); + + return false; + }); + } + + // If using toggle link, hide the tag area. Otherwise, show the tag area. + if (settings.use_toggle_link) { + tag_area.hide(); + } + + // + // Helper functions. + // + + // + // Collapse tag name + value into a single string. + // + function build_tag_str(tag_name, tag_value) { + return tag_name + ( tag_value ? ":" + tag_value : ""); + } + + + // Initialize a "delete tag image": when click, delete tag from UI and send delete request to server. + function init_delete_tag_image(delete_img) { + $(delete_img).mouseenter( function () { + $(this).attr("src", settings.delete_tag_img_rollover); + }); + $(delete_img).mouseleave( function () { + $(this).attr("src", settings.delete_tag_img); + }); + $(delete_img).click( function () { + // Tag button is image's parent. + var tag_button = $(this).parent(); + + // Get tag name, value. + var tag_name_elt = tag_button.find(".tag-name").eq(0); + var tag_str = tag_name_elt.text(); + var tag_name_and_value = tag_str.split(":"); + var tag_name = tag_name_and_value[0]; + var tag_value = tag_name_and_value[1]; + + var prev_button = tag_button.prev(); + tag_button.remove(); + + // Remove tag from local list for consistency. + delete settings.tags[tag_name]; + + // Update toggle link text. + var new_text = settings.get_toggle_link_text_fn(settings.tags); + toggle_link.text(new_text); + + // Delete tag. + $.ajax({ + url: settings.ajax_delete_tag_url, + data: { tag_name: tag_name }, + error: function() { + // Failed. Roll back changes and show alert. + settings.tags[tag_name] = tag_value; + if (prev_button.hasClass("tag-button")) { + prev_button.after(tag_button); + } else { + tag_area.prepend(tag_button); + } + alert( "Remove tag failed" ); + + toggle_link.text(settings.get_toggle_link_text_fn(settings.tags)); + + // TODO: no idea why it's necessary to set this up again. + delete_img.mouseenter( function () { + $(this).attr("src", settings.delete_tag_img_rollover); + }); + delete_img.mouseleave( function () { + $(this).attr("src", settings.delete_tag_img); + }); + }, + success: function() {} + }); + + return true; + }); + } + + // + // Function that builds a tag button. + // + function build_tag_button(tag_str) { + // Build "delete tag" image. + var delete_img = $("").attr("src", settings.delete_tag_img).addClass("delete-tag-img"); + init_delete_tag_image(delete_img); + + // Build tag button. + var tag_name_elt = $("").text(tag_str).addClass("tag-name"); + tag_name_elt.click( function() { + var tag_name_and_value = tag_str.split(":"); + settings.tag_click_fn(tag_name_and_value[0], tag_name_and_value[1]); + return true; + }); + + var tag_button = $("").addClass("tag-button"); + tag_button.append(tag_name_elt); + // Allow delete only if element is editable. + if (settings.editable) { + tag_button.append(delete_img); + } + + return tag_button; + } + +}; diff --git a/client/scripts/galaxy.base.js b/client/scripts/galaxy.base.js new file mode 100644 index 00000000000..b2b621d9a2c --- /dev/null +++ b/client/scripts/galaxy.base.js @@ -0,0 +1,636 @@ +// requestAnimationFrame polyfill +(function() { + var lastTime = 0; + var vendors = ['ms', 'moz', 'webkit', 'o']; + for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { + window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame']; + window.cancelRequestAnimationFrame = window[vendors[x]+ + 'CancelRequestAnimationFrame']; + } + + if (!window.requestAnimationFrame) + window.requestAnimationFrame = function(callback, element) { + var currTime = new Date().getTime(); + var timeToCall = Math.max(0, 16 - (currTime - lastTime)); + var id = window.setTimeout(function() { callback(currTime + timeToCall); }, + timeToCall); + lastTime = currTime + timeToCall; + return id; + }; + + if (!window.cancelAnimationFrame) + window.cancelAnimationFrame = function(id) { + clearTimeout(id); + }; +}()); + +// IE doesn't implement Array.indexOf +if (!Array.indexOf) { + Array.prototype.indexOf = function(obj) { + for (var i = 0, len = this.length; i < len; i++) { + if (this[i] == obj) { + return i; + } + } + return -1; + }; +} + +// Returns the number of keys (elements) in an array/dictionary. +function obj_length(obj) { + if (obj.length !== undefined) { + return obj.length; + } + + var count = 0; + for (var element in obj) { + count++; + } + return count; +} + +$.fn.makeAbsolute = function(rebase) { + return this.each(function() { + var el = $(this); + var pos = el.position(); + el.css({ + position: "absolute", + marginLeft: 0, marginTop: 0, + top: pos.top, left: pos.left, + right: $(window).width() - ( pos.left + el.width() ) + }); + if (rebase) { + el.remove().appendTo("body"); + } + }); +}; + +/** + * Sets up popupmenu rendering and binds options functions to the appropriate links. + * initial_options is a dict with text describing the option pointing to either (a) a + * function to perform; or (b) another dict with two required keys, 'url' and 'action' (the + * function to perform. (b) is useful for exposing the underlying URL of the option. + */ +function make_popupmenu(button_element, initial_options) { + /* Use the $.data feature to store options with the link element. + This allows options to be changed at a later time + */ + var element_menu_exists = (button_element.data("menu_options")); + button_element.data("menu_options", initial_options); + + // If element already has menu, nothing else to do since HTML and actions are already set. + if (element_menu_exists) { return; } + + button_element.bind("click.show_popup", function(e) { + // Close existing visible menus + $(".popmenu-wrapper").remove(); + + // Need setTimeouts so clicks don't interfere with each other + setTimeout( function() { + // Dynamically generate the wrapper holding all the selectable options of the menu. + var menu_element = $( "" ); + var options = button_element.data("menu_options"); + if (obj_length(options) <= 0) { + $("
  • No Options.
  • ").appendTo(menu_element); + } + $.each( options, function( k, v ) { + if (v) { + // Action can be either an anonymous function and a mapped dict. + var action = v.action || v; + menu_element.append( $("
  • ").append( $("").attr("href", v.url).html(k).click(action) ) ); + } else { + menu_element.append( $("
  • ").addClass( "head" ).append( $("
    ").html(k) ) ); + } + }); + var wrapper = $( "
    " ) + .append( menu_element ).appendTo( "body" ); + + var x = e.pageX - wrapper.width() / 2 ; + x = Math.min( x, $(document).scrollLeft() + $(window).width() - $(wrapper).width() - 5 ); + x = Math.max( x, $(document).scrollLeft() + 5 ); + + wrapper.css({ + top: e.pageY, + left: x + }); + }, 10); + + setTimeout( function() { + // Bind click event to current window and all frames to remove any visible menus + // Bind to document object instead of window object for IE compat + var close_popup = function(el) { + $(el).bind("click.close_popup", function() { + $(".popmenu-wrapper").remove(); + el.unbind("click.close_popup"); + }); + }; + close_popup( $(window.document) ); // Current frame + close_popup( $(window.top.document) ); // Parent frame + for (var frame_id = window.top.frames.length; frame_id--;) { // Sibling frames + var frame = $(window.top.frames[frame_id].document); + close_popup(frame); + } + }, 50); + + return false; + }); + +} + +/** + * Convert two seperate (often adjacent) divs into galaxy popupmenu + * - div 1 contains a number of anchors which become the menu options + * - div 1 should have a 'popupmenu' attribute + * - this popupmenu attribute contains the id of div 2 + * - div 2 becomes the 'face' of the popupmenu + * + * NOTE: make_popup_menus finds and operates on all divs with a popupmenu attr (no need to point it at something) + * but (since that selector searches the dom on the page), you can send a parent in + * NOTE: make_popup_menus, and make_popupmenu are horrible names + */ +function make_popup_menus( parent ) { + // find all popupmenu menu divs (divs that contains anchors to be converted to menu options) + // either in the parent or the document if no parent passed + parent = parent || document; + $( parent ).find( "div[popupmenu]" ).each( function() { + var options = {}; + var menu = $(this); + + // find each anchor in the menu, convert them into an options map: { a.text : click_function } + menu.find( "a" ).each( function() { + var link = $(this), + link_dom = link.get(0), + confirmtext = link_dom.getAttribute( "confirm" ), + href = link_dom.getAttribute( "href" ), + target = link_dom.getAttribute( "target" ); + + // no href - no function (gen. a label) + if (!href) { + options[ link.text() ] = null; + + } else { + options[ link.text() ] = { + url: href, + action: function() { + + // if theres confirm text, send the dialog + if ( !confirmtext || confirm( confirmtext ) ) { + // link.click() doesn't use target for some reason, + // so manually do it here. + if (target) { + window.open(href, target); + return false; + } + // For all other links, do the default action. + else { + link.click(); + } + } + } + }; + } + }); + // locate the element with the id corresponding to the menu's popupmenu attr + var box = $( parent ).find( "#" + menu.attr( 'popupmenu' ) ); + + // For menus with clickable link text, make clicking on the link go through instead + // of activating the popup menu + box.find("a").bind("click", function(e) { + e.stopPropagation(); // Stop bubbling so clicking on the link goes through + return true; + }); + + // attach the click events and menu box building to the box element + make_popupmenu(box, options); + box.addClass("popup"); + menu.remove(); + }); +} + +// Alphanumeric/natural sort fn +function naturalSort(a, b) { + // setup temp-scope variables for comparison evauluation + var re = /(-?[0-9\.]+)/g, + x = a.toString().toLowerCase() || '', + y = b.toString().toLowerCase() || '', + nC = String.fromCharCode(0), + xN = x.replace( re, nC + '$1' + nC ).split(nC), + yN = y.replace( re, nC + '$1' + nC ).split(nC), + xD = (new Date(x)).getTime(), + yD = xD ? (new Date(y)).getTime() : null; + // natural sorting of dates + if ( yD ) { + if ( xD < yD ) { return -1; } + else if ( xD > yD ) { return 1; } + } + // natural sorting through split numeric strings and default strings + var oFxNcL, oFyNcL; + for ( var cLoc = 0, numS = Math.max(xN.length, yN.length); cLoc < numS; cLoc++ ) { + oFxNcL = parseFloat(xN[cLoc]) || xN[cLoc]; + oFyNcL = parseFloat(yN[cLoc]) || yN[cLoc]; + if (oFxNcL < oFyNcL) { return -1; } + else if (oFxNcL > oFyNcL) { return 1; } + } + return 0; +} + +$.fn.refresh_select2 = function() { + var select_elt = $(this); + var options = { width: "resolve", + closeOnSelect: !select_elt.is("[MULTIPLE]") + }; + return select_elt.select2( options ); +} + +// Replace select box with a text input box + autocomplete. +function replace_big_select_inputs(min_length, max_length, select_elts) { + // To do replace, the select2 plugin must be loaded. + + if (!jQuery.fn.select2) { + return; + } + + // Set default for min_length and max_length + if (min_length === undefined) { + min_length = 20; + } + if (max_length === undefined) { + max_length = 3000; + } + + select_elts = select_elts || $('select'); + + select_elts.each( function() { + var select_elt = $(this).not('[multiple]'); + // Make sure that options is within range. + var num_options = select_elt.find('option').length; + if ( (num_options < min_length) || (num_options > max_length) ) { + return; + } + + if (select_elt.hasClass("no-autocomplete")) { + return; + } + + /* Replaced jQuery.autocomplete with select2, notes: + * - multiple selects are supported + * - the original element is updated with the value, convert_to_values should not be needed + * - events are fired when updating the original element, so refresh_on_change should just work + * + * - should we still sort dbkey fields here? + */ + select_elt.refresh_select2(); + }); +} + +/** + * Make an element with text editable: (a) when user clicks on text, a textbox/area + * is provided for editing; (b) when enter key pressed, element's text is set and on_finish + * is called. + */ +// TODO: use this function to implement async_save_text (implemented below). +$.fn.make_text_editable = function(config_dict) { + // Get config options. + var num_cols = ("num_cols" in config_dict ? config_dict.num_cols : 30), + num_rows = ("num_rows" in config_dict ? config_dict.num_rows : 4), + use_textarea = ("use_textarea" in config_dict ? config_dict.use_textarea : false), + on_finish = ("on_finish" in config_dict ? config_dict.on_finish : null), + help_text = ("help_text" in config_dict ? config_dict.help_text : null); + + // Add element behavior. + var container = $(this); + container.addClass("editable-text").click(function(e) { + // If there's already an input element, editing is active, so do nothing. + if ($(this).children(":input").length > 0) { + return; + } + + container.removeClass("editable-text"); + + // Handler for setting element text. + var set_text = function(new_text) { + container.find(":input").remove(); + + if (new_text !== "") { + container.text(new_text); + } + else { + // No text; need a line so that there is a click target. + container.html("
    "); + } + container.addClass("editable-text"); + + if (on_finish) { + on_finish(new_text); + } + }; + + // Create input element(s) for editing. + var cur_text = ("cur_text" in config_dict ? config_dict.cur_text : container.text() ), + input_elt, button_elt; + + if (use_textarea) { + input_elt = $("").attr({ rows: num_rows, cols: num_cols }).text( $.trim(old_text) ); + } else { + t = $("").attr({ value: $.trim(old_text), size: num_cols }); + } + t.attr("id", "renaming-active"); + t.blur( function() { + $(this).remove(); + text_elt.show(); + if (on_finish) { + on_finish(t); + } + }); + t.keyup( function( e ) { + if ( e.keyCode === 27 ) { + // Escape key + $(this).trigger( "blur" ); + } else if ( e.keyCode === 13 ) { + // Enter key submits + var ajax_data = {}; + ajax_data[text_parm_name] = $(this).val(); + $(this).trigger( "blur" ); + $.ajax({ + url: save_url, + data: ajax_data, + error: function() { + alert( "Text editing for elt " + text_elt_id + " failed" ); + // TODO: call finish or no? For now, let's not because error occurred. + }, + success: function(processed_text) { + // Set new text and call finish method. + if (processed_text !== "") { + text_elt.text(processed_text); + } else { + text_elt.html("None"); + } + if (on_finish) { + on_finish(t); + } + } + }); + } + }); + + if (on_start) { + on_start(t); + } + // Replace text with input object and focus & select. + text_elt.hide(); + t.insertAfter(text_elt); + t.focus(); + t.select(); + + return; + }); +} + +function commatize( number ) { + number += ''; // Convert to string + var rgx = /(\d+)(\d{3})/; + while (rgx.test(number)) { + number = number.replace(rgx, '$1' + ',' + '$2'); + } + return number; +} + +// Reset tool search to start state. +function reset_tool_search( initValue ) { + // Function may be called in top frame or in tool_menu_frame; + // in either case, get the tool menu frame. + var tool_menu_frame = $("#galaxy_tools").contents(); + if (tool_menu_frame.length === 0) { + tool_menu_frame = $(document); + } + + // Remove classes that indicate searching is active. + $(this).removeClass("search_active"); + tool_menu_frame.find(".toolTitle").removeClass("search_match"); + + // Reset visibility of tools and labels. + tool_menu_frame.find(".toolSectionBody").hide(); + tool_menu_frame.find(".toolTitle").show(); + tool_menu_frame.find(".toolPanelLabel").show(); + tool_menu_frame.find(".toolSectionWrapper").each( function() { + if ($(this).attr('id') !== 'recently_used_wrapper') { + // Default action. + $(this).show(); + } else if ($(this).hasClass("user_pref_visible")) { + $(this).show(); + } + }); + tool_menu_frame.find("#search-no-results").hide(); + + // Reset search input. + tool_menu_frame.find("#search-spinner").hide(); + if (initValue) { + var search_input = tool_menu_frame.find("#tool-search-query"); + search_input.val("search tools"); + } +} + +// Create GalaxyAsync object. +var GalaxyAsync = function(log_action) { + this.url_dict = {}; + this.log_action = (log_action === undefined ? false : log_action); +}; + +GalaxyAsync.prototype.set_func_url = function( func_name, url ) { + this.url_dict[func_name] = url; +}; + +// Set user preference asynchronously. +GalaxyAsync.prototype.set_user_pref = function( pref_name, pref_value ) { + // Get URL. + var url = this.url_dict[arguments.callee]; + if (url === undefined) { return false; } + $.ajax({ + url: url, + data: { "pref_name" : pref_name, "pref_value" : pref_value }, + error: function() { return false; }, + success: function() { return true; } + }); +}; + +// Log user action asynchronously. +GalaxyAsync.prototype.log_user_action = function( action, context, params ) { + if (!this.log_action) { return; } + + // Get URL. + var url = this.url_dict[arguments.callee]; + if (url === undefined) { return false; } + $.ajax({ + url: url, + data: { "action" : action, "context" : context, "params" : params }, + error: function() { return false; }, + success: function() { return true; } + }); +}; + +// Initialize refresh events. +function init_refresh_on_change () { + $("select[refresh_on_change='true']") + .off('change') + .change(function() { + var select_field = $(this), + select_val = select_field.val(), + refresh = false, + ref_on_change_vals = select_field.attr("refresh_on_change_values"); + if (ref_on_change_vals) { + ref_on_change_vals = ref_on_change_vals.split(','); + var last_selected_value = select_field.attr("last_selected_value"); + if ($.inArray(select_val, ref_on_change_vals) === -1 && $.inArray(last_selected_value, ref_on_change_vals) === -1) { + return; + } + } + $(window).trigger("refresh_on_change"); + $(document).trigger("convert_to_values"); // Convert autocomplete text to values + select_field.get(0).form.submit(); + }); + + // checkboxes refresh on change + $(":checkbox[refresh_on_change='true']") + .off('click') + .click( function() { + var select_field = $(this), + select_val = select_field.val(), + refresh = false, + ref_on_change_vals = select_field.attr("refresh_on_change_values"); + if (ref_on_change_vals) { + ref_on_change_vals = ref_on_change_vals.split(','); + var last_selected_value = select_field.attr("last_selected_value"); + if ($.inArray(select_val, ref_on_change_vals) === -1 && $.inArray(last_selected_value, ref_on_change_vals) === -1) { + return; + } + } + $(window).trigger("refresh_on_change"); + select_field.get(0).form.submit(); + }); + + // Links with confirmation + $( "a[confirm]" ) + .off('click') + .click( function() { + return confirm( $(this).attr("confirm") ); + }); +}; + +$(document).ready( function() { + + // Refresh events for form fields. + init_refresh_on_change(); + + // Tooltips + if ( $.fn.tooltip ) { + // Put tooltips below items in panel header so that they do not overlap masthead. + $(".unified-panel-header [title]").tooltip( { placement: 'bottom' } ); + + // default tooltip initialization, it will follow the data-placement tag for tooltip location + // and fallback to 'top' if not present + $("[title]").tooltip(); + } + // Make popup menus. + make_popup_menus(); + + // Replace big selects. + replace_big_select_inputs(20, 1500); + + // If galaxy_main frame does not exist and link targets galaxy_main, + // add use_panels=True and set target to self. + $("a").click( function() { + var anchor = $(this); + var galaxy_main_exists = (parent.frames && parent.frames.galaxy_main); + if ( ( anchor.attr( "target" ) == "galaxy_main" ) && ( !galaxy_main_exists ) ) { + var href = anchor.attr("href"); + if (href.indexOf("?") == -1) { + href += "?"; + } + else { + href += "&"; + } + href += "use_panels=True"; + anchor.attr("href", href); + anchor.attr("target", "_self"); + } + return anchor; + }); + +}); diff --git a/client/scripts/galaxy.frame.js b/client/scripts/galaxy.frame.js new file mode 100644 index 00000000000..f6b0db9bfd7 --- /dev/null +++ b/client/scripts/galaxy.frame.js @@ -0,0 +1,173 @@ +// dependencies +define(["galaxy.masthead", "mvc/ui/ui-frames"], function(mod_masthead, Frames) { + +// frame manager +var GalaxyFrame = Backbone.View.extend( +{ + // base element + el_main: 'body', + + // frame active/disabled + active: false, + + // button active + button_active: null, + + // button load + button_load : null, + + // initialize + initialize : function(options) + { + // add to masthead menu + var self = this; + + // create frames + this.frames = new Frames.View({ + visible: false, + }); + + // add activate icon + this.button_active = new mod_masthead.GalaxyMastheadIcon ( + { + icon : 'fa-th', + tooltip : 'Enable/Disable Scratchbook', + onclick : function() { self._activate(); }, + onunload : function() { + if (self.frames.length() > 0) { + return "You opened " + self.frames.length() + " frame(s) which will be lost."; + } + } + }); + + // add to masthead + Galaxy.masthead.append(this.button_active); + + // add load icon + this.button_load = new mod_masthead.GalaxyMastheadIcon ( + { + icon : 'fa-eye', + tooltip : 'Show/Hide Scratchbook', + onclick : function(e) { + if (self.frames.visible) { + self.frames.hide(); + } else { + self.frames.show(); + } + }, + with_number : true + }); + + // add to masthead + Galaxy.masthead.append(this.button_load); + + // create + this.setElement(this.frames.$el); + + // append to main + $(this.el_main).append(this.$el); + + // refresh menu + this.frames.setOnChange(function() { + self._refresh(); + }); + this._refresh(); + }, + + // adds and displays a new frame/window + add: function(options) + { + // open new tab + if (options.target == '_blank') + { + window.open(options.content); + return; + } + + // reload entire window + if (options.target == '_top' || options.target == '_parent' || options.target == '_self') + { + window.location = options.content; + return; + } + + // validate + if (!this.active) + { + // fix url if main frame is unavailable + var $galaxy_main = $(window.parent.document).find('#galaxy_main'); + if (options.target == 'galaxy_main' || options.target == 'center') + { + if ($galaxy_main.length === 0) + { + var href = options.content; + if (href.indexOf('?') == -1) + href += '?'; + else + href += '&'; + href += 'use_panels=True'; + window.location = href; + } else { + $galaxy_main.attr('src', options.content); + } + } else + window.location = options.content; + + // stop + return; + } + + // add to frames view + this.frames.add(options); + }, + + // activate/disable panel + _activate: function () + { + // check + if (this.active) + { + // disable + this.active = false; + + // toggle + this.button_active.untoggle(); + + // hide panel + this.frames.hide(); + } else { + // activate + this.active = true; + + // untoggle + this.button_active.toggle(); + } + }, + + // update frame counter + _refresh: function() + { + // update on screen counter + this.button_load.number(this.frames.length()); + + // check + if(this.frames.length() === 0) + this.button_load.hide(); + else + this.button_load.show(); + + // check + if (this.frames.visible) { + this.button_load.toggle(); + } else { + this.button_load.untoggle(); + } + } +}); + +// return +return { + GalaxyFrame: GalaxyFrame +}; + +}); diff --git a/client/scripts/galaxy.library.js b/client/scripts/galaxy.library.js new file mode 100644 index 00000000000..770550c418f --- /dev/null +++ b/client/scripts/galaxy.library.js @@ -0,0 +1,159 @@ +// MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM +// === MAIN GALAXY LIBRARY MODULE ==== +// MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM + +define([ + "galaxy.masthead", + "utils/utils", + "libs/toastr", + "mvc/base-mvc", + "mvc/library/library-model", + "mvc/library/library-folderlist-view", + "mvc/library/library-librarylist-view", + "mvc/library/library-librarytoolbar-view", + "mvc/library/library-foldertoolbar-view", + "mvc/library/library-dataset-view", + "mvc/library/library-library-view", + "mvc/library/library-folder-view" + ], +function(mod_masthead, + mod_utils, + mod_toastr, + mod_baseMVC, + mod_library_model, + mod_folderlist_view, + mod_librarylist_view, + mod_librarytoolbar_view, + mod_foldertoolbar_view, + mod_library_dataset_view, + mod_library_library_view, + mod_library_folder_view + ) { + +// ============================================================================ +// ROUTER +var LibraryRouter = Backbone.Router.extend({ + initialize: function() { + this.routesHit = 0; + //keep count of number of routes handled by the application + Backbone.history.on('route', function() { this.routesHit++; }, this); + }, + + routes: { + "" : "libraries", + "library/:library_id/permissions" : "library_permissions", + "folders/:folder_id/permissions" : "folder_permissions", + "folders/:id" : "folder_content", + "folders/:folder_id/datasets/:dataset_id" : "dataset_detail", + "folders/:folder_id/datasets/:dataset_id/permissions" : "dataset_permissions", + "folders/:folder_id/datasets/:dataset_id/versions/:ldda_id" : "dataset_version", + "folders/:folder_id/download/:format" : "download" + }, + + back: function() { + if(this.routesHit > 1) { + //more than one route hit -> user did not land to current page directly + window.history.back(); + } else { + //otherwise go to the home page. Use replaceState if available so + //the navigation doesn't create an extra history entry + this.navigate('#', {trigger:true, replace:true}); + } + } +}); + +// ============================================================================ +/** session storage for library preferences */ +var LibraryPrefs = mod_baseMVC.SessionStorageModel.extend({ + defaults : { + with_deleted : false, + sort_order : 'asc', + sort_by : 'name' + } +}); + +// ============================================================================ +// Main controller of Galaxy Library +var GalaxyLibrary = Backbone.View.extend({ + + libraryToolbarView: null, + libraryListView: null, + library_router: null, + libraryView: null, + folderToolbarView: null, + folderListView: null, + datasetView: null, + + initialize : function(){ + Galaxy.libraries = this; + + this.preferences = new LibraryPrefs( {id: 'global-lib-prefs'} ); + + this.library_router = new LibraryRouter(); + + this.library_router.on('route:libraries', function() { + Galaxy.libraries.libraryToolbarView = new mod_librarytoolbar_view.LibraryToolbarView(); + Galaxy.libraries.libraryListView = new mod_librarylist_view.LibraryListView(); + }); + + this.library_router.on('route:folder_content', function(id) { + if (Galaxy.libraries.folderToolbarView){ + Galaxy.libraries.folderToolbarView.$el.unbind('click'); + } + Galaxy.libraries.folderToolbarView = new mod_foldertoolbar_view.FolderToolbarView({id: id}); + Galaxy.libraries.folderListView = new mod_folderlist_view.FolderListView({id: id}); + }); + + this.library_router.on('route:download', function(folder_id, format) { + if ($('#folder_list_body').find(':checked').length === 0) { + mod_toastr.info('You have to select some datasets to download'); + Galaxy.libraries.library_router.navigate('folders/' + folder_id, {trigger: true, replace: true}); + } else { + Galaxy.libraries.folderToolbarView.download(folder_id, format); + Galaxy.libraries.library_router.navigate('folders/' + folder_id, {trigger: false, replace: true}); + } + }); + + this.library_router.on('route:dataset_detail', function(folder_id, dataset_id){ + if (Galaxy.libraries.datasetView){ + Galaxy.libraries.datasetView.$el.unbind('click'); + } + Galaxy.libraries.datasetView = new mod_library_dataset_view.LibraryDatasetView({id: dataset_id}); + }); + this.library_router.on('route:dataset_version', function(folder_id, dataset_id, ldda_id){ + if (Galaxy.libraries.datasetView){ + Galaxy.libraries.datasetView.$el.unbind('click'); + } + Galaxy.libraries.datasetView = new mod_library_dataset_view.LibraryDatasetView({id: dataset_id, ldda_id: ldda_id, show_version: true}); + }); + + this.library_router.on('route:dataset_permissions', function(folder_id, dataset_id){ + if (Galaxy.libraries.datasetView){ + Galaxy.libraries.datasetView.$el.unbind('click'); + } + Galaxy.libraries.datasetView = new mod_library_dataset_view.LibraryDatasetView({id: dataset_id, show_permissions: true}); + }); + + this.library_router.on('route:library_permissions', function(library_id){ + if (Galaxy.libraries.libraryView){ + Galaxy.libraries.libraryView.$el.unbind('click'); + } + Galaxy.libraries.libraryView = new mod_library_library_view.LibraryView({id: library_id, show_permissions: true}); + }); + + this.library_router.on('route:folder_permissions', function(folder_id){ + if (Galaxy.libraries.folderView){ + Galaxy.libraries.folderView.$el.unbind('click'); + } + Galaxy.libraries.folderView = new mod_library_folder_view.FolderView({id: folder_id, show_permissions: true}); + }); + + Backbone.history.start({pushState: false}); + } +}); + +return { + GalaxyApp: GalaxyLibrary +}; + +}); diff --git a/client/scripts/galaxy.masthead.js b/client/scripts/galaxy.masthead.js new file mode 100644 index 00000000000..99243410c01 --- /dev/null +++ b/client/scripts/galaxy.masthead.js @@ -0,0 +1,445 @@ +// dependencies +define([], function() { + +// masthead +var GalaxyMasthead = Backbone.View.extend( +{ + // base element + el_masthead: '#everything', + + // options + options : null, + + // background + $background: null, + + // list + list: [], + + // initialize + initialize : function(options) + { + // update options + this.options = options; + + // HACK: due to body events defined in galaxy.panels.js + $("body").off(); + + // define this element + this.setElement($(this._template(options))); + + // append to masthead + $(this.el_masthead).append($(this.el)); + + // assign background + this.$background = $(this.el).find('#masthead-background'); + + // loop through unload functions if the user attempts to unload the page + var self = this; + $(window).on('beforeunload', function() { + var text = ""; + for (key in self.list) { + if (self.list[key].options.onunload) { + var q = self.list[key].options.onunload(); + if (q) text += q + " "; + } + } + if (text != "") { + return text; + } + }); + }, + + // configure events + events: + { + 'click' : '_click', + 'mousedown' : function(e) { e.preventDefault() } + }, + + // adds a new item to the masthead + append : function(item) + { + return this._add(item, true); + }, + + // adds a new item to the masthead + prepend : function(item) + { + return this._add(item, false); + }, + + // activate + highlight: function(id) + { + var current = $(this.el).find('#' + id + '> li'); + if (current) { + current.addClass('active'); + } + }, + + // adds a new item to the masthead + _add : function(item, append) + { + var $loc = $(this.el).find('#' + item.location); + if ($loc) + { + // create frame for new item + var $current = $(item.el); + + // configure class in order to mark new items + $current.addClass('masthead-item'); + + // append to masthead + if (append) { + $loc.append($current); + } else { + $loc.prepend($current); + } + + // add to list + this.list.push(item); + } + + // location not found + return null; + }, + + // handle click event + _click: function(e) + { + // close all popups + var $all = $(this.el).find('.popup'); + if ($all) { + $all.hide(); + } + + // open current item + var $current = $(e.target).closest('.masthead-item').find('.popup'); + if ($(e.target).hasClass('head')) { + $current.show(); + this.$background.show(); + } else { + this.$background.hide(); + } + }, + + /* + HTML TEMPLATES + */ + + // fill template + _template: function(options) + { + var brand_text = options.brand ? ("/ " + options.brand) : "" ; + return '
    ' + + '' + + '
    '; + } +}); + +// icon +var GalaxyMastheadIcon = Backbone.View.extend( +{ + // icon options + options: + { + id : '', + icon : 'fa-cog', + tooltip : '', + with_number : false, + onclick : function() { alert ('clicked') }, + onunload : null, + visible : true + }, + + // location identifier for masthead class + location: 'iconbar', + + // initialize + initialize: function (options) + { + // read in defaults + if (options) + this.options = _.defaults(options, this.options); + + // add template for icon + this.setElement($(this._template(this.options))); + + // configure icon + var self = this; + $(this.el).find('.icon').tooltip({title: this.options.tooltip, placement: 'bottom'}) + .on('mouseup', self.options.onclick); + + // visiblity + if (!this.options.visible) + this.hide(); + }, + + // show + show: function() + { + $(this.el).css({visibility : 'visible'}); + }, + + // show + hide: function() + { + $(this.el).css({visibility : 'hidden'}); + }, + + // switch icon + icon: function (new_icon) + { + // update icon class + $(this.el).find('.icon').removeClass(this.options.icon) + .addClass(new_icon); + + // update icon + this.options.icon = new_icon; + }, + + // toggle + toggle: function() + { + $(this.el).addClass('toggle'); + }, + + // untoggle + untoggle: function() + { + $(this.el).removeClass('toggle'); + }, + + // set/get number + number: function(new_number) + { + $(this.el).find('.number').text(new_number); + }, + + // fill template icon + _template: function (options) + { + var tmpl = '
    ' + + '
    '; + if (options.with_number) + tmpl+= '
    '; + tmpl += '
    '; + + // return template + return tmpl; + } +}); + +// tab +var GalaxyMastheadTab = Backbone.View.extend( +{ + // main options + options: + { + id : '', + title : '', + target : '_parent', + content : '', + type : 'url', + scratchbook : false, + onunload : null, + visible : true, + disabled : false, + title_attribute : '' + }, + + // location + location: 'navbar', + + // optional sub menu + $menu: null, + + // events + events: + { + 'click .head' : '_head' + }, + + // initialize + initialize: function (options) + { + // read in defaults + if (options) + this.options = _.defaults(options, this.options); + + // update url + if (this.options.content && this.options.content.indexOf('//') === -1) + this.options.content = galaxy_config.root + this.options.content; + + // add template for tab + this.setElement($(this._template(this.options))); + + // disable menu items that are not available to anonymous user + // also show title to explain why they are disabled + if (this.options.disabled){ + $(this.el).find('.root').addClass('disabled'); + this._attachPopover(); + } + + // visiblity + if (!this.options.visible) + this.hide(); + }, + + // show + show: function() + { + $(this.el).css({visibility : 'visible'}); + }, + + // show + hide: function() + { + $(this.el).css({visibility : 'hidden'}); + }, + + // add menu item + add: function (options) + { + // menu option defaults + var menuOptions = { + title : 'Title', + content : '', + type : 'url', + target : '_parent', + scratchbook : false, + divider : false + } + + // read in defaults + if (options) + menuOptions = _.defaults(options, menuOptions); + + // update url + if (menuOptions.content && menuOptions.content.indexOf('//') === -1) + menuOptions.content = galaxy_config.root + menuOptions.content; + + // check if submenu element is available + if (!this.$menu) + { + // insert submenu element into root + $(this.el).find('.root').append(this._templateMenu()); + + // show caret + $(this.el).find('.symbol').addClass('caret'); + + // update element link + this.$menu = $(this.el).find('.popup'); + } + + // create + var $item = $(this._templateMenuItem(menuOptions)); + + // append menu + this.$menu.append($item); + + // add events + var self = this; + $item.on('click', function(e) + { + // prevent default + e.preventDefault(); + + // no modifications if new tab is requested + if (self.options.target === '_blank') + return true; + + // load into frame + Galaxy.frame.add(options); + }); + + // append divider + if (menuOptions.divider) + this.$menu.append($(this._templateDivider())); + }, + + // show menu on header click + _head: function(e) + { + // prevent default + e.preventDefault(); + + if (this.options.disabled){ + return // prevent link following if menu item is disabled + } + + // check for menu options + if (!this.$menu) { + Galaxy.frame.add(this.options); + } + }, + + _attachPopover : function() + { + var $popover_element = $(this.el).find('.head'); + $popover_element.popover({ + html: true, + content: 'Please log in or register to use this feature.', + placement: 'bottom' + }).on('shown.bs.popover', function() { // hooking on bootstrap event to automatically hide popovers after delay + setTimeout(function() { + $popover_element.popover('hide'); + }, 5000); + }); + }, + + // fill template header + _templateMenuItem: function (options) + { + return '
  • ' + options.title + '
  • '; + }, + + // fill template header + _templateMenu: function () + { + return ''; + }, + + _templateDivider: function() + { + return '
  • '; + }, + + // fill template + _template: function (options) + { + // start template + var tmpl = ''; + + // return template + return tmpl; + } +}); + +// return +return { + GalaxyMasthead: GalaxyMasthead, + GalaxyMastheadTab: GalaxyMastheadTab, + GalaxyMastheadIcon: GalaxyMastheadIcon +}; + +}); diff --git a/client/scripts/galaxy.menu.js b/client/scripts/galaxy.menu.js new file mode 100644 index 00000000000..76f493ee9ca --- /dev/null +++ b/client/scripts/galaxy.menu.js @@ -0,0 +1,358 @@ +/* + galaxy menu +*/ + +// dependencies +define(["galaxy.masthead"], function(mod_masthead) { + +// frame manager +var GalaxyMenu = Backbone.Model.extend( +{ + // options + options: null, + + // link masthead class + masthead: null, + + // initialize + initialize: function(options) + { + this.options = options.config; + this.masthead = options.masthead; + this.create(); + }, + + // default menu + create: function() + { + // + // Analyze data tab. + // + var tab_analysis = new mod_masthead.GalaxyMastheadTab({ + id : "analysis", + title : "Analyze Data", + content : "root/index", + title_attribute : 'Analysis home view' + }); + this.masthead.append(tab_analysis); + + // + // Workflow tab. + // + + var workflow_options = { + id : "workflow", + title : "Workflow", + content : "workflow", + title_attribute : 'Chain tools into workflows' + } + if (!this.options.user.valid){ + workflow_options.disabled = true; // disable workflows for anonymous users + } + + var tab_workflow = new mod_masthead.GalaxyMastheadTab(workflow_options); + this.masthead.append(tab_workflow); + + // + // 'Shared Items' or Libraries tab. + // + var tab_shared = new mod_masthead.GalaxyMastheadTab({ + id : "shared", + title : "Shared Data", + content : "library/index", + title_attribute : 'Access published resources' + }); + + tab_shared.add({ + title : "Data Libraries", + content : "library/index" + }); + + tab_shared.add({ + title : "Data Libraries Beta", + content : "library/list", + divider : true + }); + + tab_shared.add({ + title : "Published Histories", + content : "history/list_published" + }); + + tab_shared.add({ + title : "Published Workflows", + content : "workflow/list_published" + + }); + + tab_shared.add({ + title : "Published Visualizations", + content : "visualization/list_published" + }); + + tab_shared.add({ + title : "Published Pages", + content : "page/list_published" + }); + + this.masthead.append(tab_shared); + + // + // Lab menu. + // + if (this.options.user.requests) + { + var tab_lab = new mod_masthead.GalaxyMastheadTab({ + id : "lab", + title : "Lab" + }); + tab_lab.add({ + title : "Sequencing Requests", + content : "requests/index" + }); + tab_lab.add({ + title : "Find Samples", + content : "requests/find_samples_index" + }); + tab_lab.add({ + title : "Help", + content : this.options.lims_doc_url + }); + this.masthead.append(tab_lab); + } + + // + // Visualization tab. + // + + var visualization_options = { + id : "visualization", + title : "Visualization", + content : "visualization/list", + title_attribute : 'Visualize datasets' + } + if (!this.options.user.valid){ + visualization_options.disabled = true; // disable visualizations for anonymous users + } + + var tab_visualization = new mod_masthead.GalaxyMastheadTab(visualization_options); + + if (this.options.user.valid) //add submenu only when user is logged in + { + tab_visualization.add({ + title : "New Track Browser", + content : "visualization/trackster", + target : "_frame" + }); + tab_visualization.add({ + title : "Saved Visualizations", + content : "visualization/list", + target : "_frame" + }); + } + this.masthead.append(tab_visualization); + + // + // Cloud menu. + // + if (this.options.enable_cloud_launch) + { + var tab_cloud = new mod_masthead.GalaxyMastheadTab({ + id : "cloud", + title : "Cloud", + content : "cloudlaunch/index" + }); + tab_cloud.add({ + title : "New Cloud Cluster", + content : "cloudlaunch/index" + }); + this.masthead.append(tab_cloud); + } + + // + // Admin. + // + if (this.options.is_admin_user) + { + var tab_admin = new mod_masthead.GalaxyMastheadTab({ + id : "admin", + title : "Admin", + content : "admin/index", + extra_class : "admin-only", + title_attribute : 'Administer this Galaxy' + }); + this.masthead.append(tab_admin); + } + + // + // Help tab. + // + var tab_help = new mod_masthead.GalaxyMastheadTab({ + id : "help", + title : "Help", + title_attribute : 'Support, contact, and community hubs' + }); + if (this.options.biostar_url) + { + tab_help.add({ + title : "Galaxy Biostar", + content : this.options.biostar_url_redirect, + target : "_blank" + }); + tab_help.add({ + title : "Ask a question", + content : "biostar/biostar_question_redirect", + target : "_blank" + }); + } + tab_help.add({ + title : "Support", + content : this.options.support_url, + target : "_blank" + }); + tab_help.add({ + title : "Search", + content : this.options.search_url, + target : "_blank" + }); + tab_help.add({ + title : "Mailing Lists", + content : this.options.mailing_lists, + target : "_blank" + }); + tab_help.add({ + title : "Videos", + content : this.options.screencasts_url, + target : "_blank" + }); + tab_help.add({ + title : "Wiki", + content : this.options.wiki_url, + target : "_blank" + }); + tab_help.add({ + title : "How to Cite Galaxy", + content : this.options.citation_url, + target : "_blank" + }); + if (this.options.terms_url) + { + tab_help.add({ + title : "Terms and Conditions", + content : this.options.terms_url, + target : "_blank" + }); + } + this.masthead.append(tab_help); + + // + // User tab. + // + if (!this.options.user.valid) + { + var tab_user = new mod_masthead.GalaxyMastheadTab({ + id : "user", + title : "User", + extra_class : "loggedout-only", + title_attribute : 'Account registration or login' + }); + + // login + tab_user.add({ + title : "Login", + content : "user/login", + target : "galaxy_main" + }); + + // register + if (this.options.allow_user_creation) + { + tab_user.add({ + title : "Register", + content : "user/create", + target : "galaxy_main" + }); + } + + // add to masthead + this.masthead.append(tab_user); + } else { + var tab_user = new mod_masthead.GalaxyMastheadTab({ + id : "user", + title : "User", + extra_class : "loggedin-only", + title_attribute : 'Account preferences and saved data' + }); + + // show user logged in info + tab_user.add({ + title : "Logged in as " + this.options.user.email + }); + + tab_user.add({ + title : "Preferences", + content : "user?cntrller=user", + target : "galaxy_main" + }); + + tab_user.add({ + title : "Custom Builds", + content : "user/dbkeys", + target : "galaxy_main" + }); + + tab_user.add({ + title : "Logout", + content : "user/logout", + target : "_top", + divider : true + }); + + // default tabs + tab_user.add({ + title : "Saved Histories", + content : "history/list", + target : "galaxy_main" + }); + tab_user.add({ + title : "Saved Datasets", + content : "dataset/list", + target : "galaxy_main" + }); + tab_user.add({ + title : "Saved Pages", + content : "page/list", + target : "_top" + }); + + tab_user.add({ + title : "API Keys", + content : "user/api_keys?cntrller=user", + target : "galaxy_main" + }); + + if (this.options.use_remote_user) + { + tab_user.add({ + title : "Public Name", + content : "user/edit_username?cntrller=user", + target : "galaxy_main" + }); + } + + // add to masthead + this.masthead.append(tab_user); + } + + // identify active tab + if (this.options.active_view) + this.masthead.highlight(this.options.active_view); + } +}); + +// return +return { + GalaxyMenu: GalaxyMenu +}; + +}); diff --git a/client/scripts/galaxy.pages.js b/client/scripts/galaxy.pages.js new file mode 100644 index 00000000000..b1328fcc4dc --- /dev/null +++ b/client/scripts/galaxy.pages.js @@ -0,0 +1,642 @@ +// Useful Galaxy stuff. +var Galaxy = +{ + // Item types. + ITEM_HISTORY : "item_history", + ITEM_DATASET : "item_dataset", + ITEM_WORKFLOW : "item_workflow", + ITEM_PAGE : "item_page", + ITEM_VISUALIZATION : "item_visualization", + + // Link dialogs. + DIALOG_HISTORY_LINK : "link_history", + DIALOG_DATASET_LINK : "link_dataset", + DIALOG_WORKFLOW_LINK : "link_workflow", + DIALOG_PAGE_LINK : "link_page", + DIALOG_VISUALIZATION_LINK : "link_visualization", + + // Embed dialogs. + DIALOG_EMBED_HISTORY : "embed_history", + DIALOG_EMBED_DATASET : "embed_dataset", + DIALOG_EMBED_WORKFLOW : "embed_workflow", + DIALOG_EMBED_PAGE : "embed_page", + DIALOG_EMBED_VISUALIZATION : "embed_visualization" +}; + +// Initialize Galaxy elements. +function init_galaxy_elts(wym) +{ + // Set up events to make annotation easy. + $('.annotation', wym._doc.body).each( function() + { + $(this).click( function() { + // Works in Safari, not in Firefox. + var range = wym._doc.createRange(); + range.selectNodeContents( this ); + var selection = window.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); + var t = ""; + }); + }); + +}; + +// Based on the dialog type, return a dictionary of information about an item +function get_item_info( dialog_type ) +{ + var + item_singular, + item_plural, + item_controller; + switch( dialog_type ) { + case( Galaxy.ITEM_HISTORY ): + item_singular = "History"; + item_plural = "Histories"; + item_controller = "history"; + item_class = "History"; + break; + case( Galaxy.ITEM_DATASET ): + item_singular = "Dataset"; + item_plural = "Datasets"; + item_controller = "dataset"; + item_class = "HistoryDatasetAssociation"; + break; + case( Galaxy.ITEM_WORKFLOW ): + item_singular = "Workflow"; + item_plural = "Workflows"; + item_controller = "workflow"; + item_class = "StoredWorkflow"; + break; + case( Galaxy.ITEM_PAGE ): + item_singular = "Page"; + item_plural = "Pages"; + item_controller = "page"; + item_class = "Page"; + break; + case( Galaxy.ITEM_VISUALIZATION ): + item_singular = "Visualization"; + item_plural = "Visualizations"; + item_controller = "visualization"; + item_class = "Visualization"; + break; + } + + // Build ajax URL that lists items for selection. + var item_list_action = "list_" + item_plural.toLowerCase() + "_for_selection"; + var ajax_url = list_objects_url.replace( "LIST_ACTION", item_list_action ); + + // Set up and return dict. + return { + singular : item_singular, + plural : item_plural, + controller : item_controller, + iclass : item_class, + list_ajax_url : ajax_url + }; +}; + +// Make an item importable. +function make_item_importable( item_controller, item_id, item_type ) +{ + ajax_url = set_accessible_url.replace( "ITEM_CONTROLLER", item_controller ); + $.ajax({ + type: "POST", + url: ajax_url, + data: { id: item_id, accessible: 'True' }, + error: function() { alert("Making " + item_type + " accessible failed"); } + }); +}; + +// Completely replace WYM's dialog handling +WYMeditor.editor.prototype.dialog = function( dialogType, dialogFeatures, bodyHtml ) { + + var wym = this; + var sStamp = wym.uniqueStamp(); + var selected = wym.selected(); + + // Swap out URL attribute for id/name attribute in link creation to enable anchor creation in page. + function set_link_id() + { + // When "set link id" link clicked, update UI. + $('#set_link_id').click( function() + { + // Set label. + $("#link_attribute_label").text("ID/Name"); + + // Set input elt class, value. + var attribute_input = $(".wym_href"); + attribute_input.addClass("wym_id").removeClass("wym_href"); + if (selected) + attribute_input.val( $(selected).attr('id') ); + + // Remove link. + $(this).remove(); + }); + } + + // LINK DIALOG + if ( dialogType == WYMeditor.DIALOG_LINK ) { + if(selected) { + $(wym._options.hrefSelector).val($(selected).attr(WYMeditor.HREF)); + $(wym._options.srcSelector).val($(selected).attr(WYMeditor.SRC)); + $(wym._options.titleSelector).val($(selected).attr(WYMeditor.TITLE)); + $(wym._options.altSelector).val($(selected).attr(WYMeditor.ALT)); + } + // Get current URL, title. + var curURL, curTitle; + if (selected) + { + curURL = $(selected).attr("href"); + if (curURL == undefined) + curURL = ""; + curTitle = $(selected).attr("title"); + if (curTitle == undefined) + curTitle = ""; + } + show_modal( + "Create Link", + "

    " + + "

    ", + { + "Make link": function() { + // Get URL, name/title. + var sUrl = $(wym._options.hrefSelector).val() || '', + sId = $(".wym_id").val() || '', + sName = $(wym._options.titleSelector).val() || ''; + + if (sUrl || sId) { + // Create link. + wym._exec(WYMeditor.CREATE_LINK, sStamp); + + // Set link attributes. + var link = $("a[href=" + sStamp + "]", wym._doc.body); + link.attr(WYMeditor.HREF, sUrl) + .attr(WYMeditor.TITLE, sName) + .attr("id", sId); + + // If link's text is default (wym-...), change it to the title. + if (link.text().indexOf('wym-') === 0) { + link.text(sName); + } + } + hide_modal(); + }, + "Cancel": function() { + hide_modal(); + } + }, + {}, + set_link_id + ); + } + + // IMAGE DIALOG + if ( dialogType == WYMeditor.DIALOG_IMAGE ) { + if(wym._selected_image) { + $(wym._options.dialogImageSelector + " " + wym._options.srcSelector) + .val($(wym._selected_image).attr(WYMeditor.SRC)); + $(wym._options.dialogImageSelector + " " + wym._options.titleSelector) + .val($(wym._selected_image).attr(WYMeditor.TITLE)); + $(wym._options.dialogImageSelector + " " + wym._options.altSelector) + .val($(wym._selected_image).attr(WYMeditor.ALT)); + } + show_modal( + "Image", + "
    " + + "
    " + + "" + + "
    " + + "
    " + + "
    " + + "" + + "
    " + + "
    " + + "
    " + + "" + + "
    ", + { + "Insert": function() { + var sUrl = $(wym._options.srcSelector).val(); + if(sUrl.length > 0) { + wym._exec(WYMeditor.INSERT_IMAGE, sStamp); + $("img[src$=" + sStamp + "]", wym._doc.body) + .attr(WYMeditor.SRC, sUrl) + .attr(WYMeditor.TITLE, $(wym._options.titleSelector).val()) + .attr(WYMeditor.ALT, $(wym._options.altSelector).val()); + } + hide_modal(); + }, + "Cancel": function() { + hide_modal(); + } + } + ); + return; + } + + // TABLE DIALOG + if ( dialogType == WYMeditor.DIALOG_TABLE ) { + show_modal( + "Table", + "
    " + + "
    " + + "" + + "
    " + + "
    " + + "
    " + + "" + + "
    " + + "
    " + + "" + + "" + + "
    " + + "
    " + + "" + + "" + + "
    ", + { + "Insert": function() { + var iRows = $(wym._options.rowsSelector).val(); + var iCols = $(wym._options.colsSelector).val(); + + if(iRows > 0 && iCols > 0) { + + var table = wym._doc.createElement(WYMeditor.TABLE); + var newRow = null; + var newCol = null; + + var sCaption = $(wym._options.captionSelector).val(); + + //we create the caption + var newCaption = table.createCaption(); + newCaption.innerHTML = sCaption; + + //we create the rows and cells + for(x=0; x" + + "Make the selected " + item_info.plural.toLowerCase() + " accessible so that they can viewed by everyone.
    " + , + { + "Insert": function() + { + // Make selected items accessible (importable) ? + var make_importable = false; + if ( $('#make-importable:checked').val() !== null ) + make_importable = true; + + // Insert links to history for each checked item. + var item_ids = new Array(); + $('input[name=id]:checked').each(function() { + var item_id = $(this).val(); + + // Make item importable? + if (make_importable) + make_item_importable(item_info.controller, item_id, item_info.singular); + + // Insert link(s) to item(s). This is done by getting item info and then manipulating wym. + url_template = get_name_and_link_url + item_id; + ajax_url = url_template.replace( "ITEM_CONTROLLER", item_info.controller); + $.getJSON( ajax_url, function( returned_item_info ) { + // Get link text. + wym._exec(WYMeditor.CREATE_LINK, sStamp); + var link_text = $("a[href=" + sStamp + "]", wym._doc.body).text(); + + // Insert link: need to do different actions depending on link text. + if ( + link_text == "" // Firefox. + || + link_text == sStamp // Safari + ) + { + // User selected no text; create link from scratch and use default text. + wym.insert("" + item_info.singular + " '" + returned_item_info.name + "'"); + } + else + { + // Link created from selected text; add href and title. + $("a[href=" + sStamp + "]", wym._doc.body).attr(WYMeditor.HREF, returned_item_info.link).attr(WYMeditor.TITLE, item_info.singular + item_id); + } + }); + }); + + hide_modal(); + }, + "Cancel": function() + { + hide_modal(); + } + } + ); + } + }); + } + // EMBED GALAXY OBJECT DIALOGS + if ( dialogType == Galaxy.DIALOG_EMBED_HISTORY || dialogType == Galaxy.DIALOG_EMBED_DATASET || dialogType == Galaxy.DIALOG_EMBED_WORKFLOW || dialogType == Galaxy.DIALOG_EMBED_PAGE || dialogType == Galaxy.DIALOG_EMBED_VISUALIZATION ) { + // Based on item type, set useful vars. + var item_info; + switch(dialogType) + { + case(Galaxy.DIALOG_EMBED_HISTORY): + item_info = get_item_info(Galaxy.ITEM_HISTORY); + break; + case(Galaxy.DIALOG_EMBED_DATASET): + item_info = get_item_info(Galaxy.ITEM_DATASET); + break; + case(Galaxy.DIALOG_EMBED_WORKFLOW): + item_info = get_item_info(Galaxy.ITEM_WORKFLOW); + break; + case(Galaxy.DIALOG_EMBED_PAGE): + item_info = get_item_info(Galaxy.ITEM_PAGE); + break; + case(Galaxy.DIALOG_EMBED_VISUALIZATION): + item_info = get_item_info(Galaxy.ITEM_VISUALIZATION); + break; + } + + $.ajax( + { + url: item_info.list_ajax_url, + data: {}, + error: function() { alert( "Failed to list " + item_info.plural.toLowerCase() + " for selection"); }, + success: function(list_html) + { + // Can make histories, workflows importable; cannot make datasets importable. + if (dialogType == Galaxy.DIALOG_EMBED_HISTORY || dialogType == Galaxy.DIALOG_EMBED_WORKFLOW + || dialogType == Galaxy.DIALOG_EMBED_VISUALIZATION) + list_html = list_html + "
    " + + "Make the selected " + item_info.plural.toLowerCase() + " accessible so that they can viewed by everyone.
    "; + show_modal( + "Embed " + item_info.plural, + list_html, + { + "Embed": function() + { + // Make selected items accessible (importable) ? + var make_importable = false; + if ( $('#make-importable:checked').val() != null ) + make_importable = true; + + $('input[name=id]:checked').each(function() { + // Get item ID and name. + var item_id = $(this).val(); + // Use ':first' because there are many labels in table; the first one is the item name. + var item_name = $("label[for='" + item_id + "']:first").text(); + + if (make_importable) + make_item_importable(item_info.controller, item_id, item_info.singular); + + // Embedded item HTML; item class is embedded in div container classes; this is necessary because the editor strips + // all non-standard attributes when it returns its content (e.g. it will not return an element attribute of the form + // item_class='History'). + var item_elt_id = item_info.iclass + "-" + item_id; + var item_embed_html = [ + "
    ", + "

    ", + "Embedded Galaxy ", item_info.singular, " '", item_name, "'", + "

    ", + "

    ", + "[Do not edit this block; Galaxy will fill it in with the annotated ", + item_info.singular.toLowerCase(), " when it is displayed.]", + "

    ", + "
    " ].join( '' ); + + // Insert embedded item into document. + wym.insert(item_embed_html); + + }); + hide_modal(); + }, + "Cancel": function() + { + hide_modal(); + } + } + ); + } + }); + } +}; + +$(function(){ + // Generic error handling + $(document).ajaxError( function ( e, x ) { + // console.log( e, x ); + var message = x.responseText || x.statusText || "Could not connect to server"; + show_modal( "Server error", message, { "Ignore error" : hide_modal } ); + return false; + }); + // Create editor + $("[name=page_content]").wymeditor( { + skin: 'galaxy', + basePath: editor_base_path, + iframeBasePath: iframe_base_path, + boxHtml: "" + + "" + + "
    " + + WYMeditor.TOOLS + + "
    " + + "
    " + // + WYMeditor.HTML + + WYMeditor.IFRAME + + WYMeditor.STATUS + + "
    " + + "" + + "
    ", + toolsItems: [ + {'name': 'Bold', 'title': 'Strong', 'css': 'wym_tools_strong'}, + {'name': 'Italic', 'title': 'Emphasis', 'css': 'wym_tools_emphasis'}, + {'name': 'Superscript', 'title': 'Superscript', 'css': 'wym_tools_superscript'}, + {'name': 'Subscript', 'title': 'Subscript', 'css': 'wym_tools_subscript'}, + {'name': 'InsertOrderedList', 'title': 'Ordered_List', 'css': 'wym_tools_ordered_list'}, + {'name': 'InsertUnorderedList', 'title': 'Unordered_List', 'css': 'wym_tools_unordered_list'}, + {'name': 'Indent', 'title': 'Indent', 'css': 'wym_tools_indent'}, + {'name': 'Outdent', 'title': 'Outdent', 'css': 'wym_tools_outdent'}, + {'name': 'Undo', 'title': 'Undo', 'css': 'wym_tools_undo'}, + {'name': 'Redo', 'title': 'Redo', 'css': 'wym_tools_redo'}, + {'name': 'CreateLink', 'title': 'Link', 'css': 'wym_tools_link'}, + {'name': 'Unlink', 'title': 'Unlink', 'css': 'wym_tools_unlink'}, + {'name': 'InsertImage', 'title': 'Image', 'css': 'wym_tools_image'}, + {'name': 'InsertTable', 'title': 'Table', 'css': 'wym_tools_table'}, + ] + }); + // Get the editor object + var editor = $.wymeditors(0); + var save = function ( callback ) { + show_modal( "Saving page", "progress" ); + + // Do save. + $.ajax( { + url: save_url, + type: "POST", + data: { + id: page_id, + content: editor.xhtml(), + annotations: JSON.stringify(new Object()), + // annotations: JSON.stringify(annotations), + "_": "true" + }, + success: function() { + callback(); + } + }); + } + // Save button + $("#save-button").click( function() { + save( function() { hide_modal(); } ) + }); + // Close button + $("#close-button").click(function() { + // var new_content = editor.xhtml(); + // var changed = ( initial_content != new_content ); + var changed = false; + if ( changed ) { + var do_close = function() { + window.onbeforeunload = undefined; + window.document.location = page_list_url; + }; + show_modal( "Close editor", + "There are unsaved changes to your page which will be lost.", + { + "Cancel" : hide_modal, + "Save Changes" : function() { + save( do_close ); + } + }, { + "Don't Save": do_close + } ); + } else { + window.document.location = page_list_url; + } + }); + + // Initialize galaxy elements. + //init_galaxy_elts(editor); + + // + // Containers, Galaxy style + // + var containers_menu = $(""); + $(".wym_area_top").append( containers_menu ); + + // Add menu options. + var items = {} + $.each( editor._options.containersItems, function( k, v ) { + var tagname = v.name; + items[ v.title.replace( '_', ' ' ) ] = function() { editor.container( tagname ) } + }); + make_popupmenu( containers_menu, items); + + // + // Create 'Insert Link to Galaxy Object' menu. + // + + // Add menu button. + var insert_link_menu_button = $("
    Insert Link to Galaxy Object
    ").addClass('galaxy-page-editor-button'); + $(".wym_area_top").append(insert_link_menu_button); + + // Add menu options. + make_popupmenu( insert_link_menu_button, { + "Insert History Link": function() { + editor.dialog(Galaxy.DIALOG_HISTORY_LINK); + }, + "Insert Dataset Link": function() { + editor.dialog(Galaxy.DIALOG_DATASET_LINK); + }, + "Insert Workflow Link": function() { + editor.dialog(Galaxy.DIALOG_WORKFLOW_LINK); + }, + "Insert Page Link": function() { + editor.dialog(Galaxy.DIALOG_PAGE_LINK); + }, + "Insert Visualization Link": function() { + editor.dialog(Galaxy.DIALOG_VISUALIZATION_LINK); + }, + }); + + // + // Create 'Embed Galaxy Object' menu. + // + + // Add menu button. + var embed_object_button = $("
    Embed Galaxy Object
    ").addClass('galaxy-page-editor-button'); + $(".wym_area_top").append(embed_object_button); + + // Add menu options. + make_popupmenu( embed_object_button, { + "Embed History": function() { + editor.dialog(Galaxy.DIALOG_EMBED_HISTORY); + }, + "Embed Dataset": function() { + editor.dialog(Galaxy.DIALOG_EMBED_DATASET); + }, + "Embed Workflow": function() { + editor.dialog(Galaxy.DIALOG_EMBED_WORKFLOW); + }, + "Embed Visualization": function() { + editor.dialog(Galaxy.DIALOG_EMBED_VISUALIZATION); + }, + //"Embed Page": function() { + // editor.dialog(Galaxy.DIALOG_EMBED_PAGE); + //} + }); +}); diff --git a/client/scripts/galaxy.panels.js b/client/scripts/galaxy.panels.js new file mode 100644 index 00000000000..d3adc6d6e9b --- /dev/null +++ b/client/scripts/galaxy.panels.js @@ -0,0 +1,286 @@ +!function( exports, $ ){ + +"use strict" + +var ensure_dd_helper = function () { + // Insert div that covers everything when dragging the borders + if ( $( "#DD-helper" ).length == 0 ) { + $( "
    " ).appendTo( "body" ).hide(); + } +} + +// Panels + +var MIN_PANEL_WIDTH = 160, + MAX_PANEL_WIDTH = 800; + +var Panel = function( options ) { + this.$panel = options.panel; + this.$center = options.center; + this.$drag = options.drag; + this.$toggle = options.toggle; + this.left = !options.right; + this.hidden = false; + this.hidden_by_tool = false; + this.saved_size = null; + this.init(); +} +$.extend( Panel.prototype, { + resize: function( x ) { + this.$panel.css( "width", x ); + if ( this.left ) { + this.$center.css( "left", x ); + } else { + this.$center.css( "right", x ); + } + // ie7-recalc.js + if ( document.recalc ) { document.recalc(); } + }, + do_toggle: function() { + var self = this; + if ( this.hidden ) { + this.$toggle.removeClass( "hidden" ); + if ( this.left ) { + this.$panel.css( "left", - this.saved_size ).show().animate( { "left": 0 }, "fast", function () { + self.resize( self.saved_size ); + }); + } else { + this.$panel.css( "right", - this.saved_size ).show().animate( { "right": 0 }, "fast", function () { + self.resize( self.saved_size ); + }); + } + self.hidden = false; + } else { + self.saved_size = this.$panel.width(); + if ( document.recalc ) { document.recalc(); } + // Hide border + if ( this.left ) { + this.$panel.animate( { left: - this.saved_size }, "fast" ); + } else { + this.$panel.animate( { right: - this.saved_size }, "fast" ); + } + // self.resize(0); + if ( this.left ) { + this.$center.css( "left", 0 ); + } else { + this.$center.css( "right", 0 ); + } + + self.hidden = true; + self.$toggle.addClass( "hidden" ); + } + this.hidden_by_tool = false; + }, + handle_minwidth_hint: function( x ) { + var space = this.$center.width() - ( this.hidden ? this.saved_size : 0 ); + if ( space < x ) + { + if ( ! this.hidden ) { + this.do_toggle(); + this.hidden_by_tool = true; + } + } else { + if ( this.hidden_by_tool ) { + this.do_toggle(); + this.hidden_by_tool = false; + } + } + }, + force_panel: function( op ) { + if ( ( this.hidden && op == 'show' ) || ( ! this.hidden && op == 'hide' ) ) { + this.do_toggle(); + } + }, + init: function() { + var self = this, + prevX; + // Pull the collapse element out to body level so it is visible when panel is hidden + self.$toggle.remove().appendTo( "body" ); + // Hide/show using toggle element + self.$toggle.on( "click", function() { self.do_toggle(); } ); + + // Resizing using drag element + function move( e ){ + var delta = e.pageX - prevX; + prevX = e.pageX; + + var oldWidth = self.$panel.width(), + newWidth = ( self.left )?( oldWidth + delta ):( oldWidth - delta ); + // Limit range + newWidth = Math.min( MAX_PANEL_WIDTH, Math.max( MIN_PANEL_WIDTH, newWidth ) ); + self.resize( newWidth ); + } + this.$drag.on( "mousedown", function( e ) { + prevX = e.pageX; + $( '#DD-helper' ).show() + .on( 'mousemove', move ) + .one( 'mouseup', function( e ){ + $( this ).hide().off( 'mousemove', move ); + }); + }); + } +}); + +// Modal dialog boxes +var Modal = function( options ) { + this.$overlay = options.overlay; + this.$dialog = options.dialog; + this.$header = this.$dialog.find( ".modal-header" ); + this.$body = this.$dialog.find( ".modal-body" ); + this.$footer = this.$dialog.find( ".modal-footer" ); + this.$backdrop = options.backdrop; + // Close button + this.$header.find( ".close" ).on( "click", $.proxy( this.hide, this ) ); +} +$.extend( Modal.prototype, { + setContent: function( options ) { + this.$header.hide(); + // Title + if ( options.title ) { + this.$header.find( ".title" ).html( options.title ); + this.$header.show(); + } + if ( options.closeButton ) { + this.$header.find( ".close" ).show(); + this.$header.show(); + } else { + this.$header.find( ".close" ).hide(); + } + // Buttons + this.$footer.hide(); + var $buttons = this.$footer.find( ".buttons" ).html( "" ); + if ( options.buttons ) { + $.each( options.buttons, function( name, value ) { + $buttons.append( $( ' ' ).text( name ).click( value ) ).append( " " ); + }); + this.$footer.show(); + } + var $extraButtons = this.$footer.find( ".extra_buttons" ).html( "" ); + if ( options.extra_buttons ) { + $.each( options.extra_buttons, function( name, value ) { + $extraButtons.append( $( '' ).text( name ).click( value ) ).append( " " ); + }); + this.$footer.show(); + } + // Body + var body = options.body; + if ( body == "progress" ) { + body = $("
    "); + } + this.$body.html( body ); + }, + show: function( options, callback ) { + if ( ! this.$dialog.is( ":visible" ) ) { + if ( options.backdrop) { + this.$backdrop.addClass( "in" ); + } else { + this.$backdrop.removeClass( "in" ); + } + this.$overlay.show(); + this.$dialog.show(); + this.$overlay.addClass("in"); + // Fix min-width so that modal cannot shrink considerably if new content is loaded. + this.$body.css( "min-width", this.$body.width() ); + // Set max-height so that modal does not exceed window size and is in middle of page. + // TODO: this could perhaps be handled better using CSS. + this.$body.css( "max-height", + $(window).height() - + this.$footer.outerHeight() - + this.$header.outerHeight() - + parseInt( this.$dialog.css( "padding-top" ), 10 ) - + parseInt( this.$dialog.css( "padding-bottom" ), 10 ) + ); + } + // Callback on init + if ( callback ) { + callback(); + } + }, + hide: function() { + var modal = this; + modal.$dialog.fadeOut( function() { + modal.$overlay.hide(); + modal.$backdrop.removeClass( "in" ); + modal.$body.children().remove(); + // Clear min-width to allow for modal to take size of new body. + modal.$body.css( "min-width", undefined ); + }); + } +}); + +var modal; + +$(function(){ + modal = new Modal( { overlay: $("#top-modal"), dialog: $("#top-modal-dialog"), backdrop: $("#top-modal-backdrop") } ); +}); + +// Backward compatibility +function hide_modal() { + modal.hide(); +} +function show_modal( title, body, buttons, extra_buttons, init_fn ) { + modal.setContent( { title: title, body: body, buttons: buttons, extra_buttons: extra_buttons } ); + modal.show( { backdrop: true }, init_fn ); +} +function show_message( title, body, buttons, extra_buttons, init_fn ) { + modal.setContent( { title: title, body: body, buttons: buttons, extra_buttons: extra_buttons } ); + modal.show( { backdrop: false }, init_fn ); +} +function show_in_overlay( options ) { + var width = options.width || '600'; + var height = options.height || '400'; + var scroll = options.scroll || 'auto'; + $("#overlay-background").bind( "click.overlay", function() { + hide_modal(); + $("#overlay-background").unbind( "click.overlay" ); + }); + modal.setContent( { closeButton: true, title: " ", body: $( "
    " ) } ); + modal.show( { backdrop: true } ); +} + +function user_changed( user_email, is_admin ) { + if ( user_email ) { + $(".loggedin-only").show(); + $(".loggedout-only").hide(); + $("#user-email").text( user_email ); + if ( is_admin ) { + $(".admin-only").show(); + } + } else { + $(".loggedin-only").hide(); + $(".loggedout-only").show(); + $(".admin-only").hide(); + } +} + +// Masthead dropdown menus +$(function() { + var $dropdowns = $("#masthead ul.nav > li.dropdown > .dropdown-menu"); + $("body").on( "click.nav_popups", function( e ) { + $dropdowns.hide(); + $("#DD-helper").hide(); + // If the target is in the menu, treat normally + if ( $(e.target).closest( "#masthead ul.nav > li.dropdown > .dropdown-menu" ).length ) { + return; + } + // Otherwise, was the click in a tab + var $clicked = $(e.target).closest( "#masthead ul.nav > li.dropdown" ); + if ( $clicked.length ) { + $("#DD-helper").show(); + $clicked.children( ".dropdown-menu" ).show(); + e.preventDefault(); + } + }); +}); + +// Exports +exports.ensure_dd_helper = ensure_dd_helper; +exports.Panel = Panel; +exports.Modal = Modal; +exports.hide_modal = hide_modal; +exports.show_modal = show_modal; +exports.show_message = show_message; +exports.show_in_overlay = show_in_overlay; +exports.user_changed = user_changed; + +}( window, window.jQuery ); diff --git a/client/scripts/galaxy.tools.js b/client/scripts/galaxy.tools.js new file mode 100644 index 00000000000..da57624c31f --- /dev/null +++ b/client/scripts/galaxy.tools.js @@ -0,0 +1,158 @@ +// dependencies +define([ "libs/underscore", "mvc/tools" ], function( _, Tools ) { + + var checkUncheckAll = function( name, check ) { + $("input[name='" + name + "'][type='checkbox']").attr('checked', !!check); + } + + $(".tool-share-link").each( function() { + var href = $(this).attr("href"); + var href = $(this).attr("data-link"); + $(this).click(function() { + window.prompt("Copy to clipboard: Ctrl+C, Enter", href); + }); + }); + + // Inserts the Select All / Unselect All buttons for checkboxes + $("div.checkUncheckAllPlaceholder").each( function() { + var check_name = $(this).attr("checkbox_name"); + select_link = $("").text("Select All").click(function() { + checkUncheckAll(check_name, true); + }); + unselect_link = $("").text("Unselect All").click(function() { + checkUncheckAll(check_name, false); + }); + $(this).append(select_link).append(" ").append(unselect_link); + }); + + var SELECTION_TYPE = { + 'select_single': { + 'icon_class': 'fa-file-o', + 'select_by': 'Run tool on single input', + 'allow_remap': true + }, + 'select_multiple': { + 'icon_class': 'fa-files-o', + 'select_by': 'Run tool in parallel across multiple datasets', + 'allow_remap': false, + 'min_option_count': 2 // Don't show multiple select switch if only + // one dataset available. + }, + 'select_collection': { + 'icon_class': 'fa-folder-o', + 'select_by': 'Run tool in parallel across dataset collection', + 'allow_remap': false + }, + 'multiselect_single': { + 'icon_class': 'fa-list-alt', + 'select_by': 'Run tool over multiple datasets', + 'allow_remap': true + }, + 'multiselect_collection': { + 'icon_class': 'fa-folder-o', + 'select_by': 'Run tool over dataset collection', + 'allow_remap': false, + }, + 'select_single_collection': { + 'icon_class': 'fa-file-o', + 'select_by': 'Run tool on single collection', + 'allow_remap': true + }, + 'select_map_over_collections': { + 'icon_class': 'fa-folder-o', + 'select_by': 'Map tool over compontents of nested collection', + 'allow_remap': false, + } + }; + + var SwitchSelectView = Backbone.View.extend({ + initialize: function( data ) { + var defaultOption = data.default_option; + var defaultIndex = null; + var switchOptions = data.switch_options; + this.switchOptions = switchOptions; + this.prefix = data.prefix; + var el = this.$el; + var view = this; + + var index = 0; + var visibleCount = 0; + _.each( this.switchOptions, function( option, onValue ) { + var numValues = _.size( option.options ); + var selectionType = SELECTION_TYPE[ onValue ]; + var iIndex = index++; + var hidden = false; + if( defaultOption == onValue ) { + defaultIndex = iIndex; + } else if( numValues < ( selectionType.min_option_count || 1 ) ) { + hidden = true; + } + if( ! hidden ) { + visibleCount++; + var button = $('').click(function() { + view.enableSelectBy( iIndex, onValue ); + }).attr( + 'title', + selectionType['select_by'] + ).data( "index", iIndex ); + view.formRow().find( "label" ).append( button ); + } + }); + if( visibleCount < 2 ) { + // Don't show buttons to switch options... + view.formRow().find("i.runOptionIcon").hide(); + } + + if( defaultIndex != null) { + view.enableSelectBy( defaultIndex, defaultOption ); + } + }, + + formRow: function() { + return this.$el.closest( ".form-row" ); + }, + + render: function() { + }, + + enableSelectBy: function( enableIndex, onValue ) { + var selectionType = SELECTION_TYPE[onValue]; + if(selectionType["allow_remap"]) { + $("div#remap-row").css("display", "inherit"); + } else { + $("div#remap-row").css("display", "none"); + } + this.formRow().find( "i" ).each(function(_, iElement) { + var $iElement = $(iElement); + var index = $iElement.data("index"); + if(index == enableIndex) { + $iElement.css('color', 'black'); + } else { + $iElement.css('color', 'Gray'); + } + }); + var $select = this.$( "select" ); + var options = this.switchOptions[ onValue ]; + $select.attr( "name", this.prefix + options.name ); + $select.attr( "multiple", options.multiple ); + // Replace options regardless. + var select2ed = this.$(".select2-container").length > 0; + $select.html(""); // clear out select list + _.each( options.options, function( option ) { + var text = option[0]; + var value = option[1]; + var selected = option[2]; + $select.append($("