Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions js/dist/pretext-core.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

85 changes: 85 additions & 0 deletions js/pretext_add_on.js
Original file line number Diff line number Diff line change
Expand Up @@ -1337,6 +1337,86 @@ document.addEventListener("click", (ev) => {
window.location.assign(link.href);
});

// PreTeXt emits multiple-choice exercises as static, readable HTML (a
// statement plus a lettered <ol> of choices, annotated with @data-correct
// and a hidden/visible .choice-feedback div per choice) so that the exercise
// is always usable as-is: in print preview, or for a reader without JS. On a
// normal page, this rebuilds -- in place, from that same static HTML -- the
// data-component skeleton Runestone's own JS expects, then explicitly asks
// Runestone to render it via window.runestoneComponents.renderOneComponent().
// That's the same public entry point knowl.js's addKnowl() already relies on
// for interactives injected into a knowl after page load: Runestone's own
// automatic page-load scan only ever sees markup that was already present
// when *it* ran (well before this DOMContentLoaded handler gets a turn), so
// anything built afterward -- ours included -- needs this explicit call, or
// it's never picked up.
//
// This mutates each container in place rather than hiding one version and
// showing another, so there's no separate CSS toggle: skip this call (as the
// printpreview DOMContentLoaded handler below does) and the static markup is
// simply what's left on the page.
function hydrateMultipleChoice() {
document.querySelectorAll('[data-hydrate="multiplechoice"]').forEach(container => {
const id = container.id;
const statement = container.querySelector(':scope > .exercise-statement');
const choices = container.querySelectorAll(':scope > ol.runestone-static-choices > li');
if (!id || !statement || choices.length === 0) return;

const section = document.createElement('div');
section.className = 'runestone multiplechoice_section';
section.appendChild(statement);

const list = document.createElement('ul');
list.id = id;
list.className = 'exercise-interactives';
list.dataset.component = 'multiplechoice';
if (container.dataset.multipleanswers) {
list.dataset.multipleanswers = container.dataset.multipleanswers;
}
if ('random' in container.dataset) {
list.dataset.random = '';
}

choices.forEach((choice, index) => {
const letter = String.fromCharCode('a'.charCodeAt(0) + index);
const choiceId = `${id}_opt_${letter}`;
const feedback = choice.querySelector(':scope > .choice-feedback');

const answerItem = document.createElement('li');
answerItem.id = choiceId;
answerItem.dataset.component = 'answer';
if ('correct' in choice.dataset) {
answerItem.dataset.correct = '';
}
while (choice.firstChild) {
if (choice.firstChild === feedback) {
choice.removeChild(feedback);
continue;
}
answerItem.appendChild(choice.firstChild);
}
list.appendChild(answerItem);

const feedbackItem = document.createElement('li');
feedbackItem.id = choiceId;
feedbackItem.dataset.component = 'feedback';
if (feedback) {
while (feedback.firstChild) {
feedbackItem.appendChild(feedback.firstChild);
}
}
list.appendChild(feedbackItem);
});

section.appendChild(list);
container.replaceChildren(section);

if (window.runestoneComponents) {
window.runestoneComponents.renderOneComponent(container);
}
});
}

