Latest build deployed.
This commit is contained in:
@@ -1 +1 @@
|
||||
{"source/frontmatter.ptx": ["frontmatter"], "source/main.ptx": ["course-documents", "recitations"], "source/syllabus/syllabus.ptx": ["syllabus"], "source/recitations/calculus-review.ptx": ["recitation-calculus-review"]}
|
||||
{"source/frontmatter.ptx": ["frontmatter"], "source/main.ptx": ["course-documents", "ch-notes", "recitations"], "source/syllabus/syllabus.ptx": ["syllabus"], "source/notes/week01.ptx": ["notes-week-01"], "source/recitations/calculus-review.ptx": ["recitation-calculus-review"]}
|
||||
@@ -10,5 +10,5 @@
|
||||
<item type="str">prefix-runestone.677a75a9120449ff.css</item>
|
||||
</css>
|
||||
<cdn-url type="str">https://runestone.academy/cdn/runestone/</cdn-url>
|
||||
<version type="str">7.11.8</version>
|
||||
<version type="str">7.11.9</version>
|
||||
</all>
|
||||
|
||||
@@ -10,5 +10,5 @@
|
||||
<item type="str">prefix-runestone.677a75a9120449ff.css</item>
|
||||
</css>
|
||||
<cdn-url type="str">https://runestone.academy/cdn/runestone/</cdn-url>
|
||||
<version type="str">7.11.8</version>
|
||||
<version type="str">7.11.9</version>
|
||||
</all>
|
||||
|
||||
Executable
+194
@@ -0,0 +1,194 @@
|
||||
<!DOCTYPE html>
|
||||
<!--********************************************-->
|
||||
<!--* Generated from PreTeXt source *-->
|
||||
<!--* *-->
|
||||
<!--* https://pretextbook.org *-->
|
||||
<!--* *-->
|
||||
<!--********************************************-->
|
||||
<html lang="en-US" dir="ltr">
|
||||
<!--******************************************-->
|
||||
<!--* Authored with PreTeXt *-->
|
||||
<!--* pretextbook.org *-->
|
||||
<!--* Theme: boulder *-->
|
||||
<!--* Palette: *-->
|
||||
<!--******************************************-->
|
||||
<head xmlns:og="http://ogp.me/ns#" xmlns:book="https://ogp.me/ns/book#">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<title>Class Notes</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0">
|
||||
<link href="_static/pretext/css/theme.css" rel="stylesheet" type="text/css">
|
||||
<script>
|
||||
var runestoneMathReady = new Promise((resolve) => window.rsMathReady = resolve);
|
||||
window.MathJax = {
|
||||
"tex": {
|
||||
"inlineMath": [
|
||||
[
|
||||
"\\(",
|
||||
"\\)"
|
||||
]
|
||||
],
|
||||
"tags": "none",
|
||||
"tagSide": "right",
|
||||
"tagIndent": ".8em",
|
||||
"packages": {
|
||||
"[+]": [
|
||||
"base",
|
||||
"extpfeil",
|
||||
"ams",
|
||||
"amscd",
|
||||
"color",
|
||||
"newcommand",
|
||||
"knowl"
|
||||
]
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"ignoreHtmlClass": "tex2jax_ignore|ignore-math",
|
||||
"processHtmlClass": "process-math"
|
||||
},
|
||||
"chtml": {
|
||||
"scale": 0.98,
|
||||
"mtextInheritFont": true
|
||||
},
|
||||
"loader": {
|
||||
"load": [
|
||||
"input/asciimath",
|
||||
"[tex]/extpfeil",
|
||||
"[tex]/amscd",
|
||||
"[tex]/color",
|
||||
"[tex]/newcommand",
|
||||
"[pretext]/mathjaxknowl3.js"
|
||||
],
|
||||
"paths": {
|
||||
"pretext": "_static/pretext/js/lib"
|
||||
}
|
||||
},
|
||||
"startup": {
|
||||
pageReady() {
|
||||
return MathJax.startup.defaultPageReady().then(function () {
|
||||
console.log("in ready function");
|
||||
rsMathReady();
|
||||
}
|
||||
)}
|
||||
}
|
||||
};
|
||||
</script><script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js"></script><meta name="Keywords" content="Authored in PreTeXt">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta property="og:type" content="book">
|
||||
<meta property="book:title" content="Introduction to Probability and Statistics for the Life Sciences">
|
||||
<meta property="book:author" content="Andy Eisenberg">
|
||||
<script src="_static/pretext/js/lib/jquery.min.js"></script><script src="_static/pretext/js/lib/jquery.sticky.js"></script><script src="_static/pretext/js/lib/jquery.espy.min.js"></script><script src="_static/pretext/js/pretext.js"></script><script src="_static/pretext/js/pretext_add_on.js?x=1"></script><script src="_static/pretext/js/user_preferences.js"></script><!--** eBookCongig is necessary to configure interactive **-->
|
||||
<!--** Runestone components to run locally in reader's browser **-->
|
||||
<!--** No external communication: **-->
|
||||
<!--** log level is 0, Runestone Services are disabled **-->
|
||||
<script type="text/javascript">
|
||||
eBookConfig = {};
|
||||
eBookConfig.useRunestoneServices = false;
|
||||
eBookConfig.host = 'http://127.0.0.1:8000';
|
||||
eBookConfig.course = 'PTX_Course_Title_Here';
|
||||
eBookConfig.basecourse = 'PTX_Base_Course';
|
||||
eBookConfig.isLoggedIn = false;
|
||||
eBookConfig.email = '';
|
||||
eBookConfig.isInstructor = false;
|
||||
eBookConfig.logLevel = 0;
|
||||
eBookConfig.username = '';
|
||||
eBookConfig.readings = null;
|
||||
eBookConfig.activities = null;
|
||||
eBookConfig.downloadsEnabled = false;
|
||||
eBookConfig.allow_pairs = false;
|
||||
eBookConfig.enableScratchAC = false;
|
||||
eBookConfig.build_info = "";
|
||||
eBookConfig.python3 = null;
|
||||
eBookConfig.runestone_version = '7.11.9';
|
||||
eBookConfig.jobehost = '';
|
||||
eBookConfig.proxyuri_runs = '';
|
||||
eBookConfig.proxyuri_files = '';
|
||||
eBookConfig.enable_chatcodes = false;
|
||||
</script>
|
||||
<!--*** Runestone Services ***-->
|
||||
<script src="_static/prefix-runtime.991c135ccf182bf5.bundle.js"></script><script src="_static/prefix-723.3e6434f80549315a.bundle.js"></script><script src="_static/prefix-runestone.1c010553c35c544c.bundle.js"></script><link rel="stylesheet" type="text/css" href="_static/prefix-723.3bccd435914aa0ff.css">
|
||||
<link rel="stylesheet" type="text/css" href="_static/prefix-runestone.677a75a9120449ff.css">
|
||||
<script src="_static/pretext/js/lti_iframe_resizer.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/lunr.js/2.3.9/lunr.min.js" integrity="sha512-4xUl/d6D6THrAnXAwGajXkoWaeMNwEKK4iNfq5DotEbLPAfk6FSxSP3ydNxqDgCw1c/0Z1Jg6L8h2j+++9BZmg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script><script src="lunr-pretext-search-index.js" async=""></script><script src="_static/pretext/js/pretext_search.js"></script><script src="_static/pretext/js/lib/knowl.js"></script><!--knowl.js code controls Sage Cells within knowls--><script>sagecellEvalName='Evaluate (Sage)';
|
||||
</script>
|
||||
</head>
|
||||
<body class="pretext book ignore-math">
|
||||
<a class="assistive" href="#ptx-content">Skip to main content</a><header id="ptx-masthead" class="ptx-masthead"><div class="ptx-banner"><div class="title-container">
|
||||
<h1 class="heading"><a href="root-1-2.html"><span class="title">Introduction to Probability and Statistics for the Life Sciences</span> <span class="subtitle">Spring 2026 MATH 1044</span></a></h1>
|
||||
<p class="byline">Andy Eisenberg</p>
|
||||
</div></div></header><nav id="ptx-navbar" class="ptx-navbar navbar"><div class="ptx-navbar-contents">
|
||||
<button class="toc-toggle button" title="Contents"><span class="icon material-symbols-outlined" aria-hidden="true"></span><span class="name">Contents</span></button><div class="searchbox">
|
||||
<div class="searchwidget"><button id="searchbutton" class="searchbutton button" type="button" title="Search book"><span class="icon material-symbols-outlined" aria-hidden="true"></span><span class="name">Search Book</span></button></div>
|
||||
<div id="searchresultsplaceholder" class="searchresultsplaceholder" style="display: none">
|
||||
<div class="search-results-controls">
|
||||
<input aria-label="Search term" id="ptxsearch" class="ptxsearch" type="text" name="terms" placeholder="Search term"><button title="Close search" id="closesearchresults" class="closesearchresults"><span class="material-symbols-outlined">close</span></button>
|
||||
</div>
|
||||
<h2 class="search-results-heading">Search Results: </h2>
|
||||
<div id="searchempty" class="searchempty"><span>No results.</span></div>
|
||||
<ol id="searchresults" class="searchresults"></ol>
|
||||
</div>
|
||||
</div>
|
||||
<span class="nav-other-controls"><button id="embed-button" class="embed-button button" title="Embed this page"><span class="icon material-symbols-outlined" aria-hidden="true"></span><span class="name">Embed</span></button><div class="embed-popup hidden" id="embed-popup">
|
||||
<p>Copy the code below to embed this page in your own website or LMS page.</p>
|
||||
<div class="embed-code-container">
|
||||
<textarea class="embed-code-textbox" id="embed-code-textbox" readonly aria-label="textbox"><iframe src="https://example.com/embed" width="100%" height="1000"></iframe></textarea><button class="copy-embed-button button" id="copy-embed-button" title="Copy embed code"><span class="icon material-symbols-outlined" aria-hidden="true"></span><span class="name">Copy</span></button>
|
||||
</div>
|
||||
</div>
|
||||
<button id="light-dark-button" class="light-dark-button button" title="Dark Mode"><span class="icon material-symbols-outlined" aria-hidden="true"></span><span class="name">Dark Mode</span></button></span><span class="treebuttons"><a class="previous-button button" href="syllabus.html" title="Previous"><span class="icon material-symbols-outlined" aria-hidden="true"></span><span class="name">Prev</span></a><a class="up-button button" href="root-1-2.html" title="Up"><span class="icon material-symbols-outlined" aria-hidden="true"></span><span class="name">Up</span></a><a class="next-button button" href="notes-week-01.html" title="Next"><span class="name">Next</span><span class="icon material-symbols-outlined" aria-hidden="true"></span></a></span>
|
||||
</div></nav><div id="latex-macros" class="hidden-content process-math" style="display:none"><span class="process-math">\(\newcommand{\N}{\mathbb N}
|
||||
\newcommand{\Z}{\mathbb Z}
|
||||
\newcommand{\Q}{\mathbb Q}
|
||||
\newcommand{\R}{\mathbb R}
|
||||
\newcommand{\lt}{<}
|
||||
\newcommand{\gt}{>}
|
||||
\newcommand{\amp}{&}
|
||||
\definecolor{fillinmathshade}{gray}{0.9}
|
||||
\newcommand{\fillinmath}[1]{\mathchoice{\colorbox{fillinmathshade}{$\displaystyle \phantom{\,#1\,}$}}{\colorbox{fillinmathshade}{$\textstyle \phantom{\,#1\,}$}}{\colorbox{fillinmathshade}{$\scriptstyle \phantom{\,#1\,}$}}{\colorbox{fillinmathshade}{$\scriptscriptstyle\phantom{\,#1\,}$}}}
|
||||
\)</span></div>
|
||||
<div class="ptx-page">
|
||||
<div id="ptx-sidebar" class="ptx-sidebar"><nav id="ptx-toc" class="ptx-toc depth2"><ul class="structural toc-item-list contains-active">
|
||||
<li class="toc-item toc-frontmatter"><div class="toc-title-box"><a href="frontmatter.html" class="internal"><span class="title">Front Matter</span></a></div></li>
|
||||
<li class="toc-item toc-chapter">
|
||||
<div class="toc-title-box"><a href="course-documents.html" class="internal"><span class="title">Course Documents</span></a></div>
|
||||
<ul class="structural toc-item-list"><li class="toc-item toc-section">
|
||||
<div class="toc-title-box"><a href="syllabus.html" class="internal"><span class="title">Syllabus</span></a></div>
|
||||
<ul class="structural toc-item-list">
|
||||
<li class="toc-item toc-subsection"><div class="toc-title-box"><a href="syllabus.html#sec-course-info" class="internal"><span class="title">Course Information</span></a></div></li>
|
||||
<li class="toc-item toc-subsection"><div class="toc-title-box"><a href="syllabus.html#sec-materials" class="internal"><span class="title">Textbook and Other Required Materials</span></a></div></li>
|
||||
<li class="toc-item toc-subsection"><div class="toc-title-box"><a href="syllabus.html#sec-assessments" class="internal"><span class="title">Assessments</span></a></div></li>
|
||||
<li class="toc-item toc-subsection"><div class="toc-title-box"><a href="syllabus.html#sec-grading" class="internal"><span class="title">Grading</span></a></div></li>
|
||||
</ul>
|
||||
</li></ul>
|
||||
</li>
|
||||
<li class="toc-item toc-chapter contains-active">
|
||||
<div class="toc-title-box"><a href="ch-notes.html" class="internal"><span class="title">Class Notes</span></a></div>
|
||||
<ul class="structural toc-item-list active"><li class="toc-item toc-section">
|
||||
<div class="toc-title-box"><a href="notes-week-01.html" class="internal"><span class="title">Week 1</span></a></div>
|
||||
<ul class="structural toc-item-list"><li class="toc-item toc-subsection">
|
||||
<div class="toc-title-box"><a href="notes-week-01.html#notes-week-01-3" class="internal"><span class="title">Tuesday 1/13</span></a></div>
|
||||
<ul class="structural toc-item-list">
|
||||
<li class="toc-item toc-subsubsection"><div class="toc-title-box"><a href="notes-week-01.html#subsubsec-Sets" class="internal"><span class="title">Sec 1.1: Sets</span></a></div></li>
|
||||
<li class="toc-item toc-subsubsection"><div class="toc-title-box"><a href="notes-week-01.html#subsubsec-Probability" class="internal"><span class="title">Sec 1.2: Probability</span></a></div></li>
|
||||
</ul>
|
||||
</li></ul>
|
||||
</li></ul>
|
||||
</li>
|
||||
<li class="toc-item toc-chapter">
|
||||
<div class="toc-title-box"><a href="recitations.html" class="internal"><span class="title">Recitations</span></a></div>
|
||||
<ul class="structural toc-item-list"><li class="toc-item toc-worksheet"><div class="toc-title-box"><a href="recitation-calculus-review.html" class="internal"><span class="title">Calculus Review</span></a></div></li></ul>
|
||||
</li>
|
||||
</ul></nav></div>
|
||||
<main class="ptx-main"><div id="ptx-content" class="ptx-content"><section class="chapter" id="ch-notes"><h2 class="heading hide-type">
|
||||
<span class="type">Chapter</span><span class="space heading-divison-mark heading-divison-mark__space"> </span><span class="codenumber"></span><span class="space heading-divison-mark heading-divison-mark__space"> </span><span class="title">Class Notes</span>
|
||||
</h2>
|
||||
<nav class="summary-links"><ul><li><a href="notes-week-01.html" class="internal"><span class="title">Week 1</span></a></li></ul></nav><div class="autopermalink" data-description="Chapter: Class Notes"><a href="#ch-notes" title="Copy heading and permalink for Chapter: Class Notes" aria-label="Copy heading and permalink for Chapter: Class Notes">🔗</a></div></section></div>
|
||||
<div id="ptx-content-footer" class="ptx-content-footer">
|
||||
<a class="previous-button button" href="syllabus.html" title="Previous"><span class="icon material-symbols-outlined" aria-hidden="true"></span><span class="name">Prev</span></a><a class="top-button button" href="#" title="Top"><span class="icon material-symbols-outlined" aria-hidden="true"></span><span class="name">Top</span></a><a class="next-button button" href="notes-week-01.html" title="Next"><span class="name">Next</span><span class="icon material-symbols-outlined" aria-hidden="true"></span></a>
|
||||
</div></main>
|
||||
</div>
|
||||
<div id="ptx-page-footer" class="ptx-page-footer">
|
||||
<a class="pretext-link" href="https://pretextbook.org" title="PreTeXt"><div class="logo"><svg xmlns="http://www.w3.org/2000/svg" height="100%" viewBox="338 3000 8772 6866" role="img"><title>PreTeXt logo</title><g style="stroke-width:.025in; stroke:currentColor; fill:none"><polyline points="472,3590 472,9732 " style="stroke-width:174; stroke-linejoin:miter; stroke-linecap:round; "></polyline><path style="stroke-width:126;stroke-linecap:butt;" d="M 4724,9448 A 4660 4660 0 0 1 8598 9259"></path><path style="stroke-width:174;stroke-linecap:butt;" d="M 4488,9685 A 4228 4228 0 0 0 472 9732"></path><path style="stroke-width:126;stroke-linecap:butt;" d="M 4724,3590 A 4241 4241 0 0 1 8598 3496"></path><path style="stroke-width:126;stroke-linecap:round;" d="M 850,3496 A 4241 4241 0 0 1 4724 3590"></path><path style="stroke-width:126;stroke-linecap:round;" d="M 850,9259 A 4507 4507 0 0 1 4724 9448"></path><polyline points="5385,4299 4062,8125" style="stroke-width:300; stroke-linejoin:miter; stroke-linecap:round;"></polyline><polyline points="8598,3496 8598,9259" style="stroke-width:126; stroke-linejoin:miter; stroke-linecap:round;"></polyline><polyline points="850,3496 850,9259" style="stroke-width:126; stroke-linejoin:miter; stroke-linecap:round;"></polyline><polyline points="4960,9685 4488,9685" style="stroke-width:174; stroke-linejoin:miter; stroke-linecap:round;"></polyline><polyline points="3070,4582 1889,6141 3070,7700" style="stroke-width:300; stroke-linejoin:miter; stroke-linecap:round;"></polyline><polyline points="6418,4582 7600,6141 6418,7700" style="stroke-width:300; stroke-linejoin:miter; stroke-linecap:round;"></polyline><polyline points="8976,3590 8976,9732" style="stroke-width:174; stroke-linejoin:miter; stroke-linecap:round;"></polyline><path style="stroke-width:174;stroke-linecap:butt;" d="M 4960,9685 A 4228 4228 0 0 1 8976 9732"></path></g></svg></div></a><a class="runestone-link" href="https://runestone.academy" title="Runestone Academy"><img class="logo" src="https://runestone.academy/runestone/static/images/RAIcon_cropped.png" alt="Runstone Academy logo"></a><a class="mathjax-link" href="https://www.mathjax.org" title="MathJax"><img class="logo" src="https://www.mathjax.org/badge/badge-square-2.png" alt="MathJax logo"></a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -101,7 +101,7 @@ eBookConfig.allow_pairs = false;
|
||||
eBookConfig.enableScratchAC = false;
|
||||
eBookConfig.build_info = "";
|
||||
eBookConfig.python3 = null;
|
||||
eBookConfig.runestone_version = '7.11.8';
|
||||
eBookConfig.runestone_version = '7.11.9';
|
||||
eBookConfig.jobehost = '';
|
||||
eBookConfig.proxyuri_runs = '';
|
||||
eBookConfig.proxyuri_files = '';
|
||||
@@ -162,6 +162,19 @@ eBookConfig.enable_chatcodes = false;
|
||||
</li></ul>
|
||||
</li>
|
||||
<li class="toc-item toc-chapter">
|
||||
<div class="toc-title-box"><a href="ch-notes.html" class="internal"><span class="title">Class Notes</span></a></div>
|
||||
<ul class="structural toc-item-list"><li class="toc-item toc-section">
|
||||
<div class="toc-title-box"><a href="notes-week-01.html" class="internal"><span class="title">Week 1</span></a></div>
|
||||
<ul class="structural toc-item-list"><li class="toc-item toc-subsection">
|
||||
<div class="toc-title-box"><a href="notes-week-01.html#notes-week-01-3" class="internal"><span class="title">Tuesday 1/13</span></a></div>
|
||||
<ul class="structural toc-item-list">
|
||||
<li class="toc-item toc-subsubsection"><div class="toc-title-box"><a href="notes-week-01.html#subsubsec-Sets" class="internal"><span class="title">Sec 1.1: Sets</span></a></div></li>
|
||||
<li class="toc-item toc-subsubsection"><div class="toc-title-box"><a href="notes-week-01.html#subsubsec-Probability" class="internal"><span class="title">Sec 1.2: Probability</span></a></div></li>
|
||||
</ul>
|
||||
</li></ul>
|
||||
</li></ul>
|
||||
</li>
|
||||
<li class="toc-item toc-chapter">
|
||||
<div class="toc-title-box"><a href="recitations.html" class="internal"><span class="title">Recitations</span></a></div>
|
||||
<ul class="structural toc-item-list"><li class="toc-item toc-worksheet"><div class="toc-title-box"><a href="recitation-calculus-review.html" class="internal"><span class="title">Calculus Review</span></a></div></li></ul>
|
||||
</li>
|
||||
|
||||
+14
-1
@@ -101,7 +101,7 @@ eBookConfig.allow_pairs = false;
|
||||
eBookConfig.enableScratchAC = false;
|
||||
eBookConfig.build_info = "";
|
||||
eBookConfig.python3 = null;
|
||||
eBookConfig.runestone_version = '7.11.8';
|
||||
eBookConfig.runestone_version = '7.11.9';
|
||||
eBookConfig.jobehost = '';
|
||||
eBookConfig.proxyuri_runs = '';
|
||||
eBookConfig.proxyuri_files = '';
|
||||
@@ -162,6 +162,19 @@ eBookConfig.enable_chatcodes = false;
|
||||
</li></ul>
|
||||
</li>
|
||||
<li class="toc-item toc-chapter">
|
||||
<div class="toc-title-box"><a href="ch-notes.html" class="internal"><span class="title">Class Notes</span></a></div>
|
||||
<ul class="structural toc-item-list"><li class="toc-item toc-section">
|
||||
<div class="toc-title-box"><a href="notes-week-01.html" class="internal"><span class="title">Week 1</span></a></div>
|
||||
<ul class="structural toc-item-list"><li class="toc-item toc-subsection">
|
||||
<div class="toc-title-box"><a href="notes-week-01.html#notes-week-01-3" class="internal"><span class="title">Tuesday 1/13</span></a></div>
|
||||
<ul class="structural toc-item-list">
|
||||
<li class="toc-item toc-subsubsection"><div class="toc-title-box"><a href="notes-week-01.html#subsubsec-Sets" class="internal"><span class="title">Sec 1.1: Sets</span></a></div></li>
|
||||
<li class="toc-item toc-subsubsection"><div class="toc-title-box"><a href="notes-week-01.html#subsubsec-Probability" class="internal"><span class="title">Sec 1.2: Probability</span></a></div></li>
|
||||
</ul>
|
||||
</li></ul>
|
||||
</li></ul>
|
||||
</li>
|
||||
<li class="toc-item toc-chapter">
|
||||
<div class="toc-title-box"><a href="recitations.html" class="internal"><span class="title">Recitations</span></a></div>
|
||||
<ul class="structural toc-item-list"><li class="toc-item toc-worksheet"><div class="toc-title-box"><a href="recitation-calculus-review.html" class="internal"><span class="title">Calculus Review</span></a></div></li></ul>
|
||||
</li>
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" version="1.1" width="245" height="206" viewBox="0 0 245 206">
|
||||
<defs>
|
||||
<clipPath id="clip_1">
|
||||
<path transform="matrix(1,0,0,-1.00005,122.56799,72.66783)" d="M21.26012 0C21.26012 39.13928-10.46764 70.86703-49.60692 70.86703-88.74622 70.86703-120.47397 39.13928-120.47397 0-120.47397-39.13928-88.74622-70.86703-49.60692-70.86703-10.46764-70.86703 21.26012-39.13928 21.26012 0ZM-49.60692 0"/>
|
||||
</clipPath>
|
||||
<clipPath id="clip_2">
|
||||
<path transform="matrix(1,0,0,-1.00005,122.56799,72.66783)" d="M120.47397 0C120.47397 39.13928 88.74622 70.86703 49.60692 70.86703 10.46764 70.86703-21.26012 39.13928-21.26012 0-21.26012-39.13928 10.46764-70.86703 49.60692-70.86703 88.74622-70.86703 120.47397-39.13928 120.47397 0ZM49.60692 0"/>
|
||||
</clipPath>
|
||||
<path id="font_3_1" d="M.1789856 .114868167C.13899231 .047973634 .099990848 .033981325 .055999757 .0309906 .04399109 .02999878 .034988405 .02999878 .034988405 .010986328 .034988405 .004989624 .039993287 0 .04798889 0 .07499695 0 .10598755 .0029907227 .13398743 .0029907227 .16699219 .0029907227 .20199585 0 .23399353 0 .23999024 0 .25299073 0 .25299073 .018997193 .25299073 .02999878 .24398804 .0309906 .23699951 .0309906 .21398926 .032989503 .18998719 .04096985 .18998719 .06593323 .18998719 .077926639 .19599915 .088897708 .20399475 .102890018L.27999879 .23068238H.5309906C.5329895 .2097168 .5469971 .07392883 .5469971 .063949588 .5469971 .033981325 .49499513 .0309906 .47499085 .0309906 .46099855 .0309906 .45098878 .0309906 .45098878 .010986328 .45098878 0 .46299745 0 .46499635 0 .5059967 0 .548996 .0029907227 .58999636 .0029907227 .61499026 .0029907227 .6779938 0 .7029877 0 .70899966 0 .72099307 0 .72099307 .019989014 .72099307 .0309906 .71099856 .0309906 .69799807 .0309906 .6359863 .0309906 .6359863 .037979127 .6329956 .06694031L.57199099 .691803C.56999209 .71180728 .56999209 .7158203 .5529938 .7158203 .5369873 .7158203 .5329895 .7088165 .5269928 .69880679L.1789856 .114868167M.29899598 .26168824 .49598695 .5917816 .5279999 .26168824H.29899598Z"/>
|
||||
<path id="font_3_2" d="M.1599884 .07795715C.1499939 .038986207 .147995 .0309906 .068984989 .0309906 .051986696 .0309906 .041992189 .0309906 .041992189 .010986328 .041992189 0 .050994874 0 .068984989 0H.42599488C.58399966 0 .70199587 .11791992 .70199587 .21585083 .70199587 .28778077 .6439972 .3457489 .5469971 .35673524 .651001 .37573243 .7559967 .44970704 .7559967 .5446625 .7559967 .6186218 .6899872 .6826172 .56999209 .6826172H.23399353C.21499634 .6826172 .20498657 .6826172 .20498657 .6626129 .20498657 .6516113 .21398926 .6516113 .23298645 .6516113 .23498535 .6516113 .2539978 .6516113 .2709961 .6496124 .2889862 .6476135 .2979889 .64660647 .2979889 .6336212 .2979889 .6296234 .29699708 .62661746 .2939911 .614624L.1599884 .07795715M.31098939 .36573792 .37298585 .6136322C.38198854 .64860537 .38398744 .6516113 .4269867 .6516113H.55599978C.6439972 .6516113 .6649933 .5926361 .6649933 .5486603 .6649933 .46069337 .57899478 .36573792 .45700074 .36573792H.31098939M.2659912 .0309906C.2519989 .0309906 .25 .0309906 .24398804 .03199768 .23399353 .032989503 .23098755 .033996583 .23098755 .04197693 .23098755 .04498291 .23098755 .04698181 .23599243 .064971927L.30499269 .34375H.49398805C.58999636 .34375 .60899356 .2698059 .60899356 .22683716 .60899356 .12791443 .519989 .0309906 .4019928 .0309906H.2659912Z"/>
|
||||
<path id="font_4_1" d="M.61099246 .37960816C.61099246 .5316467 .45799256 .59765627 .33299256 .59765627 .20298767 .59765627 .054992677 .5276489 .054992677 .38061524V.013534546C.054992677-.004470825 .054992677-.022460938 .07499695-.022460938 .09498596-.022460938 .09498596-.004470825 .09498596 .013534546V.37561036C.09498596 .52565 .2579956 .5576477 .33299256 .5576477 .37799073 .5576477 .44099427 .548645 .49398805 .5146332 .57099917 .4656372 .57099917 .40161134 .57099917 .37460328V.013534546C.57099917-.004470825 .57099917-.022460938 .59098818-.022460938 .61099246-.022460938 .61099246-.004470825 .61099246 .013534546V.37960816Z"/>
|
||||
</defs>
|
||||
<path transform="matrix(1,0,0,-1.00005,122.56799,72.66783)" d="M21.26012 0C21.26012 39.13928-10.46764 70.86703-49.60692 70.86703-88.74622 70.86703-120.47397 39.13928-120.47397 0-120.47397-39.13928-88.74622-70.86703-49.60692-70.86703-10.46764-70.86703 21.26012-39.13928 21.26012 0ZM-49.60692 0" fill="#d9d9d9"/>
|
||||
<path transform="matrix(1,0,0,-1.00005,122.56799,72.66783)" d="M120.47397 0C120.47397 39.13928 88.74622 70.86703 49.60692 70.86703 10.46764 70.86703-21.26012 39.13928-21.26012 0-21.26012-39.13928 10.46764-70.86703 49.60692-70.86703 88.74622-70.86703 120.47397-39.13928 120.47397 0ZM49.60692 0" fill="#d9d9d9"/>
|
||||
<g clip-path="url(#clip_1)">
|
||||
<g clip-path="url(#clip_2)">
|
||||
<path transform="matrix(1,0,0,-1.00005,122.56799,72.66783)" d="M21.26012 0C21.26012 39.13928-10.46764 70.86703-49.60692 70.86703-88.74622 70.86703-120.47397 39.13928-120.47397 0-120.47397-39.13928-88.74622-70.86703-49.60692-70.86703-10.46764-70.86703 21.26012-39.13928 21.26012 0ZM-49.60692 0" fill="#ffffff"/>
|
||||
</g>
|
||||
</g>
|
||||
<path transform="matrix(1,0,0,-1.00005,122.56799,72.66783)" stroke-width=".3985" stroke-linecap="butt" stroke-miterlimit="10" stroke-linejoin="miter" fill="none" stroke="#000000" d="M21.26012 0C21.26012 39.13928-10.46764 70.86703-49.60692 70.86703-88.74622 70.86703-120.47397 39.13928-120.47397 0-120.47397-39.13928-88.74622-70.86703-49.60692-70.86703-10.46764-70.86703 21.26012-39.13928 21.26012 0ZM-49.60692 0"/>
|
||||
<use data-text="A" xlink:href="#font_3_1" transform="matrix(9.9626,0,0,-9.963098,61.968996,76.07201)"/>
|
||||
<path transform="matrix(1,0,0,-1.00005,122.56799,72.66783)" stroke-width=".3985" stroke-linecap="butt" stroke-miterlimit="10" stroke-linejoin="miter" fill="none" stroke="#000000" d="M120.47397 0C120.47397 39.13928 88.74622 70.86703 49.60692 70.86703 10.46764 70.86703-21.26012 39.13928-21.26012 0-21.26012-39.13928 10.46764-70.86703 49.60692-70.86703 88.74622-70.86703 120.47397-39.13928 120.47397 0ZM49.60692 0"/>
|
||||
<use data-text="B" xlink:href="#font_3_2" transform="matrix(9.9626,0,0,-9.963098,175.69499,76.07201)"/>
|
||||
<use data-text="A" xlink:href="#font_3_1" transform="matrix(9.9626,0,0,-9.963098,109.269,203.63938)"/>
|
||||
<use data-text="∩" xlink:href="#font_4_1" transform="matrix(9.9626,0,0,-9.963098,118.955,203.63938)"/>
|
||||
<use data-text="B" xlink:href="#font_3_2" transform="matrix(9.9626,0,0,-9.963098,127.811008,203.63938)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.3 KiB |
@@ -0,0 +1,12 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" version="1.1" width="269" height="176" viewBox="0 0 269 176">
|
||||
<defs>
|
||||
<path id="font_1_1" d="M.1789856 .114868167C.13899231 .047973634 .099990848 .033981325 .055999757 .0309906 .04399109 .02999878 .034988405 .02999878 .034988405 .010986328 .034988405 .004989624 .039993287 0 .04798889 0 .07499695 0 .10598755 .0029907227 .13398743 .0029907227 .16699219 .0029907227 .20199585 0 .23399353 0 .23999024 0 .25299073 0 .25299073 .018997193 .25299073 .02999878 .24398804 .0309906 .23699951 .0309906 .21398926 .032989503 .18998719 .04096985 .18998719 .06593323 .18998719 .077926639 .19599915 .088897708 .20399475 .102890018L.27999879 .23068238H.5309906C.5329895 .2097168 .5469971 .07392883 .5469971 .063949588 .5469971 .033981325 .49499513 .0309906 .47499085 .0309906 .46099855 .0309906 .45098878 .0309906 .45098878 .010986328 .45098878 0 .46299745 0 .46499635 0 .5059967 0 .548996 .0029907227 .58999636 .0029907227 .61499026 .0029907227 .6779938 0 .7029877 0 .70899966 0 .72099307 0 .72099307 .019989014 .72099307 .0309906 .71099856 .0309906 .69799807 .0309906 .6359863 .0309906 .6359863 .037979127 .6329956 .06694031L.57199099 .691803C.56999209 .71180728 .56999209 .7158203 .5529938 .7158203 .5369873 .7158203 .5329895 .7088165 .5269928 .69880679L.1789856 .114868167M.29899598 .26168824 .49598695 .5917816 .5279999 .26168824H.29899598Z"/>
|
||||
<path id="font_2_1" d="M.6769867 .16200257H.6519928C.6469879 .13600159 .6429901 .11299133 .6339874 .08898926 .6289978 .07299805 .6259918 .06498718 .56799319 .06498718H.48799134C.5009918 .1210022 .5319977 .16900635 .57699587 .23501587 .6239929 .306015 .6649933 .37503053 .6649933 .45404054 .6649933 .59306338 .5299988 .7050781 .36099244 .7050781 .18899536 .7050781 .055999757 .5910492 .055999757 .45404054 .055999757 .37503053 .09700012 .306015 .14399719 .23501587 .18798828 .16900635 .21998596 .1210022 .23298645 .06498718H.15299988C.09498596 .06498718 .09199524 .07299805 .08699036 .08799744 .07899475 .11099243 .07398987 .13700867 .068984989 .16200257H.04399109L.07699585 0H.23699951C.25898744 0 .2619934 0 .2619934 .020996094 .2619934 .09098816 .23098755 .17900086 .20799256 .24301148 .18699646 .3010254 .15899658 .3800354 .15899658 .45503236 .15899658 .6150665 .2689972 .68307498 .3600006 .68307498 .45599366 .68307498 .56199648 .61105349 .56199648 .45503236 .56199648 .3800354 .5349884 .3040161 .5059967 .22201538 .4909973 .18000794 .45899964 .08999634 .45899964 .020996094 .45899964 0 .46199037 0 .4850006 0H.6439972L.6769867 .16200257Z"/>
|
||||
</defs>
|
||||
<path transform="matrix(1.00002,0,0,-1.00005,115.48393,89.36787)" d="M-113.38724-85.04042V85.04042H141.73404V-85.04042ZM141.73404 85.04042" fill="#d9d9d9"/>
|
||||
<path transform="matrix(1.00002,0,0,-1.00005,115.48393,89.36787)" d="M70.86703 0C70.86703 39.13928 39.13928 70.86703 0 70.86703-39.13928 70.86703-70.86703 39.13928-70.86703 0-70.86703-39.13928-39.13928-70.86703 0-70.86703 39.13928-70.86703 70.86703-39.13928 70.86703 0ZM0 0" fill="#ffffff"/>
|
||||
<path transform="matrix(1.00002,0,0,-1.00005,115.48393,89.36787)" stroke-width=".3985" stroke-linecap="butt" stroke-miterlimit="10" stroke-linejoin="miter" fill="none" stroke="#000000" d="M70.86703 0C70.86703 39.13928 39.13928 70.86703 0 70.86703-39.13928 70.86703-70.86703 39.13928-70.86703 0-70.86703-39.13928-39.13928-70.86703 0-70.86703 39.13928-70.86703 70.86703-39.13928 70.86703 0ZM0 0"/>
|
||||
<use data-text="A" xlink:href="#font_1_1" transform="matrix(9.962799,0,0,-9.963098,111.747859,92.77203)"/>
|
||||
<path transform="matrix(1.00002,0,0,-1.00005,115.48393,89.36787)" stroke-width=".3985" stroke-linecap="butt" stroke-miterlimit="10" stroke-linejoin="miter" fill="none" stroke="#000000" d="M-113.38724-85.04042V85.04042H141.73404V-85.04042ZM141.73404 85.04042"/>
|
||||
<use data-text="Ω" xlink:href="#font_2_1" transform="matrix(9.962799,0,0,-9.963098,260.74085,7.727791)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
@@ -0,0 +1,12 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" version="1.1" width="245" height="145" viewBox="0 0 245 145">
|
||||
<defs>
|
||||
<path id="font_1_1" d="M.1789856 .114868167C.13899231 .047973634 .099990848 .033981325 .055999757 .0309906 .04399109 .02999878 .034988405 .02999878 .034988405 .010986328 .034988405 .004989624 .039993287 0 .04798889 0 .07499695 0 .10598755 .0029907227 .13398743 .0029907227 .16699219 .0029907227 .20199585 0 .23399353 0 .23999024 0 .25299073 0 .25299073 .018997193 .25299073 .02999878 .24398804 .0309906 .23699951 .0309906 .21398926 .032989503 .18998719 .04096985 .18998719 .06593323 .18998719 .077926639 .19599915 .088897708 .20399475 .102890018L.27999879 .23068238H.5309906C.5329895 .2097168 .5469971 .07392883 .5469971 .063949588 .5469971 .033981325 .49499513 .0309906 .47499085 .0309906 .46099855 .0309906 .45098878 .0309906 .45098878 .010986328 .45098878 0 .46299745 0 .46499635 0 .5059967 0 .548996 .0029907227 .58999636 .0029907227 .61499026 .0029907227 .6779938 0 .7029877 0 .70899966 0 .72099307 0 .72099307 .019989014 .72099307 .0309906 .71099856 .0309906 .69799807 .0309906 .6359863 .0309906 .6359863 .037979127 .6329956 .06694031L.57199099 .691803C.56999209 .71180728 .56999209 .7158203 .5529938 .7158203 .5369873 .7158203 .5329895 .7088165 .5269928 .69880679L.1789856 .114868167M.29899598 .26168824 .49598695 .5917816 .5279999 .26168824H.29899598Z"/>
|
||||
<path id="font_1_2" d="M.1599884 .07795715C.1499939 .038986207 .147995 .0309906 .068984989 .0309906 .051986696 .0309906 .041992189 .0309906 .041992189 .010986328 .041992189 0 .050994874 0 .068984989 0H.42599488C.58399966 0 .70199587 .11791992 .70199587 .21585083 .70199587 .28778077 .6439972 .3457489 .5469971 .35673524 .651001 .37573243 .7559967 .44970704 .7559967 .5446625 .7559967 .6186218 .6899872 .6826172 .56999209 .6826172H.23399353C.21499634 .6826172 .20498657 .6826172 .20498657 .6626129 .20498657 .6516113 .21398926 .6516113 .23298645 .6516113 .23498535 .6516113 .2539978 .6516113 .2709961 .6496124 .2889862 .6476135 .2979889 .64660647 .2979889 .6336212 .2979889 .6296234 .29699708 .62661746 .2939911 .614624L.1599884 .07795715M.31098939 .36573792 .37298585 .6136322C.38198854 .64860537 .38398744 .6516113 .4269867 .6516113H.55599978C.6439972 .6516113 .6649933 .5926361 .6649933 .5486603 .6649933 .46069337 .57899478 .36573792 .45700074 .36573792H.31098939M.2659912 .0309906C.2519989 .0309906 .25 .0309906 .24398804 .03199768 .23399353 .032989503 .23098755 .033996583 .23098755 .04197693 .23098755 .04498291 .23098755 .04698181 .23599243 .064971927L.30499269 .34375H.49398805C.58999636 .34375 .60899356 .2698059 .60899356 .22683716 .60899356 .12791443 .519989 .0309906 .4019928 .0309906H.2659912Z"/>
|
||||
</defs>
|
||||
<path transform="matrix(1,0,0,-1.00005,122.56799,72.66883)" d="M21.26012 0C21.26012 39.13928-10.46764 70.86703-49.60692 70.86703-88.74622 70.86703-120.47397 39.13928-120.47397 0-120.47397-39.13928-88.74622-70.86703-49.60692-70.86703-10.46764-70.86703 21.26012-39.13928 21.26012 0ZM-49.60692 0" fill="#d9d9d9"/>
|
||||
<path transform="matrix(1,0,0,-1.00005,122.56799,72.66883)" d="M120.47397 0C120.47397 39.13928 88.74622 70.86703 49.60692 70.86703 10.46764 70.86703-21.26012 39.13928-21.26012 0-21.26012-39.13928 10.46764-70.86703 49.60692-70.86703 88.74622-70.86703 120.47397-39.13928 120.47397 0ZM49.60692 0" fill="#ffffff"/>
|
||||
<path transform="matrix(1,0,0,-1.00005,122.56799,72.66883)" stroke-width=".3985" stroke-linecap="butt" stroke-miterlimit="10" stroke-linejoin="miter" fill="none" stroke="#000000" d="M21.26012 0C21.26012 39.13928-10.46764 70.86703-49.60692 70.86703-88.74622 70.86703-120.47397 39.13928-120.47397 0-120.47397-39.13928-88.74622-70.86703-49.60692-70.86703-10.46764-70.86703 21.26012-39.13928 21.26012 0ZM-49.60692 0"/>
|
||||
<use data-text="A" xlink:href="#font_1_1" transform="matrix(9.9626,0,0,-9.963098,61.968996,76.073)"/>
|
||||
<path transform="matrix(1,0,0,-1.00005,122.56799,72.66883)" stroke-width=".3985" stroke-linecap="butt" stroke-miterlimit="10" stroke-linejoin="miter" fill="none" stroke="#000000" d="M120.47397 0C120.47397 39.13928 88.74622 70.86703 49.60692 70.86703 10.46764 70.86703-21.26012 39.13928-21.26012 0-21.26012-39.13928 10.46764-70.86703 49.60692-70.86703 88.74622-70.86703 120.47397-39.13928 120.47397 0ZM49.60692 0"/>
|
||||
<use data-text="B" xlink:href="#font_1_2" transform="matrix(9.9626,0,0,-9.963098,175.69499,76.073)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.4 KiB |
@@ -0,0 +1,23 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" version="1.1" width="245" height="145" viewBox="0 0 245 145">
|
||||
<defs>
|
||||
<clipPath id="clip_1">
|
||||
<path transform="matrix(1,0,0,-1.00005,122.56799,72.66883)" d="M21.26012 0C21.26012 39.13928-10.46764 70.86703-49.60692 70.86703-88.74622 70.86703-120.47397 39.13928-120.47397 0-120.47397-39.13928-88.74622-70.86703-49.60692-70.86703-10.46764-70.86703 21.26012-39.13928 21.26012 0ZM-49.60692 0"/>
|
||||
</clipPath>
|
||||
<clipPath id="clip_2">
|
||||
<path transform="matrix(1,0,0,-1.00005,122.56799,72.66883)" d="M120.47397 0C120.47397 39.13928 88.74622 70.86703 49.60692 70.86703 10.46764 70.86703-21.26012 39.13928-21.26012 0-21.26012-39.13928 10.46764-70.86703 49.60692-70.86703 88.74622-70.86703 120.47397-39.13928 120.47397 0ZM49.60692 0"/>
|
||||
</clipPath>
|
||||
<path id="font_3_1" d="M.1789856 .114868167C.13899231 .047973634 .099990848 .033981325 .055999757 .0309906 .04399109 .02999878 .034988405 .02999878 .034988405 .010986328 .034988405 .004989624 .039993287 0 .04798889 0 .07499695 0 .10598755 .0029907227 .13398743 .0029907227 .16699219 .0029907227 .20199585 0 .23399353 0 .23999024 0 .25299073 0 .25299073 .018997193 .25299073 .02999878 .24398804 .0309906 .23699951 .0309906 .21398926 .032989503 .18998719 .04096985 .18998719 .06593323 .18998719 .077926639 .19599915 .088897708 .20399475 .102890018L.27999879 .23068238H.5309906C.5329895 .2097168 .5469971 .07392883 .5469971 .063949588 .5469971 .033981325 .49499513 .0309906 .47499085 .0309906 .46099855 .0309906 .45098878 .0309906 .45098878 .010986328 .45098878 0 .46299745 0 .46499635 0 .5059967 0 .548996 .0029907227 .58999636 .0029907227 .61499026 .0029907227 .6779938 0 .7029877 0 .70899966 0 .72099307 0 .72099307 .019989014 .72099307 .0309906 .71099856 .0309906 .69799807 .0309906 .6359863 .0309906 .6359863 .037979127 .6329956 .06694031L.57199099 .691803C.56999209 .71180728 .56999209 .7158203 .5529938 .7158203 .5369873 .7158203 .5329895 .7088165 .5269928 .69880679L.1789856 .114868167M.29899598 .26168824 .49598695 .5917816 .5279999 .26168824H.29899598Z"/>
|
||||
<path id="font_3_2" d="M.1599884 .07795715C.1499939 .038986207 .147995 .0309906 .068984989 .0309906 .051986696 .0309906 .041992189 .0309906 .041992189 .010986328 .041992189 0 .050994874 0 .068984989 0H.42599488C.58399966 0 .70199587 .11791992 .70199587 .21585083 .70199587 .28778077 .6439972 .3457489 .5469971 .35673524 .651001 .37573243 .7559967 .44970704 .7559967 .5446625 .7559967 .6186218 .6899872 .6826172 .56999209 .6826172H.23399353C.21499634 .6826172 .20498657 .6826172 .20498657 .6626129 .20498657 .6516113 .21398926 .6516113 .23298645 .6516113 .23498535 .6516113 .2539978 .6516113 .2709961 .6496124 .2889862 .6476135 .2979889 .64660647 .2979889 .6336212 .2979889 .6296234 .29699708 .62661746 .2939911 .614624L.1599884 .07795715M.31098939 .36573792 .37298585 .6136322C.38198854 .64860537 .38398744 .6516113 .4269867 .6516113H.55599978C.6439972 .6516113 .6649933 .5926361 .6649933 .5486603 .6649933 .46069337 .57899478 .36573792 .45700074 .36573792H.31098939M.2659912 .0309906C.2519989 .0309906 .25 .0309906 .24398804 .03199768 .23399353 .032989503 .23098755 .033996583 .23098755 .04197693 .23098755 .04498291 .23098755 .04698181 .23599243 .064971927L.30499269 .34375H.49398805C.58999636 .34375 .60899356 .2698059 .60899356 .22683716 .60899356 .12791443 .519989 .0309906 .4019928 .0309906H.2659912Z"/>
|
||||
</defs>
|
||||
<path transform="matrix(1,0,0,-1.00005,122.56799,72.66883)" d="M21.26012 0C21.26012 39.13928-10.46764 70.86703-49.60692 70.86703-88.74622 70.86703-120.47397 39.13928-120.47397 0-120.47397-39.13928-88.74622-70.86703-49.60692-70.86703-10.46764-70.86703 21.26012-39.13928 21.26012 0ZM-49.60692 0" fill="#ffffff"/>
|
||||
<path transform="matrix(1,0,0,-1.00005,122.56799,72.66883)" d="M120.47397 0C120.47397 39.13928 88.74622 70.86703 49.60692 70.86703 10.46764 70.86703-21.26012 39.13928-21.26012 0-21.26012-39.13928 10.46764-70.86703 49.60692-70.86703 88.74622-70.86703 120.47397-39.13928 120.47397 0ZM49.60692 0" fill="#ffffff"/>
|
||||
<g clip-path="url(#clip_1)">
|
||||
<g clip-path="url(#clip_2)">
|
||||
<path transform="matrix(1,0,0,-1.00005,122.56799,72.66883)" d="M21.26012 0C21.26012 39.13928-10.46764 70.86703-49.60692 70.86703-88.74622 70.86703-120.47397 39.13928-120.47397 0-120.47397-39.13928-88.74622-70.86703-49.60692-70.86703-10.46764-70.86703 21.26012-39.13928 21.26012 0ZM-49.60692 0" fill="#d9d9d9"/>
|
||||
</g>
|
||||
</g>
|
||||
<path transform="matrix(1,0,0,-1.00005,122.56799,72.66883)" stroke-width=".3985" stroke-linecap="butt" stroke-miterlimit="10" stroke-linejoin="miter" fill="none" stroke="#000000" d="M21.26012 0C21.26012 39.13928-10.46764 70.86703-49.60692 70.86703-88.74622 70.86703-120.47397 39.13928-120.47397 0-120.47397-39.13928-88.74622-70.86703-49.60692-70.86703-10.46764-70.86703 21.26012-39.13928 21.26012 0ZM-49.60692 0"/>
|
||||
<use data-text="A" xlink:href="#font_3_1" transform="matrix(9.9626,0,0,-9.963098,61.968996,76.073)"/>
|
||||
<path transform="matrix(1,0,0,-1.00005,122.56799,72.66883)" stroke-width=".3985" stroke-linecap="butt" stroke-miterlimit="10" stroke-linejoin="miter" fill="none" stroke="#000000" d="M120.47397 0C120.47397 39.13928 88.74622 70.86703 49.60692 70.86703 10.46764 70.86703-21.26012 39.13928-21.26012 0-21.26012-39.13928 10.46764-70.86703 49.60692-70.86703 88.74622-70.86703 120.47397-39.13928 120.47397 0ZM49.60692 0"/>
|
||||
<use data-text="B" xlink:href="#font_3_2" transform="matrix(9.9626,0,0,-9.963098,175.69499,76.073)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.4 KiB |
@@ -0,0 +1,12 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" version="1.1" width="245" height="145" viewBox="0 0 245 145">
|
||||
<defs>
|
||||
<path id="font_1_1" d="M.1789856 .114868167C.13899231 .047973634 .099990848 .033981325 .055999757 .0309906 .04399109 .02999878 .034988405 .02999878 .034988405 .010986328 .034988405 .004989624 .039993287 0 .04798889 0 .07499695 0 .10598755 .0029907227 .13398743 .0029907227 .16699219 .0029907227 .20199585 0 .23399353 0 .23999024 0 .25299073 0 .25299073 .018997193 .25299073 .02999878 .24398804 .0309906 .23699951 .0309906 .21398926 .032989503 .18998719 .04096985 .18998719 .06593323 .18998719 .077926639 .19599915 .088897708 .20399475 .102890018L.27999879 .23068238H.5309906C.5329895 .2097168 .5469971 .07392883 .5469971 .063949588 .5469971 .033981325 .49499513 .0309906 .47499085 .0309906 .46099855 .0309906 .45098878 .0309906 .45098878 .010986328 .45098878 0 .46299745 0 .46499635 0 .5059967 0 .548996 .0029907227 .58999636 .0029907227 .61499026 .0029907227 .6779938 0 .7029877 0 .70899966 0 .72099307 0 .72099307 .019989014 .72099307 .0309906 .71099856 .0309906 .69799807 .0309906 .6359863 .0309906 .6359863 .037979127 .6329956 .06694031L.57199099 .691803C.56999209 .71180728 .56999209 .7158203 .5529938 .7158203 .5369873 .7158203 .5329895 .7088165 .5269928 .69880679L.1789856 .114868167M.29899598 .26168824 .49598695 .5917816 .5279999 .26168824H.29899598Z"/>
|
||||
<path id="font_1_2" d="M.1599884 .07795715C.1499939 .038986207 .147995 .0309906 .068984989 .0309906 .051986696 .0309906 .041992189 .0309906 .041992189 .010986328 .041992189 0 .050994874 0 .068984989 0H.42599488C.58399966 0 .70199587 .11791992 .70199587 .21585083 .70199587 .28778077 .6439972 .3457489 .5469971 .35673524 .651001 .37573243 .7559967 .44970704 .7559967 .5446625 .7559967 .6186218 .6899872 .6826172 .56999209 .6826172H.23399353C.21499634 .6826172 .20498657 .6826172 .20498657 .6626129 .20498657 .6516113 .21398926 .6516113 .23298645 .6516113 .23498535 .6516113 .2539978 .6516113 .2709961 .6496124 .2889862 .6476135 .2979889 .64660647 .2979889 .6336212 .2979889 .6296234 .29699708 .62661746 .2939911 .614624L.1599884 .07795715M.31098939 .36573792 .37298585 .6136322C.38198854 .64860537 .38398744 .6516113 .4269867 .6516113H.55599978C.6439972 .6516113 .6649933 .5926361 .6649933 .5486603 .6649933 .46069337 .57899478 .36573792 .45700074 .36573792H.31098939M.2659912 .0309906C.2519989 .0309906 .25 .0309906 .24398804 .03199768 .23399353 .032989503 .23098755 .033996583 .23098755 .04197693 .23098755 .04498291 .23098755 .04698181 .23599243 .064971927L.30499269 .34375H.49398805C.58999636 .34375 .60899356 .2698059 .60899356 .22683716 .60899356 .12791443 .519989 .0309906 .4019928 .0309906H.2659912Z"/>
|
||||
</defs>
|
||||
<path transform="matrix(1,0,0,-1.00005,122.56799,72.66883)" d="M21.26012 0C21.26012 39.13928-10.46764 70.86703-49.60692 70.86703-88.74622 70.86703-120.47397 39.13928-120.47397 0-120.47397-39.13928-88.74622-70.86703-49.60692-70.86703-10.46764-70.86703 21.26012-39.13928 21.26012 0ZM-49.60692 0" fill="#d9d9d9"/>
|
||||
<path transform="matrix(1,0,0,-1.00005,122.56799,72.66883)" d="M120.47397 0C120.47397 39.13928 88.74622 70.86703 49.60692 70.86703 10.46764 70.86703-21.26012 39.13928-21.26012 0-21.26012-39.13928 10.46764-70.86703 49.60692-70.86703 88.74622-70.86703 120.47397-39.13928 120.47397 0ZM49.60692 0" fill="#d9d9d9"/>
|
||||
<path transform="matrix(1,0,0,-1.00005,122.56799,72.66883)" stroke-width=".3985" stroke-linecap="butt" stroke-miterlimit="10" stroke-linejoin="miter" fill="none" stroke="#000000" d="M21.26012 0C21.26012 39.13928-10.46764 70.86703-49.60692 70.86703-88.74622 70.86703-120.47397 39.13928-120.47397 0-120.47397-39.13928-88.74622-70.86703-49.60692-70.86703-10.46764-70.86703 21.26012-39.13928 21.26012 0ZM-49.60692 0"/>
|
||||
<use data-text="A" xlink:href="#font_1_1" transform="matrix(9.9626,0,0,-9.963098,61.968996,76.073)"/>
|
||||
<path transform="matrix(1,0,0,-1.00005,122.56799,72.66883)" stroke-width=".3985" stroke-linecap="butt" stroke-miterlimit="10" stroke-linejoin="miter" fill="none" stroke="#000000" d="M120.47397 0C120.47397 39.13928 88.74622 70.86703 49.60692 70.86703 10.46764 70.86703-21.26012 39.13928-21.26012 0-21.26012-39.13928 10.46764-70.86703 49.60692-70.86703 88.74622-70.86703 120.47397-39.13928 120.47397 0ZM49.60692 0"/>
|
||||
<use data-text="B" xlink:href="#font_1_2" transform="matrix(9.9626,0,0,-9.963098,175.69499,76.073)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.4 KiB |
+1
-1
@@ -2,7 +2,7 @@
|
||||
<html>
|
||||
<!--********************************************-->
|
||||
<!--* Generated from PreTeXt source *-->
|
||||
<!--* on 2026-01-11T13:28:24-05:00 *-->
|
||||
<!--* on 2026-01-13T22:25:29-05:00 *-->
|
||||
<!--* A recent stable commit (2022-07-01): *-->
|
||||
<!--* 6c761d3dba23af92cba35001c852aac04ae99a5f *-->
|
||||
<!--* *-->
|
||||
|
||||
File diff suppressed because one or more lines are too long
Executable
+501
@@ -0,0 +1,501 @@
|
||||
<!DOCTYPE html>
|
||||
<!--********************************************-->
|
||||
<!--* Generated from PreTeXt source *-->
|
||||
<!--* *-->
|
||||
<!--* https://pretextbook.org *-->
|
||||
<!--* *-->
|
||||
<!--********************************************-->
|
||||
<html lang="en-US" dir="ltr">
|
||||
<!--******************************************-->
|
||||
<!--* Authored with PreTeXt *-->
|
||||
<!--* pretextbook.org *-->
|
||||
<!--* Theme: boulder *-->
|
||||
<!--* Palette: *-->
|
||||
<!--******************************************-->
|
||||
<head xmlns:og="http://ogp.me/ns#" xmlns:book="https://ogp.me/ns/book#">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<title>Week 1</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0">
|
||||
<link href="_static/pretext/css/theme.css" rel="stylesheet" type="text/css">
|
||||
<script>
|
||||
var runestoneMathReady = new Promise((resolve) => window.rsMathReady = resolve);
|
||||
window.MathJax = {
|
||||
"tex": {
|
||||
"inlineMath": [
|
||||
[
|
||||
"\\(",
|
||||
"\\)"
|
||||
]
|
||||
],
|
||||
"tags": "none",
|
||||
"tagSide": "right",
|
||||
"tagIndent": ".8em",
|
||||
"packages": {
|
||||
"[+]": [
|
||||
"base",
|
||||
"extpfeil",
|
||||
"ams",
|
||||
"amscd",
|
||||
"color",
|
||||
"newcommand",
|
||||
"knowl"
|
||||
]
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"ignoreHtmlClass": "tex2jax_ignore|ignore-math",
|
||||
"processHtmlClass": "process-math"
|
||||
},
|
||||
"chtml": {
|
||||
"scale": 0.98,
|
||||
"mtextInheritFont": true
|
||||
},
|
||||
"loader": {
|
||||
"load": [
|
||||
"input/asciimath",
|
||||
"[tex]/extpfeil",
|
||||
"[tex]/amscd",
|
||||
"[tex]/color",
|
||||
"[tex]/newcommand",
|
||||
"[pretext]/mathjaxknowl3.js"
|
||||
],
|
||||
"paths": {
|
||||
"pretext": "_static/pretext/js/lib"
|
||||
}
|
||||
},
|
||||
"startup": {
|
||||
pageReady() {
|
||||
return MathJax.startup.defaultPageReady().then(function () {
|
||||
console.log("in ready function");
|
||||
rsMathReady();
|
||||
}
|
||||
)}
|
||||
}
|
||||
};
|
||||
</script><script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js"></script><meta name="Keywords" content="Authored in PreTeXt">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta property="og:type" content="book">
|
||||
<meta property="book:title" content="Introduction to Probability and Statistics for the Life Sciences">
|
||||
<meta property="book:author" content="Andy Eisenberg">
|
||||
<script src="_static/pretext/js/lib/jquery.min.js"></script><script src="_static/pretext/js/lib/jquery.sticky.js"></script><script src="_static/pretext/js/lib/jquery.espy.min.js"></script><script src="_static/pretext/js/pretext.js"></script><script src="_static/pretext/js/pretext_add_on.js?x=1"></script><script src="_static/pretext/js/user_preferences.js"></script><!--** eBookCongig is necessary to configure interactive **-->
|
||||
<!--** Runestone components to run locally in reader's browser **-->
|
||||
<!--** No external communication: **-->
|
||||
<!--** log level is 0, Runestone Services are disabled **-->
|
||||
<script type="text/javascript">
|
||||
eBookConfig = {};
|
||||
eBookConfig.useRunestoneServices = false;
|
||||
eBookConfig.host = 'http://127.0.0.1:8000';
|
||||
eBookConfig.course = 'PTX_Course_Title_Here';
|
||||
eBookConfig.basecourse = 'PTX_Base_Course';
|
||||
eBookConfig.isLoggedIn = false;
|
||||
eBookConfig.email = '';
|
||||
eBookConfig.isInstructor = false;
|
||||
eBookConfig.logLevel = 0;
|
||||
eBookConfig.username = '';
|
||||
eBookConfig.readings = null;
|
||||
eBookConfig.activities = null;
|
||||
eBookConfig.downloadsEnabled = false;
|
||||
eBookConfig.allow_pairs = false;
|
||||
eBookConfig.enableScratchAC = false;
|
||||
eBookConfig.build_info = "";
|
||||
eBookConfig.python3 = null;
|
||||
eBookConfig.runestone_version = '7.11.9';
|
||||
eBookConfig.jobehost = '';
|
||||
eBookConfig.proxyuri_runs = '';
|
||||
eBookConfig.proxyuri_files = '';
|
||||
eBookConfig.enable_chatcodes = false;
|
||||
</script>
|
||||
<!--*** Runestone Services ***-->
|
||||
<script src="_static/prefix-runtime.991c135ccf182bf5.bundle.js"></script><script src="_static/prefix-723.3e6434f80549315a.bundle.js"></script><script src="_static/prefix-runestone.1c010553c35c544c.bundle.js"></script><link rel="stylesheet" type="text/css" href="_static/prefix-723.3bccd435914aa0ff.css">
|
||||
<link rel="stylesheet" type="text/css" href="_static/prefix-runestone.677a75a9120449ff.css">
|
||||
<script src="_static/pretext/js/lti_iframe_resizer.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/lunr.js/2.3.9/lunr.min.js" integrity="sha512-4xUl/d6D6THrAnXAwGajXkoWaeMNwEKK4iNfq5DotEbLPAfk6FSxSP3ydNxqDgCw1c/0Z1Jg6L8h2j+++9BZmg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script><script src="lunr-pretext-search-index.js" async=""></script><script src="_static/pretext/js/pretext_search.js"></script><script src="_static/pretext/js/lib/knowl.js"></script><!--knowl.js code controls Sage Cells within knowls--><script>sagecellEvalName='Evaluate (Sage)';
|
||||
</script>
|
||||
</head>
|
||||
<body class="pretext book ignore-math">
|
||||
<a class="assistive" href="#ptx-content">Skip to main content</a><header id="ptx-masthead" class="ptx-masthead"><div class="ptx-banner"><div class="title-container">
|
||||
<h1 class="heading"><a href="root-1-2.html"><span class="title">Introduction to Probability and Statistics for the Life Sciences</span> <span class="subtitle">Spring 2026 MATH 1044</span></a></h1>
|
||||
<p class="byline">Andy Eisenberg</p>
|
||||
</div></div></header><nav id="ptx-navbar" class="ptx-navbar navbar"><div class="ptx-navbar-contents">
|
||||
<button class="toc-toggle button" title="Contents"><span class="icon material-symbols-outlined" aria-hidden="true"></span><span class="name">Contents</span></button><div class="searchbox">
|
||||
<div class="searchwidget"><button id="searchbutton" class="searchbutton button" type="button" title="Search book"><span class="icon material-symbols-outlined" aria-hidden="true"></span><span class="name">Search Book</span></button></div>
|
||||
<div id="searchresultsplaceholder" class="searchresultsplaceholder" style="display: none">
|
||||
<div class="search-results-controls">
|
||||
<input aria-label="Search term" id="ptxsearch" class="ptxsearch" type="text" name="terms" placeholder="Search term"><button title="Close search" id="closesearchresults" class="closesearchresults"><span class="material-symbols-outlined">close</span></button>
|
||||
</div>
|
||||
<h2 class="search-results-heading">Search Results: </h2>
|
||||
<div id="searchempty" class="searchempty"><span>No results.</span></div>
|
||||
<ol id="searchresults" class="searchresults"></ol>
|
||||
</div>
|
||||
</div>
|
||||
<span class="nav-other-controls"><button id="embed-button" class="embed-button button" title="Embed this page"><span class="icon material-symbols-outlined" aria-hidden="true"></span><span class="name">Embed</span></button><div class="embed-popup hidden" id="embed-popup">
|
||||
<p>Copy the code below to embed this page in your own website or LMS page.</p>
|
||||
<div class="embed-code-container">
|
||||
<textarea class="embed-code-textbox" id="embed-code-textbox" readonly aria-label="textbox"><iframe src="https://example.com/embed" width="100%" height="1000"></iframe></textarea><button class="copy-embed-button button" id="copy-embed-button" title="Copy embed code"><span class="icon material-symbols-outlined" aria-hidden="true"></span><span class="name">Copy</span></button>
|
||||
</div>
|
||||
</div>
|
||||
<button id="light-dark-button" class="light-dark-button button" title="Dark Mode"><span class="icon material-symbols-outlined" aria-hidden="true"></span><span class="name">Dark Mode</span></button></span><span class="treebuttons"><a class="previous-button button" href="ch-notes.html" title="Previous"><span class="icon material-symbols-outlined" aria-hidden="true"></span><span class="name">Prev</span></a><a class="up-button button" href="ch-notes.html" title="Up"><span class="icon material-symbols-outlined" aria-hidden="true"></span><span class="name">Up</span></a><a class="next-button button" href="recitations.html" title="Next"><span class="name">Next</span><span class="icon material-symbols-outlined" aria-hidden="true"></span></a></span>
|
||||
</div></nav><div id="latex-macros" class="hidden-content process-math" style="display:none"><span class="process-math">\(\newcommand{\N}{\mathbb N}
|
||||
\newcommand{\Z}{\mathbb Z}
|
||||
\newcommand{\Q}{\mathbb Q}
|
||||
\newcommand{\R}{\mathbb R}
|
||||
\newcommand{\lt}{<}
|
||||
\newcommand{\gt}{>}
|
||||
\newcommand{\amp}{&}
|
||||
\definecolor{fillinmathshade}{gray}{0.9}
|
||||
\newcommand{\fillinmath}[1]{\mathchoice{\colorbox{fillinmathshade}{$\displaystyle \phantom{\,#1\,}$}}{\colorbox{fillinmathshade}{$\textstyle \phantom{\,#1\,}$}}{\colorbox{fillinmathshade}{$\scriptstyle \phantom{\,#1\,}$}}{\colorbox{fillinmathshade}{$\scriptscriptstyle\phantom{\,#1\,}$}}}
|
||||
\)</span></div>
|
||||
<div class="ptx-page">
|
||||
<div id="ptx-sidebar" class="ptx-sidebar"><nav id="ptx-toc" class="ptx-toc depth2"><ul class="structural toc-item-list contains-active">
|
||||
<li class="toc-item toc-frontmatter"><div class="toc-title-box"><a href="frontmatter.html" class="internal"><span class="title">Front Matter</span></a></div></li>
|
||||
<li class="toc-item toc-chapter">
|
||||
<div class="toc-title-box"><a href="course-documents.html" class="internal"><span class="title">Course Documents</span></a></div>
|
||||
<ul class="structural toc-item-list"><li class="toc-item toc-section">
|
||||
<div class="toc-title-box"><a href="syllabus.html" class="internal"><span class="title">Syllabus</span></a></div>
|
||||
<ul class="structural toc-item-list">
|
||||
<li class="toc-item toc-subsection"><div class="toc-title-box"><a href="syllabus.html#sec-course-info" class="internal"><span class="title">Course Information</span></a></div></li>
|
||||
<li class="toc-item toc-subsection"><div class="toc-title-box"><a href="syllabus.html#sec-materials" class="internal"><span class="title">Textbook and Other Required Materials</span></a></div></li>
|
||||
<li class="toc-item toc-subsection"><div class="toc-title-box"><a href="syllabus.html#sec-assessments" class="internal"><span class="title">Assessments</span></a></div></li>
|
||||
<li class="toc-item toc-subsection"><div class="toc-title-box"><a href="syllabus.html#sec-grading" class="internal"><span class="title">Grading</span></a></div></li>
|
||||
</ul>
|
||||
</li></ul>
|
||||
</li>
|
||||
<li class="toc-item toc-chapter contains-active">
|
||||
<div class="toc-title-box"><a href="ch-notes.html" class="internal"><span class="title">Class Notes</span></a></div>
|
||||
<ul class="structural toc-item-list contains-active"><li class="toc-item toc-section contains-active">
|
||||
<div class="toc-title-box"><a href="notes-week-01.html" class="internal"><span class="title">Week 1</span></a></div>
|
||||
<ul class="structural toc-item-list active"><li class="toc-item toc-subsection">
|
||||
<div class="toc-title-box"><a href="notes-week-01.html#notes-week-01-3" class="internal"><span class="title">Tuesday 1/13</span></a></div>
|
||||
<ul class="structural toc-item-list">
|
||||
<li class="toc-item toc-subsubsection"><div class="toc-title-box"><a href="notes-week-01.html#subsubsec-Sets" class="internal"><span class="title">Sec 1.1: Sets</span></a></div></li>
|
||||
<li class="toc-item toc-subsubsection"><div class="toc-title-box"><a href="notes-week-01.html#subsubsec-Probability" class="internal"><span class="title">Sec 1.2: Probability</span></a></div></li>
|
||||
</ul>
|
||||
</li></ul>
|
||||
</li></ul>
|
||||
</li>
|
||||
<li class="toc-item toc-chapter">
|
||||
<div class="toc-title-box"><a href="recitations.html" class="internal"><span class="title">Recitations</span></a></div>
|
||||
<ul class="structural toc-item-list"><li class="toc-item toc-worksheet"><div class="toc-title-box"><a href="recitation-calculus-review.html" class="internal"><span class="title">Calculus Review</span></a></div></li></ul>
|
||||
</li>
|
||||
</ul></nav></div>
|
||||
<main class="ptx-main"><div id="ptx-content" class="ptx-content"><section class="section" id="notes-week-01"><h2 class="heading hide-type">
|
||||
<span class="type">Section</span><span class="space heading-divison-mark heading-divison-mark__space"> </span><span class="codenumber"></span><span class="space heading-divison-mark heading-divison-mark__space"> </span><span class="title">Week 1</span>
|
||||
</h2>
|
||||
<section class="introduction" id="notes-week-01-2"><div class="para" id="notes-week-01-2-1">This is an outline of the topics we covered in the first week of class. These notes are <em class="emphasis">not</em> a substitute for your own note-taking. I highly recommend that you take your own notes during class. If you ever miss a class for any reason, reach out to another student in class to get a copy of their notes.<div class="autopermalink" data-description="Paragraph"><a href="#notes-week-01-2-1" title="Copy heading and permalink for Paragraph" aria-label="Copy heading and permalink for Paragraph">🔗</a></div>
|
||||
</div></section><section class="subsection" id="notes-week-01-3"><h3 class="heading hide-type">
|
||||
<span class="type">Subsection</span><span class="space heading-divison-mark heading-divison-mark__space"> </span><span class="codenumber"></span><span class="space heading-divison-mark heading-divison-mark__space"> </span><span class="title">Tuesday 1/13</span>
|
||||
</h3>
|
||||
<section class="subsubsection" id="subsubsec-Sets"><h4 class="heading hide-type">
|
||||
<span class="type">Subsubsection</span><span class="space heading-divison-mark heading-divison-mark__space"> </span><span class="codenumber"></span><span class="space heading-divison-mark heading-divison-mark__space"> </span><span class="title">Sec 1.1: Sets</span>
|
||||
</h4>
|
||||
<div class="para" id="subsubsec-Sets-2">In prior math courses, you mostly asked <dfn class="terminology">deterministic</dfn> questions. Now, we need new tools to model <dfn class="terminology">randomness</dfn>.<div class="autopermalink" data-description="Paragraph"><a href="#subsubsec-Sets-2" title="Copy heading and permalink for Paragraph" aria-label="Copy heading and permalink for Paragraph">🔗</a></div>
|
||||
</div>
|
||||
<article class="example example-like" id="subsubsec-Sets-3"><h5 class="heading">
|
||||
<span class="type">Example</span><span class="space heading-divison-mark heading-divison-mark__space"> </span><span class="codenumber">1</span><span class="period heading-divison-mark heading-divison-mark__period">.</span>
|
||||
</h5>
|
||||
<div class="para" id="subsubsec-Sets-3-1-1">A toxin molecule in a cell has a certain chance each minute to leave the cell.<div class="autopermalink" data-description="Paragraph"><a href="#subsubsec-Sets-3-1-1" title="Copy heading and permalink for Paragraph" aria-label="Copy heading and permalink for Paragraph">🔗</a></div>
|
||||
</div>
|
||||
<div class="autopermalink" data-description="Example 1"><a href="#subsubsec-Sets-3" title="Copy heading and permalink for Example 1" aria-label="Copy heading and permalink for Example 1">🔗</a></div></article><article class="example example-like" id="subsubsec-Sets-4"><h5 class="heading">
|
||||
<span class="type">Example</span><span class="space heading-divison-mark heading-divison-mark__space"> </span><span class="codenumber">2</span><span class="period heading-divison-mark heading-divison-mark__period">.</span>
|
||||
</h5>
|
||||
<div class="para" id="subsubsec-Sets-4-1-1">A patient takes a diagnostic test for a disease and wants to know the chance that they have the disease based on the test result.<div class="autopermalink" data-description="Paragraph"><a href="#subsubsec-Sets-4-1-1" title="Copy heading and permalink for Paragraph" aria-label="Copy heading and permalink for Paragraph">🔗</a></div>
|
||||
</div>
|
||||
<div class="autopermalink" data-description="Example 2"><a href="#subsubsec-Sets-4" title="Copy heading and permalink for Example 2" aria-label="Copy heading and permalink for Example 2">🔗</a></div></article><article class="definition definition-like" id="def-sample-space"><h5 class="heading">
|
||||
<span class="type">Definition</span><span class="space heading-divison-mark heading-divison-mark__space"> </span><span class="codenumber">3</span><span class="period heading-divison-mark heading-divison-mark__period">.</span>
|
||||
</h5>
|
||||
<div class="para" id="def-sample-space-1-1">The <dfn class="terminology">sample space</dfn>, often denoted <span class="process-math">\(\Omega\text{,}\)</span> is the set of all possible results of an experiment. A single result is called an <dfn class="terminology">outcome</dfn>, while a collection of results is called an <dfn class="terminology">event</dfn>.<div class="autopermalink" data-description="Paragraph"><a href="#def-sample-space-1-1" title="Copy heading and permalink for Paragraph" aria-label="Copy heading and permalink for Paragraph">🔗</a></div>
|
||||
</div>
|
||||
<div class="autopermalink" data-description="Definition 3"><a href="#def-sample-space" title="Copy heading and permalink for Definition 3" aria-label="Copy heading and permalink for Definition 3">🔗</a></div></article><article class="example example-like" id="example-sample-space"><h5 class="heading">
|
||||
<span class="type">Example</span><span class="space heading-divison-mark heading-divison-mark__space"> </span><span class="codenumber">4</span><span class="period heading-divison-mark heading-divison-mark__period">.</span>
|
||||
</h5>
|
||||
<div class="para" id="example-sample-space-1-1">An experiment consists of rolling a standard 6-sided die (D6). The sample space is <span class="process-math">\(\Omega = \{1, 2, 3, 4, 5, 6\}\text{.}\)</span> One possible event is <span class="process-math">\(A = \{2, 4, 6\}\text{,}\)</span> i.e., the event that the result of the roll is even.<div class="autopermalink" data-description="Paragraph"><a href="#example-sample-space-1-1" title="Copy heading and permalink for Paragraph" aria-label="Copy heading and permalink for Paragraph">🔗</a></div>
|
||||
</div>
|
||||
<div class="autopermalink" data-description="Example 4"><a href="#example-sample-space" title="Copy heading and permalink for Example 4" aria-label="Copy heading and permalink for Example 4">🔗</a></div></article><div class="para" id="subsubsec-Sets-7">Note: we’ll use notation like D6 to indicate a 6-sided die with faces 1, 2, 3, 4, 5, 6. Similarly, for example, D4 will indicate a 4-sided die with faces 1, 2, 3, 4.<div class="autopermalink" data-description="Paragraph"><a href="#subsubsec-Sets-7" title="Copy heading and permalink for Paragraph" aria-label="Copy heading and permalink for Paragraph">🔗</a></div>
|
||||
</div>
|
||||
<article class="definition definition-like" id="def-subset"><h5 class="heading">
|
||||
<span class="type">Definition</span><span class="space heading-divison-mark heading-divison-mark__space"> </span><span class="codenumber">5</span><span class="period heading-divison-mark heading-divison-mark__period">.</span>
|
||||
</h5>
|
||||
<div class="para" id="def-subset-1-1">The symbol <span class="process-math">\(\in\)</span> means "is an <dfn class="terminology">element</dfn> of", as in <span class="process-math">\(4 \in A\text{.}\)</span><div class="autopermalink" data-description="Paragraph"><a href="#def-subset-1-1" title="Copy heading and permalink for Paragraph" aria-label="Copy heading and permalink for Paragraph">🔗</a></div>
|
||||
</div> <div class="para" id="def-subset-1-2">The symbol <span class="process-math">\(\subset\)</span> means "is a <dfn class="terminology">subset</dfn> of", as in <span class="process-math">\(A \subset \Omega\text{.}\)</span> This means that every element of the set <span class="process-math">\(A\)</span> is also an element of the set <span class="process-math">\(\Omega\text{.}\)</span><div class="autopermalink" data-description="Paragraph"><a href="#def-subset-1-2" title="Copy heading and permalink for Paragraph" aria-label="Copy heading and permalink for Paragraph">🔗</a></div>
|
||||
</div>
|
||||
<div class="autopermalink" data-description="Definition 5"><a href="#def-subset" title="Copy heading and permalink for Definition 5" aria-label="Copy heading and permalink for Definition 5">🔗</a></div></article><article class="definition definition-like" id="def-set-operations"><h5 class="heading">
|
||||
<span class="type">Definition</span><span class="space heading-divison-mark heading-divison-mark__space"> </span><span class="codenumber">6</span><span class="period heading-divison-mark heading-divison-mark__period">.</span>
|
||||
</h5>
|
||||
<div class="para logical" id="def-set-operations-1-1">
|
||||
<div class="para">Consider sets <span class="process-math">\(A\)</span> and <span class="process-math">\(B\text{,}\)</span> each contained inside <span class="process-math">\(\Omega\text{.}\)</span> We can combine sets in a variety of ways:</div>
|
||||
<dl class="description-list">
|
||||
<dt id="def-set-operations-1-1-4-1">Union<div class="autopermalink" data-description="Item: Union"><a href="#def-set-operations-1-1-4-1" title="Copy heading and permalink for Item: Union" aria-label="Copy heading and permalink for Item: Union">🔗</a></div>
|
||||
</dt>
|
||||
<dd><div class="para" id="def-set-operations-1-1-4-1-2">The <dfn class="terminology">union</dfn> of <span class="process-math">\(A\)</span> and <span class="process-math">\(B\)</span> is the set <span class="process-math">\(A \cup B = \{x \mid x \in A \text{ or } x \in B\}\text{.}\)</span><div class="autopermalink" data-description="Paragraph"><a href="#def-set-operations-1-1-4-1-2" title="Copy heading and permalink for Paragraph" aria-label="Copy heading and permalink for Paragraph">🔗</a></div>
|
||||
</div></dd>
|
||||
<dt id="def-set-operations-1-1-4-2">Intersection<div class="autopermalink" data-description="Item: Intersection"><a href="#def-set-operations-1-1-4-2" title="Copy heading and permalink for Item: Intersection" aria-label="Copy heading and permalink for Item: Intersection">🔗</a></div>
|
||||
</dt>
|
||||
<dd><div class="para" id="def-set-operations-1-1-4-2-2">The <dfn class="terminology">intersection</dfn> of <span class="process-math">\(A\)</span> and <span class="process-math">\(B\)</span> is the set <span class="process-math">\(A \cap B = \{x \mid x \in A \text{ and } x \in B\}\text{.}\)</span><div class="autopermalink" data-description="Paragraph"><a href="#def-set-operations-1-1-4-2-2" title="Copy heading and permalink for Paragraph" aria-label="Copy heading and permalink for Paragraph">🔗</a></div>
|
||||
</div></dd>
|
||||
<dt id="def-set-operations-1-1-4-3">Difference<div class="autopermalink" data-description="Item: Difference"><a href="#def-set-operations-1-1-4-3" title="Copy heading and permalink for Item: Difference" aria-label="Copy heading and permalink for Item: Difference">🔗</a></div>
|
||||
</dt>
|
||||
<dd><div class="para" id="def-set-operations-1-1-4-3-2">The <dfn class="terminology">set difference</dfn> <span class="process-math">\(A-B\)</span> is the set <span class="process-math">\(A - B = \{x \mid x \in A \text{ and } x \notin B\}\text{.}\)</span><div class="autopermalink" data-description="Paragraph"><a href="#def-set-operations-1-1-4-3-2" title="Copy heading and permalink for Paragraph" aria-label="Copy heading and permalink for Paragraph">🔗</a></div>
|
||||
</div></dd>
|
||||
<dt id="def-set-operations-1-1-4-4">Complement<div class="autopermalink" data-description="Item: Complement"><a href="#def-set-operations-1-1-4-4" title="Copy heading and permalink for Item: Complement" aria-label="Copy heading and permalink for Item: Complement">🔗</a></div>
|
||||
</dt>
|
||||
<dd><div class="para" id="def-set-operations-1-1-4-4-2">The <dfn class="terminology">complement</dfn> of <span class="process-math">\(A\)</span> is the set <span class="process-math">\(A^c = \{x \in \Omega \mid x \notin A\}\text{.}\)</span><div class="autopermalink" data-description="Paragraph"><a href="#def-set-operations-1-1-4-4-2" title="Copy heading and permalink for Paragraph" aria-label="Copy heading and permalink for Paragraph">🔗</a></div>
|
||||
</div></dd>
|
||||
<dt id="def-set-operations-1-1-4-5">Empty Set<div class="autopermalink" data-description="Item: Empty Set"><a href="#def-set-operations-1-1-4-5" title="Copy heading and permalink for Item: Empty Set" aria-label="Copy heading and permalink for Item: Empty Set">🔗</a></div>
|
||||
</dt>
|
||||
<dd><div class="para" id="def-set-operations-1-1-4-5-2">The <dfn class="terminology">empty set</dfn>, usually written <span class="process-math">\(\emptyset\)</span> or <span class="process-math">\(\{\}\text{,}\)</span> is the set which contains no elements.<div class="autopermalink" data-description="Paragraph"><a href="#def-set-operations-1-1-4-5-2" title="Copy heading and permalink for Paragraph" aria-label="Copy heading and permalink for Paragraph">🔗</a></div>
|
||||
</div></dd>
|
||||
</dl>
|
||||
<div class="autopermalink" data-description="Paragraph"><a href="#def-set-operations-1-1" title="Copy heading and permalink for Paragraph" aria-label="Copy heading and permalink for Paragraph">🔗</a></div>
|
||||
</div>
|
||||
<div class="autopermalink" data-description="Definition 6"><a href="#def-set-operations" title="Copy heading and permalink for Definition 6" aria-label="Copy heading and permalink for Definition 6">🔗</a></div></article><div class="para" id="subsubsec-Sets-10">It’s useful sometimes to draw pictures called <dfn class="terminology">Venn diagrams</dfn> representing set interactions.<div class="autopermalink" data-description="Paragraph"><a href="#subsubsec-Sets-10" title="Copy heading and permalink for Paragraph" aria-label="Copy heading and permalink for Paragraph">🔗</a></div>
|
||||
</div>
|
||||
<div class="sbsgroup">
|
||||
<div class="sidebyside"><div class="sbsrow" style="margin-left:5%;margin-right:5%;grid-template-columns:44.4444444444444% 44.4444444444444%;column-gap:10%;">
|
||||
<div class="sbspanel sbspanel--top top" style=""><figure class="figure figure-like" id="fig-Venn-diagram-union"><img src="generated/latex-image/fig-Venn-diagram-union-2.svg" role="img" class="contained" alt="described in detail following the image" aria-describedby="fig-Venn-diagram-union-2-description"><details class="image-description" aria-live="polite"><summary title="details"><span class="icon material-symbols-outlined" aria-hidden="true"></span></summary><div id="fig-Venn-diagram-union-2-description"><div class="para" id="fig-Venn-diagram-union-2-1-1">Venn diagram showing sets <span class="process-math">\(A, B\)</span> with the region representing <span class="process-math">\(A \cup B\)</span> shaded.<div class="autopermalink" data-description="Paragraph"><a href="#fig-Venn-diagram-union-2-1-1" title="Copy heading and permalink for Paragraph" aria-label="Copy heading and permalink for Paragraph">🔗</a></div>
|
||||
</div></div></details><figcaption><span class="type">Figure</span><span class="space heading-divison-mark heading-divison-mark__space"> </span><span class="codenumber">7<span class="period heading-divison-mark heading-divison-mark__period">.</span></span><span class="space heading-divison-mark heading-divison-mark__space"> </span>Union<div class="autopermalink" data-description="Figure 7"><a href="#fig-Venn-diagram-union" title="Copy heading and permalink for Figure 7" aria-label="Copy heading and permalink for Figure 7">🔗</a></div></figcaption></figure></div>
|
||||
<div class="sbspanel sbspanel--top top" style=""><figure class="figure figure-like" id="fig-Venn-diagram-intersection"><img src="generated/latex-image/fig-Venn-diagram-intersection-2.svg" role="img" class="contained" alt="described in detail following the image" aria-describedby="fig-Venn-diagram-intersection-2-description"><details class="image-description" aria-live="polite"><summary title="details"><span class="icon material-symbols-outlined" aria-hidden="true"></span></summary><div id="fig-Venn-diagram-intersection-2-description"><div class="para" id="fig-Venn-diagram-intersection-2-1-1">Venn diagram showing sets <span class="process-math">\(A, B\)</span> with the region representing <span class="process-math">\(A \cap B\)</span> shaded.<div class="autopermalink" data-description="Paragraph"><a href="#fig-Venn-diagram-intersection-2-1-1" title="Copy heading and permalink for Paragraph" aria-label="Copy heading and permalink for Paragraph">🔗</a></div>
|
||||
</div></div></details><figcaption><span class="type">Figure</span><span class="space heading-divison-mark heading-divison-mark__space"> </span><span class="codenumber">8<span class="period heading-divison-mark heading-divison-mark__period">.</span></span><span class="space heading-divison-mark heading-divison-mark__space"> </span>Intersection<div class="autopermalink" data-description="Figure 8"><a href="#fig-Venn-diagram-intersection" title="Copy heading and permalink for Figure 8" aria-label="Copy heading and permalink for Figure 8">🔗</a></div></figcaption></figure></div>
|
||||
</div></div>
|
||||
<div class="sidebyside"><div class="sbsrow" style="margin-left:5%;margin-right:5%;grid-template-columns:44.4444444444444% 44.4444444444444%;column-gap:10%;">
|
||||
<div class="sbspanel sbspanel--top top" style=""><figure class="figure figure-like" id="fig-Venn-diagram-difference"><img src="generated/latex-image/fig-Venn-diagram-difference-2.svg" role="img" class="contained" alt="described in detail following the image" aria-describedby="fig-Venn-diagram-difference-2-description"><details class="image-description" aria-live="polite"><summary title="details"><span class="icon material-symbols-outlined" aria-hidden="true"></span></summary><div id="fig-Venn-diagram-difference-2-description"><div class="para" id="fig-Venn-diagram-difference-2-1-1">Venn diagram showing sets <span class="process-math">\(A, B\)</span> with the region representing <span class="process-math">\(A - B\)</span> shaded.<div class="autopermalink" data-description="Paragraph"><a href="#fig-Venn-diagram-difference-2-1-1" title="Copy heading and permalink for Paragraph" aria-label="Copy heading and permalink for Paragraph">🔗</a></div>
|
||||
</div></div></details><figcaption><span class="type">Figure</span><span class="space heading-divison-mark heading-divison-mark__space"> </span><span class="codenumber">9<span class="period heading-divison-mark heading-divison-mark__period">.</span></span><span class="space heading-divison-mark heading-divison-mark__space"> </span>Difference<div class="autopermalink" data-description="Figure 9"><a href="#fig-Venn-diagram-difference" title="Copy heading and permalink for Figure 9" aria-label="Copy heading and permalink for Figure 9">🔗</a></div></figcaption></figure></div>
|
||||
<div class="sbspanel sbspanel--top top" style=""><figure class="figure figure-like" id="fig-Venn-diagram-complement"><img src="generated/latex-image/fig-Venn-diagram-complement-2.svg" role="img" class="contained" alt="described in detail following the image" aria-describedby="fig-Venn-diagram-complement-2-description"><details class="image-description" aria-live="polite"><summary title="details"><span class="icon material-symbols-outlined" aria-hidden="true"></span></summary><div id="fig-Venn-diagram-complement-2-description"><div class="para" id="fig-Venn-diagram-complement-2-1-1">Venn diagram showing set <span class="process-math">\(A \subset \Omega\)</span> with the region representing <span class="process-math">\(A^c\)</span> shaded.<div class="autopermalink" data-description="Paragraph"><a href="#fig-Venn-diagram-complement-2-1-1" title="Copy heading and permalink for Paragraph" aria-label="Copy heading and permalink for Paragraph">🔗</a></div>
|
||||
</div></div></details><figcaption><span class="type">Figure</span><span class="space heading-divison-mark heading-divison-mark__space"> </span><span class="codenumber">10<span class="period heading-divison-mark heading-divison-mark__period">.</span></span><span class="space heading-divison-mark heading-divison-mark__space"> </span>Complement<div class="autopermalink" data-description="Figure 10"><a href="#fig-Venn-diagram-complement" title="Copy heading and permalink for Figure 10" aria-label="Copy heading and permalink for Figure 10">🔗</a></div></figcaption></figure></div>
|
||||
</div></div>
|
||||
</div>
|
||||
<div class="autopermalink" data-description="Subsubsection: Sec 1.1: Sets"><a href="#subsubsec-Sets" title="Copy heading and permalink for Subsubsection: Sec 1.1: Sets" aria-label="Copy heading and permalink for Subsubsection: Sec 1.1: Sets">🔗</a></div></section><section class="subsubsection" id="subsubsec-Probability"><h4 class="heading hide-type">
|
||||
<span class="type">Subsubsection</span><span class="space heading-divison-mark heading-divison-mark__space"> </span><span class="codenumber"></span><span class="space heading-divison-mark heading-divison-mark__space"> </span><span class="title">Sec 1.2: Probability</span>
|
||||
</h4>
|
||||
<div class="para" id="subsubsec-Probability-2">Next, we want to start assigning probabilities to each individual outcome so we can then find the probabilities of events.<div class="autopermalink" data-description="Paragraph"><a href="#subsubsec-Probability-2" title="Copy heading and permalink for Paragraph" aria-label="Copy heading and permalink for Paragraph">🔗</a></div>
|
||||
</div>
|
||||
<article class="example example-like" id="subsubsec-Probability-3"><h5 class="heading">
|
||||
<span class="type">Example</span><span class="space heading-divison-mark heading-divison-mark__space"> </span><span class="codenumber">11</span><span class="period heading-divison-mark heading-divison-mark__period">.</span>
|
||||
</h5>
|
||||
<div class="para" id="subsubsec-Probability-3-1-1">An experiment consists of rolling a D6. The sample space is <span class="process-math">\(\Omega = \{1, 2, 3, 4, 5, 6\}\text{.}\)</span> We might assign probabilities as follows:<div class="autopermalink" data-description="Paragraph"><a href="#subsubsec-Probability-3-1-1" title="Copy heading and permalink for Paragraph" aria-label="Copy heading and permalink for Paragraph">🔗</a></div>
|
||||
</div> <figure class="table table-like" id="subsubsec-Probability-3-1-2"><figcaption><span class="type">Table</span><span class="space heading-divison-mark heading-divison-mark__space"> </span><span class="codenumber">12<span class="period heading-divison-mark heading-divison-mark__period">.</span></span><span class="space heading-divison-mark heading-divison-mark__space"> </span>Distribution for a fair die<div class="autopermalink" data-description="Table 12: Distribution for a fair die"><a href="#subsubsec-Probability-3-1-2" title="Copy heading and permalink for Table 12: Distribution for a fair die" aria-label="Copy heading and permalink for Table 12: Distribution for a fair die">🔗</a></div></figcaption><div class="tabular-box natural-width"><table class="tabular">
|
||||
<tr>
|
||||
<td class="c m b1 r0 l0 t0 lines"><span class="process-math">\(x\)</span></td>
|
||||
<td class="c m b1 r0 l0 t0 lines"><span class="process-math">\(\Pr(x)\)</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="c m b0 r0 l0 t0 lines">1</td>
|
||||
<td class="c m b0 r0 l0 t0 lines"><span class="process-math">\(1/6\)</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="c m b0 r0 l0 t0 lines">2</td>
|
||||
<td class="c m b0 r0 l0 t0 lines"><span class="process-math">\(1/6\)</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="c m b0 r0 l0 t0 lines">3</td>
|
||||
<td class="c m b0 r0 l0 t0 lines"><span class="process-math">\(1/6\)</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="c m b0 r0 l0 t0 lines">4</td>
|
||||
<td class="c m b0 r0 l0 t0 lines"><span class="process-math">\(1/6\)</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="c m b0 r0 l0 t0 lines">5</td>
|
||||
<td class="c m b0 r0 l0 t0 lines"><span class="process-math">\(1/6\)</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="c m b0 r0 l0 t0 lines">6</td>
|
||||
<td class="c m b0 r0 l0 t0 lines"><span class="process-math">\(1/6\)</span></td>
|
||||
</tr>
|
||||
</table></div></figure> <div class="para logical" id="subsubsec-Probability-3-1-3">
|
||||
<div class="para">Note that we don’t have to assign the same probability to each outcome. If we do, we call this distribution <dfn class="terminology">uniform</dfn>. If we have some event, such as <span class="process-math">\(A = \{2, 4, 6\}\text{,}\)</span> then we calculate the probability of the event by adding together the probabilities of the outcomes that make up the event:</div>
|
||||
<div class="displaymath process-math" id="subsubsec-Probability-3-1-3-3">
|
||||
\begin{gather*}
|
||||
\Pr(A) = \Pr(2) + \Pr(4) + \Pr(6) = \frac{1}{6} + \frac{1}{6} + \frac{1}{6} = \frac{3}{6} = \frac{1}{2}
|
||||
\end{gather*}
|
||||
</div>
|
||||
<div class="para">Note that this is the same result that we would get if we counted the total number of outcomes in <span class="process-math">\(A\)</span> and divided by the total number of outcomes in <span class="process-math">\(\Omega\text{.}\)</span>
|
||||
</div>
|
||||
<div class="autopermalink" data-description="Paragraph"><a href="#subsubsec-Probability-3-1-3" title="Copy heading and permalink for Paragraph" aria-label="Copy heading and permalink for Paragraph">🔗</a></div>
|
||||
</div>
|
||||
<div class="autopermalink" data-description="Example 11"><a href="#subsubsec-Probability-3" title="Copy heading and permalink for Example 11" aria-label="Copy heading and permalink for Example 11">🔗</a></div></article><article class="definition definition-like" id="def-cardinality"><h5 class="heading">
|
||||
<span class="type">Definition</span><span class="space heading-divison-mark heading-divison-mark__space"> </span><span class="codenumber">13</span><span class="period heading-divison-mark heading-divison-mark__period">.</span>
|
||||
</h5>
|
||||
<div class="para" id="def-cardinality-1-1">Given a finite set <span class="process-math">\(A\text{,}\)</span> the <dfn class="terminology">cardinality</dfn> of <span class="process-math">\(A\text{,}\)</span> written <span class="process-math">\(|A|\text{,}\)</span> is the number of elements in <span class="process-math">\(A\text{.}\)</span><div class="autopermalink" data-description="Paragraph"><a href="#def-cardinality-1-1" title="Copy heading and permalink for Paragraph" aria-label="Copy heading and permalink for Paragraph">🔗</a></div>
|
||||
</div>
|
||||
<div class="autopermalink" data-description="Definition 13"><a href="#def-cardinality" title="Copy heading and permalink for Definition 13" aria-label="Copy heading and permalink for Definition 13">🔗</a></div></article><article class="fact theorem-like" id="subsubsec-Probability-5"><h5 class="heading">
|
||||
<span class="type">Fact</span><span class="space heading-divison-mark heading-divison-mark__space"> </span><span class="codenumber">14</span><span class="period heading-divison-mark heading-divison-mark__period">.</span>
|
||||
</h5>
|
||||
<div class="para logical" id="subsubsec-Probability-5-1-1">
|
||||
<div class="para">If <span class="process-math">\(\Omega\)</span> is a finite probability space with the uniform distribution and <span class="process-math">\(A \subset \Omega\)</span> is an event, then:</div>
|
||||
<div class="displaymath process-math" id="subsubsec-Probability-5-1-1-3">
|
||||
\begin{gather*}
|
||||
\Pr(A) = \frac{|A|}{|\Omega|}
|
||||
\end{gather*}
|
||||
</div>
|
||||
<div class="autopermalink" data-description="Paragraph"><a href="#subsubsec-Probability-5-1-1" title="Copy heading and permalink for Paragraph" aria-label="Copy heading and permalink for Paragraph">🔗</a></div>
|
||||
</div>
|
||||
<div class="autopermalink" data-description="Fact 14"><a href="#subsubsec-Probability-5" title="Copy heading and permalink for Fact 14" aria-label="Copy heading and permalink for Fact 14">🔗</a></div></article><article class="example example-like" id="subsubsec-Probability-6"><h5 class="heading">
|
||||
<span class="type">Example</span><span class="space heading-divison-mark heading-divison-mark__space"> </span><span class="codenumber">15</span><span class="period heading-divison-mark heading-divison-mark__period">.</span>
|
||||
</h5>
|
||||
<div class="para" id="subsubsec-Probability-6-1-1">Suppose we have a weighted die that’s much more likely to come up 6 than any other outcome.<div class="autopermalink" data-description="Paragraph"><a href="#subsubsec-Probability-6-1-1" title="Copy heading and permalink for Paragraph" aria-label="Copy heading and permalink for Paragraph">🔗</a></div>
|
||||
</div> <figure class="table table-like" id="subsubsec-Probability-6-1-2"><figcaption><span class="type">Table</span><span class="space heading-divison-mark heading-divison-mark__space"> </span><span class="codenumber">16<span class="period heading-divison-mark heading-divison-mark__period">.</span></span><span class="space heading-divison-mark heading-divison-mark__space"> </span>Distribution for a fair die<div class="autopermalink" data-description="Table 16: Distribution for a fair die"><a href="#subsubsec-Probability-6-1-2" title="Copy heading and permalink for Table 16: Distribution for a fair die" aria-label="Copy heading and permalink for Table 16: Distribution for a fair die">🔗</a></div></figcaption><div class="tabular-box natural-width"><table class="tabular">
|
||||
<tr>
|
||||
<td class="c m b1 r0 l0 t0 lines"><span class="process-math">\(x\)</span></td>
|
||||
<td class="c m b1 r0 l0 t0 lines"><span class="process-math">\(\Pr(x)\)</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="c m b0 r0 l0 t0 lines">1</td>
|
||||
<td class="c m b0 r0 l0 t0 lines">0.1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="c m b0 r0 l0 t0 lines">2</td>
|
||||
<td class="c m b0 r0 l0 t0 lines">0.1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="c m b0 r0 l0 t0 lines">3</td>
|
||||
<td class="c m b0 r0 l0 t0 lines">0.1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="c m b0 r0 l0 t0 lines">4</td>
|
||||
<td class="c m b0 r0 l0 t0 lines">0.2</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="c m b0 r0 l0 t0 lines">5</td>
|
||||
<td class="c m b0 r0 l0 t0 lines">0.1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="c m b0 r0 l0 t0 lines">6</td>
|
||||
<td class="c m b0 r0 l0 t0 lines">0.4</td>
|
||||
</tr>
|
||||
</table></div></figure> <div class="para logical" id="subsubsec-Probability-6-1-3">
|
||||
<div class="para">With this distribution, if <span class="process-math">\(A = \{2, 4, 6\}\text{,}\)</span> then:</div>
|
||||
<div class="displaymath process-math" id="subsubsec-Probability-6-1-3-2">
|
||||
\begin{gather*}
|
||||
\Pr(A) = 0.1 + 0.2 + 0.4 = 0.7 \neq \frac{3}{6}.
|
||||
\end{gather*}
|
||||
</div>
|
||||
<div class="autopermalink" data-description="Paragraph"><a href="#subsubsec-Probability-6-1-3" title="Copy heading and permalink for Paragraph" aria-label="Copy heading and permalink for Paragraph">🔗</a></div>
|
||||
</div>
|
||||
<div class="autopermalink" data-description="Example 15"><a href="#subsubsec-Probability-6" title="Copy heading and permalink for Example 15" aria-label="Copy heading and permalink for Example 15">🔗</a></div></article><article class="example example-like" id="subsubsec-Probability-7"><h5 class="heading">
|
||||
<span class="type">Example</span><span class="space heading-divison-mark heading-divison-mark__space"> </span><span class="codenumber">17</span><span class="period heading-divison-mark heading-divison-mark__period">.</span>
|
||||
</h5>
|
||||
<div class="para" id="subsubsec-Probability-7-1-1">Pick a number from 1 to 10. What is the probability of picking 3?<div class="autopermalink" data-description="Paragraph"><a href="#subsubsec-Probability-7-1-1" title="Copy heading and permalink for Paragraph" aria-label="Copy heading and permalink for Paragraph">🔗</a></div>
|
||||
</div> <div class="para logical" id="subsubsec-Probability-7-1-2">
|
||||
<div class="para">Some considerations:</div>
|
||||
<ol class="decimal" id="subsubsec-Probability-7-1-2-1">
|
||||
<li id="subsubsec-Probability-7-1-2-1-1">
|
||||
<div class="para" id="subsubsec-Probability-7-1-2-1-1-1">Are we using the uniform distribution?<div class="autopermalink" data-description="Paragraph"><a href="#subsubsec-Probability-7-1-2-1-1-1" title="Copy heading and permalink for Paragraph" aria-label="Copy heading and permalink for Paragraph">🔗</a></div>
|
||||
</div>
|
||||
<div class="autopermalink" data-description="Item 1"><a href="#subsubsec-Probability-7-1-2-1-1" title="Copy heading and permalink for Item 1" aria-label="Copy heading and permalink for Item 1">🔗</a></div>
|
||||
</li>
|
||||
<li id="subsubsec-Probability-7-1-2-1-2">
|
||||
<div class="para" id="subsubsec-Probability-7-1-2-1-2-1">What even is the sample space here? Do we need to pick only integers, or is <span class="process-math">\(\pi\)</span> an outcome here?<div class="autopermalink" data-description="Paragraph"><a href="#subsubsec-Probability-7-1-2-1-2-1" title="Copy heading and permalink for Paragraph" aria-label="Copy heading and permalink for Paragraph">🔗</a></div>
|
||||
</div>
|
||||
<div class="autopermalink" data-description="Item 2"><a href="#subsubsec-Probability-7-1-2-1-2" title="Copy heading and permalink for Item 2" aria-label="Copy heading and permalink for Item 2">🔗</a></div>
|
||||
</li>
|
||||
<li id="subsubsec-Probability-7-1-2-1-3">
|
||||
<div class="para" id="subsubsec-Probability-7-1-2-1-3-1">What would "uniform" mean if the sample space was infinite?<div class="autopermalink" data-description="Paragraph"><a href="#subsubsec-Probability-7-1-2-1-3-1" title="Copy heading and permalink for Paragraph" aria-label="Copy heading and permalink for Paragraph">🔗</a></div>
|
||||
</div>
|
||||
<div class="autopermalink" data-description="Item 3"><a href="#subsubsec-Probability-7-1-2-1-3" title="Copy heading and permalink for Item 3" aria-label="Copy heading and permalink for Item 3">🔗</a></div>
|
||||
</li>
|
||||
</ol>
|
||||
<div class="autopermalink" data-description="Paragraph"><a href="#subsubsec-Probability-7-1-2" title="Copy heading and permalink for Paragraph" aria-label="Copy heading and permalink for Paragraph">🔗</a></div>
|
||||
</div>
|
||||
<div class="autopermalink" data-description="Example 17"><a href="#subsubsec-Probability-7" title="Copy heading and permalink for Example 17" aria-label="Copy heading and permalink for Example 17">🔗</a></div></article><article class="definition definition-like" id="def-probability-distribution"><h5 class="heading">
|
||||
<span class="type">Definition</span><span class="space heading-divison-mark heading-divison-mark__space"> </span><span class="codenumber">18</span><span class="period heading-divison-mark heading-divison-mark__period">.</span>
|
||||
</h5>
|
||||
<div class="para logical" id="def-probability-distribution-1-1">
|
||||
<div class="para">A <dfn class="terminology">probability distribution</dfn> on a sample space <span class="process-math">\(\Omega\)</span> assigns probabilities to every event, satisfying the following conditions:</div>
|
||||
<ol class="decimal" id="def-probability-distribution-1-1-3">
|
||||
<li id="def-probability-distribution-1-1-3-1">
|
||||
<div class="para" id="def-probability-distribution-1-1-3-1-1">
|
||||
<span class="process-math">\(\Pr(\Omega) = 1\text{.}\)</span><div class="autopermalink" data-description="Paragraph"><a href="#def-probability-distribution-1-1-3-1-1" title="Copy heading and permalink for Paragraph" aria-label="Copy heading and permalink for Paragraph">🔗</a></div>
|
||||
</div>
|
||||
<div class="autopermalink" data-description="Item 1"><a href="#def-probability-distribution-1-1-3-1" title="Copy heading and permalink for Item 1" aria-label="Copy heading and permalink for Item 1">🔗</a></div>
|
||||
</li>
|
||||
<li id="def-probability-distribution-1-1-3-2">
|
||||
<div class="para" id="def-probability-distribution-1-1-3-2-1">
|
||||
<span class="process-math">\(0 \leq \Pr(A) \leq 1\)</span> for any event <span class="process-math">\(A\text{.}\)</span><div class="autopermalink" data-description="Paragraph"><a href="#def-probability-distribution-1-1-3-2-1" title="Copy heading and permalink for Paragraph" aria-label="Copy heading and permalink for Paragraph">🔗</a></div>
|
||||
</div>
|
||||
<div class="autopermalink" data-description="Item 2"><a href="#def-probability-distribution-1-1-3-2" title="Copy heading and permalink for Item 2" aria-label="Copy heading and permalink for Item 2">🔗</a></div>
|
||||
</li>
|
||||
<li id="def-probability-distribution-1-1-3-3">
|
||||
<div class="para" id="def-probability-distribution-1-1-3-3-1">If <span class="process-math">\(A \cap B = \emptyset\text{,}\)</span> then <span class="process-math">\(\Pr(A\cup B) = \Pr(A) + \Pr(B)\text{.}\)</span><div class="autopermalink" data-description="Paragraph"><a href="#def-probability-distribution-1-1-3-3-1" title="Copy heading and permalink for Paragraph" aria-label="Copy heading and permalink for Paragraph">🔗</a></div>
|
||||
</div>
|
||||
<div class="autopermalink" data-description="Item 3"><a href="#def-probability-distribution-1-1-3-3" title="Copy heading and permalink for Item 3" aria-label="Copy heading and permalink for Item 3">🔗</a></div>
|
||||
</li>
|
||||
</ol>
|
||||
<div class="autopermalink" data-description="Paragraph"><a href="#def-probability-distribution-1-1" title="Copy heading and permalink for Paragraph" aria-label="Copy heading and permalink for Paragraph">🔗</a></div>
|
||||
</div>
|
||||
<div class="autopermalink" data-description="Definition 18"><a href="#def-probability-distribution" title="Copy heading and permalink for Definition 18" aria-label="Copy heading and permalink for Definition 18">🔗</a></div></article><article class="example example-like" id="subsubsec-Probability-9"><h5 class="heading">
|
||||
<span class="type">Example</span><span class="space heading-divison-mark heading-divison-mark__space"> </span><span class="codenumber">19</span><span class="period heading-divison-mark heading-divison-mark__period">.</span>
|
||||
</h5>
|
||||
<div class="para" id="subsubsec-Probability-9-1-1">Suppose we flip a coin until we see heads. The sample space is <span class="process-math">\(\Omega = \{H, TH, TTH, TTTH, \dotsc\}\text{.}\)</span> What would "uniform" mean here? Is there any way we could assign the same probability to each individual outcome here?<div class="autopermalink" data-description="Paragraph"><a href="#subsubsec-Probability-9-1-1" title="Copy heading and permalink for Paragraph" aria-label="Copy heading and permalink for Paragraph">🔗</a></div>
|
||||
</div> <div class="para" id="subsubsec-Probability-9-1-2">Instead of "uniform", what if we want to treat the coin as a fair coin? Then what would the distribution be?<div class="autopermalink" data-description="Paragraph"><a href="#subsubsec-Probability-9-1-2" title="Copy heading and permalink for Paragraph" aria-label="Copy heading and permalink for Paragraph">🔗</a></div>
|
||||
</div> <figure class="table table-like" id="subsubsec-Probability-9-1-3"><figcaption><span class="type">Table</span><span class="space heading-divison-mark heading-divison-mark__space"> </span><span class="codenumber">20<span class="period heading-divison-mark heading-divison-mark__period">.</span></span><span class="space heading-divison-mark heading-divison-mark__space"> </span>Distribution assuming a fair coin<div class="autopermalink" data-description="Table 20: Distribution assuming a fair coin"><a href="#subsubsec-Probability-9-1-3" title="Copy heading and permalink for Table 20: Distribution assuming a fair coin" aria-label="Copy heading and permalink for Table 20: Distribution assuming a fair coin">🔗</a></div></figcaption><div class="tabular-box natural-width"><table class="tabular">
|
||||
<tr>
|
||||
<td class="c m b1 r0 l0 t0 lines"><span class="process-math">\(x\)</span></td>
|
||||
<td class="c m b1 r0 l0 t0 lines"># flips</td>
|
||||
<td class="c m b1 r0 l0 t0 lines"><span class="process-math">\(\Pr(x)\)</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="c m b0 r0 l0 t0 lines"><span class="process-math">\(H\)</span></td>
|
||||
<td class="c m b0 r0 l0 t0 lines">1</td>
|
||||
<td class="c m b0 r0 l0 t0 lines"><span class="process-math">\(1/2\)</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="c m b0 r0 l0 t0 lines"><span class="process-math">\(TH\)</span></td>
|
||||
<td class="c m b0 r0 l0 t0 lines">2</td>
|
||||
<td class="c m b0 r0 l0 t0 lines"><span class="process-math">\(1/4\)</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="c m b0 r0 l0 t0 lines"><span class="process-math">\(TTH\)</span></td>
|
||||
<td class="c m b0 r0 l0 t0 lines">3</td>
|
||||
<td class="c m b0 r0 l0 t0 lines"><span class="process-math">\(1/8\)</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="c m b0 r0 l0 t0 lines"><span class="process-math">\(TTTH\)</span></td>
|
||||
<td class="c m b0 r0 l0 t0 lines">4</td>
|
||||
<td class="c m b0 r0 l0 t0 lines"><span class="process-math">\(1/16\)</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="c m b0 r0 l0 t0 lines"><span class="process-math">\(\vdots\)</span></td>
|
||||
<td class="c m b0 r0 l0 t0 lines"></td>
|
||||
<td class="c m b0 r0 l0 t0 lines"><span class="process-math">\(\vdots\)</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="c m b0 r0 l0 t0 lines"><span class="process-math">\(T\dotsm TH\)</span></td>
|
||||
<td class="c m b0 r0 l0 t0 lines"><span class="process-math">\(n\)</span></td>
|
||||
<td class="c m b0 r0 l0 t0 lines"><span class="process-math">\(1/2^n\)</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="c m b0 r0 l0 t0 lines"><span class="process-math">\(\vdots\)</span></td>
|
||||
<td class="c m b0 r0 l0 t0 lines"></td>
|
||||
<td class="c m b0 r0 l0 t0 lines"><span class="process-math">\(\vdots\)</span></td>
|
||||
</tr>
|
||||
</table></div></figure><div class="autopermalink" data-description="Example 19"><a href="#subsubsec-Probability-9" title="Copy heading and permalink for Example 19" aria-label="Copy heading and permalink for Example 19">🔗</a></div></article><article class="example example-like" id="subsubsec-Probability-10"><h5 class="heading">
|
||||
<span class="type">Example</span><span class="space heading-divison-mark heading-divison-mark__space"> </span><span class="codenumber">21</span><span class="period heading-divison-mark heading-divison-mark__period">.</span>
|
||||
</h5>
|
||||
<div class="para" id="subsubsec-Probability-10-1-1">One last (very useful!) observation. Recall: If <span class="process-math">\(A\cap B = \emptyset\text{,}\)</span> then <span class="process-math">\(\Pr(A\cup B) = \Pr(A) + \Pr(B)\text{.}\)</span><div class="autopermalink" data-description="Paragraph"><a href="#subsubsec-Probability-10-1-1" title="Copy heading and permalink for Paragraph" aria-label="Copy heading and permalink for Paragraph">🔗</a></div>
|
||||
</div> <div class="para logical" id="subsubsec-Probability-10-1-2">
|
||||
<div class="para">For any event <span class="process-math">\(A\text{,}\)</span> <span class="process-math">\(A\cap A^c = \emptyset\)</span> and <span class="process-math">\(A \cup A^c = \Omega\text{.}\)</span> So:</div>
|
||||
<div class="displaymath process-math" id="subsubsec-Probability-10-1-2-4">
|
||||
\begin{gather*}
|
||||
1 = \Pr(\underbrace{A \cup A^c}_{\Omega}) = \Pr(A) + \Pr(A^c).
|
||||
\end{gather*}
|
||||
</div>
|
||||
<div class="para">We can rewrite this in two useful ways:</div>
|
||||
<div class="displaymath process-math" id="subsubsec-Probability-10-1-2-5">
|
||||
\begin{align*}
|
||||
\Pr(A) \amp = 1 - \Pr(A^c) \\
|
||||
\Pr(A^c) \amp = 1 - \Pr(A)
|
||||
\end{align*}
|
||||
</div>
|
||||
<div class="autopermalink" data-description="Paragraph"><a href="#subsubsec-Probability-10-1-2" title="Copy heading and permalink for Paragraph" aria-label="Copy heading and permalink for Paragraph">🔗</a></div>
|
||||
</div>
|
||||
<div class="autopermalink" data-description="Example 21"><a href="#subsubsec-Probability-10" title="Copy heading and permalink for Example 21" aria-label="Copy heading and permalink for Example 21">🔗</a></div></article><div class="autopermalink" data-description="Subsubsection: Sec 1.2: Probability"><a href="#subsubsec-Probability" title="Copy heading and permalink for Subsubsection: Sec 1.2: Probability" aria-label="Copy heading and permalink for Subsubsection: Sec 1.2: Probability">🔗</a></div></section><div class="autopermalink" data-description="Subsection: Tuesday 1/13"><a href="#notes-week-01-3" title="Copy heading and permalink for Subsection: Tuesday 1/13" aria-label="Copy heading and permalink for Subsection: Tuesday 1/13">🔗</a></div></section><div class="autopermalink" data-description="Section: Week 1"><a href="#notes-week-01" title="Copy heading and permalink for Section: Week 1" aria-label="Copy heading and permalink for Section: Week 1">🔗</a></div></section></div>
|
||||
<div id="ptx-content-footer" class="ptx-content-footer">
|
||||
<a class="previous-button button" href="ch-notes.html" title="Previous"><span class="icon material-symbols-outlined" aria-hidden="true"></span><span class="name">Prev</span></a><a class="top-button button" href="#" title="Top"><span class="icon material-symbols-outlined" aria-hidden="true"></span><span class="name">Top</span></a><a class="next-button button" href="recitations.html" title="Next"><span class="name">Next</span><span class="icon material-symbols-outlined" aria-hidden="true"></span></a>
|
||||
</div></main>
|
||||
</div>
|
||||
<div id="ptx-page-footer" class="ptx-page-footer">
|
||||
<a class="pretext-link" href="https://pretextbook.org" title="PreTeXt"><div class="logo"><svg xmlns="http://www.w3.org/2000/svg" height="100%" viewBox="338 3000 8772 6866" role="img"><title>PreTeXt logo</title><g style="stroke-width:.025in; stroke:currentColor; fill:none"><polyline points="472,3590 472,9732 " style="stroke-width:174; stroke-linejoin:miter; stroke-linecap:round; "></polyline><path style="stroke-width:126;stroke-linecap:butt;" d="M 4724,9448 A 4660 4660 0 0 1 8598 9259"></path><path style="stroke-width:174;stroke-linecap:butt;" d="M 4488,9685 A 4228 4228 0 0 0 472 9732"></path><path style="stroke-width:126;stroke-linecap:butt;" d="M 4724,3590 A 4241 4241 0 0 1 8598 3496"></path><path style="stroke-width:126;stroke-linecap:round;" d="M 850,3496 A 4241 4241 0 0 1 4724 3590"></path><path style="stroke-width:126;stroke-linecap:round;" d="M 850,9259 A 4507 4507 0 0 1 4724 9448"></path><polyline points="5385,4299 4062,8125" style="stroke-width:300; stroke-linejoin:miter; stroke-linecap:round;"></polyline><polyline points="8598,3496 8598,9259" style="stroke-width:126; stroke-linejoin:miter; stroke-linecap:round;"></polyline><polyline points="850,3496 850,9259" style="stroke-width:126; stroke-linejoin:miter; stroke-linecap:round;"></polyline><polyline points="4960,9685 4488,9685" style="stroke-width:174; stroke-linejoin:miter; stroke-linecap:round;"></polyline><polyline points="3070,4582 1889,6141 3070,7700" style="stroke-width:300; stroke-linejoin:miter; stroke-linecap:round;"></polyline><polyline points="6418,4582 7600,6141 6418,7700" style="stroke-width:300; stroke-linejoin:miter; stroke-linecap:round;"></polyline><polyline points="8976,3590 8976,9732" style="stroke-width:174; stroke-linejoin:miter; stroke-linecap:round;"></polyline><path style="stroke-width:174;stroke-linecap:butt;" d="M 4960,9685 A 4228 4228 0 0 1 8976 9732"></path></g></svg></div></a><a class="runestone-link" href="https://runestone.academy" title="Runestone Academy"><img class="logo" src="https://runestone.academy/runestone/static/images/RAIcon_cropped.png" alt="Runstone Academy logo"></a><a class="mathjax-link" href="https://www.mathjax.org" title="MathJax"><img class="logo" src="https://www.mathjax.org/badge/badge-square-2.png" alt="MathJax logo"></a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -101,7 +101,7 @@ eBookConfig.allow_pairs = false;
|
||||
eBookConfig.enableScratchAC = false;
|
||||
eBookConfig.build_info = "";
|
||||
eBookConfig.python3 = null;
|
||||
eBookConfig.runestone_version = '7.11.8';
|
||||
eBookConfig.runestone_version = '7.11.9';
|
||||
eBookConfig.jobehost = '';
|
||||
eBookConfig.proxyuri_runs = '';
|
||||
eBookConfig.proxyuri_files = '';
|
||||
@@ -161,6 +161,19 @@ eBookConfig.enable_chatcodes = false;
|
||||
</ul>
|
||||
</li></ul>
|
||||
</li>
|
||||
<li class="toc-item toc-chapter">
|
||||
<div class="toc-title-box"><a href="ch-notes.html" class="internal"><span class="title">Class Notes</span></a></div>
|
||||
<ul class="structural toc-item-list"><li class="toc-item toc-section">
|
||||
<div class="toc-title-box"><a href="notes-week-01.html" class="internal"><span class="title">Week 1</span></a></div>
|
||||
<ul class="structural toc-item-list"><li class="toc-item toc-subsection">
|
||||
<div class="toc-title-box"><a href="notes-week-01.html#notes-week-01-3" class="internal"><span class="title">Tuesday 1/13</span></a></div>
|
||||
<ul class="structural toc-item-list">
|
||||
<li class="toc-item toc-subsubsection"><div class="toc-title-box"><a href="notes-week-01.html#subsubsec-Sets" class="internal"><span class="title">Sec 1.1: Sets</span></a></div></li>
|
||||
<li class="toc-item toc-subsubsection"><div class="toc-title-box"><a href="notes-week-01.html#subsubsec-Probability" class="internal"><span class="title">Sec 1.2: Probability</span></a></div></li>
|
||||
</ul>
|
||||
</li></ul>
|
||||
</li></ul>
|
||||
</li>
|
||||
<li class="toc-item toc-chapter contains-active">
|
||||
<div class="toc-title-box"><a href="recitations.html" class="internal"><span class="title">Recitations</span></a></div>
|
||||
<ul class="structural toc-item-list contains-active"><li class="toc-item toc-worksheet active"><div class="toc-title-box"><a href="recitation-calculus-review.html" class="internal"><span class="title">Calculus Review</span></a></div></li></ul>
|
||||
@@ -232,7 +245,7 @@ eBookConfig.enable_chatcodes = false;
|
||||
</div>
|
||||
<div class="workspace" data-space="2cm"></div>
|
||||
<div class="autopermalink" data-description="Task 4.b"><a href="#recitation-calculus-review-3-4-3" title="Copy heading and permalink for Task 4.b" aria-label="Copy heading and permalink for Task 4.b">🔗</a></div></article><div class="autopermalink" data-description="Worksheet Exercise 4"><a href="#recitation-calculus-review-3-4" title="Copy heading and permalink for Worksheet Exercise 4" aria-label="Copy heading and permalink for Worksheet Exercise 4">🔗</a></div></article></section><section class="onepage" id="recitation-calculus-review-4"><article class="theorem theorem-like" id="thm-FTC"><h3 class="heading">
|
||||
<span class="type">Theorem</span><span class="space heading-divison-mark heading-divison-mark__space"> </span><span class="codenumber">1</span><span class="period heading-divison-mark heading-divison-mark__period">.</span><span class="space heading-divison-mark heading-divison-mark__space"> </span><span class="title">Fundamental Theorem of Calculus.</span>
|
||||
<span class="type">Theorem</span><span class="space heading-divison-mark heading-divison-mark__space"> </span><span class="codenumber">22</span><span class="period heading-divison-mark heading-divison-mark__period">.</span><span class="space heading-divison-mark heading-divison-mark__space"> </span><span class="title">Fundamental Theorem of Calculus.</span>
|
||||
</h3>
|
||||
<div class="para logical" id="thm-FTC-2-1">
|
||||
<div class="para">If <span class="process-math">\(F(x)\)</span> is an antiderivative of <span class="process-math">\(f(x)\)</span>—i.e., if <span class="process-math">\(F'(x) = f(x)\)</span>—then:</div>
|
||||
@@ -243,7 +256,7 @@ eBookConfig.enable_chatcodes = false;
|
||||
</div>
|
||||
<div class="autopermalink" data-description="Paragraph"><a href="#thm-FTC-2-1" title="Copy heading and permalink for Paragraph" aria-label="Copy heading and permalink for Paragraph">🔗</a></div>
|
||||
</div>
|
||||
<div class="autopermalink" data-description="Theorem 1: Fundamental Theorem of Calculus"><a href="#thm-FTC" title="Copy heading and permalink for Theorem 1: Fundamental Theorem of Calculus" aria-label="Copy heading and permalink for Theorem 1: Fundamental Theorem of Calculus">🔗</a></div></article><div class="para" id="recitation-calculus-review-4-2">If <span class="process-math">\(f(x)\)</span> is a probability density function (don’t worry about what that means for now), then integrals such as <span class="process-math">\(\int_a^b f(x)\ dx\)</span> are used to calculate certain probabilities, as are values of the form <span class="process-math">\(F(x)\text{.}\)</span><div class="autopermalink" data-description="Paragraph"><a href="#recitation-calculus-review-4-2" title="Copy heading and permalink for Paragraph" aria-label="Copy heading and permalink for Paragraph">🔗</a></div>
|
||||
<div class="autopermalink" data-description="Theorem 22: Fundamental Theorem of Calculus"><a href="#thm-FTC" title="Copy heading and permalink for Theorem 22: Fundamental Theorem of Calculus" aria-label="Copy heading and permalink for Theorem 22: Fundamental Theorem of Calculus">🔗</a></div></article><div class="para" id="recitation-calculus-review-4-2">If <span class="process-math">\(f(x)\)</span> is a probability density function (don’t worry about what that means for now), then integrals such as <span class="process-math">\(\int_a^b f(x)\ dx\)</span> are used to calculate certain probabilities, as are values of the form <span class="process-math">\(F(x)\text{.}\)</span><div class="autopermalink" data-description="Paragraph"><a href="#recitation-calculus-review-4-2" title="Copy heading and permalink for Paragraph" aria-label="Copy heading and permalink for Paragraph">🔗</a></div>
|
||||
</div>
|
||||
<article class="exercise exercise-like" id="recitation-calculus-review-4-3"><h3 class="heading"><span class="codenumber">5<span class="period heading-divison-mark heading-divison-mark__period">.</span></span></h3>
|
||||
<div class="introduction" id="recitation-calculus-review-4-3-1"><div class="para" id="recitation-calculus-review-4-3-1-1">Practice with the following integrals:<div class="autopermalink" data-description="Paragraph"><a href="#recitation-calculus-review-4-3-1-1" title="Copy heading and permalink for Paragraph" aria-label="Copy heading and permalink for Paragraph">🔗</a></div>
|
||||
|
||||
@@ -101,7 +101,7 @@ eBookConfig.allow_pairs = false;
|
||||
eBookConfig.enableScratchAC = false;
|
||||
eBookConfig.build_info = "";
|
||||
eBookConfig.python3 = null;
|
||||
eBookConfig.runestone_version = '7.11.8';
|
||||
eBookConfig.runestone_version = '7.11.9';
|
||||
eBookConfig.jobehost = '';
|
||||
eBookConfig.proxyuri_runs = '';
|
||||
eBookConfig.proxyuri_files = '';
|
||||
@@ -161,6 +161,19 @@ eBookConfig.enable_chatcodes = false;
|
||||
</ul>
|
||||
</li></ul>
|
||||
</li>
|
||||
<li class="toc-item toc-chapter">
|
||||
<div class="toc-title-box"><a href="ch-notes.html" class="internal"><span class="title">Class Notes</span></a></div>
|
||||
<ul class="structural toc-item-list"><li class="toc-item toc-section">
|
||||
<div class="toc-title-box"><a href="notes-week-01.html" class="internal"><span class="title">Week 1</span></a></div>
|
||||
<ul class="structural toc-item-list"><li class="toc-item toc-subsection">
|
||||
<div class="toc-title-box"><a href="notes-week-01.html#notes-week-01-3" class="internal"><span class="title">Tuesday 1/13</span></a></div>
|
||||
<ul class="structural toc-item-list">
|
||||
<li class="toc-item toc-subsubsection"><div class="toc-title-box"><a href="notes-week-01.html#subsubsec-Sets" class="internal"><span class="title">Sec 1.1: Sets</span></a></div></li>
|
||||
<li class="toc-item toc-subsubsection"><div class="toc-title-box"><a href="notes-week-01.html#subsubsec-Probability" class="internal"><span class="title">Sec 1.2: Probability</span></a></div></li>
|
||||
</ul>
|
||||
</li></ul>
|
||||
</li></ul>
|
||||
</li>
|
||||
<li class="toc-item toc-chapter contains-active">
|
||||
<div class="toc-title-box"><a href="recitations.html" class="internal"><span class="title">Recitations</span></a></div>
|
||||
<ul class="structural toc-item-list contains-active"><li class="toc-item toc-worksheet active"><div class="toc-title-box"><a href="recitation-calculus-review.html" class="internal"><span class="title">Calculus Review</span></a></div></li></ul>
|
||||
@@ -217,7 +230,7 @@ eBookConfig.enable_chatcodes = false;
|
||||
</div>
|
||||
<div class="workspace" data-space="2cm"></div>
|
||||
<div class="autopermalink" data-description="Task 4.b"><a href="#recitation-calculus-review-3-4-3" title="Copy heading and permalink for Task 4.b" aria-label="Copy heading and permalink for Task 4.b">🔗</a></div></article><div class="autopermalink" data-description="Worksheet Exercise 4"><a href="#recitation-calculus-review-3-4" title="Copy heading and permalink for Worksheet Exercise 4" aria-label="Copy heading and permalink for Worksheet Exercise 4">🔗</a></div></article></section><section class="onepage" id="recitation-calculus-review-4"><article class="theorem theorem-like" id="thm-FTC"><h3 class="heading">
|
||||
<span class="type">Theorem</span><span class="space heading-divison-mark heading-divison-mark__space"> </span><span class="codenumber">1</span><span class="period heading-divison-mark heading-divison-mark__period">.</span><span class="space heading-divison-mark heading-divison-mark__space"> </span><span class="title">Fundamental Theorem of Calculus.</span>
|
||||
<span class="type">Theorem</span><span class="space heading-divison-mark heading-divison-mark__space"> </span><span class="codenumber">22</span><span class="period heading-divison-mark heading-divison-mark__period">.</span><span class="space heading-divison-mark heading-divison-mark__space"> </span><span class="title">Fundamental Theorem of Calculus.</span>
|
||||
</h3>
|
||||
<div class="para logical" id="thm-FTC-2-1">
|
||||
<div class="para">If <span class="process-math">\(F(x)\)</span> is an antiderivative of <span class="process-math">\(f(x)\)</span>—i.e., if <span class="process-math">\(F'(x) = f(x)\)</span>—then:</div>
|
||||
@@ -228,7 +241,7 @@ eBookConfig.enable_chatcodes = false;
|
||||
</div>
|
||||
<div class="autopermalink" data-description="Paragraph"><a href="#thm-FTC-2-1" title="Copy heading and permalink for Paragraph" aria-label="Copy heading and permalink for Paragraph">🔗</a></div>
|
||||
</div>
|
||||
<div class="autopermalink" data-description="Theorem 1: Fundamental Theorem of Calculus"><a href="#thm-FTC" title="Copy heading and permalink for Theorem 1: Fundamental Theorem of Calculus" aria-label="Copy heading and permalink for Theorem 1: Fundamental Theorem of Calculus">🔗</a></div></article><div class="para" id="recitation-calculus-review-4-2">If <span class="process-math">\(f(x)\)</span> is a probability density function (don’t worry about what that means for now), then integrals such as <span class="process-math">\(\int_a^b f(x)\ dx\)</span> are used to calculate certain probabilities, as are values of the form <span class="process-math">\(F(x)\text{.}\)</span><div class="autopermalink" data-description="Paragraph"><a href="#recitation-calculus-review-4-2" title="Copy heading and permalink for Paragraph" aria-label="Copy heading and permalink for Paragraph">🔗</a></div>
|
||||
<div class="autopermalink" data-description="Theorem 22: Fundamental Theorem of Calculus"><a href="#thm-FTC" title="Copy heading and permalink for Theorem 22: Fundamental Theorem of Calculus" aria-label="Copy heading and permalink for Theorem 22: Fundamental Theorem of Calculus">🔗</a></div></article><div class="para" id="recitation-calculus-review-4-2">If <span class="process-math">\(f(x)\)</span> is a probability density function (don’t worry about what that means for now), then integrals such as <span class="process-math">\(\int_a^b f(x)\ dx\)</span> are used to calculate certain probabilities, as are values of the form <span class="process-math">\(F(x)\text{.}\)</span><div class="autopermalink" data-description="Paragraph"><a href="#recitation-calculus-review-4-2" title="Copy heading and permalink for Paragraph" aria-label="Copy heading and permalink for Paragraph">🔗</a></div>
|
||||
</div>
|
||||
<article class="exercise exercise-like" id="recitation-calculus-review-4-3"><h3 class="heading"><span class="codenumber">5<span class="period heading-divison-mark heading-divison-mark__period">.</span></span></h3>
|
||||
<div class="introduction" id="recitation-calculus-review-4-3-1"><div class="para" id="recitation-calculus-review-4-3-1-1">Practice with the following integrals:<div class="autopermalink" data-description="Paragraph"><a href="#recitation-calculus-review-4-3-1-1" title="Copy heading and permalink for Paragraph" aria-label="Copy heading and permalink for Paragraph">🔗</a></div>
|
||||
|
||||
+16
-3
@@ -101,7 +101,7 @@ eBookConfig.allow_pairs = false;
|
||||
eBookConfig.enableScratchAC = false;
|
||||
eBookConfig.build_info = "";
|
||||
eBookConfig.python3 = null;
|
||||
eBookConfig.runestone_version = '7.11.8';
|
||||
eBookConfig.runestone_version = '7.11.9';
|
||||
eBookConfig.jobehost = '';
|
||||
eBookConfig.proxyuri_runs = '';
|
||||
eBookConfig.proxyuri_files = '';
|
||||
@@ -135,7 +135,7 @@ eBookConfig.enable_chatcodes = false;
|
||||
<textarea class="embed-code-textbox" id="embed-code-textbox" readonly aria-label="textbox"><iframe src="https://example.com/embed" width="100%" height="1000"></iframe></textarea><button class="copy-embed-button button" id="copy-embed-button" title="Copy embed code"><span class="icon material-symbols-outlined" aria-hidden="true"></span><span class="name">Copy</span></button>
|
||||
</div>
|
||||
</div>
|
||||
<button id="light-dark-button" class="light-dark-button button" title="Dark Mode"><span class="icon material-symbols-outlined" aria-hidden="true"></span><span class="name">Dark Mode</span></button></span><span class="treebuttons"><a class="previous-button button" href="syllabus.html" title="Previous"><span class="icon material-symbols-outlined" aria-hidden="true"></span><span class="name">Prev</span></a><a class="up-button button" href="root-1-2.html" title="Up"><span class="icon material-symbols-outlined" aria-hidden="true"></span><span class="name">Up</span></a><a class="next-button button" href="recitation-calculus-review.html" title="Next"><span class="name">Next</span><span class="icon material-symbols-outlined" aria-hidden="true"></span></a></span>
|
||||
<button id="light-dark-button" class="light-dark-button button" title="Dark Mode"><span class="icon material-symbols-outlined" aria-hidden="true"></span><span class="name">Dark Mode</span></button></span><span class="treebuttons"><a class="previous-button button" href="notes-week-01.html" title="Previous"><span class="icon material-symbols-outlined" aria-hidden="true"></span><span class="name">Prev</span></a><a class="up-button button" href="root-1-2.html" title="Up"><span class="icon material-symbols-outlined" aria-hidden="true"></span><span class="name">Up</span></a><a class="next-button button" href="recitation-calculus-review.html" title="Next"><span class="name">Next</span><span class="icon material-symbols-outlined" aria-hidden="true"></span></a></span>
|
||||
</div></nav><div id="latex-macros" class="hidden-content process-math" style="display:none"><span class="process-math">\(\newcommand{\N}{\mathbb N}
|
||||
\newcommand{\Z}{\mathbb Z}
|
||||
\newcommand{\Q}{\mathbb Q}
|
||||
@@ -161,6 +161,19 @@ eBookConfig.enable_chatcodes = false;
|
||||
</ul>
|
||||
</li></ul>
|
||||
</li>
|
||||
<li class="toc-item toc-chapter">
|
||||
<div class="toc-title-box"><a href="ch-notes.html" class="internal"><span class="title">Class Notes</span></a></div>
|
||||
<ul class="structural toc-item-list"><li class="toc-item toc-section">
|
||||
<div class="toc-title-box"><a href="notes-week-01.html" class="internal"><span class="title">Week 1</span></a></div>
|
||||
<ul class="structural toc-item-list"><li class="toc-item toc-subsection">
|
||||
<div class="toc-title-box"><a href="notes-week-01.html#notes-week-01-3" class="internal"><span class="title">Tuesday 1/13</span></a></div>
|
||||
<ul class="structural toc-item-list">
|
||||
<li class="toc-item toc-subsubsection"><div class="toc-title-box"><a href="notes-week-01.html#subsubsec-Sets" class="internal"><span class="title">Sec 1.1: Sets</span></a></div></li>
|
||||
<li class="toc-item toc-subsubsection"><div class="toc-title-box"><a href="notes-week-01.html#subsubsec-Probability" class="internal"><span class="title">Sec 1.2: Probability</span></a></div></li>
|
||||
</ul>
|
||||
</li></ul>
|
||||
</li></ul>
|
||||
</li>
|
||||
<li class="toc-item toc-chapter contains-active">
|
||||
<div class="toc-title-box"><a href="recitations.html" class="internal"><span class="title">Recitations</span></a></div>
|
||||
<ul class="structural toc-item-list active"><li class="toc-item toc-worksheet"><div class="toc-title-box"><a href="recitation-calculus-review.html" class="internal"><span class="title">Calculus Review</span></a></div></li></ul>
|
||||
@@ -171,7 +184,7 @@ eBookConfig.enable_chatcodes = false;
|
||||
</h2>
|
||||
<nav class="summary-links"><ul><li><a href="recitation-calculus-review.html" class="internal"><span class="title">Calculus Review</span></a></li></ul></nav><div class="autopermalink" data-description="Chapter: Recitations"><a href="#recitations" title="Copy heading and permalink for Chapter: Recitations" aria-label="Copy heading and permalink for Chapter: Recitations">🔗</a></div></section></div>
|
||||
<div id="ptx-content-footer" class="ptx-content-footer">
|
||||
<a class="previous-button button" href="syllabus.html" title="Previous"><span class="icon material-symbols-outlined" aria-hidden="true"></span><span class="name">Prev</span></a><a class="top-button button" href="#" title="Top"><span class="icon material-symbols-outlined" aria-hidden="true"></span><span class="name">Top</span></a><a class="next-button button" href="recitation-calculus-review.html" title="Next"><span class="name">Next</span><span class="icon material-symbols-outlined" aria-hidden="true"></span></a>
|
||||
<a class="previous-button button" href="notes-week-01.html" title="Previous"><span class="icon material-symbols-outlined" aria-hidden="true"></span><span class="name">Prev</span></a><a class="top-button button" href="#" title="Top"><span class="icon material-symbols-outlined" aria-hidden="true"></span><span class="name">Top</span></a><a class="next-button button" href="recitation-calculus-review.html" title="Next"><span class="name">Next</span><span class="icon material-symbols-outlined" aria-hidden="true"></span></a>
|
||||
</div></main>
|
||||
</div>
|
||||
<div id="ptx-page-footer" class="ptx-page-footer">
|
||||
|
||||
+15
-1
@@ -101,7 +101,7 @@ eBookConfig.allow_pairs = false;
|
||||
eBookConfig.enableScratchAC = false;
|
||||
eBookConfig.build_info = "";
|
||||
eBookConfig.python3 = null;
|
||||
eBookConfig.runestone_version = '7.11.8';
|
||||
eBookConfig.runestone_version = '7.11.9';
|
||||
eBookConfig.jobehost = '';
|
||||
eBookConfig.proxyuri_runs = '';
|
||||
eBookConfig.proxyuri_files = '';
|
||||
@@ -162,6 +162,19 @@ eBookConfig.enable_chatcodes = false;
|
||||
</li></ul>
|
||||
</li>
|
||||
<li class="toc-item toc-chapter">
|
||||
<div class="toc-title-box"><a href="ch-notes.html" class="internal"><span class="title">Class Notes</span></a></div>
|
||||
<ul class="structural toc-item-list"><li class="toc-item toc-section">
|
||||
<div class="toc-title-box"><a href="notes-week-01.html" class="internal"><span class="title">Week 1</span></a></div>
|
||||
<ul class="structural toc-item-list"><li class="toc-item toc-subsection">
|
||||
<div class="toc-title-box"><a href="notes-week-01.html#notes-week-01-3" class="internal"><span class="title">Tuesday 1/13</span></a></div>
|
||||
<ul class="structural toc-item-list">
|
||||
<li class="toc-item toc-subsubsection"><div class="toc-title-box"><a href="notes-week-01.html#subsubsec-Sets" class="internal"><span class="title">Sec 1.1: Sets</span></a></div></li>
|
||||
<li class="toc-item toc-subsubsection"><div class="toc-title-box"><a href="notes-week-01.html#subsubsec-Probability" class="internal"><span class="title">Sec 1.2: Probability</span></a></div></li>
|
||||
</ul>
|
||||
</li></ul>
|
||||
</li></ul>
|
||||
</li>
|
||||
<li class="toc-item toc-chapter">
|
||||
<div class="toc-title-box"><a href="recitations.html" class="internal"><span class="title">Recitations</span></a></div>
|
||||
<ul class="structural toc-item-list"><li class="toc-item toc-worksheet"><div class="toc-title-box"><a href="recitation-calculus-review.html" class="internal"><span class="title">Calculus Review</span></a></div></li></ul>
|
||||
</li>
|
||||
@@ -169,6 +182,7 @@ eBookConfig.enable_chatcodes = false;
|
||||
<main class="ptx-main"><div id="ptx-content" class="ptx-content"><section class="book" id="root-1-2"><nav class="summary-links"><ul>
|
||||
<li><a href="frontmatter.html" class="internal"><span class="title">Front Matter</span></a></li>
|
||||
<li><a href="course-documents.html" class="internal"><span class="title">Course Documents</span></a></li>
|
||||
<li><a href="ch-notes.html" class="internal"><span class="title">Class Notes</span></a></li>
|
||||
<li><a href="recitations.html" class="internal"><span class="title">Recitations</span></a></li>
|
||||
</ul></nav><div class="autopermalink" data-description="Book: Introduction to Probability and Statistics for the Life Sciences"><a href="#root-1-2" title="Copy heading and permalink for Book: Introduction to Probability and Statistics for the Life Sciences" aria-label="Copy heading and permalink for Book: Introduction to Probability and Statistics for the Life Sciences">🔗</a></div></section></div>
|
||||
<div id="ptx-content-footer" class="ptx-content-footer">
|
||||
|
||||
+16
-3
@@ -101,7 +101,7 @@ eBookConfig.allow_pairs = false;
|
||||
eBookConfig.enableScratchAC = false;
|
||||
eBookConfig.build_info = "";
|
||||
eBookConfig.python3 = null;
|
||||
eBookConfig.runestone_version = '7.11.8';
|
||||
eBookConfig.runestone_version = '7.11.9';
|
||||
eBookConfig.jobehost = '';
|
||||
eBookConfig.proxyuri_runs = '';
|
||||
eBookConfig.proxyuri_files = '';
|
||||
@@ -135,7 +135,7 @@ eBookConfig.enable_chatcodes = false;
|
||||
<textarea class="embed-code-textbox" id="embed-code-textbox" readonly aria-label="textbox"><iframe src="https://example.com/embed" width="100%" height="1000"></iframe></textarea><button class="copy-embed-button button" id="copy-embed-button" title="Copy embed code"><span class="icon material-symbols-outlined" aria-hidden="true"></span><span class="name">Copy</span></button>
|
||||
</div>
|
||||
</div>
|
||||
<button id="light-dark-button" class="light-dark-button button" title="Dark Mode"><span class="icon material-symbols-outlined" aria-hidden="true"></span><span class="name">Dark Mode</span></button></span><span class="treebuttons"><a class="previous-button button" href="course-documents.html" title="Previous"><span class="icon material-symbols-outlined" aria-hidden="true"></span><span class="name">Prev</span></a><a class="up-button button" href="course-documents.html" title="Up"><span class="icon material-symbols-outlined" aria-hidden="true"></span><span class="name">Up</span></a><a class="next-button button" href="recitations.html" title="Next"><span class="name">Next</span><span class="icon material-symbols-outlined" aria-hidden="true"></span></a></span>
|
||||
<button id="light-dark-button" class="light-dark-button button" title="Dark Mode"><span class="icon material-symbols-outlined" aria-hidden="true"></span><span class="name">Dark Mode</span></button></span><span class="treebuttons"><a class="previous-button button" href="course-documents.html" title="Previous"><span class="icon material-symbols-outlined" aria-hidden="true"></span><span class="name">Prev</span></a><a class="up-button button" href="course-documents.html" title="Up"><span class="icon material-symbols-outlined" aria-hidden="true"></span><span class="name">Up</span></a><a class="next-button button" href="ch-notes.html" title="Next"><span class="name">Next</span><span class="icon material-symbols-outlined" aria-hidden="true"></span></a></span>
|
||||
</div></nav><div id="latex-macros" class="hidden-content process-math" style="display:none"><span class="process-math">\(\newcommand{\N}{\mathbb N}
|
||||
\newcommand{\Z}{\mathbb Z}
|
||||
\newcommand{\Q}{\mathbb Q}
|
||||
@@ -162,6 +162,19 @@ eBookConfig.enable_chatcodes = false;
|
||||
</li></ul>
|
||||
</li>
|
||||
<li class="toc-item toc-chapter">
|
||||
<div class="toc-title-box"><a href="ch-notes.html" class="internal"><span class="title">Class Notes</span></a></div>
|
||||
<ul class="structural toc-item-list"><li class="toc-item toc-section">
|
||||
<div class="toc-title-box"><a href="notes-week-01.html" class="internal"><span class="title">Week 1</span></a></div>
|
||||
<ul class="structural toc-item-list"><li class="toc-item toc-subsection">
|
||||
<div class="toc-title-box"><a href="notes-week-01.html#notes-week-01-3" class="internal"><span class="title">Tuesday 1/13</span></a></div>
|
||||
<ul class="structural toc-item-list">
|
||||
<li class="toc-item toc-subsubsection"><div class="toc-title-box"><a href="notes-week-01.html#subsubsec-Sets" class="internal"><span class="title">Sec 1.1: Sets</span></a></div></li>
|
||||
<li class="toc-item toc-subsubsection"><div class="toc-title-box"><a href="notes-week-01.html#subsubsec-Probability" class="internal"><span class="title">Sec 1.2: Probability</span></a></div></li>
|
||||
</ul>
|
||||
</li></ul>
|
||||
</li></ul>
|
||||
</li>
|
||||
<li class="toc-item toc-chapter">
|
||||
<div class="toc-title-box"><a href="recitations.html" class="internal"><span class="title">Recitations</span></a></div>
|
||||
<ul class="structural toc-item-list"><li class="toc-item toc-worksheet"><div class="toc-title-box"><a href="recitation-calculus-review.html" class="internal"><span class="title">Calculus Review</span></a></div></li></ul>
|
||||
</li>
|
||||
@@ -432,7 +445,7 @@ eBookConfig.enable_chatcodes = false;
|
||||
</div>
|
||||
<div class="autopermalink" data-description="Paragraphs: Student Support Services"><a href="#sec-grading-14" title="Copy heading and permalink for Paragraphs: Student Support Services" aria-label="Copy heading and permalink for Paragraphs: Student Support Services">🔗</a></div></section><div class="autopermalink" data-description="Subsection: Grading"><a href="#sec-grading" title="Copy heading and permalink for Subsection: Grading" aria-label="Copy heading and permalink for Subsection: Grading">🔗</a></div></section><div class="autopermalink" data-description="Section: Syllabus"><a href="#syllabus" title="Copy heading and permalink for Section: Syllabus" aria-label="Copy heading and permalink for Section: Syllabus">🔗</a></div></section></div>
|
||||
<div id="ptx-content-footer" class="ptx-content-footer">
|
||||
<a class="previous-button button" href="course-documents.html" title="Previous"><span class="icon material-symbols-outlined" aria-hidden="true"></span><span class="name">Prev</span></a><a class="top-button button" href="#" title="Top"><span class="icon material-symbols-outlined" aria-hidden="true"></span><span class="name">Top</span></a><a class="next-button button" href="recitations.html" title="Next"><span class="name">Next</span><span class="icon material-symbols-outlined" aria-hidden="true"></span></a>
|
||||
<a class="previous-button button" href="course-documents.html" title="Previous"><span class="icon material-symbols-outlined" aria-hidden="true"></span><span class="name">Prev</span></a><a class="top-button button" href="#" title="Top"><span class="icon material-symbols-outlined" aria-hidden="true"></span><span class="name">Top</span></a><a class="next-button button" href="ch-notes.html" title="Next"><span class="name">Next</span><span class="icon material-symbols-outlined" aria-hidden="true"></span></a>
|
||||
</div></main>
|
||||
</div>
|
||||
<div id="ptx-page-footer" class="ptx-page-footer">
|
||||
|
||||
+552
@@ -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
Executable
+4988
File diff suppressed because it is too large
Load Diff
+480
@@ -0,0 +1,480 @@
|
||||
|
||||
/*
|
||||
highlight_css = document.createElement('style');
|
||||
highlight_css.type = "text/css";
|
||||
highlight_css.id = "highlight_css";
|
||||
document.head.appendChild(highlight_css);
|
||||
var css_for_hl = 'span.hl { background: yellow; }\n';
|
||||
css_for_hl += '#hlmenu { position: absolute; top: 300px; left: 200px;}\n';
|
||||
css_for_hl += '#hlmenu { padding: 8px; background: #FFF; }\n';
|
||||
css_for_hl += '#hlmenu { box-shadow: 8px 10px 5px #888; border: 1px solid #aaa;}\n';
|
||||
css_for_hl += '#hlmenu .hldelete { background: #fdd; }';
|
||||
css_for_hl += '#hlmenu .hldelete:hover { background: #fbb; }';
|
||||
css_for_hl += '#hlmenu .hlcopy { background: #ddf; }';
|
||||
css_for_hl += '#hlmenu .hlcopy:hover { background: #bbf; }';
|
||||
css_for_hl += '#hlmenu .dismiss:hover { background: #ff9; }';
|
||||
css_for_hl += '#hlmenu > div { padding: 4px; font-size: 90%}';
|
||||
highlight_css.innerHTML = css_for_hl;
|
||||
*/
|
||||
|
||||
hlmenu = document.createElement('div');
|
||||
hlmenu.id = "hlmenu";
|
||||
hlmenu.style.display = "none";
|
||||
hlmenu_contents = '<div class="hlcopy" data-hlid="">copy to clipboard</div>\n';
|
||||
hlmenu_contents += '<div class="hldelete" data-hlid="">delete highlight</div>\n';
|
||||
hlmenu_contents += '<div class="dismiss">dismiss menu</div>';
|
||||
hlmenu.innerHTML = hlmenu_contents;
|
||||
document.body.appendChild(hlmenu);
|
||||
|
||||
var all_highlights = localStorage.getObject("all_highlights");
|
||||
if (!all_highlights) {
|
||||
console.log("no highlights on this page",all_highlights);
|
||||
all_highlights= {};
|
||||
} else {
|
||||
console.log("highlights already",all_highlights);
|
||||
}
|
||||
|
||||
console.log("all_highlights.keys()", Object.keys(all_highlights));
|
||||
console.log("waiting for MathJax");
|
||||
MathJax.Hub.Register.StartupHook("End",function () {
|
||||
// need to wait for MathJax, because MathJax changes the number of nodes in a paragraph
|
||||
console.log("MathJax is done");
|
||||
display_all_highlights(all_highlights, Object.keys(all_highlights), 0);
|
||||
});
|
||||
|
||||
|
||||
/*
|
||||
for (var key in all_highlights) {
|
||||
var this_key = key;
|
||||
var these_highlights = all_highlights[key];
|
||||
console.log("adding highlights to", key);
|
||||
// await display_highlights_on(key, all_highlights[key], 0)
|
||||
for (var i=0; i< these_highlights.length; ++i) {
|
||||
hl = these_highlights[i];
|
||||
console.log("inserting highlight",i,"which is",hl, "on", key);
|
||||
// display_one_highlight(key, hl);
|
||||
//fails because
|
||||
setTimeout(function() { display_one_highlight(this_key, hl)}, i*2000);
|
||||
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
function index_of_child(child) {
|
||||
var i = 0;
|
||||
while( (child = child.previousSibling) != null )
|
||||
{i++ }
|
||||
return i
|
||||
}
|
||||
|
||||
function increment_id(list_of_things_with_ids) { // assume ids are of the form xxxxNN with N a digit and x a non-digit
|
||||
console.log("incrementing id on", list_of_things_with_ids);
|
||||
if (list_of_things_with_ids.length == 0) {
|
||||
return 0
|
||||
}
|
||||
|
||||
id_start = list_of_things_with_ids[0]["id"].replace(/^(.*?)[0-9]+$/, "$1");
|
||||
current_endings = [];
|
||||
for (var i=0; i < list_of_things_with_ids.length; ++i) {
|
||||
current_endings.push(list_of_things_with_ids[i]["id"].replace(/^.*?([0-9]+)$/, "$1"));
|
||||
}
|
||||
console.log("existing id endings", current_endings);
|
||||
current_max = Math.max(...current_endings);
|
||||
return id_start + (parseInt(current_max) + 1)
|
||||
}
|
||||
|
||||
function enclosing_p_or_li(obj) {
|
||||
console.log("obj.tagName", obj.tagName, "ggg",obj);
|
||||
if (!obj) {
|
||||
console.log("problem with previous object");
|
||||
return null
|
||||
}
|
||||
else if (obj.tagName == 'P' || obj.tagName == 'LI') {
|
||||
return obj
|
||||
}
|
||||
return enclosing_p_or_li(obj.parentNode)
|
||||
}
|
||||
|
||||
// If you just loop ofer the highlights to add them,
|
||||
// you end up with a race condition because each highlight changes
|
||||
// the structure of the paragraph.
|
||||
async function display_one_highlight(parent_id, hl) {
|
||||
var the_parent = document.getElementById(parent_id);
|
||||
if (!the_parent) {
|
||||
console.log("this id not on this page:", parent_id);
|
||||
return
|
||||
}
|
||||
console.log("setting", hl, "on", parent_id);
|
||||
var st_node_ind = hl['start_nn'];
|
||||
var st_offset = hl['start_offset'];
|
||||
var end_node_ind = hl['end_nn'];
|
||||
var end_offset = hl['end_offset'];
|
||||
console.log("st_node_ind", st_node_ind, "st_offset", st_offset, "end_node_ind", end_node_ind, "end_offset", end_offset);
|
||||
// other error checks: same parent
|
||||
if (st_offset < 0 || st_node_ind > the_parent.childNodes.length || end_node_ind > the_parent.childNodes.length || st_offset > the_parent.childNodes[st_node_ind].textContent.length || end_offset < 0 || end_offset > the_parent.childNodes[end_node_ind].textContent.length) {
|
||||
console.log("highlight data inconsistent with paragraph structure that contains", the_parent.childNodes.length, "nodes");
|
||||
return
|
||||
}
|
||||
|
||||
let this_range = document.createRange();
|
||||
console.log("setting this_range.setStart", the_parent.childNodes[st_node_ind], "with offset", st_offset, "out of", the_parent.childNodes[st_node_ind].textContent.length);
|
||||
this_range.setStart(the_parent.childNodes[st_node_ind], st_offset);
|
||||
this_range.setEnd(the_parent.childNodes[end_node_ind], end_offset);
|
||||
var inside_part = document.createElement("span")
|
||||
inside_part.classList.add("hl");
|
||||
inside_part.id = hl['id'];
|
||||
console.log("inside_part", inside_part, "going inside this_range",this_range);
|
||||
console.log("in the_parent",the_parent);
|
||||
this_range.surroundContents(inside_part);
|
||||
return;
|
||||
}
|
||||
|
||||
//var display_highlights_on = function(parent_id, highlights_on, ind) {
|
||||
async function display_highlights_on(parent_id, highlights_on, ind) {
|
||||
console.log("display_highlights_on", parent_id, "xxxx", ind, "out of", highlights_on.length);
|
||||
if (ind < highlights_on.length) {
|
||||
console.log("about to display one highlight, number", ind, "on", parent_id);
|
||||
await display_one_highlight(parent_id, highlights_on[ind]);
|
||||
++ind;
|
||||
await display_highlights_on(parent_id, highlights_on, ind)
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
async function display_all_highlights(every_highlight, hl_p_keys, i) {
|
||||
console.log("making hl on", hl_p_keys[i]);
|
||||
if (i < hl_p_keys.length) {
|
||||
await display_highlights_on(hl_p_keys[i], every_highlight[hl_p_keys[i]], 0);
|
||||
++i;
|
||||
await display_all_highlights(every_highlight, hl_p_keys, i)
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var DELAY = 500, clicks = 0, timer = null;
|
||||
$("p[id], li[id]").on("click", function(e) {
|
||||
clicks++; //count clicks
|
||||
|
||||
if(clicks === 1) {
|
||||
|
||||
if (e.target.classList.contains("hl")) {
|
||||
modifyhighlight(e);
|
||||
clicks = 0;
|
||||
} else {
|
||||
timer = setTimeout(function() {
|
||||
newhighlight()
|
||||
clicks = 0; //after action performed, reset counter
|
||||
}, DELAY);
|
||||
|
||||
}
|
||||
|
||||
|
||||
} else if (clicks === 2) {
|
||||
timer2 = setTimeout(function() {
|
||||
clearTimeout(timer);
|
||||
// alert("Double Click");
|
||||
clicks = 0;
|
||||
}, DELAY/2);
|
||||
} else {
|
||||
clearTimeout(timer); //prevent single-click action
|
||||
clearTimeout(timer2); //prevent double-click action
|
||||
// alert("Triple Click"); //perform triple-click action
|
||||
clicks = 0; //after action performed, reset counter
|
||||
}
|
||||
});
|
||||
|
||||
function save_highlights() {
|
||||
hl_data = {"action": "save", "user": uname, "pw": emanu, "bookID": bodyID, "type": "highlights", "hl": JSON.stringify(all_highlights)}
|
||||
$.ajax({
|
||||
url: "https://aimath.org/cgi-bin/u/highlights.py",
|
||||
type: "post",
|
||||
data: JSON.stringify(hl_data),
|
||||
dataType: "json",
|
||||
success: function(data) {
|
||||
console.log("something", data, "back from highlight");
|
||||
// alert(data);
|
||||
},
|
||||
error: function(errMsg) {
|
||||
console.log("seems to be an error?",errMsg);
|
||||
// alert("Error\n" + errMsg);
|
||||
}
|
||||
});
|
||||
|
||||
console.log("just ajax sent", JSON.stringify(reading_questions_object));
|
||||
}
|
||||
|
||||
function newhighlight() {
|
||||
var this_selection = window.getSelection();
|
||||
console.log("UUUUUUUUUUUUUUUUUUUUUUUUUUU");
|
||||
console.log("selection", this_selection, "as string", this_selection.toString(), "length", this_selection.toString().length);
|
||||
if(this_selection.toString().length >= 3) {
|
||||
console.log("this_selection", this_selection);
|
||||
console.log("this_selection parentNode", this_selection.parentNode);
|
||||
|
||||
num_selected_ranges = this_selection.rangeCount;
|
||||
console.log("this_selection.rangeCount", this_selection.rangeCount);
|
||||
console.log("this_selection.getRangeAt(0)", this_selection.getRangeAt(0));
|
||||
console.log("this_selection.getRangeAt(num_selected_ranges)", this_selection.getRangeAt(num_selected_ranges-1));
|
||||
console.log("this_selection.getRangeAt(0).startContainer", this_selection.getRangeAt(0).startContainer);
|
||||
console.log("this_selection.getRangeAt(0).endContainer", this_selection.getRangeAt(0).endContainer);
|
||||
console.log("this_selection.getRangeAt(-1).startContainer", this_selection.getRangeAt(num_selected_ranges-1).startContainer);
|
||||
console.log("this_selection.getRangeAt(-1).endContainer", this_selection.getRangeAt(num_selected_ranges-1).endContainer);
|
||||
console.log("start equals end", this_selection.getRangeAt(0).startContainer == this_selection.getRangeAt(0).endContainer);
|
||||
console.log("this_selection.getRangeAt(0).startContainer.parent", this_selection.getRangeAt(0).startContainer.parentNode);
|
||||
console.log("this_selection.getRangeAt(0).endContainer.parent", this_selection.getRangeAt(0).endContainer.parentNode);
|
||||
starting_parent = enclosing_p_or_li(this_selection.getRangeAt(0).startContainer);
|
||||
starting_parent_id = starting_parent.id;
|
||||
ending_parent_id = enclosing_p_or_li(this_selection.getRangeAt(num_selected_ranges-1).endContainer).id;
|
||||
console.log("starting parent", enclosing_p_or_li(this_selection.getRangeAt(0).startContainer));
|
||||
console.log("starting_parent.childNodes", starting_parent.childNodes);
|
||||
console.log("starting_parent.childNodes.length", starting_parent.childNodes.length);
|
||||
console.log("ending parent", enclosing_p_or_li(this_selection.getRangeAt(0).endContainer));
|
||||
console.log("index of starting node", index_of_child(this_selection.getRangeAt(0).startContainer));
|
||||
console.log("which is", starting_parent.childNodes[index_of_child(this_selection.getRangeAt(0).startContainer)]);
|
||||
console.log("index of ending node", index_of_child(this_selection.getRangeAt(num_selected_ranges-1).endContainer));
|
||||
console.log("which is", starting_parent.childNodes[index_of_child(this_selection.getRangeAt(num_selected_ranges-1).endContainer)]);
|
||||
console.log("XXXXXXXXXXXXXXXXXXXXXXX");
|
||||
starting_node = this_selection.getRangeAt(0);
|
||||
starting_node_container = starting_node.startContainer;
|
||||
starting_node_number = index_of_child(starting_node_container);
|
||||
starting_node_offset = starting_node.startOffset;
|
||||
console.log("starting_node", starting_node, "starting_node_container", starting_node_container);
|
||||
console.log("starting_parent.childNodes[0]", starting_parent.childNodes[0]);
|
||||
ending_node = this_selection.getRangeAt(num_selected_ranges-1);
|
||||
ending_node_container = ending_node.endContainer;
|
||||
console.log("ending_node", ending_node, "ending_node_container", ending_node_container);
|
||||
ending_node_number = index_of_child(ending_node_container);
|
||||
ending_node_offset = ending_node.endOffset;
|
||||
console.log("selection starts at character number", starting_node.startOffset, "in node number", index_of_child(starting_node), "of node", starting_node.startContainer, "within", starting_node.startContainer.parentNode,"which is", starting_node);
|
||||
console.log("selection ends at character number", ending_node.endOffset, "in node number", index_of_child(ending_node_container), "of node", ending_node.endContainer, "within", ending_node.endContainer.parentNode, "which is", ending_node);
|
||||
|
||||
// let this_range = document.createRange();
|
||||
// this_range.setStart(starting_parent.childNodes[starting_node_number], starting_node_offset);
|
||||
// this_range.setEnd(document.getElementById(starting_parent_id).childNodes[ending_node_number], ending_node_offset);
|
||||
// test_inside_part=document.createElement("span")
|
||||
// test_inside_part.classList.add("gggg");
|
||||
// this_range.surroundContents(test_inside_part);
|
||||
console.log("num_selected_ranges", num_selected_ranges, "starting_parent_id", starting_parent_id, "ending_parent_id", ending_parent_id);
|
||||
this_selection.empty();
|
||||
console.log("starting_parent_id", starting_parent_id, "ending_parent_id", ending_parent_id);
|
||||
if (starting_parent_id != ending_parent_id) {
|
||||
alert("Highlights must be within\none paragraph or list item.");
|
||||
return ""
|
||||
}
|
||||
if (starting_parent != starting_node_container.parentNode) {
|
||||
starting_node_number = index_of_child(starting_node_container.parentNode) - 1;
|
||||
console.log("new starting node",starting_node_number, " which is", starting_parent.childNodes[starting_node_number]);
|
||||
starting_node_offset = starting_parent.childNodes[starting_node_number].length;
|
||||
}
|
||||
if (starting_parent != ending_node_container.parentNode) {
|
||||
ending_node_number = index_of_child(ending_node_container.parentNode) + 1;
|
||||
console.log("new edngin node",ending_node_number, " which is", starting_parent.childNodes[ending_node_number]);
|
||||
ending_node_offset = 0;
|
||||
}
|
||||
console.log("starting_parent eq st cont parent", starting_parent == starting_node_container.parentNode, "starting_parent eq end cont parent", starting_parent == ending_node_container.parentNode);
|
||||
// display_one_highlight(starting_parent_id, starting_node_number, starting_node_offset, ending_node_number, ending_node_offset);
|
||||
// console.log("this_range", this_range);
|
||||
this_highlight = {"start_nn": starting_node_number,
|
||||
"start_offset": starting_node_offset,
|
||||
"end_nn": ending_node_number,
|
||||
"end_offset": ending_node_offset};
|
||||
if (starting_parent_id in all_highlights) {
|
||||
console.log("the starting_parent_id", starting_parent_id, "is already in all_highlights", all_highlights);
|
||||
new_id = increment_id(all_highlights[starting_parent_id]);
|
||||
this_highlight['id'] = new_id;
|
||||
all_highlights[starting_parent_id].push(this_highlight)
|
||||
} else {
|
||||
this_highlight['id'] = starting_parent_id + "-" + "hl" + 1;
|
||||
all_highlights[starting_parent_id] = [this_highlight]
|
||||
}
|
||||
display_one_highlight(starting_parent_id, this_highlight);
|
||||
localStorage.setObject("all_highlights", all_highlights);
|
||||
|
||||
if (uname != "guest" && role=="student") {
|
||||
console.log("saving highlights");
|
||||
save_highlights();
|
||||
}
|
||||
console.log("all_highlights", all_highlights);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function modifyhighlight(e) {
|
||||
this_hl = e.target;
|
||||
console.log("clicked hl", this_hl.id);
|
||||
|
||||
var x = e.clientX, y = e.clientY;
|
||||
console.log("x", x, "y", y);
|
||||
document.getElementById("hlmenu").style.top = (y - 10 + $(window).scrollTop()) + 'px';
|
||||
document.getElementById("hlmenu").style.left = (x + 20) + 'px';
|
||||
document.getElementById("hlmenu").style.display = 'block';
|
||||
document.getElementsByClassName("hlcopy")[0].setAttribute("data-hlid", this_hl.id);
|
||||
document.getElementsByClassName("hldelete")[0].setAttribute("data-hlid", this_hl.id);
|
||||
// tooltipSpan.style.left = (x + 20) + 'px';
|
||||
|
||||
var parent_id = this_hl.id.replace(/^(.*)-[^\-]*$/, "$1");
|
||||
var number_of_this_highlight = this_hl.id.slice(-1);
|
||||
var these_highlights = all_highlights[parent_id];
|
||||
var this_highlight = these_highlights[number_of_this_highlight-1];
|
||||
console.log("parent id", parent_id, "nunber",number_of_this_highlight);
|
||||
var num_child_nodes = this_hl.childNodes.length;
|
||||
console.log("which has", this_hl.childNodes.length, "child nodes");
|
||||
for (var i=0; i < num_child_nodes; ++i) {
|
||||
console.log(i, "node is", this_hl.childNodes[i])
|
||||
}
|
||||
console.log("highlights on this item",all_highlights[parent_id], "of which we clicked", these_highlights[number_of_this_highlight - 1],
|
||||
"number", number_of_this_highlight, "out of", these_highlights.length );
|
||||
for (var i=0; i<these_highlights.length; ++i) {
|
||||
console.log("highilght", i, "is", these_highlights[i])
|
||||
}
|
||||
console.log("this one starts at", this_highlight["start_nn"], "and ends at", this_highlight["end_nn"]);
|
||||
console.log("$(this_hl)", $(this_hl));
|
||||
console.log("$(this_hl).contents", $(this_hl).text());
|
||||
console.log("this_hl.innerHTML", this_hl.innerHTML);
|
||||
|
||||
if (this_hl.childNodes.length == 1) { //highlight contains only text
|
||||
if (this_hl.previousSibling.nodeType == 3 && this_hl.nextSibling.nodeType == 3) { // prev and next are also text
|
||||
console.log("this_hl", this_hl, "this_hl.previousSibling", this_hl.previousSibling, "type", this_hl.previousSibling.nodeType);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//from https://techoverflow.net/2018/03/30/copying-strings-to-the-clipboard-using-pure-javascript/
|
||||
function copyStringToClipboard (str) {
|
||||
// Create new element
|
||||
var el = document.createElement('textarea');
|
||||
// Set value (string to be copied)
|
||||
el.value = str;
|
||||
// Set non-editable to avoid focus and move outside of view
|
||||
el.setAttribute('readonly', '');
|
||||
el.style = {position: 'absolute', left: '-9999px'};
|
||||
document.body.appendChild(el);
|
||||
// Select text inside element
|
||||
el.select();
|
||||
// Copy text to clipboard
|
||||
document.execCommand('copy');
|
||||
// Remove temporary element
|
||||
document.body.removeChild(el);
|
||||
}
|
||||
|
||||
$('body').on('click','.hlcopy', function(e){
|
||||
|
||||
var hl_to_copy_id = this.getAttribute("data-hlid");
|
||||
console.log("copying highlight", hl_to_copy_id);
|
||||
hl_to_copy = document.getElementById(hl_to_copy_id);
|
||||
the_highlight = hl_to_copy.innerHTML;
|
||||
copyStringToClipboard(the_highlight);
|
||||
// console.log("hl_to_copy",hl_to_copy,"the_highlight", the_highlight);
|
||||
// hl_to_copy.focus();
|
||||
// document.execCommand('copy');
|
||||
document.getElementById("hlmenu").style.display = 'none';
|
||||
});
|
||||
|
||||
$('body').on('click','.hldelete', function(e){
|
||||
|
||||
var hl_to_del_id = this.getAttribute("data-hlid");
|
||||
console.log("going to delete", hl_to_del_id);
|
||||
hl_to_delete = document.getElementById(hl_to_del_id);
|
||||
var parent_id = hl_to_del_id.replace(/^(.*)-[^\-]*$/, "$1");
|
||||
var the_parent = document.getElementById(parent_id);
|
||||
|
||||
//and then update the stored highlights
|
||||
//this is complicated, because highlights are described in terms of the structure
|
||||
//of the paragrpah, which depends on which highlights already xist
|
||||
//So first we determine the order in which the highlights appear
|
||||
//(and find out how many nodes are being deleted)
|
||||
var sibling_nodes = the_parent.childNodes;
|
||||
var node_counter = {};
|
||||
var additional_offset, node_increment;
|
||||
var node_ct = 0;
|
||||
for (var i=0; i < sibling_nodes.length; ++i) {
|
||||
if (sibling_nodes[i].id) {
|
||||
node_counter[sibling_nodes[i].id] = node_ct;
|
||||
node_ct += 1;
|
||||
}
|
||||
if (sibling_nodes[i].id == hl_to_del_id) {
|
||||
node_increment = sibling_nodes[i].childNodes.length - 1
|
||||
}
|
||||
}
|
||||
console.log("node_increment", node_increment);
|
||||
console.log("node_counter", node_counter);
|
||||
console.log("deleting node", node_counter[hl_to_del_id]);
|
||||
//find out the offset of the node to be deleted
|
||||
console.log("all_highlights[parent_id]", all_highlights[parent_id]);
|
||||
for (var i=0; i < all_highlights[parent_id].length; ++i) {
|
||||
var one_highlight = all_highlights[parent_id][i];
|
||||
console.log("checking one_highlight", one_highlight);
|
||||
if (node_counter[one_highlight['id']] == node_counter[hl_to_del_id]) {
|
||||
additional_offset = one_highlight['end_offset'];
|
||||
the_hl_to_delete = all_highlights[parent_id][i];
|
||||
}
|
||||
}
|
||||
console.log("the_hl_to_delete", the_hl_to_delete);
|
||||
console.log("additional_offset", additional_offset);
|
||||
//then we re-make the list of highlights of this parent
|
||||
//go through the highlights, adjusting those which we added after the
|
||||
//lh being deleted, and whcich also occurs later in the text.
|
||||
//Note: this is only for the case if pure text paragraphs
|
||||
var new_highlights = [];
|
||||
var previous_highlight;
|
||||
var past_this_hl = false;
|
||||
for (var i=0; i < all_highlights[parent_id].length; ++i) {
|
||||
var one_highlight = all_highlights[parent_id][i];
|
||||
console.log("again checking one_highlight", one_highlight);
|
||||
console.log("x",one_highlight["start_nn"], "y", the_hl_to_delete["end_nn"], "z", the_hl_to_delete["end_nn"] + additional_offset);
|
||||
if (!past_this_hl) {
|
||||
if (one_highlight["id"] == hl_to_del_id) {
|
||||
console.log("found it", one_highlight);
|
||||
past_this_hl = true
|
||||
} else {
|
||||
new_highlights.push(one_highlight)
|
||||
}
|
||||
} else { // hl occurs after, but maybe not physically after
|
||||
console.log('the_hl_to_delete["start_nn"]', the_hl_to_delete["start_nn"], 'one_highlight["start_nn"]', one_highlight["start_nn"]);
|
||||
if (node_counter[one_highlight["id"]] < node_counter[hl_to_del_id]) {
|
||||
console.log("case 0");
|
||||
new_highlights.push(one_highlight)
|
||||
} else if (node_counter[one_highlight["id"]] == node_counter[hl_to_del_id]) {
|
||||
console.log("ERROR, we shoudl be past this point!");
|
||||
continue
|
||||
} else if (one_highlight["start_nn"] == the_hl_to_delete["start_nn"] + 2){
|
||||
console.log("offset match with", one_highlight);
|
||||
previous_highlight = one_highlight;
|
||||
previous_highlight['start_nn'] += -2 + node_increment;
|
||||
previous_highlight['end_nn'] += -2 + node_increment;
|
||||
previous_highlight['start_offset'] += additional_offset;
|
||||
previous_highlight['end_offset'] += additional_offset;
|
||||
new_highlights.push(previous_highlight);
|
||||
} else {
|
||||
console.log("apparently a far away match", one_highlight);
|
||||
previous_highlight = one_highlight;
|
||||
previous_highlight['start_nn'] += -2 + node_increment;
|
||||
previous_highlight['end_nn'] += -2 + node_increment;
|
||||
new_highlights.push(previous_highlight);
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log("the new_highlights", new_highlights, "parent_id", parent_id);
|
||||
if (new_highlights.length > 0) {
|
||||
all_highlights[parent_id] = new_highlights
|
||||
} else {
|
||||
delete all_highlights[parent_id]
|
||||
}
|
||||
|
||||
localStorage.setObject("all_highlights", all_highlights);
|
||||
|
||||
|
||||
// hide the current highlight
|
||||
$(hl_to_delete).replaceWith(hl_to_delete.innerHTML);
|
||||
|
||||
document.getElementById(parent_id).normalize(); // because a previous step creates adjacent text nodes
|
||||
|
||||
document.getElementById("hlmenu").style.display = 'none';
|
||||
});
|
||||
|
||||
$('body').on('click','.dismiss', function(e){
|
||||
console.log(".dismiss of",this);
|
||||
the_parent = this.closest("[id]");
|
||||
the_parent.style.display = 'none';
|
||||
});
|
||||
+131
@@ -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);
|
||||
|
||||
|
||||
+2
File diff suppressed because one or more lines are too long
+161
@@ -0,0 +1,161 @@
|
||||
// Sticky Plugin v1.0.0 for jQuery
|
||||
// =============
|
||||
// Author: Anthony Garand
|
||||
// Improvements by German M. Bravo (Kronuz) and Ruud Kamphuis (ruudk)
|
||||
// Improvements by Leonardo C. Daronco (daronco)
|
||||
// Created: 2/14/2011
|
||||
// Date: 2/12/2012
|
||||
// Website: http://labs.anthonygarand.com/sticky
|
||||
// Description: Makes an element on the page stick on the screen as you scroll
|
||||
// It will only set the 'top' and 'position' of your element, you
|
||||
// might need to adjust the width in some cases.
|
||||
|
||||
(function($) {
|
||||
var defaults = {
|
||||
topSpacing: 0,
|
||||
bottomSpacing: 0,
|
||||
className: 'is-sticky',
|
||||
wrapperClassName: 'sticky-wrapper',
|
||||
center: false,
|
||||
getWidthFrom: ''
|
||||
},
|
||||
$window = $(window),
|
||||
$document = $(document),
|
||||
sticked = [],
|
||||
windowHeight = $window.height(),
|
||||
scroller = function() {
|
||||
var scrollTop = $window.scrollTop(),
|
||||
documentHeight = $document.height(),
|
||||
dwh = documentHeight - windowHeight,
|
||||
extra = (scrollTop > dwh) ? dwh - scrollTop : 0;
|
||||
|
||||
for (var i = 0; i < sticked.length; i++) {
|
||||
var s = sticked[i],
|
||||
elementTop = s.stickyWrapper.offset().top,
|
||||
etse = elementTop - s.topSpacing - extra;
|
||||
|
||||
if (scrollTop <= etse) {
|
||||
if (s.currentTop !== null) {
|
||||
s.stickyElement
|
||||
.css('position', '')
|
||||
.css('top', '');
|
||||
s.stickyElement.parent().removeClass(s.className);
|
||||
s.currentTop = null;
|
||||
}
|
||||
}
|
||||
else {
|
||||
var newTop = documentHeight - s.stickyElement.outerHeight()
|
||||
- s.topSpacing - s.bottomSpacing - scrollTop - extra;
|
||||
if (newTop < 0) {
|
||||
newTop = newTop + s.topSpacing;
|
||||
} else {
|
||||
newTop = s.topSpacing;
|
||||
}
|
||||
if (s.currentTop != newTop) {
|
||||
s.stickyElement
|
||||
.css('position', 'fixed')
|
||||
.css('top', newTop);
|
||||
|
||||
if (typeof s.getWidthFrom !== 'undefined') {
|
||||
s.stickyElement.css('width', $(s.getWidthFrom).width());
|
||||
}
|
||||
|
||||
s.stickyElement.parent().addClass(s.className);
|
||||
s.currentTop = newTop;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
resizer = function() {
|
||||
windowHeight = $window.height();
|
||||
},
|
||||
methods = {
|
||||
init: function(options) {
|
||||
var o = $.extend({}, defaults, options);
|
||||
return this.each(function() {
|
||||
var stickyElement = $(this);
|
||||
|
||||
var stickyId = stickyElement.attr('id');
|
||||
var wrapperId = stickyId ? stickyId + '-' + defaults.wrapperClassName : defaults.wrapperClassName
|
||||
var wrapper = $('<div></div>')
|
||||
.attr('id', stickyId + '-sticky-wrapper')
|
||||
.addClass(o.wrapperClassName);
|
||||
stickyElement.wrapAll(wrapper);
|
||||
|
||||
if (o.center) {
|
||||
stickyElement.parent().css({width:stickyElement.outerWidth(),marginLeft:"auto",marginRight:"auto"});
|
||||
}
|
||||
|
||||
if (stickyElement.css("float") == "right") {
|
||||
stickyElement.css({"float":"none"}).parent().css({"float":"right"});
|
||||
}
|
||||
|
||||
var stickyWrapper = stickyElement.parent();
|
||||
stickyWrapper.css('height', stickyElement.outerHeight());
|
||||
sticked.push({
|
||||
topSpacing: o.topSpacing,
|
||||
bottomSpacing: o.bottomSpacing,
|
||||
stickyElement: stickyElement,
|
||||
currentTop: null,
|
||||
stickyWrapper: stickyWrapper,
|
||||
className: o.className,
|
||||
getWidthFrom: o.getWidthFrom
|
||||
});
|
||||
});
|
||||
},
|
||||
update: scroller,
|
||||
unstick: function(options) {
|
||||
return this.each(function() {
|
||||
var unstickyElement = $(this);
|
||||
|
||||
var removeIdx = -1;
|
||||
for (var i = 0; i < sticked.length; i++)
|
||||
{
|
||||
if (sticked[i].stickyElement.get(0) == unstickyElement.get(0))
|
||||
{
|
||||
removeIdx = i;
|
||||
}
|
||||
}
|
||||
if(removeIdx != -1)
|
||||
{
|
||||
sticked.splice(removeIdx,1);
|
||||
unstickyElement.unwrap();
|
||||
unstickyElement.removeAttr('style');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// should be more efficient than using $window.scroll(scroller) and $window.resize(resizer):
|
||||
if (window.addEventListener) {
|
||||
window.addEventListener('scroll', scroller, false);
|
||||
window.addEventListener('resize', resizer, false);
|
||||
} else if (window.attachEvent) {
|
||||
window.attachEvent('onscroll', scroller);
|
||||
window.attachEvent('onresize', resizer);
|
||||
}
|
||||
|
||||
$.fn.sticky = function(method) {
|
||||
if (methods[method]) {
|
||||
return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
|
||||
} else if (typeof method === 'object' || !method ) {
|
||||
return methods.init.apply( this, arguments );
|
||||
} else {
|
||||
$.error('Method ' + method + ' does not exist on jQuery.sticky');
|
||||
}
|
||||
};
|
||||
|
||||
$.fn.unstick = function(method) {
|
||||
if (methods[method]) {
|
||||
return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
|
||||
} else if (typeof method === 'object' || !method ) {
|
||||
return methods.unstick.apply( this, arguments );
|
||||
} else {
|
||||
$.error('Method ' + method + ' does not exist on jQuery.sticky');
|
||||
}
|
||||
|
||||
};
|
||||
$(function() {
|
||||
setTimeout(scroller, 0);
|
||||
});
|
||||
})(jQuery);
|
||||
+338
@@ -0,0 +1,338 @@
|
||||
// Code controlling behavior of xref knowls and born hidden knowls
|
||||
|
||||
// Assumes this file is loaded as part of initial page
|
||||
window.addEventListener("load", (event) => {
|
||||
addKnowls(document);
|
||||
});
|
||||
|
||||
function addKnowls(target) {
|
||||
const xrefs = target.querySelectorAll("[data-knowl]");
|
||||
for (const xref of xrefs) {
|
||||
LinkKnowl.initializeXrefKnowl(xref);
|
||||
}
|
||||
|
||||
const bornHiddens = target.querySelectorAll(".born-hidden-knowl");
|
||||
for (const bhk of bornHiddens) {
|
||||
const summary = bhk.querySelector(":scope > summary");
|
||||
const contents = bhk.querySelector(":scope > summary + *");
|
||||
new SlideRevealer(summary, contents, bhk);
|
||||
}
|
||||
}
|
||||
|
||||
// Used to animate both types of knowls
|
||||
class SlideRevealer {
|
||||
static STATE = Object.freeze({
|
||||
INACTIVE: 0,
|
||||
CLOSING: 1,
|
||||
EXPANDING: 2
|
||||
});
|
||||
|
||||
// triggerElement is the element clicked to open/close
|
||||
// contentElement is the element that will hide/reveal
|
||||
// animatedElement is the element that will grow/shrink as contentElement is modified
|
||||
// may be the same as contentElement or a parent of it
|
||||
constructor(triggerElement, contentElement, animatedElement) {
|
||||
this.triggerElement = triggerElement;
|
||||
this.contentElement = contentElement;
|
||||
this.animatedElement = animatedElement;
|
||||
|
||||
// mid animation state tracking
|
||||
this.animation = null;
|
||||
this.animationState = SlideRevealer.STATE.INACTIVE;
|
||||
|
||||
this.triggerElement.addEventListener('click', (e) => this.onClick(e));
|
||||
}
|
||||
|
||||
onClick(e) {
|
||||
// Stop default behavior from the browser
|
||||
if (e) e.preventDefault();
|
||||
|
||||
// Add an overflow on the <details> to avoid content overflowing
|
||||
this.animatedElement.style.overflow = 'hidden';
|
||||
|
||||
// Check if the element is being closed or is already closed
|
||||
if (this.animationState === SlideRevealer.STATE.CLOSING || !this.animatedElement.hasAttribute("open")) {
|
||||
// Force the [open] attributes - allow for similar targetting of xref and born-hidden knowls
|
||||
this.animatedElement.setAttribute("open","");
|
||||
this.triggerElement.setAttribute("open","");
|
||||
this.contentElement.style.display = '';
|
||||
// Trigger the animation to expand or collapse the knowl.
|
||||
// Delay the MathJax typesetting until the knowl is visible to ensure proper measurements
|
||||
// are taken, but before the unrolling begins. This helps avoid layout shifts and ensures
|
||||
// smooth animation with correctly sized content.
|
||||
MathJax.typesetPromise().then(() => window.requestAnimationFrame(() => this.toggle(true)));
|
||||
} else if (this.animationState === SlideRevealer.STATE.EXPANDING || this.animatedElement.hasAttribute("open")) {
|
||||
this.toggle(false);
|
||||
}
|
||||
}
|
||||
|
||||
toggle(expanding) {
|
||||
let closedHeight = 0;
|
||||
if (this.animatedElement.contains(this.triggerElement))
|
||||
closedHeight = this.triggerElement.offsetHeight;
|
||||
const fullHeight = closedHeight + this.contentElement.offsetHeight;
|
||||
|
||||
const startHeight = `${expanding ? closedHeight : fullHeight}px`;
|
||||
const endHeight = `${expanding ? fullHeight : closedHeight}px`;
|
||||
|
||||
// Need to animate padding to avoid extra height for xref knowls
|
||||
const padding = this.animatedElement.offsetHeight - this.animatedElement.clientHeight;
|
||||
const startPad = `${expanding ? 0 : padding}px`;
|
||||
const endPad = `${expanding ? padding : 0}px`;
|
||||
|
||||
// Cancel any existing animation
|
||||
if (this.animation) {
|
||||
this.animation.cancel();
|
||||
}
|
||||
|
||||
// Animate ~400 pixels per second with max of 0.75 second and min of 0.25
|
||||
const animDuration = Math.max( Math.min( (Math.abs(closedHeight - fullHeight) / 400 * 1000), 750), 250);
|
||||
|
||||
// Start animation
|
||||
this.animationState = expanding ? SlideRevealer.STATE.EXPANDING : SlideRevealer.STATE.CLOSING;
|
||||
this.animation = this.animatedElement.animate({
|
||||
height: [startHeight, endHeight],
|
||||
paddingTop: [startPad, endPad],
|
||||
paddingBottom: [startPad, endPad]
|
||||
}, {
|
||||
duration: animDuration,
|
||||
easing: 'ease'
|
||||
});
|
||||
|
||||
this.animation.onfinish = () => { this.onAnimationFinish(expanding); };
|
||||
this.animation.oncancel = () => { this.animationState = SlideRevealer.STATE.INACTIVE; };
|
||||
}
|
||||
|
||||
onAnimationFinish(isOpen) {
|
||||
// Clear animation state
|
||||
this.animation = null;
|
||||
this.animationState = SlideRevealer.STATE.INACTIVE;
|
||||
|
||||
// Make sure animated element has open (needed for details)
|
||||
if(!isOpen) {
|
||||
this.animatedElement.removeAttribute("open");
|
||||
this.triggerElement.removeAttribute("open");
|
||||
}
|
||||
|
||||
// Clear styles used in animation
|
||||
this.animatedElement.style.overflow = '';
|
||||
if (!isOpen)
|
||||
this.contentElement.style.display = 'none';
|
||||
|
||||
if (isOpen) {
|
||||
let hasCallback = this.contentElement.querySelectorAll("[data-knowl-callback]");
|
||||
hasCallback.forEach((el) => {
|
||||
window[el.getAttribute("data-knowl-callback")](el, open);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// A LinkKnowl manages a single link based knowl
|
||||
class LinkKnowl {
|
||||
// Used to uniquely identify XrefKnowls
|
||||
static xrefCount = 0;
|
||||
|
||||
// Factory to create an XrefKnowl from a knowl link
|
||||
// Will avoid duplicate initialization
|
||||
// This should be used by outside code to create XrefKnowls
|
||||
static initializeXrefKnowl(knowlLinkElement) {
|
||||
if (knowlLinkElement.getAttribute("data-knowl-uid") === null) {
|
||||
return new LinkKnowl(knowlLinkElement);
|
||||
}
|
||||
}
|
||||
|
||||
// "Private" constructor - should only be called by initializeXrefKnowl
|
||||
constructor(knowlLinkElement) {
|
||||
this.linkElement = knowlLinkElement;
|
||||
this.outputElement = null;
|
||||
this.uid = LinkKnowl.xrefCount++;
|
||||
knowlLinkElement.setAttribute("data-knowl-uid", this.uid);
|
||||
|
||||
// Xref's behavior is that of a button
|
||||
knowlLinkElement.setAttribute("role", "button");
|
||||
|
||||
// Stash a copy of the original title for use in aria-label
|
||||
// If no title, use textContent
|
||||
knowlLinkElement.setAttribute("data-base-title", knowlLinkElement.getAttribute("title") || this.linkElement.textContent);
|
||||
|
||||
knowlLinkElement.classList.add("knowl__link");
|
||||
|
||||
this.updateLabels(false);
|
||||
|
||||
// Bind required to force "this" of event handler to be this object
|
||||
knowlLinkElement.addEventListener("click", this.handleLinkClick.bind(this));
|
||||
}
|
||||
|
||||
// Set aria-label and title based on visibility of knowl
|
||||
updateLabels(isVisible) {
|
||||
const verb = isVisible
|
||||
? this.linkElement.getAttribute("data-close-label") || "Close"
|
||||
: this.linkElement.getAttribute("data-reveal-label") || "Reveal";
|
||||
const targetDescript = this.linkElement.getAttribute("data-base-title");
|
||||
const helpText = verb + " " + targetDescript;
|
||||
|
||||
this.linkElement.setAttribute("aria-label", helpText);
|
||||
this.linkElement.setAttribute("title", helpText);
|
||||
}
|
||||
|
||||
// Toggle the state of the knowl link and output elements
|
||||
// Assumes output is already created
|
||||
toggle() {
|
||||
this.linkElement.classList.toggle("active");
|
||||
const isActive = this.linkElement.classList.contains("active");
|
||||
this.updateLabels(isActive);
|
||||
|
||||
// Scroll to reveal if needed
|
||||
if (isActive) {
|
||||
const h = this.outputElement.getBoundingClientRect().height;
|
||||
if (h > window.innerHeight) {
|
||||
// knowl is taller than window, scroll to top of knowl
|
||||
this.outputElement.scrollIntoView(true);
|
||||
} else {
|
||||
// reveal full knowl
|
||||
if (this.outputElement.getBoundingClientRect().bottom > window.innerHeight)
|
||||
this.outputElement.scrollIntoView(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Returns element the knowl output should be inserted after
|
||||
findOutputLocation() {
|
||||
const invalidParents = "table, mjx-container, div.tabular-box, .runestone > .parsons";
|
||||
// Start with the link's parent, move up as long as there are invalid parents
|
||||
let el = this.linkElement.parentElement;
|
||||
let problemAncestor = el.closest(invalidParents);
|
||||
while (problemAncestor && problemAncestor !== el) {
|
||||
el = problemAncestor;
|
||||
problemAncestor = el.closest(invalidParents);
|
||||
}
|
||||
return el;
|
||||
}
|
||||
|
||||
// Create the knowl output element
|
||||
createOutputElement() {
|
||||
const outputId = "knowl-uid-" + this.uid;
|
||||
const outputContentsId = "knowl-output-" + this.uid;
|
||||
const linkTarget = this.linkElement.getAttribute("data-knowl");
|
||||
|
||||
const placeholderText = `<div class='knowl__content' style='display:none;' id='${outputId}' aria-live='polite' id='${outputContentsId}'>`
|
||||
+ `Loading '${linkTarget}'`
|
||||
+ `</div>`;
|
||||
|
||||
const temp = document.createElement("template");
|
||||
temp.innerHTML = placeholderText;
|
||||
this.outputElement = temp.content.children[0];
|
||||
|
||||
const insertLoc = this.findOutputLocation(this.linkElement);
|
||||
insertLoc.after(this.outputElement);
|
||||
}
|
||||
|
||||
// Get content for knowl as dom element. Returns promise that resolves to knowl content
|
||||
async getContent() {
|
||||
const contentURL = this.linkElement.getAttribute("data-knowl");
|
||||
const knowlContent = await fetch(contentURL)
|
||||
.then((response) => response.text())
|
||||
.then((data) => {
|
||||
// knowls are full HTML pages, need to just extract body
|
||||
let knowlDoc = (new DOMParser()).parseFromString(data, "text/html");
|
||||
let tempContainer = knowlDoc.body;
|
||||
// grab any scripts from head of knowl doc and add them to the output
|
||||
let scripts = knowlDoc.querySelectorAll("head script");
|
||||
tempContainer.append(...scripts);
|
||||
return tempContainer;
|
||||
})
|
||||
.catch((error) => {
|
||||
const destination = this.linkElement.getAttribute("href");
|
||||
const text = this.linkElement.textContent;
|
||||
const err_message = `<div class='knowl-output__error'>`
|
||||
+ `<div class='para'>Error fetching content. (<em>${error}</em>)</div>`
|
||||
+ `<div class='para'><a href='${destination}'>Navigate to ${text}</a> instead.</div>`
|
||||
+ `<div class='para'>If you are viewing this book from your local filesystem, this is expected behavior. To view the book with all features, you must serve the book from a web server. See the <a href="https://pretextbook.org/doc/guide/html/author-faq.html#how-do-i-view-my-book-locally">PreTeXt FAQ</a> for more information.</div>`
|
||||
+ `</div>`;
|
||||
return err_message;
|
||||
});
|
||||
return knowlContent;
|
||||
}
|
||||
|
||||
// Handle a click on the knowl link
|
||||
handleLinkClick(event) {
|
||||
// prevent navigation
|
||||
event.preventDefault();
|
||||
|
||||
if (this.outputElement !== null) {
|
||||
// output already created, toggle visibility
|
||||
this.toggle();
|
||||
} else {
|
||||
this.createOutputElement();
|
||||
|
||||
const slideHandler = new SlideRevealer(this.linkElement, this.outputElement, this.outputElement);
|
||||
//slideHandler is now responsible for handling clicks to this element
|
||||
this.linkElement.addEventListener('click', slideHandler);
|
||||
|
||||
// Wait up to a half second in hopes of avoiding double content change
|
||||
// then render to show loading message
|
||||
let loadingTimeout = setTimeout(() => {
|
||||
loadingTimeout = null;
|
||||
slideHandler.onClick(); //fake initial click
|
||||
this.toggle();
|
||||
}, 500);
|
||||
|
||||
const content = this.getContent();
|
||||
|
||||
// Content is a promise at this point, insert when resolved
|
||||
content
|
||||
.then((tempContainer) => {
|
||||
// if timeout still active, cancel it and render
|
||||
if (loadingTimeout !== null) {
|
||||
clearTimeout(loadingTimeout);
|
||||
}
|
||||
// Now give code that follows .1 seconds to render before making visible
|
||||
setTimeout(() => {
|
||||
slideHandler.onClick(); //fake initial click
|
||||
this.toggle();
|
||||
}, 100);
|
||||
|
||||
// check embedded runestone interactives by loading content into a temp container
|
||||
// we want to not render any that already are on page. Dupe IDs probably bad
|
||||
const runestoneElements = tempContainer.querySelectorAll(".ptx-runestone-container");
|
||||
[...runestoneElements].forEach((e) => {
|
||||
const rsId = e.querySelector("[data-component]")?.id;
|
||||
const onPage = document.getElementById(rsId);
|
||||
if (onPage) {
|
||||
e.innerHTML = `<div class="para">The interactive that belongs here is already on the page and cannot appear multiple times. <a href="#${rsId}">Scroll to interactive.</a>`;
|
||||
} else {
|
||||
// let runestone start rendering it
|
||||
window.runestoneComponents.renderOneComponent(e);
|
||||
}
|
||||
});
|
||||
|
||||
// now move all contents to the real output element
|
||||
const children = [...tempContainer.children];
|
||||
this.outputElement.innerHTML = "";
|
||||
this.outputElement.append(...children);
|
||||
|
||||
// render any knowls and mathjax in the knowl
|
||||
addKnowls(this.outputElement);
|
||||
|
||||
// try prism highlighting
|
||||
Prism.highlightAllUnder(this.outputElement);
|
||||
|
||||
// force any scripts (e.g. sagecell) to execute by evaling them
|
||||
[...this.outputElement.getElementsByTagName("script")].forEach((s) => {
|
||||
if (
|
||||
s.getAttribute("type") === null ||
|
||||
s.getAttribute("type") === "text/javascript"
|
||||
) {
|
||||
eval(s.innerHTML);
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch((data) => {
|
||||
console.log("Error fetching knowl content: " + data);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
+109
@@ -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"]
|
||||
}
|
||||
});
|
||||
Executable
+322
@@ -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;
|
||||
|
||||
|
||||
};
|
||||
+753
@@ -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;
|
||||
}
|
||||
+1058
File diff suppressed because it is too large
Load Diff
+1058
File diff suppressed because it is too large
Load Diff
+677
@@ -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);
|
||||
};
|
||||
+686
@@ -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);
|
||||
};
|
||||
+257
@@ -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;
|
||||
+1264
File diff suppressed because it is too large
Load Diff
+315
@@ -0,0 +1,315 @@
|
||||
|
||||
// next comment is out of date: there are more search options
|
||||
// from lunr-pretext-search-index.js we will have either
|
||||
// var ptx_lunr_search_style = "default";
|
||||
// or
|
||||
// var ptx_lunr_search_style = "reference";
|
||||
|
||||
// since there is only one search box now, this can be simplified
|
||||
function doSearch(searchlocation="A") {
|
||||
// Get the search terms from the input text box
|
||||
var terms;
|
||||
if(searchlocation == "A") {
|
||||
terms = document.getElementById("ptxsearch").value;
|
||||
} else {
|
||||
terms = document.getElementById("ptxsearchB").value;
|
||||
}
|
||||
|
||||
localStorage.setItem('last-search-terms', JSON.stringify({terms: terms, time: Date.now()}));
|
||||
|
||||
// Where do we want to put the results?
|
||||
let resultArea = document.getElementById("searchresults")
|
||||
resultArea.innerHTML = ""; // clear out any previous results
|
||||
// assume AND for multiple words
|
||||
var searchterms = terms;
|
||||
if(searchlocation == "B") {
|
||||
document.getElementById("ptxsearch").value = searchterms
|
||||
console.log("ptxsearch value", document.getElementById("ptxsearch").value);
|
||||
} else {
|
||||
searchterms = terms;
|
||||
}
|
||||
|
||||
searchterms = searchterms.toLowerCase().trim();
|
||||
let pageResult = [];
|
||||
if(searchterms != "") {
|
||||
pageResult = ptx_lunr_idx.query((q) => {
|
||||
for(let term of searchterms.split(' ')) {
|
||||
q.term(term, { fields: ["title"], boost: 20 }); //exact title match with 20x weight
|
||||
q.term(term, { wildcard: lunr.Query.wildcard.TRAILING, fields: ["title"], boost: 10 }); //inexact title 10x weight
|
||||
q.term(term, { fields: ["body"], boost: 5 }); //exact body 5x weight
|
||||
q.term(term, { wildcard: lunr.Query.wildcard.TRAILING, fields: ["body"] }); //inexact body
|
||||
}
|
||||
});
|
||||
}
|
||||
// Number the documents from first to last so we can order the results by their
|
||||
// position in the book.
|
||||
snum = 0;
|
||||
for (let doc of ptx_lunr_docs) {
|
||||
doc.snum = snum;
|
||||
snum += 1;
|
||||
}
|
||||
|
||||
//Limit to a sane number of results - otherwise search like 'e' matches every page
|
||||
const MAX_RESULTS = 100;
|
||||
let numUnshown = (pageResult.length > MAX_RESULTS) ? pageResult.length - MAX_RESULTS : 0;
|
||||
pageResult.slice(0, MAX_RESULTS);
|
||||
|
||||
// Transfer meta data from the document to the results to make it easy to add
|
||||
// our lists later.
|
||||
augmentResults(pageResult, ptx_lunr_docs);
|
||||
pageResult.sort(comparePosition);
|
||||
addResultToPage(terms, pageResult, ptx_lunr_docs, numUnshown, resultArea);
|
||||
MathJax.typeset();
|
||||
}
|
||||
|
||||
// Find the entry for a search result in the original document index
|
||||
function findEntry(resultId, db) {
|
||||
for (const page of db) {
|
||||
if (page.id === resultId) {
|
||||
return page;
|
||||
}
|
||||
}
|
||||
return resultId;
|
||||
}
|
||||
|
||||
function augmentResults(result, docs) {
|
||||
for (let res of result) {
|
||||
let info = findEntry(res.ref, docs);
|
||||
res.number = info.number;
|
||||
res.type = info.type;
|
||||
res.title = info.title;
|
||||
res.url = info.url;
|
||||
res.level = info.level;
|
||||
res.snum = info.snum;
|
||||
res.score = parseFloat(res.score);
|
||||
|
||||
//extra score multiplier based on level - prioritize sections over subsections/exercises/etc...
|
||||
const LEVEL_WEIGHTS = [3, 2, 1.5]
|
||||
if( res.level < 2 )
|
||||
res.score *= LEVEL_WEIGHTS[res.level];
|
||||
|
||||
res.body = '';
|
||||
//Add body snippets and highlights
|
||||
const REVEAL_WINDOW = 30;
|
||||
let titleMarked = false;
|
||||
for (const hit in res.matchData.metadata) {
|
||||
if(res.matchData.metadata[hit].title) {
|
||||
//only show one match in title as locations change after first markup
|
||||
if(!titleMarked) {
|
||||
if(!res.matchData.metadata[hit].title.position)
|
||||
continue;
|
||||
let positionData = res.matchData.metadata[hit].title.position[0];
|
||||
const startClipInd = positionData[0];
|
||||
const endClipInd = positionData[0] + positionData[1];
|
||||
res.title = res.title.substring(0, endClipInd) + '</span>' + res.title.substring(endClipInd);
|
||||
res.title = res.title.substring(0, startClipInd) + '<span class="search-result-clip-highlight">' + res.title.substring(startClipInd);
|
||||
titleMarked = true;
|
||||
}
|
||||
} else if (res.matchData.metadata[hit].body) {
|
||||
if(!res.matchData.metadata[hit].body.position)
|
||||
continue;
|
||||
const bodyContent = info.body;
|
||||
let positionData = res.matchData.metadata[hit].body.position[0];
|
||||
const startInd = positionData[0] - REVEAL_WINDOW;
|
||||
const endInd = positionData[0] + positionData[1] + REVEAL_WINDOW;
|
||||
const startClipInd = positionData[0];
|
||||
const endClipInd = positionData[0] + positionData[1];
|
||||
let resultSnippet = (startInd > 0 ? '...' : '' ) + bodyContent.substring(startInd, startClipInd);
|
||||
resultSnippet += '<span class="search-result-clip-highlight">' + bodyContent.substring(startClipInd, endClipInd) + '</span>';
|
||||
resultSnippet += bodyContent.substring(endClipInd, endInd) + (endInd < bodyContent.length ? '...' : '' ) + '<br/>';
|
||||
res.body += resultSnippet;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function rearrangedArray(arry) {
|
||||
// return a new array which is arry (with depth) sorted according to meas,
|
||||
// again as an array with depth.
|
||||
// "with depth'' means that large children drag along their parents.
|
||||
let newarry = [];
|
||||
let startind = 0;
|
||||
let numtograb = 0;
|
||||
let ct = 1;
|
||||
while (arry.length > 0 && ct < 500) {
|
||||
++ct; // just in case something goes wrong
|
||||
const locofmax = maxLocation(arry)
|
||||
let segmentstart = locofmax;
|
||||
let segmentlength = 1;
|
||||
while (arry[segmentstart].level == "2") {
|
||||
--segmentstart
|
||||
}
|
||||
while (segmentstart + segmentlength < arry.length && arry[segmentstart + segmentlength].level == "2") {
|
||||
++segmentlength
|
||||
}
|
||||
// console.log("locofmax", locofmax, "starting", segmentstart, "going", segmentlength, "from", arry.length);
|
||||
newarry.push(...arry.splice(segmentstart,segmentlength));
|
||||
}
|
||||
// console.log("newarry", newarry);
|
||||
return newarry
|
||||
}
|
||||
function maxLocation(arry) {
|
||||
let maxloc = 0;
|
||||
let maxvalsofar = -1;
|
||||
for (let index = 0; index < arry.length; ++index) {
|
||||
if (arry[index].score > maxvalsofar) {
|
||||
maxloc = index;
|
||||
maxvalsofar = arry[index].score
|
||||
}
|
||||
}
|
||||
return maxloc
|
||||
}
|
||||
|
||||
function comparePosition(a, b) {
|
||||
if (a.snum < b.snum) {
|
||||
return -1;
|
||||
}
|
||||
if (a.snum > b.snum) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
function compareScoreDesc(a, b) {
|
||||
if (a.score < b.score) {
|
||||
return 1;
|
||||
}
|
||||
if (a.score > b.score) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function addResultToPage(searchterms, result, docs, numUnshown, resultArea) {
|
||||
/* backward compatibility for old html */
|
||||
if (document.getElementById("searchempty")) {
|
||||
document.getElementById("searchempty").style.display = "none";
|
||||
}
|
||||
let len = result.length;
|
||||
console.log("first result", result[0]);
|
||||
if (len == 0) {
|
||||
if (document.getElementById("searchempty")) {
|
||||
document.getElementById("searchempty").style.display = "block";
|
||||
} else {
|
||||
let noresults = document.createElement("div");
|
||||
noresults.classList.add("noresults");
|
||||
search_no_results_string = "No results were found"
|
||||
noresults.innerHTML = search_no_results_string + ".";
|
||||
// console.log("the new variable", search_results_heading_string);
|
||||
resultArea.appendChild(noresults);
|
||||
}
|
||||
document.getElementById("searchresultsplaceholder").style.display = null;
|
||||
return
|
||||
}
|
||||
// console.log("result",result);
|
||||
let allScores = result.map(function (r) { return r.score });
|
||||
// console.log(typeof allScores[0], "allScores",allScores);
|
||||
allScores.sort((a,b) => (a - b));
|
||||
allScores.reverse();
|
||||
// console.log("allScores, sorted",allScores);
|
||||
|
||||
// let high = result[Math.floor(len*0.25)].score;
|
||||
// let med = result[Math.floor(len*0.5)].score;
|
||||
// let low = result[Math.floor(len*0.75)].score;
|
||||
// sort the results by their position in the book, not their score
|
||||
let high = allScores[Math.floor(len*0.20)];
|
||||
let med = allScores[Math.floor(len*0.40)];
|
||||
let low = allScores[Math.floor(len*0.75)];
|
||||
if (ptx_lunr_search_style == "reference") {
|
||||
result = rearrangedArray(result);
|
||||
}
|
||||
let indent = "1";
|
||||
let currIndent = indent;
|
||||
let origResult = resultArea;
|
||||
// Create list entries indenting as needed.
|
||||
for (const res of result) {
|
||||
let link = document.createElement("a")
|
||||
// add a class so we can colorize the results based on their rank in terms
|
||||
// of search score.
|
||||
if (res.score >= high) {
|
||||
link.classList.add("high_result")
|
||||
} else if (res.score >= med) {
|
||||
link.classList.add("medium_result")
|
||||
} else if (res.score >= low) {
|
||||
link.classList.add("low_result")
|
||||
} else {
|
||||
link.classList.add("no_result")
|
||||
}
|
||||
currIndent = res.level;
|
||||
if (currIndent > indent) {
|
||||
indent = currIndent;
|
||||
let ilist = document.createElement("ul")
|
||||
ilist.classList.add("detailed_result");
|
||||
resultArea.appendChild(ilist);
|
||||
resultArea = ilist;
|
||||
} else if (currIndent < indent) {
|
||||
resultArea = origResult;
|
||||
indent = currIndent;
|
||||
}
|
||||
link.href = `${res.url}`;
|
||||
link.innerHTML = `${res.type} ${res.number} ${res.title}`;
|
||||
let clip = document.createElement("div");
|
||||
clip.classList.add("search-result-clip");
|
||||
clip.innerHTML = `${res.body}`;
|
||||
let bullet = document.createElement("li");
|
||||
bullet.classList.add('search-result-bullet');
|
||||
bullet.appendChild(link);
|
||||
bullet.appendChild(clip);
|
||||
let p = document.createElement("text");
|
||||
p.classList.add('search-result-score');
|
||||
p.innerHTML = ` (${res.score.toFixed(2)})`;
|
||||
bullet.appendChild(p);
|
||||
resultArea.appendChild(bullet);
|
||||
}
|
||||
|
||||
// Auto-close search results when a result is clicked in case result is on
|
||||
// the same page search started from
|
||||
const resultsDiv = document.getElementById('searchresultsplaceholder');
|
||||
const backDiv = document.querySelector('.searchresultsbackground');
|
||||
resultArea.querySelectorAll("a").forEach((link) => {
|
||||
link.addEventListener('click', (e) => {
|
||||
backDiv.style.display = 'none';
|
||||
resultsDiv.style.display = 'none';
|
||||
});
|
||||
});
|
||||
//Could print message about how many results are not shown. No way to localize it though...
|
||||
// if(numUnshown > 0) {
|
||||
// let bullet = document.createElement("li");
|
||||
// bullet.classList.add('search-results-bullet');
|
||||
// bullet.classList.add('search-results-unshown-count');
|
||||
// let p = document.createElement("text");
|
||||
// p.innerHTML = `${parseInt(numUnshown)} unshown results...`;
|
||||
// bullet.appendChild(p);
|
||||
// resultArea.appendChild(bullet);
|
||||
// }
|
||||
document.getElementById("searchresultsplaceholder").style.display = null;
|
||||
MathJax.typesetPromise();
|
||||
}
|
||||
|
||||
window.addEventListener("load", function (event) {
|
||||
const resultsDiv = document.getElementById('searchresultsplaceholder');
|
||||
|
||||
//insert a div to be backgroud behind searchresultsplaceholder
|
||||
const backDiv = document.createElement("div");
|
||||
backDiv.classList.add("searchresultsbackground");
|
||||
backDiv.style.display = 'none';
|
||||
resultsDiv.parentNode.appendChild(backDiv);
|
||||
|
||||
document.getElementById("searchbutton").addEventListener('click', (e) => {
|
||||
resultsDiv.style.display = null;
|
||||
backDiv.style.display = null;
|
||||
let searchInput = document.getElementById("ptxsearch");
|
||||
searchInput.value = JSON.parse(localStorage.getItem("last-search-terms")).terms;
|
||||
searchInput.select();
|
||||
doSearch();
|
||||
});
|
||||
|
||||
document.getElementById("ptxsearch").addEventListener('input', (e) => {
|
||||
doSearch();
|
||||
});
|
||||
|
||||
document.getElementById("closesearchresults").addEventListener('click', (e) => {
|
||||
resultsDiv.style.display = 'none';
|
||||
backDiv.style.display = 'none';
|
||||
document.getElementById('searchbutton').focus();
|
||||
});
|
||||
});
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
function doSearch() {
|
||||
// Get the search terms from the input text box
|
||||
let terms = document.getElementById("ptxsearch").value;
|
||||
// Where do we want to put the results?
|
||||
let resultArea = document.getElementById("searchresults")
|
||||
resultArea.innerHTML = ""; // clear out any previous results
|
||||
// do the search using the provided index
|
||||
let pageResult = ptx_lunr_idx.search(terms);
|
||||
// Number the documents from first to last so we can order the results by their
|
||||
// position in the book.
|
||||
snum = 0;
|
||||
for (let doc of ptx_lunr_docs) {
|
||||
doc.snum = snum;
|
||||
snum += 1;
|
||||
}
|
||||
// Transfer meta data from the document to the results to make it easy to add
|
||||
// our lists later.
|
||||
augmentResults(pageResult, ptx_lunr_docs);
|
||||
addResultToPage(pageResult, ptx_lunr_docs, resultArea);
|
||||
MathJax.typeset();
|
||||
}
|
||||
|
||||
// Find the entry for a search result in the original document index
|
||||
function findEntry(resultId, db) {
|
||||
for (const page of db) {
|
||||
if (page.id === resultId) {
|
||||
return page;
|
||||
}
|
||||
}
|
||||
return resultId;
|
||||
}
|
||||
|
||||
function augmentResults(result, docs) {
|
||||
for (let res of result) {
|
||||
let info = findEntry(res.ref, docs);
|
||||
res.number = info.number;
|
||||
res.type = info.type;
|
||||
res.title = info.title;
|
||||
res.url = info.url;
|
||||
res.level = info.level;
|
||||
res.snum = info.snum;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function comparePosition(a, b) {
|
||||
if (a.snum < b.snum) {
|
||||
return -1;
|
||||
}
|
||||
if (a.snum > b.snum) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function addResultToPage(result, docs, resultArea) {
|
||||
let len = result.length
|
||||
let high = result[Math.floor(len*0.25)].score;
|
||||
let med = result[Math.floor(len*0.5)].score;
|
||||
let low = result[Math.floor(len*0.75)].score;
|
||||
// sort the results by their position in the book, not their score
|
||||
result = result.sort(comparePosition)
|
||||
let indent = "1";
|
||||
let currIndent = indent;
|
||||
let origResult = resultArea;
|
||||
// Create list entries indenting as needed.
|
||||
for (const res of result) {
|
||||
let link = document.createElement("a")
|
||||
// add a class so we can colorize the results based on their rank in terms
|
||||
// of search score.
|
||||
if (res.score >= high) {
|
||||
link.classList.add("high_result")
|
||||
} else if (res.score >= med) {
|
||||
link.classList.add("medium_result")
|
||||
} else if (res.score >= low) {
|
||||
link.classList.add("low_result")
|
||||
}
|
||||
currIndent = res.level;
|
||||
if (currIndent > indent) {
|
||||
indent = currIndent;
|
||||
let ilist = document.createElement("ul")
|
||||
ilist.classList.add("detailed_result");
|
||||
resultArea.appendChild(ilist);
|
||||
resultArea = ilist;
|
||||
} else if (currIndent < indent) {
|
||||
resultArea = origResult;
|
||||
indent = currIndent;
|
||||
}
|
||||
let bullet = document.createElement("li")
|
||||
bullet.style.marginTop = "5px";
|
||||
link.href = `${res.url}`;
|
||||
link.innerHTML = `${res.type} ${res.number} ${res.title}`;
|
||||
bullet.appendChild(link)
|
||||
let p = document.createElement("text");
|
||||
p.innerHTML = ` (${res.score.toFixed(2)})`;
|
||||
bullet.appendChild(p);
|
||||
resultArea.appendChild(bullet);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
function showHelp() {
|
||||
let state = document.getElementById("helpme").style.display;
|
||||
if (state == "none") {
|
||||
document.getElementById("helpme").style.display = "block";
|
||||
document.getElementById("helpbutt").innerHTML = "Hide Help"
|
||||
} else {
|
||||
document.getElementById("helpme").style.display = "none";
|
||||
document.getElementById("helpbutt").innerHTML = "Show Help"
|
||||
}
|
||||
}
|
||||
+510
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Executable
+206
@@ -0,0 +1,206 @@
|
||||
<!DOCTYPE html>
|
||||
<!--********************************************-->
|
||||
<!--* Generated from PreTeXt source *-->
|
||||
<!--* on 2026-01-12T10:51:53-05:00 *-->
|
||||
<!--* A recent stable commit (2022-07-01): *-->
|
||||
<!--* 6c761d3dba23af92cba35001c852aac04ae99a5f *-->
|
||||
<!--* *-->
|
||||
<!--* https://pretextbook.org *-->
|
||||
<!--* *-->
|
||||
<!--********************************************-->
|
||||
<html xmlns:pi="http://pretextbook.org/2020/pretext/internal">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<title>Slides for MATH xxx</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=yes">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/4.1.2/reset.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/4.1.2/reveal.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/4.1.2/theme/white.css" rel="stylesheet">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/4.1.2/reveal.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/4.1.2/plugin/math/math.js"></script><style>
|
||||
ul {
|
||||
display: block !important;
|
||||
}
|
||||
.reveal img {
|
||||
border: 0.5px !important;
|
||||
border-radius: 2px 10px 2px;
|
||||
padding: 4px;
|
||||
}
|
||||
.reveal pre {
|
||||
box-shadow: none;
|
||||
line-height: 1;
|
||||
font-size: inherit;
|
||||
width: auto;
|
||||
margin: inherit;
|
||||
}
|
||||
.reveal pre code {
|
||||
display: block;
|
||||
padding: 0;
|
||||
overflow: unset;
|
||||
max-height: unset;
|
||||
word-wrap: normal;
|
||||
}
|
||||
.definition-like,.theorem-like,.project-like {
|
||||
border-width: 0.5px;
|
||||
border-style: solid;
|
||||
border-radius: 2px 10px 2px;
|
||||
padding: 1%;
|
||||
margin-bottom: var(--r-block-margin);
|
||||
}
|
||||
.definition-like {
|
||||
background: #006080;
|
||||
background: color-mix(in srgb, var(--r-background-color) 75%, #006080);
|
||||
}
|
||||
.theorem-like {
|
||||
background: #ff000010;
|
||||
background: color-mix(in srgb, var(--r-background-color) 75%, #aa0000);
|
||||
}
|
||||
.proof-like {
|
||||
background: #ffffff;
|
||||
background: color-mix(in srgb, var(--r-background-color) 75%, #aaaaaa);
|
||||
}
|
||||
.project-like {
|
||||
background: #60800010;
|
||||
background: color-mix(in srgb, var(--r-background-color) 75%, #608000);
|
||||
}
|
||||
.code-inline {
|
||||
background: #60800010;
|
||||
background: color-mix(in srgb, var(--r-background-color) 75%, var(--r-link-color));
|
||||
padding: 0 3px;
|
||||
border: 1px solid;
|
||||
margin: 3px;
|
||||
display: inline-block;
|
||||
}
|
||||
.sagecell_sessionOutput {
|
||||
background: white;
|
||||
color: black;
|
||||
border: 0.5px solid var(--r-main-color);
|
||||
}
|
||||
|
||||
.program {
|
||||
background: #60800010;
|
||||
background: color-mix(in srgb, var(--r-background-color) 75%, var(--r-link-color));
|
||||
max-height: 450px;
|
||||
overflow: auto;
|
||||
border: 0.5px solid var(--r-main-color);
|
||||
}
|
||||
.ptx-sagecell, .reveal .program {
|
||||
font-size: calc(var(--r-main-font-size) * 0.6);
|
||||
}
|
||||
|
||||
|
||||
.ptx-content :is(.image-box, .audio-box, .video-box, .asymptote-box) {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ptx-content iframe.asymptote, .ptx-content iframe.asymptote, .ptx-content .video-box .video, .ptx-content .video-box .video-poster {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
|
||||
code[class*="language-"], pre[class*="language-"] {
|
||||
padding: 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
dfn {
|
||||
font-weight: bold;
|
||||
}
|
||||
.ptx-content ol.no-marker,
|
||||
.ptx-content ul.no-marker,
|
||||
.ptx-content li.no-marker {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.ptx-content ol.decimal {
|
||||
list-style-type: decimal;
|
||||
}
|
||||
.ptx-content ol.lower-alpha {
|
||||
list-style-type: lower-alpha;
|
||||
}
|
||||
.ptx-content ol.upper-alpha {
|
||||
list-style-type: upper-alpha;
|
||||
}
|
||||
.ptx-content ol.lower-roman {
|
||||
list-style-type: lower-roman;
|
||||
}
|
||||
.ptx-content ol.upper-roman {
|
||||
list-style-type: upper-roman;
|
||||
}
|
||||
.ptx-content ul.disc {
|
||||
list-style-type: disc;
|
||||
}
|
||||
.ptx-content ul.square {
|
||||
list-style-type: square;
|
||||
}
|
||||
.ptx-content ul.circle {
|
||||
list-style-type: circle;
|
||||
}
|
||||
.ptx-content ol.no-marker,
|
||||
.ptx-content ul.no-marker {
|
||||
list-style-type: none;
|
||||
}
|
||||
.ptx-content .cols1 li,
|
||||
.ptx-content .cols2 li,
|
||||
.ptx-content .cols3 li,
|
||||
.ptx-content .cols4 li,
|
||||
.ptx-content .cols5 li,
|
||||
.ptx-content .cols6 li {
|
||||
float: left;
|
||||
padding-right:2em;
|
||||
}
|
||||
|
||||
/* make small images full-width in #sidebyside */
|
||||
/* could improve with a .sidebyside class */
|
||||
div[style*="display:table-cell"] img {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body><div class="reveal ptx-content">
|
||||
<div id="latex-macros" class="hidden-content process-math" style="display:none"><span class="process-math">\(\newcommand{\N}{\mathbb N}
|
||||
\newcommand{\Z}{\mathbb Z}
|
||||
\newcommand{\Q}{\mathbb Q}
|
||||
\newcommand{\R}{\mathbb R}
|
||||
\newcommand{\lt}{<}
|
||||
\newcommand{\gt}{>}
|
||||
\newcommand{\amp}{&}
|
||||
\definecolor{fillinmathshade}{gray}{0.9}
|
||||
\newcommand{\fillinmath}[1]{\mathchoice{\colorbox{fillinmathshade}{$\displaystyle \phantom{\,#1\,}$}}{\colorbox{fillinmathshade}{$\textstyle \phantom{\,#1\,}$}}{\colorbox{fillinmathshade}{$\scriptstyle \phantom{\,#1\,}$}}{\colorbox{fillinmathshade}{$\scriptscriptstyle\phantom{\,#1\,}$}}}
|
||||
\)</span></div>
|
||||
<div class="slides">
|
||||
<section><section><h1>Slides for MATH xxx</h1></section></section><section><section><h1>Section 1.1</h1></section><section><h3>Title</h3>
|
||||
<div align="left"><p>Here is some text.</p></div></section></section><section><section><h1>Section 1.2</h1></section><section><h3>Title 2</h3>
|
||||
<div align="left"><p>Some more text.</p></div></section></section>
|
||||
</div>
|
||||
</div></body>
|
||||
<script>
|
||||
Reveal.initialize({
|
||||
controls: true,
|
||||
controlsTutorial: true,
|
||||
controlsLayout: 'bottom-right',
|
||||
controlsBackArrows: 'faded',
|
||||
navigationMode: 'default',
|
||||
progress: false,
|
||||
center: false,
|
||||
hash: true,
|
||||
transition: 'fade',
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
margin: "0.025",
|
||||
math: {
|
||||
mathjax: 'https://cdn.jsdelivr.net/gh/mathjax/mathjax@2.7.8/MathJax.js',
|
||||
config: 'TeX-AMS_HTML-full',
|
||||
// other options are passed into MathJax.Hub.Config()
|
||||
tex2jax: {
|
||||
inlineMath: [['\\(','\\)']],
|
||||
displayMath: [],
|
||||
}
|
||||
},
|
||||
plugins: [ RevealMath ]
|
||||
});
|
||||
</script>
|
||||
</html>
|
||||
Executable
+552
@@ -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();
|
||||
}
|
||||
}
|
||||
+1
File diff suppressed because one or more lines are too long
Executable
+4988
File diff suppressed because it is too large
Load Diff
Executable
+480
@@ -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';
|
||||
});
|
||||
Executable
+131
@@ -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);
|
||||
|
||||
|
||||
+2
File diff suppressed because one or more lines are too long
+161
@@ -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);
|
||||
Executable
+338
@@ -0,0 +1,338 @@
|
||||
// Code controlling behavior of xref knowls and born hidden knowls
|
||||
|
||||
// Assumes this file is loaded as part of initial page
|
||||
window.addEventListener("load", (event) => {
|
||||
addKnowls(document);
|
||||
});
|
||||
|
||||
function addKnowls(target) {
|
||||
const xrefs = target.querySelectorAll("[data-knowl]");
|
||||
for (const xref of xrefs) {
|
||||
LinkKnowl.initializeXrefKnowl(xref);
|
||||
}
|
||||
|
||||
const bornHiddens = target.querySelectorAll(".born-hidden-knowl");
|
||||
for (const bhk of bornHiddens) {
|
||||
const summary = bhk.querySelector(":scope > summary");
|
||||
const contents = bhk.querySelector(":scope > summary + *");
|
||||
new SlideRevealer(summary, contents, bhk);
|
||||
}
|
||||
}
|
||||
|
||||
// Used to animate both types of knowls
|
||||
class SlideRevealer {
|
||||
static STATE = Object.freeze({
|
||||
INACTIVE: 0,
|
||||
CLOSING: 1,
|
||||
EXPANDING: 2
|
||||
});
|
||||
|
||||
// triggerElement is the element clicked to open/close
|
||||
// contentElement is the element that will hide/reveal
|
||||
// animatedElement is the element that will grow/shrink as contentElement is modified
|
||||
// may be the same as contentElement or a parent of it
|
||||
constructor(triggerElement, contentElement, animatedElement) {
|
||||
this.triggerElement = triggerElement;
|
||||
this.contentElement = contentElement;
|
||||
this.animatedElement = animatedElement;
|
||||
|
||||
// mid animation state tracking
|
||||
this.animation = null;
|
||||
this.animationState = SlideRevealer.STATE.INACTIVE;
|
||||
|
||||
this.triggerElement.addEventListener('click', (e) => this.onClick(e));
|
||||
}
|
||||
|
||||
onClick(e) {
|
||||
// Stop default behavior from the browser
|
||||
if (e) e.preventDefault();
|
||||
|
||||
// Add an overflow on the <details> to avoid content overflowing
|
||||
this.animatedElement.style.overflow = 'hidden';
|
||||
|
||||
// Check if the element is being closed or is already closed
|
||||
if (this.animationState === SlideRevealer.STATE.CLOSING || !this.animatedElement.hasAttribute("open")) {
|
||||
// Force the [open] attributes - allow for similar targetting of xref and born-hidden knowls
|
||||
this.animatedElement.setAttribute("open","");
|
||||
this.triggerElement.setAttribute("open","");
|
||||
this.contentElement.style.display = '';
|
||||
// Trigger the animation to expand or collapse the knowl.
|
||||
// Delay the MathJax typesetting until the knowl is visible to ensure proper measurements
|
||||
// are taken, but before the unrolling begins. This helps avoid layout shifts and ensures
|
||||
// smooth animation with correctly sized content.
|
||||
MathJax.typesetPromise().then(() => window.requestAnimationFrame(() => this.toggle(true)));
|
||||
} else if (this.animationState === SlideRevealer.STATE.EXPANDING || this.animatedElement.hasAttribute("open")) {
|
||||
this.toggle(false);
|
||||
}
|
||||
}
|
||||
|
||||
toggle(expanding) {
|
||||
let closedHeight = 0;
|
||||
if (this.animatedElement.contains(this.triggerElement))
|
||||
closedHeight = this.triggerElement.offsetHeight;
|
||||
const fullHeight = closedHeight + this.contentElement.offsetHeight;
|
||||
|
||||
const startHeight = `${expanding ? closedHeight : fullHeight}px`;
|
||||
const endHeight = `${expanding ? fullHeight : closedHeight}px`;
|
||||
|
||||
// Need to animate padding to avoid extra height for xref knowls
|
||||
const padding = this.animatedElement.offsetHeight - this.animatedElement.clientHeight;
|
||||
const startPad = `${expanding ? 0 : padding}px`;
|
||||
const endPad = `${expanding ? padding : 0}px`;
|
||||
|
||||
// Cancel any existing animation
|
||||
if (this.animation) {
|
||||
this.animation.cancel();
|
||||
}
|
||||
|
||||
// Animate ~400 pixels per second with max of 0.75 second and min of 0.25
|
||||
const animDuration = Math.max( Math.min( (Math.abs(closedHeight - fullHeight) / 400 * 1000), 750), 250);
|
||||
|
||||
// Start animation
|
||||
this.animationState = expanding ? SlideRevealer.STATE.EXPANDING : SlideRevealer.STATE.CLOSING;
|
||||
this.animation = this.animatedElement.animate({
|
||||
height: [startHeight, endHeight],
|
||||
paddingTop: [startPad, endPad],
|
||||
paddingBottom: [startPad, endPad]
|
||||
}, {
|
||||
duration: animDuration,
|
||||
easing: 'ease'
|
||||
});
|
||||
|
||||
this.animation.onfinish = () => { this.onAnimationFinish(expanding); };
|
||||
this.animation.oncancel = () => { this.animationState = SlideRevealer.STATE.INACTIVE; };
|
||||
}
|
||||
|
||||
onAnimationFinish(isOpen) {
|
||||
// Clear animation state
|
||||
this.animation = null;
|
||||
this.animationState = SlideRevealer.STATE.INACTIVE;
|
||||
|
||||
// Make sure animated element has open (needed for details)
|
||||
if(!isOpen) {
|
||||
this.animatedElement.removeAttribute("open");
|
||||
this.triggerElement.removeAttribute("open");
|
||||
}
|
||||
|
||||
// Clear styles used in animation
|
||||
this.animatedElement.style.overflow = '';
|
||||
if (!isOpen)
|
||||
this.contentElement.style.display = 'none';
|
||||
|
||||
if (isOpen) {
|
||||
let hasCallback = this.contentElement.querySelectorAll("[data-knowl-callback]");
|
||||
hasCallback.forEach((el) => {
|
||||
window[el.getAttribute("data-knowl-callback")](el, open);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// A LinkKnowl manages a single link based knowl
|
||||
class LinkKnowl {
|
||||
// Used to uniquely identify XrefKnowls
|
||||
static xrefCount = 0;
|
||||
|
||||
// Factory to create an XrefKnowl from a knowl link
|
||||
// Will avoid duplicate initialization
|
||||
// This should be used by outside code to create XrefKnowls
|
||||
static initializeXrefKnowl(knowlLinkElement) {
|
||||
if (knowlLinkElement.getAttribute("data-knowl-uid") === null) {
|
||||
return new LinkKnowl(knowlLinkElement);
|
||||
}
|
||||
}
|
||||
|
||||
// "Private" constructor - should only be called by initializeXrefKnowl
|
||||
constructor(knowlLinkElement) {
|
||||
this.linkElement = knowlLinkElement;
|
||||
this.outputElement = null;
|
||||
this.uid = LinkKnowl.xrefCount++;
|
||||
knowlLinkElement.setAttribute("data-knowl-uid", this.uid);
|
||||
|
||||
// Xref's behavior is that of a button
|
||||
knowlLinkElement.setAttribute("role", "button");
|
||||
|
||||
// Stash a copy of the original title for use in aria-label
|
||||
// If no title, use textContent
|
||||
knowlLinkElement.setAttribute("data-base-title", knowlLinkElement.getAttribute("title") || this.linkElement.textContent);
|
||||
|
||||
knowlLinkElement.classList.add("knowl__link");
|
||||
|
||||
this.updateLabels(false);
|
||||
|
||||
// Bind required to force "this" of event handler to be this object
|
||||
knowlLinkElement.addEventListener("click", this.handleLinkClick.bind(this));
|
||||
}
|
||||
|
||||
// Set aria-label and title based on visibility of knowl
|
||||
updateLabels(isVisible) {
|
||||
const verb = isVisible
|
||||
? this.linkElement.getAttribute("data-close-label") || "Close"
|
||||
: this.linkElement.getAttribute("data-reveal-label") || "Reveal";
|
||||
const targetDescript = this.linkElement.getAttribute("data-base-title");
|
||||
const helpText = verb + " " + targetDescript;
|
||||
|
||||
this.linkElement.setAttribute("aria-label", helpText);
|
||||
this.linkElement.setAttribute("title", helpText);
|
||||
}
|
||||
|
||||
// Toggle the state of the knowl link and output elements
|
||||
// Assumes output is already created
|
||||
toggle() {
|
||||
this.linkElement.classList.toggle("active");
|
||||
const isActive = this.linkElement.classList.contains("active");
|
||||
this.updateLabels(isActive);
|
||||
|
||||
// Scroll to reveal if needed
|
||||
if (isActive) {
|
||||
const h = this.outputElement.getBoundingClientRect().height;
|
||||
if (h > window.innerHeight) {
|
||||
// knowl is taller than window, scroll to top of knowl
|
||||
this.outputElement.scrollIntoView(true);
|
||||
} else {
|
||||
// reveal full knowl
|
||||
if (this.outputElement.getBoundingClientRect().bottom > window.innerHeight)
|
||||
this.outputElement.scrollIntoView(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Returns element the knowl output should be inserted after
|
||||
findOutputLocation() {
|
||||
const invalidParents = "table, mjx-container, div.tabular-box, .runestone > .parsons";
|
||||
// Start with the link's parent, move up as long as there are invalid parents
|
||||
let el = this.linkElement.parentElement;
|
||||
let problemAncestor = el.closest(invalidParents);
|
||||
while (problemAncestor && problemAncestor !== el) {
|
||||
el = problemAncestor;
|
||||
problemAncestor = el.closest(invalidParents);
|
||||
}
|
||||
return el;
|
||||
}
|
||||
|
||||
// Create the knowl output element
|
||||
createOutputElement() {
|
||||
const outputId = "knowl-uid-" + this.uid;
|
||||
const outputContentsId = "knowl-output-" + this.uid;
|
||||
const linkTarget = this.linkElement.getAttribute("data-knowl");
|
||||
|
||||
const placeholderText = `<div class='knowl__content' style='display:none;' id='${outputId}' aria-live='polite' id='${outputContentsId}'>`
|
||||
+ `Loading '${linkTarget}'`
|
||||
+ `</div>`;
|
||||
|
||||
const temp = document.createElement("template");
|
||||
temp.innerHTML = placeholderText;
|
||||
this.outputElement = temp.content.children[0];
|
||||
|
||||
const insertLoc = this.findOutputLocation(this.linkElement);
|
||||
insertLoc.after(this.outputElement);
|
||||
}
|
||||
|
||||
// Get content for knowl as dom element. Returns promise that resolves to knowl content
|
||||
async getContent() {
|
||||
const contentURL = this.linkElement.getAttribute("data-knowl");
|
||||
const knowlContent = await fetch(contentURL)
|
||||
.then((response) => response.text())
|
||||
.then((data) => {
|
||||
// knowls are full HTML pages, need to just extract body
|
||||
let knowlDoc = (new DOMParser()).parseFromString(data, "text/html");
|
||||
let tempContainer = knowlDoc.body;
|
||||
// grab any scripts from head of knowl doc and add them to the output
|
||||
let scripts = knowlDoc.querySelectorAll("head script");
|
||||
tempContainer.append(...scripts);
|
||||
return tempContainer;
|
||||
})
|
||||
.catch((error) => {
|
||||
const destination = this.linkElement.getAttribute("href");
|
||||
const text = this.linkElement.textContent;
|
||||
const err_message = `<div class='knowl-output__error'>`
|
||||
+ `<div class='para'>Error fetching content. (<em>${error}</em>)</div>`
|
||||
+ `<div class='para'><a href='${destination}'>Navigate to ${text}</a> instead.</div>`
|
||||
+ `<div class='para'>If you are viewing this book from your local filesystem, this is expected behavior. To view the book with all features, you must serve the book from a web server. See the <a href="https://pretextbook.org/doc/guide/html/author-faq.html#how-do-i-view-my-book-locally">PreTeXt FAQ</a> for more information.</div>`
|
||||
+ `</div>`;
|
||||
return err_message;
|
||||
});
|
||||
return knowlContent;
|
||||
}
|
||||
|
||||
// Handle a click on the knowl link
|
||||
handleLinkClick(event) {
|
||||
// prevent navigation
|
||||
event.preventDefault();
|
||||
|
||||
if (this.outputElement !== null) {
|
||||
// output already created, toggle visibility
|
||||
this.toggle();
|
||||
} else {
|
||||
this.createOutputElement();
|
||||
|
||||
const slideHandler = new SlideRevealer(this.linkElement, this.outputElement, this.outputElement);
|
||||
//slideHandler is now responsible for handling clicks to this element
|
||||
this.linkElement.addEventListener('click', slideHandler);
|
||||
|
||||
// Wait up to a half second in hopes of avoiding double content change
|
||||
// then render to show loading message
|
||||
let loadingTimeout = setTimeout(() => {
|
||||
loadingTimeout = null;
|
||||
slideHandler.onClick(); //fake initial click
|
||||
this.toggle();
|
||||
}, 500);
|
||||
|
||||
const content = this.getContent();
|
||||
|
||||
// Content is a promise at this point, insert when resolved
|
||||
content
|
||||
.then((tempContainer) => {
|
||||
// if timeout still active, cancel it and render
|
||||
if (loadingTimeout !== null) {
|
||||
clearTimeout(loadingTimeout);
|
||||
}
|
||||
// Now give code that follows .1 seconds to render before making visible
|
||||
setTimeout(() => {
|
||||
slideHandler.onClick(); //fake initial click
|
||||
this.toggle();
|
||||
}, 100);
|
||||
|
||||
// check embedded runestone interactives by loading content into a temp container
|
||||
// we want to not render any that already are on page. Dupe IDs probably bad
|
||||
const runestoneElements = tempContainer.querySelectorAll(".ptx-runestone-container");
|
||||
[...runestoneElements].forEach((e) => {
|
||||
const rsId = e.querySelector("[data-component]")?.id;
|
||||
const onPage = document.getElementById(rsId);
|
||||
if (onPage) {
|
||||
e.innerHTML = `<div class="para">The interactive that belongs here is already on the page and cannot appear multiple times. <a href="#${rsId}">Scroll to interactive.</a>`;
|
||||
} else {
|
||||
// let runestone start rendering it
|
||||
window.runestoneComponents.renderOneComponent(e);
|
||||
}
|
||||
});
|
||||
|
||||
// now move all contents to the real output element
|
||||
const children = [...tempContainer.children];
|
||||
this.outputElement.innerHTML = "";
|
||||
this.outputElement.append(...children);
|
||||
|
||||
// render any knowls and mathjax in the knowl
|
||||
addKnowls(this.outputElement);
|
||||
|
||||
// try prism highlighting
|
||||
Prism.highlightAllUnder(this.outputElement);
|
||||
|
||||
// force any scripts (e.g. sagecell) to execute by evaling them
|
||||
[...this.outputElement.getElementsByTagName("script")].forEach((s) => {
|
||||
if (
|
||||
s.getAttribute("type") === null ||
|
||||
s.getAttribute("type") === "text/javascript"
|
||||
) {
|
||||
eval(s.innerHTML);
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch((data) => {
|
||||
console.log("Error fetching knowl content: " + data);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
/****************************************************
|
||||
*
|
||||
* mathjaxknowl.js
|
||||
*
|
||||
* Implements \knowl{url}{math} macro for MathJax. Knowls are
|
||||
* described at
|
||||
*
|
||||
* http://www.aimath.org/knowlepedia/
|
||||
*
|
||||
* Be sure to change the loadComplete() address to the URL
|
||||
* of the location of this file on your server.
|
||||
*
|
||||
* You can load it via the config=file parameter on the script
|
||||
* tag that loads MathJax.js, or by including it in the extensions
|
||||
* array in your configuration.
|
||||
*
|
||||
* Based on an approach developed by Tom Leathrum. See
|
||||
*
|
||||
* http://groups.google.com/group/mathjax-users/browse_thread/thread/d8a8d081b8e63242
|
||||
*
|
||||
* for details.
|
||||
*/
|
||||
|
||||
MathJax.Extension.Knowl = {
|
||||
version: "1.0",
|
||||
|
||||
//
|
||||
// Reveal or hide a MathJax knowl
|
||||
//
|
||||
Show: function (url,id) {
|
||||
var oid = "MathJax-knowl-output-"+id,
|
||||
uid = "MathJax-kuid-"+id;
|
||||
if ($("#"+oid).length) {
|
||||
$("#"+uid).slideToggle("fast");
|
||||
} else {
|
||||
var the_content = "<div class='knowl-output' id='"+uid+"'>" +
|
||||
"<div class='knowl'>" +
|
||||
"<div class='knowl-content' id='"+oid+"'>" +
|
||||
"loading '"+url+"'" +
|
||||
"</div>" +
|
||||
"</div>" +
|
||||
"</div>";
|
||||
|
||||
var the_parent = $("#MathJax-knowl-"+id).closest("p, article, .displaymath");
|
||||
|
||||
if(the_parent.length == 0) {
|
||||
the_parent = $("#MathJax-knowl-"+id).closest(".MJXc-display").next();
|
||||
if(the_parent.length == 0) {
|
||||
the_parent = $("#MathJax-knowl-"+id).parent().parent().parent().parent();
|
||||
}
|
||||
}
|
||||
|
||||
the_parent.after(the_content);
|
||||
|
||||
$("#"+oid).load(url,function () {
|
||||
MathJax.Hub.Queue(["Typeset",MathJax.Hub,uid]);
|
||||
});
|
||||
$("#"+uid).slideDown("slow");
|
||||
}
|
||||
},
|
||||
|
||||
//
|
||||
// Get a unique ID for the knowl
|
||||
//
|
||||
id: 0,
|
||||
GetID: function () {return this.id++}
|
||||
};
|
||||
|
||||
|
||||
MathJax.Callback.Queue(
|
||||
MathJax.Hub.Register.StartupHook("TeX Jax Ready",function () {
|
||||
var TEX = MathJax.InputJax.TeX,
|
||||
TEXDEF = TEX.Definitions,
|
||||
KNOWL = MathJax.Extension.Knowl;
|
||||
|
||||
TEXDEF.macros.knowl = "Knowl";
|
||||
|
||||
TEX.Parse.Augment({
|
||||
//
|
||||
// Implements \knowl{url}{math}
|
||||
//
|
||||
Knowl: function (name) {
|
||||
var url = this.GetArgument(name), math = this.ParseArg(name);
|
||||
if (math.inferred && math.data.length == 1)
|
||||
{math = math.data[0]} else {delete math.inferred}
|
||||
var id = KNOWL.GetID();
|
||||
this.Push(math.With({
|
||||
"class": "MathJax_knowl",
|
||||
href: "javascript:MathJax.Extension.Knowl.Show('"+url+"','"+id+"')",
|
||||
id: "MathJax-knowl-"+id,
|
||||
// border and padding will only work properly if given explicitly on the element
|
||||
style: "color:blue; border-bottom: 1px dotted #00A; padding-bottom: 1px"
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
}),
|
||||
MathJax.Hub.Register.StartupHook("onLoad",function () {
|
||||
MathJax.Ajax.Styles({
|
||||
".MathJax_knowl:hover": {"background-color": "#DDF"}
|
||||
});
|
||||
}),
|
||||
["Post",MathJax.Hub.Startup.signal,"TeX knowl ready"]
|
||||
);
|
||||
|
||||
MathJax.Ajax.loadComplete("http://pretextbook.org/js/lib/mathjaxknowl.js");
|
||||
MathJax.Ajax.loadComplete("https://pretextbook.org/js/lib/mathjaxknowl.js");
|
||||
|
||||
|
||||
+48
@@ -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"]
|
||||
}
|
||||
});
|
||||
Executable
+322
@@ -0,0 +1,322 @@
|
||||
|
||||
console.log("enabling login");
|
||||
|
||||
function createCookie(name,value,days) {
|
||||
if (days) {
|
||||
var date = new Date();
|
||||
date.setTime(date.getTime()+(days*24*60*60*1000));
|
||||
var expires = "; expires="+date.toGMTString();
|
||||
}
|
||||
else var expires = "";
|
||||
/* document.cookie = name+"="+value+expires+"; path=/; domain=aimath.org";
|
||||
*/
|
||||
document.cookie = name + "=" + value + expires + "; path=/; " + "SameSite=None; Secure";
|
||||
console.log("created cookie " + name);
|
||||
}
|
||||
|
||||
function readCookie(name) {
|
||||
var nameEQ = name + "=";
|
||||
var ca = document.cookie.split(';');
|
||||
for(var i=0;i < ca.length;i++) {
|
||||
var c = ca[i];
|
||||
while (c.charAt(0)==' ') c = c.substring(1,c.length);
|
||||
if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function eraseCookie(name) {
|
||||
createCookie(name,"",-1);
|
||||
}
|
||||
|
||||
/* next two also in edit.js. Need to clean up */
|
||||
Storage.prototype.setObject = function(key, value) {
|
||||
// this.setItem(key, JSON.stringify(value));
|
||||
this.setItem(key, JSON.stringify(value, function(key, val) {
|
||||
// console.log("key", key, "value", value, "val", val);
|
||||
return val.toFixed ? Number(val.toFixed(3)) : val;
|
||||
}));
|
||||
}
|
||||
|
||||
Storage.prototype.getObject = function(key) {
|
||||
var value = this.getItem(key);
|
||||
return value && JSON.parse(value);
|
||||
}
|
||||
|
||||
function hash_of_string(str) {
|
||||
str = str.toString();
|
||||
var the_len = str.length;
|
||||
the_len = Math.min(12,the_len);
|
||||
var the_hash = 123456;
|
||||
var hash_lis = [510149, 120151, 230157, 411063, 320167, 631973, 410179, 321081, 231091, 111093, 121097, 230199];
|
||||
for (var j=0; j < the_len; ++ j) {
|
||||
var this_char = str.charCodeAt(j);
|
||||
this_char = parseInt(this_char);
|
||||
var this_add = this_char * hash_lis[j];
|
||||
the_hash += this_add;
|
||||
the_hash = the_hash % 1000000;
|
||||
}
|
||||
return the_hash.toString()
|
||||
}
|
||||
|
||||
function login_form(mode="login") {
|
||||
var the_form = "";
|
||||
|
||||
/* the_form += '<div id="theloginform" class="modal login">';
|
||||
*/
|
||||
if (mode == 'logout') {
|
||||
the_form += '<form name="logoutform" class="modal-content animate" onSubmit="return removeLogin();" action="">';
|
||||
the_form += '<div class="container">\n';
|
||||
the_form += '<button type="submit">Yes, really logout</button>';
|
||||
the_form += '<div id="dontlogout" class=dontlogout">Stay logged in</div>'
|
||||
the_form += '</div>\n';
|
||||
the_form += '</form>\n';
|
||||
}
|
||||
else{
|
||||
the_form += '<form name="loginform" class="modal-content animate" onSubmit="return validateLogin();" action="">';
|
||||
if ((typeof guest_access !== 'undefined') && guest_access) {
|
||||
the_form += '<p class="instructions" >You can log in as "guest" with password "guest" for 3 hours of access.</p>'; }
|
||||
the_form += '<div class="container">\n';
|
||||
the_form += '<label><b>Id</b></label>\n<input type="text" placeholder="Enter Username" name="uname" required>';
|
||||
the_form += '<label><b>Password</b></label>\n<input type="password" placeholder="Enter Password" name="psw" required>';
|
||||
the_form += '<button type="submit">Login</button>';
|
||||
the_form += '</div>\n';
|
||||
the_form += '<div class="container" style="background-color:#f1f1f1">';
|
||||
the_form += '<span class="psw">Login trouble? Email <a href="mailto:help@aimath.org">help@aimath.org</a></span>';
|
||||
the_form += '</div>\n';
|
||||
the_form += '</form>\n';
|
||||
}
|
||||
|
||||
theform = document.createElement('div');
|
||||
theform.id = "the" + mode + "form";
|
||||
theform.className = "modal login";
|
||||
document.body.appendChild(theform);
|
||||
theform.innerHTML = the_form;
|
||||
}
|
||||
|
||||
function survey_form(surveylink) {
|
||||
var the_form = "";
|
||||
|
||||
/* the_form += '<div id="theloginform" class="modal login">';
|
||||
*/
|
||||
the_form += '<div class="modal-content">';
|
||||
the_form += '<p class="instructions">Can you take a brief survey<br>about your use of this book?</p>';
|
||||
the_form += '<button class="surveyresponse" id="takesurvey">Yes, I can take the survey<br>(opens new window)</button>';
|
||||
the_form += '<button class="surveyresponse" id="remindlater">Not now. Please remind me later.</button>';
|
||||
the_form += '</div>\n';
|
||||
|
||||
theform = document.createElement('div');
|
||||
theform.id = "the" + "survey" + "form";
|
||||
theform.className = "modal survey";
|
||||
document.body.appendChild(theform);
|
||||
theform.innerHTML = the_form;
|
||||
}
|
||||
|
||||
|
||||
function loadScript(script) {
|
||||
if (typeof js_version === 'undefined') { js_version = '0.12' }
|
||||
var newscript = document.createElement('script');
|
||||
newscript.type = 'text/javascript';
|
||||
newscript.async = true;
|
||||
newscript.src = 'https://pretextbook.org/js/' + js_version + '/' + script + '.js';
|
||||
var allscripts = document.getElementsByTagName('script');
|
||||
var s = allscripts[allscripts.length - 1];
|
||||
console.log('s',s);
|
||||
console.log("adding a script", newscript);
|
||||
s.parentNode.insertBefore(newscript, s.nextSibling);
|
||||
}
|
||||
|
||||
|
||||
function removeLogin() {
|
||||
eraseCookie("ut_cookie");
|
||||
}
|
||||
|
||||
var uname = "";
|
||||
var emanu = "";
|
||||
|
||||
function check_role() {
|
||||
console.log("check_role for", uname, "uname");
|
||||
if(uname=="instructor") { return "instructor" }
|
||||
var_role_data = {"action": "check", "user": uname, "pw": emanu, "type": "instructor", "instId": uname}
|
||||
var role_key = "";
|
||||
$.ajax({
|
||||
url: "https://aimath.org/cgi-bin/u/highlights.py",
|
||||
type: "post",
|
||||
data: JSON.stringify(var_role_data),
|
||||
dataType: "json",
|
||||
async: false,
|
||||
success: function(data) {
|
||||
console.log("something", data, "back from highlight");
|
||||
role_key = data
|
||||
},
|
||||
error: function(errMsg) {
|
||||
console.log("seems to be an error?",errMsg);
|
||||
// alert("Error X2\n" + errMsg);
|
||||
}
|
||||
});
|
||||
|
||||
console.log("done checking role", role_key);
|
||||
return role_key
|
||||
}
|
||||
|
||||
function validateLogin() {
|
||||
var logged_in = false;
|
||||
var un = document.loginform.uname.value;
|
||||
// un = un.toLowerCase();
|
||||
uname = un;
|
||||
var pw = document.loginform.psw.value;
|
||||
emanu = pw;
|
||||
console.log("xuname", uname, "yemanu", emanu);
|
||||
var guest_name = "guest";
|
||||
var the_password_guest = "guest";
|
||||
var editor_name = "editor";
|
||||
var the_password_editor = "editor";
|
||||
var instructor_name = "instructor";
|
||||
var the_password_instructor = "instructor";
|
||||
var the_un_enc = hash_of_string(un);
|
||||
console.log("un", un, "the_un_enc", "y"+the_un_enc+"y", "pw", "x"+pw+"x", "pw == the_un_enc", pw == the_un_enc);
|
||||
var the_url_enc = hash_of_string(window.location.hostname);
|
||||
console.log('window.location.hostname ' + window.location.hostname);
|
||||
if ((typeof guest_access !== 'undefined') && guest_access && ( ((un == guest_name) && (pw == the_password_guest)) || ((un == editor_name) && (pw == the_password_editor))) ) {
|
||||
console.log("setting the guest ut_cookie");
|
||||
createCookie('ut_cookie',un,0.25);
|
||||
logged_in = true;
|
||||
console.log("logged in as guest", logged_in);
|
||||
}
|
||||
else if ((typeof guest_access !== 'undefined') && guest_access && (un == instructor_name) && (pw == the_password_instructor)) {
|
||||
console.log("setting the instructor ut_cookie");
|
||||
createCookie('ut_cookie',un,0.25);
|
||||
logged_in = true;
|
||||
console.log("logged in as instructor", logged_in);
|
||||
role = "instructor";
|
||||
}
|
||||
else if (pw == the_un_enc) {
|
||||
console.log("setting the ut_cookie");
|
||||
createCookie('ut_cookie',un,150);
|
||||
logged_in = true;
|
||||
}
|
||||
else if (pw == the_url_enc) {
|
||||
console.log("setting the url ut_cookie");
|
||||
createCookie('ut_cookie',un,0.25);
|
||||
logged_in = true;
|
||||
}
|
||||
else {
|
||||
// alert ('Login was unsuccessful, please check id: "' + uname + '" and password"' + emanu +'". Also checking "' + pw + '" and "' + the_un_enc + '".');
|
||||
alert ('Login was unsuccessful, please check id: "' + uname + '".');
|
||||
console.log("failed to set the ut_cookie");
|
||||
logged_in = false;
|
||||
}
|
||||
console.log("logged_in", logged_in);
|
||||
if (logged_in) {
|
||||
$("#theloginform").hide();
|
||||
document.getElementById('loginlogout').innerHTML = 'logout' + ' ' + un;
|
||||
if(uname != "editor") {
|
||||
loadScript('answer');
|
||||
loadScript('highlight')
|
||||
if ((typeof trails !== 'undefined') && trails) {
|
||||
loadScript('trails');
|
||||
}
|
||||
}
|
||||
}
|
||||
var role_key = check_role();
|
||||
console.log("role_key", role_key);
|
||||
if (role_key) {
|
||||
console.log("another instructor", role_key);
|
||||
role = 'instructor'
|
||||
}
|
||||
// if (logged_in && /^\d+$/.test(uname) && ( (uname.length == 5 || uname.length == 8 && /^\d+00$/.test(uname)) )) {
|
||||
// console.log("an instructor");
|
||||
// role = 'instructor'
|
||||
// }
|
||||
|
||||
console.log("role", role);
|
||||
return logged_in
|
||||
}
|
||||
|
||||
var aa_id = readCookie('aa_cookie');
|
||||
|
||||
if (aa_id) {
|
||||
console.log("found cookie");
|
||||
console.log(aa_id);
|
||||
var date = new Date();
|
||||
var date_now = date.getTime();
|
||||
var date_str = date_now.toString();
|
||||
var date_str_trimmed = date_str.slice(5,13);
|
||||
console.log(date_str)
|
||||
console.log(date_str_trimmed)
|
||||
}
|
||||
else {
|
||||
console.log("no cookie found")
|
||||
var date = new Date();
|
||||
var date_now = date.getTime();
|
||||
var date_str = date_now.toString();
|
||||
var date_str_trimmed = date_str.slice(5,13);
|
||||
aa_id = date_str_trimmed;
|
||||
createCookie('aa_cookie',aa_id,150);
|
||||
}
|
||||
|
||||
/* dataurlbase = dataurlbase.concat("per").concat("=").concat(aa_id).concat("&");
|
||||
*/
|
||||
|
||||
var ut_id = readCookie('ut_cookie');
|
||||
uname = ut_id || "";
|
||||
|
||||
if(uname == "instructor") { role="instructor"}
|
||||
|
||||
console.log("uname", uname);
|
||||
|
||||
|
||||
var pageIdentifier = "";
|
||||
|
||||
window.addEventListener('load', function(){
|
||||
console.log("checking login", ut_id);
|
||||
bodyID = document.getElementsByTagName('body')[0].id;
|
||||
console.log("bodyID", bodyID);
|
||||
if (bodyID) {
|
||||
var secID = document.getElementsByTagName('section')[0].id;
|
||||
if (secID) {
|
||||
pageIdentifier = bodyID + "___" + secID
|
||||
}
|
||||
}
|
||||
|
||||
if (pageIdentifier) {
|
||||
|
||||
if (ut_id) {
|
||||
console.log("found "+ut_id);
|
||||
$("#theloginform").hide();
|
||||
|
||||
// if (/^\d+$/.test(uname) && ( (uname.length == 5 || uname.length == 8 && /^\d+00$/.test(uname)) )) {
|
||||
var role_key = check_role();
|
||||
console.log("the role_key", role_key);
|
||||
if (role_key) {
|
||||
console.log("another instructor", role_key);
|
||||
role = 'instructor'
|
||||
}
|
||||
document.getElementById('loginlogout').className = 'logout';
|
||||
document.getElementById('loginlogout').innerHTML = 'logout' + ' ' + ut_id;
|
||||
console.log("done hiding "+ut_id);
|
||||
loadScript('answer');
|
||||
if(uname != "editor") {loadScript('highlight')}
|
||||
if ((typeof trails !== 'undefined') && trails) {
|
||||
loadScript('trails');
|
||||
}
|
||||
}
|
||||
else if (typeof login_required !== 'undefined' && login_required) {
|
||||
login_form();
|
||||
}
|
||||
|
||||
$("#loginlogout.logout").click(function(){
|
||||
login_form('logout');
|
||||
$("#dontlogout").click(function() {
|
||||
$("#thelogoutform").remove()
|
||||
});
|
||||
});
|
||||
$("#loginlogout.login").click(function(){
|
||||
login_form('login');
|
||||
});
|
||||
} else {
|
||||
console.log("login not enabled because document not identified")
|
||||
}
|
||||
// console.log("the role", role);
|
||||
});
|
||||
|
||||
+48
@@ -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", "*");
|
||||
}
|
||||
+515
@@ -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}`;
|
||||
}
|
||||
+652
@@ -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);
|
||||
};
|
||||
Executable
+257
@@ -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;
|
||||
Executable
+1264
File diff suppressed because it is too large
Load Diff
Executable
+315
@@ -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();
|
||||
});
|
||||
});
|
||||
Executable
+112
@@ -0,0 +1,112 @@
|
||||
function doSearch() {
|
||||
// Get the search terms from the input text box
|
||||
let terms = document.getElementById("ptxsearch").value;
|
||||
// Where do we want to put the results?
|
||||
let resultArea = document.getElementById("searchresults")
|
||||
resultArea.innerHTML = ""; // clear out any previous results
|
||||
// do the search using the provided index
|
||||
let pageResult = ptx_lunr_idx.search(terms);
|
||||
// Number the documents from first to last so we can order the results by their
|
||||
// position in the book.
|
||||
snum = 0;
|
||||
for (let doc of ptx_lunr_docs) {
|
||||
doc.snum = snum;
|
||||
snum += 1;
|
||||
}
|
||||
// Transfer meta data from the document to the results to make it easy to add
|
||||
// our lists later.
|
||||
augmentResults(pageResult, ptx_lunr_docs);
|
||||
addResultToPage(pageResult, ptx_lunr_docs, resultArea);
|
||||
MathJax.typeset();
|
||||
}
|
||||
|
||||
// Find the entry for a search result in the original document index
|
||||
function findEntry(resultId, db) {
|
||||
for (const page of db) {
|
||||
if (page.id === resultId) {
|
||||
return page;
|
||||
}
|
||||
}
|
||||
return resultId;
|
||||
}
|
||||
|
||||
function augmentResults(result, docs) {
|
||||
for (let res of result) {
|
||||
let info = findEntry(res.ref, docs);
|
||||
res.number = info.number;
|
||||
res.type = info.type;
|
||||
res.title = info.title;
|
||||
res.url = info.url;
|
||||
res.level = info.level;
|
||||
res.snum = info.snum;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function comparePosition(a, b) {
|
||||
if (a.snum < b.snum) {
|
||||
return -1;
|
||||
}
|
||||
if (a.snum > b.snum) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function addResultToPage(result, docs, resultArea) {
|
||||
let len = result.length
|
||||
let high = result[Math.floor(len*0.25)].score;
|
||||
let med = result[Math.floor(len*0.5)].score;
|
||||
let low = result[Math.floor(len*0.75)].score;
|
||||
// sort the results by their position in the book, not their score
|
||||
result = result.sort(comparePosition)
|
||||
let indent = "1";
|
||||
let currIndent = indent;
|
||||
let origResult = resultArea;
|
||||
// Create list entries indenting as needed.
|
||||
for (const res of result) {
|
||||
let link = document.createElement("a")
|
||||
// add a class so we can colorize the results based on their rank in terms
|
||||
// of search score.
|
||||
if (res.score >= high) {
|
||||
link.classList.add("high_result")
|
||||
} else if (res.score >= med) {
|
||||
link.classList.add("medium_result")
|
||||
} else if (res.score >= low) {
|
||||
link.classList.add("low_result")
|
||||
}
|
||||
currIndent = res.level;
|
||||
if (currIndent > indent) {
|
||||
indent = currIndent;
|
||||
let ilist = document.createElement("ul")
|
||||
ilist.classList.add("detailed_result");
|
||||
resultArea.appendChild(ilist);
|
||||
resultArea = ilist;
|
||||
} else if (currIndent < indent) {
|
||||
resultArea = origResult;
|
||||
indent = currIndent;
|
||||
}
|
||||
let bullet = document.createElement("li")
|
||||
bullet.style.marginTop = "5px";
|
||||
link.href = `${res.url}`;
|
||||
link.innerHTML = `${res.type} ${res.number} ${res.title}`;
|
||||
bullet.appendChild(link)
|
||||
let p = document.createElement("text");
|
||||
p.innerHTML = ` (${res.score.toFixed(2)})`;
|
||||
bullet.appendChild(p);
|
||||
resultArea.appendChild(bullet);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
function showHelp() {
|
||||
let state = document.getElementById("helpme").style.display;
|
||||
if (state == "none") {
|
||||
document.getElementById("helpme").style.display = "block";
|
||||
document.getElementById("helpbutt").innerHTML = "Hide Help"
|
||||
} else {
|
||||
document.getElementById("helpme").style.display = "none";
|
||||
document.getElementById("helpbutt").innerHTML = "Show Help"
|
||||
}
|
||||
}
|
||||
+510
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Executable
+249
@@ -0,0 +1,249 @@
|
||||
<!DOCTYPE html>
|
||||
<!--********************************************-->
|
||||
<!--* Generated from PreTeXt source *-->
|
||||
<!--* on 2026-01-12T12:36:52-05:00 *-->
|
||||
<!--* A recent stable commit (2022-07-01): *-->
|
||||
<!--* 6c761d3dba23af92cba35001c852aac04ae99a5f *-->
|
||||
<!--* *-->
|
||||
<!--* https://pretextbook.org *-->
|
||||
<!--* *-->
|
||||
<!--********************************************-->
|
||||
<html xmlns:pi="http://pretextbook.org/2020/pretext/internal">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<title>Slides for MATH 1044</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=yes">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/4.1.2/reset.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/4.1.2/reveal.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/4.1.2/theme/white.css" rel="stylesheet">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/4.1.2/reveal.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/4.1.2/plugin/math/math.js"></script><style>
|
||||
ul {
|
||||
display: block !important;
|
||||
}
|
||||
.reveal img {
|
||||
border: 0.5px !important;
|
||||
border-radius: 2px 10px 2px;
|
||||
padding: 4px;
|
||||
}
|
||||
.reveal pre {
|
||||
box-shadow: none;
|
||||
line-height: 1;
|
||||
font-size: inherit;
|
||||
width: auto;
|
||||
margin: inherit;
|
||||
}
|
||||
.reveal pre code {
|
||||
display: block;
|
||||
padding: 0;
|
||||
overflow: unset;
|
||||
max-height: unset;
|
||||
word-wrap: normal;
|
||||
}
|
||||
.definition-like,.theorem-like,.project-like {
|
||||
border-width: 0.5px;
|
||||
border-style: solid;
|
||||
border-radius: 2px 10px 2px;
|
||||
padding: 1%;
|
||||
margin-bottom: var(--r-block-margin);
|
||||
}
|
||||
.definition-like {
|
||||
background: #006080;
|
||||
background: color-mix(in srgb, var(--r-background-color) 75%, #006080);
|
||||
}
|
||||
.theorem-like {
|
||||
background: #ff000010;
|
||||
background: color-mix(in srgb, var(--r-background-color) 75%, #aa0000);
|
||||
}
|
||||
.proof-like {
|
||||
background: #ffffff;
|
||||
background: color-mix(in srgb, var(--r-background-color) 75%, #aaaaaa);
|
||||
}
|
||||
.project-like {
|
||||
background: #60800010;
|
||||
background: color-mix(in srgb, var(--r-background-color) 75%, #608000);
|
||||
}
|
||||
.code-inline {
|
||||
background: #60800010;
|
||||
background: color-mix(in srgb, var(--r-background-color) 75%, var(--r-link-color));
|
||||
padding: 0 3px;
|
||||
border: 1px solid;
|
||||
margin: 3px;
|
||||
display: inline-block;
|
||||
}
|
||||
.sagecell_sessionOutput {
|
||||
background: white;
|
||||
color: black;
|
||||
border: 0.5px solid var(--r-main-color);
|
||||
}
|
||||
|
||||
.program {
|
||||
background: #60800010;
|
||||
background: color-mix(in srgb, var(--r-background-color) 75%, var(--r-link-color));
|
||||
max-height: 450px;
|
||||
overflow: auto;
|
||||
border: 0.5px solid var(--r-main-color);
|
||||
}
|
||||
.ptx-sagecell, .reveal .program {
|
||||
font-size: calc(var(--r-main-font-size) * 0.6);
|
||||
}
|
||||
|
||||
|
||||
.ptx-content :is(.image-box, .audio-box, .video-box, .asymptote-box) {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ptx-content iframe.asymptote, .ptx-content iframe.asymptote, .ptx-content .video-box .video, .ptx-content .video-box .video-poster {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
|
||||
code[class*="language-"], pre[class*="language-"] {
|
||||
padding: 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
dfn {
|
||||
font-weight: bold;
|
||||
}
|
||||
.ptx-content ol.no-marker,
|
||||
.ptx-content ul.no-marker,
|
||||
.ptx-content li.no-marker {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.ptx-content ol.decimal {
|
||||
list-style-type: decimal;
|
||||
}
|
||||
.ptx-content ol.lower-alpha {
|
||||
list-style-type: lower-alpha;
|
||||
}
|
||||
.ptx-content ol.upper-alpha {
|
||||
list-style-type: upper-alpha;
|
||||
}
|
||||
.ptx-content ol.lower-roman {
|
||||
list-style-type: lower-roman;
|
||||
}
|
||||
.ptx-content ol.upper-roman {
|
||||
list-style-type: upper-roman;
|
||||
}
|
||||
.ptx-content ul.disc {
|
||||
list-style-type: disc;
|
||||
}
|
||||
.ptx-content ul.square {
|
||||
list-style-type: square;
|
||||
}
|
||||
.ptx-content ul.circle {
|
||||
list-style-type: circle;
|
||||
}
|
||||
.ptx-content ol.no-marker,
|
||||
.ptx-content ul.no-marker {
|
||||
list-style-type: none;
|
||||
}
|
||||
.ptx-content .cols1 li,
|
||||
.ptx-content .cols2 li,
|
||||
.ptx-content .cols3 li,
|
||||
.ptx-content .cols4 li,
|
||||
.ptx-content .cols5 li,
|
||||
.ptx-content .cols6 li {
|
||||
float: left;
|
||||
padding-right:2em;
|
||||
}
|
||||
|
||||
/* make small images full-width in #sidebyside */
|
||||
/* could improve with a .sidebyside class */
|
||||
div[style*="display:table-cell"] img {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body><div class="reveal ptx-content">
|
||||
<div id="latex-macros" class="hidden-content process-math" style="display:none"><span class="process-math">\(\newcommand{\N}{\mathbb N}
|
||||
\newcommand{\Z}{\mathbb Z}
|
||||
\newcommand{\Q}{\mathbb Q}
|
||||
\newcommand{\R}{\mathbb R}
|
||||
\newcommand{\lt}{<}
|
||||
\newcommand{\gt}{>}
|
||||
\newcommand{\amp}{&}
|
||||
\newcommand{\fillinmath}[1]{\mathchoice{\underline{\displaystyle \phantom{\ \,#1\ \,}}}{\underline{\textstyle \phantom{\ \,#1\ \,}}}{\underline{\scriptstyle \phantom{\ \,#1\ \,}}}{\underline{\scriptscriptstyle\phantom{\ \,#1\ \,}}}}
|
||||
\)</span></div>
|
||||
<div class="slides">
|
||||
<section><section><h1>Slides for MATH 1044</h1></section></section><section><section><h1>§ 1.1: Set Theory</h1></section><section><h3>Sample Space</h3>
|
||||
<div align="left"><article class="definition definition-like" id="def-sample-space"><h4 class="heading">
|
||||
<span class="type">Definition</span><span class="space heading-divison-mark heading-divison-mark__space"> </span><span class="codenumber">1</span><span class="period heading-divison-mark heading-divison-mark__period">.</span>
|
||||
</h4>
|
||||
<p>The <dfn class="terminology">sample space</dfn> <span class="process-math">\(\Omega\)</span> is the set of all possible results of an experiment. A single result is called an <dfn class="terminology">outcome</dfn>. A collection of results is called an <dfn class="terminology">event</dfn>.</p></article></div></section><section><h3>Rolling Dice</h3>
|
||||
<div align="left"><article class="example example-like" id="sec-1-1-3-2"><h4 class="heading">
|
||||
<span class="type">Example</span><span class="space heading-divison-mark heading-divison-mark__space"> </span><span class="codenumber">2</span><span class="period heading-divison-mark heading-divison-mark__period">.</span>
|
||||
</h4>
|
||||
<p>We roll a 6-sided die two times. What is <span class="process-math">\(\Omega\text{?}\)</span></p></article></div></section><section><h3>Flipping Coins</h3>
|
||||
<div align="left"><article class="example example-like" id="sec-1-1-4-2"><h4 class="heading">
|
||||
<span class="type">Example</span><span class="space heading-divison-mark heading-divison-mark__space"> </span><span class="codenumber">3</span><span class="period heading-divison-mark heading-divison-mark__period">.</span>
|
||||
</h4>
|
||||
<p>We flip a coin three times. What is <span class="process-math">\(\Omega\text{?}\)</span></p></article></div></section><section><h3>Picking a Value</h3>
|
||||
<div align="left"><article class="example example-like" id="sec-1-1-5-2"><h4 class="heading">
|
||||
<span class="type">Example</span><span class="space heading-divison-mark heading-divison-mark__space"> </span><span class="codenumber">4</span><span class="period heading-divison-mark heading-divison-mark__period">.</span>
|
||||
</h4>
|
||||
<p>We pick a number between 1 and 10. What is <span class="process-math">\(\Omega\text{?}\)</span></p></article></div></section><section><h3>Sets</h3>
|
||||
<div align="left"><article class="definition definition-like" id="def-subset"><h4 class="heading">
|
||||
<span class="type">Definition</span><span class="space heading-divison-mark heading-divison-mark__space"> </span><span class="codenumber">5</span><span class="period heading-divison-mark heading-divison-mark__period">.</span>
|
||||
</h4>
|
||||
<p>Let <span class="process-math">\(A\)</span> be a set. The symbol <span class="process-math">\(\in\)</span> means "is an element of", as in <span class="process-math">\(a \in A\text{.}\)</span> Given another set <span class="process-math">\(B\text{,}\)</span> we say <span class="process-math">\(A\)</span> is a <dfn class="terminology">subset</dfn> of <span class="process-math">\(B\text{,}\)</span> written <span class="process-math">\(A\subset B\text{,}\)</span> to mean that every element of the set <span class="process-math">\(A\)</span> is also an element of the set <span class="process-math">\(B\text{.}\)</span></p></article></div></section><section><h3></h3>
|
||||
<div align="left"><article class="definition definition-like" id="def-set-operations"><h4 class="heading">
|
||||
<span class="type">Definition</span><span class="space heading-divison-mark heading-divison-mark__space"> </span><span class="codenumber">6</span><span class="period heading-divison-mark heading-divison-mark__period">.</span>
|
||||
</h4>
|
||||
<p>Let <span class="process-math">\(A, B \subset \Omega\text{.}\)</span><dl class="description-list">
|
||||
<div>
|
||||
<dt>Union</dt>
|
||||
<dd><p>The <dfn class="terminology">union</dfn> is <span class="process-math">\(A \cup B = \{x \mid x \in A \text{ or } x \in B\}\text{.}\)</span></p></dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Intersection</dt>
|
||||
<dd><p>The <dfn class="terminology">intersection</dfn> is <span class="process-math">\(A \cap B = \{x \mid x \in A \text{ and } x \in B\}\text{.}\)</span></p></dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Difference</dt>
|
||||
<dd><p>The <dfn class="terminology">set difference</dfn> is <span class="process-math">\(A - B = \{x \mid x \in A \text{ and } x \notin B\}\text{.}\)</span></p></dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Complement</dt>
|
||||
<dd><p>The <dfn class="terminology">complement</dfn> is <span class="process-math">\(A^c = \{x \in \Omega \mid x \notin A\}\text{.}\)</span></p></dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Empty Set</dt>
|
||||
<dd><p>The <dfn class="terminology">empty set</dfn>, written <span class="process-math">\(\emptyset\)</span> or <span class="process-math">\(\{\}\text{,}\)</span> contains no elements.</p></dd>
|
||||
</div>
|
||||
</dl></p></article></div></section></section><section><section><h1>Section 1.2</h1></section><section><h3>Title 2</h3>
|
||||
<div align="left"><p>Some more text.</p></div></section></section>
|
||||
</div>
|
||||
</div></body>
|
||||
<script>
|
||||
Reveal.initialize({
|
||||
controls: true,
|
||||
controlsTutorial: true,
|
||||
controlsLayout: 'bottom-right',
|
||||
controlsBackArrows: 'faded',
|
||||
navigationMode: 'default',
|
||||
progress: false,
|
||||
center: false,
|
||||
hash: true,
|
||||
transition: 'fade',
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
margin: "0.025",
|
||||
math: {
|
||||
mathjax: 'https://cdn.jsdelivr.net/gh/mathjax/mathjax@2.7.8/MathJax.js',
|
||||
config: 'TeX-AMS_HTML-full',
|
||||
// other options are passed into MathJax.Hub.Config()
|
||||
tex2jax: {
|
||||
inlineMath: [['\\(','\\)']],
|
||||
displayMath: [],
|
||||
}
|
||||
},
|
||||
plugins: [ RevealMath ]
|
||||
});
|
||||
</script>
|
||||
</html>
|
||||
Reference in New Issue
Block a user