PageRenderTime 62ms CodeModel.GetById 22ms app.highlight 32ms RepoModel.GetById 1ms app.codeStats 0ms

/static/scripts/galaxy.base.js

https://bitbucket.org/cistrome/cistrome-harvard/
JavaScript | 715 lines | 519 code | 76 blank | 120 comment | 106 complexity | ee6cedfc61ad3fe84686c4f1bd033db1 MD5 | raw file
  1// requestAnimationFrame polyfill
  2(function() {
  3    var lastTime = 0;
  4    var vendors = ['ms', 'moz', 'webkit', 'o'];
  5    for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
  6        window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame'];
  7        window.cancelRequestAnimationFrame = window[vendors[x]+
  8          'CancelRequestAnimationFrame'];
  9    }
 10
 11    if (!window.requestAnimationFrame)
 12        window.requestAnimationFrame = function(callback, element) {
 13            var currTime = new Date().getTime();
 14            var timeToCall = Math.max(0, 16 - (currTime - lastTime));
 15            var id = window.setTimeout(function() { callback(currTime + timeToCall); },
 16              timeToCall);
 17            lastTime = currTime + timeToCall;
 18            return id;
 19        };
 20
 21    if (!window.cancelAnimationFrame)
 22        window.cancelAnimationFrame = function(id) {
 23            clearTimeout(id);
 24    };
 25}());
 26
 27// IE doesn't implement Array.indexOf
 28if (!Array.indexOf) {
 29    Array.prototype.indexOf = function(obj) {
 30        for (var i = 0, len = this.length; i < len; i++) {
 31            if (this[i] == obj) {
 32                return i;
 33            }
 34        }
 35        return -1;
 36    };
 37}
 38
 39// Returns the number of keys (elements) in an array/dictionary.
 40function obj_length(obj) {
 41    if (obj.length !== undefined) {
 42        return obj.length;
 43    }
 44
 45    var count = 0;
 46    for (var element in obj) {
 47        count++;
 48    }
 49    return count;
 50}
 51
 52$.fn.makeAbsolute = function(rebase) {
 53    return this.each(function() {
 54        var el = $(this);
 55        var pos = el.position();
 56        el.css({
 57            position: "absolute",
 58            marginLeft: 0, marginTop: 0,
 59            top: pos.top, left: pos.left,
 60            right: $(window).width() - ( pos.left + el.width() )
 61        });
 62        if (rebase) {
 63            el.remove().appendTo("body");
 64        }
 65    });
 66};
 67
 68/**
 69 * Sets up popupmenu rendering and binds options functions to the appropriate links.
 70 * initial_options is a dict with text describing the option pointing to either (a) a 
 71 * function to perform; or (b) another dict with two required keys, 'url' and 'action' (the 
 72 * function to perform. (b) is useful for exposing the underlying URL of the option.
 73 */
 74function make_popupmenu(button_element, initial_options) {
 75    /*  Use the $.data feature to store options with the link element.
 76        This allows options to be changed at a later time
 77    */
 78    var element_menu_exists = (button_element.data("menu_options"));
 79    button_element.data("menu_options", initial_options);
 80    
 81    // If element already has menu, nothing else to do since HTML and actions are already set.
 82    if (element_menu_exists) { return; }
 83    
 84    button_element.bind("click.show_popup", function(e) {
 85        // Close existing visible menus
 86        $(".popmenu-wrapper").remove();
 87        
 88        // Need setTimeouts so clicks don't interfere with each other
 89        setTimeout( function() {
 90            // Dynamically generate the wrapper holding all the selectable options of the menu.
 91            var menu_element = $( "<ul class='dropdown-menu' id='" + button_element.attr('id') + "-menu'></ul>" );
 92            var options = button_element.data("menu_options");
 93            if (obj_length(options) <= 0) {
 94                $("<li>No Options.</li>").appendTo(menu_element);
 95            }
 96            $.each( options, function( k, v ) {
 97                if (v) {
 98                    // Action can be either an anonymous function and a mapped dict.
 99                    var action = v.action || v;
100                    menu_element.append( $("<li></li>").append( $("<a>").attr("href", v.url).html(k).click(action) ) );
101                } else {
102                    menu_element.append( $("<li></li>").addClass( "head" ).append( $("<a href='#'></a>").html(k) ) );
103                }
104            });
105            var wrapper = $( "<div class='popmenu-wrapper' style='position: absolute;left: 0; top: -1000;'></div>" )
106                .append( menu_element ).appendTo( "body" );
107                   
108            var x = e.pageX - wrapper.width() / 2 ;
109            x = Math.min( x, $(document).scrollLeft() + $(window).width() - $(wrapper).width() - 5 );
110            x = Math.max( x, $(document).scrollLeft() + 5 );
111
112            wrapper.css({
113               top: e.pageY,
114               left: x
115            });
116        }, 10);
117        
118        setTimeout( function() {
119            // Bind click event to current window and all frames to remove any visible menus
120            // Bind to document object instead of window object for IE compat
121            var close_popup = function(el) {
122                $(el).bind("click.close_popup", function() {
123                    $(".popmenu-wrapper").remove();
124                    el.unbind("click.close_popup");
125                });
126            };
127            close_popup( $(window.document) ); // Current frame
128            close_popup( $(window.top.document) ); // Parent frame
129            for (var frame_id = window.top.frames.length; frame_id--;) { // Sibling frames
130                var frame = $(window.top.frames[frame_id].document);
131                close_popup(frame);
132            }
133        }, 50);
134        
135        return false;
136    });
137    
138}
139
140/**
141 *  Convert two seperate (often adjacent) divs into galaxy popupmenu
142 *  - div 1 contains a number of anchors which become the menu options
143 *  - div 1 should have a 'popupmenu' attribute
144 *  - this popupmenu attribute contains the id of div 2
145 *  - div 2 becomes the 'face' of the popupmenu
146 *
147 *  NOTE: make_popup_menus finds and operates on all divs with a popupmenu attr (no need to point it at something)
148 *          but (since that selector searches the dom on the page), you can send a parent in
149 *  NOTE: make_popup_menus, and make_popupmenu are horrible names
150 */
151function make_popup_menus( parent ) {
152    // find all popupmenu menu divs (divs that contains anchors to be converted to menu options)
153    //  either in the parent or the document if no parent passed
154    parent = parent || document;
155    $( parent ).find( "div[popupmenu]" ).each( function() {
156        var options = {};
157        var menu = $(this);
158        
159        // find each anchor in the menu, convert them into an options map: { a.text : click_function }
160        menu.find( "a" ).each( function() {
161            var link = $(this),
162                link_dom = link.get(0),
163                confirmtext = link_dom.getAttribute( "confirm" ),
164                href = link_dom.getAttribute( "href" ),
165                target = link_dom.getAttribute( "target" );
166            
167            // no href - no function (gen. a label)
168            if (!href) {
169                options[ link.text() ] = null;
170                
171            } else {
172                options[ link.text() ] = {
173                    url: href,
174                    action: function() {
175
176                        // if theres confirm text, send the dialog
177                        if ( !confirmtext || confirm( confirmtext ) ) {
178                            // link.click() doesn't use target for some reason, 
179                            // so manually do it here. 
180                            if (target) {
181                                window.open(href, target);
182                                return false;
183                            }
184                            // For all other links, do the default action.
185                            else {
186                                link.click();
187                            }
188                        }
189                    }
190                };
191            }
192        });
193        // locate the element with the id corresponding to the menu's popupmenu attr
194        var box = $( parent ).find( "#" + menu.attr( 'popupmenu' ) );
195        
196        // For menus with clickable link text, make clicking on the link go through instead
197        // of activating the popup menu
198        box.find("a").bind("click", function(e) {
199            e.stopPropagation(); // Stop bubbling so clicking on the link goes through
200            return true;
201        });
202        
203        // attach the click events and menu box building to the box element
204        make_popupmenu(box, options);
205        box.addClass("popup");
206        menu.remove();
207    });
208}
209
210// Alphanumeric/natural sort fn
211function naturalSort(a, b) {
212    // setup temp-scope variables for comparison evauluation
213    var re = /(-?[0-9\.]+)/g,
214        x = a.toString().toLowerCase() || '',
215        y = b.toString().toLowerCase() || '',
216        nC = String.fromCharCode(0),
217        xN = x.replace( re, nC + '$1' + nC ).split(nC),
218        yN = y.replace( re, nC + '$1' + nC ).split(nC),
219        xD = (new Date(x)).getTime(),
220        yD = xD ? (new Date(y)).getTime() : null;
221    // natural sorting of dates
222    if ( yD ) {
223        if ( xD < yD ) { return -1; }
224        else if ( xD > yD ) { return 1; }
225    }
226    // natural sorting through split numeric strings and default strings
227    var oFxNcL, oFyNcL;
228    for ( var cLoc = 0, numS = Math.max(xN.length, yN.length); cLoc < numS; cLoc++ ) {
229        oFxNcL = parseFloat(xN[cLoc]) || xN[cLoc];
230        oFyNcL = parseFloat(yN[cLoc]) || yN[cLoc];
231        if (oFxNcL < oFyNcL) { return -1; }
232        else if (oFxNcL > oFyNcL) { return 1; }
233    }
234    return 0;
235}
236
237$.fn.refresh_select2 = function() {
238    var select_elt = $(this);
239    var options = { width: "resolve",
240                    closeOnSelect: !select_elt.is("[MULTIPLE]")
241                  };
242    return select_elt.select2( options );
243}
244
245// Replace select box with a text input box + autocomplete.
246function replace_big_select_inputs(min_length, max_length, select_elts) {
247    // To do replace, the select2 plugin must be loaded.
248
249    if (!jQuery.fn.select2) {
250        return;
251    }
252    
253    // Set default for min_length and max_length
254    if (min_length === undefined) {
255        min_length = 20;
256    }
257    if (max_length === undefined) {
258        max_length = 3000;
259    }
260
261    select_elts = select_elts || $('select');
262
263    select_elts.each( function() {
264        var select_elt = $(this);
265        // Make sure that options is within range.
266        var num_options = select_elt.find('option').length;
267        if ( (num_options < min_length) || (num_options > max_length) ) {
268            return;
269        }
270        
271        if (select_elt.hasClass("no-autocomplete")) {
272            return;
273        }
274
275        /* Replaced jQuery.autocomplete with select2, notes:
276         * - multiple selects are supported
277         * - the original element is updated with the value, convert_to_values should not be needed
278         * - events are fired when updating the original element, so refresh_on_change should just work
279         *
280         * - should we still sort dbkey fields here?
281         */
282        select_elt.refresh_select2();
283    });
284}
285
286/**
287 * Make an element with text editable: (a) when user clicks on text, a textbox/area
288 * is provided for editing; (b) when enter key pressed, element's text is set and on_finish
289 * is called.
290 */
291// TODO: use this function to implement async_save_text (implemented below).
292$.fn.make_text_editable = function(config_dict) {
293    // Get config options.
294    var num_cols = ("num_cols" in config_dict ? config_dict.num_cols : 30),
295        num_rows = ("num_rows" in config_dict ? config_dict.num_rows : 4),
296        use_textarea = ("use_textarea" in config_dict ? config_dict.use_textarea : false),
297        on_finish = ("on_finish" in config_dict ? config_dict.on_finish : null),
298        help_text = ("help_text" in config_dict ? config_dict.help_text : null);
299    
300    // Add element behavior.
301    var container = $(this);
302    container.addClass("editable-text").click(function(e) {
303        // If there's already an input element, editing is active, so do nothing.
304        if ($(this).children(":input").length > 0) {
305            return;
306        }
307        
308        container.removeClass("editable-text");
309        
310        // Handler for setting element text.
311        var set_text = function(new_text) {
312            container.find(":input").remove();
313            
314            if (new_text !== "") {
315                container.text(new_text);
316            }
317            else {
318                // No text; need a line so that there is a click target.
319                container.html("<br>");
320            }
321            container.addClass("editable-text");
322
323            if (on_finish) {
324                on_finish(new_text);
325            }
326        };
327        
328        // Create input element(s) for editing.
329        var cur_text = ("cur_text" in config_dict ? config_dict.cur_text : container.text() ),
330            input_elt, button_elt;
331            
332        if (use_textarea) {
333            input_elt = $("<textarea/>")
334                .attr({ rows: num_rows, cols: num_cols }).text($.trim(cur_text))
335                .keyup(function(e) {
336                    if (e.keyCode === 27) {
337                        // Escape key.
338                        set_text(cur_text);
339                    }
340                });
341            button_elt = $("<button/>").text("Done").click(function() {
342                set_text(input_elt.val());
343                // Return false so that click does not propogate to container.
344                return false;
345            });
346        }
347        else {
348            input_elt = $("<input type='text'/>").attr({ value: $.trim(cur_text), size: num_cols })
349            .blur(function() {
350                set_text(cur_text);
351            }).keyup(function(e) {
352                if (e.keyCode === 27) {
353                    // Escape key.
354                    $(this).trigger("blur");
355                } else if (e.keyCode === 13) {
356                    // Enter key.
357                    set_text($(this).val());
358                }
359            });
360        }
361                                
362        // Replace text with input object(s) and focus & select.
363        container.text("");
364        container.append(input_elt);
365        if (button_elt) {
366            container.append(button_elt);
367        }
368        input_elt.focus();
369        input_elt.select();
370        
371        // Do not propogate to elements below b/c that blurs input and prevents it from being used.
372        e.stopPropagation();
373    });
374    
375    // Add help text if there some.
376    if (help_text) {
377        container.attr("title", help_text).tooltip();
378    }
379    
380    return container;
381};
382
383/**
384 * Edit and save text asynchronously.
385 */
386function async_save_text( click_to_edit_elt, text_elt_id, save_url,
387                          text_parm_name, num_cols, use_textarea, num_rows, on_start, on_finish ) {
388    // Set defaults if necessary.
389    if (num_cols === undefined) {
390        num_cols = 30;
391    }
392    if (num_rows === undefined) {
393        num_rows = 4;
394    }
395    
396    // Set up input element.
397    $("#" + click_to_edit_elt).click(function() {
398        // Check if this is already active
399        if ( $("#renaming-active").length > 0) {
400            return;
401        }
402        var text_elt = $("#" + text_elt_id),
403            old_text = text_elt.text(),
404            t;
405            
406        if (use_textarea) {
407            t = $("<textarea></textarea>").attr({ rows: num_rows, cols: num_cols }).text( $.trim(old_text) );
408        } else {
409            t = $("<input type='text'></input>").attr({ value: $.trim(old_text), size: num_cols });
410        }
411        t.attr("id", "renaming-active");
412        t.blur( function() {
413            $(this).remove();
414            text_elt.show();
415            if (on_finish) {
416                on_finish(t);
417            }
418        });
419        t.keyup( function( e ) {
420            if ( e.keyCode === 27 ) {
421                // Escape key
422                $(this).trigger( "blur" );
423            } else if ( e.keyCode === 13 ) {
424                // Enter key submits
425                var ajax_data = {};
426                ajax_data[text_parm_name] = $(this).val();
427                $(this).trigger( "blur" );
428                $.ajax({
429                    url: save_url,
430                    data: ajax_data,
431                    error: function() {
432                        alert( "Text editing for elt " + text_elt_id + " failed" );
433                        // TODO: call finish or no? For now, let's not because error occurred.
434                    },
435                    success: function(processed_text) {
436                        // Set new text and call finish method.
437                        if (processed_text !== "") {
438                            text_elt.text(processed_text);
439                        } else {
440                            text_elt.html("<em>None</em>");
441                        }
442                        if (on_finish) {
443                            on_finish(t);
444                        }
445                    }
446                });
447            }
448        });
449        
450        if (on_start) {
451            on_start(t);
452        }
453        // Replace text with input object and focus & select.
454        text_elt.hide();
455        t.insertAfter(text_elt);
456        t.focus();
457        t.select();
458        
459        return;
460    });
461}
462
463function init_history_items(historywrapper, noinit, nochanges) {
464    //NOTE: still needed for non-panel history views
465
466    var action = function() {
467        // Load saved state and show as necessary
468        try {
469            var stored = $.jStorage.get("history_expand_state");
470            if (stored) {
471                for (var id in stored) {
472                    $("#" + id + " div.historyItemBody" ).show();
473                }
474            }
475        } catch(err) {
476            // Something was wrong with values in storage, so clear storage
477            $.jStorage.deleteKey("history_expand_state");
478        }
479
480        // If Mozilla, hide scrollbars in hidden items since they cause animation bugs
481        if ( $.browser.mozilla ) {
482            $( "div.historyItemBody" ).each( function() {
483                if ( !$(this).is(":visible") ) { $(this).find( "pre.peek" ).css( "overflow", "hidden" ); }
484            });
485        }
486        
487        historywrapper.each( function() {
488            var id = this.id,
489                body = $(this).children( "div.historyItemBody" ),
490                peek = body.find( "pre.peek" );
491            $(this).find( ".historyItemTitleBar > .historyItemTitle" ).wrap( "<a href='javascript:void(0);'></a>" )
492                .click( function() {
493                    var prefs;
494                    if ( body.is(":visible") ) {
495                        // Hiding stuff here
496                        if ( $.browser.mozilla ) { peek.css( "overflow", "hidden" ); }
497                        body.slideUp( "fast" );
498
499                        if (!nochanges) { // Ignore embedded item actions
500                            // Save setting
501                            prefs = $.jStorage.get("history_expand_state");
502                            if (prefs) {
503                                delete prefs[id];
504                                $.jStorage.set("history_expand_state", prefs);
505                            }
506                        }
507                    } else {
508                        // Showing stuff here
509                        body.slideDown( "fast", function() {
510                            if ( $.browser.mozilla ) { peek.css( "overflow", "auto" ); }
511                        });
512
513                        if (!nochanges) {
514                            // Save setting
515                            prefs = $.jStorage.get("history_expand_state");
516                            if (!prefs) { prefs = {}; }
517                            prefs[id] = true;
518                            $.jStorage.set("history_expand_state", prefs);
519                        }
520                    }
521                    return false;
522                });
523        });
524        
525        // Generate 'collapse all' link
526        $("#top-links > a.toggle").click( function() {
527            var prefs = $.jStorage.get("history_expand_state");
528            if (!prefs) { prefs = {}; }
529            $( "div.historyItemBody:visible" ).each( function() {
530                if ( $.browser.mozilla ) {
531                    $(this).find( "pre.peek" ).css( "overflow", "hidden" );
532                }
533                $(this).slideUp( "fast" );
534                if (prefs) {
535                    delete prefs[$(this).parent().attr("id")];
536                }
537            });
538            $.jStorage.set("history_expand_state", prefs);
539        }).show();
540    };
541    
542    action();
543}
544
545function commatize( number ) {
546    number += ''; // Convert to string
547    var rgx = /(\d+)(\d{3})/;
548    while (rgx.test(number)) {
549        number = number.replace(rgx, '$1' + ',' + '$2');
550    }
551    return number;
552}
553
554// Reset tool search to start state.
555function reset_tool_search( initValue ) {
556    // Function may be called in top frame or in tool_menu_frame;
557    // in either case, get the tool menu frame.
558    var tool_menu_frame = $("#galaxy_tools").contents();
559    if (tool_menu_frame.length === 0) {
560        tool_menu_frame = $(document);
561    }
562        
563    // Remove classes that indicate searching is active.
564    $(this).removeClass("search_active");
565    tool_menu_frame.find(".toolTitle").removeClass("search_match");
566    
567    // Reset visibility of tools and labels.
568    tool_menu_frame.find(".toolSectionBody").hide();
569    tool_menu_frame.find(".toolTitle").show();
570    tool_menu_frame.find(".toolPanelLabel").show();
571    tool_menu_frame.find(".toolSectionWrapper").each( function() {
572        if ($(this).attr('id') !== 'recently_used_wrapper') {
573            // Default action.
574            $(this).show();
575        } else if ($(this).hasClass("user_pref_visible")) {
576            $(this).show();
577        }
578    });
579    tool_menu_frame.find("#search-no-results").hide();
580    
581    // Reset search input.
582    tool_menu_frame.find("#search-spinner").hide();
583    if (initValue) {
584        var search_input = tool_menu_frame.find("#tool-search-query");
585        search_input.val("search tools");
586    }
587}
588
589// Create GalaxyAsync object.
590var GalaxyAsync = function(log_action) {
591    this.url_dict = {};
592    this.log_action = (log_action === undefined ? false : log_action);
593};
594
595GalaxyAsync.prototype.set_func_url = function( func_name, url ) {
596    this.url_dict[func_name] = url;
597};
598
599// Set user preference asynchronously.
600GalaxyAsync.prototype.set_user_pref = function( pref_name, pref_value ) {
601    // Get URL.
602    var url = this.url_dict[arguments.callee];
603    if (url === undefined) { return false; }
604    $.ajax({                   
605        url: url,
606        data: { "pref_name" : pref_name, "pref_value" : pref_value },
607        error: function() { return false; },
608        success: function() { return true; }                                           
609    });
610};
611
612// Log user action asynchronously.
613GalaxyAsync.prototype.log_user_action = function( action, context, params ) {
614    if (!this.log_action) { return; }
615        
616    // Get URL.
617    var url = this.url_dict[arguments.callee];
618    if (url === undefined) { return false; }
619    $.ajax({                   
620        url: url,
621        data: { "action" : action, "context" : context, "params" : params },
622        error: function() { return false; },
623        success: function() { return true; }                                           
624    });
625};
626
627// Initialize refresh events.
628function init_refresh_on_change () {
629    $("select[refresh_on_change='true']")
630        .off('change')
631        .change(function() {
632            var select_field = $(this),
633                select_val = select_field.val(),
634                refresh = false,
635                ref_on_change_vals = select_field.attr("refresh_on_change_values");
636            if (ref_on_change_vals) {
637                ref_on_change_vals = ref_on_change_vals.split(',');
638                var last_selected_value = select_field.attr("last_selected_value");
639                if ($.inArray(select_val, ref_on_change_vals) === -1 && $.inArray(last_selected_value, ref_on_change_vals) === -1) {
640                    return;
641                }
642            }
643            $(window).trigger("refresh_on_change");
644            $(document).trigger("convert_to_values"); // Convert autocomplete text to values
645            select_field.get(0).form.submit();
646        });
647    
648    // checkboxes refresh on change
649    $(":checkbox[refresh_on_change='true']")
650        .off('click')
651        .click( function() {
652            var select_field = $(this),
653                select_val = select_field.val(),
654                refresh = false,
655                ref_on_change_vals = select_field.attr("refresh_on_change_values");
656            if (ref_on_change_vals) {
657                ref_on_change_vals = ref_on_change_vals.split(',');
658                var last_selected_value = select_field.attr("last_selected_value");
659                if ($.inArray(select_val, ref_on_change_vals) === -1 && $.inArray(last_selected_value, ref_on_change_vals) === -1) {
660                    return;
661                }
662            }
663            $(window).trigger("refresh_on_change");
664            select_field.get(0).form.submit();
665        });
666    
667    // Links with confirmation
668    $( "a[confirm]" )
669        .off('click')
670        .click( function() {
671            return confirm( $(this).attr("confirm") );
672        });
673};
674
675$(document).ready( function() {
676
677    // Refresh events for form fields.
678    init_refresh_on_change();
679    
680    // Tooltips
681    if ( $.fn.tooltip ) {
682        // Put tooltips below items in panel header so that they do not overlap masthead.
683        $(".unified-panel-header [title]").tooltip( { placement: 'bottom' } );
684        
685        // default tooltip initialization, it will follow the data-placement tag for tooltip location
686        // and fallback to 'top' if not present
687        $("[title]").tooltip();
688    }
689    // Make popup menus.
690    make_popup_menus();
691    
692    // Replace big selects.
693    replace_big_select_inputs(20, 1500);
694    
695    // If galaxy_main frame does not exist and link targets galaxy_main, 
696    // add use_panels=True and set target to self.
697    $("a").click( function() {
698        var anchor = $(this);
699        var galaxy_main_exists = (parent.frames && parent.frames.galaxy_main);
700        if ( ( anchor.attr( "target" ) == "galaxy_main" ) && ( !galaxy_main_exists ) ) {
701            var href = anchor.attr("href");
702            if (href.indexOf("?") == -1) {
703                href += "?";
704            }
705            else {
706                href += "&";
707            }
708            href += "use_panels=True";
709            anchor.attr("href", href);
710            anchor.attr("target", "_self");
711        }
712        return anchor;
713    });
714
715});