"use strict"; var gSocket; var gCredentials; var gErrorCount = 0; var gCommandHistory = []; var gSendKeyEvents = false; var gSendDeviceOrientationEvents = false; var gGeolocatorWatch; var gEditor; var gBackup; var kMaxCommandHistory = 16; var kErrorColor = "#dc322f"; var kStatusColor = "#fff"; window.addEventListener("keydown", function(event) { if (event.keyCode == 69 && event.altKey) { if (!editing()) { edit(); event.preventDefault(); } } else if (event.keyCode == 83 && (event.altKey || event.ctrlKey)) { if (editing()) { save(); event.preventDefault(); } } else if (event.keyCode == 66 && event.altKey) { if (editing()) { closeEditor(); event.preventDefault(); } } }); function keydown(event) { if (event.keyCode == 13) { gCommandHistory.push(document.getElementById("input").value); if (gCommandHistory.length > kMaxCommandHistory) { gCommandHistory.shift(); } send(); event.preventDefault(); } else if (event.keyCode == 38 && !event.altKey) { if (gCommandHistory.length) { var input = document.getElementById("input"); gCommandHistory.unshift(input.value); input.value = gCommandHistory.pop(); event.preventDefault(); } } else if (event.keyCode == 40 && !event.altKey) { if (gCommandHistory.length) { var input = document.getElementById("input"); gCommandHistory.push(input.value); input.value = gCommandHistory.shift(); event.preventDefault(); } } } function ensureLoaded(nodes, callback) { if (!nodes.length) { callback(); return; } var search = nodes.shift(); var head = document.head; var found = false; for (var i = 0; i < head.childNodes.length; i++) { if (head.childNodes[i].tagName == search.tagName) { var match = true; for (var attribute in search.attributes) { if (head.childNodes[i].attributes[attribute].value != search.attributes[attribute]) { match = false; } } if (match) { found = true; break; } } } if (found) { ensureLoaded(nodes, callback); } else { var node = document.createElement(search.tagName); node.onreadystatechange = node.onload = function() { ensureLoaded(nodes, callback); }; for (var attribute in search.attributes) { node.setAttribute(attribute, search.attributes[attribute]); } head.insertBefore(node, head.firstChild); } } function editing() { return document.getElementById("editPane").style.display != 'none'; } function edit() { if (editing()) { return; } ensureLoaded([ {tagName: "script", attributes: {src: "https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.23.0/codemirror.min.js"}}, {tagName: "link", attributes: {rel: "stylesheet", href: "https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.23.0/theme/base16-dark.min.css"}}, {tagName: "link", attributes: {rel: "stylesheet", href: "https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.23.0/addon/search/matchesonscrollbar.min.css"}}, {tagName: "link", attributes: {rel: "stylesheet", href: "https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.23.0/addon/dialog/dialog.min.css"}}, {tagName: "link", attributes: {rel: "stylesheet", href: "https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.23.0/codemirror.min.css"}}, {tagName: "script", attributes: {src: "https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.23.0/addon/edit/trailingspace.min.js"}}, {tagName: "script", attributes: {src: "https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.23.0/addon/dialog/dialog.min.js"}}, {tagName: "script", attributes: {src: "https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.23.0/addon/search/search.min.js"}}, {tagName: "script", attributes: {src: "https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.23.0/addon/search/searchcursor.min.js"}}, {tagName: "script", attributes: {src: "https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.23.0/addon/search/jump-to-line.min.js"}}, {tagName: "script", attributes: {src: "https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.23.0/addon/search/matchesonscrollbar.min.js"}}, {tagName: "script", attributes: {src: "https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.23.0/addon/scroll/annotatescrollbar.min.js"}}, {tagName: "script", attributes: {src: "https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.23.0/mode/javascript/javascript.min.js"}}, ], function() { load(); }); } function load() { var request = new XMLHttpRequest(); request.addEventListener("loadend", function() { if (request.status == 200 || request.status == 404) { document.getElementById("editPane").style.display = 'flex'; if (!gEditor) { gEditor = CodeMirror.fromTextArea(document.getElementById("editor"), { 'theme': 'base16-dark', 'lineNumbers': true, 'tabSize': 4, 'indentUnit': 4, 'indentWithTabs': true, 'showTrailingSpace': true, }); } var text; if (request.status == 200) { text = request.responseText; } else { text = '// New script\nterminal.print("Hello, world!");\n'; } gEditor.setValue(text); gEditor.focus(); gBackup = text; } }); request.addEventListener("error", function() { alert("Error loading source."); closeEditor(); }); request.addEventListener("timeout", function() { alert("Timed out loading source."); closeEditor(); }); request.addEventListener("abort", function() { alert("Loading source aborted."); closeEditor(); }); request.open("GET", url() + "/view"); request.send(); } function closeEditor() { document.getElementById("editPane").style.display = 'none'; } function revert() { gEditor.setValue(gBackup); } function explodePath() { return /^\/~([^\/]+)\/([^\/]+)(.*)/.exec(window.location.pathname); } function packageOwner() { return explodePath()[1]; } function packageName() { return explodePath()[2]; } function save(newName) { document.getElementById("save").disabled = true; document.getElementById("saveAs").disabled = true; var contents = gEditor.getValue(); var run = document.getElementById("run").checked; var request = new XMLHttpRequest(); var always = function() { document.getElementById("save").disabled = false; document.getElementById("saveAs").disabled = false; }; request.addEventListener("error", function() { alert("Error saving: " + request.responseText); always(); }); request.addEventListener("loadend", function() { if (request.status == 200) { gBackup = contents; if (run) { if (newName) { window.location.href = "/~" + packageOwner() + "/" + newName + hash(); } else { reconnect(); } } } else { alert("Unable to save: " + request.responseText); } always(); }); request.addEventListener("timeout", function() { alert("Timed out saving: " + request.responseText); always(); }); request.addEventListener("abort", function() { alert("Save aborted: " + request.responseText); always(); }); request.open("POST", newName ? newName + "/save" : packageName() + "/save", true); request.setRequestHeader("Content-Type", "text/plain"); request.send(contents); } function saveAs() { var newName = prompt("Save as:", packageName()); if (newName) { save(newName); } } function url() { var hash = window.location.href.indexOf('#'); var question = window.location.href.indexOf('?'); var end = -1; if (hash != -1 && (hash < end || end == -1)) { end = hash; } if (question != -1 && (question < end || end == -1)) { end = question; } return end != -1 ? window.location.href.substring(0, end) : window.location.href; } function hash() { return window.location.hash != "#" ? window.location.hash : ""; } function split(container, children) { if (container) { while (container.firstChild) { container.removeChild(container.firstChild); } } if (children) { for (var i = 0; i < children.length; i++) { if (children[i].name) { var node = document.createElement("div"); node.setAttribute("id", "terminal_" + children[i].name); var grow = children[i].grow || "1"; var shrink = children[i].shrink || "1"; var basis = children[i].basis || "auto"; var style = children[i].style || ""; node.setAttribute("style", style + "; flex: " + grow + " " + shrink + " " + basis); var classes = ["terminal"]; if (children[i].type == "vertical") { classes.push("vbox"); } else if (children[i].type == "horizontal") { classes.push("hbox"); } node.setAttribute("class", classes.join(" ")); container.appendChild(node); } else if (children[i].type) { node = document.createElement("div"); if (children[i].type == "horizontal") { node.setAttribute("class", "hbox"); } else if (children[i].type == "vertical") { node.setAttribute("class", "vbox"); } container.appendChild(node); split(node, children[i].children); } } } } var gCorkCount = 0; var gCorkLines = []; function receive(data) { for (var i in data.lines) { var line = data.lines[i]; if (line && line[0] && line[0].action == "cork" || line && line.value && line.value[0].action == "cork") { ++gCorkCount; continue; } else if (line && line[0] && line[0].action == "uncork" || line && line.value && line.value[0].action == "uncork") { if (--gCorkCount <= 0) { receive({lines: gCorkLines}); gCorkLines.length = 0; } continue; } if (gCorkCount > 0) { gCorkLines.push(line); continue; } var target = document.getElementsByClassName("terminal")[0].id; if (line && line.terminal) { if (document.getElementById("terminal_" + line.terminal)) { target = "terminal_" + line.terminal; } line = line.value; } if (line && line.action == "ping") { gSocket.send(JSON.stringify({action: "pong"})); } else if (line && line.action == "session") { setStatusMessage("...Executing...", kStatusColor, true); gCredentials = line.credentials; updateLogin(); } else if (line && line[0] && line[0].action == "ready") { setStatusMessage(null); if (window.location.hash) { send({event: "hashChange", hash: window.location.hash}); } } else if (line && line[0] && line[0].action == "notify") { if (window.Notification) { new Notification(line[0].title, line[0].options); } } else if (line && line[0] && line[0].action == "setTitle") { window.document.title = line[0].value; } else if (line && line[0] && line[0].action == "setPrompt") { var prompt = document.getElementById("prompt"); while (prompt.firstChild) { prompt.removeChild(prompt.firstChild); } prompt.appendChild(document.createTextNode(line[0].value)); } else if (line && line[0] && line[0].action == "setPassword") { var prompt = document.getElementById("input"); prompt.setAttribute("type", line[0].value ? "password" : "text"); } else if (line && line[0] && line[0].action == "setHash") { window.location.hash = line[0].value; } else if (line && line[0] && line[0].action == "update") { document.getElementById("update").setAttribute("Style", "display: inline"); } else if (line && line[0] && line[0].action == "split") { split(document.getElementById("terminals"), line[0].options); } else if (line && line[0] && line[0].action == "postMessageToIframe") { var iframe = document.getElementById("iframe_" + line[0].name); if (iframe) { iframe.contentWindow.postMessage(line[0].message, "*"); } } else if (line && line[0] && line[0].action == "setSendKeyEvents") { var value = line[0].value; if (value && !gSendKeyEvents) { window.addEventListener("keydown", keyEvent); window.addEventListener("keypress", keyEvent); window.addEventListener("keyup", keyEvent); } else if (!value && gSendKeyEvents) { window.removeEventListener("keydown", keyEvent); window.removeEventListener("keypress", keyEvent); window.removeEventListener("keyup", keyEvent); } gSendKeyEvents = value; } else if (line && line[0] && line[0].action == "getCurrentPosition") { navigator.geolocation.getCurrentPosition(geolocationPosition, geolocationError, line[0].options); } else if (line && line[0] && line[0].action == "watchPosition") { if (navigator && navigator.geolocation && gGeolocatorWatch === undefined) { gGeolocatorWatch = navigator.geolocation.watchPosition(geolocationPosition, geolocationError, line[0].options); } } else if (line && line[0] && line[0].action == "clearWatch") { if (navigator && navigator.geolocation && gGeolocatorWatch !== undefined) { navigator.geolocation.clearWatch(gGeolocatorWatch); } } else if (line && line[0] && line[0].action == "setSendDeviceOrientationEvents") { let value = line[0].value; if (value && !gSendDeviceOrientationEvents) { window.addEventListener("deviceorientation", deviceOrientation); } else if (!value && gSendDeviceOrientationEvents) { window.removeEventListener("deviceorientation", deviceOrientation); } gSendDeviceOrientationEvents = value; } else { print(document.getElementById(target), line); } } } function geolocationPosition(position) { send({ event: 'geolocation', position: { timestamp: position.timestamp, coords: { latitude: position.coords.latitude, longitude: position.coords.longitude, altitude: position.coords.altitude, accuracy: position.coords.accuracy, altitudeAccuracy: position.coords.altitudeAccuracy, heading: position.coords.heading, speed: position.coords.speed, }, }, }); } function geolocationError(error) { send({ event: 'geolocation', error: { code: error.code, message: error.message, }, }); } function deviceOrientation(event) { send({ event: 'deviceorientation', orientation: { alpha: event.alpha, beta: event.beta, gamma: event.gamma, absolute: event.absolute, }, }); }; function keyEvent(event) { send({ event: "key", type: event.type, which: event.which, keyCode: event.keyCode, charCode: event.charCode, character: String.fromCharCode(event.keyCode || event.which), altKey: event.altKey, }); } function autoNewLine(terminal) { terminal.appendChild(document.createElement("br")); } function print(terminal, data) { autoNewLine(terminal); printStructured(terminal, data); autoScroll(terminal); } function printSvg(container, data, name, namespace) { var node; if (typeof data == "string") { node = document.createTextNode(data); } else { node = document.createElementNS("http://www.w3.org/2000/svg", name); for (var i in data.attributes) { node.setAttribute(i, data.attributes[i]); } if (data.children) { for (var i in data.children) { node.appendChild(printSvg(node, data.children[i], data.children[i].name)); } } } return node; } function printStructured(container, data) { if (typeof data == "string") { container.appendChild(document.createTextNode(data)); } else if (data && data[0] !== undefined) { for (var i in data) { printStructured(container, data[i]); } } else if (data && data.action == "clear") { while (container.firstChild) { container.removeChild(container.firstChild); } } else if (data) { var node; if (data.href) { node = document.createElement("a"); node.setAttribute("href", data.href); node.setAttribute("target", data.target || "_blank"); } else if (data.iframe) { node = document.createElement("iframe"); if (data.src) { node.setAttribute("src", data.src); } else { node.setAttribute("srcdoc", data.iframe); } node.setAttribute("sandbox", "allow-forms allow-scripts allow-top-navigation allow-same-origin"); if (data.width !== null) { node.setAttribute("width", data.width || 320); } if (data.height !== null) { node.setAttribute("height", data.height || 240); } if (data.name) { node.setAttribute("id", "iframe_" + data.name); } } else if (data.svg) { node = printSvg(container, data.svg, "svg"); } else if (data.image) { node = document.createElement("img"); node.setAttribute("src", data.image); } else if (data.input) { node = document.createElement("input"); node.setAttribute("type", data.input); if (data.name) { node.name = data.name; } if (data.input == "submit") { node.onclick = submitButton; } } else { node = document.createElement("span"); } if (data.style) { node.setAttribute("style", data.style); } if (data.class) { node.setAttribute("class", data.class); } var value = data.value || data.href || data.command || ""; if (data.input) { node.value = value; } else if (!value && data.message && data.stackTrace) { printStructured(node, data.message); node.appendChild(document.createElement("br")); printStructured(node, data.fileName + ":" + data.lineNumber + ":"); node.appendChild(document.createElement("br")); if (data.stackTrace.length) { for (var i = 0; i < data.stackTrace.length; i++) { printStructured(node, data.stackTrace[i]); node.appendChild(document.createElement("br")); } } else { printStructured(node, data.sourceLine); } } else if (value === undefined) { printStructured(node, JSON.stringify(value)); } else { printStructured(node, value); } if (data.command) { node.dataset.command = data.command; node.onclick = commandClick; node.setAttribute("class", "command"); } container.appendChild(node); } else { printStructured(container, JSON.stringify(data)); } } function commandClick() { send(this.dataset.command); document.getElementById("input").focus(); } function autoScroll(terminal) { terminal.scrollTop = terminal.scrollHeight - terminal.clientHeight; } function setStatusMessage(message, color, keep) { var node = document.getElementById("status"); if (!keep) { while (node.firstChild) { node.removeChild(node.firstChild); } } if (message) { node.appendChild(document.createTextNode(message)); node.setAttribute("style", "display: inline; color: " + (color || kErrorColor)); } } function send(command) { var value = command; if (!command) { value = document.getElementById("input").value; document.getElementById("input").value = ""; } try { gSocket.send(JSON.stringify({action: "command", command: value})); } catch (error) { setStatusMessage("Send failed: " + error.toString(), kErrorColor); } } function updateLogin() { var login = document.getElementById("login"); while (login.firstChild) { login.removeChild(login.firstChild); } var a = document.createElement("a"); if (gCredentials && gCredentials.session) { a.appendChild(document.createTextNode("logout " + gCredentials.session.name)); if (gCredentials.session.google) { gapi.load("auth2", function() { gapi.auth2.init(); }); a.setAttribute("onclick", "logoutGoogle()"); a.setAttribute("href", "#"); } else { a.setAttribute("href", "/login/logout?return=" + encodeURIComponent(url() + hash())); } } else { a.appendChild(document.createTextNode("login")); a.setAttribute("href", "/login?return=" + encodeURIComponent(url() + hash())); } login.appendChild(a); } function logoutGoogle() { gapi.auth2.getAuthInstance().signOut().then(function() { window.location.href = "/login/logout?return=" + encodeURIComponent(url() + hash()); }); } var gOriginalInput; function dragHover(event) { event.stopPropagation(); event.preventDefault(); var input = document.getElementById("input"); if (event.type == "dragover") { if (!input.classList.contains("drop")) { input.classList.add("drop"); gOriginalInput = input.value; input.value = "drop file to upload"; } } else { input.classList.remove("drop"); input.value = gOriginalInput; } } function fixImage(sourceData, maxWidth, maxHeight, callback) { var result = sourceData; var image = new Image(); image.crossOrigin = "anonymous"; image.referrerPolicy = "no-referrer"; image.onload = function() { if (image.width > maxWidth || image.height > maxHeight) { var downScale = Math.min(maxWidth / image.width, maxHeight / image.height); var canvas = document.createElement("canvas"); canvas.width = image.width * downScale; canvas.height = image.height * downScale; var context = canvas.getContext("2d"); context.clearRect(0, 0, canvas.width, canvas.height); image.width = canvas.width; image.height = canvas.height; context.drawImage(image, 0, 0, image.width, image.height); result = canvas.toDataURL(); } callback(result); }; image.src = sourceData; } function sendImage(image) { fixImage(image, 320, 240, function(result) { send({image: result}); }); } function fileDropRead(event) { sendImage(event.target.result); } function fileDrop(event) { dragHover(event); var done = false; if (!done) { var files = event.target.files || event.dataTransfer.files; for (var i = 0; i < files.length; i++) { var file = files[i]; if (file.type.substring(0, "image/".length) == "image/") { var reader = new FileReader(); reader.onloadend = fileDropRead; reader.readAsDataURL(file); done = true; } } } if (!done) { var html = event.dataTransfer.getData("text/html"); var match = /