*scratch*.js
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="utf-8" /> | |
<meta name="viewport" content="width=device-width, initial-scale=1" /> | |
<title>*scratch*</title> | |
<style> | |
body { | |
font-family: Hack, Menlo, Monaco, 'Droid Sans Mono', 'Courier New', monospace; | |
white-space: pre; | |
} | |
body style { | |
display: inline; | |
} | |
style::before { | |
content: '<style>' | |
} | |
style::after { | |
content: '<\/style>' | |
} | |
*::before, | |
*::after { | |
color: rgba(136, 18, 128, 0.5); | |
} | |
.definitions { | |
white-space: pre; | |
position:relative; | |
color:blueviolet; | |
background-color:#fee; | |
display:inline-block; | |
padding:10px; | |
border:1px solid plum; | |
} | |
.definitions::before { | |
position:absolute; | |
left:0px; | |
top:-15px; | |
height:10px; | |
content:"definitions"; | |
font-size: x-small; | |
padding:2px; | |
padding-right:5px; | |
background-color:#fee; | |
border:1px solid plum; | |
border-bottom:none; | |
border-radius: 0px 10px 0px 0px; | |
} | |
.error { | |
background-color: pink; | |
} | |
</style> | |
<script> | |
const selectOuterMostNonBodyNode = (node) => node.parentNode.tagName === 'BODY' ? node : node.parentNode; | |
function insertAfterSelection(selection, content) { | |
if (content === '') | |
return; | |
let nodeToInsert = document.createElement('div'); | |
if (content instanceof Element) { | |
nodeToInsert.appendChild(content) | |
} else if (typeof content === "function") { | |
nodeToInsert.innerText=content; | |
} else | |
nodeToInsert.innerHTML = content + '<br>'; | |
let range = selection.getRangeAt(0); | |
let clone = range.cloneRange(); | |
let { endContainer } = range; | |
range.setStartAfter(selectOuterMostNonBodyNode(endContainer)); | |
range.insertNode(nodeToInsert); | |
clone.setStart(clone.endContainer, clone.endOffset); | |
selection.removeRange(range); | |
selection.addRange(clone); | |
} | |
var globalEval = eval; | |
function removeErrors() { | |
let errorNotes=document.querySelectorAll('.error') | |
for (let e of errorNotes) { | |
e.parentNode.removeChild(e); | |
} | |
} | |
function evaluateDefinitions ( ) { | |
removeErrors(); | |
let defBlocks = document.querySelectorAll(".definitions"); | |
for (let b of defBlocks) { | |
try { | |
globalEval(b.innerText); | |
} | |
catch (e) { | |
console.log("eval error",{e}); | |
let errorNote=document.createElement("div"); | |
errorNote.classList.add("error"); | |
let line = (e.lineNumber)?" on line "+e.lineNumber+" ":""; | |
errorNote.innerText="error "+line+e.message; | |
b.after(errorNote); | |
throw e; | |
} | |
} | |
} | |
function evaluate() { | |
evaluateDefinitions(); | |
let selection = document.getSelection(); | |
let text = selection.toString(); | |
if (text.trim().length === 0) { | |
selection.modify("move", "backward", "lineboundary"); | |
selection.modify("extend", "forward", "lineboundary"); | |
text = selection.toString(); | |
} | |
if (text.trim().length > 0) { | |
let result = '<b>#!Error</b>'; | |
try { | |
result = globalEval(text); | |
} catch (e) { | |
console.log(e); | |
} | |
insertAfterSelection(selection, result); | |
} | |
} | |
let defs=` | |
function graph(fn) { | |
let result=document.createElement("canvas"); | |
let ctx=result.getContext("2d"); | |
ctx.beginPath(); | |
for (let tx=0; tx<result.width; tx++) { | |
let x=tx/result.width; | |
ctx.lineTo(tx,fn(x)*result.height); | |
} | |
ctx.stroke(); | |
return result; | |
} | |
`; | |
let tutorial = `<div><br></div><pre class="definitions">${defs}</pre><div><br></div><div><br></div><div>"Press Ctrl+Enter to evaluate current line or selection"<br></div><div><br></div><div><br></div><div>3 * 33 + 7<br></div><div><br></div><div><br></div><div>"<b>Result is added as HTML! (execute this line)</b>"<br></div><div><br></div><div><br></div><div>Math.random() > 0.3 ? "<style>body {background-color: #fdf6e3;}</style>" : "try again!";<br></div><div><br></div><div>graph(a=>Math.sin(a*20)/2+0.5);<br></div><div><br></div><div style="color: green;">// The document state is stored in localStorage on page-unload.<br></div><div><br></div><div><br></div><div style="color: green;">// Built-in functions<br></div><div><br></div><div style="color: green;"><i style="color: red;">reset()</i> // - reset the doc to this tutorial<br></div><div style="color: green;"><i style="color: red;">clear()</i> // - empty the doc<br></div><div style="color: green;"><i style="color: blue;">save()</i> // - save doc state to localStorage<br></div><div><br></div><div><br></div><div style="color: green;">// Feel free to save the source of this page and use it locally, it'll work the same.<br></div><div><br></div><div style="color: #585858;"> window.open("https://gist.github.com/kahole/651990b888c19b84d5700422daa961de", "_blank")<br></div><div><br></div><div><br></div><div style="color: green;">// Inspirational works<br></div><div><br></div><div style="color: #585858;">[ "Emacs scratch buffer", window.open("https://www.gnu.org/software/emacs/manual/html_node/emacs/Lisp-Interaction.html", "_blank") ]<br></div><div><br></div><div style="color: #585858;">[ "Secretgeek's html quine", window.open("https://secretgeek.github.io/html_wysiwyg/html.html", "_blank") ]<br></div><div><br></div>`; | |
if (navigator.userAgent.indexOf('Mac OS X') != -1) { | |
tutorial = tutorial.replace('Ctrl', 'Cmd'); | |
} | |
const SCRATCH_STATE = 'scratch_state'; | |
function reset() { | |
document.body.innerHTML = tutorial; | |
return ''; | |
} | |
function clear() { | |
document.body.innerHTML = '<div><br></div>'; | |
return ''; | |
} | |
function save() { | |
localStorage.setItem(SCRATCH_STATE, document.body.innerHTML); | |
return 'saved'; | |
} | |
document.addEventListener('keydown', (e) => (e.code === 'Tab' && document.execCommand('insertText', false, ' ') && e.preventDefault())); | |
document.addEventListener('keypress', (e) => (e.ctrlKey && e.key === 'Enter' && evaluate() && e.preventDefault())); | |
document.addEventListener('keydown', (e) => (e.metaKey && e.key === 'Enter' && evaluate() && e.preventDefault())); | |
window.onbeforeunload = () => { save() }; | |
window.addEventListener('DOMContentLoaded', (e) => { | |
let storedState = localStorage.getItem(SCRATCH_STATE); | |
if (storedState !== null) { | |
document.body.innerHTML = storedState; | |
} else { | |
reset(); | |
} | |
document.querySelector('[contenteditable]').addEventListener('paste', function (event) { | |
event.preventDefault(); | |
document.execCommand('inserttext', false, event.clipboardData.getData('text/plain')); | |
}); | |
}); | |
</script> | |
</head> | |
<body contenteditable="true" spellcheck="false"></body> | |
</html> | |
<!-- MIT License | |
Copyright (c) 2020 Kristian Andersen Hole | |
Permission is hereby granted, free of charge, to any person obtaining a copy | |
of this software and associated documentation files (the "Software"), to deal | |
in the Software without restriction, including without limitation the rights | |
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
copies of the Software, and to permit persons to whom the Software is | |
furnished to do so, subject to the following conditions: | |
The above copyright notice and this permission notice shall be included in all | |
copies or substantial portions of the Software. | |
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
SOFTWARE. --> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment