Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 11 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# fiddle-player
A simple JS fiddle tool which simulates typing code into a CODE editor. As the code is typed, it is also evaluated
which makes it a great way to teach someone how to use the DOM or visualise the results of a UI library.
A JS fiddle tool that simulates code being typed into an editor, while showing the results of evaluating the code as it goes.
It's a great way to teach someone how to use the DOM, or demonstrate a UI library.

# Meta commands
You can write a few different magic commands (prefixed by triple slash `///`) in the code to jump between lines, or pause at a certain line to let the user
read before continuing. `window.targetElement` points to the DOM result panel element where you can render any
HTML.
You can write a few different magic commands (prefixed by triple slash `///`) in the code to jump between lines, or pause at a
certain line to let the user read before continuing. `window.targetElement` points to the DOM result panel element where you can
render any HTML.

Add a pause before continuing:
```
Expand All @@ -17,8 +17,13 @@ Add a commented line with a checkbox for users to uncomment:
/// document.body.style="background:#000";
```

Full example:
Skip ahead to a specific line using `//->{lineNumber}`:
```javascript
const grid = new Grid(gridConfig); //->17
```

Full example:
```javascript
import { Grid } from 'https://bryntum.com/dist/grid/build/grid.module.js';

const grid = new Grid({
Expand Down
129 changes: 77 additions & 52 deletions lib/TutorialPanel.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ const
keyword : new RegExp(keywords.map(word => `^${word}`).join('|'), 'g'),
tag : /<.*?>/g
},
syntaxKeys = Object.keys(jsSyntax);
syntaxKeys = Object.keys(jsSyntax),
LINE_VISIBLE_CLASS = 'b-line-visible';

export default class TutorialPanel {
get elementTpl() {
Expand All @@ -32,8 +33,8 @@ export default class TutorialPanel {
<code class='b-idle' spellcheck='false' data-reference='codeElement'></code>
</pre>
<div class='b-code-toolbar' data-reference='toolbar' style="${this.hideToolbar ? 'display:none' : ''}">
<div class='run action' data-btip='Run code' data-action='run'><i class="b-icon b-fa-play"></i>Run</div>
<i class='b-icon b-fa-exclamation-triangle' data-reference='errorIcon'></i>
<div class='run action' data-btip='Run code' data-action='run'>${this.getRunButtonTpl()}</div>
<i class='b-icon b-fa-exclamation-triangle' data-reference='errorIcon'></i>
<div class='action' data-action='increase-fontsize'>A<i class="b-icon b-fa-caret-up"></i></div>
<div class='action' data-action='decrease-fontsize'><small>A</small><i class="b-icon b-fa-caret-down"></i></div>
<i class='action b-icon b-icon-copy' data-btip='Copy to clipboard' data-action='copy'></i>
Expand All @@ -49,6 +50,12 @@ export default class TutorialPanel {
`;
}

getRunButtonTpl(isTyping) {
return (isTyping ?? this.isTyping) ?
`<i class="b-icon b-fa-pause"></i> Pause` :
`<i class="b-icon b-fa-play"></i> Run`;
}

constructor(config) {
super.constructor(config);
Object.assign(this, config);
Expand All @@ -62,18 +69,14 @@ export default class TutorialPanel {
const me = this;

me.onPreClick = me.onPreClick.bind(me);
me.codeElement.addEventListener('input', me.onInput.bind(me));
me.pre.addEventListener('click', me.onPreClick);
me.lineNumberContainer.addEventListener('change', me.onChange.bind(me));
// todo fix
// me.codeElement.addEventListener('keydown', event => {
// if (event.key === 'Enter') {
// document.execCommand('insertHTML', false, '<br><div class="b-line"></div>');
// event.preventDefault();
// }
// });
me.onInput = me.onInput.bind(me);
me.onChange = me.onChange.bind(me);
me.onToolbarClick = me.onToolbarClick.bind(me);

me.toolbar.addEventListener('click', me.onToolbarClick.bind(me));
me.codeElement.addEventListener('input', me.onInput);
me.pre.addEventListener('click', me.onPreClick);
me.lineNumberContainer.addEventListener('change', me.onChange);
me.toolbar.addEventListener('click', me.onToolbarClick);

if (me.orientation === 'vertical') {
me.element.classList.add('b-vertical');
Expand Down Expand Up @@ -115,25 +118,37 @@ export default class TutorialPanel {

onStart() {
me.element.classList.add('b-typing');
me.element.querySelector('.run.action').innerHTML = me.getRunButtonTpl(true);
},

onStop() {
me.element.classList.remove('b-typing');
me.element.querySelector('.run.action').innerHTML = me.getRunButtonTpl(false);
},

onLineVisibilityChange(index, visible) {
me.setLineVisible(index, visible)
}

}, me.typer));

if (me.autoStart) {
me.start();
} else {
me.lineElements[0]?.setAttribute('data-placeholder', 'Click to start');
}
}

