Home | History | Annotate | Download | only in js
      1 /**
      2  * Common JS that talks XHR back to the server and runs the code and receives
      3  * the results.
      4  */
      5 
      6 
      7 /**
      8  * All the functionality is wrapped up in this anonymous closure, but we need
      9  * to be told if we are on the workspace page or a normal try page, so the
     10  * workspaceName is passed into the closure, it must be set in the global
     11  * namespace. If workspaceName is the empty string then we know we aren't
     12  * running on a workspace page.
     13  *
     14  * If we are on a workspace page we also look for a 'history'
     15  * variable in the global namespace which contains the list of tries
     16  * that are included in this workspace. That variable is used to
     17  * populate the history list.
     18  */
     19 (function() {
     20     function onLoad() {
     21       var run             = document.getElementById('run');
     22       var permalink       = document.getElementById('permalink');
     23       var embed           = document.getElementById('embed');
     24       var embedButton     = document.getElementById('embedButton');
     25       var code            = document.getElementById('code');
     26       var output          = document.getElementById('output');
     27       var stdout          = document.getElementById('stdout');
     28       var img             = document.getElementById('img');
     29       var tryHistory      = document.getElementById('tryHistory');
     30       var parser          = new DOMParser();
     31       var tryTemplate     = document.getElementById('tryTemplate');
     32       var sourcesTemplate = document.getElementById('sourcesTemplate');
     33 
     34       var enableSource   = document.getElementById('enableSource');
     35       var selectedSource = document.getElementById('selectedSource');
     36       var sourceCode     = document.getElementById('sourceCode');
     37       var chooseSource   = document.getElementById('chooseSource');
     38       var chooseList     = document.getElementById('chooseList');
     39 
     40       // Id of the source image to use, 0 if no source image is used.
     41       var sourceId = 0;
     42 
     43       sourceId = parseInt(enableSource.getAttribute('data-id'));
     44       if (sourceId) {
     45         sourceSelectByID(sourceId);
     46       }
     47 
     48 
     49       function beginWait() {
     50         document.body.classList.add('waiting');
     51         run.disabled = true;
     52       }
     53 
     54 
     55       function endWait() {
     56         document.body.classList.remove('waiting');
     57         run.disabled = false;
     58       }
     59 
     60 
     61       function sourceSelectByID(id) {
     62         sourceId = id;
     63         if (id > 0) {
     64           enableSource.checked = true;
     65           selectedSource.innerHTML = '<img with=64 height=64 src="/i/image-'+sourceId+'.png" />';
     66           selectedSource.classList.add('show');
     67           sourceCode.classList.add('show');
     68           chooseSource.classList.remove('show');
     69         } else {
     70           enableSource.checked = false;
     71           selectedSource.classList.remove('show');
     72           sourceCode.classList.remove('show');
     73         }
     74       }
     75 
     76 
     77       /**
     78        * A selection has been made in the choiceList.
     79        */
     80       function sourceSelect() {
     81         sourceSelectByID(parseInt(this.getAttribute('data-id')));
     82       }
     83 
     84 
     85       /**
     86        * Callback when the loading of the image sources is complete.
     87        *
     88        * Fills in the list of images from the data returned.
     89        */
     90       function sourcesComplete(e) {
     91         endWait();
     92         // The response is JSON of the form:
     93         // [
     94         //   {"id": 1},
     95         //   {"id": 3},
     96         //   ...
     97         // ]
     98         body = JSON.parse(e.target.response);
     99         // Clear out the old list if present.
    100         while (chooseList.firstChild) {
    101           chooseList.removeChild(chooseList.firstChild);
    102         }
    103         body.forEach(function(source) {
    104          var id = 'i'+source.id;
    105          var imgsrc = '/i/image-'+source.id+'.png';
    106          var clone = sourcesTemplate.content.cloneNode(true);
    107          clone.querySelector('img').src     = imgsrc;
    108          clone.querySelector('button').setAttribute('id', id);
    109          clone.querySelector('button').setAttribute('data-id', source.id);
    110          chooseList.insertBefore(clone, chooseList.firstChild);
    111          chooseList.querySelector('#'+id).addEventListener('click', sourceSelect, true);
    112         });
    113         chooseSource.classList.add('show');
    114       }
    115 
    116 
    117       /**
    118        * Toggle the use of a source image, or select a new source image.
    119        *
    120        * If enabling source images then load the list of available images via
    121        * XHR.
    122        */
    123       function sourceClick(e) {
    124         selectedSource.classList.remove('show');
    125         sourceCode.classList.remove('show');
    126         if (enableSource.checked) {
    127           beginWait();
    128           var req = new XMLHttpRequest();
    129           req.addEventListener('load', sourcesComplete);
    130           req.addEventListener('error', xhrError);
    131           req.overrideMimeType('application/json');
    132           req.open('GET', '/sources/', true);
    133           req.send();
    134         } else {
    135           sourceId = 0;
    136         }
    137       }
    138 
    139       enableSource.addEventListener('click', sourceClick, true);
    140       selectedSource.addEventListener('click', sourceClick, true);
    141 
    142 
    143       var editor = CodeMirror.fromTextArea(code, {
    144         theme: "default",
    145         lineNumbers: true,
    146         matchBrackets: true,
    147         mode: "text/x-c++src",
    148         indentUnit: 4,
    149       });
    150 
    151       // Match the initial textarea size.
    152       editor.setSize(editor.defaultCharWidth() * code.cols,
    153                      editor.defaultTextHeight() * code.rows);
    154 
    155 
    156       /**
    157        * Callback when there's an XHR error.
    158        * @param e The callback event.
    159        */
    160       function xhrError(e) {
    161         endWait();
    162         alert('Something bad happened: ' + e);
    163       }
    164 
    165       function clearOutput() {
    166         output.textContent = "";
    167         if (stdout) {
    168           stdout.textContent = "";
    169         }
    170         embed.style.display='none';
    171       }
    172 
    173       /**
    174        * Called when an image in the workspace history is clicked.
    175        */
    176       function historyClick() {
    177         beginWait();
    178         clearOutput();
    179         var req = new XMLHttpRequest();
    180         req.addEventListener('load', historyComplete);
    181         req.addEventListener('error', xhrError);
    182         req.overrideMimeType('application/json');
    183         req.open('GET', this.getAttribute('data-try'), true);
    184         req.send();
    185       }
    186 
    187 
    188       /**
    189        * Callback for when the XHR kicked off in historyClick() returns.
    190        */
    191       function historyComplete(e) {
    192         // The response is JSON of the form:
    193         // {
    194         //   "hash": "unique id for a try",
    195         //   "code": "source code for try"
    196         // }
    197         endWait();
    198         body = JSON.parse(e.target.response);
    199         code.value = body.code;
    200         editor.setValue(body.code);
    201         img.src = '/i/'+body.hash+'.png';
    202         sourceSelectByID(body.source);
    203         if (permalink) {
    204           permalink.href = '/c/' + body.hash;
    205         }
    206       }
    207 
    208 
    209       /**
    210        * Add the given try image to the history of a workspace.
    211        */
    212       function addToHistory(hash, imgUrl) {
    213         var clone = tryTemplate.content.cloneNode(true);
    214         clone.querySelector('img').src = imgUrl;
    215         clone.querySelector('.tries').setAttribute('data-try', '/json/' + hash);
    216         tryHistory.insertBefore(clone, tryHistory.firstChild);
    217         tryHistory.querySelector('.tries').addEventListener('click', historyClick, true);
    218       }
    219 
    220 
    221       /**
    222        * Callback for when the XHR returns after attempting to run the code.
    223        * @param e The callback event.
    224        */
    225       function codeComplete(e) {
    226         // The response is JSON of the form:
    227         // {
    228         //   "message": "you had an error...",
    229         //   "img": "<base64 encoded image but only on success>"
    230         // }
    231         //
    232         // The img is optional and only appears if there is a valid
    233         // image to display.
    234         endWait();
    235         console.log(e.target.response);
    236         body = JSON.parse(e.target.response);
    237         output.textContent = body.message;
    238         if (stdout) {
    239           stdout.textContent = body.stdout;
    240         }
    241         if (body.hasOwnProperty('img')) {
    242           img.src = 'data:image/png;base64,' + body.img;
    243         } else {
    244           img.src = '';
    245         }
    246         // Add the image to the history if we are on a workspace page.
    247         if (tryHistory) {
    248           addToHistory(body.hash, 'data:image/png;base64,' + body.img);
    249         } else {
    250           window.history.pushState(null, null, '/c/' + body.hash);
    251         }
    252         if (permalink) {
    253           permalink.href = '/c/' + body.hash;
    254         }
    255         if (embed) {
    256           var url = document.URL;
    257           url = url.replace('/c/', '/iframe/');
    258           embed.value = '<iframe src="' + url + '" width="740" height="550" style="border: solid #00a 5px; border-radius: 5px;"/>'
    259         }
    260         if (embedButton && embedButton.hasAttribute('disabled')) {
    261           embedButton.removeAttribute('disabled');
    262         }
    263       }
    264 
    265 
    266       function onSubmitCode() {
    267         beginWait();
    268         clearOutput();
    269         var req = new XMLHttpRequest();
    270         req.addEventListener('load', codeComplete);
    271         req.addEventListener('error', xhrError);
    272         req.overrideMimeType('application/json');
    273         req.open('POST', '/', true);
    274         req.setRequestHeader('content-type', 'application/json');
    275         req.send(JSON.stringify({'code': editor.getValue(), 'name': workspaceName, 'source': sourceId}));
    276       }
    277       run.addEventListener('click', onSubmitCode);
    278 
    279 
    280       function onEmbedClick() {
    281         embed.style.display='inline';
    282       }
    283 
    284       if (embedButton) {
    285         embedButton.addEventListener('click', onEmbedClick);
    286       }
    287 
    288       // Add the images to the history if we are on a workspace page.
    289       if (tryHistory && history) {
    290         for (var i=0; i<history.length; i++) {
    291           addToHistory(history[i].hash, '/i/'+history[i].hash+'.png');
    292         }
    293       }
    294     }
    295 
    296     // If loaded via HTML Imports then DOMContentLoaded will be long done.
    297     if (document.readyState != "loading") {
    298       onLoad();
    299     } else {
    300       this.addEventListener('DOMContentLoaded', onLoad);
    301     }
    302 
    303 })();
    304