Latest build deployed.

This commit is contained in:
andyeisenberg
2025-12-28 17:55:29 +00:00
commit 7e09fad64e
617 changed files with 18659 additions and 0 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+552
View File
@@ -0,0 +1,552 @@
function escapeHTML(text) {
if (!text) { return "" }
the_ans = text;
the_ans = the_ans.replace(/&/g, "&");
the_ans = the_ans.replace(/<([a-zA-Z])/g, '< $1');
return the_ans
}
function uNescapeHTML(text) {
if (!text) { return "" }
the_ans = text;
the_ans = the_ans.replace(/&lt; /g, "<");
the_ans = the_ans.replace(/&lt;/g, "<");
the_ans = the_ans.replace(/&gt;/g, ">");
the_ans = the_ans.replace(/&amp;/g, "&");
the_ans = the_ans.replace(/<([a-zA-Z])/g, "< $1");
return the_ans
}
function dollars_to_slashparen(text) {
the_ans = text;
the_ans = the_ans.replace(/(^|\s|-)\$([^\$\f\r\n]+)\$(\s|\.|,|;|:|\?|!|$)/g, "$1\\($2\\)$3");
//twice, for $5$-$6$
the_ans = the_ans.replace(/(^|\s|-)\$([^\$\f\r\n]+)\$(\s|\.|,|;|:|\?|!|-|$)/g, "$1\\($2\\)$3");
return the_ans
}
/* The structure of a reading question is an erticle with an id (this_ques_id),
containing:
#this_ques_id_text the answer given (in a div with that id)
#this_ques_id_text_hidden the answer in a raw form (in a hidden div)
#this_ques_id_text_input the answer in a textinput with that id
Try to do all operations by id, and not by carrying objects around.
*/
var reading_questions = document.querySelectorAll("section.reading-questions article.exercise-like, #boelkins-ACS .main #content > section:first-of-type > section:first-of-type > .project-like li");
var reading_answers = {};
console.log('reading_questions.length', reading_questions.length);
function make_submit_button() {
if (document.getElementById("rq_submit")) { // don't make the button if it already exists
console.log("button exists", document.getElementById("rq_submit"));
return
}
last_reading_question = reading_questions[reading_questions.length - 1];
answer_button_holder = document.createElement('div');
answer_button_holder.setAttribute('class', 'rq_submit_wrapper');
answer_button_holder.innerHTML = rq_submit_button;
last_reading_question.insertAdjacentElement("afterend", answer_button_holder);
}
function save_reading_questions() {
rq_data = {"action": "save", "user": uname, "pw": emanu, "pI": pageIdentifier, "type": "readingquestions", "rq": JSON.stringify(reading_questions_object)}
$.ajax({
url: "https://aimath.org/cgi-bin/u/highlights.py",
type: "post",
data: JSON.stringify(rq_data),
dataType: "json",
success: function(data) {
console.log("something", data, "back from highlight");
alert(data);
},
error: function(errMsg) {
console.log("seems to be an error?",errMsg);
alert("Error\n" + errMsg);
}
});
console.log("just ajax sent", JSON.stringify(reading_questions_object));
}
// no point in handling reading questions if there are not any
if (reading_questions.length) {
// retrieve the existing reading questions, if they exist
var reading_questions_object_id = pageIdentifier + "___" + "rq";
var reading_questions_object = localStorage.getObject(reading_questions_object_id);
var reading_questions_all_answered = false;
var reading_questions_submitted = false;
if (!reading_questions_object) {
reading_questions_object = {}
}
if (Object.keys(reading_questions_object).length >= reading_questions.length) {
console.log("Object.keys(reading_questions_object)",Object.keys(reading_questions_object));
console.log("reading_questions", reading_questions);
console.log("all reading questions have previously been answered");
reading_questions_all_answered = true;
}
answer_css = document.createElement('style');
answer_css.type = "text/css";
answer_css.id = "highlight_css";
document.head.appendChild(answer_css);
var css_for_ans = '#rq_submit { background: #FDD; padding: 3px 5px; border-radius: 0.5em}\n';
css_for_ans += '#rq_submit.submitted { background: #EFE; color: #BBB}';
css_for_ans += '.rq_submit_wrapper { margin-top: 0.5em; float: right}';
answer_css.innerHTML = css_for_ans;
rq_answer_label = '<span'
rq_answer_label += ' class="readingquestion_make_answer addcontent';
rq_answer_label += ' ' + role + '"';
// rq_answer_label += ' style="margin-left:1em; font-size:80%; color:#a0a;"';
rq_answer_label += '>';
if (role == "instructor") {
rq_answer_label += 'Responses&rarr;';
} else {
rq_answer_label += 'Click twice to edit...';
}
rq_answer_label +='</span>';
var rq_submit_button = '<span';
rq_submit_button += ' class="submit"';
rq_submit_button += ' id="rq_submit"';
rq_submit_button += '>';
rq_submit_button += 'Submit answers';
rq_submit_button +='</span>';
// make reading quesitons active, and insert answers if available
for (var j=0; j < reading_questions.length; ++j) {
var reading_question = reading_questions[j];
var reading_question_id = reading_question.id;
rq_answer_id = reading_question_id + "_text";
// var existing_content = localStorage.getObject(rq_answer_id);
var existing_content = reading_questions_object[rq_answer_id];
if (existing_content && role == "student") {
$('#'+reading_question_id).find(".readingquestion_make_answer").addClass("hidecontrols");
var this_ques_id_text = reading_question_id + "_text";
var this_ques_id_controls = reading_question_id + "_controls";
var answer_div = '<div';
answer_div += ' id="' + this_ques_id_text + '"';
answer_div += ' class="given_answer has_am process-math processme"';
answer_div += '>';
answer_div += dollars_to_slashparen(escapeHTML(existing_content)) + " ";
answer_div += '</div>';
/* need to save the original so that mathjax does not change it */
var hidden_answer_div = '<div';
hidden_answer_div += ' id="' + this_ques_id_text + '_hidden' + '"';
hidden_answer_div += ' class="tex2jax_ignore asciimath2jax_ignore" style="display: none">';
hidden_answer_div += escapeHTML(existing_content);
hidden_answer_div += '</div>';
var this_rq_controls = '<div id="' + this_ques_id_controls + '" class="input_controls hidecontrols">';
this_rq_controls += '<span class="action save_item rq_save">preview</span>';
// this_rq_controls += '<span class="action clear_item rq_delete">delete</span>';
this_rq_controls += '<span class="action amhelp">typing math?</span>';
this_rq_controls += '</div>'
var this_rq_answer_and_controls = document.createElement('div');
this_rq_answer_and_controls.setAttribute('style', 'width:80%; padding-left:10%; padding-right:10%; margin-top:0.5em;');
this_rq_answer_and_controls.setAttribute('class', 'rq_answer');
this_rq_answer_and_controls.innerHTML = hidden_answer_div + answer_div + this_rq_controls;
console.log("appending to ", reading_question_id);
$('#'+reading_question_id).append(this_rq_answer_and_controls);
// this.parentNode.insertAdjacentElement("afterend", this_rq_answer_and_controls);
/* typeset the math in the reading questions answers */
// MathJax.Hub.Queue(["Typeset",MathJax.Hub]);
if (mjvers && mjvers < 3) {
MathJax.Hub.Queue(['Typeset', MathJax.Hub]);
} else if (mjvers > 3) {
MathJax.typesetPromise();
}
} else if(role == "instructor" || role == "student") {
var this_answer_link = document.createElement('div');
this_answer_link.innerHTML = rq_answer_label;
console.log("inserting afterend of",reading_question);
// reading_question.insertAdjacentElement("afterend", this_answer_link);
reading_question.append(this_answer_link);
} else {
console.log("should not be here");
}
}
function allow_student_answers(){
/* make a new blank area to answer a question */
$('.readingquestion_make_answer.student').mousedown(function(e){
console.log(".readingquestion_make_answer student");
// $(this).addClass("hidecontrols");
// var this_ques_id = this.parentNode.parentNode.id;
var this_ques_id = this.parentNode.parentNode.id;
var this_ques_id_text = this_ques_id + "_text";
var this_ques_id_controls = this_ques_id + "_controls";
console.log(".rq", this_ques_id);
answer_textarea = '<textarea';
answer_textarea += ' class="rq_answer_text"'
answer_textarea += ' id="' + this_ques_id_text + '_input"'
answer_textarea += ' rows="' + '3' + '"';
answer_textarea += ' style="width:95%; height: 63px;"';
answer_textarea += '>';
answer_textarea += '</textarea>';
var this_rq_controls = '<div id="' + this_ques_id_controls + '" class="input_controls" style="margin-bottom:-1.9em;">';
this_rq_controls += '<span class="action save_item rq_save">preview</span>';
// this_rq_controls += '<span class="action clear_item rq_delete">delete</span>';
this_rq_controls += '<span class="action amhelp">typing math?</span>';
this_rq_controls += '</div>'
var this_rq_answer_and_controls = document.createElement('div');
this_rq_answer_and_controls.setAttribute('class', 'rq_answer editing');
this_rq_answer_and_controls.setAttribute('style', 'width:80%; padding-left:10%; padding-right:10%; margin-top:0.5em;');
this_rq_answer_and_controls.innerHTML = answer_textarea + this_rq_controls;
this.parentNode.insertAdjacentElement("afterend", this_rq_answer_and_controls);
this.remove();
// MathJax.Hub.Queue(["Typeset",MathJax.Hub]);
console.log("adding other keypress listener");
var this_textarea = document.getElementById(this_ques_id_text + "_input");
console.log("to", this_textarea);
this_textarea.addEventListener("keypress", function() {
// if(this_textarea.scrollTop != 0){
// console.log("this_textarea.scrollHeight", this_textarea.scrollHeight, "this_textarea.scrollTop", this_textarea.scrollTop);
// console.log("this_textarea.clientHeight", this_textarea.clientHeight);
this_textarea.overflow = "scroll";
// console.log("this_textarea.scrollHeight", this_textarea.scrollHeight, "this_textarea.scrollTop", this_textarea.scrollTop);
// console.log("this_textarea.clientHeight", this_textarea.clientHeight);
this_textarea.overflow = "hidden";
// console.log("this_textarea.scrollHeight", this_textarea.scrollHeight, "this_textarea.scrollTop", this_textarea.scrollTop);
// console.log("this_textarea.getBoundingClientRect()", this_textarea.getBoundingClientRect());
this_textarea.style.height = this_textarea.scrollHeight + "px";
// }
}, false);
var tmp_id = this_ques_id_text + "_input";
console.log("want focus to ", tmp_id);
console.log("which is ", $("#" +tmp_id));
console.log("the active ", document.hasFocus(), "element is", document.activeElement);
console.log("or maybe it is ", $(":focus"));
$("#" + tmp_id).focus();
document.getElementById(tmp_id).focus();
console.log("the active ", document.hasFocus(), "element is", document.activeElement);
});
}
allow_student_answers();
$('.readingquestion_make_answer.instructor').mousedown(function(e){
console.log(".readingquestion_make_answer instructor", "instId", uname, "pI", pageIdentifier);
if (jQuery.isEmptyObject(reading_answers) || this.classList.contains("reload")) {
rq_data = {"action": "retrieve", "instId": uname, "pw": emanu, "pI": pageIdentifier, "type": "readingquestions"};
// myjson = {"action": "retrieve", "type": "readingquestions", "instId": "100002000", "pI": "beezer-FCLA___FPm"}
$.ajax({
url: "https://aimath.org/cgi-bin/u/highlights.py",
type: "post",
data: JSON.stringify(rq_data),
dataType: "json",
async: false,
success: function(data) {
reading_answers = data;
// console.log("something", data, "back from highlight");
// alert(data);
},
error: function(errMsg) {
console.log("seems to be an error?",errMsg);
alert("Error\n" + errMsg);
}
});
}
// var this_ques_id = this.parentNode.previousSibling.id;
var this_ques_id = this.parentNode.parentNode.id;
console.log("this_ques_id", this_ques_id);
var compiled_answers = "";
var title_of_this_section = $("section > h2 > .title").html();
title_of_this_section = title_of_this_section.replace(/ /g, "%20");
title_of_this_section = title_of_this_section.replace(/\?/g, "");
var number_of_this_rq = 1 + $("#" + this_ques_id).index(".exercise-like");
// console.log("first title_of_this_section", title_of_this_section);
// console.log("first title_of_this_section.html()", title_of_this_section.html());
// console.log("second title_of_this_section[0]", title_of_this_section[0]);
for(var j=0; j < reading_answers.length; ++j) {
var this_answer_all = reading_answers[j];
var this_student_id = this_answer_all[0];
var these_specific_answers = JSON.parse(this_answer_all[1]);
console.log("these_specific_answers", these_specific_answers);
console.log("this_answer_all[2]", this_answer_all[2]);
var this_submitted_time = JSON.parse(this_answer_all[2]);
// var this_submitted_time = this_answer_all[2];
console.log("looking for this answer:", this_ques_id + "_text");
var this_specific_answer = these_specific_answers[this_ques_id + "_text"];
console.log("this_answer_all",this_answer_all);
console.log("j",j,"this_specific_answer", this_specific_answer);
console.log("this_specific_time", this_submitted_time);
this_specific_answer = dollars_to_slashparen(escapeHTML(this_specific_answer))
if (!this_specific_answer) {
this_specific_answer = "no answer submitted";
compiled_answers += '<div class="one_answer noanswer">';
} else {
compiled_answers += '<div class="one_answer">';
}
if (this_student_id.indexOf('@') > -1) {
this_student_id = '<a href="mailto:' + this_student_id + '?Subject=RQ' + number_of_this_rq + '%20of%20' + title_of_this_section +'">' + this_student_id + '</a>';
}
compiled_answers += '<div class="s_id">' + this_student_id + '</div>';
compiled_answers += '<div class="rq_sub_time">' + this_submitted_time + '</div>';
compiled_answers += '<div class="s_ans has_am process-math processme">' + this_specific_answer + '</div>';
compiled_answers += '</div>\n';
console.log(j, "j", these_specific_answers)
}
// if the answers are being reloaded, remove the previous answers
$("#" + this_ques_id + "_ans").remove();
var answers_to_this_question = document.createElement('div');
answers_to_this_question.setAttribute('class', 'compiled_answers');
answers_to_this_question.setAttribute('id', this_ques_id + "_ans");
// this_rq_answer_and_controls.setAttribute('style', 'width:80%; margin-left:auto; margin-right:auto; margin-top:0.5em;');
// this_rq_answer_and_controls.setAttribute('class', 'rq_answer');
answers_to_this_question.innerHTML = compiled_answers;
$('#'+this_ques_id).append(answers_to_this_question);
// this.parentNode.remove();
this.innerHTML = "Reload responses";
$(this).addClass("reload");
// MathJax.Hub.Queue(["Typeset",MathJax.Hub]);
if (mjvers && mjvers < 3) {
MathJax.Hub.Queue(['Typeset', MathJax.Hub]);
} else if (mjvers > 3) {
MathJax.typesetPromise();
}
// this.parentNode.insertAdjacentElement("afterend", this_rq_answe
});
function save_one_reading_question(this_ques_id) {
this_ques_id_text = this_ques_id + "_text";
var this_ans_text = $("#" + this_ques_id_text + "_input");
console.log("the value:", this_ans_text.value);
// var this_ques_id = this_ans.parentNode.previousSibling.previousSibling.id;
// var this_ques_id = this_ans.id;
console.log("this_ques_id", this_ques_id);
// console.log("this_rq_ans", this_rq_ans);
var this_ans_text_value = this_ans_text.val();
console.log("this_ans_text_value", this_ans_text_value);
if ( /[^\x00-\x7F]/.test(this_ans_text_value)) {
this_ans_text_value = this_ans_text_value.replace(/[^\x00-\x7F]/g, "XX");
alert("Illegal characters in answer have been replaced by XX");
}
this_ans_text_value = $.trim(this_ans_text_value); // jQuery trim (some chrome on windows had trouble with trim)
// we have the contents of the answer, so save it to local storage
console.log("saving in local storage at", this_ques_id_text, "the answer", this_ans_text_value);
reading_questions_object[this_ques_id_text] = this_ans_text_value;
localStorage.setObject(reading_questions_object_id, reading_questions_object);
console.log("Object.keys(reading_questions_object)",Object.keys(reading_questions_object));
console.log("reading_questions", reading_questions);
if (Object.keys(reading_questions_object).length >= reading_questions.length && uname != "guest" && role=="student") {
console.log("all reading questions have been answered");
reading_questions_all_answered = true;
make_submit_button();
}
// and save a copy hidden on the page
console.log("looking for", this_ques_id + "_text_hidden");
// when the initial answer box is created, there is no hidden version
if ( !document.getElementById(this_ques_id + "_text_hidden")) {
console.log("making a place to hide the answer");
var hidden_answer_div = document.createElement('div');
hidden_answer_div.setAttribute('id', this_ques_id + '_text_hidden');
hidden_answer_div.setAttribute('class', 'tex2jax_ignore asciimath2jax_ignore');
hidden_answer_div.setAttribute('style', 'display: none');
console.log("this_ans_text", this_ans_text);
document.getElementById(this_ques_id_text + "_input").insertAdjacentElement("beforebegin", hidden_answer_div);
// hidden_answer_div.insertBefore(this_ans_text);
}
console.log("hiding the raw answer in", this_ques_id + "_text_hidden");
document.getElementById(this_ques_id + "_text_hidden").innerHTML = escapeHTML(this_ans_text_value);
//and show it on the page
var this_ans_static = document.createElement('div');
this_ans_static.setAttribute('id', this_ques_id_text);
this_ans_static.setAttribute('class', 'given_answer has_am process-math processme');
console.log("setting this_ans_static.innerHTML to", dollars_to_slashparen(escapeHTML(this_ans_text_value)));
this_ans_static.innerHTML = dollars_to_slashparen(escapeHTML(this_ans_text_value)) + " "
// this_rq_ans.replaceWith(this_ans_static);
console.log("about to replace this_ans_text", this_ans_text);
this_ans_text.replaceWith(this_ans_static);
// MathJax.Hub.Queue(["Typeset",MathJax.Hub]);
if (mjvers && mjvers < 3) {
MathJax.Hub.Queue(['Typeset', MathJax.Hub]);
} else if (mjvers > 3) {
MathJax.typesetPromise();
}
console.log(" this_ans_text", this_ans_text);
console.log("this_ans_static", this_ans_static);
$(this_ans_static).parent().addClass("rq_answer");
$('#' + this_ques_id + "_controls").addClass("hidecontrols");
// $(this_q).parent().parent().addClass("rq_answer");
/*
var edit_button = document.createElement('span');
edit_button.setAttribute('class', "action edit_item rq_edit");
edit_button.innerHTML = "edit";
this.replaceWith(edit_button);
*/
};
/* edit an existing answer */
function edit_one_reading_question(this_ans) {
console.log(".rq_edit", this_ans);
// var this_ques_id = this.parentNode.previousSibling.id;
var this_ques_id = this_ans.parentNode.parentNode.id;
console.log("edditing this_ques_id", this_ques_id);
var this_ques_id_text = this_ques_id + "_text";
// var this_rq_ans = this.parentNode.previousSibling;
var this_rq_ans = this_ans;
// var this_rq_ans_id = this_ans.id;
console.log("now this_ques_id_text", this_ques_id_text);
console.log(".rq_edit", this_ques_id);
console.log("this_rq_ans", this_rq_ans);
var this_rq_text = this_rq_ans.innerHTML;
console.log("looking for", this_ques_id + "_hidden");
var this_rq_text_raw = uNescapeHTML(document.getElementById(this_ques_id + "_text_hidden").innerHTML);
console.log("this_rq_text_raw",this_rq_text_raw);
//this is copied from above. need to eliminate repeated code
var answer_textarea_editable = document.createElement('textarea');
answer_textarea_editable.setAttribute('id', this_ques_id_text + "_input");
answer_textarea_editable.setAttribute('class', 'rq_answer_text');
answer_textarea_editable.setAttribute('rows', '3');
answer_textarea_editable.setAttribute('style', 'width:95%; height: 44px;');
this_rq_ans.replaceWith(answer_textarea_editable);
console.log("this_ans is",this_ans);
console.log("adding editing to the parent of thing with id", this_ques_id_text, "which is the parent of", $('#' + this_ques_id_text));
$('#' + this_ques_id_text + "_input").parent().addClass("editing");
$('#' + this_ques_id + "_controls").removeClass("hidecontrols");
// WHY? $(this).parent().parent().removeClass("rq_answer");
/* var save_button = document.createElement('span');
save_button.setAttribute('class', "action edit_item rq_save");
save_button.innerHTML = "save";
this.replaceWith(save_button);
*/
$('#' + this_ques_id + "_text_input").val(this_rq_text_raw);
answer_textarea_editable.style.height = answer_textarea_editable.scrollHeight + "px";
answer_textarea_editable.addEventListener("keypress", function() {
// if(answer_textarea_editable.scrollTop != 0){
answer_textarea_editable.style.height = answer_textarea_editable.scrollHeight + "px";
// }
}, false);
};
/* handle saving when leaving an answer box, or editing an existing answer
when hovering over an existing answer */
$('body').on('click','.given_answer', function(){
// $(this).children().last().removeClass("hidecontrols");
edit_one_reading_question(this);
$(this).attr('z-index', '2000');
});
$('body').on('mousedown','.rq_save', function(){
// $(this).children().last().addClass("hidecontrols");
// $(this).attr('z-index', '');
// var this_rq = $(this).find(".given_answer");
//ooooooooo var this_ques_id = this.parentNode.id;
var this_ques_id = this.parentNode.parentNode.parentNode.id;
console.log("id of this quesiton", this_ques_id);
// var this_rq = $(this).find(".rq_answer_text");
// console.log("this_rq iiIIIIII", this_rq);
var this_current_answer = document.getElementById(this_ques_id + "_text_input");
console.log("this_current_answer", this_current_answer);
console.log("this_current_answer.value", this_current_answer.value);
// console.log("this_rq IIIIiiii value", this_rq.value);
// save_one_reading_question(this_rq.parentNode.id);
save_one_reading_question(this_ques_id);
console.log("left answer area");
$(this).removeClass("editing");
});
$('body').on('click','.rq_delete', function(){
console.log(".rq_delete");
var this_ques_id = this.parentNode.parentNode.parentNode.id;
console.log(".rq_delete", this_ques_id);
$('#' + this_ques_id + "_controls").removeClass("hidecontrols");
//and now put in controls
var this_answer_link = document.createElement('div');
this_answer_link.innerHTML = rq_answer_label;
console.log("inserting afterend of",reading_question);
// reading_question.insertAdjacentElement("afterend", this_answer_link);
$(this).parent().parent().replaceWith(this_answer_link);
allow_student_answers();
console.log("reading_questions_object", reading_questions_object);
console.log("this_ques_id + _text", this_ques_id + "_text");
delete reading_questions_object[this_ques_id + "_text"];
console.log("now reading_questions_object", reading_questions_object);
// localStorage.removeItem(this_ques_id);
localStorage.setObject(reading_questions_object_id, reading_questions_object);
});
if(reading_questions_all_answered && uname != "guest" && role=="student") {
make_submit_button();
console.log("made submit button");
}
$('body').on('click','#rq_submit', function(){
console.log("submitting rq answers");
$('#rq_submit').addClass('submitted');
document.getElementById('rq_submit').textContent = "Resubmit answers";
save_reading_questions();
});
$('body').on('click','.amhelp', function(){
var amhelpmessage = "Write math formulas as AsciiMath inside `backticks`.\n";
amhelpmessage += "For example:\nThe Pythagorean theorem says `sin^2(x) + cos^2(x) = 1`,\n";
amhelpmessage += "The quadratic formula is `x = (-b +- sqrt(b^2 - 4ac))/(2a)`. \n";
amhelpmessage += "Note the use of parentheses for grouping.\n";
amhelpmessage += "Visit http://asciimath.org for a list of AsciiMath commands.\n\n";
amhelpmessage += "You can also use LaTeX, with either slash-parentheses \\(...\\)\n";
amhelpmessage += "or dollar signs $...$ as delimiters for inline math.";
alert(amhelpmessage)
});
if(reading_questions_all_answered && uname != "guest" && role=="student") {
make_submit_button();
}
}
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
+480
View File
@@ -0,0 +1,480 @@
/*
highlight_css = document.createElement('style');
highlight_css.type = "text/css";
highlight_css.id = "highlight_css";
document.head.appendChild(highlight_css);
var css_for_hl = 'span.hl { background: yellow; }\n';
css_for_hl += '#hlmenu { position: absolute; top: 300px; left: 200px;}\n';
css_for_hl += '#hlmenu { padding: 8px; background: #FFF; }\n';
css_for_hl += '#hlmenu { box-shadow: 8px 10px 5px #888; border: 1px solid #aaa;}\n';
css_for_hl += '#hlmenu .hldelete { background: #fdd; }';
css_for_hl += '#hlmenu .hldelete:hover { background: #fbb; }';
css_for_hl += '#hlmenu .hlcopy { background: #ddf; }';
css_for_hl += '#hlmenu .hlcopy:hover { background: #bbf; }';
css_for_hl += '#hlmenu .dismiss:hover { background: #ff9; }';
css_for_hl += '#hlmenu > div { padding: 4px; font-size: 90%}';
highlight_css.innerHTML = css_for_hl;
*/
hlmenu = document.createElement('div');
hlmenu.id = "hlmenu";
hlmenu.style.display = "none";
hlmenu_contents = '<div class="hlcopy" data-hlid="">copy to clipboard</div>\n';
hlmenu_contents += '<div class="hldelete" data-hlid="">delete highlight</div>\n';
hlmenu_contents += '<div class="dismiss">dismiss menu</div>';
hlmenu.innerHTML = hlmenu_contents;
document.body.appendChild(hlmenu);
var all_highlights = localStorage.getObject("all_highlights");
if (!all_highlights) {
console.log("no highlights on this page",all_highlights);
all_highlights= {};
} else {
console.log("highlights already",all_highlights);
}
console.log("all_highlights.keys()", Object.keys(all_highlights));
console.log("waiting for MathJax");
MathJax.Hub.Register.StartupHook("End",function () {
// need to wait for MathJax, because MathJax changes the number of nodes in a paragraph
console.log("MathJax is done");
display_all_highlights(all_highlights, Object.keys(all_highlights), 0);
});
/*
for (var key in all_highlights) {
var this_key = key;
var these_highlights = all_highlights[key];
console.log("adding highlights to", key);
// await display_highlights_on(key, all_highlights[key], 0)
for (var i=0; i< these_highlights.length; ++i) {
hl = these_highlights[i];
console.log("inserting highlight",i,"which is",hl, "on", key);
// display_one_highlight(key, hl);
//fails because
setTimeout(function() { display_one_highlight(this_key, hl)}, i*2000);
}
}
*/
function index_of_child(child) {
var i = 0;
while( (child = child.previousSibling) != null )
{i++ }
return i
}
function increment_id(list_of_things_with_ids) { // assume ids are of the form xxxxNN with N a digit and x a non-digit
console.log("incrementing id on", list_of_things_with_ids);
if (list_of_things_with_ids.length == 0) {
return 0
}
id_start = list_of_things_with_ids[0]["id"].replace(/^(.*?)[0-9]+$/, "$1");
current_endings = [];
for (var i=0; i < list_of_things_with_ids.length; ++i) {
current_endings.push(list_of_things_with_ids[i]["id"].replace(/^.*?([0-9]+)$/, "$1"));
}
console.log("existing id endings", current_endings);
current_max = Math.max(...current_endings);
return id_start + (parseInt(current_max) + 1)
}
function enclosing_p_or_li(obj) {
console.log("obj.tagName", obj.tagName, "ggg",obj);
if (!obj) {
console.log("problem with previous object");
return null
}
else if (obj.tagName == 'P' || obj.tagName == 'LI') {
return obj
}
return enclosing_p_or_li(obj.parentNode)
}
// If you just loop ofer the highlights to add them,
// you end up with a race condition because each highlight changes
// the structure of the paragraph.
async function display_one_highlight(parent_id, hl) {
var the_parent = document.getElementById(parent_id);
if (!the_parent) {
console.log("this id not on this page:", parent_id);
return
}
console.log("setting", hl, "on", parent_id);
var st_node_ind = hl['start_nn'];
var st_offset = hl['start_offset'];
var end_node_ind = hl['end_nn'];
var end_offset = hl['end_offset'];
console.log("st_node_ind", st_node_ind, "st_offset", st_offset, "end_node_ind", end_node_ind, "end_offset", end_offset);
// other error checks: same parent
if (st_offset < 0 || st_node_ind > the_parent.childNodes.length || end_node_ind > the_parent.childNodes.length || st_offset > the_parent.childNodes[st_node_ind].textContent.length || end_offset < 0 || end_offset > the_parent.childNodes[end_node_ind].textContent.length) {
console.log("highlight data inconsistent with paragraph structure that contains", the_parent.childNodes.length, "nodes");
return
}
let this_range = document.createRange();
console.log("setting this_range.setStart", the_parent.childNodes[st_node_ind], "with offset", st_offset, "out of", the_parent.childNodes[st_node_ind].textContent.length);
this_range.setStart(the_parent.childNodes[st_node_ind], st_offset);
this_range.setEnd(the_parent.childNodes[end_node_ind], end_offset);
var inside_part = document.createElement("span")
inside_part.classList.add("hl");
inside_part.id = hl['id'];
console.log("inside_part", inside_part, "going inside this_range",this_range);
console.log("in the_parent",the_parent);
this_range.surroundContents(inside_part);
return;
}
//var display_highlights_on = function(parent_id, highlights_on, ind) {
async function display_highlights_on(parent_id, highlights_on, ind) {
console.log("display_highlights_on", parent_id, "xxxx", ind, "out of", highlights_on.length);
if (ind < highlights_on.length) {
console.log("about to display one highlight, number", ind, "on", parent_id);
await display_one_highlight(parent_id, highlights_on[ind]);
++ind;
await display_highlights_on(parent_id, highlights_on, ind)
}
return;
}
async function display_all_highlights(every_highlight, hl_p_keys, i) {
console.log("making hl on", hl_p_keys[i]);
if (i < hl_p_keys.length) {
await display_highlights_on(hl_p_keys[i], every_highlight[hl_p_keys[i]], 0);
++i;
await display_all_highlights(every_highlight, hl_p_keys, i)
}
return;
}
var DELAY = 500, clicks = 0, timer = null;
$("p[id], li[id]").on("click", function(e) {
clicks++; //count clicks
if(clicks === 1) {
if (e.target.classList.contains("hl")) {
modifyhighlight(e);
clicks = 0;
} else {
timer = setTimeout(function() {
newhighlight()
clicks = 0; //after action performed, reset counter
}, DELAY);
}
} else if (clicks === 2) {
timer2 = setTimeout(function() {
clearTimeout(timer);
// alert("Double Click");
clicks = 0;
}, DELAY/2);
} else {
clearTimeout(timer); //prevent single-click action
clearTimeout(timer2); //prevent double-click action
// alert("Triple Click"); //perform triple-click action
clicks = 0; //after action performed, reset counter
}
});
function save_highlights() {
hl_data = {"action": "save", "user": uname, "pw": emanu, "bookID": bodyID, "type": "highlights", "hl": JSON.stringify(all_highlights)}
$.ajax({
url: "https://aimath.org/cgi-bin/u/highlights.py",
type: "post",
data: JSON.stringify(hl_data),
dataType: "json",
success: function(data) {
console.log("something", data, "back from highlight");
// alert(data);
},
error: function(errMsg) {
console.log("seems to be an error?",errMsg);
// alert("Error\n" + errMsg);
}
});
console.log("just ajax sent", JSON.stringify(reading_questions_object));
}
function newhighlight() {
var this_selection = window.getSelection();
console.log("UUUUUUUUUUUUUUUUUUUUUUUUUUU");
console.log("selection", this_selection, "as string", this_selection.toString(), "length", this_selection.toString().length);
if(this_selection.toString().length >= 3) {
console.log("this_selection", this_selection);
console.log("this_selection parentNode", this_selection.parentNode);
num_selected_ranges = this_selection.rangeCount;
console.log("this_selection.rangeCount", this_selection.rangeCount);
console.log("this_selection.getRangeAt(0)", this_selection.getRangeAt(0));
console.log("this_selection.getRangeAt(num_selected_ranges)", this_selection.getRangeAt(num_selected_ranges-1));
console.log("this_selection.getRangeAt(0).startContainer", this_selection.getRangeAt(0).startContainer);
console.log("this_selection.getRangeAt(0).endContainer", this_selection.getRangeAt(0).endContainer);
console.log("this_selection.getRangeAt(-1).startContainer", this_selection.getRangeAt(num_selected_ranges-1).startContainer);
console.log("this_selection.getRangeAt(-1).endContainer", this_selection.getRangeAt(num_selected_ranges-1).endContainer);
console.log("start equals end", this_selection.getRangeAt(0).startContainer == this_selection.getRangeAt(0).endContainer);
console.log("this_selection.getRangeAt(0).startContainer.parent", this_selection.getRangeAt(0).startContainer.parentNode);
console.log("this_selection.getRangeAt(0).endContainer.parent", this_selection.getRangeAt(0).endContainer.parentNode);
starting_parent = enclosing_p_or_li(this_selection.getRangeAt(0).startContainer);
starting_parent_id = starting_parent.id;
ending_parent_id = enclosing_p_or_li(this_selection.getRangeAt(num_selected_ranges-1).endContainer).id;
console.log("starting parent", enclosing_p_or_li(this_selection.getRangeAt(0).startContainer));
console.log("starting_parent.childNodes", starting_parent.childNodes);
console.log("starting_parent.childNodes.length", starting_parent.childNodes.length);
console.log("ending parent", enclosing_p_or_li(this_selection.getRangeAt(0).endContainer));
console.log("index of starting node", index_of_child(this_selection.getRangeAt(0).startContainer));
console.log("which is", starting_parent.childNodes[index_of_child(this_selection.getRangeAt(0).startContainer)]);
console.log("index of ending node", index_of_child(this_selection.getRangeAt(num_selected_ranges-1).endContainer));
console.log("which is", starting_parent.childNodes[index_of_child(this_selection.getRangeAt(num_selected_ranges-1).endContainer)]);
console.log("XXXXXXXXXXXXXXXXXXXXXXX");
starting_node = this_selection.getRangeAt(0);
starting_node_container = starting_node.startContainer;
starting_node_number = index_of_child(starting_node_container);
starting_node_offset = starting_node.startOffset;
console.log("starting_node", starting_node, "starting_node_container", starting_node_container);
console.log("starting_parent.childNodes[0]", starting_parent.childNodes[0]);
ending_node = this_selection.getRangeAt(num_selected_ranges-1);
ending_node_container = ending_node.endContainer;
console.log("ending_node", ending_node, "ending_node_container", ending_node_container);
ending_node_number = index_of_child(ending_node_container);
ending_node_offset = ending_node.endOffset;
console.log("selection starts at character number", starting_node.startOffset, "in node number", index_of_child(starting_node), "of node", starting_node.startContainer, "within", starting_node.startContainer.parentNode,"which is", starting_node);
console.log("selection ends at character number", ending_node.endOffset, "in node number", index_of_child(ending_node_container), "of node", ending_node.endContainer, "within", ending_node.endContainer.parentNode, "which is", ending_node);
// let this_range = document.createRange();
// this_range.setStart(starting_parent.childNodes[starting_node_number], starting_node_offset);
// this_range.setEnd(document.getElementById(starting_parent_id).childNodes[ending_node_number], ending_node_offset);
// test_inside_part=document.createElement("span")
// test_inside_part.classList.add("gggg");
// this_range.surroundContents(test_inside_part);
console.log("num_selected_ranges", num_selected_ranges, "starting_parent_id", starting_parent_id, "ending_parent_id", ending_parent_id);
this_selection.empty();
console.log("starting_parent_id", starting_parent_id, "ending_parent_id", ending_parent_id);
if (starting_parent_id != ending_parent_id) {
alert("Highlights must be within\none paragraph or list item.");
return ""
}
if (starting_parent != starting_node_container.parentNode) {
starting_node_number = index_of_child(starting_node_container.parentNode) - 1;
console.log("new starting node",starting_node_number, " which is", starting_parent.childNodes[starting_node_number]);
starting_node_offset = starting_parent.childNodes[starting_node_number].length;
}
if (starting_parent != ending_node_container.parentNode) {
ending_node_number = index_of_child(ending_node_container.parentNode) + 1;
console.log("new edngin node",ending_node_number, " which is", starting_parent.childNodes[ending_node_number]);
ending_node_offset = 0;
}
console.log("starting_parent eq st cont parent", starting_parent == starting_node_container.parentNode, "starting_parent eq end cont parent", starting_parent == ending_node_container.parentNode);
// display_one_highlight(starting_parent_id, starting_node_number, starting_node_offset, ending_node_number, ending_node_offset);
// console.log("this_range", this_range);
this_highlight = {"start_nn": starting_node_number,
"start_offset": starting_node_offset,
"end_nn": ending_node_number,
"end_offset": ending_node_offset};
if (starting_parent_id in all_highlights) {
console.log("the starting_parent_id", starting_parent_id, "is already in all_highlights", all_highlights);
new_id = increment_id(all_highlights[starting_parent_id]);
this_highlight['id'] = new_id;
all_highlights[starting_parent_id].push(this_highlight)
} else {
this_highlight['id'] = starting_parent_id + "-" + "hl" + 1;
all_highlights[starting_parent_id] = [this_highlight]
}
display_one_highlight(starting_parent_id, this_highlight);
localStorage.setObject("all_highlights", all_highlights);
if (uname != "guest" && role=="student") {
console.log("saving highlights");
save_highlights();
}
console.log("all_highlights", all_highlights);
return "";
}
}
function modifyhighlight(e) {
this_hl = e.target;
console.log("clicked hl", this_hl.id);
var x = e.clientX, y = e.clientY;
console.log("x", x, "y", y);
document.getElementById("hlmenu").style.top = (y - 10 + $(window).scrollTop()) + 'px';
document.getElementById("hlmenu").style.left = (x + 20) + 'px';
document.getElementById("hlmenu").style.display = 'block';
document.getElementsByClassName("hlcopy")[0].setAttribute("data-hlid", this_hl.id);
document.getElementsByClassName("hldelete")[0].setAttribute("data-hlid", this_hl.id);
// tooltipSpan.style.left = (x + 20) + 'px';
var parent_id = this_hl.id.replace(/^(.*)-[^\-]*$/, "$1");
var number_of_this_highlight = this_hl.id.slice(-1);
var these_highlights = all_highlights[parent_id];
var this_highlight = these_highlights[number_of_this_highlight-1];
console.log("parent id", parent_id, "nunber",number_of_this_highlight);
var num_child_nodes = this_hl.childNodes.length;
console.log("which has", this_hl.childNodes.length, "child nodes");
for (var i=0; i < num_child_nodes; ++i) {
console.log(i, "node is", this_hl.childNodes[i])
}
console.log("highlights on this item",all_highlights[parent_id], "of which we clicked", these_highlights[number_of_this_highlight - 1],
"number", number_of_this_highlight, "out of", these_highlights.length );
for (var i=0; i<these_highlights.length; ++i) {
console.log("highilght", i, "is", these_highlights[i])
}
console.log("this one starts at", this_highlight["start_nn"], "and ends at", this_highlight["end_nn"]);
console.log("$(this_hl)", $(this_hl));
console.log("$(this_hl).contents", $(this_hl).text());
console.log("this_hl.innerHTML", this_hl.innerHTML);
if (this_hl.childNodes.length == 1) { //highlight contains only text
if (this_hl.previousSibling.nodeType == 3 && this_hl.nextSibling.nodeType == 3) { // prev and next are also text
console.log("this_hl", this_hl, "this_hl.previousSibling", this_hl.previousSibling, "type", this_hl.previousSibling.nodeType);
}
}
}
//from https://techoverflow.net/2018/03/30/copying-strings-to-the-clipboard-using-pure-javascript/
function copyStringToClipboard (str) {
// Create new element
var el = document.createElement('textarea');
// Set value (string to be copied)
el.value = str;
// Set non-editable to avoid focus and move outside of view
el.setAttribute('readonly', '');
el.style = {position: 'absolute', left: '-9999px'};
document.body.appendChild(el);
// Select text inside element
el.select();
// Copy text to clipboard
document.execCommand('copy');
// Remove temporary element
document.body.removeChild(el);
}
$('body').on('click','.hlcopy', function(e){
var hl_to_copy_id = this.getAttribute("data-hlid");
console.log("copying highlight", hl_to_copy_id);
hl_to_copy = document.getElementById(hl_to_copy_id);
the_highlight = hl_to_copy.innerHTML;
copyStringToClipboard(the_highlight);
// console.log("hl_to_copy",hl_to_copy,"the_highlight", the_highlight);
// hl_to_copy.focus();
// document.execCommand('copy');
document.getElementById("hlmenu").style.display = 'none';
});
$('body').on('click','.hldelete', function(e){
var hl_to_del_id = this.getAttribute("data-hlid");
console.log("going to delete", hl_to_del_id);
hl_to_delete = document.getElementById(hl_to_del_id);
var parent_id = hl_to_del_id.replace(/^(.*)-[^\-]*$/, "$1");
var the_parent = document.getElementById(parent_id);
//and then update the stored highlights
//this is complicated, because highlights are described in terms of the structure
//of the paragrpah, which depends on which highlights already xist
//So first we determine the order in which the highlights appear
//(and find out how many nodes are being deleted)
var sibling_nodes = the_parent.childNodes;
var node_counter = {};
var additional_offset, node_increment;
var node_ct = 0;
for (var i=0; i < sibling_nodes.length; ++i) {
if (sibling_nodes[i].id) {
node_counter[sibling_nodes[i].id] = node_ct;
node_ct += 1;
}
if (sibling_nodes[i].id == hl_to_del_id) {
node_increment = sibling_nodes[i].childNodes.length - 1
}
}
console.log("node_increment", node_increment);
console.log("node_counter", node_counter);
console.log("deleting node", node_counter[hl_to_del_id]);
//find out the offset of the node to be deleted
console.log("all_highlights[parent_id]", all_highlights[parent_id]);
for (var i=0; i < all_highlights[parent_id].length; ++i) {
var one_highlight = all_highlights[parent_id][i];
console.log("checking one_highlight", one_highlight);
if (node_counter[one_highlight['id']] == node_counter[hl_to_del_id]) {
additional_offset = one_highlight['end_offset'];
the_hl_to_delete = all_highlights[parent_id][i];
}
}
console.log("the_hl_to_delete", the_hl_to_delete);
console.log("additional_offset", additional_offset);
//then we re-make the list of highlights of this parent
//go through the highlights, adjusting those which we added after the
//lh being deleted, and whcich also occurs later in the text.
//Note: this is only for the case if pure text paragraphs
var new_highlights = [];
var previous_highlight;
var past_this_hl = false;
for (var i=0; i < all_highlights[parent_id].length; ++i) {
var one_highlight = all_highlights[parent_id][i];
console.log("again checking one_highlight", one_highlight);
console.log("x",one_highlight["start_nn"], "y", the_hl_to_delete["end_nn"], "z", the_hl_to_delete["end_nn"] + additional_offset);
if (!past_this_hl) {
if (one_highlight["id"] == hl_to_del_id) {
console.log("found it", one_highlight);
past_this_hl = true
} else {
new_highlights.push(one_highlight)
}
} else { // hl occurs after, but maybe not physically after
console.log('the_hl_to_delete["start_nn"]', the_hl_to_delete["start_nn"], 'one_highlight["start_nn"]', one_highlight["start_nn"]);
if (node_counter[one_highlight["id"]] < node_counter[hl_to_del_id]) {
console.log("case 0");
new_highlights.push(one_highlight)
} else if (node_counter[one_highlight["id"]] == node_counter[hl_to_del_id]) {
console.log("ERROR, we shoudl be past this point!");
continue
} else if (one_highlight["start_nn"] == the_hl_to_delete["start_nn"] + 2){
console.log("offset match with", one_highlight);
previous_highlight = one_highlight;
previous_highlight['start_nn'] += -2 + node_increment;
previous_highlight['end_nn'] += -2 + node_increment;
previous_highlight['start_offset'] += additional_offset;
previous_highlight['end_offset'] += additional_offset;
new_highlights.push(previous_highlight);
} else {
console.log("apparently a far away match", one_highlight);
previous_highlight = one_highlight;
previous_highlight['start_nn'] += -2 + node_increment;
previous_highlight['end_nn'] += -2 + node_increment;
new_highlights.push(previous_highlight);
}
}
}
console.log("the new_highlights", new_highlights, "parent_id", parent_id);
if (new_highlights.length > 0) {
all_highlights[parent_id] = new_highlights
} else {
delete all_highlights[parent_id]
}
localStorage.setObject("all_highlights", all_highlights);
// hide the current highlight
$(hl_to_delete).replaceWith(hl_to_delete.innerHTML);
document.getElementById(parent_id).normalize(); // because a previous step creates adjacent text nodes
document.getElementById("hlmenu").style.display = 'none';
});
$('body').on('click','.dismiss', function(e){
console.log(".dismiss of",this);
the_parent = this.closest("[id]");
the_parent.style.display = 'none';
});
+131
View File
@@ -0,0 +1,131 @@
var icon = {
"warning": "⚠️ ",
"commentary": "&#9749;",
"media": "&#9654;",
"tip": "&#128227;",
"worksheet": "&#128196;",
"assesment": "A<sup>+</sup>",
"slides": "&#128251;",
"outcomes": "&#10003;"
};
var html_words_of = {
"warning": "Common pitfall",
"commentary": "Alert",
"media": "An amusing demo",
"tip": "Tip: discussion point",
"worksheet": "Worksheet:",
"assesment": "Sample exam:",
"slides": "Slides:",
"outcomes": "Learning outcomes"
}
// <br>&nbsp;&nbsp;&nbsp;<a href='word'>Word</a> <a href='word'>PTX</a> <a href='word'>PDF</a></span>",
var type_name = {
"warning": "warning",
"commentary": "commentary",
"media": "media",
"tip": "tip",
"worksheet": "worksheet",
"assesment": "assesment",
"slides": "slides",
"outcomes": "outcomes"
};
console.log("in instructor.js", role, "role", logged_in, "logged_in");
if (role=="instructor") {
console.log(" loading instructor resources");
var instructor_resources = document.querySelectorAll(".instructor");
console.log('instructor_resources.length', instructor_resources.length);
var icons_on_this_page = [];
for (var j=0; j < instructor_resources.length; ++j) {
var instructor_resource = instructor_resources[j];
var instructor_resource_parent_id = instructor_resource.parentNode.id;
console.log(" XXXXXXXXXXXX instructor_resource.parentNode", instructor_resource.parentNode);
console.log("instructor_resource_parent_id", instructor_resource_parent_id);
if(instructor_resource_parent_id) {
} else {
instructor_resource_parent_id = instructor_resource.parentNode.parentNode.id;
}
this_type = instructor_resource.getAttribute("data-resource");
this_icon = icon[this_type];
var id_of_this_icon = "resourceid" + j;
icons_on_this_page.push([type_name[this_type], this_icon, id_of_this_icon]);
console.log("this_type", this_type, "this_icon", this_icon, "html_words_of[this_type]", html_words_of[this_type]);
this_item_with_resource = document.getElementById(instructor_resource_parent_id);
console.log("instructor_resource_parent_id", instructor_resource_parent_id, "this_item_with_resource", this_item_with_resource);
var this_margin_resource = document.createElement('div');
this_margin_resource.setAttribute('class', 'marginresource');
this_margin_resource.setAttribute('id', id_of_this_icon);
this_margin_resource.innerHTML = "<span class='icon " + this_type + "'>" + this_icon + "</span>";
if(!(this_title = instructor_resource.getAttribute("title"))) {
this_title = html_words_of[this_type]
}
if(instructor_resource.hasAttribute("data-content")) {
this_content = instructor_resource.getAttribute("data-content");
this_title = '<a href="" data-knowl="' + this_content + '">' + this_title + '</a>';
}
var links_html = ""
if(instructor_resource.hasAttribute("data-links")) {
var this_links = instructor_resource.getAttribute("data-links");
these_links = this_links.split(";");
links_html = '<span class="resource_links">'
console.log(" OOOOOOOOOOOOOO these_links", these_links, "these_links.length", these_links.length);
if(these_links.length > 0) {
console.log("these_links[0]", these_links[0]);
}
tmpJ = these_links.length;
console.log("tmpJ", tmpJ);
for(var jj=0; jj < these_links.length; ++jj) {
console.log( " UUUUUUUUUUUUUu these_links[jj]", these_links[jj]);
this_type_and_link = these_links[jj].split(",");
if(jj>0) { links_html += ", " }
links_html += '<a href="' + this_type_and_link[1] + '">' + this_type_and_link[0] + '</a>';
}
links_html += 'XX</span>';
console.log("done adding links to title");
}
this_title = '<span class="resource_description">' + this_title
if(links_html) {
this_title += links_html
}
this_title += '</span>';
this_margin_resource.innerHTML += this_title;
// this_item_with_resource.insertBefore(this_margin_resource);
this_item_with_resource.insertBefore(this_margin_resource, this_item_with_resource.firstChild);
console.log("appended to ",this_item_with_resource);
}
icons_on_this_page.sort();
var icon_legend_for_this_page = document.createElement('div');
var prev_icon = "";
var icon_list = '<span class="icongroup">' + "<span class='icon_name'>" + icons_on_this_page[0][0] + ":</span> ";
for (var j=0; j < icons_on_this_page.length; ++j) {
next_icon = icons_on_this_page[j][1];
if(j > 0 && next_icon !== prev_icon) {
icon_list += '</span>' + "<br>";
icon_list += '<span class="icongroup">' + "<span class='icon_name'>" + icons_on_this_page[j][0] + ":</span> ";
icon_list += '<a href="#' + icons_on_this_page[j][2] + '">' + next_icon + '</a>';
} else {
icon_list += '<a href="#' + icons_on_this_page[j][2] + '">' + next_icon + '</a>';
}
prev_icon = next_icon;
}
icon_list += '</span>';
icon_legend_for_this_page.setAttribute('class', 'iconlegend');
icon_legend_for_this_page.innerHTML = icon_list
document.body.appendChild(icon_legend_for_this_page);
} // if logged in as instructor
+195
View File
@@ -0,0 +1,195 @@
(function(b, u, v) {
function x(b, f, e) {
b = (b + "").match(/^(-?[0-9]+)(%)?$/);
if (!b) return !1;
var c = parseInt(b[1], 10);
b[2] && (c *= f / 100);
return 0 > c ? f + c + (e || 0) : c
}
function y(k, f) {
function e() {
function b() {
g = +new Date;
f.apply(e, t);
c && (c = clearTimeout(c))
}
var e = this,
q = +new Date - g,
t = arguments;
c && (c = clearTimeout(c));
q > k ? b() : c = setTimeout(b, k - q)
}
var c, g = 0;
b.guid && (e.guid = f.guid = f.guid || b.guid++);
return e
}
b.Espy = function(k, f, e) {
function c(a, d) {
b.isPlainObject(a) && (d = a, a = null);
b.extend(t.prototype, d);
null !== a && (w = a)
}
function g(a) {
if (a =
q(a)) {
var d = a.$el.offset()[a.settings.horizontal ? "left" : "top"] - p.offset[a.settings.horizontal ? "left" : "top"],
h = a.$el[a.settings.horizontal ? "outerWidth" : "outerHeight"]();
b.extend(a, {
start: d,
elSize: h,
end: d + h
})
}
}
function r(a) {
// console.log("r(a) with a = ", a);
if (a === v) b.each(m, r);
else if (a = q(a)) {
var d = p[a.settings.horizontal ? "width" : "height"],
h = x(a.settings.size, d),
d = p[a.settings.horizontal ? "left" : "top"] + x(a.settings.offset, d, -h),
c = d + h,
h = a.settings.contain ? d <= a.start && c >= a.end ? "inside" : d + h / 2 > a.start + a.elSize / 2 ? a.settings.horizontal ? "left" :
"up" : a.settings.horizontal ? "right" : "down" : d > a.start && d < a.end || c > a.start && c < a.end || d <= a.start && c >= a.start || d <= a.end && c >= a.end ? "inside" : d > a.end ? a.settings.horizontal ? "left" : "up" : a.settings.horizontal ? "right" : "down";
a.state !== h && (a.state = h, "function" === typeof w && w.call(a.el, "inside" === h, h), "function" === typeof a.callback && a.callback.call(a.el, "inside" === h, h))
}
}
function s(a) {
if (m.hasOwnProperty(a)) return a;
if (b.isPlainObject(a) && m.hasOwnProperty(a.id)) return a.id;
a = b(a)[0];
var d = !1;
b.each(m, function(b,
c) {
c.el === a && (d = b)
});
return d
}
// console.log("nothing yet");
function q(a) {
return (a = s(a)) ? m[a] : !1
}
"function" !== typeof f && (e = f, f = 0);
// console.log("k was", k);
var t = function(a) {
b.extend(this, a)
},
u = function(a, d, c, e) {
this.id = a;
this.el = d;
this.$el = b(d);
this.callback = c;
this.settings = new t(e);
this.configure = function(a, d) {
b.isPlainObject(a) && (d = a, a = null);
b.extend(this.settings, d);
null !== a && (this.callback = a)
}
},
n = this,
l = b(k);
// console.log("l", l, "l==window", l[0]==window);
k = b.fn.espy.defaults;
// console.log("k is", k);
var w, m = {},
z = 0;
// because window.offset() is not defined in jQuery 3 (why?!?!?!),
// we have to treat that as a special case (DF 1/28/19)
if (l[0]==window) { offSET = { top: 0, left: 0 } }
else { offSET = l.offset() }
var p = {
top: l.scrollTop(),
left: l.scrollLeft(),
width: l.innerWidth(),
height: l.innerHeight(),
offset: offSET
/*
offset: l.offset() || {
top: 0,
left: 0
}
*/
};
// console.log("p", p);
c(f, b.extend({}, k, e));
n.add = function(a, d, c) {
b.isPlainObject(d) && (c = d, d = 0);
b(a).each(function(a, b) {
var e = s(b) || "s" + z++;
m[e] = new u(e, b, d, c);
g(e);
r(e)
})
};
n.configure = function(a, d, e) {
"function" === typeof a ? (d = a, a = null, b.isPlainObject(d) && (e = d, d = null)) : b.isPlainObject(a) ? (e = a, d = a = null) : b.isPlainObject(d) && (e = d, d = null);
null === a ? (c(d, e), b.each(m, function(a, b) {
g(b)
})) : b(a).each(function(a, b) {
var c = q(b);
c && (c.configure(d, e), g(b))
})
};
n.reload = function(a) {
a === v ? b.each(m, function() {
g(this.id)
}) :
b(a).each(function(a, b) {
var c = s(b);
c && (g(c), r(c))
})
};
n.remove = function(a) {
b(a).each(function(a, b) {
var c = s(b);
c && delete m[c]
})
};
n.destroy = function() {
l.off(".espy");
m = {};
n = v
};
n.resize = function() {
b.each(m, function() {
this.reloadOnResize && g(this)
});
p.width = l.innerWidth();
p.height = l.innerHeight();
r()
};
l.on("scroll.espy", y(k.delay, function() {
p.top = l.scrollTop();
p.left = l.scrollLeft();
r()
}));
l.on("resize.espy", y(k.delay, function() {
n.resize()
}))
};
b.fn.espy = function(k, f) {
var e, c;
e = f && f.context || u;
var g = b.data(e,
"espy") || b.data(e, "espy", new b.Espy(e));
"string" !== typeof k ? g.add(this, k, f) : (e = k, c = Array.prototype.slice.call(arguments), c[0] = this, "function" === typeof g[e] && g[e].apply(g, c));
return this
};
b.fn.espy.defaults = {
delay: 100,
context: window,
horizontal: 0,
offset: 0,
size: "100%",
contain: 0,
reloadOnResize: !0
}
})(jQuery, window);
File diff suppressed because one or more lines are too long
+161
View File
@@ -0,0 +1,161 @@
// Sticky Plugin v1.0.0 for jQuery
// =============
// Author: Anthony Garand
// Improvements by German M. Bravo (Kronuz) and Ruud Kamphuis (ruudk)
// Improvements by Leonardo C. Daronco (daronco)
// Created: 2/14/2011
// Date: 2/12/2012
// Website: http://labs.anthonygarand.com/sticky
// Description: Makes an element on the page stick on the screen as you scroll
// It will only set the 'top' and 'position' of your element, you
// might need to adjust the width in some cases.
(function($) {
var defaults = {
topSpacing: 0,
bottomSpacing: 0,
className: 'is-sticky',
wrapperClassName: 'sticky-wrapper',
center: false,
getWidthFrom: ''
},
$window = $(window),
$document = $(document),
sticked = [],
windowHeight = $window.height(),
scroller = function() {
var scrollTop = $window.scrollTop(),
documentHeight = $document.height(),
dwh = documentHeight - windowHeight,
extra = (scrollTop > dwh) ? dwh - scrollTop : 0;
for (var i = 0; i < sticked.length; i++) {
var s = sticked[i],
elementTop = s.stickyWrapper.offset().top,
etse = elementTop - s.topSpacing - extra;
if (scrollTop <= etse) {
if (s.currentTop !== null) {
s.stickyElement
.css('position', '')
.css('top', '');
s.stickyElement.parent().removeClass(s.className);
s.currentTop = null;
}
}
else {
var newTop = documentHeight - s.stickyElement.outerHeight()
- s.topSpacing - s.bottomSpacing - scrollTop - extra;
if (newTop < 0) {
newTop = newTop + s.topSpacing;
} else {
newTop = s.topSpacing;
}
if (s.currentTop != newTop) {
s.stickyElement
.css('position', 'fixed')
.css('top', newTop);
if (typeof s.getWidthFrom !== 'undefined') {
s.stickyElement.css('width', $(s.getWidthFrom).width());
}
s.stickyElement.parent().addClass(s.className);
s.currentTop = newTop;
}
}
}
},
resizer = function() {
windowHeight = $window.height();
},
methods = {
init: function(options) {
var o = $.extend({}, defaults, options);
return this.each(function() {
var stickyElement = $(this);
var stickyId = stickyElement.attr('id');
var wrapperId = stickyId ? stickyId + '-' + defaults.wrapperClassName : defaults.wrapperClassName
var wrapper = $('<div></div>')
.attr('id', stickyId + '-sticky-wrapper')
.addClass(o.wrapperClassName);
stickyElement.wrapAll(wrapper);
if (o.center) {
stickyElement.parent().css({width:stickyElement.outerWidth(),marginLeft:"auto",marginRight:"auto"});
}
if (stickyElement.css("float") == "right") {
stickyElement.css({"float":"none"}).parent().css({"float":"right"});
}
var stickyWrapper = stickyElement.parent();
stickyWrapper.css('height', stickyElement.outerHeight());
sticked.push({
topSpacing: o.topSpacing,
bottomSpacing: o.bottomSpacing,
stickyElement: stickyElement,
currentTop: null,
stickyWrapper: stickyWrapper,
className: o.className,
getWidthFrom: o.getWidthFrom
});
});
},
update: scroller,
unstick: function(options) {
return this.each(function() {
var unstickyElement = $(this);
var removeIdx = -1;
for (var i = 0; i < sticked.length; i++)
{
if (sticked[i].stickyElement.get(0) == unstickyElement.get(0))
{
removeIdx = i;
}
}
if(removeIdx != -1)
{
sticked.splice(removeIdx,1);
unstickyElement.unwrap();
unstickyElement.removeAttr('style');
}
});
}
};
// should be more efficient than using $window.scroll(scroller) and $window.resize(resizer):
if (window.addEventListener) {
window.addEventListener('scroll', scroller, false);
window.addEventListener('resize', resizer, false);
} else if (window.attachEvent) {
window.attachEvent('onscroll', scroller);
window.attachEvent('onresize', resizer);
}
$.fn.sticky = function(method) {
if (methods[method]) {
return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
} else if (typeof method === 'object' || !method ) {
return methods.init.apply( this, arguments );
} else {
$.error('Method ' + method + ' does not exist on jQuery.sticky');
}
};
$.fn.unstick = function(method) {
if (methods[method]) {
return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
} else if (typeof method === 'object' || !method ) {
return methods.unstick.apply( this, arguments );
} else {
$.error('Method ' + method + ' does not exist on jQuery.sticky');
}
};
$(function() {
setTimeout(scroller, 0);
});
})(jQuery);
+338
View File
@@ -0,0 +1,338 @@
// Code controlling behavior of xref knowls and born hidden knowls
// Assumes this file is loaded as part of initial page
window.addEventListener("load", (event) => {
addKnowls(document);
});
function addKnowls(target) {
const xrefs = target.querySelectorAll("[data-knowl]");
for (const xref of xrefs) {
LinkKnowl.initializeXrefKnowl(xref);
}
const bornHiddens = target.querySelectorAll(".born-hidden-knowl");
for (const bhk of bornHiddens) {
const summary = bhk.querySelector(":scope > summary");
const contents = bhk.querySelector(":scope > summary + *");
new SlideRevealer(summary, contents, bhk);
}
}
// Used to animate both types of knowls
class SlideRevealer {
static STATE = Object.freeze({
INACTIVE: 0,
CLOSING: 1,
EXPANDING: 2
});
// triggerElement is the element clicked to open/close
// contentElement is the element that will hide/reveal
// animatedElement is the element that will grow/shrink as contentElement is modified
// may be the same as contentElement or a parent of it
constructor(triggerElement, contentElement, animatedElement) {
this.triggerElement = triggerElement;
this.contentElement = contentElement;
this.animatedElement = animatedElement;
// mid animation state tracking
this.animation = null;
this.animationState = SlideRevealer.STATE.INACTIVE;
this.triggerElement.addEventListener('click', (e) => this.onClick(e));
}
onClick(e) {
// Stop default behavior from the browser
if (e) e.preventDefault();
// Add an overflow on the <details> to avoid content overflowing
this.animatedElement.style.overflow = 'hidden';
// Check if the element is being closed or is already closed
if (this.animationState === SlideRevealer.STATE.CLOSING || !this.animatedElement.hasAttribute("open")) {
// Force the [open] attributes - allow for similar targetting of xref and born-hidden knowls
this.animatedElement.setAttribute("open","");
this.triggerElement.setAttribute("open","");
this.contentElement.style.display = '';
// Trigger the animation to expand or collapse the knowl.
// Delay the MathJax typesetting until the knowl is visible to ensure proper measurements
// are taken, but before the unrolling begins. This helps avoid layout shifts and ensures
// smooth animation with correctly sized content.
MathJax.typesetPromise().then(() => window.requestAnimationFrame(() => this.toggle(true)));
} else if (this.animationState === SlideRevealer.STATE.EXPANDING || this.animatedElement.hasAttribute("open")) {
this.toggle(false);
}
}
toggle(expanding) {
let closedHeight = 0;
if (this.animatedElement.contains(this.triggerElement))
closedHeight = this.triggerElement.offsetHeight;
const fullHeight = closedHeight + this.contentElement.offsetHeight;
const startHeight = `${expanding ? closedHeight : fullHeight}px`;
const endHeight = `${expanding ? fullHeight : closedHeight}px`;
// Need to animate padding to avoid extra height for xref knowls
const padding = this.animatedElement.offsetHeight - this.animatedElement.clientHeight;
const startPad = `${expanding ? 0 : padding}px`;
const endPad = `${expanding ? padding : 0}px`;
// Cancel any existing animation
if (this.animation) {
this.animation.cancel();
}
// Animate ~400 pixels per second with max of 0.75 second and min of 0.25
const animDuration = Math.max( Math.min( (Math.abs(closedHeight - fullHeight) / 400 * 1000), 750), 250);
// Start animation
this.animationState = expanding ? SlideRevealer.STATE.EXPANDING : SlideRevealer.STATE.CLOSING;
this.animation = this.animatedElement.animate({
height: [startHeight, endHeight],
paddingTop: [startPad, endPad],
paddingBottom: [startPad, endPad]
}, {
duration: animDuration,
easing: 'ease'
});
this.animation.onfinish = () => { this.onAnimationFinish(expanding); };
this.animation.oncancel = () => { this.animationState = SlideRevealer.STATE.INACTIVE; };
}
onAnimationFinish(isOpen) {
// Clear animation state
this.animation = null;
this.animationState = SlideRevealer.STATE.INACTIVE;
// Make sure animated element has open (needed for details)
if(!isOpen) {
this.animatedElement.removeAttribute("open");
this.triggerElement.removeAttribute("open");
}
// Clear styles used in animation
this.animatedElement.style.overflow = '';
if (!isOpen)
this.contentElement.style.display = 'none';
if (isOpen) {
let hasCallback = this.contentElement.querySelectorAll("[data-knowl-callback]");
hasCallback.forEach((el) => {
window[el.getAttribute("data-knowl-callback")](el, open);
});
}
}
}
// A LinkKnowl manages a single link based knowl
class LinkKnowl {
// Used to uniquely identify XrefKnowls
static xrefCount = 0;
// Factory to create an XrefKnowl from a knowl link
// Will avoid duplicate initialization
// This should be used by outside code to create XrefKnowls
static initializeXrefKnowl(knowlLinkElement) {
if (knowlLinkElement.getAttribute("data-knowl-uid") === null) {
return new LinkKnowl(knowlLinkElement);
}
}
// "Private" constructor - should only be called by initializeXrefKnowl
constructor(knowlLinkElement) {
this.linkElement = knowlLinkElement;
this.outputElement = null;
this.uid = LinkKnowl.xrefCount++;
knowlLinkElement.setAttribute("data-knowl-uid", this.uid);
// Xref's behavior is that of a button
knowlLinkElement.setAttribute("role", "button");
// Stash a copy of the original title for use in aria-label
// If no title, use textContent
knowlLinkElement.setAttribute("data-base-title", knowlLinkElement.getAttribute("title") || this.linkElement.textContent);
knowlLinkElement.classList.add("knowl__link");
this.updateLabels(false);
// Bind required to force "this" of event handler to be this object
knowlLinkElement.addEventListener("click", this.handleLinkClick.bind(this));
}
// Set aria-label and title based on visibility of knowl
updateLabels(isVisible) {
const verb = isVisible
? this.linkElement.getAttribute("data-close-label") || "Close"
: this.linkElement.getAttribute("data-reveal-label") || "Reveal";
const targetDescript = this.linkElement.getAttribute("data-base-title");
const helpText = verb + " " + targetDescript;
this.linkElement.setAttribute("aria-label", helpText);
this.linkElement.setAttribute("title", helpText);
}
// Toggle the state of the knowl link and output elements
// Assumes output is already created
toggle() {
this.linkElement.classList.toggle("active");
const isActive = this.linkElement.classList.contains("active");
this.updateLabels(isActive);
// Scroll to reveal if needed
if (isActive) {
const h = this.outputElement.getBoundingClientRect().height;
if (h > window.innerHeight) {
// knowl is taller than window, scroll to top of knowl
this.outputElement.scrollIntoView(true);
} else {
// reveal full knowl
if (this.outputElement.getBoundingClientRect().bottom > window.innerHeight)
this.outputElement.scrollIntoView(false);
}
}
}
// Returns element the knowl output should be inserted after
findOutputLocation() {
const invalidParents = "table, mjx-container, div.tabular-box, .runestone > .parsons";
// Start with the link's parent, move up as long as there are invalid parents
let el = this.linkElement.parentElement;
let problemAncestor = el.closest(invalidParents);
while (problemAncestor && problemAncestor !== el) {
el = problemAncestor;
problemAncestor = el.closest(invalidParents);
}
return el;
}
// Create the knowl output element
createOutputElement() {
const outputId = "knowl-uid-" + this.uid;
const outputContentsId = "knowl-output-" + this.uid;
const linkTarget = this.linkElement.getAttribute("data-knowl");
const placeholderText = `<div class='knowl__content' style='display:none;' id='${outputId}' aria-live='polite' id='${outputContentsId}'>`
+ `Loading '${linkTarget}'`
+ `</div>`;
const temp = document.createElement("template");
temp.innerHTML = placeholderText;
this.outputElement = temp.content.children[0];
const insertLoc = this.findOutputLocation(this.linkElement);
insertLoc.after(this.outputElement);
}
// Get content for knowl as dom element. Returns promise that resolves to knowl content
async getContent() {
const contentURL = this.linkElement.getAttribute("data-knowl");
const knowlContent = await fetch(contentURL)
.then((response) => response.text())
.then((data) => {
// knowls are full HTML pages, need to just extract body
let knowlDoc = (new DOMParser()).parseFromString(data, "text/html");
let tempContainer = knowlDoc.body;
// grab any scripts from head of knowl doc and add them to the output
let scripts = knowlDoc.querySelectorAll("head script");
tempContainer.append(...scripts);
return tempContainer;
})
.catch((error) => {
const destination = this.linkElement.getAttribute("href");
const text = this.linkElement.textContent;
const err_message = `<div class='knowl-output__error'>`
+ `<div class='para'>Error fetching content. (<em>${error}</em>)</div>`
+ `<div class='para'><a href='${destination}'>Navigate to ${text}</a> instead.</div>`
+ `<div class='para'>If you are viewing this book from your local filesystem, this is expected behavior. To view the book with all features, you must serve the book from a web server. See the <a href="https://pretextbook.org/doc/guide/html/author-faq.html#how-do-i-view-my-book-locally">PreTeXt FAQ</a> for more information.</div>`
+ `</div>`;
return err_message;
});
return knowlContent;
}
// Handle a click on the knowl link
handleLinkClick(event) {
// prevent navigation
event.preventDefault();
if (this.outputElement !== null) {
// output already created, toggle visibility
this.toggle();
} else {
this.createOutputElement();
const slideHandler = new SlideRevealer(this.linkElement, this.outputElement, this.outputElement);
//slideHandler is now responsible for handling clicks to this element
this.linkElement.addEventListener('click', slideHandler);
// Wait up to a half second in hopes of avoiding double content change
// then render to show loading message
let loadingTimeout = setTimeout(() => {
loadingTimeout = null;
slideHandler.onClick(); //fake initial click
this.toggle();
}, 500);
const content = this.getContent();
// Content is a promise at this point, insert when resolved
content
.then((tempContainer) => {
// if timeout still active, cancel it and render
if (loadingTimeout !== null) {
clearTimeout(loadingTimeout);
}
// Now give code that follows .1 seconds to render before making visible
setTimeout(() => {
slideHandler.onClick(); //fake initial click
this.toggle();
}, 100);
// check embedded runestone interactives by loading content into a temp container
// we want to not render any that already are on page. Dupe IDs probably bad
const runestoneElements = tempContainer.querySelectorAll(".ptx-runestone-container");
[...runestoneElements].forEach((e) => {
const rsId = e.querySelector("[data-component]")?.id;
const onPage = document.getElementById(rsId);
if (onPage) {
e.innerHTML = `<div class="para">The interactive that belongs here is already on the page and cannot appear multiple times. <a href="#${rsId}">Scroll to interactive.</a>`;
} else {
// let runestone start rendering it
window.runestoneComponents.renderOneComponent(e);
}
});
// now move all contents to the real output element
const children = [...tempContainer.children];
this.outputElement.innerHTML = "";
this.outputElement.append(...children);
// render any knowls and mathjax in the knowl
addKnowls(this.outputElement);
// try prism highlighting
Prism.highlightAllUnder(this.outputElement);
// force any scripts (e.g. sagecell) to execute by evaling them
[...this.outputElement.getElementsByTagName("script")].forEach((s) => {
if (
s.getAttribute("type") === null ||
s.getAttribute("type") === "text/javascript"
) {
eval(s.innerHTML);
}
});
})
.catch((data) => {
console.log("Error fetching knowl content: " + data);
});
}
}
}
+109
View File
@@ -0,0 +1,109 @@
/****************************************************
*
* mathjaxknowl.js
*
* Implements \knowl{url}{math} macro for MathJax. Knowls are
* described at
*
* http://www.aimath.org/knowlepedia/
*
* Be sure to change the loadComplete() address to the URL
* of the location of this file on your server.
*
* You can load it via the config=file parameter on the script
* tag that loads MathJax.js, or by including it in the extensions
* array in your configuration.
*
* Based on an approach developed by Tom Leathrum. See
*
* http://groups.google.com/group/mathjax-users/browse_thread/thread/d8a8d081b8e63242
*
* for details.
*/
MathJax.Extension.Knowl = {
version: "1.0",
//
// Reveal or hide a MathJax knowl
//
Show: function (url,id) {
var oid = "MathJax-knowl-output-"+id,
uid = "MathJax-kuid-"+id;
if ($("#"+oid).length) {
$("#"+uid).slideToggle("fast");
} else {
var the_content = "<div class='knowl-output' id='"+uid+"'>" +
"<div class='knowl'>" +
"<div class='knowl-content' id='"+oid+"'>" +
"loading '"+url+"'" +
"</div>" +
"</div>" +
"</div>";
var the_parent = $("#MathJax-knowl-"+id).closest("p, article, .displaymath");
if(the_parent.length == 0) {
the_parent = $("#MathJax-knowl-"+id).closest(".MJXc-display").next();
if(the_parent.length == 0) {
the_parent = $("#MathJax-knowl-"+id).parent().parent().parent().parent();
}
}
the_parent.after(the_content);
$("#"+oid).load(url,function () {
MathJax.Hub.Queue(["Typeset",MathJax.Hub,uid]);
});
$("#"+uid).slideDown("slow");
}
},
//
// Get a unique ID for the knowl
//
id: 0,
GetID: function () {return this.id++}
};
MathJax.Callback.Queue(
MathJax.Hub.Register.StartupHook("TeX Jax Ready",function () {
var TEX = MathJax.InputJax.TeX,
TEXDEF = TEX.Definitions,
KNOWL = MathJax.Extension.Knowl;
TEXDEF.macros.knowl = "Knowl";
TEX.Parse.Augment({
//
// Implements \knowl{url}{math}
//
Knowl: function (name) {
var url = this.GetArgument(name), math = this.ParseArg(name);
if (math.inferred && math.data.length == 1)
{math = math.data[0]} else {delete math.inferred}
var id = KNOWL.GetID();
this.Push(math.With({
"class": "MathJax_knowl",
href: "javascript:MathJax.Extension.Knowl.Show('"+url+"','"+id+"')",
id: "MathJax-knowl-"+id,
// border and padding will only work properly if given explicitly on the element
style: "color:blue; border-bottom: 1px dotted #00A; padding-bottom: 1px"
}));
}
});
}),
MathJax.Hub.Register.StartupHook("onLoad",function () {
MathJax.Ajax.Styles({
".MathJax_knowl:hover": {"background-color": "#DDF"}
});
}),
["Post",MathJax.Hub.Startup.signal,"TeX knowl ready"]
);
MathJax.Ajax.loadComplete("http://pretextbook.org/js/lib/mathjaxknowl.js");
MathJax.Ajax.loadComplete("https://pretextbook.org/js/lib/mathjaxknowl.js");
+48
View File
@@ -0,0 +1,48 @@
const { Configuration } = MathJax._.input.tex.Configuration;
const { CommandMap } = MathJax._.input.tex.SymbolMap;
const NodeUtil = MathJax._.input.tex.NodeUtil.default;
var GetArgumentMML = function (parser, name) {
var arg = parser.ParseArg(name);
if (!NodeUtil.isInferred(arg)) {
return arg;
}
var children = NodeUtil.getChildren(arg);
if (children.length === 1) {
return children[0];
}
var mrow = parser.create("node", "mrow");
NodeUtil.copyChildren(arg, mrow);
NodeUtil.copyAttributes(arg, mrow);
return mrow;
};
let mathjaxKnowl = {};
/**
* Implements \knowl{url}{math}
* @param {TexParser} parser The calling parser.
* @param {string} name The TeX string
*/
mathjaxKnowl.Knowl = function (parser, name) {
var url = parser.GetArgument(name);
var arg = GetArgumentMML(parser, name);
var mrow = parser.create("node", "mrow", [arg], {
tabindex: '0',
"data-knowl": url
});
parser.Push(mrow);
};
new CommandMap(
"knowl",
{
knowl: ["Knowl"]
},
mathjaxKnowl
);
const configuration = Configuration.create("knowl", {
handler: {
macro: ["knowl"]
}
});
+322
View File
@@ -0,0 +1,322 @@
console.log("enabling login");
function createCookie(name,value,days) {
if (days) {
var date = new Date();
date.setTime(date.getTime()+(days*24*60*60*1000));
var expires = "; expires="+date.toGMTString();
}
else var expires = "";
/* document.cookie = name+"="+value+expires+"; path=/; domain=aimath.org";
*/
document.cookie = name + "=" + value + expires + "; path=/; " + "SameSite=None; Secure";
console.log("created cookie " + name);
}
function readCookie(name) {
var nameEQ = name + "=";
var ca = document.cookie.split(';');
for(var i=0;i < ca.length;i++) {
var c = ca[i];
while (c.charAt(0)==' ') c = c.substring(1,c.length);
if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length);
}
return null;
}
function eraseCookie(name) {
createCookie(name,"",-1);
}
/* next two also in edit.js. Need to clean up */
Storage.prototype.setObject = function(key, value) {
// this.setItem(key, JSON.stringify(value));
this.setItem(key, JSON.stringify(value, function(key, val) {
// console.log("key", key, "value", value, "val", val);
return val.toFixed ? Number(val.toFixed(3)) : val;
}));
}
Storage.prototype.getObject = function(key) {
var value = this.getItem(key);
return value && JSON.parse(value);
}
function hash_of_string(str) {
str = str.toString();
var the_len = str.length;
the_len = Math.min(12,the_len);
var the_hash = 123456;
var hash_lis = [510149, 120151, 230157, 411063, 320167, 631973, 410179, 321081, 231091, 111093, 121097, 230199];
for (var j=0; j < the_len; ++ j) {
var this_char = str.charCodeAt(j);
this_char = parseInt(this_char);
var this_add = this_char * hash_lis[j];
the_hash += this_add;
the_hash = the_hash % 1000000;
}
return the_hash.toString()
}
function login_form(mode="login") {
var the_form = "";
/* the_form += '<div id="theloginform" class="modal login">';
*/
if (mode == 'logout') {
the_form += '<form name="logoutform" class="modal-content animate" onSubmit="return removeLogin();" action="">';
the_form += '<div class="container">\n';
the_form += '<button type="submit">Yes, really logout</button>';
the_form += '<div id="dontlogout" class=dontlogout">Stay logged in</div>'
the_form += '</div>\n';
the_form += '</form>\n';
}
else{
the_form += '<form name="loginform" class="modal-content animate" onSubmit="return validateLogin();" action="">';
if ((typeof guest_access !== 'undefined') && guest_access) {
the_form += '<p class="instructions" >You can log in as "guest" with password "guest" for 3 hours of access.</p>'; }
the_form += '<div class="container">\n';
the_form += '<label><b>Id</b></label>\n<input type="text" placeholder="Enter Username" name="uname" required>';
the_form += '<label><b>Password</b></label>\n<input type="password" placeholder="Enter Password" name="psw" required>';
the_form += '<button type="submit">Login</button>';
the_form += '</div>\n';
the_form += '<div class="container" style="background-color:#f1f1f1">';
the_form += '<span class="psw">Login trouble? Email <a href="mailto:help@aimath.org">help@aimath.org</a></span>';
the_form += '</div>\n';
the_form += '</form>\n';
}
theform = document.createElement('div');
theform.id = "the" + mode + "form";
theform.className = "modal login";
document.body.appendChild(theform);
theform.innerHTML = the_form;
}
function survey_form(surveylink) {
var the_form = "";
/* the_form += '<div id="theloginform" class="modal login">';
*/
the_form += '<div class="modal-content">';
the_form += '<p class="instructions">Can you take a brief survey<br>about your use of this book?</p>';
the_form += '<button class="surveyresponse" id="takesurvey">Yes, I can take the survey<br>(opens new window)</button>';
the_form += '<button class="surveyresponse" id="remindlater">Not now. Please remind me later.</button>';
the_form += '</div>\n';
theform = document.createElement('div');
theform.id = "the" + "survey" + "form";
theform.className = "modal survey";
document.body.appendChild(theform);
theform.innerHTML = the_form;
}
function loadScript(script) {
if (typeof js_version === 'undefined') { js_version = '0.12' }
var newscript = document.createElement('script');
newscript.type = 'text/javascript';
newscript.async = true;
newscript.src = 'https://pretextbook.org/js/' + js_version + '/' + script + '.js';
var allscripts = document.getElementsByTagName('script');
var s = allscripts[allscripts.length - 1];
console.log('s',s);
console.log("adding a script", newscript);
s.parentNode.insertBefore(newscript, s.nextSibling);
}
function removeLogin() {
eraseCookie("ut_cookie");
}
var uname = "";
var emanu = "";
function check_role() {
console.log("check_role for", uname, "uname");
if(uname=="instructor") { return "instructor" }
var_role_data = {"action": "check", "user": uname, "pw": emanu, "type": "instructor", "instId": uname}
var role_key = "";
$.ajax({
url: "https://aimath.org/cgi-bin/u/highlights.py",
type: "post",
data: JSON.stringify(var_role_data),
dataType: "json",
async: false,
success: function(data) {
console.log("something", data, "back from highlight");
role_key = data
},
error: function(errMsg) {
console.log("seems to be an error?",errMsg);
// alert("Error X2\n" + errMsg);
}
});
console.log("done checking role", role_key);
return role_key
}
function validateLogin() {
var logged_in = false;
var un = document.loginform.uname.value;
// un = un.toLowerCase();
uname = un;
var pw = document.loginform.psw.value;
emanu = pw;
console.log("xuname", uname, "yemanu", emanu);
var guest_name = "guest";
var the_password_guest = "guest";
var editor_name = "editor";
var the_password_editor = "editor";
var instructor_name = "instructor";
var the_password_instructor = "instructor";
var the_un_enc = hash_of_string(un);
console.log("un", un, "the_un_enc", "y"+the_un_enc+"y", "pw", "x"+pw+"x", "pw == the_un_enc", pw == the_un_enc);
var the_url_enc = hash_of_string(window.location.hostname);
console.log('window.location.hostname ' + window.location.hostname);
if ((typeof guest_access !== 'undefined') && guest_access && ( ((un == guest_name) && (pw == the_password_guest)) || ((un == editor_name) && (pw == the_password_editor))) ) {
console.log("setting the guest ut_cookie");
createCookie('ut_cookie',un,0.25);
logged_in = true;
console.log("logged in as guest", logged_in);
}
else if ((typeof guest_access !== 'undefined') && guest_access && (un == instructor_name) && (pw == the_password_instructor)) {
console.log("setting the instructor ut_cookie");
createCookie('ut_cookie',un,0.25);
logged_in = true;
console.log("logged in as instructor", logged_in);
role = "instructor";
}
else if (pw == the_un_enc) {
console.log("setting the ut_cookie");
createCookie('ut_cookie',un,150);
logged_in = true;
}
else if (pw == the_url_enc) {
console.log("setting the url ut_cookie");
createCookie('ut_cookie',un,0.25);
logged_in = true;
}
else {
// alert ('Login was unsuccessful, please check id: "' + uname + '" and password"' + emanu +'". Also checking "' + pw + '" and "' + the_un_enc + '".');
alert ('Login was unsuccessful, please check id: "' + uname + '".');
console.log("failed to set the ut_cookie");
logged_in = false;
}
console.log("logged_in", logged_in);
if (logged_in) {
$("#theloginform").hide();
document.getElementById('loginlogout').innerHTML = 'logout' + ' ' + un;
if(uname != "editor") {
loadScript('answer');
loadScript('highlight')
if ((typeof trails !== 'undefined') && trails) {
loadScript('trails');
}
}
}
var role_key = check_role();
console.log("role_key", role_key);
if (role_key) {
console.log("another instructor", role_key);
role = 'instructor'
}
// if (logged_in && /^\d+$/.test(uname) && ( (uname.length == 5 || uname.length == 8 && /^\d+00$/.test(uname)) )) {
// console.log("an instructor");
// role = 'instructor'
// }
console.log("role", role);
return logged_in
}
var aa_id = readCookie('aa_cookie');
if (aa_id) {
console.log("found cookie");
console.log(aa_id);
var date = new Date();
var date_now = date.getTime();
var date_str = date_now.toString();
var date_str_trimmed = date_str.slice(5,13);
console.log(date_str)
console.log(date_str_trimmed)
}
else {
console.log("no cookie found")
var date = new Date();
var date_now = date.getTime();
var date_str = date_now.toString();
var date_str_trimmed = date_str.slice(5,13);
aa_id = date_str_trimmed;
createCookie('aa_cookie',aa_id,150);
}
/* dataurlbase = dataurlbase.concat("per").concat("=").concat(aa_id).concat("&");
*/
var ut_id = readCookie('ut_cookie');
uname = ut_id || "";
if(uname == "instructor") { role="instructor"}
console.log("uname", uname);
var pageIdentifier = "";
window.addEventListener('load', function(){
console.log("checking login", ut_id);
bodyID = document.getElementsByTagName('body')[0].id;
console.log("bodyID", bodyID);
if (bodyID) {
var secID = document.getElementsByTagName('section')[0].id;
if (secID) {
pageIdentifier = bodyID + "___" + secID
}
}
if (pageIdentifier) {
if (ut_id) {
console.log("found "+ut_id);
$("#theloginform").hide();
// if (/^\d+$/.test(uname) && ( (uname.length == 5 || uname.length == 8 && /^\d+00$/.test(uname)) )) {
var role_key = check_role();
console.log("the role_key", role_key);
if (role_key) {
console.log("another instructor", role_key);
role = 'instructor'
}
document.getElementById('loginlogout').className = 'logout';
document.getElementById('loginlogout').innerHTML = 'logout' + ' ' + ut_id;
console.log("done hiding "+ut_id);
loadScript('answer');
if(uname != "editor") {loadScript('highlight')}
if ((typeof trails !== 'undefined') && trails) {
loadScript('trails');
}
}
else if (typeof login_required !== 'undefined' && login_required) {
login_form();
}
$("#loginlogout.logout").click(function(){
login_form('logout');
$("#dontlogout").click(function() {
$("#thelogoutform").remove()
});
});
$("#loginlogout.login").click(function(){
login_form('login');
});
} else {
console.log("login not enabled because document not identified")
}
// console.log("the role", role);
});
+48
View File
@@ -0,0 +1,48 @@
// SPLICE resize handling - https://cssplice.org/
// Expected message format:
// {
// subject: lti.frameResize',
// message_id: (a unique string ID), // optional - not used
// height: ...,
// width: ...
// }
window.addEventListener('message', function (event) {
let edata = event.data;
//MoM sends event.data as a string instead of JSON
if (typeof event.data == 'string' && event.data.match(/lti\.frameResize/)) {
edata = JSON.parse(event.data);
}
if (edata.subject === "lti.frameResize") {
if ("frame_id" in edata) {
// MoM may send frame_id
let el = document.getElementById(edata['frame_id']);
document.getElementById(edata['frame_id']).style.height = edata.height + 'px';
if (edata.wrapheight && document.getElementById(edata['frame_id'] + 'wrap')) {
document.getElementById(edata['frame_id'] + 'wrap').style.height = edata.wrapheight + 'px';
}
} else if ("iframe_resize_id" in edata) {
// MoM may send iframe_resize_id
document.getElementById(edata['iframe_resize_id']).style.height = edata.height + 'px';
} else {
// No target element specified, so resize the iframe that sent the message
// event.source.frameElement is only accessible if the iframe is on the same domain
// so loop through iframes to find the one that sent the message
const iFrames = document.getElementsByTagName('iframe');
for(const iFrame of iFrames) {
if(iFrame.contentWindow === event.source) {
if (edata.height) iFrame.height = edata.height;
if (edata.width) iFrame.width = edata.width;
break;
}
}
}
}
});
// Currently only used by My Open Math to request a resize after knowls open
function sendResizeRequest(el) {
el.contentWindow.postMessage("requestResize", "*");
}
@@ -0,0 +1,515 @@
const timeOutHandler = new Object();
const inputPrefix = 'stackapi_input_';
const feedbackPrefix = 'stackapi_fb_';
const validationPrefix = 'stackapi_val_';
// const stack_api_url = // This is pulled from the publication file
const stackstring = {
"teacheranswershow_mcq":"A correct answer is: {$a->display}",
"api_which_typed":"which can be typed as follows",
"api_valid_all_parts":"Please enter valid answers for all parts of the question.",
"api_out_of":"out of",
"api_marks_sub":"Marks for this submission",
"api_submit":"Submit Answers",
"generalfeedback":"General feedback",
"score":"Score",
"api_response":"Response summary",
"api_correct":"Correct answers"
};
// Create data for call to API.
async function collectData(qfile, qname, qprefix) {
let res = "";
await getQuestionFile(qfile, qname).then((response)=>{
if (response.questionxml != "<quiz>\nnull\n</quiz>") {
res = {
questionDefinition: response.questionxml,
answers: collectAnswer(qprefix),
seed: response.seed,
renderInputs: qprefix + inputPrefix,
readOnly: false,
};
};
});
// }
return res;
}
// Get the different input elements by tag and return object with values.
function collectAnswer(qprefix) {
const inputs = document.getElementsByTagName('input');
const textareas = document.getElementsByTagName('textarea');
const selects = document.getElementsByTagName('select');
let res = {};
res = processNodes(res, inputs, qprefix);
res = processNodes(res, textareas, qprefix);
res = processNodes(res, selects, qprefix);
return res;
}
// Return object of values of valid entries in an HTMLCollection.
function processNodes(res, nodes, qprefix) {
for (let i = 0; i < nodes.length; i++) {
const element = nodes[i];
if (element.name.indexOf(qprefix+inputPrefix) === 0 && element.name.indexOf('_val') === -1) {
if (element.type === 'checkbox' || element.type === 'radio') {
if (element.checked) {
res[element.name.slice((qprefix+inputPrefix).length)] = element.value;
}
} else {
res[element.name.slice((qprefix+inputPrefix).length)] = element.value;
}
}
}
return res;
}
// Display rendered question and solution.
function send(qfile, qname, qprefix) {
const http = new XMLHttpRequest();
const url = stack_api_url + '/render';
http.open("POST", url, true);
http.setRequestHeader('Content-Type', 'application/json');
http.onreadystatechange = function() {
if(http.readyState == 4) {
try {
const json = JSON.parse(http.responseText);
if (json.message) {
console.log(json);
document.getElementById(`${qprefix+"errors"}`).innerText = json.message;
return;
} else {
document.getElementById(`${qprefix+"errors"}`).innerText = '';
}
renameIframeHolders();
let question = json.questionrender;
const inputs = json.questioninputs;
const seed = json.questionseed;
let correctAnswers = '';
// Show correct answers.
for (const [name, input] of Object.entries(inputs)) {
question = question.replace(`[[input:${name}]]`, input.render);
// question = question.replaceAll(`${inputPrefix}`,`${qprefix+inputPrefix}`);
question = question.replace(`[[validation:${name}]]`, `<span name='${qprefix+validationPrefix + name}'></span>`);
// This is a bit of a hack. The question render returns an <a href="..."> calling the download function with
// two arguments. We add the additional arguments that we need for context (question definition) here.
question = question.replace(/javascript:download\(([^,]+?),([^,]+?)\)/, `javascript:download($1,$2, '${qfile}', '${qname}', '${qprefix}', ${seed})`);
if (input.samplesolutionrender && name !== 'remember') {
// Display render of answer and matching user input to produce the answer.
correctAnswers += `<p>
${stackstring['teacheranswershow_mcq']} \\[{${input.samplesolutionrender}}\\],
${stackstring['api_which_typed']}: `;
for (const [name, solution] of Object.entries(input.samplesolution)) {
if (name.indexOf('_val') === -1) {
correctAnswers += `<span class='correct-answer'>${solution}</span>`;
}
}
correctAnswers += '.</p>';
} else if (name !== 'remember') {
// For dropdowns, radio buttons, etc, only the correct option is displayed.
for (const solution of Object.values(input.samplesolution)) {
if (input.configuration.options) {
correctAnswers += `<p class='correct-answer'>${input.configuration.options[solution]}</p>`;
}
}
}
}
// Convert Moodle plot filenames to API filenames.
for (const [name, file] of Object.entries(json.questionassets)) {
const plotUrl = getPlotUrl(file);
question = question.replace(name, plotUrl);
json.questionsamplesolutiontext = json.questionsamplesolutiontext.replace(name, plotUrl);
correctAnswers = correctAnswers.replace(name, plotUrl);
}
question = replaceFeedbackTags(question,qprefix);
qoutput = document.getElementById(`${qprefix+'output'}`);
qoutput.innerHTML = question;
// Only display results sections once question retrieved.
document.getElementById(`${qprefix+'stackapi_qtext'}`).style.display = 'block';
document.getElementById(`${qprefix+'stackapi_correct'}`).style.display = 'block';
// Setup a validation call on inputs. Timeout length is reset if the input is updated
// before the validation call is made.
for (const inputName of Object.keys(inputs)) {
const inputElements = document.querySelectorAll(`[name^=${qprefix+inputPrefix + inputName}]`);
for (const inputElement of Object.values(inputElements)) {
inputElement.oninput = (event) => {
const currentTimeout = timeOutHandler[event.target.id];
if (currentTimeout) {
window.clearTimeout(currentTimeout);
}
timeOutHandler[event.target.id] = window.setTimeout(validate.bind(null, event.target, qfile, qname, qprefix), 1000);
};
}
}
let sampleText = json.questionsamplesolutiontext;
if (sampleText) {
sampleText = replaceFeedbackTags(sampleText,qprefix);
document.getElementById(`${qprefix+'generalfeedback'}`).innerHTML = sampleText;
document.getElementById(`${qprefix+'stackapi_generalfeedback'}`).style.display = 'block';
} else {
// If the question is updated, there may no longer be general feedback.
document.getElementById(`${qprefix+'stackapi_generalfeedback'}`).style.display = 'none';
}
document.getElementById(`${qprefix+'stackapi_score'}`).style.display = 'none';
document.getElementById(`${qprefix+'stackapi_validity'}`).innerText = '';
const innerFeedback = document.getElementById(`${qprefix+'specificfeedback'}`);
innerFeedback.innerHTML = '';
innerFeedback.classList.remove('feedback');
document.getElementById(`${qprefix+'formatcorrectresponse'}`).innerHTML = correctAnswers;
// Hide General feedback and correct answers for now
document.getElementById(`${qprefix+'stackapi_generalfeedback'}`).style.display = 'none';
document.getElementById(`${qprefix+'stackapi_correct'}`).style.display = 'none';
createIframes(json.iframes);
MathJax.Hub.Queue(["Typeset", MathJax.Hub]);
}
catch(e) {
console.log(e);
document.getElementById(`${qprefix+'errors'}`).innerText = http.responseText;
return;
}
}
};
collectData(qfile, qname, qprefix).then((data)=>{
let submitbutton = document.getElementById(`${qprefix + 'stackapi_qtext'}`).querySelector('input[type="button"]');
submitbutton.addEventListener('click', function() {answer(qfile, qname, qprefix, data.seed)});
http.send(JSON.stringify(data));
let questioncontainer = document.getElementById(`${qprefix+'stack'}`).parentElement;
if (questioncontainer.getBoundingClientRect().top<0){
questioncontainer.scrollIntoView({ behavior: 'smooth', block: 'start' });
};
});
}
// Validate an input. Called a set amount of time after an input is last updated.
function validate(element, qfile, qname, qprefix) {
const http = new XMLHttpRequest();
const url = stack_api_url + '/validate';
http.open("POST", url, true);
// Remove API prefix and subanswer id.
const answerNamePrefixTrim = (qprefix+inputPrefix).length;
const answerName = element.name.slice(answerNamePrefixTrim).split('_', 1)[0];
http.setRequestHeader('Content-Type', 'application/json');
http.onreadystatechange = function() {
if(http.readyState == 4) {
try {
const json = JSON.parse(http.responseText);
if (json.message) {
document.getElementById(`${qprefix+'errors'}`).innerText = json.message;
return;
} else {
document.getElementById(`${qprefix+'errors'}`).innerText = '';
}
renameIframeHolders();
const validationHTML = json.validation;
const element = document.getElementsByName(`${qprefix+validationPrefix + answerName}`)[0];
element.innerHTML = validationHTML;
if (validationHTML) {
element.classList.add('validation');
} else {
element.classList.remove('validation');
}
createIframes(json.iframes);
MathJax.Hub.Queue(["Typeset", MathJax.Hub]);
}
catch(e) {
document.getElementById(`${qprefix+'errors'}`).innerText = http.responseText;
return;
}
}
};
collectData(qfile, qname, qprefix).then((data)=>{
data.inputName = answerName;
http.send(JSON.stringify(data));
});
}
// Submit answers.
function answer(qfile, qname, qprefix, seed) {
const http = new XMLHttpRequest();
const url = stack_api_url + '/grade';
http.open("POST", url, true);
if (!document.getElementById(`${qprefix+'output'}`).innerText) {
return;
}
http.setRequestHeader('Content-Type', 'application/json');
http.onreadystatechange = function() {
if(http.readyState == 4) {
try {
const json = JSON.parse(http.responseText);
if (json.message) {
document.getElementById(`${qprefix+'errors'}`).innerText = json.message;
return;
} else {
document.getElementById(`${qprefix+'errors'}`).innerText = '';
}
if (!json.isgradable) {
document.getElementById(`${qprefix+'stackapi_validity'}`).innerText
= ' ' + stackstring["api_valid_all_parts"];
return;
}
renameIframeHolders();
document.getElementById(`${qprefix+'score'}`).innerText
= (json.score * json.scoreweights.total).toFixed(2) +
' ' + stackstring["api_out_of"] + ' ' + json.scoreweights.total;
document.getElementById(`${qprefix+'stackapi_score'}`).style.display = 'block';
document.getElementById(`${qprefix+'response_summary'}`).innerText = json.responsesummary;
// Show General feedback and correct answers, hide summary
document.getElementById(`${qprefix+'stackapi_generalfeedback'}`).style.display = 'block';
document.getElementById(`${qprefix+'stackapi_summary'}`).style.display = 'none';
const feedback = json.prts;
const specificFeedbackElement = document.getElementById(`${qprefix+'specificfeedback'}`);
// Replace tags and plots in specific feedback and then display.
if (json.specificfeedback) {
for (const [name, file] of Object.entries(json.gradingassets)) {
json.specificfeedback = json.specificfeedback.replace(name, getPlotUrl(file));
}
json.specificfeedback = replaceFeedbackTags(json.specificfeedback,qprefix);
specificFeedbackElement.innerHTML = json.specificfeedback;
specificFeedbackElement.classList.add('feedback');
} else {
specificFeedbackElement.classList.remove('feedback');
}
// Replace plots in tagged feedback and then display.
for (let [name, fb] of Object.entries(feedback)) {
for (const [name, file] of Object.entries(json.gradingassets)) {
fb = fb.replace(name, getPlotUrl(file));
}
const elements = document.getElementsByName(`${qprefix+feedbackPrefix + name}`);
if (elements.length > 0) {
const element = elements[0];
if (json.scores[name] !== undefined) {
fb = fb + `<div>${stackstring['api_marks_sub']}:
${(json.scores[name] * json.scoreweights[name] * json.scoreweights.total).toFixed(2)}
/ ${(json.scoreweights[name] * json.scoreweights.total).toFixed(2)}.</div>`;
}
element.innerHTML = fb;
// if (fb) {
// element.classList.add('feedback');
// } else {
// element.classList.remove('feedback');
// }
}
}
createIframes(json.iframes);
MathJax.Hub.Queue(["Typeset", MathJax.Hub]);
}
catch(e) {
console.log(e);
document.getElementById(`${qprefix+'errors'}`).innerText = http.responseText;
return;
}
}
};
// Clear previous answers and score.
const specificFeedbackElement = document.getElementById(`${qprefix+'specificfeedback'}`);
specificFeedbackElement.innerHTML = "";
specificFeedbackElement.classList.remove('feedback');
document.getElementById(`${qprefix+'response_summary'}`).innerText = "";
document.getElementById(`${qprefix+'stackapi_summary'}`).style.display = 'none';
const inputElements = document.querySelectorAll(`[name^=${qprefix+feedbackPrefix}]`);
for (const inputElement of Object.values(inputElements)) {
inputElement.innerHTML = "";
inputElement.classList.remove('feedback');
}
document.getElementById(`${qprefix+'stackapi_score'}`).style.display = 'none';
document.getElementById(`${qprefix+'stackapi_validity'}`).innerText = '';
collectData(qfile, qname, qprefix).then((data) => {
data.seed = seed;
http.send(JSON.stringify(data));
});
}
function download(filename, fileid, qfile, qname, qprefix, seed) {
const http = new XMLHttpRequest();
const url = stack_api_url + '/download';
http.open("POST", url, true);
http.setRequestHeader('Content-Type', 'application/json');
// Something funky going on with closures and callbacks. This seems
// to be the easiest way to pass through the file details.
http.filename = filename;
http.fileid = fileid;
http.onreadystatechange = function() {
if(http.readyState == 4) {
try {
// Only download the file once. Replace call to download controller with link
// to downloaded file.
const blob = new Blob([http.responseText], {type: 'application/octet-binary', endings: 'native'});
// We're matching the three additional arguments that are added in the send function here.
const selector = CSS.escape(`javascript\:download\(\'${http.filename}\'\, ${http.fileid}\, \'${qfile}\'\, \'${qname}\'\, \'${qprefix}\'\, ${seed}\)`);
const linkElements = document.querySelectorAll(`a[href^=${selector}]`);
const link = linkElements[0];
link.setAttribute('href', URL.createObjectURL(blob));
link.setAttribute('download', filename);
link.click();
}
catch(e) {
document.getElementById('errors').innerText = http.responseText;
return;
}
}
};
collectData(qfile, qname, qprefix).then((data)=>{
data.filename = filename;
data.fileid = fileid;
data.seed = seed;
http.send(JSON.stringify(data));
});
}
// Save contents of question editor locally.
function saveState(key, value) {
if (typeof(Storage) !== "undefined") {
localStorage.setItem(key, value);
}
}
// Load locally stored question on page refresh.
function loadState(key) {
if (typeof(Storage) !== "undefined") {
return localStorage.getItem(key) || '';
}
return '';
}
function renameIframeHolders() {
// Each call to STACK restarts numbering of iframe holders so we need to rename
// any old ones to make sure new iframes end up in the correct place.
for (const iframe of document.querySelectorAll(`[id^=stack-iframe-holder]:not([id$=old]`)) {
iframe.id = iframe.id + '_old';
}
}
function createIframes (iframes) {
const corsFragment = "/cors.php?name=";
for (const iframe of iframes) {
create_iframe(
iframe[0],
iframe[1].replaceAll(corsFragment, stack_api_url + corsFragment),
...iframe.slice(2)
);
}
}
// Replace feedback tags in some text with an approproately named HTML div.
function replaceFeedbackTags(text, qprefix) {
let result = text;
const feedbackTags = text.match(/\[\[feedback:.*?\]\]/g);
if (feedbackTags) {
for (const tag of feedbackTags) {
// Part name is between '[[feedback:' and ']]'.
result = result.replace(tag, `<div name='${qprefix+feedbackPrefix + tag.slice(11, -2)}'></div>`);
}
}
return result;
}
async function getQuestionFile(questionURL, questionName) {
let res = "";
if (questionURL) {
await fetch(questionURL)
.then(result => result.text())
.then((result) => {
res = loadQuestionFromFile(result, questionName);
});
}
return res;
}
function loadQuestionFromFile(fileContents, questionName) {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(fileContents, "text/xml");
let thequestion = null;
let randSeed = "";
for (const question of xmlDoc.getElementsByTagName("question")) {
if (question.getAttribute('type').toLowerCase() === 'stack' && (!questionName || question.querySelectorAll("name text")[0].textContent === questionName)) {
thequestion = question.outerHTML;
let seeds = question.querySelectorAll('deployedseed');
if (seeds.length) {
randSeed = parseInt(seeds[Math.floor(Math.random()*seeds.length)].textContent);
}
break;
}
}
return {questionxml:setQuestion(thequestion),seed:randSeed};
}
function setQuestion(question) {
return '<quiz>\n' + question + '\n</quiz>';
}
function createQuestionBlocks() {
questionBlocks = document.getElementsByClassName("que stack");
let i=0;
for (questionblock of questionBlocks){
i++;
let questionPrefix = "q" + i.toString() + "_";
var qfile = questionblock.dataset.qfile;
var qname = questionblock.dataset.qname || "";
questionblock.innerHTML =
`
<div class="collapsiblecontent" id=${questionPrefix + "stack"}>
<div class="vstack gap-3 ms-3 col-lg-8">
<div id=${questionPrefix + "errors"}></div>
<div id=${questionPrefix + "stackapi_qtext"} class="col-lg-8" style="display: none">
<!--<h2>${stackstring['questiontext']}:</h2>-->
<div id=${questionPrefix + "output"} class="formulation"></div>
<div id=${questionPrefix + "specificfeedback"}></div>
<br>
<!-- <input type="button" onclick="answer('${qfile}', '${qname}', '${questionPrefix}')" class="btn btn-primary" value=${stackstring["api_submit"]}/>-->
<input type="button" class="btn btn-primary" value=${stackstring["api_submit"]}/>
<span id=${questionPrefix + "stackapi_validity"} style="color:darkred"></span>
</div>
<div id=${questionPrefix + "stackapi_generalfeedback"} class="col-lg-8" style="display: none">
<h2>${stackstring['generalfeedback']}:</h2>
<div id=${questionPrefix + "generalfeedback"} class="feedback"></div>
</div>
<h2 id=${questionPrefix + "stackapi_score"} style="display: none">${stackstring['score']}: <span id=${questionPrefix + "score"}></span></h2>
<div id=${questionPrefix + "stackapi_summary"} class="col-lg-10" style="display: none">
<h2>${stackstring['api_response']}:</h2>
<div id=${questionPrefix + "response_summary"} class="feedback"></div>
</div>
<div id=${questionPrefix + "stackapi_correct"} class="col-lg-10" style="display: none">
<h2>${stackstring['api_correct']}:</h2>
<div id=${questionPrefix + "formatcorrectresponse"} class="feedback"></div>
</div>
</div>
<div id=${questionPrefix + "newquestionbutton"}>
<input type="button" onclick="send('${qfile}', '${qname}', '${questionPrefix}')" class="btn btn-primary" value="Show new example question"/>
</div>
</div>
`;
}
}
function addCollapsibles(){
var collapsibles = document.querySelectorAll(".level2>h2, .stack>h2");
for (let i=0; i<collapsibles.length; i++) {
collapsibles[i].addEventListener("click", () => collapseFunc(this));
}
}
function collapseFunc(e){
e.classList.toggle("collapsed");
}
function stackSetup(){
createQuestionBlocks();
addCollapsibles();
}
function getPlotUrl(file) {
return `${stack_api_url}/plots/${file}`;
}
@@ -0,0 +1,652 @@
/**
* A javascript module to handle separation of author sourced scripts into
* IFRAMES. All such scripts will have limited access to the actual document
* on the VLE side and this script represents the VLE side endpoint for
* message handling needed to give that access. When porting STACK onto VLEs
* one needs to map this script to do the following:
*
* 1. Ensure that searches for target elements/inputs are limited to questions
* and do not return any elements outside them.
*
* 2. Map any identifiers needed to identify inputs by name.
*
* 3. Any change handling related to input value modifications through this
* logic gets connected to any such handling on the VLE side.
*
*
* This script is intenttionally ordered so that the VLE specific bits should
* be at the top.
*
*
* This script assumes the following:
*
* 1. Each relevant IFRAME has an `id`-attribute that will be told to this
* script.
*
* 2. Each such IFRAME exists within the question itself, so that one can
* traverse up the DOM tree from that IFRAME to find the border of
* the question.
*
* @module qtype_stack/stackjsvle
* @copyright 2023 Aalto University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
'use strict';
// Note the VLE specific include of logic.
/* All the IFRAMES have unique identifiers that they give in their
* messages. But we only work with those that have been created by
* our logic and are found from this map.
*/
let IFRAMES = {};
/* For event handling, lists of IFRAMES listening particular inputs.
*/
let INPUTS = {};
/* For event handling, lists of IFRAMES listening particular inputs
* and their input events. By default we only listen to changes.
* We report input events as changes to the other side.
*/
let INPUTS_INPUT_EVENT = {};
/* A flag to disable certain things. */
let DISABLE_CHANGES = false;
/**
* Returns an element with a given id, if an only if that element exists
* inside a portion of DOM that represents a question.
*
* If not found or exists outside the restricted area then returns `null`.
*
* @param {String} id the identifier of the element we want.
*/
function vle_get_element(id) {
/* In the case of Moodle we are happy as long as the element is inside
something with the `formulation`-class. */
let candidate = document.getElementById(id);
let iter = candidate;
while (iter && !iter.classList.contains('formulation')) {
iter = iter.parentElement;
}
if (iter && iter.classList.contains('formulation')) {
return candidate;
}
return null;
}
/**
* Returns an input element with a given name, if and only if that element
* exists inside a portion of DOM that represents a question.
*
* Note that, the input element may have a name that multiple questions
* use and to pick the preferred element one needs to pick the one
* within the same question as the IFRAME.
*
* Note that the input can also be a select. In the case of radio buttons
* returning one of the possible buttons is enough.
*
* If not found or exists outside the restricted area then returns `null`.
*
* @param {String} name the name of the input we want
* @param {String} srciframe the identifier of the iframe wanting it
*/
function vle_get_input_element(name, srciframe) {
/* In the case of Moodle we are happy as long as the element is inside
something with the `formulation`-class. */
let initialcandidate = document.getElementById(srciframe);
let iter = initialcandidate;
while (iter && !iter.classList.contains('formulation')) {
iter = iter.parentElement;
}
if (iter && iter.classList.contains('formulation')) {
// iter now represents the borders of the question containing
// this IFRAME.
let possible = iter.querySelector('input[id$="_' + name + '"]');
if (possible !== null) {
return possible;
}
// Radios have interesting ids, but the name makes sense
possible = iter.querySelector('input[id$="_' + name + '_1"][type=radio]');
if (possible !== null) {
return possible;
}
possible = iter.querySelector('select[id$="_' + name + '"]');
if (possible !== null) {
return possible;
}
}
// If none found within the question itself, search everywhere.
let possible = document.querySelector('.formulation input[id$="_' + name + '"]');
if (possible !== null) {
return possible;
}
// Radios have interesting ids, but the name makes sense
possible = document.querySelector('.formulation input[id$="_' + name + '_1"][type=radio]');
if (possible !== null) {
return possible;
}
possible = document.querySelector('.formulation select[id$="_' + name + '"]');
return possible;
}
/**
* Triggers any VLE specific scripting related to updates of the given
* input element.
*
* @param {HTMLElement} inputelement the input element that has changed
*/
function vle_update_input(inputelement) {
// Triggering a change event may be necessary.
const c = new Event('change');
inputelement.dispatchEvent(c);
// Also there are those that listen to input events.
const i = new Event('input');
inputelement.dispatchEvent(i);
}
/**
* Triggers any VLE specific scripting related to DOM updates.
*
* @param {HTMLElement} modifiedsubtreerootelement element under which changes may have happened.
*/
function vle_update_dom(modifiedsubtreerootelement) {
CustomEvents.notifyFilterContentUpdated(modifiedsubtreerootelement);
}
/**
* Does HTML-string cleaning, i.e., removes any script payload. Returns
* a DOM version of the given input string.
*
* This is used when receiving replacement content for a div.
*
* @param {String} src a raw string to sanitise
*/
function vle_html_sanitize(src) {
// This can be implemented with many libraries or by custom code
// however as this is typically a thing that a VLE might already have
// tools for we have it at this level so that the VLE can use its own
// tools that do things that the VLE developpers consider safe.
// As Moodle does not currently seem to have such a sanitizer in
// the core libraries, here is one implementation that shows what we
// are looking for.
// TO-DO: look into replacing this with DOMPurify or some such.
let parser = new DOMParser();
let doc = parser.parseFromString(src, "text/html");
// First remove all <script> tags. Also <style> as we do not want
// to include too much style.
for (let el of doc.querySelectorAll('script, style')) {
el.remove();
}
// Check all elements for attributes.
for (let el of doc.querySelectorAll('*')) {
for (let {name, value} of el.attributes) {
if (is_evil_attribute(name, value)) {
el.removeAttribute(name);
}
}
}
return doc.body;
}
/**
* Utility function trying to determine if a given attribute is evil
* when sanitizing HTML-fragments.
*
* @param {String} name the name of an attribute.
* @param {String} value the value of an attribute.
*/
function is_evil_attribute(name, value) {
const lcname = name.toLowerCase();
if (lcname.startsWith('on')) {
// We do not allow event listeners to be defined.
return true;
}
if (lcname === 'src' || lcname.endsWith('href')) {
// Do not allow certain things in the urls.
const lcvalue = value.replace(/\s+/g, '').toLowerCase();
// Ignore es-lint false positive.
/* eslint-disable no-script-url */
if (lcvalue.includes('javascript:') || lcvalue.includes('data:text')) {
return true;
}
}
return false;
}
/*************************************************************************
* Above this are the bits that one would probably tune when porting.
*
* Below is the actuall message handling and it should be left alone.
*/
window.addEventListener("message", (e) => {
// NOTE! We do not check the source or origin of the message in
// the normal way. All actions that can bypass our filters to trigger
// something are largely irrelevant and all traffic will be kept
// "safe" as anyone could be listening.
// All messages we receive are strings, anything else is for someone
// else and will be ignored.
if (!(typeof e.data === 'string' || e.data instanceof String)) {
return;
}
// That string is a JSON encoded dictionary.
let msg = null;
try {
msg = JSON.parse(e.data);
} catch (e) {
// Only JSON objects that are parseable will work.
return;
}
// All messages we handle contain a version field with a particular
// value, for now we leave the possibility open for that value to have
// an actual version number suffix...
if (!(('version' in msg) && msg.version.startsWith('STACK-JS'))) {
return;
}
// All messages we handle must have a source and a type,
// and that source must be one of the registered ones.
if (!(('src' in msg) && ('type' in msg) && (msg.src in IFRAMES))) {
return;
}
let element = null;
let input = null;
let response = {
version: 'STACK-JS:1.1.0'
};
switch (msg.type) {
case 'register-input-listener':
// 1. Find the input.
input = vle_get_input_element(msg.name, msg.src);
if (input === null) {
// Requested something that is not available.
response.type = 'error';
response.msg = 'Failed to connect to input: "' + msg.name + '"';
response.tgt = msg.src;
IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response), '*');
return;
}
response.type = 'initial-input';
response.name = msg.name;
response.tgt = msg.src;
// 2. What type of an input is this? Note that we do not
// currently support all types in sensible ways. In particular,
// anything with multiple values will be a problem.
if (input.nodeName.toLowerCase() === 'select') {
response.value = input.value;
response['input-type'] = 'select';
response['input-readonly'] = input.hasAttribute('disabled');
} else if (input.type === 'checkbox') {
response.value = input.checked;
response['input-type'] = 'checkbox';
response['input-readonly'] = input.hasAttribute('disabled');
} else {
response.value = input.value;
response['input-type'] = input.type;
response['input-readonly'] = input.hasAttribute('readonly');
}
if (input.type === 'radio') {
response['input-readonly'] = input.hasAttribute('disabled');
response.value = '';
for (let inp of document.querySelectorAll('input[type=radio][name=' + CSS.escape(input.name) + ']')) {
if (inp.checked) {
response.value = inp.value;
}
}
}
// 3. Add listener for changes of this input.
if (input.id in INPUTS) {
if (msg.src in INPUTS[input.id]) {
// DO NOT BIND TWICE!
return;
}
if (input.type !== 'radio') {
INPUTS[input.id].push(msg.src);
} else {
let radgroup = document.querySelectorAll('input[type=radio][name=' + CSS.escape(input.name) + ']');
for (let inp of radgroup) {
INPUTS[inp.id].push(msg.src);
}
}
} else {
if (input.type !== 'radio') {
INPUTS[input.id] = [msg.src];
} else {
let radgroup = document.querySelectorAll('input[type=radio][name=' + CSS.escape(input.name) + ']');
for (let inp of radgroup) {
INPUTS[inp.id] = [msg.src];
}
}
if (input.type !== 'radio') {
input.addEventListener('change', () => {
if (DISABLE_CHANGES) {
return;
}
let resp = {
version: 'STACK-JS:1.0.0',
type: 'changed-input',
name: msg.name
};
if (input.type === 'checkbox') {
resp['value'] = input.checked;
} else {
resp['value'] = input.value;
}
for (let tgt of INPUTS[input.id]) {
resp['tgt'] = tgt;
IFRAMES[tgt].contentWindow.postMessage(JSON.stringify(resp), '*');
}
});
} else {
// Assume that if we received a radio button that is safe
// then all its friends are also safe.
let radgroup = document.querySelectorAll('input[type=radio][name=' + CSS.escape(input.name) + ']');
radgroup.forEach((inp) => {
inp.addEventListener('change', () => {
if (DISABLE_CHANGES) {
return;
}
let resp = {
version: 'STACK-JS:1.0.0',
type: 'changed-input',
name: msg.name
};
if (inp.checked) {
resp.value = inp.value;
} else {
// What about unsetting?
return;
}
for (let tgt of INPUTS[inp.id]) {
resp['tgt'] = tgt;
IFRAMES[tgt].contentWindow.postMessage(JSON.stringify(resp), '*');
}
});
});
}
}
if (('track-input' in msg) && msg['track-input'] && input.type !== 'radio') {
if (input.id in INPUTS_INPUT_EVENT) {
if (msg.src in INPUTS_INPUT_EVENT[input.id]) {
// DO NOT BIND TWICE!
return;
}
INPUTS_INPUT_EVENT[input.id].push(msg.src);
} else {
INPUTS_INPUT_EVENT[input.id] = [msg.src];
input.addEventListener('input', () => {
if (DISABLE_CHANGES) {
return;
}
let resp = {
version: 'STACK-JS:1.0.0',
type: 'changed-input',
name: msg.name
};
if (input.type === 'checkbox') {
resp['value'] = input.checked;
} else {
resp['value'] = input.value;
}
for (let tgt of INPUTS_INPUT_EVENT[input.id]) {
resp['tgt'] = tgt;
IFRAMES[tgt].contentWindow.postMessage(JSON.stringify(resp), '*');
}
});
}
}
// 4. Let the requester know that we have bound things
// and let it know the initial value.
if (!(msg.src in INPUTS[input.id])) {
IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response), '*');
}
break;
case 'changed-input':
// 1. Find the input.
input = vle_get_input_element(msg.name, msg.src);
if (input === null) {
// Requested something that is not available.
const ret = {
version: 'STACK-JS:1.0.0',
type: 'error',
msg: 'Failed to modify input: "' + msg.name + '"',
tgt: msg.src
};
IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(ret), '*');
return;
}
// Disable change events.
DISABLE_CHANGES = true;
// TO-DO: Radio buttons should we check that value is possible?
if (input.type === 'checkbox') {
input.checked = msg.value;
} else {
input.value = msg.value;
}
// Trigger VLE side actions.
vle_update_input(input);
// Enable change tracking.
DISABLE_CHANGES = false;
// Tell all other frames, that care, about this.
response.type = 'changed-input';
response.name = msg.name;
response.value = msg.value;
for (let tgt of INPUTS[input.id]) {
if (tgt !== msg.src) {
response.tgt = tgt;
IFRAMES[tgt].contentWindow.postMessage(JSON.stringify(response), '*');
}
}
break;
case 'toggle-visibility':
// 1. Find the element.
element = vle_get_element(msg.target);
if (element === null) {
// Requested something that is not available.
const ret = {
version: 'STACK-JS:1.0.0',
type: 'error',
msg: 'Failed to find element: "' + msg.target + '"',
tgt: msg.src
};
IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(ret), '*');
return;
}
// 2. Toggle display setting.
if (msg.set === 'show') {
element.style.display = 'block';
// If we make something visible we should let the VLE know about it.
vle_update_dom(element);
} else if (msg.set === 'hide') {
element.style.display = 'none';
}
break;
case 'change-content':
// 1. Find the element.
element = vle_get_element(msg.target);
if (element === null) {
// Requested something that is not available.
response.type = 'error';
response.msg = 'Failed to find element: "' + msg.target + '"';
response.tgt = msg.src;
IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response), '*');
return;
}
// 2. Secure content.
// 3. Switch the content.
element.replaceChildren(vle_html_sanitize(msg.content));
// If we tune something we should let the VLE know about it.
vle_update_dom(element);
break;
case 'get-content':
// 1. Find the element.
element = vle_get_element(msg.target);
// 2. Build the message.
response.type = 'xfer-content';
response.tgt = msg.src;
response.target = msg.target;
response.content = null;
if (element !== null) {
// TO-DO: Should we sanitise the content? Probably not as using
// this to interrogate neighbouring questions only allows
// messing with the other questions and not anything outside
// them. If we do not sanitise it we allow some interesting
// question-analytics tooling, and if we do we really don't
// gain anything sensible.
// Matti's opinnion is to not sanitise at this point as
// interraction between questions is not inherently evil
// and could be of use even at the level of reading code from
// from other questions.
response.content = element.innerHTML;
}
IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response), '*');
break;
case 'resize-frame':
// 1. Find the frames wrapper div.
element = IFRAMES[msg.src].parentElement;
// 2. Set the wrapper size.
element.style.width = msg.width;
element.style.height = msg.height;
// 3. Reset the frame size.
IFRAMES[msg.src].style.width = '100%';
IFRAMES[msg.src].style.height = '100%';
// Only touching the size but still let the VLE know.
vle_update_dom(element);
break;
case 'ping':
// This is for testing the connection. The other end will
// send these untill it receives a reply.
// Part of the logic for startup.
response.type = 'ping';
response.tgt = msg.src;
IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response), '*');
return;
case 'initial-input':
case 'error':
// These message types are for the other end.
break;
default:
// If we see something unexpected, lets let the other end know
// and make sure that they know our version. Could be that this
// end has not been upgraded.
response.type = 'error';
response.msg = 'Unknown message-type: "' + msg.type + '"';
response.tgt = msg.src;
IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response), '*');
}
});
/* To avoid any logic that forbids IFRAMEs in the VLE output one can
also create and register that IFRAME through this logic. This
also ensures that all relevant security settigns for that IFRAME
have been correctly tuned.
Here the IDs are for the secrect identifier that may be present
inside the content of that IFRAME and for the question that contains
it. One also identifies a DIV element that marks the position of
the IFRAME and limits the size of the IFRAME (all IFRAMEs this
creates will be 100% x 100%).
@param {String} iframeid the id that the IFRAME has stored inside
it and uses for communication.
@param {String} the full HTML content of that IFRAME.
@param {String} targetdivid the id of the element (div) that will
hold the IFRAME.
@param {String} title a descriptive name for the iframe.
@param {bool} scrolling whether we have overflow:scroll or
overflow:hidden.
@param {bool} evil allows certain special cases to act without
sandboxing, this is a feature that will be removed so do
not rely on it only use it to test STACK-JS before you get your
thing to run in a sandbox.
*/
function create_iframe(iframeid, content, targetdivid, title, scrolling, evil) {
const frm = document.createElement('iframe');
frm.id = iframeid;
frm.style.width = '100%';
frm.style.height = '100%';
frm.style.border = 0;
if (scrolling === false) {
frm.scrolling = 'no';
frm.style.overflow = 'hidden';
} else {
frm.scrolling = 'yes';
}
frm.title = title;
// Somewhat random limitation.
frm.referrerpolicy = 'no-referrer';
// We include that allow-downloads as an example of XLS-
// document building in JS has been seen.
// UNDER NO CIRCUMSTANCES DO WE ALLOW-SAME-ORIGIN!
// That would defeat the whole point of this.
if (!evil) {
frm.sandbox = 'allow-scripts allow-downloads';
}
// As the SOP is intentionally broken we need to allow
// scripts from everywhere.
// NOTE: this bit commented out as long as the csp-attribute
// is not supported by more browsers.
// frm.csp = "script-src: 'unsafe-inline' 'self' '*';";
// frm.csp = "script-src: 'unsafe-inline' 'self' '*';img-src: '*';";
// Plug the content into the frame.
frm.srcdoc = content;
// The target DIV will have its children removed.
// This allows that div to contain some sort of loading
// indicator until we plug in the frame.
// Naturally the frame will then start to load itself.
document.getElementById(targetdivid).replaceChildren(frm);
IFRAMES[iframeid] = frm;
};
@@ -0,0 +1,753 @@
//Critical:
// TODO: Think about what should be done for an essay question.
//Accessibility:
//Enhancement:
// TODO: In a scaffold problem, adjust the green bar for when one part is correct.
// TODO: Have randomize check that new seed is actually producing new HTML.
// TODO: Don't offer randomize or button unless we know a new version can be produced after trying a few seeds.
// TODO: In a staged problem, Make Interactive button is adjacent to solution knowls; move it.
// TODO: MathQuill
// TODO: Image pop up.
// TODO: Deal with singleResult MultiAnswer problems.
//Styling:
// TODO: Review all styling in all scenarios (staged/not, correct/partly-correct/incorrect/blank, single/multiple)
// TODO: Sean: I think I'd like to see a slightly larger font size on the buttons
function handleWW(ww_id, action) {
const ww_container = document.getElementById(ww_id);
const ww_domain = ww_container.dataset.domain;
const ww_problemSource = ww_container.dataset.problemsource;
const ww_sourceFilePath = ww_container.dataset.sourcefilepath;
const ww_course_id = ww_container.dataset.courseid;
const ww_user_id = ww_container.dataset.userid;
const ww_course_password = ww_container.dataset.coursepassword;
const localize_correct = (ww_container.dataset.localizeCorrect ? ww_container.dataset.localizeCorrect : "Correct");
const localize_incorrect = (ww_container.dataset.localizeIncorrect ? ww_container.dataset.localizeIncorrect : "Incorrect");
const localize_check_responses = (ww_container.dataset.localizeCheckResponses ? ww_container.dataset.localizeCheckResponses : "Check Responses");
const localize_randomize = (ww_container.dataset.localizeRandomize ? ww_container.dataset.localizeRandomize : "Randomize");
const localize_reset = (ww_container.dataset.localizeReset ? ww_container.dataset.localizeReset : "Reset");
// will be null on pages generated prior to late December 2022
const activate_button = document.getElementById(ww_id + '-button')
// Set the current seed
if (!action) ww_container.dataset.current_seed = ww_container.dataset.seed;
else if (action == 'randomize') ww_container.dataset.current_seed = Number(ww_container.dataset.current_seed) + 100;
let loader = document.createElement('div');
loader.style.position = 'absolute';
loader.style.left = 0;
loader.style.top = 0;
loader.style.backgroundColor = 'rgba(0.2, 0.2, 0.2, 0.4)';
loader.style.color = 'white';
loader.style.width = '100%';
loader.style.height = '100%';
loader.style.display = 'flex';
loader.style.alignItems = 'center';
loader.style.justifyContent = 'center';
loader.style.marginTop = '0';
loader.tabIndex = -1;
const loaderText = document.createElement('span');
loaderText.textContent = 'Loading';
loaderText.style.fontSize = '2rem';
loader.appendChild(loaderText);
ww_container.appendChild(loader);
loader.focus();
if (!action) {
// Determine if static version shows hints, solutions, or answers and save that information in the container dataset for later runs.
ww_container.dataset.hasHint = ww_container.getElementsByClassName('hint').length > 0;
ww_container.dataset.hasSolution = ww_container.getElementsByClassName('solution').length > 0;
ww_container.dataset.hasAnswer = ww_container.getElementsByClassName('answer').length > 0;
// Get (possibly localized) label text for hints and solutions.
ww_container.dataset.hintLabelText = ww_container.dataset.hasHint == 'true'
? ww_container.querySelectorAll('.hint-knowl span.type')[0].textContent : 'Hint';
ww_container.dataset.solutionLabelText = ww_container.dataset.hasSolution == 'true'
? ww_container.querySelectorAll('.solution-knowl span.type')[0].textContent : 'Solution';
ww_container.tabIndex = -1;
}
let url;
if (action == 'check') {
const iframe = ww_container.querySelector('.problem-iframe');
const formData = new FormData(iframe.contentDocument.getElementById(ww_id + "-form"));
const params = new URLSearchParams(formData);
url = new URL(ww_domain + '/webwork2/html2xml?' + params.toString())
url.searchParams.append("answersSubmitted", '1');
url.searchParams.append('WWsubmit', "1");
} else {
url = new URL(ww_domain + '/webwork2/html2xml');
url.searchParams.append("problemSeed", ww_container.dataset.current_seed);
if (ww_problemSource) url.searchParams.append("problemSource", ww_problemSource);
else if (ww_sourceFilePath) url.searchParams.append("sourceFilePath", ww_sourceFilePath);
url.searchParams.append("answersSubmitted", '0');
url.searchParams.append("displayMode", "MathJax");
url.searchParams.append("courseID", ww_course_id);
url.searchParams.append("userID", ww_user_id);
url.searchParams.append("course_password", ww_course_password);
url.searchParams.append("outputformat", "raw");
// note ww_container.dataset.hasSolution is a string, possibly 'false' which is true
url.searchParams.append("showSolutions", ww_container.dataset.hasSolution == 'true' ? '1' : '0');
url.searchParams.append("showHints", ww_container.dataset.hasHint == 'true' ? '1' : '0');
url.searchParams.append("problemUUID",ww_id);
}
// get the json and do stuff with what we get
$.getJSON(url.toString(), (data) => {
// Create the form that will contain the text and input fields of the interactive problem.
const form = document.createElement("form");
form.id = ww_id + "-form";
// Create a div for the problem text.
const body_div = document.createElement("div");
body_div.id = ww_id + "-body";
body_div.classList.add("exercise", "exercise-like");
body_div.lang = data.lang;
body_div.dir = data.dir;
// Dump the problem text, answer blanks, etc.
body_div.innerHTML = data.rh_result.text;
// Replace all hn headings with h6 headings.
for (const tag_name of ['h6', 'h5', 'h4', 'h3', 'h2', 'h1']) {
const headings = body_div.getElementsByTagName(tag_name);
for (heading of headings) {
const new_heading = document.createElement("h6");
new_heading.innerHTML = heading.innerHTML;
cloneAttributes(new_heading, heading);
new_heading.classList.add('webwork-part');
heading.replaceWith(new_heading);
}
}
adjustSrcHrefs(body_div, ww_domain);
translateHintSol(ww_id, body_div, ww_domain,
ww_container.dataset.hasHint == 'true', ww_container.dataset.hasSolution == 'true',
ww_container.dataset.hintLabelText, ww_container.dataset.solutionLabelText)
// insert our cleaned up problem text
form.appendChild(body_div);
// Set up hidden input fields that the form uses
const wwInputs = {
problemSeed: data.inputs_ref.problemSeed,
problemUUID: data.inputs_ref.problemUUID,
psvn: data.inputs_ref.psvn,
courseName: ww_course_id,
courseID: ww_course_id,
userID: ww_user_id,
course_password: ww_course_password,
displayMode: "MathJax",
session_key: data.rh_result.session_key,
outputformat: "raw",
language: data.formLanguage,
showSummary: data.showSummary,
// note ww_container.dataset.hasSolution is a string, possibly 'false' which is true
showSolutions: ww_container.dataset.hasSolution == 'true' ? '1' : '0',
showHints: ww_container.dataset.hasHint == 'true' ? '1' : '0',
forcePortNumber: data.forcePortNumber
};
if (ww_sourceFilePath) wwInputs.sourceFilePath = ww_sourceFilePath;
else if (ww_problemSource) wwInputs.problemSource = ww_problemSource;
for (const wwInputName of Object.keys(wwInputs)) {
const input = document.createElement('input');
input.type = 'hidden';
input.name = wwInputName;
input.value = wwInputs[wwInputName];
form.appendChild(input);
}
// Prepare answers object
const answers = {};
// id the answers even if we won't populate them
Object.keys(data.rh_result.answers).forEach(function(id) {
answers[id] = {};
}, data.rh_result.answers);
if (ww_container.dataset.hasAnswer == 'true') {
// Update answer data
Object.keys(data.rh_result.answers).forEach(function(id) {
answers[id] = {
correct_ans: this[id].correct_ans,
correct_ans_latex_string: this[id].correct_ans_latex_string,
correct_choice: this[id].correct_choice
};
}, data.rh_result.answers);
}
let buttonContainer = ww_container.querySelector('.problem-buttons.webwork');
// Create the submission buttons if they have not yet been created.
if (!buttonContainer) {
// Hide the original div that contains the old make active button.
ww_container.querySelector('.problem-buttons').classList.add('hidden-content');
// And the newer activate button if it is there
if (activate_button != null) {activate_button.classList.add('hidden-content');};
// Create a new div for the webwork buttons.
buttonContainer = document.createElement('div');
buttonContainer.classList.add('problem-buttons', 'webwork');
if (activate_button != null) {
// Make sure the button container follows the activate button in the DOM
activate_button.after(buttonContainer);
} else {
ww_container.prepend(buttonContainer);
}
// Check button
const check = document.createElement("button");
check.type = "button";
check.id = ww_id + '-check';
check.style.marginRight = "0.25rem";
check.classList.add('webwork-button');
// Adjust if more than one answer to check
const answerCount = body_div.querySelectorAll("input:not([type=hidden])").length +
body_div.querySelectorAll("select:not([type=hidden])").length;
check.textContent = localize_check_responses;
check.addEventListener('click', () => handleWW(ww_id, "check"));
buttonContainer.appendChild(check);
// Show correct answers button if original PTX has answer knowl.
if (ww_container.dataset.hasAnswer == 'true') {
const correct = document.createElement("button");
correct.classList.add("show-correct", 'webwork-button');
correct.type = "button";
correct.style.marginRight = "0.25rem";
correct.textContent = answerCount > 1 ? "Show Correct Answers" : "Show Correct Answer";
correct.addEventListener('click', () => WWshowCorrect(ww_id, answers));
buttonContainer.appendChild(correct);
}
// randomize button
const randomize = document.createElement("button")
randomize.type = "button";
randomize.classList.add('webwork-button');
randomize.style.marginRight = "0.25rem";
randomize.textContent = localize_randomize;
randomize.addEventListener('click', () => handleWW(ww_id, 'randomize'));
buttonContainer.appendChild(randomize)
// reset button
const reset = document.createElement("button")
reset.type = "button"
reset.classList.add('webwork-button');
reset.textContent = localize_reset;
reset.addEventListener('click', () => resetWW(ww_id));
buttonContainer.appendChild(reset)
} else {
// Update the click handler for the show correct button.
if (ww_container.dataset.hasAnswer == 'true') {
const correct = buttonContainer.querySelector('.show-correct');
const correctNew = correct.cloneNode(true);
correctNew.addEventListener('click', () => WWshowCorrect(ww_id, answers));
correct.replaceWith(correctNew);
}
}
if (action == 'check') {
// Runestone trigger
// TODO: chondition on platform=Runestone
$("body").trigger('runestone_ww_check', data)
const inputs = body_div.querySelectorAll("input:not([type=hidden])");
for (const input of inputs) {
const name = input.name;
if (input.type == 'text' && answers[name]) {
const score = data.rh_result.answers[name].score;
let title = '';
if (score == 1) {
title = `<span class="correct">${localize_correct}!</span>`;
} else if (score > 0 && score < 1) {
title = `<span class="partly-correct">${Math.round(score * 100)}% ${localize_correct}.</span>`;
} else if (data.rh_result.answers[name].student_ans == '') {
// do nothing if the submitted answer is blank and the problem has not already been scored as correct
continue;
} else if (score == 0) {
title = `<span class="incorrect">${localize_incorrect}.</span>`;
}
input.after(createFeedbackButton(`${ww_id}-${name}`, title, data.rh_result.answers[name].ans_message));
}
if (input.type == 'radio' && answers[name]) {
if (input.value == data.rh_result.answers[name].student_value) {
const feedbackButton = createFeedbackButton(`${ww_id}-${name}`,
data.rh_result.answers[name].student_value == data.rh_result.answers[name].correct_choice
? `<span class="correct">${localize_correct}!</span>` : `<span class="incorrect">${localize_incorrect}.</span>`)
feedbackButton.style.marginRight = '0.25rem';
input.after(feedbackButton);
}
}
}
const hiddenInputs = body_div.querySelectorAll("input[type=hidden]");
for (const input of hiddenInputs) {
const name = input.name;
if (!input.nextElementSibling) continue;
const graphtoolContainer = input.nextElementSibling.nextElementSibling;
if (graphtoolContainer && answers[name] && graphtoolContainer.classList.contains('graphtool-container')) {
graphtoolContainer.style.position = 'relative';
const score = data.rh_result.answers[name].score;
let title = '';
if (score == 1) {
title = `<span class="correct">${localize_correct}!</span>`;
} else if (score > 0 && score < 1) {
title = `<span class="partly-correct">${Math.round(score * 100)}% ${localize_correct}.</span>`;
} else if (data.rh_result.answers[name].student_ans == '') {
// do nothing if the submitted answer is blank and the problem has not already been scored as correct
continue;
} else if (score == 0) {
title = `<span class="incorrect">${localize_incorrect}.</span>`;
}
const feedbackButton = createFeedbackButton(`${ww_id}-${name}`, title, data.rh_result.answers[name].ans_message);
feedbackButton.style.position = 'absolute';
feedbackButton.style.left = '100%';
feedbackButton.style.top = 0;
feedbackButton.style.marginLeft = '0.5rem';
feedbackButton.dataset.container = 'body';
graphtoolContainer.appendChild(feedbackButton);
}
}
const selects = body_div.querySelectorAll("select:not([type=hidden])");
for (const select of selects) {
const name = select.name;
const feedbackButton = createFeedbackButton(`${ww_id}-${name}`,
data.rh_result.answers[name].score == 1 ? `<span class="correct">${localize_correct}!</span>`
: `<span class="incorrect">${localize_incorrect}.</span>`)
feedbackButton.style.marginRight = '0.25rem';
feedbackButton.style.marginLeft = '0.5rem';
select.after(feedbackButton);
}
}
let iframeContents = '<!DOCTYPE html><head>' +
'<script src="' + ww_domain + '/webwork2_files/node_modules/jquery/dist/jquery.min.js"></script>' +
`<script>window.MathJax = {
tex: {
inlineMath: [['\\\\(','\\\\)']],
tags: "none",
useLabelIds: true,
tagSide: "right",
tagIndent: ".8em",
packages: {'[+]': ['base', 'extpfeil', 'ams', 'amscd', 'newcommand', 'knowl']}
},
options: {
ignoreHtmlClass: "tex2jax_ignore",
processHtmlClass: "has_am",
renderActions: {
findScript: [10, function (doc) {
document.querySelectorAll('script[type^="math/tex"]').forEach(function(node) {
const display = !!node.type.match(/; *mode=display/);
const math = new doc.options.MathItem(node.textContent, doc.inputJax[0], display);
const text = document.createTextNode('');
node.parentNode.replaceChild(text, node);
math.start = {node: text, delim: '', n: 0};
math.end = {node: text, delim: '', n: 0};
doc.math.push(math);
});
}, '']
},
},
chtml: {
scale: 0.88,
mtextInheritFont: true
},
loader: {
load: ['input/asciimath', '[tex]/extpfeil', '[tex]/amscd', '[tex]/newcommand', '[pretext]/mathjaxknowl3.js'],
paths: {pretext: "https://pretextbook.org/js/lib"},
},
};
</script>` +
'<script src="' + ww_domain + '/webwork2_files/mathjax/es5/tex-chtml.js" id="MathJax-script" defer></script>' +
'<script src="https://pretextbook.org/js/lib/knowl.js" defer></script>' +
'<link rel="stylesheet" href="' + ww_domain + '/webwork2_files/js/vendor/bootstrap/css/bootstrap.css"/>' +
'<link rel="stylesheet" href="' + ww_domain + '/webwork2_files/themes/math4/math4.css"/>' +
'<script src="' + ww_domain + '/webwork2_files/js/vendor/bootstrap/js/bootstrap.js" id="MathJax-script" defer></script>';
// Determine javascript and css dependencies
const extra_css_files = [];
const extra_js_files = [];
//if (data.rh_result.flags.extra_js_files) {
// for (const jsFile of data.rh_result.flags.extra_js_files) {
// if (jsFile.file == "js/apps/GraphTool/graphtool.min.js" || jsFile.file == "js/apps/Scaffold/scaffold.js") {
// extra_css_files.push({ file: 'js/vendor/bootstrap/css/bootstrap.css', external: '0' });
// extra_css_files.push({ file: 'themes/math4/math4.css', external: '0' });
// extra_js_files.push({ file: 'js/vendor/bootstrap/js/bootstrap.js', external: '0' });
// }
// }
//}
if (data.rh_result.flags.extra_css_files) data.rh_result.flags.extra_css_files.unshift(...extra_css_files);
else data.rh_result.flags.extra_css_files = extra_css_files;
for (const cssFile of data.rh_result.flags.extra_css_files) {
iframeContents += '<link rel="stylesheet" href="' + (cssFile.external !== '1' ? (ww_domain + '/webwork2_files/') : '') + cssFile.file + '"/>';
}
if (data.rh_result.flags.extra_js_files) data.rh_result.flags.extra_js_files.unshift(...extra_js_files);
else data.rh_result.flags.extra_js_files = extra_js_files;
for (const jsFile of data.rh_result.flags.extra_js_files) {
iframeContents += '<script src="' + (jsFile.external !== '1' ? (ww_domain + '/webwork2_files/') : '') + jsFile.file + '" ' +
Object.keys(jsFile.attributes || {}).reduce((ret, key) => {
ret += key + '="' + jsFile.attributes[key] + '" '; return ret;
}, "") + '></script>';
}
iframeContents +=
'<link rel="stylesheet" href="https://pretextbook.org/css/0.31/pretext_add_on.css"/>' +
'<link rel="stylesheet" href="https://pretextbook.org/css/0.31/knowls_default.css"/>' +
'<script src="' + ww_domain + '/webwork2_files/node_modules/iframe-resizer/js/iframeResizer.contentWindow.min.js"></script>' +
`<style>
html { overflow-y: hidden; }
html body { background:unset; margin: 0; }
body { font-size: initial; line-height: initial; }
.hidden-content { display: none; }
input[type="text"], input[type="radio"], label, select {
height: auto;
width: auto;
max-width: unset;
margin: 0;
font-size: initial;
font-family: sans-serif;
line-height: initial;
}
input[type="text"] {
padding: 1px;
padding-inline: 2px;
border-width: 1px;
border-style: inset;
border-color: rgb(133, 133, 133);
border-radius: 0;
box-shadow: unset;
transition: none;
}
input[type="text"]:focus, input[type="text"]:active {
box-shadow: unset;
border-width: 1px;
border-style: inset;
border-radius: 4px;
border-color: rgb(133, 133, 133);
outline: auto;
}
input[type="radio"] {
box-sizing: border-box;
margin-block: 3px 0;
margin-inline: 5px 3px;
vertical-align: unset;
}
label {
display: inline-block;
}
select {
margin: 0;
border-width: 1px;
border-style: inset;
display: inline-block;
padding-block: 1px;
vertical-align: baseline;
background-color: unset;
padding-inline: 0;
}
.popover-title, .popover-content {
text-align: center;
}
.popover-title.correct {
background-color: #8F8;
}
.accordion-body.expanded {
overflow-y: visible;
overflow-x: clip;
}
.graphtool-answer-container .graphtool-graph {
margin: 0;
width: 300px;
height: 300px;
}
div.PGML img.image-view-elt {
max-width:100%;
}
</style>` +
'</head><body><main class="pretext-content">' + form.outerHTML + '</main></body>' +
'</html>';
let iframe;
// If there is no action this is the initialization call.
if (!action) {
// Create the iframe.
iframe = document.createElement('iframe');
iframe.style.width = '1px';
iframe.style.minWidth = '100%';
iframe.classList.add('problem-iframe');
// Hide the static problem
ww_container.querySelector('.problem-contents').classList.add('hidden-content');
if (activate_button != null) {
// Make sure the iframe follows the activate button in the DOM
activate_button.after(iframe);
} else {
ww_container.prepend(iframe);
}
iFrameResize({ checkOrigin: false, scrolling: 'omit', heightCalculationMethod: 'min' }, iframe);
iframe.addEventListener('load', () => {
// Set up form submission from inside the iframe.
const iframeForm = iframe.contentDocument.getElementById(ww_id + '-form');
iframeForm.addEventListener('submit', (e) => {
handleWW(ww_id, "check");
e.preventDefault();
});
iframe.contentDocument.querySelectorAll('.collapse.in').forEach(collapse => collapse.classList.add('expanded'));
iframe.contentWindow.jQuery('.collapse').on('shown', function(e) { if (e.target != this) return; this.classList.add('expanded'); });
iframe.contentWindow.jQuery('.collapse').on('hide', function(e) { if (e.target != this) return; this.classList.remove('expanded'); });
iframe.contentDocument.querySelectorAll("button.ww-feedback[data-content]").forEach(button => {
iframe.contentWindow.jQuery(button).popover("show");
const content = iframe.contentDocument.getElementById(button.id.replace("-feedback-button", "-content"));
const popover = content.parentNode.parentNode;
popover.id = button.id.replace("-feedback-button", "-feedback")
button.setAttribute('aria-describedby', popover.id);
if (button.previousElementSibling)
button.previousElementSibling.setAttribute('aria-describedby', popover.id);
popover.querySelector('.arrow').remove();
const title = popover.querySelector('.popover-title');
if (button.dataset.emptyContent) {
title.style.borderBottomWidth = 0;
content.parentNode.remove();
}
if (title.textContent == localize_correct + '!') title.classList.add('correct');
});
iframe.contentWindow.MathJax.startup.promise.then(() => iframe.contentWindow.MathJax.typesetPromise(['.popover', '.popover-content']));
});
} else {
iframe = ww_container.querySelector('.problem-iframe');
}
iframe.srcdoc = iframeContents;
iframe.addEventListener('load', () => {
// Remove the loader overlay
loader.remove();
}, { once: true })
// Place focus on the problem.
ww_container.focus()
});
}
function WWshowCorrect(ww_id, answers) {
const ww_container = document.getElementById(ww_id);
const iframe = ww_container.querySelector('.problem-iframe');
const body = iframe.contentDocument.getElementById(ww_id + '-body')
$("body").trigger("runestone_show_correct", {
"ww_id": ww_id,
"answers": answers
});
let inputs = body.querySelectorAll("input:not([type=hidden])");
for (const input of inputs) {
const name = input.name;
const span_id = `${ww_id}-${name}-correct`;
if (input.type == 'text' && answers[name] && !(iframe.contentDocument.getElementById(span_id))) {
const feedbackButton = iframe.contentDocument.getElementById(`${ww_id}-${name}-feedback-button`);
if (feedbackButton) {
feedbackButton.remove();
iframe.contentWindow.jQuery(feedbackButton).popover("hide");
}
label = iframe.contentDocument.getElementById(`${span_id}-label`);
if (label) {
label.parentElement.insertBefore(input, label);
label.remove();
}
input.type = "hidden";
// we need to convert things like &lt; in answers to <
const correct_ans_text = iframe.contentDocument.createElement('div');
correct_ans_text.innerHTML = answers[name].correct_ans;
input.value = correct_ans_text.textContent;
const show_span = iframe.contentDocument.createElement('span');
show_span.id = span_id;
show_span.appendChild(answers[name].correct_ans_latex_string
? iframe.contentDocument.createTextNode('\\(' + answers[name].correct_ans_latex_string + '\\)')
: iframe.contentDocument.createTextNode(answers[name].correct_ans));
input.parentElement.insertBefore(show_span, input);
}
if (input.type == 'radio' && answers[name]) {
const feedbackButton = iframe.contentDocument.getElementById(`${ww_id}-${name}-feedback-button`);
if (feedbackButton) {
feedbackButton.remove();
iframe.contentWindow.jQuery(feedbackButton).popover("hide");
}
correct_value = answers[name].correct_choice;
if (input.value == correct_value) input.checked = true;
}
}
const hiddenInputs = body.querySelectorAll("input[type=hidden]");
for (const input of hiddenInputs) {
const name = input.name;
if (!input.nextElementSibling) continue;
const graphtoolContainer = input.nextElementSibling.nextElementSibling;
if (graphtoolContainer && answers[name] && graphtoolContainer.classList.contains('graphtool-container')) {
const feedbackButton = iframe.contentDocument.getElementById(`${ww_id}-${name}-feedback-button`);
if (feedbackButton) {
feedbackButton.remove();
iframe.contentWindow.jQuery(feedbackButton).popover("hide");
}
const correct_ans_div = iframe.contentDocument.createElement('div');
input.parentElement.insertBefore(correct_ans_div, graphtoolContainer);
graphtoolContainer.style.display = 'none';
input.value = answers[name].correct_ans;
iframe.contentWindow.jQuery(correct_ans_div).html(answers[name].correct_ans_latex_string);
const script = iframe.contentDocument.createElement('script');
script.textContent = correct_ans_div.querySelector('script').textContent
.replace('\nwindow.addEventListener("DOMContentLoaded",', '(')
.replace(/;\n$/, '();');
iframe.contentDocument.body.appendChild(script);
}
}
let selects = body.querySelectorAll("select:not([type=hidden])");
for (const select of selects) {
const name = select.name;
const span_id = `${ww_id}-${name}-correct`;
if (answers[name] && !iframe.contentDocument.getElementById(span_id)) {
const feedbackButton = iframe.contentDocument.getElementById(`${ww_id}-${name}-feedback-button`);
if (feedbackButton) {
feedbackButton.remove();
iframe.contentWindow.jQuery(feedbackButton).popover("hide");
}
select.style.display = "none";
select.value = answers[name].correct_ans;
const show_span = iframe.contentDocument.createElement('span');
show_span.id = span_id;
show_span.appendChild(answers[name].correct_ans_latex_string
? iframe.contentDocument.createTextNode('\\(' + answers[name].correct_ans_latex_string + '\\)')
: iframe.contentDocument.createTextNode(answers[name].correct_ans));
select.parentElement.insertBefore(show_span, select);
}
}
// run MathJax on our new rendering
// FIXME: We only need to typeset the added elements, not the entire body.
const mathjaxTypesetScript = iframe.contentDocument.createElement('script');
mathjaxTypesetScript.textContent = 'MathJax.startup.promise.then(() => MathJax.typesetPromise([document.body]));';
iframe.contentDocument.body.appendChild(mathjaxTypesetScript);
}
function resetWW(ww_id) {
const ww_container = document.getElementById(ww_id);
const activate_button = document.getElementById(ww_id + '-button');
ww_container.dataset.current_seed = ww_container.dataset.seed;
iframe = ww_container.querySelector('.problem-iframe');
iframe.remove();
ww_container.querySelector('.problem-contents').classList.remove('hidden-content');
ww_container.querySelector('.problem-buttons.webwork').remove();
ww_container.querySelector('.problem-buttons').classList.remove('hidden-content');
// if the newer activate button is there (but hidden) bring it back too
if (activate_button != null) {activate_button.classList.remove('hidden-content');};
}
function adjustSrcHrefs(container,ww_domain) {
container.querySelectorAll('[href]').forEach((node) => {
const href = node.attributes.href.value;
if (href !== '#' && !href.match(/^[a-z]+:\/\//i)) node.href = ww_domain + '/' + href;
});
container.querySelectorAll('[src]').forEach((node) => {
node.src = ww_domain + '/' + node.attributes.src.value;
});
}
function translateHintSol(ww_id, body_div, ww_domain, b_ptx_has_hint, b_ptx_has_solution, hint_label_text, solution_label_text) {
// the problem text may come with "hint"s and "solution"s
// each one is an "a" with content "Hint" or "Solution", and an attribute with base64-encoded HTML content
// the WeBWorK knowl js would normally handle this, but we want PreTeXt knowl js to handle it
// so we replace the "a" with the content that should be there for PTX knowl js
// also if hint/sol were missing from the static version, we want these removed here
const ww_container = document.getElementById(ww_id);
const hintsolnodes = document.evaluate("//p[a/b]", body_div, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
if (hintsolnodes) {
let solutionlikewrapper;
for (let i = 0; i < hintsolnodes.snapshotLength; i++) {
const hintsolp = hintsolnodes.snapshotItem(i);
if (!hintsolp) continue;
const hintSolType = hintsolp.textContent.trim().toLowerCase().replace(':', '');
if (hintsolp.previousElementSibling.textContent.trim() != 'Hint:' && hintsolp.previousElementSibling.textContent.trim() != 'Solution:') {
solutionlikewrapper = document.createElement('div');
solutionlikewrapper.classList.add('webwork', 'solutions');
hintsolp.parentNode.insertBefore(solutionlikewrapper, hintsolp);
}
if ((hintSolType == 'solution' && !b_ptx_has_solution) || (hintSolType == 'hint' && !b_ptx_has_hint)) continue;
const knowlDetails = document.createElement('details');
knowlDetails.classList.add(hintSolType);
knowlDetails.classList.add('solution-like');
knowlDetails.classList.add('born-hidden-knowl');
const knowlSummary = document.createElement('summary');
const summaryLabel = document.createElement('span');
summaryLabel.classList.add('type');
summaryLabel.innerHTML = hintSolType == 'hint' ? hint_label_text : solution_label_text;
knowlSummary.appendChild(summaryLabel);
knowlDetails.appendChild(knowlSummary);
const knowlContents = document.createElement('div');
knowlContents.classList.add(hintSolType);
knowlContents.classList.add('solution-like');
knowlContents.innerHTML = hintsolp.firstElementChild.dataset.knowlContents;
knowlDetails.appendChild(knowlContents);
adjustSrcHrefs(knowlContents, ww_domain)
solutionlikewrapper.appendChild(knowlDetails);
}
}
for (let i = 0; i < hintsolnodes.snapshotLength; i++) {
hintsolnodes.snapshotItem(i).remove();
}
}
function cloneAttributes(target, source) {
[...source.attributes].forEach( attr => { target.setAttribute(attr.nodeName ,attr.nodeValue) });
}
function createFeedbackButton(id, title, content) {
const feedbackButton = document.createElement('button');
feedbackButton.dataset.title = title;
feedbackButton.dataset.content = `<div id="${id}-content">${content || ''}</div>`;
if (!content) feedbackButton.dataset.emptyContent = '1';
const contentSpan = document.createElement('span');
contentSpan.style.fontWeight = 1000;
contentSpan.textContent = '\uD83D\uDDE9'
feedbackButton.appendChild(contentSpan);
feedbackButton.type = 'button';
feedbackButton.classList.add('ww-feedback');
feedbackButton.style.borderRadius = 0;
feedbackButton.id = `${id}-feedback-button`;
feedbackButton.dataset.html = true;
feedbackButton.dataset.placement = 'bottom';
feedbackButton.dataset.trigger = 'click';
return feedbackButton;
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,677 @@
//Critical:
// TODO: Think about what should be done for an essay question.
//Accessibility:
//Enhancement:
// TODO: Have randomize check that new seed is actually producing new HTML.
// TODO: Don't offer randomize button unless we know a new version can be produced after trying a few seeds.
//Styling:
// TODO: Review all styling in all scenarios (staged/not, correct/partly-correct/incorrect/blank, single/multiple)
async function handleWW(ww_id, action) {
const ww_container = document.getElementById(ww_id);
const ww_domain = ww_container.dataset.domain;
const ww_processing = 'webwork2';
const ww_origin = ww_container.dataset.origin;
const ww_problemSource = ww_container.dataset.problemsource;
const ww_sourceFilePath = ww_container.dataset.sourcefilepath;
const ww_course_id = ww_container.dataset.courseid;
const ww_user_id = ww_container.dataset.userid;
const ww_passwd = ww_container.dataset.coursepassword;
const localize_submit = ww_container.dataset.localizeSubmit || "Submit";
const localize_check_responses = ww_container.dataset.localizeCheckResponses || "Check Responses";
const localize_reveal = ww_container.dataset.localizeReveal || "Reveal";
const localize_randomize = ww_container.dataset.localizeRandomize || "Randomize";
const localize_reset = ww_container.dataset.localizeReset || "Reset";
const runestone_logged_in = (typeof eBookConfig !== 'undefined' && eBookConfig.username !== '');
// will be null on pages generated prior to late December 2022
const activate_button = document.getElementById(ww_id + '-button')
// Set the current seed
if (!action) {
ww_container.dataset.current_seed = ww_container.dataset.seed;
if (runestone_logged_in) {
ww_container.dataset.current_seed = webworkSeedHash(eBookConfig.username + ww_container.dataset.current_seed);
}
}
else if (action == 'randomize') ww_container.dataset.current_seed = Number(ww_container.dataset.current_seed) + 100;
let loader = document.createElement('div');
loader.style.position = 'absolute';
loader.style.left = 0;
loader.style.top = 0;
loader.style.backgroundColor = 'rgba(0.2, 0.2, 0.2, 0.4)';
loader.style.color = 'white';
loader.style.width = '100%';
loader.style.height = '100%';
loader.style.display = 'flex';
loader.style.alignItems = 'center';
loader.style.justifyContent = 'center';
loader.style.marginTop = '0';
loader.tabIndex = -1;
const loaderText = document.createElement('span');
loaderText.textContent = 'Loading';
loaderText.style.fontSize = '2rem';
loader.appendChild(loaderText);
ww_container.appendChild(loader);
loader.focus();
if (!action) {
// Determine if static version shows hints, solutions, or answers and save that information in the container dataset for later runs.
ww_container.dataset.hasHint = ww_container.getElementsByClassName('hint').length > 0;
ww_container.dataset.hasSolution = ww_container.getElementsByClassName('solution').length > 0;
ww_container.dataset.hasAnswer = ww_container.getElementsByClassName('answer').length > 0;
// Get (possibly localized) label text for hints and solutions.
ww_container.dataset.hintLabelText = ww_container.dataset.hasHint == 'true'
? ww_container.querySelectorAll('.hint-knowl span.type, details.hint span.type')[0].textContent : 'Hint';
ww_container.dataset.solutionLabelText = ww_container.dataset.hasSolution == 'true'
? ww_container.querySelectorAll('.solution-knowl span.type, details.solution span.type')[0].textContent : 'Solution';
ww_container.tabIndex = -1;
}
let url;
if (ww_processing == 'webwork2') {
url = new URL(ww_domain + '/webwork2/render_rpc');
}
let formData = new FormData();
let generatedPG = 'generated/webwork/pg/';
if (runestone_logged_in) {
generatedPG = `/ns/books/published/${eBookConfig.basecourse}/${generatedPG}`;
}
if (action == 'check' || action =='reveal') {
const iframe = ww_container.querySelector('.problem-iframe');
formData = new FormData(iframe.contentDocument.getElementById(ww_id + "-form"));
formData.set("answersSubmitted", '1');
formData.set('WWsubmit', "1");
if (action == 'reveal' && ww_container.dataset.hasAnswer == 'true') {
formData.set('WWcorrectAnsOnly', "1");
}
if (ww_origin == 'generated') {
const rawProblemSource = await fetch(generatedPG + ww_problemSource).then((r) => r.text());
formData.set("rawProblemSource", rawProblemSource);
}
else if (ww_origin == 'webwork2') formData.set("sourceFilePath", ww_sourceFilePath);
} else {
formData.set("problemSeed", ww_container.dataset.current_seed);
if (ww_origin == 'generated') {
const rawProblemSource = await fetch(generatedPG + ww_problemSource).then((r) => r.text());
formData.set("rawProblemSource", rawProblemSource);
}
else if (ww_origin == 'webwork2') formData.set("sourceFilePath", ww_sourceFilePath);
formData.set("answersSubmitted", '0');
formData.set("displayMode", "MathJax");
formData.set("courseID", ww_course_id);
formData.set("user", ww_user_id);
formData.set("userID", ww_user_id);
formData.set("passwd", ww_passwd);
formData.set("disableCookies", '1');
formData.set("outputformat", "raw");
// note ww_container.dataset.hasSolution is a string, possibly 'false' which is true
formData.set("showSolutions", ww_container.dataset.hasSolution == 'true' ? '1' : '0');
formData.set("showHints", ww_container.dataset.hasHint == 'true' ? '1' : '0');
formData.set("problemUUID",ww_id);
}
// If in Runestone, check if there are previous answer submissions and get them now to use in the form.
let checkboxesString = '';
if (runestone_logged_in && !action) {
const answersObject = (wwList[ww_id.replace(/-ww-rs$/,'')].answers ? wwList[ww_id.replace(/-ww-rs$/,'')].answers : {'answers' : [], 'mqAnswers' : []});
const previousAnswers = answersObject.answers;
if (previousAnswers !== null) {
formData.set('WWsubmit', 1);
}
for (const answer in previousAnswers) {
if (previousAnswers[answer].constructor === Array) {
for (const k in previousAnswers[answer]) {
checkboxesString += '&';
checkboxesString += answer;
checkboxesString += '=';
checkboxesString += previousAnswers[answer][k];
}
} else {
formData.set(answer, previousAnswers[answer]);
}
}
}
// Need to get form data as a string, including possible repeated checkbox names
// Do not pass post data as an object, or checkbox names will overwrite one another
const formString = new URLSearchParams(formData).toString();
$.post(url, formString + checkboxesString, (data) => {
// Create the form that will contain the text and input fields of the interactive problem.
const form = document.createElement("form");
form.id = ww_id + "-form";
form.dataset.iframeHeight = 1;
// Create a div for the problem text.
const body_div = document.createElement("div");
body_div.id = ww_id + "-body";
body_div.classList.add("exercise", "exercise-like");
body_div.lang = data.lang;
body_div.dir = data.dir;
// Dump the problem text, answer blanks, etc.
body_div.innerHTML = data.rh_result.text;
// If showPartialCorrectAnswers = 0, alter the feedback buttons according to whether or not the score is 100%.
if ('showPartialCorrectAnswers' in data.rh_result.flags && data.rh_result.flags.showPartialCorrectAnswers == 0) {
if ('score' in data.rh_result.problem_result && data.rh_result.problem_result.score >= 1) {
body_div.querySelectorAll('button.ww-feedback-btn').forEach(
function(button) {
button.classList.remove('btn-info');
button.classList.add('btn-success');
button.setAttribute('aria-label', 'Correct');
button.dataset.bsCustomClass = button.dataset.bsCustomClass + ' correct';
button.dataset.bsTitle = button.dataset.bsTitle.replace('Answer Preview', 'Correct');
button.firstChild.classList.add('correct')
}
);
} else {
body_div.querySelectorAll('button.ww-feedback-btn').forEach(
function(button) {
button.setAttribute('aria-label', 'One or more answers incorrect');
button.dataset.bsTitle = button.dataset.bsTitle.replace('Answer Preview', 'One or more answers incorrect');
}
);
}
}
// Replace all hn headings with h6 headings.
for (const tag_name of ['h6', 'h5', 'h4', 'h3', 'h2', 'h1']) {
const headings = body_div.getElementsByTagName(tag_name);
for (heading of headings) {
const new_heading = document.createElement("h6");
new_heading.innerHTML = heading.innerHTML;
cloneAttributes(new_heading, heading);
new_heading.classList.add('webwork-part');
heading.replaceWith(new_heading);
}
}
// Hide textarea input and hide associated buttons
var textareas = body_div.getElementsByTagName("textarea");
for(var i = 0, max = textareas.length; i < max; i++)
{
textareas[i].style.display = "none";
textareas[i].className = '';
}
var textareabuttons = body_div.querySelectorAll(".latexentry-preview");
for(var i = 0, max = textareabuttons.length; i < max; i++)
{
textareabuttons[i].remove();
}
adjustSrcHrefs(body_div, ww_domain);
translateHintSol(ww_id, body_div, ww_domain,
ww_container.dataset.hasHint == 'true', ww_container.dataset.hasSolution == 'true',
ww_container.dataset.hintLabelText, ww_container.dataset.solutionLabelText)
// insert previous answers
if (runestone_logged_in) {
const answersObject = (wwList[ww_id.replace(/-ww-rs$/,'')].answers ? wwList[ww_id.replace(/-ww-rs$/,'')].answers : {'answers' : [], 'mqAnswers' : []});
const mqAnswers = answersObject.mqAnswers;
for (const mqAnswer in mqAnswers) {
const mqInput = body_div.querySelector('input[id=' + mqAnswer + ']');
if (mqInput && mqInput.value == '') {
mqInput.setAttribute('value', mqAnswers[mqAnswer]);
}
}
const answers = answersObject.answers;
for (const answer in answers) {
const input = body_div.querySelector('input[id=' + answer + ']');
if (input && input.value == '') {
input.setAttribute('value', answers[answer]);
}
}
}
// insert our cleaned up problem text
form.appendChild(body_div);
// Set up hidden input fields that the form uses
const wwInputs = {
problemSeed: data.inputs_ref.problemSeed,
problemUUID: data.inputs_ref.problemUUID,
psvn: data.inputs_ref.psvn,
courseName: ww_course_id,
courseID: ww_course_id,
user: ww_user_id,
passwd: ww_passwd,
displayMode: "MathJax",
session_key: data.rh_result.session_key,
outputformat: "raw",
language: data.formLanguage,
showSummary: data.showSummary,
disableCookies: '1',
// note ww_container.dataset.hasSolution is a string, possibly 'false' which is true
showSolutions: ww_container.dataset.hasSolution == 'true' ? '1' : '0',
showHints: ww_container.dataset.hasHint == 'true' ? '1' : '0',
forcePortNumber: data.forcePortNumber
};
if (ww_sourceFilePath) wwInputs.sourceFilePath = ww_sourceFilePath;
else if (ww_problemSource && ww_origin == 'webwork2') wwInputs.problemSource = ww_problemSource;
for (const wwInputName of Object.keys(wwInputs)) {
const input = document.createElement('input');
input.type = 'hidden';
input.name = wwInputName;
input.value = wwInputs[wwInputName];
form.appendChild(input);
}
let buttonContainer = ww_container.querySelector('.problem-buttons.webwork');
// Create the submission buttons if they have not yet been created.
if (!buttonContainer) {
// Hide the original div that contains the Activate.
ww_container.querySelector('.problem-buttons').classList.add('hidden-content', 'hidden');
// Create a new div for the webwork buttons.
buttonContainer = document.createElement('div');
buttonContainer.classList.add('problem-buttons', 'webwork');
if (activate_button != null) {
// Make sure the button container follows the activate button in the DOM
activate_button.after(buttonContainer);
} else {
ww_container.prepend(buttonContainer);
}
// Check button
const check = document.createElement("button");
check.type = "button";
check.id = ww_id + '-check';
check.style.marginRight = "0.25rem";
check.classList.add('webwork-button');
check.textContent = runestone_logged_in ? localize_submit : localize_check_responses;
check.addEventListener('click', () => handleWW(ww_id, "check"));
buttonContainer.appendChild(check);
// Show correct answers button if original PTX has answer knowl.
if (ww_container.dataset.hasAnswer == 'true') {
const correct = document.createElement("button");
correct.classList.add("show-correct", 'webwork-button');
correct.type = "button";
correct.style.marginRight = "0.25rem";
correct.textContent = localize_reveal;
correct.addEventListener('click', () => handleWW(ww_id, 'reveal'));
buttonContainer.appendChild(correct);
}
// randomize button
const randomize = document.createElement("button")
randomize.type = "button";
randomize.classList.add('webwork-button');
randomize.style.marginRight = "0.25rem";
randomize.textContent = localize_randomize;
randomize.addEventListener('click', () => handleWW(ww_id, 'randomize'));
buttonContainer.appendChild(randomize)
// reset button
const reset = document.createElement("button")
reset.type = "button"
reset.classList.add('webwork-button');
reset.textContent = localize_reset;
reset.addEventListener('click', () => resetWW(ww_id));
buttonContainer.appendChild(reset)
}
if (runestone_logged_in && action == 'check') {
// Runestone trigger
$("body").trigger('runestone_ww_check', data)
}
let courseUrlBase = '';
if (runestone_logged_in){
courseUrlBase = '/ns/books/published/' + eBookConfig.basecourse + '/';
}
let iframeContents = '<!DOCTYPE html><head>' +
'<script src="' + ww_domain + '/webwork2_files/node_modules/jquery/dist/jquery.min.js"></script>' +
`<script>
window.MathJax = {
tex: { packages: { '[+]': ['noerrors'] } },
loader: { load: ['input/asciimath', '[tex]/noerrors'] },
startup: {
ready() {
const AM = MathJax.InputJax.AsciiMath.AM;
// Modify existing AsciiMath triggers.
AM.symbols[AM.names.indexOf('**')] = {
input: '**',
tag: 'msup',
output: '^',
tex: null,
ttype: AM.TOKEN.INFIX
};
const i = AM.names.indexOf('infty');
AM.names[i] = 'infinity';
AM.symbols[i] = { input: 'infinity', tag: 'mo', output: '\u221E', tex: 'infty', ttype: AM.TOKEN.CONST };
return MathJax.startup.defaultReady();
}
},
options: {
processHtmlClass: "process-math",
renderActions: {
findScript: [
10,
(doc) => {
for (const node of document.querySelectorAll('script[type^="math/tex"]')) {
const math = new doc.options.MathItem(
node.textContent,
doc.inputJax[0],
!!node.type.match(/; *mode=display/)
);
const text = document.createTextNode('');
node.parentNode.replaceChild(text, node);
math.start = { node: text, delim: '', n: 0 };
math.end = { node: text, delim: '', n: 0 };
doc.math.push(math);
}
},
''
]
},
ignoreHtmlClass: 'tex2jax_ignore'
}
};
</script>` +
'<script src="' + ww_domain + '/webwork2_files/node_modules/mathjax/es5/tex-chtml.js" id="MathJax-script" defer></script>' +
`<script src="${courseUrlBase}_static/pretext/js/lib/knowl.js" defer></script>` +
'<link rel="stylesheet" href="' + ww_domain + '/webwork2_files/node_modules/bootstrap/dist/css/bootstrap.min.css"/>' +
'<script src="' + ww_domain + '/webwork2_files/node_modules/bootstrap/dist/js/bootstrap.bundle.min.js" defer></script>';
// Determine javascript and css dependencies
const extra_css_files = [];
const extra_js_files = [];
if (data.extra_css_files) data.extra_css_files.unshift(...extra_css_files);
else data.extra_css_files = extra_css_files;
for (const cssFile of data.extra_css_files) {
iframeContents += '<link rel="stylesheet" href="' + (cssFile.external !== '1' ? ww_domain : '') + cssFile.file + '"/>';
}
if (data.extra_js_files) data.extra_js_files.unshift(...extra_js_files);
else data.extra_js_files = extra_js_files;
for (const jsFile of data.extra_js_files) {
iframeContents += '<script src="' + (jsFile.external !== '1' ? ww_domain : '') + jsFile.file + '" ' +
Object.keys(jsFile.attributes || {}).reduce((ret, key) => {
ret += key + '="' + jsFile.attributes[key] + '" '; return ret;
}, "") + '></script>';
}
iframeContents +=
`<link rel="stylesheet" href="${courseUrlBase}_static/pretext/css/theme.css"/>` +
'<script src="' + ww_domain + '/webwork2_files/node_modules/iframe-resizer/js/iframeResizer.contentWindow.min.js"></script>' +
`<style>
html { overflow-y: hidden; }
html body { background:unset; margin: 0; }
body { font-size: initial; line-height: initial; padding:2px; }
.hidden-content { display: none; }
span.nobreak { white-space: nowrap; }
div.PGML img.image-view-elt { max-width:100%; }
.graphtool-answer-container .graphtool-graph { margin: 0; width: 300px; height: 300px; }
.graphtool-answer-container .graphtool-number-line { height: 57px; }
.quill-toolbar { scrollbar-width: thin; overflow-x: hidden; }
</style>` +
'</head><body>' +
'<main class="pretext-content problem-content" data-iframe-height="1">' + form.outerHTML + '</main></body>' +
'</html>';
let iframe;
// If there is no action this is the initialization call.
if (!action) {
// Create the iframe.
iframe = document.createElement('iframe');
iframe.style.width = '1px';
iframe.style.minWidth = '100%';
iframe.classList.add('problem-iframe');
// Hide the static problem
ww_container.querySelector('.problem-contents').classList.add('hidden-content', 'hidden');
if (activate_button != null) {
// Make sure the iframe follows the activate button in the DOM
activate_button.after(iframe);
} else {
ww_container.prepend(iframe);
}
iFrameResize({ checkOrigin: false, scrolling: 'omit', heightCalculationMethod: 'taggedElement' }, iframe);
iframe.addEventListener('load', () => {
// Set up form submission from inside the iframe.
const iframeForm = iframe.contentDocument.getElementById(ww_id + '-form');
iframeForm.addEventListener('submit', (e) => {
handleWW(ww_id, "check");
e.preventDefault();
});
iframe.contentDocument.querySelectorAll('.collapse.in').forEach(collapse => collapse.classList.add('expanded'));
iframe.contentWindow.jQuery('.collapse').on('shown', function(e) { if (e.target != this) return; this.classList.add('expanded'); });
iframe.contentWindow.jQuery('.collapse').on('hide', function(e) { if (e.target != this) return; this.classList.remove('expanded'); });
iframe.contentWindow.MathJax.startup.promise.then(() => iframe.contentWindow.MathJax.typesetPromise(['.popover', '.popover-content']));
});
} else {
iframe = ww_container.querySelector('.problem-iframe');
}
iframe.srcdoc = iframeContents;
iframe.addEventListener('load', () => {
// Remove the loader overlay
loader.remove();
}, { once: true })
// Place focus on the problem.
ww_container.focus()
}, "json");
}
function WWshowCorrect(ww_id, answers) {
const ww_container = document.getElementById(ww_id);
const iframe = ww_container.querySelector('.problem-iframe');
const body = iframe.contentDocument.getElementById(ww_id + '-body')
$("body").trigger("runestone_show_correct", {
"ww_id": ww_id,
"answers": answers
});
let inputs = body.querySelectorAll("input:not([type=hidden])");
for (const input of inputs) {
const name = input.name;
const span_id = `${ww_id}-${name}-correct`;
if (input.type == 'text' && answers[name] && !(iframe.contentDocument.getElementById(span_id))) {
const input_id = input.id;
const mq_span = iframe.contentDocument.getElementById(`mq-answer-${input_id}`);
if (mq_span) {
mq_span.style.display = 'none';
}
label = iframe.contentDocument.getElementById(`${span_id}-label`);
if (label) {
label.parentElement.insertBefore(input, label);
label.remove();
}
input.type = "hidden";
// we need to convert things like &lt; in answers to <
const correct_ans_text = iframe.contentDocument.createElement('div');
correct_ans_text.innerHTML = answers[name].correct_ans;
input.value = correct_ans_text.textContent;
const show_span = iframe.contentDocument.createElement('span');
show_span.id = span_id;
show_span.appendChild(answers[name].correct_ans_latex_string
? iframe.contentDocument.createTextNode('\\(' + answers[name].correct_ans_latex_string + '\\)')
: iframe.contentDocument.createTextNode(answers[name].correct_ans));
input.parentElement.insertBefore(show_span, input);
}
if (input.type.toUpperCase() == 'RADIO' && answers[name]) {
const correct_ans = answers[name].correct_choice || answers[name].correct_ans;
if (input.value == correct_ans) {
input.checked = true;
} else {
input.checked = false;
}
}
if (input.type.toUpperCase() == 'CHECKBOX' && answers[name]) {
const correct_choices = answers[name].correct_choices;
if (correct_choices.includes(input.value)) {
input.checked = true;
} else {
input.checked = false;
}
}
}
const hiddenInputs = body.querySelectorAll("input[type=hidden]");
for (const input of hiddenInputs) {
const name = input.name;
if (!input.nextElementSibling) continue;
const graphtoolContainer = input.nextElementSibling.nextElementSibling;
if (graphtoolContainer && answers[name] && graphtoolContainer.classList.contains('graphtool-container')) {
const correct_ans_div = iframe.contentDocument.createElement('div');
input.parentElement.insertBefore(correct_ans_div, graphtoolContainer);
graphtoolContainer.style.display = 'none';
input.value = answers[name].correct_ans;
iframe.contentWindow.jQuery(correct_ans_div).html(answers[name].correct_ans_latex_string);
const script = iframe.contentDocument.createElement('script');
script.textContent = correct_ans_div.querySelector('script').textContent
.replace('\nwindow.addEventListener("DOMContentLoaded",', '(')
.replace(/;\n$/, '();');
iframe.contentDocument.body.appendChild(script);
}
}
let selects = body.querySelectorAll("select:not([type=hidden])");
for (const select of selects) {
const name = select.name;
const span_id = `${ww_id}-${name}-correct`;
if (answers[name] && !iframe.contentDocument.getElementById(span_id)) {
select.style.display = "none";
select.value = answers[name].correct_ans;
const show_span = iframe.contentDocument.createElement('span');
show_span.id = span_id;
show_span.appendChild(answers[name].correct_ans_latex_string
? iframe.contentDocument.createTextNode('\\(' + answers[name].correct_ans_latex_string + '\\)')
: iframe.contentDocument.createTextNode(answers[name].correct_ans));
select.parentElement.insertBefore(show_span, select);
}
}
// run MathJax on our new rendering
// FIXME: We only need to typeset the added elements, not the entire body.
const mathjaxTypesetScript = iframe.contentDocument.createElement('script');
mathjaxTypesetScript.textContent = 'MathJax.startup.promise.then(() => MathJax.typesetPromise([document.body]));';
iframe.contentDocument.body.appendChild(mathjaxTypesetScript);
}
function resetWW(ww_id) {
const ww_container = document.getElementById(ww_id);
const activate_button = document.getElementById(ww_id + '-button');
ww_container.dataset.current_seed = ww_container.dataset.seed;
iframe = ww_container.querySelector('.problem-iframe');
iframe.remove();
ww_container.querySelector('.problem-contents').classList.remove('hidden-content', 'hidden');
ww_container.querySelector('.problem-buttons.webwork').remove();
ww_container.querySelector('.problem-buttons').classList.remove('hidden-content', 'hidden');
// if the newer activate button is there (but hidden) bring it back too
if (activate_button != null) {activate_button.classList.remove('hidden-content', 'hidden');};
}
function adjustSrcHrefs(container,ww_domain) {
container.querySelectorAll('[href]').forEach((node) => {
const href = node.attributes.href.value;
if (href !== '#' && !href.match(/^[a-z]+:\/\//i)) node.href = ww_domain + '/' + href;
});
container.querySelectorAll('[data-knowl-url]').forEach((node) => {
const dku = node.dataset.knowlUrl;
if (dku !== '#' && !dku.match(/^[a-z]+:\/\//i)) node.dataset.knowlUrl = ww_domain + dku;
});
container.querySelectorAll('[src]').forEach((node) => {
node.src = new URL(node.attributes.src.value, ww_domain).href;
});
}
function translateHintSol(ww_id, body_div, ww_domain, b_ptx_has_hint, b_ptx_has_solution, hint_label_text, solution_label_text) {
// The problem text may come with "hint"s and "solution"s
// Each one is a div.accordion > details.accordion-item > summary.accordion-button
// Styling is not the PTX way, so we change it to one div.solutions
// with (potentially multiple) details.born-hidden-knowl > summary.knowl__link
// Also if hint/sol were missing from the static version, we want these removed here
const hintSols = body_div.querySelectorAll('.accordion.hint,.accordion.solution');
if (hintSols.length == 0) {return};
for (const hintSol of hintSols) {
const parent = hintSol.parentNode;
solutionlikewrapper = document.createElement('div');
solutionlikewrapper.classList.add('solutions');
parent.insertBefore(solutionlikewrapper, hintSol);
const hintSolType = hintSol.classList.contains('hint') ? 'hint' : 'solution';
if ((hintSolType == 'solution' && !b_ptx_has_solution) ||
(hintSolType == 'hint' && !b_ptx_has_hint))
continue;
const knowlDetails = hintSol.getElementsByTagName('details')[0];
knowlDetails.className = '';
knowlDetails.classList.add(hintSolType, 'solution-like', 'born-hidden-knowl');
solutionlikewrapper.appendChild(knowlDetails);
const knowlSummary = knowlDetails.getElementsByTagName('summary')[0];
knowlSummary.className = '';
knowlSummary.classList.add('knowl__link');
const summaryLabel = knowlSummary.children[0];
summaryLabel.remove();
const newLabelSpan = document.createElement('span');
newLabelSpan.innerHTML = hintSolType == 'hint' ? hint_label_text : solution_label_text;
knowlSummary.appendChild(newLabelSpan);
const newLabelPeriod = document.createElement('span');
newLabelPeriod.innerHTML = '.';
knowlSummary.appendChild(newLabelPeriod);
adjustSrcHrefs(knowlDetails, ww_domain);
hintSol.remove();
const originalDetailsContent = knowlDetails.getElementsByTagName('div')[0];
const newDetailsContent = originalDetailsContent.getElementsByTagName('div')[0];
newDetailsContent.className = '';
newDetailsContent.classList.add(hintSolType, 'solution-like', 'knowl__content');
knowlDetails.appendChild(newDetailsContent);
originalDetailsContent.remove();
}
}
function cloneAttributes(target, source) {
[...source.attributes].forEach( attr => { target.setAttribute(attr.nodeName ,attr.nodeValue) });
}
function webworkSeedHash(string) {
var hash = 0, i, chr;
if (string.length === 0) return hash;
for (i = 0; i < string.length; i++) {
chr = string.charCodeAt(i);
hash = ((hash << 5) - hash) + chr;
hash |= 0; //Convert to 32bit integer
}
return Math.abs(hash);
};
@@ -0,0 +1,686 @@
//Critical:
// TODO: Think about what should be done for an essay question.
//Accessibility:
//Enhancement:
// TODO: Have randomize check that new seed is actually producing new HTML.
// TODO: Don't offer randomize button unless we know a new version can be produced after trying a few seeds.
//Styling:
// TODO: Review all styling in all scenarios (staged/not, correct/partly-correct/incorrect/blank, single/multiple)
async function handleWW(ww_id, action) {
const ww_container = document.getElementById(ww_id);
const ww_domain = ww_container.dataset.domain;
const ww_processing = 'webwork2';
const ww_origin = ww_container.dataset.origin;
const ww_problemSource = ww_container.dataset.problemsource;
let ww_baseCourse = ww_container.dataset.documentid;
const ww_sourceFilePath = ww_container.dataset.sourcefilepath;
const ww_course_id = ww_container.dataset.courseid;
const ww_user_id = ww_container.dataset.userid;
const ww_passwd = ww_container.dataset.coursepassword;
const localize_submit = ww_container.dataset.localizeSubmit || "Submit";
const localize_check_responses = ww_container.dataset.localizeCheckResponses || "Check Responses";
const localize_reveal = ww_container.dataset.localizeReveal || "Reveal";
const localize_randomize = ww_container.dataset.localizeRandomize || "Randomize";
const localize_reset = ww_container.dataset.localizeReset || "Reset";
const runestone_logged_in = (typeof eBookConfig !== 'undefined' && eBookConfig.username !== '');
// will be null on pages generated prior to late December 2022
const activate_button = document.getElementById(ww_id + '-button')
// Set the current seed
if (!action) {
ww_container.dataset.current_seed = ww_container.dataset.seed;
if (runestone_logged_in) {
ww_container.dataset.current_seed = webworkSeedHash(eBookConfig.username + ww_container.dataset.current_seed);
}
}
else if (action == 'randomize') ww_container.dataset.current_seed = Number(ww_container.dataset.current_seed) + 100;
let loader = document.createElement('div');
loader.style.position = 'absolute';
loader.style.left = 0;
loader.style.top = 0;
loader.style.backgroundColor = 'rgba(0.2, 0.2, 0.2, 0.4)';
loader.style.color = 'white';
loader.style.width = '100%';
loader.style.height = '100%';
loader.style.display = 'flex';
loader.style.alignItems = 'center';
loader.style.justifyContent = 'center';
loader.style.marginTop = '0';
loader.tabIndex = -1;
const loaderText = document.createElement('span');
loaderText.textContent = 'Loading';
loaderText.style.fontSize = '2rem';
loader.appendChild(loaderText);
ww_container.appendChild(loader);
loader.focus();
if (!action) {
// Determine if static version shows hints, solutions, or answers and save that information in the container dataset for later runs.
ww_container.dataset.hasHint = ww_container.getElementsByClassName('hint').length > 0;
ww_container.dataset.hasSolution = ww_container.getElementsByClassName('solution').length > 0;
ww_container.dataset.hasAnswer = ww_container.getElementsByClassName('answer').length > 0;
// Get (possibly localized) label text for hints and solutions.
ww_container.dataset.hintLabelText = ww_container.dataset.hasHint == 'true'
? ww_container.querySelectorAll('.hint-knowl span.type, details.hint span.type')[0].textContent : 'Hint';
ww_container.dataset.solutionLabelText = ww_container.dataset.hasSolution == 'true'
? ww_container.querySelectorAll('.solution-knowl span.type, details.solution span.type')[0].textContent : 'Solution';
ww_container.tabIndex = -1;
}
let url;
if (ww_processing == 'webwork2') {
url = new URL(ww_domain + '/webwork2/render_rpc');
}
let formData = new FormData();
let generatedPG = 'generated/webwork/pg/';
// For Runestone, we specify where to find the generated problems.
if (runestone_logged_in) {
if (ww_problemSource && !ww_baseCourse) {
// WeBWorK authored in source, generated after 2025/10/15-ish, should have a baseCourse specified. We fall back to parsing the baseCourse from ww_id, which *should* always start with the base course prior to the first underscore (although this is not guaranteed). If no underscore is found, we fall back to eBookConfig.basecourse.
// This is only needed for webwork problems authored in source, not OPL problems, so we only check when ww_problemSource is set.
const parts = ww_id.split('_');
ww_baseCourse = parts.length > 1 ? parts[0] : eBookConfig.basecourse;
console.log("using the base course " + ww_baseCourse);
}
generatedPG = `/ns/books/published/${ww_baseCourse}/${generatedPG}`;
}
if (action == 'check' || action =='reveal') {
const iframe = ww_container.querySelector('.problem-iframe');
formData = new FormData(iframe.contentDocument.getElementById(ww_id + "-form"));
formData.set("answersSubmitted", '1');
formData.set('WWsubmit', "1");
if (action == 'reveal' && ww_container.dataset.hasAnswer == 'true') {
formData.set('WWcorrectAnsOnly', "1");
}
if (ww_origin == 'generated') {
const rawProblemSource = await fetch(generatedPG + ww_problemSource).then((r) => r.text());
formData.set("rawProblemSource", rawProblemSource);
}
else if (ww_origin == 'webwork2') formData.set("sourceFilePath", ww_sourceFilePath);
} else {
formData.set("problemSeed", ww_container.dataset.current_seed);
if (ww_origin == 'generated') {
const rawProblemSource = await fetch(generatedPG + ww_problemSource).then((r) => r.text());
formData.set("rawProblemSource", rawProblemSource);
}
else if (ww_origin == 'webwork2') formData.set("sourceFilePath", ww_sourceFilePath);
formData.set("answersSubmitted", '0');
formData.set("displayMode", "MathJax");
formData.set("courseID", ww_course_id);
formData.set("user", ww_user_id);
formData.set("userID", ww_user_id);
formData.set("passwd", ww_passwd);
formData.set("disableCookies", '1');
formData.set("outputformat", "raw");
// note ww_container.dataset.hasSolution is a string, possibly 'false' which is true
formData.set("showSolutions", ww_container.dataset.hasSolution == 'true' ? '1' : '0');
formData.set("showHints", ww_container.dataset.hasHint == 'true' ? '1' : '0');
formData.set("problemUUID",ww_id);
}
// If in Runestone, check if there are previous answer submissions and get them now to use in the form.
let checkboxesString = '';
if (runestone_logged_in && !action) {
const answersObject = (wwList[ww_id.replace(/-ww-rs$/,'')].answers ? wwList[ww_id.replace(/-ww-rs$/,'')].answers : {'answers' : [], 'mqAnswers' : []});
const previousAnswers = answersObject.answers;
if (previousAnswers !== null) {
formData.set('WWsubmit', 1);
}
for (const answer in previousAnswers) {
if (previousAnswers[answer].constructor === Array) {
for (const k in previousAnswers[answer]) {
checkboxesString += '&';
checkboxesString += answer;
checkboxesString += '=';
checkboxesString += previousAnswers[answer][k];
}
} else {
formData.set(answer, previousAnswers[answer]);
}
}
}
// Need to get form data as a string, including possible repeated checkbox names
// Do not pass post data as an object, or checkbox names will overwrite one another
const formString = new URLSearchParams(formData).toString();
$.post(url, formString + checkboxesString, (data) => {
// Create the form that will contain the text and input fields of the interactive problem.
const form = document.createElement("form");
form.id = ww_id + "-form";
form.dataset.iframeHeight = 1;
// Create a div for the problem text.
const body_div = document.createElement("div");
body_div.id = ww_id + "-body";
body_div.classList.add("exercise", "exercise-like");
body_div.lang = data.lang;
body_div.dir = data.dir;
// Dump the problem text, answer blanks, etc.
body_div.innerHTML = data.rh_result.text;
// If showPartialCorrectAnswers = 0, alter the feedback buttons according to whether or not the score is 100%.
if ('showPartialCorrectAnswers' in data.rh_result.flags && data.rh_result.flags.showPartialCorrectAnswers == 0) {
if ('score' in data.rh_result.problem_result && data.rh_result.problem_result.score >= 1) {
body_div.querySelectorAll('button.ww-feedback-btn').forEach(
function(button) {
button.classList.remove('btn-info');
button.classList.add('btn-success');
button.setAttribute('aria-label', 'Correct');
button.dataset.bsCustomClass = button.dataset.bsCustomClass + ' correct';
button.dataset.bsTitle = button.dataset.bsTitle.replace('Answer Preview', 'Correct');
button.firstChild.classList.add('correct')
}
);
} else {
body_div.querySelectorAll('button.ww-feedback-btn').forEach(
function(button) {
button.setAttribute('aria-label', 'One or more answers incorrect');
button.dataset.bsTitle = button.dataset.bsTitle.replace('Answer Preview', 'One or more answers incorrect');
}
);
}
}
// Replace all hn headings with h6 headings.
for (const tag_name of ['h6', 'h5', 'h4', 'h3', 'h2', 'h1']) {
const headings = body_div.getElementsByTagName(tag_name);
for (heading of headings) {
const new_heading = document.createElement("h6");
new_heading.innerHTML = heading.innerHTML;
cloneAttributes(new_heading, heading);
new_heading.classList.add('webwork-part');
heading.replaceWith(new_heading);
}
}
// Hide textarea input and hide associated buttons
var textareas = body_div.getElementsByTagName("textarea");
for(var i = 0, max = textareas.length; i < max; i++)
{
textareas[i].style.display = "none";
textareas[i].className = '';
}
var textareabuttons = body_div.querySelectorAll(".latexentry-preview");
for(var i = 0, max = textareabuttons.length; i < max; i++)
{
textareabuttons[i].remove();
}
adjustSrcHrefs(body_div, ww_domain);
translateHintSol(ww_id, body_div, ww_domain,
ww_container.dataset.hasHint == 'true', ww_container.dataset.hasSolution == 'true',
ww_container.dataset.hintLabelText, ww_container.dataset.solutionLabelText)
// insert previous answers
if (runestone_logged_in) {
const answersObject = (wwList[ww_id.replace(/-ww-rs$/,'')].answers ? wwList[ww_id.replace(/-ww-rs$/,'')].answers : {'answers' : [], 'mqAnswers' : []});
const mqAnswers = answersObject.mqAnswers;
for (const mqAnswer in mqAnswers) {
const mqInput = body_div.querySelector('input[id=' + mqAnswer + ']');
if (mqInput && mqInput.value == '') {
mqInput.setAttribute('value', mqAnswers[mqAnswer]);
}
}
const answers = answersObject.answers;
for (const answer in answers) {
const input = body_div.querySelector('input[id=' + answer + ']');
if (input && input.value == '') {
input.setAttribute('value', answers[answer]);
}
}
}
// insert our cleaned up problem text
form.appendChild(body_div);
// Set up hidden input fields that the form uses
const wwInputs = {
problemSeed: data.inputs_ref.problemSeed,
problemUUID: data.inputs_ref.problemUUID,
psvn: data.inputs_ref.psvn,
courseName: ww_course_id,
courseID: ww_course_id,
user: ww_user_id,
passwd: ww_passwd,
displayMode: "MathJax",
session_key: data.rh_result.session_key,
outputformat: "raw",
language: data.formLanguage,
showSummary: data.showSummary,
disableCookies: '1',
// note ww_container.dataset.hasSolution is a string, possibly 'false' which is true
showSolutions: ww_container.dataset.hasSolution == 'true' ? '1' : '0',
showHints: ww_container.dataset.hasHint == 'true' ? '1' : '0',
forcePortNumber: data.forcePortNumber
};
if (ww_sourceFilePath) wwInputs.sourceFilePath = ww_sourceFilePath;
else if (ww_problemSource && ww_origin == 'webwork2') wwInputs.problemSource = ww_problemSource;
for (const wwInputName of Object.keys(wwInputs)) {
const input = document.createElement('input');
input.type = 'hidden';
input.name = wwInputName;
input.value = wwInputs[wwInputName];
form.appendChild(input);
}
let buttonContainer = ww_container.querySelector('.problem-buttons.webwork');
// Create the submission buttons if they have not yet been created.
if (!buttonContainer) {
// Hide the original div that contains the Activate.
ww_container.querySelector('.problem-buttons').classList.add('hidden-content', 'hidden');
// Create a new div for the webwork buttons.
buttonContainer = document.createElement('div');
buttonContainer.classList.add('problem-buttons', 'webwork');
if (activate_button != null) {
// Make sure the button container follows the activate button in the DOM
activate_button.after(buttonContainer);
} else {
ww_container.prepend(buttonContainer);
}
// Check button
const check = document.createElement("button");
check.type = "button";
check.id = ww_id + '-check';
check.style.marginRight = "0.25rem";
check.classList.add('webwork-button');
check.textContent = runestone_logged_in ? localize_submit : localize_check_responses;
check.addEventListener('click', () => handleWW(ww_id, "check"));
buttonContainer.appendChild(check);
// Show correct answers button if original PTX has answer knowl.
if (ww_container.dataset.hasAnswer == 'true') {
const correct = document.createElement("button");
correct.classList.add("show-correct", 'webwork-button');
correct.type = "button";
correct.style.marginRight = "0.25rem";
correct.textContent = localize_reveal;
correct.addEventListener('click', () => handleWW(ww_id, 'reveal'));
buttonContainer.appendChild(correct);
}
// randomize button
const randomize = document.createElement("button")
randomize.type = "button";
randomize.classList.add('webwork-button');
randomize.style.marginRight = "0.25rem";
randomize.textContent = localize_randomize;
randomize.addEventListener('click', () => handleWW(ww_id, 'randomize'));
buttonContainer.appendChild(randomize)
// reset button
const reset = document.createElement("button")
reset.type = "button"
reset.classList.add('webwork-button');
reset.textContent = localize_reset;
reset.addEventListener('click', () => resetWW(ww_id));
buttonContainer.appendChild(reset)
}
if (runestone_logged_in && action == 'check') {
// Runestone trigger
$("body").trigger('runestone_ww_check', data)
}
let courseUrlBase = '';
if (runestone_logged_in){
courseUrlBase = '/ns/books/published/' + eBookConfig.basecourse + '/';
}
let iframeContents = '<!DOCTYPE html><head>' +
'<script src="' + ww_domain + '/webwork2_files/node_modules/jquery/dist/jquery.min.js"></script>' +
`<script>
window.MathJax = {
tex: { packages: { '[+]': ['noerrors'] } },
loader: { load: ['input/asciimath', '[tex]/noerrors'] },
startup: {
ready() {
const AM = MathJax.InputJax.AsciiMath.AM;
// Modify existing AsciiMath triggers.
AM.symbols[AM.names.indexOf('**')] = {
input: '**',
tag: 'msup',
output: '^',
tex: null,
ttype: AM.TOKEN.INFIX
};
const i = AM.names.indexOf('infty');
AM.names[i] = 'infinity';
AM.symbols[i] = { input: 'infinity', tag: 'mo', output: '\u221E', tex: 'infty', ttype: AM.TOKEN.CONST };
return MathJax.startup.defaultReady();
}
},
options: {
processHtmlClass: "process-math",
renderActions: {
findScript: [
10,
(doc) => {
for (const node of document.querySelectorAll('script[type^="math/tex"]')) {
const math = new doc.options.MathItem(
node.textContent,
doc.inputJax[0],
!!node.type.match(/; *mode=display/)
);
const text = document.createTextNode('');
node.parentNode.replaceChild(text, node);
math.start = { node: text, delim: '', n: 0 };
math.end = { node: text, delim: '', n: 0 };
doc.math.push(math);
}
},
''
]
},
ignoreHtmlClass: 'tex2jax_ignore'
}
};
</script>` +
'<script src="' + ww_domain + '/webwork2_files/node_modules/mathjax/es5/tex-chtml.js" id="MathJax-script" defer></script>' +
`<script src="${courseUrlBase}_static/pretext/js/lib/knowl.js" defer></script>` +
'<link rel="stylesheet" href="' + ww_domain + '/webwork2_files/node_modules/bootstrap/dist/css/bootstrap.min.css"/>' +
'<script src="' + ww_domain + '/webwork2_files/node_modules/bootstrap/dist/js/bootstrap.bundle.min.js" defer></script>';
// Determine javascript and css dependencies
const extra_css_files = [];
const extra_js_files = [];
if (data.extra_css_files) data.extra_css_files.unshift(...extra_css_files);
else data.extra_css_files = extra_css_files;
for (const cssFile of data.extra_css_files) {
iframeContents += '<link rel="stylesheet" href="' + (cssFile.external !== '1' ? ww_domain : '') + cssFile.file + '"/>';
}
if (data.extra_js_files) data.extra_js_files.unshift(...extra_js_files);
else data.extra_js_files = extra_js_files;
for (const jsFile of data.extra_js_files) {
iframeContents += '<script src="' + (jsFile.external !== '1' ? ww_domain : '') + jsFile.file + '" ' +
Object.keys(jsFile.attributes || {}).reduce((ret, key) => {
ret += key + '="' + jsFile.attributes[key] + '" '; return ret;
}, "") + '></script>';
}
iframeContents +=
`<link rel="stylesheet" href="${courseUrlBase}_static/pretext/css/theme.css"/>` +
'<script src="' + ww_domain + '/webwork2_files/node_modules/iframe-resizer/js/iframeResizer.contentWindow.min.js"></script>' +
`<style>
html { overflow-y: hidden; }
html body { background:unset; margin: 0; }
body { font-size: initial; line-height: initial; padding:2px; }
.hidden-content { display: none; }
span.nobreak { white-space: nowrap; }
div.PGML img.image-view-elt { max-width:100%; }
.graphtool-answer-container .graphtool-graph { margin: 0; width: 300px; height: 300px; }
.graphtool-answer-container .graphtool-number-line { height: 57px; }
.quill-toolbar { scrollbar-width: thin; overflow-x: hidden; }
</style>` +
'</head><body>' +
'<main class="pretext-content problem-content" data-iframe-height="1">' + form.outerHTML + '</main></body>' +
'</html>';
let iframe;
// If there is no action this is the initialization call.
if (!action) {
// Create the iframe.
iframe = document.createElement('iframe');
iframe.style.width = '1px';
iframe.style.minWidth = '100%';
iframe.classList.add('problem-iframe');
// Hide the static problem
ww_container.querySelector('.problem-contents').classList.add('hidden-content', 'hidden');
if (activate_button != null) {
// Make sure the iframe follows the activate button in the DOM
activate_button.after(iframe);
} else {
ww_container.prepend(iframe);
}
iFrameResize({ checkOrigin: false, scrolling: 'omit', heightCalculationMethod: 'taggedElement' }, iframe);
iframe.addEventListener('load', () => {
// Set up form submission from inside the iframe.
const iframeForm = iframe.contentDocument.getElementById(ww_id + '-form');
iframeForm.addEventListener('submit', (e) => {
handleWW(ww_id, "check");
e.preventDefault();
});
iframe.contentDocument.querySelectorAll('.collapse.in').forEach(collapse => collapse.classList.add('expanded'));
iframe.contentWindow.jQuery('.collapse').on('shown', function(e) { if (e.target != this) return; this.classList.add('expanded'); });
iframe.contentWindow.jQuery('.collapse').on('hide', function(e) { if (e.target != this) return; this.classList.remove('expanded'); });
iframe.contentWindow.MathJax.startup.promise.then(() => iframe.contentWindow.MathJax.typesetPromise(['.popover', '.popover-content']));
});
} else {
iframe = ww_container.querySelector('.problem-iframe');
}
iframe.srcdoc = iframeContents;
iframe.addEventListener('load', () => {
// Remove the loader overlay
loader.remove();
}, { once: true })
// Place focus on the problem.
ww_container.focus()
}, "json");
}
function WWshowCorrect(ww_id, answers) {
const ww_container = document.getElementById(ww_id);
const iframe = ww_container.querySelector('.problem-iframe');
const body = iframe.contentDocument.getElementById(ww_id + '-body')
$("body").trigger("runestone_show_correct", {
"ww_id": ww_id,
"answers": answers
});
let inputs = body.querySelectorAll("input:not([type=hidden])");
for (const input of inputs) {
const name = input.name;
const span_id = `${ww_id}-${name}-correct`;
if (input.type == 'text' && answers[name] && !(iframe.contentDocument.getElementById(span_id))) {
const input_id = input.id;
const mq_span = iframe.contentDocument.getElementById(`mq-answer-${input_id}`);
if (mq_span) {
mq_span.style.display = 'none';
}
label = iframe.contentDocument.getElementById(`${span_id}-label`);
if (label) {
label.parentElement.insertBefore(input, label);
label.remove();
}
input.type = "hidden";
// we need to convert things like &lt; in answers to <
const correct_ans_text = iframe.contentDocument.createElement('div');
correct_ans_text.innerHTML = answers[name].correct_ans;
input.value = correct_ans_text.textContent;
const show_span = iframe.contentDocument.createElement('span');
show_span.id = span_id;
show_span.appendChild(answers[name].correct_ans_latex_string
? iframe.contentDocument.createTextNode('\\(' + answers[name].correct_ans_latex_string + '\\)')
: iframe.contentDocument.createTextNode(answers[name].correct_ans));
input.parentElement.insertBefore(show_span, input);
}
if (input.type.toUpperCase() == 'RADIO' && answers[name]) {
const correct_ans = answers[name].correct_choice || answers[name].correct_ans;
if (input.value == correct_ans) {
input.checked = true;
} else {
input.checked = false;
}
}
if (input.type.toUpperCase() == 'CHECKBOX' && answers[name]) {
const correct_choices = answers[name].correct_choices;
if (correct_choices.includes(input.value)) {
input.checked = true;
} else {
input.checked = false;
}
}
}
const hiddenInputs = body.querySelectorAll("input[type=hidden]");
for (const input of hiddenInputs) {
const name = input.name;
if (!input.nextElementSibling) continue;
const graphtoolContainer = input.nextElementSibling.nextElementSibling;
if (graphtoolContainer && answers[name] && graphtoolContainer.classList.contains('graphtool-container')) {
const correct_ans_div = iframe.contentDocument.createElement('div');
input.parentElement.insertBefore(correct_ans_div, graphtoolContainer);
graphtoolContainer.style.display = 'none';
input.value = answers[name].correct_ans;
iframe.contentWindow.jQuery(correct_ans_div).html(answers[name].correct_ans_latex_string);
const script = iframe.contentDocument.createElement('script');
script.textContent = correct_ans_div.querySelector('script').textContent
.replace('\nwindow.addEventListener("DOMContentLoaded",', '(')
.replace(/;\n$/, '();');
iframe.contentDocument.body.appendChild(script);
}
}
let selects = body.querySelectorAll("select:not([type=hidden])");
for (const select of selects) {
const name = select.name;
const span_id = `${ww_id}-${name}-correct`;
if (answers[name] && !iframe.contentDocument.getElementById(span_id)) {
select.style.display = "none";
select.value = answers[name].correct_ans;
const show_span = iframe.contentDocument.createElement('span');
show_span.id = span_id;
show_span.appendChild(answers[name].correct_ans_latex_string
? iframe.contentDocument.createTextNode('\\(' + answers[name].correct_ans_latex_string + '\\)')
: iframe.contentDocument.createTextNode(answers[name].correct_ans));
select.parentElement.insertBefore(show_span, select);
}
}
// run MathJax on our new rendering
// FIXME: We only need to typeset the added elements, not the entire body.
const mathjaxTypesetScript = iframe.contentDocument.createElement('script');
mathjaxTypesetScript.textContent = 'MathJax.startup.promise.then(() => MathJax.typesetPromise([document.body]));';
iframe.contentDocument.body.appendChild(mathjaxTypesetScript);
}
function resetWW(ww_id) {
const ww_container = document.getElementById(ww_id);
const activate_button = document.getElementById(ww_id + '-button');
ww_container.dataset.current_seed = ww_container.dataset.seed;
iframe = ww_container.querySelector('.problem-iframe');
iframe.remove();
ww_container.querySelector('.problem-contents').classList.remove('hidden-content', 'hidden');
ww_container.querySelector('.problem-buttons.webwork').remove();
ww_container.querySelector('.problem-buttons').classList.remove('hidden-content', 'hidden');
// if the newer activate button is there (but hidden) bring it back too
if (activate_button != null) {activate_button.classList.remove('hidden-content', 'hidden');};
}
function adjustSrcHrefs(container,ww_domain) {
container.querySelectorAll('[href]').forEach((node) => {
const href = node.attributes.href.value;
if (href !== '#' && !href.match(/^[a-z]+:\/\//i)) node.href = ww_domain + '/' + href;
});
container.querySelectorAll('[data-knowl-url]').forEach((node) => {
const dku = node.dataset.knowlUrl;
if (dku !== '#' && !dku.match(/^[a-z]+:\/\//i)) node.dataset.knowlUrl = ww_domain + dku;
});
container.querySelectorAll('[src]').forEach((node) => {
node.src = new URL(node.attributes.src.value, ww_domain).href;
});
}
function translateHintSol(ww_id, body_div, ww_domain, b_ptx_has_hint, b_ptx_has_solution, hint_label_text, solution_label_text) {
// The problem text may come with "hint"s and "solution"s
// Each one is a div.accordion > details.accordion-item > summary.accordion-button
// Styling is not the PTX way, so we change it to one div.solutions
// with (potentially multiple) details.born-hidden-knowl > summary.knowl__link
// Also if hint/sol were missing from the static version, we want these removed here
const hintSols = body_div.querySelectorAll('.accordion.hint,.accordion.solution');
if (hintSols.length == 0) {return};
for (const hintSol of hintSols) {
const parent = hintSol.parentNode;
solutionlikewrapper = document.createElement('div');
solutionlikewrapper.classList.add('solutions');
parent.insertBefore(solutionlikewrapper, hintSol);
const hintSolType = hintSol.classList.contains('hint') ? 'hint' : 'solution';
if ((hintSolType == 'solution' && !b_ptx_has_solution) ||
(hintSolType == 'hint' && !b_ptx_has_hint))
continue;
const knowlDetails = hintSol.getElementsByTagName('details')[0];
knowlDetails.className = '';
knowlDetails.classList.add(hintSolType, 'solution-like', 'born-hidden-knowl');
solutionlikewrapper.appendChild(knowlDetails);
const knowlSummary = knowlDetails.getElementsByTagName('summary')[0];
knowlSummary.className = '';
knowlSummary.classList.add('knowl__link');
const summaryLabel = knowlSummary.children[0];
summaryLabel.remove();
const newLabelSpan = document.createElement('span');
newLabelSpan.innerHTML = hintSolType == 'hint' ? hint_label_text : solution_label_text;
knowlSummary.appendChild(newLabelSpan);
const newLabelPeriod = document.createElement('span');
newLabelPeriod.innerHTML = '.';
knowlSummary.appendChild(newLabelPeriod);
adjustSrcHrefs(knowlDetails, ww_domain);
hintSol.remove();
const originalDetailsContent = knowlDetails.getElementsByTagName('div')[0];
const newDetailsContent = originalDetailsContent.getElementsByTagName('div')[0];
newDetailsContent.className = '';
newDetailsContent.classList.add(hintSolType, 'solution-like', 'knowl__content');
knowlDetails.appendChild(newDetailsContent);
originalDetailsContent.remove();
}
}
function cloneAttributes(target, source) {
[...source.attributes].forEach( attr => { target.setAttribute(attr.nodeName ,attr.nodeValue) });
}
function webworkSeedHash(string) {
var hash = 0, i, chr;
if (string.length === 0) return hash;
for (i = 0; i < string.length; i++) {
chr = string.charCodeAt(i);
hash = ((hash << 5) - hash) + chr;
hash |= 0; //Convert to 32bit integer
}
return Math.abs(hash);
};
+257
View File
@@ -0,0 +1,257 @@
/*******************************************************************************
* pretext.js
*******************************************************************************
* The main front-end controller for PreTeXt documents.
*
* Homepage: pretextbook.org
* Repository: https://github.com/PreTeXtBook/JS_core
*
* Authors: David Farmer, Rob Beezer
*
*******************************************************************************
*/
// from https://stackoverflow.com/questions/34422189/get-item-offset-from-the-top-of-page
function getOffsetTop(e) {
// recursively walk up the DOM via offsetParent, accumulating offsetTop as we go
if (!e) return 0;
return getOffsetTop(e.offsetParent) + e.offsetTop;
};
function scrollTocToActive() {
//Try to figure out current TocItem from URL
let fileNameWHash = window.location.href.split("/").pop();
let fileName = fileNameWHash.split("#")[0];
//Find just the filename in ToC
let tocEntry = document.querySelector('#ptx-toc a[href="' + fileName + '"]');
if (!tocEntry) {
return; //complete failure, get out
}
let tocEntryTop = 0;
//See if we can also match fileName#hash (assuming there is a fragment)
if (fileNameWHash.includes('#')) {
let tocEntryWHash = document.querySelector(
'#ptx-toc a[href="' + fileNameWHash + '"]'
);
if (tocEntryWHash) {
//Matched something below a subsection - activate the list item that contains it
tocEntry.closest("li").querySelectorAll("li").forEach(li => {
li.classList.remove("active");
});
tocEntryWHash.closest("li").classList.add("active");
tocEntryTop = getOffsetTop(tocEntryWHash);
}
}
if (!tocEntryTop) {
tocEntryTop = getOffsetTop(tocEntry);
}
//Now activate ToC item for fileName and scroll to it
// Don't use scrollIntoView because it changes users tab position in Chrome
// and messes up keyboard navigation
tocEntry.closest("li").classList.add("active");
// Scroll only if the tocEntry is below the bottom half of the window,
// scrolling to that position.
let toc = document.querySelector("#ptx-toc");
let tocTop = getOffsetTop(toc);
toc.scrollTop = tocEntryTop - tocTop - 0.4 * self.innerHeight;
}
function toggletoc() {
thesidebar = document.getElementById("ptx-sidebar");
if (thesidebar.classList.contains("hidden") || thesidebar.classList.contains("visible")) {
thesidebar.classList.toggle("hidden");
thesidebar.classList.toggle("visible");
} else if (thesidebar.offsetParent === null) { /* not currently visible */
thesidebar.classList.toggle("visible");
} else {
thesidebar.classList.toggle("hidden");
}
scrollTocToActive();
}
function samePageLink(a) {
if (!(a instanceof HTMLAnchorElement)) return false;
try {
const linkUrl = new URL(a.href, document.baseURI);
const currentUrl = new URL(window.location.href);
const sameDocument =
linkUrl.origin === currentUrl.origin &&
linkUrl.pathname === currentUrl.pathname &&
linkUrl.search === currentUrl.search;
return sameDocument && !!linkUrl.hash;
} catch (e) {
// Invalid URL
return false;
}
}
window.addEventListener("DOMContentLoaded",function(event) {
thetocbutton = document.getElementsByClassName("toc-toggle")[0];
thetocbutton.addEventListener("click", (e) => {
toggletoc();
e.stopPropagation(); // keep global click handler from immediately toggling it back
});
// For themes that want it, install click handlers to auto close the toc
// when the reader clicks anywhere outside it or selects a subsection.
// (Selecting other sections or chapters navigates away from the page so
// effectively closes the TOC.)
if (getComputedStyle(document.documentElement).getPropertyValue('--auto-collapse-toc') == "yes") {
const sidebar = document.getElementById("ptx-sidebar");
// Handle all clicks outside the sidebar
window.addEventListener("click", function(event) {
if (sidebar.classList.contains("visible")) {
if (!event.composedPath().includes(sidebar)) {
toggletoc();
}
}
});
// Handle clicks inside the sidebar but on link within a subsection.
sidebar.addEventListener("click", function (event) {
if (samePageLink(event.target.closest('a'))) {
toggletoc();
}
});
// Handle persistent sidebar if the page is restored from cache on back/forward buttons.
window.addEventListener('pageshow', (e) => {
if (e.persisted) {
sidebar.classList.remove('visible');
sidebar.classList.add('hidden');
}
});
}
});
/* jump to next page if reader tries to scroll past the bottom */
// var hitbottom = false;
// window.onscroll = function(ev) {
// if ((window.innerHeight + window.scrollY) >= document.body.scrollHeight) {
// // you're at the bottom of the page
// console.log("Bottom of page");
// if (hitbottom) {
// console.log("hit bottom again");
// thenextbutton = document.getElementsByClassName("next-button")[0];
// thenextbutton.click();
// } else {
// hitbottom = true;
// /* only jump to next page if hard scroll in quick succession */
// window.scrollBy(0, -20);
// setTimeout(function (){ hitbottom = false }, 1000);
// }
// }
// };
//-----------------------------------------------------------------------------
// Dynamic TOC logic
//-----------------------------------------------------------------------------
//item is assumed to be expander in toc-item
function toggleTOCItem(expander) {
let listItem = expander.closest(".toc-item");
listItem.classList.toggle("expanded");
let expanded = listItem.classList.contains("expanded");
let itemType = getTOCItemType(listItem);
if(expanded) {
expander.title = "Close" + (itemType !== "" ? " " + itemType : "");
} else {
expander.title = "Expand" + (itemType !== "" ? " " + itemType : "");
}
//should be one of each... for/of for safety and built in null avoidance
for (const childUL of listItem.querySelectorAll(":scope > ul.toc-item-list")) {
for (const childItem of childUL.querySelectorAll(":scope > li.toc-item")) {
if(expanded) {
childItem.classList.add("visible");
childItem.classList.remove("hidden");
} else {
childItem.classList.remove("visible");
childItem.classList.add("hidden");
}
}
}
}
//finds item type from classes or empty string on failure
function getTOCItemType(item) {
//Type should be class that looks like toc-X where X is not item. Find it and return X
for(let className of item.classList) {
if(className !== "toc-item" && className.length > 3 && className.slice(0,4) === "toc-")
return className.slice(4);
}
return "";
}
//finds depth of toc-item as defined by number .toc-item-lists it is in
function getTOCItemDepth(item) {
let depth = 0;
let curParent = item.closest(".toc-item-list");
while(curParent !== null) {
depth++;
curParent = curParent.parentElement.closest(".toc-item-list");
}
return depth;
}
window.addEventListener("DOMContentLoaded", function(event) {
if(document.querySelector(".ptx-toc.focused") === null)
return; //only in focused mode
let maxDepth = 1000; //how deep TOC goes
//check toc for depth class and get value from there
for(let className of document.querySelector(".ptx-toc").classList)
if(className.length > 5 && className.slice(0,5) === "depth")
maxDepth = Number(className.slice(5));
let preexpandedLevels = 1; //how many levels to preexpand
let tocDataSet = document.querySelector(".ptx-toc").dataset;
if(typeof tocDataSet.preexpandedLevels !== 'undefined')
preexpandedLevels = Number(tocDataSet.preexpandedLevels);
let tocItems = document.querySelectorAll(".ptx-toc ul.structural > .toc-item");
for (const tocItem of tocItems) {
let hasChildren = tocItem.querySelector('ul.structural') !== null;
let depth = getTOCItemDepth(tocItem);
if(hasChildren && depth < maxDepth) {
let expander = document.createElement("button");
expander.classList.add('toc-expander');
expander.classList.add('toc-chevron-surround');
expander.title = 'toc-expander';
// content of span is set by CSS :before rule.
expander.innerHTML = '<span class="icon material-symbols-outlined" aria-hidden="true"></span>';
tocItem.querySelector(".toc-title-box").append(expander);
expander.addEventListener('click', () => {
toggleTOCItem(expander);
});
let isActive = tocItem.classList.contains("contains-active") || tocItem.classList.contains("active");
let preExpanded = isActive || depth < preexpandedLevels;
let itemType = getTOCItemType(tocItem);
if(preExpanded) {
toggleTOCItem(expander);
} else {
expander.title = "Expand" + (itemType !== "" ? " " + itemType : "");
}
}
}
});
// This needs to be after the TOC's geometry is settled
window.addEventListener("DOMContentLoaded",function(event) {
scrollTocToActive();
});
window.onhashchange = scrollTocToActive;
File diff suppressed because it is too large Load Diff
+315
View File
@@ -0,0 +1,315 @@
// next comment is out of date: there are more search options
// from lunr-pretext-search-index.js we will have either
// var ptx_lunr_search_style = "default";
// or
// var ptx_lunr_search_style = "reference";
// since there is only one search box now, this can be simplified
function doSearch(searchlocation="A") {
// Get the search terms from the input text box
var terms;
if(searchlocation == "A") {
terms = document.getElementById("ptxsearch").value;
} else {
terms = document.getElementById("ptxsearchB").value;
}
localStorage.setItem('last-search-terms', JSON.stringify({terms: terms, time: Date.now()}));
// Where do we want to put the results?
let resultArea = document.getElementById("searchresults")
resultArea.innerHTML = ""; // clear out any previous results
// assume AND for multiple words
var searchterms = terms;
if(searchlocation == "B") {
document.getElementById("ptxsearch").value = searchterms
console.log("ptxsearch value", document.getElementById("ptxsearch").value);
} else {
searchterms = terms;
}
searchterms = searchterms.toLowerCase().trim();
let pageResult = [];
if(searchterms != "") {
pageResult = ptx_lunr_idx.query((q) => {
for(let term of searchterms.split(' ')) {
q.term(term, { fields: ["title"], boost: 20 }); //exact title match with 20x weight
q.term(term, { wildcard: lunr.Query.wildcard.TRAILING, fields: ["title"], boost: 10 }); //inexact title 10x weight
q.term(term, { fields: ["body"], boost: 5 }); //exact body 5x weight
q.term(term, { wildcard: lunr.Query.wildcard.TRAILING, fields: ["body"] }); //inexact body
}
});
}
// Number the documents from first to last so we can order the results by their
// position in the book.
snum = 0;
for (let doc of ptx_lunr_docs) {
doc.snum = snum;
snum += 1;
}
//Limit to a sane number of results - otherwise search like 'e' matches every page
const MAX_RESULTS = 100;
let numUnshown = (pageResult.length > MAX_RESULTS) ? pageResult.length - MAX_RESULTS : 0;
pageResult.slice(0, MAX_RESULTS);
// Transfer meta data from the document to the results to make it easy to add
// our lists later.
augmentResults(pageResult, ptx_lunr_docs);
pageResult.sort(comparePosition);
addResultToPage(terms, pageResult, ptx_lunr_docs, numUnshown, resultArea);
MathJax.typeset();
}
// Find the entry for a search result in the original document index
function findEntry(resultId, db) {
for (const page of db) {
if (page.id === resultId) {
return page;
}
}
return resultId;
}
function augmentResults(result, docs) {
for (let res of result) {
let info = findEntry(res.ref, docs);
res.number = info.number;
res.type = info.type;
res.title = info.title;
res.url = info.url;
res.level = info.level;
res.snum = info.snum;
res.score = parseFloat(res.score);
//extra score multiplier based on level - prioritize sections over subsections/exercises/etc...
const LEVEL_WEIGHTS = [3, 2, 1.5]
if( res.level < 2 )
res.score *= LEVEL_WEIGHTS[res.level];
res.body = '';
//Add body snippets and highlights
const REVEAL_WINDOW = 30;
let titleMarked = false;
for (const hit in res.matchData.metadata) {
if(res.matchData.metadata[hit].title) {
//only show one match in title as locations change after first markup
if(!titleMarked) {
if(!res.matchData.metadata[hit].title.position)
continue;
let positionData = res.matchData.metadata[hit].title.position[0];
const startClipInd = positionData[0];
const endClipInd = positionData[0] + positionData[1];
res.title = res.title.substring(0, endClipInd) + '</span>' + res.title.substring(endClipInd);
res.title = res.title.substring(0, startClipInd) + '<span class="search-result-clip-highlight">' + res.title.substring(startClipInd);
titleMarked = true;
}
} else if (res.matchData.metadata[hit].body) {
if(!res.matchData.metadata[hit].body.position)
continue;
const bodyContent = info.body;
let positionData = res.matchData.metadata[hit].body.position[0];
const startInd = positionData[0] - REVEAL_WINDOW;
const endInd = positionData[0] + positionData[1] + REVEAL_WINDOW;
const startClipInd = positionData[0];
const endClipInd = positionData[0] + positionData[1];
let resultSnippet = (startInd > 0 ? '...' : '' ) + bodyContent.substring(startInd, startClipInd);
resultSnippet += '<span class="search-result-clip-highlight">' + bodyContent.substring(startClipInd, endClipInd) + '</span>';
resultSnippet += bodyContent.substring(endClipInd, endInd) + (endInd < bodyContent.length ? '...' : '' ) + '<br/>';
res.body += resultSnippet;
}
}
}
}
function rearrangedArray(arry) {
// return a new array which is arry (with depth) sorted according to meas,
// again as an array with depth.
// "with depth'' means that large children drag along their parents.
let newarry = [];
let startind = 0;
let numtograb = 0;
let ct = 1;
while (arry.length > 0 && ct < 500) {
++ct; // just in case something goes wrong
const locofmax = maxLocation(arry)
let segmentstart = locofmax;
let segmentlength = 1;
while (arry[segmentstart].level == "2") {
--segmentstart
}
while (segmentstart + segmentlength < arry.length && arry[segmentstart + segmentlength].level == "2") {
++segmentlength
}
// console.log("locofmax", locofmax, "starting", segmentstart, "going", segmentlength, "from", arry.length);
newarry.push(...arry.splice(segmentstart,segmentlength));
}
// console.log("newarry", newarry);
return newarry
}
function maxLocation(arry) {
let maxloc = 0;
let maxvalsofar = -1;
for (let index = 0; index < arry.length; ++index) {
if (arry[index].score > maxvalsofar) {
maxloc = index;
maxvalsofar = arry[index].score
}
}
return maxloc
}
function comparePosition(a, b) {
if (a.snum < b.snum) {
return -1;
}
if (a.snum > b.snum) {
return 1;
}
return 0;
}
function compareScoreDesc(a, b) {
if (a.score < b.score) {
return 1;
}
if (a.score > b.score) {
return -1;
}
return 0;
}
function addResultToPage(searchterms, result, docs, numUnshown, resultArea) {
/* backward compatibility for old html */
if (document.getElementById("searchempty")) {
document.getElementById("searchempty").style.display = "none";
}
let len = result.length;
console.log("first result", result[0]);
if (len == 0) {
if (document.getElementById("searchempty")) {
document.getElementById("searchempty").style.display = "block";
} else {
let noresults = document.createElement("div");
noresults.classList.add("noresults");
search_no_results_string = "No results were found"
noresults.innerHTML = search_no_results_string + ".";
// console.log("the new variable", search_results_heading_string);
resultArea.appendChild(noresults);
}
document.getElementById("searchresultsplaceholder").style.display = null;
return
}
// console.log("result",result);
let allScores = result.map(function (r) { return r.score });
// console.log(typeof allScores[0], "allScores",allScores);
allScores.sort((a,b) => (a - b));
allScores.reverse();
// console.log("allScores, sorted",allScores);
// let high = result[Math.floor(len*0.25)].score;
// let med = result[Math.floor(len*0.5)].score;
// let low = result[Math.floor(len*0.75)].score;
// sort the results by their position in the book, not their score
let high = allScores[Math.floor(len*0.20)];
let med = allScores[Math.floor(len*0.40)];
let low = allScores[Math.floor(len*0.75)];
if (ptx_lunr_search_style == "reference") {
result = rearrangedArray(result);
}
let indent = "1";
let currIndent = indent;
let origResult = resultArea;
// Create list entries indenting as needed.
for (const res of result) {
let link = document.createElement("a")
// add a class so we can colorize the results based on their rank in terms
// of search score.
if (res.score >= high) {
link.classList.add("high_result")
} else if (res.score >= med) {
link.classList.add("medium_result")
} else if (res.score >= low) {
link.classList.add("low_result")
} else {
link.classList.add("no_result")
}
currIndent = res.level;
if (currIndent > indent) {
indent = currIndent;
let ilist = document.createElement("ul")
ilist.classList.add("detailed_result");
resultArea.appendChild(ilist);
resultArea = ilist;
} else if (currIndent < indent) {
resultArea = origResult;
indent = currIndent;
}
link.href = `${res.url}`;
link.innerHTML = `${res.type} ${res.number} ${res.title}`;
let clip = document.createElement("div");
clip.classList.add("search-result-clip");
clip.innerHTML = `${res.body}`;
let bullet = document.createElement("li");
bullet.classList.add('search-result-bullet');
bullet.appendChild(link);
bullet.appendChild(clip);
let p = document.createElement("text");
p.classList.add('search-result-score');
p.innerHTML = ` (${res.score.toFixed(2)})`;
bullet.appendChild(p);
resultArea.appendChild(bullet);
}
// Auto-close search results when a result is clicked in case result is on
// the same page search started from
const resultsDiv = document.getElementById('searchresultsplaceholder');
const backDiv = document.querySelector('.searchresultsbackground');
resultArea.querySelectorAll("a").forEach((link) => {
link.addEventListener('click', (e) => {
backDiv.style.display = 'none';
resultsDiv.style.display = 'none';
});
});
//Could print message about how many results are not shown. No way to localize it though...
// if(numUnshown > 0) {
// let bullet = document.createElement("li");
// bullet.classList.add('search-results-bullet');
// bullet.classList.add('search-results-unshown-count');
// let p = document.createElement("text");
// p.innerHTML = `${parseInt(numUnshown)} unshown results...`;
// bullet.appendChild(p);
// resultArea.appendChild(bullet);
// }
document.getElementById("searchresultsplaceholder").style.display = null;
MathJax.typesetPromise();
}
window.addEventListener("load", function (event) {
const resultsDiv = document.getElementById('searchresultsplaceholder');
//insert a div to be backgroud behind searchresultsplaceholder
const backDiv = document.createElement("div");
backDiv.classList.add("searchresultsbackground");
backDiv.style.display = 'none';
resultsDiv.parentNode.appendChild(backDiv);
document.getElementById("searchbutton").addEventListener('click', (e) => {
resultsDiv.style.display = null;
backDiv.style.display = null;
let searchInput = document.getElementById("ptxsearch");
searchInput.value = JSON.parse(localStorage.getItem("last-search-terms")).terms;
searchInput.select();
doSearch();
});
document.getElementById("ptxsearch").addEventListener('input', (e) => {
doSearch();
});
document.getElementById("closesearchresults").addEventListener('click', (e) => {
resultsDiv.style.display = 'none';
backDiv.style.display = 'none';
document.getElementById('searchbutton').focus();
});
});
+112
View File
@@ -0,0 +1,112 @@
function doSearch() {
// Get the search terms from the input text box
let terms = document.getElementById("ptxsearch").value;
// Where do we want to put the results?
let resultArea = document.getElementById("searchresults")
resultArea.innerHTML = ""; // clear out any previous results
// do the search using the provided index
let pageResult = ptx_lunr_idx.search(terms);
// Number the documents from first to last so we can order the results by their
// position in the book.
snum = 0;
for (let doc of ptx_lunr_docs) {
doc.snum = snum;
snum += 1;
}
// Transfer meta data from the document to the results to make it easy to add
// our lists later.
augmentResults(pageResult, ptx_lunr_docs);
addResultToPage(pageResult, ptx_lunr_docs, resultArea);
MathJax.typeset();
}
// Find the entry for a search result in the original document index
function findEntry(resultId, db) {
for (const page of db) {
if (page.id === resultId) {
return page;
}
}
return resultId;
}
function augmentResults(result, docs) {
for (let res of result) {
let info = findEntry(res.ref, docs);
res.number = info.number;
res.type = info.type;
res.title = info.title;
res.url = info.url;
res.level = info.level;
res.snum = info.snum;
}
}
function comparePosition(a, b) {
if (a.snum < b.snum) {
return -1;
}
if (a.snum > b.snum) {
return 1;
}
return 0;
}
function addResultToPage(result, docs, resultArea) {
let len = result.length
let high = result[Math.floor(len*0.25)].score;
let med = result[Math.floor(len*0.5)].score;
let low = result[Math.floor(len*0.75)].score;
// sort the results by their position in the book, not their score
result = result.sort(comparePosition)
let indent = "1";
let currIndent = indent;
let origResult = resultArea;
// Create list entries indenting as needed.
for (const res of result) {
let link = document.createElement("a")
// add a class so we can colorize the results based on their rank in terms
// of search score.
if (res.score >= high) {
link.classList.add("high_result")
} else if (res.score >= med) {
link.classList.add("medium_result")
} else if (res.score >= low) {
link.classList.add("low_result")
}
currIndent = res.level;
if (currIndent > indent) {
indent = currIndent;
let ilist = document.createElement("ul")
ilist.classList.add("detailed_result");
resultArea.appendChild(ilist);
resultArea = ilist;
} else if (currIndent < indent) {
resultArea = origResult;
indent = currIndent;
}
let bullet = document.createElement("li")
bullet.style.marginTop = "5px";
link.href = `${res.url}`;
link.innerHTML = `${res.type} ${res.number} ${res.title}`;
bullet.appendChild(link)
let p = document.createElement("text");
p.innerHTML = ` (${res.score.toFixed(2)})`;
bullet.appendChild(p);
resultArea.appendChild(bullet);
}
}
function showHelp() {
let state = document.getElementById("helpme").style.display;
if (state == "none") {
document.getElementById("helpme").style.display = "block";
document.getElementById("helpbutt").innerHTML = "Hide Help"
} else {
document.getElementById("helpme").style.display = "none";
document.getElementById("helpbutt").innerHTML = "Show Help"
}
}
+510
View File
@@ -0,0 +1,510 @@
editorLog = console.log;
// editorLog = function(){};
debugLog = function(){};
// debugLog = console.log;
//parseLog = function(){};
parseLog = console.log;
errorLog = console.log;
/* the structure of each object, and its realization as PreTeXt source or in HTML,
is recorded in the objectStructure dictionary.
{"xml:id": new_id, "sourcetag": new_tag, "parent": parent_description, "title": ""}
In pretext, pieces are of the form ["piecename", "tag"], while
in source, pieces are of the form ["piecename", "required_component"], while
*/
editing_mode = 0;
current_page = location.host+location.pathname;
debugLog("current_page", current_page);
chosen_edit_option_key = "edit_option".concat(current_page);
/*
chosen_edit_option = readCookie(chosen_edit_option_key) || "";
editing_mode = chosen_edit_option;
*/
chosen_edit_option = "PLACEHOLDER";
debugLog("chosen_edit_option", chosen_edit_option, "chosen_edit_option", chosen_edit_option > 0);
var font_families = {
'RS': "'Roboto Serif', serif;",
'OS': "'Open Sans', sans-serif;"
}
var font_vals = {
// 'face': 'serif',
'size': [12, 8, 20],
'height': [135, 80, 200],
'wspace': [0, -10,20],
'lspace': [0, -20,20],
'wdth': [100, 50, 150],
'wght': [400, 100, 1000]
}
function fontcss(fvals) {
console.log("in fontcss",fvals);
var csskeys = Object.keys(fvals);
var this_style = "";
for (var j=0; j < csskeys.length; ++j) {
this_key = csskeys[j];
document.getElementById("the" + this_key).innerHTML = fvals[this_key][0];
console.log("in fontcss", this_key, "with value", fvals[this_key][0], "/10", fvals[this_key][0]/10.0);
if (this_key == 'size') {
this_style += "font-size: " + fvals[this_key][0].toString() + "pt; "
} else if (this_key == 'height') {
this_style += "line-height: " + (fvals[this_key][0]/100.0).toString() + "; "
} else if (this_key == 'lspace') {
this_style += "letter-spacing: " + (fvals[this_key][0]/200.0).toString() + "rem; "
} else if (this_key == 'wspace') {
this_style += "word-spacing: " + (fvals[this_key][0]/50.0).toString() + "rem; "
}
}
this_style += "font-variation-settings: ";
for (var j=0; j < csskeys.length; ++j) {
this_key = csskeys[j];
if (this_key == 'wdth') {
this_style += "'wdth' " + fvals[this_key][0].toString() + ","
} else if (this_key == 'wght') {
this_style += "'wght' " + fvals[this_key][0].toString() + ","
}
}
this_style = this_style.slice(0, -1);
this_style += ";";
console.log("returning this_style", this_style);
return this_style
}
function choice_options(this_choice) {
console.log("choice_options of", this_choice);
val_dict = prefs_menu_vals[this_choice];
console.log("val_dict", val_dict);
var these_keys = Object.keys(val_dict);
these_keys.sort();
var these_choices = "";
for (var j=0; j < these_keys.length; ++j) {
this_key = these_keys[j];
these_choices += '<li id="' + this_key + '">';
these_choices += val_dict[this_key];
these_choices += '</li>';
}
return these_choices
}
// we have to keep track of multiple consecutive carriage returns
this_char = "";
prev_char = "";
prev_prev_char = "";
// sometimes we have to prevent Tab from changing focus
this_focused_element = "";
prev_focused_element = "";
prev_prev_focused_element = "";
var menu_neutral_background = "#ddb";
var menu_active_background = "#fdd";
var recent_editing_actions = []; // we unshift to this, so most recent edit is first.
// currently just a human-readable list
var ongoing_editing_actions = [];
var old_content = {}; // to hold old versions of changed materials
// what will happen with internationalization?
var keyletters = ["KeyA", "KeyB", "KeyC", "KeyD", "KeyE", "KeyF", "KeyG", "KeyH", "KeyI", "KeyJ", "KeyK", "KeyL", "KeyM", "KeyN", "KeyO", "KeyP", "KeyQ", "KeyR", "KeyS", "KeyT", "KeyU", "KeyV", "KeyW", "KeyX", "KeyY", "KeyZ"];
var movement_location_options = [];
var movement_location = 0;
var first_move = true; // used when starting to move, because object no longer occupies its original location
Storage.prototype.setObject = function(key, value) {
// this.setItem(key, JSON.stringify(value));
this.setItem(key, JSON.stringify(value, function(key, val) {
// console.log("key", key, "value", value, "val", val);
return val.toFixed ? Number(val.toFixed(3)) : val;
}));
}
Storage.prototype.getObject = function(key) {
var value = this.getItem(key);
return value && JSON.parse(value);
}
function randomstring(len) {
if (!len) { len = 10 }
return "tMP" + (Math.random() + 1).toString(36).substring(2,len)
}
function removeItemFromList(lis, value) {
var index = lis.indexOf(value);
if (index > -1) {
lis.splice(index, 1);
}
return lis;
}
function textNodesUnder(node){
var all = [];
for (node=node.firstChild;node;node=node.nextSibling){
if (node.nodeType==3) { all.push([3, node]) }
else if (node.nodeType==1) {
all.push([1, node])
var thistag = node.cloneNode().outerHTML;
console.log("thistag", thistag);
[thisopen, thisclose] = thistag.split("><");
thisopen += ">"; thisclose += "<";
thisinsides = textNodesUnder(node.cloneNode().innerHTML);
// all.push([0, thisopen]);
for (var j=0; j < thisinsides.length; ++j) {
// all.push(thisinsides[j])
}
// all.push([0, thisclose]);
}
// probably we only want direct children
// else all = all.concat(textNodesUnder(node));
}
return all;
}
function wordsAllWrapped(node) {
var these_text_nodes = textNodesUnder(node);
console.log("node", node);
console.log("these_text_nodes", these_text_nodes);
for (var j=0; j < these_text_nodes.length; ++j) {
var this_text_node = these_text_nodes[j];
var thistype = this_text_node[0];
var thisnode = this_text_node[1];
if (thistype == 3) {
// var these_node_words_and_spaces = these_text_nodes[j].nodeValue.split(/(\s+)/);
var these_node_words_and_spaces = thisnode.nodeValue.split(/(\s+)/);
console.log("these_node_words_and_spaces", these_node_words_and_spaces);
var spanned_words = "";
for (var k=0; k < these_node_words_and_spaces.length; ++k) {
spanned_words += '<span class="oneword">' + these_node_words_and_spaces[k] + "</span>"
}
var wass_text = document.createElement('div');
wass_text.setAttribute('class', 'wastext');
wass_text.innerHTML = spanned_words;
// these_text_nodes[j].nodeValue = spanned_words
thisnode.replaceWith(wass_text);
wass_text.outerHTML = wass_text.innerHTML
} else if (thistype == 1) {
// // leave it there, but make sure we know about it
// thisnode.classList.add("oneelement")
if (thisnode.classList.contains("process-math") ||
thisnode.classList.contains("autopermalink") ||
thisnode.classList.contains("latex-logo") ||
thisnode.classList.contains("heading") ||
// should we instead check for tags that *can* contain line breaks?
["A", "CODE", "PRE"].includes(thisnode.tagName)) {
//wrap it in a span.oneword
var word_wrapper = document.createElement('span');
word_wrapper.setAttribute('class', 'oneword');
thisnode.parentNode.insertBefore(word_wrapper, thisnode);
word_wrapper.appendChild(thisnode);
} else {
wordsAllWrapped(thisnode)
}
}
}
}
function linesAllWrapped() {
// var testID = "p-446";
// var testNode = document.getElementById(testID);
// var all_para = document.querySelectorAll(".para");
// var all_para = document.querySelectorAll("SECTION P");
var all_para = document.querySelectorAll("SECTION .para:not(.logical)");
for (var pj = 0; pj < all_para.length; ++pj) {
testNode = all_para[pj];
wordsAllWrapped(testNode);
console.log(" wordsAllWrapped", pj, "of", testNode);
var these_words = document.querySelectorAll(".oneword, .oneelement");
var this_line = [];
var all_lines = "";
var current_height = these_words[0].getBoundingClientRect().bottom;
var word_depth = 0;
for (var j=0; j < these_words.length; ++j) {
// next line is an error if the paragraphs starts with an element
var this_word = these_words[j];
var this_parent = this_word.parentElement;
if (this_word.classList.contains("oneword")) {
this_height = this_word.getBoundingClientRect().bottom;
if ( (Math.abs(this_height - current_height) < 10) && j > 0) {
if (this_parent == testNode && word_depth == 0) {
this_line.push(this_word.innerHTML)
} else {
console.log(word_depth, ":",this_parent == testNode, "this_parent", this_parent.tagName, "ccc", this_parent.ClassList, "xxx", this_word.innerHTML);
if (word_depth == 0) {
console.log("opening the tag", this_parent.tagName,"parent", this_parent,"of",this_word.innerHTML);
var parent_classlist = this_parent.classList;
console.log("parent_classlist", parent_classlist, "first", parent_classlist[0], "length", parent_classlist.length);
// did not work. why? var parent_classes = parent_classlist.join(" ");
this_line.push('<' + this_parent.tagName + ' class="' + parent_classlist[0] + '">');
this_line.push(this_word.innerHTML);
word_depth += 1
} else {
this_line.push(this_word.innerHTML);
}
if (this_word.nextElementSibling == null) {
console.log("closing the tag", this_parent.tagName);
// need to close the wrapping element
this_line.push("</" + this_parent.tagName + ">");
word_depth -= 1;
if (word_depth > 0 && this_word.parentElement.nextElementSibling == null) {
this_line.push("</" + this_parent.parentElement.tagName + ">");
word_depth -= 1;
}
}
}
} else {
html_line_contents = "";
for (var k=0; k < this_line.length; ++k) {
html_line_contents += this_line[k]
}
// hack which only handles up to depth 2
if (word_depth > 0) {
html_line_contents += "</" + this_parent.tagName + ">"
}
if (word_depth > 1) {
html_line_contents += "</" + this_parent.parentElement.tagName + ">"
}
console.log("made", html_line_contents);
all_lines += '<div class="onelineX">' + html_line_contents + '</div>';
current_height = this_height;
this_line = [this_word.innerHTML];
if (word_depth > 0) {
this_line.unshift('<' + this_parent.tagName + ' class="' + this_parent.classList[0] + '">')
}
if (word_depth > 1) {
this_line.unshift('<' + this_parent.tagName + ' class="' + this_parent.parentElement.classList[0] + '">')
}
}
} else { // we have an html node
this_line.push(this_word.outerHTML)
}
}
// partial last line may be dangling
// bug: need to fix closing tags at the end? maybe rely on automatic closure
if (this_line.length > 0) {
html_line_contents = "";
for (var k=0; k < this_line.length; ++k) {
html_line_contents += this_line[k]
}
all_lines += '<div class="onelineX">' + html_line_contents + '</div>'
}
testNode.innerHTML = all_lines
}
}
editorLog("adding tab listener");
document.addEventListener('keydown', logKeyDown);
function nextsibligntabbable(startingplace, where) {
var candidate = startingplace.nextElementSibling;
if (where == "previous" ) { candidate = startingplace.previousElementSibling }
while (candidate) {
console.log("candidate A", candidate);
if (candidate.hasAttribute("tabindex")) { return candidate }
console.log("candidate B", candidate);
if (where == "next" ) { candidate = candidate.nextElementSibling }
else { candidate = candidate.previousElementSibling }
}
}
function logKeyDown(e) {
if (e.code == "ShiftLeft" || e.code == "ShiftRight" || e.code == "Shift") { return }
prev_prev_char = prev_char;
prev_char = this_char;
this_char = e;
debugLog("logKey",e,"XXX",e.code);
var input_region = document.activeElement;
debugLog("input_region", input_region);
if (input_region.id == "user-preferences-button") {
if (e.code == "Enter") {
document.getElementById("preferences_menu_holder").classList.toggle("hidden")
} else if (e.code == "Tab") {
console.log("tabbing to main pref menu", document.getElementById("preferences_menu_holder").firstElementChild);
console.log("tabbing to main pref menu", document.getElementById("preferences_menu_holder").firstElementChild.firstElementChild);
e.preventDefault();
document.getElementById("preferences_menu_holder").firstElementChild.firstElementChild.focus()
}
} else if (input_region.hasAttribute("data-env")) {
var this_submenu = input_region.getElementsByTagName("ol")[0];
console.log("this_submenu C", this_submenu);
if ((e.code == "Enter") || (e.code == "ArrowRight")) {
console.log("selecting",input_region, "which has", input_region.getElementsByTagName("ol"), "first",input_region.getElementsByTagName("ol")[0]);
this_submenu.classList.toggle("hidden")
input_region.classList.toggle("selected")
input_region.parentElement.classList.toggle("active")
} else if (e.code == "Tab" && e.shiftKey) {
e.preventDefault();
if (input_region.classList.contains("selected")) {
this_submenu.classList.loggle("hidden");
input_region.classList.loggle("selected");
} else { // cycle up through the elements
// previous_sibling = input_region.previousElementSibling;
previous_sibling = nextsibligntabbable(input_region, "previous");
if (previous_sibling) { previous_sibling.focus() }
else {
document.getElementById("preferences_menu_holder").classList.toggle("hidden");
document.getElementById("user-preferences-button").focus();
}
}
} else if (e.code == "Tab") {
e.preventDefault();
if (input_region.classList.contains("selected")) {
var firstchild = this_submenu.firstElementChild;
if (firstchild.hasAttribute("tabindex")) {
firstchild.focus()
} else {
nextsibligntabbable(firstchild, "next").focus()
}
} else { // cycle through the elements
// next_sibling = input_region.nextElementSibling;
next_sibling = nextsibligntabbable(input_region, "next");
if (next_sibling) { next_sibling.focus() }
else {
document.getElementById("preferences_menu_holder").classList.toggle("hidden");
document.getElementById("user-preferences-button").focus();
}
}
}
} else if (input_region.hasAttribute("data-val")) {
console.log("input_region", input_region);
console.log("input_regionparentElement", input_region.parentElement);
if ((e.code == "Enter") || (e.code == "ArrowRight")) {
console.log("font_vals is", font_vals);
var dataval = input_region.getAttribute("data-val");
var datachange = input_region.getAttribute("data-change");
console.log("dataval", dataval, "datachange",datachange);
if (input_region.parentElement.classList.contains("fonts")) {
font_vals[dataval][0] += parseFloat(datachange);
if (font_vals[dataval][0] < font_vals[dataval][1]) { font_vals[dataval][0] = font_vals[dataval][1] }
else if (font_vals[dataval][0] > font_vals[dataval][2]) { font_vals[dataval][0] = font_vals[dataval][2] }
console.log("font_vals are", font_vals);
console.log("css font_vals", fontcss(font_vals));
var new_style = fontcss(font_vals);
var paras = document.getElementsByClassName('para');
for (i = 0; i < paras.length; i++) {
paras[i].setAttribute('style', new_style);
}
} else if (input_region.parentElement.classList.contains("fontfamily")) {
document.body.setAttribute("data-font", datachange);
var checks = document.getElementsByClassName('ffcheck');
for (i = 0; i < checks.length; i++) {
checks[i].innerHTML = '';
}
document.getElementById("the" + datachange).innerHTML = "✔️";
} else if (input_region.parentElement.classList.contains("avatar")) {
var checks = document.getElementsByClassName('avatarcheck');
for (i = 0; i < checks.length; i++) {
checks[i].innerHTML = '';
}
document.getElementById("theavatarbutton").innerHTML = dataval;
document.getElementById("the" + dataval).innerHTML = "✔️";
} else if (input_region.parentElement.classList.contains("atmosphere")) {
document.body.setAttribute("data-atmosphere", dataval);
var checks = document.getElementsByClassName('atmospherecheck');
for (i = 0; i < checks.length; i++) {
checks[i].innerHTML = '';
}
document.getElementById("the" + dataval).innerHTML = "✔️";
} else if (input_region.parentElement.classList.contains("ruler")) {
// could be motion or actual ruler
if (["mouse", "arrow", "eye"].includes(dataval)) {
document.body.setAttribute("data-motion", dataval);
var checks = document.getElementsByClassName('motioncheck');
for (i = 0; i < checks.length; i++) {
checks[i].innerHTML = '';
}
document.getElementById("the" + dataval).innerHTML = "✔️";
} else {
if (!document.body.hasAttribute("data-ruler")) {
linesAllWrapped()
}
document.body.setAttribute("data-ruler", dataval);
var checks = document.getElementsByClassName('rulercheck');
for (i = 0; i < checks.length; i++) {
checks[i].innerHTML = '';
}
document.getElementById("the" + dataval).innerHTML = "✔️";
}
}
} else if (e.code == "Tab" && e.shiftKey) {
e.preventDefault();
// previous_sibling = input_region.previousElementSibling;
previous_sibling = nextsibligntabbable(input_region, "previous");
console.log("previous_sibling",previous_sibling);
if (previous_sibling) { previous_sibling.focus() }
else {
input_region.parentElement.parentElement.parentElement.classList.toggle("active");
input_region.parentElement.parentElement.classList.toggle("selected");
input_region.parentElement.parentElement.focus();
input_region.parentElement.classList.toggle("hidden");
}
} else if (e.code == "Tab") {
e.preventDefault();
// next_sibling = input_region.nextElementSibling;
next_sibling = nextsibligntabbable(input_region, "next");
if (next_sibling) { next_sibling.focus() }
else {
input_region.parentElement.parentElement.parentElement.classList.toggle("active");
input_region.parentElement.parentElement.classList.toggle("selected");
input_region.parentElement.parentElement.focus();
input_region.parentElement.classList.toggle("hidden");
}
}
}
return;
editorLog("input_region", input_region);
// if we are writing something, keystrokes usually are just text input
if (document.getElementById('actively_editing')) {
editorLog(" we are actively editing");
if (e.code == "Tab" && !document.getElementById('local_menu_holder')) {
// disabled for now
e.preventDefault();
return ""
create_local_menu()
} else if (document.getElementById('local_menu_holder')) {
main_menu_navigator(e);
} else {
local_editing_action(e)
}
} else if (document.getElementById('phantomobject')) {
var the_phantomobject = document.getElementById('phantomobject');
if (the_phantomobject.classList.contains('move')) {
move_object(e)
} else {
alert("do not know what to do with that")
}
} else {
console.log("calling main_menu_navigator");
main_menu_navigator(e);
}
}