blob: f0a5bb5327213383c051cdf4ff42c9a7aad46705 [file] [log] [blame]
/**
* Common JS that talks XHR back to the server and runs the code and receives
* the results.
*/
/**
* A polyfill for HTML Templates.
*
* This just adds in the content attribute, it doesn't stop scripts
* from running nor does it stop other side-effects.
*/
(function polyfillTemplates() {
if('content' in document.createElement('template')) {
return false;
}
var templates = document.getElementsByTagName('template');
for (var i=0; i<templates.length; i++) {
var content = document.createDocumentFragment();
while (templates[i].firstChild) {
content.appendChild(templates[i].firstChild);
}
templates[i].content = content;
}
})();
/**
* Enable zooming for any images with a class of 'zoom'.
*/
(function () {
var PIXELS = 20; // The number of pixels in width and height in a zoom.
var clientX = 0;
var clientY = 0;
var lastClientX = 0;
var lastClientY = 0;
var ctx = null; // The 2D canvas context of the zoom.
var currentImage = null; // The img node we are zooming for, otherwise null.
var hex = document.getElementById('zoomHex');
var canvasCopy = null;
function zoomMove(e) {
clientX = e.clientX;
clientY = e.clientY;
}
function zoomMouseDown(e) {
e.preventDefault();
// Only do zooming on the primary mouse button.
if (e.button != 0) {
return
}
currentImage = e.target;
clientX = e.clientX;
clientY = e.clientY;
lastClientX = clientX-1;
lastClientY = clientY-1;
document.body.style.cursor = 'crosshair';
canvas = document.createElement('canvas');
canvas.width = 1024;
canvas.height = 1024;
canvas.classList.add('zoomCanvas');
ctx = canvas.getContext('2d');
ctx.imageSmoothingEnabled = false;
this.parentNode.insertBefore(canvas, this);
// Copy the image over to a canvas so we can read RGBA values for each point.
if (hex) {
canvasCopy = document.createElement('canvas');
canvasCopy.width = currentImage.width;
canvasCopy.height = currentImage.height;
canvasCopy.id = 'zoomCopy';
canvasCopy.getContext('2d').drawImage(currentImage, 0, 0, currentImage.width, currentImage.height);
this.parentNode.insertBefore(canvasCopy, this);
}
document.body.addEventListener('pointermove', zoomMove, true);
document.body.addEventListener('pointerup', zoomFinished);
document.body.addEventListener('pointerleave', zoomFinished);
// Kick off the drawing.
setTimeout(drawZoom, 1);
}
function hexify(i) {
var s = i.toString(16).toUpperCase();
// Pad out to two hex digits if necessary.
if (s.length < 2) {
s = '0' + s;
}
return s;
}
function drawZoom() {
if (currentImage) {
// Only draw if the mouse has moved from the last time we drew.
if (lastClientX != clientX || lastClientY != clientY) {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
var x = clientX - currentImage.x;
var y = clientY - currentImage.y;
var dx = Math.floor(ctx.canvas.width/PIXELS);
var dy = Math.floor(ctx.canvas.height/PIXELS);
ctx.lineWidth = 1;
ctx.strokeStyle = '#000';
// Draw out each pixel as a rect on the target canvas, as this works around
// FireFox doing a blur as it copies from one canvas to another.
var colors = canvasCopy.getContext('2d').getImageData(x, y, PIXELS, PIXELS).data;
for (var i=0; i<PIXELS; i++) {
for (var j=0; j<PIXELS; j++) {
var offset = (j*PIXELS+i)*4; // Offset into the colors array.
ctx.fillStyle = 'rgba(' + colors[offset] + ', ' + colors[offset+1] + ', ' + colors[offset+2] + ', ' + colors[offset+3]/255.0 + ')';
ctx.fillRect(i*dx, j*dy, dx-1, dy-1);
// Box and label one selected pixel with its rgba values.
if (hex && i==PIXELS/2 && j == PIXELS/2) {
ctx.strokeRect(i*dx, j*dy, dx-1, dy-1);
hex.textContent = 'rgba('
+ colors[offset] + ', '
+ colors[offset+1] + ', '
+ colors[offset+2] + ', '
+ colors[offset+3] + ') '
+ hexify(colors[offset])
+ hexify(colors[offset+1])
+ hexify(colors[offset+2])
+ hexify(colors[offset+3]);
}
}
}
lastClientX = clientX;
lastClientY = clientY;
}
setTimeout(drawZoom, 1000/30);
}
}
function zoomFinished() {
currentImage = null;
if (hex) {
hex.textContent = '';
}
document.body.style.cursor = 'default';
ctx.canvas.parentNode.removeChild(ctx.canvas);
canvasCopy.parentNode.removeChild(canvasCopy);
document.body.removeEventListener('pointermove', zoomMove, true);
document.body.removeEventListener('pointerup', zoomFinished);
document.body.removeEventListener('pointerleave', zoomFinished);
}
this.addEventListener('DOMContentLoaded', function() {
var zoomables = document.body.querySelectorAll('.zoom');
for (var i=0; i<zoomables.length; i++) {
zoomables[i].addEventListener('pointerdown', zoomMouseDown);
}
});
})();
/**
* 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(workspaceName) {
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 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);
function beginWait() {
document.body.classList.add('waiting');
run.disabled = true;
}
function endWait() {
document.body.classList.remove('waiting');
run.disabled = false;
}
/**
* 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';
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}));
}
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');
}
}
})(workspaceName);