/** * Library for managing the results tabs for Owltest-based web pages (actually, the backend testing package) * Includes processing of the JSON-decoded results dictionary, called "msg_obj" below. * Format of msg_obj: * * msg_obj = { * 'score': score, * 'max_score': max score, * 'student_filename': student filename, * 'student_code': student code body, * 'test_filename': test filename, // only if verbosity=All * 'test_code': test code body, // only if verbosity=All * 'tabs': { // may be absent if error_msg is present * 'tab_id': { * 'label': tab label, * 'msg_dicts': [{'pts':XX, 'msg':"..."] // pts field may be missing if no points associated with message. * } * } * 'error_msg': error msg string // not present for successful test run * } * */ // Make sure that the following regex's match that used in common.defs.py var CODESKULPTOR_URL_DOMAIN_RGX = "http(s?)://([a-zA-Z][a-zA-Z0-9_-]*.)?codeskulptor.org"; var CODESKULPTOR_URL_USERSAVE_BUCKET_RGX = "/#(user|save)[a-zA-Z0-9]*_"; var CODESKULPTOR_URL_GEN_BUCKET_RGX = "/#[a-zA-Z][a-zA-Z0-9]*_"; var CODESKULPTOR_URL_FILENAME_RGX = "[a-zA-Z0-9_]+.py"; var CODESKULPTOR_URL_USERSAVE_WHOLE_RGX = "^" + CODESKULPTOR_URL_DOMAIN_RGX + CODESKULPTOR_URL_USERSAVE_BUCKET_RGX + CODESKULPTOR_URL_FILENAME_RGX + "$"; // Whole string must match CS URL. For checking filenames where bucket = userXXX or saveXXX. var CODESKULPTOR_URL_GEN_WHOLE_RGX = "^" + CODESKULPTOR_URL_DOMAIN_RGX + CODESKULPTOR_URL_GEN_BUCKET_RGX + CODESKULPTOR_URL_FILENAME_RGX + "$"; // Whole string must match CS URL. For checking filenames var OwlTest = OwlTest || { "TabManager" : (function($, window){ /** * Define implementation of TabManager, i.e. its constructor * @param tabs_elt_id The id value of the overall tabs element * @param student_URL The student_URL parameter given to the web page template. Used to determine if the student code file is a CodeSkulptor or local file. */ var ManagerImpl = function(tabs_elt_id, student_URL){ this.tabs_elt_id = "#"+tabs_elt_id; this.student_URL = student_URL; this.codeTabs = {}; this.hideTabs(); }; /** * The HTML, as a template, used to define a tab header */ var tabTemplate = "
  • #{label}
  • "; /** * Returns the jQuery element representing the tabs DOM element. * This value cannot be saved as the DOM is changing as the tabs are * being built, so it must be recomputed at that the time that the * that it is being used. */ ManagerImpl.prototype.tabs_elt = function() { return $(this.tabs_elt_id); }; /** * Hides the tabs from view. */ ManagerImpl.prototype.hideTabs = function(){ this.tabs_elt().hide(); }; /** * Shows the tabs to the user. * @param active_idx The index of the tab to be activated. */ ManagerImpl.prototype.showTabs = function(active_idx){ this.tabs_elt().tabs({active : active_idx}); // Set the active tab to the first one. this.tabs_elt().show(); // show the tabs now that there is content }; /** * Dynamically add a tab with the given id, label and content. * Function is defined here so as to close over the tabs variable above. * @param tab_id The id of the new tab * @param label The label of the new tab * @param tabContentHtml The body of the new tab, as HTML. */ ManagerImpl.prototype.addTab = function(tab_id, label, tabContentHtml) { var tabs = this.tabs_elt().tabs(); var id = "tabs-" + tab_id; var li = $(tabTemplate.replace(/#\{href\}/g, "#" + id).replace( /#\{label\}/g, label)); tabs.find(".ui-tabs-nav").append(li); //console.log("li = "+li) tabs.append('
    ' + tabContentHtml + '
    '); tabs.tabs("refresh"); return id; }; /** * Sort the unsorted array by placing all string entities containing the given * strings from a list of strings, sort_order, that determines what order the * unsorted strings are sorted. Any entities not that do not contain any * of the sort_order entries are simply tacked onto the result as is. * @param unsorted_array The array to be sorted * @param sort_order An array of string values that determine the sort order * @return The sorted array is returned. */ ManagerImpl.prototype.sortBy = function(unsorted_array, sort_order) { var unsorted = unsorted_array.slice(); // copy the original array into a destructable array. var sorted = []; // final order for (var so_idx=0; so_idx < sort_order.length; so_idx++) { // loop over sort keywords for (var idx=unsorted.length-1; idx>=0; idx--){ // loop over remaining unsorted entities backwards to avoid deletion issues. if (-1 != unsorted[idx].indexOf(sort_order[so_idx])) { // check if keyword is in unsorted sorted.push(unsorted[idx]); // append the entity onto the final list unsorted.splice(idx, 1); // remove the entity from the unsorted array } } } sorted = sorted.concat(unsorted); // copy the remainder of the unsorted entities return sorted; }; var NO_BULLETS_MSGS = ["Comments", "Notes", "Feedback", "Submitted_File"]; /** * Add tabs from an dictionary of {tab_id:tab_msg_objs} where * tab_id is the id for the new tab and * tab_msg_obj is {label: string, msgs:array_of_strings} * where label is the label for the tab * and msgs is the array of messages for that tab. * @param msg_obj The message object containing the information to make the tabs {"tabs":{tab_id:{label: "...", "msgs":[msg1, msg2,...]}}} */ ManagerImpl.prototype.addMsgTabs = function(msg_obj){ console.log("TabManager.addMsgTabs(): msg_obj.tabs = "+JSON.stringify(msg_obj.tabs)); var tab_info_dict = msg_obj.tabs ? msg_obj.tabs : {}; // Firebase sends nulls instead of empty dictionary values var tag_ids = Object.keys(tab_info_dict); // get the tag ids var sorted_tag_ids = this.sortBy(tag_ids, [ "Error", "Failure", "Warning" ]);// sort tab ids containing these words in the given order. for (var idx=0; idx "; msgs_str += msg_dict["msg"]+"

    "; } tabContentHtml += msgs_str; } else { for (var i = 0; i < tab_info_dict[tab_id].msg_dicts.length; i++) { var msg_dict = tab_info_dict[tab_id].msg_dicts[i]; msgs_str += "
  • "; if ("pts" in msg_dict) { msgs_str += "["+msg_dict["pts"].toFixed(1)+" pts] "; } msgs_str += msg_dict["msg"]+"
  • "; } tabContentHtml += "
      " + msgs_str + "
    "; } this.addTab(tab_id, tab_info_dict[tab_id].label, tabContentHtml); } //this.tabs_elt().tabs().tabs("refresh"); }; /** * Install read-only CodeMirror objects to handle the text of * all elements of the class "code" who are a children * of the element whose "id" is the given id. Sets the code text to the given * string unless code_text = null or "". * Returns a function of no parameters that refreshes all * the CodeMirror objects associated with id. * @param id The id of the tab containing the code in one of more sub-elements with class='code'. * @param code_text The text of the code to put into CodeMirror. If more than one 'code' sub-element is found, all will have the same code. * @return A function of no parameters that will refresh all the code class sub-elements. */ ManagerImpl.prototype.installCodeMirror = function(id, code_text) { var cm_objs = []; // There might be more than one code class sub-element. $("#" + id + " .code").each(function() { var $this = $(this); var $code = $this.text(); $this.empty(); var codeMirrorObj = CodeMirror(this, { value : $code, mode : { name : "python", version : 2, singleLineStringErrors : false }, lineNumbers : true, readOnly : true }); if(code_text) { codeMirrorObj.setValue(code_text); } cm_objs.push(codeMirrorObj); }); // Need to return a function that refreshes all the associated CodeMirror objects. return function() { for (var idx=0; idx < cm_objs.length; idx++) { cm_objs[idx].refresh(); } }; }; /** * Add a tab containing the student code. The this.student_URL field determines if the student code is a CodeSkulptor or local * file. If it is a CodeSkulptor file, the student filename will be hyperlinked to student_URL. * The this.codeTabs dictionary field is set to map the new tab's id to the refresh function returned by installCodeMirror(). * @param msg_obj The message object holding the required data {"student_filename": "...", "student_code": "..."} */ ManagerImpl.prototype.addStudentCodeTab = function(msg_obj){ //console.log("TabManager.addStudentCodeTab(): msg_obj = "+msg_obj) if ("student_filename" in msg_obj) { var tab_id = "student-code"; var student_link = ""; //console.log("student_URL = "+this.student_URL) if (this.student_URL) { student_link = "" + msg_obj.student_filename + ""; } else { student_link = msg_obj.student_filename; } // Set up tab with empty code first. addTab() will refresh the tab. Don't want to accidentally run Javascript in the code. var id = this.addTab( tab_id, "Your code", "

    Filename: "+student_link+"

    "  + "
    "); this.codeTabs[id] = this.installCodeMirror(id, msg_obj.student_code); // install CodeMirror with the code text and save the returned refresh function. } }; /** * Add a tab containing the test code * The this.codeTabs dictionary field is set to map the new tab's id to the refresh function returned by installCodeMirror(). * @param msg_obj The message object holding the required data. */ ManagerImpl.prototype.addTestCodeTab = function(msg_obj){ if ("test_filename" in msg_obj) { var tab_id = "test-code"; // Add tab with blank code first. addTab() will refresh tab. var id = this.addTab( tab_id, "Test code", "

    Filename: " + msg_obj.test_filename + "

    " + "
    "); this.codeTabs[id] = this.installCodeMirror(id, msg_obj.test_code); // install CodeMirror with the code text and save the returned refresh function. } }; /** * Install the event handler to refresh the CodeMirror objects on a tabs that contain them. * Uses the this.codeTabs field to determine if a tab is contains CodeMirror instance(s) and the * related refresh function to refresh the code display. */ ManagerImpl.prototype.setCodeTabEventHandler = function() { var self =this; // Hack around "this" reference inside function below. // Attach an event handler to the tabs that will detect if a code tab is activated and tell CodeMirror to // refresh the associated code display. this.tabs_elt().on("tabsactivate", function(event, ui) { var id = ui.newPanel.prop("id"); if (id in self.codeTabs) { self.codeTabs[id](); // refresh the CodeMirror objects associated with id } }); }; /** * Sets the score value on the page. * @param element The page element that holds the score text. * @param msg_obj The message object that holds the required data: {"score": value, "max_score": value} */ ManagerImpl.prototype.setScore = function(element, msg_obj){ // compose the score string if it exists var score_str = ""; if ("score" in msg_obj) { score_str = "Score: " + msg_obj.score.toFixed(1) + "/" + msg_obj.max_score + ((msg_obj.score < msg_obj.max_score) ? "" : " Perfect score! All tests pass. Great job!"); //element.html(score_str); // set the inner HTML text of the element with id="status" } // else { // element.html(""); // clear the element // } if ("score_comment" in msg_obj) { if (msg_obj.score_comment !== null) { score_str += (score_str==="" ? "" : " ") +msg_obj.score_comment; } } var tab_ids = Object.keys(msg_obj.tabs); for (var idx=0; idx < tab_ids.length; idx++) { var tab_id = tab_ids[idx]; //console.log("tag_id = "+tab_id); if (-1 != tab_id.indexOf("Error")) { score_str += '
    Testing was aborted! Please fix the errors detailed below before resubmitting.'; break; } } element.html(score_str); // set the inner HTML text of the element with id="status" }; /** * Aggregate function that sets the score display, makes all the results messages tabs, the student code tab and if * present in msg_obj, the test code tab. Installs the event handler to refresh the code tabs. * @param msg_obj The message object that holds all the required information. */ ManagerImpl.prototype.makeTabs = function(msg_obj){ //console.log("TabManager.makeTabs(): msg_obj = "+msg_obj) this.addMsgTabs(msg_obj); // add the result message tabs this.addStudentCodeTab(msg_obj); if ("test_code" in msg_obj && "test_filename" in msg_obj){ this.addTestCodeTab(msg_obj); } this.setScore($("#status"), msg_obj); this.setCodeTabEventHandler(); }; // Return Constructor object return ManagerImpl; })(jQuery, window), "SubmitLock": (function($, window){ /** * Define convenience class to help prevent multiple submissions. Only stops multiple submissions before * server responds with new page. Resubmission lock-out releases after lock_time milliseconds if there is no * server response. statusElt_id is the ID of an element with an innerText that */ var SubmitLockImpl = function(statusElt_id, lock_time) { this._statusElt_id = statusElt_id; // Cannot get element yet because it may not be defined yet. this._wasSubmitted = false; this._submit_timeout = lock_time; //in milliseconds }; /** * Returns the current status of the submit lock. True if the form was already submitted and false if not. * The state of the lock will change to wasSubmitted()=true after wasSubmitted()=false for the length of the * lock_time given in the constructor. A red warning message will be displayed if the submission was * already locked. */ SubmitLockImpl.prototype.wasSubmitted = function() { if(this._wasSubmitted) { getElt(this._statusElt_id).innerHTML += ' Already submitted, please wait...'; return true; } else { this._wasSubmitted = true; setTimeout(this._reset_wasSubmitted, this._submit_timeout); return false; } }; /** * Forcibly resets the submit lock to unsubmitted. For internal use only. Be careful about delayed execution of * this method by wasSubmitted(). */ SubmitLockImpl.prototype._reset_wasSubmitted = function() { this._wasSubmitted = false; }; // Return Constructor object return SubmitLockImpl; })(jQuery, window) }; /** * Convenience wrapper for document.getElementById(id) */ function getElt(id) { return document.getElementById(id); } /** * Convenience function to help prevent multiple submissions. Only stops multiple submissions before * server responds with new page. Resubmission lock-out releases after 10 seconds if there is no * server response. * * SHOULD THIS ALL BE ENCAPSULATED IN AN OBJECT? */ /* var _submit_timeout = 10000; //in milliseconds var _wasSubmitted = false; function wasSubmitted(statusElt_id){ if(_wasSubmitted) { getElt(statusElt_id).innerText += " Already submitted, please wait..."; return true; } else { _wasSubmitted = true; setTimeout(reset_wasSubmitted, _submit_timeout); return false; } } function reset_wasSubmitted() { _wasSubmitted = false; } */ /** * Returns true if the given filename (no path) is a valid Python module name, i.e. conforms to the Python * spec for identifiers (https://docs.python.org/2/reference/lexical_analysis.html#identifiers). Returns false otherwise. */ function checkPythonFilename(filename) { return (null !== filename.match(/^[a-zA-Z_][a-zA-Z0-9_]*\.py$/)); } /** * Validates the given form input (type="file") element, to make sure that the file is a valid Python file. * Returns true if valid or if elt.value=="", false otherwise, including if more than one file is specified. * Does NOT check if the elt is required to have a value or not! * Pops up alert it invalid and returns focus to the element. * Clears the input element by rebuilding it because some browsers, i.e. IE, make the file input immutable as a security measure. * THE ELEMENT *MUST* BE SURROUNDED BY TAG AND BE THE *ONLY* ELEMENT IN THE TAG!! * @param elt The file input element * @param desc a string description of the input element, to identify it. * @returns {Boolean} */ function validatePythonFilename(elt, desc) { if(elt.value === "" || elt.files.length ===0) { return true; } else if(elt.files.length != 1) { alert("The "+desc+" input does not specify a single file."); return false; } else { var result = checkPythonFilename(elt.files[0].name); if (!result) { var html = elt.parentNode.innerHTML; // get the original HTML from the parent tag. alert("The "+desc+" input has an invalid Python module name: \""+elt.files[0].name+"\" \nThe module's filename must start with a letter, contain only letters, numbers and/or underscores (\"_\") and end with \".py\"."); elt.parentNode.innerHTML = html; // reset the HTML, i.e. rebuild the element. elt.focus(); } return result; } } /** * Validates the given form input (type="file") element, to make sure that the file has one of the allowed file extensions. * Does NOT check the rest of the file name. * Returns true if valid or if elt.value=="", false otherwise, including if more than one file is specified. * Does NOT check if the elt is required to have a value or not! * Pops up alert it invalid and returns focus to the element. * Clears the input element by rebuilding it because some browsers, i.e. IE, make the file input immutable as a security measure. * THE ELEMENT *MUST* BE SURROUNDED BY TAG AND BE THE *ONLY* ELEMENT IN THE TAG!! * @param elt The file input element * @param allowed_file_types A list of allowed extensions without the period. * @returns {Boolean} */ function validate_file_extension(elt, allowed_file_types) { if(elt.value === "" || elt.files.length ===0) { return true; } else if(elt.files.length != 1) { alert("The file input does not specify a single file."); return false; } else { regex = "[\.]("+allowed_file_types.join("|")+")$"; var result = (null !== elt.files[0].name.match(regex)); //console.log("regex = "+regex+", result = "+result) if (!result) { var html = elt.parentNode.innerHTML; // get the original HTML from the parent tag. alert("The selected file, \""+elt.files[0].name+"\" does not have one the following allowed extensions: "+allowed_file_types.join(", ")); elt.parentNode.innerHTML = html; // reset the HTML, i.e. rebuild the element. elt.focus(); } return result; } } /** * Validates the value in the given text input element to be a valid Codeskulptor URL that * starts with "http://www.codeskulptor.org/#user" or "http://www.codeskulptor.org/#save" * and ends with ".py" where the remaining characters are * letters, numbers or underscores. * Does NOT check if the elt is required to have a value or not! * Returns true if validated or if input value is an empty string, false otherwise. If false, pops up alert box with an error message and gives focus to the * given input element. * @param elt * @returns {Boolean} */ function validate_CS_URL(elt) { url = elt.value; if ("" === url) { return true; } //regex = "^http://www.codeskulptor.org/#(user|save)[a-zA-Z0-9]*_[a-zA-Z0-9_]+\.py$" //var result = (null !== url.match(regex)); var result = (null !== url.match(CODESKULPTOR_URL_USERSAVE_WHOLE_RGX)); if (!result) { alert("The URL entered, \""+url+"\", is not a valid CodeSkulptor URL!"); elt.focus(); } return result; } /** * Validates the value in the given text input element to be a valid Codeskulptor URL that * starts with "http://www.codeskulptor.org/#", has at least one underscore * and ends with ".py" where the remaining characters are * letters, numbers or underscores. * Does NOT check if the elt is required to have a value or not! * Returns true if validated or if input value is an empty string, false otherwise. If false, pops up alert box with an error message and gives focus to the * given input element. * @param elt * @returns {Boolean} */ function validate_CS_URL_gen(elt) { url = elt.value; if ("" === url) { return true; } // regex = "^http://www.codeskulptor.org/#[a-zA-Z][a-zA-Z0-9]*_[a-zA-Z0-9_]+\.py$" // var result = (null !== url.match(regex)); var result = (null !== url.match(CODESKULPTOR_URL_GEN_WHOLE_RGX)); if (!result) { alert("The URL entered, \""+url+"\", is not a valid CodeSkulptor URL!"); elt.focus(); } return result; } /** * Returns true if val is in the options list, choices, false otherwise */ function in_choice(val, choices) { for( i=0; i< choices.length; i++) { if (val == choices[i].value) { return true; } } return false; } /** * Check if the current choice is compatible with creating a new entity. * Returns false if elt's value is in choice_elt.options. * name is a string that describes the type choice being made and is used in the warning alert * when the above condition is not met and false is returned. */ function check_radio_new(name, elt, choice_elt) { if(in_choice(get_radio_btn_value(elt), choice_elt.options)) { alert("To create a new "+name+", the supplied entry must be NOT be an existing "+name+"."); elt.focus(); return false } else { return true; } } /** * Check if the current choice is compatible with copying an existing entity. * Returns true if elt's value is in choice_elt.options. * name is a string that describes the type choice being made and is used in the warning alert * when the above condition is not met and false is returned. * If false is returned, radio_new.checked is set to true, reverting the operation choice * back to the creation of a new entity. */ function check_radio_copy(name, elt, choice_elt) { if(in_choice(get_radio_btn_value(elt), choice_elt.options)) { return true; } else { alert("To copy a "+name+", the supplied entry must be an existing "+name+"."); elt.focus(); return false } } /** * Check if the current choice is compatible with replacing an existing entity. * Returns true if elt's value is in choice_elt.options. * name is a string that describes the type choice being made and is used in the warning alert * when the above condition is not met and false is returned. * If false is returned, radio_new.checked is set to true, reverting the operation choice * back to the creation of a new entity. */ function check_radio_replace(name, elt, choice_elt, radio_new) { if(in_choice(get_radio_btn_value(elt), choice_elt.options)) { return true; } else { alert("To replace a "+name+", the supplied entry must be an existing "+name+"."); elt.focus(); radio_new.click(); // want to click the button so that any event handlers run. return false } } /** * Check if the current choice is compatible with deleting an existing entity. * Returns true if elt's value is in choice_elt.options. * name is a string that describes the type choice being made and is used in the warning alert * when the above condition is not met and false is returned. * Also displays a confirmation box to warn the user that a deletion has been selected when the above condition * has been met. Clicking OK returns true. Cancelling the * confirmation dialog causes a false to be returned. * If false is returned, radio_new.checked is set to true, reverting the operation choice * back to the creation of a new entity. */ function check_radio_delete(name, elt, choice_elt, radio_new) { if(in_choice(get_radio_btn_value(elt), choice_elt.options)) { if( confirm("You about to permanently DELETE a "+name+". Are you sure you want to do this?")){ return true; } else { radio_new.click(); // want to click the button so that any event handlers run. //radio_new.checked = true; return false; } } else { alert("To delete a "+name+", the supplied entry must be an existing "+name+"."); radio_new.click(); // want to click the button so that any event handlers run. //radio_new.checked = true; elt.focus(); return false } } /** * Returns the value from a collection of associated radio buttons in a browser-independent manner. * Firefox and IE do not support a global selected value across the collection of radio buttons * so the collection of radio button must be scanned to find the checked value. * Chrome supports a simpler radio_btn_elt.value which holds the checked value. * @param radio_btn_elt The element that represents the collection of associated radio buttons * @return The value of the checked radio button */ function get_radio_btn_value(radio_btn_elt){ if (typeof(radio_btn_elt.value)== "undefined") { for(idx=0; idx < radio_btn_elt.length; idx++){ if (radio_btn_elt[idx].checked) { return radio_btn_elt[idx].value; } } } else { return radio_btn_elt.value; } } /** * POST a message to the error_log app * @param appName The name of the current app * @param client_id The channel ID being used. * @param msg The error message to send * @param extraData A dictionary of any extra information to be sent or an empty dictionary. Should NOT contain the keys: "client_id", "app", "msg", "Browser Error Log" * */ function send_error_log(appName, client_id, msg, extraData) { var xmlhttp = new XMLHttpRequest(); xmlhttp.open("POST","/error_log",true); xmlhttp.setRequestHeader("Content-type","application/x-www-form-urlencoded"); // TODO get rid of channel_token var data = {"client_id" : client_id, "app":appName, "msg": msg, "Browser Error Log":(new Date()).toJSON()}; if (extraData instanceof Object) { for(var key in extraData) { data[key] = extraData; } } var dataStr = JSON.stringify(data); console.log("send_error_log(): dataStr = "+dataStr); xmlhttp.send("data="+dataStr); } //function openDefault() { // // Avoid window.location.origin, since not supported by all browsers. // window.location.replace(window.location.protocol + "//" + // window.location.host + // window.location.pathname + // $navtabs.attr("href")); //} // //// If page URL had a hashtag, use that to open the documentation //// to that ID. //function openHash() { // if (window.location.hash == undefined || // window.location.hash.length === 0) { // // Replace no hash with default hash, for better history behavior. // openDefault(); // } // else { // var $here = $(window.location.hash); // // if ($here.length == 0) { // // Hash isn't a valid ID location. // // Replace bad hash with default hash, so that it has correct behavior. // openDefault(); // } // else { //// var accHeaderID; //// var accArray = []; //// //// // Find closest accordion. //// if ($here.hasClass("ui-accordion-header")) { //// accHeaderID = window.location.hash.substr(1); //// } //// else { //// accHeaderID = enclosingAccHeaderID($here); //// } //// // Find all enclosing accordions. //// while (accHeaderID != undefined) { //// $here = $("#"+accHeaderID).parent(); //// accArray.push([$here,accHeaderID]); //// //// accHeaderID = enclosingAccHeaderID($here); //// } // // Find enclosing tab. // var tabID = enclosingTabID($here); // // // Open enclosing tab. // openTab(tabID); // // Open top-level accordion, if any. //// if (accArray.length) { //// var last = accArray.length-1; //// openAccordion(accArray[last][0], accArray[last][1]); //// //// // Open all nested accordions. //// for (var i = last-1; i>=0; i--) { //// openAccordion(accArray[i][0], accArray[i][1]); //// } //// } // } // } //} //var currentLocationID = ""; //function useInternalLink(elt) { //// $(window).off('hashchange', openHash); //// window.location.hash = "#"+currentLocationID; //// currentLocationID = ""; //// $(window).on('hashchange', openHash); //// window.location.hash = "#"+$(elt).attr("data-dest"); //$(this).attr("data-dest"); // window.location = "http://localhost:9080/info#tabs-Folders" // return false; // prevent tab link's default behavior // } // //$('a.internal').on('click',useInternalLink); /** * Set the size of a select element to something reasonable * param elt The select element to resize * param max_size The maximum number of rows to ever display */ function set_select_size(elt, max_size) { var elt_row_count = elt.options.length + elt.querySelectorAll("optgroup").length; // Need to count optgroups too because they take up rows in the size. elt.size = Math.min(elt_row_count, max_size); } /** * A primitive object with a single method, "run(key, param, new_fun)" that will run a previously installed lambda function associated with key * and then install the given new_fn as the lambda to run next time. * key = a valid dictionary key value, typically a string to which a lambda function is associated. * param = a parameter value to call the previously installed lambda with, i.e. fn_lookup[key](param). * new_fn = (optional) lambda function to replace the currently installed lambda after it is run. Defaults to a no-op function if not supplied. */ var fn_lookup_obj = { "run":function(key, param, new_fn = function(x){}){ if(undefined != this[key]){ this[key](param); } this[key] = new_fn; }}; /** * Display a submenu element if the given element is checked. * Uses the fn_lookup object to run lambdas to disable/hide other submenus since there is no event for "unchecking" something. * This function needs to be called by the onchange or onclick method of ALL elements that need to control the appearance/disappearance of any submenus. * If the element has no submenu of its own, simply call this function without the submenu_elt parameter or with that parameter set to null. * elt_group_name = a string identifying the given elt as part of a group of cooperating elements. * elt = the element that is working with other elements to display/hide element-associated submenus * submenu_elt = the submenu element associated with the given elt. Omit this parameter if there is no associated submenu for the given elt. */ function submenu_oncheck(elt_group_name, elt, submenu_elt=null) { if(elt.checked) { // only show a submenu if the element is checked if(null == submenu_elt) { fn_lookup_obj.run(elt_group_name, elt); // even if there is no submenu, run the lambda to close the other submenus anyway. } else { fn_lookup_obj.run(elt_group_name, elt, function(current_elt) { // Run the lambda to close the previous submenu and install a lambda to close this submenu. submenu_elt.style.display = "none"; }); submenu_elt.style.display="inline"; // make the submenu visible. } } else { // not likely to get called, but just in case if(null != submenu_elt) { submenu_elt.style.display="none"; // always disable the submenu if not checked, but don't change anything else in this case } } } /** * Return the checked value of a radio button group. * There is no method in Javascript to simply return the current checked value of a button group * @param formElt The parent form element * @param btnGroupName The name of the radio button group * @returns The currently checked value */ function get_radio_btn_value(formElt, btnGroupName) { return formElt.querySelector("input[name='"+btnGroupName+"']:checked").value; } /** * REturns the selected value of a select element * @param selectElt a select element * @returns the selected value */ function get_select_value(selectElt) { return selectElt.options[selectElt.selectedIndex].value }