yourjs-box 1.3.0 → 1.4.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 CHANGED
@@ -24,7 +24,8 @@ should be loaded into the console inside of the script tag:
24
24
  </div>
25
25
  ```
26
26
 
27
- The console fills its container. The script is also available from unpkg at
27
+ The console fills its container (and is never less than 150px tall). The
28
+ script is also available from unpkg at
28
29
  `https://unpkg.com/yourjs-box@1/dist/yourjs-box.min.js`.
29
30
 
30
31
  ### Code Blocks
@@ -41,6 +42,8 @@ If the editor already has code in it you are asked before it is replaced.
41
42
 
42
43
  - **Clear** removes everything from the console. Code can also call
43
44
  `console.clear()`.
45
+ - The **layout** button switches between showing the editor beside or below
46
+ the console.
44
47
  - **Full screen** shows the console using the whole screen. If the browser
45
48
  doesn't allow that (eg. on an iPhone) the console fills the browser window
46
49
  instead. Press <kbd>Esc</kbd> to exit.
@@ -49,7 +52,6 @@ If the editor already has code in it you are asked before it is replaced.
49
52
  console's settings and keyboard shortcuts.
50
53
  - The **&#8943;** button opens a menu with:
51
54
  - **Text size**, which is remembered for every console on the same site.
52
- - **Editor beside / below the console** to change the layout.
53
55
  - **Pop out into a window**, which moves the console into a separate window
54
56
  while the code keeps running in the page (so in window mode the code can
55
57
  still change the page). Close the window or click **Bring it back** to
@@ -139,6 +141,43 @@ up in the output as a collapsed block labelled with whatever came after the
139
141
  prefix (and optional colon), or "Hidden code" if nothing did. Clicking the
140
142
  label shows or hides the code.
141
143
 
144
+ ### JavaScript API
145
+
146
+ Loading the script also provides `YourJSBox` for creating consoles from
147
+ JavaScript (eg. in React, Vue or any page that adds content dynamically). A
148
+ script tag in the `<head>` only provides the API, while one in the `<body>` is
149
+ also replaced by a console (with the code inside of it) as usual.
150
+
151
+ ```html
152
+ <script src="https://cdn.jsdelivr.net/npm/yourjs-box@1/dist/yourjs-box.min.js"></script>
153
+ ```
154
+
155
+ ```js
156
+ const box = YourJSBox.create({
157
+ target: '#lesson',
158
+ code: "// Say hello \\\\\nconsole.log('Hello!');",
159
+ theme: 'dark',
160
+ });
161
+
162
+ // Later (eg. when a component is removed):
163
+ box.destroy();
164
+ ```
165
+
166
+ `YourJSBox.create(options)` returns `{element, destroy}` where `element` is the
167
+ console's IFRAME and `destroy()` removes it, stops its code and closes its
168
+ pop-out window (in window mode it also restores the page's `console`
169
+ functions). The options are:
170
+
171
+ | Option | Description |
172
+ | --- | --- |
173
+ | `target` | Required. The element (or a CSS selector for it) that the console is placed relative to. |
174
+ | `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. |
175
+ | `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. |
176
+ | `code` | The code that the console starts with. |
177
+ | `runner`, `blockType`, `showResults`, `hidePrefix`, `dividerOrient`, `theme`, `librariesUrl` | The same as the `data-*` attributes above. |
178
+
179
+ `YourJSBox.version` is the version of JS Box that was loaded.
180
+
142
181
  ### Where Code Runs
143
182
 
144
183
  The console's interface is always in an IFRAME so that its styles and the
@@ -1,4 +1,4 @@
1
- /*! yourjs-box v1.3.0 | (c) 2023-present Christopher West | MIT License | https://github.com/westc/yourjs-box */
1
+ /*! yourjs-box v1.4.0 | (c) 2023-present Christopher 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.3.0","homepage":"https://westc.github.io/yourjs-box/","repoUrl":"https://github.com/westc/yourjs-box","bugsUrl":"https://github.com/westc/yourjs-box/issues"};
12
+ const PACKAGE_INFO = {"name":"yourjs-box","version":"1.4.0","homepage":"https://westc.github.io/yourjs-box/","repoUrl":"https://github.com/westc/yourjs-box","bugsUrl":"https://github.com/westc/yourjs-box/issues"};
13
13
  /**
14
14
  * Viewer IFRAME's HTML code
15
15
  * @type {string}
16
16
  */