destroy() {
// TODO
me.codeElement.removeEventListener('input', me.onInput);
me.pre.removeEventListener('click', me.onPreClick);
me.lineNumberContainer.removeEventListener('change', me.onChange);
me.toolbar.removeEventListener('click', me.onToolbarClick);
me.toggleFullscreen.removeEventListener('click', me.toggleFullscreen);
}

async start() {
const
me = this;
const me = this;

me.started = true;

Expand All @@ -147,41 +162,42 @@ export default class TutorialPanel {
}

await me.typer.start();
me.triggerOnHost('start')
me.triggerOnHost('start');
}

stop() {
this.typer.stop();
this.triggerOnHost('stop')
this.triggerOnHost('stop');
}

triggerOnHost(event) {
this.element.getRootNode().host.dispatchEvent(new Event(event));
}

createElements() {
this.element = Object.assign(document.createElement('div'), {
const me = this;

me.element = Object.assign(document.createElement('div'), {
className : 'b-tutorialpanel',
innerHTML : this.elementTpl
innerHTML : me.elementTpl
});

this.appendTo.appendChild(this.element);
me.appendTo.appendChild(me.element);

// Setup references to elements
Array.from(this.element.querySelectorAll('[data-reference]')).forEach(el => {
this[el.dataset.reference] = el;
Array.from(me.element.querySelectorAll('[data-reference]')).forEach(el => {
me[el.dataset.reference] = el;
});

this.splitter = new FlexChildResize({
element : this.splitter
me.splitter = new FlexChildResize({
element : me.splitter
});

this.toggleFullscreen.addEventListener('click', this.onFullScreenClick.bind(this));
me.onFullScreenClick = me.onFullScreenClick.bind(me);
me.toggleFullscreen.addEventListener('click', me.onFullScreenClick);
}

highlightCode(codeSnippet) {
let multiLineComment;

let isBlockComment = false;

return codeSnippet.split('\n').map(line => {
Expand All @@ -201,16 +217,16 @@ export default class TutorialPanel {
const
isToggleHint = trimmed.startsWith('/// '),
jumpToLine = trimmed.match(/->(\d+)/),
metaMatch = trimmed.match(/\$(.*)/);

pauseMatch = trimmed.match(/\.\.[\.]?(\d*)/);
if (isToggleHint) {
rowDataset.isHint = 1;
}

if (metaMatch) {
if (pauseMatch) {
comment = comment.split(/\$(.*)/)[0];
trimmedLine = trimmedLine.split('//$')[0];
rowDataset.pauseAfter = parseInt(metaMatch[1]);
rowDataset.pauseAfter = parseInt(pauseMatch[1]);
}

if (jumpToLine) {
Expand Down Expand Up @@ -342,7 +358,10 @@ export default class TutorialPanel {
});
}

if (inlineComment) {
if (comment) {
lineChildren.push(comment);
}
else if (inlineComment) {
inlineComment = inlineComment.replace(/^\//, '');

lineChildren.push({
Expand All @@ -352,8 +371,7 @@ export default class TutorialPanel {
});
}

const
rowEl = Object.assign(document.createElement('div'), rowConfig);
const rowEl = Object.assign(document.createElement('div'), rowConfig);

Object.assign(rowEl.dataset, rowDataset);

Expand Down Expand Up @@ -425,6 +443,7 @@ export default class TutorialPanel {
case 'run':
if (this.isTyping) {
this.typer.stop();
actionNode.dataset.btip = 'Run';
}
else {
if (this.url && this.typer.currentLineIndex === this.lineCount) {
Expand All @@ -433,8 +452,10 @@ export default class TutorialPanel {
await this.loadCode(this.url);
}
this.start();
actionNode.dataset.btip = 'Pause';
}
break;

}
}

Expand Down Expand Up @@ -501,8 +522,7 @@ export default class TutorialPanel {

// Rewrite relative imports as absolute, to work with createObjectURL approach below
imports?.forEach(importLine => {
const
parts = !importLine.includes('//') && importLine.split('../');
const parts = !importLine.includes('//') && importLine.split('../');

if (parts && parts.length) {
const
Expand All @@ -520,8 +540,6 @@ export default class TutorialPanel {
// Make targetElement global available
window.targetElement = me.resultContainer;

let loadIcon;

if (imports?.length && !me.processedImports) {
me.jsLogo.classList.add('b-fa-spin');
}
Expand Down Expand Up @@ -576,12 +594,19 @@ export default class TutorialPanel {

setProgress(lineIndex) {
// Ensure line numbers are in sync with code typing
this.lineNumberContainer.children[lineIndex].classList.add('b-line-visible');
this.lineNumberContainer.children[lineIndex].classList.add(LINE_VISIBLE_CLASS);

// Update line counter in toolbar
this.progressElement.innerHTML = `${lineIndex + 1} / ${this.lineCount}`;
}

setLineVisible(lineIndex, visible) {
[ this.lineElements[lineIndex], this.lineNumberContainer.children[lineIndex] ]
.forEach(line => visible ?
line.classList.add(LINE_VISIBLE_CLASS) :
line.classList.remove(LINE_VISIBLE_CLASS));
}

// Find all imports in the code, extracting their filename to populate combo with
extractImports(code) {
const
Expand All @@ -598,7 +623,7 @@ export default class TutorialPanel {
}

get isTyping() {
return this.typer.typing;
return this.typer?.typing ?? false;
}

get isFinished() {
Expand All @@ -618,19 +643,19 @@ export default class TutorialPanel {
}

me.lineElements = me.highlightCode(code);
me.lineElements[0]?.setAttribute('data-placeholder', 'Click to start');

me.lineElements.forEach(lineEl => me.codeElement.appendChild(lineEl));

// Render line numbers + any checkboxes for hints
me.lineNumberContainer.innerHTML = me.lineElements.map(line => {
return `<div class="b-line ${line.dataset.isHint ? 'hint' : ''}"> ${line.dataset.isHint ? '<input type="checkbox"/>' : ''}</div>`;
return `<div class="b-line ${line.dataset.isHint ? 'b-hint' : ''}"> ${line.dataset.isHint ? '<input type="checkbox"/>' : ''}</div>`;
}).join('');

me.lineCount = me.lineElements.length;
me.setProgress(0);
}


onFullScreenClick() {
const element = this.element;

Expand Down Expand Up @@ -686,7 +711,9 @@ class TutorialPanelTag extends HTMLElement {

async onHoverContainer() {
await this.loadResources();
this.start();
if (this.autoStart) {
this.start();
}
}

async loadResources() {
Expand Down Expand Up @@ -724,10 +751,10 @@ class TutorialPanelTag extends HTMLElement {
if (me.dataset.inlineCss) {
const inlineSheet = document.createElement('style');
inlineSheet.innerText = me.dataset.inlineCss;
me.shadowRoot.appendChild(inlineSheet);
shadowRoot.appendChild(inlineSheet);
}

me.shadowRoot.appendChild(link);
shadowRoot.appendChild(link);
document.head.appendChild(codeFontLink);

link.onload = () => linkResolver();
Expand All @@ -736,8 +763,8 @@ class TutorialPanelTag extends HTMLElement {

// Load FontAwesome if path was supplied
if (faPath) {
// FF cannot use the name "Font Awesome 5 Free", have if fixed in CSS to handle it also without spaces
font = new FontFace(isFF ? 'FontAwesome5Free' : 'Font Awesome 5 Free', `url("${faPath}/fa-solid-900.woff2")`);
// FF cannot use the name "Font Awesome 6 Free", have if fixed in CSS to handle it also without spaces
font = new FontFace(isFF ? 'FontAwesome6Free' : 'Font Awesome 6 Free', `url("${faPath}/fa-solid-900.woff2")`);
promises.push(font.load());
}

Expand All @@ -755,9 +782,7 @@ class TutorialPanelTag extends HTMLElement {
try {
value = JSON.parse(value);
}
catch (e) {

}
catch (e) { }

config[key] = value;
}
Expand Down
Loading