-
Notifications
You must be signed in to change notification settings - Fork 169
Description
It is currently not possible to use [[input]] boxes within TeX environments. It would be very useful to have this, though! (I tried looking for an open issue on that, but I didn't find any? If there is one, we can move over there.)
MathJax originally was not able to do this, but example scripts for MathJax 3 have been written.
I'm currently vibe coding my way through this. As a proof of concept, the following code works when added to "Appearance > Additional HTML > Within HEAD" by the Moodle admin for basic input boxes:
<style>
/* Layout rules to make native Moodle inputs fit cleanly inside an equation */
.mjx-math-input {
display: inline-block !important;
vertical-align: middle !important;
margin: 0 4px !important;
padding: 0.1rem 0.4rem !important;
height: 2.2em !important;
min-height: 2.2em !important;
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace !important;
}
.custom-validation-wrapper {
margin-top: 5px;
font-size: 0.9em;
display: inline-block;
}
</style>
<script>
(function() {
var realMathJax = window.MathJax || {};
Object.defineProperty(window, 'MathJax', {
get: function() { return realMathJax; },
set: function(newConfig) {
if (newConfig && newConfig.startup) {
newConfig.tex = newConfig.tex || {};
newConfig.tex.packages = newConfig.tex.packages || {};
if (Array.isArray(newConfig.tex.packages)) {
newConfig.tex.packages.push('custom-input-pkg');
} else {
newConfig.tex.packages['[+]'] = newConfig.tex.packages['[+]'] || [];
newConfig.tex.packages['[+]'].push('custom-input-pkg');
}
var originalReady = newConfig.startup.ready || function() { MathJax.startup.defaultReady(); };
newConfig.startup.ready = function() {
try {
const stackInputs = document.querySelectorAll('input[type="text"]');
stackInputs.forEach(input => {
if (input.id && input.id.includes('ans')) {
let target = input;
if (input.parentElement && input.parentElement.tagName === 'SPAN' && input.parentElement.className.includes('stack_input')) {
target = input.parentElement;
}
let block = target.closest('p, div, td, li') || document.body;
let range = document.createRange();
range.setStart(block, 0);
range.setEndBefore(target);
let textBefore = range.toString();
let lastOpen = Math.max(textBefore.lastIndexOf('\\('), textBefore.lastIndexOf('\\['));
let lastClose = Math.max(textBefore.lastIndexOf('\\)'), textBefore.lastIndexOf('\\]'));
let isMath = lastOpen > lastClose;
if (isMath) {
// THE FIX: Find the actual Moodle form and put the vault inside it!
const parentForm = input.closest('form') || document.body;
let vault = parentForm.querySelector('.stack-input-vault');
if (!vault) {
vault = document.createElement('div');
vault.className = 'stack-input-vault';
vault.style.display = 'none'; // Keep it safely hidden inside the form
parentForm.appendChild(vault);
}
const id = input.id;
const size = input.getAttribute('size') || '10';
const width = size + 'ch';
const macroText = ` \\input[${id}][${width}]{} `;
const parent = target.parentNode;
parent.insertBefore(document.createTextNode(macroText), target);
vault.appendChild(input);
if (target !== input) target.remove();
parent.normalize();
}
}
});
} catch(e) {
console.error("MathJax Pre-processor Error:", e);
}
try {
const {Configuration} = MathJax._.input.tex.Configuration;
const {CommandMap} = MathJax._.input.tex.SymbolMap;
const InputMacro = function(parser, name) {
const id = parser.GetBrackets(name, '');
const w = parser.GetBrackets(name, '5em');
const mtext = parser.create('node', 'mtext', [ parser.create('text', '') ], {
"class": "mjx-stack-input-placeholder",
"data-id": id,
"data-width": w
});
parser.Push(mtext);
};
new CommandMap('custom-input-pkg', { input: 'InputMacro' }, {InputMacro});
Configuration.create('custom-input-pkg', { handler: {macro: ['custom-input-pkg']} });
} catch(e) {
console.error("MathJax Package Error:", e);
}
originalReady();
};
}
realMathJax = newConfig;
}
});
function swapPlaceholders() {
const placeholders = document.querySelectorAll('.mjx-stack-input-placeholder:not(.processed)');
placeholders.forEach(function(el) {
el.classList.add('processed');
const id = el.getAttribute('data-id') || '';
const w = el.getAttribute('data-width') || '5em';
// THE FIX: Prevent Moodle from throwing errors on empty macro passes
if (!id) return;
const input = document.createElement('input');
input.type = 'text';
let realStackInput = document.getElementById(id);
if (realStackInput) {
input.className = realStackInput.className;
input.classList.add('mjx-math-input');
input.style.width = w;
input.value = realStackInput.value;
const syncAndTrigger = function(e) {
realStackInput.value = e.target.value;
realStackInput.dispatchEvent(new Event('input', { bubbles: true }));
realStackInput.dispatchEvent(new Event('change', { bubbles: true }));
if (e.type === 'keyup') {
const keyEvent = new KeyboardEvent('keyup', {
bubbles: true, cancelable: true,
key: e.key, code: e.code, keyCode: e.keyCode
});
realStackInput.dispatchEvent(keyEvent);
}
if (typeof window.jQuery !== 'undefined') {
window.jQuery(realStackInput).trigger('change').trigger('keyup');
}
};
input.addEventListener('keyup', syncAndTrigger);
input.addEventListener('input', syncAndTrigger);
input.addEventListener('blur', function(e) {
realStackInput.value = e.target.value;
realStackInput.dispatchEvent(new Event('blur', { bubbles: true }));
if (typeof window.jQuery !== 'undefined') {
window.jQuery(realStackInput).trigger('blur');
}
});
}
el.innerHTML = '';
el.appendChild(input);
});
}
const observer = new MutationObserver(swapPlaceholders);
document.addEventListener("DOMContentLoaded", function() {
observer.observe(document.body, { childList: true, subtree: true });
swapPlaceholders();
});
})();
</script>
Here's a sample question you can use:
questions-Dev Sandbox Course-Mathjax Input-20260317-1042.xml
There are currently three known issues to me with the code above:
- The styling isn't exactly the same (but that's of minor importance to me right now)
- It would be much nicer to have it shipped as part of the actual STACK code, instead of having to rely on a the Moodle admin adding this
- This code only works for "basic" input boxes, in the sense that trying it within subscripts as in
\(\displaystyle \int_{[[input:ans3]]}^{[[input:ans4]]} [[input:ans5]]\ \mathrm{d}x\)breaks the question.
I am working on both these issues and create a proper branch (off dev) when I'm ready for that.