// Function to load the printout section and switch to print stylesheet. This will run whenever a user clicks on a print preview link (which adds ?printpreview=sectionID to the URL).
async function loadPrintout(printableSectionID) {

Expand Down Expand Up @@ -1542,6 +1622,11 @@ async function applySolutionVisibility(solutionType, hidden, {paperSize, margins
window.addEventListener("DOMContentLoaded", async function(event) {
const urlParams = new URLSearchParams(window.location.search);
let pendingSettle = Promise.resolve();
// Print preview shows the static, plain-HTML exercise markup as-is; every
// other page hydrates it into the interactive Runestone widget.
if (!urlParams.has("printpreview")) {
hydrateMultipleChoice();
}
// We condition on the existence of the papersize radio buttons, which only appear in the printout print preview.
if (urlParams.has("printpreview")) {
const printableSectionID = urlParams.get("printpreview");
Expand Down
4 changes: 3 additions & 1 deletion xsl/pretext-html.xsl
Original file line number Diff line number Diff line change
Expand Up @@ -4665,7 +4665,9 @@ along with PreTeXt. If not, see <http://www.gnu.org/licenses/>.
<xsl:param name="b-has-solution" />

<xsl:if test="$b-has-statement">
<xsl:apply-templates select="." mode="runestone-to-interactive"/>
<xsl:apply-templates select="." mode="runestone-to-interactive">
<xsl:with-param name="b-has-solution" select="$b-has-solution"/>
</xsl:apply-templates>
</xsl:if>
<xsl:apply-templates select="." mode="solutions-div">
<xsl:with-param name="b-original" select="$b-original"/>
Expand Down
116 changes: 56 additions & 60 deletions xsl/pretext-runestone.xsl
Original file line number Diff line number Diff line change
Expand Up @@ -1049,76 +1049,72 @@ along with PreTeXt. If not, see <http://www.gnu.org/licenses/>.

<!-- Multiple Choice -->

<!-- Static-first, JS-hydrated: this emits genuine, readable HTML (a -->
<!-- statement plus a lettered list of choices) that is fully usable as-is -->
<!-- (print, no-JS). It is annotated with just enough data (@data-correct, -->
<!-- @data-hydrate, the multipleanswers/random flags) for client-side JS -->
<!-- (hydrateMultipleChoice() in pretext_add_on.js) to rebuild, in place, -->
<!-- the exact data-component skeleton Runestone's own JS expects, before -->
<!-- Runestone's script has a chance to scan the page. If that JS never -->
<!-- runs (print preview, or JS disabled), this static markup is simply -->
<!-- what the reader sees. -->
<xsl:template match="*[@pi:exercise-interactive = 'multiplechoice']" mode="runestone-to-interactive">
<div class="ptx-runestone-container">
<div class="runestone multiplechoice_section">
<!-- overall statement, not per-choice -->
<div class="exercise-statement">
<xsl:apply-templates select="statement"/>
</div>
<!-- ul can have multiple answer attribute -->
<ul data-component="multiplechoice" class="exercise-interactives">
<xsl:apply-templates select="." mode="runestone-id-attribute"/>
<xsl:variable name="ncorrect" select="count(choices/choice[@correct = 'yes'])"/>
<xsl:attribute name="data-multipleanswers">
<xsl:choose>
<xsl:when test="choices/@multiple-correct = 'yes'">
<xsl:text>true</xsl:text>
</xsl:when>
<xsl:when test="choices/@multiple-correct = 'no'">
<xsl:text>false</xsl:text>
</xsl:when>
<xsl:when test="$ncorrect > 1">
<xsl:text>true</xsl:text>
</xsl:when>
<xsl:otherwise>
<xsl:text>false</xsl:text>
</xsl:otherwise>
</xsl:choose>
</xsl:attribute>
<!-- bare attribute, iff requested -->
<xsl:if test="choices/@randomize = 'yes'">
<xsl:attribute name="data-random"/>
</xsl:if>
<xsl:apply-templates select="choices/choice">
<xsl:with-param name="the-id">
<xsl:apply-templates select="." mode="runestone-id"/>
</xsl:with-param>
</xsl:apply-templates>
</ul>
<xsl:param name="b-has-solution" select="false()"/>
<div class="ptx-runestone-container" data-hydrate="multiplechoice">
<xsl:apply-templates select="." mode="runestone-id-attribute"/>
<xsl:variable name="ncorrect" select="count(choices/choice[@correct = 'yes'])"/>
<xsl:attribute name="data-multipleanswers">
<xsl:choose>
<xsl:when test="choices/@multiple-correct = 'yes'">
<xsl:text>true</xsl:text>
</xsl:when>
<xsl:when test="choices/@multiple-correct = 'no'">
<xsl:text>false</xsl:text>
</xsl:when>
<xsl:when test="$ncorrect > 1">
<xsl:text>true</xsl:text>
</xsl:when>
<xsl:otherwise>
<xsl:text>false</xsl:text>
</xsl:otherwise>
</xsl:choose>
</xsl:attribute>
<!-- bare attribute, iff requested -->
<xsl:if test="choices/@randomize = 'yes'">
<xsl:attribute name="data-random"/>
</xsl:if>
<!-- overall statement, not per-choice -->
<div class="exercise-statement">
<xsl:apply-templates select="statement"/>
</div>
<ol class="runestone-static-choices" type="A">
<xsl:apply-templates select="choices/choice">
<xsl:with-param name="b-has-solution" select="$b-has-solution"/>
</xsl:apply-templates>
</ol>
</div>
</xsl:template>

<xsl:template match="choices/choice">
<xsl:param name="the-id"/>

<!-- id for each "choice" -->
<!-- with common base, then a, b, c suffix -->
<!-- Used *twice* on adjacent "li"? -->
<xsl:variable name="choice-id">
<xsl:value-of select="$the-id"/>
<xsl:text>_opt_</xsl:text>
<!-- will count preceding "choice" only -->
<xsl:number format="a"/>
</xsl:variable>
<li data-component="answer">
<xsl:attribute name="id">
<xsl:value-of select="$choice-id"/>
</xsl:attribute>
<!-- mark correct answers (empty attribute value) -->
<xsl:param name="b-has-solution" select="false()"/>

<li>
<!-- mark correct answers, consumed by hydrateMultipleChoice() -->
<xsl:if test="@correct = 'yes'">
<xsl:attribute name="data-correct"/>
<xsl:attribute name="data-correct">yes</xsl:attribute>
</xsl:if>
<!-- per-choice statement -->
<xsl:apply-templates select="statement"/>
</li>
<li data-component="feedback">
<xsl:attribute name="id">
<xsl:value-of select="$choice-id"/>
</xsl:attribute>
<!-- per-choice explanation -->
<xsl:apply-templates select="feedback"/>
<!-- per-choice explanation: visible here only when solutions are -->
<!-- visible in this exercise's context, matching every other -->
<!-- exercise type; otherwise present but hidden, for JS to move -->
<!-- into the hydrated widget's feedback reveal. -->
<div class="choice-feedback">
<xsl:if test="not($b-has-solution)">
<xsl:attribute name="hidden"/>
</xsl:if>
<xsl:apply-templates select="feedback"/>
</div>
</li>
</xsl:template>

Expand Down