blob: 3e7aff155175c1f8dd121fe5d2548f42191b0be7 [file] [log] [blame]
import 'elements-sk/styles/buttons'
import 'elements-sk/error-toast-sk'
import { errorMessage } from 'elements-sk/errorMessage'
import { $$ } from 'common-sk/modules/dom'
import { diffDate } from 'common-sk/modules/human'
import { jsonOrThrow } from 'common-sk/modules/jsonOrThrow'
import { html, render } from 'lit-html'
const PathKitInit = require('pathkit-wasm/bin/pathkit.js');
// Main template for this element
const template = (ele) => html`
<header>
<div class=title>PathKit Fiddle</div>
<div class=flex></div>
<div class=version>PathKit Version: <a href="https://www.npmjs.com/package/pathkit-wasm">0.4.0</a></div>
</header>
<main>
${codeEditor(ele)}
<div class=output>
<div class=buttons>
<button class=action @click=${() => ele._run()}>Run</button>
<button class=action @click=${() => ele._save()}>Save</button>
</div>
<canvas id=canvas width=500 height=500></canvas>
</div>
</main>
<footer>
<error-toast-sk></error-toast-sk>
</footer>`;
const codeEditor = (ele) => html`
<div id=editor>
<textarea class=code spellcheck=false rows=${lines(ele.content)} cols=80
@paste=${() => ele._changed()} @input=${() => ele._changed()}
></textarea>
<div class=numbers>
${repeat(lines(ele.content)).map((_, n) => _lineNumber(n+1))}
</div>
</editor>
`
// Returns the number of lines in str, with a minimum of 10
// (because the editor with less than 10 lines looks a bit strange).
function lines(str) {
// see https://stackoverflow.com/a/4009768
return Math.max(10, (str.match(/\n/g) || []).length + 1);
}
// repeat returns an array of n 'undefined' which allows
// for repeating a template a fixed number of times
// using map.
function repeat(n) {
// See https://stackoverflow.com/a/10050831
return [...Array(n)];
}
const _lineNumber = (n) => html`
<div id=${'L'+n}>${n}</div>
`;
function resetCanvas(canvas) {
// Reset the transform of the canvas then re-draw it blank.
let ctx = canvas.getContext('2d');
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
/**
* @module jsfiddle/modules/pathkit-fiddle
* @description <h2><code>pathkit-fiddle</code></h2>
*
* <p>
* The top level element for displaying pathkit fiddles.
* The main elements are a code editor box (textarea), a canvas
* on which to render the result and a few buttons.
* </p>
*
*/
window.customElements.define('pathkit-fiddle', class extends HTMLElement {
constructor() {
super();
this._content = '';
this.PathKit = null;
this._editor = null; // set in render to be the textarea
}
/** @prop {String} content - The current code in the editor.*/
get content() { return this._content; }
set content(c) {
this._content = c;
this._render();
this._editor.value = c;
}
connectedCallback() {
this._render();
PathKitInit({
locateFile: (file) => '/res/'+file,
}).then((PathKit) => {
this.PathKit = PathKit;
if (this.content) {
this._run(); // auto-run the code if the code was loaded.
}
});
if (!this.content) {
this._loadCode();
}
// Listen for the forward and back buttons and re-load the code
// on any changes. Without this, the url changes, but nothing
// happens in the DOM.
window.addEventListener('popstate', this._loadCode.bind(this));
}
disconnectedCallback() {
window.removeEventListener('popstate', this._loadCode.bind(this));
}
_changed() {
this.content = this._editor.value;
}
_loadCode() {
// The location should be either /pathkit or /pathkit/<fiddlehash>
let path = window.location.pathname;
let hash = '';
if (path.length > 9) { // 9 characters in '/pathkit/'
hash = path.slice(9);
}
fetch(`/_/code?type=pathkit&hash=${hash}`)
.then(jsonOrThrow)
.then((json) => {
this.content = json.code;
if (this.PathKit) {
this._run(); // auto-run the code if PathKit is loaded.
}
}
).catch((e) => {
errorMessage('Fiddle not Found', 10000);
this.content = '';
const canvas = $$('#canvas', this);
resetCanvas(canvas);
});
}
_render() {
render(template(this), this);
this._editor = $$('#editor textarea', this);
}
_run() {
if (!this.PathKit) {
errorMessage('PathKit is still loading.');
return;
}
const canvas = $$('#canvas', this);
resetCanvas(canvas);
try {
let f = new Function('PathKit', 'canvas', // actual params
// shadow these globals to at least make exploitation harder. CSP
// is our first line of defense, this adds another layer.
'window', 'document', 'open', 'event', 'Function', 'eval', 'frames',
'frameElement', 'localStorage', 'history', 'messageManager', 'name',
'opener', 'pkcs11', 'self', 'status', 'top', 'visualViewport',
'caches', 'origin', 'indexedDB', 'Worker', 'openDialog', 'alert',
'prompt', 'parent',
this.content); // user given code
f = f.bind({}); // By default, f is bound to Window. Re bind it to remove that access.
f(this.PathKit, canvas);
} catch(e) {
errorMessage(e);
}
}
_save() {
fetch('/_/save', {
method: 'PUT',
headers: new Headers({
'content-type': 'application/json',
}),
body: JSON.stringify({
code: this.content,
type: 'pathkit',
})
}).then(jsonOrThrow).then((json) => {
history.pushState(null, '', json.new_url);
}
).catch(errorMessage);
}
});