17
- const VIEWER_IFRAME_HTML = "<div id=\"splash\" aria-label=\"Loading\"><div class=\"splash-content\"><span class=\"logo logo-large\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span><div class=\"splash-progress\"><div></div></div><div class=\"splash-message splash-slow\">Still loading&hellip;</div><div class=\"splash-message splash-error\">JS Box couldn&rsquo;t load. Please check your connection and reload the page.</div></div></div><div id=\"vueApp\" v-cloak><div id=\"main\" ref=\"main\" :class=\"mainElemClassNames\" :style=\"mainElemStyles\" @mousedown=\"onMainElemMouseDown\"><div id=\"displays\"><div ref=\"displaysScroller\" @scroll=\"onDisplaysScroll\"><template v-for=\"display in displays\"><div v-if=\"display.type === 'prism'\"><div v-if=\"display.isHidden\" class=\"code-header toggleable no-select\" @click=\"display.isCodeShown = !display.isCodeShown\" :title=\"display.isCodeShown ? 'Hide Code' : 'Show Code'\"><span :class=\"['arrow', 'expandable', display.isCodeShown ? 'expanded' : '']\"></span>{{ display.header || 'Hidden code' }}</div><div v-else-if=\"display.header\" class=\"code-header\">{{ display.header }}</div><div v-if=\"display.isCodeShown\" class=\"console-row code-row\"><span class=\"row-icon\"><icon name=\"chevron\"></icon></span><prism language=\"javascript\" :code=\"display.value\" :is-dark=\"theme === 'dark'\" match-braces></prism><button class=\"copy-to-editor-button\" title=\"Copy to editor\" @click=\"copyToEditor(display.value)\"><icon name=\"copyToEditor\"></icon></button></div></div><div v-if=\"display.type === 'log' &amp;&amp; !isInCollapsedGroup(display)\" :class=\"['console-row', 'log-' + display.key, display.groupId ? 'group-header' : '']\" :style=\"{'--depth': display.groupIds.length}\" :title=\"display.name\" @click=\"onLogRowClick(display, $event)\"><span v-if=\"display.key === 'error' || display.key === 'assert'\" class=\"row-icon\"><icon name=\"consoleError\"></icon></span><span v-else-if=\"display.key === 'warn'\" class=\"row-icon\"><icon name=\"consoleWarning\"></icon></span><span v-else-if=\"display.key === 'result'\" class=\"row-icon\"><icon name=\"result\"></icon></span><span v-else-if=\"display.groupId\" :class=\"['row-icon', 'arrow', 'expandable', display.isCollapsed ? '' : 'expanded']\"></span><div class=\"row-content\"><table v-if=\"display.table\" class=\"console-table\"><thead><tr><th v-for=\"header in display.table.headers\">{{ header }}</th></tr></thead><tbody><tr v-for=\"row in display.table.rows\"><td>{{ row.index }}</td><td v-for=\"cell in row.cells\"><template v-if=\"cell\"><span v-for=\"part in cell.parts\" :class=\"'t-' + part[0]\">{{ part[1] }}</span></template></td></tr></tbody></table><js-value v-for=\"(description, index) in display.descriptions\" :description=\"description\" :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 v-model=\"jsCode\" language=\"javascript\" :theme=\"theme\" :font-size=\"Math.round(12 * textScale)\" @key-combo=\"onEditorKeyCombo\" height=\"100%\"></ace-editor></div></div><div id=\"bottomNav\"><div class=\"brand\"><button class=\"logo-button\" title=\"About JS Box\" @click=\"openAbout('about')\"><span class=\"logo\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span></button> <span class=\"runner-badge\" :title=\"runnerMode === 'window' ? 'Code runs directly in this page' : 'Code runs in a Web Worker (no DOM access)'\">{{ runnerMode === 'window' ? 'Window' : 'Worker' }}</span></div><div class=\"buttons\"><template v-for=\"bottomButton in bottomButtons\"><span v-if=\"bottomButton.isSeparator\" class=\"separator\"></span> <button v-else :class=\"bottomButton.className\" @click=\"bottomButton.callback.call(this, $event)\" :title=\"bottomButton.title\" :disabled=\"bottomButton.disableIf &amp;&amp; bottomButton.disableIf.call(this)\"><icon :name=\"bottomButton.iconName\"></icon><span v-if=\"bottomButton.label\" class=\"label\">{{ bottomButton.label }}</span></button></template></div></div><div v-if=\"isMenuOpen\" class=\"more-menu\" role=\"menu\" aria-label=\"More\" ref=\"moreMenu\" @keydown=\"onMenuKeyDown\"><div class=\"menu-row\" role=\"group\" aria-label=\"Text size\"><span class=\"menu-row-label\">Text size</span> <button class=\"menu-item menu-step\" role=\"menuitem\" title=\"Smaller text\" aria-label=\"Smaller text\" :disabled=\"!canShrinkText\" @click=\"changeTextScale(-1)\">A&minus;</button> <button class=\"menu-item menu-step menu-percent\" role=\"menuitem\" title=\"Reset the text size\" aria-label=\"Reset the text size\" @click=\"setTextScale(1)\">{{ Math.round(textScale * 100) }}%</button> <button class=\"menu-item menu-step\" role=\"menuitem\" title=\"Bigger text\" aria-label=\"Bigger text\" :disabled=\"!canGrowText\" @click=\"changeTextScale(1)\">A+</button></div><div class=\"menu-separator\"></div><button class=\"menu-item\" role=\"menuitemradio\" :aria-checked=\"dividerOrient === 'vertical'\" @click=\"setDividerOrient('vertical'); closeMenu(true)\"><span class=\"menu-icon\"><icon v-if=\"dividerOrient === 'vertical'\" name=\"check\"></icon></span>Editor beside the console</button> <button class=\"menu-item\" role=\"menuitemradio\" :aria-checked=\"dividerOrient === 'horizontal'\" @click=\"setDividerOrient('horizontal'); closeMenu(true)\"><span class=\"menu-icon\"><icon v-if=\"dividerOrient === 'horizontal'\" name=\"check\"></icon></span>Editor below the console</button><div class=\"menu-separator\"></div><button v-if=\"isPopOut\" class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); bringBack()\"><span class=\"menu-icon\"><icon name=\"popIn\"></icon></span>Bring back into the page</button> <button v-else class=\"menu-item\" role=\"menuitem\" @click=\"popOut()\"><span class=\"menu-icon\"><icon name=\"popOut\"></icon></span>Pop out into a window</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); openAbout('html')\"><span class=\"menu-icon\"><icon name=\"code\"></icon></span>Copy as HTML&hellip;</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); resetConsole()\"><span class=\"menu-icon\"><icon name=\"refresh\"></icon></span>Reset&hellip;</button><div class=\"menu-separator\"></div><button class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); openAbout('about')\"><span class=\"menu-icon\"><icon name=\"info\"></icon></span>About JS Box</button></div><div v-if=\"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>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>Esc</kbd></dt><dd>Close this window, the menu or full screen</dd></dl><div class=\"about-footer\">MIT License &copy; 2023-present Christopher West &middot; Built with Vue {{ libraryVersions.vue }}, Ace {{ libraryVersions['ace-builds'] }}, Prism {{ libraryVersions.prismjs }} and Acorn {{ libraryVersions.acorn }}</div></div><div v-else class=\"about-body export-body\"><div class=\"export-options\"><div class=\"segmented\" role=\"radiogroup\" aria-label=\"Code\"><button v-for=\"option in exportCodeOptions\" role=\"radio\" :aria-checked=\"exportCode === option.value\" :class=\"exportCode === option.value ? 'active' : ''\" @click=\"exportCode = option.value\">{{ option.label }}</button></div><div class=\"segmented\" role=\"radiogroup\" aria-label=\"Format\"><button v-for=\"option in exportFormatOptions\" role=\"radio\" :aria-checked=\"exportFormat === option.value\" :class=\"exportFormat === option.value ? 'active' : ''\" @click=\"exportFormat = option.value\">{{ option.label }}</button></div></div><div class=\"export-note\">{{ exportNote }}</div><div class=\"export-preview\"><ace-editor :model-value=\"exportHtml\" language=\"html\" :theme=\"theme\" height=\"100%\" :read-only=\"true\"></ace-editor></div><div class=\"export-actions\"><button class=\"dialog-button\" @click=\"downloadExport\">Download page</button> <button class=\"dialog-button primary\" @click=\"copyExport\">{{ copyLabel }}</button></div></div></div></div><div v-if=\"dialog\" class=\"dialog-backdrop\" @mousedown.self=\"closeDialog(false)\" @keydown.esc=\"closeDialog(false)\"><div class=\"dialog\" role=\"alertdialog\" aria-modal=\"true\" :aria-label=\"dialog.title\"><div class=\"dialog-title\">{{ dialog.title }}</div><div class=\"dialog-message\">{{ dialog.message }}</div><div class=\"dialog-buttons\"><button class=\"dialog-button\" @click=\"closeDialog(false)\">Cancel</button> <button class=\"dialog-button primary\" ref=\"dialogConfirmButton\" @click=\"closeDialog(true)\">{{ dialog.confirmText }}</button></div></div></div></div>";
17
+ const VIEWER_IFRAME_HTML = "<div id=\"splash\" aria-label=\"Loading\"><div class=\"splash-content\"><span class=\"logo logo-large\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span><div class=\"splash-progress\"><div></div></div><div class=\"splash-message splash-slow\">Still loading&hellip;</div><div class=\"splash-message splash-error\">JS Box couldn&rsquo;t load. Please check your connection and reload the page.</div></div></div><div id=\"vueApp\" v-cloak><div id=\"main\" ref=\"main\" :class=\"mainElemClassNames\" :style=\"mainElemStyles\" @mousedown=\"onMainElemMouseDown\"><div id=\"displays\"><div ref=\"displaysScroller\" @scroll=\"onDisplaysScroll\"><template v-for=\"display in displays\"><div v-if=\"display.type === 'prism'\"><div v-if=\"display.isHidden\" class=\"code-header toggleable no-select\" @click=\"display.isCodeShown = !display.isCodeShown\" :title=\"display.isCodeShown ? 'Hide Code' : 'Show Code'\"><span :class=\"['arrow', 'expandable', display.isCodeShown ? 'expanded' : '']\"></span>{{ display.header || 'Hidden code' }}</div><div v-else-if=\"display.header\" class=\"code-header\">{{ display.header }}</div><div v-if=\"display.isCodeShown\" class=\"console-row code-row\"><span class=\"row-icon\"><icon name=\"chevron\"></icon></span><prism language=\"javascript\" :code=\"display.value\" :is-dark=\"theme === 'dark'\" match-braces></prism><button class=\"copy-to-editor-button\" title=\"Copy to editor\" @click=\"copyToEditor(display.value)\"><icon name=\"copyToEditor\"></icon></button></div></div><div v-if=\"display.type === 'log' &amp;&amp; !isInCollapsedGroup(display)\" :class=\"['console-row', 'log-' + display.key, display.groupId ? 'group-header' : '']\" :style=\"{'--depth': display.groupIds.length}\" :title=\"display.name\" @click=\"onLogRowClick(display, $event)\"><span v-if=\"display.key === 'error' || display.key === 'assert'\" class=\"row-icon\"><icon name=\"consoleError\"></icon></span><span v-else-if=\"display.key === 'warn'\" class=\"row-icon\"><icon name=\"consoleWarning\"></icon></span><span v-else-if=\"display.key === 'result'\" class=\"row-icon\"><icon name=\"result\"></icon></span><span v-else-if=\"display.groupId\" :class=\"['row-icon', 'arrow', 'expandable', display.isCollapsed ? '' : 'expanded']\"></span><div class=\"row-content\"><table v-if=\"display.table\" class=\"console-table\"><thead><tr><th v-for=\"header in display.table.headers\">{{ header }}</th></tr></thead><tbody><tr v-for=\"row in display.table.rows\"><td>{{ row.index }}</td><td v-for=\"cell in row.cells\"><template v-if=\"cell\"><span v-for=\"part in cell.parts\" :class=\"'t-' + part[0]\">{{ part[1] }}</span></template></td></tr></tbody></table><js-value v-for=\"(description, index) in display.descriptions\" :description=\"description\" :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 v-model=\"jsCode\" language=\"javascript\" :theme=\"theme\" :font-size=\"Math.round(12 * textScale)\" @key-combo=\"onEditorKeyCombo\" height=\"100%\"></ace-editor></div></div><div id=\"bottomNav\"><div class=\"brand\"><button class=\"logo-button\" title=\"About JS Box\" @click=\"openAbout('about')\"><span class=\"logo\"><span class=\"logo-mark\">JS</span><span class=\"logo-text\">Box</span></span></button> <span class=\"runner-badge\" :title=\"runnerMode === 'window' ? 'Code runs directly in this page' : 'Code runs in a Web Worker (no DOM access)'\">{{ runnerMode === 'window' ? 'Window' : 'Worker' }}</span></div><div class=\"buttons\"><template v-for=\"bottomButton in bottomButtons\"><span v-if=\"bottomButton.isSeparator\" class=\"separator\"></span> <button v-else :class=\"bottomButton.className\" @click=\"bottomButton.callback.call(this, $event)\" :title=\"bottomButton.title\" :disabled=\"bottomButton.disableIf &amp;&amp; bottomButton.disableIf.call(this)\"><icon :name=\"bottomButton.iconName\"></icon><span v-if=\"bottomButton.label\" class=\"label\">{{ bottomButton.label }}</span></button></template></div></div><div v-if=\"isMenuOpen\" class=\"more-menu\" role=\"menu\" aria-label=\"More\" ref=\"moreMenu\" @keydown=\"onMenuKeyDown\"><div class=\"menu-row\" role=\"group\" aria-label=\"Text size\"><span class=\"menu-row-label\">Text size</span> <button class=\"menu-item menu-step\" role=\"menuitem\" title=\"Smaller text\" aria-label=\"Smaller text\" :disabled=\"!canShrinkText\" @click=\"changeTextScale(-1)\">A&minus;</button> <button class=\"menu-item menu-step menu-percent\" role=\"menuitem\" title=\"Reset the text size\" aria-label=\"Reset the text size\" @click=\"setTextScale(1)\">{{ Math.round(textScale * 100) }}%</button> <button class=\"menu-item menu-step\" role=\"menuitem\" title=\"Bigger text\" aria-label=\"Bigger text\" :disabled=\"!canGrowText\" @click=\"changeTextScale(1)\">A+</button></div><div class=\"menu-separator\"></div><button v-if=\"isPopOut\" class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); bringBack()\"><span class=\"menu-icon\"><icon name=\"popIn\"></icon></span>Bring back into the page</button> <button v-else class=\"menu-item\" role=\"menuitem\" @click=\"popOut()\"><span class=\"menu-icon\"><icon name=\"popOut\"></icon></span>Pop out into a window</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); openAbout('html')\"><span class=\"menu-icon\"><icon name=\"code\"></icon></span>Copy as HTML&hellip;</button> <button class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); resetConsole()\"><span class=\"menu-icon\"><icon name=\"refresh\"></icon></span>Reset&hellip;</button><div class=\"menu-separator\"></div><button class=\"menu-item\" role=\"menuitem\" @click=\"closeMenu(); openAbout('about')\"><span class=\"menu-icon\"><icon name=\"info\"></icon></span>About JS Box</button></div><div v-if=\"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>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>Esc</kbd></dt><dd>Close this window, the menu or full screen</dd></dl><div class=\"about-footer\">MIT License &copy; 2023-present Christopher West &middot; Built with Vue {{ libraryVersions.vue }}, Ace {{ libraryVersions['ace-builds'] }}, Prism {{ libraryVersions.prismjs }} and Acorn {{ libraryVersions.acorn }}</div></div><div v-else class=\"about-body export-body\"><div class=\"export-options\"><div class=\"segmented\" role=\"radiogroup\" aria-label=\"Code\"><button v-for=\"option in exportCodeOptions\" role=\"radio\" :aria-checked=\"exportCode === option.value\" :class=\"exportCode === option.value ? 'active' : ''\" @click=\"exportCode = option.value\">{{ option.label }}</button></div><div class=\"segmented\" role=\"radiogroup\" aria-label=\"Format\"><button v-for=\"option in exportFormatOptions\" role=\"radio\" :aria-checked=\"exportFormat === option.value\" :class=\"exportFormat === option.value ? 'active' : ''\" @click=\"exportFormat = option.value\">{{ option.label }}</button></div></div><div class=\"export-note\">{{ exportNote }}</div><div class=\"export-preview\"><ace-editor :model-value=\"exportHtml\" language=\"html\" :theme=\"theme\" height=\"100%\" :read-only=\"true\"></ace-editor></div><div class=\"export-actions\"><button class=\"dialog-button\" @click=\"downloadExport\">Download page</button> <button class=\"dialog-button primary\" @click=\"copyExport\">{{ copyLabel }}</button></div></div></div></div><div v-if=\"dialog\" class=\"dialog-backdrop\" @mousedown.self=\"closeDialog(false)\" @keydown.esc=\"closeDialog(false)\"><div class=\"dialog\" role=\"alertdialog\" aria-modal=\"true\" :aria-label=\"dialog.title\"><div class=\"dialog-title\">{{ dialog.title }}</div><div class=\"dialog-message\">{{ dialog.message }}</div><div class=\"dialog-buttons\"><button class=\"dialog-button\" @click=\"closeDialog(false)\">Cancel</button> <button class=\"dialog-button primary\" ref=\"dialogConfirmButton\" @click=\"closeDialog(true)\">{{ dialog.confirmText }}</button></div></div></div></div>";
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.
@@ -130,6 +130,9 @@
130
130
  const RESULT_KEY = `yourjs-box.result.${Math.random().toString(36).slice(2)}`;
