jsinput.js 8.71 KB
Newer Older
1 2 3 4 5 6 7 8 9
/* 
 * JSChannel (https://github.com/mozilla/jschannel) will be loaded prior to this
 * script. We will use it use to let JSInput call 'gradeFn', and eventually 
 * 'stateGetter' & 'stateSetter' in the iframe's content even if it hasn't the 
 * same origin, therefore bypassing SOP:
 * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Same_origin_policy_for_JavaScript
 */    

var JSInput = (function ($, undefined) {
Julian Arni committed
10 11 12 13 14
    // 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.

15 16 17 18 19 20 21 22
    // 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.

    /*                      Utils                               */


23
    // Take a string and find the nested object that corresponds to it. E.g.:
24 25
    //    _deepKey(obj, "an.example") -> obj["an"]["example"]
    function _deepKey(obj, path){
26 27
        for (var i = 0, p=path.split('.'), len = p.length; i < len; i++){
            obj = obj[p[i]];
28 29 30 31
        }
        return obj;
    };

32

33 34 35
    /*      END     Utils                                   */


36
    function jsinputConstructor(elem) {
37
        // Define an class that will be instantiated for each jsinput element
Julian Arni committed
38 39 40 41
        // of the DOM

        /*                      Private methods                          */

42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
        var section = $(elem).parent().find('section[class="jsinput"]'),
            sectionAttr = function (e) { return $(section).attr(e); },
            iframe = $(elem).find('iframe[name^="iframe_"]').get(0),
            cWindow = iframe.contentWindow,
            path = iframe.src.substring(0, iframe.src.lastIndexOf("/")+1),
            // Get the hidden input field to pass to customresponse
            inputField = $(elem).parent().find('input[id^="input_"]'),
            // Get the grade function name
            gradeFn = sectionAttr("data"),
            // Get state getter
            stateGetter = sectionAttr("data-getstate"),
            // Get state setter
            stateSetter = sectionAttr("data-setstate"),
            // Get stored state
            storedState = sectionAttr("data-stored"),
57 58
            // Get initial state
            initialState = sectionAttr("data-initial-state"),
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94
            // Bypass single-origin policy only if this attribute is "false"
            // In that case, use JSChannel to do so.
            sop = sectionAttr("data-sop"),
            channel;
        
        sop = (sop !== "false");

        if (!sop) {
            channel = Channel.build({
                window: cWindow,
                origin: path,
                scope: "JSInput"
            });
         }   

        /*                       Public methods                     */
        
        // Only one public method that updates the hidden input field.
        var update = function (callback) {
            var answer, state, store;

            if (sop) {
                answer = _deepKey(cWindow, gradeFn)();
                // Setting state presumes getting state, so don't get state
                // unless set state is defined.
                if (stateGetter && stateSetter) {
                    state = unescape(_deepKey(cWindow, stateGetter)());
                    store = {
                        answer: answer,
                        state:  state
                    };
                    inputField.val(JSON.stringify(store));
                } else {
                    inputField.val(answer);
                }
                callback();
Julian Arni committed
95
            } else {
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123
                channel.call({
                    method: "getGrade",
                    params: "",
                    success: function(val) {
                        answer = decodeURI(val.toString());

                        // Setting state presumes getting state, so don't get
                        // state unless set state is defined.
                        if (stateGetter && stateSetter) {
                            channel.call({
                                method: "getState",
                                params: "",
                                success: function(val) {
                                    state = decodeURI(val.toString());
                                    store = {
                                        answer: answer,
                                        state:  state
                                    };
                                    inputField.val(JSON.stringify(store));
                                    callback();
                                }
                            });
                        } else {
                            inputField.val(answer);
                            callback();
                        }
                    }
                });
Julian Arni committed
124 125 126 127 128
            }
        };

        /*                      Initialization                          */

129
        // Put the update function as the value of the inputField's "waitfor"
Julian Arni committed
130
        // attribute so that it is called when the check button is clicked.
131
        inputField.data('waitfor', update);
132 133

        // Check whether application takes in state and there is a saved
134
        // state to give it. If stateSetter is specified but calling it
135 136
        // fails, wait and try again, since the iframe might still be
        // loading.
137
        if (stateSetter && (storedState || initialState)) {
138
            var stateValue, jsonValue;
139

140 141 142 143 144 145
            if (storedState) {
                try {
                    jsonValue = JSON.parse(storedState);
                } catch (err) {
                    jsonValue = storedState;
                }
146

147 148 149 150 151 152 153 154 155
                if (typeof(jsonValue) === "object") {
                    stateValue = jsonValue["state"];
                } else {
                    stateValue = jsonValue;
                }
            }
            else {
                // use initial_state string as the JSON string for stateValue.
                stateValue = initialState;
156 157
            }

158 159 160 161 162
            // 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.)
163 164 165
            // 200 ms and 5 times are arbitrary but this has functioned with the
            // only application that has ever used JSInput, jsVGL. Something 
            // more sturdy should be put in place.
166
            function whileloop(n) {
167
                if (n > 0){
168
                    try {
169 170 171 172 173 174 175 176 177 178
                        if (sop) {
                            _deepKey(cWindow, stateSetter)(stateValue);
                        } else {
                            channel.call({
                                method: "setState",
                                params: stateValue,
                                success: function() {
                                }    
                            });
                        }
179
                    } catch (err) {
180
                        setTimeout(function() { whileloop(n - 1); }, 200);
181 182
                    }
                }
183
                else {
184
                    console.debug("Error: could not set state");
185
                }
Julian Arni committed
186
            }
187
            whileloop(5);
Julian Arni committed
188 189 190 191
        }
    }

    function walkDOM() {
stv committed
192
        var allSections = $('section.jsinput');
193 194 195 196 197
        // When a JSInput problem loads, its data-processed attribute is false,
        // so the jsconstructor will be called for it.
        // The constructor will not be called again on subsequent reruns of
        // this file by other JSInput. Only if it is reloaded, either with the
        // rest of the page or when it is submitted, will this constructor be
stv committed
198
        // called again.
199
        allSections.each(function(index, value) {
stv committed
200
            var dataProcessed = ($(value).attr("data-processed") === "true");
201 202 203
            if (!dataProcessed) {
                jsinputConstructor(value);
                $(value).attr("data-processed", 'true');
204
            }
Julian Arni committed
205 206 207
        });
    }

208 209
    // This is ugly, but without a timeout pages with multiple/heavy jsinputs
    // don't load properly.
210 211 212
    // 300 ms is arbitrary but this has functioned with the only application 
    // that has ever used JSInput, jsVGL. Something more sturdy should be put in
    // place.
213
    if ($.isReady) {
214
        setTimeout(walkDOM, 300);
215
    } else {
216
        $(document).ready(setTimeout(walkDOM, 300));
217 218
    }

219 220 221 222 223
    return {
        jsinputConstructor: jsinputConstructor,
        walkDOM: walkDOM
    };
    
224
})(window.jQuery);