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 |