Javascript  |  304行  |  9.75 KB

/**
 * Common JS that talks XHR back to the server and runs the code and receives
 * the results.
 */


/**
 * All the functionality is wrapped up in this anonymous closure, but we need
 * to be told if we are on the workspace page or a normal try page, so the
 * workspaceName is passed into the closure, it must be set in the global
 * namespace. If workspaceName is the empty string then we know we aren't
 * running on a workspace page.
 *
 * If we are on a workspace page we also look for a 'history'
 * variable in the global namespace which contains the list of tries
 * that are included in this workspace. That variable is used to
 * populate the history list.
 */
(function() {
    function onLoad() {
      var run             = document.getElementById('run');
      var permalink       = document.getElementById('permalink');
      var embed           = document.getElementById('embed');
      var embedButton     = document.getElementById('embedButton');
      var code            = document.getElementById('code');
      var output          = document.getElementById('output');
      var stdout          = document.getElementById('stdout');
      var img             = document.getElementById('img');
      var tryHistory      = document.getElementById('tryHistory');
      var parser          = new DOMParser();
      var tryTemplate     = document.getElementById('tryTemplate');
      var sourcesTemplate = document.getElementById('sourcesTemplate');

      var enableSource   = document.getElementById('enableSource');
      var selectedSource = document.getElementById('selectedSource');
      var sourceCode     = document.getElementById('sourceCode');
      var chooseSource   = document.getElementById('chooseSource');
      var chooseList     = document.getElementById('chooseList');

      // Id of the source image to use, 0 if no source image is used.
      var sourceId = 0;

      sourceId = parseInt(enableSource.getAttribute('data-id'));
      if (sourceId) {
        sourceSelectByID(sourceId);
      }


      function beginWait() {
        document.body.classList.add('waiting');
        run.disabled = true;
      }


      function endWait() {
        document.body.classList.remove('waiting');
        run.disabled = false;
      }


      function sourceSelectByID(id) {
        sourceId = id;
        if (id > 0) {
          enableSource.checked = true;
          selectedSource.innerHTML = '<img with=64 height=64 src="/i/image-'+sourceId+'.png" />';
          selectedSource.classList.add('show');
          sourceCode.classList.add('show');
          chooseSource.classList.remove('show');
        } else {
          enableSource.checked = false;
          selectedSource.classList.remove('show');
          sourceCode.classList.remove('show');
        }
      }


      /**
       * A selection has been made in the choiceList.
       */
      function sourceSelect() {
        sourceSelectByID(parseInt(this.getAttribute('data-id')));
      }


      /**
       * Callback when the loading of the image sources is complete.
       *
       * Fills in the list of images from the data returned.
       */
      function sourcesComplete(e) {
        endWait();
        // The response is JSON of the form:
        // [
        //   {"id": 1},
        //   {"id": 3},
        //   ...
        // ]
        body = JSON.parse(e.target.response);
        // Clear out the old list if present.
        while (chooseList.firstChild) {
          chooseList.removeChild(chooseList.firstChild);
        }
        body.forEach(function(source) {
         var id = 'i'+source.id;
         var imgsrc = '/i/image-'+source.id+'.png';
         var clone = sourcesTemplate.content.cloneNode(true);
         clone.querySelector('img').src     = imgsrc;
         clone.querySelector('button').setAttribute('id', id);
         clone.querySelector('button').setAttribute('data-id', source.id);
         chooseList.insertBefore(clone, chooseList.firstChild);
         chooseList.querySelector('#'+id).addEventListener('click', sourceSelect, true);
        });
        chooseSource.classList.add('show');
      }


      /**
       * Toggle the use of a source image, or select a new source image.
       *
       * If enabling source images then load the list of available images via
       * XHR.
       */
      function sourceClick(e) {
        selectedSource.classList.remove('show');
        sourceCode.classList.remove('show');
        if (enableSource.checked) {
          beginWait();
          var req = new XMLHttpRequest();
          req.addEventListener('load', sourcesComplete);
          req.addEventListener('error', xhrError);
          req.overrideMimeType('application/json');
          req.open('GET', '/sources/', true);
          req.send();
        } else {
          sourceId = 0;
        }
      }

      enableSource.addEventListener('click', sourceClick, true);
      selectedSource.addEventListener('click', sourceClick, true);


      var editor = CodeMirror.fromTextArea(code, {
        theme: "default",
        lineNumbers: true,
        matchBrackets: true,
        mode: "text/x-c++src",
        indentUnit: 4,
      });

      // Match the initial textarea size.
      editor.setSize(editor.defaultCharWidth() * code.cols,
                     editor.defaultTextHeight() * code.rows);


      /**
       * Callback when there's an XHR error.
       * @param e The callback event.
       */
      function xhrError(e) {
        endWait();
        alert('Something bad happened: ' + e);
      }

      function clearOutput() {
        output.textContent = "";
        if (stdout) {
          stdout.textContent = "";
        }
        embed.style.display='none';
      }

      /**
       * Called when an image in the workspace history is clicked.
       */
      function historyClick() {
        beginWait();
        clearOutput();
        var req = new XMLHttpRequest();
        req.addEventListener('load', historyComplete);
        req.addEventListener('error', xhrError);
        req.overrideMimeType('application/json');
        req.open('GET', this.getAttribute('data-try'), true);
        req.send();
      }


      /**
       * Callback for when the XHR kicked off in historyClick() returns.
       */
      function historyComplete(e) {
        // The response is JSON of the form:
        // {
        //   "hash": "unique id for a try",
        //   "code": "source code for try"
        // }
        endWait();
        body = JSON.parse(e.target.response);
        code.value = body.code;
        editor.setValue(body.code);
        img.src = '/i/'+body.hash+'.png';
        sourceSelectByID(body.source);
        if (permalink) {
          permalink.href = '/c/' + body.hash;
        }
      }


      /**
       * Add the given try image to the history of a workspace.
       */
      function addToHistory(hash, imgUrl) {
        var clone = tryTemplate.content.cloneNode(true);
        clone.querySelector('img').src = imgUrl;
        clone.querySelector('.tries').setAttribute('data-try', '/json/' + hash);
        tryHistory.insertBefore(clone, tryHistory.firstChild);
        tryHistory.querySelector('.tries').addEventListener('click', historyClick, true);
      }


      /**
       * Callback for when the XHR returns after attempting to run the code.
       * @param e The callback event.
       */
      function codeComplete(e) {
        // The response is JSON of the form:
        // {
        //   "message": "you had an error...",
        //   "img": "<base64 encoded image but only on success>"
        // }
        //
        // The img is optional and only appears if there is a valid
        // image to display.
        endWait();
        console.log(e.target.response);
        body = JSON.parse(e.target.response);
        output.textContent = body.message;
        if (stdout) {
          stdout.textContent = body.stdout;
        }
        if (body.hasOwnProperty('img')) {
          img.src = 'data:image/png;base64,' + body.img;
        } else {
          img.src = '';
        }
        // Add the image to the history if we are on a workspace page.
        if (tryHistory) {
          addToHistory(body.hash, 'data:image/png;base64,' + body.img);
        } else {
          window.history.pushState(null, null, '/c/' + body.hash);
        }
        if (permalink) {
          permalink.href = '/c/' + body.hash;
        }
        if (embed) {
          var url = document.URL;
          url = url.replace('/c/', '/iframe/');
          embed.value = '<iframe src="' + url + '" width="740" height="550" style="border: solid #00a 5px; border-radius: 5px;"/>'
        }
        if (embedButton && embedButton.hasAttribute('disabled')) {
          embedButton.removeAttribute('disabled');
        }
      }


      function onSubmitCode() {
        beginWait();
        clearOutput();
        var req = new XMLHttpRequest();
        req.addEventListener('load', codeComplete);
        req.addEventListener('error', xhrError);
        req.overrideMimeType('application/json');
        req.open('POST', '/', true);
        req.setRequestHeader('content-type', 'application/json');
        req.send(JSON.stringify({'code': editor.getValue(), 'name': workspaceName, 'source': sourceId}));
      }
      run.addEventListener('click', onSubmitCode);


      function onEmbedClick() {
        embed.style.display='inline';
      }

      if (embedButton) {
        embedButton.addEventListener('click', onEmbedClick);
      }

      // Add the images to the history if we are on a workspace page.
      if (tryHistory && history) {
        for (var i=0; i<history.length; i++) {
          addToHistory(history[i].hash, '/i/'+history[i].hash+'.png');
        }
      }
    }

    // If loaded via HTML Imports then DOMContentLoaded will be long done.
    if (document.readyState != "loading") {
      onLoad();
    } else {
      this.addEventListener('DOMContentLoaded', onLoad);
    }

})();