(function (jsinput, undefined) { // Initialize js inputs on current page. // N.B.: No library assumptions about the iframe can be made (including, // most relevantly, jquery). Keep in mind what happens in which context // when modifying this file. /* Check whether there is anything to be done */ // When all the problems are first loaded, we want to make sure the // constructor only runs once for each iframe; but we also want to make // sure that if part of the page is reloaded (e.g., a problem is // submitted), the constructor is called again. if (!jsinput) { jsinput = { runs : 1, arr : [], exists : function(id) { jsinput.arr.filter(function(e, i, a) { return e.id = id; }); } }; } jsinput.runs++; /* Utils */ // Take a string and find the nested object that corresponds to it. E.g.: // deepKey(obj, "an.example") -> obj["an"]["example"] var _deepKey = function(obj, path){ for (var i = 0, p=path.split('.'), len = p.length; i < len; i++){ obj = obj[p[i]]; } return obj; }; /* END Utils */ function jsinputConstructor(spec) { // Define an class that will be instantiated for each jsinput element // of the DOM // 'that' is the object returned by the constructor. It has a single // public method, "update", which updates the hidden input field. var that = {}; /* Private methods */ var sect = $(spec.elem).parent().find('section[class="jsinput"]'); var sectAttr = function (e) { return $(sect).attr(e); }; var thisIFrame = $(spec.elem). find('iframe[name^="iframe_"]'). get(0); var cWindow = thisIFrame.contentWindow; // Get the hidden input field to pass to customresponse function _inputField() { var parent = $(spec.elem).parent(); return parent.find('input[id^="input_"]'); } var inputField = _inputField(); // Get the grade function name var getGradeFn = sectAttr("data"); // Get state getter var getStateGetter = sectAttr("data-getstate"); // Get state setter var getStateSetter = sectAttr("data-setstate"); // Get stored state var getStoredState = sectAttr("data-stored"); // Put the return value of gradeFn in the hidden inputField. var update = function () { var ans; ans = _deepKey(cWindow, gradeFn)(); // setstate presumes getstate, so don't getstate unless setstate is // defined. if (getStateGetter && getStateSetter) { var state, store; state = unescape(_deepKey(cWindow, getStateGetter)()); store = { answer: ans, state: state }; inputField.val(JSON.stringify(store)); } else { inputField.val(ans); } return; }; /* Public methods */ that.update = update; /* Initialization */ jsinput.arr.push(that); // Put the update function as the value of the inputField's "waitfor" // attribute so that it is called when the check button is clicked. function bindCheck() { inputField.data('waitfor', that.update); return; } var gradeFn = getGradeFn; bindCheck(); // Check whether application takes in state and there is a saved // state to give it. If getStateSetter is specified but calling it // fails, wait and try again, since the iframe might still be // loading. if (getStateSetter && getStoredState) { var sval, jsonVal; try { jsonVal = JSON.parse(getStoredState); } catch (err) { jsonVal = getStoredState; } if (typeof(jsonVal) === "object") { sval = jsonVal["state"]; } else { sval = jsonVal; } // Try calling setstate every 200ms while it throws an exception, // up to five times; give up after that. // (Functions in the iframe may not be ready when we first try // calling it, but might just need more time. Give the functions // more time.) function whileloop(n) { if (n < 5){ try { _deepKey(cWindow, getStateSetter)(sval); } catch (err) { setTimeout(whileloop(n+1), 200); } } else { console.debug("Error: could not set state"); } } whileloop(0); } return that; } function walkDOM() { var newid; // Find all jsinput elements, and create a jsinput object for each one var all = $(document).find('section[class="jsinput"]'); all.each(function(index, value) { // Get just the mako variable 'id' from the id attribute newid = $(value).attr("id").replace(/^inputtype_/, ""); if (!jsinput.exists(newid)){ var newJsElem = jsinputConstructor({ id: newid, elem: value, }); } }); } // This is ugly, but without a timeout pages with multiple/heavy jsinputs // don't load properly. if ($.isReady) { setTimeout(walkDOM, 300); } else { $(document).ready(setTimeout(walkDOM, 300)); } })(window.jsinput = window.jsinput || false);