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