| OLD | NEW |
| 1 /** | 1 /** |
| 2 * Common JS that talks XHR back to the server and runs the code and receives | 2 * Common JS that talks XHR back to the server and runs the code and receives |
| 3 * the results. | 3 * the results. |
| 4 */ | 4 */ |
| 5 | 5 |
| 6 /** | |
| 7 * A polyfill for HTML Templates. | |
| 8 * | |
| 9 * This just adds in the content attribute, it doesn't stop scripts | |
| 10 * from running nor does it stop other side-effects. | |
| 11 */ | |
| 12 (function polyfillTemplates() { | |
| 13 if('content' in document.createElement('template')) { | |
| 14 return false; | |
| 15 } | |
| 16 | |
| 17 var templates = document.getElementsByTagName('template'); | |
| 18 for (var i=0; i<templates.length; i++) { | |
| 19 var content = document.createDocumentFragment(); | |
| 20 while (templates[i].firstChild) { | |
| 21 content.appendChild(templates[i].firstChild); | |
| 22 } | |
| 23 templates[i].content = content; | |
| 24 } | |
| 25 })(); | |
| 26 | |
| 27 /** | |
| 28 * Enable zooming for any images with a class of 'zoom'. | |
| 29 */ | |
| 30 (function () { | |
| 31 var PIXELS = 20; // The number of pixels in width and height in a zoom. | |
| 32 var clientX = 0; | |
| 33 var clientY = 0; | |
| 34 var lastClientX = 0; | |
| 35 var lastClientY = 0; | |
| 36 var ctx = null; // The 2D canvas context of the zoom. | |
| 37 var currentImage = null; // The img node we are zooming for, otherwise null. | |
| 38 var hex = document.getElementById('zoomHex'); | |
| 39 var canvasCopy = null; | |
| 40 | |
| 41 function zoomMove(e) { | |
| 42 clientX = e.clientX; | |
| 43 clientY = e.clientY; | |
| 44 } | |
| 45 | |
| 46 function zoomMouseDown(e) { | |
| 47 e.preventDefault(); | |
| 48 // Only do zooming on the primary mouse button. | |
| 49 if (e.button != 0) { | |
| 50 return | |
| 51 } | |
| 52 currentImage = e.target; | |
| 53 clientX = e.clientX; | |
| 54 clientY = e.clientY; | |
| 55 lastClientX = clientX-1; | |
| 56 lastClientY = clientY-1; | |
| 57 document.body.style.cursor = 'crosshair'; | |
| 58 canvas = document.createElement('canvas'); | |
| 59 canvas.width = 1024; | |
| 60 canvas.height = 1024; | |
| 61 canvas.classList.add('zoomCanvas'); | |
| 62 ctx = canvas.getContext('2d'); | |
| 63 ctx.imageSmoothingEnabled = false; | |
| 64 this.parentNode.insertBefore(canvas, this); | |
| 65 | |
| 66 // Copy the image over to a canvas so we can read RGBA values for each point
. | |
| 67 if (hex) { | |
| 68 canvasCopy = document.createElement('canvas'); | |
| 69 canvasCopy.width = currentImage.width; | |
| 70 canvasCopy.height = currentImage.height; | |
| 71 canvasCopy.id = 'zoomCopy'; | |
| 72 canvasCopy.getContext('2d').drawImage(currentImage, 0, 0, currentImage.wid
th, currentImage.height); | |
| 73 this.parentNode.insertBefore(canvasCopy, this); | |
| 74 } | |
| 75 | |
| 76 document.body.addEventListener('pointermove', zoomMove, true); | |
| 77 document.body.addEventListener('pointerup', zoomFinished); | |
| 78 document.body.addEventListener('pointerleave', zoomFinished); | |
| 79 | |
| 80 // Kick off the drawing. | |
| 81 setTimeout(drawZoom, 1); | |
| 82 } | |
| 83 | |
| 84 function hexify(i) { | |
| 85 var s = i.toString(16).toUpperCase(); | |
| 86 // Pad out to two hex digits if necessary. | |
| 87 if (s.length < 2) { | |
| 88 s = '0' + s; | |
| 89 } | |
| 90 return s; | |
| 91 } | |
| 92 | |
| 93 function drawZoom() { | |
| 94 if (currentImage) { | |
| 95 // Only draw if the mouse has moved from the last time we drew. | |
| 96 if (lastClientX != clientX || lastClientY != clientY) { | |
| 97 ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); | |
| 98 var x = clientX - currentImage.x; | |
| 99 var y = clientY - currentImage.y; | |
| 100 var dx = Math.floor(ctx.canvas.width/PIXELS); | |
| 101 var dy = Math.floor(ctx.canvas.height/PIXELS); | |
| 102 | |
| 103 ctx.lineWidth = 1; | |
| 104 ctx.strokeStyle = '#000'; | |
| 105 | |
| 106 // Draw out each pixel as a rect on the target canvas, as this works aro
und | |
| 107 // FireFox doing a blur as it copies from one canvas to another. | |
| 108 var colors = canvasCopy.getContext('2d').getImageData(x, y, PIXELS, PIXE
LS).data; | |
| 109 for (var i=0; i<PIXELS; i++) { | |
| 110 for (var j=0; j<PIXELS; j++) { | |
| 111 var offset = (j*PIXELS+i)*4; // Offset into the colors array. | |
| 112 ctx.fillStyle = 'rgba(' + colors[offset] + ', ' + colors[offset+1] +
', ' + colors[offset+2] + ', ' + colors[offset+3]/255.0 + ')'; | |
| 113 ctx.fillRect(i*dx, j*dy, dx-1, dy-1); | |
| 114 // Box and label one selected pixel with its rgba values. | |
| 115 if (hex && i==PIXELS/2 && j == PIXELS/2) { | |
| 116 ctx.strokeRect(i*dx, j*dy, dx-1, dy-1); | |
| 117 hex.textContent = 'rgba(' | |
| 118 + colors[offset] + ', ' | |
| 119 + colors[offset+1] + ', ' | |
| 120 + colors[offset+2] + ', ' | |
| 121 + colors[offset+3] + ') ' | |
| 122 + hexify(colors[offset]) | |
| 123 + hexify(colors[offset+1]) | |
| 124 + hexify(colors[offset+2]) | |
| 125 + hexify(colors[offset+3]); | |
| 126 } | |
| 127 } | |
| 128 } | |
| 129 lastClientX = clientX; | |
| 130 lastClientY = clientY; | |
| 131 } | |
| 132 setTimeout(drawZoom, 1000/30); | |
| 133 } | |
| 134 } | |
| 135 | |
| 136 function zoomFinished() { | |
| 137 currentImage = null; | |
| 138 if (hex) { | |
| 139 hex.textContent = ''; | |
| 140 } | |
| 141 document.body.style.cursor = 'default'; | |
| 142 ctx.canvas.parentNode.removeChild(ctx.canvas); | |
| 143 canvasCopy.parentNode.removeChild(canvasCopy); | |
| 144 document.body.removeEventListener('pointermove', zoomMove, true); | |
| 145 document.body.removeEventListener('pointerup', zoomFinished); | |
| 146 document.body.removeEventListener('pointerleave', zoomFinished); | |
| 147 } | |
| 148 | |
| 149 this.addEventListener('DOMContentLoaded', function() { | |
| 150 var zoomables = document.body.querySelectorAll('.zoom'); | |
| 151 for (var i=0; i<zoomables.length; i++) { | |
| 152 zoomables[i].addEventListener('pointerdown', zoomMouseDown); | |
| 153 } | |
| 154 }); | |
| 155 })(); | |
| 156 | |
| 157 | 6 |
| 158 /** | 7 /** |
| 159 * All the functionality is wrapped up in this anonymous closure, but we need | 8 * All the functionality is wrapped up in this anonymous closure, but we need |
| 160 * to be told if we are on the workspace page or a normal try page, so the | 9 * to be told if we are on the workspace page or a normal try page, so the |
| 161 * workspaceName is passed into the closure, it must be set in the global | 10 * workspaceName is passed into the closure, it must be set in the global |
| 162 * namespace. If workspaceName is the empty string then we know we aren't | 11 * namespace. If workspaceName is the empty string then we know we aren't |
| 163 * running on a workspace page. | 12 * running on a workspace page. |
| 164 * | 13 * |
| 165 * If we are on a workspace page we also look for a 'history' | 14 * If we are on a workspace page we also look for a 'history' |
| 166 * variable in the global namespace which contains the list of tries | 15 * variable in the global namespace which contains the list of tries |
| 167 * that are included in this workspace. That variable is used to | 16 * that are included in this workspace. That variable is used to |
| 168 * populate the history list. | 17 * populate the history list. |
| 169 */ | 18 */ |
| 170 (function(workspaceName) { | 19 (function() { |
| 171 var run = document.getElementById('run'); | 20 function onLoad() { |
| 172 var permalink = document.getElementById('permalink'); | 21 var run = document.getElementById('run'); |
| 173 var embed = document.getElementById('embed'); | 22 var permalink = document.getElementById('permalink'); |
| 174 var embedButton = document.getElementById('embedButton'); | 23 var embed = document.getElementById('embed'); |
| 175 var code = document.getElementById('code'); | 24 var embedButton = document.getElementById('embedButton'); |
| 176 var output = document.getElementById('output'); | 25 var code = document.getElementById('code'); |
| 177 var stdout = document.getElementById('stdout'); | 26 var output = document.getElementById('output'); |
| 178 var img = document.getElementById('img'); | 27 var stdout = document.getElementById('stdout'); |
| 179 var tryHistory = document.getElementById('tryHistory'); | 28 var img = document.getElementById('img'); |
| 180 var parser = new DOMParser(); | 29 var tryHistory = document.getElementById('tryHistory'); |
| 181 var tryTemplate = document.getElementById('tryTemplate'); | 30 var parser = new DOMParser(); |
| 31 var tryTemplate = document.getElementById('tryTemplate'); |
| 182 | 32 |
| 183 var editor = CodeMirror.fromTextArea(code, { | 33 var editor = CodeMirror.fromTextArea(code, { |
| 184 theme: "default", | 34 theme: "default", |
| 185 lineNumbers: true, | 35 lineNumbers: true, |
| 186 matchBrackets: true, | 36 matchBrackets: true, |
| 187 mode: "text/x-c++src", | 37 mode: "text/x-c++src", |
| 188 indentUnit: 4, | 38 indentUnit: 4, |
| 189 }); | 39 }); |
| 190 | 40 |
| 191 // Match the initial textarea size. | 41 // Match the initial textarea size. |
| 192 editor.setSize(editor.defaultCharWidth() * code.cols, | 42 editor.setSize(editor.defaultCharWidth() * code.cols, |
| 193 editor.defaultTextHeight() * code.rows); | 43 editor.defaultTextHeight() * code.rows); |
| 194 | 44 |
| 195 function beginWait() { | 45 function beginWait() { |
| 196 document.body.classList.add('waiting'); | 46 document.body.classList.add('waiting'); |
| 197 run.disabled = true; | 47 run.disabled = true; |
| 198 } | 48 } |
| 199 | 49 |
| 200 | 50 |
| 201 function endWait() { | 51 function endWait() { |
| 202 document.body.classList.remove('waiting'); | 52 document.body.classList.remove('waiting'); |
| 203 run.disabled = false; | 53 run.disabled = false; |
| 204 } | 54 } |
| 205 | 55 |
| 206 | 56 |
| 207 /** | 57 /** |
| 208 * Callback when there's an XHR error. | 58 * Callback when there's an XHR error. |
| 209 * @param e The callback event. | 59 * @param e The callback event. |
| 210 */ | 60 */ |
| 211 function xhrError(e) { | 61 function xhrError(e) { |
| 212 endWait(); | 62 endWait(); |
| 213 alert('Something bad happened: ' + e); | 63 alert('Something bad happened: ' + e); |
| 214 } | 64 } |
| 215 | 65 |
| 216 function clearOutput() { | 66 function clearOutput() { |
| 217 output.textContent = ""; | 67 output.textContent = ""; |
| 218 if (stdout) { | 68 if (stdout) { |
| 219 stdout.textContent = ""; | 69 stdout.textContent = ""; |
| 70 } |
| 71 embed.style.display='none'; |
| 220 } | 72 } |
| 221 embed.style.display='none'; | |
| 222 } | |
| 223 | 73 |
| 224 /** | 74 /** |
| 225 * Called when an image in the workspace history is clicked. | 75 * Called when an image in the workspace history is clicked. |
| 226 */ | 76 */ |
| 227 function historyClick() { | 77 function historyClick() { |
| 228 beginWait(); | 78 beginWait(); |
| 229 clearOutput(); | 79 clearOutput(); |
| 230 var req = new XMLHttpRequest(); | 80 var req = new XMLHttpRequest(); |
| 231 req.addEventListener('load', historyComplete); | 81 req.addEventListener('load', historyComplete); |
| 232 req.addEventListener('error', xhrError); | 82 req.addEventListener('error', xhrError); |
| 233 req.overrideMimeType('application/json'); | 83 req.overrideMimeType('application/json'); |
| 234 req.open('GET', this.getAttribute('data-try'), true); | 84 req.open('GET', this.getAttribute('data-try'), true); |
| 235 req.send(); | 85 req.send(); |
| 236 } | 86 } |
| 237 | 87 |
| 238 | 88 |
| 239 /** | 89 /** |
| 240 * Callback for when the XHR kicked off in historyClick() returns. | 90 * Callback for when the XHR kicked off in historyClick() returns. |
| 241 */ | 91 */ |
| 242 function historyComplete(e) { | 92 function historyComplete(e) { |
| 243 // The response is JSON of the form: | 93 // The response is JSON of the form: |
| 244 // { | 94 // { |
| 245 // "hash": "unique id for a try", | 95 // "hash": "unique id for a try", |
| 246 // "code": "source code for try" | 96 // "code": "source code for try" |
| 247 // } | 97 // } |
| 248 endWait(); | 98 endWait(); |
| 249 body = JSON.parse(e.target.response); | 99 body = JSON.parse(e.target.response); |
| 250 code.value = body.code; | 100 code.value = body.code; |
| 251 editor.setValue(body.code); | 101 editor.setValue(body.code); |
| 252 img.src = '/i/'+body.hash+'.png'; | 102 img.src = '/i/'+body.hash+'.png'; |
| 253 if (permalink) { | 103 if (permalink) { |
| 254 permalink.href = '/c/' + body.hash; | 104 permalink.href = '/c/' + body.hash; |
| 105 } |
| 106 } |
| 107 |
| 108 |
| 109 /** |
| 110 * Add the given try image to the history of a workspace. |
| 111 */ |
| 112 function addToHistory(hash, imgUrl) { |
| 113 var clone = tryTemplate.content.cloneNode(true); |
| 114 clone.querySelector('img').src = imgUrl; |
| 115 clone.querySelector('.tries').setAttribute('data-try', '/json/' + hash); |
| 116 tryHistory.insertBefore(clone, tryHistory.firstChild); |
| 117 tryHistory.querySelector('.tries').addEventListener('click', historyClic
k, true); |
| 118 } |
| 119 |
| 120 |
| 121 /** |
| 122 * Callback for when the XHR returns after attempting to run the code. |
| 123 * @param e The callback event. |
| 124 */ |
| 125 function codeComplete(e) { |
| 126 // The response is JSON of the form: |
| 127 // { |
| 128 // "message": "you had an error...", |
| 129 // "img": "<base64 encoded image but only on success>" |
| 130 // } |
| 131 // |
| 132 // The img is optional and only appears if there is a valid |
| 133 // image to display. |
| 134 endWait(); |
| 135 console.log(e.target.response); |
| 136 body = JSON.parse(e.target.response); |
| 137 output.textContent = body.message; |
| 138 if (stdout) { |
| 139 stdout.textContent = body.stdout; |
| 140 } |
| 141 if (body.hasOwnProperty('img')) { |
| 142 img.src = 'data:image/png;base64,' + body.img; |
| 143 } else { |
| 144 img.src = ''; |
| 145 } |
| 146 // Add the image to the history if we are on a workspace page. |
| 147 if (tryHistory) { |
| 148 addToHistory(body.hash, 'data:image/png;base64,' + body.img); |
| 149 } else { |
| 150 window.history.pushState(null, null, '/c/' + body.hash); |
| 151 } |
| 152 if (permalink) { |
| 153 permalink.href = '/c/' + body.hash; |
| 154 } |
| 155 if (embed) { |
| 156 var url = document.URL; |
| 157 url = url.replace('/c/', '/iframe/'); |
| 158 embed.value = '<iframe src="' + url + '" width="740" height="550" styl
e="border: solid #00a 5px; border-radius: 5px;"/>' |
| 159 } |
| 160 if (embedButton && embedButton.hasAttribute('disabled')) { |
| 161 embedButton.removeAttribute('disabled'); |
| 162 } |
| 163 } |
| 164 |
| 165 |
| 166 function onSubmitCode() { |
| 167 beginWait(); |
| 168 clearOutput(); |
| 169 var req = new XMLHttpRequest(); |
| 170 req.addEventListener('load', codeComplete); |
| 171 req.addEventListener('error', xhrError); |
| 172 req.overrideMimeType('application/json'); |
| 173 req.open('POST', '/', true); |
| 174 req.setRequestHeader('content-type', 'application/json'); |
| 175 req.send(JSON.stringify({'code': editor.getValue(), 'name': workspaceNam
e})); |
| 176 } |
| 177 run.addEventListener('click', onSubmitCode); |
| 178 |
| 179 |
| 180 function onEmbedClick() { |
| 181 embed.style.display='inline'; |
| 182 } |
| 183 |
| 184 if (embedButton) { |
| 185 embedButton.addEventListener('click', onEmbedClick); |
| 186 } |
| 187 |
| 188 // Add the images to the history if we are on a workspace page. |
| 189 if (tryHistory && history) { |
| 190 for (var i=0; i<history.length; i++) { |
| 191 addToHistory(history[i].hash, '/i/'+history[i].hash+'.png'); |
| 192 } |
| 255 } | 193 } |
| 256 } | 194 } |
| 257 | 195 |
| 258 | 196 // If loaded via HTML Imports then DOMContentLoaded will be long done. |
| 259 /** | 197 if (document.readyState != "loading") { |
| 260 * Add the given try image to the history of a workspace. | 198 onLoad(); |
| 261 */ | 199 } else { |
| 262 function addToHistory(hash, imgUrl) { | 200 this.addEventListener('DOMContentLoaded', onLoad); |
| 263 var clone = tryTemplate.content.cloneNode(true); | |
| 264 clone.querySelector('img').src = imgUrl; | |
| 265 clone.querySelector('.tries').setAttribute('data-try', '/json/' + hash); | |
| 266 tryHistory.insertBefore(clone, tryHistory.firstChild); | |
| 267 tryHistory.querySelector('.tries').addEventListener('click', historyClick,
true); | |
| 268 } | 201 } |
| 269 | 202 |
| 270 | 203 })(); |
| 271 /** | |
| 272 * Callback for when the XHR returns after attempting to run the code. | |
| 273 * @param e The callback event. | |
| 274 */ | |
| 275 function codeComplete(e) { | |
| 276 // The response is JSON of the form: | |
| 277 // { | |
| 278 // "message": "you had an error...", | |
| 279 // "img": "<base64 encoded image but only on success>" | |
| 280 // } | |
| 281 // | |
| 282 // The img is optional and only appears if there is a valid | |
| 283 // image to display. | |
| 284 endWait(); | |
| 285 console.log(e.target.response); | |
| 286 body = JSON.parse(e.target.response); | |
| 287 output.textContent = body.message; | |
| 288 if (stdout) { | |
| 289 stdout.textContent = body.stdout; | |
| 290 } | |
| 291 if (body.hasOwnProperty('img')) { | |
| 292 img.src = 'data:image/png;base64,' + body.img; | |
| 293 } else { | |
| 294 img.src = ''; | |
| 295 } | |
| 296 // Add the image to the history if we are on a workspace page. | |
| 297 if (tryHistory) { | |
| 298 addToHistory(body.hash, 'data:image/png;base64,' + body.img); | |
| 299 } else { | |
| 300 window.history.pushState(null, null, '/c/' + body.hash); | |
| 301 } | |
| 302 if (permalink) { | |
| 303 permalink.href = '/c/' + body.hash; | |
| 304 } | |
| 305 if (embed) { | |
| 306 var url = document.URL; | |
| 307 url = url.replace('/c/', '/iframe/'); | |
| 308 embed.value = '<iframe src="' + url + '" width="740" height="550" style=
"border: solid #00a 5px; border-radius: 5px;"/>' | |
| 309 } | |
| 310 if (embedButton && embedButton.hasAttribute('disabled')) { | |
| 311 embedButton.removeAttribute('disabled'); | |
| 312 } | |
| 313 } | |
| 314 | |
| 315 | |
| 316 function onSubmitCode() { | |
| 317 beginWait(); | |
| 318 clearOutput(); | |
| 319 var req = new XMLHttpRequest(); | |
| 320 req.addEventListener('load', codeComplete); | |
| 321 req.addEventListener('error', xhrError); | |
| 322 req.overrideMimeType('application/json'); | |
| 323 req.open('POST', '/', true); | |
| 324 req.setRequestHeader('content-type', 'application/json'); | |
| 325 req.send(JSON.stringify({'code': editor.getValue(), 'name': workspaceName}
)); | |
| 326 } | |
| 327 run.addEventListener('click', onSubmitCode); | |
| 328 | |
| 329 | |
| 330 function onEmbedClick() { | |
| 331 embed.style.display='inline'; | |
| 332 } | |
| 333 | |
| 334 if (embedButton) { | |
| 335 embedButton.addEventListener('click', onEmbedClick); | |
| 336 } | |
| 337 | |
| 338 | |
| 339 // Add the images to the history if we are on a workspace page. | |
| 340 if (tryHistory && history) { | |
| 341 for (var i=0; i<history.length; i++) { | |
| 342 addToHistory(history[i].hash, '/i/'+history[i].hash+'.png'); | |
| 343 } | |
| 344 } | |
| 345 | |
| 346 })(workspaceName); | |
| OLD | NEW |