window.addEventListener("load", () => { // If there's no ecmascript 5 support, don't try to initialize if (!Object.create || !window.JSON) return let sandboxHint = null if (window.chapNum && window.chapNum < 20 && window.localStorage && !localStorage.getItem("usedSandbox")) { let pres = document.getElementsByTagName("pre") for (let i = 0; i < pres.length; i++) { let pre = pres[i] if (!/^(text\/)?(javascript|html)$/.test(pre.getAttribute("data-language")) || chapNum == 1 && !/console\.log/.test(pre.textContent)) continue sandboxHint = elt("div", {"class": "sandboxhint"}, "edit & run code by clicking it") pre.insertBefore(sandboxHint, pre.firstChild) break } } document.body.addEventListener("click", e => { for (let n = e.target; n; n = n.parentNode) { if (n.className == "c_ident") return let lang = n.nodeName == "PRE" && n.getAttribute("data-language") if (/^(text\/)?(javascript|html)$/.test(lang)) return activateCode(n, e, lang) if (n.nodeName == "DIV" && n.className == "solution") n.className = "solution open" } }) function elt(type, attrs) { let firstChild = 1 let node = document.createElement(type) if (attrs && typeof attrs == "object" && attrs.nodeType == null) { for (let attr in attrs) if (attrs.hasOwnProperty(attr)) { if (attr == "css") node.style.cssText = attrs[attr] else node.setAttribute(attr, attrs[attr]) } firstChild = 2 } for (let i = firstChild; i < arguments.length; ++i) { let child = arguments[i] if (typeof child == "string") child = document.createTextNode(child) node.appendChild(child) } return node } CodeMirror.commands[CodeMirror.keyMap.default.Down = "lineDownEscape"] = cm => { let cur = cm.getCursor() if (cur.line == cm.lastLine()) { document.activeElement.blur() return CodeMirror.Pass } else { cm.moveV(1, "line") } } CodeMirror.commands[CodeMirror.keyMap.default.Up = "lineUpEscape"] = cm => { let cur = cm.getCursor() if (cur.line == cm.firstLine()) { document.activeElement.blur() return CodeMirror.Pass } else { cm.moveV(-1, "line") } } let keyMap = { Esc(cm) { cm.display.input.blur() }, "Ctrl-Enter"(cm) { runCode(cm.state.context) }, "Cmd-Enter"(cm) { runCode(cm.state.context) }, "Ctrl-Down"(cm) { closeCode(cm.state.context) }, "Ctrl-Esc"(cm) { resetSandbox(cm.state.context.sandbox) }, "Cmd-Esc"(cm) { resetSandbox(cm.state.context.sandbox) } } let nextID = 0 let article = document.getElementsByTagName("article")[0] function activateCode(node, e, lang) { if (sandboxHint) { sandboxHint.parentNode.removeChild(sandboxHint) sandboxHint = null localStorage.setItem("usedSandbox", "true") } const codeId = node.firstChild.id let code = (window.localStorage && localStorage.getItem(codeId)) || node.textContent let wrap = node.parentNode.insertBefore(elt("div", {"class": "editor-wrap"}), node) let editor = CodeMirror(div => wrap.insertBefore(div, wrap.firstChild), { value: code, mode: lang, extraKeys: keyMap, matchBrackets: true, lineNumbers: true }) let pollingScroll = null function pollScroll() { if (document.activeElement != editor.getInputField()) return let rect = editor.getWrapperElement().getBoundingClientRect() if (rect.bottom < 0 || rect.top > innerHeight) editor.getInputField().blur() else pollingScroll = setTimeout(pollScroll, 500) } editor.on("focus", () => { clearTimeout(pollingScroll) pollingScroll = setTimeout(pollScroll, 500) }) if (window.localStorage) editor.on("change", debounce(() => localStorage.setItem(codeId, editor.getValue()), 250)) wrap.style.marginLeft = wrap.style.marginRight = -Math.min(article.offsetLeft, 100) + "px" setTimeout(() => editor.refresh(), 600) if (e) { editor.setCursor(editor.coordsChar({left: e.clientX, top: e.clientY}, "client")) editor.focus() } let out = wrap.appendChild(elt("div", {"class": "sandbox-output"})) let menu = wrap.appendChild(elt("div", {"class": "sandbox-menu", title: "Sandbox menu..."})) let sandbox = node.getAttribute("data-sandbox") if (lang == "text/html" && !sandbox) { sandbox = "html" + nextID++ node.setAttribute("data-sandbox", sandbox) sandboxSnippets[sandbox] = node } node.style.display = "none" let data = editor.state.context = {editor: editor, wrap: wrap, orig: node, isHTML: lang == "text/html", sandbox: sandbox, meta: node.getAttribute("data-meta")} data.output = new SandBox.Output(out) menu.addEventListener("click", () => openMenu(data, menu)) } function openMenu(data, node) { let menu = elt("div", {"class": "sandbox-open-menu"}) let items = [["Run code (ctrl/cmd-enter)", () => runCode(data)], ["Revert to original code", () => revertCode(data)], ["Reset sandbox (ctrl/cmd-esc)", () => resetSandbox(data.sandbox)]] if (!data.isHTML || !data.sandbox) items.push(["Deactivate editor (ctrl-down)", () => { closeCode(data) }]) items.forEach(choice => menu.appendChild(elt("div", choice[0]))) function click(e) { let target = e.target if (e.target.parentNode == menu) { for (let i = 0; i < menu.childNodes.length; ++i) if (target == menu.childNodes[i]) items[i][1]() } menu.parentNode.removeChild(menu) window.removeEventListener("click", click) } setTimeout(() => window.addEventListener("click", click), 20) node.offsetParent.appendChild(menu) } function runCode(data) { data.output.clear() let val = data.editor.getValue() getSandbox(data.sandbox, data.isHTML, box => { if (data.isHTML) box.setHTML(val, data.output, () => { if (data.orig.getAttribute("data-focus")) { box.win.focus() box.win.document.body.focus() } }) else box.run(val, data.output, data.meta) }) } function closeCode(data) { if (data.isHTML && data.sandbox) return data.wrap.parentNode.removeChild(data.wrap) data.orig.style.display = "" } function revertCode(data) { data.editor.setValue(data.orig.textContent) } let sandboxSnippets = {} { let snippets = document.getElementsByClassName("snippet") for (let i = 0; i < snippets.length; i++) { let snippet = snippets[i] if (snippet.getAttribute("data-language") == "text/html" && snippet.getAttribute("data-sandbox")) sandboxSnippets[snippet.getAttribute("data-sandbox")] = snippet } } let sandboxes = {} function getSandbox(name, forHTML, callback) { name = name || "null" if (sandboxes.hasOwnProperty(name)) return callback(sandboxes[name]) let options = {loadFiles: window.sandboxLoadFiles}, html if (sandboxSnippets.hasOwnProperty(name)) { let snippet = sandboxSnippets[name] options.place = node => placeFrame(node, snippet) if (!forHTML) html = snippet.textContent } SandBox.create(options).then(box => { if (html != null) box.win.document.documentElement.innerHTML = html sandboxes[name] = box callback(box) }) } function resetSandbox(name) { if (!sandboxes.hasOwnProperty(name)) return let frame = sandboxes[name].frame frame.parentNode.removeChild(frame) delete sandboxes[name] } function placeFrame(frame, snippet) { let wrap = snippet.previousSibling, bot if (!wrap || wrap.className != "editor-wrap") { bot = snippet.getBoundingClientRect().bottom activateCode(snippet, null, "text/html") wrap = snippet.previousSibling } else { bot = wrap.getBoundingClientRect().bottom } wrap.insertBefore(frame, wrap.childNodes[1]) if (bot < 50) { let newBot = wrap.getBoundingClientRect().bottom window.scrollBy(0, newBot - bot) } } function debounce(fn, delay = 50) { let timeout return () => { if (timeout) clearTimeout(timeout) timeout = setTimeout(() => fn.apply(null, arguments), delay) } } })