Latest build deployed.
This commit is contained in:
File diff suppressed because one or more lines are too long
+2
File diff suppressed because one or more lines are too long
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+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"]
|
||||
}
|
||||
});
|
||||
@@ -17,6 +17,14 @@ const stackstring = {
|
||||
"api_correct":"Correct answers"
|
||||
};
|
||||
|
||||
|
||||
function wrap_math(content) {
|
||||
// Wrap instances of \[ ... \] and \( ... \) into the tags configured to be processed by MathJax
|
||||
// Here we make sure that the backslashes are not escaped like \\[
|
||||
content = content.replace(/(?<!\\)(\\\(.*?(?<!\\)\\\))/g, "<span class=\"process-math\">$1</span>");
|
||||
return content.replace(/(?<!\\)(\\\[.*?(?<!\\)\\\])/g, "<span class=\"process-math\">$1</span>");
|
||||
}
|
||||
|
||||
// Create data for call to API.
|
||||
async function collectData(qfile, qname, qprefix) {
|
||||
let res = "";
|
||||
@@ -95,6 +103,7 @@ function send(qfile, qname, qprefix) {
|
||||
// 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})`);
|
||||
question = wrap_math(question);
|
||||
if (input.samplesolutionrender && name !== 'remember') {
|
||||
// Display render of answer and matching user input to produce the answer.
|
||||
correctAnswers += `<p>
|
||||
@@ -102,7 +111,7 @@ function send(qfile, qname, qprefix) {
|
||||
${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 += `<span class='correct-answer'>${wrap_math(solution)}</span>`;
|
||||
}
|
||||
}
|
||||
correctAnswers += '.</p>';
|
||||
@@ -147,7 +156,7 @@ function send(qfile, qname, qprefix) {
|
||||
let sampleText = json.questionsamplesolutiontext;
|
||||
if (sampleText) {
|
||||
sampleText = replaceFeedbackTags(sampleText,qprefix);
|
||||
document.getElementById(`${qprefix+'generalfeedback'}`).innerHTML = sampleText;
|
||||
document.getElementById(`${qprefix+'generalfeedback'}`).innerHTML = wrap_math(sampleText);
|
||||
document.getElementById(`${qprefix+'stackapi_generalfeedback'}`).style.display = 'block';
|
||||
} else {
|
||||
// If the question is updated, there may no longer be general feedback.
|
||||
@@ -165,7 +174,7 @@ function send(qfile, qname, qprefix) {
|
||||
document.getElementById(`${qprefix+'stackapi_correct'}`).style.display = 'none';
|
||||
|
||||
createIframes(json.iframes);
|
||||
MathJax.Hub.Queue(["Typeset", MathJax.Hub]);
|
||||
MathJax.typeset();
|
||||
}
|
||||
catch(e) {
|
||||
console.log(e);
|
||||
@@ -207,14 +216,14 @@ function validate(element, qfile, qname, qprefix) {
|
||||
renameIframeHolders();
|
||||
const validationHTML = json.validation;
|
||||
const element = document.getElementsByName(`${qprefix+validationPrefix + answerName}`)[0];
|
||||
element.innerHTML = validationHTML;
|
||||
element.innerHTML = wrap_math(validationHTML);
|
||||
if (validationHTML) {
|
||||
element.classList.add('validation');
|
||||
} else {
|
||||
element.classList.remove('validation');
|
||||
}
|
||||
createIframes(json.iframes);
|
||||
MathJax.Hub.Queue(["Typeset", MathJax.Hub]);
|
||||
MathJax.typeset();
|
||||
}
|
||||
catch(e) {
|
||||
document.getElementById(`${qprefix+'errors'}`).innerText = http.responseText;
|
||||
@@ -274,7 +283,7 @@ function answer(qfile, qname, qprefix, seed) {
|
||||
json.specificfeedback = json.specificfeedback.replace(name, getPlotUrl(file));
|
||||
}
|
||||
json.specificfeedback = replaceFeedbackTags(json.specificfeedback,qprefix);
|
||||
specificFeedbackElement.innerHTML = json.specificfeedback;
|
||||
specificFeedbackElement.innerHTML = wrap_math(json.specificfeedback);
|
||||
specificFeedbackElement.classList.add('feedback');
|
||||
} else {
|
||||
specificFeedbackElement.classList.remove('feedback');
|
||||
@@ -292,7 +301,7 @@ function answer(qfile, qname, qprefix, seed) {
|
||||
${(json.scores[name] * json.scoreweights[name] * json.scoreweights.total).toFixed(2)}
|
||||
/ ${(json.scoreweights[name] * json.scoreweights.total).toFixed(2)}.</div>`;
|
||||
}
|
||||
element.innerHTML = fb;
|
||||
element.innerHTML = wrap_math(fb);
|
||||
// if (fb) {
|
||||
// element.classList.add('feedback');
|
||||
// } else {
|
||||
@@ -301,7 +310,7 @@ function answer(qfile, qname, qprefix, seed) {
|
||||
}
|
||||
}
|
||||
createIframes(json.iframes);
|
||||
MathJax.Hub.Queue(["Typeset", MathJax.Hub]);
|
||||
MathJax.typeset();
|
||||
}
|
||||
catch(e) {
|
||||
console.log(e);
|
||||
|
||||
@@ -133,25 +133,6 @@ window.addEventListener("DOMContentLoaded",function(event) {
|
||||
}
|
||||
});
|
||||
|
||||
/* 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
|
||||
@@ -246,7 +227,30 @@ window.addEventListener("DOMContentLoaded", function(event) {
|
||||
expander.title = "Expand" + (itemType !== "" ? " " + itemType : "");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Do we have a hash in the URL? If so, we need to identify up to make sure
|
||||
// all parents of that item are expanded
|
||||
if(window.location.hash) {
|
||||
let hash = window.location.hash;
|
||||
// find the link in the TOC that has an href ending in this hash
|
||||
let hashLink = document.querySelector(`.ptx-toc a[href$="${hash}"]`);
|
||||
if(hashLink) {
|
||||
let parentTocItem = hashLink.closest(".toc-item");
|
||||
while(parentTocItem && !parentTocItem.classList.contains("contains-active")) {
|
||||
parentTocItem.classList.add("contains-active");
|
||||
let expander = parentTocItem.querySelector(".toc-expander");
|
||||
if(expander) {
|
||||
//make sure it is expanded
|
||||
if(!parentTocItem.classList.contains("expanded")) {
|
||||
toggleTOCItem(expander);
|
||||
}
|
||||
}
|
||||
parentTocItem = parentTocItem.parentElement.closest(".toc-item");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// This needs to be after the TOC's geometry is settled
|
||||
|
||||
@@ -15,13 +15,6 @@
|
||||
console.log("thisbrowser.userAgent", window.navigator.userAgent);
|
||||
*/
|
||||
|
||||
var minivers = "0";
|
||||
if (typeof miniversion !== 'undefined') {
|
||||
console.log("typeof miniversion", typeof miniversion, "dddd", typeof miniversion == 'undefined');
|
||||
minivers = miniversion.toString();
|
||||
}
|
||||
console.log(" minivers", minivers);
|
||||
|
||||
/* scrollbar width from https://stackoverflow.com/questions/13382516/getting-scroll-bar-width-using-javascript */
|
||||
function getScrollbarWidth() {
|
||||
var outer = document.createElement("div");
|
||||
@@ -289,22 +282,8 @@ function updateURLParameter(url, param, paramVal){
|
||||
return baseURL + "?" + newAdditionalURL + rows_txt;
|
||||
}
|
||||
|
||||
function WWiframeReseed(iframe, seed) {
|
||||
var this_problem = document.getElementsByName(iframe)[0];
|
||||
var this_problem_url = this_problem.src;
|
||||
if (seed === undefined){seed = Number(this_problem.getAttribute('data-seed')) + 80 + 84 + 88;}
|
||||
this_problem.setAttribute('data-seed', seed);
|
||||
this_problem_url = updateURLParameter(this_problem_url, "problemSeed", seed);
|
||||
this_problem.src = this_problem_url;
|
||||
}
|
||||
|
||||
function process_workspace() {
|
||||
console.log("processing workspace");
|
||||
// next does not work, because the cursor does back to the beginning
|
||||
// so: need to handle the cursor
|
||||
// the_text = document.activeElement.innerHTML;
|
||||
// the_text = the_text.replace(/(^|\s)\$([^\$]+)\$(\s|$|[.,!?;:])/g, "\1\\(\2\\)\3")
|
||||
// document.activeElement.innerHTML = the_text
|
||||
MathJax.typesetPromise();
|
||||
}
|
||||
/* for the GeoGebra calculator */
|
||||
@@ -352,17 +331,8 @@ window.addEventListener("load",function(event) {
|
||||
ggbscript.id = "create_ggb_calc";
|
||||
ggbscript.innerHTML = "ggbApp.inject('geogebra-calculator')";
|
||||
document.body.appendChild(ggbscript);
|
||||
// setTimeout( function() {
|
||||
// $("#calculator-toggle").focus();
|
||||
// var inputfield = $("input.gwt-SuggestBox.TextField")[0];
|
||||
// console.log("inputfield", inputfield);
|
||||
// inputfield.focus();
|
||||
// }, 4000);
|
||||
} else {
|
||||
pretext_geogebra_calculator_onload();
|
||||
// var inputfield = $("input.gwt-SuggestBox.TextField")[0];
|
||||
// console.log("inputfield", inputfield);
|
||||
// inputfield.focus();
|
||||
}
|
||||
} else {
|
||||
$('#calculator-container').css('display', 'none');
|
||||
@@ -374,16 +344,6 @@ window.addEventListener("load",function(event) {
|
||||
});
|
||||
|
||||
|
||||
/*
|
||||
window.addEventListener("load",function(event) {
|
||||
// setTimeout( function() {
|
||||
console.log("changein play color");
|
||||
$('figure > div.onclick > svg > path').attr('fill', '#0000aa');
|
||||
$('path').attr('fill', '#0000aa')
|
||||
// }, 5000)
|
||||
});
|
||||
*/
|
||||
|
||||
window.addEventListener("load",function(event) {
|
||||
document.onkeyup = function(event)
|
||||
{
|
||||
@@ -402,14 +362,6 @@ window.addEventListener("load",function(event) {
|
||||
console.log("staying in the sage cell", parent_sage_cell, document.activeElement)
|
||||
just_hit_escape = true;
|
||||
setTimeout(function(){ just_hit_escape = false }, 1000);
|
||||
// console.log("parent_sage_cell", parent_sage_cell);
|
||||
// if ($(parent_sage_cell).hasClass('sagecell_editor')) {
|
||||
// console.log("I am trapped in a sage cell", $(document.activeElement).closest(".sagecell_editor"));
|
||||
// console.log($(document.activeElement));
|
||||
// var this_sage_cell = $(document.activeElement).closest(".sagecell_editor");
|
||||
// this_sage_cell.next().focus;
|
||||
// }
|
||||
// else
|
||||
} else
|
||||
if(knowl_focus_stack.length > 0 ) {
|
||||
most_recently_opened = knowl_focus_stack.pop();
|
||||
@@ -426,144 +378,6 @@ window.addEventListener("load",function(event) {
|
||||
},
|
||||
false);
|
||||
|
||||
// a hack for hosted tracking
|
||||
|
||||
window.addEventListener("load",function(event) {
|
||||
if($('body').attr('id') == "judson-AATA") {
|
||||
console.log(" found AATA");
|
||||
console.log(" looking for id");
|
||||
if (typeof eBookConfig !== 'undefined') {
|
||||
if(eBookConfig['username']) {
|
||||
aa_id = "run" + eBookConfig['username'];
|
||||
ut_id = eBookConfig['username'];
|
||||
console.log(" done looking for id", ut_id);
|
||||
var newscript = document.createElement('script');
|
||||
newscript.type = 'text/javascript';
|
||||
newscript.async = true;
|
||||
newscript.src = 'https://pretextbook.org/js/' + '0.13' + '/' + 'trails' + '.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);
|
||||
trail = true;
|
||||
console.log(" done adding script");
|
||||
} else {
|
||||
console.log(" did not find username");
|
||||
}
|
||||
} else {
|
||||
console.log(" did not find eBookConfig")
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function loadResource(type, file) {
|
||||
/* type should be js or css */
|
||||
if (typeof js_version === 'undefined') { js_version = '0.2' }
|
||||
if (typeof css_version === 'undefined') { css_version = '0.6' }
|
||||
var newresource, allresources, s;
|
||||
var linktype = "script";
|
||||
if (type == "css") { linktype = "link" }
|
||||
newresource = document.createElement(linktype);
|
||||
|
||||
if (type == "css") {
|
||||
newresource.type = 'text/css';
|
||||
newresource.rel = 'stylesheet';
|
||||
newresource.href = 'https://pretextbook.org/css/' + css_version + '/' + file + '.css';
|
||||
newresource.href += '?minivers=' + minivers;
|
||||
} else if (type == "js") {
|
||||
newresource.type = 'text/javascript';
|
||||
// newscript.async = true;
|
||||
newresource.src = 'https://pretextbook.org/js/' + js_version + '/' + file + '.js';
|
||||
newresource.src += '?minivers=' + minivers;
|
||||
} else {
|
||||
console.log("unknown resource type", type, "for", file);
|
||||
return
|
||||
}
|
||||
|
||||
allresources = document.getElementsByTagName(linktype);
|
||||
s = allresources[allresources.length - 1];
|
||||
console.log('s',s);
|
||||
console.log("adding a resource", newresource);
|
||||
s.parentNode.insertBefore(newresource, s.nextSibling);
|
||||
}
|
||||
|
||||
|
||||
window.addEventListener("load",function(event) {
|
||||
if(false && $('body').attr('id') == "pretext-SA") {
|
||||
console.log(" found DMOI");
|
||||
if (typeof uname === "undefined") { uname = "" }
|
||||
console.log("aaaa", uname, " uname");
|
||||
if(uname == "editor") {
|
||||
loadResource('js', 'edit');
|
||||
} else {
|
||||
console.log("not enabling editing")
|
||||
}
|
||||
/* } else if ($('body').attr('id') == "pugetsound-SW") { */
|
||||
} else if (false && window.location.href.includes("soundwriting.pugetsound")) {
|
||||
/* a bunch of temporary exploration for a Sound Writing survey */
|
||||
console.log("please take our survey");
|
||||
console.log(window.location.href);
|
||||
console.log(window.location.href.includes("soundwriting.pugetsound"));
|
||||
|
||||
loadResource("js", "login");
|
||||
loadResource("css", "features");
|
||||
setTimeout( loadResource("js", "survey"), 1000); /* I know: sloppy */
|
||||
|
||||
// } else if ((typeof online_editable !== 'undefined') && online_editable) {
|
||||
} else if (false && $('body').attr('id') == "pretext-SA") {
|
||||
loadResource('css', 'features');
|
||||
loadResource('js', 'login')
|
||||
loadResource('js', 'edit');
|
||||
} else {
|
||||
var this_source_txt;
|
||||
var source_url = window.location.href;
|
||||
source_url = source_url.replace(/(#|\?).*/, "");
|
||||
source_url = source_url.replace(/html$/, "ptx");
|
||||
if (typeof sourceeditable !== 'undefined') {
|
||||
fetch(source_url).then(
|
||||
function(u){ return u.text();}
|
||||
).then(
|
||||
function(text){
|
||||
this_source_txt = text;
|
||||
if (this_source_txt.includes("404 Not")) {
|
||||
console.log("Editing not enabled: source unavailable")
|
||||
} else {
|
||||
loadResource('css', 'features');
|
||||
loadResource('css', 'edit');
|
||||
loadResource('js', 'login')
|
||||
loadResource('js', 'edit');
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
console.log("Source file unavailable: editing not possible")
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// this is to open every knowl on a page
|
||||
// (this code is not actually used anywhere)
|
||||
window.addEventListener("load",function(event) {
|
||||
if($('body').hasClass("braillesample")) {
|
||||
var knowl_id_counterX = 0;
|
||||
console.log(" found braillesample");
|
||||
var all_knowls = $('[data-knowl]');
|
||||
console.log("found", all_knowls.length, "knowls");
|
||||
console.log("which are", all_knowls);
|
||||
for (var j=1; j < all_knowls.length; ++j) {
|
||||
console.log(j, "un-knowling", all_knowls[j]);
|
||||
console.log("attr", $(all_knowls[j]).attr("data-knowl"));
|
||||
$knowl = $(all_knowls[j]);
|
||||
if(!$knowl.attr("data-knowl-uid")) {
|
||||
$knowl.attr("data-knowl-uid", knowl_id_counterX);
|
||||
knowl_id_counterX++;
|
||||
}
|
||||
knowl_click_handler($knowl);
|
||||
// knowl_click_handler($(all_knowls[j]))
|
||||
}
|
||||
}});
|
||||
|
||||
// when the anchor is a knowl, open it
|
||||
window.addEventListener("load",function(event) {
|
||||
@@ -594,13 +408,6 @@ window.addEventListener("load",function(event) {
|
||||
});
|
||||
|
||||
|
||||
// What purpose does this serve?
|
||||
function urlattribute() {
|
||||
var this_urlstub = window.location.hostname;
|
||||
document.body.setAttribute("data-urlstub", this_urlstub);
|
||||
}
|
||||
|
||||
|
||||
// The new method for creating pages and adjusting workspace //
|
||||
|
||||
// This is used multiple places to set height of workspace divs to their author-provided heights
|
||||
@@ -614,6 +421,7 @@ function setInitialWorkspaceHeights() {
|
||||
|
||||
// If a printout (worksheet or handout) includes authored pages, we only need to put content before the first page and after the last page into the first and last pages, respectively.
|
||||
function adjustPrintoutPages() {
|
||||
console.log("*** Adjusting printout pages.");
|
||||
const printout = document.querySelector('section.worksheet, section.handout');
|
||||
if (!printout) {
|
||||
console.warn("No printout found, exiting adjustPrintoutPages.");
|
||||
@@ -647,6 +455,7 @@ function adjustPrintoutPages() {
|
||||
|
||||
// This is the main function we will call then a printout does not come from the XSL with pages already defined (for now, the XSL will keep the <page> behavior as an option).
|
||||
function createPrintoutPages(margins) {
|
||||
console.log("*** Creating printout pages with margins:", margins);
|
||||
|
||||
// Assumptions: needs to work for both letter (8.5in x 11in) and a4 (210mm x 297mm) paper sizes. We will work in pixels (96/in): those are 816px x 1056px and 794px x 1122.5px respectively (1 inch = 96 px, 1 cm = 37.8 px). We assume that the printing interface of the browser will do the right thing with these.
|
||||
|
||||
@@ -679,16 +488,6 @@ function createPrintoutPages(margins) {
|
||||
printout.insertBefore(tasks[i], child.nextSibling);
|
||||
}
|
||||
// Skipping separate treatment of exercisegroups for now.
|
||||
//} else if (child.classList.contains('exercisegroup')) {
|
||||
// for (const subChild of child.children) {
|
||||
// if (subChild.classList.contains('exercisegroup-exercises')){
|
||||
// for (const row of subChild.children){
|
||||
// rows.push(row);
|
||||
// }
|
||||
// } else {
|
||||
// rows.push(child);
|
||||
// }
|
||||
// }
|
||||
} else {
|
||||
rows.push(child);
|
||||
}
|
||||
@@ -743,10 +542,90 @@ function createPrintoutPages(margins) {
|
||||
}
|
||||
}
|
||||
|
||||
// Add headers and footers to all pages in a printout. Start with this set to be hidden by default; a toggle later will show/hide them.
|
||||
function addHeadersAndFootersToPrintout() {
|
||||
const printout = document.querySelector('section.worksheet, section.handout');
|
||||
if (!printout) {
|
||||
console.warn("No printout found, exiting addHeadersAndFootersToPrintout.");
|
||||
return;
|
||||
}
|
||||
const pages = printout.querySelectorAll('.onepage');
|
||||
// Loop through pages and add header and footer divs
|
||||
pages.forEach((page, index) => {
|
||||
const isFirstPage = index === 0;
|
||||
// Add header
|
||||
const headerDiv = document.createElement('div');
|
||||
headerDiv.classList.add(isFirstPage ? 'first-page-header' : 'running-header', 'hidden');
|
||||
headerDiv.innerHTML = `<div class="header-left" contenteditable="true"></div><div class="header-center" contenteditable="true"></div><div class="header-right" contenteditable="true"></div>`;
|
||||
page.insertBefore(headerDiv, page.firstChild);
|
||||
// Add footer
|
||||
const footerDiv = document.createElement('div');
|
||||
footerDiv.classList.add(isFirstPage ? 'first-page-footer' : 'running-footer', 'hidden');
|
||||
footerDiv.innerHTML = `<div class="footer-left" contenteditable="true"></div><div class="footer-center" contenteditable="true"></div><div class="footer-right" contenteditable="true"></div>`;
|
||||
page.appendChild(footerDiv);
|
||||
});
|
||||
// Add content based on local storage if available, otherwise from data-attributes on the printout
|
||||
const headerFooterKeys = ['header-first-left', 'header-first-center', 'header-first-right', 'footer-first-left', 'footer-first-center', 'footer-first-right', 'header-running-left', 'header-running-center', 'header-running-right', 'footer-running-left', 'footer-running-center', 'footer-running-right'];
|
||||
const headerFooterContent = {};
|
||||
headerFooterKeys.forEach(key => {
|
||||
headerFooterContent[key] = localStorage.getItem(key) || printout.getAttribute(`data-${key}`) || '';
|
||||
});
|
||||
// First page header and footer
|
||||
document.querySelector('.first-page-header').querySelector('.header-left').innerHTML = headerFooterContent['header-first-left'];
|
||||
document.querySelector('.first-page-header').querySelector('.header-center').innerHTML = headerFooterContent['header-first-center'];
|
||||
document.querySelector('.first-page-header').querySelector('.header-right').innerHTML = headerFooterContent['header-first-right'];
|
||||
document.querySelector('.first-page-footer').querySelector('.footer-left').innerHTML = headerFooterContent['footer-first-left'];
|
||||
document.querySelector('.first-page-footer').querySelector('.footer-center').innerHTML = headerFooterContent['footer-first-center'];
|
||||
document.querySelector('.first-page-footer').querySelector('.footer-right').innerHTML = headerFooterContent['footer-first-right'];
|
||||
// Running headers and footers
|
||||
document.querySelectorAll('.running-header').forEach(headerDiv => {
|
||||
headerDiv.querySelector('.header-left').innerHTML = headerFooterContent['header-running-left'];
|
||||
headerDiv.querySelector('.header-center').innerHTML = headerFooterContent['header-running-center'];
|
||||
headerDiv.querySelector('.header-right').innerHTML = headerFooterContent['header-running-right'];
|
||||
});
|
||||
document.querySelectorAll('.running-footer').forEach(footerDiv => {
|
||||
footerDiv.querySelector('.footer-left').innerHTML = headerFooterContent['footer-running-left'];
|
||||
footerDiv.querySelector('.footer-center').innerHTML = headerFooterContent['footer-running-center'];
|
||||
footerDiv.querySelector('.footer-right').innerHTML = headerFooterContent['footer-running-right'];
|
||||
});
|
||||
// Add event listeners to update local storage when content is edited
|
||||
headerFooterKeys.forEach(key => {
|
||||
const selectorMap = {
|
||||
'header-first-left': '.first-page-header .header-left',
|
||||
'header-first-center': '.first-page-header .header-center',
|
||||
'header-first-right': '.first-page-header .header-right',
|
||||
'footer-first-left': '.first-page-footer .footer-left',
|
||||
'footer-first-center': '.first-page-footer .footer-center',
|
||||
'footer-first-right': '.first-page-footer .footer-right',
|
||||
'header-running-left': '.running-header .header-left',
|
||||
'header-running-center': '.running-header .header-center',
|
||||
'header-running-right': '.running-header .header-right',
|
||||
'footer-running-left': '.running-footer .footer-left',
|
||||
'footer-running-center': '.running-footer .footer-center',
|
||||
'footer-running-right': '.running-footer .footer-right'
|
||||
};
|
||||
const elements = document.querySelectorAll(selectorMap[key]);
|
||||
elements.forEach(elem => {
|
||||
elem.addEventListener('input', () => {
|
||||
localStorage.setItem(key, elem.innerHTML);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// We look at each page and adjust the heights of the workspaces to fit it nicely into the page.
|
||||
// The width and height of the page will now depend on the letter or a4 setting.
|
||||
|
||||
// We look at each page and adjust the heights of the workspaces to fit it nicely into the page.
|
||||
// The width and height of the page will now depend on the letter or a4 setting.
|
||||
function adjustWorkspaceToFitPage({paperSize, margins}) {
|
||||
console.log("*** Adjusting workspace to fit page size:", paperSize, "with margins:", margins);
|
||||
|
||||
// Toggle off workspace highlight if it is on, so it doesn't interfere with resizing
|
||||
const highlightWorkspaceCheckbox = document.getElementById("highlight-workspace-checkbox");
|
||||
const wasHighlighted = highlightWorkspaceCheckbox && highlightWorkspaceCheckbox.checked;
|
||||
if (wasHighlighted) {
|
||||
toggleWorkspaceHighlight(false);
|
||||
}
|
||||
|
||||
let paperWidth, paperHeight;
|
||||
if (paperSize === 'a4' || document.body.classList.contains('a4')) {
|
||||
console.log("Setting page size to A4");
|
||||
@@ -796,6 +675,11 @@ function adjustWorkspaceToFitPage({paperSize, margins}) {
|
||||
page.style.width = "";
|
||||
});
|
||||
console.log("Set page sizes to content area of paper size.");
|
||||
|
||||
// Reset the highlight workspace checkbox state
|
||||
if (wasHighlighted) {
|
||||
toggleWorkspaceHighlight(true);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions for calculating heights and workspace sizes
|
||||
@@ -855,6 +739,7 @@ function getElemWorkspaceHeight(elem) {
|
||||
|
||||
// Functions for finding the optimal page breaks
|
||||
function findPageBreaks(rows, pageHeight) {
|
||||
console.log("*** Finding page breaks for", rows.length, "rows with page height:", pageHeight);
|
||||
// An array for the page breaks. The nth element will be the index of the last row on page n.
|
||||
let pageBreaks = [];
|
||||
// An array for the minimum cost possible for rows i to the end.
|
||||
@@ -901,7 +786,9 @@ function findPageBreaks(rows, pageHeight) {
|
||||
return pageBreaks;
|
||||
}
|
||||
|
||||
// Function to set CSS variables and @page rules for page geometry. This will be called whenever the paper size or margins change (in practice, only when page size changes, since margins are fixed for now).
|
||||
function setPageGeometryCSS({paperSize, margins}) {
|
||||
console.log("*** Setting page geometry CSS for paper size:", paperSize, "with margins:", margins);
|
||||
// Remove any existing geometry CSS to avoid duplicates
|
||||
const existingStyle = document.getElementById("page-geometry-css");
|
||||
if (existingStyle) {
|
||||
@@ -948,7 +835,6 @@ function toggleWorkspaceHighlight(isChecked) {
|
||||
original.classList.add('original-workspace');
|
||||
const originalHeight = workspace.getAttribute('data-space') || '0px';
|
||||
original.setAttribute('title', 'Author-specified workspace height (' + originalHeight + ')');
|
||||
console.log("setting original workspace height for", workspace);
|
||||
// Use the data-space attribute for height of original workspace
|
||||
original.style.height = originalHeight;
|
||||
// insert original div before the workspace content
|
||||
@@ -964,55 +850,24 @@ function toggleWorkspaceHighlight(isChecked) {
|
||||
}
|
||||
} else {
|
||||
document.body.classList.remove("highlight-workspace");
|
||||
// Remove the original workspace divs. We don't want to keep these in, as they interfere with changing page sizes and workspace heights.
|
||||
document.querySelectorAll('.workspace-container').forEach(container => {
|
||||
const workspace = container.querySelector('.workspace');
|
||||
// Move the workspace out of the container
|
||||
container.parentNode.insertBefore(workspace, container);
|
||||
// Remove the container
|
||||
container.remove();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Printout print preview and page setup
|
||||
window.addEventListener("load",function(event) {
|
||||
// We condition on the existence of the papersize radio buttons, which only appear in the printout print preview.
|
||||
if (document.querySelector('input[name="papersize"]')) {
|
||||
// First, get the margins for pages to be passed around as needed.
|
||||
const marginList = document.querySelector('section.worksheet, section.handout').getAttribute('data-margins').split(' ');
|
||||
// Convert margin values to pixels if they are not already numbers
|
||||
function toPixels(value) {
|
||||
if (typeof value === "number") return value;
|
||||
if (typeof value !== "string") return 0;
|
||||
value = value.trim();
|
||||
if (value.endsWith("px")) {
|
||||
return parseFloat(value);
|
||||
} else if (value.endsWith("in")) {
|
||||
return Math.floor(parseFloat(value) * 96);
|
||||
} else if (value.endsWith("cm")) {
|
||||
return Math.floor(parseFloat(value) * 37.8);
|
||||
} else if (value.endsWith("mm")) {
|
||||
return Math.floor(parseFloat(value) * 3.78);
|
||||
} else if (value.endsWith("pt")) {
|
||||
return Math.floor(parseFloat(value) * (96 / 72));
|
||||
} else {
|
||||
// fallback: try to parse as px
|
||||
return parseFloat(value) || 0;
|
||||
}
|
||||
}
|
||||
const margins = {
|
||||
top: toPixels(marginList[0] || "0.75in"), // Default to 0.75in if not specified
|
||||
right: toPixels(marginList[1] || "0.75in"),
|
||||
bottom: toPixels(marginList[2] || "0.75in"),
|
||||
left: toPixels(marginList[3] || "0.75in")
|
||||
}
|
||||
// Get the papersize from localStorage or set it based on user's geographic region
|
||||
function getPaperSize() {
|
||||
let paperSize = localStorage.getItem("papersize");
|
||||
if (paperSize) {
|
||||
const radio = document.querySelector(`input[name="papersize"][value="${paperSize}"]`);
|
||||
if (radio) {
|
||||
radio.checked = true;
|
||||
}
|
||||
// Set the papersize class on body
|
||||
document.body.classList.remove("a4", "letter");
|
||||
document.body.classList.add(paperSize);
|
||||
setPageGeometryCSS({paperSize: paperSize, margins: margins});
|
||||
return paperSize;
|
||||
} else {
|
||||
// Try to set papersize based on user's geographic region
|
||||
// Default to 'letter' for North and South America, 'a4' elsewhere
|
||||
// Try to set papersize based on user's geographic region
|
||||
// Default to 'letter' for North and South America, 'a4' elsewhere
|
||||
try {
|
||||
fetch('https://ipapi.co/json/')
|
||||
.then(response => response.json())
|
||||
@@ -1037,69 +892,231 @@ window.addEventListener("load",function(event) {
|
||||
const radio = document.querySelector(`input[name="papersize"][value="letter"]`);
|
||||
if (radio) radio.checked = true;
|
||||
}
|
||||
//NB: the default papersize is set to 'letter' in the body class list.
|
||||
}
|
||||
const papersizeRadios = document.querySelectorAll('input[name="papersize"]');
|
||||
papersizeRadios.forEach(radio => {
|
||||
radio.addEventListener('change', function() {
|
||||
if (this.checked) {
|
||||
document.body.classList.remove("a4", "letter");
|
||||
document.body.classList.add(this.value);
|
||||
localStorage.setItem("papersize", this.value);
|
||||
console.log("Setting papersize to", this.value);
|
||||
return paperSize || "letter";
|
||||
}
|
||||
|
||||
// If the "highlight workspace" checkbox was already checked, then we should restart the process by reloading the page. Specifically, we run into issues when there are .workspace-container divs already present.
|
||||
if (document.querySelector(".workspace-container")) {
|
||||
console.log("Reloading page to apply new papersize with workspace highlight enabled.");
|
||||
window.location.reload();
|
||||
return;
|
||||
} else {
|
||||
// Otherwise, we can just adjust the workspace heights to fit the new paper size.
|
||||
console.log("Adjusting workspace heights to fit new papersize.");
|
||||
adjustWorkspaceToFitPage({paperSize: this.value, margins: margins});
|
||||
setPageGeometryCSS({paperSize: this.value, margins: margins});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
// 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) {
|
||||
|
||||
// Open all details elements (knowls) on the page
|
||||
var born_hidden_knowls = document.querySelectorAll('details');
|
||||
console.log("born_hidden_knowls", born_hidden_knowls);
|
||||
born_hidden_knowls.forEach(function(detail) {
|
||||
detail.open = true;
|
||||
});
|
||||
// If the printout has authored pages, there will be at least one .onepage element.
|
||||
if (document.querySelector('.onepage')) {
|
||||
adjustPrintoutPages();
|
||||
/* not the right way: need to figure out what this needs to wait for */
|
||||
//window.setTimeout(adjustPrintoutPages, 1000);
|
||||
} else {
|
||||
createPrintoutPages(margins);
|
||||
}
|
||||
// After pages are set up, we adjust the workspace heights to fit the page (based on the paper size).
|
||||
adjustWorkspaceToFitPage({paperSize: paperSize, margins: margins});
|
||||
|
||||
console.log("finished adjusting workspace");
|
||||
|
||||
|
||||
// Get the 'highlight workspace' checkbox state from localStorage or set it to false by default
|
||||
const highlightWorkspaceCheckbox = document.getElementById("highlight-workspace-checkbox");
|
||||
if (highlightWorkspaceCheckbox) {
|
||||
highlightWorkspaceCheckbox.checked = localStorage.getItem("highlightWorkspace") === "true";
|
||||
highlightWorkspaceCheckbox.addEventListener("change", function() {
|
||||
localStorage.setItem("highlightWorkspace", this.checked);
|
||||
toggleWorkspaceHighlight(this.checked);
|
||||
// Switch to print-worksheet.css for print preview
|
||||
const themeStylesheetLink = document.querySelector('link[rel="stylesheet"][href*="theme"]');
|
||||
// get the href of the theme stylesheet link
|
||||
const themeStylesheetHref = themeStylesheetLink ? themeStylesheetLink.getAttribute('href') : null;
|
||||
if (themeStylesheetHref) {
|
||||
// replace 'theme.css' with 'print-worksheet.css' in the href
|
||||
const printStylesheetHref = themeStylesheetHref.replace(/theme.*\.css/, 'print-worksheet.css');
|
||||
// update the href of the theme stylesheet link
|
||||
themeStylesheetLink.setAttribute('href', printStylesheetHref);
|
||||
// Wait for the new stylesheet to load. This is important to ensure the styles are applied before the calling function tries to compute workspace sizes.
|
||||
await new Promise((resolve) => {
|
||||
themeStylesheetLink.addEventListener('load', resolve, { once: true });
|
||||
});
|
||||
// Initial toggle to apply the highlight class if checked
|
||||
toggleWorkspaceHighlight(highlightWorkspaceCheckbox.checked);
|
||||
}
|
||||
|
||||
// Find the section with this ID
|
||||
const printableSection = document.getElementById(printableSectionID);
|
||||
if (!printableSection) {
|
||||
console.error("No section found with ID:", printableSectionID);
|
||||
return;
|
||||
}
|
||||
// Remove any existing sections from .ptx-content and add only the printable section
|
||||
const ptxContent = document.querySelector('.ptx-content');
|
||||
const existingSections = ptxContent.querySelectorAll(':scope > section');
|
||||
existingSections.forEach(sec => ptxContent.removeChild(sec));
|
||||
ptxContent.appendChild(printableSection);
|
||||
}
|
||||
|
||||
// Function to redo solutions details to divs with summary as title
|
||||
function rewriteSolutions() {
|
||||
var born_hidden_knowls = document.querySelectorAll('.worksheet details, .handout details');
|
||||
born_hidden_knowls.forEach(function(detail) {
|
||||
const summary = detail.querySelector('summary');
|
||||
const content = detail.innerHTML.replace(summary.outerHTML, '');
|
||||
const div = document.createElement('div');
|
||||
div.classList = detail.classList;
|
||||
if (summary) {
|
||||
const title = document.createElement('h5');
|
||||
title.innerHTML = summary.innerHTML;
|
||||
div.appendChild(title);
|
||||
}
|
||||
const body = document.createElement('div');
|
||||
body.innerHTML = content;
|
||||
div.appendChild(body);
|
||||
detail.parentNode.replaceChild(div, detail);
|
||||
});
|
||||
}
|
||||
|
||||
// Not sure why this is here:
|
||||
window.setTimeout(urlattribute, 1500);
|
||||
}
|
||||
// Utility to convert various CSS length units to pixels
|
||||
function toPixels(value) {
|
||||
if (typeof value === "number") return value;
|
||||
if (typeof value !== "string") return 0;
|
||||
value = value.trim();
|
||||
if (value.endsWith("px")) {
|
||||
return parseFloat(value);
|
||||
} else if (value.endsWith("in")) {
|
||||
return Math.floor(parseFloat(value) * 96);
|
||||
} else if (value.endsWith("cm")) {
|
||||
return Math.floor(parseFloat(value) * 37.8);
|
||||
} else if (value.endsWith("mm")) {
|
||||
return Math.floor(parseFloat(value) * 3.78);
|
||||
} else if (value.endsWith("pt")) {
|
||||
return Math.floor(parseFloat(value) * (96 / 72));
|
||||
} else {
|
||||
// fallback: try to parse as px
|
||||
return parseFloat(value) || 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Event listener for page load to handle print preview setup
|
||||
window.addEventListener("DOMContentLoaded", async function(event) {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
// 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");
|
||||
await loadPrintout(printableSectionID);
|
||||
|
||||
// First, get the margins for pages to be passed around as needed.
|
||||
const marginList = document.querySelector('section.worksheet, section.handout').getAttribute('data-margins').split(' ');
|
||||
// Convert margin values to pixels if they are not already numbers
|
||||
const margins = {
|
||||
top: toPixels(marginList[0] || "0.75in"), // Default to 0.75in if not specified
|
||||
right: toPixels(marginList[1] || "0.75in"),
|
||||
bottom: toPixels(marginList[2] || "0.75in"),
|
||||
left: toPixels(marginList[3] || "0.75in")
|
||||
}
|
||||
|
||||
// Transform all solutions details elements to divs with the summary as a title
|
||||
rewriteSolutions();
|
||||
|
||||
// Get the papersize from localStorage or set it based on user's geographic region. This will always return a value (defaulting to 'letter' if all else fails).
|
||||
let paperSize = getPaperSize();
|
||||
if (paperSize) {
|
||||
const radio = document.querySelector(`input[name="papersize"][value="${paperSize}"]`);
|
||||
if (radio) {
|
||||
radio.checked = true;
|
||||
}
|
||||
// Set the papersize class on body
|
||||
document.body.classList.remove("a4", "letter");
|
||||
document.body.classList.add(paperSize);
|
||||
setPageGeometryCSS({paperSize: paperSize, margins: margins});
|
||||
} else {
|
||||
console.warning("Bug: paperSize should always have a value here.");
|
||||
}
|
||||
// Add event listeners to the papersize radio buttons to handle changes
|
||||
const papersizeRadios = document.querySelectorAll('input[name="papersize"]');
|
||||
papersizeRadios.forEach(radio => {
|
||||
radio.addEventListener('change', function() {
|
||||
if (this.checked) {
|
||||
document.body.classList.remove("a4", "letter");
|
||||
document.body.classList.add(this.value);
|
||||
localStorage.setItem("papersize", this.value);
|
||||
setPageGeometryCSS({paperSize: this.value, margins: margins});
|
||||
adjustWorkspaceToFitPage({paperSize: this.value, margins: margins});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Add event listeners to the hide hints/answers/solutions checkboxes
|
||||
for (const solutionType of ["hint", "answer", "solution"]) {
|
||||
const checkbox = document.getElementById(`hide-${solutionType}-checkbox`);
|
||||
if (checkbox) {
|
||||
const storageKey = `hide-${solutionType}`;
|
||||
// by default, hide answer and solution divs
|
||||
if (solutionType === "answer" || solutionType === "solution") {
|
||||
if (!localStorage.getItem(storageKey)) {
|
||||
checkbox.checked = true;
|
||||
localStorage.setItem(storageKey, "true");
|
||||
}
|
||||
}
|
||||
// Now adjust based on local storage
|
||||
// set visibility based on current checkbox state
|
||||
checkbox.checked = localStorage.getItem(storageKey) === "true";
|
||||
document.querySelectorAll(`div.${solutionType}`).forEach(elem => {
|
||||
// add hidden to class list
|
||||
if (checkbox.checked) {
|
||||
elem.classList.add("hidden");
|
||||
} else {
|
||||
elem.classList.remove("hidden");
|
||||
}
|
||||
});
|
||||
// Add event listener to toggle visibility
|
||||
checkbox.addEventListener("change", function() {
|
||||
localStorage.setItem(storageKey, this.checked);
|
||||
// toggle visibility of solution divs
|
||||
document.querySelectorAll(`div.${solutionType}`).forEach(elem => {
|
||||
if (checkbox.checked) {
|
||||
elem.classList.add("hidden");
|
||||
} else {
|
||||
elem.classList.remove("hidden");
|
||||
}
|
||||
//adjustPrintoutPages();
|
||||
adjustWorkspaceToFitPage({paperSize: paperSize, margins: margins});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, with everything set up, we create or adjust the printout pages as needed.
|
||||
|
||||
// If the printout has authored pages, there will be at least one .onepage element.
|
||||
if (document.querySelector('.onepage')) {
|
||||
adjustPrintoutPages();
|
||||
} else {
|
||||
createPrintoutPages(margins);
|
||||
}
|
||||
|
||||
// Add headers and footers to all pages in the printout
|
||||
addHeadersAndFootersToPrintout();
|
||||
|
||||
// Add event listeners to the print header/footer checkboxes
|
||||
for (const hf of ["first-page-header", "running-header", "first-page-footer", "running-footer"]) {
|
||||
const checkbox = document.getElementById(`print-${hf}-checkbox`);
|
||||
if (checkbox) {
|
||||
// set visibility based on current checkbox state
|
||||
checkbox.checked = localStorage.getItem(`print-${hf}`) === "true";
|
||||
document.querySelectorAll(`.${hf}`).forEach(elem => {
|
||||
// add hidden to class list
|
||||
if (checkbox.checked) {
|
||||
elem.classList.remove("hidden");
|
||||
} else {
|
||||
elem.classList.add("hidden");
|
||||
}
|
||||
});
|
||||
// Add event listener to toggle visibility
|
||||
checkbox.addEventListener("change", function() {
|
||||
localStorage.setItem(`print-${hf}`, this.checked);
|
||||
// toggle visibility of header/footer divs
|
||||
document.querySelectorAll(`.${hf}`).forEach(elem => {
|
||||
if (checkbox.checked) {
|
||||
elem.classList.remove("hidden");
|
||||
} else {
|
||||
elem.classList.add("hidden");
|
||||
}
|
||||
adjustWorkspaceToFitPage({paperSize: paperSize, margins: margins});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// After pages are set up, we adjust the workspace heights to fit the page (based on the paper size).
|
||||
adjustWorkspaceToFitPage({paperSize: paperSize, margins: margins});
|
||||
|
||||
// Get the 'highlight workspace' checkbox state from localStorage or set it to false by default
|
||||
// NB we need to do this after the adjustment of workspace heights so that the additional original workspace divs don't throw off the calculations when the page is reloaded.
|
||||
const highlightWorkspaceCheckbox = document.getElementById("highlight-workspace-checkbox");
|
||||
if (highlightWorkspaceCheckbox) {
|
||||
highlightWorkspaceCheckbox.checked = localStorage.getItem("highlightWorkspace") === "true";
|
||||
highlightWorkspaceCheckbox.addEventListener("change", function() {
|
||||
localStorage.setItem("highlightWorkspace", this.checked);
|
||||
toggleWorkspaceHighlight(this.checked);
|
||||
});
|
||||
// Initial toggle to apply the highlight class if checked
|
||||
toggleWorkspaceHighlight(highlightWorkspaceCheckbox.checked);
|
||||
}
|
||||
|
||||
console.log("finished adjusting workspace");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user