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.
- package/README.md +96 -19
- package/dist/yourjs-box.full.js +243 -29
- package/dist/yourjs-box.js +58 -11
- package/dist/yourjs-box.min.js +8 -7
- package/package.json +2 -1
package/dist/yourjs-box.full.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! yourjs-box v1.
|
|
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.
|
|
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…</div><div class=\"splash-message splash-error\">JS Box couldn’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' && !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 && 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−</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…</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"saveFile()\"><span class=\"menu-icon\"><icon name=\"save\"></icon></span>Save…</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…</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); resetConsole()\"><span class=\"menu-icon\"><icon name=\"refresh\"></icon></span>Reset…</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 ↑ at the start of the editor (or ↓ 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…</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>↑</kbd> / <kbd>↓</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' : '⌥' }}</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 © 2023-present Chris West · 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…</div><div class=\"splash-message splash-error\">JS Box couldn’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' && !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 && 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−</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…</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"saveFile()\"><span class=\"menu-icon\"><icon name=\"save\"></icon></span>Save…</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…</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); resetConsole()\"><span class=\"menu-icon\"><icon name=\"refresh\"></icon></span>Reset…</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 ↑ at the start of the editor (or ↓ 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…</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>↑</kbd> / <kbd>↓</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' : '⌥' }}</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 © 2023-present Chris West · 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
|
-
|
|
338
|
-
|
|
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
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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}
|
|
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 (
|
|
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
|
-
|
|
1067
|
-
|
|
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 {
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
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, {
|
|
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: [
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
|