131
131
  globalThis[Symbol.for(RESULT_KEY)] = reportResult;
132
132
 
133
+ // The console functions that were replaced so that they can be restored.
134
+ const consoleWrappers = [];
135
+
133
136
  // Overrides for console functions. In window mode this also captures
134
137
  // anything else that the page logs, just like the browser's console.
135
138
  for (const key of [
@@ -139,12 +142,14 @@
139
142
  ]) {
140
143
  const original = console[key];
141
144
  if ('function' !== typeof original) continue;
142
- console[key] = function(...args) {
145
+ const wrapper = function(...args) {
143
146
  handleConsoleCall(key, args);
144
147
 
145
148
  // Calls and returns the original console function.
146
149
  return original.apply(this, arguments);
147
150
  };
151
+ console[key] = wrapper;
152
+ consoleWrappers.push({key, original, wrapper});
148
153
  }
149
154
 
150
155
  /**
@@ -251,13 +256,29 @@
251
256
  if (value !== undefined) addLog('result', [value], {}, 'string' === typeof value ? 1 : 0);
252
257
  }
253
258
 
254
- addEventListener('error', evt => {
259
+ function onError(evt) {
255
260
  reportUncaught(evt.error !== undefined ? evt.error : evt.message);
256
- });
257
-
258
- addEventListener('unhandledrejection', evt => {
261
+ }
262
+ function onUnhandledRejection(evt) {
259
263
  reportUncaught(evt.reason, true);
260
- });
264
+ }
265
+ addEventListener('error', onError);
266
+ addEventListener('unhandledrejection', onUnhandledRejection);
267
+
268
+ /**
269
+ * Stops capturing what is logged (used when a console in window mode is
270
+ * destroyed). Each console function is restored unless something else has
271
+ * replaced it since.
272
+ */
273
+ function destroy() {
274
+ removeEventListener('error', onError);
275
+ removeEventListener('unhandledrejection', onUnhandledRejection);
276
+ delete globalThis[Symbol.for(RESULT_KEY)];
277
+ for (const {key, original, wrapper} of consoleWrappers) {
278
+ if (console[key] === wrapper) console[key] = original;
279
+ }
280
+ clearLogs();
281
+ }
261
282
 
262
283
  /**
263
284
  * @param {*} error
@@ -780,7 +801,7 @@
780
801
  groupIds = [];
781
802
  }
782
803
 
783
- return {clearLogs, runCode, sendDescriptionFor};
804
+ return {clearLogs, destroy, runCode, sendDescriptionFor};
784
805
  }
785
806
 
786
807
 
@@ -850,6 +871,10 @@
850
871
  start();
851
872
 
852
873
  return {
874
+ destroy() {
875
+ worker.terminate();
876
+ if (blobUrl) URL.revokeObjectURL(blobUrl);
877
+ },
853
878
  apply(func, args) {
854
879
  if (func === 'reset') {
855
880
  worker.terminate();
@@ -873,6 +898,9 @@
873
898
  function createWindowRunner(onMessage) {
874
899
  const runner = createRunner(onMessage, {mode: 'window', ownUrl: OWN_URL});
875
900
  return {
901
+ destroy() {
902
+ runner.destroy();
903
+ },
876
904
  apply(func, args) {
877
905
  // Anything the code defined stays defined so a reset can only forget
878
906
  // the logged values.
@@ -883,13 +911,20 @@
883
911
  }
884
912
 
885
913
  /**
886
- * Function executed when the script is included in a document.
887
- * @param {HTMLScriptElement} script
888
- * This is the current script but also the placeholder for where the
889
- * console will be inserted into the DOM.
914
+ * Creates a console.
915
+ * @param {Object} options
916
+ * @param {string} options.code
917
+ * The code that the console starts with.
918
+ * @param {{[name: string]: string}} options.dataset
919
+ * The options for the console in the same form as the data attributes of
920
+ * a script tag (eg. `{runner: 'window', hidePrefix: 'HIDE'}`).
921
+ * @param {string=} options.height
922
+ * Optional, defaults to `"100%"`. The CSS height of the console.
923
+ * @param {(element: HTMLIFrameElement) => void} options.insert
924
+ * Puts the console's element into the page.
925
+ * @returns {{element: HTMLIFrameElement, destroy: () => void}}
890
926
  */
891
- function main(script) {
892
- const dataset = JSON.parse(JSON.stringify(script.dataset));
927
+ function createConsole({code, dataset, height, insert}) {
893
928
  const runnerMode = dataset.runner === 'window' ? 'window' : 'worker';
894
929
  const blockType = dataset.blockType === 'module' ? 'module' : 'classic';
895
930
  const showResults = dataset.showResults !== 'false';
@@ -993,7 +1028,8 @@
993
1028
  };
994
1029
 
995
1030
  // The pop-out window can't work without this page so it is closed too.
996
- addEventListener('pagehide', () => popOutWindow?.close());
1031
+ const closePopOut = () => popOutWindow?.close();
1032
+ addEventListener('pagehide', closePopOut);
997
1033
 
998
1034
  /**
999
1035
  * @param {Window=} targetWindow
@@ -1246,6 +1282,18 @@
1246
1282
  title: 'Clear console',
1247
1283
  callback() { this.clearConsole(); },
1248
1284
  },
1285
+ {
1286
+ iconName: 'horizontalView',
1287
+ title: 'Show the editor below the console',
1288
+ callback() { this.setDividerOrient('horizontal'); },
1289
+ showIf() { return this.dividerOrient !== 'horizontal'; }
1290
+ },
1291
+ {
1292
+ iconName: 'verticalView',
1293
+ title: 'Show the editor beside the console',
1294
+ callback() { this.setDividerOrient('vertical'); },
1295
+ showIf() { return this.dividerOrient !== 'vertical'; }
1296
+ },
1249
1297
  {
1250
1298
  iconName: isFullscreen ? 'exitFullscreen' : 'fullscreen',
1251
1299
  title: isFullscreen ? 'Exit full screen' : 'Full screen',
@@ -1265,7 +1313,7 @@
1265
1313
  callback() { this.runCode(); },
1266
1314
  disableIf() { return !this.canRunCode; }
1267
1315
  },
1268
- ];
1316
+ ].filter(btn => !btn.showIf || btn.showIf.call(this));
1269
1317
  },
1270
1318
  canShrinkText() {
1271
1319
  return this.textScale > TEXT_SCALES[0];
@@ -1947,7 +1995,6 @@
1947
1995
  fullscreen: '<svg viewBox="0 0 16 16"><path d="M2.5 6V2.5H6M10 2.5h3.5V6M13.5 10v3.5H10M6 13.5H2.5V10" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>',
1948
1996
  exitFullscreen: '<svg viewBox="0 0 16 16"><path d="M6 2.5V6H2.5M13.5 6H10V2.5M10 13.5V10h3.5M2.5 10H6v3.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>',
1949
1997
  more: '<svg viewBox="0 0 16 16"><circle cx="3.5" cy="8" r="1.4" fill="currentColor"/><circle cx="8" cy="8" r="1.4" fill="currentColor"/><circle cx="12.5" cy="8" r="1.4" fill="currentColor"/></svg>',
1950
- check: '<svg viewBox="0 0 16 16"><path d="M3.5 8.5l3 3 6-7" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>',
1951
1998
  popOut: '<svg viewBox="0 0 16 16"><path d="M9.5 2.5h4v4M13.5 2.5L8 8M12 9.5v3a1 1 0 0 1-1 1H3.5a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h3" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>',
1952
1999
  popIn: '<svg viewBox="0 0 16 16"><path d="M12 8.5H8V4.5M8 8.5l5.5-5.5M12 10.5v2a1 1 0 0 1-1 1H3.5a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h2" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>',
1953
2000
  code: '<svg viewBox="0 0 16 16"><path d="M5.5 4.5L2 8l3.5 3.5M10.5 4.5L14 8l-3.5 3.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>',
@@ -2467,7 +2514,7 @@
2467
2514
  async onReady() {
2468
2515
  // The dataset is passed as is so that the viewer knows which options
2469
2516
  // were actually specified (eg. when copying the console as HTML).
2470
- this.call('init', script.textContent, dataset, {
2517
+ this.call('init', code, dataset, {
2471
2518
  runnerMode,
2472
2519
  blockType,
2473
2520
  showResults,
@@ -2480,7 +2527,10 @@
2480
2527
  body: VIEWER_IFRAME_HTML,
2481
2528
  style: {
2482
2529
  width: '100%',
2483
- height: '100%',
2530
+ // Fills its container unless a height is given but is never
2531
+ // squashed smaller than the default height of an IFRAME.
2532
+ height: height || '100%',
2533
+ minHeight: '150px',
2484
2534
  border: 0,
2485
2535
  display: 'block',
2486
2536
  },
@@ -2488,9 +2538,123 @@
2488
2538
  }
2489
2539
 
2490
2540
  inlineViewer = activeViewer = createViewer();
2491
- script.parentNode.insertBefore(inlineViewer.iframe, script);
2541
+ insert(inlineViewer.iframe);
2542
+
2543
+ let isDestroyed = false;
2544
+ return {
2545
+ element: inlineViewer.iframe,
2546
+ /**
2547
+ * Removes the console, stops its code and closes its pop-out window.
2548
+ */
2549
+ destroy() {
2550
+ if (isDestroyed) return;
2551
+ isDestroyed = true;
2552
+ if (popOutWindow) {
2553
+ clearInterval(popOutWatcher);
2554
+ popOutViewer.dispose();
2555
+ popOutWindow.close();
2556
+ popOutWindow = null;
2557
+ }
2558
+ removeEventListener('pagehide', closePopOut);
2559
+ runner.destroy();
2560
+ inlineViewer.dispose();
2561
+ inlineViewer.iframe.remove();
2562
+ },
2563
+ };
2492
2564
  }
