Latest build deployed.
This commit is contained in:
Executable → Regular
+162
-217
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user