Skip to content
Snippets Groups Projects
Commit 745f5629 authored by Matti Harjula's avatar Matti Harjula
Browse files

Convert [[jsxgraph]] to the new [[iframe]] system, add possibility to override JSXGraph script.

Fix [[iframe]] to function with large documents.
Allow `stack_js` input bindings to "listen" to input-events and not just change events if need be.

TODO: more focus on [[jsxgraph]] sie inside the [[iframe]] and the related scrolling issues.
parent c5bfec8c
No related branches found
No related tags found
No related merge requests found
define("qtype_stack/jsxgraph",["qtype_stack/jsxgraphcore-lazy"],(function(JXG){return{find_input_id:function(divid,name){for(var tmp=document.getElementById(divid);(tmp=tmp.parentElement)&&(!tmp.classList.contains("formulation")||!tmp.parentElement.classList.contains("content")););return(tmp=tmp.querySelector('input[id$="_'+name+'"]')).addEventListener("change",(function(){M.core_formchangechecker.set_form_changed()})),tmp.id},bind_point:function(inputRef,point){var theInput=document.getElementById(inputRef);if(theInput.value&&""!=theInput.value){var coords=JSON.parse(theInput.value);try{point.setPosition(JXG.COORDS_BY_USER,coords)}catch(err){}point.board.update(),point.update()}var initialX=point.X(),initialY=point.Y();point.board.on("update",(function(){if(initialX!==point.X()||initialY!==point.Y()){var tmp=JSON.stringify([point.X(),point.Y()]);if(initialX=!1,initialY=!1,theInput.value!=tmp&&(theInput.value=tmp,-1===window.location.pathname.indexOf("preview.php"))){var e=new Event("change");theInput.dispatchEvent(e)}}}));var lastValue=JSON.stringify([point.X(),point.Y()]);theInput.addEventListener("input",(function(){if(theInput.value!=lastValue){try{var tmp=JSON.parse(theInput.value);"number"==typeof tmp[0]&&"number"==typeof tmp[1]&&(point.setPosition(JXG.COORDS_BY_USER,tmp),point.board.update(),point.update())}catch(err){}lastValue=theInput.value}})),theInput.addEventListener("change",(function(){if(theInput.value!=lastValue){try{var tmp=JSON.parse(theInput.value);"number"==typeof tmp[0]&&"number"==typeof tmp[1]&&(point.setPosition(JXG.COORDS_BY_USER,tmp),point.board.update(),point.update())}catch(err){}lastValue=theInput.value}}))},bind_point_dual:function(inputRef,point1,point2){var theInput=document.getElementById(inputRef);if(theInput.value&&""!=theInput.value){var coords=JSON.parse(theInput.value);try{point1.setPosition(JXG.COORDS_BY_USER,coords[0]),point2.setPosition(JXG.COORDS_BY_USER,coords[1])}catch(err){}point1.board.update(),point1.update(),point2.board.update(),point2.update()}var initial1X=point1.X(),initial1Y=point1.Y();point1.board.on("update",(function(){if(initial1X!==point1.X()||initial1Y!==point1.Y()){var tmp=JSON.stringify([[point1.X(),point1.Y()],[point2.X(),point2.Y()]]);if(initial1X=!1,initial1Y=!1,theInput.value!=tmp&&(theInput.value=tmp,-1===window.location.pathname.indexOf("preview.php"))){var e=new Event("change");theInput.dispatchEvent(e)}}}));var initial2X=point2.X(),initial2Y=point2.Y();point2.board.on("update",(function(){if(initial2X!==point2.X()||initial2Y!==point2.Y()){var tmp=JSON.stringify([[point1.X(),point1.Y()],[point2.X(),point2.Y()]]);if(initial2X=!1,initial2Y=!1,theInput.value!=tmp&&(theInput.value=tmp,-1===window.location.pathname.indexOf("preview.php"))){var e=new Event("change");theInput.dispatchEvent(e)}}}));var lastValue=JSON.stringify([[point1.X(),point1.Y()],[point2.X(),point2.Y()]]);theInput.addEventListener("input",(function(){if(theInput.value!=lastValue){try{var tmp=JSON.parse(theInput.value);"number"==typeof tmp[0][0]&&"number"==typeof tmp[0][1]&&point1.setPosition(JXG.COORDS_BY_USER,tmp[0]),"number"==typeof tmp[1][0]&&"number"==typeof tmp[1][1]&&(point2.setPosition(JXG.COORDS_BY_USER,tmp[1]),point1.board.update(),point1.update(),point2.board.update(),point2.update())}catch(err){}lastValue=theInput.value}})),theInput.addEventListener("change",(function(){if(theInput.value!=lastValue){try{var tmp=JSON.parse(theInput.value);"number"==typeof tmp[0][0]&&"number"==typeof tmp[0][1]&&point1.setPosition(JXG.COORDS_BY_USER,tmp[0]),"number"==typeof tmp[1][0]&&"number"==typeof tmp[1][1]&&(point2.setPosition(JXG.COORDS_BY_USER,tmp[1]),point1.board.update(),point1.update(),point2.board.update(),point2.update())}catch(err){}lastValue=theInput.value}}))},bind_point_relative:function(inputRef,point1,point2){var theInput=document.getElementById(inputRef);if(theInput.value&&""!=theInput.value){var coords=JSON.parse(theInput.value);try{point1.setPosition(JXG.COORDS_BY_USER,coords[0]);var b=[coords[0][0]+coords[1][0],coords[0][1]+coords[1][1]];point2.setPosition(JXG.COORDS_BY_USER,b)}catch(err){}point1.board.update(),point1.update(),point2.board.update(),point2.update()}var initial1X=point1.X(),initial1Y=point1.Y();point1.board.on("update",(function(){if(initial1X!==point1.X()||initial1Y!==point1.Y()){var tmp=JSON.stringify([[point1.X(),point1.Y()],[point2.X()-point1.X(),point2.Y()-point1.Y()]]);if(initial1X=!1,initial1Y=!1,theInput.value!=tmp&&(theInput.value=tmp,-1===window.location.pathname.indexOf("preview.php"))){var e=new Event("change");theInput.dispatchEvent(e)}}}));var initial2X=point2.X(),initial2Y=point2.Y();point2.board.on("update",(function(){if(initial2X!==point2.X()||initial2Y!==point2.Y()){var tmp=JSON.stringify([[point1.X(),point1.Y()],[point2.X()-point1.X(),point2.Y()-point1.Y()]]);if(initial2X=!1,initial2Y=!1,theInput.value!=tmp&&(theInput.value=tmp,-1===window.location.pathname.indexOf("preview.php"))){var e=new Event("change");theInput.dispatchEvent(e)}}}));var lastValue=JSON.stringify([[point1.X(),point1.Y()],[point2.X()-point1.X(),point2.Y()-point1.Y()]]);theInput.addEventListener("input",(function(){if(theInput.value!=lastValue){try{var tmp=JSON.parse(theInput.value);if("number"==typeof tmp[0][0]&&"number"==typeof tmp[0][1]&&point1.setPosition(JXG.COORDS_BY_USER,tmp[0]),"number"==typeof tmp[1][0]&&"number"==typeof tmp[1][1]){var b=[tmp[0][0]+tmp[1][0],tmp[0][1]+tmp[1][1]];point2.setPosition(JXG.COORDS_BY_USER,b),point1.board.update(),point1.update(),point2.board.update(),point2.update()}}catch(err){}lastValue=theInput.value}})),theInput.addEventListener("change",(function(){if(theInput.value!=lastValue){try{var tmp=JSON.parse(theInput.value);if("number"==typeof tmp[0][0]&&"number"==typeof tmp[0][1]&&point1.setPosition(JXG.COORDS_BY_USER,tmp[0]),"number"==typeof tmp[1][0]&&"number"==typeof tmp[1][1]){var b=[tmp[0][0]+tmp[1][0],tmp[0][1]+tmp[1][1]];point2.setPosition(JXG.COORDS_BY_USER,b),point1.board.update(),point1.update(),point2.board.update(),point2.update()}}catch(err){}lastValue=theInput.value}}))},bind_point_direction:function(inputRef,point1,point2){var theInput=document.getElementById(inputRef);if(theInput.value&&""!=theInput.value){var coords=JSON.parse(theInput.value);try{point1.setPosition(JXG.COORDS_BY_USER,coords[0]);var angle=coords[1][0],len=coords[1][1],b=[coords[0][0],coords[0][1]];len>0&&(b[0]=b[0]+len*Math.cos(angle),b[1]=b[1]+len*Math.sin(angle)),point2.setPosition(JXG.COORDS_BY_USER,b)}catch(err){}point1.board.update(),point1.update(),point2.board.update(),point2.update()}var initial1X=point1.X(),initial1Y=point1.Y();point1.board.on("update",(function(){if(initial1X!==point1.X()||initial1Y!==point1.Y()){var tmp=JSON.stringify([[point1.X(),point1.Y()],[Math.atan2(point2.Y()-point1.Y(),point2.X()-point1.X()),Math.sqrt((point2.X()-point1.X())*(point2.X()-point1.X())+(point2.Y()-point1.Y())*(point2.Y()-point1.Y()))]]);if(initial1X=!1,initial1Y=!1,theInput.value!=tmp&&(theInput.value=tmp,-1===window.location.pathname.indexOf("preview.php"))){var e=new Event("change");theInput.dispatchEvent(e)}}}));var initial2X=point2.X(),initial2Y=point2.Y();point2.board.on("update",(function(){if(initial2X!==point2.X()||initial2Y!==point2.Y()){var tmp=JSON.stringify([[point1.X(),point1.Y()],[Math.atan2(point2.Y()-point1.Y(),point2.X()-point1.X()),Math.sqrt((point2.X()-point1.X())*(point2.X()-point1.X())+(point2.Y()-point1.Y())*(point2.Y()-point1.Y()))]]);if(initial2X=!1,initial2Y=!1,theInput.value!=tmp&&(theInput.value=tmp,-1===window.location.pathname.indexOf("preview.php"))){var e=new Event("change");theInput.dispatchEvent(e)}}}));var lastValue=JSON.stringify([[point1.X(),point1.Y()],[Math.atan2(point2.Y()-point1.Y(),point2.X()-point1.X()),Math.sqrt((point2.X()-point1.X())*(point2.X()-point1.X())+(point2.Y()-point1.Y())*(point2.Y()-point1.Y()))]]);theInput.addEventListener("input",(function(){if(theInput.value!=lastValue){try{var tmp=JSON.parse(theInput.value);if("number"==typeof tmp[0][0]&&"number"==typeof tmp[0][1]&&point1.setPosition(JXG.COORDS_BY_USER,tmp[0]),"number"==typeof tmp[1][0]&&"number"==typeof tmp[1][1]){var angle=tmp[1][0],len=tmp[1][1],b=[tmp[0][0],tmp[0][1]];len>0&&(b[0]=b[0]+len*Math.cos(angle),b[1]=b[1]+len*Math.sin(angle)),point2.setPosition(JXG.COORDS_BY_USER,b),point1.board.update(),point1.update(),point2.board.update(),point2.update()}}catch(err){}lastValue=theInput.value}})),theInput.addEventListener("change",(function(){if(theInput.value!=lastValue){try{var tmp=JSON.parse(theInput.value);if("number"==typeof tmp[0][0]&&"number"==typeof tmp[0][1]&&point1.setPosition(JXG.COORDS_BY_USER,tmp[0]),"number"==typeof tmp[1][0]&&"number"==typeof tmp[1][1]){var angle=tmp[1][0],len=tmp[1][1],b=[tmp[0][0],tmp[0][1]];len>0&&(b[0]=b[0]+len*Math.cos(angle),b[1]=b[1]+len*Math.sin(angle)),point2.setPosition(JXG.COORDS_BY_USER,b),point1.board.update(),point1.update(),point2.board.update(),point2.update()}}catch(err){}lastValue=theInput.value}}))},bind_slider:function(inputRef,slider){var theInput=document.getElementById(inputRef);if(theInput.value&&""!=theInput.value){try{slider.setValue(JSON.parse(theInput.value))}catch(err){}slider.board.update(),slider.update()}var initialValue=slider.Value();slider.board.on("update",(function(){if(initialValue!=slider.Value()){var tmp=JSON.stringify(slider.Value());if(initialValue=!1,theInput.value!=tmp&&(theInput.value=tmp,-1===window.location.pathname.indexOf("preview.php"))){var e=new Event("change");theInput.dispatchEvent(e)}}}));var lastValue=JSON.stringify(slider.Value());theInput.addEventListener("input",(function(){if(theInput.value!==lastValue){try{var tmp=JSON.parse(theInput.value);"number"==typeof tmp&&(slider.setValue(tmp),slider.board.update(),slider.update())}catch(err){}lastValue=theInput.value}})),theInput.addEventListener("change",(function(){if(theInput.value!==lastValue){try{var tmp=JSON.parse(theInput.value);"number"==typeof tmp&&(slider.setValue(tmp),slider.board.update(),slider.update())}catch(err){}lastValue=theInput.value}}))}}}));
//# sourceMappingURL=jsxgraph.min.js.map
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
......@@ -31,7 +31,7 @@
* @copyright 2023 Aalto University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define('qtype_stack/stackjsvle', ['core/event'], function(CustomEvents) {
define("qtype_stack/stackjsvle", ["core/event"], function(CustomEvents) {
"use strict";
// Note the VLE specific include of logic.
......@@ -42,10 +42,15 @@ define('qtype_stack/stackjsvle', ['core/event'], function(CustomEvents) {
let IFRAMES = {};
/* For event handling, lists of IFRAMES listening particular inputs.
*
*/
let INPUTS = {};
/* For event handling, lists of IFRAMES listening particular inputs
* and their input events. By default we only listen to changes.
* We report input events as changes to the other side.
*/
let INPUTS_INPUT_EVENT = {};
/* A flag to disable certain things. */
let DISABLE_CHANGES = false;
......@@ -318,6 +323,38 @@ define('qtype_stack/stackjsvle', ['core/event'], function(CustomEvents) {
});
}
if (('track-input' in msg) && msg['track-input']) {
if (input.id in INPUTS_INPUT_EVENT) {
if (msg.src in INPUTS_INPUT_EVENT[input.id]) {
// DO NOT BIND TWICE!
return;
}
INPUTS_INPUT_EVENT[input.id].push(msg.src);
} else {
INPUTS_INPUT_EVENT[input.id] = [msg.src];
input.addEventListener('input', () => {
if (DISABLE_CHANGES) {
return;
}
let resp = {
version: 'STACK-JS:1.0.0',
type: 'changed-input',
name: msg.name
};
if (input.type === 'checkbox') {
resp['value'] = input.checked;
} else {
resp['value'] = input.value;
}
for (let tgt of INPUTS_INPUT_EVENT[input.id]) {
resp['tgt'] = tgt;
IFRAMES[tgt].contentWindow.postMessage(JSON.stringify(resp), '*');
}
});
}
}
// 4. Let the requester know that we have bound things
// and let it know the initial value.
if (!(msg.src in INPUTS[input.id])) {
......
.jxgbox{position:relative;overflow:hidden;background-color:#fff;border-style:solid;border-width:1px;border-color:#356aa0;border-radius:10px;margin:0;-webkit-border-radius:10px;-ms-touch-action:none}.jxgbox svg text{cursor:default;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.JXGtext{font-family:Courier,monospace;background-color:transparent;padding:0;margin:0}.JXGinfobox{border-style:none;border-width:0;border-color:#000}.jxgbox :focus{outline-width:.5px;outline-style:dotted}.JXG_navigation{position:absolute;right:5px;bottom:5px;z-index:100;background-color:transparent;padding:2px;font-size:14px;cursor:pointer}.JXG_navigation_button{color:#666}.JXG_navigation_button:hover{border-radius:2px;background:rgba(184,184,184,.5)}.JXG_navigation_button svg{top:.2em;position:relative;padding:0}.JXG_wrap_private:-moz-full-screen{background-color:#ccc;padding:0;width:100%;height:100%}.JXG_wrap_private:-webkit-full-screen{background-color:#ccc;padding:0;width:100%;height:100%}.JXG_wrap_private:fullscreen{background-color:#ccc;padding:0;width:100%;height:100%}.JXG_wrap_private:-ms-fullscreen{background-color:#ccc;padding:0;width:100%;height:100%}
\ No newline at end of file
This diff is collapsed.
......@@ -20,7 +20,8 @@
/* A flag to disable certain things. */
let DISABLE_CHANGES = false;
/* Map of the promise reolves fot inputs to be registered.
/* Map of the promise resolves for inputs to be registered.
* Basically, the set of inputs that wait registration to complete.
*/
let INPUT_PROMISES = {};
......@@ -105,6 +106,8 @@ window.addEventListener("message", (e) => {
// 2. Set its value. But don't trigger changes.
DISABLE_CHANGES = true;
input.value = msg.value;
const c = new Event('change');
input.dispatchEvent(c);
DISABLE_CHANGES = false;
break;
......@@ -154,8 +157,11 @@ export const stack_js = {
* or timeout if the input name is not something that can be fetched.
*
* You must not call this twice for the same input.
*
* You may declare that you want to also react to input events.
* This might not be that efficient but matches the old JSXGraph binding.
*/
request_access_to_input: function(inputname) {
request_access_to_input: function(inputname, inputevents) {
const input = document.createElement('input');
input.type = 'hidden';
input.id = inputname;
......@@ -184,6 +190,9 @@ export const stack_js = {
name: inputname,
src: FRAME_ID
};
if (inputevents === true) {
msg['track-input'] = true;
}
window.parent.postMessage(JSON.stringify(msg), '*');
});
......
/**
* This is a library for bindign STACK inputs to JSXGraph primitives.
*
* @copyright 2023 Aalto University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
"use strict";
// 4.4 rewrite, now with groups and ability to mark as moved.
// Should perform better and have less listeners.
// Functions that generate the value for an input. By input then by object.
var serializers = {};
// Functions that extract values from inputs. Lists of them by input.
// Single argument functions taking the value of the input.
var deserializers = {};
// Initial values for input serialisations before restore, if an object set of an input
// serialises to something else this value will be nulled otherwise if the values match
// the input won't get updated.
var initials = {};
// Object groups, if any of these objects moves consider others to have moved as well.
// Moving an object only considers those groups the object itself belongs to touching
// "part A" does not cascade to "part B" if they overlap unless the moved thing is in
// the intersection.
var objectgroups = [];
// Object input mappings. Which inputs tie to this object. Object.id to lists of inputs.
var objectinput = {};
// Internal tally of objects that have been registered and do not need to be registered again.
var registeredobjects = {};
// Flag to stop propagation.
var active = false;
function _commonsetup(inputname) {
if (!(inputname in serializers)) {
serializers[inputname] = {};
deserializers[inputname] = [];
var input = document.getElementById(inputname);
input.addEventListener('input', () => generalinputupdatehandler(inputname));
input.addEventListener('change', () => generalinputupdatehandler(inputname));
}
}
function registerobject(object) {
if (!(object.id in registeredobjects)) {
object.board.on('update', () => generalobjectupdatehandlerid(object.id));
registeredobjects[object.id] = object;
}
}
function pointserializer(point) {
return JSON.stringify([point.X(), point.Y()]);
}
function pointdeserializer(point, data) {
try {
var tmp = JSON.parse(data);
if (typeof tmp[0] == 'number' && typeof tmp[1] == 'number') {
point.setPosition(JXG.COORDS_BY_USER, tmp);
point.board.update();
point.update();
}
} catch (err) {
// We do not care about this. What could we even do?
}
}
// And for cases where we have already parsed that.
function pointdeserializerparsed(point, data) {
try {
if (typeof data[0] == 'number' && typeof data[1] == 'number') {
point.setPosition(JXG.COORDS_BY_USER, data);
point.board.update();
point.update();
}
} catch (err) {
// We do not care about this. What could we even do?
}
}
function sliderserializer(slider) {
return JSON.stringify(slider.Value());
}
function sliderdeserializer(slider, data) {
try {
slider.setValue(JSON.parse(data));
slider.board.update();
slider.update();
} catch (err) {
// We do not care about this.
}
}
function generalobjectupdatehandler(object) {
generalobjectupdatehandlerid(object.id);
}
function generalobjectupdatehandlerid(id) {
if (!active) {
active = true;
try {
var handledinputs = [];
if (id in objectinput) {
for (var i = 0; i < objectinput[id].length; i++) {
var inputname = objectinput[id][i];
if (handledinputs.indexOf(inputname) === -1) {
handledinputs.push(inputname);
var input = document.getElementById(inputname);
var val = serializers[inputname][id]();
if (val !== initials[inputname]) {
initials[inputname] = null;
input.value = val;
} else {
// Exit.
active = false;
return;
}
}
}
}
// Update groups at the same time. Here the initial value matters not a bit.
for (var gi = 0; gi < objectgroups.length; gi++) {
var group = objectgroups[gi];
if (group.indexOf(id) !== -1) {
for (var gt = 0; gt < group.length; gt++) {
var obj = group[gt];
if (obj !== id) {
if (obj in objectinput) {
for (var i = 0; i < objectinput[obj].length; i++) {
var inputname = objectinput[obj][i];
if (handledinputs.indexOf(inputname) === -1) {
initials[inputname] = null;
handledinputs.push(inputname);
var input = document.getElementById(inputname);
input.value = serializers[inputname][obj]();
}
}
}
}
}
}
}
for (var i = 0; i < handledinputs.length; i++) {
var input = document.getElementById(handledinputs[i]);
var e = new Event('change');
input.dispatchEvent(e);
}
} catch (err) {
// If there is an error there we want to reset active anyway.
// Might be that some serializer explodes if some scripting
// messes with things.
}
active = false;
}
}
// Updates to inputs coming from outside are handled like this.
function generalinputupdatehandler(inputname) {
if (inputname in deserializers) {
// Only trigger everything if the value has truly changed.
// Check all the objects serializing to this input. Note that
// some of them may exist in different graphs.
var input = document.getElementById(inputname);
var keys = Object.keys(serializers[inputname]);
var ok = false;
for (var i = 0; i < keys.length; i++) {
var old = serializers[inputname][keys[i]]();
if (old !== input.value) {
ok = true;
i = keys.length + 1;
}
}
if (ok) {
// And yes we trigger everything as we do not actually
// keep track of the ones that truly need to be triggered.
// But this is fast and converges in a few iterations.
for (var i = 0; i < deserializers[inputname].length; i++) {
deserializers[inputname][i](input.value);
}
}
}
}
export const stack_jxg = {
define_group: function(list) {
// Moving any of these objects (points sliders) leads to all of them
// being considered moved.
var l = [];
for (var i = 0; i < list.length; i++) {
if (l.indexOf(list[i].id) === -1) {
l.push(list[i].id);
}
}
objectgroups.push(l);
},
starts_moved: function(obj) {
// Makes this object start its life as moved.
// Call after bindings have been defined and possible groups declared.
if (obj.id in objectinput) {
for (var i = 0; i < objectinput[obj.id].length; i++) {
initials[objectinput[obj.id][i]] = null;
}
// This is nto a registration of the update handler
// we actually force call it.
generalobjectupdatehandler(obj);
}
},
custom_bind: function(input, serializer, deserializer, objects) {
// Allows one to define a custom binding using whatever
// serialization one wishes.
_commonsetup(input);
// Initialse the initial value store.
initials[input] = serializer();
var theInput = document.getElementById(input);
// If a value is already in the input restore it.
if (theInput.value && theInput.value != '') {
deserializer(theInput.value);
}
// Register this as a normal deserialiser for this input.
deserializers[input].push(deserializer);
// For each of these objects make the erialsier from them to
// the input as the one defined.
// Also build the map of objects to inputs and register for update tracking.
for (var i = 0; i < objects.length; i++) {
this.register_object(input, objects[i], serializer);
}
},
register_object: function(input, object, serializer) {
// For when you need to declare a new object that was not there during
// the initial binding.
if (object.id in objectinput) {
if (!(input in objectinput[object.id])) {
objectinput[object.id].push(input);
}
} else {
objectinput[object.id] = [input];
}
serializers[input][object.id] = serializer;
registerobject(object);
},
bind_point: function(inputRef, point) {
var serializer = () => pointserializer(point);
var deserializer = (value) => pointdeserializer(point, value);
this.custom_bind(inputRef, serializer, deserializer, [point]);
},
bind_point_dual: function(inputRef, p1, p2) {
var serializer = () => {
return JSON.stringify([[p1.X(),p1.Y()],[p2.X(),p2.Y()]]);
};
var deserializer = (value) => {
var tmp = JSON.parse(value);
pointdeserializerparsed(p1, tmp[0]);
pointdeserializerparsed(p2, tmp[1]);
};
this.custom_bind(inputRef, serializer, deserializer, [p1, p2]);
},
bind_point_relative: function(inputRef, p1, p2) {
var serializer = () => {
return JSON.stringify([[p1.X(),p1.Y()],[p2.X()-p1.X(),p2.Y()-p1.Y()]]);
};
var deserializer = (value) => {
var tmp = JSON.parse(value);
pointdeserializerparsed(p1, tmp[0]);
tmp[1][0] = tmp[1][0] + tmp[0][0];
tmp[1][1] = tmp[1][1] + tmp[0][1];
pointdeserializerparsed(p2, tmp[1]);
};
this.custom_bind(inputRef, serializer, deserializer, [p1, p2]);
},
bind_point_direction: function(inputRef, p1, p2) {
var serializer = () => {
return JSON.stringify([[p1.X(),p1.Y()],[Math.atan2(p2.Y()-p1.Y(),p2.X()-p1.X()),
Math.sqrt((p2.X()-p1.X())*(p2.X()-p1.X())+(p2.Y()-p1.Y())*(p2.Y()-p1.Y()))]]);
};
var deserializer = (value) => {
var tmp = JSON.parse(value);
pointdeserializerparsed(p1, tmp[0]);
var angle = tmp[1][0];
var len = tmp[1][1];
tmp[1][0] = tmp[0][0] + len*Math.cos(angle);
tmp[1][1] = tmp[0][1] + len*Math.sin(angle);
pointdeserializerparsed(p2, tmp[1]);
};
this.custom_bind(inputRef, serializer, deserializer, [p1, p2]);
},
bind_slider: function(inputRef, slider) {
var serializer = () => sliderserializer(slider);
var deserializer = (value) => sliderdeserializer(slider, value);
this.custom_bind(inputRef, serializer, deserializer, [slider]);
},
bind_list_of: function(inputRef, list_of_objects) {
var serializer = () => {
var r = '[';
for (var i = 0; i < list_of_objects.length; i++) {
var obj = list_of_objects[i];
if (obj.getType() === 'slider') {
r = r + JSON.stringify(obj.Value()) + ',';
} else {
// Assume all else to be points.
r = r + pointserializer(obj) + ',';
}
}
r = r.substring(0, r.length - 1);
return r + ']';
};
var deserializer = (value) => {
var tmp = JSON.parse(value);
for (var i = 0; (i < list_of_objects.length && i < tmp.length); i++) {
var obj = list_of_objects[i];
if (obj.getType() === 'slider') {
obj.setValue(tmp[i]);
} else {
pointdeserializerparsed(obj, tmp[i]);
}
}
};
this.custom_bind(inputRef, serializer, deserializer, list_of_objects);
}
};
......@@ -81,7 +81,7 @@ class stack_cas_castext2_iframe extends stack_cas_castext2_block {
$parameters = json_decode($params[1], true);
$content = '';
$style = '';
$scripts = '<script>const FRAME_ID = "' . $frameid . '"</script>';
$scripts = '<script>const FRAME_ID = "' . $frameid . '";</script>';
for ($i = 2; $i < count($params); $i++) {
if (is_array($params[$i])) {
if ($params[$i][0] === 'style') {
......@@ -139,16 +139,17 @@ class stack_cas_castext2_iframe extends stack_cas_castext2_block {
$code .= $scripts;
$code .= '</head><body>' . $content . '</body></html>';
$PAGE->requires->js_call_amd(
'qtype_stack/stackjsvle',
'create_iframe',
[
$frameid,
$code,
$divid
]
);
// Escape soem JavaScript strings.
$args = [
json_encode($frameid),
json_encode($code),
json_encode($divid)
];
// As the content is large we cannot simply use the js_amd_call.
$PAGE->requires->js_amd_inline(
'require(["qtype_stack/stackjsvle"], '
. 'function(stackjsvle,){stackjsvle.create_iframe(' . implode(',',$args). ');});');
self::$countframes = self::$countframes + 1;
......
......@@ -20,129 +20,162 @@ require_once(__DIR__ . '/../block.factory.php');
require_once(__DIR__ . '/root.specialblock.php');
require_once(__DIR__ . '/stack_translate.specialblock.php');
require_once(__DIR__ . '/../../../../vle_specific.php');
class stack_cas_castext2_jsxgraph extends stack_cas_castext2_block {
private static $countgraphs = 1;
/* This is not something we want people to edit in general. */
public static $namedversions = [
/* TODO: make this `cdn-latest` if possible, no point in having it
* pointing to a particular version.
*/
'cdn' => [
'css' => 'https://cdnjs.cloudflare.com/ajax/libs/jsxgraph/1.5.0/jsxgraph.min.css',
'js' => 'https://cdnjs.cloudflare.com/ajax/libs/jsxgraph/1.5.0/jsxgraphcore.min.js'],
'local' => null
];
public function compile($format, $options): ? MP_Node {
$r = new MP_List([new MP_String('jsxgraph')]);
/* Do some static inits. */
if (self::$namedversions['local'] === null) {
self::$namedversions['local'] = [
'css' => stack_cors_link('jsxgraph.min.css'),
'js' => stack_cors_link('jsxgraphcore.min.js')
];
}
// We need to transfer the parameters forward.
$r->items[] = new MP_String(json_encode($this->params));
$r = new MP_List([new MP_String('iframe')]);
foreach ($this->children as $item) {
// Assume that all code inside is JavaScript and that we do not
// want to do the markdown escaping or any other in it.
$c = $item->compile(castext2_parser_utils::RAWFORMAT, $options);
if ($c !== null) {
$r->items[] = $c;
// We need to transfer the parameters forward.
// Only the size parameters matter.
$xpars = [];
$inputs = []; // From inputname to variable name.
foreach ($this->params as $key => $value) {
if (substr($key, 0, 10) !== 'input-ref-') {
$xpars[$key] = $value;
} else {
$inputname = substr($key, 10);
$inputs[$inputname] = $value;
}
}
return $r;
// These are some of the othe parameters we do not need to
// push forward.
if (isset($xpars['version'])) {
unset($xpars['version']);
}
public function is_flat() : bool {
// Even when the content were flat we need to evaluate this during postprocessing.
return false;
if (isset($xpars['overridecss'])) {
unset($xpars['overridecss']);
}
public function postprocess(array $params, castext2_processor $processor): string {
global $PAGE;
if (count($params) < 3) {
// Nothing at all.
return '';
if (isset($xpars['overridejs'])) {
unset($xpars['overridejs']);
}
$parameters = json_decode($params[1], true);
$content = '';
for ($i = 2; $i < count($params); $i++) {
if (is_array($params[$i])) {
$content .= $processor->process($params[$i][0], $params[$i]);
} else {
$content .= $params[$i];
// Figure out what scripts we serve.
$css = self::$namedversions['local']['css'];
$js = self::$namedversions['local']['js'];
if (isset($this->params['version']) &&
isset(self::$namedversions[$this->params['version']])) {
$css = self::$namedversions[$this->params['version']]['css'];
$js = self::$namedversions[$this->params['version']]['js'];
}
if (isset($this->params['overridecss'])) {
$css = $this->params['overridecss'];
}
if (isset($this->params['overridejs'])) {
$js = $this->params['overridejs'];
}
$divid = 'stack-jsxgraph-' . self::$countgraphs;
$r->items[] = new MP_String(json_encode($xpars));
// Plug in some style and scripts.
$r->items[] = new MP_List([
new MP_String('style'),
new MP_String(json_encode(['href' => $css]))
]);
$r->items[] = new MP_List([
new MP_String('script'),
new MP_String(json_encode(['type' => 'text/javascript', 'src' => $js]))
]);
// We need to define a size for the inner content.
$width = '500px';
$height = '400px';
$aspectratio = false;
if (array_key_exists('width', $parameters)) {
$width = $parameters['width'];
if (array_key_exists('width', $xpars)) {
$width = $xpars['width'];
}
if (array_key_exists('height', $parameters)) {
$height = $parameters['height'];
if (array_key_exists('height', $xpars)) {
$height = $xpars['height'];
}
$style = "width:$width;height:$height;";
$astyle = "width:$width;height:$height;";
if (array_key_exists('aspect-ratio', $parameters)) {
$aspectratio = $parameters['aspect-ratio'];
if (array_key_exists('aspect-ratio', $xpars)) {
$aspectratio = $xpars['aspect-ratio'];
// Unset the undefined dimension, if both are defined then we have a problem.
if (array_key_exists('height', $parameters)) {
$style = "height:$height;aspect-ratio:$aspectratio;";
} else if (array_key_exists('width', $parameters)) {
$style = "width:$width;aspect-ratio:$aspectratio;";
}
}
$code = $content;
// Input ref prefixes.
// We could simply expose the prefix Moodle uses and let the author work
// with that but suppose there exists another VLE which does not use
// the same prefix for all inputs in the question, by keepping the id
// mapping here the materials need not care about that and only this
// needs to be fixed to match the environment.
// And in any case in Moodle at the time we render this we do not know.
// The prefix.
// NOTE! We should validate that we never have input-refs-outside
// question-text, but for now lets use the old seek code to work with
// them out if that happens. We now can directly access the identtifier.
foreach ($parameters as $key => $value) {
if (substr($key, 0, 10) === 'input-ref-') {
$inputname = substr($key, 10);
if (property_exists($processor, 'qa') && $processor->qa !== null) {
// For contents useing the new renderer we have access to
// the identifier at this phase.
$namecode = "var $value='" . $processor->qa->get_qt_field_name($inputname) . "';\n";
$code = "$namecode\n$code";
} else {
$seekcode = "var $value=stack_jxg.find_input_id(divid,'$inputname');";
$code = "$seekcode\n$code";
if (array_key_exists('height', $xpars)) {
$astyle = "height:$height;aspect-ratio:$aspectratio;";
} else if (array_key_exists('width', $xpars)) {
$astyle = "width:$width;aspect-ratio:$aspectratio;";
}
}
// Add the div to the doc.
$r->items[] = new MP_String('<div class="jxgbox" id="thediv" style="' . $astyle . '"></div><script type="module">');
// Do we need to bind anything?
if (count($inputs) > 0) {
// For binding we need to import the binding libraries.
$r->items[] = new MP_String("\nimport {stack_js} from '" . stack_cors_link('stackjsiframe.js') . "';\n");
$r->items[] = new MP_String("import {stack_jxg} from '" . stack_cors_link('stackjsxgraph.js') . "';\n");
// Then we need to link up to the inputs.
$promises = [];
$vars = [];
foreach ($inputs as $key => $value) {
// That true there makes us sync input-events as well, like we did before.
$promises[] = 'stack_js.request_access_to_input("' . $key . '",true)';
$vars[] = $value;
}
$linkcode = 'Promise.all([' . implode(',', $promises) . '])';
$linkcode .= '.then(([' . implode(',', $vars) . ']) => {' . "\n";
$r->items[] = new MP_String($linkcode);
}
// Plug in the div id = board id thing.
$r->items[] = new MP_String('var divid = "thediv";var BOARDID = divid;');
foreach ($this->children as $item) {
// Assume that all code inside is JavaScript and that we do not
// want to do the markdown escaping or any other in it.
$c = $item->compile(castext2_parser_utils::RAWFORMAT, $options);
if ($c !== null) {
$r->items[] = $c;
}
}
// Prefix the code with the id of the div.
$code = "var divid = '$divid';\nvar BOARDID = divid;\n$code";
if (count($inputs) > 0) {
// Close the `then(`.
$r->items[] = new MP_String("\n});");
}
// We restrict the actions of the block code a bit by stopping it from
// rewriting some things in the surrounding scopes.
// Also catch errors inside the code and try to provide console logging
// of them for the author.
// We could calculate the actual offset but I'll leave that for
// someone else. 1+2*n probably, or we could just write all the preamble
// on the same line and make the offset always be the same?
$code = '"use strict";try{if(document.getElementById("' . $divid .
'")){' . $code . '}} '
. 'catch(err) {console.log("STACK JSXGraph error in \"' . $divid
. '\", (note a slight varying offset in the error position due to possible input references):");'
. 'console.log(err);}';
// In the end close the script tag.
$r->items[] = new MP_String('</script>');
$attributes = ['class' => 'jxgbox', 'style' => $style, 'id' => $divid];
return $r;
}
$PAGE->requires->js_amd_inline(
'require(["qtype_stack/jsxgraph","qtype_stack/jsxgraphcore-lazy","core/yui"], '
. 'function(stack_jxg, JXG, Y){Y.use("mathjax",function(){' . $code
. '});});');
public function is_flat() : bool {
// Even when the content were flat we need to evaluate this during postprocessing.
return false;
}
self::$countgraphs = self::$countgraphs + 1;
return html_writer::tag('div', '', $attributes);
public function postprocess(array $params, castext2_processor $processor): string {
return 'This is never happening! The logic goes to [[iframe]].';
}
public function validate_extract_attributes(): array {
......@@ -221,6 +254,11 @@ class stack_cas_castext2_jsxgraph extends stack_cas_castext2_block {
$err[] = stack_string('stackBlock_jsxgraph_underdefined_dimension');
}
if (array_key_exists('version', $this->params) && array_key_exists($this->params['version'], self::$namedversions)) {
$valid = false;
$err[] = stack_string('stackBlock_jsxgraph_unknown_named_version');
}
$valids = null;
foreach ($this->params as $key => $value) {
if (substr($key, 0, 10) === 'input-ref-') {
......@@ -229,11 +267,11 @@ class stack_cas_castext2_jsxgraph extends stack_cas_castext2_block {
$err[] = stack_string('stackBlock_jsxgraph_input_missing',
['var' => $varname]);
}
} else if ($key !== 'width' && $key !== 'height' && $key !== 'aspect-ratio') {
} else if ($key !== 'width' && $key !== 'height' && $key !== 'aspect-ratio' && $key !== 'version' && $key !== 'overridejs' && $key !== 'overridecss') {
$err[] = "Unknown parameter '$key' for jsxgraph-block.";
$valid = false;
if ($valids === null) {
$valids = ['width', 'height', 'aspect-ratio'];
$valids = ['width', 'height', 'aspect-ratio', 'version', 'overridecss', 'overridejs'];
// The variable $inputdefinitions is not defined!
if ($inputdefinitions !== null) {
$tmp = $root->get_parameter('ioblocks');
......
......@@ -169,3 +169,13 @@ function stack_determine_moodle_version() {
$v = get_config('moodle');
return($v->branch);
}
/*
* This function returns fully defined URL for a file present in
* the `corsscripts` directory. Either mapped through logic that
* modifies headers or a direct link.
*/
function stack_cors_link(string $filename): string {
return (new moodle_url(
'/question/type/stack/corsscripts/cors.php', ['name' => $filename]))->out(false);
}
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment