/**
* 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 = "
";
/**
* 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
}