diff options
Diffstat (limited to 'html/playground/index.html')
-rw-r--r-- | html/playground/index.html | 248 |
1 files changed, 248 insertions, 0 deletions
diff --git a/html/playground/index.html b/html/playground/index.html new file mode 100644 index 0000000..a236d2f --- /dev/null +++ b/html/playground/index.html @@ -0,0 +1,248 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>JavaScript Playground</title> + <meta name="description" content="A JavaScript jungle-gym for doing experiments and sharing scrappy fiddles."> + <style> + html, body { height: 100%; margin: 0; padding: 0; } + body { display: flex; flex-direction: column; background-color: #ddd; } + #app { display: flex; flex-direction: column; padding: 10px; height: 100%; box-sizing: border-box; } + .button-container { width: 100%; display: flex; justify-content: flex-end; margin-bottom: 10px; flex-shrink: 0; } + button { padding: 10px 20px; font-size: 1rem; margin-left: 10px; cursor: pointer; border: none; transition: background-color 0.3s ease; } + button:hover, button:focus { outline: none; } + button:disabled { background-color: #999; cursor: not-allowed; } + button.run { background-color: teal; color: #FFFFFF; } + textarea { width: 100%; height: 64%; font-family: monospace; background-color: #FFFEEC; border: 2px solid #000; scrollbar-width: none; font-size: 1rem; margin-bottom: 10px; padding: 10px; box-sizing: border-box; resize: none; border-bottom: 12px solid teal; -webkit-user-modify: read-write-plaintext-only; flex-shrink: 0; } + textarea::-webkit-scrollbar { display: none; } + textarea::selection { background-color: #EFECA7; } + textarea:focus { outline: none; } + #console { width: 100%; height: 22%; background-color: #000; color: #0fc; font-family: monospace; font-size: 1rem; overflow-y: auto; padding: 10px; box-sizing: border-box; white-space: pre-wrap; flex-grow: 1; } + #console .log-item { margin-bottom: 5px; border-bottom: 1px solid #333; } + #console details { cursor: pointer; } + #console summary { outline: none; } + </style> +</head> +<body> + <div id="app"></div> + + <iframe id="sandbox" style="display: none;" title="JavaScript Sandbox"></iframe> + + <script> + /** + * ---------------------------------------------------------------- + * Functional Utilities + * ---------------------------------------------------------------- + */ + + const pipe = (...fns) => (initialValue) => fns.reduce((acc, fn) => fn(acc), initialValue); + const compose = (...fns) => (initialValue) => fns.reduceRight((acc, fn) => fn(acc), initialValue); + + /** + * ---------------------------------------------------------------- + * The Elm Architecture (Model, View, Update) Implementation + * ---------------------------------------------------------------- + */ + + const init = { + code: ` + const add = (a, b) => a + b; + const multiply = (a, b) => a * b; + + const addAndMultiply = pipe(add, multiply); + + console.log(addAndMultiply(2, 3)); +`, + consoleOutput: [], + sandboxReady: false, + }; + + const view = (model) => ` + <div class="button-container"> + <button onclick="dispatch({ type: 'CLEAR' })">Clear</button> + <button onclick="dispatch({ type: 'DOWNLOAD' })">Download</button> + <button onclick="dispatch({ type: 'SHARE' })">Share</button> + <button onclick="dispatch({ type: 'RUN_CODE' })" class="run" ${!model.sandboxReady ? 'disabled title="Sandbox is loading..."' : ''}>Run Code</button> + </div> + <textarea id="codeInput" oninput="dispatch({ type: 'CODE_CHANGED', payload: this.value })" onkeydown="handleKeyDown(event)">${model.code}</textarea> + <div id="console"> + ${model.consoleOutput.map(log => { + if (typeof log === 'object' && log !== null) { + return `<div class="log-item"><details><summary>${Object.prototype.toString.call(log)}</summary><pre>${JSON.stringify(log, null, 2)}</pre></details></div>`; + } + return `<div class="log-item">${log}</div>`; + }).join('')} + </div> + `; + + const update = (msg, model) => { + switch (msg.type) { + case 'CODE_CHANGED': + return { ...model, code: msg.payload }; + + case 'RUN_CODE': + return model; + + case 'CONSOLE_LOG': + return { ...model, consoleOutput: [...model.consoleOutput, ...msg.payload] }; + + case 'CLEAR': + if (confirm('Are you sure you want to reset the playground?')) { + window.location.hash = ''; + window.location.reload(); + } + return model; + + case 'CLEAR_CONSOLE': + return { ...model, consoleOutput: [] }; + + case 'SHARE': + case 'DOWNLOAD': + return model; + + case 'SANDBOX_READY': + return { ...model, sandboxReady: true }; + + default: + return model; + } + }; + + /** + * ---------------------------------------------------------------- + * Application Core & Side-Effect Handling + * ---------------------------------------------------------------- + */ + + let model = null; + const appContainer = document.getElementById('app'); + const sandbox = document.getElementById('sandbox'); + + const dispatch = (msg) => { + const newModel = update(msg, model); + handleSideEffects(msg, newModel); + model = newModel; + render(model); + }; + + const handleSideEffects = (msg, model) => { + if (msg.type === 'RUN_CODE') { + if (!model.sandboxReady) return; + dispatch({ type: 'CLEAR_CONSOLE' }); + sandbox.contentWindow.postMessage({ code: model.code }, '*'); + } else if (msg.type === 'SHARE') { + const encodedCode = btoa(encodeURIComponent(model.code)); + window.location.hash = encodedCode; + window.prompt("Copy the URL to share.", window.location.href); + } else if (msg.type === 'DOWNLOAD') { + const htmlContent = document.documentElement.outerHTML.replace( + /<textarea id="codeInput"[^>]*>.*<\/textarea>/, + `<textarea id="codeInput">${model.code}</textarea>` + ); + const blob = new Blob([htmlContent], { type: 'text/html' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'playground.html'; + a.click(); + URL.revokeObjectURL(url); + } + }; + + const render = (model) => { + const focusedElementId = document.activeElement?.id; + const selectionStart = document.activeElement?.selectionStart; + const selectionEnd = document.activeElement?.selectionEnd; + + appContainer.innerHTML = view(model); + + if (focusedElementId === 'codeInput') { + const focusedElement = document.getElementById(focusedElementId); + if (focusedElement) { + focusedElement.focus(); + if (typeof selectionStart === 'number') { + focusedElement.selectionStart = selectionStart; + focusedElement.selectionEnd = selectionEnd; + } + } + } + }; + + const handleKeyDown = (event) => { + if (event.key === 'Tab') { + event.preventDefault(); + const start = event.target.selectionStart; + const end = event.target.selectionEnd; + event.target.value = event.target.value.substring(0, start) + " " + event.target.value.substring(end); + dispatch({ type: 'CODE_CHANGED', payload: event.target.value }); + event.target.selectionStart = event.target.selectionEnd = start + 2; + } + if (event.metaKey && event.key === 'Enter') { + event.preventDefault(); + dispatch({ type: 'RUN_CODE' }); + } + }; + + /** + * ---------------------------------------------------------------- + * Sandbox Initialization + * ---------------------------------------------------------------- + */ + + const sandboxSrc = ` + <script> + const pipe = ${pipe.toString()}; + const compose = ${compose.toString()}; + + const originalConsoleLog = console.log; + console.log = (...args) => { + parent.postMessage({ type: 'CONSOLE_LOG', payload: args }, '*'); + }; + + window.addEventListener('message', (event) => { + try { + eval(event.data.code); + } catch (e) { + console.log(e.toString()); + } + }); + <\/script> + `; + + window.addEventListener('message', (event) => { + if (event.source === sandbox.contentWindow && event.data.type === 'CONSOLE_LOG') { + dispatch(event.data); + } + }); + + /** + * ---------------------------------------------------------------- + * Application Entry Point + * ---------------------------------------------------------------- + */ + + const hash = window.location.hash.substring(1); + if (hash) { + try { + const decodedCode = decodeURIComponent(atob(hash)); + model = { ...init, code: decodedCode }; + } catch (error) { + console.error('Failed to decode the URL hash:', error); + model = init; + } + } else { + model = init; + } + + render(model); + + sandbox.onload = () => { + dispatch({ type: 'SANDBOX_READY' }); + }; + + sandbox.srcdoc = sandboxSrc; + + </script> +</body> +</html> \ No newline at end of file |