yourjs-box 1.6.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- /*! yourjs-box v1.6.0 | (c) 2023-present Chris West | MIT License | https://github.com/westc/yourjs-box */
1
+ /*! yourjs-box v1.8.0 | (c) 2023-present Chris West | MIT License | https://github.com/westc/yourjs-box */
2
2
  (() => {
3
3
  /**
4
4
  * Viewer IFRAME's CSS code
@@ -9,12 +9,12 @@
9
9
  * Information about this package (eg. its version).
10
10
  * @type {{name: string, version: string, homepage: string, repoUrl: string, bugsUrl: string}}
11
11
  */
12
- const PACKAGE_INFO = {"name":"yourjs-box","version":"1.6.0","homepage":"https://westc.github.io/yourjs-box/","repoUrl":"https://github.com/westc/yourjs-box","bugsUrl":"https://github.com/westc/yourjs-box/issues"};
12
+ const PACKAGE_INFO = {"name":"yourjs-box","version":"1.8.0","homepage":"https://westc.github.io/yourjs-box/","repoUrl":"https://github.com/westc/yourjs-box","bugsUrl":"https://github.com/westc/yourjs-box/issues"};
13
13
  /**
14
14
  * Viewer IFRAME's HTML code
15
15
  * @type {string}
16
16
  */
17
- const VIEWER_IFRAME_HTML = "<div id=\"splash\" aria-label=\"Loading\"><div class=\"splash-content\"><span class=\"logo logo-large\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span><div class=\"splash-progress\"><div></div></div><div class=\"splash-message splash-slow\">Still loading&hellip;</div><div class=\"splash-message splash-error\">JS Box couldn&rsquo;t load. Please check your connection and reload the page.</div></div></div><div id=\"vueApp\" v-cloak><input type=\"file\" ref=\"fileInput\" class=\"file-input\" accept=\".js,.mjs,.cjs,.txt,text/javascript,text/plain\" tabindex=\"-1\" aria-hidden=\"true\" @change=\"onFileChosen\"><div id=\"main\" ref=\"main\" :class=\"mainElemClassNames\" :style=\"mainElemStyles\" @mousedown=\"onMainElemMouseDown\"><div id=\"displays\"><div ref=\"displaysScroller\" @scroll=\"onDisplaysScroll\"><template v-for=\"display in displays\"><div v-if=\"display.type === 'prism'\"><div v-if=\"display.isHidden\" class=\"code-header toggleable no-select\" @click=\"display.isCodeShown = !display.isCodeShown\" :title=\"display.isCodeShown ? 'Hide Code' : 'Show Code'\"><span :class=\"['arrow', 'expandable', display.isCodeShown ? 'expanded' : '']\"></span>{{ display.header || 'Hidden code' }}</div><div v-else-if=\"display.header\" class=\"code-header\">{{ display.header }}</div><div v-if=\"display.isCodeShown\" class=\"console-row code-row\"><span class=\"row-icon\"><icon name=\"chevron\"></icon></span><prism language=\"javascript\" :code=\"display.value\" :is-dark=\"theme === 'dark'\" match-braces></prism><button class=\"copy-to-editor-button\" title=\"Copy to editor\" @click=\"copyToEditor(display.value)\"><icon name=\"copyToEditor\"></icon></button></div></div><div v-if=\"display.type === 'log' &amp;&amp; !isInCollapsedGroup(display)\" :class=\"['console-row', 'log-' + display.key, display.groupId ? 'group-header' : '']\" :style=\"{'--depth': display.groupIds.length}\" :title=\"display.name\" @click=\"onLogRowClick(display, $event)\"><span v-if=\"display.key === 'error' || display.key === 'assert'\" class=\"row-icon\"><icon name=\"consoleError\"></icon></span><span v-else-if=\"display.key === 'warn'\" class=\"row-icon\"><icon name=\"consoleWarning\"></icon></span><span v-else-if=\"display.key === 'result'\" class=\"row-icon\"><icon name=\"result\"></icon></span><span v-else-if=\"display.groupId\" :class=\"['row-icon', 'arrow', 'expandable', display.isCollapsed ? '' : 'expanded']\"></span><div class=\"row-content\"><table v-if=\"display.table\" class=\"console-table\"><thead><tr><th v-for=\"header in display.table.headers\">{{ header }}</th></tr></thead><tbody><tr v-for=\"row in display.table.rows\"><td>{{ row.index }}</td><td v-for=\"cell in row.cells\"><template v-if=\"cell\"><span v-for=\"part in cell.parts\" :class=\"'t-' + part[0]\">{{ part[1] }}</span></template></td></tr></tbody></table><js-value v-for=\"(description, index) in display.descriptions\" :description=\"description\" :key-path=\"[]\" :path=\"[display.logId, index]\"></js-value><div v-if=\"display.stack\" class=\"trace-stack\">{{ display.stack }}</div></div></div><div v-if=\"display.type === 'notice'\" class=\"console-row notice\">{{ display.message }}</div><div v-if=\"display.type === 'error'\" class=\"console-row log-error\"><span class=\"row-icon\"><icon name=\"consoleError\"></icon></span><div class=\"row-content\">{{ display.message }}</div></div></template></div></div><div class=\"divider\" ref=\"mainDivider\"></div><div id=\"editor\"><ace-editor ref=\"editor\" v-model=\"jsCode\" language=\"javascript\" :theme=\"theme\" :font-size=\"Math.round(12 * textScale)\" @key-combo=\"onEditorKeyCombo\" @history-key=\"onEditorHistoryKey\" height=\"100%\"></ace-editor></div></div><div id=\"bottomNav\"><div class=\"brand\"><button class=\"logo-button\" title=\"About JS Box\" @click=\"openAbout('about')\"><span class=\"logo\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span></button> <span class=\"runner-badge\" :title=\"runnerMode === 'window' ? 'Code runs directly in this page' : 'Code runs in a Web Worker (no DOM access)'\">{{ runnerMode === 'window' ? 'Window' : 'Worker' }}</span></div><div class=\"buttons\"><template v-for=\"bottomButton in bottomButtons\"><span v-if=\"bottomButton.isSeparator\" class=\"separator\"></span> <button v-else :class=\"bottomButton.className\" @click=\"bottomButton.callback.call(this, $event)\" :title=\"bottomButton.title\" :disabled=\"bottomButton.disableIf &amp;&amp; bottomButton.disableIf.call(this)\"><icon :name=\"bottomButton.iconName\"></icon><span v-if=\"bottomButton.label\" class=\"label\">{{ bottomButton.label }}</span></button></template></div></div><div v-if=\"isMenuOpen\" class=\"more-menu\" role=\"menu\" aria-label=\"More\" ref=\"moreMenu\" @keydown=\"onMenuKeyDown\"><div class=\"menu-row\" role=\"group\" aria-label=\"Text size\"><span class=\"menu-row-label\">Text size</span> <button class=\"menu-item menu-step\" role=\"menuitem\" title=\"Smaller text\" aria-label=\"Smaller text\" :disabled=\"!canShrinkText\" @click=\"changeTextScale(-1)\">A&minus;</button> <button class=\"menu-item menu-step menu-percent\" role=\"menuitem\" title=\"Reset the text size\" aria-label=\"Reset the text size\" @click=\"setTextScale(1)\">{{ Math.round(textScale * 100) }}%</button> <button class=\"menu-item menu-step\" role=\"menuitem\" title=\"Bigger text\" aria-label=\"Bigger text\" :disabled=\"!canGrowText\" @click=\"changeTextScale(1)\">A+</button></div><div class=\"menu-separator\"></div><button class=\"menu-item\" role=\"menuitem\" @click=\"openFile()\"><span class=\"menu-icon\"><icon name=\"open\"></icon></span>Open&hellip;</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"saveFile()\"><span class=\"menu-icon\"><icon name=\"save\"></icon></span>Save&hellip;</button><div class=\"menu-separator\"></div><button v-if=\"isPopOut\" class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); bringBack()\"><span class=\"menu-icon\"><icon name=\"popIn\"></icon></span>Bring back into the page</button> <button v-else class=\"menu-item\" role=\"menuitem\" @click=\"popOut()\"><span class=\"menu-icon\"><icon name=\"popOut\"></icon></span>Pop out into a window</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); openAbout('html')\"><span class=\"menu-icon\"><icon name=\"code\"></icon></span>Copy as HTML&hellip;</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); resetConsole()\"><span class=\"menu-icon\"><icon name=\"refresh\"></icon></span>Reset&hellip;</button><div class=\"menu-separator\"></div><button class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); openAbout('about')\"><span class=\"menu-icon\"><icon name=\"info\"></icon></span>About JS Box</button></div><div v-if=\"isHistoryOpen\" class=\"more-menu history-panel\" role=\"menu\" aria-label=\"History\" ref=\"historyPanel\" @keydown=\"onMenuKeyDown\"><div class=\"history-title\">History</div><div v-if=\"!historyEntries.length\" class=\"history-empty\">Code that you run will show up here.</div><button v-for=\"entry in historyEntries\" class=\"menu-item history-item\" role=\"menuitem\" :title=\"commandHistory[entry.index]\" @click=\"recallHistory(entry.index)\"><span class=\"history-label\">{{ entry.label }}</span> <span class=\"history-detail\">{{ entry.detail }}</span></button><div class=\"history-hint\">Tip: press &uarr; at the start of the editor (or &darr; at the end) to go through the history.</div></div><div v-if=\"valueMenu\" class=\"more-menu value-menu\" role=\"menu\" aria-label=\"Value\" ref=\"valueMenu\" :style=\"{left: valueMenu.x + 'px', top: valueMenu.y + 'px'}\" @keydown=\"onMenuKeyDown\"><button class=\"menu-item\" role=\"menuitem\" @click=\"copyValueAsJson()\"><span class=\"menu-icon\"><icon name=\"copy\"></icon></span>Copy as JSON</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"saveValueAsJson()\"><span class=\"menu-icon\"><icon name=\"save\"></icon></span>Save as JSON&hellip;</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"storeValueAsGlobal()\"><span class=\"menu-icon\"><icon name=\"variable\"></icon></span>Store as global variable</button> <button v-if=\"valueMenu.propertyPath\" class=\"menu-item\" role=\"menuitem\" @click=\"copyPropertyPath()\"><span class=\"menu-icon\"><icon name=\"path\"></icon></span>Copy property path</button><template v-if=\"!valueMenu.isPrimitive\"><div class=\"menu-separator\"></div><button class=\"menu-item\" role=\"menuitem\" @click=\"refreshValue()\"><span class=\"menu-icon\"><icon name=\"refresh\"></icon></span>Refresh</button></template></div><div v-if=\"toast\" class=\"toast\" role=\"status\">{{ toast.message }}</div><div v-if=\"isPoppedOut\" class=\"popped-out\"><div class=\"popped-out-content\"><span class=\"logo logo-medium\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span><div class=\"popped-out-title\">This console is open in a separate window</div><div class=\"popped-out-text\">Its code still runs in this page.</div><div class=\"dialog-buttons\"><button class=\"dialog-button\" @click=\"focusPopOut\">Show the window</button> <button class=\"dialog-button primary\" @click=\"requestPopIn\">Bring it back</button></div></div></div><div v-if=\"isAboutOpen\" class=\"dialog-backdrop\" @mousedown.self=\"closeAbout\" @keydown.esc=\"closeAbout\"><div :class=\"['dialog', 'about-dialog', aboutTab === 'html' ? 'is-export' : '']\" role=\"dialog\" aria-modal=\"true\" aria-label=\"About JS Box\"><div class=\"about-header\"><div class=\"tabs\" role=\"tablist\"><button role=\"tab\" :aria-selected=\"aboutTab === 'about'\" :class=\"['tab', aboutTab === 'about' ? 'active' : '']\" @click=\"aboutTab = 'about'\">About</button> <button role=\"tab\" :aria-selected=\"aboutTab === 'html'\" :class=\"['tab', aboutTab === 'html' ? 'active' : '']\" @click=\"aboutTab = 'html'\">Copy as HTML</button></div><button class=\"close-button\" ref=\"aboutCloseButton\" title=\"Close\" @click=\"closeAbout\"><icon name=\"close\"></icon></button></div><div v-if=\"aboutTab === 'about'\" class=\"about-body\"><div class=\"about-title\"><span class=\"logo logo-medium\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span><div><div class=\"about-name\">YourJS Box <span class=\"about-aka\">aka JS Box</span></div><div class=\"about-version\">Version {{ packageInfo.version }}</div></div></div><p class=\"about-description\">An interactive JavaScript console that can be embedded in any web page with a single script tag.</p><div class=\"about-links\"><a :href=\"packageInfo.homepage\" target=\"_blank\" rel=\"noopener\">Website</a> <a :href=\"packageInfo.repoUrl\" target=\"_blank\" rel=\"noopener\">GitHub</a> <a :href=\"packageInfo.repoUrl + '#readme'\" target=\"_blank\" rel=\"noopener\">Documentation</a> <a :href=\"packageInfo.bugsUrl\" target=\"_blank\" rel=\"noopener\">Report an issue</a></div><h3>This console</h3><dl class=\"about-details\"><dt>Code runs in</dt><dd>{{ runnerDescription }}</dd><dt>Theme</dt><dd>{{ themeDescription }}</dd><dt>Code blocks run as</dt><dd>{{ blockTypeDescription }}</dd><dt>Imports</dt><dd>{{ importsUrl ? 'Packages imported by name load from ' + importsUrl.replace('{specifier}', '\u2026') : 'Only URLs and paths can be imported' }}</dd><dt>Results</dt><dd>{{ showResults ? 'The value of the last expression is shown' : 'Not shown' }}</dd><dt>Layout</dt><dd>{{ layoutDescription }}</dd></dl><h3>Keyboard shortcuts</h3><dl class=\"about-details\"><dt><kbd>{{ modKey }}</kbd> + <kbd>Enter</kbd></dt><dd>Run the next block of code</dd><dt><kbd>&uarr;</kbd> / <kbd>&darr;</kbd></dt><dd>Go through the code that was run (at the very start or end of the editor)</dd><dt><kbd>{{ modKey === 'Ctrl' ? 'Alt' : '&#8997;' }}</kbd> + <kbd>H</kbd></dt><dd>Show the history</dd><dt><kbd>Esc</kbd></dt><dd>Close this window, the menu or full screen</dd></dl><div class=\"about-footer\">MIT License &copy; 2023-present Chris West &middot; Built with Vue {{ libraryVersions.vue }}, Ace {{ libraryVersions['ace-builds'] }}, Prism {{ libraryVersions.prismjs }} and Acorn {{ libraryVersions.acorn }}</div></div><div v-else class=\"about-body export-body\"><div class=\"export-options\"><div class=\"segmented\" role=\"radiogroup\" aria-label=\"Code\"><button v-for=\"option in exportCodeOptions\" role=\"radio\" :aria-checked=\"exportCode === option.value\" :class=\"exportCode === option.value ? 'active' : ''\" @click=\"exportCode = option.value\">{{ option.label }}</button></div><div class=\"segmented\" role=\"radiogroup\" aria-label=\"Format\"><button v-for=\"option in exportFormatOptions\" role=\"radio\" :aria-checked=\"exportFormat === option.value\" :class=\"exportFormat === option.value ? 'active' : ''\" @click=\"exportFormat = option.value\">{{ option.label }}</button></div></div><div class=\"export-note\">{{ exportNote }}</div><div class=\"export-preview\"><ace-editor :model-value=\"exportHtml\" language=\"html\" :theme=\"theme\" height=\"100%\" :read-only=\"true\"></ace-editor></div><div class=\"export-actions\"><button class=\"dialog-button\" @click=\"downloadExport\">Download page</button> <button class=\"dialog-button primary\" @click=\"copyExport\">{{ copyLabel }}</button></div></div></div></div><div v-if=\"dialog\" class=\"dialog-backdrop\" @mousedown.self=\"closeDialog(false)\" @keydown.esc=\"closeDialog(false)\"><div class=\"dialog\" role=\"alertdialog\" aria-modal=\"true\" :aria-label=\"dialog.title\"><div class=\"dialog-title\">{{ dialog.title }}</div><div class=\"dialog-message\">{{ dialog.message }}</div><div class=\"dialog-buttons\"><button class=\"dialog-button\" @click=\"closeDialog(false)\">Cancel</button> <button class=\"dialog-button primary\" ref=\"dialogConfirmButton\" @click=\"closeDialog(true)\">{{ dialog.confirmText }}</button></div></div></div></div>";
17
+ const VIEWER_IFRAME_HTML = "<div id=\"splash\" aria-label=\"Loading\"><div class=\"splash-content\"><span class=\"logo logo-large\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span><div class=\"splash-progress\"><div></div></div><div class=\"splash-message splash-slow\">Still loading&hellip;</div><div class=\"splash-message splash-error\">JS Box couldn&rsquo;t load. Please check your connection and reload the page.</div></div></div><div id=\"vueApp\" v-cloak><input type=\"file\" ref=\"fileInput\" class=\"file-input\" accept=\".js,.mjs,.cjs,.ts,.mts,.cts,.txt,text/javascript,text/plain\" tabindex=\"-1\" aria-hidden=\"true\" @change=\"onFileChosen\"><div id=\"main\" ref=\"main\" :class=\"mainElemClassNames\" :style=\"mainElemStyles\" @mousedown=\"onMainElemMouseDown\"><div id=\"displays\"><div ref=\"displaysScroller\" @scroll=\"onDisplaysScroll\"><template v-for=\"display in displays\"><div v-if=\"display.type === 'prism'\"><div v-if=\"display.isHidden\" class=\"code-header toggleable no-select\" @click=\"display.isCodeShown = !display.isCodeShown\" :title=\"display.isCodeShown ? 'Hide Code' : 'Show Code'\"><span :class=\"['arrow', 'expandable', display.isCodeShown ? 'expanded' : '']\"></span>{{ display.header || 'Hidden code' }}</div><div v-else-if=\"display.header\" class=\"code-header\">{{ display.header }}</div><div v-if=\"display.isCodeShown\" class=\"console-row code-row\"><span class=\"row-icon\"><icon name=\"chevron\"></icon></span><prism :language=\"language\" :code=\"display.value\" :is-dark=\"theme === 'dark'\" match-braces></prism><button class=\"copy-to-editor-button\" title=\"Copy to editor\" @click=\"copyToEditor(display.value)\"><icon name=\"copyToEditor\"></icon></button></div></div><div v-if=\"display.type === 'log' &amp;&amp; !isInCollapsedGroup(display)\" :class=\"['console-row', 'log-' + display.key, display.groupId ? 'group-header' : '']\" :style=\"{'--depth': display.groupIds.length}\" :title=\"display.name\" @click=\"onLogRowClick(display, $event)\"><span v-if=\"display.key === 'error' || display.key === 'assert'\" class=\"row-icon\"><icon name=\"consoleError\"></icon></span><span v-else-if=\"display.key === 'warn'\" class=\"row-icon\"><icon name=\"consoleWarning\"></icon></span><span v-else-if=\"display.key === 'result'\" class=\"row-icon\"><icon name=\"result\"></icon></span><span v-else-if=\"display.groupId\" :class=\"['row-icon', 'arrow', 'expandable', display.isCollapsed ? '' : 'expanded']\"></span><div class=\"row-content\"><table v-if=\"display.table\" class=\"console-table\"><thead><tr><th v-for=\"header in display.table.headers\">{{ header }}</th></tr></thead><tbody><tr v-for=\"row in display.table.rows\"><td>{{ row.index }}</td><td v-for=\"cell in row.cells\"><template v-if=\"cell\"><span v-for=\"part in cell.parts\" :class=\"'t-' + part[0]\">{{ part[1] }}</span></template></td></tr></tbody></table><js-value v-for=\"(description, index) in display.descriptions\" :description=\"description\" :key-path=\"[]\" :path=\"[display.logId, index]\"></js-value><div v-if=\"display.stack\" class=\"trace-stack\">{{ display.stack }}</div></div></div><div v-if=\"display.type === 'notice'\" class=\"console-row notice\">{{ display.message }}</div><div v-if=\"display.type === 'error'\" class=\"console-row log-error\"><span class=\"row-icon\"><icon name=\"consoleError\"></icon></span><div class=\"row-content\">{{ display.message }}</div></div></template></div></div><div class=\"divider\" ref=\"mainDivider\"></div><div id=\"editor\"><ace-editor ref=\"editor\" v-model=\"jsCode\" :language=\"language\" :theme=\"theme\" :font-size=\"Math.round(12 * textScale)\" @key-combo=\"onEditorKeyCombo\" @history-key=\"onEditorHistoryKey\" height=\"100%\"></ace-editor></div></div><div id=\"bottomNav\"><div class=\"brand\"><button class=\"logo-button\" title=\"About JS Box\" @click=\"openAbout('about')\"><span class=\"logo\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span></button> <span class=\"runner-badge\" :title=\"runnerMode === 'window' ? 'Code runs directly in this page' : 'Code runs in a Web Worker (no DOM access)'\">{{ runnerMode === 'window' ? 'Window' : 'Worker' }}</span></div><div class=\"buttons\"><template v-for=\"bottomButton in bottomButtons\"><span v-if=\"bottomButton.isSeparator\" class=\"separator\"></span> <button v-else :class=\"bottomButton.className\" @click=\"bottomButton.callback.call(this, $event)\" :title=\"bottomButton.title\" :disabled=\"bottomButton.disableIf &amp;&amp; bottomButton.disableIf.call(this)\"><icon :name=\"bottomButton.iconName\"></icon><span v-if=\"bottomButton.label\" class=\"label\">{{ bottomButton.label }}</span></button></template></div></div><div v-if=\"isMenuOpen\" class=\"more-menu\" role=\"menu\" aria-label=\"More\" ref=\"moreMenu\" @keydown=\"onMenuKeyDown\"><div class=\"menu-row\" role=\"group\" aria-label=\"Text size\"><span class=\"menu-row-label\">Text size</span> <button class=\"menu-item menu-step\" role=\"menuitem\" title=\"Smaller text\" aria-label=\"Smaller text\" :disabled=\"!canShrinkText\" @click=\"changeTextScale(-1)\">A&minus;</button> <button class=\"menu-item menu-step menu-percent\" role=\"menuitem\" title=\"Reset the text size\" aria-label=\"Reset the text size\" @click=\"setTextScale(1)\">{{ Math.round(textScale * 100) }}%</button> <button class=\"menu-item menu-step\" role=\"menuitem\" title=\"Bigger text\" aria-label=\"Bigger text\" :disabled=\"!canGrowText\" @click=\"changeTextScale(1)\">A+</button></div><div class=\"menu-separator\"></div><button class=\"menu-item\" role=\"menuitem\" @click=\"openFile()\"><span class=\"menu-icon\"><icon name=\"open\"></icon></span>Open&hellip;</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"saveFile()\"><span class=\"menu-icon\"><icon name=\"save\"></icon></span>Save&hellip;</button><div class=\"menu-separator\"></div><button v-if=\"isPopOut\" class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); bringBack()\"><span class=\"menu-icon\"><icon name=\"popIn\"></icon></span>Bring back into the page</button> <button v-else class=\"menu-item\" role=\"menuitem\" @click=\"popOut()\"><span class=\"menu-icon\"><icon name=\"popOut\"></icon></span>Pop out into a window</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); openAbout('html')\"><span class=\"menu-icon\"><icon name=\"code\"></icon></span>Copy as HTML&hellip;</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); resetConsole()\"><span class=\"menu-icon\"><icon name=\"refresh\"></icon></span>Reset&hellip;</button><div class=\"menu-separator\"></div><button class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); openAbout('about')\"><span class=\"menu-icon\"><icon name=\"info\"></icon></span>About JS Box</button></div><div v-if=\"isHistoryOpen\" class=\"more-menu history-panel\" role=\"menu\" aria-label=\"History\" ref=\"historyPanel\" @keydown=\"onMenuKeyDown\"><div class=\"history-title\">History</div><div v-if=\"!historyEntries.length\" class=\"history-empty\">Code that you run will show up here.</div><button v-for=\"entry in historyEntries\" class=\"menu-item history-item\" role=\"menuitem\" :title=\"commandHistory[entry.index]\" @click=\"recallHistory(entry.index)\"><span class=\"history-label\">{{ entry.label }}</span> <span class=\"history-detail\">{{ entry.detail }}</span></button><div class=\"history-hint\">Tip: press &uarr; at the start of the editor (or &darr; at the end) to go through the history.</div></div><div v-if=\"valueMenu\" class=\"more-menu value-menu\" role=\"menu\" aria-label=\"Value\" ref=\"valueMenu\" :style=\"{left: valueMenu.x + 'px', top: valueMenu.y + 'px'}\" @keydown=\"onMenuKeyDown\"><button class=\"menu-item\" role=\"menuitem\" @click=\"copyValueAsJson()\"><span class=\"menu-icon\"><icon name=\"copy\"></icon></span>Copy as JSON</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"saveValueAsJson()\"><span class=\"menu-icon\"><icon name=\"save\"></icon></span>Save as JSON&hellip;</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"storeValueAsGlobal()\"><span class=\"menu-icon\"><icon name=\"variable\"></icon></span>Store as global variable</button> <button v-if=\"valueMenu.propertyPath\" class=\"menu-item\" role=\"menuitem\" @click=\"copyPropertyPath()\"><span class=\"menu-icon\"><icon name=\"path\"></icon></span>Copy property path</button><template v-if=\"!valueMenu.isPrimitive\"><div class=\"menu-separator\"></div><button class=\"menu-item\" role=\"menuitem\" @click=\"refreshValue()\"><span class=\"menu-icon\"><icon name=\"refresh\"></icon></span>Refresh</button></template></div><div v-if=\"toast\" class=\"toast\" role=\"status\">{{ toast.message }}</div><div v-if=\"isPoppedOut\" class=\"popped-out\"><div class=\"popped-out-content\"><span class=\"logo logo-medium\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span><div class=\"popped-out-title\">This console is open in a separate window</div><div class=\"popped-out-text\">Its code still runs in this page.</div><div class=\"dialog-buttons\"><button class=\"dialog-button\" @click=\"focusPopOut\">Show the window</button> <button class=\"dialog-button primary\" @click=\"requestPopIn\">Bring it back</button></div></div></div><div v-if=\"isAboutOpen\" class=\"dialog-backdrop\" @mousedown.self=\"closeAbout\" @keydown.esc=\"closeAbout\"><div :class=\"['dialog', 'about-dialog', aboutTab === 'html' ? 'is-export' : '']\" role=\"dialog\" aria-modal=\"true\" aria-label=\"About JS Box\"><div class=\"about-header\"><div class=\"tabs\" role=\"tablist\"><button role=\"tab\" :aria-selected=\"aboutTab === 'about'\" :class=\"['tab', aboutTab === 'about' ? 'active' : '']\" @click=\"aboutTab = 'about'\">About</button> <button role=\"tab\" :aria-selected=\"aboutTab === 'html'\" :class=\"['tab', aboutTab === 'html' ? 'active' : '']\" @click=\"aboutTab = 'html'\">Copy as HTML</button></div><button class=\"close-button\" ref=\"aboutCloseButton\" title=\"Close\" @click=\"closeAbout\"><icon name=\"close\"></icon></button></div><div v-if=\"aboutTab === 'about'\" class=\"about-body\"><div class=\"about-title\"><span class=\"logo logo-medium\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span><div><div class=\"about-name\">YourJS Box <span class=\"about-aka\">aka JS Box</span></div><div class=\"about-version\">Version {{ packageInfo.version }}</div></div></div><p class=\"about-description\">An interactive JavaScript console that can be embedded in any web page with a single script tag.</p><div class=\"about-links\"><a :href=\"packageInfo.homepage\" target=\"_blank\" rel=\"noopener\">Website</a> <a :href=\"packageInfo.repoUrl\" target=\"_blank\" rel=\"noopener\">GitHub</a> <a :href=\"packageInfo.repoUrl + '#readme'\" target=\"_blank\" rel=\"noopener\">Documentation</a> <a :href=\"packageInfo.bugsUrl\" target=\"_blank\" rel=\"noopener\">Report an issue</a></div><h3>This console</h3><dl class=\"about-details\"><dt>Code runs in</dt><dd>{{ runnerDescription }}</dd><dt>Theme</dt><dd>{{ themeDescription }}</dd><dt>Language</dt><dd>{{ isTypeScript ? 'TypeScript (types are removed by Babel, not checked)' : 'JavaScript' }}</dd><dt>Code blocks run as</dt><dd>{{ blockTypeDescription }}</dd><dt>Imports</dt><dd>{{ importsUrl ? 'Packages imported by name load from ' + importsUrl.replace('{specifier}', '\u2026') : 'Only URLs and paths can be imported' }}</dd><dt>Results</dt><dd>{{ showResults ? 'The value of the last expression is shown' : 'Not shown' }}</dd><dt>Layout</dt><dd>{{ layoutDescription }}</dd></dl><h3>Keyboard shortcuts</h3><dl class=\"about-details\"><dt><kbd>{{ modKey }}</kbd> + <kbd>Enter</kbd></dt><dd>Run the next block of code</dd><dt><kbd>&uarr;</kbd> / <kbd>&darr;</kbd></dt><dd>Go through the code that was run (at the very start or end of the editor)</dd><dt><kbd>{{ modKey === 'Ctrl' ? 'Alt' : '&#8997;' }}</kbd> + <kbd>H</kbd></dt><dd>Show the history</dd><dt><kbd>Esc</kbd></dt><dd>Close this window, the menu or full screen</dd></dl><div class=\"about-footer\">MIT License &copy; 2023-present Chris West &middot; Built with {{ builtWith }}</div></div><div v-else class=\"about-body export-body\"><div class=\"export-options\"><div class=\"segmented\" role=\"radiogroup\" aria-label=\"Code\"><button v-for=\"option in exportCodeOptions\" role=\"radio\" :aria-checked=\"exportCode === option.value\" :class=\"exportCode === option.value ? 'active' : ''\" @click=\"exportCode = option.value\">{{ option.label }}</button></div><div class=\"segmented\" role=\"radiogroup\" aria-label=\"Format\"><button v-for=\"option in exportFormatOptions\" role=\"radio\" :aria-checked=\"exportFormat === option.value\" :class=\"exportFormat === option.value ? 'active' : ''\" @click=\"exportFormat = option.value\">{{ option.label }}</button></div></div><div class=\"export-note\">{{ exportNote }}</div><div class=\"export-preview\"><ace-editor :model-value=\"exportHtml\" language=\"html\" :theme=\"theme\" height=\"100%\" :read-only=\"true\"></ace-editor></div><div class=\"export-actions\"><button class=\"dialog-button\" @click=\"downloadExport\">Download page</button> <button class=\"dialog-button primary\" @click=\"copyExport\">{{ copyLabel }}</button></div></div></div></div><div v-if=\"dialog\" class=\"dialog-backdrop\" @mousedown.self=\"closeDialog(false)\" @keydown.esc=\"closeDialog(false)\"><div class=\"dialog\" role=\"alertdialog\" aria-modal=\"true\" :aria-label=\"dialog.title\"><div class=\"dialog-title\">{{ dialog.title }}</div><div class=\"dialog-message\">{{ dialog.message }}</div><div class=\"dialog-buttons\"><button class=\"dialog-button\" @click=\"closeDialog(false)\">Cancel</button> <button class=\"dialog-button primary\" ref=\"dialogConfirmButton\" @click=\"closeDialog(true)\">{{ dialog.confirmText }}</button></div></div></div></div>";
18
18
  /**
19
19
  * The URL of this script which is used to remove this script's lines from the
20
20
  * stack traces of errors in window mode.
@@ -33,6 +33,8 @@
33
33
  * was tested with.
34
34
  */
35
35
  const LIBRARY_VERSIONS = {
36
+ // Only loaded by TypeScript consoles.
37
+ '@babel/standalone': '7.29.9',
36
38
  'ace-builds': '1.44.0',
37
39
  'acorn': '8.18.0',
38
40
  'prism-themes': '1.9.0',
@@ -138,6 +140,9 @@
138
140
  // How wrapping the last expression of each block shifted the columns on its
139
141
  // line so that stack traces can show the original columns.
140
142
  const columnShiftsBySnippet = {};
143
+ // For TypeScript, where each position in the code that ran came from in the
144
+ // TypeScript code (see runCode()).
145
+ const sourceMapsBySnippet = {};
141
146
 
142
147
  const counts = {};
143
148
  const timers = {};
@@ -334,15 +339,40 @@
334
339
  .split('\n')
335
340
  .filter(line => !(ownUrl && /^\s*at\b/.test(line) && line.includes(ownUrl)))
336
341
  .join('\n')
337
- // Undoes the shift in columns caused by wrapping the last expression.
338
- .replace(/(snippet-\d+\.js):(\d+):(\d+)/g, (match, name, line, column) => {
342
+ .replace(/(snippet-\d+\.[jt]s):(\d+):(\d+)/g, (match, name, line, column) => {
343
+ line = +line;
344
+ column = +column;
345
+ // Undoes the shift in columns caused by wrapping the last expression.
339
346
  const info = columnShiftsBySnippet[name];
340
- return info && +line === info.line && +column > info.column
341
- ? `${name}:${line}:${Math.max(info.column, column - info.shift)}`
342
- : match;
347
+ if (info && line === info.line && column > info.column) {
348
+ column = Math.max(info.column, column - info.shift);
349
+ }
350
+ [line, column] = toSourcePosition(sourceMapsBySnippet[name], line, column);
351
+ return `${name}:${line}:${column}`;
343
352
  });
344
353
  }
345
354
 
355
+ /**
356
+ * Finds where a position in compiled code came from in the original code.
357
+ * @param {number[][][]=} sourceMap
358
+ * For each line of the compiled code, its mappings as [compiled column,
359
+ * original line, original column] (all starting at 0) sorted by column.
360
+ * @param {number} line
361
+ * @param {number} column
362
+ * @returns {[number, number]}
363
+ * The original line and column (both starting at 1) or the given ones if
364
+ * the position isn't mapped.
365
+ */
366
+ function toSourcePosition(sourceMap, line, column) {
367
+ const mappings = sourceMap?.[line - 1];
368
+ let best;
369
+ for (const mapping of mappings ?? []) {
370
+ if (mapping[0] > column - 1) break;
371
+ best = mapping;
372
+ }
373
+ return best ? [best[1] + 1, best[2] + 1 + (column - 1 - best[0])] : [line, column];
374
+ }
375
+
346
376
  function without(props, obj) {
347
377
  if (!obj) return obj => without(props, obj);
348
378
  const ret = {...obj};
@@ -709,12 +739,22 @@
709
739
  * @param {[number, number]=} runOptions.resultRange
710
740
  * The start and end index of the last expression in the code whose value
711
741
  * should be shown.
742
+ * @param {"javascript"|"typescript"=} runOptions.language
743
+ * The language that the code was written in. TypeScript is compiled to
744
+ * JavaScript before it is sent here.
745
+ * @param {number[][][]=} runOptions.sourceMap
746
+ * For TypeScript, where the compiled code came from (see
747
+ * toSourcePosition()).
748
+ * @param {{message: string, line: number, column: number}=} runOptions.compileError
749
+ * For TypeScript, the syntax error that kept the code from being compiled
750
+ * which is reported instead of running the code.
712
751
  */
713
752
  async function runCode(jsCode, runOptions) {
714
- const {blockType, resultRange} = Object(runOptions);
753
+ const {blockType, resultRange, language, sourceMap, compileError} = Object(runOptions);
715
754
 
716
755
  // Names the code so that stack traces refer to it by this name.
717
- const snippetName = `snippet-${++snippetCount}.js`;
756
+ const snippetName = `snippet-${++snippetCount}.${language === 'typescript' ? 'ts' : 'js'}`;
757
+ if (Array.isArray(sourceMap)) sourceMapsBySnippet[snippetName] = sourceMap;
718
758
 
719
759
  // Wraps the last expression so that its value is reported. This keeps
720
760
  // everything on the same lines so that line numbers in errors still match
@@ -735,7 +775,17 @@
735
775
 
736
776
  runningBlockType = blockType === 'module' ? 'module' : 'classic';
737
777
  try {
738
- if (blockType === 'module') {
778
+ if (compileError) {
779
+ // Reported here so that it shows up after anything logged by the code
780
+ // that ran before it.
781
+ const {message, line, column} = compileError;
782
+ reportUncaught({
783
+ name: 'SyntaxError',
784
+ message: `${message}`,
785
+ stack: `SyntaxError: ${message}\n at ${snippetName}:${+line}:${+column}`,
786
+ });
787
+ }
788
+ else if (blockType === 'module') {
739
789
  // Waits for the module to finish (including any top-level await).
740
790
  await import('data:text/javascript;charset=utf-8,' + encodeURIComponent(source));
741
791
  }
@@ -1045,6 +1095,7 @@
1045
1095
  function createConsole({code, dataset, height, insert}) {
1046
1096
  const runnerMode = dataset.runner === 'window' ? 'window' : 'worker';
1047
1097
  const blockType = dataset.blockType === 'module' ? 'module' : 'classic';
1098
+ const language = dataset.language === 'typescript' ? 'typescript' : 'javascript';
1048
1099
  const showResults = dataset.showResults !== 'false';
1049
1100
  // An empty data-imports-url turns off loading packages by name.
1050
1101
  const importsUrl = dataset.importsUrl == null
@@ -1063,9 +1114,8 @@
1063
1114
  let popOutWatcher;
1064
1115
  let activeViewer;
1065
1116
  const sendToViewer = data => relayMessage(data, {viewer: activeViewer});
1066
- const runner = runnerMode === 'window'
1067
- ? createWindowRunner(sendToViewer)
1068
- : createWorkerRunner(sendToViewer);
1117
+ // Created once the console is loaded (see load()).
1118
+ let runner = null;
1069
1119
 
1070
1120
  // The theme is determined up front so that the loading screen uses it.
1071
1121
  const theme = /^(light|dark)$/.test(dataset.theme)
@@ -1162,6 +1212,7 @@
1162
1212
  */
1163
1213
  function createViewer(targetWindow, state) {
1164
1214
  return createCallableFrame({
1215
+ deferLoad: !targetWindow,
1165
1216
  jsCode() {
1166
1217
  let mountedApp;
1167
1218
 
@@ -1236,6 +1287,12 @@
1236
1287
  */
1237
1288
  function init(jsCode, dataset, meta) {
1238
1289
  if (IS_MISSING_LIBRARIES) return;
1290
+ // TypeScript can't run without Babel.
1291
+ if (meta.language === 'typescript' && !window.Babel) {
1292
+ document.querySelector('#splash').classList.add('failed');
1293
+ return;
1294
+ }
1295
+ Prism.plugins.autoloader.loadLanguages(meta.language);
1239
1296
 
1240
1297
  const hidePrefix = dataset.hidePrefix ?? '';
1241
1298
  const darkSchemeQuery = matchMedia('(prefers-color-scheme: dark)');
@@ -1259,6 +1316,7 @@
1259
1316
  // The name of the file that was last opened or saved.
1260
1317
  fileName: null,
1261
1318
  blockType: meta.blockType,
1319
+ language: meta.language,
1262
1320
  showResults: meta.showResults,
1263
1321
  packageInfo: meta.packageInfo,
1264
1322
  libraryVersions: meta.libraryVersions,
@@ -1334,6 +1392,21 @@
1334
1392
  const name = this.theme === 'dark' ? 'Dark' : 'Light';
1335
1393
  return `${name} (${this.forcedTheme ? 'set by data-theme' : 'follows your system'})`;
1336
1394
  },
1395
+ isTypeScript() {
1396
+ return this.language === 'typescript';
1397
+ },
1398
+ /** The libraries (and their versions) that this console uses. */
1399
+ builtWith() {
1400
+ const versions = this.libraryVersions;
1401
+ const names = [
1402
+ `Vue ${versions.vue}`,
1403
+ `Ace ${versions['ace-builds']}`,
1404
+ `Prism ${versions.prismjs}`,
1405
+ `Acorn ${versions.acorn}`,
1406
+ ...this.isTypeScript ? [`Babel ${versions['@babel/standalone']}`] : [],
1407
+ ];
1408
+ return names.slice(0, -1).join(', ') + ' and ' + names[names.length - 1];
1409
+ },
1337
1410
  blockTypeDescription() {
1338
1411
  return this.blockType === 'module'
1339
1412
  ? 'Modules (top-level await and imports work, declarations stay in each block)'
@@ -1663,17 +1736,29 @@
1663
1736
  this.commandHistory.push(historyCode);
1664
1737
  if (this.commandHistory.length > 200) this.commandHistory.shift();
1665
1738
  }
1666
- const {code, resultRange} = prepareCode(group.lines, {
1667
- blockType: this.blockType,
1668
- importsUrl: this.importsUrl,
1669
- // Like the browser's console, show the value of the last
1670
- // expression (except for hidden code).
1671
- findResult: this.showResults && !isHidden,
1672
- });
1739
+ const {language, blockType} = this;
1740
+ const compiled = language === 'typescript'
1741
+ ? compileTypeScript(group.lines, {blockType})
1742
+ : {code: group.lines};
1743
+ const {code, resultRange} = compiled.error
1744
+ ? {code: '', resultRange: null}
1745
+ : prepareCode(compiled.code, {
1746
+ blockType,
1747
+ importsUrl: this.importsUrl,
1748
+ // Like the browser's console, show the value of the last
1749
+ // expression (except for hidden code).
1750
+ findResult: this.showResults && !isHidden,
1751
+ });
1673
1752
  messageParent({
1674
1753
  target: 'runner',
1675
1754
  func: 'runCode',
1676
- args: [code, {blockType: this.blockType, resultRange}],
1755
+ args: [code, {
1756
+ blockType,
1757
+ resultRange,
1758
+ language,
1759
+ sourceMap: compiled.sourceMap,
1760
+ compileError: compiled.error,
1761
+ }],
1677
1762
  });
1678
1763
  },
1679
1764
  clearConsole() {
@@ -1747,14 +1832,17 @@
1747
1832
  async saveFile() {
1748
1833
  this.closeMenu();
1749
1834
  const code = this.currentCode + '\n';
1750
- const suggestedName = this.fileName || 'js-box.js';
1835
+ const suggestedName = this.fileName || (this.isTypeScript ? 'js-box.ts' : 'js-box.js');
1836
+ const fileType = this.isTypeScript
1837
+ ? {description: 'TypeScript', accept: {'text/typescript': ['.ts', '.mts']}}
1838
+ : {description: 'JavaScript', accept: {'text/javascript': ['.js', '.mjs']}};
1751
1839
 
1752
1840
  // Shows a "Save as" dialog where it is supported.
1753
1841
  if ('function' === typeof window.showSaveFilePicker) {
1754
1842
  try {
1755
1843
  const handle = await showSaveFilePicker({
1756
1844
  suggestedName,
1757
- types: [{description: 'JavaScript', accept: {'text/javascript': ['.js', '.mjs']}}],
1845
+ types: [fileType],
1758
1846
  });
1759
1847
  const writable = await handle.createWritable();
1760
1848
  await writable.write(code);
@@ -1768,7 +1856,7 @@
1768
1856
  }
1769
1857
  }
1770
1858
 
1771
- const url = URL.createObjectURL(new Blob([code], {type: 'text/javascript'}));
1859
+ const url = URL.createObjectURL(new Blob([code], {type: Object.keys(fileType.accept)[0]}));
1772
1860
  Object.assign(document.createElement('a'), {href: url, download: suggestedName}).click();
1773
1861
  setTimeout(() => URL.revokeObjectURL(url), 1000);
1774
1862
  },
@@ -2510,7 +2598,7 @@
2510
2598
 
2511
2599
  codeElem.textContent = this.code;
2512
2600
 
2513
- codeElem.className = `language-javascript`;
2601
+ codeElem.className = `language-${this.language ?? 'javascript'}`;
2514
2602
  preElem.className = '';
2515
2603
 
2516
2604
  for (const propName of ['lineNumbers', 'matchBraces']) {
@@ -2783,6 +2871,88 @@
2783
2871
  };
2784
2872
  }
2785
2873
 
2874
+ /**
2875
+ * Compiles TypeScript to JavaScript by removing the types (without checking
2876
+ * them) using Babel. Every line stays on the same line so that line numbers
2877
+ * in errors still match.
2878
+ * @param {string} code
2879
+ * @param {Object} options
2880
+ * @param {"classic"|"module"} options.blockType
2881
+ * @returns {{code: string, sourceMap?: number[][][], error?: {message: string, line: number, column: number}}}
2882
+ * The JavaScript code and, for each of its lines, where it came from (see
2883
+ * decodeSourceMap()) or the syntax error that kept it from being compiled.
2884
+ */
2885
+ function compileTypeScript(code, {blockType}) {
2886
+ try {
2887
+ const result = Babel.transform(code, {
2888
+ filename: 'snippet.ts',
2889
+ presets: [['typescript', {
2890
+ allExtensions: true,
2891
+ // Only `import type` is removed so that an import is never dropped
2892
+ // just because it isn't used yet.
2893
+ onlyRemoveTypeImports: true,
2894
+ }]],
2895
+ sourceType: blockType === 'module' ? 'module' : 'script',
2896
+ // Allows `export` in a namespace in classic blocks. Imports in classic
2897
+ // blocks are left in so that running them shows the usual error.
2898
+ parserOpts: {allowImportExportEverywhere: blockType !== 'module'},
2899
+ retainLines: true,
2900
+ sourceMaps: true,
2901
+ });
2902
+ return {code: result.code, sourceMap: decodeSourceMap(result.map.mappings)};
2903
+ }
2904
+ catch (e) {
2905
+ if (e?.name !== 'SyntaxError' || !e.loc) throw e;
2906
+ return {
2907
+ code,
2908
+ error: {
2909
+ // Babel's message starts with the file name and ends with the
2910
+ // position and the code where the error is.
2911
+ message: `${e.message}`.split('\n')[0].replace(/^\/?snippet\.ts:\s*/, '').replace(/\s*\(\d+:\d+\)$/, ''),
2912
+ line: e.loc.line,
2913
+ column: e.loc.column + 1,
2914
+ },
2915
+ };
2916
+ }
2917
+ }
2918
+
2919
+ /**
2920
+ * Decodes the mappings of a source map (for a single source).
2921
+ * @param {string} mappings
2922
+ * @returns {number[][][]}
2923
+ * For each line of the compiled code, its mappings as [compiled column,
2924
+ * original line, original column] (all starting at 0) sorted by column.
2925
+ */
2926
+ function decodeSourceMap(mappings) {
2927
+ const BASE64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
2928
+ const totals = [0, 0, 0, 0, 0];
2929
+ return mappings.split(';').map(line => {
2930
+ totals[0] = 0;
2931
+ const lineMappings = [];
2932
+ for (const segment of line.split(',')) {
2933
+ if (!segment) continue;
2934
+ const fields = [];
2935
+ let value = 0, shift = 0;
2936
+ for (const char of segment) {
2937
+ const digit = BASE64.indexOf(char);
2938
+ value += (digit & 31) << shift;
2939
+ if (digit & 32) {
2940
+ shift += 5;
2941
+ }
2942
+ else {
2943
+ fields.push(value & 1 ? -(value >>> 1) : value >>> 1);
2944
+ value = shift = 0;
2945
+ }
2946
+ }
2947
+ // The fields are the compiled column, the source index, the original
2948
+ // line and the original column, each relative to the previous one.
2949
+ fields.forEach((field, index) => totals[index] += field);
2950
+ if (fields.length >= 4) lineMappings.push([totals[0], totals[2], totals[3]]);
2951
+ }
2952
+ return lineMappings.sort((a, b) => a[0] - b[0]);
2953
+ });
2954
+ }
2955
+
2786
2956
  /**
2787
2957
  * Gets a block of code ready to run: packages imported by name (eg.
2788
2958
  * `import _ from 'lodash'` or `import('lodash')`) are loaded from importsUrl
@@ -3223,6 +3393,8 @@
3223
3393
  // Used to find the last expression in each block of code (so that its
3224
3394
  // value can be shown) and the packages that it imports.
3225
3395
  libraryUrl('acorn', 'dist/acorn.js'),
3396
+ // Removes the types from TypeScript code before it is run.
3397
+ ...language === 'typescript' ? [libraryUrl('@babel/standalone', 'babel.min.js')] : [],
3226
3398
  ],
3227
3399
  cssUrls: [
3228
3400
  'data:text/css,' + encodeURIComponent(VIEWER_IFRAME_CSS),
@@ -3241,6 +3413,7 @@
3241
3413
  this.call('init', code, dataset, {
3242
3414
  runnerMode,
3243
3415
  blockType,
3416
+ language,
3244
3417
  showResults,
3245
3418
  importsUrl,
3246
3419
  packageInfo: PACKAGE_INFO,
@@ -3266,6 +3439,37 @@
3266
3439
  insert(inlineViewer.iframe);
3267
3440
 
3268
3441
  let isDestroyed = false;
3442
+
3443
+ /**
3444
+ * Loads the viewer and starts the runner (which, in window mode, is when
3445
+ * the page's console functions start being captured).
3446
+ */
3447
+ function load() {
3448
+ if (runner || isDestroyed) return;
3449
+ runner = runnerMode === 'window'
3450
+ ? createWindowRunner(sendToViewer)
3451
+ : createWorkerRunner(sendToViewer);
3452
+ inlineViewer.load();
3453
+ }
3454
+
3455
+ // Unless data-loading="eager" is given, nothing is loaded until the console
3456
+ // is about to be scrolled into view (or a hidden console is shown).
3457
+ /** @type {IntersectionObserver?} */
3458
+ let loadObserver = null;
3459
+ if (dataset.loading !== 'eager' && 'function' === typeof window.IntersectionObserver) {
3460
+ loadObserver = new IntersectionObserver(entries => {
3461
+ if (entries.some(entry => entry.isIntersecting)) {
3462
+ loadObserver.disconnect();
3463
+ loadObserver = null;
3464
+ load();
3465
+ }
3466
+ }, {rootMargin: '200px'});
3467
+ loadObserver.observe(inlineViewer.iframe);
3468
+ }
3469
+ else {
3470
+ load();
3471
+ }
3472
+
3269
3473
  return {
3270
3474
  element: inlineViewer.iframe,
3271
3475
  /**
@@ -3281,7 +3485,8 @@
3281
3485
  popOutWindow = null;
3282
3486
  }
3283
3487
  removeEventListener('pagehide', closePopOut);
3284
- runner.destroy();
3488
+ loadObserver?.disconnect();
3489
+ runner?.destroy();
3285
3490
  inlineViewer.dispose();
3286
3491
  inlineViewer.iframe.remove();
3287
3492
  },
@@ -3317,7 +3522,7 @@
3317
3522
  * The options of YourJSBox.create() which are the same as the data
3318
3523
  * attributes of a script tag.
3319
3524
  */
3320
- const CONSOLE_OPTION_NAMES = ['blockType', 'dividerOrient', 'hidePrefix', 'importsUrl', 'librariesUrl', 'runner', 'showResults', 'theme'];
3525
+ const CONSOLE_OPTION_NAMES = ['blockType', 'dividerOrient', 'hidePrefix', 'importsUrl', 'language', 'librariesUrl', 'loading', 'runner', 'showResults', 'theme'];
3321
3526
 
3322
3527
  /**
3323
3528
  * The JavaScript API for creating consoles (available as window.YourJSBox).
@@ -3343,12 +3548,14 @@
3343
3548
  * Optional. The code that the console starts with.
3344
3549
  * @param {string=} options.runner
3345
3550
  * @param {string=} options.blockType
3551
+ * @param {string=} options.language
3346
3552
  * @param {(boolean|string)=} options.showResults
3347
3553
  * @param {string=} options.hidePrefix
3348
3554
  * @param {string=} options.dividerOrient
3349
3555
  * @param {string=} options.theme
3350
3556
  * @param {string=} options.librariesUrl
3351
3557
  * @param {string=} options.importsUrl
3558
+ * @param {string=} options.loading
3352
3559
  * The same as the data attributes of a script tag.
3353
3560
  * @returns {{element: HTMLIFrameElement, destroy: () => void}}
3354
3561
  */
@@ -3433,6 +3640,8 @@
3433
3640
  * @param {Window=} options.targetWindow
3434
3641
  * If given the page is written into this (same origin) window (eg. a
3435
3642
  * pop-out window) instead of into a new IFRAME.
3643
+ * @param {boolean=} options.deferLoad
3644
+ * If true the IFRAME's page isn't loaded until `load()` is called.
3436
3645
  * @param {((this: R, event: MessageEvent) => void)=} options.onMessage
3437
3646
  * @param {((this: R, event: MessageEvent) => void)=} options.onReady
3438
3647
  * @returns {R}
@@ -3514,9 +3723,9 @@
3514
3723
  TARGET_WINDOW.document.close();
3515
3724
  }
3516
3725
  else {
3517
- IFRAME.srcdoc = HTML_CODE;
3518
3726
  // Allows the console to go full screen.
3519
3727
  IFRAME.setAttribute('allow', 'fullscreen');
3728
+ if (!options.deferLoad) IFRAME.srcdoc = HTML_CODE;
3520
3729
  }
3521
3730
 
3522
3731
  // Set the style of the iframe.
@@ -3578,6 +3787,10 @@
3578
3787
  },
3579
3788
  iframe: IFRAME,
3580
3789
  window: TARGET_WINDOW,
3790
+ // Loads the IFRAME's page if it was deferred.
3791
+ load: function() {
3792
+ if (IFRAME && !IFRAME.srcdoc) IFRAME.srcdoc = HTML_CODE;
3793
+ },
3581
3794
  // Stops listening for messages from the frame.
3582
3795
  dispose: function() {
3583
3796
  removeEventListener('message', onWindowMessage);
@@ -3591,6 +3804,7 @@
3591
3804
  * @property {(funcName: string, ...args: any[]) => void} call
3592
3805
  * @property {HTMLIFrameElement?} iframe
3593
3806
  * @property {Window=} window
3807
+ * @property {() => void} load
3594
3808
  * @property {() => void} dispose
3595
3809
  */
3596
3810