Latest build deployed.

This commit is contained in:
2026-08-01 15:00:14 +00:00
parent e43fb0d0ff
commit 331f133dfe
1278 changed files with 19041 additions and 18362 deletions
+162 -217
View File
@@ -11,35 +11,17 @@
*******************************************************************************
*/
/*
console.log("thisbrowser.userAgent", window.navigator.userAgent);
*/
/* scrollbar width from https://stackoverflow.com/questions/13382516/getting-scroll-bar-width-using-javascript */
function getScrollbarWidth() {
var outer = document.createElement("div");
outer.style.visibility = "hidden";
outer.style.width = "100px";
outer.style.msOverflowStyle = "scrollbar"; // needed for WinJS apps
document.body.appendChild(outer);
var widthNoScroll = outer.offsetWidth;
// force scrollbars
outer.style.overflow = "scroll";
// add innerdiv
var inner = document.createElement("div");
inner.style.width = "100%";
outer.appendChild(inner);
var widthWithScroll = inner.offsetWidth;
// remove divs
outer.parentNode.removeChild(outer);
return widthNoScroll - widthWithScroll;
}
// stub for i18next to future-proof the code. We don't actually use it for
// anything right now, but it will be needed if we want to localize the
// accessibility search status messages.
window.i18next = window.i18next || {
t(key, params = {}) {
for (const param in params) {
key = key.replace(`{{${param}}}`, params[param]);
}
return key;
}
};
/*
copy permalink address to clipboard
@@ -122,15 +104,33 @@ window.addEventListener("load",function(event) {
/* click an image to magnify */
$('body').on('click','.image-box > img:not(.draw_on_me):not(.mag_popup), .sbspanel > img:not(.draw_on_me):not(.mag_popup), figure > img:not(.draw_on_me):not(.mag_popup), figure > div > img:not(.draw_on_me):not(.mag_popup)', function(){
var img_big = document.createElement('div');
img_big.setAttribute('style', 'background:#fff;');
const content_element = document.getElementById('ptx-content');
img_big.setAttribute('class', 'mag_popup_container');
img_big.innerHTML = '<img src="' + $(this).attr("src") + '" style="width:100%" class="mag_popup"/>';
img_big.innerHTML = `<img src="${$(this).attr("src")}" style="width:100%;" class="mag_popup"/>`;
// place_to_put_big_img = $(this).parents(".sbsrow, figure, li").last();
place_to_put_big_img = $(this).parents(".image-box, .sbsrow, figure, li, .cols2 article:nth-of-type(2n)").last();
// for .cols2, the even ones have to go inside the previous odd one
if (place_to_put_big_img.prop("tagName") == "ARTICLE") {
place_to_put_big_img = place_to_put_big_img.prev().children().first();
}
// find ancestor so that place_to_put_big_img's position is relative to that ancestor
var img_big_parent = place_to_put_big_img[0].parentElement;
while (img_big_parent.id !== "ptx-content") {
const computed_position = getComputedStyle(img_big_parent).position;
if (computed_position !== "static") {
break;
}
img_big_parent = img_big_parent.parentElement;
}
const content_element_computed_style = getComputedStyle(content_element);
const content_padding_left = parseFloat(content_element_computed_style.paddingLeft );
const content_padding_right = parseFloat(content_element_computed_style.paddingRight);
const img_big_offset = content_element.getBoundingClientRect().left - img_big_parent.getBoundingClientRect().left + content_padding_left;
const doc_width = content_element.offsetWidth - content_padding_left - content_padding_right;
img_big.setAttribute('style', `width:${doc_width.toString()}px; left:${img_big_offset.toString()}px;`);
$(img_big).insertBefore(place_to_put_big_img);
});
@@ -286,61 +286,67 @@ function process_workspace() {
console.log("processing workspace");
MathJax.typesetPromise();
}
/* for the GeoGebra calculator */
function pretext_geogebra_calculator_onload() {
$("#calculator-toggle").focus();
var inputfield = $("input.gwt-SuggestBox.TextField")[0];
console.log("inputfield", inputfield);
inputfield.focus();
}
window.addEventListener("load",function(event) {
const calcDialogElement = document.getElementById('ptx-calculator-container');
const calcButtonElement = document.getElementById('ptx-calculator-toggle');
if (!calcDialogElement || !calcButtonElement) {
return;
}
const calcDialog = new PTXDialog(calcDialogElement, calcButtonElement, {"kind": "non-modal"});
/* scrolling on GG plot should scale, not move browser body */
// var scrollWidth = 15; //currently correct for FF, Ch, and Saf, but would be better to calculate
var scrollWidth = getScrollbarWidth();
if ( (navigator.userAgent.match(/Mozilla/i) != null) ) {
// scrollWidth += 0.5
}
console.log("scrollWidth", scrollWidth);
calcoffsetR = 5;
calcoffsetB = 5;
$('body').on('mouseover','#geogebra-calculator canvas', function(){
$('body').css('overflow', 'hidden');
$('html').css('margin-right', '15px');
$('#calculator-container').css('right', (calcoffsetR+scrollWidth).toString() + 'px');
$('#calculator-container').css('bottom', (calcoffsetB+scrollWidth).toString() + 'px');
});
const focusCalcInput = function() {
const inputField = document.querySelector("#ptx-geogebra-calculator input.gwt-SuggestBox.TextField");
if (inputField) {
inputField.focus();
}
}
function initGeogebra() {
// Some paramaters are fixed here, others are set by publisher options in the HTML source
// and stored in ggbParams. Merge those here.
const fixedParams = {
showToolBar: true,
showAlgebraInput: true,
perspective: "G/A",
algebraInputPosition: "bottom",
appletOnLoad: focusCalcInput,
scaleContainerClass: "ptx-calculator-container",
allowUpscale: false,
autoHeight: false,
}
const generatedParams = (typeof ggbParams === "object" && ggbParams) ? ggbParams : {};
const params = {...generatedParams, ...fixedParams};
let applet = new GGBApplet(params, true);
applet.inject('ptx-geogebra-calculator');
return applet;
}
$('body').on('mouseout','#geogebra-calculator canvas', function(){
$('body').css('overflow', 'scroll')
$('html').css('margin-right', '0');
$('#calculator-container').css('right', calcoffsetR.toString() + 'px');
$('#calculator-container').css('bottom', calcoffsetB.toString() + 'px');
});
let applet;
calcButtonElement.addEventListener('click', function() {
if (calcDialog.dialog.open) {
let initialized = calcDialogElement.dataset.initialized || false;
if (!initialized) {
applet = initGeogebra();
calcDialogElement.dataset.initialized = true;
} else {
focusCalcInput();
}
}
});
$('body').on('click', '#calculator-toggle', function() {
if ($('#calculator-container').css('display') == 'none') {
$('#calculator-container').css('display', 'block');
$('#calculator-toggle').addClass('open');
$('#calculator-toggle').attr('title', 'Hide calculator');
$('#calculator-toggle').attr('aria-expanded', 'true');
create_calc_script = document.getElementById("create_ggb_calc");
if (!create_calc_script) {
var ggbscript = document.createElement("script");
ggbscript.id = "create_ggb_calc";
ggbscript.innerHTML = "ggbApp.inject('geogebra-calculator')";
document.body.appendChild(ggbscript);
} else {
pretext_geogebra_calculator_onload();
}
} else {
$('#calculator-container').css('display', 'none');
$('#calculator-toggle').removeClass('open');
$('#calculator-toggle').attr('title', 'Show calculator');
$('#calculator-toggle').attr('aria-expanded', 'false');
}
});
//add resize observer for dialog
const resizeObserver = new ResizeObserver(entries => {
for (let entry of entries) {
if (entry.target === calcDialogElement && applet && applet.getAppletObject()) {
const width = entry.contentRect.width;
const height = entry.contentRect.height;
const topBarHeight = calcDialogElement.querySelector('.ptx-dialog-topbar').clientHeight || 0;
applet.getAppletObject().setSize(width, height - topBarHeight);
applet.getAppletObject().recalculateEnvironments();
}
}
});
resizeObserver.observe(calcDialogElement);
});
@@ -410,6 +416,42 @@ window.addEventListener("load",function(event) {
// The new method for creating pages and adjusting workspace //
// Unwrap section.paragraphs containers so their children flow directly
// into the parent, enabling CSS page breaks between the inner elements.
function flattenParagraphsSections(printout) {
const paragraphsSections = printout.querySelectorAll('section.paragraphs');
paragraphsSections.forEach(section => {
const parent = section.parentNode;
// Move all children out of the section wrapper and into the parent
while (section.firstChild) {
parent.insertBefore(section.firstChild, section);
}
// Remove the now-empty section wrapper
parent.removeChild(section);
});
}
// Wait for all images inside a container to finish loading.
// Returns a promise that resolves when every <img> has loaded (or on timeout).
function waitForImages(container, timeoutMs = 5000) {
const images = container.querySelectorAll('img');
const promises = [];
for (const img of images) {
if (!img.complete) {
promises.push(new Promise(resolve => {
img.addEventListener('load', resolve, { once: true });
img.addEventListener('error', resolve, { once: true });
}));
}
}
if (promises.length === 0) return Promise.resolve();
// Race all image loads against a timeout so broken images don't block forever
return Promise.race([
Promise.all(promises),
new Promise(resolve => setTimeout(resolve, timeoutMs))
]);
}
// This is used multiple places to set height of workspace divs to their author-provided heights
function setInitialWorkspaceHeights() {
const workspaces = document.querySelectorAll('.workspace');
@@ -482,7 +524,18 @@ function createPrintoutPages(margins) {
} else if (child.querySelector('.task')) {
// Keep the child as a block, but put each task after the first one as its own row:
rows.push(child);
const tasks = child.querySelectorAll('.task');
const tasks = child.querySelectorAll('.task, .conclusion');
//Determine how many levels of nesting each task has. If parent is an .exercise, leave alone. If parent is a .task, add .subtask class. If grandparent is .task, add .subsubtask to it so it can be indented by css:
for (let i = 0; i < tasks.length; i++) {
let parent = tasks[i].parentElement;
let grandparent = parent.parentElement;
if (grandparent.classList.contains('task')) {
tasks[i].classList.add('subsubtask');
} else if (parent.classList.contains('task')) {
tasks[i].classList.add('subtask');
}
}
for (let i = tasks.length-1; i > 0; i--) {
// Move the task out of the original child and place it directly after it in the printout. We do this in reverse order so when every task is moved, they return to the original order. They will then be added to the rows list as their own blocks.
printout.insertBefore(tasks[i], child.nextSibling);
@@ -740,10 +793,10 @@ 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.
// An array for the page breaks. The nth element will be the index of the first row on page n+1.
let pageBreaks = [];
// An array for the minimum cost possible for rows i to the end.
let minCost = Array(rows.length).fill(Infinity);
let minCost = Array(rows.length + 1).fill(Infinity);
minCost[rows.length] = 0; // No cost for no rows
// An array to keep track of the next row to start a new page after i in minCost.
let nextPageBreak = Array(rows.length).fill(-1);
@@ -1059,6 +1112,17 @@ window.addEventListener("DOMContentLoaded", async function(event) {
// Finally, with everything set up, we create or adjust the printout pages as needed.
// Flatten paragraphs sections so page breaks can occur inside them.
const printoutSection = document.querySelector('section.worksheet, section.handout');
if (printoutSection) {
flattenParagraphsSections(printoutSection);
}
// Wait for all images to load so height measurements are accurate.
if (printoutSection) {
await waitForImages(printoutSection);
}
// If the printout has authored pages, there will be at least one .onepage element.
if (document.querySelector('.onepage')) {
adjustPrintoutPages();
@@ -1119,144 +1183,16 @@ window.addEventListener("DOMContentLoaded", async function(event) {
}
});
//-----------------------------------------------------------------
// Dark/Light mode swiching
function isDarkMode() {
if (document.documentElement.dataset.darkmode === 'disabled')
return false;
const currentTheme = localStorage.getItem("theme");
if (currentTheme === "dark")
return true;
else if (currentTheme === "light")
return false;
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
}
function setDarkMode(isDark) {
if(document.documentElement.dataset.darkmode === 'disabled')
return;
const parentHtml = document.documentElement;
const iframes = document.querySelectorAll("iframe[data-dark-mode-enabled]");
// Update the parent document
if (isDark) {
parentHtml.classList.add("dark-mode");
} else {
parentHtml.classList.remove("dark-mode");
}
// Sync each iframe's <html> class with the parent
for (const iframe of iframes) {
try {
const iframeHtml = iframe.contentWindow.document.documentElement;
if (isDark) {
iframeHtml.classList.add("dark-mode")
} else {
iframeHtml.classList.remove("dark-mode")
}
} catch (err) {
console.warn("Dark mode sync to iframe failed:", err);
}
}
const modeButton = document.getElementById("light-dark-button");
if (modeButton) {
modeButton.querySelector('.icon').innerText = isDark ? "light_mode" : "dark_mode";
modeButton.querySelector('.name').innerText = isDark ? "Light Mode" : "Dark Mode";
}
}
// Run this as soon as possible to avoid flicker
setDarkMode(isDarkMode());
// Rest of dark mode setup logic waits until after load
window.addEventListener("DOMContentLoaded", function(event) {
// Rerun setDarkMode now that it can update buttons
const isDark = isDarkMode();
setDarkMode(isDark);
const modeButton = document.getElementById("light-dark-button");
modeButton.addEventListener("click", function() {
const wasDark = isDarkMode();
setDarkMode(!wasDark);
localStorage.setItem("theme", wasDark ? "light" : "dark");
});
});
// Share button and embed in LMS code
window.addEventListener("DOMContentLoaded", function(event) {
const shareButton = document.getElementById("embed-button");
if (shareButton) {
const sharePopup = document.getElementById("embed-popup");
const embedCode = "<iframe src='" + window.location.href + "?embed' width='100%' height='1000px' frameborder='0'></iframe>";
const embedTextbox = document.getElementById("embed-code-textbox");
if (embedTextbox) {
embedTextbox.value = embedCode;
}
shareButton.addEventListener("click", function() {
sharePopup.classList.toggle("hidden");
});
const copyButton = document.getElementById("copy-embed-button");
if (copyButton) {
copyButton.addEventListener("click", function() {
const embedTextbox = document.getElementById("embed-code-textbox");
if (embedTextbox) {
navigator.clipboard.writeText(embedCode).then(() => {
console.log("Embed code copied to clipboard!");
}).catch(err => {
console.error("Failed to copy embed code: ", err);
});
//copyButton.innerHTML = "✓✓";
// show confirmation for 2 seconds:
copyButton.querySelector('.icon').innerText = "library_add_check";
setTimeout(function() {
copyButton.querySelector('.icon').innerText = "content_copy";
sharePopup.classList.add("hidden");
}, 450);
}
});
}
}
});
// Hide everything except the content when the URL has "embed" in it
window.addEventListener("DOMContentLoaded", function(event) {
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.has("embed")) {
// Set dark mode based on value of param
if (urlParams.get("embed") === "dark") {
setDarkMode(true);
} else {
setDarkMode(false);
}
const elemsToHide = [
"ptx-navbar",
"ptx-masthead",
"ptx-page-footer",
"ptx-sidebar",
"ptx-content-footer"
];
for (let id of elemsToHide) {
const elem = document.getElementById(id);
if (elem) {
elem.classList.add("hidden");
}
}
}
});
// START Support for code-copy button functionality
document.addEventListener("click", (ev) => {
const codeBox = ev.target.closest(".clipboardable");
if (!navigator.clipboard || !codeBox) return;
const button = ev.target.closest(".code-copy");
const preContent = codeBox.querySelector("pre").textContent;
// Copy a clone with "unselectable" content removed (e.g. a console prompt),
// so the copied text matches what a manual selection would capture.
const pre = codeBox.querySelector("pre").cloneNode(true);
pre.querySelectorAll(".unselectable").forEach((el) => el.remove());
const preContent = pre.textContent;
navigator.clipboard.writeText(preContent);
button.classList.toggle("copied")
setTimeout(() => button.classList.toggle("copied"), 1000);
@@ -1279,3 +1215,12 @@ document.addEventListener("DOMContentLoaded", () => {
}
});
// END Support for code-copy button functionality
window.addEventListener("DOMContentLoaded", () => {
const userDropdownButton = document.getElementById("ptx-user-dropdown-button");
const userDropdownContent = document.getElementById("ptx-user-dropdown-content");
if (userDropdownButton && userDropdownContent) {
new PTXDropdown(userDropdownContent, userDropdownButton);
}
});