diff --git a/docs/jme-reference.rst b/docs/jme-reference.rst index bb12aba0..5df80554 100644 --- a/docs/jme-reference.rst +++ b/docs/jme-reference.rst @@ -492,6 +492,12 @@ Some extensions add new data types. See functions related to :ref:`jme-fns-html`. + .. warning:: + + Interactive HTML nodes can not be safely copied, so each HTML value should only be used once in a question. + You can mark an HTML node as non-interactive by adding the attribute ``data-interactive="false"`` to it. + Elements created using the built-in HTML functions are automatically marked as non-interactive. + .. data:: expression A JME sub-expression. diff --git a/editor/static/js/numbas/numbas-runtime.js b/editor/static/js/numbas/numbas-runtime.js index 5bd374e0..71633711 100644 --- a/editor/static/js/numbas/numbas-runtime.js +++ b/editor/static/js/numbas/numbas-runtime.js @@ -3564,6 +3564,9 @@ var TBool = types.TBool = function(b) { jme.registerType(TBool,'boolean'); /** HTML DOM element. + * + * If the element has the attribute `data-interactive="false"` then it can be safely copied and embedded multiple times. + * If the attribute is not present or has any other value, then it's assumed that it can't be safely copied. * * @memberof Numbas.jme.types * @augments Numbas.jme.token @@ -3589,6 +3592,11 @@ var THTML = types.THTML = function(html) { this.value = Array.from(elem.childNodes); this.html = elem.innerHTML; } +THTML.prototype = { + isInteractive: function() { + return this.value.some(e => e.getAttribute('data-interactive') !== 'false'); + } +} jme.registerType(THTML,'html'); /** List of elements of any data type. @@ -6314,7 +6322,9 @@ newBuiltin('html',[TString],THTML,null, { container.innerHTML = args[0].value; var subber = new jme.variables.DOMcontentsubber(scope); subber.subvars(container); - return new THTML(Array.from(container.childNodes)); + var nodes = Array.from(container.childNodes); + nodes.forEach(node => node.setAttribute('data-interactive', 'false')); + return new THTML(nodes); } }); newBuiltin('isnonemptyhtml',[TString],TBool,function(html) { @@ -6335,6 +6345,7 @@ newBuiltin('image',[TString, '[number]', '[number]'],THTML,null, { } var subber = new jme.variables.DOMcontentsubber(scope); var element = subber.subvars(img); + element.setAttribute('data-interactive', 'false'); return new THTML(element); } }); @@ -6810,6 +6821,7 @@ newBuiltin('scientificnumberhtml', [TDecimal], THTML, function(n) { var bits = math.parseScientific(n.re.toExponential()); var s = document.createElement('span'); s.innerHTML = math.niceRealNumber(bits.significand)+' × 10'+bits.exponent+''; + s.setAttribute('data-interactive', 'false'); return s; }); newBuiltin('scientificnumberhtml', [TNum], THTML, function(n) { @@ -6819,6 +6831,7 @@ newBuiltin('scientificnumberhtml', [TNum], THTML, function(n) { var bits = math.parseScientific(math.niceRealNumber(n,{style:'scientific', scientificStyle:'plain'})); var s = document.createElement('span'); s.innerHTML = math.niceRealNumber(bits.significand)+' × 10'+bits.exponent+''; + s.setAttribute('data-interactive', 'false'); return s; }); @@ -8378,6 +8391,7 @@ newBuiltin('table',[TList,TList],THTML, null, { row.appendChild(td); } } + table.setAttribute('data-interactive','false'); return new THTML(table); } }); @@ -8396,6 +8410,7 @@ newBuiltin('table',[TList],THTML, null, { row.appendChild(td); } } + table.setAttribute('data-interactive','false'); return new THTML(table); } }); @@ -14110,6 +14125,9 @@ jme.variables = /** @lends Numbas.jme.variables */ { function doToken(token) { if(jme.isType(token,'html')) { token = jme.castToType(token,'html'); + if(!token.isInteractive()) { + return token.value.map(e => e.cloneNode(true)); + } if(token.value.numbas_embedded) { throw(new Numbas.Error('jme.subvars.html inserted twice')) } diff --git a/editor/static/js/question/edit.js b/editor/static/js/question/edit.js index 92d30919..4cbcd4ab 100644 --- a/editor/static/js/question/edit.js +++ b/editor/static/js/question/edit.js @@ -2431,12 +2431,12 @@ $(document).ready(function() { return val.type; },this); - this.isHTML = ko.pureComputed(function() { + this.isInteractiveHTML = ko.pureComputed(function() { var val = this.value(); if(!val || this.error()) { return false; } - return Numbas.jme.isType(val,'html'); + return Numbas.jme.isType(val,'html') && val.isInteractive(); },this); this.thisLocked = ko.observable(false); diff --git a/editor/templates/question/tabs/variables.html b/editor/templates/question/tabs/variables.html index 1188817a..1bd0ac64 100644 --- a/editor/templates/question/tabs/variables.html +++ b/editor/templates/question/tabs/variables.html @@ -251,8 +251,8 @@
This variable is an HTML node. HTML nodes can not be relied upon to work correctly when resuming a session - for example, attached event callbacks will be lost, and mathematical notation will likely also break.
+This variable is an interactive HTML node. Interactive HTML nodes can not be relied upon to work correctly when resuming a session - for example, attached event callbacks will be lost, and mathematical notation will likely also break.
If this causes problems, try to create HTML nodes where you use them in content areas, instead of storing them in variables.