yourjs-box 1.6.0 → 1.7.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 +41 -14
- package/dist/yourjs-box.full.js +198 -24
- package/dist/yourjs-box.js +13 -6
- package/dist/yourjs-box.min.js +8 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Embed an interactive JavaScript console on any web page with one script tag.
|
|
4
4
|
Step-by-step code blocks, DevTools-style output, and code that runs in a Web
|
|
5
|
-
Worker or the page itself.
|
|
5
|
+
Worker or the page itself. TypeScript works too.
|
|
6
6
|
|
|
7
7
|
**[Live demo](https://westc.github.io/yourjs-box/)** ·
|
|
8
8
|
[Examples](https://westc.github.io/yourjs-box/examples/)
|
|
@@ -61,10 +61,10 @@ If the editor already has code in it you are asked before it is replaced.
|
|
|
61
61
|
history.
|
|
62
62
|
- The **⋯** menu has:
|
|
63
63
|
- **Text size**, which is remembered for every console on the same site.
|
|
64
|
-
- **Open**, which loads a JavaScript file as the code that the console starts
|
|
64
|
+
- **Open**, which loads a JavaScript (or TypeScript) file as the code that the console starts
|
|
65
65
|
with (its hidden blocks are hidden and Reset goes back to it).
|
|
66
66
|
- **Save**, which saves the code that already ran and the code in the editor
|
|
67
|
-
as a JavaScript file that can be opened later.
|
|
67
|
+
as a JavaScript (or TypeScript) file that can be opened later.
|
|
68
68
|
- **Pop out into a window**, which moves the console into a separate window
|
|
69
69
|
while the code keeps running in the page (so in window mode the code can
|
|
70
70
|
still change the page). Close the window or click **Bring it back** to
|
|
@@ -99,9 +99,10 @@ objects and arrays) for a menu with:
|
|
|
99
99
|
| Attribute | Description |
|
|
100
100
|
| --- | --- |
|
|
101
101
|
| `data-block-type` | `"classic"` (default) runs each block like a regular `<script>` so top-level declarations are shared between blocks. `"module"` runs each block like a `<script type="module">`. See below. |
|
|
102
|
+
| `data-language` | `"javascript"` (default) or `"typescript"`. See "TypeScript" below. |
|
|
102
103
|
| `data-show-results` | `"false"` stops the value of the last expression in each block from being shown. |
|
|
103
104
|
| `data-imports-url` | Where packages imported by name are loaded from. See "Importing Packages" below. |
|
|
104
|
-
| `data-libraries-url` | Where to load Vue, Ace, Prism and
|
|
105
|
+
| `data-libraries-url` | Where to load Vue, Ace, Prism, Acorn and Babel from. See "Self-Hosting the Libraries" below. |
|
|
105
106
|
| `data-runner` | `"worker"` (default) runs the code in a Web Worker. `"window"` runs it directly in the page. See below. |
|
|
106
107
|
| `data-divider-orient` | `"vertical"` puts the editor beside the output and `"horizontal"` puts it below. If not specified the editor is beside the output unless the console is narrower than 600px. |
|
|
107
108
|
| `data-hide-prefix` | Any block whose header starts with this prefix is hidden. See below. |
|
|
@@ -126,6 +127,36 @@ one block can be used in the next, but `await` can only be used inside of
|
|
|
126
127
|
- Top-level declarations stay inside of their block, just like in a module.
|
|
127
128
|
Use `globalThis` to share values between blocks.
|
|
128
129
|
|
|
130
|
+
### TypeScript
|
|
131
|
+
|
|
132
|
+
Add `data-language="typescript"` to write the code in TypeScript:
|
|
133
|
+
|
|
134
|
+
```html
|
|
135
|
+
<script src="https://cdn.jsdelivr.net/npm/yourjs-box@1/dist/yourjs-box.min.js" data-language="typescript">
|
|
136
|
+
// Describe a person \\
|
|
137
|
+
interface Person { name: string; born: number }
|
|
138
|
+
const people: Person[] = [{name: 'Ada', born: 1815}, {name: 'Grace', born: 1906}];
|
|
139
|
+
|
|
140
|
+
// Find the oldest \\
|
|
141
|
+
function oldest<T extends Person>(list: T[]): T {
|
|
142
|
+
return list.reduce((a, b) => a.born <= b.born ? a : b);
|
|
143
|
+
}
|
|
144
|
+
oldest(people)
|
|
145
|
+
</script>
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Before each block runs, [Babel](https://babeljs.io/) removes the types, which
|
|
149
|
+
is how most TypeScript tools run code without a build step. The types aren't
|
|
150
|
+
checked, so code with type errors still runs, just like it would in
|
|
151
|
+
JavaScript. Enums, namespaces and parameter properties all work, and errors
|
|
152
|
+
point to the lines and columns in the TypeScript code.
|
|
153
|
+
|
|
154
|
+
TypeScript works with both block types. Only type imports are removed (eg.
|
|
155
|
+
`import type {Options} from 'x'` or the `type Options` part of
|
|
156
|
+
`import {type Options, format} from 'x'`), so an import is never dropped just
|
|
157
|
+
because nothing uses it yet. Babel is only downloaded by consoles that use
|
|
158
|
+
TypeScript.
|
|
159
|
+
|
|
129
160
|
### Importing Packages
|
|
130
161
|
|
|
131
162
|
npm packages can be imported by name. They are loaded from
|
|
@@ -158,7 +189,8 @@ To load packages from somewhere else set `data-imports-url` to a URL where
|
|
|
158
189
|
|
|
159
190
|
The console's interface uses [Vue](https://vuejs.org/),
|
|
160
191
|
[Ace](https://ace.c9.io/), [Prism](https://prismjs.com/) and
|
|
161
|
-
[Acorn](https://github.com/acornjs/acorn)
|
|
192
|
+
[Acorn](https://github.com/acornjs/acorn) (plus
|
|
193
|
+
[Babel](https://babeljs.io/) for TypeScript), which are loaded from unpkg by
|
|
162
194
|
default. Exact versions are always used so that a new release of one of them
|
|
163
195
|
can't change how the console works.
|
|
164
196
|
|
|
@@ -179,6 +211,9 @@ next to their main files):
|
|
|
179
211
|
|
|
180
212
|
```bash
|
|
181
213
|
npm install vue@3.5.43 ace-builds@1.44.0 prismjs@1.30.0 prism-themes@1.9.0 acorn@8.18.0
|
|
214
|
+
|
|
215
|
+
# Only needed for TypeScript consoles
|
|
216
|
+
npm install @babel/standalone@7.29.9
|
|
182
217
|
```
|
|
183
218
|
|
|
184
219
|
The About window lists the versions that each version of JS Box uses.
|
|
@@ -232,7 +267,7 @@ functions). The options are:
|
|
|
232
267
|
| `placement` | Where the console goes: `"fill"` (default) replaces the target's contents, `"append"` and `"prepend"` add it inside of the target, `"replace"` replaces the target itself and `"before"` and `"after"` add it next to the target. |
|
|
233
268
|
| `height` | The CSS height of the console (a number is treated as pixels). Defaults to `"100%"` so the console fills its container. It is never less than 150px. |
|
|
234
269
|
| `code` | The code that the console starts with. |
|
|
235
|
-
| `runner`, `blockType`, `showResults`, `hidePrefix`, `dividerOrient`, `theme`, `importsUrl`, `librariesUrl` | The same as the `data-*` attributes above. |
|
|
270
|
+
| `runner`, `blockType`, `language`, `showResults`, `hidePrefix`, `dividerOrient`, `theme`, `importsUrl`, `librariesUrl` | The same as the `data-*` attributes above. |
|
|
236
271
|
|
|
237
272
|
`YourJSBox.version` is the version of JS Box that was loaded.
|
|
238
273
|
|
|
@@ -286,14 +321,6 @@ which cover every feature. Add `?build=standard` or `?build=min` to its URL to
|
|
|
286
321
|
test `dist/yourjs-box.js` or `dist/yourjs-box.min.js` instead of
|
|
287
322
|
`dist/yourjs-box.full.js`.
|
|
288
323
|
|
|
289
|
-
## Roadmap
|
|
290
|
-
|
|
291
|
-
- Open button - Load a JS file from the filesystem.
|
|
292
|
-
- Save button - Save the current inputs as a JS file that can be opened later.
|
|
293
|
-
- Add `@timeout` annotation to the special comments that will allow you to input the amount of seconds to wait since the last call to a console logging function before automatically running the next comment segmented block.
|
|
294
|
-
- Allow for TypeScript
|
|
295
|
-
- Allow for CoffeeScript
|
|
296
|
-
|
|
297
324
|
## License
|
|
298
325
|
|
|
299
326
|
Released under the [MIT License](LICENSE). You're free to use, modify and
|
package/dist/yourjs-box.full.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! yourjs-box v1.
|
|
1
|
+
/*! yourjs-box v1.7.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.7.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
|
|
@@ -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,
|
|
@@ -3317,7 +3490,7 @@
|
|
|
3317
3490
|
* The options of YourJSBox.create() which are the same as the data
|
|
3318
3491
|
* attributes of a script tag.
|
|
3319
3492
|
*/
|
|
3320
|
-
const CONSOLE_OPTION_NAMES = ['blockType', 'dividerOrient', 'hidePrefix', 'importsUrl', 'librariesUrl', 'runner', 'showResults', 'theme'];
|
|
3493
|
+
const CONSOLE_OPTION_NAMES = ['blockType', 'dividerOrient', 'hidePrefix', 'importsUrl', 'language', 'librariesUrl', 'runner', 'showResults', 'theme'];
|
|
3321
3494
|
|
|
3322
3495
|
/**
|
|
3323
3496
|
* The JavaScript API for creating consoles (available as window.YourJSBox).
|
|
@@ -3343,6 +3516,7 @@
|
|
|
3343
3516
|
* Optional. The code that the console starts with.
|
|
3344
3517
|
* @param {string=} options.runner
|
|
3345
3518
|
* @param {string=} options.blockType
|
|
3519
|
+
* @param {string=} options.language
|
|
3346
3520
|
* @param {(boolean|string)=} options.showResults
|
|
3347
3521
|
* @param {string=} options.hidePrefix
|
|
3348
3522
|
* @param {string=} options.dividerOrient
|