2493
2565
 
2566
+ /**
2567
+ * Creates a console in place of a script tag using its code and data
2568
+ * attributes.
2569
+ * @param {HTMLScriptElement} script
2570
+ */
2571
+ function createConsoleFromScript(script) {
2572
+ return createConsole({
2573
+ code: script.textContent,
2574
+ dataset: JSON.parse(JSON.stringify(script.dataset)),
2575
+ insert: element => script.parentNode.insertBefore(element, script),
2576
+ });
2577
+ }
2578
+
2579
+ /**
2580
+ * Where YourJSBox.create() can put a console relative to its target.
2581
+ */
2582
+ const PLACEMENTS = {
2583
+ fill: (target, element) => target.replaceChildren(element),
2584
+ append: (target, element) => target.append(element),
2585
+ prepend: (target, element) => target.prepend(element),
2586
+ replace: (target, element) => target.replaceWith(element),
2587
+ before: (target, element) => target.before(element),
2588
+ after: (target, element) => target.after(element),
2589
+ };
2590
+
2591
+ /**
2592
+ * The options of YourJSBox.create() which are the same as the data
2593
+ * attributes of a script tag.
2594
+ */
2595
+ const CONSOLE_OPTION_NAMES = ['blockType', 'dividerOrient', 'hidePrefix', 'librariesUrl', 'runner', 'showResults', 'theme'];
2596
+
2597
+ /**
2598
+ * The JavaScript API for creating consoles (available as window.YourJSBox).
2599
+ */
2600
+ const YourJSBox = Object.freeze({
2601
+ version: PACKAGE_INFO.version,
2602
+
2603
+ /**
2604
+ * Creates a console.
2605
+ * @param {Object} options
2606
+ * @param {string|Element} options.target
2607
+ * The element (or a CSS selector for it) that the console is placed
2608
+ * relative to.
2609
+ * @param {"fill"|"append"|"prepend"|"replace"|"before"|"after"=} options.placement
2610
+ * Optional, defaults to `"fill"`. Where the console goes: "fill"
2611
+ * replaces the target's contents, "append" and "prepend" add it inside
2612
+ * of the target, "replace" replaces the target itself and "before" and
2613
+ * "after" add it next to the target.
2614
+ * @param {(string|number)=} options.height
2615
+ * Optional, defaults to `"100%"`. The CSS height of the console (a
2616
+ * number is treated as pixels). It is never less than 150px.
2617
+ * @param {string=} options.code
2618
+ * Optional. The code that the console starts with.
2619
+ * @param {string=} options.runner
2620
+ * @param {string=} options.blockType
2621
+ * @param {(boolean|string)=} options.showResults
2622
+ * @param {string=} options.hidePrefix
2623
+ * @param {string=} options.dividerOrient
2624
+ * @param {string=} options.theme
2625
+ * @param {string=} options.librariesUrl
2626
+ * The same as the data attributes of a script tag.
2627
+ * @returns {{element: HTMLIFrameElement, destroy: () => void}}
2628
+ */
2629
+ create(options) {
2630
+ options = Object(options);
2631
+ const {target, placement = 'fill', height, code = ''} = options;
2632
+ const targetElement = 'string' === typeof target ? document.querySelector(target) : target;
2633
+ if (targetElement?.nodeType !== 1) {
2634
+ throw new TypeError(
2635
+ 'string' === typeof target
2636
+ ? `YourJSBox.create(): no element matches the target ${JSON.stringify(target)}.`
2637
+ : 'YourJSBox.create(): target must be an element or a CSS selector.'
2638
+ );
2639
+ }
2640
+ if (!Object.hasOwn(PLACEMENTS, placement)) {
2641
+ throw new TypeError(`YourJSBox.create(): placement must be one of ${Object.keys(PLACEMENTS).join(', ')}.`);
2642
+ }
2643
+
2644
+ const dataset = {};
2645
+ for (const name of CONSOLE_OPTION_NAMES) {
2646
+ if (options[name] != null) dataset[name] = `${options[name]}`;
2647
+ }
2648
+
2649
+ return createConsole({
2650
+ code: `${code}`,
2651
+ dataset,
2652
+ height: 'number' === typeof height ? `${height}px` : height != null ? `${height}` : undefined,
2653
+ insert: element => PLACEMENTS[placement](targetElement, element),
2654
+ });
2655
+ },
2656
+ });
2657
+
2494
2658
  // NOTE: This solution was intentionally written without using newer JS
2495
2659
  // features to make the minified version even smaller.
2496
2660
  var createCallableFrame = (function () {
@@ -2778,5 +2942,13 @@
2778
2942
  return createCallableFrame;
2779
2943
  })();
2780
2944
 
2781
- main(document.currentScript);
2945
+ // The first copy of this script that is loaded provides the API.
2946
+ if (!window.YourJSBox) window.YourJSBox = YourJSBox;
2947
+
2948
+ // A script in the body is replaced by a console while a script in the head
2949
+ // only provides the API.
2950
+ const currentScript = document.currentScript;
2951
+ if (currentScript && !document.head?.contains(currentScript)) {
2952
+ createConsoleFromScript(currentScript);
2953
+ }
2782
2954
  })();