Latest build deployed.
This commit is contained in:
@@ -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(/< /g, "<");
|
||||
the_ans = the_ans.replace(/</g, "<");
|
||||
the_ans = the_ans.replace(/>/g, ">");
|
||||
the_ans = the_ans.replace(/&/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→';
|
||||
} 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
@@ -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';
|
||||
});
|
||||
@@ -0,0 +1,131 @@
|
||||
|
||||
var icon = {
|
||||
"warning": "⚠️ ",
|
||||
"commentary": "☕",
|
||||
"media": "▶",
|
||||
"tip": "📣",
|
||||
"worksheet": "📄",
|
||||
"assesment": "A<sup>+</sup>",
|
||||
"slides": "📻",
|
||||
"outcomes": "✓"
|
||||
};
|
||||
|
||||
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> <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
@@ -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);
|
||||
|
||||
|
||||
Vendored
+2
File diff suppressed because one or more lines are too long
@@ -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);
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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 < 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 < 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 < 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,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
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user